tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 9cb3e30d6196792680795d2e19fa20f766fde36b
parent 2239f1d5ffbac29deec1e16f87b87d151f1085bd
Author: Florian Zia <zia.florian@gmail.com>
Date:   Fri, 12 Dec 2025 11:08:52 +0000

Bug 2000992 - Vendor ProseMirror r=sylvestre,mak,Gijs

Differential Revision: https://phabricator.services.mozilla.com/D274588

Diffstat:
Athird_party/js/prosemirror/.gitignore | 3+++
Athird_party/js/prosemirror/LICENSE | 19+++++++++++++++++++
Athird_party/js/prosemirror/README.md | 32++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/bundle_entry.mjs | 22++++++++++++++++++++++
Athird_party/js/prosemirror/make_bundle.sh | 26++++++++++++++++++++++++++
Athird_party/js/prosemirror/moz.build | 12++++++++++++
Athird_party/js/prosemirror/moz.sh | 37+++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/moz.yaml | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/package.json | 26++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-commands/LICENSE | 19+++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-commands/moz.yaml | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-commands/package.json | 39+++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-commands/src/README.md | 40++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-commands/src/commands.ts | 783+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-history/LICENSE | 19+++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-history/moz.yaml | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-history/package.json | 40++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-history/src/README.md | 23+++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-history/src/history.ts | 465+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-keymap/LICENSE | 19+++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-keymap/moz.yaml | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-keymap/package.json | 38++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-keymap/src/README.md | 5+++++
Athird_party/js/prosemirror/prosemirror-keymap/src/keymap.ts | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-markdown/LICENSE | 19+++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-markdown/moz.yaml | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-markdown/package.json | 40++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-markdown/src/README.md | 42++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-markdown/src/from_markdown.ts | 272+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-markdown/src/index.ts | 5+++++
Athird_party/js/prosemirror/prosemirror-markdown/src/schema.ts | 156+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-markdown/src/to_markdown.ts | 474+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/LICENSE | 19+++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/moz.yaml | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/package.json | 38++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/README.md | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/comparedeep.ts | 15+++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/content.ts | 413+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/diff.ts | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/dom.ts | 1+
Athird_party/js/prosemirror/prosemirror-model/src/fragment.ts | 268+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/from_dom.ts | 848+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/index.ts | 11+++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/mark.ts | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/node.ts | 402+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/replace.ts | 225+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/resolvedpos.ts | 289++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/schema.ts | 705+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-model/src/to_dom.ts | 238+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-schema-basic/LICENSE | 19+++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-schema-basic/moz.yaml | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-schema-basic/package.json | 37+++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-schema-basic/src/README.md | 8++++++++
Athird_party/js/prosemirror/prosemirror-schema-basic/src/schema-basic.ts | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-state/LICENSE | 19+++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-state/moz.yaml | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-state/package.json | 39+++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-state/src/README.md | 42++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-state/src/index.ts | 7+++++++
Athird_party/js/prosemirror/prosemirror-state/src/plugin.ts | 142+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-state/src/selection.ts | 462+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-state/src/state.ts | 266+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-state/src/transaction.ts | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-suggestions/.babelrc | 14++++++++++++++
Athird_party/js/prosemirror/prosemirror-suggestions/LICENSE | 14++++++++++++++
Athird_party/js/prosemirror/prosemirror-suggestions/moz.yaml | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-suggestions/package.json | 36++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-suggestions/rollup.config.js | 16++++++++++++++++
Athird_party/js/prosemirror/prosemirror-suggestions/src/index.js | 10++++++++++
Athird_party/js/prosemirror/prosemirror-suggestions/src/mentions.js | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-suggestions/src/suggestions.js | 187+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-suggestions/src/suggestions.test.js | 240+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-suggestions/src/tags.js | 45+++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/LICENSE | 19+++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/moz.yaml | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/package.json | 37+++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/README.md | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/attr_step.ts | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/index.ts | 11+++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/map.ts | 284+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/mark.ts | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/mark_step.ts | 224+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/replace.ts | 460+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/replace_step.ts | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/step.ts | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/structure.ts | 349+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-transform/src/transform.ts | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/LICENSE | 19+++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/moz.yaml | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/package.json | 43+++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/README.md | 38++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/browser.ts | 24++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/capturekeys.ts | 345+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/clipboard.ts | 263+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/decoration.ts | 793+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/dom.ts | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/domchange.ts | 383+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/domcoords.ts | 515+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/domobserver.ts | 349+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/index.ts | 825+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/input.ts | 828+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/selection.ts | 216+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/src/viewdesc.ts | 1586+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror-view/style/prosemirror.css | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/js/prosemirror/prosemirror.bundle.mjs | 1+
Athird_party/js/prosemirror/rollup.config.mjs | 44++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/content/license.html | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
107 files changed, 17966 insertions(+), 0 deletions(-)

diff --git a/third_party/js/prosemirror/.gitignore b/third_party/js/prosemirror/.gitignore @@ -0,0 +1,3 @@ +package-lock.json +**/node_modules/ +**/dist/ diff --git a/third_party/js/prosemirror/LICENSE b/third_party/js/prosemirror/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2016 by Marijn Haverbeke <marijnh@gmail.com> and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/js/prosemirror/README.md b/third_party/js/prosemirror/README.md @@ -0,0 +1,32 @@ +# ProseMirror vendoring + +This directory vendors a ProseMirror bundle. Individual ProseMirror packages are vendored into subfolders. The bundle is built by Rollup from the entry point in `bundle_entry.mjs` and exported via `moz.build`. + +## Updating the bundle + +### Using moz.sh + +To vendor all packages and rebuild the bundle at once you can run: + +```bash +./third_party/js/prosemirror/moz.sh [--mach-vendor-flags] +``` + +`--mach-vendor-flags`: Any `mach vendor` flags (like `--ignore-modified`, `--force`, etc.) will be passed along to each vendoring operation. + +The script: +- Vendors the root `moz.yaml` (only fetches a LICENSE file) +- Vendors each `prosemirror-*/moz.yaml` in turn with `--ignore-modified` to handle spurious changes +- Rebuild the bundle by running `make_bundle.sh` + +### Manual options + +1. Per-package vendoring: `mach vendor third_party/js/prosemirror/prosemirror-*/moz.yaml` +2. Bundle only: Run `./third_party/js/prosemirror/moz.sh` + +After vendoring, commit the updated files. + +## Development notes + +- If you need a bundle that is not minified for stack traces during development you can generate one with `npm run build:dev`. +- Individual package metadata and sources live under `third_party/js/prosemirror/prosemirror-*` with separate `moz.yaml` files. The bundle consumes these via `file:` dependencies in `package.json`. diff --git a/third_party/js/prosemirror/bundle_entry.mjs b/third_party/js/prosemirror/bundle_entry.mjs @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This file exports only the ProseMirror APIs that are being used. +// When adding new functionality the required exports can be added here before +// rebuilding the bundle with. + +// Core +export { baseKeymap } from "prosemirror-commands"; +export { history, undo, redo } from "prosemirror-history"; +export { keymap } from "prosemirror-keymap"; +export { Schema, DOMSerializer, DOMParser } from "prosemirror-model"; +export { schema as basicSchema } from "prosemirror-schema-basic"; +export { EditorState, Plugin, PluginKey, TextSelection } from "prosemirror-state"; +export { EditorView, Decoration, DecorationSet } from "prosemirror-view"; + +// Non-core +export { defaultMarkdownParser } from "prosemirror-markdown"; + +// Third-party +export { suggestionsPlugin, triggerCharacter } from "./prosemirror-suggestions/src/index.js"; diff --git a/third_party/js/prosemirror/make_bundle.sh b/third_party/js/prosemirror/make_bundle.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This script bundles all vendored ProseMirror packages in this directory. + +set -euo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" +MACH=$(realpath "../../../mach") + +# Install local dependencies +"$MACH" npm install + +# Build the bundle +"$MACH" npm run build + +# Remove artifacts +rm -rf node_modules package-lock.json prosemirror-*/{dist,node_modules,package-lock.json} + +# Report bundle size +if [[ -f prosemirror.bundle.mjs ]]; then + BUNDLE_SIZE=$(ls -lh prosemirror.bundle.mjs | awk '{print $5}') + echo "Bundle size: $BUNDLE_SIZE" +fi diff --git a/third_party/js/prosemirror/moz.build b/third_party/js/prosemirror/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Firefox", "General") + +MOZ_SRC_FILES += [ + "prosemirror.bundle.mjs", +] diff --git a/third_party/js/prosemirror/moz.sh b/third_party/js/prosemirror/moz.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +# This script vendors all ProseMirror packages in this directory. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MACH=$(realpath "$SCRIPT_DIR/../../../mach") +ROOT_MOZ_YAML="$SCRIPT_DIR/moz.yaml" + +# Vendor root moz.yaml first +if [[ -f "$ROOT_MOZ_YAML" ]]; then + echo "Vendoring $ROOT_MOZ_YAML" + "$MACH" vendor "$ROOT_MOZ_YAML" "$@" || true +fi + +for manifest in "$SCRIPT_DIR"/prosemirror-*/moz.yaml; do + echo "Vendoring $manifest" + # Always pass --ignore-modified to subfolder vendoring since a previous run + # might have modified `moz.yaml` files. + if ! "$MACH" vendor "$manifest" --ignore-modified "$@"; then + code=$? + # Continue on spurious-check exit + if [[ $code -eq 254 ]]; then + echo "Vendored $manifest with success code $code; continuing" + else + echo "Vendoring failed for $manifest with exit code $code" + exit "$code" + fi + fi +done + +# Build the bundle +"$SCRIPT_DIR/make_bundle.sh" diff --git a/third_party/js/prosemirror/moz.yaml b/third_party/js/prosemirror/moz.yaml @@ -0,0 +1,58 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror + + description: ProseMirror is a content editor based on contentEditable. + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: 0126a40c14d408a275743058d8cd04d3fc64834c (2025-04-22T12:43:21Z). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: 0126a40c14d408a275743058d8cd04d3fc64834c + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MIT + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/ProseMirror/prosemirror + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + flavor: individual-files + + individual-files: + - upstream: LICENSE + destination: LICENSE diff --git a/third_party/js/prosemirror/package.json b/third_party/js/prosemirror/package.json @@ -0,0 +1,26 @@ +{ + "description": "ProseMirror bundle of core, non-core and third-party packages", + "scripts": { + "build": "rollup -c --configMinify rollup.config.mjs", + "build:dev": "rollup -c rollup.config.mjs" + }, + "dependencies": { + "prosemirror-commands": "file:./prosemirror-commands", + "prosemirror-history": "file:./prosemirror-history", + "prosemirror-keymap": "file:./prosemirror-keymap", + "prosemirror-markdown": "file:./prosemirror-markdown", + "prosemirror-model": "file:./prosemirror-model", + "prosemirror-schema-basic": "file:./prosemirror-schema-basic", + "prosemirror-state": "file:./prosemirror-state", + "prosemirror-suggestions": "file:./prosemirror-suggestions", + "prosemirror-transform": "file:./prosemirror-transform", + "prosemirror-view": "file:./prosemirror-view" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^29.0.0", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.1", + "@rollup/plugin-terser": "^0.4.4", + "rollup": "^3.29.4" + } +} diff --git a/third_party/js/prosemirror/prosemirror-commands/LICENSE b/third_party/js/prosemirror/prosemirror-commands/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/js/prosemirror/prosemirror-commands/moz.yaml b/third_party/js/prosemirror/prosemirror-commands/moz.yaml @@ -0,0 +1,63 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror-commands + + description: Editing commands for ProseMirror + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: 1.7.1 (2025-04-13T21:37:45+02:00). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: 1.7.1 + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MIT + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/ProseMirror/prosemirror-commands + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + # Whether to track by commit or tag + tracking: tag + + exclude: + - "**" + + include: + - LICENSE + - src/ + - package.json diff --git a/third_party/js/prosemirror/prosemirror-commands/package.json b/third_party/js/prosemirror/prosemirror-commands/package.json @@ -0,0 +1,39 @@ +{ + "name": "prosemirror-commands", + "version": "1.7.1", + "description": "Editing commands for ProseMirror", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "sideEffects": false, + "license": "MIT", + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijn@haverbeke.berlin", + "web": "http://marijnhaverbeke.nl" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/prosemirror/prosemirror-commands.git" + }, + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-state": "^1.0.0" + }, + "devDependencies": { + "@prosemirror/buildhelper": "^0.1.5", + "prosemirror-test-builder": "^1.0.0" + }, + "scripts": { + "test": "pm-runtests", + "prepare": "pm-buildhelper src/commands.ts" + } +} diff --git a/third_party/js/prosemirror/prosemirror-commands/src/README.md b/third_party/js/prosemirror/prosemirror-commands/src/README.md @@ -0,0 +1,40 @@ +This module exports a number of _commands_, which are building block +functions that encapsulate an editing action. A command function takes +an editor state, _optionally_ a `dispatch` function that it can use +to dispatch a transaction and _optionally_ an `EditorView` instance. +It should return a boolean that indicates whether it could perform any +action. When no `dispatch` callback is passed, the command should do a +'dry run', determining whether it is applicable, but not actually doing +anything. + +These are mostly used to bind keys and define menu items. + +@chainCommands +@deleteSelection +@joinBackward +@selectNodeBackward +@joinTextblockBackward +@joinForward +@selectNodeForward +@joinTextblockForward +@joinUp +@joinDown +@lift +@newlineInCode +@exitCode +@createParagraphNear +@liftEmptyBlock +@splitBlock +@splitBlockAs +@splitBlockKeepMarks +@selectParentNode +@selectAll +@selectTextblockStart +@selectTextblockEnd +@wrapIn +@setBlockType +@toggleMark +@autoJoin +@baseKeymap +@pcBaseKeymap +@macBaseKeymap diff --git a/third_party/js/prosemirror/prosemirror-commands/src/commands.ts b/third_party/js/prosemirror/prosemirror-commands/src/commands.ts @@ -0,0 +1,783 @@ +import {joinPoint, canJoin, findWrapping, liftTarget, canSplit, + ReplaceStep, ReplaceAroundStep, replaceStep} from "prosemirror-transform" +import {Slice, Fragment, Node, NodeType, Attrs, MarkType, ResolvedPos, ContentMatch} from "prosemirror-model" +import {Selection, EditorState, Transaction, TextSelection, NodeSelection, + SelectionRange, AllSelection, Command} from "prosemirror-state" +import {EditorView} from "prosemirror-view" + +/// Delete the selection, if there is one. +export const deleteSelection: Command = (state, dispatch) => { + if (state.selection.empty) return false + if (dispatch) dispatch(state.tr.deleteSelection().scrollIntoView()) + return true +} + +function atBlockStart(state: EditorState, view?: EditorView): ResolvedPos | null { + let {$cursor} = state.selection as TextSelection + if (!$cursor || (view ? !view.endOfTextblock("backward", state) + : $cursor.parentOffset > 0)) + return null + return $cursor +} + +/// If the selection is empty and at the start of a textblock, try to +/// reduce the distance between that block and the one before it—if +/// there's a block directly before it that can be joined, join them. +/// If not, try to move the selected block closer to the next one in +/// the document structure by lifting it out of its parent or moving it +/// into a parent of the previous block. Will use the view for accurate +/// (bidi-aware) start-of-textblock detection if given. +export const joinBackward: Command = (state, dispatch, view) => { + let $cursor = atBlockStart(state, view) + if (!$cursor) return false + + let $cut = findCutBefore($cursor) + + // If there is no node before this, try to lift + if (!$cut) { + let range = $cursor.blockRange(), target = range && liftTarget(range) + if (target == null) return false + if (dispatch) dispatch(state.tr.lift(range!, target).scrollIntoView()) + return true + } + + let before = $cut.nodeBefore! + // Apply the joining algorithm + if (deleteBarrier(state, $cut, dispatch, -1)) return true + + // If the node below has no content and the node above is + // selectable, delete the node below and select the one above. + if ($cursor.parent.content.size == 0 && + (textblockAt(before, "end") || NodeSelection.isSelectable(before))) { + for (let depth = $cursor.depth;; depth--) { + let delStep = replaceStep(state.doc, $cursor.before(depth), $cursor.after(depth), Slice.empty) + if (delStep && (delStep as ReplaceStep).slice.size < (delStep as ReplaceStep).to - (delStep as ReplaceStep).from) { + if (dispatch) { + let tr = state.tr.step(delStep) + tr.setSelection(textblockAt(before, "end") + ? Selection.findFrom(tr.doc.resolve(tr.mapping.map($cut.pos, -1)), -1)! + : NodeSelection.create(tr.doc, $cut.pos - before.nodeSize)) + dispatch(tr.scrollIntoView()) + } + return true + } + if (depth == 1 || $cursor.node(depth - 1).childCount > 1) break + } + } + + // If the node before is an atom, delete it + if (before.isAtom && $cut.depth == $cursor.depth - 1) { + if (dispatch) dispatch(state.tr.delete($cut.pos - before.nodeSize, $cut.pos).scrollIntoView()) + return true + } + + return false +} + +/// A more limited form of [`joinBackward`](#commands.joinBackward) +/// that only tries to join the current textblock to the one before +/// it, if the cursor is at the start of a textblock. +export const joinTextblockBackward: Command = (state, dispatch, view) => { + let $cursor = atBlockStart(state, view) + if (!$cursor) return false + let $cut = findCutBefore($cursor) + return $cut ? joinTextblocksAround(state, $cut, dispatch) : false +} + +/// A more limited form of [`joinForward`](#commands.joinForward) +/// that only tries to join the current textblock to the one after +/// it, if the cursor is at the end of a textblock. +export const joinTextblockForward: Command = (state, dispatch, view) => { + let $cursor = atBlockEnd(state, view) + if (!$cursor) return false + let $cut = findCutAfter($cursor) + return $cut ? joinTextblocksAround(state, $cut, dispatch) : false +} + +function joinTextblocksAround(state: EditorState, $cut: ResolvedPos, dispatch?: (tr: Transaction) => void) { + let before = $cut.nodeBefore!, beforeText = before, beforePos = $cut.pos - 1 + for (; !beforeText.isTextblock; beforePos--) { + if (beforeText.type.spec.isolating) return false + let child = beforeText.lastChild + if (!child) return false + beforeText = child + } + let after = $cut.nodeAfter!, afterText = after, afterPos = $cut.pos + 1 + for (; !afterText.isTextblock; afterPos++) { + if (afterText.type.spec.isolating) return false + let child = afterText.firstChild + if (!child) return false + afterText = child + } + let step = replaceStep(state.doc, beforePos, afterPos, Slice.empty) as ReplaceStep | null + if (!step || step.from != beforePos || + step instanceof ReplaceStep && step.slice.size >= afterPos - beforePos) return false + if (dispatch) { + let tr = state.tr.step(step) + tr.setSelection(TextSelection.create(tr.doc, beforePos)) + dispatch(tr.scrollIntoView()) + } + return true + +} + +function textblockAt(node: Node, side: "start" | "end", only = false) { + for (let scan: Node | null = node; scan; scan = (side == "start" ? scan.firstChild : scan.lastChild)) { + if (scan.isTextblock) return true + if (only && scan.childCount != 1) return false + } + return false +} + +/// When the selection is empty and at the start of a textblock, select +/// the node before that textblock, if possible. This is intended to be +/// bound to keys like backspace, after +/// [`joinBackward`](#commands.joinBackward) or other deleting +/// commands, as a fall-back behavior when the schema doesn't allow +/// deletion at the selected point. +export const selectNodeBackward: Command = (state, dispatch, view) => { + let {$head, empty} = state.selection, $cut: ResolvedPos | null = $head + if (!empty) return false + + if ($head.parent.isTextblock) { + if (view ? !view.endOfTextblock("backward", state) : $head.parentOffset > 0) return false + $cut = findCutBefore($head) + } + let node = $cut && $cut.nodeBefore + if (!node || !NodeSelection.isSelectable(node)) return false + if (dispatch) + dispatch(state.tr.setSelection(NodeSelection.create(state.doc, $cut!.pos - node.nodeSize)).scrollIntoView()) + return true +} + +function findCutBefore($pos: ResolvedPos): ResolvedPos | null { + if (!$pos.parent.type.spec.isolating) for (let i = $pos.depth - 1; i >= 0; i--) { + if ($pos.index(i) > 0) return $pos.doc.resolve($pos.before(i + 1)) + if ($pos.node(i).type.spec.isolating) break + } + return null +} + +function atBlockEnd(state: EditorState, view?: EditorView): ResolvedPos | null { + let {$cursor} = state.selection as TextSelection + if (!$cursor || (view ? !view.endOfTextblock("forward", state) + : $cursor.parentOffset < $cursor.parent.content.size)) + return null + return $cursor +} + +/// If the selection is empty and the cursor is at the end of a +/// textblock, try to reduce or remove the boundary between that block +/// and the one after it, either by joining them or by moving the other +/// block closer to this one in the tree structure. Will use the view +/// for accurate start-of-textblock detection if given. +export const joinForward: Command = (state, dispatch, view) => { + let $cursor = atBlockEnd(state, view) + if (!$cursor) return false + + let $cut = findCutAfter($cursor) + // If there is no node after this, there's nothing to do + if (!$cut) return false + + let after = $cut.nodeAfter! + // Try the joining algorithm + if (deleteBarrier(state, $cut, dispatch, 1)) return true + + // If the node above has no content and the node below is + // selectable, delete the node above and select the one below. + if ($cursor.parent.content.size == 0 && + (textblockAt(after, "start") || NodeSelection.isSelectable(after))) { + let delStep = replaceStep(state.doc, $cursor.before(), $cursor.after(), Slice.empty) + if (delStep && (delStep as ReplaceStep).slice.size < (delStep as ReplaceStep).to - (delStep as ReplaceStep).from) { + if (dispatch) { + let tr = state.tr.step(delStep) + tr.setSelection(textblockAt(after, "start") ? Selection.findFrom(tr.doc.resolve(tr.mapping.map($cut.pos)), 1)! + : NodeSelection.create(tr.doc, tr.mapping.map($cut.pos))) + dispatch(tr.scrollIntoView()) + } + return true + } + } + + // If the next node is an atom, delete it + if (after.isAtom && $cut.depth == $cursor.depth - 1) { + if (dispatch) dispatch(state.tr.delete($cut.pos, $cut.pos + after.nodeSize).scrollIntoView()) + return true + } + + return false +} + +/// When the selection is empty and at the end of a textblock, select +/// the node coming after that textblock, if possible. This is intended +/// to be bound to keys like delete, after +/// [`joinForward`](#commands.joinForward) and similar deleting +/// commands, to provide a fall-back behavior when the schema doesn't +/// allow deletion at the selected point. +export const selectNodeForward: Command = (state, dispatch, view) => { + let {$head, empty} = state.selection, $cut: ResolvedPos | null = $head + if (!empty) return false + if ($head.parent.isTextblock) { + if (view ? !view.endOfTextblock("forward", state) : $head.parentOffset < $head.parent.content.size) + return false + $cut = findCutAfter($head) + } + let node = $cut && $cut.nodeAfter + if (!node || !NodeSelection.isSelectable(node)) return false + if (dispatch) + dispatch(state.tr.setSelection(NodeSelection.create(state.doc, $cut!.pos)).scrollIntoView()) + return true +} + +function findCutAfter($pos: ResolvedPos) { + if (!$pos.parent.type.spec.isolating) for (let i = $pos.depth - 1; i >= 0; i--) { + let parent = $pos.node(i) + if ($pos.index(i) + 1 < parent.childCount) return $pos.doc.resolve($pos.after(i + 1)) + if (parent.type.spec.isolating) break + } + return null +} + +/// Join the selected block or, if there is a text selection, the +/// closest ancestor block of the selection that can be joined, with +/// the sibling above it. +export const joinUp: Command = (state, dispatch) => { + let sel = state.selection, nodeSel = sel instanceof NodeSelection, point + if (nodeSel) { + if ((sel as NodeSelection).node.isTextblock || !canJoin(state.doc, sel.from)) return false + point = sel.from + } else { + point = joinPoint(state.doc, sel.from, -1) + if (point == null) return false + } + if (dispatch) { + let tr = state.tr.join(point) + if (nodeSel) tr.setSelection(NodeSelection.create(tr.doc, point - state.doc.resolve(point).nodeBefore!.nodeSize)) + dispatch(tr.scrollIntoView()) + } + return true +} + +/// Join the selected block, or the closest ancestor of the selection +/// that can be joined, with the sibling after it. +export const joinDown: Command = (state, dispatch) => { + let sel = state.selection, point + if (sel instanceof NodeSelection) { + if (sel.node.isTextblock || !canJoin(state.doc, sel.to)) return false + point = sel.to + } else { + point = joinPoint(state.doc, sel.to, 1) + if (point == null) return false + } + if (dispatch) + dispatch(state.tr.join(point).scrollIntoView()) + return true +} + +/// Lift the selected block, or the closest ancestor block of the +/// selection that can be lifted, out of its parent node. +export const lift: Command = (state, dispatch) => { + let {$from, $to} = state.selection + let range = $from.blockRange($to), target = range && liftTarget(range) + if (target == null) return false + if (dispatch) dispatch(state.tr.lift(range!, target).scrollIntoView()) + return true +} + +/// If the selection is in a node whose type has a truthy +/// [`code`](#model.NodeSpec.code) property in its spec, replace the +/// selection with a newline character. +export const newlineInCode: Command = (state, dispatch) => { + let {$head, $anchor} = state.selection + if (!$head.parent.type.spec.code || !$head.sameParent($anchor)) return false + if (dispatch) dispatch(state.tr.insertText("\n").scrollIntoView()) + return true +} + +function defaultBlockAt(match: ContentMatch) { + for (let i = 0; i < match.edgeCount; i++) { + let {type} = match.edge(i) + if (type.isTextblock && !type.hasRequiredAttrs()) return type + } + return null +} + +/// When the selection is in a node with a truthy +/// [`code`](#model.NodeSpec.code) property in its spec, create a +/// default block after the code block, and move the cursor there. +export const exitCode: Command = (state, dispatch) => { + let {$head, $anchor} = state.selection + if (!$head.parent.type.spec.code || !$head.sameParent($anchor)) return false + let above = $head.node(-1), after = $head.indexAfter(-1), type = defaultBlockAt(above.contentMatchAt(after)) + if (!type || !above.canReplaceWith(after, after, type)) return false + if (dispatch) { + let pos = $head.after(), tr = state.tr.replaceWith(pos, pos, type.createAndFill()!) + tr.setSelection(Selection.near(tr.doc.resolve(pos), 1)) + dispatch(tr.scrollIntoView()) + } + return true +} + +/// If a block node is selected, create an empty paragraph before (if +/// it is its parent's first child) or after it. +export const createParagraphNear: Command = (state, dispatch) => { + let sel = state.selection, {$from, $to} = sel + if (sel instanceof AllSelection || $from.parent.inlineContent || $to.parent.inlineContent) return false + let type = defaultBlockAt($to.parent.contentMatchAt($to.indexAfter())) + if (!type || !type.isTextblock) return false + if (dispatch) { + let side = (!$from.parentOffset && $to.index() < $to.parent.childCount ? $from : $to).pos + let tr = state.tr.insert(side, type.createAndFill()!) + tr.setSelection(TextSelection.create(tr.doc, side + 1)) + dispatch(tr.scrollIntoView()) + } + return true +} + +/// If the cursor is in an empty textblock that can be lifted, lift the +/// block. +export const liftEmptyBlock: Command = (state, dispatch) => { + let {$cursor} = state.selection as TextSelection + if (!$cursor || $cursor.parent.content.size) return false + if ($cursor.depth > 1 && $cursor.after() != $cursor.end(-1)) { + let before = $cursor.before() + if (canSplit(state.doc, before)) { + if (dispatch) dispatch(state.tr.split(before).scrollIntoView()) + return true + } + } + let range = $cursor.blockRange(), target = range && liftTarget(range) + if (target == null) return false + if (dispatch) dispatch(state.tr.lift(range!, target).scrollIntoView()) + return true +} + +/// Create a variant of [`splitBlock`](#commands.splitBlock) that uses +/// a custom function to determine the type of the newly split off block. +export function splitBlockAs( + splitNode?: (node: Node, atEnd: boolean, $from: ResolvedPos) => {type: NodeType, attrs?: Attrs} | null +): Command { + return (state, dispatch) => { + let {$from, $to} = state.selection + if (state.selection instanceof NodeSelection && state.selection.node.isBlock) { + if (!$from.parentOffset || !canSplit(state.doc, $from.pos)) return false + if (dispatch) dispatch(state.tr.split($from.pos).scrollIntoView()) + return true + } + + if (!$from.depth) return false + let types: (null | {type: NodeType, attrs?: Attrs | null})[] = [] + let splitDepth, deflt, atEnd = false, atStart = false + for (let d = $from.depth;; d--) { + let node = $from.node(d) + if (node.isBlock) { + atEnd = $from.end(d) == $from.pos + ($from.depth - d) + atStart = $from.start(d) == $from.pos - ($from.depth - d) + deflt = defaultBlockAt($from.node(d - 1).contentMatchAt($from.indexAfter(d - 1))) + let splitType = splitNode && splitNode($to.parent, atEnd, $from) + types.unshift(splitType || (atEnd && deflt ? {type: deflt} : null)) + splitDepth = d + break + } else { + if (d == 1) return false + types.unshift(null) + } + } + + let tr = state.tr + if (state.selection instanceof TextSelection || state.selection instanceof AllSelection) tr.deleteSelection() + let splitPos = tr.mapping.map($from.pos) + let can = canSplit(tr.doc, splitPos, types.length, types) + if (!can) { + types[0] = deflt ? {type: deflt} : null + can = canSplit(tr.doc, splitPos, types.length, types) + } + if (!can) return false + tr.split(splitPos, types.length, types) + if (!atEnd && atStart && $from.node(splitDepth).type != deflt) { + let first = tr.mapping.map($from.before(splitDepth)), $first = tr.doc.resolve(first) + if (deflt && $from.node(splitDepth - 1).canReplaceWith($first.index(), $first.index() + 1, deflt)) + tr.setNodeMarkup(tr.mapping.map($from.before(splitDepth)), deflt) + } + if (dispatch) dispatch(tr.scrollIntoView()) + return true + } +} + +/// Split the parent block of the selection. If the selection is a text +/// selection, also delete its content. +export const splitBlock: Command = splitBlockAs() + +/// Acts like [`splitBlock`](#commands.splitBlock), but without +/// resetting the set of active marks at the cursor. +export const splitBlockKeepMarks: Command = (state, dispatch) => { + return splitBlock(state, dispatch && (tr => { + let marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()) + if (marks) tr.ensureMarks(marks) + dispatch(tr) + })) +} + +/// Move the selection to the node wrapping the current selection, if +/// any. (Will not select the document node.) +export const selectParentNode: Command = (state, dispatch) => { + let {$from, to} = state.selection, pos + let same = $from.sharedDepth(to) + if (same == 0) return false + pos = $from.before(same) + if (dispatch) dispatch(state.tr.setSelection(NodeSelection.create(state.doc, pos))) + return true +} + +/// Select the whole document. +export const selectAll: Command = (state, dispatch) => { + if (dispatch) dispatch(state.tr.setSelection(new AllSelection(state.doc))) + return true +} + +function joinMaybeClear(state: EditorState, $pos: ResolvedPos, dispatch: ((tr: Transaction) => void) | undefined) { + let before = $pos.nodeBefore, after = $pos.nodeAfter, index = $pos.index() + if (!before || !after || !before.type.compatibleContent(after.type)) return false + if (!before.content.size && $pos.parent.canReplace(index - 1, index)) { + if (dispatch) dispatch(state.tr.delete($pos.pos - before.nodeSize, $pos.pos).scrollIntoView()) + return true + } + if (!$pos.parent.canReplace(index, index + 1) || !(after.isTextblock || canJoin(state.doc, $pos.pos))) + return false + if (dispatch) + dispatch(state.tr.join($pos.pos).scrollIntoView()) + return true +} + +function deleteBarrier(state: EditorState, $cut: ResolvedPos, dispatch: ((tr: Transaction) => void) | undefined, dir: number) { + let before = $cut.nodeBefore!, after = $cut.nodeAfter!, conn, match + let isolated = before.type.spec.isolating || after.type.spec.isolating + if (!isolated && joinMaybeClear(state, $cut, dispatch)) return true + + let canDelAfter = !isolated && $cut.parent.canReplace($cut.index(), $cut.index() + 1) + if (canDelAfter && + (conn = (match = before.contentMatchAt(before.childCount)).findWrapping(after.type)) && + match.matchType(conn[0] || after.type)!.validEnd) { + if (dispatch) { + let end = $cut.pos + after.nodeSize, wrap = Fragment.empty + for (let i = conn.length - 1; i >= 0; i--) + wrap = Fragment.from(conn[i].create(null, wrap)) + wrap = Fragment.from(before.copy(wrap)) + let tr = state.tr.step(new ReplaceAroundStep($cut.pos - 1, end, $cut.pos, end, new Slice(wrap, 1, 0), conn.length, true)) + let $joinAt = tr.doc.resolve(end + 2 * conn.length) + if ($joinAt.nodeAfter && $joinAt.nodeAfter.type == before.type && + canJoin(tr.doc, $joinAt.pos)) tr.join($joinAt.pos) + dispatch(tr.scrollIntoView()) + } + return true + } + + let selAfter = after.type.spec.isolating || (dir > 0 && isolated) ? null : Selection.findFrom($cut, 1) + let range = selAfter && selAfter.$from.blockRange(selAfter.$to), target = range && liftTarget(range) + if (target != null && target >= $cut.depth) { + if (dispatch) dispatch(state.tr.lift(range!, target).scrollIntoView()) + return true + } + + if (canDelAfter && textblockAt(after, "start", true) && textblockAt(before, "end")) { + let at = before, wrap = [] + for (;;) { + wrap.push(at) + if (at.isTextblock) break + at = at.lastChild! + } + let afterText = after, afterDepth = 1 + for (; !afterText.isTextblock; afterText = afterText.firstChild!) afterDepth++ + if (at.canReplace(at.childCount, at.childCount, afterText.content)) { + if (dispatch) { + let end = Fragment.empty + for (let i = wrap.length - 1; i >= 0; i--) end = Fragment.from(wrap[i].copy(end)) + let tr = state.tr.step(new ReplaceAroundStep($cut.pos - wrap.length, $cut.pos + after.nodeSize, + $cut.pos + afterDepth, $cut.pos + after.nodeSize - afterDepth, + new Slice(end, wrap.length, 0), 0, true)) + dispatch(tr.scrollIntoView()) + } + return true + } + } + + return false +} + +function selectTextblockSide(side: number): Command { + return function(state, dispatch) { + let sel = state.selection, $pos = side < 0 ? sel.$from : sel.$to + let depth = $pos.depth + while ($pos.node(depth).isInline) { + if (!depth) return false + depth-- + } + if (!$pos.node(depth).isTextblock) return false + if (dispatch) + dispatch(state.tr.setSelection(TextSelection.create( + state.doc, side < 0 ? $pos.start(depth) : $pos.end(depth)))) + return true + } +} + +/// Moves the cursor to the start of current text block. +export const selectTextblockStart = selectTextblockSide(-1) + +/// Moves the cursor to the end of current text block. +export const selectTextblockEnd = selectTextblockSide(1) + +// Parameterized commands + +/// Wrap the selection in a node of the given type with the given +/// attributes. +export function wrapIn(nodeType: NodeType, attrs: Attrs | null = null): Command { + return function(state, dispatch) { + let {$from, $to} = state.selection + let range = $from.blockRange($to), wrapping = range && findWrapping(range, nodeType, attrs) + if (!wrapping) return false + if (dispatch) dispatch(state.tr.wrap(range!, wrapping).scrollIntoView()) + return true + } +} + +/// Returns a command that tries to set the selected textblocks to the +/// given node type with the given attributes. +export function setBlockType(nodeType: NodeType, attrs: Attrs | null = null): Command { + return function(state, dispatch) { + let applicable = false + for (let i = 0; i < state.selection.ranges.length && !applicable; i++) { + let {$from: {pos: from}, $to: {pos: to}} = state.selection.ranges[i] + state.doc.nodesBetween(from, to, (node, pos) => { + if (applicable) return false + if (!node.isTextblock || node.hasMarkup(nodeType, attrs)) return + if (node.type == nodeType) { + applicable = true + } else { + let $pos = state.doc.resolve(pos), index = $pos.index() + applicable = $pos.parent.canReplaceWith(index, index + 1, nodeType) + } + }) + } + if (!applicable) return false + if (dispatch) { + let tr = state.tr + for (let i = 0; i < state.selection.ranges.length; i++) { + let {$from: {pos: from}, $to: {pos: to}} = state.selection.ranges[i] + tr.setBlockType(from, to, nodeType, attrs) + } + dispatch(tr.scrollIntoView()) + } + return true + } +} + +function markApplies(doc: Node, ranges: readonly SelectionRange[], type: MarkType, enterAtoms: boolean) { + for (let i = 0; i < ranges.length; i++) { + let {$from, $to} = ranges[i] + let can = $from.depth == 0 ? doc.inlineContent && doc.type.allowsMarkType(type) : false + doc.nodesBetween($from.pos, $to.pos, (node, pos) => { + if (can || !enterAtoms && node.isAtom && node.isInline && pos >= $from.pos && pos + node.nodeSize <= $to.pos) + return false + can = node.inlineContent && node.type.allowsMarkType(type) + }) + if (can) return true + } + return false +} + +function removeInlineAtoms(ranges: readonly SelectionRange[]): readonly SelectionRange[] { + let result = [] + for (let i = 0; i < ranges.length; i++) { + let {$from, $to} = ranges[i] + $from.doc.nodesBetween($from.pos, $to.pos, (node, pos) => { + if (node.isAtom && node.content.size && node.isInline && pos >= $from.pos && pos + node.nodeSize <= $to.pos) { + if (pos + 1 > $from.pos) result.push(new SelectionRange($from, $from.doc.resolve(pos + 1))) + $from = $from.doc.resolve(pos + 1 + node.content.size) + return false + } + }) + if ($from.pos < $to.pos) result.push(new SelectionRange($from, $to)) + } + return result +} + +/// Create a command function that toggles the given mark with the +/// given attributes. Will return `false` when the current selection +/// doesn't support that mark. This will remove the mark if any marks +/// of that type exist in the selection, or add it otherwise. If the +/// selection is empty, this applies to the [stored +/// marks](#state.EditorState.storedMarks) instead of a range of the +/// document. +export function toggleMark(markType: MarkType, attrs: Attrs | null = null, options?: { + /// Controls whether, when part of the selected range has the mark + /// already and part doesn't, the mark is removed (`true`, the + /// default) or added (`false`). + removeWhenPresent?: boolean + /// When set to false, this will prevent the command from acting on + /// the content of inline nodes marked as + /// [atoms](#model.NodeSpec.atom) that are completely covered by a + /// selection range. + enterInlineAtoms?: boolean + /// By default, this command doesn't apply to leading and trailing + /// whitespace in the selection. Set this to `true` to change that. + includeWhitespace?: boolean +}): Command { + let removeWhenPresent = (options && options.removeWhenPresent) !== false + let enterAtoms = (options && options.enterInlineAtoms) !== false + let dropSpace = !(options && options.includeWhitespace) + return function(state, dispatch) { + let {empty, $cursor, ranges} = state.selection as TextSelection + if ((empty && !$cursor) || !markApplies(state.doc, ranges, markType, enterAtoms)) return false + if (dispatch) { + if ($cursor) { + if (markType.isInSet(state.storedMarks || $cursor.marks())) + dispatch(state.tr.removeStoredMark(markType)) + else + dispatch(state.tr.addStoredMark(markType.create(attrs))) + } else { + let add, tr = state.tr + if (!enterAtoms) ranges = removeInlineAtoms(ranges) + if (removeWhenPresent) { + add = !ranges.some(r => state.doc.rangeHasMark(r.$from.pos, r.$to.pos, markType)) + } else { + add = !ranges.every(r => { + let missing = false + tr.doc.nodesBetween(r.$from.pos, r.$to.pos, (node, pos, parent) => { + if (missing) return false + missing = !markType.isInSet(node.marks) && !!parent && parent.type.allowsMarkType(markType) && + !(node.isText && /^\s*$/.test(node.textBetween(Math.max(0, r.$from.pos - pos), + Math.min(node.nodeSize, r.$to.pos - pos)))) + }) + return !missing + }) + } + for (let i = 0; i < ranges.length; i++) { + let {$from, $to} = ranges[i] + if (!add) { + tr.removeMark($from.pos, $to.pos, markType) + } else { + let from = $from.pos, to = $to.pos, start = $from.nodeAfter, end = $to.nodeBefore + let spaceStart = dropSpace && start && start.isText ? /^\s*/.exec(start.text!)![0].length : 0 + let spaceEnd = dropSpace && end && end.isText ? /\s*$/.exec(end.text!)![0].length : 0 + if (from + spaceStart < to) { from += spaceStart; to -= spaceEnd } + tr.addMark(from, to, markType.create(attrs)) + } + } + dispatch(tr.scrollIntoView()) + } + } + return true + } +} + +function wrapDispatchForJoin(dispatch: (tr: Transaction) => void, isJoinable: (a: Node, b: Node) => boolean) { + return (tr: Transaction) => { + if (!tr.isGeneric) return dispatch(tr) + + let ranges: number[] = [] + for (let i = 0; i < tr.mapping.maps.length; i++) { + let map = tr.mapping.maps[i] + for (let j = 0; j < ranges.length; j++) + ranges[j] = map.map(ranges[j]) + map.forEach((_s, _e, from, to) => ranges.push(from, to)) + } + + // Figure out which joinable points exist inside those ranges, + // by checking all node boundaries in their parent nodes. + let joinable = [] + for (let i = 0; i < ranges.length; i += 2) { + let from = ranges[i], to = ranges[i + 1] + let $from = tr.doc.resolve(from), depth = $from.sharedDepth(to), parent = $from.node(depth) + for (let index = $from.indexAfter(depth), pos = $from.after(depth + 1); pos <= to; ++index) { + let after = parent.maybeChild(index) + if (!after) break + if (index && joinable.indexOf(pos) == -1) { + let before = parent.child(index - 1) + if (before.type == after.type && isJoinable(before, after)) + joinable.push(pos) + } + pos += after.nodeSize + } + } + // Join the joinable points + joinable.sort((a, b) => a - b) + for (let i = joinable.length - 1; i >= 0; i--) { + if (canJoin(tr.doc, joinable[i])) tr.join(joinable[i]) + } + dispatch(tr) + } +} + +/// Wrap a command so that, when it produces a transform that causes +/// two joinable nodes to end up next to each other, those are joined. +/// Nodes are considered joinable when they are of the same type and +/// when the `isJoinable` predicate returns true for them or, if an +/// array of strings was passed, if their node type name is in that +/// array. +export function autoJoin( + command: Command, + isJoinable: ((before: Node, after: Node) => boolean) | readonly string[] +): Command { + let canJoin = Array.isArray(isJoinable) ? (node: Node) => isJoinable.indexOf(node.type.name) > -1 + : isJoinable as (a: Node, b: Node) => boolean + return (state, dispatch, view) => command(state, dispatch && wrapDispatchForJoin(dispatch, canJoin), view) +} + +/// Combine a number of command functions into a single function (which +/// calls them one by one until one returns true). +export function chainCommands(...commands: readonly Command[]): Command { + return function(state, dispatch, view) { + for (let i = 0; i < commands.length; i++) + if (commands[i](state, dispatch, view)) return true + return false + } +} + +let backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward) +let del = chainCommands(deleteSelection, joinForward, selectNodeForward) + +/// A basic keymap containing bindings not specific to any schema. +/// Binds the following keys (when multiple commands are listed, they +/// are chained with [`chainCommands`](#commands.chainCommands)): +/// +/// * **Enter** to `newlineInCode`, `createParagraphNear`, `liftEmptyBlock`, `splitBlock` +/// * **Mod-Enter** to `exitCode` +/// * **Backspace** and **Mod-Backspace** to `deleteSelection`, `joinBackward`, `selectNodeBackward` +/// * **Delete** and **Mod-Delete** to `deleteSelection`, `joinForward`, `selectNodeForward` +/// * **Mod-Delete** to `deleteSelection`, `joinForward`, `selectNodeForward` +/// * **Mod-a** to `selectAll` +export const pcBaseKeymap: {[key: string]: Command} = { + "Enter": chainCommands(newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock), + "Mod-Enter": exitCode, + "Backspace": backspace, + "Mod-Backspace": backspace, + "Shift-Backspace": backspace, + "Delete": del, + "Mod-Delete": del, + "Mod-a": selectAll +} + +/// A copy of `pcBaseKeymap` that also binds **Ctrl-h** like Backspace, +/// **Ctrl-d** like Delete, **Alt-Backspace** like Ctrl-Backspace, and +/// **Ctrl-Alt-Backspace**, **Alt-Delete**, and **Alt-d** like +/// Ctrl-Delete. +export const macBaseKeymap: {[key: string]: Command} = { + "Ctrl-h": pcBaseKeymap["Backspace"], + "Alt-Backspace": pcBaseKeymap["Mod-Backspace"], + "Ctrl-d": pcBaseKeymap["Delete"], + "Ctrl-Alt-Backspace": pcBaseKeymap["Mod-Delete"], + "Alt-Delete": pcBaseKeymap["Mod-Delete"], + "Alt-d": pcBaseKeymap["Mod-Delete"], + "Ctrl-a": selectTextblockStart, + "Ctrl-e": selectTextblockEnd +} +for (let key in pcBaseKeymap) (macBaseKeymap as any)[key] = pcBaseKeymap[key] + +const mac = typeof navigator != "undefined" ? /Mac|iP(hone|[oa]d)/.test(navigator.platform) + // @ts-ignore + : typeof os != "undefined" && os.platform ? os.platform() == "darwin" : false + +/// Depending on the detected platform, this will hold +/// [`pcBasekeymap`](#commands.pcBaseKeymap) or +/// [`macBaseKeymap`](#commands.macBaseKeymap). +export const baseKeymap: {[key: string]: Command} = mac ? macBaseKeymap : pcBaseKeymap diff --git a/third_party/js/prosemirror/prosemirror-history/LICENSE b/third_party/js/prosemirror/prosemirror-history/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/js/prosemirror/prosemirror-history/moz.yaml b/third_party/js/prosemirror/prosemirror-history/moz.yaml @@ -0,0 +1,63 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror-history + + description: Undo history for ProseMirror + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: 1.5.0 (2025-11-13T08:22:24+01:00). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: 1.5.0 + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MIT + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/ProseMirror/prosemirror-history + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + # Whether to track by commit or tag + tracking: tag + + exclude: + - "**" + + include: + - LICENSE + - src/ + - package.json diff --git a/third_party/js/prosemirror/prosemirror-history/package.json b/third_party/js/prosemirror/prosemirror-history/package.json @@ -0,0 +1,40 @@ +{ + "name": "prosemirror-history", + "version": "1.5.0", + "description": "Undo history for ProseMirror", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "sideEffects": false, + "license": "MIT", + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijn@haverbeke.berlin", + "web": "http://marijnhaverbeke.nl" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/prosemirror/prosemirror-history.git" + }, + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + }, + "devDependencies": { + "@prosemirror/buildhelper": "^0.1.5", + "prosemirror-test-builder": "^1.0.0" + }, + "scripts": { + "test": "pm-runtests", + "prepare": "pm-buildhelper src/history.ts" + } +} diff --git a/third_party/js/prosemirror/prosemirror-history/src/README.md b/third_party/js/prosemirror/prosemirror-history/src/README.md @@ -0,0 +1,23 @@ +An implementation of an undo/redo history for ProseMirror. This +history is _selective_, meaning it does not just roll back to a +previous state but can undo some changes while keeping other, later +changes intact. (This is necessary for collaborative editing, and +comes up in other situations as well.) + +@history + +@undo + +@redo + +@undoNoScroll + +@redoNoScroll + +@undoDepth + +@redoDepth + +@closeHistory + +@isHistoryTransaction diff --git a/third_party/js/prosemirror/prosemirror-history/src/history.ts b/third_party/js/prosemirror/prosemirror-history/src/history.ts @@ -0,0 +1,465 @@ +import RopeSequence from "rope-sequence" +import {Mapping, Step, StepMap, Transform} from "prosemirror-transform" +import {Plugin, Command, PluginKey, EditorState, Transaction, SelectionBookmark} from "prosemirror-state" + +// ProseMirror's history isn't simply a way to roll back to a previous +// state, because ProseMirror supports applying changes without adding +// them to the history (for example during collaboration). +// +// To this end, each 'Branch' (one for the undo history and one for +// the redo history) keeps an array of 'Items', which can optionally +// hold a step (an actual undoable change), and always hold a position +// map (which is needed to move changes below them to apply to the +// current document). +// +// An item that has both a step and a selection bookmark is the start +// of an 'event' — a group of changes that will be undone or redone at +// once. (It stores only the bookmark, since that way we don't have to +// provide a document until the selection is actually applied, which +// is useful when compressing.) + +// Used to schedule history compression +const max_empty_items = 500 + +class Branch { + constructor(readonly items: RopeSequence<Item>, readonly eventCount: number) {} + + // Pop the latest event off the branch's history and apply it + // to a document transform. + popEvent(state: EditorState, preserveItems: boolean) { + if (this.eventCount == 0) return null + + let end = this.items.length + for (;; end--) { + let next = this.items.get(end - 1) + if (next.selection) { --end; break } + } + + let remap: Mapping | undefined, mapFrom: number | undefined + if (preserveItems) { + remap = this.remapping(end, this.items.length) + mapFrom = remap.maps.length + } + let transform = state.tr + let selection: SelectionBookmark | undefined, remaining: Branch | undefined + let addAfter: Item[] = [], addBefore: Item[] = [] + + this.items.forEach((item, i) => { + if (!item.step) { + if (!remap) { + remap = this.remapping(end, i + 1) + mapFrom = remap.maps.length + } + mapFrom!-- + addBefore.push(item) + return + } + + if (remap) { + addBefore.push(new Item(item.map)) + let step = item.step.map(remap.slice(mapFrom)), map + + if (step && transform.maybeStep(step).doc) { + map = transform.mapping.maps[transform.mapping.maps.length - 1] + addAfter.push(new Item(map, undefined, undefined, addAfter.length + addBefore.length)) + } + mapFrom!-- + if (map) remap.appendMap(map, mapFrom) + } else { + transform.maybeStep(item.step) + } + + if (item.selection) { + selection = remap ? item.selection.map(remap.slice(mapFrom)) : item.selection + remaining = new Branch(this.items.slice(0, end).append(addBefore.reverse().concat(addAfter)), this.eventCount - 1) + return false + } + }, this.items.length, 0) + + return {remaining: remaining!, transform, selection: selection!} + } + + // Create a new branch with the given transform added. + addTransform(transform: Transform, selection: SelectionBookmark | undefined, + histOptions: Required<HistoryOptions>, preserveItems: boolean) { + let newItems = [], eventCount = this.eventCount + let oldItems = this.items, lastItem = !preserveItems && oldItems.length ? oldItems.get(oldItems.length - 1) : null + + for (let i = 0; i < transform.steps.length; i++) { + let step = transform.steps[i].invert(transform.docs[i]) + let item = new Item(transform.mapping.maps[i], step, selection), merged + if (merged = lastItem && lastItem.merge(item)) { + item = merged + if (i) newItems.pop() + else oldItems = oldItems.slice(0, oldItems.length - 1) + } + newItems.push(item) + if (selection) { + eventCount++ + selection = undefined + } + if (!preserveItems) lastItem = item + } + let overflow = eventCount - histOptions.depth + if (overflow > DEPTH_OVERFLOW) { + oldItems = cutOffEvents(oldItems, overflow) + eventCount -= overflow + } + return new Branch(oldItems.append(newItems), eventCount) + } + + remapping(from: number, to: number): Mapping { + let maps = new Mapping + this.items.forEach((item, i) => { + let mirrorPos = item.mirrorOffset != null && i - item.mirrorOffset >= from + ? maps.maps.length - item.mirrorOffset : undefined + maps.appendMap(item.map, mirrorPos) + }, from, to) + return maps + } + + addMaps(array: readonly StepMap[]) { + if (this.eventCount == 0) return this + return new Branch(this.items.append(array.map(map => new Item(map))), this.eventCount) + } + + // When the collab module receives remote changes, the history has + // to know about those, so that it can adjust the steps that were + // rebased on top of the remote changes, and include the position + // maps for the remote changes in its array of items. + rebased(rebasedTransform: Transform, rebasedCount: number) { + if (!this.eventCount) return this + + let rebasedItems: Item[] = [], start = Math.max(0, this.items.length - rebasedCount) + + let mapping = rebasedTransform.mapping + let newUntil = rebasedTransform.steps.length + let eventCount = this.eventCount + this.items.forEach(item => { if (item.selection) eventCount-- }, start) + + let iRebased = rebasedCount + this.items.forEach(item => { + let pos = mapping.getMirror(--iRebased) + if (pos == null) return + newUntil = Math.min(newUntil, pos) + let map = mapping.maps[pos] + if (item.step) { + let step = rebasedTransform.steps[pos].invert(rebasedTransform.docs[pos]) + let selection = item.selection && item.selection.map(mapping.slice(iRebased + 1, pos)) + if (selection) eventCount++ + rebasedItems.push(new Item(map, step, selection)) + } else { + rebasedItems.push(new Item(map)) + } + }, start) + + let newMaps = [] + for (let i = rebasedCount; i < newUntil; i++) + newMaps.push(new Item(mapping.maps[i])) + let items = this.items.slice(0, start).append(newMaps).append(rebasedItems) + let branch = new Branch(items, eventCount) + + if (branch.emptyItemCount() > max_empty_items) + branch = branch.compress(this.items.length - rebasedItems.length) + return branch + } + + emptyItemCount() { + let count = 0 + this.items.forEach(item => { if (!item.step) count++ }) + return count + } + + // Compressing a branch means rewriting it to push the air (map-only + // items) out. During collaboration, these naturally accumulate + // because each remote change adds one. The `upto` argument is used + // to ensure that only the items below a given level are compressed, + // because `rebased` relies on a clean, untouched set of items in + // order to associate old items with rebased steps. + compress(upto = this.items.length) { + let remap = this.remapping(0, upto), mapFrom = remap.maps.length + let items: Item[] = [], events = 0 + this.items.forEach((item, i) => { + if (i >= upto) { + items.push(item) + if (item.selection) events++ + } else if (item.step) { + let step = item.step.map(remap.slice(mapFrom)), map = step && step.getMap() + mapFrom-- + if (map) remap.appendMap(map, mapFrom) + if (step) { + let selection = item.selection && item.selection.map(remap.slice(mapFrom)) + if (selection) events++ + let newItem = new Item(map!.invert(), step, selection), merged, last = items.length - 1 + if (merged = items.length && items[last].merge(newItem)) + items[last] = merged + else + items.push(newItem) + } + } else if (item.map) { + mapFrom-- + } + }, this.items.length, 0) + return new Branch(RopeSequence.from(items.reverse()), events) + } + + static empty = new Branch(RopeSequence.empty, 0) +} + +function cutOffEvents(items: RopeSequence<Item>, n: number) { + let cutPoint: number | undefined + items.forEach((item, i) => { + if (item.selection && (n-- == 0)) { + cutPoint = i + return false + } + }) + return items.slice(cutPoint!) +} + +class Item { + constructor( + // The (forward) step map for this item. + readonly map: StepMap, + // The inverted step + readonly step?: Step, + // If this is non-null, this item is the start of a group, and + // this selection is the starting selection for the group (the one + // that was active before the first step was applied) + readonly selection?: SelectionBookmark, + // If this item is the inverse of a previous mapping on the stack, + // this points at the inverse's offset + readonly mirrorOffset?: number + ) {} + + merge(other: Item) { + if (this.step && other.step && !other.selection) { + let step = other.step.merge(this.step) + if (step) return new Item(step.getMap().invert(), step, this.selection) + } + } +} + +// The value of the state field that tracks undo/redo history for that +// state. Will be stored in the plugin state when the history plugin +// is active. +class HistoryState { + constructor( + readonly done: Branch, + readonly undone: Branch, + readonly prevRanges: readonly number[] | null, + readonly prevTime: number, + readonly prevComposition: number + ) {} +} + +const DEPTH_OVERFLOW = 20 + +// Record a transformation in undo history. +function applyTransaction(history: HistoryState, state: EditorState, tr: Transaction, options: Required<HistoryOptions>) { + let historyTr = tr.getMeta(historyKey), rebased + if (historyTr) return historyTr.historyState + + if (tr.getMeta(closeHistoryKey)) history = new HistoryState(history.done, history.undone, null, 0, -1) + + let appended = tr.getMeta("appendedTransaction") + + if (tr.steps.length == 0) { + return history + } else if (appended && appended.getMeta(historyKey)) { + if (appended.getMeta(historyKey).redo) + return new HistoryState(history.done.addTransform(tr, undefined, options, mustPreserveItems(state)), + history.undone, rangesFor(tr.mapping.maps), + history.prevTime, history.prevComposition) + else + return new HistoryState(history.done, history.undone.addTransform(tr, undefined, options, mustPreserveItems(state)), + null, history.prevTime, history.prevComposition) + } else if (tr.getMeta("addToHistory") !== false && !(appended && appended.getMeta("addToHistory") === false)) { + // Group transforms that occur in quick succession into one event. + let composition = tr.getMeta("composition") + let newGroup = history.prevTime == 0 || + (!appended && history.prevComposition != composition && + (history.prevTime < (tr.time || 0) - options.newGroupDelay || !isAdjacentTo(tr, history.prevRanges!))) + let prevRanges = appended ? mapRanges(history.prevRanges!, tr.mapping) : rangesFor(tr.mapping.maps) + return new HistoryState(history.done.addTransform(tr, newGroup ? state.selection.getBookmark() : undefined, + options, mustPreserveItems(state)), + Branch.empty, prevRanges, tr.time, composition == null ? history.prevComposition : composition) + } else if (rebased = tr.getMeta("rebased")) { + // Used by the collab module to tell the history that some of its + // content has been rebased. + return new HistoryState(history.done.rebased(tr, rebased), + history.undone.rebased(tr, rebased), + mapRanges(history.prevRanges!, tr.mapping), history.prevTime, history.prevComposition) + } else { + return new HistoryState(history.done.addMaps(tr.mapping.maps), + history.undone.addMaps(tr.mapping.maps), + mapRanges(history.prevRanges!, tr.mapping), history.prevTime, history.prevComposition) + } +} + +function isAdjacentTo(transform: Transform, prevRanges: readonly number[]) { + if (!prevRanges) return false + if (!transform.docChanged) return true + let adjacent = false + transform.mapping.maps[0].forEach((start, end) => { + for (let i = 0; i < prevRanges.length; i += 2) + if (start <= prevRanges[i + 1] && end >= prevRanges[i]) + adjacent = true + }) + return adjacent +} + +function rangesFor(maps: readonly StepMap[]) { + let result: number[] = [] + for (let i = maps.length - 1; i >= 0 && result.length == 0; i--) + maps[i].forEach((_from, _to, from, to) => result.push(from, to)) + return result +} + +function mapRanges(ranges: readonly number[], mapping: Mapping) { + if (!ranges) return null + let result = [] + for (let i = 0; i < ranges.length; i += 2) { + let from = mapping.map(ranges[i], 1), to = mapping.map(ranges[i + 1], -1) + if (from <= to) result.push(from, to) + } + return result +} + +// Apply the latest event from one branch to the document and shift the event +// onto the other branch. +function histTransaction(history: HistoryState, state: EditorState, redo: boolean): Transaction | null { + let preserveItems = mustPreserveItems(state) + let histOptions = (historyKey.get(state)!.spec as any).config as Required<HistoryOptions> + let pop = (redo ? history.undone : history.done).popEvent(state, preserveItems) + if (!pop) return null + + let selection = pop.selection!.resolve(pop.transform.doc) + let added = (redo ? history.done : history.undone).addTransform(pop.transform, state.selection.getBookmark(), + histOptions, preserveItems) + + let newHist = new HistoryState(redo ? added : pop.remaining, redo ? pop.remaining : added, null, 0, -1) + return pop.transform.setSelection(selection).setMeta(historyKey, {redo, historyState: newHist}) +} + +let cachedPreserveItems = false, cachedPreserveItemsPlugins: readonly Plugin[] | null = null +// Check whether any plugin in the given state has a +// `historyPreserveItems` property in its spec, in which case we must +// preserve steps exactly as they came in, so that they can be +// rebased. +function mustPreserveItems(state: EditorState) { + let plugins = state.plugins + if (cachedPreserveItemsPlugins != plugins) { + cachedPreserveItems = false + cachedPreserveItemsPlugins = plugins + for (let i = 0; i < plugins.length; i++) if ((plugins[i].spec as any).historyPreserveItems) { + cachedPreserveItems = true + break + } + } + return cachedPreserveItems +} + +/// Set a flag on the given transaction that will prevent further steps +/// from being appended to an existing history event (so that they +/// require a separate undo command to undo). +export function closeHistory(tr: Transaction) { + return tr.setMeta(closeHistoryKey, true) +} + +const historyKey = new PluginKey("history") +const closeHistoryKey = new PluginKey("closeHistory") + +interface HistoryOptions { + /// The amount of history events that are collected before the + /// oldest events are discarded. Defaults to 100. + depth?: number + + /// The delay between changes after which a new group should be + /// started. Defaults to 500 (milliseconds). Note that when changes + /// aren't adjacent, a new group is always started. + newGroupDelay?: number +} + +/// Returns a plugin that enables the undo history for an editor. The +/// plugin will track undo and redo stacks, which can be used with the +/// [`undo`](#history.undo) and [`redo`](#history.redo) commands. +/// +/// You can set an `"addToHistory"` [metadata +/// property](#state.Transaction.setMeta) of `false` on a transaction +/// to prevent it from being rolled back by undo. +export function history(config: HistoryOptions = {}): Plugin { + config = {depth: config.depth || 100, + newGroupDelay: config.newGroupDelay || 500} + + return new Plugin({ + key: historyKey, + + state: { + init() { + return new HistoryState(Branch.empty, Branch.empty, null, 0, -1) + }, + apply(tr, hist, state) { + return applyTransaction(hist, state, tr, config as Required<HistoryOptions>) + } + }, + + config, + + props: { + handleDOMEvents: { + beforeinput(view, e: Event) { + let inputType = (e as InputEvent).inputType + let command = inputType == "historyUndo" ? undo : inputType == "historyRedo" ? redo : null + if (!command || !view.editable) return false + e.preventDefault() + return command(view.state, view.dispatch) + } + } + } + }) +} + +function buildCommand(redo: boolean, scroll: boolean): Command { + return (state, dispatch) => { + let hist = historyKey.getState(state) + if (!hist || (redo ? hist.undone : hist.done).eventCount == 0) return false + if (dispatch) { + let tr = histTransaction(hist, state, redo) + if (tr) dispatch(scroll ? tr.scrollIntoView() : tr) + } + return true + } +} + +/// A command function that undoes the last change, if any. +export const undo = buildCommand(false, true) + +/// A command function that redoes the last undone change, if any. +export const redo = buildCommand(true, true) + +/// A command function that undoes the last change. Don't scroll the +/// selection into view. +export const undoNoScroll = buildCommand(false, false) + +/// A command function that redoes the last undone change. Don't +/// scroll the selection into view. +export const redoNoScroll = buildCommand(true, false) + +/// The amount of undoable events available in a given state. +export function undoDepth(state: EditorState) { + let hist = historyKey.getState(state) + return hist ? hist.done.eventCount : 0 +} + +/// The amount of redoable events available in a given editor state. +export function redoDepth(state: EditorState) { + let hist = historyKey.getState(state) + return hist ? hist.undone.eventCount : 0 +} + +/// Returns true if the given transaction was generated by the history +/// plugin. +export function isHistoryTransaction(tr: Transaction) { + return tr.getMeta(historyKey) != null +} diff --git a/third_party/js/prosemirror/prosemirror-keymap/LICENSE b/third_party/js/prosemirror/prosemirror-keymap/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/js/prosemirror/prosemirror-keymap/moz.yaml b/third_party/js/prosemirror/prosemirror-keymap/moz.yaml @@ -0,0 +1,63 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror-keymap + + description: Keymap plugin for ProseMirror + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: 1.2.3 (2025-05-04T13:17:40+02:00). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: 1.2.3 + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MIT + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/ProseMirror/prosemirror-keymap + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + # Whether to track by commit or tag + tracking: tag + + exclude: + - "**" + + include: + - LICENSE + - src/ + - package.json diff --git a/third_party/js/prosemirror/prosemirror-keymap/package.json b/third_party/js/prosemirror/prosemirror-keymap/package.json @@ -0,0 +1,38 @@ +{ + "name": "prosemirror-keymap", + "version": "1.2.3", + "description": "Keymap plugin for ProseMirror", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "sideEffects": false, + "license": "MIT", + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijn@haverbeke.berlin", + "web": "http://marijnhaverbeke.nl" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/prosemirror/prosemirror-keymap.git" + }, + "dependencies": { + "w3c-keyname": "^2.2.0", + "prosemirror-state": "^1.0.0" + }, + "devDependencies": { + "@prosemirror/buildhelper": "^0.1.5", + "prosemirror-test-builder": "^1.0.0" + }, + "scripts": { + "test": "pm-runtests", + "prepare": "pm-buildhelper src/keymap.ts" + } +} diff --git a/third_party/js/prosemirror/prosemirror-keymap/src/README.md b/third_party/js/prosemirror/prosemirror-keymap/src/README.md @@ -0,0 +1,5 @@ +A plugin for conveniently defining key bindings. + +@keymap + +@keydownHandler diff --git a/third_party/js/prosemirror/prosemirror-keymap/src/keymap.ts b/third_party/js/prosemirror/prosemirror-keymap/src/keymap.ts @@ -0,0 +1,105 @@ +import {base, keyName} from "w3c-keyname" +import {Plugin, Command} from "prosemirror-state" +import {EditorView} from "prosemirror-view" + +const mac = typeof navigator != "undefined" && /Mac|iP(hone|[oa]d)/.test(navigator.platform) +const windows = typeof navigator != "undefined" && /Win/.test(navigator.platform) + +function normalizeKeyName(name: string) { + let parts = name.split(/-(?!$)/), result = parts[parts.length - 1] + if (result == "Space") result = " " + let alt, ctrl, shift, meta + for (let i = 0; i < parts.length - 1; i++) { + let mod = parts[i] + if (/^(cmd|meta|m)$/i.test(mod)) meta = true + else if (/^a(lt)?$/i.test(mod)) alt = true + else if (/^(c|ctrl|control)$/i.test(mod)) ctrl = true + else if (/^s(hift)?$/i.test(mod)) shift = true + else if (/^mod$/i.test(mod)) { if (mac) meta = true; else ctrl = true } + else throw new Error("Unrecognized modifier name: " + mod) + } + if (alt) result = "Alt-" + result + if (ctrl) result = "Ctrl-" + result + if (meta) result = "Meta-" + result + if (shift) result = "Shift-" + result + return result +} + +function normalize(map: {[key: string]: Command}) { + let copy: {[key: string]: Command} = Object.create(null) + for (let prop in map) copy[normalizeKeyName(prop)] = map[prop] + return copy +} + +function modifiers(name: string, event: KeyboardEvent, shift = true) { + if (event.altKey) name = "Alt-" + name + if (event.ctrlKey) name = "Ctrl-" + name + if (event.metaKey) name = "Meta-" + name + if (shift && event.shiftKey) name = "Shift-" + name + return name +} + +/// Create a keymap plugin for the given set of bindings. +/// +/// Bindings should map key names to [command](#commands)-style +/// functions, which will be called with `(EditorState, dispatch, +/// EditorView)` arguments, and should return true when they've handled +/// the key. Note that the view argument isn't part of the command +/// protocol, but can be used as an escape hatch if a binding needs to +/// directly interact with the UI. +/// +/// Key names may be strings like `"Shift-Ctrl-Enter"`—a key +/// identifier prefixed with zero or more modifiers. Key identifiers +/// are based on the strings that can appear in +/// [`KeyEvent.key`](https:///developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key). +/// Use lowercase letters to refer to letter keys (or uppercase letters +/// if you want shift to be held). You may use `"Space"` as an alias +/// for the `" "` name. +/// +/// Modifiers can be given in any order. `Shift-` (or `s-`), `Alt-` (or +/// `a-`), `Ctrl-` (or `c-` or `Control-`) and `Cmd-` (or `m-` or +/// `Meta-`) are recognized. For characters that are created by holding +/// shift, the `Shift-` prefix is implied, and should not be added +/// explicitly. +/// +/// You can use `Mod-` as a shorthand for `Cmd-` on Mac and `Ctrl-` on +/// other platforms. +/// +/// You can add multiple keymap plugins to an editor. The order in +/// which they appear determines their precedence (the ones early in +/// the array get to dispatch first). +export function keymap(bindings: {[key: string]: Command}): Plugin { + return new Plugin({props: {handleKeyDown: keydownHandler(bindings)}}) +} + +/// Given a set of bindings (using the same format as +/// [`keymap`](#keymap.keymap)), return a [keydown +/// handler](#view.EditorProps.handleKeyDown) that handles them. +export function keydownHandler(bindings: {[key: string]: Command}): (view: EditorView, event: KeyboardEvent) => boolean { + let map = normalize(bindings) + return function(view, event) { + let name = keyName(event), baseName, direct = map[modifiers(name, event)] + if (direct && direct(view.state, view.dispatch, view)) return true + // A character key + if (name.length == 1 && name != " ") { + if (event.shiftKey) { + // In case the name was already modified by shift, try looking + // it up without its shift modifier + let noShift = map[modifiers(name, event, false)] + if (noShift && noShift(view.state, view.dispatch, view)) return true + } + if ((event.altKey || event.metaKey || event.ctrlKey) && + // Ctrl-Alt may be used for AltGr on Windows + !(windows && event.ctrlKey && event.altKey) && + (baseName = base[event.keyCode]) && baseName != name) { + // Try falling back to the keyCode when there's a modifier + // active or the character produced isn't ASCII, and our table + // produces a different name from the the keyCode. See #668, + // #1060, #1529. + let fromCode = map[modifiers(baseName, event)] + if (fromCode && fromCode(view.state, view.dispatch, view)) return true + } + } + return false + } +} diff --git a/third_party/js/prosemirror/prosemirror-markdown/LICENSE b/third_party/js/prosemirror/prosemirror-markdown/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/js/prosemirror/prosemirror-markdown/moz.yaml b/third_party/js/prosemirror/prosemirror-markdown/moz.yaml @@ -0,0 +1,63 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror-transform + + description: ProseMirror Markdown integration + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: 1.13.2 (2025-03-18T09:15:25+01:00). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: 1.13.2 + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MIT + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/ProseMirror/prosemirror-markdown + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + # Whether to track by commit or tag + tracking: tag + + exclude: + - "**" + + include: + - LICENSE + - src/ + - package.json diff --git a/third_party/js/prosemirror/prosemirror-markdown/package.json b/third_party/js/prosemirror/prosemirror-markdown/package.json @@ -0,0 +1,40 @@ +{ + "name": "prosemirror-markdown", + "version": "1.13.2", + "description": "ProseMirror Markdown integration", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "sideEffects": false, + "license": "MIT", + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijn@haverbeke.berlin", + "web": "http://marijnhaverbeke.nl" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/prosemirror/prosemirror-markdown.git" + }, + "dependencies": { + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0", + "@types/markdown-it": "^14.0.0" + }, + "devDependencies": { + "@prosemirror/buildhelper": "^0.1.5", + "prosemirror-test-builder": "^1.0.0", + "punycode": "^1.4.0" + }, + "scripts": { + "test": "pm-runtests", + "prepare": "pm-buildhelper src/index.ts" + } +} diff --git a/third_party/js/prosemirror/prosemirror-markdown/src/README.md b/third_party/js/prosemirror/prosemirror-markdown/src/README.md @@ -0,0 +1,42 @@ +# prosemirror-markdown + +[ [**WEBSITE**](http://prosemirror.net) | [**ISSUES**](https://github.com/prosemirror/prosemirror-markdown/issues) | [**FORUM**](https://discuss.prosemirror.net) | [**GITTER**](https://gitter.im/ProseMirror/prosemirror) ] + +This is a (non-core) module for [ProseMirror](http://prosemirror.net). +ProseMirror is a well-behaved rich semantic content editor based on +contentEditable, with support for collaborative editing and custom +document schemas. + +This module implements a ProseMirror +[schema](https://prosemirror.net/docs/guide/#schema) that corresponds to +the document schema used by [CommonMark](http://commonmark.org/), and +a parser and serializer to convert between ProseMirror documents in +that schema and CommonMark/Markdown text. + +This code is released under an +[MIT license](https://github.com/prosemirror/prosemirror/tree/master/LICENSE). +There's a [forum](http://discuss.prosemirror.net) for general +discussion and support requests, and the +[Github bug tracker](https://github.com/prosemirror/prosemirror/issues) +is the place to report issues. + +We aim to be an inclusive, welcoming community. To make that explicit, +we have a [code of +conduct](http://contributor-covenant.org/version/1/1/0/) that applies +to communication around the project. + +## Documentation + +@schema + +@MarkdownParser + +@ParseSpec + +@defaultMarkdownParser + +@MarkdownSerializer + +@MarkdownSerializerState + +@defaultMarkdownSerializer diff --git a/third_party/js/prosemirror/prosemirror-markdown/src/from_markdown.ts b/third_party/js/prosemirror/prosemirror-markdown/src/from_markdown.ts @@ -0,0 +1,272 @@ +// @ts-ignore +import MarkdownIt from "markdown-it" +import Token from "markdown-it/lib/token.mjs" +import {schema} from "./schema" +import {Mark, MarkType, Node, Attrs, Schema, NodeType} from "prosemirror-model" + +function maybeMerge(a: Node, b: Node): Node | undefined { + if (a.isText && b.isText && Mark.sameSet(a.marks, b.marks)) + return (a as any).withText(a.text! + b.text!) +} + +// Object used to track the context of a running parse. +class MarkdownParseState { + stack: {type: NodeType, attrs: Attrs | null, content: Node[], marks: readonly Mark[]}[] + + constructor( + readonly schema: Schema, + readonly tokenHandlers: {[token: string]: (stat: MarkdownParseState, token: Token, tokens: Token[], i: number) => void} + ) { + this.stack = [{type: schema.topNodeType, attrs: null, content: [], marks: Mark.none}] + } + + top() { + return this.stack[this.stack.length - 1] + } + + push(elt: Node) { + if (this.stack.length) this.top().content.push(elt) + } + + // Adds the given text to the current position in the document, + // using the current marks as styling. + addText(text: string) { + if (!text) return + let top = this.top(), nodes = top.content, last = nodes[nodes.length - 1] + let node = this.schema.text(text, top.marks), merged + if (last && (merged = maybeMerge(last, node))) nodes[nodes.length - 1] = merged + else nodes.push(node) + } + + // Adds the given mark to the set of active marks. + openMark(mark: Mark) { + let top = this.top() + top.marks = mark.addToSet(top.marks) + } + + // Removes the given mark from the set of active marks. + closeMark(mark: MarkType) { + let top = this.top() + top.marks = mark.removeFromSet(top.marks) + } + + parseTokens(toks: Token[]) { + for (let i = 0; i < toks.length; i++) { + let tok = toks[i] + let handler = this.tokenHandlers[tok.type] + if (!handler) + throw new Error("Token type `" + tok.type + "` not supported by Markdown parser") + handler(this, tok, toks, i) + } + } + + // Add a node at the current position. + addNode(type: NodeType, attrs: Attrs | null, content?: readonly Node[]) { + let top = this.top() + let node = type.createAndFill(attrs, content, top ? top.marks : []) + if (!node) return null + this.push(node) + return node + } + + // Wrap subsequent content in a node of the given type. + openNode(type: NodeType, attrs: Attrs | null) { + this.stack.push({type: type, attrs: attrs, content: [], marks: Mark.none}) + } + + // Close and return the node that is currently on top of the stack. + closeNode() { + let info = this.stack.pop()! + return this.addNode(info.type, info.attrs, info.content) + } +} + +function attrs(spec: ParseSpec, token: Token, tokens: Token[], i: number) { + if (spec.getAttrs) return spec.getAttrs(token, tokens, i) + // For backwards compatibility when `attrs` is a Function + else if (spec.attrs instanceof Function) return spec.attrs(token) + else return spec.attrs +} + +// Code content is represented as a single token with a `content` +// property in Markdown-it. +function noCloseToken(spec: ParseSpec, type: string) { + return spec.noCloseToken || type == "code_inline" || type == "code_block" || type == "fence" +} + +function withoutTrailingNewline(str: string) { + return str[str.length - 1] == "\n" ? str.slice(0, str.length - 1) : str +} + +function noOp() {} + +function tokenHandlers(schema: Schema, tokens: {[token: string]: ParseSpec}) { + let handlers: {[token: string]: (stat: MarkdownParseState, token: Token, tokens: Token[], i: number) => void} = + Object.create(null) + for (let type in tokens) { + let spec = tokens[type] + if (spec.block) { + let nodeType = schema.nodeType(spec.block) + if (noCloseToken(spec, type)) { + handlers[type] = (state, tok, tokens, i) => { + state.openNode(nodeType, attrs(spec, tok, tokens, i)) + state.addText(withoutTrailingNewline(tok.content)) + state.closeNode() + } + } else { + handlers[type + "_open"] = (state, tok, tokens, i) => state.openNode(nodeType, attrs(spec, tok, tokens, i)) + handlers[type + "_close"] = state => state.closeNode() + } + } else if (spec.node) { + let nodeType = schema.nodeType(spec.node) + handlers[type] = (state, tok, tokens, i) => state.addNode(nodeType, attrs(spec, tok, tokens, i)) + } else if (spec.mark) { + let markType = schema.marks[spec.mark] + if (noCloseToken(spec, type)) { + handlers[type] = (state, tok, tokens, i) => { + state.openMark(markType.create(attrs(spec, tok, tokens, i))) + state.addText(withoutTrailingNewline(tok.content)) + state.closeMark(markType) + } + } else { + handlers[type + "_open"] = (state, tok, tokens, i) => state.openMark(markType.create(attrs(spec, tok, tokens, i))) + handlers[type + "_close"] = state => state.closeMark(markType) + } + } else if (spec.ignore) { + if (noCloseToken(spec, type)) { + handlers[type] = noOp + } else { + handlers[type + "_open"] = noOp + handlers[type + "_close"] = noOp + } + } else { + throw new RangeError("Unrecognized parsing spec " + JSON.stringify(spec)) + } + } + + handlers.text = (state, tok) => state.addText(tok.content) + handlers.inline = (state, tok) => state.parseTokens(tok.children!) + handlers.softbreak = handlers.softbreak || (state => state.addText(" ")) + + return handlers +} + +/// Object type used to specify how Markdown tokens should be parsed. +export interface ParseSpec { + /// This token maps to a single node, whose type can be looked up + /// in the schema under the given name. Exactly one of `node`, + /// `block`, or `mark` must be set. + node?: string + + /// This token (unless `noCloseToken` is true) comes in `_open` + /// and `_close` variants (which are appended to the base token + /// name provides a the object property), and wraps a block of + /// content. The block should be wrapped in a node of the type + /// named to by the property's value. If the token does not have + /// `_open` or `_close`, use the `noCloseToken` option. + block?: string + + /// This token (again, unless `noCloseToken` is true) also comes + /// in `_open` and `_close` variants, but should add a mark + /// (named by the value) to its content, rather than wrapping it + /// in a node. + mark?: string + + /// Attributes for the node or mark. When `getAttrs` is provided, + /// it takes precedence. + attrs?: Attrs | null + + /// A function used to compute the attributes for the node or mark + /// that takes a [markdown-it + /// token](https://markdown-it.github.io/markdown-it/#Token) and + /// returns an attribute object. + getAttrs?: (token: Token, tokenStream: Token[], index: number) => Attrs | null + + /// Indicates that the [markdown-it + /// token](https://markdown-it.github.io/markdown-it/#Token) has + /// no `_open` or `_close` for the nodes. This defaults to `true` + /// for `code_inline`, `code_block` and `fence`. + noCloseToken?: boolean + + /// When true, ignore content for the matched token. + ignore?: boolean +} + +/// A configuration of a Markdown parser. Such a parser uses +/// [markdown-it](https://github.com/markdown-it/markdown-it) to +/// tokenize a file, and then runs the custom rules it is given over +/// the tokens to create a ProseMirror document tree. +export class MarkdownParser { + /// @internal + tokenHandlers: {[token: string]: (stat: MarkdownParseState, token: Token, tokens: Token[], i: number) => void} + + /// Create a parser with the given configuration. You can configure + /// the markdown-it parser to parse the dialect you want, and provide + /// a description of the ProseMirror entities those tokens map to in + /// the `tokens` object, which maps token names to descriptions of + /// what to do with them. Such a description is an object, and may + /// have the following properties: + constructor( + /// The parser's document schema. + readonly schema: Schema, + /// This parser's markdown-it tokenizer. + readonly tokenizer: MarkdownIt, + /// The value of the `tokens` object used to construct this + /// parser. Can be useful to copy and modify to base other parsers + /// on. + readonly tokens: {[name: string]: ParseSpec} + ) { + this.tokenHandlers = tokenHandlers(schema, tokens) + } + + /// Parse a string as [CommonMark](http://commonmark.org/) markup, + /// and create a ProseMirror document as prescribed by this parser's + /// rules. + /// + /// The second argument, when given, is passed through to the + /// [Markdown + /// parser](https://markdown-it.github.io/markdown-it/#MarkdownIt.parse). + parse(text: string, markdownEnv: Object = {}) { + let state = new MarkdownParseState(this.schema, this.tokenHandlers), doc + state.parseTokens(this.tokenizer.parse(text, markdownEnv)) + do { doc = state.closeNode() } while (state.stack.length) + return doc || this.schema.topNodeType.createAndFill()! + } +} + +function listIsTight(tokens: readonly Token[], i: number) { + while (++i < tokens.length) + if (tokens[i].type != "list_item_open") return tokens[i].hidden + return false +} + +/// A parser parsing unextended [CommonMark](http://commonmark.org/), +/// without inline HTML, and producing a document in the basic schema. +export const defaultMarkdownParser = new MarkdownParser(schema, MarkdownIt("commonmark", {html: false}), { + blockquote: {block: "blockquote"}, + paragraph: {block: "paragraph"}, + list_item: {block: "list_item"}, + bullet_list: {block: "bullet_list", getAttrs: (_, tokens, i) => ({tight: listIsTight(tokens, i)})}, + ordered_list: {block: "ordered_list", getAttrs: (tok, tokens, i) => ({ + order: +tok.attrGet("start")! || 1, + tight: listIsTight(tokens, i) + })}, + heading: {block: "heading", getAttrs: tok => ({level: +tok.tag.slice(1)})}, + code_block: {block: "code_block", noCloseToken: true}, + fence: {block: "code_block", getAttrs: tok => ({params: tok.info || ""}), noCloseToken: true}, + hr: {node: "horizontal_rule"}, + image: {node: "image", getAttrs: tok => ({ + src: tok.attrGet("src"), + title: tok.attrGet("title") || null, + alt: tok.children![0] && tok.children![0].content || null + })}, + hardbreak: {node: "hard_break"}, + + em: {mark: "em"}, + strong: {mark: "strong"}, + link: {mark: "link", getAttrs: tok => ({ + href: tok.attrGet("href"), + title: tok.attrGet("title") || null + })}, + code_inline: {mark: "code", noCloseToken: true} +}) diff --git a/third_party/js/prosemirror/prosemirror-markdown/src/index.ts b/third_party/js/prosemirror/prosemirror-markdown/src/index.ts @@ -0,0 +1,5 @@ +// Defines a parser and serializer for [CommonMark](http://commonmark.org/) text. + +export {schema} from "./schema" +export {defaultMarkdownParser, MarkdownParser, ParseSpec} from "./from_markdown" +export {MarkdownSerializer, defaultMarkdownSerializer, MarkdownSerializerState} from "./to_markdown" diff --git a/third_party/js/prosemirror/prosemirror-markdown/src/schema.ts b/third_party/js/prosemirror/prosemirror-markdown/src/schema.ts @@ -0,0 +1,156 @@ +import {Schema, MarkSpec} from "prosemirror-model" + +/// Document schema for the data model used by CommonMark. +export const schema = new Schema({ + nodes: { + doc: { + content: "block+" + }, + + paragraph: { + content: "inline*", + group: "block", + parseDOM: [{tag: "p"}], + toDOM() { return ["p", 0] } + }, + + blockquote: { + content: "block+", + group: "block", + parseDOM: [{tag: "blockquote"}], + toDOM() { return ["blockquote", 0] } + }, + + horizontal_rule: { + group: "block", + parseDOM: [{tag: "hr"}], + toDOM() { return ["div", ["hr"]] } + }, + + heading: { + attrs: {level: {default: 1}}, + content: "(text | image)*", + group: "block", + defining: true, + parseDOM: [{tag: "h1", attrs: {level: 1}}, + {tag: "h2", attrs: {level: 2}}, + {tag: "h3", attrs: {level: 3}}, + {tag: "h4", attrs: {level: 4}}, + {tag: "h5", attrs: {level: 5}}, + {tag: "h6", attrs: {level: 6}}], + toDOM(node) { return ["h" + node.attrs.level, 0] } + }, + + code_block: { + content: "text*", + group: "block", + code: true, + defining: true, + marks: "", + attrs: {params: {default: ""}}, + parseDOM: [{tag: "pre", preserveWhitespace: "full", getAttrs: node => ( + {params: (node as HTMLElement).getAttribute("data-params") || ""} + )}], + toDOM(node) { return ["pre", node.attrs.params ? {"data-params": node.attrs.params} : {}, ["code", 0]] } + }, + + ordered_list: { + content: "list_item+", + group: "block", + attrs: {order: {default: 1}, tight: {default: false}}, + parseDOM: [{tag: "ol", getAttrs(dom) { + return {order: (dom as HTMLElement).hasAttribute("start") ? +(dom as HTMLElement).getAttribute("start")! : 1, + tight: (dom as HTMLElement).hasAttribute("data-tight")} + }}], + toDOM(node) { + return ["ol", {start: node.attrs.order == 1 ? null : node.attrs.order, + "data-tight": node.attrs.tight ? "true" : null}, 0] + } + }, + + bullet_list: { + content: "list_item+", + group: "block", + attrs: {tight: {default: false}}, + parseDOM: [{tag: "ul", getAttrs: dom => ({tight: (dom as HTMLElement).hasAttribute("data-tight")})}], + toDOM(node) { return ["ul", {"data-tight": node.attrs.tight ? "true" : null}, 0] } + }, + + list_item: { + content: "block+", + defining: true, + parseDOM: [{tag: "li"}], + toDOM() { return ["li", 0] } + }, + + text: { + group: "inline" + }, + + image: { + inline: true, + attrs: { + src: {}, + alt: {default: null}, + title: {default: null} + }, + group: "inline", + draggable: true, + parseDOM: [{tag: "img[src]", getAttrs(dom) { + return { + src: (dom as HTMLElement).getAttribute("src"), + title: (dom as HTMLElement).getAttribute("title"), + alt: (dom as HTMLElement).getAttribute("alt") + } + }}], + toDOM(node) { return ["img", node.attrs] } + }, + + hard_break: { + inline: true, + group: "inline", + selectable: false, + parseDOM: [{tag: "br"}], + toDOM() { return ["br"] } + } + }, + + marks: { + em: { + parseDOM: [ + {tag: "i"}, {tag: "em"}, + {style: "font-style=italic"}, + {style: "font-style=normal", clearMark: m => m.type.name == "em"} + ], + toDOM() { return ["em"] } + }, + + strong: { + parseDOM: [ + {tag: "strong"}, + {tag: "b", getAttrs: node => node.style.fontWeight != "normal" && null}, + {style: "font-weight=400", clearMark: m => m.type.name == "strong"}, + {style: "font-weight", getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null} + ], + toDOM() { return ["strong"] } + } as MarkSpec, + + link: { + attrs: { + href: {}, + title: {default: null} + }, + inclusive: false, + parseDOM: [{tag: "a[href]", getAttrs(dom) { + return {href: (dom as HTMLElement).getAttribute("href"), title: dom.getAttribute("title")} + }}], + toDOM(node) { return ["a", node.attrs] } + }, + + code: { + code: true, + parseDOM: [{tag: "code"}], + toDOM() { return ["code"] } + } + } +}) diff --git a/third_party/js/prosemirror/prosemirror-markdown/src/to_markdown.ts b/third_party/js/prosemirror/prosemirror-markdown/src/to_markdown.ts @@ -0,0 +1,474 @@ +import {Node, Mark} from "prosemirror-model" + +type MarkSerializerSpec = { + /// The string that should appear before a piece of content marked + /// by this mark, either directly or as a function that returns an + /// appropriate string. + open: string | ((state: MarkdownSerializerState, mark: Mark, parent: Node, index: number) => string), + /// The string that should appear after a piece of content marked by + /// this mark. + close: string | ((state: MarkdownSerializerState, mark: Mark, parent: Node, index: number) => string), + /// When `true`, this indicates that the order in which the mark's + /// opening and closing syntax appears relative to other mixable + /// marks can be varied. (For example, you can say `**a *b***` and + /// `*a **b***`, but not `` `a *b*` ``.) + mixable?: boolean, + /// When enabled, causes the serializer to move enclosing whitespace + /// from inside the marks to outside the marks. This is necessary + /// for emphasis marks as CommonMark does not permit enclosing + /// whitespace inside emphasis marks, see: + /// http:///spec.commonmark.org/0.26/#example-330 + expelEnclosingWhitespace?: boolean, + /// Can be set to `false` to disable character escaping in a mark. A + /// non-escaping mark has to have the highest precedence (must + /// always be the innermost mark). + escape?: boolean +} + +const blankMark: MarkSerializerSpec = {open: "", close: "", mixable: true} + +/// A specification for serializing a ProseMirror document as +/// Markdown/CommonMark text. +export class MarkdownSerializer { + /// Construct a serializer with the given configuration. The `nodes` + /// object should map node names in a given schema to function that + /// take a serializer state and such a node, and serialize the node. + constructor( + /// The node serializer functions for this serializer. + readonly nodes: {[node: string]: (state: MarkdownSerializerState, node: Node, parent: Node, index: number) => void}, + /// The mark serializer info. + readonly marks: {[mark: string]: MarkSerializerSpec}, + readonly options: { + /// Extra characters can be added for escaping. This is passed + /// directly to String.replace(), and the matching characters are + /// preceded by a backslash. + escapeExtraCharacters?: RegExp, + /// Specify the node name of hard breaks. + /// Defaults to "hard_break" + hardBreakNodeName?: string, + /// By default, the serializer raises an error when it finds a + /// node or mark type for which no serializer is defined. Set + /// this to `false` to make it just ignore such elements, + /// rendering only their content. + strict?: boolean + } = {} + ) {} + + /// Serialize the content of the given node to + /// [CommonMark](http://commonmark.org/). + serialize(content: Node, options: { + /// Whether to render lists in a tight style. This can be overridden + /// on a node level by specifying a tight attribute on the node. + /// Defaults to false. + tightLists?: boolean + } = {}) { + options = Object.assign({}, this.options, options) + let state = new MarkdownSerializerState(this.nodes, this.marks, options) + state.renderContent(content) + return state.out + } +} + +/// A serializer for the [basic schema](#schema). +export const defaultMarkdownSerializer = new MarkdownSerializer({ + blockquote(state, node) { + state.wrapBlock("> ", null, node, () => state.renderContent(node)) + }, + code_block(state, node) { + // Make sure the front matter fences are longer than any dash sequence within it + const backticks = node.textContent.match(/`{3,}/gm) + const fence = backticks ? (backticks.sort().slice(-1)[0] + "`") : "```" + + state.write(fence + (node.attrs.params || "") + "\n") + state.text(node.textContent, false) + // Add a newline to the current content before adding closing marker + state.write("\n") + state.write(fence) + state.closeBlock(node) + }, + heading(state, node) { + state.write(state.repeat("#", node.attrs.level) + " ") + state.renderInline(node, false) + state.closeBlock(node) + }, + horizontal_rule(state, node) { + state.write(node.attrs.markup || "---") + state.closeBlock(node) + }, + bullet_list(state, node) { + state.renderList(node, " ", () => (node.attrs.bullet || "*") + " ") + }, + ordered_list(state, node) { + let start = node.attrs.order || 1 + let maxW = String(start + node.childCount - 1).length + let space = state.repeat(" ", maxW + 2) + state.renderList(node, space, i => { + let nStr = String(start + i) + return state.repeat(" ", maxW - nStr.length) + nStr + ". " + }) + }, + list_item(state, node) { + state.renderContent(node) + }, + paragraph(state, node) { + state.renderInline(node) + state.closeBlock(node) + }, + + image(state, node) { + state.write("![" + state.esc(node.attrs.alt || "") + "](" + node.attrs.src.replace(/[\(\)]/g, "\\$&") + + (node.attrs.title ? ' "' + node.attrs.title.replace(/"/g, '\\"') + '"' : "") + ")") + }, + hard_break(state, node, parent, index) { + for (let i = index + 1; i < parent.childCount; i++) + if (parent.child(i).type != node.type) { + state.write("\\\n") + return + } + }, + text(state, node) { + state.text(node.text!, !state.inAutolink) + } +}, { + em: {open: "*", close: "*", mixable: true, expelEnclosingWhitespace: true}, + strong: {open: "**", close: "**", mixable: true, expelEnclosingWhitespace: true}, + link: { + open(state, mark, parent, index) { + state.inAutolink = isPlainURL(mark, parent, index) + return state.inAutolink ? "<" : "[" + }, + close(state, mark, parent, index) { + let {inAutolink} = state + state.inAutolink = undefined + return inAutolink ? ">" + : "](" + mark.attrs.href.replace(/[\(\)"]/g, "\\$&") + (mark.attrs.title ? ` "${mark.attrs.title.replace(/"/g, '\\"')}"` : "") + ")" + }, + mixable: true + }, + code: {open(_state, _mark, parent, index) { return backticksFor(parent.child(index), -1) }, + close(_state, _mark, parent, index) { return backticksFor(parent.child(index - 1), 1) }, + escape: false} +}) + +function backticksFor(node: Node, side: number) { + let ticks = /`+/g, m, len = 0 + if (node.isText) while (m = ticks.exec(node.text!)) len = Math.max(len, m[0].length) + let result = len > 0 && side > 0 ? " `" : "`" + for (let i = 0; i < len; i++) result += "`" + if (len > 0 && side < 0) result += " " + return result +} + +function isPlainURL(link: Mark, parent: Node, index: number) { + if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false + let content = parent.child(index) + if (!content.isText || content.text != link.attrs.href || content.marks[content.marks.length - 1] != link) return false + return index == parent.childCount - 1 || !link.isInSet(parent.child(index + 1).marks) +} + +/// This is an object used to track state and expose +/// methods related to markdown serialization. Instances are passed to +/// node and mark serialization methods (see `toMarkdown`). +export class MarkdownSerializerState { + /// @internal + delim: string = "" + /// @internal + out: string = "" + /// @internal + closed: Node | null = null + /// @internal + inAutolink: boolean | undefined = undefined + /// @internal + atBlockStart: boolean = false + /// @internal + inTightList: boolean = false + + /// @internal + constructor( + /// @internal + readonly nodes: {[node: string]: (state: MarkdownSerializerState, node: Node, parent: Node, index: number) => void}, + /// @internal + readonly marks: {[mark: string]: MarkSerializerSpec}, + /// The options passed to the serializer. + readonly options: {tightLists?: boolean, escapeExtraCharacters?: RegExp, hardBreakNodeName?: string, strict?: boolean} + ) { + if (typeof this.options.tightLists == "undefined") + this.options.tightLists = false + if (typeof this.options.hardBreakNodeName == "undefined") + this.options.hardBreakNodeName = "hard_break" + } + + /// @internal + flushClose(size: number = 2) { + if (this.closed) { + if (!this.atBlank()) this.out += "\n" + if (size > 1) { + let delimMin = this.delim + let trim = /\s+$/.exec(delimMin) + if (trim) delimMin = delimMin.slice(0, delimMin.length - trim[0].length) + for (let i = 1; i < size; i++) + this.out += delimMin + "\n" + } + this.closed = null + } + } + + /// @internal + getMark(name: string) { + let info = this.marks[name] + if (!info) { + if (this.options.strict !== false) + throw new Error(`Mark type \`${name}\` not supported by Markdown renderer`) + info = blankMark + } + return info + } + + /// Render a block, prefixing each line with `delim`, and the first + /// line in `firstDelim`. `node` should be the node that is closed at + /// the end of the block, and `f` is a function that renders the + /// content of the block. + wrapBlock(delim: string, firstDelim: string | null, node: Node, f: () => void) { + let old = this.delim + this.write(firstDelim != null ? firstDelim : delim) + this.delim += delim + f() + this.delim = old + this.closeBlock(node) + } + + /// @internal + atBlank() { + return /(^|\n)$/.test(this.out) + } + + /// Ensure the current content ends with a newline. + ensureNewLine() { + if (!this.atBlank()) this.out += "\n" + } + + /// Prepare the state for writing output (closing closed paragraphs, + /// adding delimiters, and so on), and then optionally add content + /// (unescaped) to the output. + write(content?: string) { + this.flushClose() + if (this.delim && this.atBlank()) + this.out += this.delim + if (content) this.out += content + } + + /// Close the block for the given node. + closeBlock(node: Node) { + this.closed = node + } + + /// Add the given text to the document. When escape is not `false`, + /// it will be escaped. + text(text: string, escape = true) { + let lines = text.split("\n") + for (let i = 0; i < lines.length; i++) { + this.write() + // Escape exclamation marks in front of links + if (!escape && lines[i][0] == "[" && /(^|[^\\])\!$/.test(this.out)) + this.out = this.out.slice(0, this.out.length - 1) + "\\!" + this.out += escape ? this.esc(lines[i], this.atBlockStart) : lines[i] + if (i != lines.length - 1) this.out += "\n" + } + } + + /// Render the given node as a block. + render(node: Node, parent: Node, index: number) { + if (this.nodes[node.type.name]) { + this.nodes[node.type.name](this, node, parent, index) + } else { + if (this.options.strict !== false) { + throw new Error("Token type `" + node.type.name + "` not supported by Markdown renderer") + } else if (!node.type.isLeaf) { + if (node.type.inlineContent) this.renderInline(node) + else this.renderContent(node) + if (node.isBlock) this.closeBlock(node) + } + } + } + + /// Render the contents of `parent` as block nodes. + renderContent(parent: Node) { + parent.forEach((node, _, i) => this.render(node, parent, i)) + } + + /// Render the contents of `parent` as inline content. + renderInline(parent: Node, fromBlockStart = true) { + this.atBlockStart = fromBlockStart + let active: Mark[] = [], trailing = "" + let progress = (node: Node | null, offset: number, index: number) => { + let marks = node ? node.marks : [] + + // Remove marks from `hard_break` that are the last node inside + // that mark to prevent parser edge cases with new lines just + // before closing marks. + if (node && node.type.name === this.options.hardBreakNodeName) + marks = marks.filter(m => { + if (index + 1 == parent.childCount) return false + let next = parent.child(index + 1) + return m.isInSet(next.marks) && (!next.isText || /\S/.test(next.text!)) + }) + + let leading = trailing + trailing = "" + // If whitespace has to be expelled from the node, adjust + // leading and trailing accordingly. + if (node && node.isText && marks.some(mark => { + let info = this.getMark(mark.type.name) + return info && info.expelEnclosingWhitespace && !mark.isInSet(active) + })) { + let [_, lead, rest] = /^(\s*)(.*)$/m.exec(node.text!)! + if (lead) { + leading += lead + node = rest ? (node as any).withText(rest) : null + if (!node) marks = active + } + } + if (node && node.isText && marks.some(mark => { + let info = this.getMark(mark.type.name) + return info && info.expelEnclosingWhitespace && + (index == parent.childCount - 1 || !mark.isInSet(parent.child(index + 1).marks)) + })) { + let [_, rest, trail] = /^(.*?)(\s*)$/m.exec(node.text!)! + if (trail) { + trailing = trail + node = rest ? (node as any).withText(rest) : null + if (!node) marks = active + } + } + let inner = marks.length ? marks[marks.length - 1] : null + let noEsc = inner && this.getMark(inner.type.name).escape === false + let len = marks.length - (noEsc ? 1 : 0) + + // Try to reorder 'mixable' marks, such as em and strong, which + // in Markdown may be opened and closed in different order, so + // that order of the marks for the token matches the order in + // active. + outer: for (let i = 0; i < len; i++) { + let mark = marks[i] + if (!this.getMark(mark.type.name).mixable) break + for (let j = 0; j < active.length; j++) { + let other = active[j] + if (!this.getMark(other.type.name).mixable) break + if (mark.eq(other)) { + if (i > j) + marks = marks.slice(0, j).concat(mark).concat(marks.slice(j, i)).concat(marks.slice(i + 1, len)) + else if (j > i) + marks = marks.slice(0, i).concat(marks.slice(i + 1, j)).concat(mark).concat(marks.slice(j, len)) + continue outer + } + } + } + + // Find the prefix of the mark set that didn't change + let keep = 0 + while (keep < Math.min(active.length, len) && marks[keep].eq(active[keep])) ++keep + + // Close the marks that need to be closed + while (keep < active.length) + this.text(this.markString(active.pop()!, false, parent, index), false) + + // Output any previously expelled trailing whitespace outside the marks + if (leading) this.text(leading) + + // Open the marks that need to be opened + if (node) { + while (active.length < len) { + let add = marks[active.length] + active.push(add) + this.text(this.markString(add, true, parent, index), false) + this.atBlockStart = false + } + + // Render the node. Special case code marks, since their content + // may not be escaped. + if (noEsc && node.isText) + this.text(this.markString(inner!, true, parent, index) + node.text + + this.markString(inner!, false, parent, index + 1), false) + else + this.render(node, parent, index) + this.atBlockStart = false + } + + // After the first non-empty text node is rendered, the end of output + // is no longer at block start. + // + // FIXME: If a non-text node writes something to the output for this + // block, the end of output is also no longer at block start. But how + // can we detect that? + if (node?.isText && node.nodeSize > 0) { + this.atBlockStart = false + } + } + parent.forEach(progress) + progress(null, 0, parent.childCount) + this.atBlockStart = false + } + + /// Render a node's content as a list. `delim` should be the extra + /// indentation added to all lines except the first in an item, + /// `firstDelim` is a function going from an item index to a + /// delimiter for the first line of the item. + renderList(node: Node, delim: string, firstDelim: (index: number) => string) { + if (this.closed && this.closed.type == node.type) + this.flushClose(3) + else if (this.inTightList) + this.flushClose(1) + + let isTight = typeof node.attrs.tight != "undefined" ? node.attrs.tight : this.options.tightLists + let prevTight = this.inTightList + this.inTightList = isTight + node.forEach((child, _, i) => { + if (i && isTight) this.flushClose(1) + this.wrapBlock(delim, firstDelim(i), node, () => this.render(child, node, i)) + }) + this.inTightList = prevTight + } + + /// Escape the given string so that it can safely appear in Markdown + /// content. If `startOfLine` is true, also escape characters that + /// have special meaning only at the start of the line. + esc(str: string, startOfLine = false) { + str = str.replace( + /[`*\\~\[\]_]/g, + (m, i) => m == "_" && i > 0 && i + 1 < str.length && str[i-1].match(/\w/) && str[i+1].match(/\w/) ? m : "\\" + m + ) + if (startOfLine) str = str.replace(/^(\+[ ]|[\-*>])/, "\\$&").replace(/^(\s*)(#{1,6})(\s|$)/, '$1\\$2$3').replace(/^(\s*\d+)\.\s/, "$1\\. ") + if (this.options.escapeExtraCharacters) str = str.replace(this.options.escapeExtraCharacters, "\\$&") + return str + } + + /// @internal + quote(str: string) { + let wrap = str.indexOf('"') == -1 ? '""' : str.indexOf("'") == -1 ? "''" : "()" + return wrap[0] + str + wrap[1] + } + + /// Repeat the given string `n` times. + repeat(str: string, n: number) { + let out = "" + for (let i = 0; i < n; i++) out += str + return out + } + + /// Get the markdown string for a given opening or closing mark. + markString(mark: Mark, open: boolean, parent: Node, index: number) { + let info = this.getMark(mark.type.name) + let value = open ? info.open : info.close + return typeof value == "string" ? value : value(this, mark, parent, index) + } + + /// Get leading and trailing whitespace from a string. Values of + /// leading or trailing property of the return object will be undefined + /// if there is no match. + getEnclosingWhitespace(text: string): {leading?: string, trailing?: string} { + return { + leading: (text.match(/^(\s+)/) || [undefined])[0], + trailing: (text.match(/(\s+)$/) || [undefined])[0] + } + } +} diff --git a/third_party/js/prosemirror/prosemirror-model/LICENSE b/third_party/js/prosemirror/prosemirror-model/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/js/prosemirror/prosemirror-model/moz.yaml b/third_party/js/prosemirror/prosemirror-model/moz.yaml @@ -0,0 +1,63 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror-model + + description: ProseMirror's document model + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: 1.25.4 (2025-10-21T10:30:22+02:00). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: 1.25.4 + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MIT + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/ProseMirror/prosemirror-model + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + # Whether to track by commit or tag + tracking: tag + + exclude: + - "**" + + include: + - LICENSE + - src/ + - package.json diff --git a/third_party/js/prosemirror/prosemirror-model/package.json b/third_party/js/prosemirror/prosemirror-model/package.json @@ -0,0 +1,38 @@ +{ + "name": "prosemirror-model", + "version": "1.25.4", + "description": "ProseMirror's document model", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "sideEffects": false, + "license": "MIT", + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijn@haverbeke.berlin", + "web": "http://marijnhaverbeke.nl" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/prosemirror/prosemirror-model.git" + }, + "dependencies": { + "orderedmap": "^2.0.0" + }, + "devDependencies": { + "@prosemirror/buildhelper": "^0.1.5", + "jsdom": "^20.0.0", + "prosemirror-test-builder": "^1.0.0" + }, + "scripts": { + "test": "pm-runtests", + "prepare": "pm-buildhelper src/index.ts" + } +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/README.md b/third_party/js/prosemirror/prosemirror-model/src/README.md @@ -0,0 +1,62 @@ +This module defines ProseMirror's content model, the data structures +used to represent and work with documents. + +### Document Structure + +A ProseMirror document is a tree. At each level, a [node](#model.Node) +describes the type of the content, and holds a +[fragment](#model.Fragment) containing its children. + +@Node +@Fragment +@Mark +@Slice +@Attrs +@ReplaceError + +### Resolved Positions + +Positions in a document can be represented as integer +[offsets](/docs/guide/#doc.indexing). But you'll often want to use a +more convenient representation. + +@ResolvedPos +@NodeRange + +### Document Schema + +Every ProseMirror document conforms to a +[schema](/docs/guide/#schema), which describes the set of nodes and +marks that it is made out of, along with the relations between those, +such as which node may occur as a child node of which other nodes. + +@Schema + +@SchemaSpec +@NodeSpec +@MarkSpec +@AttributeSpec + +@NodeType +@MarkType + +@ContentMatch + +### DOM Representation + +Because representing a document as a tree of DOM nodes is central to +the way ProseMirror operates, DOM [parsing](#model.DOMParser) and +[serializing](#model.DOMSerializer) is integrated with the model. + +(But note that you do _not_ need to have a DOM implementation loaded +to use this module.) + +@DOMParser +@ParseOptions +@GenericParseRule +@TagParseRule +@StyleParseRule +@ParseRule + +@DOMSerializer +@DOMOutputSpec diff --git a/third_party/js/prosemirror/prosemirror-model/src/comparedeep.ts b/third_party/js/prosemirror/prosemirror-model/src/comparedeep.ts @@ -0,0 +1,15 @@ +export function compareDeep(a: any, b: any) { + if (a === b) return true + if (!(a && typeof a == "object") || + !(b && typeof b == "object")) return false + let array = Array.isArray(a) + if (Array.isArray(b) != array) return false + if (array) { + if (a.length != b.length) return false + for (let i = 0; i < a.length; i++) if (!compareDeep(a[i], b[i])) return false + } else { + for (let p in a) if (!(p in b) || !compareDeep(a[p], b[p])) return false + for (let p in b) if (!(p in a)) return false + } + return true +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/content.ts b/third_party/js/prosemirror/prosemirror-model/src/content.ts @@ -0,0 +1,413 @@ +import {Fragment} from "./fragment" +import {NodeType} from "./schema" + +type MatchEdge = {type: NodeType, next: ContentMatch} + +/// Instances of this class represent a match state of a node type's +/// [content expression](#model.NodeSpec.content), and can be used to +/// find out whether further content matches here, and whether a given +/// position is a valid end of the node. +export class ContentMatch { + /// @internal + readonly next: MatchEdge[] = [] + /// @internal + readonly wrapCache: (NodeType | readonly NodeType[] | null)[] = [] + + /// @internal + constructor( + /// True when this match state represents a valid end of the node. + readonly validEnd: boolean + ) {} + + /// @internal + static parse(string: string, nodeTypes: {readonly [name: string]: NodeType}): ContentMatch { + let stream = new TokenStream(string, nodeTypes) + if (stream.next == null) return ContentMatch.empty + let expr = parseExpr(stream) + if (stream.next) stream.err("Unexpected trailing text") + let match = dfa(nfa(expr)) + checkForDeadEnds(match, stream) + return match + } + + /// Match a node type, returning a match after that node if + /// successful. + matchType(type: NodeType): ContentMatch | null { + for (let i = 0; i < this.next.length; i++) + if (this.next[i].type == type) return this.next[i].next + return null + } + + /// Try to match a fragment. Returns the resulting match when + /// successful. + matchFragment(frag: Fragment, start = 0, end = frag.childCount): ContentMatch | null { + let cur: ContentMatch | null = this + for (let i = start; cur && i < end; i++) + cur = cur.matchType(frag.child(i).type) + return cur + } + + /// @internal + get inlineContent() { + return this.next.length != 0 && this.next[0].type.isInline + } + + /// Get the first matching node type at this match position that can + /// be generated. + get defaultType(): NodeType | null { + for (let i = 0; i < this.next.length; i++) { + let {type} = this.next[i] + if (!(type.isText || type.hasRequiredAttrs())) return type + } + return null + } + + /// @internal + compatible(other: ContentMatch) { + for (let i = 0; i < this.next.length; i++) + for (let j = 0; j < other.next.length; j++) + if (this.next[i].type == other.next[j].type) return true + return false + } + + /// Try to match the given fragment, and if that fails, see if it can + /// be made to match by inserting nodes in front of it. When + /// successful, return a fragment of inserted nodes (which may be + /// empty if nothing had to be inserted). When `toEnd` is true, only + /// return a fragment if the resulting match goes to the end of the + /// content expression. + fillBefore(after: Fragment, toEnd = false, startIndex = 0): Fragment | null { + let seen: ContentMatch[] = [this] + function search(match: ContentMatch, types: readonly NodeType[]): Fragment | null { + let finished = match.matchFragment(after, startIndex) + if (finished && (!toEnd || finished.validEnd)) + return Fragment.from(types.map(tp => tp.createAndFill()!)) + + for (let i = 0; i < match.next.length; i++) { + let {type, next} = match.next[i] + if (!(type.isText || type.hasRequiredAttrs()) && seen.indexOf(next) == -1) { + seen.push(next) + let found = search(next, types.concat(type)) + if (found) return found + } + } + return null + } + + return search(this, []) + } + + /// Find a set of wrapping node types that would allow a node of the + /// given type to appear at this position. The result may be empty + /// (when it fits directly) and will be null when no such wrapping + /// exists. + findWrapping(target: NodeType): readonly NodeType[] | null { + for (let i = 0; i < this.wrapCache.length; i += 2) + if (this.wrapCache[i] == target) return this.wrapCache[i + 1] as (readonly NodeType[] | null) + let computed = this.computeWrapping(target) + this.wrapCache.push(target, computed) + return computed + } + + /// @internal + computeWrapping(target: NodeType): readonly NodeType[] | null { + type Active = {match: ContentMatch, type: NodeType | null, via: Active | null} + let seen = Object.create(null), active: Active[] = [{match: this, type: null, via: null}] + while (active.length) { + let current = active.shift()!, match = current.match + if (match.matchType(target)) { + let result: NodeType[] = [] + for (let obj: Active = current; obj.type; obj = obj.via!) + result.push(obj.type) + return result.reverse() + } + for (let i = 0; i < match.next.length; i++) { + let {type, next} = match.next[i] + if (!type.isLeaf && !type.hasRequiredAttrs() && !(type.name in seen) && (!current.type || next.validEnd)) { + active.push({match: type.contentMatch, type, via: current}) + seen[type.name] = true + } + } + } + return null + } + + /// The number of outgoing edges this node has in the finite + /// automaton that describes the content expression. + get edgeCount() { + return this.next.length + } + + /// Get the _n_​th outgoing edge from this node in the finite + /// automaton that describes the content expression. + edge(n: number): MatchEdge { + if (n >= this.next.length) throw new RangeError(`There's no ${n}th edge in this content match`) + return this.next[n] + } + + /// @internal + toString() { + let seen: ContentMatch[] = [] + function scan(m: ContentMatch) { + seen.push(m) + for (let i = 0; i < m.next.length; i++) + if (seen.indexOf(m.next[i].next) == -1) scan(m.next[i].next) + } + scan(this) + return seen.map((m, i) => { + let out = i + (m.validEnd ? "*" : " ") + " " + for (let i = 0; i < m.next.length; i++) + out += (i ? ", " : "") + m.next[i].type.name + "->" + seen.indexOf(m.next[i].next) + return out + }).join("\n") + } + + /// @internal + static empty = new ContentMatch(true) +} + +class TokenStream { + inline: boolean | null = null + pos = 0 + tokens: string[] + + constructor( + readonly string: string, + readonly nodeTypes: {readonly [name: string]: NodeType} + ) { + this.tokens = string.split(/\s*(?=\b|\W|$)/) + if (this.tokens[this.tokens.length - 1] == "") this.tokens.pop() + if (this.tokens[0] == "") this.tokens.shift() + } + + get next() { return this.tokens[this.pos] } + + eat(tok: string) { return this.next == tok && (this.pos++ || true) } + + err(str: string): never { throw new SyntaxError(str + " (in content expression '" + this.string + "')") } +} + +type Expr = + {type: "choice", exprs: Expr[]} | + {type: "seq", exprs: Expr[]} | + {type: "plus", expr: Expr} | + {type: "star", expr: Expr} | + {type: "opt", expr: Expr} | + {type: "range", min: number, max: number, expr: Expr} | + {type: "name", value: NodeType} + +function parseExpr(stream: TokenStream): Expr { + let exprs: Expr[] = [] + do { exprs.push(parseExprSeq(stream)) } + while (stream.eat("|")) + return exprs.length == 1 ? exprs[0] : {type: "choice", exprs} +} + +function parseExprSeq(stream: TokenStream): Expr { + let exprs: Expr[] = [] + do { exprs.push(parseExprSubscript(stream)) } + while (stream.next && stream.next != ")" && stream.next != "|") + return exprs.length == 1 ? exprs[0] : {type: "seq", exprs} +} + +function parseExprSubscript(stream: TokenStream): Expr { + let expr = parseExprAtom(stream) + for (;;) { + if (stream.eat("+")) + expr = {type: "plus", expr} + else if (stream.eat("*")) + expr = {type: "star", expr} + else if (stream.eat("?")) + expr = {type: "opt", expr} + else if (stream.eat("{")) + expr = parseExprRange(stream, expr) + else break + } + return expr +} + +function parseNum(stream: TokenStream) { + if (/\D/.test(stream.next)) stream.err("Expected number, got '" + stream.next + "'") + let result = Number(stream.next) + stream.pos++ + return result +} + +function parseExprRange(stream: TokenStream, expr: Expr): Expr { + let min = parseNum(stream), max = min + if (stream.eat(",")) { + if (stream.next != "}") max = parseNum(stream) + else max = -1 + } + if (!stream.eat("}")) stream.err("Unclosed braced range") + return {type: "range", min, max, expr} +} + +function resolveName(stream: TokenStream, name: string): readonly NodeType[] { + let types = stream.nodeTypes, type = types[name] + if (type) return [type] + let result: NodeType[] = [] + for (let typeName in types) { + let type = types[typeName] + if (type.isInGroup(name)) result.push(type) + } + if (result.length == 0) stream.err("No node type or group '" + name + "' found") + return result +} + +function parseExprAtom(stream: TokenStream): Expr { + if (stream.eat("(")) { + let expr = parseExpr(stream) + if (!stream.eat(")")) stream.err("Missing closing paren") + return expr + } else if (!/\W/.test(stream.next)) { + let exprs = resolveName(stream, stream.next).map(type => { + if (stream.inline == null) stream.inline = type.isInline + else if (stream.inline != type.isInline) stream.err("Mixing inline and block content") + return {type: "name", value: type} as Expr + }) + stream.pos++ + return exprs.length == 1 ? exprs[0] : {type: "choice", exprs} + } else { + stream.err("Unexpected token '" + stream.next + "'") + } +} + +// The code below helps compile a regular-expression-like language +// into a deterministic finite automaton. For a good introduction to +// these concepts, see https://swtch.com/~rsc/regexp/regexp1.html + +type Edge = {term: NodeType | undefined, to: number | undefined} + +// Construct an NFA from an expression as returned by the parser. The +// NFA is represented as an array of states, which are themselves +// arrays of edges, which are `{term, to}` objects. The first state is +// the entry state and the last node is the success state. +// +// Note that unlike typical NFAs, the edge ordering in this one is +// significant, in that it is used to contruct filler content when +// necessary. +function nfa(expr: Expr): Edge[][] { + let nfa: Edge[][] = [[]] + connect(compile(expr, 0), node()) + return nfa + + function node() { return nfa.push([]) - 1 } + function edge(from: number, to?: number, term?: NodeType) { + let edge = {term, to} + nfa[from].push(edge) + return edge + } + function connect(edges: Edge[], to: number) { + edges.forEach(edge => edge.to = to) + } + + function compile(expr: Expr, from: number): Edge[] { + if (expr.type == "choice") { + return expr.exprs.reduce((out, expr) => out.concat(compile(expr, from)), [] as Edge[]) + } else if (expr.type == "seq") { + for (let i = 0;; i++) { + let next = compile(expr.exprs[i], from) + if (i == expr.exprs.length - 1) return next + connect(next, from = node()) + } + } else if (expr.type == "star") { + let loop = node() + edge(from, loop) + connect(compile(expr.expr, loop), loop) + return [edge(loop)] + } else if (expr.type == "plus") { + let loop = node() + connect(compile(expr.expr, from), loop) + connect(compile(expr.expr, loop), loop) + return [edge(loop)] + } else if (expr.type == "opt") { + return [edge(from)].concat(compile(expr.expr, from)) + } else if (expr.type == "range") { + let cur = from + for (let i = 0; i < expr.min; i++) { + let next = node() + connect(compile(expr.expr, cur), next) + cur = next + } + if (expr.max == -1) { + connect(compile(expr.expr, cur), cur) + } else { + for (let i = expr.min; i < expr.max; i++) { + let next = node() + edge(cur, next) + connect(compile(expr.expr, cur), next) + cur = next + } + } + return [edge(cur)] + } else if (expr.type == "name") { + return [edge(from, undefined, expr.value)] + } else { + throw new Error("Unknown expr type") + } + } +} + +function cmp(a: number, b: number) { return b - a } + +// Get the set of nodes reachable by null edges from `node`. Omit +// nodes with only a single null-out-edge, since they may lead to +// needless duplicated nodes. +function nullFrom(nfa: Edge[][], node: number): readonly number[] { + let result: number[] = [] + scan(node) + return result.sort(cmp) + + function scan(node: number): void { + let edges = nfa[node] + if (edges.length == 1 && !edges[0].term) return scan(edges[0].to!) + result.push(node) + for (let i = 0; i < edges.length; i++) { + let {term, to} = edges[i] + if (!term && result.indexOf(to!) == -1) scan(to!) + } + } +} + +// Compiles an NFA as produced by `nfa` into a DFA, modeled as a set +// of state objects (`ContentMatch` instances) with transitions +// between them. +function dfa(nfa: Edge[][]): ContentMatch { + let labeled = Object.create(null) + return explore(nullFrom(nfa, 0)) + + function explore(states: readonly number[]) { + let out: [NodeType, number[]][] = [] + states.forEach(node => { + nfa[node].forEach(({term, to}) => { + if (!term) return + let set: number[] | undefined + for (let i = 0; i < out.length; i++) if (out[i][0] == term) set = out[i][1] + nullFrom(nfa, to!).forEach(node => { + if (!set) out.push([term, set = []]) + if (set.indexOf(node) == -1) set.push(node) + }) + }) + }) + let state = labeled[states.join(",")] = new ContentMatch(states.indexOf(nfa.length - 1) > -1) + for (let i = 0; i < out.length; i++) { + let states = out[i][1].sort(cmp) + state.next.push({type: out[i][0], next: labeled[states.join(",")] || explore(states)}) + } + return state + } +} + +function checkForDeadEnds(match: ContentMatch, stream: TokenStream) { + for (let i = 0, work = [match]; i < work.length; i++) { + let state = work[i], dead = !state.validEnd, nodes: string[] = [] + for (let j = 0; j < state.next.length; j++) { + let {type, next} = state.next[j] + nodes.push(type.name) + if (dead && !(type.isText || type.hasRequiredAttrs())) dead = false + if (work.indexOf(next) == -1) work.push(next) + } + if (dead) stream.err("Only non-generatable nodes (" + nodes.join(", ") + ") in a required position (see https://prosemirror.net/docs/guide/#generatable)") + } +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/diff.ts b/third_party/js/prosemirror/prosemirror-model/src/diff.ts @@ -0,0 +1,52 @@ +import {Fragment} from "./fragment" + +export function findDiffStart(a: Fragment, b: Fragment, pos: number): number | null { + for (let i = 0;; i++) { + if (i == a.childCount || i == b.childCount) + return a.childCount == b.childCount ? null : pos + + let childA = a.child(i), childB = b.child(i) + if (childA == childB) { pos += childA.nodeSize; continue } + + if (!childA.sameMarkup(childB)) return pos + + if (childA.isText && childA.text != childB.text) { + for (let j = 0; childA.text![j] == childB.text![j]; j++) + pos++ + return pos + } + if (childA.content.size || childB.content.size) { + let inner = findDiffStart(childA.content, childB.content, pos + 1) + if (inner != null) return inner + } + pos += childA.nodeSize + } +} + +export function findDiffEnd(a: Fragment, b: Fragment, posA: number, posB: number): {a: number, b: number} | null { + for (let iA = a.childCount, iB = b.childCount;;) { + if (iA == 0 || iB == 0) + return iA == iB ? null : {a: posA, b: posB} + + let childA = a.child(--iA), childB = b.child(--iB), size = childA.nodeSize + if (childA == childB) { + posA -= size; posB -= size + continue + } + + if (!childA.sameMarkup(childB)) return {a: posA, b: posB} + + if (childA.isText && childA.text != childB.text) { + let same = 0, minSize = Math.min(childA.text!.length, childB.text!.length) + while (same < minSize && childA.text![childA.text!.length - same - 1] == childB.text![childB.text!.length - same - 1]) { + same++; posA--; posB-- + } + return {a: posA, b: posB} + } + if (childA.content.size || childB.content.size) { + let inner = findDiffEnd(childA.content, childB.content, posA - 1, posB - 1) + if (inner) return inner + } + posA -= size; posB -= size + } +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/dom.ts b/third_party/js/prosemirror/prosemirror-model/src/dom.ts @@ -0,0 +1 @@ +export type DOMNode = InstanceType<typeof window.Node> diff --git a/third_party/js/prosemirror/prosemirror-model/src/fragment.ts b/third_party/js/prosemirror/prosemirror-model/src/fragment.ts @@ -0,0 +1,268 @@ +import {findDiffStart, findDiffEnd} from "./diff" +import {Node, TextNode} from "./node" +import {Schema} from "./schema" + +/// A fragment represents a node's collection of child nodes. +/// +/// Like nodes, fragments are persistent data structures, and you +/// should not mutate them or their content. Rather, you create new +/// instances whenever needed. The API tries to make this easy. +export class Fragment { + /// The size of the fragment, which is the total of the size of + /// its content nodes. + readonly size: number + + /// @internal + constructor( + /// The child nodes in this fragment. + readonly content: readonly Node[], + size?: number + ) { + this.size = size || 0 + if (size == null) for (let i = 0; i < content.length; i++) + this.size += content[i].nodeSize + } + + /// Invoke a callback for all descendant nodes between the given two + /// positions (relative to start of this fragment). Doesn't descend + /// into a node when the callback returns `false`. + nodesBetween(from: number, to: number, + f: (node: Node, start: number, parent: Node | null, index: number) => boolean | void, + nodeStart = 0, + parent?: Node) { + for (let i = 0, pos = 0; pos < to; i++) { + let child = this.content[i], end = pos + child.nodeSize + if (end > from && f(child, nodeStart + pos, parent || null, i) !== false && child.content.size) { + let start = pos + 1 + child.nodesBetween(Math.max(0, from - start), + Math.min(child.content.size, to - start), + f, nodeStart + start) + } + pos = end + } + } + + /// Call the given callback for every descendant node. `pos` will be + /// relative to the start of the fragment. The callback may return + /// `false` to prevent traversal of a given node's children. + descendants(f: (node: Node, pos: number, parent: Node | null, index: number) => boolean | void) { + this.nodesBetween(0, this.size, f) + } + + /// Extract the text between `from` and `to`. See the same method on + /// [`Node`](#model.Node.textBetween). + textBetween(from: number, to: number, blockSeparator?: string | null, leafText?: string | null | ((leafNode: Node) => string)) { + let text = "", first = true + this.nodesBetween(from, to, (node, pos) => { + let nodeText = node.isText ? node.text!.slice(Math.max(from, pos) - pos, to - pos) + : !node.isLeaf ? "" + : leafText ? (typeof leafText === "function" ? leafText(node) : leafText) + : node.type.spec.leafText ? node.type.spec.leafText(node) + : "" + if (node.isBlock && (node.isLeaf && nodeText || node.isTextblock) && blockSeparator) { + if (first) first = false + else text += blockSeparator + } + text += nodeText + }, 0) + return text + } + + /// Create a new fragment containing the combined content of this + /// fragment and the other. + append(other: Fragment) { + if (!other.size) return this + if (!this.size) return other + let last = this.lastChild!, first = other.firstChild!, content = this.content.slice(), i = 0 + if (last.isText && last.sameMarkup(first)) { + content[content.length - 1] = (last as TextNode).withText(last.text! + first.text!) + i = 1 + } + for (; i < other.content.length; i++) content.push(other.content[i]) + return new Fragment(content, this.size + other.size) + } + + /// Cut out the sub-fragment between the two given positions. + cut(from: number, to = this.size) { + if (from == 0 && to == this.size) return this + let result: Node[] = [], size = 0 + if (to > from) for (let i = 0, pos = 0; pos < to; i++) { + let child = this.content[i], end = pos + child.nodeSize + if (end > from) { + if (pos < from || end > to) { + if (child.isText) + child = child.cut(Math.max(0, from - pos), Math.min(child.text!.length, to - pos)) + else + child = child.cut(Math.max(0, from - pos - 1), Math.min(child.content.size, to - pos - 1)) + } + result.push(child) + size += child.nodeSize + } + pos = end + } + return new Fragment(result, size) + } + + /// @internal + cutByIndex(from: number, to: number) { + if (from == to) return Fragment.empty + if (from == 0 && to == this.content.length) return this + return new Fragment(this.content.slice(from, to)) + } + + /// Create a new fragment in which the node at the given index is + /// replaced by the given node. + replaceChild(index: number, node: Node) { + let current = this.content[index] + if (current == node) return this + let copy = this.content.slice() + let size = this.size + node.nodeSize - current.nodeSize + copy[index] = node + return new Fragment(copy, size) + } + + /// Create a new fragment by prepending the given node to this + /// fragment. + addToStart(node: Node) { + return new Fragment([node].concat(this.content), this.size + node.nodeSize) + } + + /// Create a new fragment by appending the given node to this + /// fragment. + addToEnd(node: Node) { + return new Fragment(this.content.concat(node), this.size + node.nodeSize) + } + + /// Compare this fragment to another one. + eq(other: Fragment): boolean { + if (this.content.length != other.content.length) return false + for (let i = 0; i < this.content.length; i++) + if (!this.content[i].eq(other.content[i])) return false + return true + } + + /// The first child of the fragment, or `null` if it is empty. + get firstChild(): Node | null { return this.content.length ? this.content[0] : null } + + /// The last child of the fragment, or `null` if it is empty. + get lastChild(): Node | null { return this.content.length ? this.content[this.content.length - 1] : null } + + /// The number of child nodes in this fragment. + get childCount() { return this.content.length } + + /// Get the child node at the given index. Raise an error when the + /// index is out of range. + child(index: number) { + let found = this.content[index] + if (!found) throw new RangeError("Index " + index + " out of range for " + this) + return found + } + + /// Get the child node at the given index, if it exists. + maybeChild(index: number): Node | null { + return this.content[index] || null + } + + /// Call `f` for every child node, passing the node, its offset + /// into this parent node, and its index. + forEach(f: (node: Node, offset: number, index: number) => void) { + for (let i = 0, p = 0; i < this.content.length; i++) { + let child = this.content[i] + f(child, p, i) + p += child.nodeSize + } + } + + /// Find the first position at which this fragment and another + /// fragment differ, or `null` if they are the same. + findDiffStart(other: Fragment, pos = 0) { + return findDiffStart(this, other, pos) + } + + /// Find the first position, searching from the end, at which this + /// fragment and the given fragment differ, or `null` if they are + /// the same. Since this position will not be the same in both + /// nodes, an object with two separate positions is returned. + findDiffEnd(other: Fragment, pos = this.size, otherPos = other.size) { + return findDiffEnd(this, other, pos, otherPos) + } + + /// Find the index and inner offset corresponding to a given relative + /// position in this fragment. The result object will be reused + /// (overwritten) the next time the function is called. @internal + findIndex(pos: number): {index: number, offset: number} { + if (pos == 0) return retIndex(0, pos) + if (pos == this.size) return retIndex(this.content.length, pos) + if (pos > this.size || pos < 0) throw new RangeError(`Position ${pos} outside of fragment (${this})`) + for (let i = 0, curPos = 0;; i++) { + let cur = this.child(i), end = curPos + cur.nodeSize + if (end >= pos) { + if (end == pos) return retIndex(i + 1, end) + return retIndex(i, curPos) + } + curPos = end + } + } + + /// Return a debugging string that describes this fragment. + toString(): string { return "<" + this.toStringInner() + ">" } + + /// @internal + toStringInner() { return this.content.join(", ") } + + /// Create a JSON-serializeable representation of this fragment. + toJSON(): any { + return this.content.length ? this.content.map(n => n.toJSON()) : null + } + + /// Deserialize a fragment from its JSON representation. + static fromJSON(schema: Schema, value: any) { + if (!value) return Fragment.empty + if (!Array.isArray(value)) throw new RangeError("Invalid input for Fragment.fromJSON") + return new Fragment(value.map(schema.nodeFromJSON)) + } + + /// Build a fragment from an array of nodes. Ensures that adjacent + /// text nodes with the same marks are joined together. + static fromArray(array: readonly Node[]) { + if (!array.length) return Fragment.empty + let joined: Node[] | undefined, size = 0 + for (let i = 0; i < array.length; i++) { + let node = array[i] + size += node.nodeSize + if (i && node.isText && array[i - 1].sameMarkup(node)) { + if (!joined) joined = array.slice(0, i) + joined[joined.length - 1] = (node as TextNode) + .withText((joined[joined.length - 1] as TextNode).text + (node as TextNode).text) + } else if (joined) { + joined.push(node) + } + } + return new Fragment(joined || array, size) + } + + /// Create a fragment from something that can be interpreted as a + /// set of nodes. For `null`, it returns the empty fragment. For a + /// fragment, the fragment itself. For a node or array of nodes, a + /// fragment containing those nodes. + static from(nodes?: Fragment | Node | readonly Node[] | null) { + if (!nodes) return Fragment.empty + if (nodes instanceof Fragment) return nodes + if (Array.isArray(nodes)) return this.fromArray(nodes) + if ((nodes as Node).attrs) return new Fragment([nodes as Node], (nodes as Node).nodeSize) + throw new RangeError("Can not convert " + nodes + " to a Fragment" + + ((nodes as any).nodesBetween ? " (looks like multiple versions of prosemirror-model were loaded)" : "")) + } + + /// An empty fragment. Intended to be reused whenever a node doesn't + /// contain anything (rather than allocating a new empty fragment for + /// each leaf node). + static empty: Fragment = new Fragment([], 0) +} + +const found = {index: 0, offset: 0} +function retIndex(index: number, offset: number) { + found.index = index + found.offset = offset + return found +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/from_dom.ts b/third_party/js/prosemirror/prosemirror-model/src/from_dom.ts @@ -0,0 +1,848 @@ +import {Fragment} from "./fragment" +import {Slice} from "./replace" +import {Mark} from "./mark" +import {Node, TextNode} from "./node" +import {ContentMatch} from "./content" +import {ResolvedPos} from "./resolvedpos" +import {Schema, Attrs, NodeType, MarkType} from "./schema" +import {DOMNode} from "./dom" + +/// These are the options recognized by the +/// [`parse`](#model.DOMParser.parse) and +/// [`parseSlice`](#model.DOMParser.parseSlice) methods. +export interface ParseOptions { + /// By default, whitespace is collapsed as per HTML's rules. Pass + /// `true` to preserve whitespace, but normalize newlines to + /// spaces or, if available, [line break replacements](#model.NodeSpec.linebreakReplacement), + /// and `"full"` to preserve whitespace entirely. + preserveWhitespace?: boolean | "full" + + /// When given, the parser will, beside parsing the content, + /// record the document positions of the given DOM positions. It + /// will do so by writing to the objects, adding a `pos` property + /// that holds the document position. DOM positions that are not + /// in the parsed content will not be written to. + findPositions?: {node: DOMNode, offset: number, pos?: number}[] + + /// The child node index to start parsing from. + from?: number + + /// The child node index to stop parsing at. + to?: number + + /// By default, the content is parsed into the schema's default + /// [top node type](#model.Schema.topNodeType). You can pass this + /// option to use the type and attributes from a different node + /// as the top container. + topNode?: Node + + /// Provide the starting content match that content parsed into the + /// top node is matched against. + topMatch?: ContentMatch + + /// A set of additional nodes to count as + /// [context](#model.GenericParseRule.context) when parsing, above the + /// given [top node](#model.ParseOptions.topNode). + context?: ResolvedPos + + /// @internal + ruleFromNode?: (node: DOMNode) => Omit<TagParseRule, "tag"> | null + /// @internal + topOpen?: boolean +} + +/// Fields that may be present in both [tag](#model.TagParseRule) and +/// [style](#model.StyleParseRule) parse rules. +export interface GenericParseRule { + /// Can be used to change the order in which the parse rules in a + /// schema are tried. Those with higher priority come first. Rules + /// without a priority are counted as having priority 50. This + /// property is only meaningful in a schema—when directly + /// constructing a parser, the order of the rule array is used. + priority?: number + + /// By default, when a rule matches an element or style, no further + /// rules get a chance to match it. By setting this to `false`, you + /// indicate that even when this rule matches, other rules that come + /// after it should also run. + consuming?: boolean + + /// When given, restricts this rule to only match when the current + /// context—the parent nodes into which the content is being + /// parsed—matches this expression. Should contain one or more node + /// names or node group names followed by single or double slashes. + /// For example `"paragraph/"` means the rule only matches when the + /// parent node is a paragraph, `"blockquote/paragraph/"` restricts + /// it to be in a paragraph that is inside a blockquote, and + /// `"section//"` matches any position inside a section—a double + /// slash matches any sequence of ancestor nodes. To allow multiple + /// different contexts, they can be separated by a pipe (`|`) + /// character, as in `"blockquote/|list_item/"`. + context?: string + + /// The name of the mark type to wrap the matched content in. + mark?: string + + /// When true, ignore content that matches this rule. + ignore?: boolean + + /// When true, finding an element that matches this rule will close + /// the current node. + closeParent?: boolean + + /// When true, ignore the node that matches this rule, but do parse + /// its content. + skip?: boolean + + /// Attributes for the node or mark created by this rule. When + /// `getAttrs` is provided, it takes precedence. + attrs?: Attrs +} + +/// Parse rule targeting a DOM element. +export interface TagParseRule extends GenericParseRule { + /// A CSS selector describing the kind of DOM elements to match. + tag: string + + /// The namespace to match. Nodes are only matched when the + /// namespace matches or this property is null. + namespace?: string + + /// The name of the node type to create when this rule matches. Each + /// rule should have either a `node`, `mark`, or `ignore` property + /// (except when it appears in a [node](#model.NodeSpec.parseDOM) or + /// [mark spec](#model.MarkSpec.parseDOM), in which case the `node` + /// or `mark` property will be derived from its position). + node?: string + + /// A function used to compute the attributes for the node or mark + /// created by this rule. Can also be used to describe further + /// conditions the DOM element or style must match. When it returns + /// `false`, the rule won't match. When it returns null or undefined, + /// that is interpreted as an empty/default set of attributes. + getAttrs?: (node: HTMLElement) => Attrs | false | null + + /// For rules that produce non-leaf nodes, by default the content of + /// the DOM element is parsed as content of the node. If the child + /// nodes are in a descendent node, this may be a CSS selector + /// string that the parser must use to find the actual content + /// element, or a function that returns the actual content element + /// to the parser. + contentElement?: string | HTMLElement | ((node: HTMLElement) => HTMLElement) + + /// Can be used to override the content of a matched node. When + /// present, instead of parsing the node's child nodes, the result of + /// this function is used. + getContent?: (node: DOMNode, schema: Schema) => Fragment + + /// Controls whether whitespace should be preserved when parsing the + /// content inside the matched element. `false` means whitespace may + /// be collapsed, `true` means that whitespace should be preserved + /// but newlines normalized to spaces, and `"full"` means that + /// newlines should also be preserved. + preserveWhitespace?: boolean | "full" +} + +/// A parse rule targeting a style property. +export interface StyleParseRule extends GenericParseRule { + /// A CSS property name to match. This rule will match inline styles + /// that list that property. May also have the form + /// `"property=value"`, in which case the rule only matches if the + /// property's value exactly matches the given value. (For more + /// complicated filters, use [`getAttrs`](#model.StyleParseRule.getAttrs) + /// and return false to indicate that the match failed.) Rules + /// matching styles may only produce [marks](#model.GenericParseRule.mark), + /// not nodes. + style: string + + /// Given to make TS see ParseRule as a tagged union @hide + tag?: undefined + + /// Style rules can remove marks from the set of active marks. + clearMark?: (mark: Mark) => boolean + + /// A function used to compute the attributes for the node or mark + /// created by this rule. Called with the style's value. + getAttrs?: (node: string) => Attrs | false | null +} + +/// A value that describes how to parse a given DOM node or inline +/// style as a ProseMirror node or mark. +export type ParseRule = TagParseRule | StyleParseRule + +function isTagRule(rule: ParseRule): rule is TagParseRule { return (rule as TagParseRule).tag != null } +function isStyleRule(rule: ParseRule): rule is StyleParseRule { return (rule as StyleParseRule).style != null } + +/// A DOM parser represents a strategy for parsing DOM content into a +/// ProseMirror document conforming to a given schema. Its behavior is +/// defined by an array of [rules](#model.ParseRule). +export class DOMParser { + /// @internal + tags: TagParseRule[] = [] + /// @internal + styles: StyleParseRule[] = [] + /// @internal + matchedStyles: readonly string[] + /// @internal + normalizeLists: boolean + + /// Create a parser that targets the given schema, using the given + /// parsing rules. + constructor( + /// The schema into which the parser parses. + readonly schema: Schema, + /// The set of [parse rules](#model.ParseRule) that the parser + /// uses, in order of precedence. + readonly rules: readonly ParseRule[] + ) { + let matchedStyles: string[] = this.matchedStyles = [] + rules.forEach(rule => { + if (isTagRule(rule)) { + this.tags.push(rule) + } else if (isStyleRule(rule)) { + let prop = /[^=]*/.exec(rule.style)![0] + if (matchedStyles.indexOf(prop) < 0) matchedStyles.push(prop) + this.styles.push(rule) + } + }) + + // Only normalize list elements when lists in the schema can't directly contain themselves + this.normalizeLists = !this.tags.some(r => { + if (!/^(ul|ol)\b/.test(r.tag!) || !r.node) return false + let node = schema.nodes[r.node] + return node.contentMatch.matchType(node) + }) + } + + /// Parse a document from the content of a DOM node. + parse(dom: DOMNode, options: ParseOptions = {}): Node { + let context = new ParseContext(this, options, false) + context.addAll(dom, Mark.none, options.from, options.to) + return context.finish() as Node + } + + /// Parses the content of the given DOM node, like + /// [`parse`](#model.DOMParser.parse), and takes the same set of + /// options. But unlike that method, which produces a whole node, + /// this one returns a slice that is open at the sides, meaning that + /// the schema constraints aren't applied to the start of nodes to + /// the left of the input and the end of nodes at the end. + parseSlice(dom: DOMNode, options: ParseOptions = {}) { + let context = new ParseContext(this, options, true) + context.addAll(dom, Mark.none, options.from, options.to) + return Slice.maxOpen(context.finish() as Fragment) + } + + /// @internal + matchTag(dom: DOMNode, context: ParseContext, after?: TagParseRule) { + for (let i = after ? this.tags.indexOf(after) + 1 : 0; i < this.tags.length; i++) { + let rule = this.tags[i] + if (matches(dom, rule.tag!) && + (rule.namespace === undefined || (dom as HTMLElement).namespaceURI == rule.namespace) && + (!rule.context || context.matchesContext(rule.context))) { + if (rule.getAttrs) { + let result = rule.getAttrs(dom as HTMLElement) + if (result === false) continue + rule.attrs = result || undefined + } + return rule + } + } + } + + /// @internal + matchStyle(prop: string, value: string, context: ParseContext, after?: StyleParseRule) { + for (let i = after ? this.styles.indexOf(after) + 1 : 0; i < this.styles.length; i++) { + let rule = this.styles[i], style = rule.style! + if (style.indexOf(prop) != 0 || + rule.context && !context.matchesContext(rule.context) || + // Test that the style string either precisely matches the prop, + // or has an '=' sign after the prop, followed by the given + // value. + style.length > prop.length && + (style.charCodeAt(prop.length) != 61 || style.slice(prop.length + 1) != value)) + continue + if (rule.getAttrs) { + let result = rule.getAttrs(value) + if (result === false) continue + rule.attrs = result || undefined + } + return rule + } + } + + /// @internal + static schemaRules(schema: Schema) { + let result: ParseRule[] = [] + function insert(rule: ParseRule) { + let priority = rule.priority == null ? 50 : rule.priority, i = 0 + for (; i < result.length; i++) { + let next = result[i], nextPriority = next.priority == null ? 50 : next.priority + if (nextPriority < priority) break + } + result.splice(i, 0, rule) + } + + for (let name in schema.marks) { + let rules = schema.marks[name].spec.parseDOM + if (rules) rules.forEach(rule => { + insert(rule = copy(rule) as ParseRule) + if (!(rule.mark || rule.ignore || (rule as StyleParseRule).clearMark)) + rule.mark = name + }) + } + for (let name in schema.nodes) { + let rules = schema.nodes[name].spec.parseDOM + if (rules) rules.forEach(rule => { + insert(rule = copy(rule) as TagParseRule) + if (!((rule as TagParseRule).node || rule.ignore || rule.mark)) + rule.node = name + }) + } + return result + } + + /// Construct a DOM parser using the parsing rules listed in a + /// schema's [node specs](#model.NodeSpec.parseDOM), reordered by + /// [priority](#model.GenericParseRule.priority). + static fromSchema(schema: Schema) { + return schema.cached.domParser as DOMParser || + (schema.cached.domParser = new DOMParser(schema, DOMParser.schemaRules(schema))) + } +} + +const blockTags: {[tagName: string]: boolean} = { + address: true, article: true, aside: true, blockquote: true, canvas: true, + dd: true, div: true, dl: true, fieldset: true, figcaption: true, figure: true, + footer: true, form: true, h1: true, h2: true, h3: true, h4: true, h5: true, + h6: true, header: true, hgroup: true, hr: true, li: true, noscript: true, ol: true, + output: true, p: true, pre: true, section: true, table: true, tfoot: true, ul: true +} + +const ignoreTags: {[tagName: string]: boolean} = { + head: true, noscript: true, object: true, script: true, style: true, title: true +} + +const listTags: {[tagName: string]: boolean} = {ol: true, ul: true} + +// Using a bitfield for node context options +const OPT_PRESERVE_WS = 1, OPT_PRESERVE_WS_FULL = 2, OPT_OPEN_LEFT = 4 + +function wsOptionsFor(type: NodeType | null, preserveWhitespace: boolean | "full" | undefined, base: number) { + if (preserveWhitespace != null) return (preserveWhitespace ? OPT_PRESERVE_WS : 0) | + (preserveWhitespace === "full" ? OPT_PRESERVE_WS_FULL : 0) + return type && type.whitespace == "pre" ? OPT_PRESERVE_WS | OPT_PRESERVE_WS_FULL : base & ~OPT_OPEN_LEFT +} + +class NodeContext { + match: ContentMatch | null + content: Node[] = [] + + // Marks applied to the node's children + activeMarks: readonly Mark[] = Mark.none + + constructor( + readonly type: NodeType | null, + readonly attrs: Attrs | null, + readonly marks: readonly Mark[], + readonly solid: boolean, + match: ContentMatch | null, + public options: number + ) { + this.match = match || (options & OPT_OPEN_LEFT ? null : type!.contentMatch) + } + + findWrapping(node: Node) { + if (!this.match) { + if (!this.type) return [] + let fill = this.type.contentMatch.fillBefore(Fragment.from(node)) + if (fill) { + this.match = this.type.contentMatch.matchFragment(fill)! + } else { + let start = this.type.contentMatch, wrap + if (wrap = start.findWrapping(node.type)) { + this.match = start + return wrap + } else { + return null + } + } + } + return this.match.findWrapping(node.type) + } + + finish(openEnd: boolean): Node | Fragment { + if (!(this.options & OPT_PRESERVE_WS)) { // Strip trailing whitespace + let last = this.content[this.content.length - 1], m + if (last && last.isText && (m = /[ \t\r\n\u000c]+$/.exec(last.text!))) { + let text = last as TextNode + if (last.text!.length == m[0].length) this.content.pop() + else this.content[this.content.length - 1] = text.withText(text.text.slice(0, text.text.length - m[0].length)) + } + } + let content = Fragment.from(this.content) + if (!openEnd && this.match) + content = content.append(this.match.fillBefore(Fragment.empty, true)!) + return this.type ? this.type.create(this.attrs, content, this.marks) : content + } + + inlineContext(node: DOMNode) { + if (this.type) return this.type.inlineContent + if (this.content.length) return this.content[0].isInline + return node.parentNode && !blockTags.hasOwnProperty(node.parentNode.nodeName.toLowerCase()) + } +} + +class ParseContext { + open: number = 0 + find: {node: DOMNode, offset: number, pos?: number}[] | undefined + needsBlock: boolean + nodes: NodeContext[] + localPreserveWS = false + + constructor( + // The parser we are using. + readonly parser: DOMParser, + // The options passed to this parse. + readonly options: ParseOptions, + readonly isOpen: boolean + ) { + let topNode = options.topNode, topContext: NodeContext + let topOptions = wsOptionsFor(null, options.preserveWhitespace, 0) | (isOpen ? OPT_OPEN_LEFT : 0) + if (topNode) + topContext = new NodeContext(topNode.type, topNode.attrs, Mark.none, true, + options.topMatch || topNode.type.contentMatch, topOptions) + else if (isOpen) + topContext = new NodeContext(null, null, Mark.none, true, null, topOptions) + else + topContext = new NodeContext(parser.schema.topNodeType, null, Mark.none, true, null, topOptions) + this.nodes = [topContext] + this.find = options.findPositions + this.needsBlock = false + } + + get top() { + return this.nodes[this.open] + } + + // Add a DOM node to the content. Text is inserted as text node, + // otherwise, the node is passed to `addElement` or, if it has a + // `style` attribute, `addElementWithStyles`. + addDOM(dom: DOMNode, marks: readonly Mark[]) { + if (dom.nodeType == 3) this.addTextNode(dom as Text, marks) + else if (dom.nodeType == 1) this.addElement(dom as HTMLElement, marks) + } + + addTextNode(dom: Text, marks: readonly Mark[]) { + let value = dom.nodeValue! + let top = this.top, preserveWS = (top.options & OPT_PRESERVE_WS_FULL) ? "full" + : this.localPreserveWS || (top.options & OPT_PRESERVE_WS) > 0 + let {schema} = this.parser + if (preserveWS === "full" || + top.inlineContext(dom) || + /[^ \t\r\n\u000c]/.test(value)) { + if (!preserveWS) { + value = value.replace(/[ \t\r\n\u000c]+/g, " ") + // If this starts with whitespace, and there is no node before it, or + // a hard break, or a text node that ends with whitespace, strip the + // leading space. + if (/^[ \t\r\n\u000c]/.test(value) && this.open == this.nodes.length - 1) { + let nodeBefore = top.content[top.content.length - 1] + let domNodeBefore = dom.previousSibling + if (!nodeBefore || + (domNodeBefore && domNodeBefore.nodeName == 'BR') || + (nodeBefore.isText && /[ \t\r\n\u000c]$/.test(nodeBefore.text!))) + value = value.slice(1) + } + } else if (preserveWS === "full") { + value = value.replace(/\r\n?/g, "\n") + } else if (schema.linebreakReplacement && /[\r\n]/.test(value) && this.top.findWrapping(schema.linebreakReplacement.create())) { + let lines = value.split(/\r?\n|\r/) + for (let i = 0; i < lines.length; i++) { + if (i) this.insertNode(schema.linebreakReplacement.create(), marks, true) + if (lines[i]) this.insertNode(schema.text(lines[i]), marks, !/\S/.test(lines[i])) + } + value = "" + } else { + value = value.replace(/\r?\n|\r/g, " ") + } + if (value) this.insertNode(schema.text(value), marks, !/\S/.test(value)) + this.findInText(dom) + } else { + this.findInside(dom) + } + } + + // Try to find a handler for the given tag and use that to parse. If + // none is found, the element's content nodes are added directly. + addElement(dom: HTMLElement, marks: readonly Mark[], matchAfter?: TagParseRule) { + let outerWS = this.localPreserveWS, top = this.top + if (dom.tagName == "PRE" || /pre/.test(dom.style && dom.style.whiteSpace)) + this.localPreserveWS = true + let name = dom.nodeName.toLowerCase(), ruleID: TagParseRule | undefined + if (listTags.hasOwnProperty(name) && this.parser.normalizeLists) normalizeList(dom) + let rule = (this.options.ruleFromNode && this.options.ruleFromNode(dom)) || + (ruleID = this.parser.matchTag(dom, this, matchAfter)) + out: + if (rule ? rule.ignore : ignoreTags.hasOwnProperty(name)) { + this.findInside(dom) + this.ignoreFallback(dom, marks) + } else if (!rule || rule.skip || rule.closeParent) { + if (rule && rule.closeParent) this.open = Math.max(0, this.open - 1) + else if (rule && (rule.skip as any).nodeType) dom = rule.skip as any as HTMLElement + let sync, oldNeedsBlock = this.needsBlock + if (blockTags.hasOwnProperty(name)) { + if (top.content.length && top.content[0].isInline && this.open) { + this.open-- + top = this.top + } + sync = true + if (!top.type) this.needsBlock = true + } else if (!dom.firstChild) { + this.leafFallback(dom, marks) + break out + } + let innerMarks = rule && rule.skip ? marks : this.readStyles(dom, marks) + if (innerMarks) this.addAll(dom, innerMarks) + if (sync) this.sync(top) + this.needsBlock = oldNeedsBlock + } else { + let innerMarks = this.readStyles(dom, marks) + if (innerMarks) + this.addElementByRule(dom, rule as TagParseRule, innerMarks, rule!.consuming === false ? ruleID : undefined) + } + this.localPreserveWS = outerWS + } + + // Called for leaf DOM nodes that would otherwise be ignored + leafFallback(dom: DOMNode, marks: readonly Mark[]) { + if (dom.nodeName == "BR" && this.top.type && this.top.type.inlineContent) + this.addTextNode(dom.ownerDocument!.createTextNode("\n"), marks) + } + + // Called for ignored nodes + ignoreFallback(dom: DOMNode, marks: readonly Mark[]) { + // Ignored BR nodes should at least create an inline context + if (dom.nodeName == "BR" && (!this.top.type || !this.top.type.inlineContent)) + this.findPlace(this.parser.schema.text("-"), marks, true) + } + + // Run any style parser associated with the node's styles. Either + // return an updated array of marks, or null to indicate some of the + // styles had a rule with `ignore` set. + readStyles(dom: HTMLElement, marks: readonly Mark[]) { + let styles = dom.style + // Because many properties will only show up in 'normalized' form + // in `style.item` (i.e. text-decoration becomes + // text-decoration-line, text-decoration-color, etc), we directly + // query the styles mentioned in our rules instead of iterating + // over the items. + if (styles && styles.length) for (let i = 0; i < this.parser.matchedStyles.length; i++) { + let name = this.parser.matchedStyles[i], value = styles.getPropertyValue(name) + if (value) for (let after: StyleParseRule | undefined = undefined;;) { + let rule = this.parser.matchStyle(name, value, this, after) + if (!rule) break + if (rule.ignore) return null + if (rule.clearMark) + marks = marks.filter(m => !rule!.clearMark!(m)) + else + marks = marks.concat(this.parser.schema.marks[rule.mark!].create(rule.attrs)) + if (rule.consuming === false) after = rule + else break + } + } + return marks + } + + // Look up a handler for the given node. If none are found, return + // false. Otherwise, apply it, use its return value to drive the way + // the node's content is wrapped, and return true. + addElementByRule(dom: HTMLElement, rule: TagParseRule, marks: readonly Mark[], continueAfter?: TagParseRule) { + let sync, nodeType + if (rule.node) { + nodeType = this.parser.schema.nodes[rule.node] + if (!nodeType.isLeaf) { + let inner = this.enter(nodeType, rule.attrs || null, marks, rule.preserveWhitespace) + if (inner) { + sync = true + marks = inner + } + } else if (!this.insertNode(nodeType.create(rule.attrs), marks, dom.nodeName == "BR")) { + this.leafFallback(dom, marks) + } + } else { + let markType = this.parser.schema.marks[rule.mark!] + marks = marks.concat(markType.create(rule.attrs)) + } + let startIn = this.top + + if (nodeType && nodeType.isLeaf) { + this.findInside(dom) + } else if (continueAfter) { + this.addElement(dom, marks, continueAfter) + } else if (rule.getContent) { + this.findInside(dom) + rule.getContent(dom, this.parser.schema).forEach(node => this.insertNode(node, marks, false)) + } else { + let contentDOM = dom + if (typeof rule.contentElement == "string") contentDOM = dom.querySelector(rule.contentElement)! + else if (typeof rule.contentElement == "function") contentDOM = rule.contentElement(dom) + else if (rule.contentElement) contentDOM = rule.contentElement + this.findAround(dom, contentDOM, true) + this.addAll(contentDOM, marks) + this.findAround(dom, contentDOM, false) + } + if (sync && this.sync(startIn)) this.open-- + } + + // Add all child nodes between `startIndex` and `endIndex` (or the + // whole node, if not given). If `sync` is passed, use it to + // synchronize after every block element. + addAll(parent: DOMNode, marks: readonly Mark[], startIndex?: number, endIndex?: number) { + let index = startIndex || 0 + for (let dom = startIndex ? parent.childNodes[startIndex] : parent.firstChild, + end = endIndex == null ? null : parent.childNodes[endIndex]; + dom != end; dom = dom!.nextSibling, ++index) { + this.findAtPoint(parent, index) + this.addDOM(dom!, marks) + } + this.findAtPoint(parent, index) + } + + // Try to find a way to fit the given node type into the current + // context. May add intermediate wrappers and/or leave non-solid + // nodes that we're in. + findPlace(node: Node, marks: readonly Mark[], cautious: boolean) { + let route, sync: NodeContext | undefined + for (let depth = this.open, penalty = 0; depth >= 0; depth--) { + let cx = this.nodes[depth] + let found = cx.findWrapping(node) + if (found && (!route || route.length > found.length + penalty)) { + route = found + sync = cx + if (!found.length) break + } + if (cx.solid) { + if (cautious) break + penalty += 2 + } + } + if (!route) return null + this.sync(sync!) + for (let i = 0; i < route.length; i++) + marks = this.enterInner(route[i], null, marks, false) + return marks + } + + // Try to insert the given node, adjusting the context when needed. + insertNode(node: Node, marks: readonly Mark[], cautious: boolean) { + if (node.isInline && this.needsBlock && !this.top.type) { + let block = this.textblockFromContext() + if (block) marks = this.enterInner(block, null, marks) + } + let innerMarks = this.findPlace(node, marks, cautious) + if (innerMarks) { + this.closeExtra() + let top = this.top + if (top.match) top.match = top.match.matchType(node.type) + let nodeMarks = Mark.none + for (let m of innerMarks.concat(node.marks)) + if (top.type ? top.type.allowsMarkType(m.type) : markMayApply(m.type, node.type)) + nodeMarks = m.addToSet(nodeMarks) + top.content.push(node.mark(nodeMarks)) + return true + } + return false + } + + // Try to start a node of the given type, adjusting the context when + // necessary. + enter(type: NodeType, attrs: Attrs | null, marks: readonly Mark[], preserveWS?: boolean | "full") { + let innerMarks = this.findPlace(type.create(attrs), marks, false) + if (innerMarks) innerMarks = this.enterInner(type, attrs, marks, true, preserveWS) + return innerMarks + } + + // Open a node of the given type + enterInner(type: NodeType, attrs: Attrs | null, marks: readonly Mark[], + solid: boolean = false, preserveWS?: boolean | "full") { + this.closeExtra() + let top = this.top + top.match = top.match && top.match.matchType(type) + let options = wsOptionsFor(type, preserveWS, top.options) + if ((top.options & OPT_OPEN_LEFT) && top.content.length == 0) options |= OPT_OPEN_LEFT + let applyMarks = Mark.none + marks = marks.filter(m => { + if (top.type ? top.type.allowsMarkType(m.type) : markMayApply(m.type, type)) { + applyMarks = m.addToSet(applyMarks) + return false + } + return true + }) + this.nodes.push(new NodeContext(type, attrs, applyMarks, solid, null, options)) + this.open++ + return marks + } + + // Make sure all nodes above this.open are finished and added to + // their parents + closeExtra(openEnd = false) { + let i = this.nodes.length - 1 + if (i > this.open) { + for (; i > this.open; i--) this.nodes[i - 1].content.push(this.nodes[i].finish(openEnd) as Node) + this.nodes.length = this.open + 1 + } + } + + finish() { + this.open = 0 + this.closeExtra(this.isOpen) + return this.nodes[0].finish(!!(this.isOpen || this.options.topOpen)) + } + + sync(to: NodeContext) { + for (let i = this.open; i >= 0; i--) { + if (this.nodes[i] == to) { + this.open = i + return true + } else if (this.localPreserveWS) { + this.nodes[i].options |= OPT_PRESERVE_WS + } + } + return false + } + + get currentPos() { + this.closeExtra() + let pos = 0 + for (let i = this.open; i >= 0; i--) { + let content = this.nodes[i].content + for (let j = content.length - 1; j >= 0; j--) + pos += content[j].nodeSize + if (i) pos++ + } + return pos + } + + findAtPoint(parent: DOMNode, offset: number) { + if (this.find) for (let i = 0; i < this.find.length; i++) { + if (this.find[i].node == parent && this.find[i].offset == offset) + this.find[i].pos = this.currentPos + } + } + + findInside(parent: DOMNode) { + if (this.find) for (let i = 0; i < this.find.length; i++) { + if (this.find[i].pos == null && parent.nodeType == 1 && parent.contains(this.find[i].node)) + this.find[i].pos = this.currentPos + } + } + + findAround(parent: DOMNode, content: DOMNode, before: boolean) { + if (parent != content && this.find) for (let i = 0; i < this.find.length; i++) { + if (this.find[i].pos == null && parent.nodeType == 1 && parent.contains(this.find[i].node)) { + let pos = content.compareDocumentPosition(this.find[i].node) + if (pos & (before ? 2 : 4)) + this.find[i].pos = this.currentPos + } + } + } + + findInText(textNode: Text) { + if (this.find) for (let i = 0; i < this.find.length; i++) { + if (this.find[i].node == textNode) + this.find[i].pos = this.currentPos - (textNode.nodeValue!.length - this.find[i].offset) + } + } + + // Determines whether the given context string matches this context. + matchesContext(context: string) { + if (context.indexOf("|") > -1) + return context.split(/\s*\|\s*/).some(this.matchesContext, this) + + let parts = context.split("/") + let option = this.options.context + let useRoot = !this.isOpen && (!option || option.parent.type == this.nodes[0].type) + let minDepth = -(option ? option.depth + 1 : 0) + (useRoot ? 0 : 1) + let match = (i: number, depth: number) => { + for (; i >= 0; i--) { + let part = parts[i] + if (part == "") { + if (i == parts.length - 1 || i == 0) continue + for (; depth >= minDepth; depth--) + if (match(i - 1, depth)) return true + return false + } else { + let next = depth > 0 || (depth == 0 && useRoot) ? this.nodes[depth].type + : option && depth >= minDepth ? option.node(depth - minDepth).type + : null + if (!next || (next.name != part && !next.isInGroup(part))) + return false + depth-- + } + } + return true + } + return match(parts.length - 1, this.open) + } + + textblockFromContext() { + let $context = this.options.context + if ($context) for (let d = $context.depth; d >= 0; d--) { + let deflt = $context.node(d).contentMatchAt($context.indexAfter(d)).defaultType + if (deflt && deflt.isTextblock && deflt.defaultAttrs) return deflt + } + for (let name in this.parser.schema.nodes) { + let type = this.parser.schema.nodes[name] + if (type.isTextblock && type.defaultAttrs) return type + } + } +} + +// Kludge to work around directly nested list nodes produced by some +// tools and allowed by browsers to mean that the nested list is +// actually part of the list item above it. +function normalizeList(dom: DOMNode) { + for (let child = dom.firstChild, prevItem: ChildNode | null = null; child; child = child.nextSibling) { + let name = child.nodeType == 1 ? child.nodeName.toLowerCase() : null + if (name && listTags.hasOwnProperty(name) && prevItem) { + prevItem.appendChild(child) + child = prevItem + } else if (name == "li") { + prevItem = child + } else if (name) { + prevItem = null + } + } +} + +// Apply a CSS selector. +function matches(dom: any, selector: string): boolean { + return (dom.matches || dom.msMatchesSelector || dom.webkitMatchesSelector || dom.mozMatchesSelector).call(dom, selector) +} + +function copy(obj: {[prop: string]: any}) { + let copy: {[prop: string]: any} = {} + for (let prop in obj) copy[prop] = obj[prop] + return copy +} + +// Used when finding a mark at the top level of a fragment parse. +// Checks whether it would be reasonable to apply a given mark type to +// a given node, by looking at the way the mark occurs in the schema. +function markMayApply(markType: MarkType, nodeType: NodeType) { + let nodes = nodeType.schema.nodes + for (let name in nodes) { + let parent = nodes[name] + if (!parent.allowsMarkType(markType)) continue + let seen: ContentMatch[] = [], scan = (match: ContentMatch) => { + seen.push(match) + for (let i = 0; i < match.edgeCount; i++) { + let {type, next} = match.edge(i) + if (type == nodeType) return true + if (seen.indexOf(next) < 0 && scan(next)) return true + } + } + if (scan(parent.contentMatch)) return true + } +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/index.ts b/third_party/js/prosemirror/prosemirror-model/src/index.ts @@ -0,0 +1,11 @@ +export {Node} from "./node" +export {ResolvedPos, NodeRange} from "./resolvedpos" +export {Fragment} from "./fragment" +export {Slice, ReplaceError} from "./replace" +export {Mark} from "./mark" + +export {Schema, NodeType, Attrs, MarkType, NodeSpec, MarkSpec, AttributeSpec, SchemaSpec} from "./schema" +export {ContentMatch} from "./content" + +export {DOMParser, GenericParseRule, TagParseRule, StyleParseRule, ParseRule, ParseOptions} from "./from_dom" +export {DOMSerializer, DOMOutputSpec} from "./to_dom" diff --git a/third_party/js/prosemirror/prosemirror-model/src/mark.ts b/third_party/js/prosemirror/prosemirror-model/src/mark.ts @@ -0,0 +1,111 @@ +import {compareDeep} from "./comparedeep" +import {Attrs, MarkType, Schema} from "./schema" + +/// A mark is a piece of information that can be attached to a node, +/// such as it being emphasized, in code font, or a link. It has a +/// type and optionally a set of attributes that provide further +/// information (such as the target of the link). Marks are created +/// through a `Schema`, which controls which types exist and which +/// attributes they have. +export class Mark { + /// @internal + constructor( + /// The type of this mark. + readonly type: MarkType, + /// The attributes associated with this mark. + readonly attrs: Attrs + ) {} + + /// Given a set of marks, create a new set which contains this one as + /// well, in the right position. If this mark is already in the set, + /// the set itself is returned. If any marks that are set to be + /// [exclusive](#model.MarkSpec.excludes) with this mark are present, + /// those are replaced by this one. + addToSet(set: readonly Mark[]): readonly Mark[] { + let copy, placed = false + for (let i = 0; i < set.length; i++) { + let other = set[i] + if (this.eq(other)) return set + if (this.type.excludes(other.type)) { + if (!copy) copy = set.slice(0, i) + } else if (other.type.excludes(this.type)) { + return set + } else { + if (!placed && other.type.rank > this.type.rank) { + if (!copy) copy = set.slice(0, i) + copy.push(this) + placed = true + } + if (copy) copy.push(other) + } + } + if (!copy) copy = set.slice() + if (!placed) copy.push(this) + return copy + } + + /// Remove this mark from the given set, returning a new set. If this + /// mark is not in the set, the set itself is returned. + removeFromSet(set: readonly Mark[]): readonly Mark[] { + for (let i = 0; i < set.length; i++) + if (this.eq(set[i])) + return set.slice(0, i).concat(set.slice(i + 1)) + return set + } + + /// Test whether this mark is in the given set of marks. + isInSet(set: readonly Mark[]) { + for (let i = 0; i < set.length; i++) + if (this.eq(set[i])) return true + return false + } + + /// Test whether this mark has the same type and attributes as + /// another mark. + eq(other: Mark) { + return this == other || + (this.type == other.type && compareDeep(this.attrs, other.attrs)) + } + + /// Convert this mark to a JSON-serializeable representation. + toJSON(): any { + let obj: any = {type: this.type.name} + for (let _ in this.attrs) { + obj.attrs = this.attrs + break + } + return obj + } + + /// Deserialize a mark from JSON. + static fromJSON(schema: Schema, json: any) { + if (!json) throw new RangeError("Invalid input for Mark.fromJSON") + let type = schema.marks[json.type] + if (!type) throw new RangeError(`There is no mark type ${json.type} in this schema`) + let mark = type.create(json.attrs) + type.checkAttrs(mark.attrs) + return mark + } + + /// Test whether two sets of marks are identical. + static sameSet(a: readonly Mark[], b: readonly Mark[]) { + if (a == b) return true + if (a.length != b.length) return false + for (let i = 0; i < a.length; i++) + if (!a[i].eq(b[i])) return false + return true + } + + /// Create a properly sorted mark set from null, a single mark, or an + /// unsorted array of marks. + static setFrom(marks?: Mark | readonly Mark[] | null): readonly Mark[] { + if (!marks || Array.isArray(marks) && marks.length == 0) return Mark.none + if (marks instanceof Mark) return [marks] + let copy = marks.slice() + copy.sort((a, b) => a.type.rank - b.type.rank) + return copy + } + + /// The empty set of marks. + static none: readonly Mark[] = [] +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/node.ts b/third_party/js/prosemirror/prosemirror-model/src/node.ts @@ -0,0 +1,402 @@ +import {Fragment} from "./fragment" +import {Mark} from "./mark" +import {Schema, NodeType, Attrs, MarkType} from "./schema" +import {Slice, replace} from "./replace" +import {ResolvedPos} from "./resolvedpos" +import {compareDeep} from "./comparedeep" + +const emptyAttrs: Attrs = Object.create(null) + +/// This class represents a node in the tree that makes up a +/// ProseMirror document. So a document is an instance of `Node`, with +/// children that are also instances of `Node`. +/// +/// Nodes are persistent data structures. Instead of changing them, you +/// create new ones with the content you want. Old ones keep pointing +/// at the old document shape. This is made cheaper by sharing +/// structure between the old and new data as much as possible, which a +/// tree shape like this (without back pointers) makes easy. +/// +/// **Do not** directly mutate the properties of a `Node` object. See +/// [the guide](/docs/guide/#doc) for more information. +export class Node { + /// @internal + constructor( + /// The type of node that this is. + readonly type: NodeType, + /// An object mapping attribute names to values. The kind of + /// attributes allowed and required are + /// [determined](#model.NodeSpec.attrs) by the node type. + readonly attrs: Attrs, + // A fragment holding the node's children. + content?: Fragment | null, + /// The marks (things like whether it is emphasized or part of a + /// link) applied to this node. + readonly marks = Mark.none + ) { + this.content = content || Fragment.empty + } + + /// A container holding the node's children. + readonly content: Fragment + + /// The array of this node's child nodes. + get children() { return this.content.content } + + /// For text nodes, this contains the node's text content. + readonly text: string | undefined + + /// The size of this node, as defined by the integer-based [indexing + /// scheme](/docs/guide/#doc.indexing). For text nodes, this is the + /// amount of characters. For other leaf nodes, it is one. For + /// non-leaf nodes, it is the size of the content plus two (the + /// start and end token). + get nodeSize(): number { return this.isLeaf ? 1 : 2 + this.content.size } + + /// The number of children that the node has. + get childCount() { return this.content.childCount } + + /// Get the child node at the given index. Raises an error when the + /// index is out of range. + child(index: number) { return this.content.child(index) } + + /// Get the child node at the given index, if it exists. + maybeChild(index: number) { return this.content.maybeChild(index) } + + /// Call `f` for every child node, passing the node, its offset + /// into this parent node, and its index. + forEach(f: (node: Node, offset: number, index: number) => void) { this.content.forEach(f) } + + /// Invoke a callback for all descendant nodes recursively between + /// the given two positions that are relative to start of this + /// node's content. The callback is invoked with the node, its + /// position relative to the original node (method receiver), + /// its parent node, and its child index. When the callback returns + /// false for a given node, that node's children will not be + /// recursed over. The last parameter can be used to specify a + /// starting position to count from. + nodesBetween(from: number, to: number, + f: (node: Node, pos: number, parent: Node | null, index: number) => void | boolean, + startPos = 0) { + this.content.nodesBetween(from, to, f, startPos, this) + } + + /// Call the given callback for every descendant node. Doesn't + /// descend into a node when the callback returns `false`. + descendants(f: (node: Node, pos: number, parent: Node | null, index: number) => void | boolean) { + this.nodesBetween(0, this.content.size, f) + } + + /// Concatenates all the text nodes found in this fragment and its + /// children. + get textContent() { + return (this.isLeaf && this.type.spec.leafText) + ? this.type.spec.leafText(this) + : this.textBetween(0, this.content.size, "") + } + + /// Get all text between positions `from` and `to`. When + /// `blockSeparator` is given, it will be inserted to separate text + /// from different block nodes. If `leafText` is given, it'll be + /// inserted for every non-text leaf node encountered, otherwise + /// [`leafText`](#model.NodeSpec.leafText) will be used. + textBetween(from: number, to: number, blockSeparator?: string | null, + leafText?: null | string | ((leafNode: Node) => string)) { + return this.content.textBetween(from, to, blockSeparator, leafText) + } + + /// Returns this node's first child, or `null` if there are no + /// children. + get firstChild(): Node | null { return this.content.firstChild } + + /// Returns this node's last child, or `null` if there are no + /// children. + get lastChild(): Node | null { return this.content.lastChild } + + /// Test whether two nodes represent the same piece of document. + eq(other: Node) { + return this == other || (this.sameMarkup(other) && this.content.eq(other.content)) + } + + /// Compare the markup (type, attributes, and marks) of this node to + /// those of another. Returns `true` if both have the same markup. + sameMarkup(other: Node) { + return this.hasMarkup(other.type, other.attrs, other.marks) + } + + /// Check whether this node's markup correspond to the given type, + /// attributes, and marks. + hasMarkup(type: NodeType, attrs?: Attrs | null, marks?: readonly Mark[]): boolean { + return this.type == type && + compareDeep(this.attrs, attrs || type.defaultAttrs || emptyAttrs) && + Mark.sameSet(this.marks, marks || Mark.none) + } + + /// Create a new node with the same markup as this node, containing + /// the given content (or empty, if no content is given). + copy(content: Fragment | null = null): Node { + if (content == this.content) return this + return new Node(this.type, this.attrs, content, this.marks) + } + + /// Create a copy of this node, with the given set of marks instead + /// of the node's own marks. + mark(marks: readonly Mark[]): Node { + return marks == this.marks ? this : new Node(this.type, this.attrs, this.content, marks) + } + + /// Create a copy of this node with only the content between the + /// given positions. If `to` is not given, it defaults to the end of + /// the node. + cut(from: number, to: number = this.content.size): Node { + if (from == 0 && to == this.content.size) return this + return this.copy(this.content.cut(from, to)) + } + + /// Cut out the part of the document between the given positions, and + /// return it as a `Slice` object. + slice(from: number, to: number = this.content.size, includeParents = false) { + if (from == to) return Slice.empty + + let $from = this.resolve(from), $to = this.resolve(to) + let depth = includeParents ? 0 : $from.sharedDepth(to) + let start = $from.start(depth), node = $from.node(depth) + let content = node.content.cut($from.pos - start, $to.pos - start) + return new Slice(content, $from.depth - depth, $to.depth - depth) + } + + /// Replace the part of the document between the given positions with + /// the given slice. The slice must 'fit', meaning its open sides + /// must be able to connect to the surrounding content, and its + /// content nodes must be valid children for the node they are placed + /// into. If any of this is violated, an error of type + /// [`ReplaceError`](#model.ReplaceError) is thrown. + replace(from: number, to: number, slice: Slice) { + return replace(this.resolve(from), this.resolve(to), slice) + } + + /// Find the node directly after the given position. + nodeAt(pos: number): Node | null { + for (let node: Node | null = this;;) { + let {index, offset} = node.content.findIndex(pos) + node = node.maybeChild(index) + if (!node) return null + if (offset == pos || node.isText) return node + pos -= offset + 1 + } + } + + /// Find the (direct) child node after the given offset, if any, + /// and return it along with its index and offset relative to this + /// node. + childAfter(pos: number): {node: Node | null, index: number, offset: number} { + let {index, offset} = this.content.findIndex(pos) + return {node: this.content.maybeChild(index), index, offset} + } + + /// Find the (direct) child node before the given offset, if any, + /// and return it along with its index and offset relative to this + /// node. + childBefore(pos: number): {node: Node | null, index: number, offset: number} { + if (pos == 0) return {node: null, index: 0, offset: 0} + let {index, offset} = this.content.findIndex(pos) + if (offset < pos) return {node: this.content.child(index), index, offset} + let node = this.content.child(index - 1) + return {node, index: index - 1, offset: offset - node.nodeSize} + } + + /// Resolve the given position in the document, returning an + /// [object](#model.ResolvedPos) with information about its context. + resolve(pos: number) { return ResolvedPos.resolveCached(this, pos) } + + /// @internal + resolveNoCache(pos: number) { return ResolvedPos.resolve(this, pos) } + + /// Test whether a given mark or mark type occurs in this document + /// between the two given positions. + rangeHasMark(from: number, to: number, type: Mark | MarkType): boolean { + let found = false + if (to > from) this.nodesBetween(from, to, node => { + if (type.isInSet(node.marks)) found = true + return !found + }) + return found + } + + /// True when this is a block (non-inline node) + get isBlock() { return this.type.isBlock } + + /// True when this is a textblock node, a block node with inline + /// content. + get isTextblock() { return this.type.isTextblock } + + /// True when this node allows inline content. + get inlineContent() { return this.type.inlineContent } + + /// True when this is an inline node (a text node or a node that can + /// appear among text). + get isInline() { return this.type.isInline } + + /// True when this is a text node. + get isText() { return this.type.isText } + + /// True when this is a leaf node. + get isLeaf() { return this.type.isLeaf } + + /// True when this is an atom, i.e. when it does not have directly + /// editable content. This is usually the same as `isLeaf`, but can + /// be configured with the [`atom` property](#model.NodeSpec.atom) + /// on a node's spec (typically used when the node is displayed as + /// an uneditable [node view](#view.NodeView)). + get isAtom() { return this.type.isAtom } + + /// Return a string representation of this node for debugging + /// purposes. + toString(): string { + if (this.type.spec.toDebugString) return this.type.spec.toDebugString(this) + let name = this.type.name + if (this.content.size) + name += "(" + this.content.toStringInner() + ")" + return wrapMarks(this.marks, name) + } + + /// Get the content match in this node at the given index. + contentMatchAt(index: number) { + let match = this.type.contentMatch.matchFragment(this.content, 0, index) + if (!match) throw new Error("Called contentMatchAt on a node with invalid content") + return match + } + + /// Test whether replacing the range between `from` and `to` (by + /// child index) with the given replacement fragment (which defaults + /// to the empty fragment) would leave the node's content valid. You + /// can optionally pass `start` and `end` indices into the + /// replacement fragment. + canReplace(from: number, to: number, replacement = Fragment.empty, start = 0, end = replacement.childCount) { + let one = this.contentMatchAt(from).matchFragment(replacement, start, end) + let two = one && one.matchFragment(this.content, to) + if (!two || !two.validEnd) return false + for (let i = start; i < end; i++) if (!this.type.allowsMarks(replacement.child(i).marks)) return false + return true + } + + /// Test whether replacing the range `from` to `to` (by index) with + /// a node of the given type would leave the node's content valid. + canReplaceWith(from: number, to: number, type: NodeType, marks?: readonly Mark[]) { + if (marks && !this.type.allowsMarks(marks)) return false + let start = this.contentMatchAt(from).matchType(type) + let end = start && start.matchFragment(this.content, to) + return end ? end.validEnd : false + } + + /// Test whether the given node's content could be appended to this + /// node. If that node is empty, this will only return true if there + /// is at least one node type that can appear in both nodes (to avoid + /// merging completely incompatible nodes). + canAppend(other: Node) { + if (other.content.size) return this.canReplace(this.childCount, this.childCount, other.content) + else return this.type.compatibleContent(other.type) + } + + /// Check whether this node and its descendants conform to the + /// schema, and raise an exception when they do not. + check() { + this.type.checkContent(this.content) + this.type.checkAttrs(this.attrs) + let copy = Mark.none + for (let i = 0; i < this.marks.length; i++) { + let mark = this.marks[i] + mark.type.checkAttrs(mark.attrs) + copy = mark.addToSet(copy) + } + if (!Mark.sameSet(copy, this.marks)) + throw new RangeError(`Invalid collection of marks for node ${this.type.name}: ${this.marks.map(m => m.type.name)}`) + this.content.forEach(node => node.check()) + } + + /// Return a JSON-serializeable representation of this node. + toJSON(): any { + let obj: any = {type: this.type.name} + for (let _ in this.attrs) { + obj.attrs = this.attrs + break + } + if (this.content.size) + obj.content = this.content.toJSON() + if (this.marks.length) + obj.marks = this.marks.map(n => n.toJSON()) + return obj + } + + /// Deserialize a node from its JSON representation. + static fromJSON(schema: Schema, json: any): Node { + if (!json) throw new RangeError("Invalid input for Node.fromJSON") + let marks: Mark[] | undefined = undefined + if (json.marks) { + if (!Array.isArray(json.marks)) throw new RangeError("Invalid mark data for Node.fromJSON") + marks = json.marks.map(schema.markFromJSON) + } + if (json.type == "text") { + if (typeof json.text != "string") throw new RangeError("Invalid text node in JSON") + return schema.text(json.text, marks) + } + let content = Fragment.fromJSON(schema, json.content) + let node = schema.nodeType(json.type).create(json.attrs, content, marks) + node.type.checkAttrs(node.attrs) + return node + } +} + +;(Node.prototype as any).text = undefined + +export class TextNode extends Node { + readonly text: string + + /// @internal + constructor(type: NodeType, attrs: Attrs, content: string, marks?: readonly Mark[]) { + super(type, attrs, null, marks) + if (!content) throw new RangeError("Empty text nodes are not allowed") + this.text = content + } + + toString() { + if (this.type.spec.toDebugString) return this.type.spec.toDebugString(this) + return wrapMarks(this.marks, JSON.stringify(this.text)) + } + + get textContent() { return this.text } + + textBetween(from: number, to: number) { return this.text.slice(from, to) } + + get nodeSize() { return this.text.length } + + mark(marks: readonly Mark[]) { + return marks == this.marks ? this : new TextNode(this.type, this.attrs, this.text, marks) + } + + withText(text: string) { + if (text == this.text) return this + return new TextNode(this.type, this.attrs, text, this.marks) + } + + cut(from = 0, to = this.text.length) { + if (from == 0 && to == this.text.length) return this + return this.withText(this.text.slice(from, to)) + } + + eq(other: Node) { + return this.sameMarkup(other) && this.text == other.text + } + + toJSON() { + let base = super.toJSON() + base.text = this.text + return base + } +} + +function wrapMarks(marks: readonly Mark[], str: string) { + for (let i = marks.length - 1; i >= 0; i--) + str = marks[i].type.name + "(" + str + ")" + return str +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/replace.ts b/third_party/js/prosemirror/prosemirror-model/src/replace.ts @@ -0,0 +1,225 @@ +import {Fragment} from "./fragment" +import {Schema} from "./schema" +import {Node, TextNode} from "./node" +import {ResolvedPos} from "./resolvedpos" + +/// Error type raised by [`Node.replace`](#model.Node.replace) when +/// given an invalid replacement. +export class ReplaceError extends Error {} +/* +ReplaceError = function(this: any, message: string) { + let err = Error.call(this, message) + ;(err as any).__proto__ = ReplaceError.prototype + return err +} as any + +ReplaceError.prototype = Object.create(Error.prototype) +ReplaceError.prototype.constructor = ReplaceError +ReplaceError.prototype.name = "ReplaceError" +*/ + +/// A slice represents a piece cut out of a larger document. It +/// stores not only a fragment, but also the depth up to which nodes on +/// both side are ‘open’ (cut through). +export class Slice { + /// Create a slice. When specifying a non-zero open depth, you must + /// make sure that there are nodes of at least that depth at the + /// appropriate side of the fragment—i.e. if the fragment is an + /// empty paragraph node, `openStart` and `openEnd` can't be greater + /// than 1. + /// + /// It is not necessary for the content of open nodes to conform to + /// the schema's content constraints, though it should be a valid + /// start/end/middle for such a node, depending on which sides are + /// open. + constructor( + /// The slice's content. + readonly content: Fragment, + /// The open depth at the start of the fragment. + readonly openStart: number, + /// The open depth at the end. + readonly openEnd: number + ) {} + + /// The size this slice would add when inserted into a document. + get size(): number { + return this.content.size - this.openStart - this.openEnd + } + + /// @internal + insertAt(pos: number, fragment: Fragment) { + let content = insertInto(this.content, pos + this.openStart, fragment) + return content && new Slice(content, this.openStart, this.openEnd) + } + + /// @internal + removeBetween(from: number, to: number) { + return new Slice(removeRange(this.content, from + this.openStart, to + this.openStart), this.openStart, this.openEnd) + } + + /// Tests whether this slice is equal to another slice. + eq(other: Slice): boolean { + return this.content.eq(other.content) && this.openStart == other.openStart && this.openEnd == other.openEnd + } + + /// @internal + toString() { + return this.content + "(" + this.openStart + "," + this.openEnd + ")" + } + + /// Convert a slice to a JSON-serializable representation. + toJSON(): any { + if (!this.content.size) return null + let json: any = {content: this.content.toJSON()} + if (this.openStart > 0) json.openStart = this.openStart + if (this.openEnd > 0) json.openEnd = this.openEnd + return json + } + + /// Deserialize a slice from its JSON representation. + static fromJSON(schema: Schema, json: any): Slice { + if (!json) return Slice.empty + let openStart = json.openStart || 0, openEnd = json.openEnd || 0 + if (typeof openStart != "number" || typeof openEnd != "number") + throw new RangeError("Invalid input for Slice.fromJSON") + return new Slice(Fragment.fromJSON(schema, json.content), openStart, openEnd) + } + + /// Create a slice from a fragment by taking the maximum possible + /// open value on both side of the fragment. + static maxOpen(fragment: Fragment, openIsolating = true) { + let openStart = 0, openEnd = 0 + for (let n = fragment.firstChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.firstChild) openStart++ + for (let n = fragment.lastChild; n && !n.isLeaf && (openIsolating || !n.type.spec.isolating); n = n.lastChild) openEnd++ + return new Slice(fragment, openStart, openEnd) + } + + /// The empty slice. + static empty = new Slice(Fragment.empty, 0, 0) +} + +function removeRange(content: Fragment, from: number, to: number): Fragment { + let {index, offset} = content.findIndex(from), child = content.maybeChild(index) + let {index: indexTo, offset: offsetTo} = content.findIndex(to) + if (offset == from || child!.isText) { + if (offsetTo != to && !content.child(indexTo).isText) throw new RangeError("Removing non-flat range") + return content.cut(0, from).append(content.cut(to)) + } + if (index != indexTo) throw new RangeError("Removing non-flat range") + return content.replaceChild(index, child!.copy(removeRange(child!.content, from - offset - 1, to - offset - 1))) +} + +function insertInto(content: Fragment, dist: number, insert: Fragment, parent?: Node | null): Fragment | null { + let {index, offset} = content.findIndex(dist), child = content.maybeChild(index) + if (offset == dist || child!.isText) { + if (parent && !parent.canReplace(index, index, insert)) return null + return content.cut(0, dist).append(insert).append(content.cut(dist)) + } + let inner = insertInto(child!.content, dist - offset - 1, insert, child) + return inner && content.replaceChild(index, child!.copy(inner)) +} + +export function replace($from: ResolvedPos, $to: ResolvedPos, slice: Slice) { + if (slice.openStart > $from.depth) + throw new ReplaceError("Inserted content deeper than insertion position") + if ($from.depth - slice.openStart != $to.depth - slice.openEnd) + throw new ReplaceError("Inconsistent open depths") + return replaceOuter($from, $to, slice, 0) +} + +function replaceOuter($from: ResolvedPos, $to: ResolvedPos, slice: Slice, depth: number): Node { + let index = $from.index(depth), node = $from.node(depth) + if (index == $to.index(depth) && depth < $from.depth - slice.openStart) { + let inner = replaceOuter($from, $to, slice, depth + 1) + return node.copy(node.content.replaceChild(index, inner)) + } else if (!slice.content.size) { + return close(node, replaceTwoWay($from, $to, depth)) + } else if (!slice.openStart && !slice.openEnd && $from.depth == depth && $to.depth == depth) { // Simple, flat case + let parent = $from.parent, content = parent.content + return close(parent, content.cut(0, $from.parentOffset).append(slice.content).append(content.cut($to.parentOffset))) + } else { + let {start, end} = prepareSliceForReplace(slice, $from) + return close(node, replaceThreeWay($from, start, end, $to, depth)) + } +} + +function checkJoin(main: Node, sub: Node) { + if (!sub.type.compatibleContent(main.type)) + throw new ReplaceError("Cannot join " + sub.type.name + " onto " + main.type.name) +} + +function joinable($before: ResolvedPos, $after: ResolvedPos, depth: number) { + let node = $before.node(depth) + checkJoin(node, $after.node(depth)) + return node +} + +function addNode(child: Node, target: Node[]) { + let last = target.length - 1 + if (last >= 0 && child.isText && child.sameMarkup(target[last])) + target[last] = (child as TextNode).withText(target[last].text! + child.text!) + else + target.push(child) +} + +function addRange($start: ResolvedPos | null, $end: ResolvedPos | null, depth: number, target: Node[]) { + let node = ($end || $start)!.node(depth) + let startIndex = 0, endIndex = $end ? $end.index(depth) : node.childCount + if ($start) { + startIndex = $start.index(depth) + if ($start.depth > depth) { + startIndex++ + } else if ($start.textOffset) { + addNode($start.nodeAfter!, target) + startIndex++ + } + } + for (let i = startIndex; i < endIndex; i++) addNode(node.child(i), target) + if ($end && $end.depth == depth && $end.textOffset) + addNode($end.nodeBefore!, target) +} + +function close(node: Node, content: Fragment) { + node.type.checkContent(content) + return node.copy(content) +} + +function replaceThreeWay($from: ResolvedPos, $start: ResolvedPos, $end: ResolvedPos, $to: ResolvedPos, depth: number) { + let openStart = $from.depth > depth && joinable($from, $start, depth + 1) + let openEnd = $to.depth > depth && joinable($end, $to, depth + 1) + + let content: Node[] = [] + addRange(null, $from, depth, content) + if (openStart && openEnd && $start.index(depth) == $end.index(depth)) { + checkJoin(openStart, openEnd) + addNode(close(openStart, replaceThreeWay($from, $start, $end, $to, depth + 1)), content) + } else { + if (openStart) + addNode(close(openStart, replaceTwoWay($from, $start, depth + 1)), content) + addRange($start, $end, depth, content) + if (openEnd) + addNode(close(openEnd, replaceTwoWay($end, $to, depth + 1)), content) + } + addRange($to, null, depth, content) + return new Fragment(content) +} + +function replaceTwoWay($from: ResolvedPos, $to: ResolvedPos, depth: number) { + let content: Node[] = [] + addRange(null, $from, depth, content) + if ($from.depth > depth) { + let type = joinable($from, $to, depth + 1) + addNode(close(type, replaceTwoWay($from, $to, depth + 1)), content) + } + addRange($to, null, depth, content) + return new Fragment(content) +} + +function prepareSliceForReplace(slice: Slice, $along: ResolvedPos) { + let extra = $along.depth - slice.openStart, parent = $along.node(extra) + let node = parent.copy(slice.content) + for (let i = extra - 1; i >= 0; i--) + node = $along.node(i).copy(Fragment.from(node)) + return {start: node.resolveNoCache(slice.openStart + extra), + end: node.resolveNoCache(node.content.size - slice.openEnd - extra)} +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/resolvedpos.ts b/third_party/js/prosemirror/prosemirror-model/src/resolvedpos.ts @@ -0,0 +1,289 @@ +import {Mark} from "./mark" +import {Node} from "./node" + +/// You can [_resolve_](#model.Node.resolve) a position to get more +/// information about it. Objects of this class represent such a +/// resolved position, providing various pieces of context +/// information, and some helper methods. +/// +/// Throughout this interface, methods that take an optional `depth` +/// parameter will interpret undefined as `this.depth` and negative +/// numbers as `this.depth + value`. +export class ResolvedPos { + /// The number of levels the parent node is from the root. If this + /// position points directly into the root node, it is 0. If it + /// points into a top-level paragraph, 1, and so on. + depth: number + + /// @internal + constructor( + /// The position that was resolved. + readonly pos: number, + /// @internal + readonly path: any[], + /// The offset this position has into its parent node. + readonly parentOffset: number + ) { + this.depth = path.length / 3 - 1 + } + + /// @internal + resolveDepth(val: number | undefined | null) { + if (val == null) return this.depth + if (val < 0) return this.depth + val + return val + } + + /// The parent node that the position points into. Note that even if + /// a position points into a text node, that node is not considered + /// the parent—text nodes are ‘flat’ in this model, and have no content. + get parent() { return this.node(this.depth) } + + /// The root node in which the position was resolved. + get doc() { return this.node(0) } + + /// The ancestor node at the given level. `p.node(p.depth)` is the + /// same as `p.parent`. + node(depth?: number | null): Node { return this.path[this.resolveDepth(depth) * 3] } + + /// The index into the ancestor at the given level. If this points + /// at the 3rd node in the 2nd paragraph on the top level, for + /// example, `p.index(0)` is 1 and `p.index(1)` is 2. + index(depth?: number | null): number { return this.path[this.resolveDepth(depth) * 3 + 1] } + + /// The index pointing after this position into the ancestor at the + /// given level. + indexAfter(depth?: number | null): number { + depth = this.resolveDepth(depth) + return this.index(depth) + (depth == this.depth && !this.textOffset ? 0 : 1) + } + + /// The (absolute) position at the start of the node at the given + /// level. + start(depth?: number | null): number { + depth = this.resolveDepth(depth) + return depth == 0 ? 0 : this.path[depth * 3 - 1] + 1 + } + + /// The (absolute) position at the end of the node at the given + /// level. + end(depth?: number | null): number { + depth = this.resolveDepth(depth) + return this.start(depth) + this.node(depth).content.size + } + + /// The (absolute) position directly before the wrapping node at the + /// given level, or, when `depth` is `this.depth + 1`, the original + /// position. + before(depth?: number | null): number { + depth = this.resolveDepth(depth) + if (!depth) throw new RangeError("There is no position before the top-level node") + return depth == this.depth + 1 ? this.pos : this.path[depth * 3 - 1] + } + + /// The (absolute) position directly after the wrapping node at the + /// given level, or the original position when `depth` is `this.depth + 1`. + after(depth?: number | null): number { + depth = this.resolveDepth(depth) + if (!depth) throw new RangeError("There is no position after the top-level node") + return depth == this.depth + 1 ? this.pos : this.path[depth * 3 - 1] + this.path[depth * 3].nodeSize + } + + /// When this position points into a text node, this returns the + /// distance between the position and the start of the text node. + /// Will be zero for positions that point between nodes. + get textOffset(): number { return this.pos - this.path[this.path.length - 1] } + + /// Get the node directly after the position, if any. If the position + /// points into a text node, only the part of that node after the + /// position is returned. + get nodeAfter(): Node | null { + let parent = this.parent, index = this.index(this.depth) + if (index == parent.childCount) return null + let dOff = this.pos - this.path[this.path.length - 1], child = parent.child(index) + return dOff ? parent.child(index).cut(dOff) : child + } + + /// Get the node directly before the position, if any. If the + /// position points into a text node, only the part of that node + /// before the position is returned. + get nodeBefore(): Node | null { + let index = this.index(this.depth) + let dOff = this.pos - this.path[this.path.length - 1] + if (dOff) return this.parent.child(index).cut(0, dOff) + return index == 0 ? null : this.parent.child(index - 1) + } + + /// Get the position at the given index in the parent node at the + /// given depth (which defaults to `this.depth`). + posAtIndex(index: number, depth?: number | null): number { + depth = this.resolveDepth(depth) + let node = this.path[depth * 3], pos = depth == 0 ? 0 : this.path[depth * 3 - 1] + 1 + for (let i = 0; i < index; i++) pos += node.child(i).nodeSize + return pos + } + + /// Get the marks at this position, factoring in the surrounding + /// marks' [`inclusive`](#model.MarkSpec.inclusive) property. If the + /// position is at the start of a non-empty node, the marks of the + /// node after it (if any) are returned. + marks(): readonly Mark[] { + let parent = this.parent, index = this.index() + + // In an empty parent, return the empty array + if (parent.content.size == 0) return Mark.none + + // When inside a text node, just return the text node's marks + if (this.textOffset) return parent.child(index).marks + + let main = parent.maybeChild(index - 1), other = parent.maybeChild(index) + // If the `after` flag is true of there is no node before, make + // the node after this position the main reference. + if (!main) { let tmp = main; main = other; other = tmp } + + // Use all marks in the main node, except those that have + // `inclusive` set to false and are not present in the other node. + let marks = main!.marks + for (var i = 0; i < marks.length; i++) + if (marks[i].type.spec.inclusive === false && (!other || !marks[i].isInSet(other.marks))) + marks = marks[i--].removeFromSet(marks) + + return marks + } + + /// Get the marks after the current position, if any, except those + /// that are non-inclusive and not present at position `$end`. This + /// is mostly useful for getting the set of marks to preserve after a + /// deletion. Will return `null` if this position is at the end of + /// its parent node or its parent node isn't a textblock (in which + /// case no marks should be preserved). + marksAcross($end: ResolvedPos): readonly Mark[] | null { + let after = this.parent.maybeChild(this.index()) + if (!after || !after.isInline) return null + + let marks = after.marks, next = $end.parent.maybeChild($end.index()) + for (var i = 0; i < marks.length; i++) + if (marks[i].type.spec.inclusive === false && (!next || !marks[i].isInSet(next.marks))) + marks = marks[i--].removeFromSet(marks) + return marks + } + + /// The depth up to which this position and the given (non-resolved) + /// position share the same parent nodes. + sharedDepth(pos: number): number { + for (let depth = this.depth; depth > 0; depth--) + if (this.start(depth) <= pos && this.end(depth) >= pos) return depth + return 0 + } + + /// Returns a range based on the place where this position and the + /// given position diverge around block content. If both point into + /// the same textblock, for example, a range around that textblock + /// will be returned. If they point into different blocks, the range + /// around those blocks in their shared ancestor is returned. You can + /// pass in an optional predicate that will be called with a parent + /// node to see if a range into that parent is acceptable. + blockRange(other: ResolvedPos = this, pred?: (node: Node) => boolean): NodeRange | null { + if (other.pos < this.pos) return other.blockRange(this) + for (let d = this.depth - (this.parent.inlineContent || this.pos == other.pos ? 1 : 0); d >= 0; d--) + if (other.pos <= this.end(d) && (!pred || pred(this.node(d)))) + return new NodeRange(this, other, d) + return null + } + + /// Query whether the given position shares the same parent node. + sameParent(other: ResolvedPos): boolean { + return this.pos - this.parentOffset == other.pos - other.parentOffset + } + + /// Return the greater of this and the given position. + max(other: ResolvedPos): ResolvedPos { + return other.pos > this.pos ? other : this + } + + /// Return the smaller of this and the given position. + min(other: ResolvedPos): ResolvedPos { + return other.pos < this.pos ? other : this + } + + /// @internal + toString() { + let str = "" + for (let i = 1; i <= this.depth; i++) + str += (str ? "/" : "") + this.node(i).type.name + "_" + this.index(i - 1) + return str + ":" + this.parentOffset + } + + /// @internal + static resolve(doc: Node, pos: number): ResolvedPos { + if (!(pos >= 0 && pos <= doc.content.size)) throw new RangeError("Position " + pos + " out of range") + let path: Array<Node | number> = [] + let start = 0, parentOffset = pos + for (let node = doc;;) { + let {index, offset} = node.content.findIndex(parentOffset) + let rem = parentOffset - offset + path.push(node, index, start + offset) + if (!rem) break + node = node.child(index) + if (node.isText) break + parentOffset = rem - 1 + start += offset + 1 + } + return new ResolvedPos(pos, path, parentOffset) + } + + /// @internal + static resolveCached(doc: Node, pos: number): ResolvedPos { + let cache = resolveCache.get(doc) + if (cache) { + for (let i = 0; i < cache.elts.length; i++) { + let elt = cache.elts[i] + if (elt.pos == pos) return elt + } + } else { + resolveCache.set(doc, cache = new ResolveCache) + } + let result = cache.elts[cache.i] = ResolvedPos.resolve(doc, pos) + cache.i = (cache.i + 1) % resolveCacheSize + return result + } +} + +class ResolveCache { + elts: ResolvedPos[] = [] + i = 0 +} + +const resolveCacheSize = 12, resolveCache = new WeakMap<Node, ResolveCache>() + +/// Represents a flat range of content, i.e. one that starts and +/// ends in the same node. +export class NodeRange { + /// Construct a node range. `$from` and `$to` should point into the + /// same node until at least the given `depth`, since a node range + /// denotes an adjacent set of nodes in a single parent node. + constructor( + /// A resolved position along the start of the content. May have a + /// `depth` greater than this object's `depth` property, since + /// these are the positions that were used to compute the range, + /// not re-resolved positions directly at its boundaries. + readonly $from: ResolvedPos, + /// A position along the end of the content. See + /// caveat for [`$from`](#model.NodeRange.$from). + readonly $to: ResolvedPos, + /// The depth of the node that this range points into. + readonly depth: number + ) {} + + /// The position at the start of the range. + get start() { return this.$from.before(this.depth + 1) } + /// The position at the end of the range. + get end() { return this.$to.after(this.depth + 1) } + + /// The parent node that the range points into. + get parent() { return this.$from.node(this.depth) } + /// The start index of the range in the parent node. + get startIndex() { return this.$from.index(this.depth) } + /// The end index of the range in the parent node. + get endIndex() { return this.$to.indexAfter(this.depth) } +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/schema.ts b/third_party/js/prosemirror/prosemirror-model/src/schema.ts @@ -0,0 +1,705 @@ +import OrderedMap from "orderedmap" + +import {Node, TextNode} from "./node" +import {Fragment} from "./fragment" +import {Mark} from "./mark" +import {ContentMatch} from "./content" +import {DOMOutputSpec} from "./to_dom" +import {ParseRule, TagParseRule} from "./from_dom" + +/// An object holding the attributes of a node. +export type Attrs = {readonly [attr: string]: any} + +// For node types where all attrs have a default value (or which don't +// have any attributes), build up a single reusable default attribute +// object, and use it for all nodes that don't specify specific +// attributes. +function defaultAttrs(attrs: {[name: string]: Attribute}) { + let defaults = Object.create(null) + for (let attrName in attrs) { + let attr = attrs[attrName] + if (!attr.hasDefault) return null + defaults[attrName] = attr.default + } + return defaults +} + +function computeAttrs(attrs: {[name: string]: Attribute}, value: Attrs | null) { + let built = Object.create(null) + for (let name in attrs) { + let given = value && value[name] + if (given === undefined) { + let attr = attrs[name] + if (attr.hasDefault) given = attr.default + else throw new RangeError("No value supplied for attribute " + name) + } + built[name] = given + } + return built +} + +export function checkAttrs(attrs: {[name: string]: Attribute}, values: Attrs, type: string, name: string) { + for (let name in values) + if (!(name in attrs)) throw new RangeError(`Unsupported attribute ${name} for ${type} of type ${name}`) + for (let name in attrs) { + let attr = attrs[name] + if (attr.validate) attr.validate(values[name]) + } +} + +function initAttrs(typeName: string, attrs?: {[name: string]: AttributeSpec}) { + let result: {[name: string]: Attribute} = Object.create(null) + if (attrs) for (let name in attrs) result[name] = new Attribute(typeName, name, attrs[name]) + return result +} + +/// Node types are objects allocated once per `Schema` and used to +/// [tag](#model.Node.type) `Node` instances. They contain information +/// about the node type, such as its name and what kind of node it +/// represents. +export class NodeType { + /// @internal + groups: readonly string[] + /// @internal + attrs: {[name: string]: Attribute} + /// @internal + defaultAttrs: Attrs + + /// @internal + constructor( + /// The name the node type has in this schema. + readonly name: string, + /// A link back to the `Schema` the node type belongs to. + readonly schema: Schema, + /// The spec that this type is based on + readonly spec: NodeSpec + ) { + this.groups = spec.group ? spec.group.split(" ") : [] + this.attrs = initAttrs(name, spec.attrs) + this.defaultAttrs = defaultAttrs(this.attrs) + + // Filled in later + ;(this as any).contentMatch = null + ;(this as any).inlineContent = null + + this.isBlock = !(spec.inline || name == "text") + this.isText = name == "text" + } + + /// True if this node type has inline content. + declare inlineContent: boolean + /// True if this is a block type + isBlock: boolean + /// True if this is the text node type. + isText: boolean + + /// True if this is an inline type. + get isInline() { return !this.isBlock } + + /// True if this is a textblock type, a block that contains inline + /// content. + get isTextblock() { return this.isBlock && this.inlineContent } + + /// True for node types that allow no content. + get isLeaf() { return this.contentMatch == ContentMatch.empty } + + /// True when this node is an atom, i.e. when it does not have + /// directly editable content. + get isAtom() { return this.isLeaf || !!this.spec.atom } + + /// Return true when this node type is part of the given + /// [group](#model.NodeSpec.group). + isInGroup(group: string) { + return this.groups.indexOf(group) > -1 + } + + /// The starting match of the node type's content expression. + declare contentMatch: ContentMatch + + /// The set of marks allowed in this node. `null` means all marks + /// are allowed. + markSet: readonly MarkType[] | null = null + + /// The node type's [whitespace](#model.NodeSpec.whitespace) option. + get whitespace(): "pre" | "normal" { + return this.spec.whitespace || (this.spec.code ? "pre" : "normal") + } + + /// Tells you whether this node type has any required attributes. + hasRequiredAttrs() { + for (let n in this.attrs) if (this.attrs[n].isRequired) return true + return false + } + + /// Indicates whether this node allows some of the same content as + /// the given node type. + compatibleContent(other: NodeType) { + return this == other || this.contentMatch.compatible(other.contentMatch) + } + + /// @internal + computeAttrs(attrs: Attrs | null): Attrs { + if (!attrs && this.defaultAttrs) return this.defaultAttrs + else return computeAttrs(this.attrs, attrs) + } + + /// Create a `Node` of this type. The given attributes are + /// checked and defaulted (you can pass `null` to use the type's + /// defaults entirely, if no required attributes exist). `content` + /// may be a `Fragment`, a node, an array of nodes, or + /// `null`. Similarly `marks` may be `null` to default to the empty + /// set of marks. + create(attrs: Attrs | null = null, content?: Fragment | Node | readonly Node[] | null, marks?: readonly Mark[]) { + if (this.isText) throw new Error("NodeType.create can't construct text nodes") + return new Node(this, this.computeAttrs(attrs), Fragment.from(content), Mark.setFrom(marks)) + } + + /// Like [`create`](#model.NodeType.create), but check the given content + /// against the node type's content restrictions, and throw an error + /// if it doesn't match. + createChecked(attrs: Attrs | null = null, content?: Fragment | Node | readonly Node[] | null, marks?: readonly Mark[]) { + content = Fragment.from(content) + this.checkContent(content) + return new Node(this, this.computeAttrs(attrs), content, Mark.setFrom(marks)) + } + + /// Like [`create`](#model.NodeType.create), but see if it is + /// necessary to add nodes to the start or end of the given fragment + /// to make it fit the node. If no fitting wrapping can be found, + /// return null. Note that, due to the fact that required nodes can + /// always be created, this will always succeed if you pass null or + /// `Fragment.empty` as content. + createAndFill(attrs: Attrs | null = null, content?: Fragment | Node | readonly Node[] | null, marks?: readonly Mark[]) { + attrs = this.computeAttrs(attrs) + content = Fragment.from(content) + if (content.size) { + let before = this.contentMatch.fillBefore(content) + if (!before) return null + content = before.append(content) + } + let matched = this.contentMatch.matchFragment(content) + let after = matched && matched.fillBefore(Fragment.empty, true) + if (!after) return null + return new Node(this, attrs, (content as Fragment).append(after), Mark.setFrom(marks)) + } + + /// Returns true if the given fragment is valid content for this node + /// type. + validContent(content: Fragment) { + let result = this.contentMatch.matchFragment(content) + if (!result || !result.validEnd) return false + for (let i = 0; i < content.childCount; i++) + if (!this.allowsMarks(content.child(i).marks)) return false + return true + } + + /// Throws a RangeError if the given fragment is not valid content for this + /// node type. + /// @internal + checkContent(content: Fragment) { + if (!this.validContent(content)) + throw new RangeError(`Invalid content for node ${this.name}: ${content.toString().slice(0, 50)}`) + } + + /// @internal + checkAttrs(attrs: Attrs) { + checkAttrs(this.attrs, attrs, "node", this.name) + } + + /// Check whether the given mark type is allowed in this node. + allowsMarkType(markType: MarkType) { + return this.markSet == null || this.markSet.indexOf(markType) > -1 + } + + /// Test whether the given set of marks are allowed in this node. + allowsMarks(marks: readonly Mark[]) { + if (this.markSet == null) return true + for (let i = 0; i < marks.length; i++) if (!this.allowsMarkType(marks[i].type)) return false + return true + } + + /// Removes the marks that are not allowed in this node from the given set. + allowedMarks(marks: readonly Mark[]): readonly Mark[] { + if (this.markSet == null) return marks + let copy + for (let i = 0; i < marks.length; i++) { + if (!this.allowsMarkType(marks[i].type)) { + if (!copy) copy = marks.slice(0, i) + } else if (copy) { + copy.push(marks[i]) + } + } + return !copy ? marks : copy.length ? copy : Mark.none + } + + /// @internal + static compile<Nodes extends string>(nodes: OrderedMap<NodeSpec>, schema: Schema<Nodes>): {readonly [name in Nodes]: NodeType} { + let result = Object.create(null) + nodes.forEach((name, spec) => result[name] = new NodeType(name, schema, spec)) + + let topType = schema.spec.topNode || "doc" + if (!result[topType]) throw new RangeError("Schema is missing its top node type ('" + topType + "')") + if (!result.text) throw new RangeError("Every schema needs a 'text' type") + for (let _ in result.text.attrs) throw new RangeError("The text node type should not have attributes") + + return result + } +} + +function validateType(typeName: string, attrName: string, type: string) { + let types = type.split("|") + return (value: any) => { + let name = value === null ? "null" : typeof value + if (types.indexOf(name) < 0) throw new RangeError(`Expected value of type ${types} for attribute ${attrName} on type ${typeName}, got ${name}`) + } +} + +// Attribute descriptors + +class Attribute { + hasDefault: boolean + default: any + validate: undefined | ((value: any) => void) + + constructor(typeName: string, attrName: string, options: AttributeSpec) { + this.hasDefault = Object.prototype.hasOwnProperty.call(options, "default") + this.default = options.default + this.validate = typeof options.validate == "string" ? validateType(typeName, attrName, options.validate) : options.validate + } + + get isRequired() { + return !this.hasDefault + } +} + +// Marks + +/// Like nodes, marks (which are associated with nodes to signify +/// things like emphasis or being part of a link) are +/// [tagged](#model.Mark.type) with type objects, which are +/// instantiated once per `Schema`. +export class MarkType { + /// @internal + attrs: {[name: string]: Attribute} + /// @internal + declare excluded: readonly MarkType[] + /// @internal + instance: Mark | null + + /// @internal + constructor( + /// The name of the mark type. + readonly name: string, + /// @internal + readonly rank: number, + /// The schema that this mark type instance is part of. + readonly schema: Schema, + /// The spec on which the type is based. + readonly spec: MarkSpec + ) { + this.attrs = initAttrs(name, spec.attrs) + ;(this as any).excluded = null + let defaults = defaultAttrs(this.attrs) + this.instance = defaults ? new Mark(this, defaults) : null + } + + /// Create a mark of this type. `attrs` may be `null` or an object + /// containing only some of the mark's attributes. The others, if + /// they have defaults, will be added. + create(attrs: Attrs | null = null) { + if (!attrs && this.instance) return this.instance + return new Mark(this, computeAttrs(this.attrs, attrs)) + } + + /// @internal + static compile(marks: OrderedMap<MarkSpec>, schema: Schema) { + let result = Object.create(null), rank = 0 + marks.forEach((name, spec) => result[name] = new MarkType(name, rank++, schema, spec)) + return result + } + + /// When there is a mark of this type in the given set, a new set + /// without it is returned. Otherwise, the input set is returned. + removeFromSet(set: readonly Mark[]): readonly Mark[] { + for (var i = 0; i < set.length; i++) if (set[i].type == this) { + set = set.slice(0, i).concat(set.slice(i + 1)) + i-- + } + return set + } + + /// Tests whether there is a mark of this type in the given set. + isInSet(set: readonly Mark[]): Mark | undefined { + for (let i = 0; i < set.length; i++) + if (set[i].type == this) return set[i] + } + + /// @internal + checkAttrs(attrs: Attrs) { + checkAttrs(this.attrs, attrs, "mark", this.name) + } + + /// Queries whether a given mark type is + /// [excluded](#model.MarkSpec.excludes) by this one. + excludes(other: MarkType) { + return this.excluded.indexOf(other) > -1 + } +} + +/// An object describing a schema, as passed to the [`Schema`](#model.Schema) +/// constructor. +export interface SchemaSpec<Nodes extends string = any, Marks extends string = any> { + /// The node types in this schema. Maps names to + /// [`NodeSpec`](#model.NodeSpec) objects that describe the node type + /// associated with that name. Their order is significant—it + /// determines which [parse rules](#model.NodeSpec.parseDOM) take + /// precedence by default, and which nodes come first in a given + /// [group](#model.NodeSpec.group). + nodes: {[name in Nodes]: NodeSpec} | OrderedMap<NodeSpec>, + + /// The mark types that exist in this schema. The order in which they + /// are provided determines the order in which [mark + /// sets](#model.Mark.addToSet) are sorted and in which [parse + /// rules](#model.MarkSpec.parseDOM) are tried. + marks?: {[name in Marks]: MarkSpec} | OrderedMap<MarkSpec> + + /// The name of the default top-level node for the schema. Defaults + /// to `"doc"`. + topNode?: string +} + +/// A description of a node type, used when defining a schema. +export interface NodeSpec { + /// The content expression for this node, as described in the [schema + /// guide](/docs/guide/#schema.content_expressions). When not given, + /// the node does not allow any content. + content?: string + + /// The marks that are allowed inside of this node. May be a + /// space-separated string referring to mark names or groups, `"_"` + /// to explicitly allow all marks, or `""` to disallow marks. When + /// not given, nodes with inline content default to allowing all + /// marks, other nodes default to not allowing marks. + marks?: string + + /// The group or space-separated groups to which this node belongs, + /// which can be referred to in the content expressions for the + /// schema. + group?: string + + /// Should be set to true for inline nodes. (Implied for text nodes.) + inline?: boolean + + /// Can be set to true to indicate that, though this isn't a [leaf + /// node](#model.NodeType.isLeaf), it doesn't have directly editable + /// content and should be treated as a single unit in the view. + atom?: boolean + + /// The attributes that nodes of this type get. + attrs?: {[name: string]: AttributeSpec} + + /// Controls whether nodes of this type can be selected as a [node + /// selection](#state.NodeSelection). Defaults to true for non-text + /// nodes. + selectable?: boolean + + /// Determines whether nodes of this type can be dragged without + /// being selected. Defaults to false. + draggable?: boolean + + /// Can be used to indicate that this node contains code, which + /// causes some commands to behave differently. + code?: boolean + + /// Controls way whitespace in this a node is parsed. The default is + /// `"normal"`, which causes the [DOM parser](#model.DOMParser) to + /// collapse whitespace in normal mode, and normalize it (replacing + /// newlines and such with spaces) otherwise. `"pre"` causes the + /// parser to preserve spaces inside the node. When this option isn't + /// given, but [`code`](#model.NodeSpec.code) is true, `whitespace` + /// will default to `"pre"`. Note that this option doesn't influence + /// the way the node is rendered—that should be handled by `toDOM` + /// and/or styling. + whitespace?: "pre" | "normal" + + /// Determines whether this node is considered an important parent + /// node during replace operations (such as paste). Non-defining (the + /// default) nodes get dropped when their entire content is replaced, + /// whereas defining nodes persist and wrap the inserted content. + definingAsContext?: boolean + + /// In inserted content the defining parents of the content are + /// preserved when possible. Typically, non-default-paragraph + /// textblock types, and possibly list items, are marked as defining. + definingForContent?: boolean + + /// When enabled, enables both + /// [`definingAsContext`](#model.NodeSpec.definingAsContext) and + /// [`definingForContent`](#model.NodeSpec.definingForContent). + defining?: boolean + + /// When enabled (default is false), the sides of nodes of this type + /// count as boundaries that regular editing operations, like + /// backspacing or lifting, won't cross. An example of a node that + /// should probably have this enabled is a table cell. + isolating?: boolean + + /// Defines the default way a node of this type should be serialized + /// to DOM/HTML (as used by + /// [`DOMSerializer.fromSchema`](#model.DOMSerializer^fromSchema)). + /// Should return a DOM node or an [array + /// structure](#model.DOMOutputSpec) that describes one, with an + /// optional number zero (“hole”) in it to indicate where the node's + /// content should be inserted. + /// + /// For text nodes, the default is to create a text DOM node. Though + /// it is possible to create a serializer where text is rendered + /// differently, this is not supported inside the editor, so you + /// shouldn't override that in your text node spec. + toDOM?: (node: Node) => DOMOutputSpec + + /// Associates DOM parser information with this node, which can be + /// used by [`DOMParser.fromSchema`](#model.DOMParser^fromSchema) to + /// automatically derive a parser. The `node` field in the rules is + /// implied (the name of this node will be filled in automatically). + /// If you supply your own parser, you do not need to also specify + /// parsing rules in your schema. + parseDOM?: readonly TagParseRule[] + + /// Defines the default way a node of this type should be serialized + /// to a string representation for debugging (e.g. in error messages). + toDebugString?: (node: Node) => string + + /// Defines the default way a [leaf node](#model.NodeType.isLeaf) of + /// this type should be serialized to a string (as used by + /// [`Node.textBetween`](#model.Node.textBetween) and + /// [`Node.textContent`](#model.Node.textContent)). + leafText?: (node: Node) => string + + /// A single inline node in a schema can be set to be a linebreak + /// equivalent. When converting between block types that support the + /// node and block types that don't but have + /// [`whitespace`](#model.NodeSpec.whitespace) set to `"pre"`, + /// [`setBlockType`](#transform.Transform.setBlockType) will convert + /// between newline characters to or from linebreak nodes as + /// appropriate. + linebreakReplacement?: boolean + + /// Node specs may include arbitrary properties that can be read by + /// other code via [`NodeType.spec`](#model.NodeType.spec). + [key: string]: any +} + +/// Used to define marks when creating a schema. +export interface MarkSpec { + /// The attributes that marks of this type get. + attrs?: {[name: string]: AttributeSpec} + + /// Whether this mark should be active when the cursor is positioned + /// at its end (or at its start when that is also the start of the + /// parent node). Defaults to true. + inclusive?: boolean + + /// Determines which other marks this mark can coexist with. Should + /// be a space-separated strings naming other marks or groups of marks. + /// When a mark is [added](#model.Mark.addToSet) to a set, all marks + /// that it excludes are removed in the process. If the set contains + /// any mark that excludes the new mark but is not, itself, excluded + /// by the new mark, the mark can not be added an the set. You can + /// use the value `"_"` to indicate that the mark excludes all + /// marks in the schema. + /// + /// Defaults to only being exclusive with marks of the same type. You + /// can set it to an empty string (or any string not containing the + /// mark's own name) to allow multiple marks of a given type to + /// coexist (as long as they have different attributes). + excludes?: string + + /// The group or space-separated groups to which this mark belongs. + group?: string + + /// Determines whether marks of this type can span multiple adjacent + /// nodes when serialized to DOM/HTML. Defaults to true. + spanning?: boolean + + /// Marks the content of this span as being code, which causes some + /// commands and extensions to treat it differently. + code?: boolean + + /// Defines the default way marks of this type should be serialized + /// to DOM/HTML. When the resulting spec contains a hole, that is + /// where the marked content is placed. Otherwise, it is appended to + /// the top node. + toDOM?: (mark: Mark, inline: boolean) => DOMOutputSpec + + /// Associates DOM parser information with this mark (see the + /// corresponding [node spec field](#model.NodeSpec.parseDOM)). The + /// `mark` field in the rules is implied. + parseDOM?: readonly ParseRule[] + + /// Mark specs can include additional properties that can be + /// inspected through [`MarkType.spec`](#model.MarkType.spec) when + /// working with the mark. + [key: string]: any +} + +/// Used to [define](#model.NodeSpec.attrs) attributes on nodes or +/// marks. +export interface AttributeSpec { + /// The default value for this attribute, to use when no explicit + /// value is provided. Attributes that have no default must be + /// provided whenever a node or mark of a type that has them is + /// created. + default?: any + /// A function or type name used to validate values of this + /// attribute. This will be used when deserializing the attribute + /// from JSON, and when running [`Node.check`](#model.Node.check). + /// When a function, it should raise an exception if the value isn't + /// of the expected type or shape. When a string, it should be a + /// `|`-separated string of primitive types (`"number"`, `"string"`, + /// `"boolean"`, `"null"`, and `"undefined"`), and the library will + /// raise an error when the value is not one of those types. + validate?: string | ((value: any) => void) +} + +/// A document schema. Holds [node](#model.NodeType) and [mark +/// type](#model.MarkType) objects for the nodes and marks that may +/// occur in conforming documents, and provides functionality for +/// creating and deserializing such documents. +/// +/// When given, the type parameters provide the names of the nodes and +/// marks in this schema. +export class Schema<Nodes extends string = any, Marks extends string = any> { + /// The [spec](#model.SchemaSpec) on which the schema is based, + /// with the added guarantee that its `nodes` and `marks` + /// properties are + /// [`OrderedMap`](https://github.com/marijnh/orderedmap) instances + /// (not raw objects). + spec: { + nodes: OrderedMap<NodeSpec>, + marks: OrderedMap<MarkSpec>, + topNode?: string + } + + /// An object mapping the schema's node names to node type objects. + nodes: {readonly [name in Nodes]: NodeType} & {readonly [key: string]: NodeType} + + /// A map from mark names to mark type objects. + marks: {readonly [name in Marks]: MarkType} & {readonly [key: string]: MarkType} + + /// The [linebreak + /// replacement](#model.NodeSpec.linebreakReplacement) node defined + /// in this schema, if any. + linebreakReplacement: NodeType | null = null + + /// Construct a schema from a schema [specification](#model.SchemaSpec). + constructor(spec: SchemaSpec<Nodes, Marks>) { + let instanceSpec = this.spec = {} as any + for (let prop in spec) instanceSpec[prop] = (spec as any)[prop] + instanceSpec.nodes = OrderedMap.from(spec.nodes), + instanceSpec.marks = OrderedMap.from(spec.marks || {}), + + this.nodes = NodeType.compile(this.spec.nodes, this) + this.marks = MarkType.compile(this.spec.marks, this) + + let contentExprCache = Object.create(null) + for (let prop in this.nodes) { + if (prop in this.marks) + throw new RangeError(prop + " can not be both a node and a mark") + let type = this.nodes[prop], contentExpr = type.spec.content || "", markExpr = type.spec.marks + type.contentMatch = contentExprCache[contentExpr] || + (contentExprCache[contentExpr] = ContentMatch.parse(contentExpr, this.nodes)) + ;(type as any).inlineContent = type.contentMatch.inlineContent + if (type.spec.linebreakReplacement) { + if (this.linebreakReplacement) throw new RangeError("Multiple linebreak nodes defined") + if (!type.isInline || !type.isLeaf) throw new RangeError("Linebreak replacement nodes must be inline leaf nodes") + this.linebreakReplacement = type + } + type.markSet = markExpr == "_" ? null : + markExpr ? gatherMarks(this, markExpr.split(" ")) : + markExpr == "" || !type.inlineContent ? [] : null + } + for (let prop in this.marks) { + let type = this.marks[prop], excl = type.spec.excludes + type.excluded = excl == null ? [type] : excl == "" ? [] : gatherMarks(this, excl.split(" ")) + } + + this.nodeFromJSON = json => Node.fromJSON(this, json) + this.markFromJSON = json => Mark.fromJSON(this, json) + this.topNodeType = this.nodes[this.spec.topNode || "doc"] + this.cached.wrappings = Object.create(null) + } + + /// The type of the [default top node](#model.SchemaSpec.topNode) + /// for this schema. + topNodeType: NodeType + + /// An object for storing whatever values modules may want to + /// compute and cache per schema. (If you want to store something + /// in it, try to use property names unlikely to clash.) + cached: {[key: string]: any} = Object.create(null) + + /// Create a node in this schema. The `type` may be a string or a + /// `NodeType` instance. Attributes will be extended with defaults, + /// `content` may be a `Fragment`, `null`, a `Node`, or an array of + /// nodes. + node(type: string | NodeType, + attrs: Attrs | null = null, + content?: Fragment | Node | readonly Node[], + marks?: readonly Mark[]) { + if (typeof type == "string") + type = this.nodeType(type) + else if (!(type instanceof NodeType)) + throw new RangeError("Invalid node type: " + type) + else if (type.schema != this) + throw new RangeError("Node type from different schema used (" + type.name + ")") + + return type.createChecked(attrs, content, marks) + } + + /// Create a text node in the schema. Empty text nodes are not + /// allowed. + text(text: string, marks?: readonly Mark[] | null): Node { + let type = this.nodes.text + return new TextNode(type, type.defaultAttrs, text, Mark.setFrom(marks)) + } + + /// Create a mark with the given type and attributes. + mark(type: string | MarkType, attrs?: Attrs | null) { + if (typeof type == "string") type = this.marks[type] + return type.create(attrs) + } + + /// Deserialize a node from its JSON representation. This method is + /// bound. + nodeFromJSON: (json: any) => Node + + /// Deserialize a mark from its JSON representation. This method is + /// bound. + markFromJSON: (json: any) => Mark + + /// @internal + nodeType(name: string) { + let found = this.nodes[name] + if (!found) throw new RangeError("Unknown node type: " + name) + return found + } +} + +function gatherMarks(schema: Schema, marks: readonly string[]) { + let found: MarkType[] = [] + for (let i = 0; i < marks.length; i++) { + let name = marks[i], mark = schema.marks[name], ok = mark + if (mark) { + found.push(mark) + } else { + for (let prop in schema.marks) { + let mark = schema.marks[prop] + if (name == "_" || (mark.spec.group && mark.spec.group.split(" ").indexOf(name) > -1)) + found.push(ok = mark) + } + } + if (!ok) throw new SyntaxError("Unknown mark type: '" + marks[i] + "'") + } + return found +} diff --git a/third_party/js/prosemirror/prosemirror-model/src/to_dom.ts b/third_party/js/prosemirror/prosemirror-model/src/to_dom.ts @@ -0,0 +1,238 @@ +import {Fragment} from "./fragment" +import {Node} from "./node" +import {Schema, NodeType, MarkType} from "./schema" +import {Mark} from "./mark" +import {DOMNode} from "./dom" + +/// A description of a DOM structure. Can be either a string, which is +/// interpreted as a text node, a DOM node, which is interpreted as +/// itself, a `{dom, contentDOM}` object, or an array. +/// +/// An array describes a DOM element. The first value in the array +/// should be a string—the name of the DOM element, optionally prefixed +/// by a namespace URL and a space. If the second element is plain +/// object, it is interpreted as a set of attributes for the element. +/// Any elements after that (including the 2nd if it's not an attribute +/// object) are interpreted as children of the DOM elements, and must +/// either be valid `DOMOutputSpec` values, or the number zero. +/// +/// The number zero (pronounced “hole”) is used to indicate the place +/// where a node's child nodes should be inserted. If it occurs in an +/// output spec, it should be the only child element in its parent +/// node. +export type DOMOutputSpec = string | DOMNode | {dom: DOMNode, contentDOM?: HTMLElement} | readonly [string, ...any[]] + +/// A DOM serializer knows how to convert ProseMirror nodes and +/// marks of various types to DOM nodes. +export class DOMSerializer { + /// Create a serializer. `nodes` should map node names to functions + /// that take a node and return a description of the corresponding + /// DOM. `marks` does the same for mark names, but also gets an + /// argument that tells it whether the mark's content is block or + /// inline content (for typical use, it'll always be inline). A mark + /// serializer may be `null` to indicate that marks of that type + /// should not be serialized. + constructor( + /// The node serialization functions. + readonly nodes: {[node: string]: (node: Node) => DOMOutputSpec}, + /// The mark serialization functions. + readonly marks: {[mark: string]: (mark: Mark, inline: boolean) => DOMOutputSpec} + ) {} + + /// Serialize the content of this fragment to a DOM fragment. When + /// not in the browser, the `document` option, containing a DOM + /// document, should be passed so that the serializer can create + /// nodes. + serializeFragment(fragment: Fragment, options: {document?: Document} = {}, target?: HTMLElement | DocumentFragment) { + if (!target) target = doc(options).createDocumentFragment() + + let top = target!, active: [Mark, HTMLElement | DocumentFragment][] = [] + fragment.forEach(node => { + if (active.length || node.marks.length) { + let keep = 0, rendered = 0 + while (keep < active.length && rendered < node.marks.length) { + let next = node.marks[rendered] + if (!this.marks[next.type.name]) { rendered++; continue } + if (!next.eq(active[keep][0]) || next.type.spec.spanning === false) break + keep++; rendered++ + } + while (keep < active.length) top = active.pop()![1] + while (rendered < node.marks.length) { + let add = node.marks[rendered++] + let markDOM = this.serializeMark(add, node.isInline, options) + if (markDOM) { + active.push([add, top]) + top.appendChild(markDOM.dom) + top = markDOM.contentDOM || markDOM.dom as HTMLElement + } + } + } + top.appendChild(this.serializeNodeInner(node, options)) + }) + + return target + } + + /// @internal + serializeNodeInner(node: Node, options: {document?: Document}) { + let {dom, contentDOM} = + renderSpec(doc(options), this.nodes[node.type.name](node), null, node.attrs) + if (contentDOM) { + if (node.isLeaf) + throw new RangeError("Content hole not allowed in a leaf node spec") + this.serializeFragment(node.content, options, contentDOM) + } + return dom + } + + /// Serialize this node to a DOM node. This can be useful when you + /// need to serialize a part of a document, as opposed to the whole + /// document. To serialize a whole document, use + /// [`serializeFragment`](#model.DOMSerializer.serializeFragment) on + /// its [content](#model.Node.content). + serializeNode(node: Node, options: {document?: Document} = {}) { + let dom = this.serializeNodeInner(node, options) + for (let i = node.marks.length - 1; i >= 0; i--) { + let wrap = this.serializeMark(node.marks[i], node.isInline, options) + if (wrap) { + ;(wrap.contentDOM || wrap.dom).appendChild(dom) + dom = wrap.dom + } + } + return dom + } + + /// @internal + serializeMark(mark: Mark, inline: boolean, options: {document?: Document} = {}) { + let toDOM = this.marks[mark.type.name] + return toDOM && renderSpec(doc(options), toDOM(mark, inline), null, mark.attrs) + } + + /// Render an [output spec](#model.DOMOutputSpec) to a DOM node. If + /// the spec has a hole (zero) in it, `contentDOM` will point at the + /// node with the hole. + static renderSpec(doc: Document, structure: DOMOutputSpec, xmlNS?: string | null): { + dom: DOMNode, + contentDOM?: HTMLElement + } + static renderSpec(doc: Document, structure: DOMOutputSpec, xmlNS: string | null = null, + blockArraysIn?: {[name: string]: any}): { + dom: DOMNode, + contentDOM?: HTMLElement + } { + return renderSpec(doc, structure, xmlNS, blockArraysIn) + } + + /// Build a serializer using the [`toDOM`](#model.NodeSpec.toDOM) + /// properties in a schema's node and mark specs. + static fromSchema(schema: Schema): DOMSerializer { + return schema.cached.domSerializer as DOMSerializer || + (schema.cached.domSerializer = new DOMSerializer(this.nodesFromSchema(schema), this.marksFromSchema(schema))) + } + + /// Gather the serializers in a schema's node specs into an object. + /// This can be useful as a base to build a custom serializer from. + static nodesFromSchema(schema: Schema) { + let result = gatherToDOM(schema.nodes) + if (!result.text) result.text = node => node.text + return result as {[node: string]: (node: Node) => DOMOutputSpec} + } + + /// Gather the serializers in a schema's mark specs into an object. + static marksFromSchema(schema: Schema) { + return gatherToDOM(schema.marks) as {[mark: string]: (mark: Mark, inline: boolean) => DOMOutputSpec} + } +} + +function gatherToDOM(obj: {[node: string]: NodeType | MarkType}) { + let result: {[node: string]: (value: any, inline: boolean) => DOMOutputSpec} = {} + for (let name in obj) { + let toDOM = obj[name].spec.toDOM + if (toDOM) result[name] = toDOM + } + return result +} + +function doc(options: {document?: Document}) { + return options.document || window.document +} + +const suspiciousAttributeCache = new WeakMap<any, readonly any[] | null>() + +function suspiciousAttributes(attrs: {[name: string]: any}): readonly any[] | null { + let value = suspiciousAttributeCache.get(attrs) + if (value === undefined) + suspiciousAttributeCache.set(attrs, value = suspiciousAttributesInner(attrs)) + return value +} + +function suspiciousAttributesInner(attrs: {[name: string]: any}): readonly any[] | null { + let result: any[] | null = null + function scan(value: any) { + if (value && typeof value == "object") { + if (Array.isArray(value)) { + if (typeof value[0] == "string") { + if (!result) result = [] + result.push(value) + } else { + for (let i = 0; i < value.length; i++) scan(value[i]) + } + } else { + for (let prop in value) scan(value[prop]) + } + } + } + scan(attrs) + return result +} + +function renderSpec(doc: Document, structure: DOMOutputSpec, xmlNS: string | null, + blockArraysIn?: {[name: string]: any}): { + dom: DOMNode, + contentDOM?: HTMLElement +} { + if (typeof structure == "string") + return {dom: doc.createTextNode(structure)} + if ((structure as DOMNode).nodeType != null) + return {dom: structure as DOMNode} + if ((structure as any).dom && (structure as any).dom.nodeType != null) + return structure as {dom: DOMNode, contentDOM?: HTMLElement} + let tagName = (structure as [string])[0], suspicious + if (typeof tagName != "string") throw new RangeError("Invalid array passed to renderSpec") + if (blockArraysIn && (suspicious = suspiciousAttributes(blockArraysIn)) && + suspicious.indexOf(structure) > -1) + throw new RangeError("Using an array from an attribute object as a DOM spec. This may be an attempted cross site scripting attack.") + let space = tagName.indexOf(" ") + if (space > 0) { + xmlNS = tagName.slice(0, space) + tagName = tagName.slice(space + 1) + } + let contentDOM: HTMLElement | undefined + let dom = (xmlNS ? doc.createElementNS(xmlNS, tagName) : doc.createElement(tagName)) as HTMLElement + let attrs = (structure as any)[1], start = 1 + if (attrs && typeof attrs == "object" && attrs.nodeType == null && !Array.isArray(attrs)) { + start = 2 + for (let name in attrs) if (attrs[name] != null) { + let space = name.indexOf(" ") + if (space > 0) dom.setAttributeNS(name.slice(0, space), name.slice(space + 1), attrs[name]) + else if (name == "style" && dom.style) dom.style.cssText = attrs[name] + else dom.setAttribute(name, attrs[name]) + } + } + for (let i = start; i < (structure as readonly any[]).length; i++) { + let child = (structure as any)[i] as DOMOutputSpec | 0 + if (child === 0) { + if (i < (structure as readonly any[]).length - 1 || i > start) + throw new RangeError("Content hole must be the only child of its parent node") + return {dom, contentDOM: dom} + } else { + let {dom: inner, contentDOM: innerContent} = renderSpec(doc, child, xmlNS, blockArraysIn) + dom.appendChild(inner) + if (innerContent) { + if (contentDOM) throw new RangeError("Multiple content holes") + contentDOM = innerContent as HTMLElement + } + } + } + return {dom, contentDOM} +} diff --git a/third_party/js/prosemirror/prosemirror-schema-basic/LICENSE b/third_party/js/prosemirror/prosemirror-schema-basic/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/js/prosemirror/prosemirror-schema-basic/moz.yaml b/third_party/js/prosemirror/prosemirror-schema-basic/moz.yaml @@ -0,0 +1,63 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror-schema-basic + + description: Basic schema elements for ProseMirror + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: 1.2.4 (2025-03-18T09:14:56+01:00). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: 1.2.4 + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MIT + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/ProseMirror/prosemirror-schema-basic + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + # Whether to track by commit or tag + tracking: tag + + exclude: + - "**" + + include: + - LICENSE + - src/ + - package.json diff --git a/third_party/js/prosemirror/prosemirror-schema-basic/package.json b/third_party/js/prosemirror/prosemirror-schema-basic/package.json @@ -0,0 +1,37 @@ +{ + "name": "prosemirror-schema-basic", + "version": "1.2.4", + "description": "Basic schema elements for ProseMirror", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "sideEffects": false, + "license": "MIT", + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijn@haverbeke.berlin", + "web": "http://marijnhaverbeke.nl" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/prosemirror/prosemirror-schema-basic.git" + }, + "dependencies": { + "prosemirror-model": "^1.25.0" + }, + "devDependencies": { + "@prosemirror/buildhelper": "^0.1.5", + "prosemirror-test-builder": "^1.0.0" + }, + "scripts": { + "test": "pm-runtests", + "prepare": "pm-buildhelper src/schema-basic.ts" + } +} diff --git a/third_party/js/prosemirror/prosemirror-schema-basic/src/README.md b/third_party/js/prosemirror/prosemirror-schema-basic/src/README.md @@ -0,0 +1,8 @@ +This module defines a simple schema. You can use it directly, extend +it, or just pick out a few node and mark specs to use in a new schema. + +@schema + +@nodes + +@marks diff --git a/third_party/js/prosemirror/prosemirror-schema-basic/src/schema-basic.ts b/third_party/js/prosemirror/prosemirror-schema-basic/src/schema-basic.ts @@ -0,0 +1,166 @@ +import {Schema, NodeSpec, MarkSpec, DOMOutputSpec} from "prosemirror-model" + +const pDOM: DOMOutputSpec = ["p", 0], blockquoteDOM: DOMOutputSpec = ["blockquote", 0], + hrDOM: DOMOutputSpec = ["hr"], preDOM: DOMOutputSpec = ["pre", ["code", 0]], + brDOM: DOMOutputSpec = ["br"] + +/// [Specs](#model.NodeSpec) for the nodes defined in this schema. +export const nodes = { + /// NodeSpec The top level document node. + doc: { + content: "block+" + } as NodeSpec, + + /// A plain paragraph textblock. Represented in the DOM + /// as a `<p>` element. + paragraph: { + content: "inline*", + group: "block", + parseDOM: [{tag: "p"}], + toDOM() { return pDOM } + } as NodeSpec, + + /// A blockquote (`<blockquote>`) wrapping one or more blocks. + blockquote: { + content: "block+", + group: "block", + defining: true, + parseDOM: [{tag: "blockquote"}], + toDOM() { return blockquoteDOM } + } as NodeSpec, + + /// A horizontal rule (`<hr>`). + horizontal_rule: { + group: "block", + parseDOM: [{tag: "hr"}], + toDOM() { return hrDOM } + } as NodeSpec, + + /// A heading textblock, with a `level` attribute that + /// should hold the number 1 to 6. Parsed and serialized as `<h1>` to + /// `<h6>` elements. + heading: { + attrs: {level: {default: 1, validate: "number"}}, + content: "inline*", + group: "block", + defining: true, + parseDOM: [{tag: "h1", attrs: {level: 1}}, + {tag: "h2", attrs: {level: 2}}, + {tag: "h3", attrs: {level: 3}}, + {tag: "h4", attrs: {level: 4}}, + {tag: "h5", attrs: {level: 5}}, + {tag: "h6", attrs: {level: 6}}], + toDOM(node) { return ["h" + node.attrs.level, 0] } + } as NodeSpec, + + /// A code listing. Disallows marks or non-text inline + /// nodes by default. Represented as a `<pre>` element with a + /// `<code>` element inside of it. + code_block: { + content: "text*", + marks: "", + group: "block", + code: true, + defining: true, + parseDOM: [{tag: "pre", preserveWhitespace: "full"}], + toDOM() { return preDOM } + } as NodeSpec, + + /// The text node. + text: { + group: "inline" + } as NodeSpec, + + /// An inline image (`<img>`) node. Supports `src`, + /// `alt`, and `href` attributes. The latter two default to the empty + /// string. + image: { + inline: true, + attrs: { + src: {validate: "string"}, + alt: {default: null, validate: "string|null"}, + title: {default: null, validate: "string|null"} + }, + group: "inline", + draggable: true, + parseDOM: [{tag: "img[src]", getAttrs(dom: HTMLElement) { + return { + src: dom.getAttribute("src"), + title: dom.getAttribute("title"), + alt: dom.getAttribute("alt") + } + }}], + toDOM(node) { let {src, alt, title} = node.attrs; return ["img", {src, alt, title}] } + } as NodeSpec, + + /// A hard line break, represented in the DOM as `<br>`. + hard_break: { + inline: true, + group: "inline", + selectable: false, + parseDOM: [{tag: "br"}], + toDOM() { return brDOM } + } as NodeSpec +} + +const emDOM: DOMOutputSpec = ["em", 0], strongDOM: DOMOutputSpec = ["strong", 0], codeDOM: DOMOutputSpec = ["code", 0] + +/// [Specs](#model.MarkSpec) for the marks in the schema. +export const marks = { + /// A link. Has `href` and `title` attributes. `title` + /// defaults to the empty string. Rendered and parsed as an `<a>` + /// element. + link: { + attrs: { + href: {validate: "string"}, + title: {default: null, validate: "string|null"} + }, + inclusive: false, + parseDOM: [{tag: "a[href]", getAttrs(dom: HTMLElement) { + return {href: dom.getAttribute("href"), title: dom.getAttribute("title")} + }}], + toDOM(node) { let {href, title} = node.attrs; return ["a", {href, title}, 0] } + } as MarkSpec, + + /// An emphasis mark. Rendered as an `<em>` element. Has parse rules + /// that also match `<i>` and `font-style: italic`. + em: { + parseDOM: [ + {tag: "i"}, {tag: "em"}, + {style: "font-style=italic"}, + {style: "font-style=normal", clearMark: m => m.type.name == "em"} + ], + toDOM() { return emDOM } + } as MarkSpec, + + /// A strong mark. Rendered as `<strong>`, parse rules also match + /// `<b>` and `font-weight: bold`. + strong: { + parseDOM: [ + {tag: "strong"}, + // This works around a Google Docs misbehavior where + // pasted content will be inexplicably wrapped in `<b>` + // tags with a font-weight normal. + {tag: "b", getAttrs: (node: HTMLElement) => node.style.fontWeight != "normal" && null}, + {style: "font-weight=400", clearMark: m => m.type.name == "strong"}, + {style: "font-weight", getAttrs: (value: string) => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null}, + ], + toDOM() { return strongDOM } + } as MarkSpec, + + /// Code font mark. Represented as a `<code>` element. + code: { + code: true, + parseDOM: [{tag: "code"}], + toDOM() { return codeDOM } + } as MarkSpec +} + +/// This schema roughly corresponds to the document schema used by +/// [CommonMark](http://commonmark.org/), minus the list elements, +/// which are defined in the [`prosemirror-schema-list`](#schema-list) +/// module. +/// +/// To reuse elements from this schema, extend or read from its +/// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec). +export const schema = new Schema({nodes, marks}) diff --git a/third_party/js/prosemirror/prosemirror-state/LICENSE b/third_party/js/prosemirror/prosemirror-state/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/js/prosemirror/prosemirror-state/moz.yaml b/third_party/js/prosemirror/prosemirror-state/moz.yaml @@ -0,0 +1,63 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror-state + + description: ProseMirror editor state + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: 1.4.4 (2025-10-23T13:57:31+02:00). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: 1.4.4 + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MIT + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/ProseMirror/prosemirror-state + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + # Whether to track by commit or tag + tracking: tag + + exclude: + - "**" + + include: + - LICENSE + - src/ + - package.json diff --git a/third_party/js/prosemirror/prosemirror-state/package.json b/third_party/js/prosemirror/prosemirror-state/package.json @@ -0,0 +1,39 @@ +{ + "name": "prosemirror-state", + "version": "1.4.4", + "description": "ProseMirror editor state", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "sideEffects": false, + "license": "MIT", + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijn@haverbeke.berlin", + "web": "http://marijnhaverbeke.nl" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/prosemirror/prosemirror-state.git" + }, + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + }, + "devDependencies": { + "@prosemirror/buildhelper": "^0.1.5", + "prosemirror-test-builder": "^1.0.0" + }, + "scripts": { + "test": "pm-runtests", + "prepare": "pm-buildhelper src/index.ts" + } +} diff --git a/third_party/js/prosemirror/prosemirror-state/src/README.md b/third_party/js/prosemirror/prosemirror-state/src/README.md @@ -0,0 +1,42 @@ +This module implements the state object of a ProseMirror editor, along +with the representation of the selection and the plugin abstraction. + +### Editor State + +ProseMirror keeps all editor state (the things, basically, that would +be required to create an editor just like the current one) in a single +[object](#state.EditorState). That object is updated (creating a new +state) by applying [transactions](#state.Transaction) to it. + +@EditorState +@EditorStateConfig +@Transaction +@Command + +### Selection + +A ProseMirror selection can be one of several types. This module +defines types for classical [text selections](#state.TextSelection) +(of which cursors are a special case) and [_node_ +selections](#state.NodeSelection), where a specific document node is +selected. It is possible to extend the editor with custom selection +types. + +@Selection +@TextSelection +@NodeSelection +@AllSelection + +@SelectionRange +@SelectionBookmark + +### Plugin System + +To make it easy to package and enable extra editor functionality, +ProseMirror has a plugin system. + +@PluginSpec +@StateField +@PluginView +@Plugin +@PluginKey diff --git a/third_party/js/prosemirror/prosemirror-state/src/index.ts b/third_party/js/prosemirror/prosemirror-state/src/index.ts @@ -0,0 +1,7 @@ +export {Selection, SelectionRange, TextSelection, NodeSelection, AllSelection, SelectionBookmark} from "./selection" + +export {Transaction, Command} from "./transaction" + +export {EditorState, EditorStateConfig} from "./state" + +export {Plugin, PluginKey, PluginSpec, StateField, PluginView} from "./plugin" diff --git a/third_party/js/prosemirror/prosemirror-state/src/plugin.ts b/third_party/js/prosemirror/prosemirror-state/src/plugin.ts @@ -0,0 +1,142 @@ +import {type EditorView, type EditorProps} from "prosemirror-view" +import {EditorState, EditorStateConfig} from "./state" +import {Transaction} from "./transaction" + +/// This is the type passed to the [`Plugin`](#state.Plugin) +/// constructor. It provides a definition for a plugin. +export interface PluginSpec<PluginState> { + /// The [view props](#view.EditorProps) added by this plugin. Props + /// that are functions will be bound to have the plugin instance as + /// their `this` binding. + props?: EditorProps<Plugin<PluginState>> + + /// Allows a plugin to define a [state field](#state.StateField), an + /// extra slot in the state object in which it can keep its own data. + state?: StateField<PluginState> + + /// Can be used to make this a keyed plugin. You can have only one + /// plugin with a given key in a given state, but it is possible to + /// access the plugin's configuration and state through the key, + /// without having access to the plugin instance object. + key?: PluginKey + + /// When the plugin needs to interact with the editor view, or + /// set something up in the DOM, use this field. The function + /// will be called when the plugin's state is associated with an + /// editor view. + view?: (view: EditorView) => PluginView + + /// When present, this will be called before a transaction is + /// applied by the state, allowing the plugin to cancel it (by + /// returning false). + filterTransaction?: (tr: Transaction, state: EditorState) => boolean + + /// Allows the plugin to append another transaction to be applied + /// after the given array of transactions. When another plugin + /// appends a transaction after this was called, it is called again + /// with the new state and new transactions—but only the new + /// transactions, i.e. it won't be passed transactions that it + /// already saw. + appendTransaction?: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => Transaction | null | undefined + + /// Additional properties are allowed on plugin specs, which can be + /// read via [`Plugin.spec`](#state.Plugin.spec). + [key: string]: any +} + +/// A stateful object that can be installed in an editor by a +/// [plugin](#state.PluginSpec.view). +export type PluginView = { + /// Called whenever the view's state is updated. + update?: (view: EditorView, prevState: EditorState) => void + + /// Called when the view is destroyed or receives a state + /// with different plugins. + destroy?: () => void +} + +function bindProps(obj: {[prop: string]: any}, self: any, target: {[prop: string]: any}) { + for (let prop in obj) { + let val = obj[prop] + if (val instanceof Function) val = val.bind(self) + else if (prop == "handleDOMEvents") val = bindProps(val, self, {}) + target[prop] = val + } + return target +} + +/// Plugins bundle functionality that can be added to an editor. +/// They are part of the [editor state](#state.EditorState) and +/// may influence that state and the view that contains it. +export class Plugin<PluginState = any> { + /// Create a plugin. + constructor( + /// The plugin's [spec object](#state.PluginSpec). + readonly spec: PluginSpec<PluginState> + ) { + if (spec.props) bindProps(spec.props, this, this.props) + this.key = spec.key ? spec.key.key : createKey("plugin") + } + + /// The [props](#view.EditorProps) exported by this plugin. + readonly props: EditorProps<Plugin<PluginState>> = {} + + /// @internal + key: string + + /// Extract the plugin's state field from an editor state. + getState(state: EditorState): PluginState | undefined { return (state as any)[this.key] } +} + +/// A plugin spec may provide a state field (under its +/// [`state`](#state.PluginSpec.state) property) of this type, which +/// describes the state it wants to keep. Functions provided here are +/// always called with the plugin instance as their `this` binding. +export interface StateField<T> { + /// Initialize the value of the field. `config` will be the object + /// passed to [`EditorState.create`](#state.EditorState^create). Note + /// that `instance` is a half-initialized state instance, and will + /// not have values for plugin fields initialized after this one. + init: (config: EditorStateConfig, instance: EditorState) => T + + /// Apply the given transaction to this state field, producing a new + /// field value. Note that the `newState` argument is again a partially + /// constructed state does not yet contain the state from plugins + /// coming after this one. + apply: (tr: Transaction, value: T, oldState: EditorState, newState: EditorState) => T + + /// Convert this field to JSON. Optional, can be left off to disable + /// JSON serialization for the field. + toJSON?: (value: T) => any + + /// Deserialize the JSON representation of this field. Note that the + /// `state` argument is again a half-initialized state. + fromJSON?: (config: EditorStateConfig, value: any, state: EditorState) => T +} + +const keys = Object.create(null) + +function createKey(name: string) { + if (name in keys) return name + "$" + ++keys[name] + keys[name] = 0 + return name + "$" +} + +/// A key is used to [tag](#state.PluginSpec.key) plugins in a way +/// that makes it possible to find them, given an editor state. +/// Assigning a key does mean only one plugin of that type can be +/// active in a state. +export class PluginKey<PluginState = any> { + /// @internal + key: string + + /// Create a plugin key. + constructor(name = "key") { this.key = createKey(name) } + + /// Get the active plugin with this key, if any, from an editor + /// state. + get(state: EditorState): Plugin<PluginState> | undefined { return state.config.pluginsByKey[this.key] } + + /// Get the plugin's state from an editor state. + getState(state: EditorState): PluginState | undefined { return (state as any)[this.key] } +} diff --git a/third_party/js/prosemirror/prosemirror-state/src/selection.ts b/third_party/js/prosemirror/prosemirror-state/src/selection.ts @@ -0,0 +1,462 @@ +import {Slice, Fragment, ResolvedPos, Node} from "prosemirror-model" +import {ReplaceStep, ReplaceAroundStep, Mappable} from "prosemirror-transform" +import {Transaction} from "./transaction" + +const classesById = Object.create(null) + +/// Superclass for editor selections. Every selection type should +/// extend this. Should not be instantiated directly. +export abstract class Selection { + /// Initialize a selection with the head and anchor and ranges. If no + /// ranges are given, constructs a single range across `$anchor` and + /// `$head`. + constructor( + /// The resolved anchor of the selection (the side that stays in + /// place when the selection is modified). + readonly $anchor: ResolvedPos, + /// The resolved head of the selection (the side that moves when + /// the selection is modified). + readonly $head: ResolvedPos, + ranges?: readonly SelectionRange[] + ) { + this.ranges = ranges || [new SelectionRange($anchor.min($head), $anchor.max($head))] + } + + /// The ranges covered by the selection. + ranges: readonly SelectionRange[] + + /// The selection's anchor, as an unresolved position. + get anchor() { return this.$anchor.pos } + + /// The selection's head. + get head() { return this.$head.pos } + + /// The lower bound of the selection's main range. + get from() { return this.$from.pos } + + /// The upper bound of the selection's main range. + get to() { return this.$to.pos } + + /// The resolved lower bound of the selection's main range. + get $from() { + return this.ranges[0].$from + } + + /// The resolved upper bound of the selection's main range. + get $to() { + return this.ranges[0].$to + } + + /// Indicates whether the selection contains any content. + get empty(): boolean { + let ranges = this.ranges + for (let i = 0; i < ranges.length; i++) + if (ranges[i].$from.pos != ranges[i].$to.pos) return false + return true + } + + /// Test whether the selection is the same as another selection. + abstract eq(selection: Selection): boolean + + /// Map this selection through a [mappable](#transform.Mappable) + /// thing. `doc` should be the new document to which we are mapping. + abstract map(doc: Node, mapping: Mappable): Selection + + /// Get the content of this selection as a slice. + content() { + return this.$from.doc.slice(this.from, this.to, true) + } + + /// Replace the selection with a slice or, if no slice is given, + /// delete the selection. Will append to the given transaction. + replace(tr: Transaction, content = Slice.empty) { + // Put the new selection at the position after the inserted + // content. When that ended in an inline node, search backwards, + // to get the position after that node. If not, search forward. + let lastNode = content.content.lastChild, lastParent = null + for (let i = 0; i < content.openEnd; i++) { + lastParent = lastNode! + lastNode = lastNode!.lastChild + } + + let mapFrom = tr.steps.length, ranges = this.ranges + for (let i = 0; i < ranges.length; i++) { + let {$from, $to} = ranges[i], mapping = tr.mapping.slice(mapFrom) + tr.replaceRange(mapping.map($from.pos), mapping.map($to.pos), i ? Slice.empty : content) + if (i == 0) + selectionToInsertionEnd(tr, mapFrom, (lastNode ? lastNode.isInline : lastParent && lastParent.isTextblock) ? -1 : 1) + } + } + + /// Replace the selection with the given node, appending the changes + /// to the given transaction. + replaceWith(tr: Transaction, node: Node) { + let mapFrom = tr.steps.length, ranges = this.ranges + for (let i = 0; i < ranges.length; i++) { + let {$from, $to} = ranges[i], mapping = tr.mapping.slice(mapFrom) + let from = mapping.map($from.pos), to = mapping.map($to.pos) + if (i) { + tr.deleteRange(from, to) + } else { + tr.replaceRangeWith(from, to, node) + selectionToInsertionEnd(tr, mapFrom, node.isInline ? -1 : 1) + } + } + } + + /// Convert the selection to a JSON representation. When implementing + /// this for a custom selection class, make sure to give the object a + /// `type` property whose value matches the ID under which you + /// [registered](#state.Selection^jsonID) your class. + abstract toJSON(): any + + /// Find a valid cursor or leaf node selection starting at the given + /// position and searching back if `dir` is negative, and forward if + /// positive. When `textOnly` is true, only consider cursor + /// selections. Will return null when no valid selection position is + /// found. + static findFrom($pos: ResolvedPos, dir: number, textOnly: boolean = false): Selection | null { + let inner = $pos.parent.inlineContent ? new TextSelection($pos) + : findSelectionIn($pos.node(0), $pos.parent, $pos.pos, $pos.index(), dir, textOnly) + if (inner) return inner + + for (let depth = $pos.depth - 1; depth >= 0; depth--) { + let found = dir < 0 + ? findSelectionIn($pos.node(0), $pos.node(depth), $pos.before(depth + 1), $pos.index(depth), dir, textOnly) + : findSelectionIn($pos.node(0), $pos.node(depth), $pos.after(depth + 1), $pos.index(depth) + 1, dir, textOnly) + if (found) return found + } + return null + } + + /// Find a valid cursor or leaf node selection near the given + /// position. Searches forward first by default, but if `bias` is + /// negative, it will search backwards first. + static near($pos: ResolvedPos, bias = 1): Selection { + return this.findFrom($pos, bias) || this.findFrom($pos, -bias) || new AllSelection($pos.node(0)) + } + + /// Find the cursor or leaf node selection closest to the start of + /// the given document. Will return an + /// [`AllSelection`](#state.AllSelection) if no valid position + /// exists. + static atStart(doc: Node): Selection { + return findSelectionIn(doc, doc, 0, 0, 1) || new AllSelection(doc) + } + + /// Find the cursor or leaf node selection closest to the end of the + /// given document. + static atEnd(doc: Node): Selection { + return findSelectionIn(doc, doc, doc.content.size, doc.childCount, -1) || new AllSelection(doc) + } + + /// Deserialize the JSON representation of a selection. Must be + /// implemented for custom classes (as a static class method). + static fromJSON(doc: Node, json: any): Selection { + if (!json || !json.type) throw new RangeError("Invalid input for Selection.fromJSON") + let cls = classesById[json.type] + if (!cls) throw new RangeError(`No selection type ${json.type} defined`) + return cls.fromJSON(doc, json) + } + + /// To be able to deserialize selections from JSON, custom selection + /// classes must register themselves with an ID string, so that they + /// can be disambiguated. Try to pick something that's unlikely to + /// clash with classes from other modules. + static jsonID(id: string, selectionClass: {fromJSON: (doc: Node, json: any) => Selection}) { + if (id in classesById) throw new RangeError("Duplicate use of selection JSON ID " + id) + classesById[id] = selectionClass + ;(selectionClass as any).prototype.jsonID = id + return selectionClass + } + + /// Get a [bookmark](#state.SelectionBookmark) for this selection, + /// which is a value that can be mapped without having access to a + /// current document, and later resolved to a real selection for a + /// given document again. (This is used mostly by the history to + /// track and restore old selections.) The default implementation of + /// this method just converts the selection to a text selection and + /// returns the bookmark for that. + getBookmark(): SelectionBookmark { + return TextSelection.between(this.$anchor, this.$head).getBookmark() + } + + /// Controls whether, when a selection of this type is active in the + /// browser, the selected range should be visible to the user. + /// Defaults to `true`. + declare visible: boolean +} + +Selection.prototype.visible = true + +/// A lightweight, document-independent representation of a selection. +/// You can define a custom bookmark type for a custom selection class +/// to make the history handle it well. +export interface SelectionBookmark { + /// Map the bookmark through a set of changes. + map: (mapping: Mappable) => SelectionBookmark + + /// Resolve the bookmark to a real selection again. This may need to + /// do some error checking and may fall back to a default (usually + /// [`TextSelection.between`](#state.TextSelection^between)) if + /// mapping made the bookmark invalid. + resolve: (doc: Node) => Selection +} + +/// Represents a selected range in a document. +export class SelectionRange { + /// Create a range. + constructor( + /// The lower bound of the range. + readonly $from: ResolvedPos, + /// The upper bound of the range. + readonly $to: ResolvedPos + ) {} +} + +let warnedAboutTextSelection = false +function checkTextSelection($pos: ResolvedPos) { + if (!warnedAboutTextSelection && !$pos.parent.inlineContent) { + warnedAboutTextSelection = true + console["warn"]("TextSelection endpoint not pointing into a node with inline content (" + $pos.parent.type.name + ")") + } +} + +/// A text selection represents a classical editor selection, with a +/// head (the moving side) and anchor (immobile side), both of which +/// point into textblock nodes. It can be empty (a regular cursor +/// position). +export class TextSelection extends Selection { + /// Construct a text selection between the given points. + constructor($anchor: ResolvedPos, $head = $anchor) { + checkTextSelection($anchor) + checkTextSelection($head) + super($anchor, $head) + } + + /// Returns a resolved position if this is a cursor selection (an + /// empty text selection), and null otherwise. + get $cursor() { return this.$anchor.pos == this.$head.pos ? this.$head : null } + + map(doc: Node, mapping: Mappable): Selection { + let $head = doc.resolve(mapping.map(this.head)) + if (!$head.parent.inlineContent) return Selection.near($head) + let $anchor = doc.resolve(mapping.map(this.anchor)) + return new TextSelection($anchor.parent.inlineContent ? $anchor : $head, $head) + } + + replace(tr: Transaction, content = Slice.empty) { + super.replace(tr, content) + if (content == Slice.empty) { + let marks = this.$from.marksAcross(this.$to) + if (marks) tr.ensureMarks(marks) + } + } + + eq(other: Selection): boolean { + return other instanceof TextSelection && other.anchor == this.anchor && other.head == this.head + } + + getBookmark() { + return new TextBookmark(this.anchor, this.head) + } + + toJSON(): any { + return {type: "text", anchor: this.anchor, head: this.head} + } + + /// @internal + static fromJSON(doc: Node, json: any) { + if (typeof json.anchor != "number" || typeof json.head != "number") + throw new RangeError("Invalid input for TextSelection.fromJSON") + return new TextSelection(doc.resolve(json.anchor), doc.resolve(json.head)) + } + + /// Create a text selection from non-resolved positions. + static create(doc: Node, anchor: number, head = anchor) { + let $anchor = doc.resolve(anchor) + return new this($anchor, head == anchor ? $anchor : doc.resolve(head)) + } + + /// Return a text selection that spans the given positions or, if + /// they aren't text positions, find a text selection near them. + /// `bias` determines whether the method searches forward (default) + /// or backwards (negative number) first. Will fall back to calling + /// [`Selection.near`](#state.Selection^near) when the document + /// doesn't contain a valid text position. + static between($anchor: ResolvedPos, $head: ResolvedPos, bias?: number): Selection { + let dPos = $anchor.pos - $head.pos + if (!bias || dPos) bias = dPos >= 0 ? 1 : -1 + if (!$head.parent.inlineContent) { + let found = Selection.findFrom($head, bias, true) || Selection.findFrom($head, -bias, true) + if (found) $head = found.$head + else return Selection.near($head, bias) + } + if (!$anchor.parent.inlineContent) { + if (dPos == 0) { + $anchor = $head + } else { + $anchor = (Selection.findFrom($anchor, -bias, true) || Selection.findFrom($anchor, bias, true))!.$anchor + if (($anchor.pos < $head.pos) != (dPos < 0)) $anchor = $head + } + } + return new TextSelection($anchor, $head) + } +} + +Selection.jsonID("text", TextSelection) + +class TextBookmark { + constructor(readonly anchor: number, readonly head: number) {} + + map(mapping: Mappable) { + return new TextBookmark(mapping.map(this.anchor), mapping.map(this.head)) + } + resolve(doc: Node) { + return TextSelection.between(doc.resolve(this.anchor), doc.resolve(this.head)) + } +} + +/// A node selection is a selection that points at a single node. All +/// nodes marked [selectable](#model.NodeSpec.selectable) can be the +/// target of a node selection. In such a selection, `from` and `to` +/// point directly before and after the selected node, `anchor` equals +/// `from`, and `head` equals `to`.. +export class NodeSelection extends Selection { + /// Create a node selection. Does not verify the validity of its + /// argument. + constructor($pos: ResolvedPos) { + let node = $pos.nodeAfter! + let $end = $pos.node(0).resolve($pos.pos + node.nodeSize) + super($pos, $end) + this.node = node + } + + /// The selected node. + node: Node + + map(doc: Node, mapping: Mappable): Selection { + let {deleted, pos} = mapping.mapResult(this.anchor) + let $pos = doc.resolve(pos) + if (deleted) return Selection.near($pos) + return new NodeSelection($pos) + } + + content() { + return new Slice(Fragment.from(this.node), 0, 0) + } + + eq(other: Selection): boolean { + return other instanceof NodeSelection && other.anchor == this.anchor + } + + toJSON(): any { + return {type: "node", anchor: this.anchor} + } + + getBookmark() { return new NodeBookmark(this.anchor) } + + /// @internal + static fromJSON(doc: Node, json: any) { + if (typeof json.anchor != "number") + throw new RangeError("Invalid input for NodeSelection.fromJSON") + return new NodeSelection(doc.resolve(json.anchor)) + } + + /// Create a node selection from non-resolved positions. + static create(doc: Node, from: number) { + return new NodeSelection(doc.resolve(from)) + } + + /// Determines whether the given node may be selected as a node + /// selection. + static isSelectable(node: Node) { + return !node.isText && node.type.spec.selectable !== false + } +} + +NodeSelection.prototype.visible = false + +Selection.jsonID("node", NodeSelection) + +class NodeBookmark { + constructor(readonly anchor: number) {} + map(mapping: Mappable) { + let {deleted, pos} = mapping.mapResult(this.anchor) + return deleted ? new TextBookmark(pos, pos) : new NodeBookmark(pos) + } + resolve(doc: Node) { + let $pos = doc.resolve(this.anchor), node = $pos.nodeAfter + if (node && NodeSelection.isSelectable(node)) return new NodeSelection($pos) + return Selection.near($pos) + } +} + +/// A selection type that represents selecting the whole document +/// (which can not necessarily be expressed with a text selection, when +/// there are for example leaf block nodes at the start or end of the +/// document). +export class AllSelection extends Selection { + /// Create an all-selection over the given document. + constructor(doc: Node) { + super(doc.resolve(0), doc.resolve(doc.content.size)) + } + + replace(tr: Transaction, content = Slice.empty) { + if (content == Slice.empty) { + tr.delete(0, tr.doc.content.size) + let sel = Selection.atStart(tr.doc) + if (!sel.eq(tr.selection)) tr.setSelection(sel) + } else { + super.replace(tr, content) + } + } + + toJSON(): any { return {type: "all"} } + + /// @internal + static fromJSON(doc: Node) { return new AllSelection(doc) } + + map(doc: Node) { return new AllSelection(doc) } + + eq(other: Selection) { return other instanceof AllSelection } + + getBookmark() { return AllBookmark } +} + +Selection.jsonID("all", AllSelection) + +const AllBookmark = { + map() { return this }, + resolve(doc: Node) { return new AllSelection(doc) } +} + +// FIXME we'll need some awareness of text direction when scanning for selections + +// Try to find a selection inside the given node. `pos` points at the +// position where the search starts. When `text` is true, only return +// text selections. +function findSelectionIn(doc: Node, node: Node, pos: number, index: number, dir: number, text = false): Selection | null { + if (node.inlineContent) return TextSelection.create(doc, pos) + for (let i = index - (dir > 0 ? 0 : 1); dir > 0 ? i < node.childCount : i >= 0; i += dir) { + let child = node.child(i) + if (!child.isAtom) { + let inner = findSelectionIn(doc, child, pos + dir, dir < 0 ? child.childCount : 0, dir, text) + if (inner) return inner + } else if (!text && NodeSelection.isSelectable(child)) { + return NodeSelection.create(doc, pos - (dir < 0 ? child.nodeSize : 0)) + } + pos += child.nodeSize * dir + } + return null +} + +function selectionToInsertionEnd(tr: Transaction, startLen: number, bias: number) { + let last = tr.steps.length - 1 + if (last < startLen) return + let step = tr.steps[last] + if (!(step instanceof ReplaceStep || step instanceof ReplaceAroundStep)) return + let map = tr.mapping.maps[last], end: number | undefined + map.forEach((_from, _to, _newFrom, newTo) => { if (end == null) end = newTo }) + tr.setSelection(Selection.near(tr.doc.resolve(end!), bias)) +} diff --git a/third_party/js/prosemirror/prosemirror-state/src/state.ts b/third_party/js/prosemirror/prosemirror-state/src/state.ts @@ -0,0 +1,266 @@ +import {Node, Mark, Schema} from "prosemirror-model" + +import {Selection, TextSelection} from "./selection" +import {Transaction} from "./transaction" +import {Plugin, StateField} from "./plugin" + +function bind<T extends Function>(f: T, self: any): T { + return !self || !f ? f : f.bind(self) +} + +class FieldDesc<T> { + init: (config: EditorStateConfig, instance: EditorState) => T + apply: (tr: Transaction, value: T, oldState: EditorState, newState: EditorState) => T + + constructor(readonly name: string, desc: StateField<any>, self?: any) { + this.init = bind(desc.init, self) + this.apply = bind(desc.apply, self) + } +} + +const baseFields = [ + new FieldDesc<Node>("doc", { + init(config) { return config.doc || config.schema!.topNodeType.createAndFill() }, + apply(tr) { return tr.doc } + }), + + new FieldDesc<Selection>("selection", { + init(config, instance) { return config.selection || Selection.atStart(instance.doc) }, + apply(tr) { return tr.selection } + }), + + new FieldDesc<readonly Mark[] | null>("storedMarks", { + init(config) { return config.storedMarks || null }, + apply(tr, _marks, _old, state) { return (state.selection as TextSelection).$cursor ? tr.storedMarks : null } + }), + + new FieldDesc<number>("scrollToSelection", { + init() { return 0 }, + apply(tr, prev) { return tr.scrolledIntoView ? prev + 1 : prev } + }) +] + +// Object wrapping the part of a state object that stays the same +// across transactions. Stored in the state's `config` property. +class Configuration { + fields: FieldDesc<any>[] + plugins: Plugin[] = [] + pluginsByKey: {[key: string]: Plugin} = Object.create(null) + + constructor(readonly schema: Schema, plugins?: readonly Plugin[]) { + this.fields = baseFields.slice() + if (plugins) plugins.forEach(plugin => { + if (this.pluginsByKey[plugin.key]) + throw new RangeError("Adding different instances of a keyed plugin (" + plugin.key + ")") + this.plugins.push(plugin) + this.pluginsByKey[plugin.key] = plugin + if (plugin.spec.state) + this.fields.push(new FieldDesc<any>(plugin.key, plugin.spec.state, plugin)) + }) + } +} + +/// The type of object passed to +/// [`EditorState.create`](#state.EditorState^create). +export interface EditorStateConfig { + /// The schema to use (only relevant if no `doc` is specified). + schema?: Schema + + /// The starting document. Either this or `schema` _must_ be + /// provided. + doc?: Node + + /// A valid selection in the document. + selection?: Selection + + /// The initial set of [stored marks](#state.EditorState.storedMarks). + storedMarks?: readonly Mark[] | null + + /// The plugins that should be active in this state. + plugins?: readonly Plugin[] +} + +/// The state of a ProseMirror editor is represented by an object of +/// this type. A state is a persistent data structure—it isn't +/// updated, but rather a new state value is computed from an old one +/// using the [`apply`](#state.EditorState.apply) method. +/// +/// A state holds a number of built-in fields, and plugins can +/// [define](#state.PluginSpec.state) additional fields. +export class EditorState { + /// @internal + constructor( + /// @internal + readonly config: Configuration + ) {} + + /// The current document. + declare doc: Node + + /// The selection. + declare selection: Selection + + /// A set of marks to apply to the next input. Will be null when + /// no explicit marks have been set. + declare storedMarks: readonly Mark[] | null + + /// The schema of the state's document. + get schema(): Schema { + return this.config.schema + } + + /// The plugins that are active in this state. + get plugins(): readonly Plugin[] { + return this.config.plugins + } + + /// Apply the given transaction to produce a new state. + apply(tr: Transaction): EditorState { + return this.applyTransaction(tr).state + } + + /// @internal + filterTransaction(tr: Transaction, ignore = -1) { + for (let i = 0; i < this.config.plugins.length; i++) if (i != ignore) { + let plugin = this.config.plugins[i] + if (plugin.spec.filterTransaction && !plugin.spec.filterTransaction.call(plugin, tr, this)) + return false + } + return true + } + + /// Verbose variant of [`apply`](#state.EditorState.apply) that + /// returns the precise transactions that were applied (which might + /// be influenced by the [transaction + /// hooks](#state.PluginSpec.filterTransaction) of + /// plugins) along with the new state. + applyTransaction(rootTr: Transaction): {state: EditorState, transactions: readonly Transaction[]} { + if (!this.filterTransaction(rootTr)) return {state: this, transactions: []} + + let trs = [rootTr], newState = this.applyInner(rootTr), seen = null + // This loop repeatedly gives plugins a chance to respond to + // transactions as new transactions are added, making sure to only + // pass the transactions the plugin did not see before. + for (;;) { + let haveNew = false + for (let i = 0; i < this.config.plugins.length; i++) { + let plugin = this.config.plugins[i] + if (plugin.spec.appendTransaction) { + let n = seen ? seen[i].n : 0, oldState = seen ? seen[i].state : this + let tr = n < trs.length && + plugin.spec.appendTransaction.call(plugin, n ? trs.slice(n) : trs, oldState, newState) + if (tr && newState.filterTransaction(tr, i)) { + tr.setMeta("appendedTransaction", rootTr) + if (!seen) { + seen = [] + for (let j = 0; j < this.config.plugins.length; j++) + seen.push(j < i ? {state: newState, n: trs.length} : {state: this, n: 0}) + } + trs.push(tr) + newState = newState.applyInner(tr) + haveNew = true + } + if (seen) seen[i] = {state: newState, n: trs.length} + } + } + if (!haveNew) return {state: newState, transactions: trs} + } + } + + /// @internal + applyInner(tr: Transaction) { + if (!tr.before.eq(this.doc)) throw new RangeError("Applying a mismatched transaction") + let newInstance = new EditorState(this.config), fields = this.config.fields + for (let i = 0; i < fields.length; i++) { + let field = fields[i] + ;(newInstance as any)[field.name] = field.apply(tr, (this as any)[field.name], this, newInstance) + } + return newInstance + } + + /// Accessor that constructs and returns a new [transaction](#state.Transaction) from this state. + get tr(): Transaction { return new Transaction(this) } + + /// Create a new state. + static create(config: EditorStateConfig) { + let $config = new Configuration(config.doc ? config.doc.type.schema : config.schema!, config.plugins) + let instance = new EditorState($config) + for (let i = 0; i < $config.fields.length; i++) + (instance as any)[$config.fields[i].name] = $config.fields[i].init(config, instance) + return instance + } + + /// Create a new state based on this one, but with an adjusted set + /// of active plugins. State fields that exist in both sets of + /// plugins are kept unchanged. Those that no longer exist are + /// dropped, and those that are new are initialized using their + /// [`init`](#state.StateField.init) method, passing in the new + /// configuration object.. + reconfigure(config: { + /// New set of active plugins. + plugins?: readonly Plugin[] + }) { + let $config = new Configuration(this.schema, config.plugins) + let fields = $config.fields, instance = new EditorState($config) + for (let i = 0; i < fields.length; i++) { + let name = fields[i].name + ;(instance as any)[name] = this.hasOwnProperty(name) ? (this as any)[name] : fields[i].init(config, instance) + } + return instance + } + + /// Serialize this state to JSON. If you want to serialize the state + /// of plugins, pass an object mapping property names to use in the + /// resulting JSON object to plugin objects. The argument may also be + /// a string or number, in which case it is ignored, to support the + /// way `JSON.stringify` calls `toString` methods. + toJSON(pluginFields?: {[propName: string]: Plugin}): any { + let result: any = {doc: this.doc.toJSON(), selection: this.selection.toJSON()} + if (this.storedMarks) result.storedMarks = this.storedMarks.map(m => m.toJSON()) + if (pluginFields && typeof pluginFields == 'object') for (let prop in pluginFields) { + if (prop == "doc" || prop == "selection") + throw new RangeError("The JSON fields `doc` and `selection` are reserved") + let plugin = pluginFields[prop], state = plugin.spec.state + if (state && state.toJSON) result[prop] = state.toJSON.call(plugin, (this as any)[plugin.key]) + } + return result + } + + /// Deserialize a JSON representation of a state. `config` should + /// have at least a `schema` field, and should contain array of + /// plugins to initialize the state with. `pluginFields` can be used + /// to deserialize the state of plugins, by associating plugin + /// instances with the property names they use in the JSON object. + static fromJSON(config: { + /// The schema to use. + schema: Schema + /// The set of active plugins. + plugins?: readonly Plugin[] + }, json: any, pluginFields?: {[propName: string]: Plugin}) { + if (!json) throw new RangeError("Invalid input for EditorState.fromJSON") + if (!config.schema) throw new RangeError("Required config field 'schema' missing") + let $config = new Configuration(config.schema, config.plugins) + let instance = new EditorState($config) + $config.fields.forEach(field => { + if (field.name == "doc") { + instance.doc = Node.fromJSON(config.schema, json.doc) + } else if (field.name == "selection") { + instance.selection = Selection.fromJSON(instance.doc, json.selection) + } else if (field.name == "storedMarks") { + if (json.storedMarks) instance.storedMarks = json.storedMarks.map(config.schema.markFromJSON) + } else { + if (pluginFields) for (let prop in pluginFields) { + let plugin = pluginFields[prop], state = plugin.spec.state + if (plugin.key == field.name && state && state.fromJSON && + Object.prototype.hasOwnProperty.call(json, prop)) { + // This field belongs to a plugin mapped to a JSON field, read it from there. + ;(instance as any)[field.name] = state.fromJSON.call(plugin, config, json[prop], instance) + return + } + } + ;(instance as any)[field.name] = field.init(config, instance) + } + }) + return instance + } +} diff --git a/third_party/js/prosemirror/prosemirror-state/src/transaction.ts b/third_party/js/prosemirror/prosemirror-state/src/transaction.ts @@ -0,0 +1,215 @@ +import {Transform, Step} from "prosemirror-transform" +import {Mark, MarkType, Node, Slice} from "prosemirror-model" +import {type EditorView} from "prosemirror-view" +import {Selection} from "./selection" +import {Plugin, PluginKey} from "./plugin" +import {EditorState} from "./state" + +/// Commands are functions that take a state and a an optional +/// transaction dispatch function and... +/// +/// - determine whether they apply to this state +/// - if not, return false +/// - if `dispatch` was passed, perform their effect, possibly by +/// passing a transaction to `dispatch` +/// - return true +/// +/// In some cases, the editor view is passed as a third argument. +export type Command = (state: EditorState, dispatch?: (tr: Transaction) => void, view?: EditorView) => boolean + +const UPDATED_SEL = 1, UPDATED_MARKS = 2, UPDATED_SCROLL = 4 + +/// An editor state transaction, which can be applied to a state to +/// create an updated state. Use +/// [`EditorState.tr`](#state.EditorState.tr) to create an instance. +/// +/// Transactions track changes to the document (they are a subclass of +/// [`Transform`](#transform.Transform)), but also other state changes, +/// like selection updates and adjustments of the set of [stored +/// marks](#state.EditorState.storedMarks). In addition, you can store +/// metadata properties in a transaction, which are extra pieces of +/// information that client code or plugins can use to describe what a +/// transaction represents, so that they can update their [own +/// state](#state.StateField) accordingly. +/// +/// The [editor view](#view.EditorView) uses a few metadata +/// properties: it will attach a property `"pointer"` with the value +/// `true` to selection transactions directly caused by mouse or touch +/// input, a `"composition"` property holding an ID identifying the +/// composition that caused it to transactions caused by composed DOM +/// input, and a `"uiEvent"` property of that may be `"paste"`, +/// `"cut"`, or `"drop"`. +export class Transaction extends Transform { + /// The timestamp associated with this transaction, in the same + /// format as `Date.now()`. + time: number + + private curSelection: Selection + // The step count for which the current selection is valid. + private curSelectionFor = 0 + // Bitfield to track which aspects of the state were updated by + // this transaction. + private updated = 0 + // Object used to store metadata properties for the transaction. + private meta: {[name: string]: any} = Object.create(null) + + /// The stored marks set by this transaction, if any. + storedMarks: readonly Mark[] | null + + /// @internal + constructor(state: EditorState) { + super(state.doc) + this.time = Date.now() + this.curSelection = state.selection + this.storedMarks = state.storedMarks + } + + /// The transaction's current selection. This defaults to the editor + /// selection [mapped](#state.Selection.map) through the steps in the + /// transaction, but can be overwritten with + /// [`setSelection`](#state.Transaction.setSelection). + get selection(): Selection { + if (this.curSelectionFor < this.steps.length) { + this.curSelection = this.curSelection.map(this.doc, this.mapping.slice(this.curSelectionFor)) + this.curSelectionFor = this.steps.length + } + return this.curSelection + } + + /// Update the transaction's current selection. Will determine the + /// selection that the editor gets when the transaction is applied. + setSelection(selection: Selection): this { + if (selection.$from.doc != this.doc) + throw new RangeError("Selection passed to setSelection must point at the current document") + this.curSelection = selection + this.curSelectionFor = this.steps.length + this.updated = (this.updated | UPDATED_SEL) & ~UPDATED_MARKS + this.storedMarks = null + return this + } + + /// Whether the selection was explicitly updated by this transaction. + get selectionSet() { + return (this.updated & UPDATED_SEL) > 0 + } + + /// Set the current stored marks. + setStoredMarks(marks: readonly Mark[] | null): this { + this.storedMarks = marks + this.updated |= UPDATED_MARKS + return this + } + + /// Make sure the current stored marks or, if that is null, the marks + /// at the selection, match the given set of marks. Does nothing if + /// this is already the case. + ensureMarks(marks: readonly Mark[]): this { + if (!Mark.sameSet(this.storedMarks || this.selection.$from.marks(), marks)) + this.setStoredMarks(marks) + return this + } + + /// Add a mark to the set of stored marks. + addStoredMark(mark: Mark): this { + return this.ensureMarks(mark.addToSet(this.storedMarks || this.selection.$head.marks())) + } + + /// Remove a mark or mark type from the set of stored marks. + removeStoredMark(mark: Mark | MarkType): this { + return this.ensureMarks(mark.removeFromSet(this.storedMarks || this.selection.$head.marks())) + } + + /// Whether the stored marks were explicitly set for this transaction. + get storedMarksSet() { + return (this.updated & UPDATED_MARKS) > 0 + } + + /// @internal + addStep(step: Step, doc: Node) { + super.addStep(step, doc) + this.updated = this.updated & ~UPDATED_MARKS + this.storedMarks = null + } + + /// Update the timestamp for the transaction. + setTime(time: number): this { + this.time = time + return this + } + + /// Replace the current selection with the given slice. + replaceSelection(slice: Slice): this { + this.selection.replace(this, slice) + return this + } + + /// Replace the selection with the given node. When `inheritMarks` is + /// true and the content is inline, it inherits the marks from the + /// place where it is inserted. + replaceSelectionWith(node: Node, inheritMarks = true): this { + let selection = this.selection + if (inheritMarks) + node = node.mark(this.storedMarks || (selection.empty ? selection.$from.marks() : (selection.$from.marksAcross(selection.$to) || Mark.none))) + selection.replaceWith(this, node) + return this + } + + /// Delete the selection. + deleteSelection(): this { + this.selection.replace(this) + return this + } + + /// Replace the given range, or the selection if no range is given, + /// with a text node containing the given string. + insertText(text: string, from?: number, to?: number): this { + let schema = this.doc.type.schema + if (from == null) { + if (!text) return this.deleteSelection() + return this.replaceSelectionWith(schema.text(text), true) + } else { + if (to == null) to = from + if (!text) return this.deleteRange(from, to) + let marks = this.storedMarks + if (!marks) { + let $from = this.doc.resolve(from) + marks = to == from ? $from.marks() : $from.marksAcross(this.doc.resolve(to)) + } + this.replaceRangeWith(from, to, schema.text(text, marks)) + if (!this.selection.empty && this.selection.to == from + text.length) + this.setSelection(Selection.near(this.selection.$to)) + return this + } + } + + /// Store a metadata property in this transaction, keyed either by + /// name or by plugin. + setMeta(key: string | Plugin | PluginKey, value: any): this { + this.meta[typeof key == "string" ? key : key.key] = value + return this + } + + /// Retrieve a metadata property for a given name or plugin. + getMeta(key: string | Plugin | PluginKey) { + return this.meta[typeof key == "string" ? key : key.key] + } + + /// Returns true if this transaction doesn't contain any metadata, + /// and can thus safely be extended. + get isGeneric() { + for (let _ in this.meta) return false + return true + } + + /// Indicate that the editor should scroll the selection into view + /// when updated to the state produced by this transaction. + scrollIntoView(): this { + this.updated |= UPDATED_SCROLL + return this + } + + /// True when this transaction has had `scrollIntoView` called on it. + get scrolledIntoView() { + return (this.updated & UPDATED_SCROLL) > 0 + } +} diff --git a/third_party/js/prosemirror/prosemirror-suggestions/.babelrc b/third_party/js/prosemirror/prosemirror-suggestions/.babelrc @@ -0,0 +1,13 @@ +{ + "presets": [ + ["es2015", { "modules": false }] + ], + "plugins": [ + "transform-object-rest-spread" + ], + "env": { + "test": { + "presets": ["es2015"] + } + } +} +\ No newline at end of file diff --git a/third_party/js/prosemirror/prosemirror-suggestions/LICENSE b/third_party/js/prosemirror/prosemirror-suggestions/LICENSE @@ -0,0 +1,14 @@ +Copyright 2017 Quartzy, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + diff --git a/third_party/js/prosemirror/prosemirror-suggestions/moz.yaml b/third_party/js/prosemirror/prosemirror-suggestions/moz.yaml @@ -0,0 +1,65 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror-suggestions + + description: A plugin for ProseMirror that helps you add suggestions to your editor. + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: v0.1.1 (2017-10-05T23:32:20-07:00). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: v0.1.1 + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: Apache-2.0 + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/quartzy/prosemirror-suggestions + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + # Whether to track by commit or tag + tracking: tag + + exclude: + - "**" + + include: + - LICENSE + - .babelrc + - src/ + - rollup.config.js + - package.json diff --git a/third_party/js/prosemirror/prosemirror-suggestions/package.json b/third_party/js/prosemirror/prosemirror-suggestions/package.json @@ -0,0 +1,36 @@ +{ + "name": "@quartzy/prosemirror-suggestions", + "version": "0.1.1", + "description": "ProseMirror plugin for suggestions (i.e. mentions, tags)", + "main": "dist/index.js", + "author": "Tristan Pemble", + "license": "Apache-2.0", + "peerDependencies": { + "prosemirror-markdown": "^0.24.0", + "prosemirror-state": "^0.24.0", + "prosemirror-view": "^0.24.0" + }, + "devDependencies": { + "@types/jest": "^21.1.2", + "babel": "^6.23.0", + "babel-core": "^6.26.0", + "babel-jest": "^21.2.0", + "babel-plugin-external-helpers": "^6.22.0", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-preset-es2015": "^6.24.1", + "jest": "^21.2.1", + "prosemirror-markdown": "^0.24.0", + "prosemirror-schema-basic": "^0.24.0", + "prosemirror-state": "^0.24.0", + "prosemirror-view": "^0.24.0", + "regenerator-runtime": "^0.11.0", + "rollup": "^0.50.0", + "rollup-plugin-babel": "^3.0.2" + }, + "scripts": { + "build": "rollup -c", + "watch": "rollup -c -w", + "prepare": "npm run build", + "test": "jest" + } +} diff --git a/third_party/js/prosemirror/prosemirror-suggestions/rollup.config.js b/third_party/js/prosemirror/prosemirror-suggestions/rollup.config.js @@ -0,0 +1,16 @@ +import babel from 'rollup-plugin-babel'; + +export default { + input: './src/index.js', + output: { + format: 'cjs', + file: 'dist/index.js' + }, + sourcemap: true, + plugins: [ + babel() + ], + external(id) { + return !/^[\.\/]/.test(id) + }, +}; diff --git a/third_party/js/prosemirror/prosemirror-suggestions/src/index.js b/third_party/js/prosemirror/prosemirror-suggestions/src/index.js @@ -0,0 +1,10 @@ +export { suggestionsPlugin, triggerCharacter } from './suggestions'; +export { addTagNodes, tagNodeSpec } from './tags'; +export { + addMentionNodes, + mentionNodeSpec, + markdownParser, + markdownSerializer, + addMentionsToMarkdownParser, + addMentionsToMarkdownSerializer, +} from './mentions'; diff --git a/third_party/js/prosemirror/prosemirror-suggestions/src/mentions.js b/third_party/js/prosemirror/prosemirror-suggestions/src/mentions.js @@ -0,0 +1,79 @@ +import { MarkdownSerializer, MarkdownParser } from 'prosemirror-markdown'; + +/** + * @type {NodeSpec} + */ +export const mentionNodeSpec = { + attrs: { + type: {}, + id: {}, + label: {}, + }, + + group: 'inline', + inline: true, + selectable: false, + atom: true, + + toDOM: node => ['span', { + 'class': 'mention', + 'data-mention-type': node.attrs.type, + 'data-mention-id': node.attrs.id, + }, `@${node.attrs.label}`], + + parseDOM: [{ + tag: 'span[data-mention-type][data-mention-id]', + + /** + * @param {Element} dom + * @returns {{type: string, id: string, label: string}} + */ + getAttrs: dom => { + const type = dom.getAttribute('data-mention-type'); + const id = dom.getAttribute('data-mention-id'); + const label = dom.innerText; + + return { type, id, label }; + }, + }], +}; + +/** + * @param {OrderedMap} nodes + * @returns {OrderedMap} + */ +export function addMentionNodes(nodes) { + return nodes.append({ + mention: mentionNodeSpec, + }); +} + +export function markdownSerializer() { + return (state, node) => { + const label = state.esc(node.attrs.label || ''); + const uri = state.esc(`mention://${node.attrs.type}/${node.attrs.id}`); + + state.write(`@[${label}](${uri})`) + }; +} + +export function addMentionsToMarkdownSerializer(serializer) { + return new MarkdownSerializer({ + ...serializer.nodes, + mention: markdownSerializer(), + }, serializer.marks); +} + +export function markdownParser() { + return { + node: 'mention', + getAttrs: ({ mention: { type, id, label } }) => ({ type, id, label }), + }; +} + +export function addMentionsToMarkdownParser(parser) { + return new MarkdownParser(parser.schema, parser.tokenizer, { + ...parser.tokens, + mention: markdownParser(), + }); +} diff --git a/third_party/js/prosemirror/prosemirror-suggestions/src/suggestions.js b/third_party/js/prosemirror/prosemirror-suggestions/src/suggestions.js @@ -0,0 +1,187 @@ +import { Plugin, PluginKey } from 'prosemirror-state'; +import { Decoration, DecorationSet } from 'prosemirror-view'; + +/** + * Create a matcher that matches when a specific character is typed. Useful for @mentions and #tags. + * + * @param {String} char + * @param {Boolean} allowSpaces + * @returns {function(*)} + */ +export function triggerCharacter(char, { allowSpaces = false }) { + /** + * @param {ResolvedPos} $position + */ + return $position => { + // Matching expressions used for later + const suffix = new RegExp(`\\s${char}$`); + const regexp = allowSpaces + ? new RegExp(`${char}.*?(?=\\s${char}|$)`, 'g') + : new RegExp(`(?:^)?${char}[^\\s${char}]*`, 'g'); + + // Lookup the boundaries of the current node + const textFrom = $position.before(); + const textTo = $position.end(); + + const text = $position.doc.textBetween(textFrom, textTo, '\0', '\0'); + + let match; + + while (match = regexp.exec(text)) { + // Javascript doesn't have lookbehinds; this hacks a check that first character is " " or the line beginning + const prefix = match.input.slice(Math.max(0, match.index - 1), match.index); + if (!/^[\s\0]?$/.test(prefix)) { + continue; + } + + // The absolute position of the match in the document + const from = match.index + $position.start(); + let to = from + match[0].length; + + // Edge case handling; if spaces are allowed and we're directly in between two triggers + if (allowSpaces && suffix.test(text.slice(to - 1, to + 1))) { + match[0] += ' '; + to++; + } + + // If the $position is located within the matched substring, return that range + if (from < $position.pos && to >= $position.pos) { + return { range: { from, to }, text: match[0] }; + } + } + } +} + +/** + * @returns {Plugin} + */ +export function suggestionsPlugin({ + matcher = triggerCharacter('#'), + suggestionClass = 'ProseMirror-suggestion', + onEnter = () => false, + onChange = () => false, + onExit = () => false, + onKeyDown = () => false, + debug = false, +}) { + return new Plugin({ + key: new PluginKey('suggestions'), + + view() { + return { + update: (view, prevState) => { + const prev = this.key.getState(prevState); + const next = this.key.getState(view.state); + + // See how the state changed + const moved = prev.active && next.active && prev.range.from !== next.range.from; + const started = !prev.active && next.active; + const stopped = prev.active && !next.active; + const changed = !started && !stopped && prev.text !== next.text; + + // Trigger the hooks when necessary + if (stopped || moved) onExit({ view, range: prev.range, text: prev.text }); + if (changed && !moved) onChange({ view, range: next.range, text: next.text }); + if (started || moved) onEnter({ view, range: next.range, text: next.text }); + }, + }; + }, + + state: { + /** + * Initialize the plugin's internal state. + * + * @returns {Object} + */ + init() { + return { + active: false, + range: {}, + text: null, + }; + }, + + /** + * Apply changes to the plugin state from a view transaction. + * + * @param {Transaction} tr + * @param {Object} prev + * + * @returns {Object} + */ + apply(tr, prev) { + const { selection } = tr; + const next = { ...prev }; + + // We can only be suggesting if there is no selection + if (selection.from === selection.to) { + // Reset active state if we just left the previous suggestion range + if (selection.from < prev.range.from || selection.from > prev.range.to) { + next.active = false; + } + + // Try to match against where our cursor currently is + const $position = selection.$from; + const match = matcher($position); + + // If we found a match, update the current state to show it + if (match) { + next.active = true; + next.range = match.range; + next.text = match.text; + } else { + next.active = false; + } + } else { + next.active = false; + } + + // Make sure to empty the range if suggestion is inactive + if (!next.active) { + next.range = {}; + next.text = null; + } + + return next; + }, + }, + + props: { + /** + * Call the keydown hook if suggestion is active. + * + * @param view + * @param event + * @returns {boolean} + */ + handleKeyDown(view, event) { + const { active } = this.getState(view.state); + + if (!active) return false; + + return onKeyDown({ view, event }); + }, + + /** + * Setup decorator on the currently active suggestion. + * + * @param {EditorState} editorState + * + * @returns {?DecorationSet} + */ + decorations(editorState) { + const { active, range } = this.getState(editorState); + + if (!active) return null; + + return DecorationSet.create(editorState.doc, [ + Decoration.inline(range.from, range.to, { + nodeName: 'span', + class: suggestionClass, + style: debug ? 'background: rgba(0, 0, 255, 0.05); color: blue; border: 2px solid blue;' : null, + }), + ]); + }, + }, + }); +} diff --git a/third_party/js/prosemirror/prosemirror-suggestions/src/suggestions.test.js b/third_party/js/prosemirror/prosemirror-suggestions/src/suggestions.test.js @@ -0,0 +1,240 @@ +import { schema } from 'prosemirror-schema-basic'; +import { triggerCharacter } from './suggestions'; + +const CUR = '\u2038'; + +function createPosition(text) { + const position = text.indexOf(CUR) + 1; + const stripped = text.replace(CUR, ''); + + expect(position).toBeGreaterThan(0); + expect(position).toBeLessThanOrEqual(stripped.length + 1); + + const doc = schema.node('doc', null, [ + schema.node('paragraph', null, [ + schema.text(stripped), + ]), + ]); + + return doc.resolve(position); +} + +function mention(text) { + const $position = createPosition(text); + + return triggerCharacter('@', { allowSpaces: true })($position); +} + +function tag(text) { + const $position = createPosition(text); + + return triggerCharacter('#', { allowSpaces: false })($position); +} + +describe('the triggerCharacter matcher', () => { + it('will match when cursor is immediately after a node', () => { + const doc = schema.node('doc', null, [ + schema.node('paragraph', null, [ + schema.node('hard_break'), + schema.text('@mention'), + ]), + ]); + + const $position = doc.resolve(3); + const result = triggerCharacter('@', { allowSpaces: true })($position); + + expect(result).toMatchObject({ + text: '@mention', + }); + }); + + describe(`for tags`, () => { + it(`won't match "${CUR}#"`, () => { + const text = `${CUR}#`; + + expect(tag(text)).toBeUndefined(); + }); + + it(`will match "#${CUR}"`, () => { + const text = `#${CUR}`; + + expect(tag(text)).toMatchObject({ + text: '#', + }); + }); + + it(`won't match "${CUR}#tag"`, () => { + const text = `${CUR}#tag`; + + expect(tag(text)).toBeUndefined(); + }); + + it(`will match "#${CUR}tag"`, () => { + const text = `#${CUR}tag`; + + expect(tag(text)).toMatchObject({ + text: '#tag', + range: { from: 1, to: 5 }, + }); + }); + + it(`will match "#tag-with-dashes${CUR}"`, () => { + const text = `#tag-with-dashes${CUR}`; + + expect(tag(text)).toMatchObject({ + text: '#tag-with-dashes', + }); + }); + + it(`won't match "#tag with spaces${CUR}"`, () => { + const text = `#tag with spaces${CUR}`; + + expect(tag(text)).toBeUndefined(); + }); + + it(`won't match "#tag#with#hashes${CUR}"`, () => { + const text = `#tag#with#hashes${CUR}`; + + expect(tag(text)).toBeUndefined(); + }); + + it(`will match "#multiple${CUR} #tags #separately"`, () => { + const text = `#multiple${CUR} #tags #separately`; + + expect(tag(text)).toMatchObject({ + text: '#multiple', + }); + }); + + it(`will match "#multiple #tags${CUR} #separately"`, () => { + const text = `#multiple #tags${CUR} #separately`; + + expect(tag(text)).toMatchObject({ + text: '#tags', + }); + }); + + it(`will match "#multiple #tags #separately${CUR}"`, () => { + const text = `#multiple #tags #separately${CUR}`; + + expect(tag(text)).toMatchObject({ + text: '#separately', + }); + }); + + it(`won't match "#multiple ${CUR}#tags" when cursor is outside`, () => { + const text = `#multiple ${CUR}#tags`; + + expect(tag(text)).toBeUndefined(); + }); + }); + + describe('for mentions', () => { + it(`won't match "${CUR}@"`, () => { + const text = `${CUR}@`; + + expect(mention(text)).toBeUndefined(); + }); + + it(`will match "@${CUR}"`, () => { + const text = `@${CUR}`; + + expect(mention(text)).toMatchObject({ + text: '@', + }); + }); + + it(`won't match "${CUR}@mention"`, () => { + const text = `${CUR}@mention`; + + expect(mention(text)).toBeUndefined(); + }); + + it(`will match "@${CUR}mention"`, () => { + const text = `@${CUR}mention`; + + expect(mention(text)).toMatchObject({ + text: '@mention', + range: { from: 1, to: 9 }, + }); + }); + + it(`will match "@m${CUR}ention"`, () => { + const text = `@m${CUR}ention`; + + expect(mention(text)).toMatchObject({ + text: '@mention', + }); + }); + + it(`will match "@mention${CUR}"`, () => { + const text = `@mention${CUR}`; + + expect(mention(text)).toMatchObject({ + text: '@mention', + }); + }); + + it(`will match "@mention ${CUR}"`, () => { + const text = `@mention ${CUR}`; + + expect(mention(text)).toMatchObject({ + text: '@mention ', + }); + }); + + it(`will match "@mentions with${CUR} spaces"`, () => { + const text = `@mentions with${CUR} spaces`; + + expect(mention(text)).toMatchObject({ + text: '@mentions with spaces', + }); + }); + + it(`will match "@${CUR}mentions with spaces"`, () => { + const text = `@${CUR}mentions with spaces`; + + expect(mention(text)).toMatchObject({ + text: '@mentions with spaces', + }); + }); + + it(`will match "@multiple ${CUR}@mentions"`, () => { + const text = `@multiple ${CUR}@mentions`; + + expect(mention(text)).toMatchObject({ + text: '@multiple ', + }); + }); + + it(`will match "@multiple @${CUR}mentions"`, () => { + const text = `@multiple @${CUR}mentions`; + + expect(mention(text)).toMatchObject({ + text: '@mentions', + }); + }); + + it(`will match "mentions with text @${CUR}before them"`, () => { + const text = `mentions with text @${CUR}before them`; + + expect(mention(text)).toMatchObject({ + text: '@before them', + }); + }); + + it(`will match "@${CUR}mentioned@email.address"`, () => { + const text = `@${CUR}mentioned@email.address`; + + expect(mention(text)).toMatchObject({ + text: '@mentioned@email.address', + }); + }); + + it(`won't match "normal@${CUR}email.address"`, () => { + const text = `normal@${CUR}email.address`; + + expect(mention(text)).toBeUndefined(); + }); + }); +}); diff --git a/third_party/js/prosemirror/prosemirror-suggestions/src/tags.js b/third_party/js/prosemirror/prosemirror-suggestions/src/tags.js @@ -0,0 +1,45 @@ +/** + * @type {NodeSpec} + */ +export const tagNodeSpec = { + attrs: { + id: {}, + }, + + group: 'inline', + inline: true, + selectable: false, + atom: true, + + /** + * @param {Node} node + */ + toDOM: node => ['span', { + 'class': 'tag', + 'data-tag-id': node.attrs.id, + }, node.attrs.id], + + parseDOM: [{ + tag: 'span[data-tag-id]', + + /** + * @param {Element} dom + * @returns {{id: string}} + */ + getAttrs: dom => { + const id = dom.getAttribute('data-tag-id'); + + return { id }; + }, + }], +}; + +/** + * @param {OrderedMap} nodes + * @returns {OrderedMap} + */ +export function addTagNodes(nodes) { + return nodes.append({ + tag: tagNodeSpec, + }); +} diff --git a/third_party/js/prosemirror/prosemirror-transform/LICENSE b/third_party/js/prosemirror/prosemirror-transform/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/js/prosemirror/prosemirror-transform/moz.yaml b/third_party/js/prosemirror/prosemirror-transform/moz.yaml @@ -0,0 +1,64 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror-transform + + description: ProseMirror document transformations + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: 1.10.5 (2025-11-11T09:10:08+01:00). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: 1.10.5 + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MIT + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/ProseMirror/prosemirror-transform + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + # Whether to track by commit or tag + tracking: tag + + exclude: + - "**" + + include: + - LICENSE + - src/ + - style/ + - package.json diff --git a/third_party/js/prosemirror/prosemirror-transform/package.json b/third_party/js/prosemirror/prosemirror-transform/package.json @@ -0,0 +1,37 @@ +{ + "name": "prosemirror-transform", + "version": "1.10.5", + "description": "ProseMirror document transformations", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "sideEffects": false, + "license": "MIT", + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijn@haverbeke.berlin", + "web": "http://marijnhaverbeke.nl" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/prosemirror/prosemirror-transform.git" + }, + "dependencies": { + "prosemirror-model": "^1.21.0" + }, + "devDependencies": { + "@prosemirror/buildhelper": "^0.1.5", + "prosemirror-test-builder": "^1.0.0" + }, + "scripts": { + "test": "pm-runtests", + "prepare": "pm-buildhelper src/index.ts" + } +} diff --git a/third_party/js/prosemirror/prosemirror-transform/src/README.md b/third_party/js/prosemirror/prosemirror-transform/src/README.md @@ -0,0 +1,59 @@ +This module defines a way of modifying documents that allows changes +to be recorded, replayed, and reordered. You can read more about +transformations in [the guide](/docs/guide/#transform). + +### Steps + +Transforming happens in `Step`s, which are atomic, well-defined +modifications to a document. [Applying](#transform.Step.apply) a step +produces a new document. + +Each step provides a [change map](#transform.StepMap) that maps +positions in the old document to position in the transformed document. +Steps can be [inverted](#transform.Step.invert) to create a step that +undoes their effect, and chained together in a convenience object +called a [`Transform`](#transform.Transform). + +@Step +@StepResult +@ReplaceStep +@ReplaceAroundStep +@AddMarkStep +@RemoveMarkStep +@AddNodeMarkStep +@RemoveNodeMarkStep +@AttrStep +@DocAttrStep + +### Position Mapping + +Mapping positions from one document to another by running through the +[step maps](#transform.StepMap) produced by steps is an important +operation in ProseMirror. It is used, for example, for updating the +selection when the document changes. + +@Mappable +@MapResult +@StepMap +@Mapping + +### Document transforms + +Because you often need to collect a number of steps together to effect +a composite change, ProseMirror provides an abstraction to make this +easy. [State transactions](#state.Transaction) are a subclass of +transforms. + +@Transform + +The following helper functions can be useful when creating +transformations or determining whether they are even possible. + +@replaceStep +@liftTarget +@findWrapping +@canSplit +@canJoin +@joinPoint +@insertPoint +@dropPoint diff --git a/third_party/js/prosemirror/prosemirror-transform/src/attr_step.ts b/third_party/js/prosemirror/prosemirror-transform/src/attr_step.ts @@ -0,0 +1,98 @@ +import {Fragment, Slice, Node, Schema} from "prosemirror-model" +import {Step, StepResult} from "./step" +import {StepMap, Mappable} from "./map" + +/// Update an attribute in a specific node. +export class AttrStep extends Step { + /// Construct an attribute step. + constructor( + /// The position of the target node. + readonly pos: number, + /// The attribute to set. + readonly attr: string, + // The attribute's new value. + readonly value: any + ) { + super() + } + + apply(doc: Node) { + let node = doc.nodeAt(this.pos) + if (!node) return StepResult.fail("No node at attribute step's position") + let attrs = Object.create(null) + for (let name in node.attrs) attrs[name] = node.attrs[name] + attrs[this.attr] = this.value + let updated = node.type.create(attrs, null, node.marks) + return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1)) + } + + getMap() { + return StepMap.empty + } + + invert(doc: Node) { + return new AttrStep(this.pos, this.attr, doc.nodeAt(this.pos)!.attrs[this.attr]) + } + + map(mapping: Mappable) { + let pos = mapping.mapResult(this.pos, 1) + return pos.deletedAfter ? null : new AttrStep(pos.pos, this.attr, this.value) + } + + toJSON(): any { + return {stepType: "attr", pos: this.pos, attr: this.attr, value: this.value} + } + + static fromJSON(schema: Schema, json: any) { + if (typeof json.pos != "number" || typeof json.attr != "string") + throw new RangeError("Invalid input for AttrStep.fromJSON") + return new AttrStep(json.pos, json.attr, json.value) + } +} + +Step.jsonID("attr", AttrStep) + +/// Update an attribute in the doc node. +export class DocAttrStep extends Step { + /// Construct an attribute step. + constructor( + /// The attribute to set. + readonly attr: string, + // The attribute's new value. + readonly value: any + ) { + super() + } + + apply(doc: Node) { + let attrs = Object.create(null) + for (let name in doc.attrs) attrs[name] = doc.attrs[name] + attrs[this.attr] = this.value + let updated = doc.type.create(attrs, doc.content, doc.marks) + return StepResult.ok(updated) + } + + getMap() { + return StepMap.empty + } + + invert(doc: Node) { + return new DocAttrStep(this.attr, doc.attrs[this.attr]) + } + + map(mapping: Mappable) { + return this + } + + toJSON(): any { + return {stepType: "docAttr", attr: this.attr, value: this.value} + } + + static fromJSON(schema: Schema, json: any) { + if (typeof json.attr != "string") + throw new RangeError("Invalid input for DocAttrStep.fromJSON") + return new DocAttrStep(json.attr, json.value) + } +} + +Step.jsonID("docAttr", DocAttrStep) +\ No newline at end of file diff --git a/third_party/js/prosemirror/prosemirror-transform/src/index.ts b/third_party/js/prosemirror/prosemirror-transform/src/index.ts @@ -0,0 +1,11 @@ +export {Transform} from "./transform" +/// @internal +export {TransformError} from "./transform" +export {Step, StepResult} from "./step" +export {joinPoint, canJoin, canSplit, insertPoint, dropPoint, liftTarget, findWrapping} from "./structure" +export {StepMap, MapResult, Mapping, Mappable} from "./map" +export {AddMarkStep, RemoveMarkStep, AddNodeMarkStep, RemoveNodeMarkStep} from "./mark_step" +export {ReplaceStep, ReplaceAroundStep} from "./replace_step" +export {AttrStep, DocAttrStep} from "./attr_step" +import "./mark" +export {replaceStep} from "./replace" diff --git a/third_party/js/prosemirror/prosemirror-transform/src/map.ts b/third_party/js/prosemirror/prosemirror-transform/src/map.ts @@ -0,0 +1,284 @@ +/// There are several things that positions can be mapped through. +/// Such objects conform to this interface. +export interface Mappable { + /// Map a position through this object. When given, `assoc` (should + /// be -1 or 1, defaults to 1) determines with which side the + /// position is associated, which determines in which direction to + /// move when a chunk of content is inserted at the mapped position. + map: (pos: number, assoc?: number) => number + + /// Map a position, and return an object containing additional + /// information about the mapping. The result's `deleted` field tells + /// you whether the position was deleted (completely enclosed in a + /// replaced range) during the mapping. When content on only one side + /// is deleted, the position itself is only considered deleted when + /// `assoc` points in the direction of the deleted content. + mapResult: (pos: number, assoc?: number) => MapResult +} + +// Recovery values encode a range index and an offset. They are +// represented as numbers, because tons of them will be created when +// mapping, for example, a large number of decorations. The number's +// lower 16 bits provide the index, the remaining bits the offset. +// +// Note: We intentionally don't use bit shift operators to en- and +// decode these, since those clip to 32 bits, which we might in rare +// cases want to overflow. A 64-bit float can represent 48-bit +// integers precisely. + +const lower16 = 0xffff +const factor16 = Math.pow(2, 16) + +function makeRecover(index: number, offset: number) { return index + offset * factor16 } +function recoverIndex(value: number) { return value & lower16 } +function recoverOffset(value: number) { return (value - (value & lower16)) / factor16 } + +const DEL_BEFORE = 1, DEL_AFTER = 2, DEL_ACROSS = 4, DEL_SIDE = 8 + +/// An object representing a mapped position with extra +/// information. +export class MapResult { + /// @internal + constructor( + /// The mapped version of the position. + readonly pos: number, + /// @internal + readonly delInfo: number, + /// @internal + readonly recover: number | null + ) {} + + /// Tells you whether the position was deleted, that is, whether the + /// step removed the token on the side queried (via the `assoc`) + /// argument from the document. + get deleted() { return (this.delInfo & DEL_SIDE) > 0 } + + /// Tells you whether the token before the mapped position was deleted. + get deletedBefore() { return (this.delInfo & (DEL_BEFORE | DEL_ACROSS)) > 0 } + + /// True when the token after the mapped position was deleted. + get deletedAfter() { return (this.delInfo & (DEL_AFTER | DEL_ACROSS)) > 0 } + + /// Tells whether any of the steps mapped through deletes across the + /// position (including both the token before and after the + /// position). + get deletedAcross() { return (this.delInfo & DEL_ACROSS) > 0 } +} + +/// A map describing the deletions and insertions made by a step, which +/// can be used to find the correspondence between positions in the +/// pre-step version of a document and the same position in the +/// post-step version. +export class StepMap implements Mappable { + /// Create a position map. The modifications to the document are + /// represented as an array of numbers, in which each group of three + /// represents a modified chunk as `[start, oldSize, newSize]`. + constructor( + /// @internal + readonly ranges: readonly number[], + /// @internal + readonly inverted = false + ) { + if (!ranges.length && StepMap.empty) return StepMap.empty + } + + /// @internal + recover(value: number) { + let diff = 0, index = recoverIndex(value) + if (!this.inverted) for (let i = 0; i < index; i++) + diff += this.ranges[i * 3 + 2] - this.ranges[i * 3 + 1] + return this.ranges[index * 3] + diff + recoverOffset(value) + } + + mapResult(pos: number, assoc = 1): MapResult { return this._map(pos, assoc, false) as MapResult } + + map(pos: number, assoc = 1): number { return this._map(pos, assoc, true) as number } + + /// @internal + _map(pos: number, assoc: number, simple: boolean) { + let diff = 0, oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2 + for (let i = 0; i < this.ranges.length; i += 3) { + let start = this.ranges[i] - (this.inverted ? diff : 0) + if (start > pos) break + let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex], end = start + oldSize + if (pos <= end) { + let side = !oldSize ? assoc : pos == start ? -1 : pos == end ? 1 : assoc + let result = start + diff + (side < 0 ? 0 : newSize) + if (simple) return result + let recover = pos == (assoc < 0 ? start : end) ? null : makeRecover(i / 3, pos - start) + let del = pos == start ? DEL_AFTER : pos == end ? DEL_BEFORE : DEL_ACROSS + if (assoc < 0 ? pos != start : pos != end) del |= DEL_SIDE + return new MapResult(result, del, recover) + } + diff += newSize - oldSize + } + return simple ? pos + diff : new MapResult(pos + diff, 0, null) + } + + /// @internal + touches(pos: number, recover: number) { + let diff = 0, index = recoverIndex(recover) + let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2 + for (let i = 0; i < this.ranges.length; i += 3) { + let start = this.ranges[i] - (this.inverted ? diff : 0) + if (start > pos) break + let oldSize = this.ranges[i + oldIndex], end = start + oldSize + if (pos <= end && i == index * 3) return true + diff += this.ranges[i + newIndex] - oldSize + } + return false + } + + /// Calls the given function on each of the changed ranges included in + /// this map. + forEach(f: (oldStart: number, oldEnd: number, newStart: number, newEnd: number) => void) { + let oldIndex = this.inverted ? 2 : 1, newIndex = this.inverted ? 1 : 2 + for (let i = 0, diff = 0; i < this.ranges.length; i += 3) { + let start = this.ranges[i], oldStart = start - (this.inverted ? diff : 0), newStart = start + (this.inverted ? 0 : diff) + let oldSize = this.ranges[i + oldIndex], newSize = this.ranges[i + newIndex] + f(oldStart, oldStart + oldSize, newStart, newStart + newSize) + diff += newSize - oldSize + } + } + + /// Create an inverted version of this map. The result can be used to + /// map positions in the post-step document to the pre-step document. + invert() { + return new StepMap(this.ranges, !this.inverted) + } + + /// @internal + toString() { + return (this.inverted ? "-" : "") + JSON.stringify(this.ranges) + } + + /// Create a map that moves all positions by offset `n` (which may be + /// negative). This can be useful when applying steps meant for a + /// sub-document to a larger document, or vice-versa. + static offset(n: number) { + return n == 0 ? StepMap.empty : new StepMap(n < 0 ? [0, -n, 0] : [0, 0, n]) + } + + /// A StepMap that contains no changed ranges. + static empty = new StepMap([]) +} + +/// A mapping represents a pipeline of zero or more [step +/// maps](#transform.StepMap). It has special provisions for losslessly +/// handling mapping positions through a series of steps in which some +/// steps are inverted versions of earlier steps. (This comes up when +/// ‘[rebasing](/docs/guide/#transform.rebasing)’ steps for +/// collaboration or history management.) +export class Mapping implements Mappable { + /// Create a new mapping with the given position maps. + constructor( + maps?: readonly StepMap[], + /// @internal + public mirror?: number[], + /// The starting position in the `maps` array, used when `map` or + /// `mapResult` is called. + public from = 0, + /// The end position in the `maps` array. + public to = maps ? maps.length : 0 + ) { + this._maps = (maps as StepMap[]) || [] + this.ownData = !(maps || mirror) + } + + /// The step maps in this mapping. + get maps(): readonly StepMap[] { return this._maps } + + private _maps: StepMap[] + // False if maps/mirror are shared arrays that we shouldn't mutate + private ownData: boolean + + /// Create a mapping that maps only through a part of this one. + slice(from = 0, to = this.maps.length) { + return new Mapping(this._maps, this.mirror, from, to) + } + + /// Add a step map to the end of this mapping. If `mirrors` is + /// given, it should be the index of the step map that is the mirror + /// image of this one. + appendMap(map: StepMap, mirrors?: number) { + if (!this.ownData) { + this._maps = this._maps.slice() + this.mirror = this.mirror && this.mirror.slice() + this.ownData = true + } + this.to = this._maps.push(map) + if (mirrors != null) this.setMirror(this._maps.length - 1, mirrors) + } + + /// Add all the step maps in a given mapping to this one (preserving + /// mirroring information). + appendMapping(mapping: Mapping) { + for (let i = 0, startSize = this._maps.length; i < mapping._maps.length; i++) { + let mirr = mapping.getMirror(i) + this.appendMap(mapping._maps[i], mirr != null && mirr < i ? startSize + mirr : undefined) + } + } + + /// Finds the offset of the step map that mirrors the map at the + /// given offset, in this mapping (as per the second argument to + /// `appendMap`). + getMirror(n: number): number | undefined { + if (this.mirror) for (let i = 0; i < this.mirror.length; i++) + if (this.mirror[i] == n) return this.mirror[i + (i % 2 ? -1 : 1)] + } + + /// @internal + setMirror(n: number, m: number) { + if (!this.mirror) this.mirror = [] + this.mirror.push(n, m) + } + + /// Append the inverse of the given mapping to this one. + appendMappingInverted(mapping: Mapping) { + for (let i = mapping.maps.length - 1, totalSize = this._maps.length + mapping._maps.length; i >= 0; i--) { + let mirr = mapping.getMirror(i) + this.appendMap(mapping._maps[i].invert(), mirr != null && mirr > i ? totalSize - mirr - 1 : undefined) + } + } + + /// Create an inverted version of this mapping. + invert() { + let inverse = new Mapping + inverse.appendMappingInverted(this) + return inverse + } + + /// Map a position through this mapping. + map(pos: number, assoc = 1) { + if (this.mirror) return this._map(pos, assoc, true) as number + for (let i = this.from; i < this.to; i++) + pos = this._maps[i].map(pos, assoc) + return pos + } + + /// Map a position through this mapping, returning a mapping + /// result. + mapResult(pos: number, assoc = 1) { return this._map(pos, assoc, false) as MapResult } + + /// @internal + _map(pos: number, assoc: number, simple: boolean) { + let delInfo = 0 + + for (let i = this.from; i < this.to; i++) { + let map = this._maps[i], result = map.mapResult(pos, assoc) + if (result.recover != null) { + let corr = this.getMirror(i) + if (corr != null && corr > i && corr < this.to) { + i = corr + pos = this._maps[corr].recover(result.recover) + continue + } + } + + delInfo |= result.delInfo + pos = result.pos + } + + return simple ? pos : new MapResult(pos, delInfo, null) + } +} diff --git a/third_party/js/prosemirror/prosemirror-transform/src/mark.ts b/third_party/js/prosemirror/prosemirror-transform/src/mark.ts @@ -0,0 +1,106 @@ +import {Mark, MarkType, Slice, Fragment, NodeType} from "prosemirror-model" + +import {Step} from "./step" +import {Transform} from "./transform" +import {AddMarkStep, RemoveMarkStep} from "./mark_step" +import {ReplaceStep} from "./replace_step" + +export function addMark(tr: Transform, from: number, to: number, mark: Mark) { + let removed: Step[] = [], added: Step[] = [] + let removing: RemoveMarkStep | undefined, adding: AddMarkStep | undefined + tr.doc.nodesBetween(from, to, (node, pos, parent) => { + if (!node.isInline) return + let marks = node.marks + if (!mark.isInSet(marks) && parent!.type.allowsMarkType(mark.type)) { + let start = Math.max(pos, from), end = Math.min(pos + node.nodeSize, to) + let newSet = mark.addToSet(marks) + + for (let i = 0; i < marks.length; i++) { + if (!marks[i].isInSet(newSet)) { + if (removing && removing.to == start && removing.mark.eq(marks[i])) + (removing as any).to = end + else + removed.push(removing = new RemoveMarkStep(start, end, marks[i])) + } + } + + if (adding && adding.to == start) + (adding as any).to = end + else + added.push(adding = new AddMarkStep(start, end, mark)) + } + }) + + removed.forEach(s => tr.step(s)) + added.forEach(s => tr.step(s)) +} + +export function removeMark(tr: Transform, from: number, to: number, mark?: Mark | MarkType | null) { + let matched: {style: Mark, from: number, to: number, step: number}[] = [], step = 0 + tr.doc.nodesBetween(from, to, (node, pos) => { + if (!node.isInline) return + step++ + let toRemove = null + if (mark instanceof MarkType) { + let set = node.marks, found + while (found = mark.isInSet(set)) { + ;(toRemove || (toRemove = [])).push(found) + set = found.removeFromSet(set) + } + } else if (mark) { + if (mark.isInSet(node.marks)) toRemove = [mark] + } else { + toRemove = node.marks + } + if (toRemove && toRemove.length) { + let end = Math.min(pos + node.nodeSize, to) + for (let i = 0; i < toRemove.length; i++) { + let style = toRemove[i], found + for (let j = 0; j < matched.length; j++) { + let m = matched[j] + if (m.step == step - 1 && style.eq(matched[j].style)) found = m + } + if (found) { + found.to = end + found.step = step + } else { + matched.push({style, from: Math.max(pos, from), to: end, step}) + } + } + } + }) + matched.forEach(m => tr.step(new RemoveMarkStep(m.from, m.to, m.style))) +} + +export function clearIncompatible(tr: Transform, pos: number, parentType: NodeType, + match = parentType.contentMatch, + clearNewlines = true) { + let node = tr.doc.nodeAt(pos)! + let replSteps: Step[] = [], cur = pos + 1 + for (let i = 0; i < node.childCount; i++) { + let child = node.child(i), end = cur + child.nodeSize + let allowed = match.matchType(child.type) + if (!allowed) { + replSteps.push(new ReplaceStep(cur, end, Slice.empty)) + } else { + match = allowed + for (let j = 0; j < child.marks.length; j++) if (!parentType.allowsMarkType(child.marks[j].type)) + tr.step(new RemoveMarkStep(cur, end, child.marks[j])) + + if (clearNewlines && child.isText && parentType.whitespace != "pre") { + let m, newline = /\r?\n|\r/g, slice + while (m = newline.exec(child.text!)) { + if (!slice) slice = new Slice(Fragment.from(parentType.schema.text(" ", parentType.allowedMarks(child.marks))), + 0, 0) + replSteps.push(new ReplaceStep(cur + m.index, cur + m.index + m[0].length, slice)) + } + } + } + cur = end + } + if (!match.validEnd) { + let fill = match.fillBefore(Fragment.empty, true) + tr.replace(cur, cur, new Slice(fill!, 0, 0)) + } + for (let i = replSteps.length - 1; i >= 0; i--) tr.step(replSteps[i]) +} diff --git a/third_party/js/prosemirror/prosemirror-transform/src/mark_step.ts b/third_party/js/prosemirror/prosemirror-transform/src/mark_step.ts @@ -0,0 +1,224 @@ +import {Fragment, Slice, Node, Mark, Schema} from "prosemirror-model" +import {Step, StepResult} from "./step" +import {Mappable} from "./map" + +function mapFragment(fragment: Fragment, f: (child: Node, parent: Node, i: number) => Node, parent: Node): Fragment { + let mapped = [] + for (let i = 0; i < fragment.childCount; i++) { + let child = fragment.child(i) + if (child.content.size) child = child.copy(mapFragment(child.content, f, child)) + if (child.isInline) child = f(child, parent, i) + mapped.push(child) + } + return Fragment.fromArray(mapped) +} + +/// Add a mark to all inline content between two positions. +export class AddMarkStep extends Step { + /// Create a mark step. + constructor( + /// The start of the marked range. + readonly from: number, + /// The end of the marked range. + readonly to: number, + /// The mark to add. + readonly mark: Mark + ) { + super() + } + + apply(doc: Node) { + let oldSlice = doc.slice(this.from, this.to), $from = doc.resolve(this.from) + let parent = $from.node($from.sharedDepth(this.to)) + let slice = new Slice(mapFragment(oldSlice.content, (node, parent) => { + if (!node.isAtom || !parent.type.allowsMarkType(this.mark.type)) return node + return node.mark(this.mark.addToSet(node.marks)) + }, parent), oldSlice.openStart, oldSlice.openEnd) + return StepResult.fromReplace(doc, this.from, this.to, slice) + } + + invert(): Step { + return new RemoveMarkStep(this.from, this.to, this.mark) + } + + map(mapping: Mappable): Step | null { + let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) + if (from.deleted && to.deleted || from.pos >= to.pos) return null + return new AddMarkStep(from.pos, to.pos, this.mark) + } + + merge(other: Step): Step | null { + if (other instanceof AddMarkStep && + other.mark.eq(this.mark) && + this.from <= other.to && this.to >= other.from) + return new AddMarkStep(Math.min(this.from, other.from), + Math.max(this.to, other.to), this.mark) + return null + } + + toJSON(): any { + return {stepType: "addMark", mark: this.mark.toJSON(), + from: this.from, to: this.to} + } + + /// @internal + static fromJSON(schema: Schema, json: any) { + if (typeof json.from != "number" || typeof json.to != "number") + throw new RangeError("Invalid input for AddMarkStep.fromJSON") + return new AddMarkStep(json.from, json.to, schema.markFromJSON(json.mark)) + } +} + +Step.jsonID("addMark", AddMarkStep) + +/// Remove a mark from all inline content between two positions. +export class RemoveMarkStep extends Step { + /// Create a mark-removing step. + constructor( + /// The start of the unmarked range. + readonly from: number, + /// The end of the unmarked range. + readonly to: number, + /// The mark to remove. + readonly mark: Mark + ) { + super() + } + + apply(doc: Node) { + let oldSlice = doc.slice(this.from, this.to) + let slice = new Slice(mapFragment(oldSlice.content, node => { + return node.mark(this.mark.removeFromSet(node.marks)) + }, doc), oldSlice.openStart, oldSlice.openEnd) + return StepResult.fromReplace(doc, this.from, this.to, slice) + } + + invert(): Step { + return new AddMarkStep(this.from, this.to, this.mark) + } + + map(mapping: Mappable): Step | null { + let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) + if (from.deleted && to.deleted || from.pos >= to.pos) return null + return new RemoveMarkStep(from.pos, to.pos, this.mark) + } + + merge(other: Step): Step | null { + if (other instanceof RemoveMarkStep && + other.mark.eq(this.mark) && + this.from <= other.to && this.to >= other.from) + return new RemoveMarkStep(Math.min(this.from, other.from), + Math.max(this.to, other.to), this.mark) + return null + } + + toJSON(): any { + return {stepType: "removeMark", mark: this.mark.toJSON(), + from: this.from, to: this.to} + } + + /// @internal + static fromJSON(schema: Schema, json: any) { + if (typeof json.from != "number" || typeof json.to != "number") + throw new RangeError("Invalid input for RemoveMarkStep.fromJSON") + return new RemoveMarkStep(json.from, json.to, schema.markFromJSON(json.mark)) + } +} + +Step.jsonID("removeMark", RemoveMarkStep) + +/// Add a mark to a specific node. +export class AddNodeMarkStep extends Step { + /// Create a node mark step. + constructor( + /// The position of the target node. + readonly pos: number, + /// The mark to add. + readonly mark: Mark + ) { + super() + } + + apply(doc: Node) { + let node = doc.nodeAt(this.pos) + if (!node) return StepResult.fail("No node at mark step's position") + let updated = node.type.create(node.attrs, null, this.mark.addToSet(node.marks)) + return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1)) + } + + invert(doc: Node): Step { + let node = doc.nodeAt(this.pos) + if (node) { + let newSet = this.mark.addToSet(node.marks) + if (newSet.length == node.marks.length) { + for (let i = 0; i < node.marks.length; i++) + if (!node.marks[i].isInSet(newSet)) + return new AddNodeMarkStep(this.pos, node.marks[i]) + return new AddNodeMarkStep(this.pos, this.mark) + } + } + return new RemoveNodeMarkStep(this.pos, this.mark) + } + + map(mapping: Mappable): Step | null { + let pos = mapping.mapResult(this.pos, 1) + return pos.deletedAfter ? null : new AddNodeMarkStep(pos.pos, this.mark) + } + + toJSON(): any { + return {stepType: "addNodeMark", pos: this.pos, mark: this.mark.toJSON()} + } + + /// @internal + static fromJSON(schema: Schema, json: any) { + if (typeof json.pos != "number") + throw new RangeError("Invalid input for AddNodeMarkStep.fromJSON") + return new AddNodeMarkStep(json.pos, schema.markFromJSON(json.mark)) + } +} + +Step.jsonID("addNodeMark", AddNodeMarkStep) + +/// Remove a mark from a specific node. +export class RemoveNodeMarkStep extends Step { + /// Create a mark-removing step. + constructor( + /// The position of the target node. + readonly pos: number, + /// The mark to remove. + readonly mark: Mark + ) { + super() + } + + apply(doc: Node) { + let node = doc.nodeAt(this.pos) + if (!node) return StepResult.fail("No node at mark step's position") + let updated = node.type.create(node.attrs, null, this.mark.removeFromSet(node.marks)) + return StepResult.fromReplace(doc, this.pos, this.pos + 1, new Slice(Fragment.from(updated), 0, node.isLeaf ? 0 : 1)) + } + + invert(doc: Node): Step { + let node = doc.nodeAt(this.pos) + if (!node || !this.mark.isInSet(node.marks)) return this + return new AddNodeMarkStep(this.pos, this.mark) + } + + map(mapping: Mappable): Step | null { + let pos = mapping.mapResult(this.pos, 1) + return pos.deletedAfter ? null : new RemoveNodeMarkStep(pos.pos, this.mark) + } + + toJSON(): any { + return {stepType: "removeNodeMark", pos: this.pos, mark: this.mark.toJSON()} + } + + /// @internal + static fromJSON(schema: Schema, json: any) { + if (typeof json.pos != "number") + throw new RangeError("Invalid input for RemoveNodeMarkStep.fromJSON") + return new RemoveNodeMarkStep(json.pos, schema.markFromJSON(json.mark)) + } +} + +Step.jsonID("removeNodeMark", RemoveNodeMarkStep) diff --git a/third_party/js/prosemirror/prosemirror-transform/src/replace.ts b/third_party/js/prosemirror/prosemirror-transform/src/replace.ts @@ -0,0 +1,460 @@ +import {Fragment, Slice, Node, ResolvedPos, NodeType, ContentMatch, Attrs} from "prosemirror-model" + +import {Step} from "./step" +import {ReplaceStep, ReplaceAroundStep} from "./replace_step" +import {Transform} from "./transform" +import {insertPoint} from "./structure" + +/// ‘Fit’ a slice into a given position in the document, producing a +/// [step](#transform.Step) that inserts it. Will return null if +/// there's no meaningful way to insert the slice here, or inserting it +/// would be a no-op (an empty slice over an empty range). +export function replaceStep(doc: Node, from: number, to = from, slice = Slice.empty): Step | null { + if (from == to && !slice.size) return null + + let $from = doc.resolve(from), $to = doc.resolve(to) + // Optimization -- avoid work if it's obvious that it's not needed. + if (fitsTrivially($from, $to, slice)) return new ReplaceStep(from, to, slice) + return new Fitter($from, $to, slice).fit() +} + +function fitsTrivially($from: ResolvedPos, $to: ResolvedPos, slice: Slice) { + return !slice.openStart && !slice.openEnd && $from.start() == $to.start() && + $from.parent.canReplace($from.index(), $to.index(), slice.content) +} + +interface Fittable { + sliceDepth: number + frontierDepth: number + parent: Node | null + inject?: Fragment | null + wrap?: readonly NodeType[] +} + +// Algorithm for 'placing' the elements of a slice into a gap: +// +// We consider the content of each node that is open to the left to be +// independently placeable. I.e. in <p("foo"), p("bar")>, when the +// paragraph on the left is open, "foo" can be placed (somewhere on +// the left side of the replacement gap) independently from p("bar"). +// +// This class tracks the state of the placement progress in the +// following properties: +// +// - `frontier` holds a stack of `{type, match}` objects that +// represent the open side of the replacement. It starts at +// `$from`, then moves forward as content is placed, and is finally +// reconciled with `$to`. +// +// - `unplaced` is a slice that represents the content that hasn't +// been placed yet. +// +// - `placed` is a fragment of placed content. Its open-start value +// is implicit in `$from`, and its open-end value in `frontier`. +class Fitter { + frontier: {type: NodeType, match: ContentMatch}[] = [] + placed: Fragment = Fragment.empty + + constructor( + readonly $from: ResolvedPos, + readonly $to: ResolvedPos, + public unplaced: Slice + ) { + for (let i = 0; i <= $from.depth; i++) { + let node = $from.node(i) + this.frontier.push({ + type: node.type, + match: node.contentMatchAt($from.indexAfter(i)) + }) + } + + for (let i = $from.depth; i > 0; i--) + this.placed = Fragment.from($from.node(i).copy(this.placed)) + } + + get depth() { return this.frontier.length - 1 } + + fit() { + // As long as there's unplaced content, try to place some of it. + // If that fails, either increase the open score of the unplaced + // slice, or drop nodes from it, and then try again. + while (this.unplaced.size) { + let fit = this.findFittable() + if (fit) this.placeNodes(fit) + else this.openMore() || this.dropNode() + } + // When there's inline content directly after the frontier _and_ + // directly after `this.$to`, we must generate a `ReplaceAround` + // step that pulls that content into the node after the frontier. + // That means the fitting must be done to the end of the textblock + // node after `this.$to`, not `this.$to` itself. + let moveInline = this.mustMoveInline(), placedSize = this.placed.size - this.depth - this.$from.depth + let $from = this.$from, $to = this.close(moveInline < 0 ? this.$to : $from.doc.resolve(moveInline)) + if (!$to) return null + + // If closing to `$to` succeeded, create a step + let content = this.placed, openStart = $from.depth, openEnd = $to.depth + while (openStart && openEnd && content.childCount == 1) { // Normalize by dropping open parent nodes + content = content.firstChild!.content + openStart--; openEnd-- + } + let slice = new Slice(content, openStart, openEnd) + if (moveInline > -1) + return new ReplaceAroundStep($from.pos, moveInline, this.$to.pos, this.$to.end(), slice, placedSize) + if (slice.size || $from.pos != this.$to.pos) // Don't generate no-op steps + return new ReplaceStep($from.pos, $to.pos, slice) + return null + } + + // Find a position on the start spine of `this.unplaced` that has + // content that can be moved somewhere on the frontier. Returns two + // depths, one for the slice and one for the frontier. + findFittable(): Fittable | undefined { + let startDepth = this.unplaced.openStart + for (let cur = this.unplaced.content, d = 0, openEnd = this.unplaced.openEnd; d < startDepth; d++) { + let node = cur.firstChild! + if (cur.childCount > 1) openEnd = 0 + if (node.type.spec.isolating && openEnd <= d) { + startDepth = d + break + } + cur = node.content + } + + // Only try wrapping nodes (pass 2) after finding a place without + // wrapping failed. + for (let pass = 1; pass <= 2; pass++) { + for (let sliceDepth = pass == 1 ? startDepth : this.unplaced.openStart; sliceDepth >= 0; sliceDepth--) { + let fragment, parent = null + if (sliceDepth) { + parent = contentAt(this.unplaced.content, sliceDepth - 1).firstChild + fragment = parent!.content + } else { + fragment = this.unplaced.content + } + let first = fragment.firstChild + for (let frontierDepth = this.depth; frontierDepth >= 0; frontierDepth--) { + let {type, match} = this.frontier[frontierDepth], wrap, inject: Fragment | null = null + // In pass 1, if the next node matches, or there is no next + // node but the parents look compatible, we've found a + // place. + if (pass == 1 && (first ? match.matchType(first.type) || (inject = match.fillBefore(Fragment.from(first), false)) + : parent && type.compatibleContent(parent.type))) + return {sliceDepth, frontierDepth, parent, inject} + // In pass 2, look for a set of wrapping nodes that make + // `first` fit here. + else if (pass == 2 && first && (wrap = match.findWrapping(first.type))) + return {sliceDepth, frontierDepth, parent, wrap} + // Don't continue looking further up if the parent node + // would fit here. + if (parent && match.matchType(parent.type)) break + } + } + } + } + + openMore() { + let {content, openStart, openEnd} = this.unplaced + let inner = contentAt(content, openStart) + if (!inner.childCount || inner.firstChild!.isLeaf) return false + this.unplaced = new Slice(content, openStart + 1, + Math.max(openEnd, inner.size + openStart >= content.size - openEnd ? openStart + 1 : 0)) + return true + } + + dropNode() { + let {content, openStart, openEnd} = this.unplaced + let inner = contentAt(content, openStart) + if (inner.childCount <= 1 && openStart > 0) { + let openAtEnd = content.size - openStart <= openStart + inner.size + this.unplaced = new Slice(dropFromFragment(content, openStart - 1, 1), openStart - 1, + openAtEnd ? openStart - 1 : openEnd) + } else { + this.unplaced = new Slice(dropFromFragment(content, openStart, 1), openStart, openEnd) + } + } + + // Move content from the unplaced slice at `sliceDepth` to the + // frontier node at `frontierDepth`. Close that frontier node when + // applicable. + placeNodes({sliceDepth, frontierDepth, parent, inject, wrap}: Fittable) { + while (this.depth > frontierDepth) this.closeFrontierNode() + if (wrap) for (let i = 0; i < wrap.length; i++) this.openFrontierNode(wrap[i]) + + let slice = this.unplaced, fragment = parent ? parent.content : slice.content + let openStart = slice.openStart - sliceDepth + let taken = 0, add = [] + let {match, type} = this.frontier[frontierDepth] + if (inject) { + for (let i = 0; i < inject.childCount; i++) add.push(inject.child(i)) + match = match.matchFragment(inject)! + } + // Computes the amount of (end) open nodes at the end of the + // fragment. When 0, the parent is open, but no more. When + // negative, nothing is open. + let openEndCount = (fragment.size + sliceDepth) - (slice.content.size - slice.openEnd) + // Scan over the fragment, fitting as many child nodes as + // possible. + while (taken < fragment.childCount) { + let next = fragment.child(taken), matches = match.matchType(next.type) + if (!matches) break + taken++ + if (taken > 1 || openStart == 0 || next.content.size) { // Drop empty open nodes + match = matches + add.push(closeNodeStart(next.mark(type.allowedMarks(next.marks)), taken == 1 ? openStart : 0, + taken == fragment.childCount ? openEndCount : -1)) + } + } + let toEnd = taken == fragment.childCount + if (!toEnd) openEndCount = -1 + + this.placed = addToFragment(this.placed, frontierDepth, Fragment.from(add)) + this.frontier[frontierDepth].match = match + + // If the parent types match, and the entire node was moved, and + // it's not open, close this frontier node right away. + if (toEnd && openEndCount < 0 && parent && parent.type == this.frontier[this.depth].type && this.frontier.length > 1) + this.closeFrontierNode() + + // Add new frontier nodes for any open nodes at the end. + for (let i = 0, cur = fragment; i < openEndCount; i++) { + let node = cur.lastChild! + this.frontier.push({type: node.type, match: node.contentMatchAt(node.childCount)}) + cur = node.content + } + + // Update `this.unplaced`. Drop the entire node from which we + // placed it we got to its end, otherwise just drop the placed + // nodes. + this.unplaced = !toEnd ? new Slice(dropFromFragment(slice.content, sliceDepth, taken), slice.openStart, slice.openEnd) + : sliceDepth == 0 ? Slice.empty + : new Slice(dropFromFragment(slice.content, sliceDepth - 1, 1), + sliceDepth - 1, openEndCount < 0 ? slice.openEnd : sliceDepth - 1) + } + + mustMoveInline() { + if (!this.$to.parent.isTextblock) return -1 + let top = this.frontier[this.depth], level + if (!top.type.isTextblock || !contentAfterFits(this.$to, this.$to.depth, top.type, top.match, false) || + (this.$to.depth == this.depth && (level = this.findCloseLevel(this.$to)) && level.depth == this.depth)) return -1 + + let {depth} = this.$to, after = this.$to.after(depth) + while (depth > 1 && after == this.$to.end(--depth)) ++after + return after + } + + findCloseLevel($to: ResolvedPos) { + scan: for (let i = Math.min(this.depth, $to.depth); i >= 0; i--) { + let {match, type} = this.frontier[i] + let dropInner = i < $to.depth && $to.end(i + 1) == $to.pos + ($to.depth - (i + 1)) + let fit = contentAfterFits($to, i, type, match, dropInner) + if (!fit) continue + for (let d = i - 1; d >= 0; d--) { + let {match, type} = this.frontier[d] + let matches = contentAfterFits($to, d, type, match, true) + if (!matches || matches.childCount) continue scan + } + return {depth: i, fit, move: dropInner ? $to.doc.resolve($to.after(i + 1)) : $to} + } + } + + close($to: ResolvedPos) { + let close = this.findCloseLevel($to) + if (!close) return null + + while (this.depth > close.depth) this.closeFrontierNode() + if (close.fit.childCount) this.placed = addToFragment(this.placed, close.depth, close.fit) + $to = close.move + for (let d = close.depth + 1; d <= $to.depth; d++) { + let node = $to.node(d), add = node.type.contentMatch.fillBefore(node.content, true, $to.index(d))! + this.openFrontierNode(node.type, node.attrs, add) + } + return $to + } + + openFrontierNode(type: NodeType, attrs: Attrs | null = null, content?: Fragment) { + let top = this.frontier[this.depth] + top.match = top.match.matchType(type)! + this.placed = addToFragment(this.placed, this.depth, Fragment.from(type.create(attrs, content))) + this.frontier.push({type, match: type.contentMatch}) + } + + closeFrontierNode() { + let open = this.frontier.pop()! + let add = open.match.fillBefore(Fragment.empty, true)! + if (add.childCount) this.placed = addToFragment(this.placed, this.frontier.length, add) + } +} + +function dropFromFragment(fragment: Fragment, depth: number, count: number): Fragment { + if (depth == 0) return fragment.cutByIndex(count, fragment.childCount) + return fragment.replaceChild(0, fragment.firstChild!.copy(dropFromFragment(fragment.firstChild!.content, depth - 1, count))) +} + +function addToFragment(fragment: Fragment, depth: number, content: Fragment): Fragment { + if (depth == 0) return fragment.append(content) + return fragment.replaceChild(fragment.childCount - 1, + fragment.lastChild!.copy(addToFragment(fragment.lastChild!.content, depth - 1, content))) +} + +function contentAt(fragment: Fragment, depth: number) { + for (let i = 0; i < depth; i++) fragment = fragment.firstChild!.content + return fragment +} + +function closeNodeStart(node: Node, openStart: number, openEnd: number) { + if (openStart <= 0) return node + let frag = node.content + if (openStart > 1) + frag = frag.replaceChild(0, closeNodeStart(frag.firstChild!, openStart - 1, frag.childCount == 1 ? openEnd - 1 : 0)) + if (openStart > 0) { + frag = node.type.contentMatch.fillBefore(frag)!.append(frag) + if (openEnd <= 0) frag = frag.append(node.type.contentMatch.matchFragment(frag)!.fillBefore(Fragment.empty, true)!) + } + return node.copy(frag) +} + +function contentAfterFits($to: ResolvedPos, depth: number, type: NodeType, match: ContentMatch, open: boolean) { + let node = $to.node(depth), index = open ? $to.indexAfter(depth) : $to.index(depth) + if (index == node.childCount && !type.compatibleContent(node.type)) return null + let fit = match.fillBefore(node.content, true, index) + return fit && !invalidMarks(type, node.content, index) ? fit : null +} + +function invalidMarks(type: NodeType, fragment: Fragment, start: number) { + for (let i = start; i < fragment.childCount; i++) + if (!type.allowsMarks(fragment.child(i).marks)) return true + return false +} + +function definesContent(type: NodeType) { + return type.spec.defining || type.spec.definingForContent +} + +export function replaceRange(tr: Transform, from: number, to: number, slice: Slice) { + if (!slice.size) return tr.deleteRange(from, to) + + let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to) + if (fitsTrivially($from, $to, slice)) + return tr.step(new ReplaceStep(from, to, slice)) + + let targetDepths = coveredDepths($from, $to) + // Can't replace the whole document, so remove 0 if it's present + if (targetDepths[targetDepths.length - 1] == 0) targetDepths.pop() + // Negative numbers represent not expansion over the whole node at + // that depth, but replacing from $from.before(-D) to $to.pos. + let preferredTarget = -($from.depth + 1) + targetDepths.unshift(preferredTarget) + // This loop picks a preferred target depth, if one of the covering + // depths is not outside of a defining node, and adds negative + // depths for any depth that has $from at its start and does not + // cross a defining node. + for (let d = $from.depth, pos = $from.pos - 1; d > 0; d--, pos--) { + let spec = $from.node(d).type.spec + if (spec.defining || spec.definingAsContext || spec.isolating) break + if (targetDepths.indexOf(d) > -1) preferredTarget = d + else if ($from.before(d) == pos) targetDepths.splice(1, 0, -d) + } + // Try to fit each possible depth of the slice into each possible + // target depth, starting with the preferred depths. + let preferredTargetIndex = targetDepths.indexOf(preferredTarget) + + let leftNodes: Node[] = [], preferredDepth = slice.openStart + for (let content = slice.content, i = 0;; i++) { + let node = content.firstChild! + leftNodes.push(node) + if (i == slice.openStart) break + content = node.content + } + + // Back up preferredDepth to cover defining textblocks directly + // above it, possibly skipping a non-defining textblock. + for (let d = preferredDepth - 1; d >= 0; d--) { + let leftNode = leftNodes[d], def = definesContent(leftNode.type) + if (def && !leftNode.sameMarkup($from.node(Math.abs(preferredTarget) - 1))) preferredDepth = d + else if (def || !leftNode.type.isTextblock) break + } + + for (let j = slice.openStart; j >= 0; j--) { + let openDepth = (j + preferredDepth + 1) % (slice.openStart + 1) + let insert = leftNodes[openDepth] + if (!insert) continue + for (let i = 0; i < targetDepths.length; i++) { + // Loop over possible expansion levels, starting with the + // preferred one + let targetDepth = targetDepths[(i + preferredTargetIndex) % targetDepths.length], expand = true + if (targetDepth < 0) { expand = false; targetDepth = -targetDepth } + let parent = $from.node(targetDepth - 1), index = $from.index(targetDepth - 1) + if (parent.canReplaceWith(index, index, insert.type, insert.marks)) + return tr.replace($from.before(targetDepth), expand ? $to.after(targetDepth) : to, + new Slice(closeFragment(slice.content, 0, slice.openStart, openDepth), + openDepth, slice.openEnd)) + } + } + + let startSteps = tr.steps.length + for (let i = targetDepths.length - 1; i >= 0; i--) { + tr.replace(from, to, slice) + if (tr.steps.length > startSteps) break + let depth = targetDepths[i] + if (depth < 0) continue + from = $from.before(depth); to = $to.after(depth) + } +} + +function closeFragment(fragment: Fragment, depth: number, oldOpen: number, newOpen: number, parent?: Node) { + if (depth < oldOpen) { + let first = fragment.firstChild! + fragment = fragment.replaceChild(0, first.copy(closeFragment(first.content, depth + 1, oldOpen, newOpen, first))) + } + if (depth > newOpen) { + let match = parent!.contentMatchAt(0)! + let start = match.fillBefore(fragment)!.append(fragment) + fragment = start.append(match.matchFragment(start)!.fillBefore(Fragment.empty, true)!) + } + return fragment +} + +export function replaceRangeWith(tr: Transform, from: number, to: number, node: Node) { + if (!node.isInline && from == to && tr.doc.resolve(from).parent.content.size) { + let point = insertPoint(tr.doc, from, node.type) + if (point != null) from = to = point + } + tr.replaceRange(from, to, new Slice(Fragment.from(node), 0, 0)) +} + +export function deleteRange(tr: Transform, from: number, to: number) { + let $from = tr.doc.resolve(from), $to = tr.doc.resolve(to) + let covered = coveredDepths($from, $to) + for (let i = 0; i < covered.length; i++) { + let depth = covered[i], last = i == covered.length - 1 + if ((last && depth == 0) || $from.node(depth).type.contentMatch.validEnd) + return tr.delete($from.start(depth), $to.end(depth)) + if (depth > 0 && (last || $from.node(depth - 1).canReplace($from.index(depth - 1), $to.indexAfter(depth - 1)))) + return tr.delete($from.before(depth), $to.after(depth)) + } + for (let d = 1; d <= $from.depth && d <= $to.depth; d++) { + if (from - $from.start(d) == $from.depth - d && to > $from.end(d) && $to.end(d) - to != $to.depth - d && + $from.start(d - 1) == $to.start(d - 1) && $from.node(d - 1).canReplace($from.index(d - 1), $to.index(d - 1))) + return tr.delete($from.before(d), to) + } + tr.delete(from, to) +} + +// Returns an array of all depths for which $from - $to spans the +// whole content of the nodes at that depth. +function coveredDepths($from: ResolvedPos, $to: ResolvedPos) { + let result: number[] = [], minDepth = Math.min($from.depth, $to.depth) + for (let d = minDepth; d >= 0; d--) { + let start = $from.start(d) + if (start < $from.pos - ($from.depth - d) || + $to.end(d) > $to.pos + ($to.depth - d) || + $from.node(d).type.spec.isolating || + $to.node(d).type.spec.isolating) break + if (start == $to.start(d) || + (d == $from.depth && d == $to.depth && $from.parent.inlineContent && $to.parent.inlineContent && + d && $to.start(d - 1) == start - 1)) + result.push(d) + } + return result +} diff --git a/third_party/js/prosemirror/prosemirror-transform/src/replace_step.ts b/third_party/js/prosemirror/prosemirror-transform/src/replace_step.ts @@ -0,0 +1,178 @@ +import {Slice, Node, Schema} from "prosemirror-model" + +import {Step, StepResult} from "./step" +import {StepMap, Mappable} from "./map" + +/// Replace a part of the document with a slice of new content. +export class ReplaceStep extends Step { + /// The given `slice` should fit the 'gap' between `from` and + /// `to`—the depths must line up, and the surrounding nodes must be + /// able to be joined with the open sides of the slice. When + /// `structure` is true, the step will fail if the content between + /// from and to is not just a sequence of closing and then opening + /// tokens (this is to guard against rebased replace steps + /// overwriting something they weren't supposed to). + constructor( + /// The start position of the replaced range. + readonly from: number, + /// The end position of the replaced range. + readonly to: number, + /// The slice to insert. + readonly slice: Slice, + /// @internal + readonly structure = false + ) { + super() + } + + apply(doc: Node) { + if (this.structure && contentBetween(doc, this.from, this.to)) + return StepResult.fail("Structure replace would overwrite content") + return StepResult.fromReplace(doc, this.from, this.to, this.slice) + } + + getMap() { + return new StepMap([this.from, this.to - this.from, this.slice.size]) + } + + invert(doc: Node) { + return new ReplaceStep(this.from, this.from + this.slice.size, doc.slice(this.from, this.to)) + } + + map(mapping: Mappable) { + let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) + if (from.deletedAcross && to.deletedAcross) return null + return new ReplaceStep(from.pos, Math.max(from.pos, to.pos), this.slice, this.structure) + } + + merge(other: Step) { + if (!(other instanceof ReplaceStep) || other.structure || this.structure) return null + + if (this.from + this.slice.size == other.from && !this.slice.openEnd && !other.slice.openStart) { + let slice = this.slice.size + other.slice.size == 0 ? Slice.empty + : new Slice(this.slice.content.append(other.slice.content), this.slice.openStart, other.slice.openEnd) + return new ReplaceStep(this.from, this.to + (other.to - other.from), slice, this.structure) + } else if (other.to == this.from && !this.slice.openStart && !other.slice.openEnd) { + let slice = this.slice.size + other.slice.size == 0 ? Slice.empty + : new Slice(other.slice.content.append(this.slice.content), other.slice.openStart, this.slice.openEnd) + return new ReplaceStep(other.from, this.to, slice, this.structure) + } else { + return null + } + } + + toJSON(): any { + let json: any = {stepType: "replace", from: this.from, to: this.to} + if (this.slice.size) json.slice = this.slice.toJSON() + if (this.structure) json.structure = true + return json + } + + /// @internal + static fromJSON(schema: Schema, json: any) { + if (typeof json.from != "number" || typeof json.to != "number") + throw new RangeError("Invalid input for ReplaceStep.fromJSON") + return new ReplaceStep(json.from, json.to, Slice.fromJSON(schema, json.slice), !!json.structure) + } +} + +Step.jsonID("replace", ReplaceStep) + +/// Replace a part of the document with a slice of content, but +/// preserve a range of the replaced content by moving it into the +/// slice. +export class ReplaceAroundStep extends Step { + /// Create a replace-around step with the given range and gap. + /// `insert` should be the point in the slice into which the content + /// of the gap should be moved. `structure` has the same meaning as + /// it has in the [`ReplaceStep`](#transform.ReplaceStep) class. + constructor( + /// The start position of the replaced range. + readonly from: number, + /// The end position of the replaced range. + readonly to: number, + /// The start of preserved range. + readonly gapFrom: number, + /// The end of preserved range. + readonly gapTo: number, + /// The slice to insert. + readonly slice: Slice, + /// The position in the slice where the preserved range should be + /// inserted. + readonly insert: number, + /// @internal + readonly structure = false + ) { + super() + } + + apply(doc: Node) { + if (this.structure && (contentBetween(doc, this.from, this.gapFrom) || + contentBetween(doc, this.gapTo, this.to))) + return StepResult.fail("Structure gap-replace would overwrite content") + + let gap = doc.slice(this.gapFrom, this.gapTo) + if (gap.openStart || gap.openEnd) + return StepResult.fail("Gap is not a flat range") + let inserted = this.slice.insertAt(this.insert, gap.content) + if (!inserted) return StepResult.fail("Content does not fit in gap") + return StepResult.fromReplace(doc, this.from, this.to, inserted) + } + + getMap() { + return new StepMap([this.from, this.gapFrom - this.from, this.insert, + this.gapTo, this.to - this.gapTo, this.slice.size - this.insert]) + } + + invert(doc: Node) { + let gap = this.gapTo - this.gapFrom + return new ReplaceAroundStep(this.from, this.from + this.slice.size + gap, + this.from + this.insert, this.from + this.insert + gap, + doc.slice(this.from, this.to).removeBetween(this.gapFrom - this.from, this.gapTo - this.from), + this.gapFrom - this.from, this.structure) + } + + map(mapping: Mappable) { + let from = mapping.mapResult(this.from, 1), to = mapping.mapResult(this.to, -1) + let gapFrom = this.from == this.gapFrom ? from.pos : mapping.map(this.gapFrom, -1) + let gapTo = this.to == this.gapTo ? to.pos : mapping.map(this.gapTo, 1) + if ((from.deletedAcross && to.deletedAcross) || gapFrom < from.pos || gapTo > to.pos) return null + return new ReplaceAroundStep(from.pos, to.pos, gapFrom, gapTo, this.slice, this.insert, this.structure) + } + + toJSON(): any { + let json: any = {stepType: "replaceAround", from: this.from, to: this.to, + gapFrom: this.gapFrom, gapTo: this.gapTo, insert: this.insert} + if (this.slice.size) json.slice = this.slice.toJSON() + if (this.structure) json.structure = true + return json + } + + /// @internal + static fromJSON(schema: Schema, json: any) { + if (typeof json.from != "number" || typeof json.to != "number" || + typeof json.gapFrom != "number" || typeof json.gapTo != "number" || typeof json.insert != "number") + throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON") + return new ReplaceAroundStep(json.from, json.to, json.gapFrom, json.gapTo, + Slice.fromJSON(schema, json.slice), json.insert, !!json.structure) + } +} + +Step.jsonID("replaceAround", ReplaceAroundStep) + +function contentBetween(doc: Node, from: number, to: number) { + let $from = doc.resolve(from), dist = to - from, depth = $from.depth + while (dist > 0 && depth > 0 && $from.indexAfter(depth) == $from.node(depth).childCount) { + depth-- + dist-- + } + if (dist > 0) { + let next = $from.node(depth).maybeChild($from.indexAfter(depth)) + while (dist > 0) { + if (!next || next.isLeaf) return true + next = next.firstChild + dist-- + } + } + return false +} diff --git a/third_party/js/prosemirror/prosemirror-transform/src/step.ts b/third_party/js/prosemirror/prosemirror-transform/src/step.ts @@ -0,0 +1,97 @@ +import {ReplaceError, Schema, Slice, Node} from "prosemirror-model" + +import {StepMap, Mappable} from "./map" + +const stepsByID: {[id: string]: {fromJSON(schema: Schema, json: any): Step}} = Object.create(null) + +/// A step object represents an atomic change. It generally applies +/// only to the document it was created for, since the positions +/// stored in it will only make sense for that document. +/// +/// New steps are defined by creating classes that extend `Step`, +/// overriding the `apply`, `invert`, `map`, `getMap` and `fromJSON` +/// methods, and registering your class with a unique +/// JSON-serialization identifier using +/// [`Step.jsonID`](#transform.Step^jsonID). +export abstract class Step { + /// Applies this step to the given document, returning a result + /// object that either indicates failure, if the step can not be + /// applied to this document, or indicates success by containing a + /// transformed document. + abstract apply(doc: Node): StepResult + + /// Get the step map that represents the changes made by this step, + /// and which can be used to transform between positions in the old + /// and the new document. + getMap(): StepMap { return StepMap.empty } + + /// Create an inverted version of this step. Needs the document as it + /// was before the step as argument. + abstract invert(doc: Node): Step + + /// Map this step through a mappable thing, returning either a + /// version of that step with its positions adjusted, or `null` if + /// the step was entirely deleted by the mapping. + abstract map(mapping: Mappable): Step | null + + /// Try to merge this step with another one, to be applied directly + /// after it. Returns the merged step when possible, null if the + /// steps can't be merged. + merge(other: Step): Step | null { return null } + + /// Create a JSON-serializeable representation of this step. When + /// defining this for a custom subclass, make sure the result object + /// includes the step type's [JSON id](#transform.Step^jsonID) under + /// the `stepType` property. + abstract toJSON(): any + + /// Deserialize a step from its JSON representation. Will call + /// through to the step class' own implementation of this method. + static fromJSON(schema: Schema, json: any): Step { + if (!json || !json.stepType) throw new RangeError("Invalid input for Step.fromJSON") + let type = stepsByID[json.stepType] + if (!type) throw new RangeError(`No step type ${json.stepType} defined`) + return type.fromJSON(schema, json) + } + + /// To be able to serialize steps to JSON, each step needs a string + /// ID to attach to its JSON representation. Use this method to + /// register an ID for your step classes. Try to pick something + /// that's unlikely to clash with steps from other modules. + static jsonID(id: string, stepClass: {fromJSON(schema: Schema, json: any): Step}) { + if (id in stepsByID) throw new RangeError("Duplicate use of step JSON ID " + id) + stepsByID[id] = stepClass + ;(stepClass as any).prototype.jsonID = id + return stepClass + } +} + +/// The result of [applying](#transform.Step.apply) a step. Contains either a +/// new document or a failure value. +export class StepResult { + /// @internal + constructor( + /// The transformed document, if successful. + readonly doc: Node | null, + /// The failure message, if unsuccessful. + readonly failed: string | null + ) {} + + /// Create a successful step result. + static ok(doc: Node) { return new StepResult(doc, null) } + + /// Create a failed step result. + static fail(message: string) { return new StepResult(null, message) } + + /// Call [`Node.replace`](#model.Node.replace) with the given + /// arguments. Create a successful result if it succeeds, and a + /// failed one if it throws a `ReplaceError`. + static fromReplace(doc: Node, from: number, to: number, slice: Slice) { + try { + return StepResult.ok(doc.replace(from, to, slice)) + } catch (e) { + if (e instanceof ReplaceError) return StepResult.fail(e.message) + throw e + } + } +} diff --git a/third_party/js/prosemirror/prosemirror-transform/src/structure.ts b/third_party/js/prosemirror/prosemirror-transform/src/structure.ts @@ -0,0 +1,349 @@ +import {Slice, Fragment, NodeRange, NodeType, Node, Mark, Attrs, ContentMatch} from "prosemirror-model" + +import {Transform} from "./transform" +import {ReplaceStep, ReplaceAroundStep} from "./replace_step" +import {clearIncompatible} from "./mark" + +function canCut(node: Node, start: number, end: number) { + return (start == 0 || node.canReplace(start, node.childCount)) && + (end == node.childCount || node.canReplace(0, end)) +} + +/// Try to find a target depth to which the content in the given range +/// can be lifted. Will not go across +/// [isolating](#model.NodeSpec.isolating) parent nodes. +export function liftTarget(range: NodeRange): number | null { + let parent = range.parent + let content = parent.content.cutByIndex(range.startIndex, range.endIndex) + for (let depth = range.depth, contentBefore = 0, contentAfter = 0;; --depth) { + let node = range.$from.node(depth) + let index = range.$from.index(depth) + contentBefore, endIndex = range.$to.indexAfter(depth) - contentAfter + if (depth < range.depth && node.canReplace(index, endIndex, content)) + return depth + if (depth == 0 || node.type.spec.isolating || !canCut(node, index, endIndex)) break + if (index) contentBefore = 1 + if (endIndex < node.childCount) contentAfter = 1 + } + return null +} + +export function lift(tr: Transform, range: NodeRange, target: number) { + let {$from, $to, depth} = range + + let gapStart = $from.before(depth + 1), gapEnd = $to.after(depth + 1) + let start = gapStart, end = gapEnd + + let before = Fragment.empty, openStart = 0 + for (let d = depth, splitting = false; d > target; d--) + if (splitting || $from.index(d) > 0) { + splitting = true + before = Fragment.from($from.node(d).copy(before)) + openStart++ + } else { + start-- + } + let after = Fragment.empty, openEnd = 0 + for (let d = depth, splitting = false; d > target; d--) + if (splitting || $to.after(d + 1) < $to.end(d)) { + splitting = true + after = Fragment.from($to.node(d).copy(after)) + openEnd++ + } else { + end++ + } + + tr.step(new ReplaceAroundStep(start, end, gapStart, gapEnd, + new Slice(before.append(after), openStart, openEnd), + before.size - openStart, true)) +} + +/// Try to find a valid way to wrap the content in the given range in a +/// node of the given type. May introduce extra nodes around and inside +/// the wrapper node, if necessary. Returns null if no valid wrapping +/// could be found. When `innerRange` is given, that range's content is +/// used as the content to fit into the wrapping, instead of the +/// content of `range`. +export function findWrapping( + range: NodeRange, + nodeType: NodeType, + attrs: Attrs | null = null, + innerRange = range +): {type: NodeType, attrs: Attrs | null}[] | null { + let around = findWrappingOutside(range, nodeType) + let inner = around && findWrappingInside(innerRange, nodeType) + if (!inner) return null + return (around!.map(withAttrs) as {type: NodeType, attrs: Attrs | null}[]) + .concat({type: nodeType, attrs}).concat(inner.map(withAttrs)) +} + +function withAttrs(type: NodeType) { return {type, attrs: null} } + +function findWrappingOutside(range: NodeRange, type: NodeType) { + let {parent, startIndex, endIndex} = range + let around = parent.contentMatchAt(startIndex).findWrapping(type) + if (!around) return null + let outer = around.length ? around[0] : type + return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null +} + +function findWrappingInside(range: NodeRange, type: NodeType) { + let {parent, startIndex, endIndex} = range + let inner = parent.child(startIndex) + let inside = type.contentMatch.findWrapping(inner.type) + if (!inside) return null + let lastType = inside.length ? inside[inside.length - 1] : type + let innerMatch: ContentMatch | null = lastType.contentMatch + for (let i = startIndex; innerMatch && i < endIndex; i++) + innerMatch = innerMatch.matchType(parent.child(i).type) + if (!innerMatch || !innerMatch.validEnd) return null + return inside +} + +export function wrap(tr: Transform, range: NodeRange, wrappers: readonly {type: NodeType, attrs?: Attrs | null}[]) { + let content = Fragment.empty + for (let i = wrappers.length - 1; i >= 0; i--) { + if (content.size) { + let match = wrappers[i].type.contentMatch.matchFragment(content) + if (!match || !match.validEnd) + throw new RangeError("Wrapper type given to Transform.wrap does not form valid content of its parent wrapper") + } + content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content)) + } + + let start = range.start, end = range.end + tr.step(new ReplaceAroundStep(start, end, start, end, new Slice(content, 0, 0), wrappers.length, true)) +} + +export function setBlockType(tr: Transform, from: number, to: number, + type: NodeType, attrs: Attrs | null | ((oldNode: Node) => Attrs)) { + if (!type.isTextblock) throw new RangeError("Type given to setBlockType should be a textblock") + let mapFrom = tr.steps.length + tr.doc.nodesBetween(from, to, (node, pos) => { + let attrsHere = typeof attrs == "function" ? attrs(node) : attrs + if (node.isTextblock && !node.hasMarkup(type, attrsHere) && + canChangeType(tr.doc, tr.mapping.slice(mapFrom).map(pos), type)) { + let convertNewlines = null + if (type.schema.linebreakReplacement) { + let pre = type.whitespace == "pre", supportLinebreak = !!type.contentMatch.matchType(type.schema.linebreakReplacement) + if (pre && !supportLinebreak) convertNewlines = false + else if (!pre && supportLinebreak) convertNewlines = true + } + // Ensure all markup that isn't allowed in the new node type is cleared + if (convertNewlines === false) replaceLinebreaks(tr, node, pos, mapFrom) + clearIncompatible(tr, tr.mapping.slice(mapFrom).map(pos, 1), type, undefined, convertNewlines === null) + let mapping = tr.mapping.slice(mapFrom) + let startM = mapping.map(pos, 1), endM = mapping.map(pos + node.nodeSize, 1) + tr.step(new ReplaceAroundStep(startM, endM, startM + 1, endM - 1, + new Slice(Fragment.from(type.create(attrsHere, null, node.marks)), 0, 0), 1, true)) + if (convertNewlines === true) replaceNewlines(tr, node, pos, mapFrom) + return false + } + }) +} + +function replaceNewlines(tr: Transform, node: Node, pos: number, mapFrom: number) { + node.forEach((child, offset) => { + if (child.isText) { + let m, newline = /\r?\n|\r/g + while (m = newline.exec(child.text!)) { + let start = tr.mapping.slice(mapFrom).map(pos + 1 + offset + m.index) + tr.replaceWith(start, start + 1, node.type.schema.linebreakReplacement!.create()) + } + } + }) +} + +function replaceLinebreaks(tr: Transform, node: Node, pos: number, mapFrom: number) { + node.forEach((child, offset) => { + if (child.type == child.type.schema.linebreakReplacement) { + let start = tr.mapping.slice(mapFrom).map(pos + 1 + offset) + tr.replaceWith(start, start + 1, node.type.schema.text("\n")) + } + }) +} + +function canChangeType(doc: Node, pos: number, type: NodeType) { + let $pos = doc.resolve(pos), index = $pos.index() + return $pos.parent.canReplaceWith(index, index + 1, type) +} + +/// Change the type, attributes, and/or marks of the node at `pos`. +/// When `type` isn't given, the existing node type is preserved, +export function setNodeMarkup(tr: Transform, pos: number, type: NodeType | undefined | null, + attrs: Attrs | null, marks: readonly Mark[] | undefined) { + let node = tr.doc.nodeAt(pos) + if (!node) throw new RangeError("No node at given position") + if (!type) type = node.type + let newNode = type.create(attrs, null, marks || node.marks) + if (node.isLeaf) + return tr.replaceWith(pos, pos + node.nodeSize, newNode) + + if (!type.validContent(node.content)) + throw new RangeError("Invalid content for node type " + type.name) + + tr.step(new ReplaceAroundStep(pos, pos + node.nodeSize, pos + 1, pos + node.nodeSize - 1, + new Slice(Fragment.from(newNode), 0, 0), 1, true)) +} + +/// Check whether splitting at the given position is allowed. +export function canSplit(doc: Node, pos: number, depth = 1, + typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]): boolean { + let $pos = doc.resolve(pos), base = $pos.depth - depth + let innerType = (typesAfter && typesAfter[typesAfter.length - 1]) || $pos.parent + if (base < 0 || $pos.parent.type.spec.isolating || + !$pos.parent.canReplace($pos.index(), $pos.parent.childCount) || + !innerType.type.validContent($pos.parent.content.cutByIndex($pos.index(), $pos.parent.childCount))) + return false + for (let d = $pos.depth - 1, i = depth - 2; d > base; d--, i--) { + let node = $pos.node(d), index = $pos.index(d) + if (node.type.spec.isolating) return false + let rest = node.content.cutByIndex(index, node.childCount) + let overrideChild = typesAfter && typesAfter[i + 1] + if (overrideChild) + rest = rest.replaceChild(0, overrideChild.type.create(overrideChild.attrs)) + let after = (typesAfter && typesAfter[i]) || node + if (!node.canReplace(index + 1, node.childCount) || !after.type.validContent(rest)) + return false + } + let index = $pos.indexAfter(base) + let baseType = typesAfter && typesAfter[0] + return $pos.node(base).canReplaceWith(index, index, baseType ? baseType.type : $pos.node(base + 1).type) +} + +export function split(tr: Transform, pos: number, depth = 1, typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]) { + let $pos = tr.doc.resolve(pos), before = Fragment.empty, after = Fragment.empty + for (let d = $pos.depth, e = $pos.depth - depth, i = depth - 1; d > e; d--, i--) { + before = Fragment.from($pos.node(d).copy(before)) + let typeAfter = typesAfter && typesAfter[i] + after = Fragment.from(typeAfter ? typeAfter.type.create(typeAfter.attrs, after) : $pos.node(d).copy(after)) + } + tr.step(new ReplaceStep(pos, pos, new Slice(before.append(after), depth, depth), true)) +} + +/// Test whether the blocks before and after a given position can be +/// joined. +export function canJoin(doc: Node, pos: number): boolean { + let $pos = doc.resolve(pos), index = $pos.index() + return joinable($pos.nodeBefore, $pos.nodeAfter) && + $pos.parent.canReplace(index, index + 1) +} + +function canAppendWithSubstitutedLinebreaks(a: Node, b: Node) { + if (!b.content.size) a.type.compatibleContent(b.type) + let match: ContentMatch | null = a.contentMatchAt(a.childCount) + let {linebreakReplacement} = a.type.schema + for (let i = 0; i < b.childCount; i++) { + let child = b.child(i) + let type = child.type == linebreakReplacement ? a.type.schema.nodes.text : child.type + match = match.matchType(type) + if (!match) return false + if (!a.type.allowsMarks(child.marks)) return false + } + return match.validEnd +} + +function joinable(a: Node | null, b: Node | null) { + return !!(a && b && !a.isLeaf && canAppendWithSubstitutedLinebreaks(a, b)) +} + +/// Find an ancestor of the given position that can be joined to the +/// block before (or after if `dir` is positive). Returns the joinable +/// point, if any. +export function joinPoint(doc: Node, pos: number, dir = -1) { + let $pos = doc.resolve(pos) + for (let d = $pos.depth;; d--) { + let before, after, index = $pos.index(d) + if (d == $pos.depth) { + before = $pos.nodeBefore + after = $pos.nodeAfter + } else if (dir > 0) { + before = $pos.node(d + 1) + index++ + after = $pos.node(d).maybeChild(index) + } else { + before = $pos.node(d).maybeChild(index - 1) + after = $pos.node(d + 1) + } + if (before && !before.isTextblock && joinable(before, after) && + $pos.node(d).canReplace(index, index + 1)) return pos + if (d == 0) break + pos = dir < 0 ? $pos.before(d) : $pos.after(d) + } +} + +export function join(tr: Transform, pos: number, depth: number) { + let convertNewlines = null + let {linebreakReplacement} = tr.doc.type.schema + let $before = tr.doc.resolve(pos - depth), beforeType = $before.node().type + if (linebreakReplacement && beforeType.inlineContent) { + let pre = beforeType.whitespace == "pre" + let supportLinebreak = !!beforeType.contentMatch.matchType(linebreakReplacement) + if (pre && !supportLinebreak) convertNewlines = false + else if (!pre && supportLinebreak) convertNewlines = true + } + let mapFrom = tr.steps.length + if (convertNewlines === false) { + let $after = tr.doc.resolve(pos + depth) + replaceLinebreaks(tr, $after.node(), $after.before(), mapFrom) + } + if (beforeType.inlineContent) + clearIncompatible(tr, pos + depth - 1, beforeType, + $before.node().contentMatchAt($before.index()), convertNewlines == null) + let mapping = tr.mapping.slice(mapFrom), start = mapping.map(pos - depth) + tr.step(new ReplaceStep(start, mapping.map(pos + depth, - 1), Slice.empty, true)) + if (convertNewlines === true) { + let $full = tr.doc.resolve(start) + replaceNewlines(tr, $full.node(), $full.before(), tr.steps.length) + } + return tr +} + +/// Try to find a point where a node of the given type can be inserted +/// near `pos`, by searching up the node hierarchy when `pos` itself +/// isn't a valid place but is at the start or end of a node. Return +/// null if no position was found. +export function insertPoint(doc: Node, pos: number, nodeType: NodeType): number | null { + let $pos = doc.resolve(pos) + if ($pos.parent.canReplaceWith($pos.index(), $pos.index(), nodeType)) return pos + + if ($pos.parentOffset == 0) + for (let d = $pos.depth - 1; d >= 0; d--) { + let index = $pos.index(d) + if ($pos.node(d).canReplaceWith(index, index, nodeType)) return $pos.before(d + 1) + if (index > 0) return null + } + if ($pos.parentOffset == $pos.parent.content.size) + for (let d = $pos.depth - 1; d >= 0; d--) { + let index = $pos.indexAfter(d) + if ($pos.node(d).canReplaceWith(index, index, nodeType)) return $pos.after(d + 1) + if (index < $pos.node(d).childCount) return null + } + return null +} + +/// Finds a position at or around the given position where the given +/// slice can be inserted. Will look at parent nodes' nearest boundary +/// and try there, even if the original position wasn't directly at the +/// start or end of that node. Returns null when no position was found. +export function dropPoint(doc: Node, pos: number, slice: Slice): number | null { + let $pos = doc.resolve(pos) + if (!slice.content.size) return pos + let content = slice.content + for (let i = 0; i < slice.openStart; i++) content = content.firstChild!.content + for (let pass = 1; pass <= (slice.openStart == 0 && slice.size ? 2 : 1); pass++) { + for (let d = $pos.depth; d >= 0; d--) { + let bias = d == $pos.depth ? 0 : $pos.pos <= ($pos.start(d + 1) + $pos.end(d + 1)) / 2 ? -1 : 1 + let insertPos = $pos.index(d) + (bias > 0 ? 1 : 0) + let parent = $pos.node(d), fits: boolean | null = false + if (pass == 1) { + fits = parent.canReplace(insertPos, insertPos, content) + } else { + let wrapping = parent.contentMatchAt(insertPos).findWrapping(content.firstChild!.type) + fits = wrapping && parent.canReplaceWith(insertPos, insertPos, wrapping[0]) + } + if (fits) + return bias == 0 ? $pos.pos : bias < 0 ? $pos.before(d + 1) : $pos.after(d + 1) + } + } + return null +} diff --git a/third_party/js/prosemirror/prosemirror-transform/src/transform.ts b/third_party/js/prosemirror/prosemirror-transform/src/transform.ts @@ -0,0 +1,252 @@ +import {Node, NodeType, Mark, MarkType, ContentMatch, Slice, Fragment, NodeRange, Attrs} from "prosemirror-model" + +import {Mapping} from "./map" +import {Step} from "./step" +import {addMark, removeMark, clearIncompatible} from "./mark" +import {replaceStep, replaceRange, replaceRangeWith, deleteRange} from "./replace" +import {lift, wrap, setBlockType, setNodeMarkup, split, join} from "./structure" +import {AttrStep, DocAttrStep} from "./attr_step" +import {AddNodeMarkStep, RemoveNodeMarkStep} from "./mark_step" + +/// @internal +export let TransformError = class extends Error {} + +TransformError = function TransformError(this: any, message: string) { + let err = Error.call(this, message) + ;(err as any).__proto__ = TransformError.prototype + return err +} as any + +TransformError.prototype = Object.create(Error.prototype) +TransformError.prototype.constructor = TransformError +TransformError.prototype.name = "TransformError" + +/// Abstraction to build up and track an array of +/// [steps](#transform.Step) representing a document transformation. +/// +/// Most transforming methods return the `Transform` object itself, so +/// that they can be chained. +export class Transform { + /// The steps in this transform. + readonly steps: Step[] = [] + /// The documents before each of the steps. + readonly docs: Node[] = [] + /// A mapping with the maps for each of the steps in this transform. + readonly mapping: Mapping = new Mapping + + /// Create a transform that starts with the given document. + constructor( + /// The current document (the result of applying the steps in the + /// transform). + public doc: Node + ) {} + + /// The starting document. + get before() { return this.docs.length ? this.docs[0] : this.doc } + + /// Apply a new step in this transform, saving the result. Throws an + /// error when the step fails. + step(step: Step) { + let result = this.maybeStep(step) + if (result.failed) throw new TransformError(result.failed) + return this + } + + /// Try to apply a step in this transformation, ignoring it if it + /// fails. Returns the step result. + maybeStep(step: Step) { + let result = step.apply(this.doc) + if (!result.failed) this.addStep(step, result.doc!) + return result + } + + /// True when the document has been changed (when there are any + /// steps). + get docChanged() { + return this.steps.length > 0 + } + + /// @internal + addStep(step: Step, doc: Node) { + this.docs.push(this.doc) + this.steps.push(step) + this.mapping.appendMap(step.getMap()) + this.doc = doc + } + + /// Replace the part of the document between `from` and `to` with the + /// given `slice`. + replace(from: number, to = from, slice = Slice.empty): this { + let step = replaceStep(this.doc, from, to, slice) + if (step) this.step(step) + return this + } + + /// Replace the given range with the given content, which may be a + /// fragment, node, or array of nodes. + replaceWith(from: number, to: number, content: Fragment | Node | readonly Node[]): this { + return this.replace(from, to, new Slice(Fragment.from(content), 0, 0)) + } + + /// Delete the content between the given positions. + delete(from: number, to: number): this { + return this.replace(from, to, Slice.empty) + } + + /// Insert the given content at the given position. + insert(pos: number, content: Fragment | Node | readonly Node[]): this { + return this.replaceWith(pos, pos, content) + } + + /// Replace a range of the document with a given slice, using + /// `from`, `to`, and the slice's + /// [`openStart`](#model.Slice.openStart) property as hints, rather + /// than fixed start and end points. This method may grow the + /// replaced area or close open nodes in the slice in order to get a + /// fit that is more in line with WYSIWYG expectations, by dropping + /// fully covered parent nodes of the replaced region when they are + /// marked [non-defining as + /// context](#model.NodeSpec.definingAsContext), or including an + /// open parent node from the slice that _is_ marked as [defining + /// its content](#model.NodeSpec.definingForContent). + /// + /// This is the method, for example, to handle paste. The similar + /// [`replace`](#transform.Transform.replace) method is a more + /// primitive tool which will _not_ move the start and end of its given + /// range, and is useful in situations where you need more precise + /// control over what happens. + replaceRange(from: number, to: number, slice: Slice): this { + replaceRange(this, from, to, slice) + return this + } + + /// Replace the given range with a node, but use `from` and `to` as + /// hints, rather than precise positions. When from and to are the same + /// and are at the start or end of a parent node in which the given + /// node doesn't fit, this method may _move_ them out towards a parent + /// that does allow the given node to be placed. When the given range + /// completely covers a parent node, this method may completely replace + /// that parent node. + replaceRangeWith(from: number, to: number, node: Node): this { + replaceRangeWith(this, from, to, node) + return this + } + + /// Delete the given range, expanding it to cover fully covered + /// parent nodes until a valid replace is found. + deleteRange(from: number, to: number): this { + deleteRange(this, from, to) + return this + } + + /// Split the content in the given range off from its parent, if there + /// is sibling content before or after it, and move it up the tree to + /// the depth specified by `target`. You'll probably want to use + /// [`liftTarget`](#transform.liftTarget) to compute `target`, to make + /// sure the lift is valid. + lift(range: NodeRange, target: number): this { + lift(this, range, target) + return this + } + + /// Join the blocks around the given position. If depth is 2, their + /// last and first siblings are also joined, and so on. + join(pos: number, depth: number = 1): this { + join(this, pos, depth) + return this + } + + /// Wrap the given [range](#model.NodeRange) in the given set of wrappers. + /// The wrappers are assumed to be valid in this position, and should + /// probably be computed with [`findWrapping`](#transform.findWrapping). + wrap(range: NodeRange, wrappers: readonly {type: NodeType, attrs?: Attrs | null}[]): this { + wrap(this, range, wrappers) + return this + } + + /// Set the type of all textblocks (partly) between `from` and `to` to + /// the given node type with the given attributes. + setBlockType(from: number, to = from, type: NodeType, attrs: Attrs | null | ((oldNode: Node) => Attrs) = null): this { + setBlockType(this, from, to, type, attrs) + return this + } + + /// Change the type, attributes, and/or marks of the node at `pos`. + /// When `type` isn't given, the existing node type is preserved, + setNodeMarkup(pos: number, type?: NodeType | null, attrs: Attrs | null = null, marks?: readonly Mark[]): this { + setNodeMarkup(this, pos, type, attrs, marks) + return this + } + + /// Set a single attribute on a given node to a new value. + /// The `pos` addresses the document content. Use `setDocAttribute` + /// to set attributes on the document itself. + setNodeAttribute(pos: number, attr: string, value: any): this { + this.step(new AttrStep(pos, attr, value)) + return this + } + + /// Set a single attribute on the document to a new value. + setDocAttribute(attr: string, value: any): this { + this.step(new DocAttrStep(attr, value)) + return this + } + + /// Add a mark to the node at position `pos`. + addNodeMark(pos: number, mark: Mark): this { + this.step(new AddNodeMarkStep(pos, mark)) + return this + } + + /// Remove a mark (or all marks of the given type) from the node at + /// position `pos`. + removeNodeMark(pos: number, mark: Mark | MarkType): this { + let node = this.doc.nodeAt(pos) + if (!node) throw new RangeError("No node at position " + pos) + if (mark instanceof Mark) { + if (mark.isInSet(node.marks)) this.step(new RemoveNodeMarkStep(pos, mark)) + } else { + let set = node.marks, found, steps: Step[] = [] + while (found = mark.isInSet(set)) { + steps.push(new RemoveNodeMarkStep(pos, found)) + set = found.removeFromSet(set) + } + for (let i = steps.length - 1; i >= 0; i--) this.step(steps[i]) + } + return this + } + + /// Split the node at the given position, and optionally, if `depth` is + /// greater than one, any number of nodes above that. By default, the + /// parts split off will inherit the node type of the original node. + /// This can be changed by passing an array of types and attributes to + /// use after the split (with the outermost nodes coming first). + split(pos: number, depth = 1, typesAfter?: (null | {type: NodeType, attrs?: Attrs | null})[]) { + split(this, pos, depth, typesAfter) + return this + } + + /// Add the given mark to the inline content between `from` and `to`. + addMark(from: number, to: number, mark: Mark): this { + addMark(this, from, to, mark) + return this + } + + /// Remove marks from inline nodes between `from` and `to`. When + /// `mark` is a single mark, remove precisely that mark. When it is + /// a mark type, remove all marks of that type. When it is null, + /// remove all marks of any type. + removeMark(from: number, to: number, mark?: Mark | MarkType | null) { + removeMark(this, from, to, mark) + return this + } + + /// Removes all marks and nodes from the content of the node at + /// `pos` that don't match the given new parent node type. Accepts + /// an optional starting [content match](#model.ContentMatch) as + /// third argument. + clearIncompatible(pos: number, parentType: NodeType, match?: ContentMatch) { + clearIncompatible(this, pos, parentType, match) + return this + } +} diff --git a/third_party/js/prosemirror/prosemirror-view/LICENSE b/third_party/js/prosemirror/prosemirror-view/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/third_party/js/prosemirror/prosemirror-view/moz.yaml b/third_party/js/prosemirror/prosemirror-view/moz.yaml @@ -0,0 +1,64 @@ +# Version of this schema +schema: 1 + +bugzilla: + # Bugzilla product and component for this directory and subdirectories + product: Firefox + component: General + +# Document the source of externally hosted code +origin: + + # Short name of the package/library + name: prosemirror-view + + description: ProseMirror's view component + + # Full URL for the package's homepage/etc + # Usually different from repository url + url: https://prosemirror.net/ + + # Human-readable identifier for this version/release + # Generally "version NNN", "tag SSS", "bookmark SSS" + release: 1.41.4 (2025-12-01T17:21:00+01:00). + + # Revision to pull in + # Must be a long or short commit SHA (long preferred) or a tag + revision: 1.41.4 + + # The package's license, where possible using the mnemonic from + # https://spdx.org/licenses/ + # Multiple licenses can be specified (as a YAML list) + # A "LICENSE" file must exist containing the full license text + license: MIT + + # If the package's license is specified in a particular file, + # this is the name of the file. + # optional + license-file: LICENSE + +# Configuration for the automated vendoring system. +# optional +vendoring: + + # Repository URL to vendor from + # eg. https://github.com/kinetiknz/nestegg + # Any repository host can be specified here, however initially we'll only + # support automated vendoring from selected sources. + url: https://github.com/ProseMirror/prosemirror-view + + # Type of hosting for the upstream repository + # Valid values are 'gitlab', 'github', googlesource + source-hosting: github + + # Whether to track by commit or tag + tracking: tag + + exclude: + - "**" + + include: + - LICENSE + - src/ + - style/ + - package.json diff --git a/third_party/js/prosemirror/prosemirror-view/package.json b/third_party/js/prosemirror/prosemirror-view/package.json @@ -0,0 +1,43 @@ +{ + "name": "prosemirror-view", + "version": "1.41.4", + "description": "ProseMirror's view component", + "type": "module", + "main": "dist/index.cjs", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist/index.cjs" + }, + "./style/prosemirror.css": "./style/prosemirror.css" + }, + "sideEffects": ["./style/prosemirror.css"], + "style": "style/prosemirror.css", + "license": "MIT", + "maintainers": [ + { + "name": "Marijn Haverbeke", + "email": "marijn@haverbeke.berlin", + "web": "http://marijnhaverbeke.nl" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/prosemirror/prosemirror-view.git" + }, + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + }, + "devDependencies": { + "@prosemirror/buildhelper": "^0.1.5", + "prosemirror-test-builder": "^1.0.0" + }, + "scripts": { + "test": "pm-runtests", + "prepare": "pm-buildhelper src/index.ts" + } +} diff --git a/third_party/js/prosemirror/prosemirror-view/src/README.md b/third_party/js/prosemirror/prosemirror-view/src/README.md @@ -0,0 +1,38 @@ +ProseMirror's view module displays a given [editor +state](#state.EditorState) in the DOM, and handles user events. + +Make sure you load `style/prosemirror.css` as a stylesheet when using +this module. + +@EditorView + +### Props + +@EditorProps + +@NodeViewConstructor + +@MarkViewConstructor + +@DirectEditorProps + +@NodeView + +@MarkView + +@ViewMutationRecord + +@DOMEventMap + +### Decorations + +Decorations make it possible to influence the way the document is +drawn, without actually changing the document. + +@Decoration + +@DecorationAttrs + +@DecorationSet + +@DecorationSource diff --git a/third_party/js/prosemirror/prosemirror-view/src/browser.ts b/third_party/js/prosemirror/prosemirror-view/src/browser.ts @@ -0,0 +1,24 @@ +const nav = typeof navigator != "undefined" ? navigator : null +const doc = typeof document != "undefined" ? document : null +const agent = (nav && nav.userAgent) || "" + +const ie_edge = /Edge\/(\d+)/.exec(agent) +const ie_upto10 = /MSIE \d/.exec(agent) +const ie_11up = /Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(agent) + +export const ie = !!(ie_upto10 || ie_11up || ie_edge) +export const ie_version = ie_upto10 ? (document as any).documentMode : ie_11up ? +ie_11up[1] : ie_edge ? +ie_edge[1] : 0 +export const gecko = !ie && /gecko\/(\d+)/i.test(agent) +export const gecko_version = gecko && +(/Firefox\/(\d+)/.exec(agent) || [0, 0])[1] + +const _chrome = !ie && /Chrome\/(\d+)/.exec(agent) +export const chrome = !!_chrome +export const chrome_version = _chrome ? +_chrome[1] : 0 +export const safari = !ie && !!nav && /Apple Computer/.test(nav.vendor) +// Is true for both iOS and iPadOS for convenience +export const ios = safari && (/Mobile\/\w+/.test(agent) || !!nav && nav.maxTouchPoints > 2) +export const mac = ios || (nav ? /Mac/.test(nav.platform) : false) +export const windows = nav ? /Win/.test(nav.platform) : false +export const android = /Android \d/.test(agent) +export const webkit = !!doc && "webkitFontSmoothing" in doc.documentElement.style +export const webkit_version = webkit ? +(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent) || [0, 0])[1] : 0 diff --git a/third_party/js/prosemirror/prosemirror-view/src/capturekeys.ts b/third_party/js/prosemirror/prosemirror-view/src/capturekeys.ts @@ -0,0 +1,345 @@ +import {Selection, NodeSelection, TextSelection, AllSelection, EditorState} from "prosemirror-state" +import {EditorView} from "./index" +import * as browser from "./browser" +import {domIndex, selectionCollapsed, hasBlockDesc} from "./dom" +import {selectionToDOM} from "./selection" + +function moveSelectionBlock(state: EditorState, dir: number) { + let {$anchor, $head} = state.selection + let $side = dir > 0 ? $anchor.max($head) : $anchor.min($head) + let $start = !$side.parent.inlineContent ? $side : $side.depth ? state.doc.resolve(dir > 0 ? $side.after() : $side.before()) : null + return $start && Selection.findFrom($start, dir) +} + +function apply(view: EditorView, sel: Selection) { + view.dispatch(view.state.tr.setSelection(sel).scrollIntoView()) + return true +} + +function selectHorizontally(view: EditorView, dir: number, mods: string) { + let sel = view.state.selection + if (sel instanceof TextSelection) { + if (mods.indexOf("s") > -1) { + let {$head} = sel, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter + if (!node || node.isText || !node.isLeaf) return false + let $newHead = view.state.doc.resolve($head.pos + node.nodeSize * (dir < 0 ? -1 : 1)) + return apply(view, new TextSelection(sel.$anchor, $newHead)) + } else if (!sel.empty) { + return false + } else if (view.endOfTextblock(dir > 0 ? "forward" : "backward")) { + let next = moveSelectionBlock(view.state, dir) + if (next && (next instanceof NodeSelection)) return apply(view, next) + return false + } else if (!(browser.mac && mods.indexOf("m") > -1)) { + let $head = sel.$head, node = $head.textOffset ? null : dir < 0 ? $head.nodeBefore : $head.nodeAfter, desc + if (!node || node.isText) return false + let nodePos = dir < 0 ? $head.pos - node.nodeSize : $head.pos + if (!(node.isAtom || (desc = view.docView.descAt(nodePos)) && !desc.contentDOM)) return false + if (NodeSelection.isSelectable(node)) { + return apply(view, new NodeSelection(dir < 0 ? view.state.doc.resolve($head.pos - node.nodeSize) : $head)) + } else if (browser.webkit) { + // Chrome and Safari will introduce extra pointless cursor + // positions around inline uneditable nodes, so we have to + // take over and move the cursor past them (#937) + return apply(view, new TextSelection(view.state.doc.resolve(dir < 0 ? nodePos : nodePos + node.nodeSize))) + } else { + return false + } + } + } else if (sel instanceof NodeSelection && sel.node.isInline) { + return apply(view, new TextSelection(dir > 0 ? sel.$to : sel.$from)) + } else { + let next = moveSelectionBlock(view.state, dir) + if (next) return apply(view, next) + return false + } +} + +function nodeLen(node: Node) { + return node.nodeType == 3 ? node.nodeValue!.length : node.childNodes.length +} + +function isIgnorable(dom: Node, dir: number) { + let desc = dom.pmViewDesc + return desc && desc.size == 0 && (dir < 0 || dom.nextSibling || dom.nodeName != "BR") +} + +function skipIgnoredNodes(view: EditorView, dir: number) { + return dir < 0 ? skipIgnoredNodesBefore(view) : skipIgnoredNodesAfter(view) +} + +// Make sure the cursor isn't directly after one or more ignored +// nodes, which will confuse the browser's cursor motion logic. +function skipIgnoredNodesBefore(view: EditorView) { + let sel = view.domSelectionRange() + let node = sel.focusNode!, offset = sel.focusOffset + if (!node) return + let moveNode, moveOffset: number | undefined, force = false + // Gecko will do odd things when the selection is directly in front + // of a non-editable node, so in that case, move it into the next + // node if possible. Issue prosemirror/prosemirror#832. + if (browser.gecko && node.nodeType == 1 && offset < nodeLen(node) && isIgnorable(node.childNodes[offset], -1)) force = true + for (;;) { + if (offset > 0) { + if (node.nodeType != 1) { + break + } else { + let before = node.childNodes[offset - 1] + if (isIgnorable(before, -1)) { + moveNode = node + moveOffset = --offset + } else if (before.nodeType == 3) { + node = before + offset = node.nodeValue!.length + } else break + } + } else if (isBlockNode(node)) { + break + } else { + let prev = node.previousSibling + while (prev && isIgnorable(prev, -1)) { + moveNode = node.parentNode + moveOffset = domIndex(prev) + prev = prev.previousSibling + } + if (!prev) { + node = node.parentNode! + if (node == view.dom) break + offset = 0 + } else { + node = prev + offset = nodeLen(node) + } + } + } + if (force) setSelFocus(view, node, offset) + else if (moveNode) setSelFocus(view, moveNode, moveOffset!) +} + +// Make sure the cursor isn't directly before one or more ignored +// nodes. +function skipIgnoredNodesAfter(view: EditorView) { + let sel = view.domSelectionRange() + let node = sel.focusNode!, offset = sel.focusOffset + if (!node) return + let len = nodeLen(node) + let moveNode, moveOffset: number | undefined + for (;;) { + if (offset < len) { + if (node.nodeType != 1) break + let after = node.childNodes[offset] + if (isIgnorable(after, 1)) { + moveNode = node + moveOffset = ++offset + } + else break + } else if (isBlockNode(node)) { + break + } else { + let next = node.nextSibling + while (next && isIgnorable(next, 1)) { + moveNode = next.parentNode + moveOffset = domIndex(next) + 1 + next = next.nextSibling + } + if (!next) { + node = node.parentNode! + if (node == view.dom) break + offset = len = 0 + } else { + node = next + offset = 0 + len = nodeLen(node) + } + } + } + if (moveNode) setSelFocus(view, moveNode, moveOffset!) +} + +function isBlockNode(dom: Node) { + let desc = dom.pmViewDesc + return desc && desc.node && desc.node.isBlock +} + +function textNodeAfter(node: Node | null, offset: number): Text | undefined { + while (node && offset == node.childNodes.length && !hasBlockDesc(node)) { + offset = domIndex(node) + 1 + node = node.parentNode + } + while (node && offset < node.childNodes.length) { + let next = node.childNodes[offset] + if (next.nodeType == 3) return next as Text + if (next.nodeType == 1 && (next as HTMLElement).contentEditable == "false") break + node = next + offset = 0 + } +} + +function textNodeBefore(node: Node | null, offset: number): Text | undefined { + while (node && !offset && !hasBlockDesc(node)) { + offset = domIndex(node) + node = node.parentNode + } + while (node && offset) { + let next = node.childNodes[offset - 1] + if (next.nodeType == 3) return next as Text + if (next.nodeType == 1 && (next as HTMLElement).contentEditable == "false") break + node = next + offset = node.childNodes.length + } +} + +function setSelFocus(view: EditorView, node: Node, offset: number) { + if (node.nodeType != 3) { + let before, after + if (after = textNodeAfter(node, offset)) { + node = after + offset = 0 + } else if (before = textNodeBefore(node, offset)) { + node = before + offset = before.nodeValue!.length + } + } + + let sel = view.domSelection() + if (!sel) return + if (selectionCollapsed(sel)) { + let range = document.createRange() + range.setEnd(node, offset) + range.setStart(node, offset) + sel.removeAllRanges() + sel.addRange(range) + } else if (sel.extend) { + sel.extend(node, offset) + } + view.domObserver.setCurSelection() + let {state} = view + // If no state update ends up happening, reset the selection. + setTimeout(() => { + if (view.state == state) selectionToDOM(view) + }, 50) +} + +function findDirection(view: EditorView, pos: number): "rtl" | "ltr" { + let $pos = view.state.doc.resolve(pos) + if (!(browser.chrome || browser.windows) && $pos.parent.inlineContent) { + let coords = view.coordsAtPos(pos) + if (pos > $pos.start()) { + let before = view.coordsAtPos(pos - 1) + let mid = (before.top + before.bottom) / 2 + if (mid > coords.top && mid < coords.bottom && Math.abs(before.left - coords.left) > 1) + return before.left < coords.left ? "ltr" : "rtl" + } + if (pos < $pos.end()) { + let after = view.coordsAtPos(pos + 1) + let mid = (after.top + after.bottom) / 2 + if (mid > coords.top && mid < coords.bottom && Math.abs(after.left - coords.left) > 1) + return after.left > coords.left ? "ltr" : "rtl" + } + } + let computed = getComputedStyle(view.dom).direction + return computed == "rtl" ? "rtl" : "ltr" +} + +// Check whether vertical selection motion would involve node +// selections. If so, apply it (if not, the result is left to the +// browser) +function selectVertically(view: EditorView, dir: number, mods: string) { + let sel = view.state.selection + if (sel instanceof TextSelection && !sel.empty || mods.indexOf("s") > -1) return false + if (browser.mac && mods.indexOf("m") > -1) return false + let {$from, $to} = sel + + if (!$from.parent.inlineContent || view.endOfTextblock(dir < 0 ? "up" : "down")) { + let next = moveSelectionBlock(view.state, dir) + if (next && (next instanceof NodeSelection)) + return apply(view, next) + } + if (!$from.parent.inlineContent) { + let side = dir < 0 ? $from : $to + let beyond = sel instanceof AllSelection ? Selection.near(side, dir) : Selection.findFrom(side, dir) + return beyond ? apply(view, beyond) : false + } + return false +} + +function stopNativeHorizontalDelete(view: EditorView, dir: number) { + if (!(view.state.selection instanceof TextSelection)) return true + let {$head, $anchor, empty} = view.state.selection + if (!$head.sameParent($anchor)) return true + if (!empty) return false + if (view.endOfTextblock(dir > 0 ? "forward" : "backward")) return true + let nextNode = !$head.textOffset && (dir < 0 ? $head.nodeBefore : $head.nodeAfter) + if (nextNode && !nextNode.isText) { + let tr = view.state.tr + if (dir < 0) tr.delete($head.pos - nextNode.nodeSize, $head.pos) + else tr.delete($head.pos, $head.pos + nextNode.nodeSize) + view.dispatch(tr) + return true + } + return false +} + +function switchEditable(view: EditorView, node: HTMLElement, state: string) { + view.domObserver.stop() + node.contentEditable = state + view.domObserver.start() +} + +// Issue #867 / #1090 / https://bugs.chromium.org/p/chromium/issues/detail?id=903821 +// In which Safari (and at some point in the past, Chrome) does really +// wrong things when the down arrow is pressed when the cursor is +// directly at the start of a textblock and has an uneditable node +// after it +function safariDownArrowBug(view: EditorView) { + if (!browser.safari || view.state.selection.$head.parentOffset > 0) return false + let {focusNode, focusOffset} = view.domSelectionRange() + if (focusNode && focusNode.nodeType == 1 && focusOffset == 0 && + focusNode.firstChild && (focusNode.firstChild as HTMLElement).contentEditable == "false") { + let child = focusNode.firstChild as HTMLElement + switchEditable(view, child, "true") + setTimeout(() => switchEditable(view, child, "false"), 20) + } + return false +} + +// A backdrop key mapping used to make sure we always suppress keys +// that have a dangerous default effect, even if the commands they are +// bound to return false, and to make sure that cursor-motion keys +// find a cursor (as opposed to a node selection) when pressed. For +// cursor-motion keys, the code in the handlers also takes care of +// block selections. + +function getMods(event: KeyboardEvent) { + let result = "" + if (event.ctrlKey) result += "c" + if (event.metaKey) result += "m" + if (event.altKey) result += "a" + if (event.shiftKey) result += "s" + return result +} + +export function captureKeyDown(view: EditorView, event: KeyboardEvent) { + let code = event.keyCode, mods = getMods(event) + if (code == 8 || (browser.mac && code == 72 && mods == "c")) { // Backspace, Ctrl-h on Mac + return stopNativeHorizontalDelete(view, -1) || skipIgnoredNodes(view, -1) + } else if ((code == 46 && !event.shiftKey) || (browser.mac && code == 68 && mods == "c")) { // Delete, Ctrl-d on Mac + return stopNativeHorizontalDelete(view, 1) || skipIgnoredNodes(view, 1) + } else if (code == 13 || code == 27) { // Enter, Esc + return true + } else if (code == 37 || (browser.mac && code == 66 && mods == "c")) { // Left arrow, Ctrl-b on Mac + let dir = code == 37 ? (findDirection(view, view.state.selection.from) == "ltr" ? -1 : 1) : -1 + return selectHorizontally(view, dir, mods) || skipIgnoredNodes(view, dir) + } else if (code == 39 || (browser.mac && code == 70 && mods == "c")) { // Right arrow, Ctrl-f on Mac + let dir = code == 39 ? (findDirection(view, view.state.selection.from) == "ltr" ? 1 : -1) : 1 + return selectHorizontally(view, dir, mods) || skipIgnoredNodes(view, dir) + } else if (code == 38 || (browser.mac && code == 80 && mods == "c")) { // Up arrow, Ctrl-p on Mac + return selectVertically(view, -1, mods) || skipIgnoredNodes(view, -1) + } else if (code == 40 || (browser.mac && code == 78 && mods == "c")) { // Down arrow, Ctrl-n on Mac + return safariDownArrowBug(view) || selectVertically(view, 1, mods) || skipIgnoredNodes(view, 1) + } else if (mods == (browser.mac ? "m" : "c") && + (code == 66 || code == 73 || code == 89 || code == 90)) { // Mod-[biyz] + return true + } + return false +} diff --git a/third_party/js/prosemirror/prosemirror-view/src/clipboard.ts b/third_party/js/prosemirror/prosemirror-view/src/clipboard.ts @@ -0,0 +1,263 @@ +import {Slice, Fragment, DOMParser, DOMSerializer, ResolvedPos, NodeType, Node} from "prosemirror-model" +import * as browser from "./browser" +import {EditorView} from "./index" + +export function serializeForClipboard(view: EditorView, slice: Slice) { + view.someProp("transformCopied", f => { slice = f(slice!, view) }) + + let context = [], {content, openStart, openEnd} = slice + while (openStart > 1 && openEnd > 1 && content.childCount == 1 && content.firstChild!.childCount == 1) { + openStart-- + openEnd-- + let node = content.firstChild! + context.push(node.type.name, node.attrs != node.type.defaultAttrs ? node.attrs : null) + content = node.content + } + + let serializer = view.someProp("clipboardSerializer") || DOMSerializer.fromSchema(view.state.schema) + let doc = detachedDoc(), wrap = doc.createElement("div") + wrap.appendChild(serializer.serializeFragment(content, {document: doc})) + + let firstChild = wrap.firstChild, needsWrap, wrappers = 0 + while (firstChild && firstChild.nodeType == 1 && (needsWrap = wrapMap[firstChild.nodeName.toLowerCase()])) { + for (let i = needsWrap.length - 1; i >= 0; i--) { + let wrapper = doc.createElement(needsWrap[i]) + while (wrap.firstChild) wrapper.appendChild(wrap.firstChild) + wrap.appendChild(wrapper) + wrappers++ + } + firstChild = wrap.firstChild + } + + if (firstChild && firstChild.nodeType == 1) + (firstChild as HTMLElement).setAttribute( + "data-pm-slice", `${openStart} ${openEnd}${wrappers ? ` -${wrappers}` : ""} ${JSON.stringify(context)}`) + + let text = view.someProp("clipboardTextSerializer", f => f(slice, view)) || + slice.content.textBetween(0, slice.content.size, "\n\n") + + return {dom: wrap, text, slice} +} + +// Read a slice of content from the clipboard (or drop data). +export function parseFromClipboard(view: EditorView, text: string, html: string | null, plainText: boolean, $context: ResolvedPos) { + let inCode = $context.parent.type.spec.code + let dom: HTMLElement | undefined, slice: Slice | undefined + if (!html && !text) return null + let asText = !!text && (plainText || inCode || !html) + if (asText) { + view.someProp("transformPastedText", f => { text = f(text, inCode || plainText, view) }) + if (inCode) { + slice = new Slice(Fragment.from(view.state.schema.text(text.replace(/\r\n?/g, "\n"))), 0, 0) + view.someProp("transformPasted", f => { slice = f(slice!, view, true) }) + return slice + } + let parsed = view.someProp("clipboardTextParser", f => f(text, $context, plainText, view)) + if (parsed) { + slice = parsed + } else { + let marks = $context.marks() + let {schema} = view.state, serializer = DOMSerializer.fromSchema(schema) + dom = document.createElement("div") + text.split(/(?:\r\n?|\n)+/).forEach(block => { + let p = dom!.appendChild(document.createElement("p")) + if (block) p.appendChild(serializer.serializeNode(schema.text(block, marks))) + }) + } + } else { + view.someProp("transformPastedHTML", f => { html = f(html!, view) }) + dom = readHTML(html!) + if (browser.webkit) restoreReplacedSpaces(dom) + } + + let contextNode = dom && dom.querySelector("[data-pm-slice]") + let sliceData = contextNode && /^(\d+) (\d+)(?: -(\d+))? (.*)/.exec(contextNode.getAttribute("data-pm-slice") || "") + if (sliceData && sliceData[3]) for (let i = +sliceData[3]; i > 0; i--) { + let child = dom!.firstChild + while (child && child.nodeType != 1) child = child.nextSibling + if (!child) break + dom = child as HTMLElement + } + + if (!slice) { + let parser = view.someProp("clipboardParser") || view.someProp("domParser") || DOMParser.fromSchema(view.state.schema) + slice = parser.parseSlice(dom!, { + preserveWhitespace: !!(asText || sliceData), + context: $context, + ruleFromNode(dom) { + if (dom.nodeName == "BR" && !dom.nextSibling && + dom.parentNode && !inlineParents.test(dom.parentNode.nodeName)) return {ignore: true} + return null + } + }) + } + if (sliceData) { + slice = addContext(closeSlice(slice, +sliceData[1], +sliceData[2]), sliceData[4]) + } else { // HTML wasn't created by ProseMirror. Make sure top-level siblings are coherent + slice = Slice.maxOpen(normalizeSiblings(slice.content, $context), true) + if (slice.openStart || slice.openEnd) { + let openStart = 0, openEnd = 0 + for (let node = slice.content.firstChild; openStart < slice.openStart && !node!.type.spec.isolating; + openStart++, node = node!.firstChild) {} + for (let node = slice.content.lastChild; openEnd < slice.openEnd && !node!.type.spec.isolating; + openEnd++, node = node!.lastChild) {} + slice = closeSlice(slice, openStart, openEnd) + } + } + + view.someProp("transformPasted", f => { slice = f(slice!, view, asText) }) + return slice +} + +const inlineParents = /^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i + +// Takes a slice parsed with parseSlice, which means there hasn't been +// any content-expression checking done on the top nodes, tries to +// find a parent node in the current context that might fit the nodes, +// and if successful, rebuilds the slice so that it fits into that parent. +// +// This addresses the problem that Transform.replace expects a +// coherent slice, and will fail to place a set of siblings that don't +// fit anywhere in the schema. +function normalizeSiblings(fragment: Fragment, $context: ResolvedPos) { + if (fragment.childCount < 2) return fragment + for (let d = $context.depth; d >= 0; d--) { + let parent = $context.node(d) + let match = parent.contentMatchAt($context.index(d)) + let lastWrap: readonly NodeType[] | undefined, result: Node[] | null = [] + fragment.forEach(node => { + if (!result) return + let wrap = match.findWrapping(node.type), inLast + if (!wrap) return result = null + if (inLast = result.length && lastWrap!.length && addToSibling(wrap, lastWrap!, node, result[result.length - 1], 0)) { + result[result.length - 1] = inLast + } else { + if (result.length) result[result.length - 1] = closeRight(result[result.length - 1], lastWrap!.length) + let wrapped = withWrappers(node, wrap) + result.push(wrapped) + match = match.matchType(wrapped.type)! + lastWrap = wrap + } + }) + if (result) return Fragment.from(result) + } + return fragment +} + +function withWrappers(node: Node, wrap: readonly NodeType[], from = 0) { + for (let i = wrap.length - 1; i >= from; i--) + node = wrap[i].create(null, Fragment.from(node)) + return node +} + +// Used to group adjacent nodes wrapped in similar parents by +// normalizeSiblings into the same parent node +function addToSibling(wrap: readonly NodeType[], lastWrap: readonly NodeType[], + node: Node, sibling: Node, depth: number): Node | undefined { + if (depth < wrap.length && depth < lastWrap.length && wrap[depth] == lastWrap[depth]) { + let inner = addToSibling(wrap, lastWrap, node, sibling.lastChild!, depth + 1) + if (inner) return sibling.copy(sibling.content.replaceChild(sibling.childCount - 1, inner)) + let match = sibling.contentMatchAt(sibling.childCount) + if (match.matchType(depth == wrap.length - 1 ? node.type : wrap[depth + 1])) + return sibling.copy(sibling.content.append(Fragment.from(withWrappers(node, wrap, depth + 1)))) + } +} + +function closeRight(node: Node, depth: number) { + if (depth == 0) return node + let fragment = node.content.replaceChild(node.childCount - 1, closeRight(node.lastChild!, depth - 1)) + let fill = node.contentMatchAt(node.childCount).fillBefore(Fragment.empty, true)! + return node.copy(fragment.append(fill)) +} + +function closeRange(fragment: Fragment, side: number, from: number, to: number, depth: number, openEnd: number) { + let node = side < 0 ? fragment.firstChild! : fragment.lastChild!, inner = node.content + if (fragment.childCount > 1) openEnd = 0 + if (depth < to - 1) inner = closeRange(inner, side, from, to, depth + 1, openEnd) + if (depth >= from) + inner = side < 0 ? node.contentMatchAt(0)!.fillBefore(inner, openEnd <= depth)!.append(inner) + : inner.append(node.contentMatchAt(node.childCount)!.fillBefore(Fragment.empty, true)!) + return fragment.replaceChild(side < 0 ? 0 : fragment.childCount - 1, node.copy(inner)) +} + +function closeSlice(slice: Slice, openStart: number, openEnd: number) { + if (openStart < slice.openStart) + slice = new Slice(closeRange(slice.content, -1, openStart, slice.openStart, 0, slice.openEnd), openStart, slice.openEnd) + if (openEnd < slice.openEnd) + slice = new Slice(closeRange(slice.content, 1, openEnd, slice.openEnd, 0, 0), slice.openStart, openEnd) + return slice +} + +// Trick from jQuery -- some elements must be wrapped in other +// elements for innerHTML to work. I.e. if you do `div.innerHTML = +// "<td>..</td>"` the table cells are ignored. +const wrapMap: {[node: string]: string[]} = { + thead: ["table"], + tbody: ["table"], + tfoot: ["table"], + caption: ["table"], + colgroup: ["table"], + col: ["table", "colgroup"], + tr: ["table", "tbody"], + td: ["table", "tbody", "tr"], + th: ["table", "tbody", "tr"] +} + +let _detachedDoc: Document | null = null +function detachedDoc() { + return _detachedDoc || (_detachedDoc = document.implementation.createHTMLDocument("title")) +} + +let _policy: any = null + +function maybeWrapTrusted(html: string): string { + let trustedTypes = (window as any).trustedTypes + if (!trustedTypes) return html + // With the require-trusted-types-for CSP, Chrome will block + // innerHTML, even on a detached document. This wraps the string in + // a way that makes the browser allow us to use its parser again. + if (!_policy) + _policy = trustedTypes.defaultPolicy || trustedTypes.createPolicy("ProseMirrorClipboard", {createHTML: (s: string) => s}) + return _policy.createHTML(html) +} + +function readHTML(html: string) { + let metas = /^(\s*<meta [^>]*>)*/.exec(html) + if (metas) html = html.slice(metas[0].length) + let elt = detachedDoc().createElement("div") + let firstTag = /<([a-z][^>\s]+)/i.exec(html), wrap + if (wrap = firstTag && wrapMap[firstTag[1].toLowerCase()]) + html = wrap.map(n => "<" + n + ">").join("") + html + wrap.map(n => "</" + n + ">").reverse().join("") + elt.innerHTML = maybeWrapTrusted(html) + if (wrap) for (let i = 0; i < wrap.length; i++) elt = elt.querySelector(wrap[i]) || elt + return elt +} + +// Webkit browsers do some hard-to-predict replacement of regular +// spaces with non-breaking spaces when putting content on the +// clipboard. This tries to convert such non-breaking spaces (which +// will be wrapped in a plain span on Chrome, a span with class +// Apple-converted-space on Safari) back to regular spaces. +function restoreReplacedSpaces(dom: HTMLElement) { + let nodes = dom.querySelectorAll(browser.chrome ? "span:not([class]):not([style])" : "span.Apple-converted-space") + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i] + if (node.childNodes.length == 1 && node.textContent == "\u00a0" && node.parentNode) + node.parentNode.replaceChild(dom.ownerDocument.createTextNode(" "), node) + } +} + +function addContext(slice: Slice, context: string) { + if (!slice.size) return slice + let schema = slice.content.firstChild!.type.schema, array + try { array = JSON.parse(context) } + catch(e) { return slice } + let {content, openStart, openEnd} = slice + for (let i = array.length - 2; i >= 0; i -= 2) { + let type = schema.nodes[array[i]] + if (!type || type.hasRequiredAttrs()) break + content = Fragment.from(type.create(array[i + 1], content)) + openStart++; openEnd++ + } + return new Slice(content, openStart, openEnd) +} diff --git a/third_party/js/prosemirror/prosemirror-view/src/decoration.ts b/third_party/js/prosemirror/prosemirror-view/src/decoration.ts @@ -0,0 +1,793 @@ +import {Node, Mark} from "prosemirror-model" +import {Mappable, Mapping} from "prosemirror-transform" +import {EditorView} from "./index" +import {DOMNode} from "./dom" + +function compareObjs(a: {[prop: string]: any}, b: {[prop: string]: any}) { + if (a == b) return true + for (let p in a) if (a[p] !== b[p]) return false + for (let p in b) if (!(p in a)) return false + return true +} + +export interface DecorationType { + spec: any + map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null + valid(node: Node, span: Decoration): boolean + eq(other: DecorationType): boolean + destroy(dom: DOMNode): void +} + +export type WidgetConstructor = ((view: EditorView, getPos: () => number | undefined) => DOMNode) | DOMNode + +export class WidgetType implements DecorationType { + spec: any + side: number + + constructor(readonly toDOM: WidgetConstructor, spec: any) { + this.spec = spec || noSpec + this.side = this.spec.side || 0 + } + + map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null { + let {pos, deleted} = mapping.mapResult(span.from + oldOffset, this.side < 0 ? -1 : 1) + return deleted ? null : new Decoration(pos - offset, pos - offset, this) + } + + valid() { return true } + + eq(other: WidgetType) { + return this == other || + (other instanceof WidgetType && + (this.spec.key && this.spec.key == other.spec.key || + this.toDOM == other.toDOM && compareObjs(this.spec, other.spec))) + } + + destroy(node: DOMNode) { + if (this.spec.destroy) this.spec.destroy(node) + } +} + +export class InlineType implements DecorationType { + spec: any + + constructor(readonly attrs: DecorationAttrs, spec: any) { + this.spec = spec || noSpec + } + + map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null { + let from = mapping.map(span.from + oldOffset, this.spec.inclusiveStart ? -1 : 1) - offset + let to = mapping.map(span.to + oldOffset, this.spec.inclusiveEnd ? 1 : -1) - offset + return from >= to ? null : new Decoration(from, to, this) + } + + valid(_: Node, span: Decoration) { return span.from < span.to } + + eq(other: DecorationType): boolean { + return this == other || + (other instanceof InlineType && compareObjs(this.attrs, other.attrs) && + compareObjs(this.spec, other.spec)) + } + + static is(span: Decoration) { return span.type instanceof InlineType } + + destroy() {} +} + +export class NodeType implements DecorationType { + spec: any + constructor(readonly attrs: DecorationAttrs, spec: any) { + this.spec = spec || noSpec + } + + map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null { + let from = mapping.mapResult(span.from + oldOffset, 1) + if (from.deleted) return null + let to = mapping.mapResult(span.to + oldOffset, -1) + if (to.deleted || to.pos <= from.pos) return null + return new Decoration(from.pos - offset, to.pos - offset, this) + } + + valid(node: Node, span: Decoration): boolean { + let {index, offset} = node.content.findIndex(span.from), child + return offset == span.from && !(child = node.child(index)).isText && offset + child.nodeSize == span.to + } + + eq(other: DecorationType): boolean { + return this == other || + (other instanceof NodeType && compareObjs(this.attrs, other.attrs) && + compareObjs(this.spec, other.spec)) + } + + destroy() {} +} + +/// Decoration objects can be provided to the view through the +/// [`decorations` prop](#view.EditorProps.decorations). They come in +/// several variants—see the static members of this class for details. +export class Decoration { + /// @internal + constructor( + /// The start position of the decoration. + readonly from: number, + /// The end position. Will be the same as `from` for [widget + /// decorations](#view.Decoration^widget). + readonly to: number, + /// @internal + readonly type: DecorationType + ) {} + + /// @internal + copy(from: number, to: number) { + return new Decoration(from, to, this.type) + } + + /// @internal + eq(other: Decoration, offset = 0) { + return this.type.eq(other.type) && this.from + offset == other.from && this.to + offset == other.to + } + + /// @internal + map(mapping: Mappable, offset: number, oldOffset: number) { + return this.type.map(mapping, this, offset, oldOffset) + } + + /// Creates a widget decoration, which is a DOM node that's shown in + /// the document at the given position. It is recommended that you + /// delay rendering the widget by passing a function that will be + /// called when the widget is actually drawn in a view, but you can + /// also directly pass a DOM node. `getPos` can be used to find the + /// widget's current document position. + static widget(pos: number, toDOM: WidgetConstructor, spec?: { + /// Controls which side of the document position this widget is + /// associated with. When negative, it is drawn before a cursor + /// at its position, and content inserted at that position ends + /// up after the widget. When zero (the default) or positive, the + /// widget is drawn after the cursor and content inserted there + /// ends up before the widget. + /// + /// When there are multiple widgets at a given position, their + /// `side` values determine the order in which they appear. Those + /// with lower values appear first. The ordering of widgets with + /// the same `side` value is unspecified. + /// + /// When `marks` is null, `side` also determines the marks that + /// the widget is wrapped in—those of the node before when + /// negative, those of the node after when positive. + side?: number + + /// By default, the cursor, when at the position of the widget, + /// will be strictly kept on the side indicated by + /// [`side`](#view.Decoration^widget^spec.side). Set this to true + /// to allow the DOM selection to stay on the other side if the + /// client sets it there. + /// + /// **Note**: Mapping of this decoration, which decides on which + /// side insertions at its position appear, will still happen + /// according to `side`, and keyboard cursor motion will not, + /// without further custom handling, visit both sides of the + /// widget. + relaxedSide?: boolean + + /// The precise set of marks to draw around the widget. + marks?: readonly Mark[] + + /// Can be used to control which DOM events, when they bubble out + /// of this widget, the editor view should ignore. + stopEvent?: (event: Event) => boolean + + /// When set (defaults to false), selection changes inside the + /// widget are ignored, and don't cause ProseMirror to try and + /// re-sync the selection with its selection state. + ignoreSelection?: boolean + + /// When comparing decorations of this type (in order to decide + /// whether it needs to be redrawn), ProseMirror will by default + /// compare the widget DOM node by identity. If you pass a key, + /// that key will be compared instead, which can be useful when + /// you generate decorations on the fly and don't want to store + /// and reuse DOM nodes. Make sure that any widgets with the same + /// key are interchangeable—if widgets differ in, for example, + /// the behavior of some event handler, they should get + /// different keys. + key?: string + + /// Called when the widget decoration is removed or the editor is + /// destroyed. + destroy?: (node: DOMNode) => void + + /// Specs allow arbitrary additional properties. + [key: string]: any + }): Decoration { + return new Decoration(pos, pos, new WidgetType(toDOM, spec)) + } + + /// Creates an inline decoration, which adds the given attributes to + /// each inline node between `from` and `to`. + static inline(from: number, to: number, attrs: DecorationAttrs, spec?: { + /// Determines how the left side of the decoration is + /// [mapped](#transform.Position_Mapping) when content is + /// inserted directly at that position. By default, the decoration + /// won't include the new content, but you can set this to `true` + /// to make it inclusive. + inclusiveStart?: boolean + + /// Determines how the right side of the decoration is mapped. + /// See + /// [`inclusiveStart`](#view.Decoration^inline^spec.inclusiveStart). + inclusiveEnd?: boolean + + /// Specs may have arbitrary additional properties. + [key: string]: any + }) { + return new Decoration(from, to, new InlineType(attrs, spec)) + } + + /// Creates a node decoration. `from` and `to` should point precisely + /// before and after a node in the document. That node, and only that + /// node, will receive the given attributes. + static node(from: number, to: number, attrs: DecorationAttrs, spec?: any) { + return new Decoration(from, to, new NodeType(attrs, spec)) + } + + /// The spec provided when creating this decoration. Can be useful + /// if you've stored extra information in that object. + get spec() { return this.type.spec } + + /// @internal + get inline() { return this.type instanceof InlineType } + + /// @internal + get widget() { return this.type instanceof WidgetType } +} + +/// A set of attributes to add to a decorated node. Most properties +/// simply directly correspond to DOM attributes of the same name, +/// which will be set to the property's value. These are exceptions: +export type DecorationAttrs = { + /// When non-null, the target node is wrapped in a DOM element of + /// this type (and the other attributes are applied to this element). + nodeName?: string + + /// A CSS class name or a space-separated set of class names to be + /// _added_ to the classes that the node already had. + class?: string + + /// A string of CSS to be _added_ to the node's existing `style` property. + style?: string + + /// Any other properties are treated as regular DOM attributes. + [attribute: string]: string | undefined +} + +const none: readonly any[] = [], noSpec = {} + +/// An object that can [provide](#view.EditorProps.decorations) +/// decorations. Implemented by [`DecorationSet`](#view.DecorationSet), +/// and passed to [node views](#view.EditorProps.nodeViews). +export interface DecorationSource { + /// Map the set of decorations in response to a change in the + /// document. + map: (mapping: Mapping, node: Node) => DecorationSource + /// @internal + locals(node: Node): readonly Decoration[] + /// Extract a DecorationSource containing decorations for the given child node at the given offset. + forChild(offset: number, child: Node): DecorationSource + /// @internal + eq(other: DecorationSource): boolean + /// Call the given function for each decoration set in the group. + forEachSet(f: (set: DecorationSet) => void): void +} + +/// A collection of [decorations](#view.Decoration), organized in such +/// a way that the drawing algorithm can efficiently use and compare +/// them. This is a persistent data structure—it is not modified, +/// updates create a new value. +export class DecorationSet implements DecorationSource { + /// @internal + local: readonly Decoration[] + /// @internal + children: readonly (number | DecorationSet)[] + + /// @internal + constructor(local: readonly Decoration[], children: readonly (number | DecorationSet)[]) { + this.local = local.length ? local : none + this.children = children.length ? children : none + } + + /// Create a set of decorations, using the structure of the given + /// document. This will consume (modify) the `decorations` array, so + /// you must make a copy if you want need to preserve that. + static create(doc: Node, decorations: Decoration[]) { + return decorations.length ? buildTree(decorations, doc, 0, noSpec) : empty + } + + /// Find all decorations in this set which touch the given range + /// (including decorations that start or end directly at the + /// boundaries) and match the given predicate on their spec. When + /// `start` and `end` are omitted, all decorations in the set are + /// considered. When `predicate` isn't given, all decorations are + /// assumed to match. + find(start?: number, end?: number, predicate?: (spec: any) => boolean): Decoration[] { + let result: Decoration[] = [] + this.findInner(start == null ? 0 : start, end == null ? 1e9 : end, result, 0, predicate) + return result + } + + private findInner(start: number, end: number, result: Decoration[], offset: number, predicate?: (spec: any) => boolean) { + for (let i = 0; i < this.local.length; i++) { + let span = this.local[i] + if (span.from <= end && span.to >= start && (!predicate || predicate(span.spec))) + result.push(span.copy(span.from + offset, span.to + offset)) + } + for (let i = 0; i < this.children.length; i += 3) { + if ((this.children[i] as number) < end && (this.children[i + 1] as number) > start) { + let childOff = (this.children[i] as number) + 1 + ;(this.children[i + 2] as DecorationSet).findInner(start - childOff, end - childOff, + result, offset + childOff, predicate) + } + } + } + + /// Map the set of decorations in response to a change in the + /// document. + map(mapping: Mapping, doc: Node, options?: { + /// When given, this function will be called for each decoration + /// that gets dropped as a result of the mapping, passing the + /// spec of that decoration. + onRemove?: (decorationSpec: any) => void + }) { + if (this == empty || mapping.maps.length == 0) return this + return this.mapInner(mapping, doc, 0, 0, options || noSpec) + } + + /// @internal + mapInner(mapping: Mapping, node: Node, offset: number, oldOffset: number, options: { + onRemove?: (decorationSpec: any) => void + }) { + let newLocal: Decoration[] | undefined + for (let i = 0; i < this.local.length; i++) { + let mapped = this.local[i].map(mapping, offset, oldOffset) + if (mapped && mapped.type.valid(node, mapped)) (newLocal || (newLocal = [])).push(mapped) + else if (options.onRemove) options.onRemove(this.local[i].spec) + } + + if (this.children.length) + return mapChildren(this.children, newLocal || [], mapping, node, offset, oldOffset, options) + else + return newLocal ? new DecorationSet(newLocal.sort(byPos), none) : empty + } + + /// Add the given array of decorations to the ones in the set, + /// producing a new set. Consumes the `decorations` array. Needs + /// access to the current document to create the appropriate tree + /// structure. + add(doc: Node, decorations: Decoration[]) { + if (!decorations.length) return this + if (this == empty) return DecorationSet.create(doc, decorations) + return this.addInner(doc, decorations, 0) + } + + private addInner(doc: Node, decorations: Decoration[], offset: number) { + let children: (number | DecorationSet)[] | undefined, childIndex = 0 + doc.forEach((childNode, childOffset) => { + let baseOffset = childOffset + offset, found + if (!(found = takeSpansForNode(decorations, childNode, baseOffset))) return + + if (!children) children = this.children.slice() + while (childIndex < children.length && (children[childIndex] as number) < childOffset) childIndex += 3 + if (children[childIndex] == childOffset) + children[childIndex + 2] = (children[childIndex + 2] as DecorationSet).addInner(childNode, found, baseOffset + 1) + else + children.splice(childIndex, 0, childOffset, childOffset + childNode.nodeSize, buildTree(found, childNode, baseOffset + 1, noSpec)) + childIndex += 3 + }) + + let local = moveSpans(childIndex ? withoutNulls(decorations) : decorations, -offset) + for (let i = 0; i < local.length; i++) if (!local[i].type.valid(doc, local[i])) local.splice(i--, 1) + + return new DecorationSet(local.length ? this.local.concat(local).sort(byPos) : this.local, + children || this.children) + } + + /// Create a new set that contains the decorations in this set, minus + /// the ones in the given array. + remove(decorations: Decoration[]) { + if (decorations.length == 0 || this == empty) return this + return this.removeInner(decorations, 0) + } + + private removeInner(decorations: (Decoration | null)[], offset: number) { + let children = this.children as (number | DecorationSet)[], local = this.local as Decoration[] + for (let i = 0; i < children.length; i += 3) { + let found: Decoration[] | undefined + let from = (children[i] as number) + offset, to = (children[i + 1] as number) + offset + for (let j = 0, span; j < decorations.length; j++) if (span = decorations[j]) { + if (span.from > from && span.to < to) { + decorations[j] = null + ;(found || (found = [])).push(span) + } + } + if (!found) continue + if (children == this.children) children = this.children.slice() + let removed = (children[i + 2] as DecorationSet).removeInner(found, from + 1) + if (removed != empty) { + children[i + 2] = removed + } else { + children.splice(i, 3) + i -= 3 + } + } + if (local.length) for (let i = 0, span; i < decorations.length; i++) if (span = decorations[i]) { + for (let j = 0; j < local.length; j++) if (local[j].eq(span, offset)) { + if (local == this.local) local = this.local.slice() + local.splice(j--, 1) + } + } + if (children == this.children && local == this.local) return this + return local.length || children.length ? new DecorationSet(local, children) : empty + } + + forChild(offset: number, node: Node): DecorationSet | DecorationGroup { + if (this == empty) return this + if (node.isLeaf) return DecorationSet.empty + + let child, local: Decoration[] | undefined + for (let i = 0; i < this.children.length; i += 3) if ((this.children[i] as number) >= offset) { + if (this.children[i] == offset) child = this.children[i + 2] as DecorationSet + break + } + let start = offset + 1, end = start + node.content.size + for (let i = 0; i < this.local.length; i++) { + let dec = this.local[i] + if (dec.from < end && dec.to > start && (dec.type instanceof InlineType)) { + let from = Math.max(start, dec.from) - start, to = Math.min(end, dec.to) - start + if (from < to) (local || (local = [])).push(dec.copy(from, to)) + } + } + if (local) { + let localSet = new DecorationSet(local.sort(byPos), none) + return child ? new DecorationGroup([localSet, child]) : localSet + } + return child || empty + } + + /// @internal + eq(other: DecorationSet) { + if (this == other) return true + if (!(other instanceof DecorationSet) || + this.local.length != other.local.length || + this.children.length != other.children.length) return false + for (let i = 0; i < this.local.length; i++) + if (!this.local[i].eq(other.local[i])) return false + for (let i = 0; i < this.children.length; i += 3) + if (this.children[i] != other.children[i] || + this.children[i + 1] != other.children[i + 1] || + !(this.children[i + 2] as DecorationSet).eq(other.children[i + 2] as DecorationSet)) + return false + return true + } + + /// @internal + locals(node: Node) { + return removeOverlap(this.localsInner(node)) + } + + /// @internal + localsInner(node: Node): readonly Decoration[] { + if (this == empty) return none + if (node.inlineContent || !this.local.some(InlineType.is)) return this.local + let result = [] + for (let i = 0; i < this.local.length; i++) { + if (!(this.local[i].type instanceof InlineType)) + result.push(this.local[i]) + } + return result + } + + /// The empty set of decorations. + static empty: DecorationSet = new DecorationSet([], []) + + /// @internal + static removeOverlap = removeOverlap + + forEachSet(f: (set: DecorationSet) => void) { f(this) } +} + +const empty = DecorationSet.empty + +// An abstraction that allows the code dealing with decorations to +// treat multiple DecorationSet objects as if it were a single object +// with (a subset of) the same interface. +class DecorationGroup implements DecorationSource { + constructor(readonly members: readonly DecorationSet[]) {} + + map(mapping: Mapping, doc: Node) { + const mappedDecos = this.members.map( + member => member.map(mapping, doc, noSpec) + ) + return DecorationGroup.from(mappedDecos) + } + + forChild(offset: number, child: Node) { + if (child.isLeaf) return DecorationSet.empty + let found: DecorationSet[] = [] + for (let i = 0; i < this.members.length; i++) { + let result = this.members[i].forChild(offset, child) + if (result == empty) continue + if (result instanceof DecorationGroup) found = found.concat(result.members) + else found.push(result) + } + return DecorationGroup.from(found) + } + + eq(other: DecorationGroup) { + if (!(other instanceof DecorationGroup) || + other.members.length != this.members.length) return false + for (let i = 0; i < this.members.length; i++) + if (!this.members[i].eq(other.members[i])) return false + return true + } + + locals(node: Node) { + let result: Decoration[] | undefined, sorted = true + for (let i = 0; i < this.members.length; i++) { + let locals = this.members[i].localsInner(node) + if (!locals.length) continue + if (!result) { + result = locals as Decoration[] + } else { + if (sorted) { + result = result.slice() + sorted = false + } + for (let j = 0; j < locals.length; j++) result.push(locals[j]) + } + } + return result ? removeOverlap(sorted ? result : result.sort(byPos)) : none + } + + // Create a group for the given array of decoration sets, or return + // a single set when possible. + static from(members: readonly DecorationSource[]): DecorationSource { + switch (members.length) { + case 0: return empty + case 1: return members[0] + default: return new DecorationGroup( + members.every(m => m instanceof DecorationSet) ? members as DecorationSet[] : + members.reduce((r, m) => r.concat(m instanceof DecorationSet ? m : (m as DecorationGroup).members), + [] as DecorationSet[])) + } + } + + forEachSet(f: (set: DecorationSet) => void) { + for (let i = 0; i < this.members.length; i++) this.members[i].forEachSet(f) + } +} + +function mapChildren( + oldChildren: readonly (number | DecorationSet)[], + newLocal: Decoration[], + mapping: Mapping, + node: Node, + offset: number, + oldOffset: number, + options: {onRemove?: (decorationSpec: any) => void} +) { + let children = oldChildren.slice() as (number | DecorationSet)[] + + // Mark the children that are directly touched by changes, and + // move those that are after the changes. + for (let i = 0, baseOffset = oldOffset; i < mapping.maps.length; i++) { + let moved = 0 + mapping.maps[i].forEach((oldStart: number, oldEnd: number, newStart: number, newEnd: number) => { + let dSize = (newEnd - newStart) - (oldEnd - oldStart) + for (let i = 0; i < children.length; i += 3) { + let end = children[i + 1] as number + if (end < 0 || oldStart > end + baseOffset - moved) continue + let start = (children[i] as number) + baseOffset - moved + if (oldEnd >= start) { + children[i + 1] = oldStart <= start ? -2 : -1 + } else if (oldStart >= baseOffset && dSize) { + ;(children[i] as number) += dSize + ;(children[i + 1] as number) += dSize + } + } + moved += dSize + }) + baseOffset = mapping.maps[i].map(baseOffset, -1) + } + + // Find the child nodes that still correspond to a single node, + // recursively call mapInner on them and update their positions. + let mustRebuild = false + for (let i = 0; i < children.length; i += 3) if ((children[i + 1] as number) < 0) { // Touched nodes + if (children[i + 1] == -2) { + mustRebuild = true + children[i + 1] = -1 + continue + } + let from = mapping.map((oldChildren[i] as number) + oldOffset), fromLocal = from - offset + if (fromLocal < 0 || fromLocal >= node.content.size) { + mustRebuild = true + continue + } + // Must read oldChildren because children was tagged with -1 + let to = mapping.map((oldChildren[i + 1] as number) + oldOffset, -1), toLocal = to - offset + let {index, offset: childOffset} = node.content.findIndex(fromLocal) + let childNode = node.maybeChild(index) + if (childNode && childOffset == fromLocal && childOffset + childNode.nodeSize == toLocal) { + let mapped = (children[i + 2] as DecorationSet) + .mapInner(mapping, childNode, from + 1, (oldChildren[i] as number) + oldOffset + 1, options) + if (mapped != empty) { + children[i] = fromLocal + children[i + 1] = toLocal + children[i + 2] = mapped + } else { + children[i + 1] = -2 + mustRebuild = true + } + } else { + mustRebuild = true + } + } + + // Remaining children must be collected and rebuilt into the appropriate structure + if (mustRebuild) { + let decorations = mapAndGatherRemainingDecorations(children, oldChildren, newLocal, mapping, + offset, oldOffset, options) + let built = buildTree(decorations, node, 0, options) + newLocal = built.local as Decoration[] + for (let i = 0; i < children.length; i += 3) if ((children[i + 1] as number) < 0) { + children.splice(i, 3) + i -= 3 + } + for (let i = 0, j = 0; i < built.children.length; i += 3) { + let from = built.children[i] + while (j < children.length && children[j] < from) j += 3 + children.splice(j, 0, built.children[i], built.children[i + 1], built.children[i + 2]) + } + } + + return new DecorationSet(newLocal.sort(byPos), children) +} + +function moveSpans(spans: Decoration[], offset: number) { + if (!offset || !spans.length) return spans + let result = [] + for (let i = 0; i < spans.length; i++) { + let span = spans[i] + result.push(new Decoration(span.from + offset, span.to + offset, span.type)) + } + return result +} + +function mapAndGatherRemainingDecorations( + children: (number | DecorationSet)[], + oldChildren: readonly (number | DecorationSet)[], + decorations: Decoration[], + mapping: Mapping, + offset: number, + oldOffset: number, + options: {onRemove?: (decorationSpec: any) => void} +) { + // Gather all decorations from the remaining marked children + function gather(set: DecorationSet, oldOffset: number) { + for (let i = 0; i < set.local.length; i++) { + let mapped = set.local[i].map(mapping, offset, oldOffset) + if (mapped) decorations.push(mapped) + else if (options.onRemove) options.onRemove(set.local[i].spec) + } + for (let i = 0; i < set.children.length; i += 3) + gather(set.children[i + 2] as DecorationSet, set.children[i] as number + oldOffset + 1) + } + for (let i = 0; i < children.length; i += 3) if (children[i + 1] == -1) + gather(children[i + 2] as DecorationSet, oldChildren[i] as number + oldOffset + 1) + + return decorations +} + +function takeSpansForNode(spans: (Decoration | null)[], node: Node, offset: number): Decoration[] | null { + if (node.isLeaf) return null + let end = offset + node.nodeSize, found = null + for (let i = 0, span; i < spans.length; i++) { + if ((span = spans[i]) && span.from > offset && span.to < end) { + ;(found || (found = [])).push(span) + spans[i] = null + } + } + return found +} + +function withoutNulls<T>(array: readonly (T | null)[]): T[] { + let result: T[] = [] + for (let i = 0; i < array.length; i++) + if (array[i] != null) result.push(array[i]!) + return result +} + +// Build up a tree that corresponds to a set of decorations. `offset` +// is a base offset that should be subtracted from the `from` and `to` +// positions in the spans (so that we don't have to allocate new spans +// for recursive calls). +function buildTree( + spans: Decoration[], + node: Node, + offset: number, + options: {onRemove?: (decorationSpec: any) => void} +) { + let children: (DecorationSet | number)[] = [], hasNulls = false + node.forEach((childNode, localStart) => { + let found = takeSpansForNode(spans, childNode, localStart + offset) + if (found) { + hasNulls = true + let subtree = buildTree(found, childNode, offset + localStart + 1, options) + if (subtree != empty) + children.push(localStart, localStart + childNode.nodeSize, subtree) + } + }) + let locals = moveSpans(hasNulls ? withoutNulls(spans) : spans, -offset).sort(byPos) + for (let i = 0; i < locals.length; i++) if (!locals[i].type.valid(node, locals[i])) { + if (options.onRemove) options.onRemove(locals[i].spec) + locals.splice(i--, 1) + } + return locals.length || children.length ? new DecorationSet(locals, children) : empty +} + +// Used to sort decorations so that ones with a low start position +// come first, and within a set with the same start position, those +// with an smaller end position come first. +function byPos(a: Decoration, b: Decoration) { + return a.from - b.from || a.to - b.to +} + +// Scan a sorted array of decorations for partially overlapping spans, +// and split those so that only fully overlapping spans are left (to +// make subsequent rendering easier). Will return the input array if +// no partially overlapping spans are found (the common case). +function removeOverlap(spans: readonly Decoration[]): Decoration[] { + let working: Decoration[] = spans as Decoration[] + for (let i = 0; i < working.length - 1; i++) { + let span = working[i] + if (span.from != span.to) for (let j = i + 1; j < working.length; j++) { + let next = working[j] + if (next.from == span.from) { + if (next.to != span.to) { + if (working == spans) working = spans.slice() + // Followed by a partially overlapping larger span. Split that + // span. + working[j] = next.copy(next.from, span.to) + insertAhead(working, j + 1, next.copy(span.to, next.to)) + } + continue + } else { + if (next.from < span.to) { + if (working == spans) working = spans.slice() + // The end of this one overlaps with a subsequent span. Split + // this one. + working[i] = span.copy(span.from, next.from) + insertAhead(working, j, span.copy(next.from, span.to)) + } + break + } + } + } + return working +} + +function insertAhead(array: Decoration[], i: number, deco: Decoration) { + while (i < array.length && byPos(deco, array[i]) > 0) i++ + array.splice(i, 0, deco) +} + +// Get the decorations associated with the current props of a view. +export function viewDecorations(view: EditorView): DecorationSource { + let found: DecorationSource[] = [] + view.someProp("decorations", f => { + let result = f(view.state) + if (result && result != empty) found.push(result) + }) + if (view.cursorWrapper) + found.push(DecorationSet.create(view.state.doc, [view.cursorWrapper.deco])) + return DecorationGroup.from(found) +} diff --git a/third_party/js/prosemirror/prosemirror-view/src/dom.ts b/third_party/js/prosemirror/prosemirror-view/src/dom.ts @@ -0,0 +1,159 @@ +export type DOMNode = InstanceType<typeof window.Node> +export type DOMSelection = InstanceType<typeof window.Selection> +export type DOMSelectionRange = { + focusNode: DOMNode | null, focusOffset: number, + anchorNode: DOMNode | null, anchorOffset: number +} + +export const domIndex = function(node: Node) { + for (var index = 0;; index++) { + node = node.previousSibling! + if (!node) return index + } +} + +export const parentNode = function(node: Node): Node | null { + let parent = (node as HTMLSlotElement).assignedSlot || node.parentNode + return parent && parent.nodeType == 11 ? (parent as ShadowRoot).host : parent +} + +let reusedRange: Range | null = null + +// Note that this will always return the same range, because DOM range +// objects are every expensive, and keep slowing down subsequent DOM +// updates, for some reason. +export const textRange = function(node: Text, from?: number, to?: number) { + let range = reusedRange || (reusedRange = document.createRange()) + range.setEnd(node, to == null ? node.nodeValue!.length : to) + range.setStart(node, from || 0) + return range +} + +export const clearReusedRange = function() { + reusedRange = null +} + +// Scans forward and backward through DOM positions equivalent to the +// given one to see if the two are in the same place (i.e. after a +// text node vs at the end of that text node) +export const isEquivalentPosition = function(node: Node, off: number, targetNode: Node, targetOff: number) { + return targetNode && (scanFor(node, off, targetNode, targetOff, -1) || + scanFor(node, off, targetNode, targetOff, 1)) +} + +const atomElements = /^(img|br|input|textarea|hr)$/i + +function scanFor(node: Node, off: number, targetNode: Node, targetOff: number, dir: number) { + for (;;) { + if (node == targetNode && off == targetOff) return true + if (off == (dir < 0 ? 0 : nodeSize(node))) { + let parent = node.parentNode + if (!parent || parent.nodeType != 1 || hasBlockDesc(node) || atomElements.test(node.nodeName) || + (node as HTMLElement).contentEditable == "false") + return false + off = domIndex(node) + (dir < 0 ? 0 : 1) + node = parent + } else if (node.nodeType == 1) { + let child = node.childNodes[off + (dir < 0 ? -1 : 0)] + if (child.nodeType == 1 && (child as HTMLElement).contentEditable == "false") { + if (child.pmViewDesc?.ignoreForSelection) off += dir + else return false + } else { + node = child + off = dir < 0 ? nodeSize(node) : 0 + } + } else { + return false + } + } +} + +export function nodeSize(node: Node) { + return node.nodeType == 3 ? node.nodeValue!.length : node.childNodes.length +} + +export function textNodeBefore(node: Node, offset: number) { + for (;;) { + if (node.nodeType == 3 && offset) return node as Text + if (node.nodeType == 1 && offset > 0) { + if ((node as HTMLElement).contentEditable == "false") return null + node = node.childNodes[offset - 1] + offset = nodeSize(node) + } else if (node.parentNode && !hasBlockDesc(node)) { + offset = domIndex(node) + node = node.parentNode + } else { + return null + } + } +} + +export function textNodeAfter(node: Node, offset: number) { + for (;;) { + if (node.nodeType == 3 && offset < node.nodeValue!.length) return node as Text + if (node.nodeType == 1 && offset < node.childNodes.length) { + if ((node as HTMLElement).contentEditable == "false") return null + node = node.childNodes[offset] + offset = 0 + } else if (node.parentNode && !hasBlockDesc(node)) { + offset = domIndex(node) + 1 + node = node.parentNode + } else { + return null + } + } +} + +export function isOnEdge(node: Node, offset: number, parent: Node) { + for (let atStart = offset == 0, atEnd = offset == nodeSize(node); atStart || atEnd;) { + if (node == parent) return true + let index = domIndex(node) + node = node.parentNode! + if (!node) return false + atStart = atStart && index == 0 + atEnd = atEnd && index == nodeSize(node) + } +} + +export function hasBlockDesc(dom: Node) { + let desc + for (let cur: Node | null = dom; cur; cur = cur.parentNode) if (desc = cur.pmViewDesc) break + return desc && desc.node && desc.node.isBlock && (desc.dom == dom || desc.contentDOM == dom) +} + +// Work around Chrome issue https://bugs.chromium.org/p/chromium/issues/detail?id=447523 +// (isCollapsed inappropriately returns true in shadow dom) +export const selectionCollapsed = function(domSel: DOMSelectionRange) { + return domSel.focusNode && isEquivalentPosition(domSel.focusNode, domSel.focusOffset, + domSel.anchorNode!, domSel.anchorOffset) +} + +export function keyEvent(keyCode: number, key: string) { + let event = document.createEvent("Event") as KeyboardEvent + event.initEvent("keydown", true, true) + ;(event as any).keyCode = keyCode + ;(event as any).key = (event as any).code = key + return event +} + +export function deepActiveElement(doc: Document) { + let elt = doc.activeElement + while (elt && elt.shadowRoot) elt = elt.shadowRoot.activeElement + return elt +} + +export function caretFromPoint(doc: Document, x: number, y: number): {node: Node, offset: number} | undefined { + if ((doc as any).caretPositionFromPoint) { + try { // Firefox throws for this call in hard-to-predict circumstances (#994) + let pos = (doc as any).caretPositionFromPoint(x, y) + // Clip the offset, because Chrome will return a text offset + // into <input> nodes, which can't be treated as a regular DOM + // offset + if (pos) return {node: pos.offsetNode, offset: Math.min(nodeSize(pos.offsetNode), pos.offset)} + } catch (_) {} + } + if (doc.caretRangeFromPoint) { + let range = doc.caretRangeFromPoint(x, y) + if (range) return {node: range.startContainer, offset: Math.min(nodeSize(range.startContainer), range.startOffset)} + } +} diff --git a/third_party/js/prosemirror/prosemirror-view/src/domchange.ts b/third_party/js/prosemirror/prosemirror-view/src/domchange.ts @@ -0,0 +1,383 @@ +import {Fragment, DOMParser, TagParseRule, Node, Mark, ResolvedPos} from "prosemirror-model" +import {TextSelection, Transaction} from "prosemirror-state" + +import {selectionBetween, selectionFromDOM, selectionToDOM} from "./selection" +import {selectionCollapsed, keyEvent, DOMNode} from "./dom" +import * as browser from "./browser" +import {EditorView} from "./index" + +// Note that all referencing and parsing is done with the +// start-of-operation selection and document, since that's the one +// that the DOM represents. If any changes came in in the meantime, +// the modification is mapped over those before it is applied, in +// readDOMChange. + +function parseBetween(view: EditorView, from_: number, to_: number) { + let {node: parent, fromOffset, toOffset, from, to} = view.docView.parseRange(from_, to_) + + let domSel = view.domSelectionRange() + let find: {node: DOMNode, offset: number, pos?: number}[] | undefined + let anchor = domSel.anchorNode + if (anchor && view.dom.contains(anchor.nodeType == 1 ? anchor : anchor.parentNode)) { + find = [{node: anchor, offset: domSel.anchorOffset}] + if (!selectionCollapsed(domSel)) + find.push({node: domSel.focusNode!, offset: domSel.focusOffset}) + } + // Work around issue in Chrome where backspacing sometimes replaces + // the deleted content with a random BR node (issues #799, #831) + if (browser.chrome && view.input.lastKeyCode === 8) { + for (let off = toOffset; off > fromOffset; off--) { + let node = parent.childNodes[off - 1], desc = node.pmViewDesc + if (node.nodeName == "BR" && !desc) { toOffset = off; break } + if (!desc || desc.size) break + } + } + let startDoc = view.state.doc + let parser = view.someProp("domParser") || DOMParser.fromSchema(view.state.schema) + let $from = startDoc.resolve(from) + + let sel = null, doc = parser.parse(parent, { + topNode: $from.parent, + topMatch: $from.parent.contentMatchAt($from.index()), + topOpen: true, + from: fromOffset, + to: toOffset, + preserveWhitespace: $from.parent.type.whitespace == "pre" ? "full" : true, + findPositions: find, + ruleFromNode, + context: $from + }) + if (find && find[0].pos != null) { + let anchor = find[0].pos, head = find[1] && find[1].pos + if (head == null) head = anchor + sel = {anchor: anchor + from, head: head + from} + } + return {doc, sel, from, to} +} + +function ruleFromNode(dom: DOMNode): Omit<TagParseRule, "tag"> | null { + let desc = dom.pmViewDesc + if (desc) { + return desc.parseRule() + } else if (dom.nodeName == "BR" && dom.parentNode) { + // Safari replaces the list item or table cell with a BR + // directly in the list node (?!) if you delete the last + // character in a list item or table cell (#708, #862) + if (browser.safari && /^(ul|ol)$/i.test(dom.parentNode.nodeName)) { + let skip = document.createElement("div") + skip.appendChild(document.createElement("li")) + return {skip} as any + } else if (dom.parentNode.lastChild == dom || browser.safari && /^(tr|table)$/i.test(dom.parentNode.nodeName)) { + return {ignore: true} + } + } else if (dom.nodeName == "IMG" && (dom as HTMLElement).getAttribute("mark-placeholder")) { + return {ignore: true} + } + return null +} + +const isInline = /^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i + +export function readDOMChange(view: EditorView, from: number, to: number, typeOver: boolean, addedNodes: readonly DOMNode[]) { + let compositionID = view.input.compositionPendingChanges || (view.composing ? view.input.compositionID : 0) + view.input.compositionPendingChanges = 0 + + if (from < 0) { + let origin = view.input.lastSelectionTime > Date.now() - 50 ? view.input.lastSelectionOrigin : null + let newSel = selectionFromDOM(view, origin) + if (newSel && !view.state.selection.eq(newSel)) { + if (browser.chrome && browser.android && + view.input.lastKeyCode === 13 && Date.now() - 100 < view.input.lastKeyCodeTime && + view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) + return + let tr = view.state.tr.setSelection(newSel) + if (origin == "pointer") tr.setMeta("pointer", true) + else if (origin == "key") tr.scrollIntoView() + if (compositionID) tr.setMeta("composition", compositionID) + view.dispatch(tr) + } + return + } + + let $before = view.state.doc.resolve(from) + let shared = $before.sharedDepth(to) + from = $before.before(shared + 1) + to = view.state.doc.resolve(to).after(shared + 1) + + let sel = view.state.selection + let parse = parseBetween(view, from, to) + + let doc = view.state.doc, compare = doc.slice(parse.from, parse.to) + let preferredPos, preferredSide: "start" | "end" + // Prefer anchoring to end when Backspace is pressed + if (view.input.lastKeyCode === 8 && Date.now() - 100 < view.input.lastKeyCodeTime) { + preferredPos = view.state.selection.to + preferredSide = "end" + } else { + preferredPos = view.state.selection.from + preferredSide = "start" + } + view.input.lastKeyCode = null + + let change = findDiff(compare.content, parse.doc.content, parse.from, preferredPos, preferredSide) + if (change) view.input.domChangeCount++ + if ((browser.ios && view.input.lastIOSEnter > Date.now() - 225 || browser.android) && + addedNodes.some(n => n.nodeType == 1 && !isInline.test(n.nodeName)) && + (!change || change.endA >= change.endB) && + view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) { + view.input.lastIOSEnter = 0 + return + } + if (!change) { + if (typeOver && sel instanceof TextSelection && !sel.empty && sel.$head.sameParent(sel.$anchor) && + !view.composing && !(parse.sel && parse.sel.anchor != parse.sel.head)) { + change = {start: sel.from, endA: sel.to, endB: sel.to} + } else { + if (parse.sel) { + let sel = resolveSelection(view, view.state.doc, parse.sel) + if (sel && !sel.eq(view.state.selection)) { + let tr = view.state.tr.setSelection(sel) + if (compositionID) tr.setMeta("composition", compositionID) + view.dispatch(tr) + } + } + return + } + } + + // Handle the case where overwriting a selection by typing matches + // the start or end of the selected content, creating a change + // that's smaller than what was actually overwritten. + if (view.state.selection.from < view.state.selection.to && + change.start == change.endB && + view.state.selection instanceof TextSelection) { + if (change.start > view.state.selection.from && change.start <= view.state.selection.from + 2 && + view.state.selection.from >= parse.from) { + change.start = view.state.selection.from + } else if (change.endA < view.state.selection.to && change.endA >= view.state.selection.to - 2 && + view.state.selection.to <= parse.to) { + change.endB += (view.state.selection.to - change.endA) + change.endA = view.state.selection.to + } + } + + // IE11 will insert a non-breaking space _ahead_ of the space after + // the cursor space when adding a space before another space. When + // that happened, adjust the change to cover the space instead. + if (browser.ie && browser.ie_version <= 11 && change.endB == change.start + 1 && + change.endA == change.start && change.start > parse.from && + parse.doc.textBetween(change.start - parse.from - 1, change.start - parse.from + 1) == " \u00a0") { + change.start-- + change.endA-- + change.endB-- + } + + let $from = parse.doc.resolveNoCache(change.start - parse.from) + let $to = parse.doc.resolveNoCache(change.endB - parse.from) + let $fromA = doc.resolve(change.start) + let inlineChange = $from.sameParent($to) && $from.parent.inlineContent && $fromA.end() >= change.endA + // If this looks like the effect of pressing Enter (or was recorded + // as being an iOS enter press), just dispatch an Enter key instead. + if (((browser.ios && view.input.lastIOSEnter > Date.now() - 225 && + (!inlineChange || addedNodes.some(n => n.nodeName == "DIV" || n.nodeName == "P"))) || + (!inlineChange && $from.pos < parse.doc.content.size && + (!$from.sameParent($to) || !$from.parent.inlineContent) && + $from.pos < $to.pos && !/\S/.test(parse.doc.textBetween($from.pos, $to.pos, "", "")))) && + view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter")))) { + view.input.lastIOSEnter = 0 + return + } + // Same for backspace + if (view.state.selection.anchor > change.start && + looksLikeBackspace(doc, change.start, change.endA, $from, $to) && + view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace")))) { + if (browser.android && browser.chrome) view.domObserver.suppressSelectionUpdates() // #820 + return + } + + // Chrome will occasionally, during composition, delete the + // entire composition and then immediately insert it again. This is + // used to detect that situation. + if (browser.chrome && change.endB == change.start) + view.input.lastChromeDelete = Date.now() + + // This tries to detect Android virtual keyboard + // enter-and-pick-suggestion action. That sometimes (see issue + // #1059) first fires a DOM mutation, before moving the selection to + // the newly created block. And then, because ProseMirror cleans up + // the DOM selection, it gives up moving the selection entirely, + // leaving the cursor in the wrong place. When that happens, we drop + // the new paragraph from the initial change, and fire a simulated + // enter key afterwards. + if (browser.android && !inlineChange && $from.start() != $to.start() && $to.parentOffset == 0 && $from.depth == $to.depth && + parse.sel && parse.sel.anchor == parse.sel.head && parse.sel.head == change.endA) { + change.endB -= 2 + $to = parse.doc.resolveNoCache(change.endB - parse.from) + setTimeout(() => { + view.someProp("handleKeyDown", function (f) { return f(view, keyEvent(13, "Enter")); }) + }, 20) + } + + let chFrom = change.start, chTo = change.endA + + let mkTr = (base?: Transaction) => { + let tr = base || view.state.tr.replace(chFrom, chTo, parse.doc.slice(change!.start - parse.from, + change!.endB - parse.from)) + if (parse.sel) { + let sel = resolveSelection(view, tr.doc, parse.sel) + // Chrome will sometimes, during composition, report the + // selection in the wrong place. If it looks like that is + // happening, don't update the selection. + // Edge just doesn't move the cursor forward when you start typing + // in an empty block or between br nodes. + if (sel && !(browser.chrome && view.composing && sel.empty && + (change!.start != change!.endB || view.input.lastChromeDelete < Date.now() - 100) && + (sel.head == chFrom || sel.head == tr.mapping.map(chTo) - 1) || + browser.ie && sel.empty && sel.head == chFrom)) + tr.setSelection(sel) + } + if (compositionID) tr.setMeta("composition", compositionID) + return tr.scrollIntoView() + } + + let markChange + if (inlineChange) { + if ($from.pos == $to.pos) { // Deletion + // IE11 sometimes weirdly moves the DOM selection around after + // backspacing out the first element in a textblock + if (browser.ie && browser.ie_version <= 11 && $from.parentOffset == 0) { + view.domObserver.suppressSelectionUpdates() + setTimeout(() => selectionToDOM(view), 20) + } + let tr = mkTr(view.state.tr.delete(chFrom, chTo)) + let marks = doc.resolve(change.start).marksAcross(doc.resolve(change.endA)) + if (marks) tr.ensureMarks(marks) + view.dispatch(tr) + } else if ( // Adding or removing a mark + change.endA == change.endB && + (markChange = isMarkChange($from.parent.content.cut($from.parentOffset, $to.parentOffset), + $fromA.parent.content.cut($fromA.parentOffset, change.endA - $fromA.start()))) + ) { + let tr = mkTr(view.state.tr) + if (markChange.type == "add") tr.addMark(chFrom, chTo, markChange.mark) + else tr.removeMark(chFrom, chTo, markChange.mark) + view.dispatch(tr) + } else if ($from.parent.child($from.index()).isText && $from.index() == $to.index() - ($to.textOffset ? 0 : 1)) { + // Both positions in the same text node -- simply insert text + let text = $from.parent.textBetween($from.parentOffset, $to.parentOffset) + let deflt = () => mkTr(view.state.tr.insertText(text, chFrom, chTo)) + if (!view.someProp("handleTextInput", f => f(view, chFrom, chTo, text, deflt))) + view.dispatch(deflt()) + } else { + view.dispatch(mkTr()) + } + } else { + view.dispatch(mkTr()) + } +} + +function resolveSelection(view: EditorView, doc: Node, parsedSel: {anchor: number, head: number}) { + if (Math.max(parsedSel.anchor, parsedSel.head) > doc.content.size) return null + return selectionBetween(view, doc.resolve(parsedSel.anchor), doc.resolve(parsedSel.head)) +} + +// Given two same-length, non-empty fragments of inline content, +// determine whether the first could be created from the second by +// removing or adding a single mark type. +function isMarkChange(cur: Fragment, prev: Fragment) { + let curMarks = cur.firstChild!.marks, prevMarks = prev.firstChild!.marks + let added = curMarks, removed = prevMarks, type, mark: Mark | undefined, update + for (let i = 0; i < prevMarks.length; i++) added = prevMarks[i].removeFromSet(added) + for (let i = 0; i < curMarks.length; i++) removed = curMarks[i].removeFromSet(removed) + if (added.length == 1 && removed.length == 0) { + mark = added[0] + type = "add" + update = (node: Node) => node.mark(mark!.addToSet(node.marks)) + } else if (added.length == 0 && removed.length == 1) { + mark = removed[0] + type = "remove" + update = (node: Node) => node.mark(mark!.removeFromSet(node.marks)) + } else { + return null + } + let updated = [] + for (let i = 0; i < prev.childCount; i++) updated.push(update(prev.child(i))) + if (Fragment.from(updated).eq(cur)) return {mark, type} +} + +function looksLikeBackspace(old: Node, start: number, end: number, $newStart: ResolvedPos, $newEnd: ResolvedPos) { + if (// The content must have shrunk + end - start <= $newEnd.pos - $newStart.pos || + // newEnd must point directly at or after the end of the block that newStart points into + skipClosingAndOpening($newStart, true, false) < $newEnd.pos) + return false + + let $start = old.resolve(start) + + // Handle the case where, rather than joining blocks, the change just removed an entire block + if (!$newStart.parent.isTextblock) { + let after = $start.nodeAfter + return after != null && end == start + after.nodeSize + } + + // Start must be at the end of a block + if ($start.parentOffset < $start.parent.content.size || !$start.parent.isTextblock) + return false + let $next = old.resolve(skipClosingAndOpening($start, true, true)) + // The next textblock must start before end and end near it + if (!$next.parent.isTextblock || $next.pos > end || + skipClosingAndOpening($next, true, false) < end) + return false + + // The fragments after the join point must match + return $newStart.parent.content.cut($newStart.parentOffset).eq($next.parent.content) +} + +function skipClosingAndOpening($pos: ResolvedPos, fromEnd: boolean, mayOpen: boolean) { + let depth = $pos.depth, end = fromEnd ? $pos.end() : $pos.pos + while (depth > 0 && (fromEnd || $pos.indexAfter(depth) == $pos.node(depth).childCount)) { + depth-- + end++ + fromEnd = false + } + if (mayOpen) { + let next = $pos.node(depth).maybeChild($pos.indexAfter(depth)) + while (next && !next.isLeaf) { + next = next.firstChild + end++ + } + } + return end +} + +function findDiff(a: Fragment, b: Fragment, pos: number, preferredPos: number, preferredSide: "start" | "end") { + let start = a.findDiffStart(b, pos) + if (start == null) return null + let {a: endA, b: endB} = a.findDiffEnd(b, pos + a.size, pos + b.size)! + if (preferredSide == "end") { + let adjust = Math.max(0, start - Math.min(endA, endB)) + preferredPos -= endA + adjust - start + } + if (endA < start && a.size < b.size) { + let move = preferredPos <= start && preferredPos >= endA ? start - preferredPos : 0 + start -= move + if (start && start < b.size && isSurrogatePair(b.textBetween(start - 1, start + 1))) + start += move ? 1 : -1 + endB = start + (endB - endA) + endA = start + } else if (endB < start) { + let move = preferredPos <= start && preferredPos >= endB ? start - preferredPos : 0 + start -= move + if (start && start < a.size && isSurrogatePair(a.textBetween(start - 1, start + 1))) + start += move ? 1 : -1 + endA = start + (endA - endB) + endB = start + } + return {start, endA, endB} +} + +function isSurrogatePair(str: string) { + if (str.length != 2) return false + let a = str.charCodeAt(0), b = str.charCodeAt(1) + return a >= 0xDC00 && a <= 0xDFFF && b >= 0xD800 && b <= 0xDBFF +} diff --git a/third_party/js/prosemirror/prosemirror-view/src/domcoords.ts b/third_party/js/prosemirror/prosemirror-view/src/domcoords.ts @@ -0,0 +1,515 @@ +import {EditorState} from "prosemirror-state" +import {nodeSize, textRange, parentNode, caretFromPoint} from "./dom" +import * as browser from "./browser" +import {EditorView} from "./index" + +export type Rect = {left: number, right: number, top: number, bottom: number} + +function windowRect(doc: Document): Rect { + let vp = doc.defaultView && doc.defaultView.visualViewport + if (vp) return { + left: 0, right: vp.width, + top: 0, bottom: vp.height + } + return {left: 0, right: doc.documentElement.clientWidth, + top: 0, bottom: doc.documentElement.clientHeight} +} + +function getSide(value: number | Rect, side: keyof Rect): number { + return typeof value == "number" ? value : value[side] +} + +function clientRect(node: HTMLElement): Rect { + let rect = node.getBoundingClientRect() + // Adjust for elements with style "transform: scale()" + let scaleX = (rect.width / node.offsetWidth) || 1 + let scaleY = (rect.height / node.offsetHeight) || 1 + // Make sure scrollbar width isn't included in the rectangle + return {left: rect.left, right: rect.left + node.clientWidth * scaleX, + top: rect.top, bottom: rect.top + node.clientHeight * scaleY} +} + +export function scrollRectIntoView(view: EditorView, rect: Rect, startDOM: Node) { + let scrollThreshold = view.someProp("scrollThreshold") || 0, scrollMargin = view.someProp("scrollMargin") || 5 + let doc = view.dom.ownerDocument + for (let parent: Node | null = startDOM || view.dom;;) { + if (!parent) break + if (parent.nodeType != 1) { parent = parentNode(parent); continue } + let elt = parent as HTMLElement + let atTop = elt == doc.body + let bounding = atTop ? windowRect(doc) : clientRect(elt as HTMLElement) + let moveX = 0, moveY = 0 + if (rect.top < bounding.top + getSide(scrollThreshold, "top")) + moveY = -(bounding.top - rect.top + getSide(scrollMargin, "top")) + else if (rect.bottom > bounding.bottom - getSide(scrollThreshold, "bottom")) + moveY = rect.bottom - rect.top > bounding.bottom - bounding.top + ? rect.top + getSide(scrollMargin, "top") - bounding.top + : rect.bottom - bounding.bottom + getSide(scrollMargin, "bottom") + if (rect.left < bounding.left + getSide(scrollThreshold, "left")) + moveX = -(bounding.left - rect.left + getSide(scrollMargin, "left")) + else if (rect.right > bounding.right - getSide(scrollThreshold, "right")) + moveX = rect.right - bounding.right + getSide(scrollMargin, "right") + if (moveX || moveY) { + if (atTop) { + doc.defaultView!.scrollBy(moveX, moveY) + } else { + let startX = elt.scrollLeft, startY = elt.scrollTop + if (moveY) elt.scrollTop += moveY + if (moveX) elt.scrollLeft += moveX + let dX = elt.scrollLeft - startX, dY = elt.scrollTop - startY + rect = {left: rect.left - dX, top: rect.top - dY, right: rect.right - dX, bottom: rect.bottom - dY} + } + } + let pos: string = atTop ? "fixed" : getComputedStyle(parent as HTMLElement).position + if (/^(fixed|sticky)$/.test(pos)) break + parent = pos == "absolute" ? (parent as HTMLElement).offsetParent : parentNode(parent) + } +} + +// Store the scroll position of the editor's parent nodes, along with +// the top position of an element near the top of the editor, which +// will be used to make sure the visible viewport remains stable even +// when the size of the content above changes. +export function storeScrollPos(view: EditorView): { + refDOM: HTMLElement, + refTop: number, + stack: {dom: HTMLElement, top: number, left: number}[] +} { + let rect = view.dom.getBoundingClientRect(), startY = Math.max(0, rect.top) + let refDOM: HTMLElement, refTop: number + for (let x = (rect.left + rect.right) / 2, y = startY + 1; + y < Math.min(innerHeight, rect.bottom); y += 5) { + let dom = view.root.elementFromPoint(x, y) + if (!dom || dom == view.dom || !view.dom.contains(dom)) continue + let localRect = (dom as HTMLElement).getBoundingClientRect() + if (localRect.top >= startY - 20) { + refDOM = dom as HTMLElement + refTop = localRect.top + break + } + } + return {refDOM: refDOM!, refTop: refTop!, stack: scrollStack(view.dom)} +} + +function scrollStack(dom: Node): {dom: HTMLElement, top: number, left: number}[] { + let stack = [], doc = dom.ownerDocument + for (let cur: Node | null = dom; cur; cur = parentNode(cur)) { + stack.push({dom: cur as HTMLElement, top: (cur as HTMLElement).scrollTop, left: (cur as HTMLElement).scrollLeft}) + if (dom == doc) break + } + return stack +} + +// Reset the scroll position of the editor's parent nodes to that what +// it was before, when storeScrollPos was called. +export function resetScrollPos({refDOM, refTop, stack}: { + refDOM: HTMLElement, + refTop: number, + stack: {dom: HTMLElement, top: number, left: number}[] +}) { + let newRefTop = refDOM ? refDOM.getBoundingClientRect().top : 0 + restoreScrollStack(stack, newRefTop == 0 ? 0 : newRefTop - refTop) +} + +function restoreScrollStack(stack: {dom: HTMLElement, top: number, left: number}[], dTop: number) { + for (let i = 0; i < stack.length; i++) { + let {dom, top, left} = stack[i] + if (dom.scrollTop != top + dTop) dom.scrollTop = top + dTop + if (dom.scrollLeft != left) dom.scrollLeft = left + } +} + +let preventScrollSupported: false | null | {preventScroll: boolean} = null +// Feature-detects support for .focus({preventScroll: true}), and uses +// a fallback kludge when not supported. +export function focusPreventScroll(dom: HTMLElement) { + if ((dom as any).setActive) return (dom as any).setActive() // in IE + if (preventScrollSupported) return dom.focus(preventScrollSupported) + + let stored = scrollStack(dom) + dom.focus(preventScrollSupported == null ? { + get preventScroll() { + preventScrollSupported = {preventScroll: true} + return true + } + } : undefined) + if (!preventScrollSupported) { + preventScrollSupported = false + restoreScrollStack(stored, 0) + } +} + +function findOffsetInNode(node: HTMLElement, coords: {top: number, left: number}): {node: Node, offset: number} { + let closest, dxClosest = 2e8, coordsClosest: {left: number, top: number} | undefined, offset = 0 + let rowBot = coords.top, rowTop = coords.top + let firstBelow: Node | undefined, coordsBelow: {left: number, top: number} | undefined + for (let child = node.firstChild, childIndex = 0; child; child = child.nextSibling, childIndex++) { + let rects + if (child.nodeType == 1) rects = (child as HTMLElement).getClientRects() + else if (child.nodeType == 3) rects = textRange(child as Text).getClientRects() + else continue + + for (let i = 0; i < rects.length; i++) { + let rect = rects[i] + if (rect.top <= rowBot && rect.bottom >= rowTop) { + rowBot = Math.max(rect.bottom, rowBot) + rowTop = Math.min(rect.top, rowTop) + let dx = rect.left > coords.left ? rect.left - coords.left + : rect.right < coords.left ? coords.left - rect.right : 0 + if (dx < dxClosest) { + closest = child + dxClosest = dx + coordsClosest = dx && closest.nodeType == 3 ? { + left: rect.right < coords.left ? rect.right : rect.left, + top: coords.top + } : coords + if (child.nodeType == 1 && dx) + offset = childIndex + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0) + continue + } + } else if (rect.top > coords.top && !firstBelow && rect.left <= coords.left && rect.right >= coords.left) { + firstBelow = child + coordsBelow = {left: Math.max(rect.left, Math.min(rect.right, coords.left)), top: rect.top} + } + if (!closest && (coords.left >= rect.right && coords.top >= rect.top || + coords.left >= rect.left && coords.top >= rect.bottom)) + offset = childIndex + 1 + } + } + if (!closest && firstBelow) { closest = firstBelow; coordsClosest = coordsBelow; dxClosest = 0 } + if (closest && closest.nodeType == 3) return findOffsetInText(closest as Text, coordsClosest!) + if (!closest || (dxClosest && closest.nodeType == 1)) return {node, offset} + return findOffsetInNode(closest as HTMLElement, coordsClosest!) +} + +function findOffsetInText(node: Text, coords: {top: number, left: number}) { + let len = node.nodeValue!.length + let range = document.createRange(), result: {node: Node, offset: number} | undefined + for (let i = 0; i < len; i++) { + range.setEnd(node, i + 1) + range.setStart(node, i) + let rect = singleRect(range, 1) + if (rect.top == rect.bottom) continue + if (inRect(coords, rect)) { + result = {node, offset: i + (coords.left >= (rect.left + rect.right) / 2 ? 1 : 0)} + break + } + } + range.detach() + return result || {node, offset: 0} +} + +function inRect(coords: {top: number, left: number}, rect: Rect) { + return coords.left >= rect.left - 1 && coords.left <= rect.right + 1&& + coords.top >= rect.top - 1 && coords.top <= rect.bottom + 1 +} + +function targetKludge(dom: HTMLElement, coords: {top: number, left: number}) { + let parent = dom.parentNode + if (parent && /^li$/i.test(parent.nodeName) && coords.left < dom.getBoundingClientRect().left) + return parent as HTMLElement + return dom +} + +function posFromElement(view: EditorView, elt: HTMLElement, coords: {top: number, left: number}) { + let {node, offset} = findOffsetInNode(elt, coords), bias = -1 + if (node.nodeType == 1 && !node.firstChild) { + let rect = (node as HTMLElement).getBoundingClientRect() + bias = rect.left != rect.right && coords.left > (rect.left + rect.right) / 2 ? 1 : -1 + } + return view.docView.posFromDOM(node, offset, bias) +} + +function posFromCaret(view: EditorView, node: Node, offset: number, coords: {top: number, left: number}) { + // Browser (in caretPosition/RangeFromPoint) will agressively + // normalize towards nearby inline nodes. Since we are interested in + // positions between block nodes too, we first walk up the hierarchy + // of nodes to see if there are block nodes that the coordinates + // fall outside of. If so, we take the position before/after that + // block. If not, we call `posFromDOM` on the raw node/offset. + let outsideBlock = -1 + for (let cur = node, sawBlock = false;;) { + if (cur == view.dom) break + let desc = view.docView.nearestDesc(cur, true), rect + if (!desc) return null + if (desc.dom.nodeType == 1 && (desc.node.isBlock && desc.parent || !desc.contentDOM) && + // Ignore elements with zero-size bounding rectangles + ((rect = (desc.dom as HTMLElement).getBoundingClientRect()).width || rect.height)) { + if (desc.node.isBlock && desc.parent && !/^T(R|BODY|HEAD|FOOT)$/.test(desc.dom!.nodeName)) { + // Only apply the horizontal test to the innermost block. Vertical for any parent. + if (!sawBlock && rect.left > coords.left || rect.top > coords.top) outsideBlock = desc.posBefore + else if (!sawBlock && rect.right < coords.left || rect.bottom < coords.top) outsideBlock = desc.posAfter + sawBlock = true + } + if (!desc.contentDOM && outsideBlock < 0 && !desc.node.isText) { + // If we are inside a leaf, return the side of the leaf closer to the coords + let before = desc.node.isBlock ? coords.top < (rect.top + rect.bottom) / 2 + : coords.left < (rect.left + rect.right) / 2 + return before ? desc.posBefore : desc.posAfter + } + } + cur = desc.dom.parentNode! + } + return outsideBlock > -1 ? outsideBlock : view.docView.posFromDOM(node, offset, -1) +} + +function elementFromPoint(element: HTMLElement, coords: {top: number, left: number}, box: Rect): HTMLElement { + let len = element.childNodes.length + if (len && box.top < box.bottom) { + for (let startI = Math.max(0, Math.min(len - 1, Math.floor(len * (coords.top - box.top) / (box.bottom - box.top)) - 2)), i = startI;;) { + let child = element.childNodes[i] + if (child.nodeType == 1) { + let rects = (child as HTMLElement).getClientRects() + for (let j = 0; j < rects.length; j++) { + let rect = rects[j] + if (inRect(coords, rect)) return elementFromPoint(child as HTMLElement, coords, rect) + } + } + if ((i = (i + 1) % len) == startI) break + } + } + return element +} + +// Given an x,y position on the editor, get the position in the document. +export function posAtCoords(view: EditorView, coords: {top: number, left: number}) { + let doc = view.dom.ownerDocument, node: Node | undefined, offset = 0 + let caret = caretFromPoint(doc, coords.left, coords.top) + if (caret) ({node, offset} = caret) + + let elt = ((view.root as any).elementFromPoint ? view.root : doc) + .elementFromPoint(coords.left, coords.top) as HTMLElement + let pos + if (!elt || !view.dom.contains(elt.nodeType != 1 ? elt.parentNode : elt)) { + let box = view.dom.getBoundingClientRect() + if (!inRect(coords, box)) return null + elt = elementFromPoint(view.dom, coords, box) + if (!elt) return null + } + // Safari's caretRangeFromPoint returns nonsense when on a draggable element + if (browser.safari) { + for (let p: Node | null = elt; node && p; p = parentNode(p)) + if ((p as HTMLElement).draggable) node = undefined + } + elt = targetKludge(elt, coords) + if (node) { + if (browser.gecko && node.nodeType == 1) { + // Firefox will sometimes return offsets into <input> nodes, which + // have no actual children, from caretPositionFromPoint (#953) + offset = Math.min(offset, node.childNodes.length) + // It'll also move the returned position before image nodes, + // even if those are behind it. + if (offset < node.childNodes.length) { + let next = node.childNodes[offset], box + if (next.nodeName == "IMG" && (box = (next as HTMLElement).getBoundingClientRect()).right <= coords.left && + box.bottom > coords.top) + offset++ + } + } + let prev + // When clicking above the right side of an uneditable node, Chrome will report a cursor position after that node. + if (browser.webkit && offset && node.nodeType == 1 && (prev = node.childNodes[offset - 1]).nodeType == 1 && + (prev as HTMLElement).contentEditable == "false" && (prev as HTMLElement).getBoundingClientRect().top >= coords.top) + offset-- + // Suspiciously specific kludge to work around caret*FromPoint + // never returning a position at the end of the document + if (node == view.dom && offset == node.childNodes.length - 1 && node.lastChild!.nodeType == 1 && + coords.top > (node.lastChild as HTMLElement).getBoundingClientRect().bottom) + pos = view.state.doc.content.size + // Ignore positions directly after a BR, since caret*FromPoint + // 'round up' positions that would be more accurately placed + // before the BR node. + else if (offset == 0 || node.nodeType != 1 || node.childNodes[offset - 1].nodeName != "BR") + pos = posFromCaret(view, node, offset, coords) + } + if (pos == null) pos = posFromElement(view, elt, coords) + + let desc = view.docView.nearestDesc(elt, true) + return {pos, inside: desc ? desc.posAtStart - desc.border : -1} +} + +function nonZero(rect: DOMRect) { + return rect.top < rect.bottom || rect.left < rect.right +} + +function singleRect(target: HTMLElement | Range, bias: number): DOMRect { + let rects = target.getClientRects() + if (rects.length) { + let first = rects[bias < 0 ? 0 : rects.length - 1] + if (nonZero(first)) return first + } + return Array.prototype.find.call(rects, nonZero) || target.getBoundingClientRect() +} + +const BIDI = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/ + +// Given a position in the document model, get a bounding box of the +// character at that position, relative to the window. +export function coordsAtPos(view: EditorView, pos: number, side: number): Rect { + let {node, offset, atom} = view.docView.domFromPos(pos, side < 0 ? -1 : 1) + + let supportEmptyRange = browser.webkit || browser.gecko + if (node.nodeType == 3) { + // These browsers support querying empty text ranges. Prefer that in + // bidi context or when at the end of a node. + if (supportEmptyRange && (BIDI.test(node.nodeValue!) || (side < 0 ? !offset : offset == node.nodeValue!.length))) { + let rect = singleRect(textRange(node as Text, offset, offset), side) + // Firefox returns bad results (the position before the space) + // when querying a position directly after line-broken + // whitespace. Detect this situation and and kludge around it + if (browser.gecko && offset && /\s/.test(node.nodeValue![offset - 1]) && offset < node.nodeValue!.length) { + let rectBefore = singleRect(textRange(node as Text, offset - 1, offset - 1), -1) + if (rectBefore.top == rect.top) { + let rectAfter = singleRect(textRange(node as Text, offset, offset + 1), -1) + if (rectAfter.top != rect.top) + return flattenV(rectAfter, rectAfter.left < rectBefore.left) + } + } + return rect + } else { + let from = offset, to = offset, takeSide = side < 0 ? 1 : -1 + if (side < 0 && !offset) { to++; takeSide = -1 } + else if (side >= 0 && offset == node.nodeValue!.length) { from--; takeSide = 1 } + else if (side < 0) { from-- } + else { to ++ } + return flattenV(singleRect(textRange(node as Text, from, to), takeSide), takeSide < 0) + } + } + + let $dom = view.state.doc.resolve(pos - (atom || 0)) + // Return a horizontal line in block context + if (!$dom.parent.inlineContent) { + if (atom == null && offset && (side < 0 || offset == nodeSize(node))) { + let before = node.childNodes[offset - 1] + if (before.nodeType == 1) return flattenH((before as HTMLElement).getBoundingClientRect(), false) + } + if (atom == null && offset < nodeSize(node)) { + let after = node.childNodes[offset] + if (after.nodeType == 1) return flattenH((after as HTMLElement).getBoundingClientRect(), true) + } + return flattenH((node as HTMLElement).getBoundingClientRect(), side >= 0) + } + + // Inline, not in text node (this is not Bidi-safe) + if (atom == null && offset && (side < 0 || offset == nodeSize(node))) { + let before = node.childNodes[offset - 1] + let target = before.nodeType == 3 ? textRange(before as Text, nodeSize(before) - (supportEmptyRange ? 0 : 1)) + // BR nodes tend to only return the rectangle before them. + // Only use them if they are the last element in their parent + : before.nodeType == 1 && (before.nodeName != "BR" || !before.nextSibling) ? before : null + if (target) return flattenV(singleRect(target as Range | HTMLElement, 1), false) + } + if (atom == null && offset < nodeSize(node)) { + let after = node.childNodes[offset] + while (after.pmViewDesc && after.pmViewDesc.ignoreForCoords) after = after.nextSibling! + let target = !after ? null : after.nodeType == 3 ? textRange(after as Text, 0, (supportEmptyRange ? 0 : 1)) + : after.nodeType == 1 ? after : null + if (target) return flattenV(singleRect(target as Range | HTMLElement, -1), true) + } + // All else failed, just try to get a rectangle for the target node + return flattenV(singleRect(node.nodeType == 3 ? textRange(node as Text) : node as HTMLElement, -side), side >= 0) +} + +function flattenV(rect: DOMRect, left: boolean) { + if (rect.width == 0) return rect + let x = left ? rect.left : rect.right + return {top: rect.top, bottom: rect.bottom, left: x, right: x} +} + +function flattenH(rect: DOMRect, top: boolean) { + if (rect.height == 0) return rect + let y = top ? rect.top : rect.bottom + return {top: y, bottom: y, left: rect.left, right: rect.right} +} + +function withFlushedState<T>(view: EditorView, state: EditorState, f: () => T): T { + let viewState = view.state, active = view.root.activeElement as HTMLElement + if (viewState != state) view.updateState(state) + if (active != view.dom) view.focus() + try { + return f() + } finally { + if (viewState != state) view.updateState(viewState) + if (active != view.dom && active) active.focus() + } +} + +// Whether vertical position motion in a given direction +// from a position would leave a text block. +function endOfTextblockVertical(view: EditorView, state: EditorState, dir: "up" | "down") { + let sel = state.selection + let $pos = dir == "up" ? sel.$from : sel.$to + return withFlushedState(view, state, () => { + let {node: dom} = view.docView.domFromPos($pos.pos, dir == "up" ? -1 : 1) + for (;;) { + let nearest = view.docView.nearestDesc(dom, true) + if (!nearest) break + if (nearest.node.isBlock) { dom = nearest.contentDOM || nearest.dom; break } + dom = nearest.dom.parentNode! + } + let coords = coordsAtPos(view, $pos.pos, 1) + for (let child = dom.firstChild; child; child = child.nextSibling) { + let boxes + if (child.nodeType == 1) boxes = (child as HTMLElement).getClientRects() + else if (child.nodeType == 3) boxes = textRange(child as Text, 0, child.nodeValue!.length).getClientRects() + else continue + for (let i = 0; i < boxes.length; i++) { + let box = boxes[i] + if (box.bottom > box.top + 1 && + (dir == "up" ? coords.top - box.top > (box.bottom - coords.top) * 2 + : box.bottom - coords.bottom > (coords.bottom - box.top) * 2)) + return false + } + } + return true + }) +} + +const maybeRTL = /[\u0590-\u08ac]/ + +function endOfTextblockHorizontal(view: EditorView, state: EditorState, dir: "left" | "right" | "forward" | "backward") { + let {$head} = state.selection + if (!$head.parent.isTextblock) return false + let offset = $head.parentOffset, atStart = !offset, atEnd = offset == $head.parent.content.size + let sel: Selection = view.domSelection()! + if (!sel) return $head.pos == $head.start() || $head.pos == $head.end() + // If the textblock is all LTR, or the browser doesn't support + // Selection.modify (Edge), fall back to a primitive approach + if (!maybeRTL.test($head.parent.textContent) || !(sel as any).modify) + return dir == "left" || dir == "backward" ? atStart : atEnd + + return withFlushedState(view, state, () => { + // This is a huge hack, but appears to be the best we can + // currently do: use `Selection.modify` to move the selection by + // one character, and see if that moves the cursor out of the + // textblock (or doesn't move it at all, when at the start/end of + // the document). + let {focusNode: oldNode, focusOffset: oldOff, anchorNode, anchorOffset} = view.domSelectionRange() + let oldBidiLevel = (sel as any).caretBidiLevel // Only for Firefox + ;(sel as any).modify("move", dir, "character") + let parentDOM = $head.depth ? view.docView.domAfterPos($head.before()) : view.dom + let {focusNode: newNode, focusOffset: newOff} = view.domSelectionRange() + let result = newNode && !parentDOM.contains(newNode.nodeType == 1 ? newNode : newNode.parentNode) || + (oldNode == newNode && oldOff == newOff) + // Restore the previous selection + try { + sel.collapse(anchorNode, anchorOffset) + if (oldNode && (oldNode != anchorNode || oldOff != anchorOffset) && sel.extend) sel.extend(oldNode, oldOff) + } catch (_) {} + if (oldBidiLevel != null) (sel as any).caretBidiLevel = oldBidiLevel + return result + }) +} + +export type TextblockDir = "up" | "down" | "left" | "right" | "forward" | "backward" + +let cachedState: EditorState | null = null +let cachedDir: TextblockDir | null = null +let cachedResult: boolean = false +export function endOfTextblock(view: EditorView, state: EditorState, dir: TextblockDir) { + if (cachedState == state && cachedDir == dir) return cachedResult + cachedState = state; cachedDir = dir + return cachedResult = dir == "up" || dir == "down" + ? endOfTextblockVertical(view, state, dir) + : endOfTextblockHorizontal(view, state, dir) +} diff --git a/third_party/js/prosemirror/prosemirror-view/src/domobserver.ts b/third_party/js/prosemirror/prosemirror-view/src/domobserver.ts @@ -0,0 +1,349 @@ +import {Selection} from "prosemirror-state" +import * as browser from "./browser" +import {domIndex, isEquivalentPosition, selectionCollapsed, parentNode, DOMSelectionRange, DOMNode, DOMSelection} from "./dom" +import {hasFocusAndSelection, selectionToDOM, selectionFromDOM} from "./selection" +import {EditorView} from "./index" + +const observeOptions = { + childList: true, + characterData: true, + characterDataOldValue: true, + attributes: true, + attributeOldValue: true, + subtree: true +} +// IE11 has very broken mutation observers, so we also listen to DOMCharacterDataModified +const useCharData = browser.ie && browser.ie_version <= 11 + +class SelectionState { + anchorNode: Node | null = null + anchorOffset: number = 0 + focusNode: Node | null = null + focusOffset: number = 0 + + set(sel: DOMSelectionRange) { + this.anchorNode = sel.anchorNode; this.anchorOffset = sel.anchorOffset + this.focusNode = sel.focusNode; this.focusOffset = sel.focusOffset + } + + clear() { + this.anchorNode = this.focusNode = null + } + + eq(sel: DOMSelectionRange) { + return sel.anchorNode == this.anchorNode && sel.anchorOffset == this.anchorOffset && + sel.focusNode == this.focusNode && sel.focusOffset == this.focusOffset + } +} + +export class DOMObserver { + queue: MutationRecord[] = [] + flushingSoon = -1 + observer: MutationObserver | null = null + currentSelection = new SelectionState + onCharData: ((e: Event) => void) | null = null + suppressingSelectionUpdates = false + lastChangedTextNode: Text | null = null + + constructor( + readonly view: EditorView, + readonly handleDOMChange: (from: number, to: number, typeOver: boolean, added: Node[]) => void + ) { + this.observer = window.MutationObserver && + new window.MutationObserver(mutations => { + for (let i = 0; i < mutations.length; i++) this.queue.push(mutations[i]) + // IE11 will sometimes (on backspacing out a single character + // text node after a BR node) call the observer callback + // before actually updating the DOM, which will cause + // ProseMirror to miss the change (see #930) + if (browser.ie && browser.ie_version <= 11 && mutations.some( + m => m.type == "childList" && m.removedNodes.length || + m.type == "characterData" && m.oldValue!.length > m.target.nodeValue!.length)) + this.flushSoon() + else + this.flush() + }) + if (useCharData) { + this.onCharData = e => { + this.queue.push({target: e.target as Node, type: "characterData", oldValue: (e as any).prevValue} as MutationRecord) + this.flushSoon() + } + } + this.onSelectionChange = this.onSelectionChange.bind(this) + } + + flushSoon() { + if (this.flushingSoon < 0) + this.flushingSoon = window.setTimeout(() => { this.flushingSoon = -1; this.flush() }, 20) + } + + forceFlush() { + if (this.flushingSoon > -1) { + window.clearTimeout(this.flushingSoon) + this.flushingSoon = -1 + this.flush() + } + } + + start() { + if (this.observer) { + this.observer.takeRecords() + this.observer.observe(this.view.dom, observeOptions) + } + if (this.onCharData) + this.view.dom.addEventListener("DOMCharacterDataModified", this.onCharData) + this.connectSelection() + } + + stop() { + if (this.observer) { + let take = this.observer.takeRecords() + if (take.length) { + for (let i = 0; i < take.length; i++) this.queue.push(take[i]) + window.setTimeout(() => this.flush(), 20) + } + this.observer.disconnect() + } + if (this.onCharData) this.view.dom.removeEventListener("DOMCharacterDataModified", this.onCharData) + this.disconnectSelection() + } + + connectSelection() { + this.view.dom.ownerDocument.addEventListener("selectionchange", this.onSelectionChange) + } + + disconnectSelection() { + this.view.dom.ownerDocument.removeEventListener("selectionchange", this.onSelectionChange) + } + + suppressSelectionUpdates() { + this.suppressingSelectionUpdates = true + setTimeout(() => this.suppressingSelectionUpdates = false, 50) + } + + onSelectionChange() { + if (!hasFocusAndSelection(this.view)) return + if (this.suppressingSelectionUpdates) return selectionToDOM(this.view) + // Deletions on IE11 fire their events in the wrong order, giving + // us a selection change event before the DOM changes are + // reported. + if (browser.ie && browser.ie_version <= 11 && !this.view.state.selection.empty) { + let sel = this.view.domSelectionRange() + // Selection.isCollapsed isn't reliable on IE + if (sel.focusNode && isEquivalentPosition(sel.focusNode, sel.focusOffset, sel.anchorNode!, sel.anchorOffset)) + return this.flushSoon() + } + this.flush() + } + + setCurSelection() { + this.currentSelection.set(this.view.domSelectionRange()) + } + + ignoreSelectionChange(sel: DOMSelectionRange) { + if (!sel.focusNode) return true + let ancestors: Set<Node> = new Set, container: DOMNode | undefined + for (let scan: DOMNode | null = sel.focusNode; scan; scan = parentNode(scan)) ancestors.add(scan) + for (let scan = sel.anchorNode; scan; scan = parentNode(scan)) if (ancestors.has(scan)) { + container = scan + break + } + let desc = container && this.view.docView.nearestDesc(container) + if (desc && desc.ignoreMutation({ + type: "selection", + target: container!.nodeType == 3 ? container!.parentNode! : container! + })) { + this.setCurSelection() + return true + } + } + + pendingRecords() { + if (this.observer) for (let mut of this.observer.takeRecords()) this.queue.push(mut) + return this.queue + } + + flush() { + let {view} = this + if (!view.docView || this.flushingSoon > -1) return + let mutations = this.pendingRecords() + if (mutations.length) this.queue = [] + + let sel = view.domSelectionRange() + let newSel = !this.suppressingSelectionUpdates && !this.currentSelection.eq(sel) && hasFocusAndSelection(view) && !this.ignoreSelectionChange(sel) + + let from = -1, to = -1, typeOver = false, added: Node[] = [] + if (view.editable) { + for (let i = 0; i < mutations.length; i++) { + let result = this.registerMutation(mutations[i], added) + if (result) { + from = from < 0 ? result.from : Math.min(result.from, from) + to = to < 0 ? result.to : Math.max(result.to, to) + if (result.typeOver) typeOver = true + } + } + } + + if (browser.gecko && added.length) { + let brs = added.filter(n => n.nodeName == "BR") as HTMLElement[] + if (brs.length == 2) { + let [a, b] = brs + if (a.parentNode && a.parentNode.parentNode == b.parentNode) b.remove() + else a.remove() + } else { + let {focusNode} = this.currentSelection + for (let br of brs) { + let parent = br.parentNode + if (parent && parent.nodeName == "LI" && (!focusNode || blockParent(view, focusNode) != parent)) + br.remove() + } + } + } else if ((browser.chrome || browser.safari) && added.some(n => n.nodeName == "BR") && + (view.input.lastKeyCode == 8 || view.input.lastKeyCode == 46)) { + // Chrome/Safari sometimes insert a bogus break node if you + // backspace out the last bit of text before an inline-flex node (#1552) + for (let node of added) if (node.nodeName == "BR" && node.parentNode) { + let after = node.nextSibling + if (after && after.nodeType == 1 && (after as HTMLElement).contentEditable == "false") + node.parentNode.removeChild(node) + } + } + + let readSel: Selection | null = null + // If it looks like the browser has reset the selection to the + // start of the document after focus, restore the selection from + // the state + if (from < 0 && newSel && view.input.lastFocus > Date.now() - 200 && + Math.max(view.input.lastTouch, view.input.lastClick.time) < Date.now() - 300 && + selectionCollapsed(sel) && (readSel = selectionFromDOM(view)) && + readSel.eq(Selection.near(view.state.doc.resolve(0), 1))) { + view.input.lastFocus = 0 + selectionToDOM(view) + this.currentSelection.set(sel) + view.scrollToSelection() + } else if (from > -1 || newSel) { + if (from > -1) { + view.docView.markDirty(from, to) + checkCSS(view) + } + this.handleDOMChange(from, to, typeOver, added) + if (view.docView && view.docView.dirty) view.updateState(view.state) + else if (!this.currentSelection.eq(sel)) selectionToDOM(view) + this.currentSelection.set(sel) + } + } + + registerMutation(mut: MutationRecord, added: Node[]) { + // Ignore mutations inside nodes that were already noted as inserted + if (added.indexOf(mut.target) > -1) return null + let desc = this.view.docView.nearestDesc(mut.target) + if (mut.type == "attributes" && + (desc == this.view.docView || mut.attributeName == "contenteditable" || + // Firefox sometimes fires spurious events for null/empty styles + (mut.attributeName == "style" && !mut.oldValue && !(mut.target as HTMLElement).getAttribute("style")))) + return null + if (!desc || desc.ignoreMutation(mut)) return null + + if (mut.type == "childList") { + for (let i = 0; i < mut.addedNodes.length; i++) { + let node = mut.addedNodes[i] + added.push(node) + if (node.nodeType == 3) this.lastChangedTextNode = node as Text + } + if (desc.contentDOM && desc.contentDOM != desc.dom && !desc.contentDOM.contains(mut.target)) + return {from: desc.posBefore, to: desc.posAfter} + let prev = mut.previousSibling, next = mut.nextSibling + if (browser.ie && browser.ie_version <= 11 && mut.addedNodes.length) { + // IE11 gives us incorrect next/prev siblings for some + // insertions, so if there are added nodes, recompute those + for (let i = 0; i < mut.addedNodes.length; i++) { + let {previousSibling, nextSibling} = mut.addedNodes[i] + if (!previousSibling || Array.prototype.indexOf.call(mut.addedNodes, previousSibling) < 0) prev = previousSibling + if (!nextSibling || Array.prototype.indexOf.call(mut.addedNodes, nextSibling) < 0) next = nextSibling + } + } + let fromOffset = prev && prev.parentNode == mut.target + ? domIndex(prev) + 1 : 0 + let from = desc.localPosFromDOM(mut.target, fromOffset, -1) + let toOffset = next && next.parentNode == mut.target + ? domIndex(next) : mut.target.childNodes.length + let to = desc.localPosFromDOM(mut.target, toOffset, 1) + return {from, to} + } else if (mut.type == "attributes") { + return {from: desc.posAtStart - desc.border, to: desc.posAtEnd + desc.border} + } else { // "characterData" + this.lastChangedTextNode = mut.target as Text + return { + from: desc.posAtStart, + to: desc.posAtEnd, + // An event was generated for a text change that didn't change + // any text. Mark the dom change to fall back to assuming the + // selection was typed over with an identical value if it can't + // find another change. + typeOver: mut.target.nodeValue == mut.oldValue + } + } + } +} + +let cssChecked: WeakMap<EditorView, null> = new WeakMap() +let cssCheckWarned: boolean = false + +function checkCSS(view: EditorView) { + if (cssChecked.has(view)) return + cssChecked.set(view, null) + if (['normal', 'nowrap', 'pre-line'].indexOf(getComputedStyle(view.dom).whiteSpace) !== -1) { + view.requiresGeckoHackNode = browser.gecko + if (cssCheckWarned) return + console["warn"]("ProseMirror expects the CSS white-space property to be set, preferably to 'pre-wrap'. It is recommended to load style/prosemirror.css from the prosemirror-view package.") + cssCheckWarned = true + } +} + +function rangeToSelectionRange(view: EditorView, range: StaticRange) { + let anchorNode = range.startContainer, anchorOffset = range.startOffset + let focusNode = range.endContainer, focusOffset = range.endOffset + + let currentAnchor = view.domAtPos(view.state.selection.anchor) + // Since such a range doesn't distinguish between anchor and head, + // use a heuristic that flips it around if its end matches the + // current anchor. + if (isEquivalentPosition(currentAnchor.node, currentAnchor.offset, focusNode, focusOffset)) + [anchorNode, anchorOffset, focusNode, focusOffset] = [focusNode, focusOffset, anchorNode, anchorOffset] + return {anchorNode, anchorOffset, focusNode, focusOffset} +} + +// Used to work around a Safari Selection/shadow DOM bug +// Based on https://github.com/codemirror/dev/issues/414 fix +export function safariShadowSelectionRange(view: EditorView, selection: DOMSelection): DOMSelectionRange | null { + if ((selection as any).getComposedRanges) { + let range = (selection as any).getComposedRanges(view.root)[0] as StaticRange + if (range) return rangeToSelectionRange(view, range) + } + + let found: StaticRange | undefined + function read(event: InputEvent) { + event.preventDefault() + event.stopImmediatePropagation() + found = event.getTargetRanges()[0] + } + + // Because Safari (at least in 2018-2022) doesn't provide regular + // access to the selection inside a shadowRoot, we have to perform a + // ridiculous hack to get at it—using `execCommand` to trigger a + // `beforeInput` event so that we can read the target range from the + // event. + view.dom.addEventListener("beforeinput", read, true) + document.execCommand("indent") + view.dom.removeEventListener("beforeinput", read, true) + + return found ? rangeToSelectionRange(view, found) : null +} + +function blockParent(view: EditorView, node: DOMNode): Node | null { + for (let p = node.parentNode; p && p != view.dom; p = p.parentNode) { + let desc = view.docView.nearestDesc(p, true) + if (desc && desc.node.isBlock) return p + } + return null +} diff --git a/third_party/js/prosemirror/prosemirror-view/src/index.ts b/third_party/js/prosemirror/prosemirror-view/src/index.ts @@ -0,0 +1,825 @@ +import {NodeSelection, EditorState, Plugin, PluginView, Transaction, Selection} from "prosemirror-state" +import {Slice, ResolvedPos, DOMParser, DOMSerializer, Node, Mark} from "prosemirror-model" + +import {scrollRectIntoView, posAtCoords, coordsAtPos, endOfTextblock, storeScrollPos, + resetScrollPos, focusPreventScroll} from "./domcoords" +import {docViewDesc, ViewDesc, NodeView, NodeViewDesc, MarkView} from "./viewdesc" +import {initInput, destroyInput, dispatchEvent, ensureListeners, clearComposition, + InputState, doPaste, Dragging, findCompositionNode} from "./input" +import {selectionToDOM, anchorInRightPlace, syncNodeSelection} from "./selection" +import {Decoration, viewDecorations, DecorationSource} from "./decoration" +import {DOMObserver, safariShadowSelectionRange} from "./domobserver" +import {readDOMChange} from "./domchange" +import {DOMSelection, DOMNode, DOMSelectionRange, deepActiveElement, clearReusedRange} from "./dom" +import * as browser from "./browser" + +export {Decoration, DecorationSet, DecorationAttrs, DecorationSource} from "./decoration" +export {NodeView, MarkView, ViewMutationRecord} from "./viewdesc" + +// Exported for testing +import {serializeForClipboard, parseFromClipboard} from "./clipboard" +import {endComposition} from "./input" +/// @internal +export const __parseFromClipboard = parseFromClipboard +/// @internal +export const __endComposition = endComposition + +/// An editor view manages the DOM structure that represents an +/// editable document. Its state and behavior are determined by its +/// [props](#view.DirectEditorProps). +export class EditorView { + /// @internal + _props: DirectEditorProps + private directPlugins: readonly Plugin[] + private _root: Document | ShadowRoot | null = null + /// @internal + focused = false + /// Kludge used to work around a Chrome bug @internal + trackWrites: DOMNode | null = null + private mounted = false + /// @internal + markCursor: readonly Mark[] | null = null + /// @internal + cursorWrapper: {dom: DOMNode, deco: Decoration} | null = null + /// @internal + nodeViews: NodeViewSet + /// @internal + lastSelectedViewDesc: ViewDesc | undefined = undefined + /// @internal + docView: NodeViewDesc + /// @internal + input = new InputState + private prevDirectPlugins: readonly Plugin[] = [] + private pluginViews: PluginView[] = [] + /// @internal + declare domObserver: DOMObserver + /// Holds `true` when a hack node is needed in Firefox to prevent the + /// [space is eaten issue](https://github.com/ProseMirror/prosemirror/issues/651) + /// @internal + requiresGeckoHackNode: boolean = false + + /// The view's current [state](#state.EditorState). + public state: EditorState + + /// Create a view. `place` may be a DOM node that the editor should + /// be appended to, a function that will place it into the document, + /// or an object whose `mount` property holds the node to use as the + /// document container. If it is `null`, the editor will not be + /// added to the document. + constructor(place: null | DOMNode | ((editor: HTMLElement) => void) | {mount: HTMLElement}, props: DirectEditorProps) { + this._props = props + this.state = props.state + this.directPlugins = props.plugins || [] + this.directPlugins.forEach(checkStateComponent) + + this.dispatch = this.dispatch.bind(this) + + this.dom = (place && (place as {mount: HTMLElement}).mount) || document.createElement("div") + if (place) { + if ((place as DOMNode).appendChild) (place as DOMNode).appendChild(this.dom) + else if (typeof place == "function") place(this.dom) + else if ((place as {mount: HTMLElement}).mount) this.mounted = true + } + + this.editable = getEditable(this) + updateCursorWrapper(this) + this.nodeViews = buildNodeViews(this) + this.docView = docViewDesc(this.state.doc, computeDocDeco(this), viewDecorations(this), this.dom, this) + + this.domObserver = new DOMObserver(this, (from, to, typeOver, added) => readDOMChange(this, from, to, typeOver, added)) + this.domObserver.start() + initInput(this) + this.updatePluginViews() + } + + /// An editable DOM node containing the document. (You probably + /// should not directly interfere with its content.) + readonly dom: HTMLElement + + /// Indicates whether the editor is currently [editable](#view.EditorProps.editable). + editable: boolean + + /// When editor content is being dragged, this object contains + /// information about the dragged slice and whether it is being + /// copied or moved. At any other time, it is null. + dragging: null | {slice: Slice, move: boolean} = null + + /// Holds `true` when a + /// [composition](https://w3c.github.io/uievents/#events-compositionevents) + /// is active. + get composing() { return this.input.composing } + + /// The view's current [props](#view.EditorProps). + get props() { + if (this._props.state != this.state) { + let prev = this._props + this._props = {} as any + for (let name in prev) (this._props as any)[name] = (prev as any)[name] + this._props.state = this.state + } + return this._props + } + + /// Update the view's props. Will immediately cause an update to + /// the DOM. + update(props: DirectEditorProps) { + if (props.handleDOMEvents != this._props.handleDOMEvents) ensureListeners(this) + let prevProps = this._props + this._props = props + if (props.plugins) { + props.plugins.forEach(checkStateComponent) + this.directPlugins = props.plugins + } + this.updateStateInner(props.state, prevProps) + } + + /// Update the view by updating existing props object with the object + /// given as argument. Equivalent to `view.update(Object.assign({}, + /// view.props, props))`. + setProps(props: Partial<DirectEditorProps>) { + let updated = {} as DirectEditorProps + for (let name in this._props) (updated as any)[name] = (this._props as any)[name] + updated.state = this.state + for (let name in props) (updated as any)[name] = (props as any)[name] + this.update(updated) + } + + /// Update the editor's `state` prop, without touching any of the + /// other props. + updateState(state: EditorState) { + this.updateStateInner(state, this._props) + } + + private updateStateInner(state: EditorState, prevProps: DirectEditorProps) { + let prev = this.state, redraw = false, updateSel = false + // When stored marks are added, stop composition, so that they can + // be displayed. + if (state.storedMarks && this.composing) { + clearComposition(this) + updateSel = true + } + this.state = state + let pluginsChanged = prev.plugins != state.plugins || this._props.plugins != prevProps.plugins + if (pluginsChanged || this._props.plugins != prevProps.plugins || this._props.nodeViews != prevProps.nodeViews) { + let nodeViews = buildNodeViews(this) + if (changedNodeViews(nodeViews, this.nodeViews)) { + this.nodeViews = nodeViews + redraw = true + } + } + if (pluginsChanged || prevProps.handleDOMEvents != this._props.handleDOMEvents) { + ensureListeners(this) + } + + this.editable = getEditable(this) + updateCursorWrapper(this) + let innerDeco = viewDecorations(this), outerDeco = computeDocDeco(this) + + let scroll = prev.plugins != state.plugins && !prev.doc.eq(state.doc) ? "reset" + : (state as any).scrollToSelection > (prev as any).scrollToSelection ? "to selection" : "preserve" + let updateDoc = redraw || !this.docView.matchesNode(state.doc, outerDeco, innerDeco) + if (updateDoc || !state.selection.eq(prev.selection)) updateSel = true + let oldScrollPos = scroll == "preserve" && updateSel && this.dom.style.overflowAnchor == null && storeScrollPos(this) + + if (updateSel) { + this.domObserver.stop() + // Work around an issue in Chrome, IE, and Edge where changing + // the DOM around an active selection puts it into a broken + // state where the thing the user sees differs from the + // selection reported by the Selection object (#710, #973, + // #1011, #1013, #1035). + let forceSelUpdate = updateDoc && (browser.ie || browser.chrome) && !this.composing && + !prev.selection.empty && !state.selection.empty && selectionContextChanged(prev.selection, state.selection) + if (updateDoc) { + // If the node that the selection points into is written to, + // Chrome sometimes starts misreporting the selection, so this + // tracks that and forces a selection reset when our update + // did write to the node. + let chromeKludge = browser.chrome ? (this.trackWrites = this.domSelectionRange().focusNode) : null + if (this.composing) this.input.compositionNode = findCompositionNode(this) + if (redraw || !this.docView.update(state.doc, outerDeco, innerDeco, this)) { + this.docView.updateOuterDeco(outerDeco) + this.docView.destroy() + this.docView = docViewDesc(state.doc, outerDeco, innerDeco, this.dom, this) + } + if (chromeKludge && !this.trackWrites) forceSelUpdate = true + } + // Work around for an issue where an update arriving right between + // a DOM selection change and the "selectionchange" event for it + // can cause a spurious DOM selection update, disrupting mouse + // drag selection. + if (forceSelUpdate || + !(this.input.mouseDown && this.domObserver.currentSelection.eq(this.domSelectionRange()) && + anchorInRightPlace(this))) { + selectionToDOM(this, forceSelUpdate) + } else { + syncNodeSelection(this, state.selection) + this.domObserver.setCurSelection() + } + this.domObserver.start() + } + + this.updatePluginViews(prev) + if ((this.dragging as Dragging)?.node && !prev.doc.eq(state.doc)) + this.updateDraggedNode(this.dragging as Dragging, prev) + + if (scroll == "reset") { + this.dom.scrollTop = 0 + } else if (scroll == "to selection") { + this.scrollToSelection() + } else if (oldScrollPos) { + resetScrollPos(oldScrollPos) + } + } + + /// @internal + scrollToSelection() { + let startDOM = this.domSelectionRange().focusNode + if (!startDOM || !this.dom.contains(startDOM.nodeType == 1 ? startDOM : startDOM.parentNode)) { + // Ignore selections outside the editor + } else if (this.someProp("handleScrollToSelection", f => f(this))) { + // Handled + } else if (this.state.selection instanceof NodeSelection) { + let target = this.docView.domAfterPos(this.state.selection.from) + if (target.nodeType == 1) scrollRectIntoView(this, (target as HTMLElement).getBoundingClientRect(), startDOM) + } else { + scrollRectIntoView(this, this.coordsAtPos(this.state.selection.head, 1), startDOM) + } + } + + private destroyPluginViews() { + let view + while (view = this.pluginViews.pop()) if (view.destroy) view.destroy() + } + + private updatePluginViews(prevState?: EditorState) { + if (!prevState || prevState.plugins != this.state.plugins || this.directPlugins != this.prevDirectPlugins) { + this.prevDirectPlugins = this.directPlugins + this.destroyPluginViews() + for (let i = 0; i < this.directPlugins.length; i++) { + let plugin = this.directPlugins[i] + if (plugin.spec.view) this.pluginViews.push(plugin.spec.view(this)) + } + for (let i = 0; i < this.state.plugins.length; i++) { + let plugin = this.state.plugins[i] + if (plugin.spec.view) this.pluginViews.push(plugin.spec.view(this)) + } + } else { + for (let i = 0; i < this.pluginViews.length; i++) { + let pluginView = this.pluginViews[i] + if (pluginView.update) pluginView.update(this, prevState) + } + } + } + + private updateDraggedNode(dragging: Dragging, prev: EditorState) { + let sel = dragging.node!, found = -1 + if (this.state.doc.nodeAt(sel.from) == sel.node) { + found = sel.from + } else { + let movedPos = sel.from + (this.state.doc.content.size - prev.doc.content.size) + let moved = movedPos > 0 && this.state.doc.nodeAt(movedPos) + if (moved == sel.node) found = movedPos + } + this.dragging = new Dragging(dragging.slice, dragging.move, + found < 0 ? undefined : NodeSelection.create(this.state.doc, found)) + } + + /// Goes over the values of a prop, first those provided directly, + /// then those from plugins given to the view, then from plugins in + /// the state (in order), and calls `f` every time a non-undefined + /// value is found. When `f` returns a truthy value, that is + /// immediately returned. When `f` isn't provided, it is treated as + /// the identity function (the prop value is returned directly). + someProp<PropName extends keyof EditorProps, Result>( + propName: PropName, + f: (value: NonNullable<EditorProps[PropName]>) => Result + ): Result | undefined + someProp<PropName extends keyof EditorProps>(propName: PropName): NonNullable<EditorProps[PropName]> | undefined + someProp<PropName extends keyof EditorProps, Result>( + propName: PropName, + f?: (value: NonNullable<EditorProps[PropName]>) => Result + ): Result | undefined { + let prop = this._props && this._props[propName], value + if (prop != null && (value = f ? f(prop as any) : prop)) return value as any + for (let i = 0; i < this.directPlugins.length; i++) { + let prop = this.directPlugins[i].props[propName] + if (prop != null && (value = f ? f(prop as any) : prop)) return value as any + } + let plugins = this.state.plugins + if (plugins) for (let i = 0; i < plugins.length; i++) { + let prop = plugins[i].props[propName] + if (prop != null && (value = f ? f(prop as any) : prop)) return value as any + } + } + + /// Query whether the view has focus. + hasFocus() { + // Work around IE not handling focus correctly if resize handles are shown. + // If the cursor is inside an element with resize handles, activeElement + // will be that element instead of this.dom. + if (browser.ie) { + // If activeElement is within this.dom, and there are no other elements + // setting `contenteditable` to false in between, treat it as focused. + let node = this.root.activeElement + if (node == this.dom) return true + if (!node || !this.dom.contains(node)) return false + while (node && this.dom != node && this.dom.contains(node)) { + if ((node as HTMLElement).contentEditable == 'false') return false + node = node.parentElement + } + return true + } + return this.root.activeElement == this.dom + } + + /// Focus the editor. + focus() { + this.domObserver.stop() + if (this.editable) focusPreventScroll(this.dom) + selectionToDOM(this) + this.domObserver.start() + } + + /// Get the document root in which the editor exists. This will + /// usually be the top-level `document`, but might be a [shadow + /// DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM) + /// root if the editor is inside one. + get root(): Document | ShadowRoot { + let cached = this._root + if (cached == null) for (let search = this.dom.parentNode; search; search = search.parentNode) { + if (search.nodeType == 9 || (search.nodeType == 11 && (search as any).host)) { + if (!(search as any).getSelection) + Object.getPrototypeOf(search).getSelection = () => (search as DOMNode).ownerDocument!.getSelection() + return this._root = search as Document | ShadowRoot + } + } + return cached || document + } + + /// When an existing editor view is moved to a new document or + /// shadow tree, call this to make it recompute its root. + updateRoot() { + this._root = null + } + + /// Given a pair of viewport coordinates, return the document + /// position that corresponds to them. May return null if the given + /// coordinates aren't inside of the editor. When an object is + /// returned, its `pos` property is the position nearest to the + /// coordinates, and its `inside` property holds the position of the + /// inner node that the position falls inside of, or -1 if it is at + /// the top level, not in any node. + posAtCoords(coords: {left: number, top: number}): {pos: number, inside: number} | null { + return posAtCoords(this, coords) + } + + /// Returns the viewport rectangle at a given document position. + /// `left` and `right` will be the same number, as this returns a + /// flat cursor-ish rectangle. If the position is between two things + /// that aren't directly adjacent, `side` determines which element + /// is used. When < 0, the element before the position is used, + /// otherwise the element after. + coordsAtPos(pos: number, side = 1): {left: number, right: number, top: number, bottom: number} { + return coordsAtPos(this, pos, side) + } + + /// Find the DOM position that corresponds to the given document + /// position. When `side` is negative, find the position as close as + /// possible to the content before the position. When positive, + /// prefer positions close to the content after the position. When + /// zero, prefer as shallow a position as possible. + /// + /// Note that you should **not** mutate the editor's internal DOM, + /// only inspect it (and even that is usually not necessary). + domAtPos(pos: number, side = 0): {node: DOMNode, offset: number} { + return this.docView.domFromPos(pos, side) + } + + /// Find the DOM node that represents the document node after the + /// given position. May return `null` when the position doesn't point + /// in front of a node or if the node is inside an opaque node view. + /// + /// This is intended to be able to call things like + /// `getBoundingClientRect` on that DOM node. Do **not** mutate the + /// editor DOM directly, or add styling this way, since that will be + /// immediately overriden by the editor as it redraws the node. + nodeDOM(pos: number): DOMNode | null { + let desc = this.docView.descAt(pos) + return desc ? (desc as NodeViewDesc).nodeDOM : null + } + + /// Find the document position that corresponds to a given DOM + /// position. (Whenever possible, it is preferable to inspect the + /// document structure directly, rather than poking around in the + /// DOM, but sometimes—for example when interpreting an event + /// target—you don't have a choice.) + /// + /// The `bias` parameter can be used to influence which side of a DOM + /// node to use when the position is inside a leaf node. + posAtDOM(node: DOMNode, offset: number, bias = -1): number { + let pos = this.docView.posFromDOM(node, offset, bias) + if (pos == null) throw new RangeError("DOM position not inside the editor") + return pos + } + + /// Find out whether the selection is at the end of a textblock when + /// moving in a given direction. When, for example, given `"left"`, + /// it will return true if moving left from the current cursor + /// position would leave that position's parent textblock. Will apply + /// to the view's current state by default, but it is possible to + /// pass a different state. + endOfTextblock(dir: "up" | "down" | "left" | "right" | "forward" | "backward", state?: EditorState): boolean { + return endOfTextblock(this, state || this.state, dir) + } + + /// Run the editor's paste logic with the given HTML string. The + /// `event`, if given, will be passed to the + /// [`handlePaste`](#view.EditorProps.handlePaste) hook. + pasteHTML(html: string, event?: ClipboardEvent) { + return doPaste(this, "", html, false, event || new ClipboardEvent("paste")) + } + + /// Run the editor's paste logic with the given plain-text input. + pasteText(text: string, event?: ClipboardEvent) { + return doPaste(this, text, null, true, event || new ClipboardEvent("paste")) + } + + /// Serialize the given slice as it would be if it was copied from + /// this editor. Returns a DOM element that contains a + /// representation of the slice as its children, a textual + /// representation, and the transformed slice (which can be + /// different from the given input due to hooks like + /// [`transformCopied`](#view.EditorProps.transformCopied)). + serializeForClipboard(slice: Slice): {dom: HTMLElement, text: string, slice: Slice} { + return serializeForClipboard(this, slice) + } + + /// Removes the editor from the DOM and destroys all [node + /// views](#view.NodeView). + destroy() { + if (!this.docView) return + destroyInput(this) + this.destroyPluginViews() + if (this.mounted) { + this.docView.update(this.state.doc, [], viewDecorations(this), this) + this.dom.textContent = "" + } else if (this.dom.parentNode) { + this.dom.parentNode.removeChild(this.dom) + } + this.docView.destroy() + ;(this as any).docView = null + clearReusedRange() + } + + /// This is true when the view has been + /// [destroyed](#view.EditorView.destroy) (and thus should not be + /// used anymore). + get isDestroyed() { + return this.docView == null + } + + /// Used for testing. + dispatchEvent(event: Event) { + return dispatchEvent(this, event) + } + + /// Dispatch a transaction. Will call + /// [`dispatchTransaction`](#view.DirectEditorProps.dispatchTransaction) + /// when given, and otherwise defaults to applying the transaction to + /// the current state and calling + /// [`updateState`](#view.EditorView.updateState) with the result. + /// This method is bound to the view instance, so that it can be + /// easily passed around. + declare dispatch: (tr: Transaction) => void + + /// @internal + domSelectionRange(): DOMSelectionRange { + let sel = this.domSelection() + if (!sel) return {focusNode: null, focusOffset: 0, anchorNode: null, anchorOffset: 0} + return browser.safari && this.root.nodeType === 11 && + deepActiveElement(this.dom.ownerDocument) == this.dom && safariShadowSelectionRange(this, sel) || sel + } + + /// @internal + domSelection(): DOMSelection | null { + return (this.root as Document).getSelection() + } +} + +EditorView.prototype.dispatch = function(tr: Transaction) { + let dispatchTransaction = this._props.dispatchTransaction + if (dispatchTransaction) dispatchTransaction.call(this, tr) + else this.updateState(this.state.apply(tr)) +} + +function computeDocDeco(view: EditorView) { + let attrs = Object.create(null) + attrs.class = "ProseMirror" + attrs.contenteditable = String(view.editable) + + view.someProp("attributes", value => { + if (typeof value == "function") value = value(view.state) + if (value) for (let attr in value) { + if (attr == "class") + attrs.class += " " + value[attr] + else if (attr == "style") + attrs.style = (attrs.style ? attrs.style + ";" : "") + value[attr] + else if (!attrs[attr] && attr != "contenteditable" && attr != "nodeName") + attrs[attr] = String(value[attr]) + } + }) + if (!attrs.translate) attrs.translate = "no" + + return [Decoration.node(0, view.state.doc.content.size, attrs)] +} + +function updateCursorWrapper(view: EditorView) { + if (view.markCursor) { + let dom = document.createElement("img") + dom.className = "ProseMirror-separator" + dom.setAttribute("mark-placeholder", "true") + dom.setAttribute("alt", "") + view.cursorWrapper = {dom, deco: Decoration.widget(view.state.selection.from, + dom, {raw: true, marks: view.markCursor} as any)} + } else { + view.cursorWrapper = null + } +} + +function getEditable(view: EditorView) { + return !view.someProp("editable", value => value(view.state) === false) +} + +function selectionContextChanged(sel1: Selection, sel2: Selection) { + let depth = Math.min(sel1.$anchor.sharedDepth(sel1.head), sel2.$anchor.sharedDepth(sel2.head)) + return sel1.$anchor.start(depth) != sel2.$anchor.start(depth) +} + +function buildNodeViews(view: EditorView) { + let result: NodeViewSet = Object.create(null) + function add(obj: NodeViewSet) { + for (let prop in obj) if (!Object.prototype.hasOwnProperty.call(result, prop)) + result[prop] = obj[prop] + } + view.someProp("nodeViews", add) + view.someProp("markViews", add) + return result +} + +function changedNodeViews(a: NodeViewSet, b: NodeViewSet) { + let nA = 0, nB = 0 + for (let prop in a) { + if (a[prop] != b[prop]) return true + nA++ + } + for (let _ in b) nB++ + return nA != nB +} + +function checkStateComponent(plugin: Plugin) { + if (plugin.spec.state || plugin.spec.filterTransaction || plugin.spec.appendTransaction) + throw new RangeError("Plugins passed directly to the view must not have a state component") +} + +/// The type of function [provided](#view.EditorProps.nodeViews) to +/// create [node views](#view.NodeView). +export type NodeViewConstructor = (node: Node, view: EditorView, getPos: () => number | undefined, + decorations: readonly Decoration[], innerDecorations: DecorationSource) => NodeView + +/// The function types [used](#view.EditorProps.markViews) to create +/// mark views. +export type MarkViewConstructor = (mark: Mark, view: EditorView, inline: boolean) => MarkView + +type NodeViewSet = {[name: string]: NodeViewConstructor | MarkViewConstructor} + +/// Helper type that maps event names to event object types, but +/// includes events that TypeScript's HTMLElementEventMap doesn't know +/// about. +export interface DOMEventMap extends HTMLElementEventMap { + [event: string]: any +} + +/// Props are configuration values that can be passed to an editor view +/// or included in a plugin. This interface lists the supported props. +/// +/// The various event-handling functions may all return `true` to +/// indicate that they handled the given event. The view will then take +/// care to call `preventDefault` on the event, except with +/// `handleDOMEvents`, where the handler itself is responsible for that. +/// +/// How a prop is resolved depends on the prop. Handler functions are +/// called one at a time, starting with the base props and then +/// searching through the plugins (in order of appearance) until one of +/// them returns true. For some props, the first plugin that yields a +/// value gets precedence. +/// +/// The optional type parameter refers to the type of `this` in prop +/// functions, and is used to pass in the plugin type when defining a +/// [plugin](#state.Plugin). +export interface EditorProps<P = any> { + /// Can be an object mapping DOM event type names to functions that + /// handle them. Such functions will be called before any handling + /// ProseMirror does of events fired on the editable DOM element. + /// Contrary to the other event handling props, when returning true + /// from such a function, you are responsible for calling + /// `preventDefault` yourself (or not, if you want to allow the + /// default behavior). + handleDOMEvents?: { + [event in keyof DOMEventMap]?: (this: P, view: EditorView, event: DOMEventMap[event]) => boolean | void + } + + /// Called when the editor receives a `keydown` event. + handleKeyDown?: (this: P, view: EditorView, event: KeyboardEvent) => boolean | void + + /// Handler for `keypress` events. + handleKeyPress?: (this: P, view: EditorView, event: KeyboardEvent) => boolean | void + + /// Whenever the user directly input text, this handler is called + /// before the input is applied. If it returns `true`, the default + /// behavior of actually inserting the text is suppressed. + handleTextInput?: (this: P, view: EditorView, from: number, to: number, text: string, deflt: () => Transaction) => boolean | void + + /// Called for each node around a click, from the inside out. The + /// `direct` flag will be true for the inner node. + handleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void + + /// Called when the editor is clicked, after `handleClickOn` handlers + /// have been called. + handleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void + + /// Called for each node around a double click. + handleDoubleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void + + /// Called when the editor is double-clicked, after `handleDoubleClickOn`. + handleDoubleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void + + /// Called for each node around a triple click. + handleTripleClickOn?: (this: P, view: EditorView, pos: number, node: Node, nodePos: number, event: MouseEvent, direct: boolean) => boolean | void + + /// Called when the editor is triple-clicked, after `handleTripleClickOn`. + handleTripleClick?: (this: P, view: EditorView, pos: number, event: MouseEvent) => boolean | void + + /// Can be used to override the behavior of pasting. `slice` is the + /// pasted content parsed by the editor, but you can directly access + /// the event to get at the raw content. + handlePaste?: (this: P, view: EditorView, event: ClipboardEvent, slice: Slice) => boolean | void + + /// Called when something is dropped on the editor. `moved` will be + /// true if this drop moves from the current selection (which should + /// thus be deleted). + handleDrop?: (this: P, view: EditorView, event: DragEvent, slice: Slice, moved: boolean) => boolean | void + + /// Called when the view, after updating its state, tries to scroll + /// the selection into view. A handler function may return false to + /// indicate that it did not handle the scrolling and further + /// handlers or the default behavior should be tried. + handleScrollToSelection?: (this: P, view: EditorView) => boolean + + /// Determines whether an in-editor drag event should copy or move + /// the selection. When not given, the event's `altKey` property is + /// used on macOS, `ctrlKey` on other platforms. + dragCopies?: (event: DragEvent) => boolean + + /// Can be used to override the way a selection is created when + /// reading a DOM selection between the given anchor and head. + createSelectionBetween?: (this: P, view: EditorView, anchor: ResolvedPos, head: ResolvedPos) => Selection | null + + /// The [parser](#model.DOMParser) to use when reading editor changes + /// from the DOM. Defaults to calling + /// [`DOMParser.fromSchema`](#model.DOMParser^fromSchema) on the + /// editor's schema. + domParser?: DOMParser + + /// Can be used to transform pasted HTML text, _before_ it is parsed, + /// for example to clean it up. + transformPastedHTML?: (this: P, html: string, view: EditorView) => string + + /// The [parser](#model.DOMParser) to use when reading content from + /// the clipboard. When not given, the value of the + /// [`domParser`](#view.EditorProps.domParser) prop is used. + clipboardParser?: DOMParser + + /// Transform pasted plain text. The `plain` flag will be true when + /// the text is pasted as plain text. + transformPastedText?: (this: P, text: string, plain: boolean, view: EditorView) => string + + /// A function to parse text from the clipboard into a document + /// slice. Called after + /// [`transformPastedText`](#view.EditorProps.transformPastedText). + /// The default behavior is to split the text into lines, wrap them + /// in `<p>` tags, and call + /// [`clipboardParser`](#view.EditorProps.clipboardParser) on it. + /// The `plain` flag will be true when the text is pasted as plain text. + clipboardTextParser?: (this: P, text: string, $context: ResolvedPos, plain: boolean, view: EditorView) => Slice + + /// Can be used to transform pasted or dragged-and-dropped content + /// before it is applied to the document. The `plain` flag will be + /// true when the text is pasted as plain text. + transformPasted?: (this: P, slice: Slice, view: EditorView, plain: boolean) => Slice + + /// Can be used to transform copied or cut content before it is + /// serialized to the clipboard. + transformCopied?: (this: P, slice: Slice, view: EditorView) => Slice + + /// Allows you to pass custom rendering and behavior logic for + /// nodes. Should map node names to constructor functions that + /// produce a [`NodeView`](#view.NodeView) object implementing the + /// node's display behavior. The third argument `getPos` is a + /// function that can be called to get the node's current position, + /// which can be useful when creating transactions to update it. + /// Note that if the node is not in the document, the position + /// returned by this function will be `undefined`. + /// + /// `decorations` is an array of node or inline decorations that are + /// active around the node. They are automatically drawn in the + /// normal way, and you will usually just want to ignore this, but + /// they can also be used as a way to provide context information to + /// the node view without adding it to the document itself. + /// + /// `innerDecorations` holds the decorations for the node's content. + /// You can safely ignore this if your view has no content or a + /// `contentDOM` property, since the editor will draw the decorations + /// on the content. But if you, for example, want to create a nested + /// editor with the content, it may make sense to provide it with the + /// inner decorations. + /// + /// (For backwards compatibility reasons, [mark + /// views](#view.EditorProps.markViews) can also be included in this + /// object.) + nodeViews?: {[node: string]: NodeViewConstructor} + + /// Pass custom mark rendering functions. Note that these cannot + /// provide the kind of dynamic behavior that [node + /// views](#view.NodeView) can—they just provide custom rendering + /// logic. The third argument indicates whether the mark's content + /// is inline. + markViews?: {[mark: string]: MarkViewConstructor} + + /// The DOM serializer to use when putting content onto the + /// clipboard. If not given, the result of + /// [`DOMSerializer.fromSchema`](#model.DOMSerializer^fromSchema) + /// will be used. This object will only have its + /// [`serializeFragment`](#model.DOMSerializer.serializeFragment) + /// method called, and you may provide an alternative object type + /// implementing a compatible method. + clipboardSerializer?: DOMSerializer + + /// A function that will be called to get the text for the current + /// selection when copying text to the clipboard. By default, the + /// editor will use [`textBetween`](#model.Node.textBetween) on the + /// selected range. + clipboardTextSerializer?: (this: P, content: Slice, view: EditorView) => string + + /// A set of [document decorations](#view.Decoration) to show in the + /// view. + decorations?: (this: P, state: EditorState) => DecorationSource | null | undefined + + /// When this returns false, the content of the view is not directly + /// editable. + editable?: (this: P, state: EditorState) => boolean + + /// Control the DOM attributes of the editable element. May be either + /// an object or a function going from an editor state to an object. + /// By default, the element will get a class `"ProseMirror"`, and + /// will have its `contentEditable` attribute determined by the + /// [`editable` prop](#view.EditorProps.editable). Additional classes + /// provided here will be added to the class. For other attributes, + /// the value provided first (as in + /// [`someProp`](#view.EditorView.someProp)) will be used. + attributes?: {[name: string]: string} | ((state: EditorState) => {[name: string]: string}) + + /// Determines the distance (in pixels) between the cursor and the + /// end of the visible viewport at which point, when scrolling the + /// cursor into view, scrolling takes place. Defaults to 0. + scrollThreshold?: number | {top: number, right: number, bottom: number, left: number} + + /// Determines the extra space (in pixels) that is left above or + /// below the cursor when it is scrolled into view. Defaults to 5. + scrollMargin?: number | {top: number, right: number, bottom: number, left: number} +} + +/// The props object given directly to the editor view supports some +/// fields that can't be used in plugins: +export interface DirectEditorProps extends EditorProps { + /// The current state of the editor. + state: EditorState + + /// A set of plugins to use in the view, applying their [plugin + /// view](#state.PluginSpec.view) and + /// [props](#state.PluginSpec.props). Passing plugins with a state + /// component (a [state field](#state.PluginSpec.state) field or a + /// [transaction](#state.PluginSpec.filterTransaction) filter or + /// appender) will result in an error, since such plugins must be + /// present in the state to work. + plugins?: readonly Plugin[] + + /// The callback over which to send transactions (state updates) + /// produced by the view. If you specify this, you probably want to + /// make sure this ends up calling the view's + /// [`updateState`](#view.EditorView.updateState) method with a new + /// state that has the transaction + /// [applied](#state.EditorState.apply). The callback will be bound to have + /// the view instance as its `this` binding. + dispatchTransaction?: (tr: Transaction) => void +} diff --git a/third_party/js/prosemirror/prosemirror-view/src/input.ts b/third_party/js/prosemirror/prosemirror-view/src/input.ts @@ -0,0 +1,828 @@ +import {Selection, NodeSelection, TextSelection} from "prosemirror-state" +import {dropPoint} from "prosemirror-transform" +import {Slice, Node} from "prosemirror-model" + +import * as browser from "./browser" +import {captureKeyDown} from "./capturekeys" +import {parseFromClipboard, serializeForClipboard} from "./clipboard" +import {selectionBetween, selectionToDOM, selectionFromDOM} from "./selection" +import {keyEvent, DOMNode, textNodeBefore, textNodeAfter} from "./dom" +import {EditorView} from "./index" +import {ViewDesc} from "./viewdesc" + +// A collection of DOM events that occur within the editor, and callback functions +// to invoke when the event fires. +const handlers: {[event: string]: (view: EditorView, event: Event) => void} = {} +const editHandlers: {[event: string]: (view: EditorView, event: Event) => void} = {} +const passiveHandlers: Record<string, boolean> = {touchstart: true, touchmove: true} + +export class InputState { + shiftKey = false + mouseDown: MouseDown | null = null + lastKeyCode: number | null = null + lastKeyCodeTime = 0 + lastClick = {time: 0, x: 0, y: 0, type: "", button: 0} + lastSelectionOrigin: string | null = null + lastSelectionTime = 0 + lastIOSEnter = 0 + lastIOSEnterFallbackTimeout = -1 + lastFocus = 0 + lastTouch = 0 + lastChromeDelete = 0 + composing = false + compositionNode: Text | null = null + composingTimeout = -1 + compositionNodes: ViewDesc[] = [] + compositionEndedAt = -2e8 + compositionID = 1 + // Set to a composition ID when there are pending changes at compositionend + compositionPendingChanges = 0 + domChangeCount = 0 + eventHandlers: {[event: string]: (event: Event) => void} = Object.create(null) + hideSelectionGuard: (() => void) | null = null +} + +export function initInput(view: EditorView) { + for (let event in handlers) { + let handler = handlers[event] + view.dom.addEventListener(event, view.input.eventHandlers[event] = (event: Event) => { + if (eventBelongsToView(view, event) && !runCustomHandler(view, event) && + (view.editable || !(event.type in editHandlers))) + handler(view, event) + }, passiveHandlers[event] ? {passive: true} : undefined) + } + // On Safari, for reasons beyond my understanding, adding an input + // event handler makes an issue where the composition vanishes when + // you press enter go away. + if (browser.safari) view.dom.addEventListener("input", () => null) + + ensureListeners(view) +} + +function setSelectionOrigin(view: EditorView, origin: string) { + view.input.lastSelectionOrigin = origin + view.input.lastSelectionTime = Date.now() +} + +export function destroyInput(view: EditorView) { + view.domObserver.stop() + for (let type in view.input.eventHandlers) + view.dom.removeEventListener(type, view.input.eventHandlers[type]) + clearTimeout(view.input.composingTimeout) + clearTimeout(view.input.lastIOSEnterFallbackTimeout) +} + +export function ensureListeners(view: EditorView) { + view.someProp("handleDOMEvents", currentHandlers => { + for (let type in currentHandlers) if (!view.input.eventHandlers[type]) + view.dom.addEventListener(type, view.input.eventHandlers[type] = event => runCustomHandler(view, event)) + }) +} + +function runCustomHandler(view: EditorView, event: Event) { + return view.someProp("handleDOMEvents", handlers => { + let handler = handlers[event.type] + return handler ? handler(view, event) || event.defaultPrevented : false + }) +} + +function eventBelongsToView(view: EditorView, event: Event) { + if (!event.bubbles) return true + if (event.defaultPrevented) return false + for (let node = event.target as DOMNode; node != view.dom; node = node.parentNode!) + if (!node || node.nodeType == 11 || + (node.pmViewDesc && node.pmViewDesc.stopEvent(event))) + return false + return true +} + +export function dispatchEvent(view: EditorView, event: Event) { + if (!runCustomHandler(view, event) && handlers[event.type] && + (view.editable || !(event.type in editHandlers))) + handlers[event.type](view, event) +} + +editHandlers.keydown = (view: EditorView, _event: Event) => { + let event = _event as KeyboardEvent + view.input.shiftKey = event.keyCode == 16 || event.shiftKey + if (inOrNearComposition(view, event)) return + view.input.lastKeyCode = event.keyCode + view.input.lastKeyCodeTime = Date.now() + // Suppress enter key events on Chrome Android, because those tend + // to be part of a confused sequence of composition events fired, + // and handling them eagerly tends to corrupt the input. + if (browser.android && browser.chrome && event.keyCode == 13) return + if (event.keyCode != 229) view.domObserver.forceFlush() + + // On iOS, if we preventDefault enter key presses, the virtual + // keyboard gets confused. So the hack here is to set a flag that + // makes the DOM change code recognize that what just happens should + // be replaced by whatever the Enter key handlers do. + if (browser.ios && event.keyCode == 13 && !event.ctrlKey && !event.altKey && !event.metaKey) { + let now = Date.now() + view.input.lastIOSEnter = now + view.input.lastIOSEnterFallbackTimeout = setTimeout(() => { + if (view.input.lastIOSEnter == now) { + view.someProp("handleKeyDown", f => f(view, keyEvent(13, "Enter"))) + view.input.lastIOSEnter = 0 + } + }, 200) + } else if (view.someProp("handleKeyDown", f => f(view, event)) || captureKeyDown(view, event)) { + event.preventDefault() + } else { + setSelectionOrigin(view, "key") + } +} + +editHandlers.keyup = (view, event) => { + if ((event as KeyboardEvent).keyCode == 16) view.input.shiftKey = false +} + +editHandlers.keypress = (view, _event) => { + let event = _event as KeyboardEvent + if (inOrNearComposition(view, event) || !event.charCode || + event.ctrlKey && !event.altKey || browser.mac && event.metaKey) return + + if (view.someProp("handleKeyPress", f => f(view, event))) { + event.preventDefault() + return + } + + let sel = view.state.selection + if (!(sel instanceof TextSelection) || !sel.$from.sameParent(sel.$to)) { + let text = String.fromCharCode(event.charCode) + let deflt = () => view.state.tr.insertText(text).scrollIntoView() + if (!/[\r\n]/.test(text) && !view.someProp("handleTextInput", f => f(view, sel.$from.pos, sel.$to.pos, text, deflt))) + view.dispatch(deflt()) + event.preventDefault() + } +} + +function eventCoords(event: MouseEvent) { return {left: event.clientX, top: event.clientY} } + +function isNear(event: MouseEvent, click: {x: number, y: number}) { + let dx = click.x - event.clientX, dy = click.y - event.clientY + return dx * dx + dy * dy < 100 +} + +function runHandlerOnContext( + view: EditorView, + propName: "handleClickOn" | "handleDoubleClickOn" | "handleTripleClickOn", + pos: number, + inside: number, + event: MouseEvent +) { + if (inside == -1) return false + let $pos = view.state.doc.resolve(inside) + for (let i = $pos.depth + 1; i > 0; i--) { + if (view.someProp(propName, f => i > $pos.depth ? f(view, pos, $pos.nodeAfter!, $pos.before(i), event, true) + : f(view, pos, $pos.node(i), $pos.before(i), event, false))) + return true + } + return false +} + +function updateSelection(view: EditorView, selection: Selection, origin: string) { + if (!view.focused) view.focus() + if (view.state.selection.eq(selection)) return + let tr = view.state.tr.setSelection(selection) + if (origin == "pointer") tr.setMeta("pointer", true) + view.dispatch(tr) +} + +function selectClickedLeaf(view: EditorView, inside: number) { + if (inside == -1) return false + let $pos = view.state.doc.resolve(inside), node = $pos.nodeAfter + if (node && node.isAtom && NodeSelection.isSelectable(node)) { + updateSelection(view, new NodeSelection($pos), "pointer") + return true + } + return false +} + +function selectClickedNode(view: EditorView, inside: number) { + if (inside == -1) return false + let sel = view.state.selection, selectedNode, selectAt + if (sel instanceof NodeSelection) selectedNode = sel.node + + let $pos = view.state.doc.resolve(inside) + for (let i = $pos.depth + 1; i > 0; i--) { + let node = i > $pos.depth ? $pos.nodeAfter! : $pos.node(i) + if (NodeSelection.isSelectable(node)) { + if (selectedNode && sel.$from.depth > 0 && + i >= sel.$from.depth && $pos.before(sel.$from.depth + 1) == sel.$from.pos) + selectAt = $pos.before(sel.$from.depth) + else + selectAt = $pos.before(i) + break + } + } + + if (selectAt != null) { + updateSelection(view, NodeSelection.create(view.state.doc, selectAt), "pointer") + return true + } else { + return false + } +} + +function handleSingleClick(view: EditorView, pos: number, inside: number, event: MouseEvent, selectNode: boolean) { + return runHandlerOnContext(view, "handleClickOn", pos, inside, event) || + view.someProp("handleClick", f => f(view, pos, event)) || + (selectNode ? selectClickedNode(view, inside) : selectClickedLeaf(view, inside)) +} + +function handleDoubleClick(view: EditorView, pos: number, inside: number, event: MouseEvent) { + return runHandlerOnContext(view, "handleDoubleClickOn", pos, inside, event) || + view.someProp("handleDoubleClick", f => f(view, pos, event)) +} + +function handleTripleClick(view: EditorView, pos: number, inside: number, event: MouseEvent) { + return runHandlerOnContext(view, "handleTripleClickOn", pos, inside, event) || + view.someProp("handleTripleClick", f => f(view, pos, event)) || + defaultTripleClick(view, inside, event) +} + +function defaultTripleClick(view: EditorView, inside: number, event: MouseEvent) { + if (event.button != 0) return false + let doc = view.state.doc + if (inside == -1) { + if (doc.inlineContent) { + updateSelection(view, TextSelection.create(doc, 0, doc.content.size), "pointer") + return true + } + return false + } + + let $pos = doc.resolve(inside) + for (let i = $pos.depth + 1; i > 0; i--) { + let node = i > $pos.depth ? $pos.nodeAfter! : $pos.node(i) + let nodePos = $pos.before(i) + if (node.inlineContent) + updateSelection(view, TextSelection.create(doc, nodePos + 1, nodePos + 1 + node.content.size), "pointer") + else if (NodeSelection.isSelectable(node)) + updateSelection(view, NodeSelection.create(doc, nodePos), "pointer") + else + continue + return true + } +} + +function forceDOMFlush(view: EditorView) { + return endComposition(view) +} + +const selectNodeModifier: keyof MouseEvent = browser.mac ? "metaKey" : "ctrlKey" + +handlers.mousedown = (view, _event) => { + let event = _event as MouseEvent + view.input.shiftKey = event.shiftKey + let flushed = forceDOMFlush(view) + let now = Date.now(), type = "singleClick" + if (now - view.input.lastClick.time < 500 && isNear(event, view.input.lastClick) && !event[selectNodeModifier] && + view.input.lastClick.button == event.button) { + if (view.input.lastClick.type == "singleClick") type = "doubleClick" + else if (view.input.lastClick.type == "doubleClick") type = "tripleClick" + } + view.input.lastClick = {time: now, x: event.clientX, y: event.clientY, type, button: event.button} + + let pos = view.posAtCoords(eventCoords(event)) + if (!pos) return + + if (type == "singleClick") { + if (view.input.mouseDown) view.input.mouseDown.done() + view.input.mouseDown = new MouseDown(view, pos, event, !!flushed) + } else if ((type == "doubleClick" ? handleDoubleClick : handleTripleClick)(view, pos.pos, pos.inside, event)) { + event.preventDefault() + } else { + setSelectionOrigin(view, "pointer") + } +} + +class MouseDown { + startDoc: Node + selectNode: boolean + allowDefault: boolean + delayedSelectionSync = false + mightDrag: {node: Node, pos: number, addAttr: boolean, setUneditable: boolean} | null = null + target: HTMLElement | null + + constructor( + readonly view: EditorView, + readonly pos: {pos: number, inside: number}, + readonly event: MouseEvent, + readonly flushed: boolean + ) { + this.startDoc = view.state.doc + this.selectNode = !!event[selectNodeModifier] + this.allowDefault = event.shiftKey + + let targetNode: Node, targetPos + if (pos.inside > -1) { + targetNode = view.state.doc.nodeAt(pos.inside)! + targetPos = pos.inside + } else { + let $pos = view.state.doc.resolve(pos.pos) + targetNode = $pos.parent + targetPos = $pos.depth ? $pos.before() : 0 + } + + const target = flushed ? null : event.target as HTMLElement + const targetDesc = target ? view.docView.nearestDesc(target, true) : null + this.target = targetDesc && targetDesc.nodeDOM.nodeType == 1 ? targetDesc.nodeDOM as HTMLElement : null + + let {selection} = view.state + if (event.button == 0 && + targetNode.type.spec.draggable && targetNode.type.spec.selectable !== false || + selection instanceof NodeSelection && selection.from <= targetPos && selection.to > targetPos) + this.mightDrag = { + node: targetNode, + pos: targetPos, + addAttr: !!(this.target && !this.target.draggable), + setUneditable: !!(this.target && browser.gecko && !this.target.hasAttribute("contentEditable")) + } + + if (this.target && this.mightDrag && (this.mightDrag.addAttr || this.mightDrag.setUneditable)) { + this.view.domObserver.stop() + if (this.mightDrag.addAttr) this.target.draggable = true + if (this.mightDrag.setUneditable) + setTimeout(() => { + if (this.view.input.mouseDown == this) this.target!.setAttribute("contentEditable", "false") + }, 20) + this.view.domObserver.start() + } + + view.root.addEventListener("mouseup", this.up = this.up.bind(this) as any) + view.root.addEventListener("mousemove", this.move = this.move.bind(this) as any) + setSelectionOrigin(view, "pointer") + } + + done() { + this.view.root.removeEventListener("mouseup", this.up as any) + this.view.root.removeEventListener("mousemove", this.move as any) + if (this.mightDrag && this.target) { + this.view.domObserver.stop() + if (this.mightDrag.addAttr) this.target.removeAttribute("draggable") + if (this.mightDrag.setUneditable) this.target.removeAttribute("contentEditable") + this.view.domObserver.start() + } + if (this.delayedSelectionSync) setTimeout(() => selectionToDOM(this.view)) + this.view.input.mouseDown = null + } + + up(event: MouseEvent) { + this.done() + + if (!this.view.dom.contains(event.target as HTMLElement)) + return + + let pos: {pos: number, inside: number} | null = this.pos + if (this.view.state.doc != this.startDoc) pos = this.view.posAtCoords(eventCoords(event)) + + this.updateAllowDefault(event) + if (this.allowDefault || !pos) { + setSelectionOrigin(this.view, "pointer") + } else if (handleSingleClick(this.view, pos.pos, pos.inside, event, this.selectNode)) { + event.preventDefault() + } else if (event.button == 0 && + (this.flushed || + // Safari ignores clicks on draggable elements + (browser.safari && this.mightDrag && !this.mightDrag.node.isAtom) || + // Chrome will sometimes treat a node selection as a + // cursor, but still report that the node is selected + // when asked through getSelection. You'll then get a + // situation where clicking at the point where that + // (hidden) cursor is doesn't change the selection, and + // thus doesn't get a reaction from ProseMirror. This + // works around that. + (browser.chrome && !this.view.state.selection.visible && + Math.min(Math.abs(pos.pos - this.view.state.selection.from), + Math.abs(pos.pos - this.view.state.selection.to)) <= 2))) { + updateSelection(this.view, Selection.near(this.view.state.doc.resolve(pos.pos)), "pointer") + event.preventDefault() + } else { + setSelectionOrigin(this.view, "pointer") + } + } + + move(event: MouseEvent) { + this.updateAllowDefault(event) + setSelectionOrigin(this.view, "pointer") + if (event.buttons == 0) this.done() + } + + updateAllowDefault(event: MouseEvent) { + if (!this.allowDefault && (Math.abs(this.event.x - event.clientX) > 4 || + Math.abs(this.event.y - event.clientY) > 4)) + this.allowDefault = true + } +} + +handlers.touchstart = view => { + view.input.lastTouch = Date.now() + forceDOMFlush(view) + setSelectionOrigin(view, "pointer") +} + +handlers.touchmove = view => { + view.input.lastTouch = Date.now() + setSelectionOrigin(view, "pointer") +} + +handlers.contextmenu = view => forceDOMFlush(view) + +function inOrNearComposition(view: EditorView, event: Event) { + if (view.composing) return true + // See https://www.stum.de/2016/06/24/handling-ime-events-in-javascript/. + // On Japanese input method editors (IMEs), the Enter key is used to confirm character + // selection. On Safari, when Enter is pressed, compositionend and keydown events are + // emitted. The keydown event triggers newline insertion, which we don't want. + // This method returns true if the keydown event should be ignored. + // We only ignore it once, as pressing Enter a second time *should* insert a newline. + // Furthermore, the keydown event timestamp must be close to the compositionEndedAt timestamp. + // This guards against the case where compositionend is triggered without the keyboard + // (e.g. character confirmation may be done with the mouse), and keydown is triggered + // afterwards- we wouldn't want to ignore the keydown event in this case. + if (browser.safari && Math.abs(event.timeStamp - view.input.compositionEndedAt) < 500) { + view.input.compositionEndedAt = -2e8 + return true + } + return false +} + +// Drop active composition after 5 seconds of inactivity on Android +const timeoutComposition = browser.android ? 5000 : -1 + +editHandlers.compositionstart = editHandlers.compositionupdate = view => { + if (!view.composing) { + view.domObserver.flush() + let {state} = view, $pos = state.selection.$to + if (state.selection instanceof TextSelection && + (state.storedMarks || + (!$pos.textOffset && $pos.parentOffset && $pos.nodeBefore!.marks.some(m => m.type.spec.inclusive === false)) || + browser.chrome && browser.windows && selectionBeforeUneditable(view))) { // Issue #1500 + // Need to wrap the cursor in mark nodes different from the ones in the DOM context + view.markCursor = view.state.storedMarks || $pos.marks() + endComposition(view, true) + view.markCursor = null + } else { + endComposition(view, !state.selection.empty) + // In firefox, if the cursor is after but outside a marked node, + // the inserted text won't inherit the marks. So this moves it + // inside if necessary. + if (browser.gecko && state.selection.empty && $pos.parentOffset && !$pos.textOffset && $pos.nodeBefore!.marks.length) { + let sel = view.domSelectionRange() + for (let node = sel.focusNode, offset = sel.focusOffset; node && node.nodeType == 1 && offset != 0;) { + let before = offset < 0 ? node.lastChild : node.childNodes[offset - 1] + if (!before) break + if (before.nodeType == 3) { + let sel = view.domSelection() + if (sel) sel.collapse(before, before.nodeValue!.length) + break + } else { + node = before + offset = -1 + } + } + } + } + view.input.composing = true + } + scheduleComposeEnd(view, timeoutComposition) +} + +function selectionBeforeUneditable(view: EditorView) { + let {focusNode, focusOffset} = view.domSelectionRange() + if (!focusNode || focusNode.nodeType != 1 || focusOffset >= focusNode.childNodes.length) return false + let next = focusNode.childNodes[focusOffset] + return next.nodeType == 1 && (next as HTMLElement).contentEditable == "false" +} + +editHandlers.compositionend = (view, event) => { + if (view.composing) { + view.input.composing = false + view.input.compositionEndedAt = event.timeStamp + view.input.compositionPendingChanges = view.domObserver.pendingRecords().length ? view.input.compositionID : 0 + view.input.compositionNode = null + if (view.input.compositionPendingChanges) Promise.resolve().then(() => view.domObserver.flush()) + view.input.compositionID++ + scheduleComposeEnd(view, 20) + } +} + +function scheduleComposeEnd(view: EditorView, delay: number) { + clearTimeout(view.input.composingTimeout) + if (delay > -1) view.input.composingTimeout = setTimeout(() => endComposition(view), delay) +} + +export function clearComposition(view: EditorView) { + if (view.composing) { + view.input.composing = false + view.input.compositionEndedAt = timestampFromCustomEvent() + } + while (view.input.compositionNodes.length > 0) view.input.compositionNodes.pop()!.markParentsDirty() +} + +export function findCompositionNode(view: EditorView) { + let sel = view.domSelectionRange() + if (!sel.focusNode) return null + let textBefore = textNodeBefore(sel.focusNode, sel.focusOffset) + let textAfter = textNodeAfter(sel.focusNode, sel.focusOffset) + if (textBefore && textAfter && textBefore != textAfter) { + let descAfter = textAfter.pmViewDesc, lastChanged = view.domObserver.lastChangedTextNode + if (textBefore == lastChanged || textAfter == lastChanged) return lastChanged + if (!descAfter || !descAfter.isText(textAfter.nodeValue!)) { + return textAfter + } else if (view.input.compositionNode == textAfter) { + let descBefore = textBefore.pmViewDesc + if (!(!descBefore || !descBefore.isText(textBefore.nodeValue!))) + return textAfter + } + } + return textBefore || textAfter +} + +function timestampFromCustomEvent() { + let event = document.createEvent("Event") + event.initEvent("event", true, true) + return event.timeStamp +} + +/// @internal +export function endComposition(view: EditorView, restarting = false) { + if (browser.android && view.domObserver.flushingSoon >= 0) return + view.domObserver.forceFlush() + clearComposition(view) + if (restarting || view.docView && view.docView.dirty) { + let sel = selectionFromDOM(view), cur = view.state.selection + if (sel && !sel.eq(cur)) view.dispatch(view.state.tr.setSelection(sel)) + else if ((view.markCursor || restarting) && !cur.$from.node(cur.$from.sharedDepth(cur.to)).inlineContent) view.dispatch(view.state.tr.deleteSelection()) + else view.updateState(view.state) + return true + } + return false +} + +function captureCopy(view: EditorView, dom: HTMLElement) { + // The extra wrapper is somehow necessary on IE/Edge to prevent the + // content from being mangled when it is put onto the clipboard + if (!view.dom.parentNode) return + let wrap = view.dom.parentNode.appendChild(document.createElement("div")) + wrap.appendChild(dom) + wrap.style.cssText = "position: fixed; left: -10000px; top: 10px" + let sel = getSelection()!, range = document.createRange() + range.selectNodeContents(dom) + // Done because IE will fire a selectionchange moving the selection + // to its start when removeAllRanges is called and the editor still + // has focus (which will mess up the editor's selection state). + view.dom.blur() + sel.removeAllRanges() + sel.addRange(range) + setTimeout(() => { + if (wrap.parentNode) wrap.parentNode.removeChild(wrap) + view.focus() + }, 50) +} + +// This is very crude, but unfortunately both these browsers _pretend_ +// that they have a clipboard API—all the objects and methods are +// there, they just don't work, and they are hard to test. +const brokenClipboardAPI = (browser.ie && browser.ie_version < 15) || + (browser.ios && browser.webkit_version < 604) + +handlers.copy = editHandlers.cut = (view, _event) => { + let event = _event as ClipboardEvent + let sel = view.state.selection, cut = event.type == "cut" + if (sel.empty) return + + // IE and Edge's clipboard interface is completely broken + let data = brokenClipboardAPI ? null : event.clipboardData + let slice = sel.content(), {dom, text} = serializeForClipboard(view, slice) + if (data) { + event.preventDefault() + data.clearData() + data.setData("text/html", dom.innerHTML) + data.setData("text/plain", text) + } else { + captureCopy(view, dom) + } + if (cut) view.dispatch(view.state.tr.deleteSelection().scrollIntoView().setMeta("uiEvent", "cut")) +} + +function sliceSingleNode(slice: Slice) { + return slice.openStart == 0 && slice.openEnd == 0 && slice.content.childCount == 1 ? slice.content.firstChild : null +} + +function capturePaste(view: EditorView, event: ClipboardEvent) { + if (!view.dom.parentNode) return + let plainText = view.input.shiftKey || view.state.selection.$from.parent.type.spec.code + let target = view.dom.parentNode.appendChild(document.createElement(plainText ? "textarea" : "div")) + if (!plainText) target.contentEditable = "true" + target.style.cssText = "position: fixed; left: -10000px; top: 10px" + target.focus() + let plain = view.input.shiftKey && view.input.lastKeyCode != 45 + setTimeout(() => { + view.focus() + if (target.parentNode) target.parentNode.removeChild(target) + if (plainText) doPaste(view, (target as HTMLTextAreaElement).value, null, plain, event) + else doPaste(view, target.textContent!, target.innerHTML, plain, event) + }, 50) +} + +export function doPaste(view: EditorView, text: string, html: string | null, preferPlain: boolean, event: ClipboardEvent) { + let slice = parseFromClipboard(view, text, html, preferPlain, view.state.selection.$from) + if (view.someProp("handlePaste", f => f(view, event, slice || Slice.empty))) return true + if (!slice) return false + + let singleNode = sliceSingleNode(slice) + let tr = singleNode + ? view.state.tr.replaceSelectionWith(singleNode, preferPlain) + : view.state.tr.replaceSelection(slice) + view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")) + return true +} + +function getText(clipboardData: DataTransfer) { + let text = clipboardData.getData("text/plain") || clipboardData.getData("Text") + if (text) return text + let uris = clipboardData.getData("text/uri-list") + return uris ? uris.replace(/\r?\n/g, " ") : "" +} + +editHandlers.paste = (view, _event) => { + let event = _event as ClipboardEvent + // Handling paste from JavaScript during composition is very poorly + // handled by browsers, so as a dodgy but preferable kludge, we just + // let the browser do its native thing there, except on Android, + // where the editor is almost always composing. + if (view.composing && !browser.android) return + let data = brokenClipboardAPI ? null : event.clipboardData + let plain = view.input.shiftKey && view.input.lastKeyCode != 45 + if (data && doPaste(view, getText(data), data.getData("text/html"), plain, event)) + event.preventDefault() + else + capturePaste(view, event) +} + +export class Dragging { + constructor(readonly slice: Slice, readonly move: boolean, readonly node?: NodeSelection) {} +} + +const dragCopyModifier: keyof DragEvent = browser.mac ? "altKey" : "ctrlKey" + +function dragMoves(view: EditorView, event: DragEvent) { + let moves = view.someProp("dragCopies", test => !test(event)) + return moves != null ? moves : !event[dragCopyModifier] +} + +handlers.dragstart = (view, _event) => { + let event = _event as DragEvent + let mouseDown = view.input.mouseDown + if (mouseDown) mouseDown.done() + if (!event.dataTransfer) return + + let sel = view.state.selection + let pos = sel.empty ? null : view.posAtCoords(eventCoords(event)) + let node: undefined | NodeSelection + if (pos && pos.pos >= sel.from && pos.pos <= (sel instanceof NodeSelection ? sel.to - 1: sel.to)) { + // In selection + } else if (mouseDown && mouseDown.mightDrag) { + node = NodeSelection.create(view.state.doc, mouseDown.mightDrag.pos) + } else if (event.target && (event.target as HTMLElement).nodeType == 1) { + let desc = view.docView.nearestDesc(event.target as HTMLElement, true) + if (desc && desc.node.type.spec.draggable && desc != view.docView) + node = NodeSelection.create(view.state.doc, desc.posBefore) + } + let draggedSlice = (node || view.state.selection).content() + let {dom, text, slice} = serializeForClipboard(view, draggedSlice) + // Pre-120 Chrome versions clear files when calling `clearData` (#1472) + if (!event.dataTransfer.files.length || !browser.chrome || browser.chrome_version > 120) + event.dataTransfer.clearData() + event.dataTransfer.setData(brokenClipboardAPI ? "Text" : "text/html", dom.innerHTML) + // See https://github.com/ProseMirror/prosemirror/issues/1156 + event.dataTransfer.effectAllowed = "copyMove" + if (!brokenClipboardAPI) event.dataTransfer.setData("text/plain", text) + view.dragging = new Dragging(slice, dragMoves(view, event), node) +} + +handlers.dragend = view => { + let dragging = view.dragging + window.setTimeout(() => { + if (view.dragging == dragging) view.dragging = null + }, 50) +} + +editHandlers.dragover = editHandlers.dragenter = (_, e) => e.preventDefault() + +editHandlers.drop = (view, event) => { + try { + handleDrop(view, event as DragEvent, view.dragging) + } finally { + view.dragging = null + } +} + +function handleDrop(view: EditorView, event: DragEvent, dragging: Dragging | null) { + if (!event.dataTransfer) return + + let eventPos = view.posAtCoords(eventCoords(event)) + if (!eventPos) return + let $mouse = view.state.doc.resolve(eventPos.pos) + let slice = dragging && dragging.slice + if (slice) { + view.someProp("transformPasted", f => { slice = f(slice!, view, false) }) + } else { + slice = parseFromClipboard(view, getText(event.dataTransfer), + brokenClipboardAPI ? null : event.dataTransfer.getData("text/html"), false, $mouse) + } + let move = !!(dragging && dragMoves(view, event)) + if (view.someProp("handleDrop", f => f(view, event, slice || Slice.empty, move))) { + event.preventDefault() + return + } + if (!slice) return + + event.preventDefault() + let insertPos = slice ? dropPoint(view.state.doc, $mouse.pos, slice) : $mouse.pos + if (insertPos == null) insertPos = $mouse.pos + + let tr = view.state.tr + if (move) { + let {node} = dragging as Dragging + if (node) node.replace(tr) + else tr.deleteSelection() + } + + let pos = tr.mapping.map(insertPos) + let isNode = slice.openStart == 0 && slice.openEnd == 0 && slice.content.childCount == 1 + let beforeInsert = tr.doc + if (isNode) + tr.replaceRangeWith(pos, pos, slice.content.firstChild!) + else + tr.replaceRange(pos, pos, slice) + if (tr.doc.eq(beforeInsert)) return + + let $pos = tr.doc.resolve(pos) + if (isNode && NodeSelection.isSelectable(slice.content.firstChild!) && + $pos.nodeAfter && $pos.nodeAfter.sameMarkup(slice.content.firstChild!)) { + tr.setSelection(new NodeSelection($pos)) + } else { + let end = tr.mapping.map(insertPos) + tr.mapping.maps[tr.mapping.maps.length - 1].forEach((_from, _to, _newFrom, newTo) => end = newTo) + tr.setSelection(selectionBetween(view, $pos, tr.doc.resolve(end))) + } + view.focus() + view.dispatch(tr.setMeta("uiEvent", "drop")) +} + +handlers.focus = view => { + view.input.lastFocus = Date.now() + if (!view.focused) { + view.domObserver.stop() + view.dom.classList.add("ProseMirror-focused") + view.domObserver.start() + view.focused = true + setTimeout(() => { + if (view.docView && view.hasFocus() && !view.domObserver.currentSelection.eq(view.domSelectionRange())) + selectionToDOM(view) + }, 20) + } +} + +handlers.blur = (view, _event) => { + let event = _event as FocusEvent + if (view.focused) { + view.domObserver.stop() + view.dom.classList.remove("ProseMirror-focused") + view.domObserver.start() + if (event.relatedTarget && view.dom.contains(event.relatedTarget as HTMLElement)) + view.domObserver.currentSelection.clear() + view.focused = false + } +} + +handlers.beforeinput = (view, _event: Event) => { + let event = _event as InputEvent + // We should probably do more with beforeinput events, but support + // is so spotty that I'm still waiting to see where they are going. + + // Very specific hack to deal with backspace sometimes failing on + // Chrome Android when after an uneditable node. + if (browser.chrome && browser.android && event.inputType == "deleteContentBackward") { + view.domObserver.flushSoon() + let {domChangeCount} = view.input + setTimeout(() => { + if (view.input.domChangeCount != domChangeCount) return // Event already had some effect + // This bug tends to close the virtual keyboard, so we refocus + view.dom.blur() + view.focus() + if (view.someProp("handleKeyDown", f => f(view, keyEvent(8, "Backspace")))) return + let {$cursor} = view.state.selection as TextSelection + // Crude approximation of backspace behavior when no command handled it + if ($cursor && $cursor.pos > 0) view.dispatch(view.state.tr.delete($cursor.pos - 1, $cursor.pos).scrollIntoView()) + }, 50) + } +} + +// Make sure all handlers get registered +for (let prop in editHandlers) handlers[prop] = editHandlers[prop] diff --git a/third_party/js/prosemirror/prosemirror-view/src/selection.ts b/third_party/js/prosemirror/prosemirror-view/src/selection.ts @@ -0,0 +1,216 @@ +import {TextSelection, NodeSelection, Selection} from "prosemirror-state" +import {ResolvedPos} from "prosemirror-model" + +import * as browser from "./browser" +import {isEquivalentPosition, domIndex, isOnEdge, selectionCollapsed} from "./dom" +import {EditorView} from "./index" +import {NodeViewDesc} from "./viewdesc" + +export function selectionFromDOM(view: EditorView, origin: string | null = null) { + let domSel = view.domSelectionRange(), doc = view.state.doc + if (!domSel.focusNode) return null + let nearestDesc = view.docView.nearestDesc(domSel.focusNode), inWidget = nearestDesc && nearestDesc.size == 0 + let head = view.docView.posFromDOM(domSel.focusNode, domSel.focusOffset, 1) + if (head < 0) return null + let $head = doc.resolve(head), anchor, selection + if (selectionCollapsed(domSel)) { + anchor = head + while (nearestDesc && !nearestDesc.node) nearestDesc = nearestDesc.parent + let nearestDescNode = (nearestDesc as NodeViewDesc).node + if (nearestDesc && nearestDescNode.isAtom && NodeSelection.isSelectable(nearestDescNode) && nearestDesc.parent + && !(nearestDescNode.isInline && isOnEdge(domSel.focusNode, domSel.focusOffset, nearestDesc.dom))) { + let pos = nearestDesc.posBefore + selection = new NodeSelection(head == pos ? $head : doc.resolve(pos)) + } + } else { + if (domSel instanceof view.dom.ownerDocument.defaultView!.Selection && domSel.rangeCount > 1) { + let min = head, max = head + for (let i = 0; i < domSel.rangeCount; i++) { + let range = domSel.getRangeAt(i) + min = Math.min(min, view.docView.posFromDOM(range.startContainer, range.startOffset, 1)) + max = Math.max(max, view.docView.posFromDOM(range.endContainer, range.endOffset, -1)) + } + if (min < 0) return null + ;[anchor, head] = max == view.state.selection.anchor ? [max, min] : [min, max] + $head = doc.resolve(head) + } else { + anchor = view.docView.posFromDOM(domSel.anchorNode!, domSel.anchorOffset, 1) + } + if (anchor < 0) return null + } + let $anchor = doc.resolve(anchor) + + if (!selection) { + let bias = origin == "pointer" || (view.state.selection.head < $head.pos && !inWidget) ? 1 : -1 + selection = selectionBetween(view, $anchor, $head, bias) + } + return selection +} + +function editorOwnsSelection(view: EditorView) { + return view.editable ? view.hasFocus() : + hasSelection(view) && document.activeElement && document.activeElement.contains(view.dom) +} + +export function selectionToDOM(view: EditorView, force = false) { + let sel = view.state.selection + syncNodeSelection(view, sel) + + if (!editorOwnsSelection(view)) return + + // The delayed drag selection causes issues with Cell Selections + // in Safari. And the drag selection delay is to workarond issues + // which only present in Chrome. + if (!force && view.input.mouseDown && view.input.mouseDown.allowDefault && browser.chrome) { + let domSel = view.domSelectionRange(), curSel = view.domObserver.currentSelection + if (domSel.anchorNode && curSel.anchorNode && + isEquivalentPosition(domSel.anchorNode, domSel.anchorOffset, + curSel.anchorNode, curSel.anchorOffset)) { + view.input.mouseDown.delayedSelectionSync = true + view.domObserver.setCurSelection() + return + } + } + + view.domObserver.disconnectSelection() + + if (view.cursorWrapper) { + selectCursorWrapper(view) + } else { + let {anchor, head} = sel, resetEditableFrom, resetEditableTo + if (brokenSelectBetweenUneditable && !(sel instanceof TextSelection)) { + if (!sel.$from.parent.inlineContent) + resetEditableFrom = temporarilyEditableNear(view, sel.from) + if (!sel.empty && !sel.$from.parent.inlineContent) + resetEditableTo = temporarilyEditableNear(view, sel.to) + } + view.docView.setSelection(anchor, head, view, force) + if (brokenSelectBetweenUneditable) { + if (resetEditableFrom) resetEditable(resetEditableFrom) + if (resetEditableTo) resetEditable(resetEditableTo) + } + if (sel.visible) { + view.dom.classList.remove("ProseMirror-hideselection") + } else { + view.dom.classList.add("ProseMirror-hideselection") + if ("onselectionchange" in document) removeClassOnSelectionChange(view) + } + } + + view.domObserver.setCurSelection() + view.domObserver.connectSelection() +} + +// Kludge to work around Webkit not allowing a selection to start/end +// between non-editable block nodes. We briefly make something +// editable, set the selection, then set it uneditable again. + +const brokenSelectBetweenUneditable = browser.safari || browser.chrome && browser.chrome_version < 63 + +function temporarilyEditableNear(view: EditorView, pos: number) { + let {node, offset} = view.docView.domFromPos(pos, 0) + let after = offset < node.childNodes.length ? node.childNodes[offset] : null + let before = offset ? node.childNodes[offset - 1] : null + if (browser.safari && after && (after as HTMLElement).contentEditable == "false") return setEditable(after as HTMLElement) + if ((!after || (after as HTMLElement).contentEditable == "false") && + (!before || (before as HTMLElement).contentEditable == "false")) { + if (after) return setEditable(after as HTMLElement) + else if (before) return setEditable(before as HTMLElement) + } +} + +function setEditable(element: HTMLElement) { + element.contentEditable = "true" + if (browser.safari && element.draggable) { element.draggable = false; (element as any).wasDraggable = true } + return element +} + +function resetEditable(element: HTMLElement) { + element.contentEditable = "false" + if ((element as any).wasDraggable) { element.draggable = true; (element as any).wasDraggable = null } +} + +function removeClassOnSelectionChange(view: EditorView) { + let doc = view.dom.ownerDocument + doc.removeEventListener("selectionchange", view.input.hideSelectionGuard!) + let domSel = view.domSelectionRange() + let node = domSel.anchorNode, offset = domSel.anchorOffset + doc.addEventListener("selectionchange", view.input.hideSelectionGuard = () => { + if (domSel.anchorNode != node || domSel.anchorOffset != offset) { + doc.removeEventListener("selectionchange", view.input.hideSelectionGuard!) + setTimeout(() => { + if (!editorOwnsSelection(view) || view.state.selection.visible) + view.dom.classList.remove("ProseMirror-hideselection") + }, 20) + } + }) +} + +function selectCursorWrapper(view: EditorView) { + let domSel = view.domSelection() + if (!domSel) return + let node = view.cursorWrapper!.dom, img = node.nodeName == "IMG" + if (img) domSel.collapse(node.parentNode!, domIndex(node) + 1) + else domSel.collapse(node, 0) + // Kludge to kill 'control selection' in IE11 when selecting an + // invisible cursor wrapper, since that would result in those weird + // resize handles and a selection that considers the absolutely + // positioned wrapper, rather than the root editable node, the + // focused element. + if (!img && !view.state.selection.visible && browser.ie && browser.ie_version <= 11) { + ;(node as any).disabled = true + ;(node as any).disabled = false + } +} + +export function syncNodeSelection(view: EditorView, sel: Selection) { + if (sel instanceof NodeSelection) { + let desc = view.docView.descAt(sel.from) + if (desc != view.lastSelectedViewDesc) { + clearNodeSelection(view) + if (desc) (desc as NodeViewDesc).selectNode() + view.lastSelectedViewDesc = desc + } + } else { + clearNodeSelection(view) + } +} + +// Clear all DOM statefulness of the last node selection. +function clearNodeSelection(view: EditorView) { + if (view.lastSelectedViewDesc) { + if (view.lastSelectedViewDesc.parent) + (view.lastSelectedViewDesc as NodeViewDesc).deselectNode() + view.lastSelectedViewDesc = undefined + } +} + +export function selectionBetween(view: EditorView, $anchor: ResolvedPos, $head: ResolvedPos, bias?: number) { + return view.someProp("createSelectionBetween", f => f(view, $anchor, $head)) + || TextSelection.between($anchor, $head, bias) +} + +export function hasFocusAndSelection(view: EditorView) { + if (view.editable && !view.hasFocus()) return false + return hasSelection(view) +} + +export function hasSelection(view: EditorView) { + let sel = view.domSelectionRange() + if (!sel.anchorNode) return false + try { + // Firefox will raise 'permission denied' errors when accessing + // properties of `sel.anchorNode` when it's in a generated CSS + // element. + return view.dom.contains(sel.anchorNode.nodeType == 3 ? sel.anchorNode.parentNode : sel.anchorNode) && + (view.editable || view.dom.contains(sel.focusNode!.nodeType == 3 ? sel.focusNode!.parentNode : sel.focusNode)) + } catch(_) { + return false + } +} + +export function anchorInRightPlace(view: EditorView) { + let anchorDOM = view.docView.domFromPos(view.state.selection.anchor, 0) + let domSel = view.domSelectionRange() + return isEquivalentPosition(anchorDOM.node, anchorDOM.offset, domSel.anchorNode!, domSel.anchorOffset) +} diff --git a/third_party/js/prosemirror/prosemirror-view/src/viewdesc.ts b/third_party/js/prosemirror/prosemirror-view/src/viewdesc.ts @@ -0,0 +1,1586 @@ +import {DOMSerializer, Fragment, Mark, Node, TagParseRule} from "prosemirror-model" +import {TextSelection} from "prosemirror-state" + +import {domIndex, isEquivalentPosition, DOMNode} from "./dom" +import * as browser from "./browser" +import {Decoration, DecorationSource, WidgetConstructor, WidgetType, NodeType} from "./decoration" +import {EditorView} from "./index" + +declare global { + interface Node { + /// @internal + pmViewDesc?: ViewDesc + } +} + +/// A ViewMutationRecord represents a DOM +/// [mutation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) +/// or a selection change happens within the view. When the change is +/// a selection change, the record will have a `type` property of +/// `"selection"` (which doesn't occur for native mutation records). +export type ViewMutationRecord = MutationRecord | { type: "selection", target: DOMNode } + +/// By default, document nodes are rendered using the result of the +/// [`toDOM`](#model.NodeSpec.toDOM) method of their spec, and managed +/// entirely by the editor. For some use cases, such as embedded +/// node-specific editing interfaces, you want more control over +/// the behavior of a node's in-editor representation, and need to +/// [define](#view.EditorProps.nodeViews) a custom node view. +/// +/// Objects returned as node views must conform to this interface. +export interface NodeView { + /// The outer DOM node that represents the document node. + dom: DOMNode + + /// The DOM node that should hold the node's content. Only meaningful + /// if the node view also defines a `dom` property and if its node + /// type is not a leaf node type. When this is present, ProseMirror + /// will take care of rendering the node's children into it. When it + /// is not present, the node view itself is responsible for rendering + /// (or deciding not to render) its child nodes. + contentDOM?: HTMLElement | null + + /// When given, this will be called when the view is updating + /// itself. It will be given a node, an array of active decorations + /// around the node (which are automatically drawn, and the node + /// view may ignore if it isn't interested in them), and a + /// [decoration source](#view.DecorationSource) that represents any + /// decorations that apply to the content of the node (which again + /// may be ignored). It should return true if it was able to update + /// to that node, and false otherwise. If the node view has a + /// `contentDOM` property (or no `dom` property), updating its child + /// nodes will be handled by ProseMirror. + update?: (node: Node, decorations: readonly Decoration[], innerDecorations: DecorationSource) => boolean + + /// By default, `update` will only be called when a node of the same + /// node type appears in this view's position. When you set this to + /// true, it will be called for any node, making it possible to have + /// a node view that representsmultiple types of nodes. You will + /// need to check the type of the nodes you get in `update` and + /// return `false` for types you cannot handle. + multiType?: boolean + + /// Can be used to override the way the node's selected status (as a + /// node selection) is displayed. + selectNode?: () => void + + /// When defining a `selectNode` method, you should also provide a + /// `deselectNode` method to remove the effect again. + deselectNode?: () => void + + /// This will be called to handle setting the selection inside the + /// node. The `anchor` and `head` positions are relative to the start + /// of the node. By default, a DOM selection will be created between + /// the DOM positions corresponding to those positions, but if you + /// override it you can do something else. + setSelection?: (anchor: number, head: number, root: Document | ShadowRoot) => void + + /// Can be used to prevent the editor view from trying to handle some + /// or all DOM events that bubble up from the node view. Events for + /// which this returns true are not handled by the editor. + stopEvent?: (event: Event) => boolean + + /// Called when a [mutation](#view.ViewMutationRecord) happens within the + /// view. Return false if the editor should re-read the selection or re-parse + /// the range around the mutation, true if it can safely be ignored. + ignoreMutation?: (mutation: ViewMutationRecord) => boolean + + /// Called when the node view is removed from the editor or the whole + /// editor is destroyed. + destroy?: () => void +} + +/// By default, document marks are rendered using the result of the +/// [`toDOM`](#model.MarkSpec.toDOM) method of their spec, and managed entirely +/// by the editor. For some use cases, you want more control over the behavior +/// of a mark's in-editor representation, and need to +/// [define](#view.EditorProps.markViews) a custom mark view. +/// +/// Objects returned as mark views must conform to this interface. +export interface MarkView { + /// The outer DOM node that represents the document node. + dom: DOMNode + + /// The DOM node that should hold the mark's content. When this is not + /// present, the `dom` property is used as the content DOM. + contentDOM?: HTMLElement | null + + /// Called when a [mutation](#view.ViewMutationRecord) happens within the + /// view. Return false if the editor should re-read the selection or re-parse + /// the range around the mutation, true if it can safely be ignored. + ignoreMutation?: (mutation: ViewMutationRecord) => boolean + + + /// Called when the mark view is removed from the editor or the whole + /// editor is destroyed. + destroy?: () => void +} + +// View descriptions are data structures that describe the DOM that is +// used to represent the editor's content. They are used for: +// +// - Incremental redrawing when the document changes +// +// - Figuring out what part of the document a given DOM position +// corresponds to +// +// - Wiring in custom implementations of the editing interface for a +// given node +// +// They form a doubly-linked mutable tree, starting at `view.docView`. + +const NOT_DIRTY = 0, CHILD_DIRTY = 1, CONTENT_DIRTY = 2, NODE_DIRTY = 3 + +// Superclass for the various kinds of descriptions. Defines their +// basic structure and shared methods. +export class ViewDesc { + dirty = NOT_DIRTY + declare node: Node | null + + constructor( + public parent: ViewDesc | undefined, + public children: ViewDesc[], + public dom: DOMNode, + // This is the node that holds the child views. It may be null for + // descs that don't have children. + public contentDOM: HTMLElement | null + ) { + // An expando property on the DOM node provides a link back to its + // description. + dom.pmViewDesc = this + } + + // Used to check whether a given description corresponds to a + // widget/mark/node. + matchesWidget(widget: Decoration) { return false } + matchesMark(mark: Mark) { return false } + matchesNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource) { return false } + matchesHack(nodeName: string) { return false } + + // When parsing in-editor content (in domchange.js), we allow + // descriptions to determine the parse rules that should be used to + // parse them. + parseRule(): Omit<TagParseRule, "tag"> | null { return null } + + // Used by the editor's event handler to ignore events that come + // from certain descs. + stopEvent(event: Event) { return false } + + // The size of the content represented by this desc. + get size() { + let size = 0 + for (let i = 0; i < this.children.length; i++) size += this.children[i].size + return size + } + + // For block nodes, this represents the space taken up by their + // start/end tokens. + get border() { return 0 } + + destroy() { + this.parent = undefined + if (this.dom.pmViewDesc == this) this.dom.pmViewDesc = undefined + for (let i = 0; i < this.children.length; i++) + this.children[i].destroy() + } + + posBeforeChild(child: ViewDesc): number { + for (let i = 0, pos = this.posAtStart;; i++) { + let cur = this.children[i] + if (cur == child) return pos + pos += cur.size + } + } + + get posBefore() { + return this.parent!.posBeforeChild(this) + } + + get posAtStart() { + return this.parent ? this.parent.posBeforeChild(this) + this.border : 0 + } + + get posAfter() { + return this.posBefore + this.size + } + + get posAtEnd() { + return this.posAtStart + this.size - 2 * this.border + } + + localPosFromDOM(dom: DOMNode, offset: number, bias: number): number { + // If the DOM position is in the content, use the child desc after + // it to figure out a position. + if (this.contentDOM && this.contentDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode)) { + if (bias < 0) { + let domBefore, desc: ViewDesc | undefined + if (dom == this.contentDOM) { + domBefore = dom.childNodes[offset - 1] + } else { + while (dom.parentNode != this.contentDOM) dom = dom.parentNode! + domBefore = dom.previousSibling + } + while (domBefore && !((desc = domBefore.pmViewDesc) && desc.parent == this)) domBefore = domBefore.previousSibling + return domBefore ? this.posBeforeChild(desc!) + desc!.size : this.posAtStart + } else { + let domAfter, desc: ViewDesc | undefined + if (dom == this.contentDOM) { + domAfter = dom.childNodes[offset] + } else { + while (dom.parentNode != this.contentDOM) dom = dom.parentNode! + domAfter = dom.nextSibling + } + while (domAfter && !((desc = domAfter.pmViewDesc) && desc.parent == this)) domAfter = domAfter.nextSibling + return domAfter ? this.posBeforeChild(desc!) : this.posAtEnd + } + } + // Otherwise, use various heuristics, falling back on the bias + // parameter, to determine whether to return the position at the + // start or at the end of this view desc. + let atEnd + if (dom == this.dom && this.contentDOM) { + atEnd = offset > domIndex(this.contentDOM) + } else if (this.contentDOM && this.contentDOM != this.dom && this.dom.contains(this.contentDOM)) { + atEnd = dom.compareDocumentPosition(this.contentDOM) & 2 + } else if (this.dom.firstChild) { + if (offset == 0) for (let search = dom;; search = search.parentNode!) { + if (search == this.dom) { atEnd = false; break } + if (search.previousSibling) break + } + if (atEnd == null && offset == dom.childNodes.length) for (let search = dom;; search = search.parentNode!) { + if (search == this.dom) { atEnd = true; break } + if (search.nextSibling) break + } + } + return (atEnd == null ? bias > 0 : atEnd) ? this.posAtEnd : this.posAtStart + } + + // Scan up the dom finding the first desc that is a descendant of + // this one. + nearestDesc(dom: DOMNode): ViewDesc | undefined + nearestDesc(dom: DOMNode, onlyNodes: true): NodeViewDesc | undefined + nearestDesc(dom: DOMNode, onlyNodes: boolean = false) { + for (let first = true, cur: DOMNode | null = dom; cur; cur = cur.parentNode) { + let desc = this.getDesc(cur), nodeDOM + if (desc && (!onlyNodes || desc.node)) { + // If dom is outside of this desc's nodeDOM, don't count it. + if (first && (nodeDOM = (desc as NodeViewDesc).nodeDOM) && + !(nodeDOM.nodeType == 1 ? nodeDOM.contains(dom.nodeType == 1 ? dom : dom.parentNode) : nodeDOM == dom)) + first = false + else + return desc + } + } + } + + getDesc(dom: DOMNode) { + let desc = dom.pmViewDesc + for (let cur: ViewDesc | undefined = desc; cur; cur = cur.parent) if (cur == this) return desc + } + + posFromDOM(dom: DOMNode, offset: number, bias: number) { + for (let scan: DOMNode | null = dom; scan; scan = scan.parentNode) { + let desc = this.getDesc(scan) + if (desc) return desc.localPosFromDOM(dom, offset, bias) + } + return -1 + } + + // Find the desc for the node after the given pos, if any. (When a + // parent node overrode rendering, there might not be one.) + descAt(pos: number): ViewDesc | undefined { + for (let i = 0, offset = 0; i < this.children.length; i++) { + let child = this.children[i], end = offset + child.size + if (offset == pos && end != offset) { + while (!child.border && child.children.length) { + for (let i = 0; i < child.children.length; i++) { + let inner = child.children[i] + if (inner.size) { child = inner; break } + } + } + return child + } + if (pos < end) return child.descAt(pos - offset - child.border) + offset = end + } + } + + domFromPos(pos: number, side: number): {node: DOMNode, offset: number, atom?: number} { + if (!this.contentDOM) return {node: this.dom, offset: 0, atom: pos + 1} + // First find the position in the child array + let i = 0, offset = 0 + for (let curPos = 0; i < this.children.length; i++) { + let child = this.children[i], end = curPos + child.size + if (end > pos || child instanceof TrailingHackViewDesc) { offset = pos - curPos; break } + curPos = end + } + // If this points into the middle of a child, call through + if (offset) return this.children[i].domFromPos(offset - this.children[i].border, side) + // Go back if there were any zero-length widgets with side >= 0 before this point + for (let prev; i && !(prev = this.children[i - 1]).size && prev instanceof WidgetViewDesc && prev.side >= 0; i--) {} + // Scan towards the first useable node + if (side <= 0) { + let prev, enter = true + for (;; i--, enter = false) { + prev = i ? this.children[i - 1] : null + if (!prev || prev.dom.parentNode == this.contentDOM) break + } + if (prev && side && enter && !prev.border && !prev.domAtom) return prev.domFromPos(prev.size, side) + return {node: this.contentDOM, offset: prev ? domIndex(prev.dom) + 1 : 0} + } else { + let next, enter = true + for (;; i++, enter = false) { + next = i < this.children.length ? this.children[i] : null + if (!next || next.dom.parentNode == this.contentDOM) break + } + if (next && enter && !next.border && !next.domAtom) return next.domFromPos(0, side) + return {node: this.contentDOM, offset: next ? domIndex(next.dom) : this.contentDOM.childNodes.length} + } + } + + // Used to find a DOM range in a single parent for a given changed + // range. + parseRange( + from: number, to: number, base = 0 + ): {node: DOMNode, from: number, to: number, fromOffset: number, toOffset: number} { + if (this.children.length == 0) + return {node: this.contentDOM!, from, to, fromOffset: 0, toOffset: this.contentDOM!.childNodes.length} + + let fromOffset = -1, toOffset = -1 + for (let offset = base, i = 0;; i++) { + let child = this.children[i], end = offset + child.size + if (fromOffset == -1 && from <= end) { + let childBase = offset + child.border + // FIXME maybe descend mark views to parse a narrower range? + if (from >= childBase && to <= end - child.border && child.node && + child.contentDOM && this.contentDOM!.contains(child.contentDOM)) + return child.parseRange(from, to, childBase) + + from = offset + for (let j = i; j > 0; j--) { + let prev = this.children[j - 1] + if (prev.size && prev.dom.parentNode == this.contentDOM && !prev.emptyChildAt(1)) { + fromOffset = domIndex(prev.dom) + 1 + break + } + from -= prev.size + } + if (fromOffset == -1) fromOffset = 0 + } + if (fromOffset > -1 && (end > to || i == this.children.length - 1)) { + to = end + for (let j = i + 1; j < this.children.length; j++) { + let next = this.children[j] + if (next.size && next.dom.parentNode == this.contentDOM && !next.emptyChildAt(-1)) { + toOffset = domIndex(next.dom) + break + } + to += next.size + } + if (toOffset == -1) toOffset = this.contentDOM!.childNodes.length + break + } + offset = end + } + return {node: this.contentDOM!, from, to, fromOffset, toOffset} + } + + emptyChildAt(side: number): boolean { + if (this.border || !this.contentDOM || !this.children.length) return false + let child = this.children[side < 0 ? 0 : this.children.length - 1] + return child.size == 0 || child.emptyChildAt(side) + } + + domAfterPos(pos: number): DOMNode { + let {node, offset} = this.domFromPos(pos, 0) + if (node.nodeType != 1 || offset == node.childNodes.length) + throw new RangeError("No node after pos " + pos) + return node.childNodes[offset] + } + + // View descs are responsible for setting any selection that falls + // entirely inside of them, so that custom implementations can do + // custom things with the selection. Note that this falls apart when + // a selection starts in such a node and ends in another, in which + // case we just use whatever domFromPos produces as a best effort. + setSelection(anchor: number, head: number, view: EditorView, force = false): void { + // If the selection falls entirely in a child, give it to that child + let from = Math.min(anchor, head), to = Math.max(anchor, head) + for (let i = 0, offset = 0; i < this.children.length; i++) { + let child = this.children[i], end = offset + child.size + if (from > offset && to < end) + return child.setSelection(anchor - offset - child.border, head - offset - child.border, view, force) + offset = end + } + + let anchorDOM = this.domFromPos(anchor, anchor ? -1 : 1) + let headDOM = head == anchor ? anchorDOM : this.domFromPos(head, head ? -1 : 1) + let domSel = (view.root as Document).getSelection()! + let selRange = view.domSelectionRange() + + let brKludge = false + // On Firefox, using Selection.collapse to put the cursor after a + // BR node for some reason doesn't always work (#1073). On Safari, + // the cursor sometimes inexplicable visually lags behind its + // reported position in such situations (#1092). + if ((browser.gecko || browser.safari) && anchor == head) { + let {node, offset} = anchorDOM + if (node.nodeType == 3) { + brKludge = !!(offset && node.nodeValue![offset - 1] == "\n") + // Issue #1128 + if (brKludge && offset == node.nodeValue!.length) { + for (let scan: DOMNode | null = node, after; scan; scan = scan.parentNode) { + if (after = scan.nextSibling) { + if (after.nodeName == "BR") + anchorDOM = headDOM = {node: after.parentNode!, offset: domIndex(after) + 1} + break + } + let desc = scan.pmViewDesc + if (desc && desc.node && desc.node.isBlock) break + } + } + } else { + let prev = node.childNodes[offset - 1] + brKludge = prev && (prev.nodeName == "BR" || (prev as HTMLElement).contentEditable == "false") + } + } + // Firefox can act strangely when the selection is in front of an + // uneditable node. See #1163 and https://bugzilla.mozilla.org/show_bug.cgi?id=1709536 + if (browser.gecko && selRange.focusNode && selRange.focusNode != headDOM.node && selRange.focusNode.nodeType == 1) { + let after = selRange.focusNode.childNodes[selRange.focusOffset] + if (after && (after as HTMLElement).contentEditable == "false") force = true + } + + if (!(force || brKludge && browser.safari) && + isEquivalentPosition(anchorDOM.node, anchorDOM.offset, selRange.anchorNode!, selRange.anchorOffset) && + isEquivalentPosition(headDOM.node, headDOM.offset, selRange.focusNode!, selRange.focusOffset)) + return + + // Selection.extend can be used to create an 'inverted' selection + // (one where the focus is before the anchor), but not all + // browsers support it yet. + let domSelExtended = false + if ((domSel.extend || anchor == head) && !(brKludge && browser.gecko)) { + domSel.collapse(anchorDOM.node, anchorDOM.offset) + try { + if (anchor != head) + domSel.extend(headDOM.node, headDOM.offset) + domSelExtended = true + } catch (_) { + // In some cases with Chrome the selection is empty after calling + // collapse, even when it should be valid. This appears to be a bug, but + // it is difficult to isolate. If this happens fallback to the old path + // without using extend. + // Similarly, this could crash on Safari if the editor is hidden, and + // there was no selection. + } + } + if (!domSelExtended) { + if (anchor > head) { let tmp = anchorDOM; anchorDOM = headDOM; headDOM = tmp } + let range = document.createRange() + range.setEnd(headDOM.node, headDOM.offset) + range.setStart(anchorDOM.node, anchorDOM.offset) + domSel.removeAllRanges() + domSel.addRange(range) + } + } + + ignoreMutation(mutation: ViewMutationRecord): boolean { + return !this.contentDOM && mutation.type != "selection" + } + + get contentLost() { + return this.contentDOM && this.contentDOM != this.dom && !this.dom.contains(this.contentDOM) + } + + // Remove a subtree of the element tree that has been touched + // by a DOM change, so that the next update will redraw it. + markDirty(from: number, to: number) { + for (let offset = 0, i = 0; i < this.children.length; i++) { + let child = this.children[i], end = offset + child.size + if (offset == end ? from <= end && to >= offset : from < end && to > offset) { + let startInside = offset + child.border, endInside = end - child.border + if (from >= startInside && to <= endInside) { + this.dirty = from == offset || to == end ? CONTENT_DIRTY : CHILD_DIRTY + if (from == startInside && to == endInside && + (child.contentLost || child.dom.parentNode != this.contentDOM)) child.dirty = NODE_DIRTY + else child.markDirty(from - startInside, to - startInside) + return + } else { + child.dirty = child.dom == child.contentDOM && child.dom.parentNode == this.contentDOM && !child.children.length + ? CONTENT_DIRTY : NODE_DIRTY + } + } + offset = end + } + this.dirty = CONTENT_DIRTY + } + + markParentsDirty() { + let level = 1 + for (let node = this.parent; node; node = node.parent, level++) { + let dirty = level == 1 ? CONTENT_DIRTY : CHILD_DIRTY + if (node.dirty < dirty) node.dirty = dirty + } + } + + get domAtom() { return false } + + get ignoreForCoords() { return false } + + get ignoreForSelection() { return false } + + isText(text: string) { return false } +} + +// A widget desc represents a widget decoration, which is a DOM node +// drawn between the document nodes. +class WidgetViewDesc extends ViewDesc { + constructor(parent: ViewDesc, readonly widget: Decoration, view: EditorView, pos: number) { + let self: WidgetViewDesc, dom = (widget.type as any).toDOM as WidgetConstructor + if (typeof dom == "function") dom = dom(view, () => { + if (!self) return pos + if (self.parent) return self.parent.posBeforeChild(self) + }) + if (!widget.type.spec.raw) { + if (dom.nodeType != 1) { + let wrap = document.createElement("span") + wrap.appendChild(dom) + dom = wrap + } + ;(dom as HTMLElement).contentEditable = "false" + ;(dom as HTMLElement).classList.add("ProseMirror-widget") + } + super(parent, [], dom, null) + this.widget = widget + self = this + } + + matchesWidget(widget: Decoration) { + return this.dirty == NOT_DIRTY && widget.type.eq(this.widget.type) + } + + parseRule() { return {ignore: true} } + + stopEvent(event: Event) { + let stop = this.widget.spec.stopEvent + return stop ? stop(event) : false + } + + ignoreMutation(mutation: ViewMutationRecord) { + return mutation.type != "selection" || this.widget.spec.ignoreSelection + } + + destroy() { + this.widget.type.destroy(this.dom) + super.destroy() + } + + get domAtom() { return true } + + get ignoreForSelection() { return !!this.widget.type.spec.relaxedSide } + + get side() { return (this.widget.type as any).side as number } +} + +class CompositionViewDesc extends ViewDesc { + constructor(parent: ViewDesc, dom: DOMNode, readonly textDOM: Text, readonly text: string) { + super(parent, [], dom, null) + } + + get size() { return this.text.length } + + localPosFromDOM(dom: DOMNode, offset: number) { + if (dom != this.textDOM) return this.posAtStart + (offset ? this.size : 0) + return this.posAtStart + offset + } + + domFromPos(pos: number) { + return {node: this.textDOM, offset: pos} + } + + ignoreMutation(mut: ViewMutationRecord) { + return mut.type === 'characterData' && mut.target.nodeValue == mut.oldValue + } +} + +// A mark desc represents a mark. May have multiple children, +// depending on how the mark is split. Note that marks are drawn using +// a fixed nesting order, for simplicity and predictability, so in +// some cases they will be split more often than would appear +// necessary. +class MarkViewDesc extends ViewDesc { + constructor(parent: ViewDesc, readonly mark: Mark, dom: DOMNode, contentDOM: HTMLElement, readonly spec: MarkView) { + super(parent, [], dom, contentDOM) + } + + static create(parent: ViewDesc, mark: Mark, inline: boolean, view: EditorView) { + let custom = view.nodeViews[mark.type.name] + let spec: {dom: HTMLElement, contentDOM?: HTMLElement} = custom && (custom as any)(mark, view, inline) + if (!spec || !spec.dom) + spec = (DOMSerializer.renderSpec as any)(document, mark.type.spec.toDOM!(mark, inline), null, mark.attrs) as any + return new MarkViewDesc(parent, mark, spec.dom, spec.contentDOM || spec.dom as HTMLElement, spec) + } + + parseRule() { + if ((this.dirty & NODE_DIRTY) || this.mark.type.spec.reparseInView) return null + return {mark: this.mark.type.name, attrs: this.mark.attrs, contentElement: this.contentDOM!} + } + + matchesMark(mark: Mark) { return this.dirty != NODE_DIRTY && this.mark.eq(mark) } + + markDirty(from: number, to: number) { + super.markDirty(from, to) + // Move dirty info to nearest node view + if (this.dirty != NOT_DIRTY) { + let parent = this.parent! + while (!parent.node) parent = parent.parent! + if (parent.dirty < this.dirty) parent.dirty = this.dirty + this.dirty = NOT_DIRTY + } + } + + slice(from: number, to: number, view: EditorView) { + let copy = MarkViewDesc.create(this.parent!, this.mark, true, view) + let nodes = this.children, size = this.size + if (to < size) nodes = replaceNodes(nodes, to, size, view) + if (from > 0) nodes = replaceNodes(nodes, 0, from, view) + for (let i = 0; i < nodes.length; i++) nodes[i].parent = copy + copy.children = nodes + return copy + } + + ignoreMutation(mutation: ViewMutationRecord) { + return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation) + } + + destroy() { + if (this.spec.destroy) this.spec.destroy() + super.destroy() + } +} + +// Node view descs are the main, most common type of view desc, and +// correspond to an actual node in the document. Unlike mark descs, +// they populate their child array themselves. +export class NodeViewDesc extends ViewDesc { + constructor( + parent: ViewDesc | undefined, + public node: Node, + public outerDeco: readonly Decoration[], + public innerDeco: DecorationSource, + dom: DOMNode, + contentDOM: HTMLElement | null, + readonly nodeDOM: DOMNode, + view: EditorView, + pos: number + ) { + super(parent, [], dom, contentDOM) + } + + // By default, a node is rendered using the `toDOM` method from the + // node type spec. But client code can use the `nodeViews` spec to + // supply a custom node view, which can influence various aspects of + // the way the node works. + // + // (Using subclassing for this was intentionally decided against, + // since it'd require exposing a whole slew of finicky + // implementation details to the user code that they probably will + // never need.) + static create(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], + innerDeco: DecorationSource, view: EditorView, pos: number) { + let custom = view.nodeViews[node.type.name], descObj: ViewDesc + let spec: NodeView | undefined = custom && (custom as any)(node, view, () => { + // (This is a function that allows the custom view to find its + // own position) + if (!descObj) return pos + if (descObj.parent) return descObj.parent.posBeforeChild(descObj) + }, outerDeco, innerDeco) + + let dom = spec && spec.dom, contentDOM = spec && spec.contentDOM + if (node.isText) { + if (!dom) dom = document.createTextNode(node.text!) + else if (dom.nodeType != 3) throw new RangeError("Text must be rendered as a DOM text node") + } else if (!dom) { + let spec = (DOMSerializer.renderSpec as any)(document, node.type.spec.toDOM!(node), null, node.attrs) + ;({dom, contentDOM} = spec as {dom: DOMNode, contentDOM?: HTMLElement}) + } + if (!contentDOM && !node.isText && dom.nodeName != "BR") { // Chrome gets confused by <br contenteditable=false> + if (!(dom as HTMLElement).hasAttribute("contenteditable")) (dom as HTMLElement).contentEditable = "false" + if (node.type.spec.draggable) (dom as HTMLElement).draggable = true + } + + let nodeDOM = dom + dom = applyOuterDeco(dom, outerDeco, node) + + if (spec) + return descObj = new CustomNodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM, + spec, view, pos + 1) + else if (node.isText) + return new TextViewDesc(parent, node, outerDeco, innerDeco, dom, nodeDOM, view) + else + return new NodeViewDesc(parent, node, outerDeco, innerDeco, dom, contentDOM || null, nodeDOM, view, pos + 1) + } + + parseRule() { + // Experimental kludge to allow opt-in re-parsing of nodes + if (this.node.type.spec.reparseInView) return null + // FIXME the assumption that this can always return the current + // attrs means that if the user somehow manages to change the + // attrs in the dom, that won't be picked up. Not entirely sure + // whether this is a problem + let rule: Omit<TagParseRule, "tag"> = {node: this.node.type.name, attrs: this.node.attrs} + if (this.node.type.whitespace == "pre") rule.preserveWhitespace = "full" + if (!this.contentDOM) { + rule.getContent = () => this.node.content + } else if (!this.contentLost) { + rule.contentElement = this.contentDOM + } else { + // Chrome likes to randomly recreate parent nodes when + // backspacing things. When that happens, this tries to find the + // new parent. + for (let i = this.children.length - 1; i >= 0; i--) { + let child = this.children[i] + if (this.dom.contains(child.dom.parentNode)) { + rule.contentElement = child.dom.parentNode as HTMLElement + break + } + } + if (!rule.contentElement) rule.getContent = () => Fragment.empty + } + return rule + } + + matchesNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource) { + return this.dirty == NOT_DIRTY && node.eq(this.node) && + sameOuterDeco(outerDeco, this.outerDeco) && innerDeco.eq(this.innerDeco) + } + + get size() { return this.node.nodeSize } + + get border() { return this.node.isLeaf ? 0 : 1 } + + // Syncs `this.children` to match `this.node.content` and the local + // decorations, possibly introducing nesting for marks. Then, in a + // separate step, syncs the DOM inside `this.contentDOM` to + // `this.children`. + updateChildren(view: EditorView, pos: number) { + let inline = this.node.inlineContent, off = pos + let composition = view.composing ? this.localCompositionInfo(view, pos) : null + let localComposition = composition && composition.pos > -1 ? composition : null + let compositionInChild = composition && composition.pos < 0 + let updater = new ViewTreeUpdater(this, localComposition && localComposition.node, view) + iterDeco(this.node, this.innerDeco, (widget, i, insideNode) => { + if (widget.spec.marks) + updater.syncToMarks(widget.spec.marks, inline, view) + else if ((widget.type as WidgetType).side >= 0 && !insideNode) + updater.syncToMarks(i == this.node.childCount ? Mark.none : this.node.child(i).marks, inline, view) + // If the next node is a desc matching this widget, reuse it, + // otherwise insert the widget as a new view desc. + updater.placeWidget(widget, view, off) + }, (child, outerDeco, innerDeco, i) => { + // Make sure the wrapping mark descs match the node's marks. + updater.syncToMarks(child.marks, inline, view) + // Try several strategies for drawing this node + let compIndex + if (updater.findNodeMatch(child, outerDeco, innerDeco, i)) { + // Found precise match with existing node view + } else if (compositionInChild && view.state.selection.from > off && + view.state.selection.to < off + child.nodeSize && + (compIndex = updater.findIndexWithChild(composition!.node)) > -1 && + updater.updateNodeAt(child, outerDeco, innerDeco, compIndex, view)) { + // Updated the specific node that holds the composition + } else if (updater.updateNextNode(child, outerDeco, innerDeco, view, i, off)) { + // Could update an existing node to reflect this node + } else { + // Add it as a new view + updater.addNode(child, outerDeco, innerDeco, view, off) + } + off += child.nodeSize + }) + // Drop all remaining descs after the current position. + updater.syncToMarks([], inline, view) + if (this.node.isTextblock) updater.addTextblockHacks() + updater.destroyRest() + + // Sync the DOM if anything changed + if (updater.changed || this.dirty == CONTENT_DIRTY) { + // May have to protect focused DOM from being changed if a composition is active + if (localComposition) this.protectLocalComposition(view, localComposition) + renderDescs(this.contentDOM!, this.children, view) + if (browser.ios) iosHacks(this.dom as HTMLElement) + } + } + + localCompositionInfo(view: EditorView, pos: number): {node: Text, pos: number, text: string} | null { + // Only do something if both the selection and a focused text node + // are inside of this node + let {from, to} = view.state.selection + if (!(view.state.selection instanceof TextSelection) || from < pos || to > pos + this.node.content.size) return null + let textNode = view.input.compositionNode + if (!textNode || !this.dom.contains(textNode.parentNode)) return null + + if (this.node.inlineContent) { + // Find the text in the focused node in the node, stop if it's not + // there (may have been modified through other means, in which + // case it should overwritten) + let text = textNode.nodeValue! + let textPos = findTextInFragment(this.node.content, text, from - pos, to - pos) + return textPos < 0 ? null : {node: textNode, pos: textPos, text} + } else { + return {node: textNode, pos: -1, text: ""} + } + } + + protectLocalComposition(view: EditorView, {node, pos, text}: {node: Text, pos: number, text: string}) { + // The node is already part of a local view desc, leave it there + if (this.getDesc(node)) return + + // Create a composition view for the orphaned nodes + let topNode: DOMNode = node + for (;; topNode = topNode.parentNode!) { + if (topNode.parentNode == this.contentDOM) break + while (topNode.previousSibling) topNode.parentNode!.removeChild(topNode.previousSibling) + while (topNode.nextSibling) topNode.parentNode!.removeChild(topNode.nextSibling) + if (topNode.pmViewDesc) topNode.pmViewDesc = undefined + } + let desc = new CompositionViewDesc(this, topNode, node, text) + view.input.compositionNodes.push(desc) + + // Patch up this.children to contain the composition view + this.children = replaceNodes(this.children, pos, pos + text.length, view, desc) + } + + // If this desc must be updated to match the given node decoration, + // do so and return true. + update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) { + if (this.dirty == NODE_DIRTY || + !node.sameMarkup(this.node)) return false + this.updateInner(node, outerDeco, innerDeco, view) + return true + } + + updateInner(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) { + this.updateOuterDeco(outerDeco) + this.node = node + this.innerDeco = innerDeco + if (this.contentDOM) this.updateChildren(view, this.posAtStart) + this.dirty = NOT_DIRTY + } + + updateOuterDeco(outerDeco: readonly Decoration[]) { + if (sameOuterDeco(outerDeco, this.outerDeco)) return + let needsWrap = this.nodeDOM.nodeType != 1 + let oldDOM = this.dom + this.dom = patchOuterDeco(this.dom, this.nodeDOM, + computeOuterDeco(this.outerDeco, this.node, needsWrap), + computeOuterDeco(outerDeco, this.node, needsWrap)) + if (this.dom != oldDOM) { + oldDOM.pmViewDesc = undefined + this.dom.pmViewDesc = this + } + this.outerDeco = outerDeco + } + + // Mark this node as being the selected node. + selectNode() { + if (this.nodeDOM.nodeType == 1) { + ;(this.nodeDOM as HTMLElement).classList.add("ProseMirror-selectednode") + if (this.contentDOM || !this.node.type.spec.draggable) (this.nodeDOM as HTMLElement).draggable = true + } + } + + // Remove selected node marking from this node. + deselectNode() { + if (this.nodeDOM.nodeType == 1) { + ;(this.nodeDOM as HTMLElement).classList.remove("ProseMirror-selectednode") + if (this.contentDOM || !this.node.type.spec.draggable) (this.nodeDOM as HTMLElement).removeAttribute("draggable") + } + } + + get domAtom() { return this.node.isAtom } +} + +// Create a view desc for the top-level document node, to be exported +// and used by the view class. +export function docViewDesc(doc: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, + dom: HTMLElement, view: EditorView): NodeViewDesc { + applyOuterDeco(dom, outerDeco, doc) + let docView = new NodeViewDesc(undefined, doc, outerDeco, innerDeco, dom, dom, dom, view, 0) + if (docView.contentDOM) docView.updateChildren(view, 0) + return docView +} + +class TextViewDesc extends NodeViewDesc { + constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], + innerDeco: DecorationSource, dom: DOMNode, nodeDOM: DOMNode, view: EditorView) { + super(parent, node, outerDeco, innerDeco, dom, null, nodeDOM, view, 0) + } + + parseRule() { + let skip = this.nodeDOM.parentNode + while (skip && skip != this.dom && !(skip as any).pmIsDeco) skip = skip.parentNode + return {skip: (skip || true) as any} + } + + update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) { + if (this.dirty == NODE_DIRTY || (this.dirty != NOT_DIRTY && !this.inParent()) || + !node.sameMarkup(this.node)) return false + this.updateOuterDeco(outerDeco) + if ((this.dirty != NOT_DIRTY || node.text != this.node.text) && node.text != this.nodeDOM.nodeValue) { + this.nodeDOM.nodeValue = node.text! + if (view.trackWrites == this.nodeDOM) view.trackWrites = null + } + this.node = node + this.dirty = NOT_DIRTY + return true + } + + inParent() { + let parentDOM = this.parent!.contentDOM + for (let n: DOMNode | null = this.nodeDOM; n; n = n.parentNode) if (n == parentDOM) return true + return false + } + + domFromPos(pos: number) { + return {node: this.nodeDOM, offset: pos} + } + + localPosFromDOM(dom: DOMNode, offset: number, bias: number) { + if (dom == this.nodeDOM) return this.posAtStart + Math.min(offset, this.node.text!.length) + return super.localPosFromDOM(dom, offset, bias) + } + + ignoreMutation(mutation: ViewMutationRecord) { + return mutation.type != "characterData" && mutation.type != "selection" + } + + slice(from: number, to: number, view: EditorView) { + let node = this.node.cut(from, to), dom = document.createTextNode(node.text!) + return new TextViewDesc(this.parent, node, this.outerDeco, this.innerDeco, dom, dom, view) + } + + markDirty(from: number, to: number) { + super.markDirty(from, to) + if (this.dom != this.nodeDOM && (from == 0 || to == this.nodeDOM.nodeValue!.length)) + this.dirty = NODE_DIRTY + } + + get domAtom() { return false } + + isText(text: string) { return this.node.text == text } +} + +// A dummy desc used to tag trailing BR or IMG nodes created to work +// around contentEditable terribleness. +class TrailingHackViewDesc extends ViewDesc { + parseRule() { return {ignore: true} } + matchesHack(nodeName: string) { return this.dirty == NOT_DIRTY && this.dom.nodeName == nodeName } + get domAtom() { return true } + get ignoreForCoords() { return this.dom.nodeName == "IMG" } +} + +// A separate subclass is used for customized node views, so that the +// extra checks only have to be made for nodes that are actually +// customized. +class CustomNodeViewDesc extends NodeViewDesc { + constructor(parent: ViewDesc | undefined, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, + dom: DOMNode, contentDOM: HTMLElement | null, nodeDOM: DOMNode, readonly spec: NodeView, + view: EditorView, pos: number) { + super(parent, node, outerDeco, innerDeco, dom, contentDOM, nodeDOM, view, pos) + } + + // A custom `update` method gets to decide whether the update goes + // through. If it does, and there's a `contentDOM` node, our logic + // updates the children. + update(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView) { + if (this.dirty == NODE_DIRTY) return false + if (this.spec.update && (this.node.type == node.type || this.spec.multiType)) { + let result = this.spec.update(node, outerDeco, innerDeco) + if (result) this.updateInner(node, outerDeco, innerDeco, view) + return result + } else if (!this.contentDOM && !node.isLeaf) { + return false + } else { + return super.update(node, outerDeco, innerDeco, view) + } + } + + selectNode() { + this.spec.selectNode ? this.spec.selectNode() : super.selectNode() + } + + deselectNode() { + this.spec.deselectNode ? this.spec.deselectNode() : super.deselectNode() + } + + setSelection(anchor: number, head: number, view: EditorView, force: boolean) { + this.spec.setSelection ? this.spec.setSelection(anchor, head, view.root) + : super.setSelection(anchor, head, view, force) + } + + destroy() { + if (this.spec.destroy) this.spec.destroy() + super.destroy() + } + + stopEvent(event: Event) { + return this.spec.stopEvent ? this.spec.stopEvent(event) : false + } + + ignoreMutation(mutation: ViewMutationRecord) { + return this.spec.ignoreMutation ? this.spec.ignoreMutation(mutation) : super.ignoreMutation(mutation) + } +} + +// Sync the content of the given DOM node with the nodes associated +// with the given array of view descs, recursing into mark descs +// because this should sync the subtree for a whole node at a time. +function renderDescs(parentDOM: HTMLElement, descs: readonly ViewDesc[], view: EditorView) { + let dom = parentDOM.firstChild, written = false + for (let i = 0; i < descs.length; i++) { + let desc = descs[i], childDOM = desc.dom + if (childDOM.parentNode == parentDOM) { + while (childDOM != dom) { dom = rm(dom!); written = true } + dom = dom.nextSibling + } else { + written = true + parentDOM.insertBefore(childDOM, dom) + } + if (desc instanceof MarkViewDesc) { + let pos = dom ? dom.previousSibling : parentDOM.lastChild + renderDescs(desc.contentDOM!, desc.children, view) + dom = pos ? pos.nextSibling : parentDOM.firstChild + } + } + while (dom) { dom = rm(dom); written = true } + if (written && view.trackWrites == parentDOM) view.trackWrites = null +} + +type OuterDecoLevel = {[attr: string]: string} + +const OuterDecoLevel: {new (nodeName?: string): OuterDecoLevel} = function(this: any, nodeName?: string) { + if (nodeName) this.nodeName = nodeName +} as any +OuterDecoLevel.prototype = Object.create(null) + +const noDeco = [new OuterDecoLevel] + +function computeOuterDeco(outerDeco: readonly Decoration[], node: Node, needsWrap: boolean) { + if (outerDeco.length == 0) return noDeco + + let top = needsWrap ? noDeco[0] : new OuterDecoLevel, result = [top] + + for (let i = 0; i < outerDeco.length; i++) { + let attrs = (outerDeco[i].type as NodeType).attrs + if (!attrs) continue + if (attrs.nodeName) + result.push(top = new OuterDecoLevel(attrs.nodeName)) + + for (let name in attrs) { + let val = attrs[name] + if (val == null) continue + if (needsWrap && result.length == 1) + result.push(top = new OuterDecoLevel(node.isInline ? "span" : "div")) + if (name == "class") top.class = (top.class ? top.class + " " : "") + val + else if (name == "style") top.style = (top.style ? top.style + ";" : "") + val + else if (name != "nodeName") top[name] = val + } + } + + return result +} + +function patchOuterDeco(outerDOM: DOMNode, nodeDOM: DOMNode, + prevComputed: readonly OuterDecoLevel[], curComputed: readonly OuterDecoLevel[]) { + // Shortcut for trivial case + if (prevComputed == noDeco && curComputed == noDeco) return nodeDOM + + let curDOM = nodeDOM + for (let i = 0; i < curComputed.length; i++) { + let deco = curComputed[i], prev = prevComputed[i] + if (i) { + let parent: DOMNode | null + if (prev && prev.nodeName == deco.nodeName && curDOM != outerDOM && + (parent = curDOM.parentNode) && parent.nodeName!.toLowerCase() == deco.nodeName) { + curDOM = parent + } else { + parent = document.createElement(deco.nodeName) + ;(parent as any).pmIsDeco = true + parent.appendChild(curDOM) + prev = noDeco[0] + curDOM = parent + } + } + patchAttributes(curDOM as HTMLElement, prev || noDeco[0], deco) + } + return curDOM +} + +function patchAttributes(dom: HTMLElement, prev: {[name: string]: string}, cur: {[name: string]: string}) { + for (let name in prev) + if (name != "class" && name != "style" && name != "nodeName" && !(name in cur)) + dom.removeAttribute(name) + for (let name in cur) + if (name != "class" && name != "style" && name != "nodeName" && cur[name] != prev[name]) + dom.setAttribute(name, cur[name]) + if (prev.class != cur.class) { + let prevList = prev.class ? prev.class.split(" ").filter(Boolean) : [] + let curList = cur.class ? cur.class.split(" ").filter(Boolean) : [] + for (let i = 0; i < prevList.length; i++) if (curList.indexOf(prevList[i]) == -1) + dom.classList.remove(prevList[i]) + for (let i = 0; i < curList.length; i++) if (prevList.indexOf(curList[i]) == -1) + dom.classList.add(curList[i]) + if (dom.classList.length == 0) + dom.removeAttribute("class") + } + if (prev.style != cur.style) { + if (prev.style) { + let prop = /\s*([\w\-\xa1-\uffff]+)\s*:(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\(.*?\)|[^;])*/g, m + while (m = prop.exec(prev.style)) + dom.style.removeProperty(m[1]) + } + if (cur.style) + dom.style.cssText += cur.style + } +} + +function applyOuterDeco(dom: DOMNode, deco: readonly Decoration[], node: Node) { + return patchOuterDeco(dom, dom, noDeco, computeOuterDeco(deco, node, dom.nodeType != 1)) +} + +function sameOuterDeco(a: readonly Decoration[], b: readonly Decoration[]) { + if (a.length != b.length) return false + for (let i = 0; i < a.length; i++) if (!a[i].type.eq(b[i].type)) return false + return true +} + +// Remove a DOM node and return its next sibling. +function rm(dom: DOMNode) { + let next = dom.nextSibling + dom.parentNode!.removeChild(dom) + return next +} + +// Helper class for incrementally updating a tree of mark descs and +// the widget and node descs inside of them. +class ViewTreeUpdater { + // Index into `this.top`'s child array, represents the current + // update position. + index = 0 + // When entering a mark, the current top and index are pushed + // onto this. + stack: (ViewDesc | number)[] = [] + // Tracks whether anything was changed + changed = false + preMatch: {index: number, matched: Map<ViewDesc, number>, matches: readonly ViewDesc[]} + top: ViewDesc + + constructor(top: NodeViewDesc, readonly lock: DOMNode | null, private readonly view: EditorView) { + this.top = top + this.preMatch = preMatch(top.node.content, top) + } + + // Destroy and remove the children between the given indices in + // `this.top`. + destroyBetween(start: number, end: number) { + if (start == end) return + for (let i = start; i < end; i++) this.top.children[i].destroy() + this.top.children.splice(start, end - start) + this.changed = true + } + + // Destroy all remaining children in `this.top`. + destroyRest() { + this.destroyBetween(this.index, this.top.children.length) + } + + // Sync the current stack of mark descs with the given array of + // marks, reusing existing mark descs when possible. + syncToMarks(marks: readonly Mark[], inline: boolean, view: EditorView) { + let keep = 0, depth = this.stack.length >> 1 + let maxKeep = Math.min(depth, marks.length) + while (keep < maxKeep && + (keep == depth - 1 ? this.top : this.stack[(keep + 1) << 1] as ViewDesc) + .matchesMark(marks[keep]) && marks[keep].type.spec.spanning !== false) + keep++ + + while (keep < depth) { + this.destroyRest() + this.top.dirty = NOT_DIRTY + this.index = this.stack.pop() as number + this.top = this.stack.pop() as ViewDesc + depth-- + } + while (depth < marks.length) { + this.stack.push(this.top, this.index + 1) + let found = -1 + for (let i = this.index; i < Math.min(this.index + 3, this.top.children.length); i++) { + let next = this.top.children[i] + if (next.matchesMark(marks[depth]) && !this.isLocked(next.dom)) { found = i; break } + } + if (found > -1) { + if (found > this.index) { + this.changed = true + this.destroyBetween(this.index, found) + } + this.top = this.top.children[this.index] + } else { + let markDesc = MarkViewDesc.create(this.top, marks[depth], inline, view) + this.top.children.splice(this.index, 0, markDesc) + this.top = markDesc + this.changed = true + } + this.index = 0 + depth++ + } + } + + // Try to find a node desc matching the given data. Skip over it and + // return true when successful. + findNodeMatch(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, index: number): boolean { + let found = -1, targetDesc + if (index >= this.preMatch.index && + (targetDesc = this.preMatch.matches[index - this.preMatch.index]).parent == this.top && + targetDesc.matchesNode(node, outerDeco, innerDeco)) { + found = this.top.children.indexOf(targetDesc, this.index) + } else { + for (let i = this.index, e = Math.min(this.top.children.length, i + 5); i < e; i++) { + let child = this.top.children[i] + if (child.matchesNode(node, outerDeco, innerDeco) && !this.preMatch.matched.has(child)) { + found = i + break + } + } + } + if (found < 0) return false + this.destroyBetween(this.index, found) + this.index++ + return true + } + + updateNodeAt(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, index: number, view: EditorView) { + let child = this.top.children[index] as NodeViewDesc + if (child.dirty == NODE_DIRTY && child.dom == child.contentDOM) child.dirty = CONTENT_DIRTY + if (!child.update(node, outerDeco, innerDeco, view)) return false + this.destroyBetween(this.index, index) + this.index++ + return true + } + + findIndexWithChild(domNode: DOMNode) { + for (;;) { + let parent = domNode.parentNode + if (!parent) return -1 + if (parent == this.top.contentDOM) { + let desc = domNode.pmViewDesc + if (desc) for (let i = this.index; i < this.top.children.length; i++) { + if (this.top.children[i] == desc) return i + } + return -1 + } + domNode = parent + } + } + + // Try to update the next node, if any, to the given data. Checks + // pre-matches to avoid overwriting nodes that could still be used. + updateNextNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, + view: EditorView, index: number, pos: number): boolean { + for (let i = this.index; i < this.top.children.length; i++) { + let next = this.top.children[i] + if (next instanceof NodeViewDesc) { + let preMatch = this.preMatch.matched.get(next) + if (preMatch != null && preMatch != index) return false + let nextDOM = next.dom, updated + + // Can't update if nextDOM is or contains this.lock, except if + // it's a text node whose content already matches the new text + // and whose decorations match the new ones. + let locked = this.isLocked(nextDOM) && + !(node.isText && next.node && next.node.isText && next.nodeDOM.nodeValue == node.text && + next.dirty != NODE_DIRTY && sameOuterDeco(outerDeco, next.outerDeco)) + if (!locked && next.update(node, outerDeco, innerDeco, view)) { + this.destroyBetween(this.index, i) + if (next.dom != nextDOM) this.changed = true + this.index++ + return true + } else if (!locked && (updated = this.recreateWrapper(next, node, outerDeco, innerDeco, view, pos))) { + this.destroyBetween(this.index, i) + this.top.children[this.index] = updated + if (updated.contentDOM) { + updated.dirty = CONTENT_DIRTY + updated.updateChildren(view, pos + 1) + updated.dirty = NOT_DIRTY + } + this.changed = true + this.index++ + return true + } + break + } + } + return false + } + + // When a node with content is replaced by a different node with + // identical content, move over its children. + recreateWrapper(next: NodeViewDesc, node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, + view: EditorView, pos: number) { + if (next.dirty || node.isAtom || !next.children.length || + !next.node.content.eq(node.content) || + !sameOuterDeco(outerDeco, next.outerDeco) || !innerDeco.eq(next.innerDeco)) return null + let wrapper = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos) + if (wrapper.contentDOM) { + wrapper.children = next.children + next.children = [] + for (let ch of wrapper.children) ch.parent = wrapper + } + next.destroy() + return wrapper + } + + // Insert the node as a newly created node desc. + addNode(node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, view: EditorView, pos: number) { + let desc = NodeViewDesc.create(this.top, node, outerDeco, innerDeco, view, pos) + if (desc.contentDOM) desc.updateChildren(view, pos + 1) + this.top.children.splice(this.index++, 0, desc) + this.changed = true + } + + placeWidget(widget: Decoration, view: EditorView, pos: number) { + let next = this.index < this.top.children.length ? this.top.children[this.index] : null + if (next && next.matchesWidget(widget) && + (widget == (next as WidgetViewDesc).widget || !(next as any).widget.type.toDOM.parentNode)) { + this.index++ + } else { + let desc = new WidgetViewDesc(this.top, widget, view, pos) + this.top.children.splice(this.index++, 0, desc) + this.changed = true + } + } + + // Make sure a textblock looks and behaves correctly in + // contentEditable. + addTextblockHacks() { + let lastChild = this.top.children[this.index - 1], parent = this.top + while (lastChild instanceof MarkViewDesc) { + parent = lastChild + lastChild = parent.children[parent.children.length - 1] + } + + if (!lastChild || // Empty textblock + !(lastChild instanceof TextViewDesc) || + /\n$/.test(lastChild.node.text!) || + (this.view.requiresGeckoHackNode && /\s$/.test(lastChild.node.text!))) { + // Avoid bugs in Safari's cursor drawing (#1165) and Chrome's mouse selection (#1152) + if ((browser.safari || browser.chrome) && lastChild && (lastChild.dom as HTMLElement).contentEditable == "false") + this.addHackNode("IMG", parent) + this.addHackNode("BR", this.top) + } + } + + addHackNode(nodeName: string, parent: ViewDesc) { + if (parent == this.top && this.index < parent.children.length && parent.children[this.index].matchesHack(nodeName)) { + this.index++ + } else { + let dom = document.createElement(nodeName) + if (nodeName == "IMG") { + dom.className = "ProseMirror-separator" + ;(dom as HTMLImageElement).alt = "" + } + if (nodeName == "BR") dom.className = "ProseMirror-trailingBreak" + let hack = new TrailingHackViewDesc(this.top, [], dom, null) + if (parent != this.top) parent.children.push(hack) + else parent.children.splice(this.index++, 0, hack) + this.changed = true + } + } + + isLocked(node: DOMNode) { + return this.lock && (node == this.lock || node.nodeType == 1 && node.contains(this.lock.parentNode)) + } +} + +// Iterate from the end of the fragment and array of descs to find +// directly matching ones, in order to avoid overeagerly reusing those +// for other nodes. Returns the fragment index of the first node that +// is part of the sequence of matched nodes at the end of the +// fragment. +function preMatch( + frag: Fragment, parentDesc: ViewDesc +): {index: number, matched: Map<ViewDesc, number>, matches: readonly ViewDesc[]} { + let curDesc = parentDesc, descI = curDesc.children.length + let fI = frag.childCount, matched = new Map, matches = [] + outer: while (fI > 0) { + let desc + for (;;) { + if (descI) { + let next = curDesc.children[descI - 1] + if (next instanceof MarkViewDesc) { + curDesc = next + descI = next.children.length + } else { + desc = next + descI-- + break + } + } else if (curDesc == parentDesc) { + break outer + } else { + // FIXME + descI = curDesc.parent!.children.indexOf(curDesc) + curDesc = curDesc.parent! + } + } + let node = desc.node + if (!node) continue + if (node != frag.child(fI - 1)) break + --fI + matched.set(desc, fI) + matches.push(desc) + } + return {index: fI, matched, matches: matches.reverse()} +} + +function compareSide(a: Decoration, b: Decoration) { + return (a.type as WidgetType).side - (b.type as WidgetType).side +} + +// This function abstracts iterating over the nodes and decorations in +// a fragment. Calls `onNode` for each node, with its local and child +// decorations. Splits text nodes when there is a decoration starting +// or ending inside of them. Calls `onWidget` for each widget. +function iterDeco( + parent: Node, + deco: DecorationSource, + onWidget: (widget: Decoration, index: number, insideNode: boolean) => void, + onNode: (node: Node, outerDeco: readonly Decoration[], innerDeco: DecorationSource, index: number) => void +) { + let locals = deco.locals(parent), offset = 0 + // Simple, cheap variant for when there are no local decorations + if (locals.length == 0) { + for (let i = 0; i < parent.childCount; i++) { + let child = parent.child(i) + onNode(child, locals, deco.forChild(offset, child), i) + offset += child.nodeSize + } + return + } + + let decoIndex = 0, active = [], restNode = null + for (let parentIndex = 0;;) { + let widget, widgets + while (decoIndex < locals.length && locals[decoIndex].to == offset) { + let next = locals[decoIndex++] + if (next.widget) { + if (!widget) widget = next + else (widgets || (widgets = [widget])).push(next) + } + } + if (widget) { + if (widgets) { + widgets.sort(compareSide) + for (let i = 0; i < widgets.length; i++) onWidget(widgets[i], parentIndex, !!restNode) + } else { + onWidget(widget, parentIndex, !!restNode) + } + } + + let child, index + if (restNode) { + index = -1 + child = restNode + restNode = null + } else if (parentIndex < parent.childCount) { + index = parentIndex + child = parent.child(parentIndex++) + } else { + break + } + + for (let i = 0; i < active.length; i++) if (active[i].to <= offset) active.splice(i--, 1) + while (decoIndex < locals.length && locals[decoIndex].from <= offset && locals[decoIndex].to > offset) + active.push(locals[decoIndex++]) + + let end = offset + child.nodeSize + if (child.isText) { + let cutAt = end + if (decoIndex < locals.length && locals[decoIndex].from < cutAt) cutAt = locals[decoIndex].from + for (let i = 0; i < active.length; i++) if (active[i].to < cutAt) cutAt = active[i].to + if (cutAt < end) { + restNode = child.cut(cutAt - offset) + child = child.cut(0, cutAt - offset) + end = cutAt + index = -1 + } + } else { + while (decoIndex < locals.length && locals[decoIndex].to < end) decoIndex++ + } + + let outerDeco = child.isInline && !child.isLeaf ? active.filter(d => !d.inline) : active.slice() + onNode(child, outerDeco, deco.forChild(offset, child), index) + offset = end + } +} + +// List markers in Mobile Safari will mysteriously disappear +// sometimes. This works around that. +function iosHacks(dom: HTMLElement) { + if (dom.nodeName == "UL" || dom.nodeName == "OL") { + let oldCSS = dom.style.cssText + dom.style.cssText = oldCSS + "; list-style: square !important" + window.getComputedStyle(dom).listStyle + dom.style.cssText = oldCSS + } +} + +// Find a piece of text in an inline fragment, overlapping from-to +function findTextInFragment(frag: Fragment, text: string, from: number, to: number) { + for (let i = 0, pos = 0; i < frag.childCount && pos <= to;) { + let child = frag.child(i++), childStart = pos + pos += child.nodeSize + if (!child.isText) continue + let str = child.text! + while (i < frag.childCount) { + let next = frag.child(i++) + pos += next.nodeSize + if (!next.isText) break + str += next.text + } + if (pos >= from) { + if (pos >= to && str.slice(to - text.length - childStart, to - childStart) == text) + return to - text.length + let found = childStart < to ? str.lastIndexOf(text, to - childStart - 1) : -1 + if (found >= 0 && found + text.length + childStart >= from) + return childStart + found + if (from == to && str.length >= (to + text.length) - childStart && + str.slice(to - childStart, to - childStart + text.length) == text) + return to + } + } + return -1 +} + +// Replace range from-to in an array of view descs with replacement +// (may be null to just delete). This goes very much against the grain +// of the rest of this code, which tends to create nodes with the +// right shape in one go, rather than messing with them after +// creation, but is necessary in the composition hack. +function replaceNodes(nodes: readonly ViewDesc[], from: number, to: number, view: EditorView, replacement?: ViewDesc) { + let result = [] + for (let i = 0, off = 0; i < nodes.length; i++) { + let child = nodes[i], start = off, end = off += child.size + if (start >= to || end <= from) { + result.push(child) + } else { + if (start < from) result.push((child as MarkViewDesc | TextViewDesc).slice(0, from - start, view)) + if (replacement) { + result.push(replacement) + replacement = undefined + } + if (end > to) result.push((child as MarkViewDesc | TextViewDesc).slice(to - start, child.size, view)) + } + } + return result +} diff --git a/third_party/js/prosemirror/prosemirror-view/style/prosemirror.css b/third_party/js/prosemirror/prosemirror-view/style/prosemirror.css @@ -0,0 +1,54 @@ +.ProseMirror { + position: relative; +} + +.ProseMirror { + word-wrap: break-word; + white-space: pre-wrap; + white-space: break-spaces; + -webkit-font-variant-ligatures: none; + font-variant-ligatures: none; + font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */ +} + +.ProseMirror pre { + white-space: pre-wrap; +} + +.ProseMirror li { + position: relative; +} + +.ProseMirror-hideselection *::selection { background: transparent; } +.ProseMirror-hideselection *::-moz-selection { background: transparent; } +.ProseMirror-hideselection { caret-color: transparent; } + +/* See https://github.com/ProseMirror/prosemirror/issues/1421#issuecomment-1759320191 */ +.ProseMirror [draggable][contenteditable=false] { user-select: text } + +.ProseMirror-selectednode { + outline: 2px solid #8cf; +} + +/* Make sure li selections wrap around markers */ + +li.ProseMirror-selectednode { + outline: none; +} + +li.ProseMirror-selectednode:after { + content: ""; + position: absolute; + left: -32px; + right: -2px; top: -2px; bottom: -2px; + border: 2px solid #8cf; + pointer-events: none; +} + +/* Protect against generic img rules */ + +img.ProseMirror-separator { + display: inline !important; + border: none !important; + margin: 0 !important; +} diff --git a/third_party/js/prosemirror/prosemirror.bundle.mjs b/third_party/js/prosemirror/prosemirror.bundle.mjs @@ -0,0 +1 @@ +function e(e){this.content=e}function t(e,n,r){for(let i=0;;i++){if(i==e.childCount||i==n.childCount)return e.childCount==n.childCount?null:r;let o=e.child(i),s=n.child(i);if(o!=s){if(!o.sameMarkup(s))return r;if(o.isText&&o.text!=s.text){for(let e=0;o.text[e]==s.text[e];e++)r++;return r}if(o.content.size||s.content.size){let e=t(o.content,s.content,r+1);if(null!=e)return e}r+=o.nodeSize}else r+=o.nodeSize}}function n(e,t,r,i){for(let o=e.childCount,s=t.childCount;;){if(0==o||0==s)return o==s?null:{a:r,b:i};let l=e.child(--o),a=t.child(--s),c=l.nodeSize;if(l!=a){if(!l.sameMarkup(a))return{a:r,b:i};if(l.isText&&l.text!=a.text){let e=0,t=Math.min(l.text.length,a.text.length);for(;e<t&&l.text[l.text.length-e-1]==a.text[a.text.length-e-1];)e++,r--,i--;return{a:r,b:i}}if(l.content.size||a.content.size){let e=n(l.content,a.content,r-1,i-1);if(e)return e}r-=c,i-=c}else r-=c,i-=c}}e.prototype={constructor:e,find:function(e){for(var t=0;t<this.content.length;t+=2)if(this.content[t]===e)return t;return-1},get:function(e){var t=this.find(e);return-1==t?void 0:this.content[t+1]},update:function(t,n,r){var i=r&&r!=t?this.remove(r):this,o=i.find(t),s=i.content.slice();return-1==o?s.push(r||t,n):(s[o+1]=n,r&&(s[o]=r)),new e(s)},remove:function(t){var n=this.find(t);if(-1==n)return this;var r=this.content.slice();return r.splice(n,2),new e(r)},addToStart:function(t,n){return new e([t,n].concat(this.remove(t).content))},addToEnd:function(t,n){var r=this.remove(t).content.slice();return r.push(t,n),new e(r)},addBefore:function(t,n,r){var i=this.remove(n),o=i.content.slice(),s=i.find(t);return o.splice(-1==s?o.length:s,0,n,r),new e(o)},forEach:function(e){for(var t=0;t<this.content.length;t+=2)e(this.content[t],this.content[t+1])},prepend:function(t){return(t=e.from(t)).size?new e(t.content.concat(this.subtract(t).content)):this},append:function(t){return(t=e.from(t)).size?new e(this.subtract(t).content.concat(t.content)):this},subtract:function(t){var n=this;t=e.from(t);for(var r=0;r<t.content.length;r+=2)n=n.remove(t.content[r]);return n},toObject:function(){var e={};return this.forEach(function(t,n){e[t]=n}),e},get size(){return this.content.length>>1}},e.from=function(t){if(t instanceof e)return t;var n=[];if(t)for(var r in t)n.push(r,t[r]);return new e(n)};class r{constructor(e,t){if(this.content=e,this.size=t||0,null==t)for(let t=0;t<e.length;t++)this.size+=e[t].nodeSize}nodesBetween(e,t,n,r=0,i){for(let o=0,s=0;s<t;o++){let l=this.content[o],a=s+l.nodeSize;if(a>e&&!1!==n(l,r+s,i||null,o)&&l.content.size){let i=s+1;l.nodesBetween(Math.max(0,e-i),Math.min(l.content.size,t-i),n,r+i)}s=a}}descendants(e){this.nodesBetween(0,this.size,e)}textBetween(e,t,n,r){let i="",o=!0;return this.nodesBetween(e,t,(s,l)=>{let a=s.isText?s.text.slice(Math.max(e,l)-l,t-l):s.isLeaf?r?"function"==typeof r?r(s):r:s.type.spec.leafText?s.type.spec.leafText(s):"":"";s.isBlock&&(s.isLeaf&&a||s.isTextblock)&&n&&(o?o=!1:i+=n),i+=a},0),i}append(e){if(!e.size)return this;if(!this.size)return e;let t=this.lastChild,n=e.firstChild,i=this.content.slice(),o=0;for(t.isText&&t.sameMarkup(n)&&(i[i.length-1]=t.withText(t.text+n.text),o=1);o<e.content.length;o++)i.push(e.content[o]);return new r(i,this.size+e.size)}cut(e,t=this.size){if(0==e&&t==this.size)return this;let n=[],i=0;if(t>e)for(let r=0,o=0;o<t;r++){let s=this.content[r],l=o+s.nodeSize;l>e&&((o<e||l>t)&&(s=s.isText?s.cut(Math.max(0,e-o),Math.min(s.text.length,t-o)):s.cut(Math.max(0,e-o-1),Math.min(s.content.size,t-o-1))),n.push(s),i+=s.nodeSize),o=l}return new r(n,i)}cutByIndex(e,t){return e==t?r.empty:0==e&&t==this.content.length?this:new r(this.content.slice(e,t))}replaceChild(e,t){let n=this.content[e];if(n==t)return this;let i=this.content.slice(),o=this.size+t.nodeSize-n.nodeSize;return i[e]=t,new r(i,o)}addToStart(e){return new r([e].concat(this.content),this.size+e.nodeSize)}addToEnd(e){return new r(this.content.concat(e),this.size+e.nodeSize)}eq(e){if(this.content.length!=e.content.length)return!1;for(let t=0;t<this.content.length;t++)if(!this.content[t].eq(e.content[t]))return!1;return!0}get firstChild(){return this.content.length?this.content[0]:null}get lastChild(){return this.content.length?this.content[this.content.length-1]:null}get childCount(){return this.content.length}child(e){let t=this.content[e];if(!t)throw new RangeError("Index "+e+" out of range for "+this);return t}maybeChild(e){return this.content[e]||null}forEach(e){for(let t=0,n=0;t<this.content.length;t++){let r=this.content[t];e(r,n,t),n+=r.nodeSize}}findDiffStart(e,n=0){return t(this,e,n)}findDiffEnd(e,t=this.size,r=e.size){return n(this,e,t,r)}findIndex(e){if(0==e)return o(0,e);if(e==this.size)return o(this.content.length,e);if(e>this.size||e<0)throw new RangeError(`Position ${e} outside of fragment (${this})`);for(let t=0,n=0;;t++){let r=n+this.child(t).nodeSize;if(r>=e)return r==e?o(t+1,r):o(t,n);n=r}}toString(){return"<"+this.toStringInner()+">"}toStringInner(){return this.content.join(", ")}toJSON(){return this.content.length?this.content.map(e=>e.toJSON()):null}static fromJSON(e,t){if(!t)return r.empty;if(!Array.isArray(t))throw new RangeError("Invalid input for Fragment.fromJSON");return new r(t.map(e.nodeFromJSON))}static fromArray(e){if(!e.length)return r.empty;let t,n=0;for(let r=0;r<e.length;r++){let i=e[r];n+=i.nodeSize,r&&i.isText&&e[r-1].sameMarkup(i)?(t||(t=e.slice(0,r)),t[t.length-1]=i.withText(t[t.length-1].text+i.text)):t&&t.push(i)}return new r(t||e,n)}static from(e){if(!e)return r.empty;if(e instanceof r)return e;if(Array.isArray(e))return this.fromArray(e);if(e.attrs)return new r([e],e.nodeSize);throw new RangeError("Can not convert "+e+" to a Fragment"+(e.nodesBetween?" (looks like multiple versions of prosemirror-model were loaded)":""))}}r.empty=new r([],0);const i={index:0,offset:0};function o(e,t){return i.index=e,i.offset=t,i}function s(e,t){if(e===t)return!0;if(!e||"object"!=typeof e||!t||"object"!=typeof t)return!1;let n=Array.isArray(e);if(Array.isArray(t)!=n)return!1;if(n){if(e.length!=t.length)return!1;for(let n=0;n<e.length;n++)if(!s(e[n],t[n]))return!1}else{for(let n in e)if(!(n in t)||!s(e[n],t[n]))return!1;for(let n in t)if(!(n in e))return!1}return!0}class l{constructor(e,t){this.type=e,this.attrs=t}addToSet(e){let t,n=!1;for(let r=0;r<e.length;r++){let i=e[r];if(this.eq(i))return e;if(this.type.excludes(i.type))t||(t=e.slice(0,r));else{if(i.type.excludes(this.type))return e;!n&&i.type.rank>this.type.rank&&(t||(t=e.slice(0,r)),t.push(this),n=!0),t&&t.push(i)}}return t||(t=e.slice()),n||t.push(this),t}removeFromSet(e){for(let t=0;t<e.length;t++)if(this.eq(e[t]))return e.slice(0,t).concat(e.slice(t+1));return e}isInSet(e){for(let t=0;t<e.length;t++)if(this.eq(e[t]))return!0;return!1}eq(e){return this==e||this.type==e.type&&s(this.attrs,e.attrs)}toJSON(){let e={type:this.type.name};for(let t in this.attrs){e.attrs=this.attrs;break}return e}static fromJSON(e,t){if(!t)throw new RangeError("Invalid input for Mark.fromJSON");let n=e.marks[t.type];if(!n)throw new RangeError(`There is no mark type ${t.type} in this schema`);let r=n.create(t.attrs);return n.checkAttrs(r.attrs),r}static sameSet(e,t){if(e==t)return!0;if(e.length!=t.length)return!1;for(let n=0;n<e.length;n++)if(!e[n].eq(t[n]))return!1;return!0}static setFrom(e){if(!e||Array.isArray(e)&&0==e.length)return l.none;if(e instanceof l)return[e];let t=e.slice();return t.sort((e,t)=>e.type.rank-t.type.rank),t}}l.none=[];class a extends Error{}class c{constructor(e,t,n){this.content=e,this.openStart=t,this.openEnd=n}get size(){return this.content.size-this.openStart-this.openEnd}insertAt(e,t){let n=u(this.content,e+this.openStart,t);return n&&new c(n,this.openStart,this.openEnd)}removeBetween(e,t){return new c(h(this.content,e+this.openStart,t+this.openStart),this.openStart,this.openEnd)}eq(e){return this.content.eq(e.content)&&this.openStart==e.openStart&&this.openEnd==e.openEnd}toString(){return this.content+"("+this.openStart+","+this.openEnd+")"}toJSON(){if(!this.content.size)return null;let e={content:this.content.toJSON()};return this.openStart>0&&(e.openStart=this.openStart),this.openEnd>0&&(e.openEnd=this.openEnd),e}static fromJSON(e,t){if(!t)return c.empty;let n=t.openStart||0,i=t.openEnd||0;if("number"!=typeof n||"number"!=typeof i)throw new RangeError("Invalid input for Slice.fromJSON");return new c(r.fromJSON(e,t.content),n,i)}static maxOpen(e,t=!0){let n=0,r=0;for(let r=e.firstChild;r&&!r.isLeaf&&(t||!r.type.spec.isolating);r=r.firstChild)n++;for(let n=e.lastChild;n&&!n.isLeaf&&(t||!n.type.spec.isolating);n=n.lastChild)r++;return new c(e,n,r)}}function h(e,t,n){let{index:r,offset:i}=e.findIndex(t),o=e.maybeChild(r),{index:s,offset:l}=e.findIndex(n);if(i==t||o.isText){if(l!=n&&!e.child(s).isText)throw new RangeError("Removing non-flat range");return e.cut(0,t).append(e.cut(n))}if(r!=s)throw new RangeError("Removing non-flat range");return e.replaceChild(r,o.copy(h(o.content,t-i-1,n-i-1)))}function u(e,t,n,r){let{index:i,offset:o}=e.findIndex(t),s=e.maybeChild(i);if(o==t||s.isText)return r&&!r.canReplace(i,i,n)?null:e.cut(0,t).append(n).append(e.cut(t));let l=u(s.content,t-o-1,n,s);return l&&e.replaceChild(i,s.copy(l))}function p(e,t,n){if(n.openStart>e.depth)throw new a("Inserted content deeper than insertion position");if(e.depth-n.openStart!=t.depth-n.openEnd)throw new a("Inconsistent open depths");return d(e,t,n,0)}function d(e,t,n,i){let o=e.index(i),s=e.node(i);if(o==t.index(i)&&i<e.depth-n.openStart){let r=d(e,t,n,i+1);return s.copy(s.content.replaceChild(o,r))}if(n.content.size){if(n.openStart||n.openEnd||e.depth!=i||t.depth!=i){let{start:o,end:l}=function(e,t){let n=t.depth-e.openStart,i=t.node(n).copy(e.content);for(let e=n-1;e>=0;e--)i=t.node(e).copy(r.from(i));return{start:i.resolveNoCache(e.openStart+n),end:i.resolveNoCache(i.content.size-e.openEnd-n)}}(n,e);return k(s,b(e,o,l,t,i))}{let r=e.parent,i=r.content;return k(r,i.cut(0,e.parentOffset).append(n.content).append(i.cut(t.parentOffset)))}}return k(s,w(e,t,i))}function f(e,t){if(!t.type.compatibleContent(e.type))throw new a("Cannot join "+t.type.name+" onto "+e.type.name)}function m(e,t,n){let r=e.node(n);return f(r,t.node(n)),r}function g(e,t){let n=t.length-1;n>=0&&e.isText&&e.sameMarkup(t[n])?t[n]=e.withText(t[n].text+e.text):t.push(e)}function y(e,t,n,r){let i=(t||e).node(n),o=0,s=t?t.index(n):i.childCount;e&&(o=e.index(n),e.depth>n?o++:e.textOffset&&(g(e.nodeAfter,r),o++));for(let e=o;e<s;e++)g(i.child(e),r);t&&t.depth==n&&t.textOffset&&g(t.nodeBefore,r)}function k(e,t){return e.type.checkContent(t),e.copy(t)}function b(e,t,n,i,o){let s=e.depth>o&&m(e,t,o+1),l=i.depth>o&&m(n,i,o+1),a=[];return y(null,e,o,a),s&&l&&t.index(o)==n.index(o)?(f(s,l),g(k(s,b(e,t,n,i,o+1)),a)):(s&&g(k(s,w(e,t,o+1)),a),y(t,n,o,a),l&&g(k(l,w(n,i,o+1)),a)),y(i,null,o,a),new r(a)}function w(e,t,n){let i=[];if(y(null,e,n,i),e.depth>n){g(k(m(e,t,n+1),w(e,t,n+1)),i)}return y(t,null,n,i),new r(i)}c.empty=new c(r.empty,0,0);class C{constructor(e,t,n){this.pos=e,this.path=t,this.parentOffset=n,this.depth=t.length/3-1}resolveDepth(e){return null==e?this.depth:e<0?this.depth+e:e}get parent(){return this.node(this.depth)}get doc(){return this.node(0)}node(e){return this.path[3*this.resolveDepth(e)]}index(e){return this.path[3*this.resolveDepth(e)+1]}indexAfter(e){return e=this.resolveDepth(e),this.index(e)+(e!=this.depth||this.textOffset?1:0)}start(e){return 0==(e=this.resolveDepth(e))?0:this.path[3*e-1]+1}end(e){return e=this.resolveDepth(e),this.start(e)+this.node(e).content.size}before(e){if(!(e=this.resolveDepth(e)))throw new RangeError("There is no position before the top-level node");return e==this.depth+1?this.pos:this.path[3*e-1]}after(e){if(!(e=this.resolveDepth(e)))throw new RangeError("There is no position after the top-level node");return e==this.depth+1?this.pos:this.path[3*e-1]+this.path[3*e].nodeSize}get textOffset(){return this.pos-this.path[this.path.length-1]}get nodeAfter(){let e=this.parent,t=this.index(this.depth);if(t==e.childCount)return null;let n=this.pos-this.path[this.path.length-1],r=e.child(t);return n?e.child(t).cut(n):r}get nodeBefore(){let e=this.index(this.depth),t=this.pos-this.path[this.path.length-1];return t?this.parent.child(e).cut(0,t):0==e?null:this.parent.child(e-1)}posAtIndex(e,t){t=this.resolveDepth(t);let n=this.path[3*t],r=0==t?0:this.path[3*t-1]+1;for(let t=0;t<e;t++)r+=n.child(t).nodeSize;return r}marks(){let e=this.parent,t=this.index();if(0==e.content.size)return l.none;if(this.textOffset)return e.child(t).marks;let n=e.maybeChild(t-1),r=e.maybeChild(t);if(!n){let e=n;n=r,r=e}let i=n.marks;for(var o=0;o<i.length;o++)!1!==i[o].type.spec.inclusive||r&&i[o].isInSet(r.marks)||(i=i[o--].removeFromSet(i));return i}marksAcross(e){let t=this.parent.maybeChild(this.index());if(!t||!t.isInline)return null;let n=t.marks,r=e.parent.maybeChild(e.index());for(var i=0;i<n.length;i++)!1!==n[i].type.spec.inclusive||r&&n[i].isInSet(r.marks)||(n=n[i--].removeFromSet(n));return n}sharedDepth(e){for(let t=this.depth;t>0;t--)if(this.start(t)<=e&&this.end(t)>=e)return t;return 0}blockRange(e=this,t){if(e.pos<this.pos)return e.blockRange(this);for(let n=this.depth-(this.parent.inlineContent||this.pos==e.pos?1:0);n>=0;n--)if(e.pos<=this.end(n)&&(!t||t(this.node(n))))return new _(this,e,n);return null}sameParent(e){return this.pos-this.parentOffset==e.pos-e.parentOffset}max(e){return e.pos>this.pos?e:this}min(e){return e.pos<this.pos?e:this}toString(){let e="";for(let t=1;t<=this.depth;t++)e+=(e?"/":"")+this.node(t).type.name+"_"+this.index(t-1);return e+":"+this.parentOffset}static resolve(e,t){if(!(t>=0&&t<=e.content.size))throw new RangeError("Position "+t+" out of range");let n=[],r=0,i=t;for(let t=e;;){let{index:e,offset:o}=t.content.findIndex(i),s=i-o;if(n.push(t,e,r+o),!s)break;if(t=t.child(e),t.isText)break;i=s-1,r+=o+1}return new C(t,n,i)}static resolveCached(e,t){let n=v.get(e);if(n)for(let e=0;e<n.elts.length;e++){let r=n.elts[e];if(r.pos==t)return r}else v.set(e,n=new x);let r=n.elts[n.i]=C.resolve(e,t);return n.i=(n.i+1)%D,r}}class x{constructor(){this.elts=[],this.i=0}}const D=12,v=new WeakMap;class _{constructor(e,t,n){this.$from=e,this.$to=t,this.depth=n}get start(){return this.$from.before(this.depth+1)}get end(){return this.$to.after(this.depth+1)}get parent(){return this.$from.node(this.depth)}get startIndex(){return this.$from.index(this.depth)}get endIndex(){return this.$to.indexAfter(this.depth)}}const S=Object.create(null);class E{constructor(e,t,n,i=l.none){this.type=e,this.attrs=t,this.marks=i,this.content=n||r.empty}get children(){return this.content.content}get nodeSize(){return this.isLeaf?1:2+this.content.size}get childCount(){return this.content.childCount}child(e){return this.content.child(e)}maybeChild(e){return this.content.maybeChild(e)}forEach(e){this.content.forEach(e)}nodesBetween(e,t,n,r=0){this.content.nodesBetween(e,t,n,r,this)}descendants(e){this.nodesBetween(0,this.content.size,e)}get textContent(){return this.isLeaf&&this.type.spec.leafText?this.type.spec.leafText(this):this.textBetween(0,this.content.size,"")}textBetween(e,t,n,r){return this.content.textBetween(e,t,n,r)}get firstChild(){return this.content.firstChild}get lastChild(){return this.content.lastChild}eq(e){return this==e||this.sameMarkup(e)&&this.content.eq(e.content)}sameMarkup(e){return this.hasMarkup(e.type,e.attrs,e.marks)}hasMarkup(e,t,n){return this.type==e&&s(this.attrs,t||e.defaultAttrs||S)&&l.sameSet(this.marks,n||l.none)}copy(e=null){return e==this.content?this:new E(this.type,this.attrs,e,this.marks)}mark(e){return e==this.marks?this:new E(this.type,this.attrs,this.content,e)}cut(e,t=this.content.size){return 0==e&&t==this.content.size?this:this.copy(this.content.cut(e,t))}slice(e,t=this.content.size,n=!1){if(e==t)return c.empty;let r=this.resolve(e),i=this.resolve(t),o=n?0:r.sharedDepth(t),s=r.start(o),l=r.node(o).content.cut(r.pos-s,i.pos-s);return new c(l,r.depth-o,i.depth-o)}replace(e,t,n){return p(this.resolve(e),this.resolve(t),n)}nodeAt(e){for(let t=this;;){let{index:n,offset:r}=t.content.findIndex(e);if(t=t.maybeChild(n),!t)return null;if(r==e||t.isText)return t;e-=r+1}}childAfter(e){let{index:t,offset:n}=this.content.findIndex(e);return{node:this.content.maybeChild(t),index:t,offset:n}}childBefore(e){if(0==e)return{node:null,index:0,offset:0};let{index:t,offset:n}=this.content.findIndex(e);if(n<e)return{node:this.content.child(t),index:t,offset:n};let r=this.content.child(t-1);return{node:r,index:t-1,offset:n-r.nodeSize}}resolve(e){return C.resolveCached(this,e)}resolveNoCache(e){return C.resolve(this,e)}rangeHasMark(e,t,n){let r=!1;return t>e&&this.nodesBetween(e,t,e=>(n.isInSet(e.marks)&&(r=!0),!r)),r}get isBlock(){return this.type.isBlock}get isTextblock(){return this.type.isTextblock}get inlineContent(){return this.type.inlineContent}get isInline(){return this.type.isInline}get isText(){return this.type.isText}get isLeaf(){return this.type.isLeaf}get isAtom(){return this.type.isAtom}toString(){if(this.type.spec.toDebugString)return this.type.spec.toDebugString(this);let e=this.type.name;return this.content.size&&(e+="("+this.content.toStringInner()+")"),M(this.marks,e)}contentMatchAt(e){let t=this.type.contentMatch.matchFragment(this.content,0,e);if(!t)throw new Error("Called contentMatchAt on a node with invalid content");return t}canReplace(e,t,n=r.empty,i=0,o=n.childCount){let s=this.contentMatchAt(e).matchFragment(n,i,o),l=s&&s.matchFragment(this.content,t);if(!l||!l.validEnd)return!1;for(let e=i;e<o;e++)if(!this.type.allowsMarks(n.child(e).marks))return!1;return!0}canReplaceWith(e,t,n,r){if(r&&!this.type.allowsMarks(r))return!1;let i=this.contentMatchAt(e).matchType(n),o=i&&i.matchFragment(this.content,t);return!!o&&o.validEnd}canAppend(e){return e.content.size?this.canReplace(this.childCount,this.childCount,e.content):this.type.compatibleContent(e.type)}check(){this.type.checkContent(this.content),this.type.checkAttrs(this.attrs);let e=l.none;for(let t=0;t<this.marks.length;t++){let n=this.marks[t];n.type.checkAttrs(n.attrs),e=n.addToSet(e)}if(!l.sameSet(e,this.marks))throw new RangeError(`Invalid collection of marks for node ${this.type.name}: ${this.marks.map(e=>e.type.name)}`);this.content.forEach(e=>e.check())}toJSON(){let e={type:this.type.name};for(let t in this.attrs){e.attrs=this.attrs;break}return this.content.size&&(e.content=this.content.toJSON()),this.marks.length&&(e.marks=this.marks.map(e=>e.toJSON())),e}static fromJSON(e,t){if(!t)throw new RangeError("Invalid input for Node.fromJSON");let n;if(t.marks){if(!Array.isArray(t.marks))throw new RangeError("Invalid mark data for Node.fromJSON");n=t.marks.map(e.markFromJSON)}if("text"==t.type){if("string"!=typeof t.text)throw new RangeError("Invalid text node in JSON");return e.text(t.text,n)}let i=r.fromJSON(e,t.content),o=e.nodeType(t.type).create(t.attrs,i,n);return o.type.checkAttrs(o.attrs),o}}E.prototype.text=void 0;class A extends E{constructor(e,t,n,r){if(super(e,t,null,r),!n)throw new RangeError("Empty text nodes are not allowed");this.text=n}toString(){return this.type.spec.toDebugString?this.type.spec.toDebugString(this):M(this.marks,JSON.stringify(this.text))}get textContent(){return this.text}textBetween(e,t){return this.text.slice(e,t)}get nodeSize(){return this.text.length}mark(e){return e==this.marks?this:new A(this.type,this.attrs,this.text,e)}withText(e){return e==this.text?this:new A(this.type,this.attrs,e,this.marks)}cut(e=0,t=this.text.length){return 0==e&&t==this.text.length?this:this.withText(this.text.slice(e,t))}eq(e){return this.sameMarkup(e)&&this.text==e.text}toJSON(){let e=super.toJSON();return e.text=this.text,e}}function M(e,t){for(let n=e.length-1;n>=0;n--)t=e[n].type.name+"("+t+")";return t}class O{constructor(e){this.validEnd=e,this.next=[],this.wrapCache=[]}static parse(e,t){let n=new N(e,t);if(null==n.next)return O.empty;let r=F(n);n.next&&n.err("Unexpected trailing text");let i=function(e){let t=Object.create(null);return n(P(e,0));function n(r){let i=[];r.forEach(t=>{e[t].forEach(({term:t,to:n})=>{if(!t)return;let r;for(let e=0;e<i.length;e++)i[e][0]==t&&(r=i[e][1]);P(e,n).forEach(e=>{r||i.push([t,r=[]]),-1==r.indexOf(e)&&r.push(e)})})});let o=t[r.join(",")]=new O(r.indexOf(e.length-1)>-1);for(let e=0;e<i.length;e++){let r=i[e][1].sort(B);o.next.push({type:i[e][0],next:t[r.join(",")]||n(r)})}return o}}(function(e){let t=[[]];return i(o(e,0),n()),t;function n(){return t.push([])-1}function r(e,n,r){let i={term:r,to:n};return t[e].push(i),i}function i(e,t){e.forEach(e=>e.to=t)}function o(e,t){if("choice"==e.type)return e.exprs.reduce((e,n)=>e.concat(o(n,t)),[]);if("seq"!=e.type){if("star"==e.type){let s=n();return r(t,s),i(o(e.expr,s),s),[r(s)]}if("plus"==e.type){let s=n();return i(o(e.expr,t),s),i(o(e.expr,s),s),[r(s)]}if("opt"==e.type)return[r(t)].concat(o(e.expr,t));if("range"==e.type){let s=t;for(let t=0;t<e.min;t++){let t=n();i(o(e.expr,s),t),s=t}if(-1==e.max)i(o(e.expr,s),s);else for(let t=e.min;t<e.max;t++){let t=n();r(s,t),i(o(e.expr,s),t),s=t}return[r(s)]}if("name"==e.type)return[r(t,void 0,e.value)];throw new Error("Unknown expr type")}for(let r=0;;r++){let s=o(e.exprs[r],t);if(r==e.exprs.length-1)return s;i(s,t=n())}}}(r));return function(e,t){for(let n=0,r=[e];n<r.length;n++){let e=r[n],i=!e.validEnd,o=[];for(let t=0;t<e.next.length;t++){let{type:n,next:s}=e.next[t];o.push(n.name),!i||n.isText||n.hasRequiredAttrs()||(i=!1),-1==r.indexOf(s)&&r.push(s)}i&&t.err("Only non-generatable nodes ("+o.join(", ")+") in a required position (see https://prosemirror.net/docs/guide/#generatable)")}}(i,n),i}matchType(e){for(let t=0;t<this.next.length;t++)if(this.next[t].type==e)return this.next[t].next;return null}matchFragment(e,t=0,n=e.childCount){let r=this;for(let i=t;r&&i<n;i++)r=r.matchType(e.child(i).type);return r}get inlineContent(){return 0!=this.next.length&&this.next[0].type.isInline}get defaultType(){for(let e=0;e<this.next.length;e++){let{type:t}=this.next[e];if(!t.isText&&!t.hasRequiredAttrs())return t}return null}compatible(e){for(let t=0;t<this.next.length;t++)for(let n=0;n<e.next.length;n++)if(this.next[t].type==e.next[n].type)return!0;return!1}fillBefore(e,t=!1,n=0){let i=[this];return function o(s,l){let a=s.matchFragment(e,n);if(a&&(!t||a.validEnd))return r.from(l.map(e=>e.createAndFill()));for(let e=0;e<s.next.length;e++){let{type:t,next:n}=s.next[e];if(!t.isText&&!t.hasRequiredAttrs()&&-1==i.indexOf(n)){i.push(n);let e=o(n,l.concat(t));if(e)return e}}return null}(this,[])}findWrapping(e){for(let t=0;t<this.wrapCache.length;t+=2)if(this.wrapCache[t]==e)return this.wrapCache[t+1];let t=this.computeWrapping(e);return this.wrapCache.push(e,t),t}computeWrapping(e){let t=Object.create(null),n=[{match:this,type:null,via:null}];for(;n.length;){let r=n.shift(),i=r.match;if(i.matchType(e)){let e=[];for(let t=r;t.type;t=t.via)e.push(t.type);return e.reverse()}for(let e=0;e<i.next.length;e++){let{type:o,next:s}=i.next[e];o.isLeaf||o.hasRequiredAttrs()||o.name in t||r.type&&!s.validEnd||(n.push({match:o.contentMatch,type:o,via:r}),t[o.name]=!0)}}return null}get edgeCount(){return this.next.length}edge(e){if(e>=this.next.length)throw new RangeError(`There's no ${e}th edge in this content match`);return this.next[e]}toString(){let e=[];return function t(n){e.push(n);for(let r=0;r<n.next.length;r++)-1==e.indexOf(n.next[r].next)&&t(n.next[r].next)}(this),e.map((t,n)=>{let r=n+(t.validEnd?"*":" ")+" ";for(let n=0;n<t.next.length;n++)r+=(n?", ":"")+t.next[n].type.name+"->"+e.indexOf(t.next[n].next);return r}).join("\n")}}O.empty=new O(!0);class N{constructor(e,t){this.string=e,this.nodeTypes=t,this.inline=null,this.pos=0,this.tokens=e.split(/\s*(?=\b|\W|$)/),""==this.tokens[this.tokens.length-1]&&this.tokens.pop(),""==this.tokens[0]&&this.tokens.shift()}get next(){return this.tokens[this.pos]}eat(e){return this.next==e&&(this.pos++||!0)}err(e){throw new SyntaxError(e+" (in content expression '"+this.string+"')")}}function F(e){let t=[];do{t.push(T(e))}while(e.eat("|"));return 1==t.length?t[0]:{type:"choice",exprs:t}}function T(e){let t=[];do{t.push(R(e))}while(e.next&&")"!=e.next&&"|"!=e.next);return 1==t.length?t[0]:{type:"seq",exprs:t}}function R(e){let t=function(e){if(e.eat("(")){let t=F(e);return e.eat(")")||e.err("Missing closing paren"),t}if(!/\W/.test(e.next)){let t=function(e,t){let n=e.nodeTypes,r=n[t];if(r)return[r];let i=[];for(let e in n){let r=n[e];r.isInGroup(t)&&i.push(r)}0==i.length&&e.err("No node type or group '"+t+"' found");return i}(e,e.next).map(t=>(null==e.inline?e.inline=t.isInline:e.inline!=t.isInline&&e.err("Mixing inline and block content"),{type:"name",value:t}));return e.pos++,1==t.length?t[0]:{type:"choice",exprs:t}}e.err("Unexpected token '"+e.next+"'")}(e);for(;;)if(e.eat("+"))t={type:"plus",expr:t};else if(e.eat("*"))t={type:"star",expr:t};else if(e.eat("?"))t={type:"opt",expr:t};else{if(!e.eat("{"))break;t=I(e,t)}return t}function z(e){/\D/.test(e.next)&&e.err("Expected number, got '"+e.next+"'");let t=Number(e.next);return e.pos++,t}function I(e,t){let n=z(e),r=n;return e.eat(",")&&(r="}"!=e.next?z(e):-1),e.eat("}")||e.err("Unclosed braced range"),{type:"range",min:n,max:r,expr:t}}function B(e,t){return t-e}function P(e,t){let n=[];return function t(r){let i=e[r];if(1==i.length&&!i[0].term)return t(i[0].to);n.push(r);for(let e=0;e<i.length;e++){let{term:r,to:o}=i[e];r||-1!=n.indexOf(o)||t(o)}}(t),n.sort(B)}function L(e){let t=Object.create(null);for(let n in e){let r=e[n];if(!r.hasDefault)return null;t[n]=r.default}return t}function q(e,t){let n=Object.create(null);for(let r in e){let i=t&&t[r];if(void 0===i){let t=e[r];if(!t.hasDefault)throw new RangeError("No value supplied for attribute "+r);i=t.default}n[r]=i}return n}function $(e,t,n,r){for(let r in t)if(!(r in e))throw new RangeError(`Unsupported attribute ${r} for ${n} of type ${r}`);for(let n in e){let r=e[n];r.validate&&r.validate(t[n])}}function V(e,t){let n=Object.create(null);if(t)for(let r in t)n[r]=new J(e,r,t[r]);return n}let j=class e{constructor(e,t,n){this.name=e,this.schema=t,this.spec=n,this.markSet=null,this.groups=n.group?n.group.split(" "):[],this.attrs=V(e,n.attrs),this.defaultAttrs=L(this.attrs),this.contentMatch=null,this.inlineContent=null,this.isBlock=!(n.inline||"text"==e),this.isText="text"==e}get isInline(){return!this.isBlock}get isTextblock(){return this.isBlock&&this.inlineContent}get isLeaf(){return this.contentMatch==O.empty}get isAtom(){return this.isLeaf||!!this.spec.atom}isInGroup(e){return this.groups.indexOf(e)>-1}get whitespace(){return this.spec.whitespace||(this.spec.code?"pre":"normal")}hasRequiredAttrs(){for(let e in this.attrs)if(this.attrs[e].isRequired)return!0;return!1}compatibleContent(e){return this==e||this.contentMatch.compatible(e.contentMatch)}computeAttrs(e){return!e&&this.defaultAttrs?this.defaultAttrs:q(this.attrs,e)}create(e=null,t,n){if(this.isText)throw new Error("NodeType.create can't construct text nodes");return new E(this,this.computeAttrs(e),r.from(t),l.setFrom(n))}createChecked(e=null,t,n){return t=r.from(t),this.checkContent(t),new E(this,this.computeAttrs(e),t,l.setFrom(n))}createAndFill(e=null,t,n){if(e=this.computeAttrs(e),(t=r.from(t)).size){let e=this.contentMatch.fillBefore(t);if(!e)return null;t=e.append(t)}let i=this.contentMatch.matchFragment(t),o=i&&i.fillBefore(r.empty,!0);return o?new E(this,e,t.append(o),l.setFrom(n)):null}validContent(e){let t=this.contentMatch.matchFragment(e);if(!t||!t.validEnd)return!1;for(let t=0;t<e.childCount;t++)if(!this.allowsMarks(e.child(t).marks))return!1;return!0}checkContent(e){if(!this.validContent(e))throw new RangeError(`Invalid content for node ${this.name}: ${e.toString().slice(0,50)}`)}checkAttrs(e){$(this.attrs,e,"node",this.name)}allowsMarkType(e){return null==this.markSet||this.markSet.indexOf(e)>-1}allowsMarks(e){if(null==this.markSet)return!0;for(let t=0;t<e.length;t++)if(!this.allowsMarkType(e[t].type))return!1;return!0}allowedMarks(e){if(null==this.markSet)return e;let t;for(let n=0;n<e.length;n++)this.allowsMarkType(e[n].type)?t&&t.push(e[n]):t||(t=e.slice(0,n));return t?t.length?t:l.none:e}static compile(t,n){let r=Object.create(null);t.forEach((t,i)=>r[t]=new e(t,n,i));let i=n.spec.topNode||"doc";if(!r[i])throw new RangeError("Schema is missing its top node type ('"+i+"')");if(!r.text)throw new RangeError("Every schema needs a 'text' type");for(let e in r.text.attrs)throw new RangeError("The text node type should not have attributes");return r}};class J{constructor(e,t,n){this.hasDefault=Object.prototype.hasOwnProperty.call(n,"default"),this.default=n.default,this.validate="string"==typeof n.validate?function(e,t,n){let r=n.split("|");return n=>{let i=null===n?"null":typeof n;if(r.indexOf(i)<0)throw new RangeError(`Expected value of type ${r} for attribute ${t} on type ${e}, got ${i}`)}}(e,t,n.validate):n.validate}get isRequired(){return!this.hasDefault}}class W{constructor(e,t,n,r){this.name=e,this.rank=t,this.schema=n,this.spec=r,this.attrs=V(e,r.attrs),this.excluded=null;let i=L(this.attrs);this.instance=i?new l(this,i):null}create(e=null){return!e&&this.instance?this.instance:new l(this,q(this.attrs,e))}static compile(e,t){let n=Object.create(null),r=0;return e.forEach((e,i)=>n[e]=new W(e,r++,t,i)),n}removeFromSet(e){for(var t=0;t<e.length;t++)e[t].type==this&&(e=e.slice(0,t).concat(e.slice(t+1)),t--);return e}isInSet(e){for(let t=0;t<e.length;t++)if(e[t].type==this)return e[t]}checkAttrs(e){$(this.attrs,e,"mark",this.name)}excludes(e){return this.excluded.indexOf(e)>-1}}class H{constructor(t){this.linebreakReplacement=null,this.cached=Object.create(null);let n=this.spec={};for(let e in t)n[e]=t[e];n.nodes=e.from(t.nodes),n.marks=e.from(t.marks||{}),this.nodes=j.compile(this.spec.nodes,this),this.marks=W.compile(this.spec.marks,this);let r=Object.create(null);for(let e in this.nodes){if(e in this.marks)throw new RangeError(e+" can not be both a node and a mark");let t=this.nodes[e],n=t.spec.content||"",i=t.spec.marks;if(t.contentMatch=r[n]||(r[n]=O.parse(n,this.nodes)),t.inlineContent=t.contentMatch.inlineContent,t.spec.linebreakReplacement){if(this.linebreakReplacement)throw new RangeError("Multiple linebreak nodes defined");if(!t.isInline||!t.isLeaf)throw new RangeError("Linebreak replacement nodes must be inline leaf nodes");this.linebreakReplacement=t}t.markSet="_"==i?null:i?U(this,i.split(" ")):""!=i&&t.inlineContent?null:[]}for(let e in this.marks){let t=this.marks[e],n=t.spec.excludes;t.excluded=null==n?[t]:""==n?[]:U(this,n.split(" "))}this.nodeFromJSON=e=>E.fromJSON(this,e),this.markFromJSON=e=>l.fromJSON(this,e),this.topNodeType=this.nodes[this.spec.topNode||"doc"],this.cached.wrappings=Object.create(null)}node(e,t=null,n,r){if("string"==typeof e)e=this.nodeType(e);else{if(!(e instanceof j))throw new RangeError("Invalid node type: "+e);if(e.schema!=this)throw new RangeError("Node type from different schema used ("+e.name+")")}return e.createChecked(t,n,r)}text(e,t){let n=this.nodes.text;return new A(n,n.defaultAttrs,e,l.setFrom(t))}mark(e,t){return"string"==typeof e&&(e=this.marks[e]),e.create(t)}nodeType(e){let t=this.nodes[e];if(!t)throw new RangeError("Unknown node type: "+e);return t}}function U(e,t){let n=[];for(let r=0;r<t.length;r++){let i=t[r],o=e.marks[i],s=o;if(o)n.push(o);else for(let t in e.marks){let r=e.marks[t];("_"==i||r.spec.group&&r.spec.group.split(" ").indexOf(i)>-1)&&n.push(s=r)}if(!s)throw new SyntaxError("Unknown mark type: '"+t[r]+"'")}return n}class K{constructor(e,t){this.schema=e,this.rules=t,this.tags=[],this.styles=[];let n=this.matchedStyles=[];t.forEach(e=>{if(function(e){return null!=e.tag}(e))this.tags.push(e);else if(function(e){return null!=e.style}(e)){let t=/[^=]*/.exec(e.style)[0];n.indexOf(t)<0&&n.push(t),this.styles.push(e)}}),this.normalizeLists=!this.tags.some(t=>{if(!/^(ul|ol)\b/.test(t.tag)||!t.node)return!1;let n=e.nodes[t.node];return n.contentMatch.matchType(n)})}parse(e,t={}){let n=new ee(this,t,!1);return n.addAll(e,l.none,t.from,t.to),n.finish()}parseSlice(e,t={}){let n=new ee(this,t,!0);return n.addAll(e,l.none,t.from,t.to),c.maxOpen(n.finish())}matchTag(e,t,n){for(let r=n?this.tags.indexOf(n)+1:0;r<this.tags.length;r++){let n=this.tags[r];if(te(e,n.tag)&&(void 0===n.namespace||e.namespaceURI==n.namespace)&&(!n.context||t.matchesContext(n.context))){if(n.getAttrs){let t=n.getAttrs(e);if(!1===t)continue;n.attrs=t||void 0}return n}}}matchStyle(e,t,n,r){for(let i=r?this.styles.indexOf(r)+1:0;i<this.styles.length;i++){let r=this.styles[i],o=r.style;if(!(0!=o.indexOf(e)||r.context&&!n.matchesContext(r.context)||o.length>e.length&&(61!=o.charCodeAt(e.length)||o.slice(e.length+1)!=t))){if(r.getAttrs){let e=r.getAttrs(t);if(!1===e)continue;r.attrs=e||void 0}return r}}}static schemaRules(e){let t=[];function n(e){let n=null==e.priority?50:e.priority,r=0;for(;r<t.length;r++){let e=t[r];if((null==e.priority?50:e.priority)<n)break}t.splice(r,0,e)}for(let t in e.marks){let r=e.marks[t].spec.parseDOM;r&&r.forEach(e=>{n(e=ne(e)),e.mark||e.ignore||e.clearMark||(e.mark=t)})}for(let t in e.nodes){let r=e.nodes[t].spec.parseDOM;r&&r.forEach(e=>{n(e=ne(e)),e.node||e.ignore||e.mark||(e.node=t)})}return t}static fromSchema(e){return e.cached.domParser||(e.cached.domParser=new K(e,K.schemaRules(e)))}}const Z={address:!0,article:!0,aside:!0,blockquote:!0,canvas:!0,dd:!0,div:!0,dl:!0,fieldset:!0,figcaption:!0,figure:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,li:!0,noscript:!0,ol:!0,output:!0,p:!0,pre:!0,section:!0,table:!0,tfoot:!0,ul:!0},G={head:!0,noscript:!0,object:!0,script:!0,style:!0,title:!0},Q={ol:!0,ul:!0};function X(e,t,n){return null!=t?(t?1:0)|("full"===t?2:0):e&&"pre"==e.whitespace?3:-5&n}class Y{constructor(e,t,n,r,i,o){this.type=e,this.attrs=t,this.marks=n,this.solid=r,this.options=o,this.content=[],this.activeMarks=l.none,this.match=i||(4&o?null:e.contentMatch)}findWrapping(e){if(!this.match){if(!this.type)return[];let t=this.type.contentMatch.fillBefore(r.from(e));if(!t){let t,n=this.type.contentMatch;return(t=n.findWrapping(e.type))?(this.match=n,t):null}this.match=this.type.contentMatch.matchFragment(t)}return this.match.findWrapping(e.type)}finish(e){if(!(1&this.options)){let e,t=this.content[this.content.length-1];if(t&&t.isText&&(e=/[ \t\r\n\u000c]+$/.exec(t.text))){let n=t;t.text.length==e[0].length?this.content.pop():this.content[this.content.length-1]=n.withText(n.text.slice(0,n.text.length-e[0].length))}}let t=r.from(this.content);return!e&&this.match&&(t=t.append(this.match.fillBefore(r.empty,!0))),this.type?this.type.create(this.attrs,t,this.marks):t}inlineContext(e){return this.type?this.type.inlineContent:this.content.length?this.content[0].isInline:e.parentNode&&!Z.hasOwnProperty(e.parentNode.nodeName.toLowerCase())}}class ee{constructor(e,t,n){this.parser=e,this.options=t,this.isOpen=n,this.open=0,this.localPreserveWS=!1;let r,i=t.topNode,o=X(null,t.preserveWhitespace,0)|(n?4:0);r=i?new Y(i.type,i.attrs,l.none,!0,t.topMatch||i.type.contentMatch,o):new Y(n?null:e.schema.topNodeType,null,l.none,!0,null,o),this.nodes=[r],this.find=t.findPositions,this.needsBlock=!1}get top(){return this.nodes[this.open]}addDOM(e,t){3==e.nodeType?this.addTextNode(e,t):1==e.nodeType&&this.addElement(e,t)}addTextNode(e,t){let n=e.nodeValue,r=this.top,i=2&r.options?"full":this.localPreserveWS||(1&r.options)>0,{schema:o}=this.parser;if("full"===i||r.inlineContext(e)||/[^ \t\r\n\u000c]/.test(n)){if(i)if("full"===i)n=n.replace(/\r\n?/g,"\n");else if(o.linebreakReplacement&&/[\r\n]/.test(n)&&this.top.findWrapping(o.linebreakReplacement.create())){let e=n.split(/\r?\n|\r/);for(let n=0;n<e.length;n++)n&&this.insertNode(o.linebreakReplacement.create(),t,!0),e[n]&&this.insertNode(o.text(e[n]),t,!/\S/.test(e[n]));n=""}else n=n.replace(/\r?\n|\r/g," ");else if(n=n.replace(/[ \t\r\n\u000c]+/g," "),/^[ \t\r\n\u000c]/.test(n)&&this.open==this.nodes.length-1){let t=r.content[r.content.length-1],i=e.previousSibling;(!t||i&&"BR"==i.nodeName||t.isText&&/[ \t\r\n\u000c]$/.test(t.text))&&(n=n.slice(1))}n&&this.insertNode(o.text(n),t,!/\S/.test(n)),this.findInText(e)}else this.findInside(e)}addElement(e,t,n){let r=this.localPreserveWS,i=this.top;("PRE"==e.tagName||/pre/.test(e.style&&e.style.whiteSpace))&&(this.localPreserveWS=!0);let o,s=e.nodeName.toLowerCase();Q.hasOwnProperty(s)&&this.parser.normalizeLists&&function(e){for(let t=e.firstChild,n=null;t;t=t.nextSibling){let e=1==t.nodeType?t.nodeName.toLowerCase():null;e&&Q.hasOwnProperty(e)&&n?(n.appendChild(t),t=n):"li"==e?n=t:e&&(n=null)}}(e);let l=this.options.ruleFromNode&&this.options.ruleFromNode(e)||(o=this.parser.matchTag(e,this,n));e:if(l?l.ignore:G.hasOwnProperty(s))this.findInside(e),this.ignoreFallback(e,t);else if(!l||l.skip||l.closeParent){l&&l.closeParent?this.open=Math.max(0,this.open-1):l&&l.skip.nodeType&&(e=l.skip);let n,r=this.needsBlock;if(Z.hasOwnProperty(s))i.content.length&&i.content[0].isInline&&this.open&&(this.open--,i=this.top),n=!0,i.type||(this.needsBlock=!0);else if(!e.firstChild){this.leafFallback(e,t);break e}let o=l&&l.skip?t:this.readStyles(e,t);o&&this.addAll(e,o),n&&this.sync(i),this.needsBlock=r}else{let n=this.readStyles(e,t);n&&this.addElementByRule(e,l,n,!1===l.consuming?o:void 0)}this.localPreserveWS=r}leafFallback(e,t){"BR"==e.nodeName&&this.top.type&&this.top.type.inlineContent&&this.addTextNode(e.ownerDocument.createTextNode("\n"),t)}ignoreFallback(e,t){"BR"!=e.nodeName||this.top.type&&this.top.type.inlineContent||this.findPlace(this.parser.schema.text("-"),t,!0)}readStyles(e,t){let n=e.style;if(n&&n.length)for(let e=0;e<this.parser.matchedStyles.length;e++){let r=this.parser.matchedStyles[e],i=n.getPropertyValue(r);if(i)for(let e;;){let n=this.parser.matchStyle(r,i,this,e);if(!n)break;if(n.ignore)return null;if(t=n.clearMark?t.filter(e=>!n.clearMark(e)):t.concat(this.parser.schema.marks[n.mark].create(n.attrs)),!1!==n.consuming)break;e=n}}return t}addElementByRule(e,t,n,r){let i,o;if(t.node)if(o=this.parser.schema.nodes[t.node],o.isLeaf)this.insertNode(o.create(t.attrs),n,"BR"==e.nodeName)||this.leafFallback(e,n);else{let e=this.enter(o,t.attrs||null,n,t.preserveWhitespace);e&&(i=!0,n=e)}else{let e=this.parser.schema.marks[t.mark];n=n.concat(e.create(t.attrs))}let s=this.top;if(o&&o.isLeaf)this.findInside(e);else if(r)this.addElement(e,n,r);else if(t.getContent)this.findInside(e),t.getContent(e,this.parser.schema).forEach(e=>this.insertNode(e,n,!1));else{let r=e;"string"==typeof t.contentElement?r=e.querySelector(t.contentElement):"function"==typeof t.contentElement?r=t.contentElement(e):t.contentElement&&(r=t.contentElement),this.findAround(e,r,!0),this.addAll(r,n),this.findAround(e,r,!1)}i&&this.sync(s)&&this.open--}addAll(e,t,n,r){let i=n||0;for(let o=n?e.childNodes[n]:e.firstChild,s=null==r?null:e.childNodes[r];o!=s;o=o.nextSibling,++i)this.findAtPoint(e,i),this.addDOM(o,t);this.findAtPoint(e,i)}findPlace(e,t,n){let r,i;for(let t=this.open,o=0;t>=0;t--){let s=this.nodes[t],l=s.findWrapping(e);if(l&&(!r||r.length>l.length+o)&&(r=l,i=s,!l.length))break;if(s.solid){if(n)break;o+=2}}if(!r)return null;this.sync(i);for(let e=0;e<r.length;e++)t=this.enterInner(r[e],null,t,!1);return t}insertNode(e,t,n){if(e.isInline&&this.needsBlock&&!this.top.type){let e=this.textblockFromContext();e&&(t=this.enterInner(e,null,t))}let r=this.findPlace(e,t,n);if(r){this.closeExtra();let t=this.top;t.match&&(t.match=t.match.matchType(e.type));let n=l.none;for(let i of r.concat(e.marks))(t.type?t.type.allowsMarkType(i.type):re(i.type,e.type))&&(n=i.addToSet(n));return t.content.push(e.mark(n)),!0}return!1}enter(e,t,n,r){let i=this.findPlace(e.create(t),n,!1);return i&&(i=this.enterInner(e,t,n,!0,r)),i}enterInner(e,t,n,r=!1,i){this.closeExtra();let o=this.top;o.match=o.match&&o.match.matchType(e);let s=X(e,i,o.options);4&o.options&&0==o.content.length&&(s|=4);let a=l.none;return n=n.filter(t=>!(o.type?o.type.allowsMarkType(t.type):re(t.type,e))||(a=t.addToSet(a),!1)),this.nodes.push(new Y(e,t,a,r,null,s)),this.open++,n}closeExtra(e=!1){let t=this.nodes.length-1;if(t>this.open){for(;t>this.open;t--)this.nodes[t-1].content.push(this.nodes[t].finish(e));this.nodes.length=this.open+1}}finish(){return this.open=0,this.closeExtra(this.isOpen),this.nodes[0].finish(!(!this.isOpen&&!this.options.topOpen))}sync(e){for(let t=this.open;t>=0;t--){if(this.nodes[t]==e)return this.open=t,!0;this.localPreserveWS&&(this.nodes[t].options|=1)}return!1}get currentPos(){this.closeExtra();let e=0;for(let t=this.open;t>=0;t--){let n=this.nodes[t].content;for(let t=n.length-1;t>=0;t--)e+=n[t].nodeSize;t&&e++}return e}findAtPoint(e,t){if(this.find)for(let n=0;n<this.find.length;n++)this.find[n].node==e&&this.find[n].offset==t&&(this.find[n].pos=this.currentPos)}findInside(e){if(this.find)for(let t=0;t<this.find.length;t++)null==this.find[t].pos&&1==e.nodeType&&e.contains(this.find[t].node)&&(this.find[t].pos=this.currentPos)}findAround(e,t,n){if(e!=t&&this.find)for(let r=0;r<this.find.length;r++)if(null==this.find[r].pos&&1==e.nodeType&&e.contains(this.find[r].node)){t.compareDocumentPosition(this.find[r].node)&(n?2:4)&&(this.find[r].pos=this.currentPos)}}findInText(e){if(this.find)for(let t=0;t<this.find.length;t++)this.find[t].node==e&&(this.find[t].pos=this.currentPos-(e.nodeValue.length-this.find[t].offset))}matchesContext(e){if(e.indexOf("|")>-1)return e.split(/\s*\|\s*/).some(this.matchesContext,this);let t=e.split("/"),n=this.options.context,r=!(this.isOpen||n&&n.parent.type!=this.nodes[0].type),i=-(n?n.depth+1:0)+(r?0:1),o=(e,s)=>{for(;e>=0;e--){let l=t[e];if(""==l){if(e==t.length-1||0==e)continue;for(;s>=i;s--)if(o(e-1,s))return!0;return!1}{let e=s>0||0==s&&r?this.nodes[s].type:n&&s>=i?n.node(s-i).type:null;if(!e||e.name!=l&&!e.isInGroup(l))return!1;s--}}return!0};return o(t.length-1,this.open)}textblockFromContext(){let e=this.options.context;if(e)for(let t=e.depth;t>=0;t--){let n=e.node(t).contentMatchAt(e.indexAfter(t)).defaultType;if(n&&n.isTextblock&&n.defaultAttrs)return n}for(let e in this.parser.schema.nodes){let t=this.parser.schema.nodes[e];if(t.isTextblock&&t.defaultAttrs)return t}}}function te(e,t){return(e.matches||e.msMatchesSelector||e.webkitMatchesSelector||e.mozMatchesSelector).call(e,t)}function ne(e){let t={};for(let n in e)t[n]=e[n];return t}function re(e,t){let n=t.schema.nodes;for(let r in n){let i=n[r];if(!i.allowsMarkType(e))continue;let o=[],s=e=>{o.push(e);for(let n=0;n<e.edgeCount;n++){let{type:r,next:i}=e.edge(n);if(r==t)return!0;if(o.indexOf(i)<0&&s(i))return!0}};if(s(i.contentMatch))return!0}}class ie{constructor(e,t){this.nodes=e,this.marks=t}serializeFragment(e,t={},n){n||(n=se(t).createDocumentFragment());let r=n,i=[];return e.forEach(e=>{if(i.length||e.marks.length){let n=0,o=0;for(;n<i.length&&o<e.marks.length;){let t=e.marks[o];if(this.marks[t.type.name]){if(!t.eq(i[n][0])||!1===t.type.spec.spanning)break;n++,o++}else o++}for(;n<i.length;)r=i.pop()[1];for(;o<e.marks.length;){let n=e.marks[o++],s=this.serializeMark(n,e.isInline,t);s&&(i.push([n,r]),r.appendChild(s.dom),r=s.contentDOM||s.dom)}}r.appendChild(this.serializeNodeInner(e,t))}),n}serializeNodeInner(e,t){let{dom:n,contentDOM:r}=ce(se(t),this.nodes[e.type.name](e),null,e.attrs);if(r){if(e.isLeaf)throw new RangeError("Content hole not allowed in a leaf node spec");this.serializeFragment(e.content,t,r)}return n}serializeNode(e,t={}){let n=this.serializeNodeInner(e,t);for(let r=e.marks.length-1;r>=0;r--){let i=this.serializeMark(e.marks[r],e.isInline,t);i&&((i.contentDOM||i.dom).appendChild(n),n=i.dom)}return n}serializeMark(e,t,n={}){let r=this.marks[e.type.name];return r&&ce(se(n),r(e,t),null,e.attrs)}static renderSpec(e,t,n=null,r){return ce(e,t,n,r)}static fromSchema(e){return e.cached.domSerializer||(e.cached.domSerializer=new ie(this.nodesFromSchema(e),this.marksFromSchema(e)))}static nodesFromSchema(e){let t=oe(e.nodes);return t.text||(t.text=e=>e.text),t}static marksFromSchema(e){return oe(e.marks)}}function oe(e){let t={};for(let n in e){let r=e[n].spec.toDOM;r&&(t[n]=r)}return t}function se(e){return e.document||window.document}const le=new WeakMap;function ae(e){let t=le.get(e);return void 0===t&&le.set(e,t=function(e){let t=null;function n(e){if(e&&"object"==typeof e)if(Array.isArray(e))if("string"==typeof e[0])t||(t=[]),t.push(e);else for(let t=0;t<e.length;t++)n(e[t]);else for(let t in e)n(e[t])}return n(e),t}(e)),t}function ce(e,t,n,r){if("string"==typeof t)return{dom:e.createTextNode(t)};if(null!=t.nodeType)return{dom:t};if(t.dom&&null!=t.dom.nodeType)return t;let i,o=t[0];if("string"!=typeof o)throw new RangeError("Invalid array passed to renderSpec");if(r&&(i=ae(r))&&i.indexOf(t)>-1)throw new RangeError("Using an array from an attribute object as a DOM spec. This may be an attempted cross site scripting attack.");let s,l=o.indexOf(" ");l>0&&(n=o.slice(0,l),o=o.slice(l+1));let a=n?e.createElementNS(n,o):e.createElement(o),c=t[1],h=1;if(c&&"object"==typeof c&&null==c.nodeType&&!Array.isArray(c)){h=2;for(let e in c)if(null!=c[e]){let t=e.indexOf(" ");t>0?a.setAttributeNS(e.slice(0,t),e.slice(t+1),c[e]):"style"==e&&a.style?a.style.cssText=c[e]:a.setAttribute(e,c[e])}}for(let i=h;i<t.length;i++){let o=t[i];if(0===o){if(i<t.length-1||i>h)throw new RangeError("Content hole must be the only child of its parent node");return{dom:a,contentDOM:a}}{let{dom:t,contentDOM:i}=ce(e,o,n,r);if(a.appendChild(t),i){if(s)throw new RangeError("Multiple content holes");s=i}}}return{dom:a,contentDOM:s}}const he=Math.pow(2,16);function ue(e,t){return e+t*he}function pe(e){return 65535&e}class de{constructor(e,t,n){this.pos=e,this.delInfo=t,this.recover=n}get deleted(){return(8&this.delInfo)>0}get deletedBefore(){return(5&this.delInfo)>0}get deletedAfter(){return(6&this.delInfo)>0}get deletedAcross(){return(4&this.delInfo)>0}}class fe{constructor(e,t=!1){if(this.ranges=e,this.inverted=t,!e.length&&fe.empty)return fe.empty}recover(e){let t=0,n=pe(e);if(!this.inverted)for(let e=0;e<n;e++)t+=this.ranges[3*e+2]-this.ranges[3*e+1];return this.ranges[3*n]+t+function(e){return(e-(65535&e))/he}(e)}mapResult(e,t=1){return this._map(e,t,!1)}map(e,t=1){return this._map(e,t,!0)}_map(e,t,n){let r=0,i=this.inverted?2:1,o=this.inverted?1:2;for(let s=0;s<this.ranges.length;s+=3){let l=this.ranges[s]-(this.inverted?r:0);if(l>e)break;let a=this.ranges[s+i],c=this.ranges[s+o],h=l+a;if(e<=h){let i=l+r+((a?e==l?-1:e==h?1:t:t)<0?0:c);if(n)return i;let o=e==(t<0?l:h)?null:ue(s/3,e-l),u=e==l?2:e==h?1:4;return(t<0?e!=l:e!=h)&&(u|=8),new de(i,u,o)}r+=c-a}return n?e+r:new de(e+r,0,null)}touches(e,t){let n=0,r=pe(t),i=this.inverted?2:1,o=this.inverted?1:2;for(let t=0;t<this.ranges.length;t+=3){let s=this.ranges[t]-(this.inverted?n:0);if(s>e)break;let l=this.ranges[t+i];if(e<=s+l&&t==3*r)return!0;n+=this.ranges[t+o]-l}return!1}forEach(e){let t=this.inverted?2:1,n=this.inverted?1:2;for(let r=0,i=0;r<this.ranges.length;r+=3){let o=this.ranges[r],s=o-(this.inverted?i:0),l=o+(this.inverted?0:i),a=this.ranges[r+t],c=this.ranges[r+n];e(s,s+a,l,l+c),i+=c-a}}invert(){return new fe(this.ranges,!this.inverted)}toString(){return(this.inverted?"-":"")+JSON.stringify(this.ranges)}static offset(e){return 0==e?fe.empty:new fe(e<0?[0,-e,0]:[0,0,e])}}fe.empty=new fe([]);class me{constructor(e,t,n=0,r=(e?e.length:0)){this.mirror=t,this.from=n,this.to=r,this._maps=e||[],this.ownData=!(e||t)}get maps(){return this._maps}slice(e=0,t=this.maps.length){return new me(this._maps,this.mirror,e,t)}appendMap(e,t){this.ownData||(this._maps=this._maps.slice(),this.mirror=this.mirror&&this.mirror.slice(),this.ownData=!0),this.to=this._maps.push(e),null!=t&&this.setMirror(this._maps.length-1,t)}appendMapping(e){for(let t=0,n=this._maps.length;t<e._maps.length;t++){let r=e.getMirror(t);this.appendMap(e._maps[t],null!=r&&r<t?n+r:void 0)}}getMirror(e){if(this.mirror)for(let t=0;t<this.mirror.length;t++)if(this.mirror[t]==e)return this.mirror[t+(t%2?-1:1)]}setMirror(e,t){this.mirror||(this.mirror=[]),this.mirror.push(e,t)}appendMappingInverted(e){for(let t=e.maps.length-1,n=this._maps.length+e._maps.length;t>=0;t--){let r=e.getMirror(t);this.appendMap(e._maps[t].invert(),null!=r&&r>t?n-r-1:void 0)}}invert(){let e=new me;return e.appendMappingInverted(this),e}map(e,t=1){if(this.mirror)return this._map(e,t,!0);for(let n=this.from;n<this.to;n++)e=this._maps[n].map(e,t);return e}mapResult(e,t=1){return this._map(e,t,!1)}_map(e,t,n){let r=0;for(let n=this.from;n<this.to;n++){let i=this._maps[n].mapResult(e,t);if(null!=i.recover){let t=this.getMirror(n);if(null!=t&&t>n&&t<this.to){n=t,e=this._maps[t].recover(i.recover);continue}}r|=i.delInfo,e=i.pos}return n?e:new de(e,r,null)}}const ge=Object.create(null);class ye{getMap(){return fe.empty}merge(e){return null}static fromJSON(e,t){if(!t||!t.stepType)throw new RangeError("Invalid input for Step.fromJSON");let n=ge[t.stepType];if(!n)throw new RangeError(`No step type ${t.stepType} defined`);return n.fromJSON(e,t)}static jsonID(e,t){if(e in ge)throw new RangeError("Duplicate use of step JSON ID "+e);return ge[e]=t,t.prototype.jsonID=e,t}}class ke{constructor(e,t){this.doc=e,this.failed=t}static ok(e){return new ke(e,null)}static fail(e){return new ke(null,e)}static fromReplace(e,t,n,r){try{return ke.ok(e.replace(t,n,r))}catch(e){if(e instanceof a)return ke.fail(e.message);throw e}}}function be(e,t,n){let i=[];for(let r=0;r<e.childCount;r++){let o=e.child(r);o.content.size&&(o=o.copy(be(o.content,t,o))),o.isInline&&(o=t(o,n,r)),i.push(o)}return r.fromArray(i)}class we extends ye{constructor(e,t,n){super(),this.from=e,this.to=t,this.mark=n}apply(e){let t=e.slice(this.from,this.to),n=e.resolve(this.from),r=n.node(n.sharedDepth(this.to)),i=new c(be(t.content,(e,t)=>e.isAtom&&t.type.allowsMarkType(this.mark.type)?e.mark(this.mark.addToSet(e.marks)):e,r),t.openStart,t.openEnd);return ke.fromReplace(e,this.from,this.to,i)}invert(){return new Ce(this.from,this.to,this.mark)}map(e){let t=e.mapResult(this.from,1),n=e.mapResult(this.to,-1);return t.deleted&&n.deleted||t.pos>=n.pos?null:new we(t.pos,n.pos,this.mark)}merge(e){return e instanceof we&&e.mark.eq(this.mark)&&this.from<=e.to&&this.to>=e.from?new we(Math.min(this.from,e.from),Math.max(this.to,e.to),this.mark):null}toJSON(){return{stepType:"addMark",mark:this.mark.toJSON(),from:this.from,to:this.to}}static fromJSON(e,t){if("number"!=typeof t.from||"number"!=typeof t.to)throw new RangeError("Invalid input for AddMarkStep.fromJSON");return new we(t.from,t.to,e.markFromJSON(t.mark))}}ye.jsonID("addMark",we);class Ce extends ye{constructor(e,t,n){super(),this.from=e,this.to=t,this.mark=n}apply(e){let t=e.slice(this.from,this.to),n=new c(be(t.content,e=>e.mark(this.mark.removeFromSet(e.marks)),e),t.openStart,t.openEnd);return ke.fromReplace(e,this.from,this.to,n)}invert(){return new we(this.from,this.to,this.mark)}map(e){let t=e.mapResult(this.from,1),n=e.mapResult(this.to,-1);return t.deleted&&n.deleted||t.pos>=n.pos?null:new Ce(t.pos,n.pos,this.mark)}merge(e){return e instanceof Ce&&e.mark.eq(this.mark)&&this.from<=e.to&&this.to>=e.from?new Ce(Math.min(this.from,e.from),Math.max(this.to,e.to),this.mark):null}toJSON(){return{stepType:"removeMark",mark:this.mark.toJSON(),from:this.from,to:this.to}}static fromJSON(e,t){if("number"!=typeof t.from||"number"!=typeof t.to)throw new RangeError("Invalid input for RemoveMarkStep.fromJSON");return new Ce(t.from,t.to,e.markFromJSON(t.mark))}}ye.jsonID("removeMark",Ce);class xe extends ye{constructor(e,t){super(),this.pos=e,this.mark=t}apply(e){let t=e.nodeAt(this.pos);if(!t)return ke.fail("No node at mark step's position");let n=t.type.create(t.attrs,null,this.mark.addToSet(t.marks));return ke.fromReplace(e,this.pos,this.pos+1,new c(r.from(n),0,t.isLeaf?0:1))}invert(e){let t=e.nodeAt(this.pos);if(t){let e=this.mark.addToSet(t.marks);if(e.length==t.marks.length){for(let n=0;n<t.marks.length;n++)if(!t.marks[n].isInSet(e))return new xe(this.pos,t.marks[n]);return new xe(this.pos,this.mark)}}return new De(this.pos,this.mark)}map(e){let t=e.mapResult(this.pos,1);return t.deletedAfter?null:new xe(t.pos,this.mark)}toJSON(){return{stepType:"addNodeMark",pos:this.pos,mark:this.mark.toJSON()}}static fromJSON(e,t){if("number"!=typeof t.pos)throw new RangeError("Invalid input for AddNodeMarkStep.fromJSON");return new xe(t.pos,e.markFromJSON(t.mark))}}ye.jsonID("addNodeMark",xe);class De extends ye{constructor(e,t){super(),this.pos=e,this.mark=t}apply(e){let t=e.nodeAt(this.pos);if(!t)return ke.fail("No node at mark step's position");let n=t.type.create(t.attrs,null,this.mark.removeFromSet(t.marks));return ke.fromReplace(e,this.pos,this.pos+1,new c(r.from(n),0,t.isLeaf?0:1))}invert(e){let t=e.nodeAt(this.pos);return t&&this.mark.isInSet(t.marks)?new xe(this.pos,this.mark):this}map(e){let t=e.mapResult(this.pos,1);return t.deletedAfter?null:new De(t.pos,this.mark)}toJSON(){return{stepType:"removeNodeMark",pos:this.pos,mark:this.mark.toJSON()}}static fromJSON(e,t){if("number"!=typeof t.pos)throw new RangeError("Invalid input for RemoveNodeMarkStep.fromJSON");return new De(t.pos,e.markFromJSON(t.mark))}}ye.jsonID("removeNodeMark",De);class ve extends ye{constructor(e,t,n,r=!1){super(),this.from=e,this.to=t,this.slice=n,this.structure=r}apply(e){return this.structure&&Se(e,this.from,this.to)?ke.fail("Structure replace would overwrite content"):ke.fromReplace(e,this.from,this.to,this.slice)}getMap(){return new fe([this.from,this.to-this.from,this.slice.size])}invert(e){return new ve(this.from,this.from+this.slice.size,e.slice(this.from,this.to))}map(e){let t=e.mapResult(this.from,1),n=e.mapResult(this.to,-1);return t.deletedAcross&&n.deletedAcross?null:new ve(t.pos,Math.max(t.pos,n.pos),this.slice,this.structure)}merge(e){if(!(e instanceof ve)||e.structure||this.structure)return null;if(this.from+this.slice.size!=e.from||this.slice.openEnd||e.slice.openStart){if(e.to!=this.from||this.slice.openStart||e.slice.openEnd)return null;{let t=this.slice.size+e.slice.size==0?c.empty:new c(e.slice.content.append(this.slice.content),e.slice.openStart,this.slice.openEnd);return new ve(e.from,this.to,t,this.structure)}}{let t=this.slice.size+e.slice.size==0?c.empty:new c(this.slice.content.append(e.slice.content),this.slice.openStart,e.slice.openEnd);return new ve(this.from,this.to+(e.to-e.from),t,this.structure)}}toJSON(){let e={stepType:"replace",from:this.from,to:this.to};return this.slice.size&&(e.slice=this.slice.toJSON()),this.structure&&(e.structure=!0),e}static fromJSON(e,t){if("number"!=typeof t.from||"number"!=typeof t.to)throw new RangeError("Invalid input for ReplaceStep.fromJSON");return new ve(t.from,t.to,c.fromJSON(e,t.slice),!!t.structure)}}ye.jsonID("replace",ve);class _e extends ye{constructor(e,t,n,r,i,o,s=!1){super(),this.from=e,this.to=t,this.gapFrom=n,this.gapTo=r,this.slice=i,this.insert=o,this.structure=s}apply(e){if(this.structure&&(Se(e,this.from,this.gapFrom)||Se(e,this.gapTo,this.to)))return ke.fail("Structure gap-replace would overwrite content");let t=e.slice(this.gapFrom,this.gapTo);if(t.openStart||t.openEnd)return ke.fail("Gap is not a flat range");let n=this.slice.insertAt(this.insert,t.content);return n?ke.fromReplace(e,this.from,this.to,n):ke.fail("Content does not fit in gap")}getMap(){return new fe([this.from,this.gapFrom-this.from,this.insert,this.gapTo,this.to-this.gapTo,this.slice.size-this.insert])}invert(e){let t=this.gapTo-this.gapFrom;return new _e(this.from,this.from+this.slice.size+t,this.from+this.insert,this.from+this.insert+t,e.slice(this.from,this.to).removeBetween(this.gapFrom-this.from,this.gapTo-this.from),this.gapFrom-this.from,this.structure)}map(e){let t=e.mapResult(this.from,1),n=e.mapResult(this.to,-1),r=this.from==this.gapFrom?t.pos:e.map(this.gapFrom,-1),i=this.to==this.gapTo?n.pos:e.map(this.gapTo,1);return t.deletedAcross&&n.deletedAcross||r<t.pos||i>n.pos?null:new _e(t.pos,n.pos,r,i,this.slice,this.insert,this.structure)}toJSON(){let e={stepType:"replaceAround",from:this.from,to:this.to,gapFrom:this.gapFrom,gapTo:this.gapTo,insert:this.insert};return this.slice.size&&(e.slice=this.slice.toJSON()),this.structure&&(e.structure=!0),e}static fromJSON(e,t){if("number"!=typeof t.from||"number"!=typeof t.to||"number"!=typeof t.gapFrom||"number"!=typeof t.gapTo||"number"!=typeof t.insert)throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON");return new _e(t.from,t.to,t.gapFrom,t.gapTo,c.fromJSON(e,t.slice),t.insert,!!t.structure)}}function Se(e,t,n){let r=e.resolve(t),i=n-t,o=r.depth;for(;i>0&&o>0&&r.indexAfter(o)==r.node(o).childCount;)o--,i--;if(i>0){let e=r.node(o).maybeChild(r.indexAfter(o));for(;i>0;){if(!e||e.isLeaf)return!0;e=e.firstChild,i--}}return!1}function Ee(e,t,n,i=n.contentMatch,o=!0){let s=e.doc.nodeAt(t),l=[],a=t+1;for(let t=0;t<s.childCount;t++){let h=s.child(t),u=a+h.nodeSize,p=i.matchType(h.type);if(p){i=p;for(let t=0;t<h.marks.length;t++)n.allowsMarkType(h.marks[t].type)||e.step(new Ce(a,u,h.marks[t]));if(o&&h.isText&&"pre"!=n.whitespace){let e,t,i=/\r?\n|\r/g;for(;e=i.exec(h.text);)t||(t=new c(r.from(n.schema.text(" ",n.allowedMarks(h.marks))),0,0)),l.push(new ve(a+e.index,a+e.index+e[0].length,t))}}else l.push(new ve(a,u,c.empty));a=u}if(!i.validEnd){let t=i.fillBefore(r.empty,!0);e.replace(a,a,new c(t,0,0))}for(let t=l.length-1;t>=0;t--)e.step(l[t])}function Ae(e,t,n){return(0==t||e.canReplace(t,e.childCount))&&(n==e.childCount||e.canReplace(0,n))}function Me(e){let t=e.parent.content.cutByIndex(e.startIndex,e.endIndex);for(let n=e.depth,r=0,i=0;;--n){let o=e.$from.node(n),s=e.$from.index(n)+r,l=e.$to.indexAfter(n)-i;if(n<e.depth&&o.canReplace(s,l,t))return n;if(0==n||o.type.spec.isolating||!Ae(o,s,l))break;s&&(r=1),l<o.childCount&&(i=1)}return null}function Oe(e,t,n,r){t.forEach((i,o)=>{if(i.isText){let s,l=/\r?\n|\r/g;for(;s=l.exec(i.text);){let i=e.mapping.slice(r).map(n+1+o+s.index);e.replaceWith(i,i+1,t.type.schema.linebreakReplacement.create())}}})}function Ne(e,t,n,r){t.forEach((i,o)=>{if(i.type==i.type.schema.linebreakReplacement){let i=e.mapping.slice(r).map(n+1+o);e.replaceWith(i,i+1,t.type.schema.text("\n"))}})}function Fe(e,t,n=1,r){let i=e.resolve(t),o=i.depth-n,s=r&&r[r.length-1]||i.parent;if(o<0||i.parent.type.spec.isolating||!i.parent.canReplace(i.index(),i.parent.childCount)||!s.type.validContent(i.parent.content.cutByIndex(i.index(),i.parent.childCount)))return!1;for(let e=i.depth-1,t=n-2;e>o;e--,t--){let n=i.node(e),o=i.index(e);if(n.type.spec.isolating)return!1;let s=n.content.cutByIndex(o,n.childCount),l=r&&r[t+1];l&&(s=s.replaceChild(0,l.type.create(l.attrs)));let a=r&&r[t]||n;if(!n.canReplace(o+1,n.childCount)||!a.type.validContent(s))return!1}let l=i.indexAfter(o),a=r&&r[0];return i.node(o).canReplaceWith(l,l,a?a.type:i.node(o+1).type)}function Te(e,t){let n=e.resolve(t),r=n.index();return i=n.nodeBefore,o=n.nodeAfter,!(!i||!o||i.isLeaf||!function(e,t){t.content.size||e.type.compatibleContent(t.type);let n=e.contentMatchAt(e.childCount),{linebreakReplacement:r}=e.type.schema;for(let i=0;i<t.childCount;i++){let o=t.child(i),s=o.type==r?e.type.schema.nodes.text:o.type;if(n=n.matchType(s),!n)return!1;if(!e.type.allowsMarks(o.marks))return!1}return n.validEnd}(i,o))&&n.parent.canReplace(r,r+1);var i,o}function Re(e,t,n=t,r=c.empty){if(t==n&&!r.size)return null;let i=e.resolve(t),o=e.resolve(n);return ze(i,o,r)?new ve(t,n,r):new Ie(i,o,r).fit()}function ze(e,t,n){return!n.openStart&&!n.openEnd&&e.start()==t.start()&&e.parent.canReplace(e.index(),t.index(),n.content)}ye.jsonID("replaceAround",_e);class Ie{constructor(e,t,n){this.$from=e,this.$to=t,this.unplaced=n,this.frontier=[],this.placed=r.empty;for(let t=0;t<=e.depth;t++){let n=e.node(t);this.frontier.push({type:n.type,match:n.contentMatchAt(e.indexAfter(t))})}for(let t=e.depth;t>0;t--)this.placed=r.from(e.node(t).copy(this.placed))}get depth(){return this.frontier.length-1}fit(){for(;this.unplaced.size;){let e=this.findFittable();e?this.placeNodes(e):this.openMore()||this.dropNode()}let e=this.mustMoveInline(),t=this.placed.size-this.depth-this.$from.depth,n=this.$from,r=this.close(e<0?this.$to:n.doc.resolve(e));if(!r)return null;let i=this.placed,o=n.depth,s=r.depth;for(;o&&s&&1==i.childCount;)i=i.firstChild.content,o--,s--;let l=new c(i,o,s);return e>-1?new _e(n.pos,e,this.$to.pos,this.$to.end(),l,t):l.size||n.pos!=this.$to.pos?new ve(n.pos,r.pos,l):null}findFittable(){let e=this.unplaced.openStart;for(let t=this.unplaced.content,n=0,r=this.unplaced.openEnd;n<e;n++){let i=t.firstChild;if(t.childCount>1&&(r=0),i.type.spec.isolating&&r<=n){e=n;break}t=i.content}for(let t=1;t<=2;t++)for(let n=1==t?e:this.unplaced.openStart;n>=0;n--){let e,i=null;n?(i=Le(this.unplaced.content,n-1).firstChild,e=i.content):e=this.unplaced.content;let o=e.firstChild;for(let e=this.depth;e>=0;e--){let s,{type:l,match:a}=this.frontier[e],c=null;if(1==t&&(o?a.matchType(o.type)||(c=a.fillBefore(r.from(o),!1)):i&&l.compatibleContent(i.type)))return{sliceDepth:n,frontierDepth:e,parent:i,inject:c};if(2==t&&o&&(s=a.findWrapping(o.type)))return{sliceDepth:n,frontierDepth:e,parent:i,wrap:s};if(i&&a.matchType(i.type))break}}}openMore(){let{content:e,openStart:t,openEnd:n}=this.unplaced,r=Le(e,t);return!(!r.childCount||r.firstChild.isLeaf)&&(this.unplaced=new c(e,t+1,Math.max(n,r.size+t>=e.size-n?t+1:0)),!0)}dropNode(){let{content:e,openStart:t,openEnd:n}=this.unplaced,r=Le(e,t);if(r.childCount<=1&&t>0){let i=e.size-t<=t+r.size;this.unplaced=new c(Be(e,t-1,1),t-1,i?t-1:n)}else this.unplaced=new c(Be(e,t,1),t,n)}placeNodes({sliceDepth:e,frontierDepth:t,parent:n,inject:i,wrap:o}){for(;this.depth>t;)this.closeFrontierNode();if(o)for(let e=0;e<o.length;e++)this.openFrontierNode(o[e]);let s=this.unplaced,l=n?n.content:s.content,a=s.openStart-e,h=0,u=[],{match:p,type:d}=this.frontier[t];if(i){for(let e=0;e<i.childCount;e++)u.push(i.child(e));p=p.matchFragment(i)}let f=l.size+e-(s.content.size-s.openEnd);for(;h<l.childCount;){let e=l.child(h),t=p.matchType(e.type);if(!t)break;h++,(h>1||0==a||e.content.size)&&(p=t,u.push(qe(e.mark(d.allowedMarks(e.marks)),1==h?a:0,h==l.childCount?f:-1)))}let m=h==l.childCount;m||(f=-1),this.placed=Pe(this.placed,t,r.from(u)),this.frontier[t].match=p,m&&f<0&&n&&n.type==this.frontier[this.depth].type&&this.frontier.length>1&&this.closeFrontierNode();for(let e=0,t=l;e<f;e++){let e=t.lastChild;this.frontier.push({type:e.type,match:e.contentMatchAt(e.childCount)}),t=e.content}this.unplaced=m?0==e?c.empty:new c(Be(s.content,e-1,1),e-1,f<0?s.openEnd:e-1):new c(Be(s.content,e,h),s.openStart,s.openEnd)}mustMoveInline(){if(!this.$to.parent.isTextblock)return-1;let e,t=this.frontier[this.depth];if(!t.type.isTextblock||!$e(this.$to,this.$to.depth,t.type,t.match,!1)||this.$to.depth==this.depth&&(e=this.findCloseLevel(this.$to))&&e.depth==this.depth)return-1;let{depth:n}=this.$to,r=this.$to.after(n);for(;n>1&&r==this.$to.end(--n);)++r;return r}findCloseLevel(e){e:for(let t=Math.min(this.depth,e.depth);t>=0;t--){let{match:n,type:r}=this.frontier[t],i=t<e.depth&&e.end(t+1)==e.pos+(e.depth-(t+1)),o=$e(e,t,r,n,i);if(o){for(let n=t-1;n>=0;n--){let{match:t,type:r}=this.frontier[n],i=$e(e,n,r,t,!0);if(!i||i.childCount)continue e}return{depth:t,fit:o,move:i?e.doc.resolve(e.after(t+1)):e}}}}close(e){let t=this.findCloseLevel(e);if(!t)return null;for(;this.depth>t.depth;)this.closeFrontierNode();t.fit.childCount&&(this.placed=Pe(this.placed,t.depth,t.fit)),e=t.move;for(let n=t.depth+1;n<=e.depth;n++){let t=e.node(n),r=t.type.contentMatch.fillBefore(t.content,!0,e.index(n));this.openFrontierNode(t.type,t.attrs,r)}return e}openFrontierNode(e,t=null,n){let i=this.frontier[this.depth];i.match=i.match.matchType(e),this.placed=Pe(this.placed,this.depth,r.from(e.create(t,n))),this.frontier.push({type:e,match:e.contentMatch})}closeFrontierNode(){let e=this.frontier.pop().match.fillBefore(r.empty,!0);e.childCount&&(this.placed=Pe(this.placed,this.frontier.length,e))}}function Be(e,t,n){return 0==t?e.cutByIndex(n,e.childCount):e.replaceChild(0,e.firstChild.copy(Be(e.firstChild.content,t-1,n)))}function Pe(e,t,n){return 0==t?e.append(n):e.replaceChild(e.childCount-1,e.lastChild.copy(Pe(e.lastChild.content,t-1,n)))}function Le(e,t){for(let n=0;n<t;n++)e=e.firstChild.content;return e}function qe(e,t,n){if(t<=0)return e;let i=e.content;return t>1&&(i=i.replaceChild(0,qe(i.firstChild,t-1,1==i.childCount?n-1:0))),t>0&&(i=e.type.contentMatch.fillBefore(i).append(i),n<=0&&(i=i.append(e.type.contentMatch.matchFragment(i).fillBefore(r.empty,!0)))),e.copy(i)}function $e(e,t,n,r,i){let o=e.node(t),s=i?e.indexAfter(t):e.index(t);if(s==o.childCount&&!n.compatibleContent(o.type))return null;let l=r.fillBefore(o.content,!0,s);return l&&!function(e,t,n){for(let r=n;r<t.childCount;r++)if(!e.allowsMarks(t.child(r).marks))return!0;return!1}(n,o.content,s)?l:null}function Ve(e){return e.spec.defining||e.spec.definingForContent}function je(e,t,n,i,o){if(t<n){let r=e.firstChild;e=e.replaceChild(0,r.copy(je(r.content,t+1,n,i,r)))}if(t>i){let t=o.contentMatchAt(0),n=t.fillBefore(e).append(e);e=n.append(t.matchFragment(n).fillBefore(r.empty,!0))}return e}function Je(e,t){let n=[];for(let r=Math.min(e.depth,t.depth);r>=0;r--){let i=e.start(r);if(i<e.pos-(e.depth-r)||t.end(r)>t.pos+(t.depth-r)||e.node(r).type.spec.isolating||t.node(r).type.spec.isolating)break;(i==t.start(r)||r==e.depth&&r==t.depth&&e.parent.inlineContent&&t.parent.inlineContent&&r&&t.start(r-1)==i-1)&&n.push(r)}return n}class We extends ye{constructor(e,t,n){super(),this.pos=e,this.attr=t,this.value=n}apply(e){let t=e.nodeAt(this.pos);if(!t)return ke.fail("No node at attribute step's position");let n=Object.create(null);for(let e in t.attrs)n[e]=t.attrs[e];n[this.attr]=this.value;let i=t.type.create(n,null,t.marks);return ke.fromReplace(e,this.pos,this.pos+1,new c(r.from(i),0,t.isLeaf?0:1))}getMap(){return fe.empty}invert(e){return new We(this.pos,this.attr,e.nodeAt(this.pos).attrs[this.attr])}map(e){let t=e.mapResult(this.pos,1);return t.deletedAfter?null:new We(t.pos,this.attr,this.value)}toJSON(){return{stepType:"attr",pos:this.pos,attr:this.attr,value:this.value}}static fromJSON(e,t){if("number"!=typeof t.pos||"string"!=typeof t.attr)throw new RangeError("Invalid input for AttrStep.fromJSON");return new We(t.pos,t.attr,t.value)}}ye.jsonID("attr",We);class He extends ye{constructor(e,t){super(),this.attr=e,this.value=t}apply(e){let t=Object.create(null);for(let n in e.attrs)t[n]=e.attrs[n];t[this.attr]=this.value;let n=e.type.create(t,e.content,e.marks);return ke.ok(n)}getMap(){return fe.empty}invert(e){return new He(this.attr,e.attrs[this.attr])}map(e){return this}toJSON(){return{stepType:"docAttr",attr:this.attr,value:this.value}}static fromJSON(e,t){if("string"!=typeof t.attr)throw new RangeError("Invalid input for DocAttrStep.fromJSON");return new He(t.attr,t.value)}}ye.jsonID("docAttr",He);let Ue=class extends Error{};Ue=function e(t){let n=Error.call(this,t);return n.__proto__=e.prototype,n},(Ue.prototype=Object.create(Error.prototype)).constructor=Ue,Ue.prototype.name="TransformError";class Ke{constructor(e){this.doc=e,this.steps=[],this.docs=[],this.mapping=new me}get before(){return this.docs.length?this.docs[0]:this.doc}step(e){let t=this.maybeStep(e);if(t.failed)throw new Ue(t.failed);return this}maybeStep(e){let t=e.apply(this.doc);return t.failed||this.addStep(e,t.doc),t}get docChanged(){return this.steps.length>0}addStep(e,t){this.docs.push(this.doc),this.steps.push(e),this.mapping.appendMap(e.getMap()),this.doc=t}replace(e,t=e,n=c.empty){let r=Re(this.doc,e,t,n);return r&&this.step(r),this}replaceWith(e,t,n){return this.replace(e,t,new c(r.from(n),0,0))}delete(e,t){return this.replace(e,t,c.empty)}insert(e,t){return this.replaceWith(e,e,t)}replaceRange(e,t,n){return function(e,t,n,r){if(!r.size)return e.deleteRange(t,n);let i=e.doc.resolve(t),o=e.doc.resolve(n);if(ze(i,o,r))return e.step(new ve(t,n,r));let s=Je(i,o);0==s[s.length-1]&&s.pop();let l=-(i.depth+1);s.unshift(l);for(let e=i.depth,t=i.pos-1;e>0;e--,t--){let n=i.node(e).type.spec;if(n.defining||n.definingAsContext||n.isolating)break;s.indexOf(e)>-1?l=e:i.before(e)==t&&s.splice(1,0,-e)}let a=s.indexOf(l),h=[],u=r.openStart;for(let e=r.content,t=0;;t++){let n=e.firstChild;if(h.push(n),t==r.openStart)break;e=n.content}for(let e=u-1;e>=0;e--){let t=h[e],n=Ve(t.type);if(n&&!t.sameMarkup(i.node(Math.abs(l)-1)))u=e;else if(n||!t.type.isTextblock)break}for(let t=r.openStart;t>=0;t--){let l=(t+u+1)%(r.openStart+1),p=h[l];if(p)for(let t=0;t<s.length;t++){let h=s[(t+a)%s.length],u=!0;h<0&&(u=!1,h=-h);let d=i.node(h-1),f=i.index(h-1);if(d.canReplaceWith(f,f,p.type,p.marks))return e.replace(i.before(h),u?o.after(h):n,new c(je(r.content,0,r.openStart,l),l,r.openEnd))}}let p=e.steps.length;for(let l=s.length-1;l>=0&&(e.replace(t,n,r),!(e.steps.length>p));l--){let e=s[l];e<0||(t=i.before(e),n=o.after(e))}}(this,e,t,n),this}replaceRangeWith(e,t,n){return function(e,t,n,i){if(!i.isInline&&t==n&&e.doc.resolve(t).parent.content.size){let r=function(e,t,n){let r=e.resolve(t);if(r.parent.canReplaceWith(r.index(),r.index(),n))return t;if(0==r.parentOffset)for(let e=r.depth-1;e>=0;e--){let t=r.index(e);if(r.node(e).canReplaceWith(t,t,n))return r.before(e+1);if(t>0)return null}if(r.parentOffset==r.parent.content.size)for(let e=r.depth-1;e>=0;e--){let t=r.indexAfter(e);if(r.node(e).canReplaceWith(t,t,n))return r.after(e+1);if(t<r.node(e).childCount)return null}return null}(e.doc,t,i.type);null!=r&&(t=n=r)}e.replaceRange(t,n,new c(r.from(i),0,0))}(this,e,t,n),this}deleteRange(e,t){return function(e,t,n){let r=e.doc.resolve(t),i=e.doc.resolve(n),o=Je(r,i);for(let t=0;t<o.length;t++){let n=o[t],s=t==o.length-1;if(s&&0==n||r.node(n).type.contentMatch.validEnd)return e.delete(r.start(n),i.end(n));if(n>0&&(s||r.node(n-1).canReplace(r.index(n-1),i.indexAfter(n-1))))return e.delete(r.before(n),i.after(n))}for(let o=1;o<=r.depth&&o<=i.depth;o++)if(t-r.start(o)==r.depth-o&&n>r.end(o)&&i.end(o)-n!=i.depth-o&&r.start(o-1)==i.start(o-1)&&r.node(o-1).canReplace(r.index(o-1),i.index(o-1)))return e.delete(r.before(o),n);e.delete(t,n)}(this,e,t),this}lift(e,t){return function(e,t,n){let{$from:i,$to:o,depth:s}=t,l=i.before(s+1),a=o.after(s+1),h=l,u=a,p=r.empty,d=0;for(let e=s,t=!1;e>n;e--)t||i.index(e)>0?(t=!0,p=r.from(i.node(e).copy(p)),d++):h--;let f=r.empty,m=0;for(let e=s,t=!1;e>n;e--)t||o.after(e+1)<o.end(e)?(t=!0,f=r.from(o.node(e).copy(f)),m++):u++;e.step(new _e(h,u,l,a,new c(p.append(f),d,m),p.size-d,!0))}(this,e,t),this}join(e,t=1){return function(e,t,n){let r=null,{linebreakReplacement:i}=e.doc.type.schema,o=e.doc.resolve(t-n),s=o.node().type;if(i&&s.inlineContent){let e="pre"==s.whitespace,t=!!s.contentMatch.matchType(i);e&&!t?r=!1:!e&&t&&(r=!0)}let l=e.steps.length;if(!1===r){let r=e.doc.resolve(t+n);Ne(e,r.node(),r.before(),l)}s.inlineContent&&Ee(e,t+n-1,s,o.node().contentMatchAt(o.index()),null==r);let a=e.mapping.slice(l),h=a.map(t-n);if(e.step(new ve(h,a.map(t+n,-1),c.empty,!0)),!0===r){let t=e.doc.resolve(h);Oe(e,t.node(),t.before(),e.steps.length)}}(this,e,t),this}wrap(e,t){return function(e,t,n){let i=r.empty;for(let e=n.length-1;e>=0;e--){if(i.size){let t=n[e].type.contentMatch.matchFragment(i);if(!t||!t.validEnd)throw new RangeError("Wrapper type given to Transform.wrap does not form valid content of its parent wrapper")}i=r.from(n[e].type.create(n[e].attrs,i))}let o=t.start,s=t.end;e.step(new _e(o,s,o,s,new c(i,0,0),n.length,!0))}(this,e,t),this}setBlockType(e,t=e,n,i=null){return function(e,t,n,i,o){if(!i.isTextblock)throw new RangeError("Type given to setBlockType should be a textblock");let s=e.steps.length;e.doc.nodesBetween(t,n,(t,n)=>{let l="function"==typeof o?o(t):o;if(t.isTextblock&&!t.hasMarkup(i,l)&&function(e,t,n){let r=e.resolve(t),i=r.index();return r.parent.canReplaceWith(i,i+1,n)}(e.doc,e.mapping.slice(s).map(n),i)){let o=null;if(i.schema.linebreakReplacement){let e="pre"==i.whitespace,t=!!i.contentMatch.matchType(i.schema.linebreakReplacement);e&&!t?o=!1:!e&&t&&(o=!0)}!1===o&&Ne(e,t,n,s),Ee(e,e.mapping.slice(s).map(n,1),i,void 0,null===o);let a=e.mapping.slice(s),h=a.map(n,1),u=a.map(n+t.nodeSize,1);return e.step(new _e(h,u,h+1,u-1,new c(r.from(i.create(l,null,t.marks)),0,0),1,!0)),!0===o&&Oe(e,t,n,s),!1}})}(this,e,t,n,i),this}setNodeMarkup(e,t,n=null,i){return function(e,t,n,i,o){let s=e.doc.nodeAt(t);if(!s)throw new RangeError("No node at given position");n||(n=s.type);let l=n.create(i,null,o||s.marks);if(s.isLeaf)return e.replaceWith(t,t+s.nodeSize,l);if(!n.validContent(s.content))throw new RangeError("Invalid content for node type "+n.name);e.step(new _e(t,t+s.nodeSize,t+1,t+s.nodeSize-1,new c(r.from(l),0,0),1,!0))}(this,e,t,n,i),this}setNodeAttribute(e,t,n){return this.step(new We(e,t,n)),this}setDocAttribute(e,t){return this.step(new He(e,t)),this}addNodeMark(e,t){return this.step(new xe(e,t)),this}removeNodeMark(e,t){let n=this.doc.nodeAt(e);if(!n)throw new RangeError("No node at position "+e);if(t instanceof l)t.isInSet(n.marks)&&this.step(new De(e,t));else{let r,i=n.marks,o=[];for(;r=t.isInSet(i);)o.push(new De(e,r)),i=r.removeFromSet(i);for(let e=o.length-1;e>=0;e--)this.step(o[e])}return this}split(e,t=1,n){return function(e,t,n=1,i){let o=e.doc.resolve(t),s=r.empty,l=r.empty;for(let e=o.depth,t=o.depth-n,a=n-1;e>t;e--,a--){s=r.from(o.node(e).copy(s));let t=i&&i[a];l=r.from(t?t.type.create(t.attrs,l):o.node(e).copy(l))}e.step(new ve(t,t,new c(s.append(l),n,n),!0))}(this,e,t,n),this}addMark(e,t,n){return function(e,t,n,r){let i,o,s=[],l=[];e.doc.nodesBetween(t,n,(e,a,c)=>{if(!e.isInline)return;let h=e.marks;if(!r.isInSet(h)&&c.type.allowsMarkType(r.type)){let c=Math.max(a,t),u=Math.min(a+e.nodeSize,n),p=r.addToSet(h);for(let e=0;e<h.length;e++)h[e].isInSet(p)||(i&&i.to==c&&i.mark.eq(h[e])?i.to=u:s.push(i=new Ce(c,u,h[e])));o&&o.to==c?o.to=u:l.push(o=new we(c,u,r))}}),s.forEach(t=>e.step(t)),l.forEach(t=>e.step(t))}(this,e,t,n),this}removeMark(e,t,n){return function(e,t,n,r){let i=[],o=0;e.doc.nodesBetween(t,n,(e,s)=>{if(!e.isInline)return;o++;let l=null;if(r instanceof W){let t,n=e.marks;for(;t=r.isInSet(n);)(l||(l=[])).push(t),n=t.removeFromSet(n)}else r?r.isInSet(e.marks)&&(l=[r]):l=e.marks;if(l&&l.length){let r=Math.min(s+e.nodeSize,n);for(let e=0;e<l.length;e++){let n,a=l[e];for(let e=0;e<i.length;e++){let t=i[e];t.step==o-1&&a.eq(i[e].style)&&(n=t)}n?(n.to=r,n.step=o):i.push({style:a,from:Math.max(s,t),to:r,step:o})}}}),i.forEach(t=>e.step(new Ce(t.from,t.to,t.style)))}(this,e,t,n),this}clearIncompatible(e,t,n){return Ee(this,e,t,n),this}}const Ze=Object.create(null);class Ge{constructor(e,t,n){this.$anchor=e,this.$head=t,this.ranges=n||[new Qe(e.min(t),e.max(t))]}get anchor(){return this.$anchor.pos}get head(){return this.$head.pos}get from(){return this.$from.pos}get to(){return this.$to.pos}get $from(){return this.ranges[0].$from}get $to(){return this.ranges[0].$to}get empty(){let e=this.ranges;for(let t=0;t<e.length;t++)if(e[t].$from.pos!=e[t].$to.pos)return!1;return!0}content(){return this.$from.doc.slice(this.from,this.to,!0)}replace(e,t=c.empty){let n=t.content.lastChild,r=null;for(let e=0;e<t.openEnd;e++)r=n,n=n.lastChild;let i=e.steps.length,o=this.ranges;for(let s=0;s<o.length;s++){let{$from:l,$to:a}=o[s],h=e.mapping.slice(i);e.replaceRange(h.map(l.pos),h.map(a.pos),s?c.empty:t),0==s&&lt(e,i,(n?n.isInline:r&&r.isTextblock)?-1:1)}}replaceWith(e,t){let n=e.steps.length,r=this.ranges;for(let i=0;i<r.length;i++){let{$from:o,$to:s}=r[i],l=e.mapping.slice(n),a=l.map(o.pos),c=l.map(s.pos);i?e.deleteRange(a,c):(e.replaceRangeWith(a,c,t),lt(e,n,t.isInline?-1:1))}}static findFrom(e,t,n=!1){let r=e.parent.inlineContent?new et(e):st(e.node(0),e.parent,e.pos,e.index(),t,n);if(r)return r;for(let r=e.depth-1;r>=0;r--){let i=t<0?st(e.node(0),e.node(r),e.before(r+1),e.index(r),t,n):st(e.node(0),e.node(r),e.after(r+1),e.index(r)+1,t,n);if(i)return i}return null}static near(e,t=1){return this.findFrom(e,t)||this.findFrom(e,-t)||new it(e.node(0))}static atStart(e){return st(e,e,0,0,1)||new it(e)}static atEnd(e){return st(e,e,e.content.size,e.childCount,-1)||new it(e)}static fromJSON(e,t){if(!t||!t.type)throw new RangeError("Invalid input for Selection.fromJSON");let n=Ze[t.type];if(!n)throw new RangeError(`No selection type ${t.type} defined`);return n.fromJSON(e,t)}static jsonID(e,t){if(e in Ze)throw new RangeError("Duplicate use of selection JSON ID "+e);return Ze[e]=t,t.prototype.jsonID=e,t}getBookmark(){return et.between(this.$anchor,this.$head).getBookmark()}}Ge.prototype.visible=!0;class Qe{constructor(e,t){this.$from=e,this.$to=t}}let Xe=!1;function Ye(e){Xe||e.parent.inlineContent||(Xe=!0,console.warn("TextSelection endpoint not pointing into a node with inline content ("+e.parent.type.name+")"))}class et extends Ge{constructor(e,t=e){Ye(e),Ye(t),super(e,t)}get $cursor(){return this.$anchor.pos==this.$head.pos?this.$head:null}map(e,t){let n=e.resolve(t.map(this.head));if(!n.parent.inlineContent)return Ge.near(n);let r=e.resolve(t.map(this.anchor));return new et(r.parent.inlineContent?r:n,n)}replace(e,t=c.empty){if(super.replace(e,t),t==c.empty){let t=this.$from.marksAcross(this.$to);t&&e.ensureMarks(t)}}eq(e){return e instanceof et&&e.anchor==this.anchor&&e.head==this.head}getBookmark(){return new tt(this.anchor,this.head)}toJSON(){return{type:"text",anchor:this.anchor,head:this.head}}static fromJSON(e,t){if("number"!=typeof t.anchor||"number"!=typeof t.head)throw new RangeError("Invalid input for TextSelection.fromJSON");return new et(e.resolve(t.anchor),e.resolve(t.head))}static create(e,t,n=t){let r=e.resolve(t);return new this(r,n==t?r:e.resolve(n))}static between(e,t,n){let r=e.pos-t.pos;if(n&&!r||(n=r>=0?1:-1),!t.parent.inlineContent){let e=Ge.findFrom(t,n,!0)||Ge.findFrom(t,-n,!0);if(!e)return Ge.near(t,n);t=e.$head}return e.parent.inlineContent||(0==r||(e=(Ge.findFrom(e,-n,!0)||Ge.findFrom(e,n,!0)).$anchor).pos<t.pos!=r<0)&&(e=t),new et(e,t)}}Ge.jsonID("text",et);class tt{constructor(e,t){this.anchor=e,this.head=t}map(e){return new tt(e.map(this.anchor),e.map(this.head))}resolve(e){return et.between(e.resolve(this.anchor),e.resolve(this.head))}}class nt extends Ge{constructor(e){let t=e.nodeAfter,n=e.node(0).resolve(e.pos+t.nodeSize);super(e,n),this.node=t}map(e,t){let{deleted:n,pos:r}=t.mapResult(this.anchor),i=e.resolve(r);return n?Ge.near(i):new nt(i)}content(){return new c(r.from(this.node),0,0)}eq(e){return e instanceof nt&&e.anchor==this.anchor}toJSON(){return{type:"node",anchor:this.anchor}}getBookmark(){return new rt(this.anchor)}static fromJSON(e,t){if("number"!=typeof t.anchor)throw new RangeError("Invalid input for NodeSelection.fromJSON");return new nt(e.resolve(t.anchor))}static create(e,t){return new nt(e.resolve(t))}static isSelectable(e){return!e.isText&&!1!==e.type.spec.selectable}}nt.prototype.visible=!1,Ge.jsonID("node",nt);class rt{constructor(e){this.anchor=e}map(e){let{deleted:t,pos:n}=e.mapResult(this.anchor);return t?new tt(n,n):new rt(n)}resolve(e){let t=e.resolve(this.anchor),n=t.nodeAfter;return n&&nt.isSelectable(n)?new nt(t):Ge.near(t)}}class it extends Ge{constructor(e){super(e.resolve(0),e.resolve(e.content.size))}replace(e,t=c.empty){if(t==c.empty){e.delete(0,e.doc.content.size);let t=Ge.atStart(e.doc);t.eq(e.selection)||e.setSelection(t)}else super.replace(e,t)}toJSON(){return{type:"all"}}static fromJSON(e){return new it(e)}map(e){return new it(e)}eq(e){return e instanceof it}getBookmark(){return ot}}Ge.jsonID("all",it);const ot={map(){return this},resolve:e=>new it(e)};function st(e,t,n,r,i,o=!1){if(t.inlineContent)return et.create(e,n);for(let s=r-(i>0?0:1);i>0?s<t.childCount:s>=0;s+=i){let r=t.child(s);if(r.isAtom){if(!o&&nt.isSelectable(r))return nt.create(e,n-(i<0?r.nodeSize:0))}else{let t=st(e,r,n+i,i<0?r.childCount:0,i,o);if(t)return t}n+=r.nodeSize*i}return null}function lt(e,t,n){let r=e.steps.length-1;if(r<t)return;let i,o=e.steps[r];(o instanceof ve||o instanceof _e)&&(e.mapping.maps[r].forEach((e,t,n,r)=>{null==i&&(i=r)}),e.setSelection(Ge.near(e.doc.resolve(i),n)))}class at extends Ke{constructor(e){super(e.doc),this.curSelectionFor=0,this.updated=0,this.meta=Object.create(null),this.time=Date.now(),this.curSelection=e.selection,this.storedMarks=e.storedMarks}get selection(){return this.curSelectionFor<this.steps.length&&(this.curSelection=this.curSelection.map(this.doc,this.mapping.slice(this.curSelectionFor)),this.curSelectionFor=this.steps.length),this.curSelection}setSelection(e){if(e.$from.doc!=this.doc)throw new RangeError("Selection passed to setSelection must point at the current document");return this.curSelection=e,this.curSelectionFor=this.steps.length,this.updated=-3&this.updated|1,this.storedMarks=null,this}get selectionSet(){return(1&this.updated)>0}setStoredMarks(e){return this.storedMarks=e,this.updated|=2,this}ensureMarks(e){return l.sameSet(this.storedMarks||this.selection.$from.marks(),e)||this.setStoredMarks(e),this}addStoredMark(e){return this.ensureMarks(e.addToSet(this.storedMarks||this.selection.$head.marks()))}removeStoredMark(e){return this.ensureMarks(e.removeFromSet(this.storedMarks||this.selection.$head.marks()))}get storedMarksSet(){return(2&this.updated)>0}addStep(e,t){super.addStep(e,t),this.updated=-3&this.updated,this.storedMarks=null}setTime(e){return this.time=e,this}replaceSelection(e){return this.selection.replace(this,e),this}replaceSelectionWith(e,t=!0){let n=this.selection;return t&&(e=e.mark(this.storedMarks||(n.empty?n.$from.marks():n.$from.marksAcross(n.$to)||l.none))),n.replaceWith(this,e),this}deleteSelection(){return this.selection.replace(this),this}insertText(e,t,n){let r=this.doc.type.schema;if(null==t)return e?this.replaceSelectionWith(r.text(e),!0):this.deleteSelection();{if(null==n&&(n=t),!e)return this.deleteRange(t,n);let i=this.storedMarks;if(!i){let e=this.doc.resolve(t);i=n==t?e.marks():e.marksAcross(this.doc.resolve(n))}return this.replaceRangeWith(t,n,r.text(e,i)),this.selection.empty||this.selection.to!=t+e.length||this.setSelection(Ge.near(this.selection.$to)),this}}setMeta(e,t){return this.meta["string"==typeof e?e:e.key]=t,this}getMeta(e){return this.meta["string"==typeof e?e:e.key]}get isGeneric(){for(let e in this.meta)return!1;return!0}scrollIntoView(){return this.updated|=4,this}get scrolledIntoView(){return(4&this.updated)>0}}function ct(e,t){return t&&e?e.bind(t):e}class ht{constructor(e,t,n){this.name=e,this.init=ct(t.init,n),this.apply=ct(t.apply,n)}}const ut=[new ht("doc",{init:e=>e.doc||e.schema.topNodeType.createAndFill(),apply:e=>e.doc}),new ht("selection",{init:(e,t)=>e.selection||Ge.atStart(t.doc),apply:e=>e.selection}),new ht("storedMarks",{init:e=>e.storedMarks||null,apply:(e,t,n,r)=>r.selection.$cursor?e.storedMarks:null}),new ht("scrollToSelection",{init:()=>0,apply:(e,t)=>e.scrolledIntoView?t+1:t})];class pt{constructor(e,t){this.schema=e,this.plugins=[],this.pluginsByKey=Object.create(null),this.fields=ut.slice(),t&&t.forEach(e=>{if(this.pluginsByKey[e.key])throw new RangeError("Adding different instances of a keyed plugin ("+e.key+")");this.plugins.push(e),this.pluginsByKey[e.key]=e,e.spec.state&&this.fields.push(new ht(e.key,e.spec.state,e))})}}class dt{constructor(e){this.config=e}get schema(){return this.config.schema}get plugins(){return this.config.plugins}apply(e){return this.applyTransaction(e).state}filterTransaction(e,t=-1){for(let n=0;n<this.config.plugins.length;n++)if(n!=t){let t=this.config.plugins[n];if(t.spec.filterTransaction&&!t.spec.filterTransaction.call(t,e,this))return!1}return!0}applyTransaction(e){if(!this.filterTransaction(e))return{state:this,transactions:[]};let t=[e],n=this.applyInner(e),r=null;for(;;){let i=!1;for(let o=0;o<this.config.plugins.length;o++){let s=this.config.plugins[o];if(s.spec.appendTransaction){let l=r?r[o].n:0,a=r?r[o].state:this,c=l<t.length&&s.spec.appendTransaction.call(s,l?t.slice(l):t,a,n);if(c&&n.filterTransaction(c,o)){if(c.setMeta("appendedTransaction",e),!r){r=[];for(let e=0;e<this.config.plugins.length;e++)r.push(e<o?{state:n,n:t.length}:{state:this,n:0})}t.push(c),n=n.applyInner(c),i=!0}r&&(r[o]={state:n,n:t.length})}}if(!i)return{state:n,transactions:t}}}applyInner(e){if(!e.before.eq(this.doc))throw new RangeError("Applying a mismatched transaction");let t=new dt(this.config),n=this.config.fields;for(let r=0;r<n.length;r++){let i=n[r];t[i.name]=i.apply(e,this[i.name],this,t)}return t}get tr(){return new at(this)}static create(e){let t=new pt(e.doc?e.doc.type.schema:e.schema,e.plugins),n=new dt(t);for(let r=0;r<t.fields.length;r++)n[t.fields[r].name]=t.fields[r].init(e,n);return n}reconfigure(e){let t=new pt(this.schema,e.plugins),n=t.fields,r=new dt(t);for(let t=0;t<n.length;t++){let i=n[t].name;r[i]=this.hasOwnProperty(i)?this[i]:n[t].init(e,r)}return r}toJSON(e){let t={doc:this.doc.toJSON(),selection:this.selection.toJSON()};if(this.storedMarks&&(t.storedMarks=this.storedMarks.map(e=>e.toJSON())),e&&"object"==typeof e)for(let n in e){if("doc"==n||"selection"==n)throw new RangeError("The JSON fields `doc` and `selection` are reserved");let r=e[n],i=r.spec.state;i&&i.toJSON&&(t[n]=i.toJSON.call(r,this[r.key]))}return t}static fromJSON(e,t,n){if(!t)throw new RangeError("Invalid input for EditorState.fromJSON");if(!e.schema)throw new RangeError("Required config field 'schema' missing");let r=new pt(e.schema,e.plugins),i=new dt(r);return r.fields.forEach(r=>{if("doc"==r.name)i.doc=E.fromJSON(e.schema,t.doc);else if("selection"==r.name)i.selection=Ge.fromJSON(i.doc,t.selection);else if("storedMarks"==r.name)t.storedMarks&&(i.storedMarks=t.storedMarks.map(e.schema.markFromJSON));else{if(n)for(let o in n){let s=n[o],l=s.spec.state;if(s.key==r.name&&l&&l.fromJSON&&Object.prototype.hasOwnProperty.call(t,o))return void(i[r.name]=l.fromJSON.call(s,e,t[o],i))}i[r.name]=r.init(e,i)}}),i}}function ft(e,t,n){for(let r in e){let i=e[r];i instanceof Function?i=i.bind(t):"handleDOMEvents"==r&&(i=ft(i,t,{})),n[r]=i}return n}class mt{constructor(e){this.spec=e,this.props={},e.props&&ft(e.props,this,this.props),this.key=e.key?e.key.key:yt("plugin")}getState(e){return e[this.key]}}const gt=Object.create(null);function yt(e){return e in gt?e+"$"+ ++gt[e]:(gt[e]=0,e+"$")}class kt{constructor(e="key"){this.key=yt(e)}get(e){return e.config.pluginsByKey[this.key]}getState(e){return e[this.key]}}const bt=(e,t)=>!e.selection.empty&&(t&&t(e.tr.deleteSelection().scrollIntoView()),!0);function wt(e,t,n=!1){for(let r=e;r;r="start"==t?r.firstChild:r.lastChild){if(r.isTextblock)return!0;if(n&&1!=r.childCount)return!1}return!1}function Ct(e){if(!e.parent.type.spec.isolating)for(let t=e.depth-1;t>=0;t--){if(e.index(t)>0)return e.doc.resolve(e.before(t+1));if(e.node(t).type.spec.isolating)break}return null}function xt(e){if(!e.parent.type.spec.isolating)for(let t=e.depth-1;t>=0;t--){let n=e.node(t);if(e.index(t)+1<n.childCount)return e.doc.resolve(e.after(t+1));if(n.type.spec.isolating)break}return null}function Dt(e){for(let t=0;t<e.edgeCount;t++){let{type:n}=e.edge(t);if(n.isTextblock&&!n.hasRequiredAttrs())return n}return null}const vt=(e,t)=>{let{$from:n,$to:r}=e.selection;if(e.selection instanceof nt&&e.selection.node.isBlock)return!(!n.parentOffset||!Fe(e.doc,n.pos)||(t&&t(e.tr.split(n.pos).scrollIntoView()),0));if(!n.depth)return!1;let i,o,s=[],l=!1,a=!1;for(let e=n.depth;;e--){if(n.node(e).isBlock){l=n.end(e)==n.pos+(n.depth-e),a=n.start(e)==n.pos-(n.depth-e),o=Dt(n.node(e-1).contentMatchAt(n.indexAfter(e-1)));let t=_t&&_t(r.parent,l,n);s.unshift(t||(l&&o?{type:o}:null)),i=e;break}if(1==e)return!1;s.unshift(null)}let c=e.tr;(e.selection instanceof et||e.selection instanceof it)&&c.deleteSelection();let h=c.mapping.map(n.pos),u=Fe(c.doc,h,s.length,s);if(u||(s[0]=o?{type:o}:null,u=Fe(c.doc,h,s.length,s)),!u)return!1;if(c.split(h,s.length,s),!l&&a&&n.node(i).type!=o){let e=c.mapping.map(n.before(i)),t=c.doc.resolve(e);o&&n.node(i-1).canReplaceWith(t.index(),t.index()+1,o)&&c.setNodeMarkup(c.mapping.map(n.before(i)),o)}return t&&t(c.scrollIntoView()),!0};var _t;function St(e,t,n,i){let o,s,l=t.nodeBefore,a=t.nodeAfter,h=l.type.spec.isolating||a.type.spec.isolating;if(!h&&function(e,t,n){let r=t.nodeBefore,i=t.nodeAfter,o=t.index();return!(!(r&&i&&r.type.compatibleContent(i.type))||(!r.content.size&&t.parent.canReplace(o-1,o)?(n&&n(e.tr.delete(t.pos-r.nodeSize,t.pos).scrollIntoView()),0):!t.parent.canReplace(o,o+1)||!i.isTextblock&&!Te(e.doc,t.pos)||(n&&n(e.tr.join(t.pos).scrollIntoView()),0)))}(e,t,n))return!0;let u=!h&&t.parent.canReplace(t.index(),t.index()+1);if(u&&(o=(s=l.contentMatchAt(l.childCount)).findWrapping(a.type))&&s.matchType(o[0]||a.type).validEnd){if(n){let i=t.pos+a.nodeSize,s=r.empty;for(let e=o.length-1;e>=0;e--)s=r.from(o[e].create(null,s));s=r.from(l.copy(s));let h=e.tr.step(new _e(t.pos-1,i,t.pos,i,new c(s,1,0),o.length,!0)),u=h.doc.resolve(i+2*o.length);u.nodeAfter&&u.nodeAfter.type==l.type&&Te(h.doc,u.pos)&&h.join(u.pos),n(h.scrollIntoView())}return!0}let p=a.type.spec.isolating||i>0&&h?null:Ge.findFrom(t,1),d=p&&p.$from.blockRange(p.$to),f=d&&Me(d);if(null!=f&&f>=t.depth)return n&&n(e.tr.lift(d,f).scrollIntoView()),!0;if(u&&wt(a,"start",!0)&&wt(l,"end")){let i=l,o=[];for(;o.push(i),!i.isTextblock;)i=i.lastChild;let s=a,h=1;for(;!s.isTextblock;s=s.firstChild)h++;if(i.canReplace(i.childCount,i.childCount,s.content)){if(n){let i=r.empty;for(let e=o.length-1;e>=0;e--)i=r.from(o[e].copy(i));n(e.tr.step(new _e(t.pos-o.length,t.pos+a.nodeSize,t.pos+h,t.pos+a.nodeSize-h,new c(i,o.length,0),0,!0)).scrollIntoView())}return!0}}return!1}function Et(e){return function(t,n){let r=t.selection,i=e<0?r.$from:r.$to,o=i.depth;for(;i.node(o).isInline;){if(!o)return!1;o--}return!!i.node(o).isTextblock&&(n&&n(t.tr.setSelection(et.create(t.doc,e<0?i.start(o):i.end(o)))),!0)}}const At=Et(-1),Mt=Et(1);function Ot(...e){return function(t,n,r){for(let i=0;i<e.length;i++)if(e[i](t,n,r))return!0;return!1}}let Nt=Ot(bt,(e,t,n)=>{let r=function(e,t){let{$cursor:n}=e.selection;return!n||(t?!t.endOfTextblock("backward",e):n.parentOffset>0)?null:n}(e,n);if(!r)return!1;let i=Ct(r);if(!i){let n=r.blockRange(),i=n&&Me(n);return null!=i&&(t&&t(e.tr.lift(n,i).scrollIntoView()),!0)}let o=i.nodeBefore;if(St(e,i,t,-1))return!0;if(0==r.parent.content.size&&(wt(o,"end")||nt.isSelectable(o)))for(let n=r.depth;;n--){let s=Re(e.doc,r.before(n),r.after(n),c.empty);if(s&&s.slice.size<s.to-s.from){if(t){let n=e.tr.step(s);n.setSelection(wt(o,"end")?Ge.findFrom(n.doc.resolve(n.mapping.map(i.pos,-1)),-1):nt.create(n.doc,i.pos-o.nodeSize)),t(n.scrollIntoView())}return!0}if(1==n||r.node(n-1).childCount>1)break}return!(!o.isAtom||i.depth!=r.depth-1)&&(t&&t(e.tr.delete(i.pos-o.nodeSize,i.pos).scrollIntoView()),!0)},(e,t,n)=>{let{$head:r,empty:i}=e.selection,o=r;if(!i)return!1;if(r.parent.isTextblock){if(n?!n.endOfTextblock("backward",e):r.parentOffset>0)return!1;o=Ct(r)}let s=o&&o.nodeBefore;return!(!s||!nt.isSelectable(s))&&(t&&t(e.tr.setSelection(nt.create(e.doc,o.pos-s.nodeSize)).scrollIntoView()),!0)}),Ft=Ot(bt,(e,t,n)=>{let r=function(e,t){let{$cursor:n}=e.selection;return!n||(t?!t.endOfTextblock("forward",e):n.parentOffset<n.parent.content.size)?null:n}(e,n);if(!r)return!1;let i=xt(r);if(!i)return!1;let o=i.nodeAfter;if(St(e,i,t,1))return!0;if(0==r.parent.content.size&&(wt(o,"start")||nt.isSelectable(o))){let n=Re(e.doc,r.before(),r.after(),c.empty);if(n&&n.slice.size<n.to-n.from){if(t){let r=e.tr.step(n);r.setSelection(wt(o,"start")?Ge.findFrom(r.doc.resolve(r.mapping.map(i.pos)),1):nt.create(r.doc,r.mapping.map(i.pos))),t(r.scrollIntoView())}return!0}}return!(!o.isAtom||i.depth!=r.depth-1)&&(t&&t(e.tr.delete(i.pos,i.pos+o.nodeSize).scrollIntoView()),!0)},(e,t,n)=>{let{$head:r,empty:i}=e.selection,o=r;if(!i)return!1;if(r.parent.isTextblock){if(n?!n.endOfTextblock("forward",e):r.parentOffset<r.parent.content.size)return!1;o=xt(r)}let s=o&&o.nodeAfter;return!(!s||!nt.isSelectable(s))&&(t&&t(e.tr.setSelection(nt.create(e.doc,o.pos)).scrollIntoView()),!0)});const Tt={Enter:Ot((e,t)=>{let{$head:n,$anchor:r}=e.selection;return!(!n.parent.type.spec.code||!n.sameParent(r))&&(t&&t(e.tr.insertText("\n").scrollIntoView()),!0)},(e,t)=>{let n=e.selection,{$from:r,$to:i}=n;if(n instanceof it||r.parent.inlineContent||i.parent.inlineContent)return!1;let o=Dt(i.parent.contentMatchAt(i.indexAfter()));if(!o||!o.isTextblock)return!1;if(t){let n=(!r.parentOffset&&i.index()<i.parent.childCount?r:i).pos,s=e.tr.insert(n,o.createAndFill());s.setSelection(et.create(s.doc,n+1)),t(s.scrollIntoView())}return!0},(e,t)=>{let{$cursor:n}=e.selection;if(!n||n.parent.content.size)return!1;if(n.depth>1&&n.after()!=n.end(-1)){let r=n.before();if(Fe(e.doc,r))return t&&t(e.tr.split(r).scrollIntoView()),!0}let r=n.blockRange(),i=r&&Me(r);return null!=i&&(t&&t(e.tr.lift(r,i).scrollIntoView()),!0)},vt),"Mod-Enter":(e,t)=>{let{$head:n,$anchor:r}=e.selection;if(!n.parent.type.spec.code||!n.sameParent(r))return!1;let i=n.node(-1),o=n.indexAfter(-1),s=Dt(i.contentMatchAt(o));if(!s||!i.canReplaceWith(o,o,s))return!1;if(t){let r=n.after(),i=e.tr.replaceWith(r,r,s.createAndFill());i.setSelection(Ge.near(i.doc.resolve(r),1)),t(i.scrollIntoView())}return!0},Backspace:Nt,"Mod-Backspace":Nt,"Shift-Backspace":Nt,Delete:Ft,"Mod-Delete":Ft,"Mod-a":(e,t)=>(t&&t(e.tr.setSelection(new it(e.doc))),!0)},Rt={"Ctrl-h":Tt.Backspace,"Alt-Backspace":Tt["Mod-Backspace"],"Ctrl-d":Tt.Delete,"Ctrl-Alt-Backspace":Tt["Mod-Delete"],"Alt-Delete":Tt["Mod-Delete"],"Alt-d":Tt["Mod-Delete"],"Ctrl-a":At,"Ctrl-e":Mt};for(let e in Tt)Rt[e]=Tt[e];const zt=("undefined"!=typeof navigator?/Mac|iP(hone|[oa]d)/.test(navigator.platform):!("undefined"==typeof os||!os.platform)&&"darwin"==os.platform())?Rt:Tt;var It=200,Bt=function(){};Bt.prototype.append=function(e){return e.length?(e=Bt.from(e),!this.length&&e||e.length<It&&this.leafAppend(e)||this.length<It&&e.leafPrepend(this)||this.appendInner(e)):this},Bt.prototype.prepend=function(e){return e.length?Bt.from(e).append(this):this},Bt.prototype.appendInner=function(e){return new Lt(this,e)},Bt.prototype.slice=function(e,t){return void 0===e&&(e=0),void 0===t&&(t=this.length),e>=t?Bt.empty:this.sliceInner(Math.max(0,e),Math.min(this.length,t))},Bt.prototype.get=function(e){if(!(e<0||e>=this.length))return this.getInner(e)},Bt.prototype.forEach=function(e,t,n){void 0===t&&(t=0),void 0===n&&(n=this.length),t<=n?this.forEachInner(e,t,n,0):this.forEachInvertedInner(e,t,n,0)},Bt.prototype.map=function(e,t,n){void 0===t&&(t=0),void 0===n&&(n=this.length);var r=[];return this.forEach(function(t,n){return r.push(e(t,n))},t,n),r},Bt.from=function(e){return e instanceof Bt?e:e&&e.length?new Pt(e):Bt.empty};var Pt=function(e){function t(t){e.call(this),this.values=t}e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t;var n={length:{configurable:!0},depth:{configurable:!0}};return t.prototype.flatten=function(){return this.values},t.prototype.sliceInner=function(e,n){return 0==e&&n==this.length?this:new t(this.values.slice(e,n))},t.prototype.getInner=function(e){return this.values[e]},t.prototype.forEachInner=function(e,t,n,r){for(var i=t;i<n;i++)if(!1===e(this.values[i],r+i))return!1},t.prototype.forEachInvertedInner=function(e,t,n,r){for(var i=t-1;i>=n;i--)if(!1===e(this.values[i],r+i))return!1},t.prototype.leafAppend=function(e){if(this.length+e.length<=It)return new t(this.values.concat(e.flatten()))},t.prototype.leafPrepend=function(e){if(this.length+e.length<=It)return new t(e.flatten().concat(this.values))},n.length.get=function(){return this.values.length},n.depth.get=function(){return 0},Object.defineProperties(t.prototype,n),t}(Bt);Bt.empty=new Pt([]);var Lt=function(e){function t(t,n){e.call(this),this.left=t,this.right=n,this.length=t.length+n.length,this.depth=Math.max(t.depth,n.depth)+1}return e&&(t.__proto__=e),t.prototype=Object.create(e&&e.prototype),t.prototype.constructor=t,t.prototype.flatten=function(){return this.left.flatten().concat(this.right.flatten())},t.prototype.getInner=function(e){return e<this.left.length?this.left.get(e):this.right.get(e-this.left.length)},t.prototype.forEachInner=function(e,t,n,r){var i=this.left.length;return!(t<i&&!1===this.left.forEachInner(e,t,Math.min(n,i),r))&&(!(n>i&&!1===this.right.forEachInner(e,Math.max(t-i,0),Math.min(this.length,n)-i,r+i))&&void 0)},t.prototype.forEachInvertedInner=function(e,t,n,r){var i=this.left.length;return!(t>i&&!1===this.right.forEachInvertedInner(e,t-i,Math.max(n,i)-i,r+i))&&(!(n<i&&!1===this.left.forEachInvertedInner(e,Math.min(t,i),n,r))&&void 0)},t.prototype.sliceInner=function(e,t){if(0==e&&t==this.length)return this;var n=this.left.length;return t<=n?this.left.slice(e,t):e>=n?this.right.slice(e-n,t-n):this.left.slice(e,n).append(this.right.slice(0,t-n))},t.prototype.leafAppend=function(e){var n=this.right.leafAppend(e);if(n)return new t(this.left,n)},t.prototype.leafPrepend=function(e){var n=this.left.leafPrepend(e);if(n)return new t(n,this.right)},t.prototype.appendInner=function(e){return this.left.depth>=Math.max(this.right.depth,e.depth)+1?new t(this.left,new t(this.right,e)):new t(this,e)},t}(Bt);class qt{constructor(e,t){this.items=e,this.eventCount=t}popEvent(e,t){if(0==this.eventCount)return null;let n,r,i=this.items.length;for(;;i--){if(this.items.get(i-1).selection){--i;break}}t&&(n=this.remapping(i,this.items.length),r=n.maps.length);let o,s,l=e.tr,a=[],c=[];return this.items.forEach((e,t)=>{if(!e.step)return n||(n=this.remapping(i,t+1),r=n.maps.length),r--,void c.push(e);if(n){c.push(new $t(e.map));let t,i=e.step.map(n.slice(r));i&&l.maybeStep(i).doc&&(t=l.mapping.maps[l.mapping.maps.length-1],a.push(new $t(t,void 0,void 0,a.length+c.length))),r--,t&&n.appendMap(t,r)}else l.maybeStep(e.step);return e.selection?(o=n?e.selection.map(n.slice(r)):e.selection,s=new qt(this.items.slice(0,i).append(c.reverse().concat(a)),this.eventCount-1),!1):void 0},this.items.length,0),{remaining:s,transform:l,selection:o}}addTransform(e,t,n,r){let i=[],o=this.eventCount,s=this.items,l=!r&&s.length?s.get(s.length-1):null;for(let n=0;n<e.steps.length;n++){let a,c=e.steps[n].invert(e.docs[n]),h=new $t(e.mapping.maps[n],c,t);(a=l&&l.merge(h))&&(h=a,n?i.pop():s=s.slice(0,s.length-1)),i.push(h),t&&(o++,t=void 0),r||(l=h)}let a=o-n.depth;return a>jt&&(s=function(e,t){let n;return e.forEach((e,r)=>{if(e.selection&&0==t--)return n=r,!1}),e.slice(n)}(s,a),o-=a),new qt(s.append(i),o)}remapping(e,t){let n=new me;return this.items.forEach((t,r)=>{let i=null!=t.mirrorOffset&&r-t.mirrorOffset>=e?n.maps.length-t.mirrorOffset:void 0;n.appendMap(t.map,i)},e,t),n}addMaps(e){return 0==this.eventCount?this:new qt(this.items.append(e.map(e=>new $t(e))),this.eventCount)}rebased(e,t){if(!this.eventCount)return this;let n=[],r=Math.max(0,this.items.length-t),i=e.mapping,o=e.steps.length,s=this.eventCount;this.items.forEach(e=>{e.selection&&s--},r);let l=t;this.items.forEach(t=>{let r=i.getMirror(--l);if(null==r)return;o=Math.min(o,r);let a=i.maps[r];if(t.step){let o=e.steps[r].invert(e.docs[r]),c=t.selection&&t.selection.map(i.slice(l+1,r));c&&s++,n.push(new $t(a,o,c))}else n.push(new $t(a))},r);let a=[];for(let e=t;e<o;e++)a.push(new $t(i.maps[e]));let c=this.items.slice(0,r).append(a).append(n),h=new qt(c,s);return h.emptyItemCount()>500&&(h=h.compress(this.items.length-n.length)),h}emptyItemCount(){let e=0;return this.items.forEach(t=>{t.step||e++}),e}compress(e=this.items.length){let t=this.remapping(0,e),n=t.maps.length,r=[],i=0;return this.items.forEach((o,s)=>{if(s>=e)r.push(o),o.selection&&i++;else if(o.step){let e=o.step.map(t.slice(n)),s=e&&e.getMap();if(n--,s&&t.appendMap(s,n),e){let l=o.selection&&o.selection.map(t.slice(n));l&&i++;let a,c=new $t(s.invert(),e,l),h=r.length-1;(a=r.length&&r[h].merge(c))?r[h]=a:r.push(c)}}else o.map&&n--},this.items.length,0),new qt(Bt.from(r.reverse()),i)}}qt.empty=new qt(Bt.empty,0);class $t{constructor(e,t,n,r){this.map=e,this.step=t,this.selection=n,this.mirrorOffset=r}merge(e){if(this.step&&e.step&&!e.selection){let t=e.step.merge(this.step);if(t)return new $t(t.getMap().invert(),t,this.selection)}}}class Vt{constructor(e,t,n,r,i){this.done=e,this.undone=t,this.prevRanges=n,this.prevTime=r,this.prevComposition=i}}const jt=20;function Jt(e){let t=[];for(let n=e.length-1;n>=0&&0==t.length;n--)e[n].forEach((e,n,r,i)=>t.push(r,i));return t}function Wt(e,t){if(!e)return null;let n=[];for(let r=0;r<e.length;r+=2){let i=t.map(e[r],1),o=t.map(e[r+1],-1);i<=o&&n.push(i,o)}return n}let Ht=!1,Ut=null;function Kt(e){let t=e.plugins;if(Ut!=t){Ht=!1,Ut=t;for(let e=0;e<t.length;e++)if(t[e].spec.historyPreserveItems){Ht=!0;break}}return Ht}const Zt=new kt("history"),Gt=new kt("closeHistory");function Qt(e={}){return e={depth:e.depth||100,newGroupDelay:e.newGroupDelay||500},new mt({key:Zt,state:{init:()=>new Vt(qt.empty,qt.empty,null,0,-1),apply:(t,n,r)=>function(e,t,n,r){let i,o=n.getMeta(Zt);if(o)return o.historyState;n.getMeta(Gt)&&(e=new Vt(e.done,e.undone,null,0,-1));let s=n.getMeta("appendedTransaction");if(0==n.steps.length)return e;if(s&&s.getMeta(Zt))return s.getMeta(Zt).redo?new Vt(e.done.addTransform(n,void 0,r,Kt(t)),e.undone,Jt(n.mapping.maps),e.prevTime,e.prevComposition):new Vt(e.done,e.undone.addTransform(n,void 0,r,Kt(t)),null,e.prevTime,e.prevComposition);if(!1===n.getMeta("addToHistory")||s&&!1===s.getMeta("addToHistory"))return(i=n.getMeta("rebased"))?new Vt(e.done.rebased(n,i),e.undone.rebased(n,i),Wt(e.prevRanges,n.mapping),e.prevTime,e.prevComposition):new Vt(e.done.addMaps(n.mapping.maps),e.undone.addMaps(n.mapping.maps),Wt(e.prevRanges,n.mapping),e.prevTime,e.prevComposition);{let i=n.getMeta("composition"),o=0==e.prevTime||!s&&e.prevComposition!=i&&(e.prevTime<(n.time||0)-r.newGroupDelay||!function(e,t){if(!t)return!1;if(!e.docChanged)return!0;let n=!1;return e.mapping.maps[0].forEach((e,r)=>{for(let i=0;i<t.length;i+=2)e<=t[i+1]&&r>=t[i]&&(n=!0)}),n}(n,e.prevRanges)),l=s?Wt(e.prevRanges,n.mapping):Jt(n.mapping.maps);return new Vt(e.done.addTransform(n,o?t.selection.getBookmark():void 0,r,Kt(t)),qt.empty,l,n.time,null==i?e.prevComposition:i)}}(n,r,t,e)},config:e,props:{handleDOMEvents:{beforeinput(e,t){let n=t.inputType,r="historyUndo"==n?Yt:"historyRedo"==n?en:null;return!(!r||!e.editable)&&(t.preventDefault(),r(e.state,e.dispatch))}}}})}function Xt(e,t){return(n,r)=>{let i=Zt.getState(n);if(!i||0==(e?i.undone:i.done).eventCount)return!1;if(r){let o=function(e,t,n){let r=Kt(t),i=Zt.get(t).spec.config,o=(n?e.undone:e.done).popEvent(t,r);if(!o)return null;let s=o.selection.resolve(o.transform.doc),l=(n?e.done:e.undone).addTransform(o.transform,t.selection.getBookmark(),i,r),a=new Vt(n?l:o.remaining,n?o.remaining:l,null,0,-1);return o.transform.setSelection(s).setMeta(Zt,{redo:n,historyState:a})}(i,n,e);o&&r(t?o.scrollIntoView():o)}return!0}}const Yt=Xt(!1,!0),en=Xt(!0,!0);for(var tn={8:"Backspace",9:"Tab",10:"Enter",12:"NumLock",13:"Enter",16:"Shift",17:"Control",18:"Alt",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",44:"PrintScreen",45:"Insert",46:"Delete",59:";",61:"=",91:"Meta",92:"Meta",106:"*",107:"+",108:",",109:"-",110:".",111:"/",144:"NumLock",145:"ScrollLock",160:"Shift",161:"Shift",162:"Control",163:"Control",164:"Alt",165:"Alt",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},nn={48:")",49:"!",50:"@",51:"#",52:"$",53:"%",54:"^",55:"&",56:"*",57:"(",59:":",61:"+",173:"_",186:":",187:"+",188:"<",189:"_",190:">",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'},rn="undefined"!=typeof navigator&&/Mac/.test(navigator.platform),on="undefined"!=typeof navigator&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),sn=0;sn<10;sn++)tn[48+sn]=tn[96+sn]=String(sn);for(sn=1;sn<=24;sn++)tn[sn+111]="F"+sn;for(sn=65;sn<=90;sn++)tn[sn]=String.fromCharCode(sn+32),nn[sn]=String.fromCharCode(sn);for(var ln in tn)nn.hasOwnProperty(ln)||(nn[ln]=tn[ln]);const an="undefined"!=typeof navigator&&/Mac|iP(hone|[oa]d)/.test(navigator.platform),cn="undefined"!=typeof navigator&&/Win/.test(navigator.platform);function hn(e){let t,n,r,i,o=e.split(/-(?!$)/),s=o[o.length-1];"Space"==s&&(s=" ");for(let e=0;e<o.length-1;e++){let s=o[e];if(/^(cmd|meta|m)$/i.test(s))i=!0;else if(/^a(lt)?$/i.test(s))t=!0;else if(/^(c|ctrl|control)$/i.test(s))n=!0;else if(/^s(hift)?$/i.test(s))r=!0;else{if(!/^mod$/i.test(s))throw new Error("Unrecognized modifier name: "+s);an?i=!0:n=!0}}return t&&(s="Alt-"+s),n&&(s="Ctrl-"+s),i&&(s="Meta-"+s),r&&(s="Shift-"+s),s}function un(e,t,n=!0){return t.altKey&&(e="Alt-"+e),t.ctrlKey&&(e="Ctrl-"+e),t.metaKey&&(e="Meta-"+e),n&&t.shiftKey&&(e="Shift-"+e),e}function pn(e){return new mt({props:{handleKeyDown:dn(e)}})}function dn(e){let t=function(e){let t=Object.create(null);for(let n in e)t[hn(n)]=e[n];return t}(e);return function(e,n){let r,i=function(e){var t=!(rn&&e.metaKey&&e.shiftKey&&!e.ctrlKey&&!e.altKey||on&&e.shiftKey&&e.key&&1==e.key.length||"Unidentified"==e.key)&&e.key||(e.shiftKey?nn:tn)[e.keyCode]||e.key||"Unidentified";return"Esc"==t&&(t="Escape"),"Del"==t&&(t="Delete"),"Left"==t&&(t="ArrowLeft"),"Up"==t&&(t="ArrowUp"),"Right"==t&&(t="ArrowRight"),"Down"==t&&(t="ArrowDown"),t}(n),o=t[un(i,n)];if(o&&o(e.state,e.dispatch,e))return!0;if(1==i.length&&" "!=i){if(n.shiftKey){let r=t[un(i,n,!1)];if(r&&r(e.state,e.dispatch,e))return!0}if((n.altKey||n.metaKey||n.ctrlKey)&&!(cn&&n.ctrlKey&&n.altKey)&&(r=tn[n.keyCode])&&r!=i){let i=t[un(r,n)];if(i&&i(e.state,e.dispatch,e))return!0}}return!1}}const fn=["p",0],mn=["blockquote",0],gn=["hr"],yn=["pre",["code",0]],kn=["br"],bn=["em",0],wn=["strong",0],Cn=["code",0],xn=new H({nodes:{doc:{content:"block+"},paragraph:{content:"inline*",group:"block",parseDOM:[{tag:"p"}],toDOM:()=>fn},blockquote:{content:"block+",group:"block",defining:!0,parseDOM:[{tag:"blockquote"}],toDOM:()=>mn},horizontal_rule:{group:"block",parseDOM:[{tag:"hr"}],toDOM:()=>gn},heading:{attrs:{level:{default:1,validate:"number"}},content:"inline*",group:"block",defining:!0,parseDOM:[{tag:"h1",attrs:{level:1}},{tag:"h2",attrs:{level:2}},{tag:"h3",attrs:{level:3}},{tag:"h4",attrs:{level:4}},{tag:"h5",attrs:{level:5}},{tag:"h6",attrs:{level:6}}],toDOM:e=>["h"+e.attrs.level,0]},code_block:{content:"text*",marks:"",group:"block",code:!0,defining:!0,parseDOM:[{tag:"pre",preserveWhitespace:"full"}],toDOM:()=>yn},text:{group:"inline"},image:{inline:!0,attrs:{src:{validate:"string"},alt:{default:null,validate:"string|null"},title:{default:null,validate:"string|null"}},group:"inline",draggable:!0,parseDOM:[{tag:"img[src]",getAttrs:e=>({src:e.getAttribute("src"),title:e.getAttribute("title"),alt:e.getAttribute("alt")})}],toDOM(e){let{src:t,alt:n,title:r}=e.attrs;return["img",{src:t,alt:n,title:r}]}},hard_break:{inline:!0,group:"inline",selectable:!1,parseDOM:[{tag:"br"}],toDOM:()=>kn}},marks:{link:{attrs:{href:{validate:"string"},title:{default:null,validate:"string|null"}},inclusive:!1,parseDOM:[{tag:"a[href]",getAttrs:e=>({href:e.getAttribute("href"),title:e.getAttribute("title")})}],toDOM(e){let{href:t,title:n}=e.attrs;return["a",{href:t,title:n},0]}},em:{parseDOM:[{tag:"i"},{tag:"em"},{style:"font-style=italic"},{style:"font-style=normal",clearMark:e=>"em"==e.type.name}],toDOM:()=>bn},strong:{parseDOM:[{tag:"strong"},{tag:"b",getAttrs:e=>"normal"!=e.style.fontWeight&&null},{style:"font-weight=400",clearMark:e=>"strong"==e.type.name},{style:"font-weight",getAttrs:e=>/^(bold(er)?|[5-9]\d{2,})$/.test(e)&&null}],toDOM:()=>wn},code:{code:!0,parseDOM:[{tag:"code"}],toDOM:()=>Cn}}}),Dn=function(e){for(var t=0;;t++)if(!(e=e.previousSibling))return t},vn=function(e){let t=e.assignedSlot||e.parentNode;return t&&11==t.nodeType?t.host:t};let _n=null;const Sn=function(e,t,n){let r=_n||(_n=document.createRange());return r.setEnd(e,null==n?e.nodeValue.length:n),r.setStart(e,t||0),r},En=function(e,t,n,r){return n&&(Mn(e,t,n,r,-1)||Mn(e,t,n,r,1))},An=/^(img|br|input|textarea|hr)$/i;function Mn(e,t,n,r,i){for(var o;;){if(e==n&&t==r)return!0;if(t==(i<0?0:On(e))){let n=e.parentNode;if(!n||1!=n.nodeType||Nn(e)||An.test(e.nodeName)||"false"==e.contentEditable)return!1;t=Dn(e)+(i<0?0:1),e=n}else{if(1!=e.nodeType)return!1;{let n=e.childNodes[t+(i<0?-1:0)];if(1==n.nodeType&&"false"==n.contentEditable){if(!(null===(o=n.pmViewDesc)||void 0===o?void 0:o.ignoreForSelection))return!1;t+=i}else e=n,t=i<0?On(e):0}}}}function On(e){return 3==e.nodeType?e.nodeValue.length:e.childNodes.length}function Nn(e){let t;for(let n=e;n&&!(t=n.pmViewDesc);n=n.parentNode);return t&&t.node&&t.node.isBlock&&(t.dom==e||t.contentDOM==e)}const Fn=function(e){return e.focusNode&&En(e.focusNode,e.focusOffset,e.anchorNode,e.anchorOffset)};function Tn(e,t){let n=document.createEvent("Event");return n.initEvent("keydown",!0,!0),n.keyCode=e,n.key=n.code=t,n}const Rn="undefined"!=typeof navigator?navigator:null,zn="undefined"!=typeof document?document:null,In=Rn&&Rn.userAgent||"",Bn=/Edge\/(\d+)/.exec(In),Pn=/MSIE \d/.exec(In),Ln=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(In),qn=!!(Pn||Ln||Bn),$n=Pn?document.documentMode:Ln?+Ln[1]:Bn?+Bn[1]:0,Vn=!qn&&/gecko\/(\d+)/i.test(In);Vn&&(/Firefox\/(\d+)/.exec(In)||[0,0])[1];const jn=!qn&&/Chrome\/(\d+)/.exec(In),Jn=!!jn,Wn=jn?+jn[1]:0,Hn=!qn&&!!Rn&&/Apple Computer/.test(Rn.vendor),Un=Hn&&(/Mobile\/\w+/.test(In)||!!Rn&&Rn.maxTouchPoints>2),Kn=Un||!!Rn&&/Mac/.test(Rn.platform),Zn=!!Rn&&/Win/.test(Rn.platform),Gn=/Android \d/.test(In),Qn=!!zn&&"webkitFontSmoothing"in zn.documentElement.style,Xn=Qn?+(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent)||[0,0])[1]:0;function Yn(e){let t=e.defaultView&&e.defaultView.visualViewport;return t?{left:0,right:t.width,top:0,bottom:t.height}:{left:0,right:e.documentElement.clientWidth,top:0,bottom:e.documentElement.clientHeight}}function er(e,t){return"number"==typeof e?e:e[t]}function tr(e){let t=e.getBoundingClientRect(),n=t.width/e.offsetWidth||1,r=t.height/e.offsetHeight||1;return{left:t.left,right:t.left+e.clientWidth*n,top:t.top,bottom:t.top+e.clientHeight*r}}function nr(e,t,n){let r=e.someProp("scrollThreshold")||0,i=e.someProp("scrollMargin")||5,o=e.dom.ownerDocument;for(let s=n||e.dom;s;){if(1!=s.nodeType){s=vn(s);continue}let e=s,n=e==o.body,l=n?Yn(o):tr(e),a=0,c=0;if(t.top<l.top+er(r,"top")?c=-(l.top-t.top+er(i,"top")):t.bottom>l.bottom-er(r,"bottom")&&(c=t.bottom-t.top>l.bottom-l.top?t.top+er(i,"top")-l.top:t.bottom-l.bottom+er(i,"bottom")),t.left<l.left+er(r,"left")?a=-(l.left-t.left+er(i,"left")):t.right>l.right-er(r,"right")&&(a=t.right-l.right+er(i,"right")),a||c)if(n)o.defaultView.scrollBy(a,c);else{let n=e.scrollLeft,r=e.scrollTop;c&&(e.scrollTop+=c),a&&(e.scrollLeft+=a);let i=e.scrollLeft-n,o=e.scrollTop-r;t={left:t.left-i,top:t.top-o,right:t.right-i,bottom:t.bottom-o}}let h=n?"fixed":getComputedStyle(s).position;if(/^(fixed|sticky)$/.test(h))break;s="absolute"==h?s.offsetParent:vn(s)}}function rr(e){let t=[],n=e.ownerDocument;for(let r=e;r&&(t.push({dom:r,top:r.scrollTop,left:r.scrollLeft}),e!=n);r=vn(r));return t}function ir(e,t){for(let n=0;n<e.length;n++){let{dom:r,top:i,left:o}=e[n];r.scrollTop!=i+t&&(r.scrollTop=i+t),r.scrollLeft!=o&&(r.scrollLeft=o)}}let or=null;function sr(e,t){let n,r,i,o,s=2e8,l=0,a=t.top,c=t.top;for(let h=e.firstChild,u=0;h;h=h.nextSibling,u++){let e;if(1==h.nodeType)e=h.getClientRects();else{if(3!=h.nodeType)continue;e=Sn(h).getClientRects()}for(let p=0;p<e.length;p++){let d=e[p];if(d.top<=a&&d.bottom>=c){a=Math.max(d.bottom,a),c=Math.min(d.top,c);let e=d.left>t.left?d.left-t.left:d.right<t.left?t.left-d.right:0;if(e<s){n=h,s=e,r=e&&3==n.nodeType?{left:d.right<t.left?d.right:d.left,top:t.top}:t,1==h.nodeType&&e&&(l=u+(t.left>=(d.left+d.right)/2?1:0));continue}}else d.top>t.top&&!i&&d.left<=t.left&&d.right>=t.left&&(i=h,o={left:Math.max(d.left,Math.min(d.right,t.left)),top:d.top});!n&&(t.left>=d.right&&t.top>=d.top||t.left>=d.left&&t.top>=d.bottom)&&(l=u+1)}}return!n&&i&&(n=i,r=o,s=0),n&&3==n.nodeType?function(e,t){let n,r=e.nodeValue.length,i=document.createRange();for(let o=0;o<r;o++){i.setEnd(e,o+1),i.setStart(e,o);let r=ur(i,1);if(r.top!=r.bottom&&lr(t,r)){n={node:e,offset:o+(t.left>=(r.left+r.right)/2?1:0)};break}}return i.detach(),n||{node:e,offset:0}}(n,r):!n||s&&1==n.nodeType?{node:e,offset:l}:sr(n,r)}function lr(e,t){return e.left>=t.left-1&&e.left<=t.right+1&&e.top>=t.top-1&&e.top<=t.bottom+1}function ar(e,t,n){let r=e.childNodes.length;if(r&&n.top<n.bottom)for(let i=Math.max(0,Math.min(r-1,Math.floor(r*(t.top-n.top)/(n.bottom-n.top))-2)),o=i;;){let n=e.childNodes[o];if(1==n.nodeType){let e=n.getClientRects();for(let r=0;r<e.length;r++){let i=e[r];if(lr(t,i))return ar(n,t,i)}}if((o=(o+1)%r)==i)break}return e}function cr(e,t){let n,r=e.dom.ownerDocument,i=0,o=function(e,t,n){if(e.caretPositionFromPoint)try{let r=e.caretPositionFromPoint(t,n);if(r)return{node:r.offsetNode,offset:Math.min(On(r.offsetNode),r.offset)}}catch(e){}if(e.caretRangeFromPoint){let r=e.caretRangeFromPoint(t,n);if(r)return{node:r.startContainer,offset:Math.min(On(r.startContainer),r.startOffset)}}}(r,t.left,t.top);o&&({node:n,offset:i}=o);let s,l=(e.root.elementFromPoint?e.root:r).elementFromPoint(t.left,t.top);if(!l||!e.dom.contains(1!=l.nodeType?l.parentNode:l)){let n=e.dom.getBoundingClientRect();if(!lr(t,n))return null;if(l=ar(e.dom,t,n),!l)return null}if(Hn)for(let e=l;n&&e;e=vn(e))e.draggable&&(n=void 0);if(l=function(e,t){let n=e.parentNode;return n&&/^li$/i.test(n.nodeName)&&t.left<e.getBoundingClientRect().left?n:e}(l,t),n){if(Vn&&1==n.nodeType&&(i=Math.min(i,n.childNodes.length),i<n.childNodes.length)){let e,r=n.childNodes[i];"IMG"==r.nodeName&&(e=r.getBoundingClientRect()).right<=t.left&&e.bottom>t.top&&i++}let r;Qn&&i&&1==n.nodeType&&1==(r=n.childNodes[i-1]).nodeType&&"false"==r.contentEditable&&r.getBoundingClientRect().top>=t.top&&i--,n==e.dom&&i==n.childNodes.length-1&&1==n.lastChild.nodeType&&t.top>n.lastChild.getBoundingClientRect().bottom?s=e.state.doc.content.size:0!=i&&1==n.nodeType&&"BR"==n.childNodes[i-1].nodeName||(s=function(e,t,n,r){let i=-1;for(let n=t,o=!1;n!=e.dom;){let t,s=e.docView.nearestDesc(n,!0);if(!s)return null;if(1==s.dom.nodeType&&(s.node.isBlock&&s.parent||!s.contentDOM)&&((t=s.dom.getBoundingClientRect()).width||t.height)&&(s.node.isBlock&&s.parent&&!/^T(R|BODY|HEAD|FOOT)$/.test(s.dom.nodeName)&&(!o&&t.left>r.left||t.top>r.top?i=s.posBefore:(!o&&t.right<r.left||t.bottom<r.top)&&(i=s.posAfter),o=!0),!s.contentDOM&&i<0&&!s.node.isText))return(s.node.isBlock?r.top<(t.top+t.bottom)/2:r.left<(t.left+t.right)/2)?s.posBefore:s.posAfter;n=s.dom.parentNode}return i>-1?i:e.docView.posFromDOM(t,n,-1)}(e,n,i,t))}null==s&&(s=function(e,t,n){let{node:r,offset:i}=sr(t,n),o=-1;if(1==r.nodeType&&!r.firstChild){let e=r.getBoundingClientRect();o=e.left!=e.right&&n.left>(e.left+e.right)/2?1:-1}return e.docView.posFromDOM(r,i,o)}(e,l,t));let a=e.docView.nearestDesc(l,!0);return{pos:s,inside:a?a.posAtStart-a.border:-1}}function hr(e){return e.top<e.bottom||e.left<e.right}function ur(e,t){let n=e.getClientRects();if(n.length){let e=n[t<0?0:n.length-1];if(hr(e))return e}return Array.prototype.find.call(n,hr)||e.getBoundingClientRect()}const pr=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;function dr(e,t,n){let{node:r,offset:i,atom:o}=e.docView.domFromPos(t,n<0?-1:1),s=Qn||Vn;if(3==r.nodeType){if(!s||!pr.test(r.nodeValue)&&(n<0?i:i!=r.nodeValue.length)){let e=i,t=i,o=n<0?1:-1;return n<0&&!i?(t++,o=-1):n>=0&&i==r.nodeValue.length?(e--,o=1):n<0?e--:t++,fr(ur(Sn(r,e,t),o),o<0)}{let e=ur(Sn(r,i,i),n);if(Vn&&i&&/\s/.test(r.nodeValue[i-1])&&i<r.nodeValue.length){let t=ur(Sn(r,i-1,i-1),-1);if(t.top==e.top){let n=ur(Sn(r,i,i+1),-1);if(n.top!=e.top)return fr(n,n.left<t.left)}}return e}}if(!e.state.doc.resolve(t-(o||0)).parent.inlineContent){if(null==o&&i&&(n<0||i==On(r))){let e=r.childNodes[i-1];if(1==e.nodeType)return mr(e.getBoundingClientRect(),!1)}if(null==o&&i<On(r)){let e=r.childNodes[i];if(1==e.nodeType)return mr(e.getBoundingClientRect(),!0)}return mr(r.getBoundingClientRect(),n>=0)}if(null==o&&i&&(n<0||i==On(r))){let e=r.childNodes[i-1],t=3==e.nodeType?Sn(e,On(e)-(s?0:1)):1!=e.nodeType||"BR"==e.nodeName&&e.nextSibling?null:e;if(t)return fr(ur(t,1),!1)}if(null==o&&i<On(r)){let e=r.childNodes[i];for(;e.pmViewDesc&&e.pmViewDesc.ignoreForCoords;)e=e.nextSibling;let t=e?3==e.nodeType?Sn(e,0,s?0:1):1==e.nodeType?e:null:null;if(t)return fr(ur(t,-1),!0)}return fr(ur(3==r.nodeType?Sn(r):r,-n),n>=0)}function fr(e,t){if(0==e.width)return e;let n=t?e.left:e.right;return{top:e.top,bottom:e.bottom,left:n,right:n}}function mr(e,t){if(0==e.height)return e;let n=t?e.top:e.bottom;return{top:n,bottom:n,left:e.left,right:e.right}}function gr(e,t,n){let r=e.state,i=e.root.activeElement;r!=t&&e.updateState(t),i!=e.dom&&e.focus();try{return n()}finally{r!=t&&e.updateState(r),i!=e.dom&&i&&i.focus()}}const yr=/[\u0590-\u08ac]/;let kr=null,br=null,wr=!1;function Cr(e,t,n){return kr==t&&br==n?wr:(kr=t,br=n,wr="up"==n||"down"==n?function(e,t,n){let r=t.selection,i="up"==n?r.$from:r.$to;return gr(e,t,()=>{let{node:t}=e.docView.domFromPos(i.pos,"up"==n?-1:1);for(;;){let n=e.docView.nearestDesc(t,!0);if(!n)break;if(n.node.isBlock){t=n.contentDOM||n.dom;break}t=n.dom.parentNode}let r=dr(e,i.pos,1);for(let e=t.firstChild;e;e=e.nextSibling){let t;if(1==e.nodeType)t=e.getClientRects();else{if(3!=e.nodeType)continue;t=Sn(e,0,e.nodeValue.length).getClientRects()}for(let e=0;e<t.length;e++){let i=t[e];if(i.bottom>i.top+1&&("up"==n?r.top-i.top>2*(i.bottom-r.top):i.bottom-r.bottom>2*(r.bottom-i.top)))return!1}}return!0})}(e,t,n):function(e,t,n){let{$head:r}=t.selection;if(!r.parent.isTextblock)return!1;let i=r.parentOffset,o=!i,s=i==r.parent.content.size,l=e.domSelection();return l?yr.test(r.parent.textContent)&&l.modify?gr(e,t,()=>{let{focusNode:t,focusOffset:i,anchorNode:o,anchorOffset:s}=e.domSelectionRange(),a=l.caretBidiLevel;l.modify("move",n,"character");let c=r.depth?e.docView.domAfterPos(r.before()):e.dom,{focusNode:h,focusOffset:u}=e.domSelectionRange(),p=h&&!c.contains(1==h.nodeType?h:h.parentNode)||t==h&&i==u;try{l.collapse(o,s),t&&(t!=o||i!=s)&&l.extend&&l.extend(t,i)}catch(e){}return null!=a&&(l.caretBidiLevel=a),p}):"left"==n||"backward"==n?o:s:r.pos==r.start()||r.pos==r.end()}(e,t,n))}class xr{constructor(e,t,n,r){this.parent=e,this.children=t,this.dom=n,this.contentDOM=r,this.dirty=0,n.pmViewDesc=this}matchesWidget(e){return!1}matchesMark(e){return!1}matchesNode(e,t,n){return!1}matchesHack(e){return!1}parseRule(){return null}stopEvent(e){return!1}get size(){let e=0;for(let t=0;t<this.children.length;t++)e+=this.children[t].size;return e}get border(){return 0}destroy(){this.parent=void 0,this.dom.pmViewDesc==this&&(this.dom.pmViewDesc=void 0);for(let e=0;e<this.children.length;e++)this.children[e].destroy()}posBeforeChild(e){for(let t=0,n=this.posAtStart;;t++){let r=this.children[t];if(r==e)return n;n+=r.size}}get posBefore(){return this.parent.posBeforeChild(this)}get posAtStart(){return this.parent?this.parent.posBeforeChild(this)+this.border:0}get posAfter(){return this.posBefore+this.size}get posAtEnd(){return this.posAtStart+this.size-2*this.border}localPosFromDOM(e,t,n){if(this.contentDOM&&this.contentDOM.contains(1==e.nodeType?e:e.parentNode)){if(n<0){let n,r;if(e==this.contentDOM)n=e.childNodes[t-1];else{for(;e.parentNode!=this.contentDOM;)e=e.parentNode;n=e.previousSibling}for(;n&&(!(r=n.pmViewDesc)||r.parent!=this);)n=n.previousSibling;return n?this.posBeforeChild(r)+r.size:this.posAtStart}{let n,r;if(e==this.contentDOM)n=e.childNodes[t];else{for(;e.parentNode!=this.contentDOM;)e=e.parentNode;n=e.nextSibling}for(;n&&(!(r=n.pmViewDesc)||r.parent!=this);)n=n.nextSibling;return n?this.posBeforeChild(r):this.posAtEnd}}let r;if(e==this.dom&&this.contentDOM)r=t>Dn(this.contentDOM);else if(this.contentDOM&&this.contentDOM!=this.dom&&this.dom.contains(this.contentDOM))r=2&e.compareDocumentPosition(this.contentDOM);else if(this.dom.firstChild){if(0==t)for(let t=e;;t=t.parentNode){if(t==this.dom){r=!1;break}if(t.previousSibling)break}if(null==r&&t==e.childNodes.length)for(let t=e;;t=t.parentNode){if(t==this.dom){r=!0;break}if(t.nextSibling)break}}return(null==r?n>0:r)?this.posAtEnd:this.posAtStart}nearestDesc(e,t=!1){for(let n=!0,r=e;r;r=r.parentNode){let i,o=this.getDesc(r);if(o&&(!t||o.node)){if(!n||!(i=o.nodeDOM)||(1==i.nodeType?i.contains(1==e.nodeType?e:e.parentNode):i==e))return o;n=!1}}}getDesc(e){let t=e.pmViewDesc;for(let e=t;e;e=e.parent)if(e==this)return t}posFromDOM(e,t,n){for(let r=e;r;r=r.parentNode){let i=this.getDesc(r);if(i)return i.localPosFromDOM(e,t,n)}return-1}descAt(e){for(let t=0,n=0;t<this.children.length;t++){let r=this.children[t],i=n+r.size;if(n==e&&i!=n){for(;!r.border&&r.children.length;)for(let e=0;e<r.children.length;e++){let t=r.children[e];if(t.size){r=t;break}}return r}if(e<i)return r.descAt(e-n-r.border);n=i}}domFromPos(e,t){if(!this.contentDOM)return{node:this.dom,offset:0,atom:e+1};let n=0,r=0;for(let t=0;n<this.children.length;n++){let i=this.children[n],o=t+i.size;if(o>e||i instanceof Mr){r=e-t;break}t=o}if(r)return this.children[n].domFromPos(r-this.children[n].border,t);for(let e;n&&!(e=this.children[n-1]).size&&e instanceof Dr&&e.side>=0;n--);if(t<=0){let e,r=!0;for(;e=n?this.children[n-1]:null,e&&e.dom.parentNode!=this.contentDOM;n--,r=!1);return e&&t&&r&&!e.border&&!e.domAtom?e.domFromPos(e.size,t):{node:this.contentDOM,offset:e?Dn(e.dom)+1:0}}{let e,r=!0;for(;e=n<this.children.length?this.children[n]:null,e&&e.dom.parentNode!=this.contentDOM;n++,r=!1);return e&&r&&!e.border&&!e.domAtom?e.domFromPos(0,t):{node:this.contentDOM,offset:e?Dn(e.dom):this.contentDOM.childNodes.length}}}parseRange(e,t,n=0){if(0==this.children.length)return{node:this.contentDOM,from:e,to:t,fromOffset:0,toOffset:this.contentDOM.childNodes.length};let r=-1,i=-1;for(let o=n,s=0;;s++){let n=this.children[s],l=o+n.size;if(-1==r&&e<=l){let i=o+n.border;if(e>=i&&t<=l-n.border&&n.node&&n.contentDOM&&this.contentDOM.contains(n.contentDOM))return n.parseRange(e,t,i);e=o;for(let t=s;t>0;t--){let n=this.children[t-1];if(n.size&&n.dom.parentNode==this.contentDOM&&!n.emptyChildAt(1)){r=Dn(n.dom)+1;break}e-=n.size}-1==r&&(r=0)}if(r>-1&&(l>t||s==this.children.length-1)){t=l;for(let e=s+1;e<this.children.length;e++){let n=this.children[e];if(n.size&&n.dom.parentNode==this.contentDOM&&!n.emptyChildAt(-1)){i=Dn(n.dom);break}t+=n.size}-1==i&&(i=this.contentDOM.childNodes.length);break}o=l}return{node:this.contentDOM,from:e,to:t,fromOffset:r,toOffset:i}}emptyChildAt(e){if(this.border||!this.contentDOM||!this.children.length)return!1;let t=this.children[e<0?0:this.children.length-1];return 0==t.size||t.emptyChildAt(e)}domAfterPos(e){let{node:t,offset:n}=this.domFromPos(e,0);if(1!=t.nodeType||n==t.childNodes.length)throw new RangeError("No node after pos "+e);return t.childNodes[n]}setSelection(e,t,n,r=!1){let i=Math.min(e,t),o=Math.max(e,t);for(let s=0,l=0;s<this.children.length;s++){let a=this.children[s],c=l+a.size;if(i>l&&o<c)return a.setSelection(e-l-a.border,t-l-a.border,n,r);l=c}let s=this.domFromPos(e,e?-1:1),l=t==e?s:this.domFromPos(t,t?-1:1),a=n.root.getSelection(),c=n.domSelectionRange(),h=!1;if((Vn||Hn)&&e==t){let{node:e,offset:t}=s;if(3==e.nodeType){if(h=!(!t||"\n"!=e.nodeValue[t-1]),h&&t==e.nodeValue.length)for(let t,n=e;n;n=n.parentNode){if(t=n.nextSibling){"BR"==t.nodeName&&(s=l={node:t.parentNode,offset:Dn(t)+1});break}let e=n.pmViewDesc;if(e&&e.node&&e.node.isBlock)break}}else{let n=e.childNodes[t-1];h=n&&("BR"==n.nodeName||"false"==n.contentEditable)}}if(Vn&&c.focusNode&&c.focusNode!=l.node&&1==c.focusNode.nodeType){let e=c.focusNode.childNodes[c.focusOffset];e&&"false"==e.contentEditable&&(r=!0)}if(!(r||h&&Hn)&&En(s.node,s.offset,c.anchorNode,c.anchorOffset)&&En(l.node,l.offset,c.focusNode,c.focusOffset))return;let u=!1;if((a.extend||e==t)&&(!h||!Vn)){a.collapse(s.node,s.offset);try{e!=t&&a.extend(l.node,l.offset),u=!0}catch(e){}}if(!u){if(e>t){let e=s;s=l,l=e}let n=document.createRange();n.setEnd(l.node,l.offset),n.setStart(s.node,s.offset),a.removeAllRanges(),a.addRange(n)}}ignoreMutation(e){return!this.contentDOM&&"selection"!=e.type}get contentLost(){return this.contentDOM&&this.contentDOM!=this.dom&&!this.dom.contains(this.contentDOM)}markDirty(e,t){for(let n=0,r=0;r<this.children.length;r++){let i=this.children[r],o=n+i.size;if(n==o?e<=o&&t>=n:e<o&&t>n){let r=n+i.border,s=o-i.border;if(e>=r&&t<=s)return this.dirty=e==n||t==o?2:1,void(e!=r||t!=s||!i.contentLost&&i.dom.parentNode==this.contentDOM?i.markDirty(e-r,t-r):i.dirty=3);i.dirty=i.dom!=i.contentDOM||i.dom.parentNode!=this.contentDOM||i.children.length?3:2}n=o}this.dirty=2}markParentsDirty(){let e=1;for(let t=this.parent;t;t=t.parent,e++){let n=1==e?2:1;t.dirty<n&&(t.dirty=n)}}get domAtom(){return!1}get ignoreForCoords(){return!1}get ignoreForSelection(){return!1}isText(e){return!1}}class Dr extends xr{constructor(e,t,n,r){let i,o=t.type.toDOM;if("function"==typeof o&&(o=o(n,()=>i?i.parent?i.parent.posBeforeChild(i):void 0:r)),!t.type.spec.raw){if(1!=o.nodeType){let e=document.createElement("span");e.appendChild(o),o=e}o.contentEditable="false",o.classList.add("ProseMirror-widget")}super(e,[],o,null),this.widget=t,this.widget=t,i=this}matchesWidget(e){return 0==this.dirty&&e.type.eq(this.widget.type)}parseRule(){return{ignore:!0}}stopEvent(e){let t=this.widget.spec.stopEvent;return!!t&&t(e)}ignoreMutation(e){return"selection"!=e.type||this.widget.spec.ignoreSelection}destroy(){this.widget.type.destroy(this.dom),super.destroy()}get domAtom(){return!0}get ignoreForSelection(){return!!this.widget.type.spec.relaxedSide}get side(){return this.widget.type.side}}class vr extends xr{constructor(e,t,n,r){super(e,[],t,null),this.textDOM=n,this.text=r}get size(){return this.text.length}localPosFromDOM(e,t){return e!=this.textDOM?this.posAtStart+(t?this.size:0):this.posAtStart+t}domFromPos(e){return{node:this.textDOM,offset:e}}ignoreMutation(e){return"characterData"===e.type&&e.target.nodeValue==e.oldValue}}class _r extends xr{constructor(e,t,n,r,i){super(e,[],n,r),this.mark=t,this.spec=i}static create(e,t,n,r){let i=r.nodeViews[t.type.name],o=i&&i(t,r,n);return o&&o.dom||(o=ie.renderSpec(document,t.type.spec.toDOM(t,n),null,t.attrs)),new _r(e,t,o.dom,o.contentDOM||o.dom,o)}parseRule(){return 3&this.dirty||this.mark.type.spec.reparseInView?null:{mark:this.mark.type.name,attrs:this.mark.attrs,contentElement:this.contentDOM}}matchesMark(e){return 3!=this.dirty&&this.mark.eq(e)}markDirty(e,t){if(super.markDirty(e,t),0!=this.dirty){let e=this.parent;for(;!e.node;)e=e.parent;e.dirty<this.dirty&&(e.dirty=this.dirty),this.dirty=0}}slice(e,t,n){let r=_r.create(this.parent,this.mark,!0,n),i=this.children,o=this.size;t<o&&(i=Vr(i,t,o,n)),e>0&&(i=Vr(i,0,e,n));for(let e=0;e<i.length;e++)i[e].parent=r;return r.children=i,r}ignoreMutation(e){return this.spec.ignoreMutation?this.spec.ignoreMutation(e):super.ignoreMutation(e)}destroy(){this.spec.destroy&&this.spec.destroy(),super.destroy()}}class Sr extends xr{constructor(e,t,n,r,i,o,s,l,a){super(e,[],i,o),this.node=t,this.outerDeco=n,this.innerDeco=r,this.nodeDOM=s}static create(e,t,n,r,i,o){let s,l=i.nodeViews[t.type.name],a=l&&l(t,i,()=>s?s.parent?s.parent.posBeforeChild(s):void 0:o,n,r),c=a&&a.dom,h=a&&a.contentDOM;if(t.isText)if(c){if(3!=c.nodeType)throw new RangeError("Text must be rendered as a DOM text node")}else c=document.createTextNode(t.text);else if(!c){let e=ie.renderSpec(document,t.type.spec.toDOM(t),null,t.attrs);({dom:c,contentDOM:h}=e)}h||t.isText||"BR"==c.nodeName||(c.hasAttribute("contenteditable")||(c.contentEditable="false"),t.type.spec.draggable&&(c.draggable=!0));let u=c;return c=Br(c,n,t),a?s=new Or(e,t,n,r,c,h||null,u,a,i,o+1):t.isText?new Ar(e,t,n,r,c,u,i):new Sr(e,t,n,r,c,h||null,u,i,o+1)}parseRule(){if(this.node.type.spec.reparseInView)return null;let e={node:this.node.type.name,attrs:this.node.attrs};if("pre"==this.node.type.whitespace&&(e.preserveWhitespace="full"),this.contentDOM)if(this.contentLost){for(let t=this.children.length-1;t>=0;t--){let n=this.children[t];if(this.dom.contains(n.dom.parentNode)){e.contentElement=n.dom.parentNode;break}}e.contentElement||(e.getContent=()=>r.empty)}else e.contentElement=this.contentDOM;else e.getContent=()=>this.node.content;return e}matchesNode(e,t,n){return 0==this.dirty&&e.eq(this.node)&&Pr(t,this.outerDeco)&&n.eq(this.innerDeco)}get size(){return this.node.nodeSize}get border(){return this.node.isLeaf?0:1}updateChildren(e,t){let n=this.node.inlineContent,r=t,i=e.composing?this.localCompositionInfo(e,t):null,o=i&&i.pos>-1?i:null,s=i&&i.pos<0,a=new qr(this,o&&o.node,e);!function(e,t,n,r){let i=t.locals(e),o=0;if(0==i.length){for(let n=0;n<e.childCount;n++){let s=e.child(n);r(s,i,t.forChild(o,s),n),o+=s.nodeSize}return}let s=0,l=[],a=null;for(let c=0;;){let h,u,p,d;for(;s<i.length&&i[s].to==o;){let e=i[s++];e.widget&&(h?(u||(u=[h])).push(e):h=e)}if(h)if(u){u.sort($r);for(let e=0;e<u.length;e++)n(u[e],c,!!a)}else n(h,c,!!a);if(a)d=-1,p=a,a=null;else{if(!(c<e.childCount))break;d=c,p=e.child(c++)}for(let e=0;e<l.length;e++)l[e].to<=o&&l.splice(e--,1);for(;s<i.length&&i[s].from<=o&&i[s].to>o;)l.push(i[s++]);let f=o+p.nodeSize;if(p.isText){let e=f;s<i.length&&i[s].from<e&&(e=i[s].from);for(let t=0;t<l.length;t++)l[t].to<e&&(e=l[t].to);e<f&&(a=p.cut(e-o),p=p.cut(0,e-o),f=e,d=-1)}else for(;s<i.length&&i[s].to<f;)s++;r(p,p.isInline&&!p.isLeaf?l.filter(e=>!e.inline):l.slice(),t.forChild(o,p),d),o=f}}(this.node,this.innerDeco,(t,i,o)=>{t.spec.marks?a.syncToMarks(t.spec.marks,n,e):t.type.side>=0&&!o&&a.syncToMarks(i==this.node.childCount?l.none:this.node.child(i).marks,n,e),a.placeWidget(t,e,r)},(t,o,l,c)=>{let h;a.syncToMarks(t.marks,n,e),a.findNodeMatch(t,o,l,c)||s&&e.state.selection.from>r&&e.state.selection.to<r+t.nodeSize&&(h=a.findIndexWithChild(i.node))>-1&&a.updateNodeAt(t,o,l,h,e)||a.updateNextNode(t,o,l,e,c,r)||a.addNode(t,o,l,e,r),r+=t.nodeSize}),a.syncToMarks([],n,e),this.node.isTextblock&&a.addTextblockHacks(),a.destroyRest(),(a.changed||2==this.dirty)&&(o&&this.protectLocalComposition(e,o),Nr(this.contentDOM,this.children,e),Un&&function(e){if("UL"==e.nodeName||"OL"==e.nodeName){let t=e.style.cssText;e.style.cssText=t+"; list-style: square !important",window.getComputedStyle(e).listStyle,e.style.cssText=t}}(this.dom))}localCompositionInfo(e,t){let{from:n,to:r}=e.state.selection;if(!(e.state.selection instanceof et)||n<t||r>t+this.node.content.size)return null;let i=e.input.compositionNode;if(!i||!this.dom.contains(i.parentNode))return null;if(this.node.inlineContent){let e=i.nodeValue,o=function(e,t,n,r){for(let i=0,o=0;i<e.childCount&&o<=r;){let s=e.child(i++),l=o;if(o+=s.nodeSize,!s.isText)continue;let a=s.text;for(;i<e.childCount;){let t=e.child(i++);if(o+=t.nodeSize,!t.isText)break;a+=t.text}if(o>=n){if(o>=r&&a.slice(r-t.length-l,r-l)==t)return r-t.length;let e=l<r?a.lastIndexOf(t,r-l-1):-1;if(e>=0&&e+t.length+l>=n)return l+e;if(n==r&&a.length>=r+t.length-l&&a.slice(r-l,r-l+t.length)==t)return r}}return-1}(this.node.content,e,n-t,r-t);return o<0?null:{node:i,pos:o,text:e}}return{node:i,pos:-1,text:""}}protectLocalComposition(e,{node:t,pos:n,text:r}){if(this.getDesc(t))return;let i=t;for(;i.parentNode!=this.contentDOM;i=i.parentNode){for(;i.previousSibling;)i.parentNode.removeChild(i.previousSibling);for(;i.nextSibling;)i.parentNode.removeChild(i.nextSibling);i.pmViewDesc&&(i.pmViewDesc=void 0)}let o=new vr(this,i,t,r);e.input.compositionNodes.push(o),this.children=Vr(this.children,n,n+r.length,e,o)}update(e,t,n,r){return!(3==this.dirty||!e.sameMarkup(this.node))&&(this.updateInner(e,t,n,r),!0)}updateInner(e,t,n,r){this.updateOuterDeco(t),this.node=e,this.innerDeco=n,this.contentDOM&&this.updateChildren(r,this.posAtStart),this.dirty=0}updateOuterDeco(e){if(Pr(e,this.outerDeco))return;let t=1!=this.nodeDOM.nodeType,n=this.dom;this.dom=zr(this.dom,this.nodeDOM,Rr(this.outerDeco,this.node,t),Rr(e,this.node,t)),this.dom!=n&&(n.pmViewDesc=void 0,this.dom.pmViewDesc=this),this.outerDeco=e}selectNode(){1==this.nodeDOM.nodeType&&(this.nodeDOM.classList.add("ProseMirror-selectednode"),!this.contentDOM&&this.node.type.spec.draggable||(this.nodeDOM.draggable=!0))}deselectNode(){1==this.nodeDOM.nodeType&&(this.nodeDOM.classList.remove("ProseMirror-selectednode"),!this.contentDOM&&this.node.type.spec.draggable||this.nodeDOM.removeAttribute("draggable"))}get domAtom(){return this.node.isAtom}}function Er(e,t,n,r,i){Br(r,t,e);let o=new Sr(void 0,e,t,n,r,r,r,i,0);return o.contentDOM&&o.updateChildren(i,0),o}class Ar extends Sr{constructor(e,t,n,r,i,o,s){super(e,t,n,r,i,null,o,s,0)}parseRule(){let e=this.nodeDOM.parentNode;for(;e&&e!=this.dom&&!e.pmIsDeco;)e=e.parentNode;return{skip:e||!0}}update(e,t,n,r){return!(3==this.dirty||0!=this.dirty&&!this.inParent()||!e.sameMarkup(this.node))&&(this.updateOuterDeco(t),0==this.dirty&&e.text==this.node.text||e.text==this.nodeDOM.nodeValue||(this.nodeDOM.nodeValue=e.text,r.trackWrites==this.nodeDOM&&(r.trackWrites=null)),this.node=e,this.dirty=0,!0)}inParent(){let e=this.parent.contentDOM;for(let t=this.nodeDOM;t;t=t.parentNode)if(t==e)return!0;return!1}domFromPos(e){return{node:this.nodeDOM,offset:e}}localPosFromDOM(e,t,n){return e==this.nodeDOM?this.posAtStart+Math.min(t,this.node.text.length):super.localPosFromDOM(e,t,n)}ignoreMutation(e){return"characterData"!=e.type&&"selection"!=e.type}slice(e,t,n){let r=this.node.cut(e,t),i=document.createTextNode(r.text);return new Ar(this.parent,r,this.outerDeco,this.innerDeco,i,i,n)}markDirty(e,t){super.markDirty(e,t),this.dom==this.nodeDOM||0!=e&&t!=this.nodeDOM.nodeValue.length||(this.dirty=3)}get domAtom(){return!1}isText(e){return this.node.text==e}}class Mr extends xr{parseRule(){return{ignore:!0}}matchesHack(e){return 0==this.dirty&&this.dom.nodeName==e}get domAtom(){return!0}get ignoreForCoords(){return"IMG"==this.dom.nodeName}}class Or extends Sr{constructor(e,t,n,r,i,o,s,l,a,c){super(e,t,n,r,i,o,s,a,c),this.spec=l}update(e,t,n,r){if(3==this.dirty)return!1;if(this.spec.update&&(this.node.type==e.type||this.spec.multiType)){let i=this.spec.update(e,t,n);return i&&this.updateInner(e,t,n,r),i}return!(!this.contentDOM&&!e.isLeaf)&&super.update(e,t,n,r)}selectNode(){this.spec.selectNode?this.spec.selectNode():super.selectNode()}deselectNode(){this.spec.deselectNode?this.spec.deselectNode():super.deselectNode()}setSelection(e,t,n,r){this.spec.setSelection?this.spec.setSelection(e,t,n.root):super.setSelection(e,t,n,r)}destroy(){this.spec.destroy&&this.spec.destroy(),super.destroy()}stopEvent(e){return!!this.spec.stopEvent&&this.spec.stopEvent(e)}ignoreMutation(e){return this.spec.ignoreMutation?this.spec.ignoreMutation(e):super.ignoreMutation(e)}}function Nr(e,t,n){let r=e.firstChild,i=!1;for(let o=0;o<t.length;o++){let s=t[o],l=s.dom;if(l.parentNode==e){for(;l!=r;)r=Lr(r),i=!0;r=r.nextSibling}else i=!0,e.insertBefore(l,r);if(s instanceof _r){let t=r?r.previousSibling:e.lastChild;Nr(s.contentDOM,s.children,n),r=t?t.nextSibling:e.firstChild}}for(;r;)r=Lr(r),i=!0;i&&n.trackWrites==e&&(n.trackWrites=null)}const Fr=function(e){e&&(this.nodeName=e)};Fr.prototype=Object.create(null);const Tr=[new Fr];function Rr(e,t,n){if(0==e.length)return Tr;let r=n?Tr[0]:new Fr,i=[r];for(let o=0;o<e.length;o++){let s=e[o].type.attrs;if(s){s.nodeName&&i.push(r=new Fr(s.nodeName));for(let e in s){let o=s[e];null!=o&&(n&&1==i.length&&i.push(r=new Fr(t.isInline?"span":"div")),"class"==e?r.class=(r.class?r.class+" ":"")+o:"style"==e?r.style=(r.style?r.style+";":"")+o:"nodeName"!=e&&(r[e]=o))}}}return i}function zr(e,t,n,r){if(n==Tr&&r==Tr)return t;let i=t;for(let t=0;t<r.length;t++){let o=r[t],s=n[t];if(t){let t;s&&s.nodeName==o.nodeName&&i!=e&&(t=i.parentNode)&&t.nodeName.toLowerCase()==o.nodeName||(t=document.createElement(o.nodeName),t.pmIsDeco=!0,t.appendChild(i),s=Tr[0]),i=t}Ir(i,s||Tr[0],o)}return i}function Ir(e,t,n){for(let r in t)"class"==r||"style"==r||"nodeName"==r||r in n||e.removeAttribute(r);for(let r in n)"class"!=r&&"style"!=r&&"nodeName"!=r&&n[r]!=t[r]&&e.setAttribute(r,n[r]);if(t.class!=n.class){let r=t.class?t.class.split(" ").filter(Boolean):[],i=n.class?n.class.split(" ").filter(Boolean):[];for(let t=0;t<r.length;t++)-1==i.indexOf(r[t])&&e.classList.remove(r[t]);for(let t=0;t<i.length;t++)-1==r.indexOf(i[t])&&e.classList.add(i[t]);0==e.classList.length&&e.removeAttribute("class")}if(t.style!=n.style){if(t.style){let n,r=/\s*([\w\-\xa1-\uffff]+)\s*:(?:"(?:\\.|[^"])*"|'(?:\\.|[^'])*'|\(.*?\)|[^;])*/g;for(;n=r.exec(t.style);)e.style.removeProperty(n[1])}n.style&&(e.style.cssText+=n.style)}}function Br(e,t,n){return zr(e,e,Tr,Rr(t,n,1!=e.nodeType))}function Pr(e,t){if(e.length!=t.length)return!1;for(let n=0;n<e.length;n++)if(!e[n].type.eq(t[n].type))return!1;return!0}function Lr(e){let t=e.nextSibling;return e.parentNode.removeChild(e),t}class qr{constructor(e,t,n){this.lock=t,this.view=n,this.index=0,this.stack=[],this.changed=!1,this.top=e,this.preMatch=function(e,t){let n=t,r=n.children.length,i=e.childCount,o=new Map,s=[];e:for(;i>0;){let l;for(;;)if(r){let e=n.children[r-1];if(!(e instanceof _r)){l=e,r--;break}n=e,r=e.children.length}else{if(n==t)break e;r=n.parent.children.indexOf(n),n=n.parent}let a=l.node;if(a){if(a!=e.child(i-1))break;--i,o.set(l,i),s.push(l)}}return{index:i,matched:o,matches:s.reverse()}}(e.node.content,e)}destroyBetween(e,t){if(e!=t){for(let n=e;n<t;n++)this.top.children[n].destroy();this.top.children.splice(e,t-e),this.changed=!0}}destroyRest(){this.destroyBetween(this.index,this.top.children.length)}syncToMarks(e,t,n){let r=0,i=this.stack.length>>1,o=Math.min(i,e.length);for(;r<o&&(r==i-1?this.top:this.stack[r+1<<1]).matchesMark(e[r])&&!1!==e[r].type.spec.spanning;)r++;for(;r<i;)this.destroyRest(),this.top.dirty=0,this.index=this.stack.pop(),this.top=this.stack.pop(),i--;for(;i<e.length;){this.stack.push(this.top,this.index+1);let r=-1;for(let t=this.index;t<Math.min(this.index+3,this.top.children.length);t++){let n=this.top.children[t];if(n.matchesMark(e[i])&&!this.isLocked(n.dom)){r=t;break}}if(r>-1)r>this.index&&(this.changed=!0,this.destroyBetween(this.index,r)),this.top=this.top.children[this.index];else{let r=_r.create(this.top,e[i],t,n);this.top.children.splice(this.index,0,r),this.top=r,this.changed=!0}this.index=0,i++}}findNodeMatch(e,t,n,r){let i,o=-1;if(r>=this.preMatch.index&&(i=this.preMatch.matches[r-this.preMatch.index]).parent==this.top&&i.matchesNode(e,t,n))o=this.top.children.indexOf(i,this.index);else for(let r=this.index,i=Math.min(this.top.children.length,r+5);r<i;r++){let i=this.top.children[r];if(i.matchesNode(e,t,n)&&!this.preMatch.matched.has(i)){o=r;break}}return!(o<0)&&(this.destroyBetween(this.index,o),this.index++,!0)}updateNodeAt(e,t,n,r,i){let o=this.top.children[r];return 3==o.dirty&&o.dom==o.contentDOM&&(o.dirty=2),!!o.update(e,t,n,i)&&(this.destroyBetween(this.index,r),this.index++,!0)}findIndexWithChild(e){for(;;){let t=e.parentNode;if(!t)return-1;if(t==this.top.contentDOM){let t=e.pmViewDesc;if(t)for(let e=this.index;e<this.top.children.length;e++)if(this.top.children[e]==t)return e;return-1}e=t}}updateNextNode(e,t,n,r,i,o){for(let s=this.index;s<this.top.children.length;s++){let l=this.top.children[s];if(l instanceof Sr){let a=this.preMatch.matched.get(l);if(null!=a&&a!=i)return!1;let c,h=l.dom,u=this.isLocked(h)&&!(e.isText&&l.node&&l.node.isText&&l.nodeDOM.nodeValue==e.text&&3!=l.dirty&&Pr(t,l.outerDeco));if(!u&&l.update(e,t,n,r))return this.destroyBetween(this.index,s),l.dom!=h&&(this.changed=!0),this.index++,!0;if(!u&&(c=this.recreateWrapper(l,e,t,n,r,o)))return this.destroyBetween(this.index,s),this.top.children[this.index]=c,c.contentDOM&&(c.dirty=2,c.updateChildren(r,o+1),c.dirty=0),this.changed=!0,this.index++,!0;break}}return!1}recreateWrapper(e,t,n,r,i,o){if(e.dirty||t.isAtom||!e.children.length||!e.node.content.eq(t.content)||!Pr(n,e.outerDeco)||!r.eq(e.innerDeco))return null;let s=Sr.create(this.top,t,n,r,i,o);if(s.contentDOM){s.children=e.children,e.children=[];for(let e of s.children)e.parent=s}return e.destroy(),s}addNode(e,t,n,r,i){let o=Sr.create(this.top,e,t,n,r,i);o.contentDOM&&o.updateChildren(r,i+1),this.top.children.splice(this.index++,0,o),this.changed=!0}placeWidget(e,t,n){let r=this.index<this.top.children.length?this.top.children[this.index]:null;if(!r||!r.matchesWidget(e)||e!=r.widget&&r.widget.type.toDOM.parentNode){let r=new Dr(this.top,e,t,n);this.top.children.splice(this.index++,0,r),this.changed=!0}else this.index++}addTextblockHacks(){let e=this.top.children[this.index-1],t=this.top;for(;e instanceof _r;)t=e,e=t.children[t.children.length-1];(!e||!(e instanceof Ar)||/\n$/.test(e.node.text)||this.view.requiresGeckoHackNode&&/\s$/.test(e.node.text))&&((Hn||Jn)&&e&&"false"==e.dom.contentEditable&&this.addHackNode("IMG",t),this.addHackNode("BR",this.top))}addHackNode(e,t){if(t==this.top&&this.index<t.children.length&&t.children[this.index].matchesHack(e))this.index++;else{let n=document.createElement(e);"IMG"==e&&(n.className="ProseMirror-separator",n.alt=""),"BR"==e&&(n.className="ProseMirror-trailingBreak");let r=new Mr(this.top,[],n,null);t!=this.top?t.children.push(r):t.children.splice(this.index++,0,r),this.changed=!0}}isLocked(e){return this.lock&&(e==this.lock||1==e.nodeType&&e.contains(this.lock.parentNode))}}function $r(e,t){return e.type.side-t.type.side}function Vr(e,t,n,r,i){let o=[];for(let s=0,l=0;s<e.length;s++){let a=e[s],c=l,h=l+=a.size;c>=n||h<=t?o.push(a):(c<t&&o.push(a.slice(0,t-c,r)),i&&(o.push(i),i=void 0),h>n&&o.push(a.slice(n-c,a.size,r)))}return o}function jr(e,t=null){let n=e.domSelectionRange(),r=e.state.doc;if(!n.focusNode)return null;let i=e.docView.nearestDesc(n.focusNode),o=i&&0==i.size,s=e.docView.posFromDOM(n.focusNode,n.focusOffset,1);if(s<0)return null;let l,a,c=r.resolve(s);if(Fn(n)){for(l=s;i&&!i.node;)i=i.parent;let e=i.node;if(i&&e.isAtom&&nt.isSelectable(e)&&i.parent&&(!e.isInline||!function(e,t,n){for(let r=0==t,i=t==On(e);r||i;){if(e==n)return!0;let t=Dn(e);if(!(e=e.parentNode))return!1;r=r&&0==t,i=i&&t==On(e)}}(n.focusNode,n.focusOffset,i.dom))){let e=i.posBefore;a=new nt(s==e?c:r.resolve(e))}}else{if(n instanceof e.dom.ownerDocument.defaultView.Selection&&n.rangeCount>1){let t=s,i=s;for(let r=0;r<n.rangeCount;r++){let o=n.getRangeAt(r);t=Math.min(t,e.docView.posFromDOM(o.startContainer,o.startOffset,1)),i=Math.max(i,e.docView.posFromDOM(o.endContainer,o.endOffset,-1))}if(t<0)return null;[l,s]=i==e.state.selection.anchor?[i,t]:[t,i],c=r.resolve(s)}else l=e.docView.posFromDOM(n.anchorNode,n.anchorOffset,1);if(l<0)return null}let h=r.resolve(l);if(!a){a=Xr(e,h,c,"pointer"==t||e.state.selection.head<c.pos&&!o?1:-1)}return a}function Jr(e){return e.editable?e.hasFocus():ei(e)&&document.activeElement&&document.activeElement.contains(e.dom)}function Wr(e,t=!1){let n=e.state.selection;if(Gr(e,n),Jr(e)){if(!t&&e.input.mouseDown&&e.input.mouseDown.allowDefault&&Jn){let t=e.domSelectionRange(),n=e.domObserver.currentSelection;if(t.anchorNode&&n.anchorNode&&En(t.anchorNode,t.anchorOffset,n.anchorNode,n.anchorOffset))return e.input.mouseDown.delayedSelectionSync=!0,void e.domObserver.setCurSelection()}if(e.domObserver.disconnectSelection(),e.cursorWrapper)!function(e){let t=e.domSelection();if(!t)return;let n=e.cursorWrapper.dom,r="IMG"==n.nodeName;r?t.collapse(n.parentNode,Dn(n)+1):t.collapse(n,0);!r&&!e.state.selection.visible&&qn&&$n<=11&&(n.disabled=!0,n.disabled=!1)}(e);else{let r,i,{anchor:o,head:s}=n;!Hr||n instanceof et||(n.$from.parent.inlineContent||(r=Ur(e,n.from)),n.empty||n.$from.parent.inlineContent||(i=Ur(e,n.to))),e.docView.setSelection(o,s,e,t),Hr&&(r&&Zr(r),i&&Zr(i)),n.visible?e.dom.classList.remove("ProseMirror-hideselection"):(e.dom.classList.add("ProseMirror-hideselection"),"onselectionchange"in document&&function(e){let t=e.dom.ownerDocument;t.removeEventListener("selectionchange",e.input.hideSelectionGuard);let n=e.domSelectionRange(),r=n.anchorNode,i=n.anchorOffset;t.addEventListener("selectionchange",e.input.hideSelectionGuard=()=>{n.anchorNode==r&&n.anchorOffset==i||(t.removeEventListener("selectionchange",e.input.hideSelectionGuard),setTimeout(()=>{Jr(e)&&!e.state.selection.visible||e.dom.classList.remove("ProseMirror-hideselection")},20))})}(e))}e.domObserver.setCurSelection(),e.domObserver.connectSelection()}}const Hr=Hn||Jn&&Wn<63;function Ur(e,t){let{node:n,offset:r}=e.docView.domFromPos(t,0),i=r<n.childNodes.length?n.childNodes[r]:null,o=r?n.childNodes[r-1]:null;if(Hn&&i&&"false"==i.contentEditable)return Kr(i);if(!(i&&"false"!=i.contentEditable||o&&"false"!=o.contentEditable)){if(i)return Kr(i);if(o)return Kr(o)}}function Kr(e){return e.contentEditable="true",Hn&&e.draggable&&(e.draggable=!1,e.wasDraggable=!0),e}function Zr(e){e.contentEditable="false",e.wasDraggable&&(e.draggable=!0,e.wasDraggable=null)}function Gr(e,t){if(t instanceof nt){let n=e.docView.descAt(t.from);n!=e.lastSelectedViewDesc&&(Qr(e),n&&n.selectNode(),e.lastSelectedViewDesc=n)}else Qr(e)}function Qr(e){e.lastSelectedViewDesc&&(e.lastSelectedViewDesc.parent&&e.lastSelectedViewDesc.deselectNode(),e.lastSelectedViewDesc=void 0)}function Xr(e,t,n,r){return e.someProp("createSelectionBetween",r=>r(e,t,n))||et.between(t,n,r)}function Yr(e){return!(e.editable&&!e.hasFocus())&&ei(e)}function ei(e){let t=e.domSelectionRange();if(!t.anchorNode)return!1;try{return e.dom.contains(3==t.anchorNode.nodeType?t.anchorNode.parentNode:t.anchorNode)&&(e.editable||e.dom.contains(3==t.focusNode.nodeType?t.focusNode.parentNode:t.focusNode))}catch(e){return!1}}function ti(e,t){let{$anchor:n,$head:r}=e.selection,i=t>0?n.max(r):n.min(r),o=i.parent.inlineContent?i.depth?e.doc.resolve(t>0?i.after():i.before()):null:i;return o&&Ge.findFrom(o,t)}function ni(e,t){return e.dispatch(e.state.tr.setSelection(t).scrollIntoView()),!0}function ri(e,t,n){let r=e.state.selection;if(!(r instanceof et)){if(r instanceof nt&&r.node.isInline)return ni(e,new et(t>0?r.$to:r.$from));{let n=ti(e.state,t);return!!n&&ni(e,n)}}if(n.indexOf("s")>-1){let{$head:n}=r,i=n.textOffset?null:t<0?n.nodeBefore:n.nodeAfter;if(!i||i.isText||!i.isLeaf)return!1;let o=e.state.doc.resolve(n.pos+i.nodeSize*(t<0?-1:1));return ni(e,new et(r.$anchor,o))}if(!r.empty)return!1;if(e.endOfTextblock(t>0?"forward":"backward")){let n=ti(e.state,t);return!!(n&&n instanceof nt)&&ni(e,n)}if(!(Kn&&n.indexOf("m")>-1)){let n,i=r.$head,o=i.textOffset?null:t<0?i.nodeBefore:i.nodeAfter;if(!o||o.isText)return!1;let s=t<0?i.pos-o.nodeSize:i.pos;return!!(o.isAtom||(n=e.docView.descAt(s))&&!n.contentDOM)&&(nt.isSelectable(o)?ni(e,new nt(t<0?e.state.doc.resolve(i.pos-o.nodeSize):i)):!!Qn&&ni(e,new et(e.state.doc.resolve(t<0?s:s+o.nodeSize))))}}function ii(e){return 3==e.nodeType?e.nodeValue.length:e.childNodes.length}function oi(e,t){let n=e.pmViewDesc;return n&&0==n.size&&(t<0||e.nextSibling||"BR"!=e.nodeName)}function si(e,t){return t<0?function(e){let t=e.domSelectionRange(),n=t.focusNode,r=t.focusOffset;if(!n)return;let i,o,s=!1;Vn&&1==n.nodeType&&r<ii(n)&&oi(n.childNodes[r],-1)&&(s=!0);for(;;)if(r>0){if(1!=n.nodeType)break;{let e=n.childNodes[r-1];if(oi(e,-1))i=n,o=--r;else{if(3!=e.nodeType)break;n=e,r=n.nodeValue.length}}}else{if(li(n))break;{let t=n.previousSibling;for(;t&&oi(t,-1);)i=n.parentNode,o=Dn(t),t=t.previousSibling;if(t)n=t,r=ii(n);else{if(n=n.parentNode,n==e.dom)break;r=0}}}s?ai(e,n,r):i&&ai(e,i,o)}(e):function(e){let t=e.domSelectionRange(),n=t.focusNode,r=t.focusOffset;if(!n)return;let i,o,s=ii(n);for(;;)if(r<s){if(1!=n.nodeType)break;if(!oi(n.childNodes[r],1))break;i=n,o=++r}else{if(li(n))break;{let t=n.nextSibling;for(;t&&oi(t,1);)i=t.parentNode,o=Dn(t)+1,t=t.nextSibling;if(t)n=t,r=0,s=ii(n);else{if(n=n.parentNode,n==e.dom)break;r=s=0}}}i&&ai(e,i,o)}(e)}function li(e){let t=e.pmViewDesc;return t&&t.node&&t.node.isBlock}function ai(e,t,n){if(3!=t.nodeType){let e,r;(r=function(e,t){for(;e&&t==e.childNodes.length&&!Nn(e);)t=Dn(e)+1,e=e.parentNode;for(;e&&t<e.childNodes.length;){let n=e.childNodes[t];if(3==n.nodeType)return n;if(1==n.nodeType&&"false"==n.contentEditable)break;e=n,t=0}}(t,n))?(t=r,n=0):(e=function(e,t){for(;e&&!t&&!Nn(e);)t=Dn(e),e=e.parentNode;for(;e&&t;){let n=e.childNodes[t-1];if(3==n.nodeType)return n;if(1==n.nodeType&&"false"==n.contentEditable)break;t=(e=n).childNodes.length}}(t,n))&&(t=e,n=e.nodeValue.length)}let r=e.domSelection();if(!r)return;if(Fn(r)){let e=document.createRange();e.setEnd(t,n),e.setStart(t,n),r.removeAllRanges(),r.addRange(e)}else r.extend&&r.extend(t,n);e.domObserver.setCurSelection();let{state:i}=e;setTimeout(()=>{e.state==i&&Wr(e)},50)}function ci(e,t){let n=e.state.doc.resolve(t);if(!Jn&&!Zn&&n.parent.inlineContent){let r=e.coordsAtPos(t);if(t>n.start()){let n=e.coordsAtPos(t-1),i=(n.top+n.bottom)/2;if(i>r.top&&i<r.bottom&&Math.abs(n.left-r.left)>1)return n.left<r.left?"ltr":"rtl"}if(t<n.end()){let n=e.coordsAtPos(t+1),i=(n.top+n.bottom)/2;if(i>r.top&&i<r.bottom&&Math.abs(n.left-r.left)>1)return n.left>r.left?"ltr":"rtl"}}return"rtl"==getComputedStyle(e.dom).direction?"rtl":"ltr"}function hi(e,t,n){let r=e.state.selection;if(r instanceof et&&!r.empty||n.indexOf("s")>-1)return!1;if(Kn&&n.indexOf("m")>-1)return!1;let{$from:i,$to:o}=r;if(!i.parent.inlineContent||e.endOfTextblock(t<0?"up":"down")){let n=ti(e.state,t);if(n&&n instanceof nt)return ni(e,n)}if(!i.parent.inlineContent){let n=t<0?i:o,s=r instanceof it?Ge.near(n,t):Ge.findFrom(n,t);return!!s&&ni(e,s)}return!1}function ui(e,t){if(!(e.state.selection instanceof et))return!0;let{$head:n,$anchor:r,empty:i}=e.state.selection;if(!n.sameParent(r))return!0;if(!i)return!1;if(e.endOfTextblock(t>0?"forward":"backward"))return!0;let o=!n.textOffset&&(t<0?n.nodeBefore:n.nodeAfter);if(o&&!o.isText){let r=e.state.tr;return t<0?r.delete(n.pos-o.nodeSize,n.pos):r.delete(n.pos,n.pos+o.nodeSize),e.dispatch(r),!0}return!1}function pi(e,t,n){e.domObserver.stop(),t.contentEditable=n,e.domObserver.start()}function di(e,t){let n=t.keyCode,r=function(e){let t="";return e.ctrlKey&&(t+="c"),e.metaKey&&(t+="m"),e.altKey&&(t+="a"),e.shiftKey&&(t+="s"),t}(t);if(8==n||Kn&&72==n&&"c"==r)return ui(e,-1)||si(e,-1);if(46==n&&!t.shiftKey||Kn&&68==n&&"c"==r)return ui(e,1)||si(e,1);if(13==n||27==n)return!0;if(37==n||Kn&&66==n&&"c"==r){let t=37==n?"ltr"==ci(e,e.state.selection.from)?-1:1:-1;return ri(e,t,r)||si(e,t)}if(39==n||Kn&&70==n&&"c"==r){let t=39==n?"ltr"==ci(e,e.state.selection.from)?1:-1:1;return ri(e,t,r)||si(e,t)}return 38==n||Kn&&80==n&&"c"==r?hi(e,-1,r)||si(e,-1):40==n||Kn&&78==n&&"c"==r?function(e){if(!Hn||e.state.selection.$head.parentOffset>0)return!1;let{focusNode:t,focusOffset:n}=e.domSelectionRange();if(t&&1==t.nodeType&&0==n&&t.firstChild&&"false"==t.firstChild.contentEditable){let n=t.firstChild;pi(e,n,"true"),setTimeout(()=>pi(e,n,"false"),20)}return!1}(e)||hi(e,1,r)||si(e,1):r==(Kn?"m":"c")&&(66==n||73==n||89==n||90==n)}function fi(e,t){e.someProp("transformCopied",n=>{t=n(t,e)});let n=[],{content:r,openStart:i,openEnd:o}=t;for(;i>1&&o>1&&1==r.childCount&&1==r.firstChild.childCount;){i--,o--;let e=r.firstChild;n.push(e.type.name,e.attrs!=e.type.defaultAttrs?e.attrs:null),r=e.content}let s=e.someProp("clipboardSerializer")||ie.fromSchema(e.state.schema),l=vi(),a=l.createElement("div");a.appendChild(s.serializeFragment(r,{document:l}));let c,h=a.firstChild,u=0;for(;h&&1==h.nodeType&&(c=xi[h.nodeName.toLowerCase()]);){for(let e=c.length-1;e>=0;e--){let t=l.createElement(c[e]);for(;a.firstChild;)t.appendChild(a.firstChild);a.appendChild(t),u++}h=a.firstChild}return h&&1==h.nodeType&&h.setAttribute("data-pm-slice",`${i} ${o}${u?` -${u}`:""} ${JSON.stringify(n)}`),{dom:a,text:e.someProp("clipboardTextSerializer",n=>n(t,e))||t.content.textBetween(0,t.content.size,"\n\n"),slice:t}}function mi(e,t,n,i,o){let s,l,a=o.parent.type.spec.code;if(!n&&!t)return null;let h=!!t&&(i||a||!n);if(h){if(e.someProp("transformPastedText",n=>{t=n(t,a||i,e)}),a)return l=new c(r.from(e.state.schema.text(t.replace(/\r\n?/g,"\n"))),0,0),e.someProp("transformPasted",t=>{l=t(l,e,!0)}),l;let n=e.someProp("clipboardTextParser",n=>n(t,o,i,e));if(n)l=n;else{let n=o.marks(),{schema:r}=e.state,i=ie.fromSchema(r);s=document.createElement("div"),t.split(/(?:\r\n?|\n)+/).forEach(e=>{let t=s.appendChild(document.createElement("p"));e&&t.appendChild(i.serializeNode(r.text(e,n)))})}}else e.someProp("transformPastedHTML",t=>{n=t(n,e)}),s=function(e){let t=/^(\s*<meta [^>]*>)*/.exec(e);t&&(e=e.slice(t[0].length));let n,r=vi().createElement("div"),i=/<([a-z][^>\s]+)/i.exec(e);(n=i&&xi[i[1].toLowerCase()])&&(e=n.map(e=>"<"+e+">").join("")+e+n.map(e=>"</"+e+">").reverse().join(""));if(r.innerHTML=function(e){let t=window.trustedTypes;if(!t)return e;_i||(_i=t.defaultPolicy||t.createPolicy("ProseMirrorClipboard",{createHTML:e=>e}));return _i.createHTML(e)}(e),n)for(let e=0;e<n.length;e++)r=r.querySelector(n[e])||r;return r}(n),Qn&&function(e){let t=e.querySelectorAll(Jn?"span:not([class]):not([style])":"span.Apple-converted-space");for(let n=0;n<t.length;n++){let r=t[n];1==r.childNodes.length&&" "==r.textContent&&r.parentNode&&r.parentNode.replaceChild(e.ownerDocument.createTextNode(" "),r)}}(s);let u=s&&s.querySelector("[data-pm-slice]"),p=u&&/^(\d+) (\d+)(?: -(\d+))? (.*)/.exec(u.getAttribute("data-pm-slice")||"");if(p&&p[3])for(let e=+p[3];e>0;e--){let e=s.firstChild;for(;e&&1!=e.nodeType;)e=e.nextSibling;if(!e)break;s=e}if(!l){let t=e.someProp("clipboardParser")||e.someProp("domParser")||K.fromSchema(e.state.schema);l=t.parseSlice(s,{preserveWhitespace:!(!h&&!p),context:o,ruleFromNode:e=>"BR"!=e.nodeName||e.nextSibling||!e.parentNode||gi.test(e.parentNode.nodeName)?null:{ignore:!0}})}if(p)l=function(e,t){if(!e.size)return e;let n,i=e.content.firstChild.type.schema;try{n=JSON.parse(t)}catch(t){return e}let{content:o,openStart:s,openEnd:l}=e;for(let e=n.length-2;e>=0;e-=2){let t=i.nodes[n[e]];if(!t||t.hasRequiredAttrs())break;o=r.from(t.create(n[e+1],o)),s++,l++}return new c(o,s,l)}(Ci(l,+p[1],+p[2]),p[4]);else if(l=c.maxOpen(function(e,t){if(e.childCount<2)return e;for(let n=t.depth;n>=0;n--){let i,o=t.node(n).contentMatchAt(t.index(n)),s=[];if(e.forEach(e=>{if(!s)return;let t,n=o.findWrapping(e.type);if(!n)return s=null;if(t=s.length&&i.length&&ki(n,i,e,s[s.length-1],0))s[s.length-1]=t;else{s.length&&(s[s.length-1]=bi(s[s.length-1],i.length));let t=yi(e,n);s.push(t),o=o.matchType(t.type),i=n}}),s)return r.from(s)}return e}(l.content,o),!0),l.openStart||l.openEnd){let e=0,t=0;for(let t=l.content.firstChild;e<l.openStart&&!t.type.spec.isolating;e++,t=t.firstChild);for(let e=l.content.lastChild;t<l.openEnd&&!e.type.spec.isolating;t++,e=e.lastChild);l=Ci(l,e,t)}return e.someProp("transformPasted",t=>{l=t(l,e,h)}),l}const gi=/^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i;function yi(e,t,n=0){for(let i=t.length-1;i>=n;i--)e=t[i].create(null,r.from(e));return e}function ki(e,t,n,i,o){if(o<e.length&&o<t.length&&e[o]==t[o]){let s=ki(e,t,n,i.lastChild,o+1);if(s)return i.copy(i.content.replaceChild(i.childCount-1,s));if(i.contentMatchAt(i.childCount).matchType(o==e.length-1?n.type:e[o+1]))return i.copy(i.content.append(r.from(yi(n,e,o+1))))}}function bi(e,t){if(0==t)return e;let n=e.content.replaceChild(e.childCount-1,bi(e.lastChild,t-1)),i=e.contentMatchAt(e.childCount).fillBefore(r.empty,!0);return e.copy(n.append(i))}function wi(e,t,n,i,o,s){let l=t<0?e.firstChild:e.lastChild,a=l.content;return e.childCount>1&&(s=0),o<i-1&&(a=wi(a,t,n,i,o+1,s)),o>=n&&(a=t<0?l.contentMatchAt(0).fillBefore(a,s<=o).append(a):a.append(l.contentMatchAt(l.childCount).fillBefore(r.empty,!0))),e.replaceChild(t<0?0:e.childCount-1,l.copy(a))}function Ci(e,t,n){return t<e.openStart&&(e=new c(wi(e.content,-1,t,e.openStart,0,e.openEnd),t,e.openEnd)),n<e.openEnd&&(e=new c(wi(e.content,1,n,e.openEnd,0,0),e.openStart,n)),e}const xi={thead:["table"],tbody:["table"],tfoot:["table"],caption:["table"],colgroup:["table"],col:["table","colgroup"],tr:["table","tbody"],td:["table","tbody","tr"],th:["table","tbody","tr"]};let Di=null;function vi(){return Di||(Di=document.implementation.createHTMLDocument("title"))}let _i=null;const Si={},Ei={},Ai={touchstart:!0,touchmove:!0};class Mi{constructor(){this.shiftKey=!1,this.mouseDown=null,this.lastKeyCode=null,this.lastKeyCodeTime=0,this.lastClick={time:0,x:0,y:0,type:"",button:0},this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastIOSEnter=0,this.lastIOSEnterFallbackTimeout=-1,this.lastFocus=0,this.lastTouch=0,this.lastChromeDelete=0,this.composing=!1,this.compositionNode=null,this.composingTimeout=-1,this.compositionNodes=[],this.compositionEndedAt=-2e8,this.compositionID=1,this.compositionPendingChanges=0,this.domChangeCount=0,this.eventHandlers=Object.create(null),this.hideSelectionGuard=null}}function Oi(e,t){e.input.lastSelectionOrigin=t,e.input.lastSelectionTime=Date.now()}function Ni(e){e.someProp("handleDOMEvents",t=>{for(let n in t)e.input.eventHandlers[n]||e.dom.addEventListener(n,e.input.eventHandlers[n]=t=>Fi(e,t))})}function Fi(e,t){return e.someProp("handleDOMEvents",n=>{let r=n[t.type];return!!r&&(r(e,t)||t.defaultPrevented)})}function Ti(e,t){if(!t.bubbles)return!0;if(t.defaultPrevented)return!1;for(let n=t.target;n!=e.dom;n=n.parentNode)if(!n||11==n.nodeType||n.pmViewDesc&&n.pmViewDesc.stopEvent(t))return!1;return!0}function Ri(e){return{left:e.clientX,top:e.clientY}}function zi(e,t,n,r,i){if(-1==r)return!1;let o=e.state.doc.resolve(r);for(let r=o.depth+1;r>0;r--)if(e.someProp(t,t=>r>o.depth?t(e,n,o.nodeAfter,o.before(r),i,!0):t(e,n,o.node(r),o.before(r),i,!1)))return!0;return!1}function Ii(e,t,n){if(e.focused||e.focus(),e.state.selection.eq(t))return;let r=e.state.tr.setSelection(t);r.setMeta("pointer",!0),e.dispatch(r)}function Bi(e,t,n,r,i){return zi(e,"handleClickOn",t,n,r)||e.someProp("handleClick",n=>n(e,t,r))||(i?function(e,t){if(-1==t)return!1;let n,r,i=e.state.selection;i instanceof nt&&(n=i.node);let o=e.state.doc.resolve(t);for(let e=o.depth+1;e>0;e--){let t=e>o.depth?o.nodeAfter:o.node(e);if(nt.isSelectable(t)){r=n&&i.$from.depth>0&&e>=i.$from.depth&&o.before(i.$from.depth+1)==i.$from.pos?o.before(i.$from.depth):o.before(e);break}}return null!=r&&(Ii(e,nt.create(e.state.doc,r)),!0)}(e,n):function(e,t){if(-1==t)return!1;let n=e.state.doc.resolve(t),r=n.nodeAfter;return!!(r&&r.isAtom&&nt.isSelectable(r))&&(Ii(e,new nt(n)),!0)}(e,n))}function Pi(e,t,n,r){return zi(e,"handleDoubleClickOn",t,n,r)||e.someProp("handleDoubleClick",n=>n(e,t,r))}function Li(e,t,n,r){return zi(e,"handleTripleClickOn",t,n,r)||e.someProp("handleTripleClick",n=>n(e,t,r))||function(e,t,n){if(0!=n.button)return!1;let r=e.state.doc;if(-1==t)return!!r.inlineContent&&(Ii(e,et.create(r,0,r.content.size)),!0);let i=r.resolve(t);for(let t=i.depth+1;t>0;t--){let n=t>i.depth?i.nodeAfter:i.node(t),o=i.before(t);if(n.inlineContent)Ii(e,et.create(r,o+1,o+1+n.content.size));else{if(!nt.isSelectable(n))continue;Ii(e,nt.create(r,o))}return!0}}(e,n,r)}function qi(e){return Ki(e)}Ei.keydown=(e,t)=>{let n=t;if(e.input.shiftKey=16==n.keyCode||n.shiftKey,!ji(e,n)&&(e.input.lastKeyCode=n.keyCode,e.input.lastKeyCodeTime=Date.now(),!Gn||!Jn||13!=n.keyCode))if(229!=n.keyCode&&e.domObserver.forceFlush(),!Un||13!=n.keyCode||n.ctrlKey||n.altKey||n.metaKey)e.someProp("handleKeyDown",t=>t(e,n))||di(e,n)?n.preventDefault():Oi(e,"key");else{let t=Date.now();e.input.lastIOSEnter=t,e.input.lastIOSEnterFallbackTimeout=setTimeout(()=>{e.input.lastIOSEnter==t&&(e.someProp("handleKeyDown",t=>t(e,Tn(13,"Enter"))),e.input.lastIOSEnter=0)},200)}},Ei.keyup=(e,t)=>{16==t.keyCode&&(e.input.shiftKey=!1)},Ei.keypress=(e,t)=>{let n=t;if(ji(e,n)||!n.charCode||n.ctrlKey&&!n.altKey||Kn&&n.metaKey)return;if(e.someProp("handleKeyPress",t=>t(e,n)))return void n.preventDefault();let r=e.state.selection;if(!(r instanceof et&&r.$from.sameParent(r.$to))){let t=String.fromCharCode(n.charCode),i=()=>e.state.tr.insertText(t).scrollIntoView();/[\r\n]/.test(t)||e.someProp("handleTextInput",n=>n(e,r.$from.pos,r.$to.pos,t,i))||e.dispatch(i()),n.preventDefault()}};const $i=Kn?"metaKey":"ctrlKey";Si.mousedown=(e,t)=>{let n=t;e.input.shiftKey=n.shiftKey;let r=qi(e),i=Date.now(),o="singleClick";i-e.input.lastClick.time<500&&function(e,t){let n=t.x-e.clientX,r=t.y-e.clientY;return n*n+r*r<100}(n,e.input.lastClick)&&!n[$i]&&e.input.lastClick.button==n.button&&("singleClick"==e.input.lastClick.type?o="doubleClick":"doubleClick"==e.input.lastClick.type&&(o="tripleClick")),e.input.lastClick={time:i,x:n.clientX,y:n.clientY,type:o,button:n.button};let s=e.posAtCoords(Ri(n));s&&("singleClick"==o?(e.input.mouseDown&&e.input.mouseDown.done(),e.input.mouseDown=new Vi(e,s,n,!!r)):("doubleClick"==o?Pi:Li)(e,s.pos,s.inside,n)?n.preventDefault():Oi(e,"pointer"))};class Vi{constructor(e,t,n,r){let i,o;if(this.view=e,this.pos=t,this.event=n,this.flushed=r,this.delayedSelectionSync=!1,this.mightDrag=null,this.startDoc=e.state.doc,this.selectNode=!!n[$i],this.allowDefault=n.shiftKey,t.inside>-1)i=e.state.doc.nodeAt(t.inside),o=t.inside;else{let n=e.state.doc.resolve(t.pos);i=n.parent,o=n.depth?n.before():0}const s=r?null:n.target,l=s?e.docView.nearestDesc(s,!0):null;this.target=l&&1==l.nodeDOM.nodeType?l.nodeDOM:null;let{selection:a}=e.state;(0==n.button&&i.type.spec.draggable&&!1!==i.type.spec.selectable||a instanceof nt&&a.from<=o&&a.to>o)&&(this.mightDrag={node:i,pos:o,addAttr:!(!this.target||this.target.draggable),setUneditable:!(!this.target||!Vn||this.target.hasAttribute("contentEditable"))}),this.target&&this.mightDrag&&(this.mightDrag.addAttr||this.mightDrag.setUneditable)&&(this.view.domObserver.stop(),this.mightDrag.addAttr&&(this.target.draggable=!0),this.mightDrag.setUneditable&&setTimeout(()=>{this.view.input.mouseDown==this&&this.target.setAttribute("contentEditable","false")},20),this.view.domObserver.start()),e.root.addEventListener("mouseup",this.up=this.up.bind(this)),e.root.addEventListener("mousemove",this.move=this.move.bind(this)),Oi(e,"pointer")}done(){this.view.root.removeEventListener("mouseup",this.up),this.view.root.removeEventListener("mousemove",this.move),this.mightDrag&&this.target&&(this.view.domObserver.stop(),this.mightDrag.addAttr&&this.target.removeAttribute("draggable"),this.mightDrag.setUneditable&&this.target.removeAttribute("contentEditable"),this.view.domObserver.start()),this.delayedSelectionSync&&setTimeout(()=>Wr(this.view)),this.view.input.mouseDown=null}up(e){if(this.done(),!this.view.dom.contains(e.target))return;let t=this.pos;this.view.state.doc!=this.startDoc&&(t=this.view.posAtCoords(Ri(e))),this.updateAllowDefault(e),this.allowDefault||!t?Oi(this.view,"pointer"):Bi(this.view,t.pos,t.inside,e,this.selectNode)?e.preventDefault():0==e.button&&(this.flushed||Hn&&this.mightDrag&&!this.mightDrag.node.isAtom||Jn&&!this.view.state.selection.visible&&Math.min(Math.abs(t.pos-this.view.state.selection.from),Math.abs(t.pos-this.view.state.selection.to))<=2)?(Ii(this.view,Ge.near(this.view.state.doc.resolve(t.pos))),e.preventDefault()):Oi(this.view,"pointer")}move(e){this.updateAllowDefault(e),Oi(this.view,"pointer"),0==e.buttons&&this.done()}updateAllowDefault(e){!this.allowDefault&&(Math.abs(this.event.x-e.clientX)>4||Math.abs(this.event.y-e.clientY)>4)&&(this.allowDefault=!0)}}function ji(e,t){return!!e.composing||!!(Hn&&Math.abs(t.timeStamp-e.input.compositionEndedAt)<500)&&(e.input.compositionEndedAt=-2e8,!0)}Si.touchstart=e=>{e.input.lastTouch=Date.now(),qi(e),Oi(e,"pointer")},Si.touchmove=e=>{e.input.lastTouch=Date.now(),Oi(e,"pointer")},Si.contextmenu=e=>qi(e);const Ji=Gn?5e3:-1;function Wi(e,t){clearTimeout(e.input.composingTimeout),t>-1&&(e.input.composingTimeout=setTimeout(()=>Ki(e),t))}function Hi(e){for(e.composing&&(e.input.composing=!1,e.input.compositionEndedAt=function(){let e=document.createEvent("Event");return e.initEvent("event",!0,!0),e.timeStamp}());e.input.compositionNodes.length>0;)e.input.compositionNodes.pop().markParentsDirty()}function Ui(e){let t=e.domSelectionRange();if(!t.focusNode)return null;let n=function(e,t){for(;;){if(3==e.nodeType&&t)return e;if(1==e.nodeType&&t>0){if("false"==e.contentEditable)return null;t=On(e=e.childNodes[t-1])}else{if(!e.parentNode||Nn(e))return null;t=Dn(e),e=e.parentNode}}}(t.focusNode,t.focusOffset),r=function(e,t){for(;;){if(3==e.nodeType&&t<e.nodeValue.length)return e;if(1==e.nodeType&&t<e.childNodes.length){if("false"==e.contentEditable)return null;e=e.childNodes[t],t=0}else{if(!e.parentNode||Nn(e))return null;t=Dn(e)+1,e=e.parentNode}}}(t.focusNode,t.focusOffset);if(n&&r&&n!=r){let t=r.pmViewDesc,i=e.domObserver.lastChangedTextNode;if(n==i||r==i)return i;if(!t||!t.isText(r.nodeValue))return r;if(e.input.compositionNode==r){let e=n.pmViewDesc;if(e&&e.isText(n.nodeValue))return r}}return n||r}function Ki(e,t=!1){if(!(Gn&&e.domObserver.flushingSoon>=0)){if(e.domObserver.forceFlush(),Hi(e),t||e.docView&&e.docView.dirty){let n=jr(e),r=e.state.selection;return n&&!n.eq(r)?e.dispatch(e.state.tr.setSelection(n)):!e.markCursor&&!t||r.$from.node(r.$from.sharedDepth(r.to)).inlineContent?e.updateState(e.state):e.dispatch(e.state.tr.deleteSelection()),!0}return!1}}Ei.compositionstart=Ei.compositionupdate=e=>{if(!e.composing){e.domObserver.flush();let{state:t}=e,n=t.selection.$to;if(t.selection instanceof et&&(t.storedMarks||!n.textOffset&&n.parentOffset&&n.nodeBefore.marks.some(e=>!1===e.type.spec.inclusive)||Jn&&Zn&&function(e){let{focusNode:t,focusOffset:n}=e.domSelectionRange();if(!t||1!=t.nodeType||n>=t.childNodes.length)return!1;let r=t.childNodes[n];return 1==r.nodeType&&"false"==r.contentEditable}(e)))e.markCursor=e.state.storedMarks||n.marks(),Ki(e,!0),e.markCursor=null;else if(Ki(e,!t.selection.empty),Vn&&t.selection.empty&&n.parentOffset&&!n.textOffset&&n.nodeBefore.marks.length){let t=e.domSelectionRange();for(let n=t.focusNode,r=t.focusOffset;n&&1==n.nodeType&&0!=r;){let t=r<0?n.lastChild:n.childNodes[r-1];if(!t)break;if(3==t.nodeType){let n=e.domSelection();n&&n.collapse(t,t.nodeValue.length);break}n=t,r=-1}}e.input.composing=!0}Wi(e,Ji)},Ei.compositionend=(e,t)=>{e.composing&&(e.input.composing=!1,e.input.compositionEndedAt=t.timeStamp,e.input.compositionPendingChanges=e.domObserver.pendingRecords().length?e.input.compositionID:0,e.input.compositionNode=null,e.input.compositionPendingChanges&&Promise.resolve().then(()=>e.domObserver.flush()),e.input.compositionID++,Wi(e,20))};const Zi=qn&&$n<15||Un&&Xn<604;function Gi(e,t,n,r,i){let o=mi(e,t,n,r,e.state.selection.$from);if(e.someProp("handlePaste",t=>t(e,i,o||c.empty)))return!0;if(!o)return!1;let s=function(e){return 0==e.openStart&&0==e.openEnd&&1==e.content.childCount?e.content.firstChild:null}(o),l=s?e.state.tr.replaceSelectionWith(s,r):e.state.tr.replaceSelection(o);return e.dispatch(l.scrollIntoView().setMeta("paste",!0).setMeta("uiEvent","paste")),!0}function Qi(e){let t=e.getData("text/plain")||e.getData("Text");if(t)return t;let n=e.getData("text/uri-list");return n?n.replace(/\r?\n/g," "):""}Si.copy=Ei.cut=(e,t)=>{let n=t,r=e.state.selection,i="cut"==n.type;if(r.empty)return;let o=Zi?null:n.clipboardData,s=r.content(),{dom:l,text:a}=fi(e,s);o?(n.preventDefault(),o.clearData(),o.setData("text/html",l.innerHTML),o.setData("text/plain",a)):function(e,t){if(!e.dom.parentNode)return;let n=e.dom.parentNode.appendChild(document.createElement("div"));n.appendChild(t),n.style.cssText="position: fixed; left: -10000px; top: 10px";let r=getSelection(),i=document.createRange();i.selectNodeContents(t),e.dom.blur(),r.removeAllRanges(),r.addRange(i),setTimeout(()=>{n.parentNode&&n.parentNode.removeChild(n),e.focus()},50)}(e,l),i&&e.dispatch(e.state.tr.deleteSelection().scrollIntoView().setMeta("uiEvent","cut"))},Ei.paste=(e,t)=>{let n=t;if(e.composing&&!Gn)return;let r=Zi?null:n.clipboardData,i=e.input.shiftKey&&45!=e.input.lastKeyCode;r&&Gi(e,Qi(r),r.getData("text/html"),i,n)?n.preventDefault():function(e,t){if(!e.dom.parentNode)return;let n=e.input.shiftKey||e.state.selection.$from.parent.type.spec.code,r=e.dom.parentNode.appendChild(document.createElement(n?"textarea":"div"));n||(r.contentEditable="true"),r.style.cssText="position: fixed; left: -10000px; top: 10px",r.focus();let i=e.input.shiftKey&&45!=e.input.lastKeyCode;setTimeout(()=>{e.focus(),r.parentNode&&r.parentNode.removeChild(r),n?Gi(e,r.value,null,i,t):Gi(e,r.textContent,r.innerHTML,i,t)},50)}(e,n)};class Xi{constructor(e,t,n){this.slice=e,this.move=t,this.node=n}}const Yi=Kn?"altKey":"ctrlKey";function eo(e,t){let n=e.someProp("dragCopies",e=>!e(t));return null!=n?n:!t[Yi]}Si.dragstart=(e,t)=>{let n=t,r=e.input.mouseDown;if(r&&r.done(),!n.dataTransfer)return;let i,o=e.state.selection,s=o.empty?null:e.posAtCoords(Ri(n));if(s&&s.pos>=o.from&&s.pos<=(o instanceof nt?o.to-1:o.to));else if(r&&r.mightDrag)i=nt.create(e.state.doc,r.mightDrag.pos);else if(n.target&&1==n.target.nodeType){let t=e.docView.nearestDesc(n.target,!0);t&&t.node.type.spec.draggable&&t!=e.docView&&(i=nt.create(e.state.doc,t.posBefore))}let l=(i||e.state.selection).content(),{dom:a,text:c,slice:h}=fi(e,l);(!n.dataTransfer.files.length||!Jn||Wn>120)&&n.dataTransfer.clearData(),n.dataTransfer.setData(Zi?"Text":"text/html",a.innerHTML),n.dataTransfer.effectAllowed="copyMove",Zi||n.dataTransfer.setData("text/plain",c),e.dragging=new Xi(h,eo(e,n),i)},Si.dragend=e=>{let t=e.dragging;window.setTimeout(()=>{e.dragging==t&&(e.dragging=null)},50)},Ei.dragover=Ei.dragenter=(e,t)=>t.preventDefault(),Ei.drop=(e,t)=>{try{!function(e,t,n){if(!t.dataTransfer)return;let r=e.posAtCoords(Ri(t));if(!r)return;let i=e.state.doc.resolve(r.pos),o=n&&n.slice;o?e.someProp("transformPasted",t=>{o=t(o,e,!1)}):o=mi(e,Qi(t.dataTransfer),Zi?null:t.dataTransfer.getData("text/html"),!1,i);let s=!(!n||!eo(e,t));if(e.someProp("handleDrop",n=>n(e,t,o||c.empty,s)))return void t.preventDefault();if(!o)return;t.preventDefault();let l=o?function(e,t,n){let r=e.resolve(t);if(!n.content.size)return t;let i=n.content;for(let e=0;e<n.openStart;e++)i=i.firstChild.content;for(let e=1;e<=(0==n.openStart&&n.size?2:1);e++)for(let t=r.depth;t>=0;t--){let n=t==r.depth?0:r.pos<=(r.start(t+1)+r.end(t+1))/2?-1:1,o=r.index(t)+(n>0?1:0),s=r.node(t),l=!1;if(1==e)l=s.canReplace(o,o,i);else{let e=s.contentMatchAt(o).findWrapping(i.firstChild.type);l=e&&s.canReplaceWith(o,o,e[0])}if(l)return 0==n?r.pos:n<0?r.before(t+1):r.after(t+1)}return null}(e.state.doc,i.pos,o):i.pos;null==l&&(l=i.pos);let a=e.state.tr;if(s){let{node:e}=n;e?e.replace(a):a.deleteSelection()}let h=a.mapping.map(l),u=0==o.openStart&&0==o.openEnd&&1==o.content.childCount,p=a.doc;u?a.replaceRangeWith(h,h,o.content.firstChild):a.replaceRange(h,h,o);if(a.doc.eq(p))return;let d=a.doc.resolve(h);if(u&&nt.isSelectable(o.content.firstChild)&&d.nodeAfter&&d.nodeAfter.sameMarkup(o.content.firstChild))a.setSelection(new nt(d));else{let t=a.mapping.map(l);a.mapping.maps[a.mapping.maps.length-1].forEach((e,n,r,i)=>t=i),a.setSelection(Xr(e,d,a.doc.resolve(t)))}e.focus(),e.dispatch(a.setMeta("uiEvent","drop"))}(e,t,e.dragging)}finally{e.dragging=null}},Si.focus=e=>{e.input.lastFocus=Date.now(),e.focused||(e.domObserver.stop(),e.dom.classList.add("ProseMirror-focused"),e.domObserver.start(),e.focused=!0,setTimeout(()=>{e.docView&&e.hasFocus()&&!e.domObserver.currentSelection.eq(e.domSelectionRange())&&Wr(e)},20))},Si.blur=(e,t)=>{let n=t;e.focused&&(e.domObserver.stop(),e.dom.classList.remove("ProseMirror-focused"),e.domObserver.start(),n.relatedTarget&&e.dom.contains(n.relatedTarget)&&e.domObserver.currentSelection.clear(),e.focused=!1)},Si.beforeinput=(e,t)=>{if(Jn&&Gn&&"deleteContentBackward"==t.inputType){e.domObserver.flushSoon();let{domChangeCount:t}=e.input;setTimeout(()=>{if(e.input.domChangeCount!=t)return;if(e.dom.blur(),e.focus(),e.someProp("handleKeyDown",t=>t(e,Tn(8,"Backspace"))))return;let{$cursor:n}=e.state.selection;n&&n.pos>0&&e.dispatch(e.state.tr.delete(n.pos-1,n.pos).scrollIntoView())},50)}};for(let e in Ei)Si[e]=Ei[e];function to(e,t){if(e==t)return!0;for(let n in e)if(e[n]!==t[n])return!1;for(let n in t)if(!(n in e))return!1;return!0}class no{constructor(e,t){this.toDOM=e,this.spec=t||lo,this.side=this.spec.side||0}map(e,t,n,r){let{pos:i,deleted:o}=e.mapResult(t.from+r,this.side<0?-1:1);return o?null:new oo(i-n,i-n,this)}valid(){return!0}eq(e){return this==e||e instanceof no&&(this.spec.key&&this.spec.key==e.spec.key||this.toDOM==e.toDOM&&to(this.spec,e.spec))}destroy(e){this.spec.destroy&&this.spec.destroy(e)}}class ro{constructor(e,t){this.attrs=e,this.spec=t||lo}map(e,t,n,r){let i=e.map(t.from+r,this.spec.inclusiveStart?-1:1)-n,o=e.map(t.to+r,this.spec.inclusiveEnd?1:-1)-n;return i>=o?null:new oo(i,o,this)}valid(e,t){return t.from<t.to}eq(e){return this==e||e instanceof ro&&to(this.attrs,e.attrs)&&to(this.spec,e.spec)}static is(e){return e.type instanceof ro}destroy(){}}class io{constructor(e,t){this.attrs=e,this.spec=t||lo}map(e,t,n,r){let i=e.mapResult(t.from+r,1);if(i.deleted)return null;let o=e.mapResult(t.to+r,-1);return o.deleted||o.pos<=i.pos?null:new oo(i.pos-n,o.pos-n,this)}valid(e,t){let n,{index:r,offset:i}=e.content.findIndex(t.from);return i==t.from&&!(n=e.child(r)).isText&&i+n.nodeSize==t.to}eq(e){return this==e||e instanceof io&&to(this.attrs,e.attrs)&&to(this.spec,e.spec)}destroy(){}}class oo{constructor(e,t,n){this.from=e,this.to=t,this.type=n}copy(e,t){return new oo(e,t,this.type)}eq(e,t=0){return this.type.eq(e.type)&&this.from+t==e.from&&this.to+t==e.to}map(e,t,n){return this.type.map(e,this,t,n)}static widget(e,t,n){return new oo(e,e,new no(t,n))}static inline(e,t,n,r){return new oo(e,t,new ro(n,r))}static node(e,t,n,r){return new oo(e,t,new io(n,r))}get spec(){return this.type.spec}get inline(){return this.type instanceof ro}get widget(){return this.type instanceof no}}const so=[],lo={};class ao{constructor(e,t){this.local=e.length?e:so,this.children=t.length?t:so}static create(e,t){return t.length?mo(t,e,0,lo):co}find(e,t,n){let r=[];return this.findInner(null==e?0:e,null==t?1e9:t,r,0,n),r}findInner(e,t,n,r,i){for(let o=0;o<this.local.length;o++){let s=this.local[o];s.from<=t&&s.to>=e&&(!i||i(s.spec))&&n.push(s.copy(s.from+r,s.to+r))}for(let o=0;o<this.children.length;o+=3)if(this.children[o]<t&&this.children[o+1]>e){let s=this.children[o]+1;this.children[o+2].findInner(e-s,t-s,n,r+s,i)}}map(e,t,n){return this==co||0==e.maps.length?this:this.mapInner(e,t,0,0,n||lo)}mapInner(e,t,n,r,i){let o;for(let s=0;s<this.local.length;s++){let l=this.local[s].map(e,n,r);l&&l.type.valid(t,l)?(o||(o=[])).push(l):i.onRemove&&i.onRemove(this.local[s].spec)}return this.children.length?function(e,t,n,r,i,o,s){let l=e.slice();for(let e=0,t=o;e<n.maps.length;e++){let r=0;n.maps[e].forEach((e,n,i,o)=>{let s=o-i-(n-e);for(let i=0;i<l.length;i+=3){let o=l[i+1];if(o<0||e>o+t-r)continue;let a=l[i]+t-r;n>=a?l[i+1]=e<=a?-2:-1:e>=t&&s&&(l[i]+=s,l[i+1]+=s)}r+=s}),t=n.maps[e].map(t,-1)}let a=!1;for(let t=0;t<l.length;t+=3)if(l[t+1]<0){if(-2==l[t+1]){a=!0,l[t+1]=-1;continue}let c=n.map(e[t]+o),h=c-i;if(h<0||h>=r.content.size){a=!0;continue}let u=n.map(e[t+1]+o,-1)-i,{index:p,offset:d}=r.content.findIndex(h),f=r.maybeChild(p);if(f&&d==h&&d+f.nodeSize==u){let r=l[t+2].mapInner(n,f,c+1,e[t]+o+1,s);r!=co?(l[t]=h,l[t+1]=u,l[t+2]=r):(l[t+1]=-2,a=!0)}else a=!0}if(a){let a=function(e,t,n,r,i,o,s){function l(e,t){for(let o=0;o<e.local.length;o++){let l=e.local[o].map(r,i,t);l?n.push(l):s.onRemove&&s.onRemove(e.local[o].spec)}for(let n=0;n<e.children.length;n+=3)l(e.children[n+2],e.children[n]+t+1)}for(let n=0;n<e.length;n+=3)-1==e[n+1]&&l(e[n+2],t[n]+o+1);return n}(l,e,t,n,i,o,s),c=mo(a,r,0,s);t=c.local;for(let e=0;e<l.length;e+=3)l[e+1]<0&&(l.splice(e,3),e-=3);for(let e=0,t=0;e<c.children.length;e+=3){let n=c.children[e];for(;t<l.length&&l[t]<n;)t+=3;l.splice(t,0,c.children[e],c.children[e+1],c.children[e+2])}}return new ao(t.sort(go),l)}(this.children,o||[],e,t,n,r,i):o?new ao(o.sort(go),so):co}add(e,t){return t.length?this==co?ao.create(e,t):this.addInner(e,t,0):this}addInner(e,t,n){let r,i=0;e.forEach((e,o)=>{let s,l=o+n;if(s=po(t,e,l)){for(r||(r=this.children.slice());i<r.length&&r[i]<o;)i+=3;r[i]==o?r[i+2]=r[i+2].addInner(e,s,l+1):r.splice(i,0,o,o+e.nodeSize,mo(s,e,l+1,lo)),i+=3}});let o=uo(i?fo(t):t,-n);for(let t=0;t<o.length;t++)o[t].type.valid(e,o[t])||o.splice(t--,1);return new ao(o.length?this.local.concat(o).sort(go):this.local,r||this.children)}remove(e){return 0==e.length||this==co?this:this.removeInner(e,0)}removeInner(e,t){let n=this.children,r=this.local;for(let r=0;r<n.length;r+=3){let i,o=n[r]+t,s=n[r+1]+t;for(let t,n=0;n<e.length;n++)(t=e[n])&&t.from>o&&t.to<s&&(e[n]=null,(i||(i=[])).push(t));if(!i)continue;n==this.children&&(n=this.children.slice());let l=n[r+2].removeInner(i,o+1);l!=co?n[r+2]=l:(n.splice(r,3),r-=3)}if(r.length)for(let n,i=0;i<e.length;i++)if(n=e[i])for(let e=0;e<r.length;e++)r[e].eq(n,t)&&(r==this.local&&(r=this.local.slice()),r.splice(e--,1));return n==this.children&&r==this.local?this:r.length||n.length?new ao(r,n):co}forChild(e,t){if(this==co)return this;if(t.isLeaf)return ao.empty;let n,r;for(let t=0;t<this.children.length;t+=3)if(this.children[t]>=e){this.children[t]==e&&(n=this.children[t+2]);break}let i=e+1,o=i+t.content.size;for(let e=0;e<this.local.length;e++){let t=this.local[e];if(t.from<o&&t.to>i&&t.type instanceof ro){let e=Math.max(i,t.from)-i,n=Math.min(o,t.to)-i;e<n&&(r||(r=[])).push(t.copy(e,n))}}if(r){let e=new ao(r.sort(go),so);return n?new ho([e,n]):e}return n||co}eq(e){if(this==e)return!0;if(!(e instanceof ao)||this.local.length!=e.local.length||this.children.length!=e.children.length)return!1;for(let t=0;t<this.local.length;t++)if(!this.local[t].eq(e.local[t]))return!1;for(let t=0;t<this.children.length;t+=3)if(this.children[t]!=e.children[t]||this.children[t+1]!=e.children[t+1]||!this.children[t+2].eq(e.children[t+2]))return!1;return!0}locals(e){return yo(this.localsInner(e))}localsInner(e){if(this==co)return so;if(e.inlineContent||!this.local.some(ro.is))return this.local;let t=[];for(let e=0;e<this.local.length;e++)this.local[e].type instanceof ro||t.push(this.local[e]);return t}forEachSet(e){e(this)}}ao.empty=new ao([],[]),ao.removeOverlap=yo;const co=ao.empty;class ho{constructor(e){this.members=e}map(e,t){const n=this.members.map(n=>n.map(e,t,lo));return ho.from(n)}forChild(e,t){if(t.isLeaf)return ao.empty;let n=[];for(let r=0;r<this.members.length;r++){let i=this.members[r].forChild(e,t);i!=co&&(i instanceof ho?n=n.concat(i.members):n.push(i))}return ho.from(n)}eq(e){if(!(e instanceof ho)||e.members.length!=this.members.length)return!1;for(let t=0;t<this.members.length;t++)if(!this.members[t].eq(e.members[t]))return!1;return!0}locals(e){let t,n=!0;for(let r=0;r<this.members.length;r++){let i=this.members[r].localsInner(e);if(i.length)if(t){n&&(t=t.slice(),n=!1);for(let e=0;e<i.length;e++)t.push(i[e])}else t=i}return t?yo(n?t:t.sort(go)):so}static from(e){switch(e.length){case 0:return co;case 1:return e[0];default:return new ho(e.every(e=>e instanceof ao)?e:e.reduce((e,t)=>e.concat(t instanceof ao?t:t.members),[]))}}forEachSet(e){for(let t=0;t<this.members.length;t++)this.members[t].forEachSet(e)}}function uo(e,t){if(!t||!e.length)return e;let n=[];for(let r=0;r<e.length;r++){let i=e[r];n.push(new oo(i.from+t,i.to+t,i.type))}return n}function po(e,t,n){if(t.isLeaf)return null;let r=n+t.nodeSize,i=null;for(let t,o=0;o<e.length;o++)(t=e[o])&&t.from>n&&t.to<r&&((i||(i=[])).push(t),e[o]=null);return i}function fo(e){let t=[];for(let n=0;n<e.length;n++)null!=e[n]&&t.push(e[n]);return t}function mo(e,t,n,r){let i=[],o=!1;t.forEach((t,s)=>{let l=po(e,t,s+n);if(l){o=!0;let e=mo(l,t,n+s+1,r);e!=co&&i.push(s,s+t.nodeSize,e)}});let s=uo(o?fo(e):e,-n).sort(go);for(let e=0;e<s.length;e++)s[e].type.valid(t,s[e])||(r.onRemove&&r.onRemove(s[e].spec),s.splice(e--,1));return s.length||i.length?new ao(s,i):co}function go(e,t){return e.from-t.from||e.to-t.to}function yo(e){let t=e;for(let n=0;n<t.length-1;n++){let r=t[n];if(r.from!=r.to)for(let i=n+1;i<t.length;i++){let o=t[i];if(o.from!=r.from){o.from<r.to&&(t==e&&(t=e.slice()),t[n]=r.copy(r.from,o.from),ko(t,i,r.copy(o.from,r.to)));break}o.to!=r.to&&(t==e&&(t=e.slice()),t[i]=o.copy(o.from,r.to),ko(t,i+1,o.copy(r.to,o.to)))}}return t}function ko(e,t,n){for(;t<e.length&&go(n,e[t])>0;)t++;e.splice(t,0,n)}function bo(e){let t=[];return e.someProp("decorations",n=>{let r=n(e.state);r&&r!=co&&t.push(r)}),e.cursorWrapper&&t.push(ao.create(e.state.doc,[e.cursorWrapper.deco])),ho.from(t)}const wo={childList:!0,characterData:!0,characterDataOldValue:!0,attributes:!0,attributeOldValue:!0,subtree:!0},Co=qn&&$n<=11;class xo{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}set(e){this.anchorNode=e.anchorNode,this.anchorOffset=e.anchorOffset,this.focusNode=e.focusNode,this.focusOffset=e.focusOffset}clear(){this.anchorNode=this.focusNode=null}eq(e){return e.anchorNode==this.anchorNode&&e.anchorOffset==this.anchorOffset&&e.focusNode==this.focusNode&&e.focusOffset==this.focusOffset}}class Do{constructor(e,t){this.view=e,this.handleDOMChange=t,this.queue=[],this.flushingSoon=-1,this.observer=null,this.currentSelection=new xo,this.onCharData=null,this.suppressingSelectionUpdates=!1,this.lastChangedTextNode=null,this.observer=window.MutationObserver&&new window.MutationObserver(e=>{for(let t=0;t<e.length;t++)this.queue.push(e[t]);qn&&$n<=11&&e.some(e=>"childList"==e.type&&e.removedNodes.length||"characterData"==e.type&&e.oldValue.length>e.target.nodeValue.length)?this.flushSoon():this.flush()}),Co&&(this.onCharData=e=>{this.queue.push({target:e.target,type:"characterData",oldValue:e.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this)}flushSoon(){this.flushingSoon<0&&(this.flushingSoon=window.setTimeout(()=>{this.flushingSoon=-1,this.flush()},20))}forceFlush(){this.flushingSoon>-1&&(window.clearTimeout(this.flushingSoon),this.flushingSoon=-1,this.flush())}start(){this.observer&&(this.observer.takeRecords(),this.observer.observe(this.view.dom,wo)),this.onCharData&&this.view.dom.addEventListener("DOMCharacterDataModified",this.onCharData),this.connectSelection()}stop(){if(this.observer){let e=this.observer.takeRecords();if(e.length){for(let t=0;t<e.length;t++)this.queue.push(e[t]);window.setTimeout(()=>this.flush(),20)}this.observer.disconnect()}this.onCharData&&this.view.dom.removeEventListener("DOMCharacterDataModified",this.onCharData),this.disconnectSelection()}connectSelection(){this.view.dom.ownerDocument.addEventListener("selectionchange",this.onSelectionChange)}disconnectSelection(){this.view.dom.ownerDocument.removeEventListener("selectionchange",this.onSelectionChange)}suppressSelectionUpdates(){this.suppressingSelectionUpdates=!0,setTimeout(()=>this.suppressingSelectionUpdates=!1,50)}onSelectionChange(){if(Yr(this.view)){if(this.suppressingSelectionUpdates)return Wr(this.view);if(qn&&$n<=11&&!this.view.state.selection.empty){let e=this.view.domSelectionRange();if(e.focusNode&&En(e.focusNode,e.focusOffset,e.anchorNode,e.anchorOffset))return this.flushSoon()}this.flush()}}setCurSelection(){this.currentSelection.set(this.view.domSelectionRange())}ignoreSelectionChange(e){if(!e.focusNode)return!0;let t,n=new Set;for(let t=e.focusNode;t;t=vn(t))n.add(t);for(let r=e.anchorNode;r;r=vn(r))if(n.has(r)){t=r;break}let r=t&&this.view.docView.nearestDesc(t);return r&&r.ignoreMutation({type:"selection",target:3==t.nodeType?t.parentNode:t})?(this.setCurSelection(),!0):void 0}pendingRecords(){if(this.observer)for(let e of this.observer.takeRecords())this.queue.push(e);return this.queue}flush(){let{view:e}=this;if(!e.docView||this.flushingSoon>-1)return;let t=this.pendingRecords();t.length&&(this.queue=[]);let n=e.domSelectionRange(),r=!this.suppressingSelectionUpdates&&!this.currentSelection.eq(n)&&Yr(e)&&!this.ignoreSelectionChange(n),i=-1,o=-1,s=!1,l=[];if(e.editable)for(let e=0;e<t.length;e++){let n=this.registerMutation(t[e],l);n&&(i=i<0?n.from:Math.min(n.from,i),o=o<0?n.to:Math.max(n.to,o),n.typeOver&&(s=!0))}if(Vn&&l.length){let t=l.filter(e=>"BR"==e.nodeName);if(2==t.length){let[e,n]=t;e.parentNode&&e.parentNode.parentNode==n.parentNode?n.remove():e.remove()}else{let{focusNode:n}=this.currentSelection;for(let r of t){let t=r.parentNode;!t||"LI"!=t.nodeName||n&&Eo(e,n)==t||r.remove()}}}else if((Jn||Hn)&&l.some(e=>"BR"==e.nodeName)&&(8==e.input.lastKeyCode||46==e.input.lastKeyCode))for(let e of l)if("BR"==e.nodeName&&e.parentNode){let t=e.nextSibling;t&&1==t.nodeType&&"false"==t.contentEditable&&e.parentNode.removeChild(e)}let a=null;i<0&&r&&e.input.lastFocus>Date.now()-200&&Math.max(e.input.lastTouch,e.input.lastClick.time)<Date.now()-300&&Fn(n)&&(a=jr(e))&&a.eq(Ge.near(e.state.doc.resolve(0),1))?(e.input.lastFocus=0,Wr(e),this.currentSelection.set(n),e.scrollToSelection()):(i>-1||r)&&(i>-1&&(e.docView.markDirty(i,o),function(e){if(vo.has(e))return;if(vo.set(e,null),-1!==["normal","nowrap","pre-line"].indexOf(getComputedStyle(e.dom).whiteSpace)){if(e.requiresGeckoHackNode=Vn,_o)return;console.warn("ProseMirror expects the CSS white-space property to be set, preferably to 'pre-wrap'. It is recommended to load style/prosemirror.css from the prosemirror-view package."),_o=!0}}(e)),this.handleDOMChange(i,o,s,l),e.docView&&e.docView.dirty?e.updateState(e.state):this.currentSelection.eq(n)||Wr(e),this.currentSelection.set(n))}registerMutation(e,t){if(t.indexOf(e.target)>-1)return null;let n=this.view.docView.nearestDesc(e.target);if("attributes"==e.type&&(n==this.view.docView||"contenteditable"==e.attributeName||"style"==e.attributeName&&!e.oldValue&&!e.target.getAttribute("style")))return null;if(!n||n.ignoreMutation(e))return null;if("childList"==e.type){for(let n=0;n<e.addedNodes.length;n++){let r=e.addedNodes[n];t.push(r),3==r.nodeType&&(this.lastChangedTextNode=r)}if(n.contentDOM&&n.contentDOM!=n.dom&&!n.contentDOM.contains(e.target))return{from:n.posBefore,to:n.posAfter};let r=e.previousSibling,i=e.nextSibling;if(qn&&$n<=11&&e.addedNodes.length)for(let t=0;t<e.addedNodes.length;t++){let{previousSibling:n,nextSibling:o}=e.addedNodes[t];(!n||Array.prototype.indexOf.call(e.addedNodes,n)<0)&&(r=n),(!o||Array.prototype.indexOf.call(e.addedNodes,o)<0)&&(i=o)}let o=r&&r.parentNode==e.target?Dn(r)+1:0,s=n.localPosFromDOM(e.target,o,-1),l=i&&i.parentNode==e.target?Dn(i):e.target.childNodes.length;return{from:s,to:n.localPosFromDOM(e.target,l,1)}}return"attributes"==e.type?{from:n.posAtStart-n.border,to:n.posAtEnd+n.border}:(this.lastChangedTextNode=e.target,{from:n.posAtStart,to:n.posAtEnd,typeOver:e.target.nodeValue==e.oldValue})}}let vo=new WeakMap,_o=!1;function So(e,t){let n=t.startContainer,r=t.startOffset,i=t.endContainer,o=t.endOffset,s=e.domAtPos(e.state.selection.anchor);return En(s.node,s.offset,i,o)&&([n,r,i,o]=[i,o,n,r]),{anchorNode:n,anchorOffset:r,focusNode:i,focusOffset:o}}function Eo(e,t){for(let n=t.parentNode;n&&n!=e.dom;n=n.parentNode){let t=e.docView.nearestDesc(n,!0);if(t&&t.node.isBlock)return n}return null}function Ao(e){let t=e.pmViewDesc;if(t)return t.parseRule();if("BR"==e.nodeName&&e.parentNode){if(Hn&&/^(ul|ol)$/i.test(e.parentNode.nodeName)){let e=document.createElement("div");return e.appendChild(document.createElement("li")),{skip:e}}if(e.parentNode.lastChild==e||Hn&&/^(tr|table)$/i.test(e.parentNode.nodeName))return{ignore:!0}}else if("IMG"==e.nodeName&&e.getAttribute("mark-placeholder"))return{ignore:!0};return null}const Mo=/^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;function Oo(e,t,n,i,o){let s=e.input.compositionPendingChanges||(e.composing?e.input.compositionID:0);if(e.input.compositionPendingChanges=0,t<0){let t=e.input.lastSelectionTime>Date.now()-50?e.input.lastSelectionOrigin:null,n=jr(e,t);if(n&&!e.state.selection.eq(n)){if(Jn&&Gn&&13===e.input.lastKeyCode&&Date.now()-100<e.input.lastKeyCodeTime&&e.someProp("handleKeyDown",t=>t(e,Tn(13,"Enter"))))return;let r=e.state.tr.setSelection(n);"pointer"==t?r.setMeta("pointer",!0):"key"==t&&r.scrollIntoView(),s&&r.setMeta("composition",s),e.dispatch(r)}return}let l=e.state.doc.resolve(t),a=l.sharedDepth(n);t=l.before(a+1),n=e.state.doc.resolve(n).after(a+1);let c,h,u=e.state.selection,p=function(e,t,n){let r,{node:i,fromOffset:o,toOffset:s,from:l,to:a}=e.docView.parseRange(t,n),c=e.domSelectionRange(),h=c.anchorNode;if(h&&e.dom.contains(1==h.nodeType?h:h.parentNode)&&(r=[{node:h,offset:c.anchorOffset}],Fn(c)||r.push({node:c.focusNode,offset:c.focusOffset})),Jn&&8===e.input.lastKeyCode)for(let e=s;e>o;e--){let t=i.childNodes[e-1],n=t.pmViewDesc;if("BR"==t.nodeName&&!n){s=e;break}if(!n||n.size)break}let u=e.state.doc,p=e.someProp("domParser")||K.fromSchema(e.state.schema),d=u.resolve(l),f=null,m=p.parse(i,{topNode:d.parent,topMatch:d.parent.contentMatchAt(d.index()),topOpen:!0,from:o,to:s,preserveWhitespace:"pre"!=d.parent.type.whitespace||"full",findPositions:r,ruleFromNode:Ao,context:d});if(r&&null!=r[0].pos){let e=r[0].pos,t=r[1]&&r[1].pos;null==t&&(t=e),f={anchor:e+l,head:t+l}}return{doc:m,sel:f,from:l,to:a}}(e,t,n),d=e.state.doc,f=d.slice(p.from,p.to);8===e.input.lastKeyCode&&Date.now()-100<e.input.lastKeyCodeTime?(c=e.state.selection.to,h="end"):(c=e.state.selection.from,h="start"),e.input.lastKeyCode=null;let m=function(e,t,n,r,i){let o=e.findDiffStart(t,n);if(null==o)return null;let{a:s,b:l}=e.findDiffEnd(t,n+e.size,n+t.size);if("end"==i){r-=s+Math.max(0,o-Math.min(s,l))-o}if(s<o&&e.size<t.size){let e=r<=o&&r>=s?o-r:0;o-=e,o&&o<t.size&&To(t.textBetween(o-1,o+1))&&(o+=e?1:-1),l=o+(l-s),s=o}else if(l<o){let t=r<=o&&r>=l?o-r:0;o-=t,o&&o<e.size&&To(e.textBetween(o-1,o+1))&&(o+=t?1:-1),s=o+(s-l),l=o}return{start:o,endA:s,endB:l}}(f.content,p.doc.content,p.from,c,h);if(m&&e.input.domChangeCount++,(Un&&e.input.lastIOSEnter>Date.now()-225||Gn)&&o.some(e=>1==e.nodeType&&!Mo.test(e.nodeName))&&(!m||m.endA>=m.endB)&&e.someProp("handleKeyDown",t=>t(e,Tn(13,"Enter"))))return void(e.input.lastIOSEnter=0);if(!m){if(!(i&&u instanceof et&&!u.empty&&u.$head.sameParent(u.$anchor))||e.composing||p.sel&&p.sel.anchor!=p.sel.head){if(p.sel){let t=No(e,e.state.doc,p.sel);if(t&&!t.eq(e.state.selection)){let n=e.state.tr.setSelection(t);s&&n.setMeta("composition",s),e.dispatch(n)}}return}m={start:u.from,endA:u.to,endB:u.to}}e.state.selection.from<e.state.selection.to&&m.start==m.endB&&e.state.selection instanceof et&&(m.start>e.state.selection.from&&m.start<=e.state.selection.from+2&&e.state.selection.from>=p.from?m.start=e.state.selection.from:m.endA<e.state.selection.to&&m.endA>=e.state.selection.to-2&&e.state.selection.to<=p.to&&(m.endB+=e.state.selection.to-m.endA,m.endA=e.state.selection.to)),qn&&$n<=11&&m.endB==m.start+1&&m.endA==m.start&&m.start>p.from&&"  "==p.doc.textBetween(m.start-p.from-1,m.start-p.from+1)&&(m.start--,m.endA--,m.endB--);let g=p.doc.resolveNoCache(m.start-p.from),y=p.doc.resolveNoCache(m.endB-p.from),k=d.resolve(m.start),b=g.sameParent(y)&&g.parent.inlineContent&&k.end()>=m.endA;if((Un&&e.input.lastIOSEnter>Date.now()-225&&(!b||o.some(e=>"DIV"==e.nodeName||"P"==e.nodeName))||!b&&g.pos<p.doc.content.size&&(!g.sameParent(y)||!g.parent.inlineContent)&&g.pos<y.pos&&!/\S/.test(p.doc.textBetween(g.pos,y.pos,"","")))&&e.someProp("handleKeyDown",t=>t(e,Tn(13,"Enter"))))return void(e.input.lastIOSEnter=0);if(e.state.selection.anchor>m.start&&function(e,t,n,r,i){if(n-t<=i.pos-r.pos||Fo(r,!0,!1)<i.pos)return!1;let o=e.resolve(t);if(!r.parent.isTextblock){let e=o.nodeAfter;return null!=e&&n==t+e.nodeSize}if(o.parentOffset<o.parent.content.size||!o.parent.isTextblock)return!1;let s=e.resolve(Fo(o,!0,!0));return!(!s.parent.isTextblock||s.pos>n||Fo(s,!0,!1)<n)&&r.parent.content.cut(r.parentOffset).eq(s.parent.content)}(d,m.start,m.endA,g,y)&&e.someProp("handleKeyDown",t=>t(e,Tn(8,"Backspace"))))return void(Gn&&Jn&&e.domObserver.suppressSelectionUpdates());Jn&&m.endB==m.start&&(e.input.lastChromeDelete=Date.now()),Gn&&!b&&g.start()!=y.start()&&0==y.parentOffset&&g.depth==y.depth&&p.sel&&p.sel.anchor==p.sel.head&&p.sel.head==m.endA&&(m.endB-=2,y=p.doc.resolveNoCache(m.endB-p.from),setTimeout(()=>{e.someProp("handleKeyDown",function(t){return t(e,Tn(13,"Enter"))})},20));let w,C=m.start,x=m.endA,D=t=>{let n=t||e.state.tr.replace(C,x,p.doc.slice(m.start-p.from,m.endB-p.from));if(p.sel){let t=No(e,n.doc,p.sel);t&&!(Jn&&e.composing&&t.empty&&(m.start!=m.endB||e.input.lastChromeDelete<Date.now()-100)&&(t.head==C||t.head==n.mapping.map(x)-1)||qn&&t.empty&&t.head==C)&&n.setSelection(t)}return s&&n.setMeta("composition",s),n.scrollIntoView()};if(b)if(g.pos==y.pos){qn&&$n<=11&&0==g.parentOffset&&(e.domObserver.suppressSelectionUpdates(),setTimeout(()=>Wr(e),20));let t=D(e.state.tr.delete(C,x)),n=d.resolve(m.start).marksAcross(d.resolve(m.endA));n&&t.ensureMarks(n),e.dispatch(t)}else if(m.endA==m.endB&&(w=function(e,t){let n,i,o,s=e.firstChild.marks,l=t.firstChild.marks,a=s,c=l;for(let e=0;e<l.length;e++)a=l[e].removeFromSet(a);for(let e=0;e<s.length;e++)c=s[e].removeFromSet(c);if(1==a.length&&0==c.length)i=a[0],n="add",o=e=>e.mark(i.addToSet(e.marks));else{if(0!=a.length||1!=c.length)return null;i=c[0],n="remove",o=e=>e.mark(i.removeFromSet(e.marks))}let h=[];for(let e=0;e<t.childCount;e++)h.push(o(t.child(e)));if(r.from(h).eq(e))return{mark:i,type:n}}(g.parent.content.cut(g.parentOffset,y.parentOffset),k.parent.content.cut(k.parentOffset,m.endA-k.start())))){let t=D(e.state.tr);"add"==w.type?t.addMark(C,x,w.mark):t.removeMark(C,x,w.mark),e.dispatch(t)}else if(g.parent.child(g.index()).isText&&g.index()==y.index()-(y.textOffset?0:1)){let t=g.parent.textBetween(g.parentOffset,y.parentOffset),n=()=>D(e.state.tr.insertText(t,C,x));e.someProp("handleTextInput",r=>r(e,C,x,t,n))||e.dispatch(n())}else e.dispatch(D());else e.dispatch(D())}function No(e,t,n){return Math.max(n.anchor,n.head)>t.content.size?null:Xr(e,t.resolve(n.anchor),t.resolve(n.head))}function Fo(e,t,n){let r=e.depth,i=t?e.end():e.pos;for(;r>0&&(t||e.indexAfter(r)==e.node(r).childCount);)r--,i++,t=!1;if(n){let t=e.node(r).maybeChild(e.indexAfter(r));for(;t&&!t.isLeaf;)t=t.firstChild,i++}return i}function To(e){if(2!=e.length)return!1;let t=e.charCodeAt(0),n=e.charCodeAt(1);return t>=56320&&t<=57343&&n>=55296&&n<=56319}class Ro{constructor(e,t){this._root=null,this.focused=!1,this.trackWrites=null,this.mounted=!1,this.markCursor=null,this.cursorWrapper=null,this.lastSelectedViewDesc=void 0,this.input=new Mi,this.prevDirectPlugins=[],this.pluginViews=[],this.requiresGeckoHackNode=!1,this.dragging=null,this._props=t,this.state=t.state,this.directPlugins=t.plugins||[],this.directPlugins.forEach(Lo),this.dispatch=this.dispatch.bind(this),this.dom=e&&e.mount||document.createElement("div"),e&&(e.appendChild?e.appendChild(this.dom):"function"==typeof e?e(this.dom):e.mount&&(this.mounted=!0)),this.editable=Bo(this),Io(this),this.nodeViews=Po(this),this.docView=Er(this.state.doc,zo(this),bo(this),this.dom,this),this.domObserver=new Do(this,(e,t,n,r)=>Oo(this,e,t,n,r)),this.domObserver.start(),function(e){for(let t in Si){let n=Si[t];e.dom.addEventListener(t,e.input.eventHandlers[t]=t=>{!Ti(e,t)||Fi(e,t)||!e.editable&&t.type in Ei||n(e,t)},Ai[t]?{passive:!0}:void 0)}Hn&&e.dom.addEventListener("input",()=>null),Ni(e)}(this),this.updatePluginViews()}get composing(){return this.input.composing}get props(){if(this._props.state!=this.state){let e=this._props;this._props={};for(let t in e)this._props[t]=e[t];this._props.state=this.state}return this._props}update(e){e.handleDOMEvents!=this._props.handleDOMEvents&&Ni(this);let t=this._props;this._props=e,e.plugins&&(e.plugins.forEach(Lo),this.directPlugins=e.plugins),this.updateStateInner(e.state,t)}setProps(e){let t={};for(let e in this._props)t[e]=this._props[e];t.state=this.state;for(let n in e)t[n]=e[n];this.update(t)}updateState(e){this.updateStateInner(e,this._props)}updateStateInner(e,t){var n;let r=this.state,i=!1,o=!1;e.storedMarks&&this.composing&&(Hi(this),o=!0),this.state=e;let s=r.plugins!=e.plugins||this._props.plugins!=t.plugins;if(s||this._props.plugins!=t.plugins||this._props.nodeViews!=t.nodeViews){let e=Po(this);(function(e,t){let n=0,r=0;for(let r in e){if(e[r]!=t[r])return!0;n++}for(let e in t)r++;return n!=r})(e,this.nodeViews)&&(this.nodeViews=e,i=!0)}(s||t.handleDOMEvents!=this._props.handleDOMEvents)&&Ni(this),this.editable=Bo(this),Io(this);let l=bo(this),a=zo(this),c=r.plugins==e.plugins||r.doc.eq(e.doc)?e.scrollToSelection>r.scrollToSelection?"to selection":"preserve":"reset",h=i||!this.docView.matchesNode(e.doc,a,l);!h&&e.selection.eq(r.selection)||(o=!0);let u="preserve"==c&&o&&null==this.dom.style.overflowAnchor&&function(e){let t,n,r=e.dom.getBoundingClientRect(),i=Math.max(0,r.top);for(let o=(r.left+r.right)/2,s=i+1;s<Math.min(innerHeight,r.bottom);s+=5){let r=e.root.elementFromPoint(o,s);if(!r||r==e.dom||!e.dom.contains(r))continue;let l=r.getBoundingClientRect();if(l.top>=i-20){t=r,n=l.top;break}}return{refDOM:t,refTop:n,stack:rr(e.dom)}}(this);if(o){this.domObserver.stop();let t=h&&(qn||Jn)&&!this.composing&&!r.selection.empty&&!e.selection.empty&&function(e,t){let n=Math.min(e.$anchor.sharedDepth(e.head),t.$anchor.sharedDepth(t.head));return e.$anchor.start(n)!=t.$anchor.start(n)}(r.selection,e.selection);if(h){let n=Jn?this.trackWrites=this.domSelectionRange().focusNode:null;this.composing&&(this.input.compositionNode=Ui(this)),!i&&this.docView.update(e.doc,a,l,this)||(this.docView.updateOuterDeco(a),this.docView.destroy(),this.docView=Er(e.doc,a,l,this.dom,this)),n&&!this.trackWrites&&(t=!0)}t||!(this.input.mouseDown&&this.domObserver.currentSelection.eq(this.domSelectionRange())&&function(e){let t=e.docView.domFromPos(e.state.selection.anchor,0),n=e.domSelectionRange();return En(t.node,t.offset,n.anchorNode,n.anchorOffset)}(this))?Wr(this,t):(Gr(this,e.selection),this.domObserver.setCurSelection()),this.domObserver.start()}this.updatePluginViews(r),(null===(n=this.dragging)||void 0===n?void 0:n.node)&&!r.doc.eq(e.doc)&&this.updateDraggedNode(this.dragging,r),"reset"==c?this.dom.scrollTop=0:"to selection"==c?this.scrollToSelection():u&&function({refDOM:e,refTop:t,stack:n}){let r=e?e.getBoundingClientRect().top:0;ir(n,0==r?0:r-t)}(u)}scrollToSelection(){let e=this.domSelectionRange().focusNode;if(e&&this.dom.contains(1==e.nodeType?e:e.parentNode))if(this.someProp("handleScrollToSelection",e=>e(this)));else if(this.state.selection instanceof nt){let t=this.docView.domAfterPos(this.state.selection.from);1==t.nodeType&&nr(this,t.getBoundingClientRect(),e)}else nr(this,this.coordsAtPos(this.state.selection.head,1),e);else;}destroyPluginViews(){let e;for(;e=this.pluginViews.pop();)e.destroy&&e.destroy()}updatePluginViews(e){if(e&&e.plugins==this.state.plugins&&this.directPlugins==this.prevDirectPlugins)for(let t=0;t<this.pluginViews.length;t++){let n=this.pluginViews[t];n.update&&n.update(this,e)}else{this.prevDirectPlugins=this.directPlugins,this.destroyPluginViews();for(let e=0;e<this.directPlugins.length;e++){let t=this.directPlugins[e];t.spec.view&&this.pluginViews.push(t.spec.view(this))}for(let e=0;e<this.state.plugins.length;e++){let t=this.state.plugins[e];t.spec.view&&this.pluginViews.push(t.spec.view(this))}}}updateDraggedNode(e,t){let n=e.node,r=-1;if(this.state.doc.nodeAt(n.from)==n.node)r=n.from;else{let e=n.from+(this.state.doc.content.size-t.doc.content.size);(e>0&&this.state.doc.nodeAt(e))==n.node&&(r=e)}this.dragging=new Xi(e.slice,e.move,r<0?void 0:nt.create(this.state.doc,r))}someProp(e,t){let n,r=this._props&&this._props[e];if(null!=r&&(n=t?t(r):r))return n;for(let r=0;r<this.directPlugins.length;r++){let i=this.directPlugins[r].props[e];if(null!=i&&(n=t?t(i):i))return n}let i=this.state.plugins;if(i)for(let r=0;r<i.length;r++){let o=i[r].props[e];if(null!=o&&(n=t?t(o):o))return n}}hasFocus(){if(qn){let e=this.root.activeElement;if(e==this.dom)return!0;if(!e||!this.dom.contains(e))return!1;for(;e&&this.dom!=e&&this.dom.contains(e);){if("false"==e.contentEditable)return!1;e=e.parentElement}return!0}return this.root.activeElement==this.dom}focus(){this.domObserver.stop(),this.editable&&function(e){if(e.setActive)return e.setActive();if(or)return e.focus(or);let t=rr(e);e.focus(null==or?{get preventScroll(){return or={preventScroll:!0},!0}}:void 0),or||(or=!1,ir(t,0))}(this.dom),Wr(this),this.domObserver.start()}get root(){let e=this._root;if(null==e)for(let e=this.dom.parentNode;e;e=e.parentNode)if(9==e.nodeType||11==e.nodeType&&e.host)return e.getSelection||(Object.getPrototypeOf(e).getSelection=()=>e.ownerDocument.getSelection()),this._root=e;return e||document}updateRoot(){this._root=null}posAtCoords(e){return cr(this,e)}coordsAtPos(e,t=1){return dr(this,e,t)}domAtPos(e,t=0){return this.docView.domFromPos(e,t)}nodeDOM(e){let t=this.docView.descAt(e);return t?t.nodeDOM:null}posAtDOM(e,t,n=-1){let r=this.docView.posFromDOM(e,t,n);if(null==r)throw new RangeError("DOM position not inside the editor");return r}endOfTextblock(e,t){return Cr(this,t||this.state,e)}pasteHTML(e,t){return Gi(this,"",e,!1,t||new ClipboardEvent("paste"))}pasteText(e,t){return Gi(this,e,null,!0,t||new ClipboardEvent("paste"))}serializeForClipboard(e){return fi(this,e)}destroy(){this.docView&&(!function(e){e.domObserver.stop();for(let t in e.input.eventHandlers)e.dom.removeEventListener(t,e.input.eventHandlers[t]);clearTimeout(e.input.composingTimeout),clearTimeout(e.input.lastIOSEnterFallbackTimeout)}(this),this.destroyPluginViews(),this.mounted?(this.docView.update(this.state.doc,[],bo(this),this),this.dom.textContent=""):this.dom.parentNode&&this.dom.parentNode.removeChild(this.dom),this.docView.destroy(),this.docView=null,_n=null)}get isDestroyed(){return null==this.docView}dispatchEvent(e){return function(e,t){Fi(e,t)||!Si[t.type]||!e.editable&&t.type in Ei||Si[t.type](e,t)}(this,e)}domSelectionRange(){let e=this.domSelection();return e?Hn&&11===this.root.nodeType&&function(e){let t=e.activeElement;for(;t&&t.shadowRoot;)t=t.shadowRoot.activeElement;return t}(this.dom.ownerDocument)==this.dom&&function(e,t){if(t.getComposedRanges){let n=t.getComposedRanges(e.root)[0];if(n)return So(e,n)}let n;function r(e){e.preventDefault(),e.stopImmediatePropagation(),n=e.getTargetRanges()[0]}return e.dom.addEventListener("beforeinput",r,!0),document.execCommand("indent"),e.dom.removeEventListener("beforeinput",r,!0),n?So(e,n):null}(this,e)||e:{focusNode:null,focusOffset:0,anchorNode:null,anchorOffset:0}}domSelection(){return this.root.getSelection()}}function zo(e){let t=Object.create(null);return t.class="ProseMirror",t.contenteditable=String(e.editable),e.someProp("attributes",n=>{if("function"==typeof n&&(n=n(e.state)),n)for(let e in n)"class"==e?t.class+=" "+n[e]:"style"==e?t.style=(t.style?t.style+";":"")+n[e]:t[e]||"contenteditable"==e||"nodeName"==e||(t[e]=String(n[e]))}),t.translate||(t.translate="no"),[oo.node(0,e.state.doc.content.size,t)]}function Io(e){if(e.markCursor){let t=document.createElement("img");t.className="ProseMirror-separator",t.setAttribute("mark-placeholder","true"),t.setAttribute("alt",""),e.cursorWrapper={dom:t,deco:oo.widget(e.state.selection.from,t,{raw:!0,marks:e.markCursor})}}else e.cursorWrapper=null}function Bo(e){return!e.someProp("editable",t=>!1===t(e.state))}function Po(e){let t=Object.create(null);function n(e){for(let n in e)Object.prototype.hasOwnProperty.call(t,n)||(t[n]=e[n])}return e.someProp("nodeViews",n),e.someProp("markViews",n),t}function Lo(e){if(e.spec.state||e.spec.filterTransaction||e.spec.appendTransaction)throw new RangeError("Plugins passed directly to the view must not have a state component")}Ro.prototype.dispatch=function(e){let t=this._props.dispatchTransaction;t?t.call(this,e):this.updateState(this.state.apply(e))};const qo={};function $o(e,t){"string"!=typeof t&&(t=$o.defaultChars);const n=function(e){let t=qo[e];if(t)return t;t=qo[e]=[];for(let e=0;e<128;e++){const n=String.fromCharCode(e);t.push(n)}for(let n=0;n<e.length;n++){const r=e.charCodeAt(n);t[r]="%"+("0"+r.toString(16).toUpperCase()).slice(-2)}return t}(t);return e.replace(/(%[a-f0-9]{2})+/gi,function(e){let t="";for(let r=0,i=e.length;r<i;r+=3){const o=parseInt(e.slice(r+1,r+3),16);if(o<128)t+=n[o];else{if(192==(224&o)&&r+3<i){const n=parseInt(e.slice(r+4,r+6),16);if(128==(192&n)){const e=o<<6&1984|63&n;t+=e<128?"��":String.fromCharCode(e),r+=3;continue}}if(224==(240&o)&&r+6<i){const n=parseInt(e.slice(r+4,r+6),16),i=parseInt(e.slice(r+7,r+9),16);if(128==(192&n)&&128==(192&i)){const e=o<<12&61440|n<<6&4032|63&i;t+=e<2048||e>=55296&&e<=57343?"���":String.fromCharCode(e),r+=6;continue}}if(240==(248&o)&&r+9<i){const n=parseInt(e.slice(r+4,r+6),16),i=parseInt(e.slice(r+7,r+9),16),s=parseInt(e.slice(r+10,r+12),16);if(128==(192&n)&&128==(192&i)&&128==(192&s)){let e=o<<18&1835008|n<<12&258048|i<<6&4032|63&s;e<65536||e>1114111?t+="����":(e-=65536,t+=String.fromCharCode(55296+(e>>10),56320+(1023&e))),r+=9;continue}}t+="�"}}return t})}$o.defaultChars=";/?:@&=+$,#",$o.componentChars="";const Vo={};function jo(e,t,n){"string"!=typeof t&&(n=t,t=jo.defaultChars),void 0===n&&(n=!0);const r=function(e){let t=Vo[e];if(t)return t;t=Vo[e]=[];for(let e=0;e<128;e++){const n=String.fromCharCode(e);/^[0-9a-z]$/i.test(n)?t.push(n):t.push("%"+("0"+e.toString(16).toUpperCase()).slice(-2))}for(let n=0;n<e.length;n++)t[e.charCodeAt(n)]=e[n];return t}(t);let i="";for(let t=0,o=e.length;t<o;t++){const s=e.charCodeAt(t);if(n&&37===s&&t+2<o&&/^[0-9a-f]{2}$/i.test(e.slice(t+1,t+3)))i+=e.slice(t,t+3),t+=2;else if(s<128)i+=r[s];else if(s>=55296&&s<=57343){if(s>=55296&&s<=56319&&t+1<o){const n=e.charCodeAt(t+1);if(n>=56320&&n<=57343){i+=encodeURIComponent(e[t]+e[t+1]),t++;continue}}i+="%EF%BF%BD"}else i+=encodeURIComponent(e[t])}return i}function Jo(e){let t="";return t+=e.protocol||"",t+=e.slashes?"//":"",t+=e.auth?e.auth+"@":"",e.hostname&&-1!==e.hostname.indexOf(":")?t+="["+e.hostname+"]":t+=e.hostname||"",t+=e.port?":"+e.port:"",t+=e.pathname||"",t+=e.search||"",t+=e.hash||"",t}function Wo(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}jo.defaultChars=";/?:@&=+$,-_.!~*'()#",jo.componentChars="-_.!~*'()";const Ho=/^([a-z0-9.+-]+:)/i,Uo=/:[0-9]*$/,Ko=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,Zo=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),Go=["'"].concat(Zo),Qo=["%","/","?",";","#"].concat(Go),Xo=["/","?","#"],Yo=/^[+a-z0-9A-Z_-]{0,63}$/,es=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,ts={javascript:!0,"javascript:":!0},ns={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};function rs(e,t){if(e&&e instanceof Wo)return e;const n=new Wo;return n.parse(e,t),n}Wo.prototype.parse=function(e,t){let n,r,i,o=e;if(o=o.trim(),!t&&1===e.split("#").length){const e=Ko.exec(o);if(e)return this.pathname=e[1],e[2]&&(this.search=e[2]),this}let s=Ho.exec(o);if(s&&(s=s[0],n=s.toLowerCase(),this.protocol=s,o=o.substr(s.length)),(t||s||o.match(/^\/\/[^@\/]+@[^@\/]+/))&&(i="//"===o.substr(0,2),!i||s&&ts[s]||(o=o.substr(2),this.slashes=!0)),!ts[s]&&(i||s&&!ns[s])){let e,t,n=-1;for(let e=0;e<Xo.length;e++)r=o.indexOf(Xo[e]),-1!==r&&(-1===n||r<n)&&(n=r);t=-1===n?o.lastIndexOf("@"):o.lastIndexOf("@",n),-1!==t&&(e=o.slice(0,t),o=o.slice(t+1),this.auth=e),n=-1;for(let e=0;e<Qo.length;e++)r=o.indexOf(Qo[e]),-1!==r&&(-1===n||r<n)&&(n=r);-1===n&&(n=o.length),":"===o[n-1]&&n--;const i=o.slice(0,n);o=o.slice(n),this.parseHost(i),this.hostname=this.hostname||"";const s="["===this.hostname[0]&&"]"===this.hostname[this.hostname.length-1];if(!s){const e=this.hostname.split(/\./);for(let t=0,n=e.length;t<n;t++){const n=e[t];if(n&&!n.match(Yo)){let r="";for(let e=0,t=n.length;e<t;e++)n.charCodeAt(e)>127?r+="x":r+=n[e];if(!r.match(Yo)){const r=e.slice(0,t),i=e.slice(t+1),s=n.match(es);s&&(r.push(s[1]),i.unshift(s[2])),i.length&&(o=i.join(".")+o),this.hostname=r.join(".");break}}}}this.hostname.length>255&&(this.hostname=""),s&&(this.hostname=this.hostname.substr(1,this.hostname.length-2))}const l=o.indexOf("#");-1!==l&&(this.hash=o.substr(l),o=o.slice(0,l));const a=o.indexOf("?");return-1!==a&&(this.search=o.substr(a),o=o.slice(0,a)),o&&(this.pathname=o),ns[n]&&this.hostname&&!this.pathname&&(this.pathname=""),this},Wo.prototype.parseHost=function(e){let t=Uo.exec(e);t&&(t=t[0],":"!==t&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)};var is,ss=Object.freeze({__proto__:null,decode:$o,encode:jo,format:Jo,parse:rs}),ls=/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,as=/[\0-\x1F\x7F-\x9F]/,cs=/[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061D-\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1B7D\u1B7E\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u2E52-\u2E5D\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDEAD\uDF55-\uDF59\uDF86-\uDF89]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5A\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDEB9\uDF3C-\uDF3E]|\uD806[\uDC3B\uDD44-\uDD46\uDDE2\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2\uDF00-\uDF09]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8\uDF43-\uDF4F\uDFFF]|\uD809[\uDC70-\uDC74]|\uD80B[\uDFF1\uDFF2]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A\uDFE2]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/,hs=/[\$\+<->\^`\|~\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u07FE\u07FF\u0888\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D4F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u166D\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20C0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B97-\u2BFF\u2CE5-\u2CEA\u2E50\u2E51\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFF\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u31EF\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uAB6A\uAB6B\uFB29\uFBB2-\uFBC2\uFD40-\uFD4F\uFDCF\uFDFC-\uFDFF\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD]|\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C-\uDD8E\uDD90-\uDD9C\uDDA0\uDDD0-\uDDFC]|\uD802[\uDC77\uDC78\uDEC8]|\uD805\uDF3F|\uD807[\uDFD5-\uDFF1]|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD82F\uDC9C|\uD833[\uDF50-\uDFC3]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDEA\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD838[\uDD4F\uDEFF]|\uD83B[\uDCAC\uDCB0\uDD2E\uDEF0\uDEF1]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD0D-\uDDAD\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDE60-\uDE65\uDF00-\uDFFF]|\uD83D[\uDC00-\uDED7\uDEDC-\uDEEC\uDEF0-\uDEFC\uDF00-\uDF76\uDF7B-\uDFD9\uDFE0-\uDFEB\uDFF0]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDCB0\uDCB1\uDD00-\uDE53\uDE60-\uDE6D\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC5\uDECE-\uDEDB\uDEE0-\uDEE8\uDEF0-\uDEF8\uDF00-\uDF92\uDF94-\uDFCA]/,us=/[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/,ps=Object.freeze({__proto__:null,Any:ls,Cc:as,Cf:/[\xAD\u0600-\u0605\u061C\u06DD\u070F\u0890\u0891\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC3F]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/,P:cs,S:hs,Z:us}),ds=new Uint16Array('ᵁ<Õıʊҝջאٵ۞ޢߖࠏ੊ઑඡ๭༉༦჊ረዡᐕᒝᓃᓟᔥ\0\0\0\0\0\0ᕫᛍᦍᰒᷝ὾⁠↰⊍⏀⏻⑂⠤⤒ⴈ⹈⿎〖㊺㘹㞬㣾㨨㩱㫠㬮ࠀEMabcfglmnoprstu\\bfms„‹•˜¦³¹ÈÏlig耻Æ䃆P耻&䀦cute耻Á䃁reve;䄂Āiyx}rc耻Â䃂;䐐r;쀀𝔄rave耻À䃀pha;䎑acr;䄀d;橓Āgp¡on;䄄f;쀀𝔸plyFunction;恡ing耻Å䃅Ācs¾Ãr;쀀𝒜ign;扔ilde耻Ã䃃ml耻Ä䃄ЀaceforsuåûþėĜĢħĪĀcrêòkslash;或Ŷöø;櫧ed;挆y;䐑ƀcrtąċĔause;戵noullis;愬a;䎒r;쀀𝔅pf;쀀𝔹eve;䋘còēmpeq;扎܀HOacdefhilorsuōőŖƀƞƢƵƷƺǜȕɳɸɾcy;䐧PY耻©䂩ƀcpyŝŢźute;䄆Ā;iŧŨ拒talDifferentialD;慅leys;愭ȀaeioƉƎƔƘron;䄌dil耻Ç䃇rc;䄈nint;戰ot;䄊ĀdnƧƭilla;䂸terDot;䂷òſi;䎧rcleȀDMPTLJNjǑǖot;抙inus;抖lus;投imes;抗oĀcsǢǸkwiseContourIntegral;戲eCurlyĀDQȃȏoubleQuote;思uote;怙ȀlnpuȞȨɇɕonĀ;eȥȦ户;橴ƀgitȯȶȺruent;扡nt;戯ourIntegral;戮ĀfrɌɎ;愂oduct;成nterClockwiseContourIntegral;戳oss;樯cr;쀀𝒞pĀ;Cʄʅ拓ap;才րDJSZacefiosʠʬʰʴʸˋ˗ˡ˦̳ҍĀ;oŹʥtrahd;椑cy;䐂cy;䐅cy;䐏ƀgrsʿ˄ˇger;怡r;憡hv;櫤Āayː˕ron;䄎;䐔lĀ;t˝˞戇a;䎔r;쀀𝔇Āaf˫̧Ācm˰̢riticalȀADGT̖̜̀̆cute;䂴oŴ̋̍;䋙bleAcute;䋝rave;䁠ilde;䋜ond;拄ferentialD;慆Ѱ̽\0\0\0͔͂\0Ѕf;쀀𝔻ƀ;DE͈͉͍䂨ot;惜qual;扐blèCDLRUVͣͲ΂ϏϢϸontourIntegraìȹoɴ͹\0\0ͻ»͉nArrow;懓Āeo·ΤftƀARTΐΖΡrrow;懐ightArrow;懔eåˊngĀLRΫτeftĀARγιrrow;柸ightArrow;柺ightArrow;柹ightĀATϘϞrrow;懒ee;抨pɁϩ\0\0ϯrrow;懑ownArrow;懕erticalBar;戥ǹABLRTaВЪаўѿͼrrowƀ;BUНОТ憓ar;椓pArrow;懵reve;䌑eft˒к\0ц\0ѐightVector;楐eeVector;楞ectorĀ;Bљњ憽ar;楖ightǔѧ\0ѱeeVector;楟ectorĀ;BѺѻ懁ar;楗eeĀ;A҆҇护rrow;憧ĀctҒҗr;쀀𝒟rok;䄐ࠀNTacdfglmopqstuxҽӀӄӋӞӢӧӮӵԡԯԶՒ՝ՠեG;䅊H耻Ð䃐cute耻É䃉ƀaiyӒӗӜron;䄚rc耻Ê䃊;䐭ot;䄖r;쀀𝔈rave耻È䃈ement;戈ĀapӺӾcr;䄒tyɓԆ\0\0ԒmallSquare;旻erySmallSquare;斫ĀgpԦԪon;䄘f;쀀𝔼silon;䎕uĀaiԼՉlĀ;TՂՃ橵ilde;扂librium;懌Āci՗՚r;愰m;橳a;䎗ml耻Ë䃋Āipժկsts;戃onentialE;慇ʀcfiosօֈ֍ֲ׌y;䐤r;쀀𝔉lledɓ֗\0\0֣mallSquare;旼erySmallSquare;斪Ͱֺ\0ֿ\0\0ׄf;쀀𝔽All;戀riertrf;愱cò׋؀JTabcdfgorstר׬ׯ׺؀ؒؖ؛؝أ٬ٲcy;䐃耻>䀾mmaĀ;d׷׸䎓;䏜reve;䄞ƀeiy؇،ؐdil;䄢rc;䄜;䐓ot;䄠r;쀀𝔊;拙pf;쀀𝔾eater̀EFGLSTصلَٖٛ٦qualĀ;Lؾؿ扥ess;招ullEqual;执reater;檢ess;扷lantEqual;橾ilde;扳cr;쀀𝒢;扫ЀAacfiosuڅڋږڛڞڪھۊRDcy;䐪Āctڐڔek;䋇;䁞irc;䄤r;愌lbertSpace;愋ǰگ\0ڲf;愍izontalLine;攀Āctۃۅòکrok;䄦mpńېۘownHumðįqual;扏܀EJOacdfgmnostuۺ۾܃܇܎ܚܞܡܨ݄ݸދޏޕcy;䐕lig;䄲cy;䐁cute耻Í䃍Āiyܓܘrc耻Î䃎;䐘ot;䄰r;愑rave耻Ì䃌ƀ;apܠܯܿĀcgܴܷr;䄪inaryI;慈lieóϝǴ݉\0ݢĀ;eݍݎ戬Āgrݓݘral;戫section;拂isibleĀCTݬݲomma;恣imes;恢ƀgptݿރވon;䄮f;쀀𝕀a;䎙cr;愐ilde;䄨ǫޚ\0ޞcy;䐆l耻Ï䃏ʀcfosuެ޷޼߂ߐĀiyޱ޵rc;䄴;䐙r;쀀𝔍pf;쀀𝕁ǣ߇\0ߌr;쀀𝒥rcy;䐈kcy;䐄΀HJacfosߤߨ߽߬߱ࠂࠈcy;䐥cy;䐌ppa;䎚Āey߶߻dil;䄶;䐚r;쀀𝔎pf;쀀𝕂cr;쀀𝒦րJTaceflmostࠥࠩࠬࡐࡣ঳সে্਷ੇcy;䐉耻<䀼ʀcmnpr࠷࠼ࡁࡄࡍute;䄹bda;䎛g;柪lacetrf;愒r;憞ƀaeyࡗ࡜ࡡron;䄽dil;䄻;䐛Āfsࡨ॰tԀACDFRTUVarࡾࢩࢱࣦ࣠ࣼयज़ΐ४Ānrࢃ࢏gleBracket;柨rowƀ;BR࢙࢚࢞憐ar;懤ightArrow;懆eiling;挈oǵࢷ\0ࣃbleBracket;柦nǔࣈ\0࣒eeVector;楡ectorĀ;Bࣛࣜ懃ar;楙loor;挊ightĀAV࣯ࣵrrow;憔ector;楎Āerँगeƀ;AVउऊऐ抣rrow;憤ector;楚iangleƀ;BEतथऩ抲ar;槏qual;抴pƀDTVषूौownVector;楑eeVector;楠ectorĀ;Bॖॗ憿ar;楘ectorĀ;B॥०憼ar;楒ightáΜs̀EFGLSTॾঋকঝঢভqualGreater;拚ullEqual;扦reater;扶ess;檡lantEqual;橽ilde;扲r;쀀𝔏Ā;eঽা拘ftarrow;懚idot;䄿ƀnpw৔ਖਛgȀLRlr৞৷ਂਐeftĀAR০৬rrow;柵ightArrow;柷ightArrow;柶eftĀarγਊightáοightáϊf;쀀𝕃erĀLRਢਬeftArrow;憙ightArrow;憘ƀchtਾੀੂòࡌ;憰rok;䅁;扪Ѐacefiosuਗ਼੝੠੷੼અઋ઎p;椅y;䐜Ādl੥੯iumSpace;恟lintrf;愳r;쀀𝔐nusPlus;戓pf;쀀𝕄cò੶;䎜ҀJacefostuણધભીଔଙඑ඗ඞcy;䐊cute;䅃ƀaey઴હાron;䅇dil;䅅;䐝ƀgswે૰଎ativeƀMTV૓૟૨ediumSpace;怋hiĀcn૦૘ë૙eryThiî૙tedĀGL૸ଆreaterGreateòٳessLesóੈLine;䀊r;쀀𝔑ȀBnptଢନଷ଺reak;恠BreakingSpace;䂠f;愕ڀ;CDEGHLNPRSTV୕ୖ୪୼஡௫ఄ౞಄ದ೘ൡඅ櫬Āou୛୤ngruent;扢pCap;扭oubleVerticalBar;戦ƀlqxஃஊ஛ement;戉ualĀ;Tஒஓ扠ilde;쀀≂̸ists;戄reater΀;EFGLSTஶஷ஽௉௓௘௥扯qual;扱ullEqual;쀀≧̸reater;쀀≫̸ess;批lantEqual;쀀⩾̸ilde;扵umpń௲௽ownHump;쀀≎̸qual;쀀≏̸eĀfsఊధtTriangleƀ;BEచఛడ拪ar;쀀⧏̸qual;括s̀;EGLSTవశ఼ౄోౘ扮qual;扰reater;扸ess;쀀≪̸lantEqual;쀀⩽̸ilde;扴estedĀGL౨౹reaterGreater;쀀⪢̸essLess;쀀⪡̸recedesƀ;ESಒಓಛ技qual;쀀⪯̸lantEqual;拠ĀeiಫಹverseElement;戌ghtTriangleƀ;BEೋೌ೒拫ar;쀀⧐̸qual;拭ĀquೝഌuareSuĀbp೨೹setĀ;E೰ೳ쀀⊏̸qual;拢ersetĀ;Eഃആ쀀⊐̸qual;拣ƀbcpഓതൎsetĀ;Eഛഞ쀀⊂⃒qual;抈ceedsȀ;ESTലള഻െ抁qual;쀀⪰̸lantEqual;拡ilde;쀀≿̸ersetĀ;E൘൛쀀⊃⃒qual;抉ildeȀ;EFT൮൯൵ൿ扁qual;扄ullEqual;扇ilde;扉erticalBar;戤cr;쀀𝒩ilde耻Ñ䃑;䎝܀Eacdfgmoprstuvලෂ෉෕ෛ෠෧෼ขภยา฿ไlig;䅒cute耻Ó䃓Āiy෎ීrc耻Ô䃔;䐞blac;䅐r;쀀𝔒rave耻Ò䃒ƀaei෮ෲ෶cr;䅌ga;䎩cron;䎟pf;쀀𝕆enCurlyĀDQฎบoubleQuote;怜uote;怘;橔Āclวฬr;쀀𝒪ash耻Ø䃘iŬื฼de耻Õ䃕es;樷ml耻Ö䃖erĀBP๋๠Āar๐๓r;怾acĀek๚๜;揞et;掴arenthesis;揜Ҁacfhilors๿ງຊຏຒດຝະ໼rtialD;戂y;䐟r;쀀𝔓i;䎦;䎠usMinus;䂱Āipຢອncareplanåڝf;愙Ȁ;eio຺ູ໠໤檻cedesȀ;EST່້໏໚扺qual;檯lantEqual;扼ilde;找me;怳Ādp໩໮uct;戏ortionĀ;aȥ໹l;戝Āci༁༆r;쀀𝒫;䎨ȀUfos༑༖༛༟OT耻"䀢r;쀀𝔔pf;愚cr;쀀𝒬؀BEacefhiorsu༾གྷཇའཱིྦྷྪྭ႖ႩႴႾarr;椐G耻®䂮ƀcnrཎནབute;䅔g;柫rĀ;tཛྷཝ憠l;椖ƀaeyཧཬཱron;䅘dil;䅖;䐠Ā;vླྀཹ愜erseĀEUྂྙĀlq྇ྎement;戋uilibrium;懋pEquilibrium;楯r»ཹo;䎡ghtЀACDFTUVa࿁࿫࿳ဢဨၛႇϘĀnr࿆࿒gleBracket;柩rowƀ;BL࿜࿝࿡憒ar;懥eftArrow;懄eiling;按oǵ࿹\0စbleBracket;柧nǔည\0နeeVector;楝ectorĀ;Bဝသ懂ar;楕loor;挋Āerိ၃eƀ;AVဵံြ抢rrow;憦ector;楛iangleƀ;BEၐၑၕ抳ar;槐qual;抵pƀDTVၣၮၸownVector;楏eeVector;楜ectorĀ;Bႂႃ憾ar;楔ectorĀ;B႑႒懀ar;楓Āpuႛ႞f;愝ndImplies;楰ightarrow;懛ĀchႹႼr;愛;憱leDelayed;槴ڀHOacfhimoqstuფჱჷჽᄙᄞᅑᅖᅡᅧᆵᆻᆿĀCcჩხHcy;䐩y;䐨FTcy;䐬cute;䅚ʀ;aeiyᄈᄉᄎᄓᄗ檼ron;䅠dil;䅞rc;䅜;䐡r;쀀𝔖ortȀDLRUᄪᄴᄾᅉownArrow»ОeftArrow»࢚ightArrow»࿝pArrow;憑gma;䎣allCircle;战pf;쀀𝕊ɲᅭ\0\0ᅰt;戚areȀ;ISUᅻᅼᆉᆯ斡ntersection;抓uĀbpᆏᆞsetĀ;Eᆗᆘ抏qual;抑ersetĀ;Eᆨᆩ抐qual;抒nion;抔cr;쀀𝒮ar;拆ȀbcmpᇈᇛሉላĀ;sᇍᇎ拐etĀ;Eᇍᇕqual;抆ĀchᇠህeedsȀ;ESTᇭᇮᇴᇿ扻qual;檰lantEqual;扽ilde;承Tháྌ;我ƀ;esሒሓሣ拑rsetĀ;Eሜም抃qual;抇et»ሓրHRSacfhiorsሾቄ቉ቕ቞ቱቶኟዂወዑORN耻Þ䃞ADE;愢ĀHc቎ቒcy;䐋y;䐦Ābuቚቜ;䀉;䎤ƀaeyብቪቯron;䅤dil;䅢;䐢r;쀀𝔗Āeiቻ኉Dzኀ\0ኇefore;戴a;䎘Ācn኎ኘkSpace;쀀  Space;怉ldeȀ;EFTካኬኲኼ戼qual;扃ullEqual;扅ilde;扈pf;쀀𝕋ipleDot;惛Āctዖዛr;쀀𝒯rok;䅦ૡዷጎጚጦ\0ጬጱ\0\0\0\0\0ጸጽ፷ᎅ\0᏿ᐄᐊᐐĀcrዻጁute耻Ú䃚rĀ;oጇገ憟cir;楉rǣጓ\0጖y;䐎ve;䅬Āiyጞጣrc耻Û䃛;䐣blac;䅰r;쀀𝔘rave耻Ù䃙acr;䅪Ādiፁ፩erĀBPፈ፝Āarፍፐr;䁟acĀekፗፙ;揟et;掵arenthesis;揝onĀ;P፰፱拃lus;抎Āgp፻፿on;䅲f;쀀𝕌ЀADETadps᎕ᎮᎸᏄϨᏒᏗᏳrrowƀ;BDᅐᎠᎤar;椒ownArrow;懅ownArrow;憕quilibrium;楮eeĀ;AᏋᏌ报rrow;憥ownáϳerĀLRᏞᏨeftArrow;憖ightArrow;憗iĀ;lᏹᏺ䏒on;䎥ing;䅮cr;쀀𝒰ilde;䅨ml耻Ü䃜ҀDbcdefosvᐧᐬᐰᐳᐾᒅᒊᒐᒖash;披ar;櫫y;䐒ashĀ;lᐻᐼ抩;櫦Āerᑃᑅ;拁ƀbtyᑌᑐᑺar;怖Ā;iᑏᑕcalȀBLSTᑡᑥᑪᑴar;戣ine;䁼eparator;杘ilde;所ThinSpace;怊r;쀀𝔙pf;쀀𝕍cr;쀀𝒱dash;抪ʀcefosᒧᒬᒱᒶᒼirc;䅴dge;拀r;쀀𝔚pf;쀀𝕎cr;쀀𝒲Ȁfiosᓋᓐᓒᓘr;쀀𝔛;䎞pf;쀀𝕏cr;쀀𝒳ҀAIUacfosuᓱᓵᓹᓽᔄᔏᔔᔚᔠcy;䐯cy;䐇cy;䐮cute耻Ý䃝Āiyᔉᔍrc;䅶;䐫r;쀀𝔜pf;쀀𝕐cr;쀀𝒴ml;䅸ЀHacdefosᔵᔹᔿᕋᕏᕝᕠᕤcy;䐖cute;䅹Āayᕄᕉron;䅽;䐗ot;䅻Dzᕔ\0ᕛoWidtè૙a;䎖r;愨pf;愤cr;쀀𝒵௡ᖃᖊᖐ\0ᖰᖶᖿ\0\0\0\0ᗆᗛᗫᙟ᙭\0ᚕ᚛ᚲᚹ\0ᚾcute耻á䃡reve;䄃̀;Ediuyᖜᖝᖡᖣᖨᖭ戾;쀀∾̳;房rc耻â䃢te肻´̆;䐰lig耻æ䃦Ā;r²ᖺ;쀀𝔞rave耻à䃠ĀepᗊᗖĀfpᗏᗔsym;愵èᗓha;䎱ĀapᗟcĀclᗤᗧr;䄁g;樿ɤᗰ\0\0ᘊʀ;adsvᗺᗻᗿᘁᘇ戧nd;橕;橜lope;橘;橚΀;elmrszᘘᘙᘛᘞᘿᙏᙙ戠;榤e»ᘙsdĀ;aᘥᘦ戡ѡᘰᘲᘴᘶᘸᘺᘼᘾ;榨;榩;榪;榫;榬;榭;榮;榯tĀ;vᙅᙆ戟bĀ;dᙌᙍ抾;榝Āptᙔᙗh;戢»¹arr;捼Āgpᙣᙧon;䄅f;쀀𝕒΀;Eaeiop዁ᙻᙽᚂᚄᚇᚊ;橰cir;橯;扊d;手s;䀧roxĀ;e዁ᚒñᚃing耻å䃥ƀctyᚡᚦᚨr;쀀𝒶;䀪mpĀ;e዁ᚯñʈilde耻ã䃣ml耻ä䃤Āciᛂᛈoninôɲnt;樑ࠀNabcdefiklnoprsu᛭ᛱᜰ᜼ᝃᝈ᝸᝽០៦ᠹᡐᜍ᤽᥈ᥰot;櫭Ācrᛶ᜞kȀcepsᜀᜅᜍᜓong;扌psilon;䏶rime;怵imĀ;e᜚᜛戽q;拍Ŷᜢᜦee;抽edĀ;gᜬᜭ挅e»ᜭrkĀ;t፜᜷brk;掶Āoyᜁᝁ;䐱quo;怞ʀcmprtᝓ᝛ᝡᝤᝨausĀ;eĊĉptyv;榰séᜌnoõēƀahwᝯ᝱ᝳ;䎲;愶een;扬r;쀀𝔟g΀costuvwឍឝឳេ៕៛៞ƀaiuបពរðݠrc;旯p»፱ƀdptឤឨឭot;樀lus;樁imes;樂ɱឹ\0\0ើcup;樆ar;昅riangleĀdu៍្own;施p;斳plus;樄eåᑄåᒭarow;植ƀako៭ᠦᠵĀcn៲ᠣkƀlst៺֫᠂ozenge;槫riangleȀ;dlr᠒᠓᠘᠝斴own;斾eft;旂ight;斸k;搣Ʊᠫ\0ᠳƲᠯ\0ᠱ;斒;斑4;斓ck;斈ĀeoᠾᡍĀ;qᡃᡆ쀀=⃥uiv;쀀≡⃥t;挐Ȁptwxᡙᡞᡧᡬf;쀀𝕓Ā;tᏋᡣom»Ꮜtie;拈؀DHUVbdhmptuvᢅᢖᢪᢻᣗᣛᣬ᣿ᤅᤊᤐᤡȀLRlrᢎᢐᢒᢔ;敗;敔;敖;敓ʀ;DUduᢡᢢᢤᢦᢨ敐;敦;敩;敤;敧ȀLRlrᢳᢵᢷᢹ;敝;敚;敜;教΀;HLRhlrᣊᣋᣍᣏᣑᣓᣕ救;敬;散;敠;敫;敢;敟ox;槉ȀLRlrᣤᣦᣨᣪ;敕;敒;攐;攌ʀ;DUduڽ᣷᣹᣻᣽;敥;敨;攬;攴inus;抟lus;択imes;抠ȀLRlrᤙᤛᤝ᤟;敛;敘;攘;攔΀;HLRhlrᤰᤱᤳᤵᤷ᤻᤹攂;敪;敡;敞;攼;攤;攜Āevģ᥂bar耻¦䂦Ȁceioᥑᥖᥚᥠr;쀀𝒷mi;恏mĀ;e᜚᜜lƀ;bhᥨᥩᥫ䁜;槅sub;柈Ŭᥴ᥾lĀ;e᥹᥺怢t»᥺pƀ;Eeįᦅᦇ;檮Ā;qۜۛೡᦧ\0᧨ᨑᨕᨲ\0ᨷᩐ\0\0᪴\0\0᫁\0\0ᬡᬮ᭍᭒\0᯽\0ᰌƀcpr᦭ᦲ᧝ute;䄇̀;abcdsᦿᧀᧄ᧊᧕᧙戩nd;橄rcup;橉Āau᧏᧒p;橋p;橇ot;橀;쀀∩︀Āeo᧢᧥t;恁îړȀaeiu᧰᧻ᨁᨅǰ᧵\0᧸s;橍on;䄍dil耻ç䃧rc;䄉psĀ;sᨌᨍ橌m;橐ot;䄋ƀdmnᨛᨠᨦil肻¸ƭptyv;榲t脀¢;eᨭᨮ䂢räƲr;쀀𝔠ƀceiᨽᩀᩍy;䑇ckĀ;mᩇᩈ朓ark»ᩈ;䏇r΀;Ecefms᩟᩠ᩢᩫ᪤᪪᪮旋;槃ƀ;elᩩᩪᩭ䋆q;扗eɡᩴ\0\0᪈rrowĀlr᩼᪁eft;憺ight;憻ʀRSacd᪒᪔᪖᪚᪟»ཇ;擈st;抛irc;抚ash;抝nint;樐id;櫯cir;槂ubsĀ;u᪻᪼晣it»᪼ˬ᫇᫔᫺\0ᬊonĀ;eᫍᫎ䀺Ā;qÇÆɭ᫙\0\0᫢aĀ;t᫞᫟䀬;䁀ƀ;fl᫨᫩᫫戁îᅠeĀmx᫱᫶ent»᫩eóɍǧ᫾\0ᬇĀ;dኻᬂot;橭nôɆƀfryᬐᬔᬗ;쀀𝕔oäɔ脀©;sŕᬝr;愗Āaoᬥᬩrr;憵ss;朗Ācuᬲᬷr;쀀𝒸Ābpᬼ᭄Ā;eᭁᭂ櫏;櫑Ā;eᭉᭊ櫐;櫒dot;拯΀delprvw᭠᭬᭷ᮂᮬᯔ᯹arrĀlr᭨᭪;椸;椵ɰ᭲\0\0᭵r;拞c;拟arrĀ;p᭿ᮀ憶;椽̀;bcdosᮏᮐᮖᮡᮥᮨ截rcap;橈Āauᮛᮞp;橆p;橊ot;抍r;橅;쀀∪︀Ȁalrv᮵ᮿᯞᯣrrĀ;mᮼᮽ憷;椼yƀevwᯇᯔᯘqɰᯎ\0\0ᯒreã᭳uã᭵ee;拎edge;拏en耻¤䂤earrowĀlrᯮ᯳eft»ᮀight»ᮽeäᯝĀciᰁᰇoninôǷnt;戱lcty;挭ঀAHabcdefhijlorstuwz᰸᰻᰿ᱝᱩᱵᲊᲞᲬᲷ᳻᳿ᴍᵻᶑᶫᶻ᷆᷍rò΁ar;楥Ȁglrs᱈ᱍ᱒᱔ger;怠eth;愸òᄳhĀ;vᱚᱛ怐»ऊūᱡᱧarow;椏aã̕Āayᱮᱳron;䄏;䐴ƀ;ao̲ᱼᲄĀgrʿᲁr;懊tseq;橷ƀglmᲑᲔᲘ耻°䂰ta;䎴ptyv;榱ĀirᲣᲨsht;楿;쀀𝔡arĀlrᲳᲵ»ࣜ»သʀaegsv᳂͸᳖᳜᳠mƀ;oș᳊᳔ndĀ;ș᳑uit;晦amma;䏝in;拲ƀ;io᳧᳨᳸䃷de脀÷;o᳧ᳰntimes;拇nø᳷cy;䑒cɯᴆ\0\0ᴊrn;挞op;挍ʀlptuwᴘᴝᴢᵉᵕlar;䀤f;쀀𝕕ʀ;emps̋ᴭᴷᴽᵂqĀ;d͒ᴳot;扑inus;戸lus;戔quare;抡blebarwedgåúnƀadhᄮᵝᵧownarrowóᲃarpoonĀlrᵲᵶefôᲴighôᲶŢᵿᶅkaro÷གɯᶊ\0\0ᶎrn;挟op;挌ƀcotᶘᶣᶦĀryᶝᶡ;쀀𝒹;䑕l;槶rok;䄑Ādrᶰᶴot;拱iĀ;fᶺ᠖斿Āah᷀᷃ròЩaòྦangle;榦Āci᷒ᷕy;䑟grarr;柿ऀDacdefglmnopqrstuxḁḉḙḸոḼṉṡṾấắẽỡἪἷὄ὎὚ĀDoḆᴴoôᲉĀcsḎḔute耻é䃩ter;橮ȀaioyḢḧḱḶron;䄛rĀ;cḭḮ扖耻ê䃪lon;払;䑍ot;䄗ĀDrṁṅot;扒;쀀𝔢ƀ;rsṐṑṗ檚ave耻è䃨Ā;dṜṝ檖ot;檘Ȁ;ilsṪṫṲṴ檙nters;揧;愓Ā;dṹṺ檕ot;檗ƀapsẅẉẗcr;䄓tyƀ;svẒẓẕ戅et»ẓpĀ1;ẝẤijạả;怄;怅怃ĀgsẪẬ;䅋p;怂ĀgpẴẸon;䄙f;쀀𝕖ƀalsỄỎỒrĀ;sỊị拕l;槣us;橱iƀ;lvỚớở䎵on»ớ;䏵ȀcsuvỪỳἋἣĀioữḱrc»Ḯɩỹ\0\0ỻíՈantĀglἂἆtr»ṝess»Ṻƀaeiἒ἖Ἒls;䀽st;扟vĀ;DȵἠD;橸parsl;槥ĀDaἯἳot;打rr;楱ƀcdiἾὁỸr;愯oô͒ĀahὉὋ;䎷耻ð䃰Āmrὓὗl耻ë䃫o;悬ƀcipὡὤὧl;䀡sôծĀeoὬὴctatioîՙnentialåչৡᾒ\0ᾞ\0ᾡᾧ\0\0ῆῌ\0ΐ\0ῦῪ \0 ⁚llingdotseñṄy;䑄male;晀ƀilrᾭᾳ῁lig;耀ffiɩᾹ\0\0᾽g;耀ffig;耀ffl;쀀𝔣lig;耀filig;쀀fjƀaltῙ῜ῡt;晭ig;耀flns;斱of;䆒ǰ΅\0ῳf;쀀𝕗ĀakֿῷĀ;vῼ´拔;櫙artint;樍Āao‌⁕Ācs‑⁒ႉ‸⁅⁈\0⁐β•‥‧‪‬\0‮耻½䂽;慓耻¼䂼;慕;慙;慛Ƴ‴\0‶;慔;慖ʴ‾⁁\0\0⁃耻¾䂾;慗;慜5;慘ƶ⁌\0⁎;慚;慝8;慞l;恄wn;挢cr;쀀𝒻ࢀEabcdefgijlnorstv₂₉₟₥₰₴⃰⃵⃺⃿℃ℒℸ̗ℾ⅒↞Ā;lٍ₇;檌ƀcmpₐₕ₝ute;䇵maĀ;dₜ᳚䎳;檆reve;䄟Āiy₪₮rc;䄝;䐳ot;䄡Ȁ;lqsؾق₽⃉ƀ;qsؾٌ⃄lanô٥Ȁ;cdl٥⃒⃥⃕c;檩otĀ;o⃜⃝檀Ā;l⃢⃣檂;檄Ā;e⃪⃭쀀⋛︀s;檔r;쀀𝔤Ā;gٳ؛mel;愷cy;䑓Ȁ;Eajٚℌℎℐ;檒;檥;檤ȀEaesℛℝ℩ℴ;扩pĀ;p℣ℤ檊rox»ℤĀ;q℮ℯ檈Ā;q℮ℛim;拧pf;쀀𝕘Āci⅃ⅆr;愊mƀ;el٫ⅎ⅐;檎;檐茀>;cdlqr׮ⅠⅪⅮⅳⅹĀciⅥⅧ;檧r;橺ot;拗Par;榕uest;橼ʀadelsↄⅪ←ٖ↛ǰ↉\0↎proø₞r;楸qĀlqؿ↖lesó₈ií٫Āen↣↭rtneqq;쀀≩︀Å↪ԀAabcefkosy⇄⇇⇱⇵⇺∘∝∯≨≽ròΠȀilmr⇐⇔⇗⇛rsðᒄf»․ilôکĀdr⇠⇤cy;䑊ƀ;cwࣴ⇫⇯ir;楈;憭ar;意irc;䄥ƀalr∁∎∓rtsĀ;u∉∊晥it»∊lip;怦con;抹r;쀀𝔥sĀew∣∩arow;椥arow;椦ʀamopr∺∾≃≞≣rr;懿tht;戻kĀlr≉≓eftarrow;憩ightarrow;憪f;쀀𝕙bar;怕ƀclt≯≴≸r;쀀𝒽asè⇴rok;䄧Ābp⊂⊇ull;恃hen»ᱛૡ⊣\0⊪\0⊸⋅⋎\0⋕⋳\0\0⋸⌢⍧⍢⍿\0⎆⎪⎴cute耻í䃭ƀ;iyݱ⊰⊵rc耻î䃮;䐸Ācx⊼⊿y;䐵cl耻¡䂡ĀfrΟ⋉;쀀𝔦rave耻ì䃬Ȁ;inoܾ⋝⋩⋮Āin⋢⋦nt;樌t;戭fin;槜ta;愩lig;䄳ƀaop⋾⌚⌝ƀcgt⌅⌈⌗r;䄫ƀelpܟ⌏⌓inåގarôܠh;䄱f;抷ed;䆵ʀ;cfotӴ⌬⌱⌽⍁are;愅inĀ;t⌸⌹戞ie;槝doô⌙ʀ;celpݗ⍌⍐⍛⍡al;抺Āgr⍕⍙eróᕣã⍍arhk;樗rod;樼Ȁcgpt⍯⍲⍶⍻y;䑑on;䄯f;쀀𝕚a;䎹uest耻¿䂿Āci⎊⎏r;쀀𝒾nʀ;EdsvӴ⎛⎝⎡ӳ;拹ot;拵Ā;v⎦⎧拴;拳Ā;iݷ⎮lde;䄩ǫ⎸\0⎼cy;䑖l耻ï䃯̀cfmosu⏌⏗⏜⏡⏧⏵Āiy⏑⏕rc;䄵;䐹r;쀀𝔧ath;䈷pf;쀀𝕛ǣ⏬\0⏱r;쀀𝒿rcy;䑘kcy;䑔Ѐacfghjos␋␖␢␧␭␱␵␻ppaĀ;v␓␔䎺;䏰Āey␛␠dil;䄷;䐺r;쀀𝔨reen;䄸cy;䑅cy;䑜pf;쀀𝕜cr;쀀𝓀஀ABEHabcdefghjlmnoprstuv⑰⒁⒆⒍⒑┎┽╚▀♎♞♥♹♽⚚⚲⛘❝❨➋⟀⠁⠒ƀart⑷⑺⑼rò৆òΕail;椛arr;椎Ā;gঔ⒋;檋ar;楢ॣ⒥\0⒪\0⒱\0\0\0\0\0⒵Ⓔ\0ⓆⓈⓍ\0⓹ute;䄺mptyv;榴raîࡌbda;䎻gƀ;dlࢎⓁⓃ;榑åࢎ;檅uo耻«䂫rЀ;bfhlpst࢙ⓞⓦⓩ⓫⓮⓱⓵Ā;f࢝ⓣs;椟s;椝ë≒p;憫l;椹im;楳l;憢ƀ;ae⓿─┄檫il;椙Ā;s┉┊檭;쀀⪭︀ƀabr┕┙┝rr;椌rk;杲Āak┢┬cĀek┨┪;䁻;䁛Āes┱┳;榋lĀdu┹┻;榏;榍Ȁaeuy╆╋╖╘ron;䄾Ādi═╔il;䄼ìࢰâ┩;䐻Ȁcqrs╣╦╭╽a;椶uoĀ;rนᝆĀdu╲╷har;楧shar;楋h;憲ʀ;fgqs▋▌উ◳◿扤tʀahlrt▘▤▷◂◨rrowĀ;t࢙□aé⓶arpoonĀdu▯▴own»њp»०eftarrows;懇ightƀahs◍◖◞rrowĀ;sࣴࢧarpoonó྘quigarro÷⇰hreetimes;拋ƀ;qs▋ও◺lanôবʀ;cdgsব☊☍☝☨c;檨otĀ;o☔☕橿Ā;r☚☛檁;檃Ā;e☢☥쀀⋚︀s;檓ʀadegs☳☹☽♉♋pproøⓆot;拖qĀgq♃♅ôউgtò⒌ôছiíলƀilr♕࣡♚sht;楼;쀀𝔩Ā;Eজ♣;檑š♩♶rĀdu▲♮Ā;l॥♳;楪lk;斄cy;䑙ʀ;achtੈ⚈⚋⚑⚖rò◁orneòᴈard;楫ri;旺Āio⚟⚤dot;䅀ustĀ;a⚬⚭掰che»⚭ȀEaes⚻⚽⛉⛔;扨pĀ;p⛃⛄檉rox»⛄Ā;q⛎⛏檇Ā;q⛎⚻im;拦Ѐabnoptwz⛩⛴⛷✚✯❁❇❐Ānr⛮⛱g;柬r;懽rëࣁgƀlmr⛿✍✔eftĀar০✇ightá৲apsto;柼ightá৽parrowĀlr✥✩efô⓭ight;憬ƀafl✶✹✽r;榅;쀀𝕝us;樭imes;樴š❋❏st;戗áፎƀ;ef❗❘᠀旊nge»❘arĀ;l❤❥䀨t;榓ʀachmt❳❶❼➅➇ròࢨorneòᶌarĀ;d྘➃;業;怎ri;抿̀achiqt➘➝ੀ➢➮➻quo;怹r;쀀𝓁mƀ;egল➪➬;檍;檏Ābu┪➳oĀ;rฟ➹;怚rok;䅂萀<;cdhilqrࠫ⟒☹⟜⟠⟥⟪⟰Āci⟗⟙;檦r;橹reå◲mes;拉arr;楶uest;橻ĀPi⟵⟹ar;榖ƀ;ef⠀भ᠛旃rĀdu⠇⠍shar;楊har;楦Āen⠗⠡rtneqq;쀀≨︀Å⠞܀Dacdefhilnopsu⡀⡅⢂⢎⢓⢠⢥⢨⣚⣢⣤ઃ⣳⤂Dot;戺Ȁclpr⡎⡒⡣⡽r耻¯䂯Āet⡗⡙;時Ā;e⡞⡟朠se»⡟Ā;sျ⡨toȀ;dluျ⡳⡷⡻owîҌefôएðᏑker;斮Āoy⢇⢌mma;権;䐼ash;怔asuredangle»ᘦr;쀀𝔪o;愧ƀcdn⢯⢴⣉ro耻µ䂵Ȁ;acdᑤ⢽⣀⣄sôᚧir;櫰ot肻·Ƶusƀ;bd⣒ᤃ⣓戒Ā;uᴼ⣘;横ţ⣞⣡p;櫛ò−ðઁĀdp⣩⣮els;抧f;쀀𝕞Āct⣸⣽r;쀀𝓂pos»ᖝƀ;lm⤉⤊⤍䎼timap;抸ఀGLRVabcdefghijlmoprstuvw⥂⥓⥾⦉⦘⧚⧩⨕⨚⩘⩝⪃⪕⪤⪨⬄⬇⭄⭿⮮ⰴⱧⱼ⳩Āgt⥇⥋;쀀⋙̸Ā;v⥐௏쀀≫⃒ƀelt⥚⥲⥶ftĀar⥡⥧rrow;懍ightarrow;懎;쀀⋘̸Ā;v⥻ే쀀≪⃒ightarrow;懏ĀDd⦎⦓ash;抯ash;抮ʀbcnpt⦣⦧⦬⦱⧌la»˞ute;䅄g;쀀∠⃒ʀ;Eiop඄⦼⧀⧅⧈;쀀⩰̸d;쀀≋̸s;䅉roø඄urĀ;a⧓⧔普lĀ;s⧓ସdz⧟\0⧣p肻 ଷmpĀ;e௹ఀʀaeouy⧴⧾⨃⨐⨓ǰ⧹\0⧻;橃on;䅈dil;䅆ngĀ;dൾ⨊ot;쀀⩭̸p;橂;䐽ash;怓΀;Aadqsxஒ⨩⨭⨻⩁⩅⩐rr;懗rĀhr⨳⨶k;椤Ā;oᏲᏰot;쀀≐̸uiöୣĀei⩊⩎ar;椨í஘istĀ;s஠டr;쀀𝔫ȀEest௅⩦⩹⩼ƀ;qs஼⩭௡ƀ;qs஼௅⩴lanô௢ií௪Ā;rஶ⪁»ஷƀAap⪊⪍⪑rò⥱rr;憮ar;櫲ƀ;svྍ⪜ྌĀ;d⪡⪢拼;拺cy;䑚΀AEadest⪷⪺⪾⫂⫅⫶⫹rò⥦;쀀≦̸rr;憚r;急Ȁ;fqs఻⫎⫣⫯tĀar⫔⫙rro÷⫁ightarro÷⪐ƀ;qs఻⪺⫪lanôౕĀ;sౕ⫴»శiíౝĀ;rవ⫾iĀ;eచథiäඐĀpt⬌⬑f;쀀𝕟膀¬;in⬙⬚⬶䂬nȀ;Edvஉ⬤⬨⬮;쀀⋹̸ot;쀀⋵̸ǡஉ⬳⬵;拷;拶iĀ;vಸ⬼ǡಸ⭁⭃;拾;拽ƀaor⭋⭣⭩rȀ;ast୻⭕⭚⭟lleì୻l;쀀⫽⃥;쀀∂̸lint;樔ƀ;ceಒ⭰⭳uåಥĀ;cಘ⭸Ā;eಒ⭽ñಘȀAait⮈⮋⮝⮧rò⦈rrƀ;cw⮔⮕⮙憛;쀀⤳̸;쀀↝̸ghtarrow»⮕riĀ;eೋೖ΀chimpqu⮽⯍⯙⬄୸⯤⯯Ȁ;cerല⯆ഷ⯉uå൅;쀀𝓃ortɭ⬅\0\0⯖ará⭖mĀ;e൮⯟Ā;q൴൳suĀbp⯫⯭å೸åഋƀbcp⯶ⰑⰙȀ;Ees⯿ⰀഢⰄ抄;쀀⫅̸etĀ;eഛⰋqĀ;qണⰀcĀ;eലⰗñസȀ;EesⰢⰣൟⰧ抅;쀀⫆̸etĀ;e൘ⰮqĀ;qൠⰣȀgilrⰽⰿⱅⱇìௗlde耻ñ䃱çృiangleĀlrⱒⱜeftĀ;eచⱚñదightĀ;eೋⱥñ೗Ā;mⱬⱭ䎽ƀ;esⱴⱵⱹ䀣ro;愖p;怇ҀDHadgilrsⲏⲔⲙⲞⲣⲰⲶⳓⳣash;抭arr;椄p;쀀≍⃒ash;抬ĀetⲨⲬ;쀀≥⃒;쀀>⃒nfin;槞ƀAetⲽⳁⳅrr;椂;쀀≤⃒Ā;rⳊⳍ쀀<⃒ie;쀀⊴⃒ĀAtⳘⳜrr;椃rie;쀀⊵⃒im;쀀∼⃒ƀAan⳰⳴ⴂrr;懖rĀhr⳺⳽k;椣Ā;oᏧᏥear;椧ቓ᪕\0\0\0\0\0\0\0\0\0\0\0\0\0ⴭ\0ⴸⵈⵠⵥ⵲ⶄᬇ\0\0ⶍⶫ\0ⷈⷎ\0ⷜ⸙⸫⸾⹃Ācsⴱ᪗ute耻ó䃳ĀiyⴼⵅrĀ;c᪞ⵂ耻ô䃴;䐾ʀabios᪠ⵒⵗLjⵚlac;䅑v;樸old;榼lig;䅓Ācr⵩⵭ir;榿;쀀𝔬ͯ⵹\0\0⵼\0ⶂn;䋛ave耻ò䃲;槁Ābmⶈ෴ar;榵Ȁacitⶕ⶘ⶥⶨrò᪀Āir⶝ⶠr;榾oss;榻nå๒;槀ƀaeiⶱⶵⶹcr;䅍ga;䏉ƀcdnⷀⷅǍron;䎿;榶pf;쀀𝕠ƀaelⷔ⷗ǒr;榷rp;榹΀;adiosvⷪⷫⷮ⸈⸍⸐⸖戨rò᪆Ȁ;efmⷷⷸ⸂⸅橝rĀ;oⷾⷿ愴f»ⷿ耻ª䂪耻º䂺gof;抶r;橖lope;橗;橛ƀclo⸟⸡⸧ò⸁ash耻ø䃸l;折iŬⸯ⸴de耻õ䃵esĀ;aǛ⸺s;樶ml耻ö䃶bar;挽ૡ⹞\0⹽\0⺀⺝\0⺢⺹\0\0⻋ຜ\0⼓\0\0⼫⾼\0⿈rȀ;astЃ⹧⹲຅脀¶;l⹭⹮䂶leìЃɩ⹸\0\0⹻m;櫳;櫽y;䐿rʀcimpt⺋⺏⺓ᡥ⺗nt;䀥od;䀮il;怰enk;怱r;쀀𝔭ƀimo⺨⺰⺴Ā;v⺭⺮䏆;䏕maô੶ne;明ƀ;tv⺿⻀⻈䏀chfork»´;䏖Āau⻏⻟nĀck⻕⻝kĀ;h⇴⻛;愎ö⇴sҀ;abcdemst⻳⻴ᤈ⻹⻽⼄⼆⼊⼎䀫cir;樣ir;樢Āouᵀ⼂;樥;橲n肻±ຝim;樦wo;樧ƀipu⼙⼠⼥ntint;樕f;쀀𝕡nd耻£䂣Ԁ;Eaceinosu່⼿⽁⽄⽇⾁⾉⾒⽾⾶;檳p;檷uå໙Ā;c໎⽌̀;acens່⽙⽟⽦⽨⽾pproø⽃urlyeñ໙ñ໎ƀaes⽯⽶⽺pprox;檹qq;檵im;拨iíໟmeĀ;s⾈ຮ怲ƀEas⽸⾐⽺ð⽵ƀdfp໬⾙⾯ƀals⾠⾥⾪lar;挮ine;挒urf;挓Ā;t໻⾴ï໻rel;抰Āci⿀⿅r;쀀𝓅;䏈ncsp;怈̀fiopsu⿚⋢⿟⿥⿫⿱r;쀀𝔮pf;쀀𝕢rime;恗cr;쀀𝓆ƀaeo⿸〉〓tĀei⿾々rnionóڰnt;樖stĀ;e【】䀿ñἙô༔઀ABHabcdefhilmnoprstux぀けさすムㄎㄫㅇㅢㅲㆎ㈆㈕㈤㈩㉘㉮㉲㊐㊰㊷ƀartぇおがròႳòϝail;検aròᱥar;楤΀cdenqrtとふへみわゔヌĀeuねぱ;쀀∽̱te;䅕iãᅮmptyv;榳gȀ;del࿑らるろ;榒;榥å࿑uo耻»䂻rր;abcfhlpstw࿜ガクシスゼゾダッデナp;極Ā;f࿠ゴs;椠;椳s;椞ë≝ð✮l;楅im;楴l;憣;憝Āaiパフil;椚oĀ;nホボ戶aló༞ƀabrョリヮrò៥rk;杳ĀakンヽcĀekヹ・;䁽;䁝Āes㄂㄄;榌lĀduㄊㄌ;榎;榐Ȁaeuyㄗㄜㄧㄩron;䅙Ādiㄡㄥil;䅗ì࿲âヺ;䑀Ȁclqsㄴㄷㄽㅄa;椷dhar;楩uoĀ;rȎȍh;憳ƀacgㅎㅟངlȀ;ipsླྀㅘㅛႜnåႻarôྩt;断ƀilrㅩဣㅮsht;楽;쀀𝔯ĀaoㅷㆆrĀduㅽㅿ»ѻĀ;l႑ㆄ;楬Ā;vㆋㆌ䏁;䏱ƀgns㆕ㇹㇼht̀ahlrstㆤㆰ㇂㇘㇤㇮rrowĀ;t࿜ㆭaéトarpoonĀduㆻㆿowîㅾp»႒eftĀah㇊㇐rrowó࿪arpoonóՑightarrows;應quigarro÷ニhreetimes;拌g;䋚ingdotseñἲƀahm㈍㈐㈓rò࿪aòՑ;怏oustĀ;a㈞㈟掱che»㈟mid;櫮Ȁabpt㈲㈽㉀㉒Ānr㈷㈺g;柭r;懾rëဃƀafl㉇㉊㉎r;榆;쀀𝕣us;樮imes;樵Āap㉝㉧rĀ;g㉣㉤䀩t;榔olint;樒arò㇣Ȁachq㉻㊀Ⴜ㊅quo;怺r;쀀𝓇Ābu・㊊oĀ;rȔȓƀhir㊗㊛㊠reåㇸmes;拊iȀ;efl㊪ၙᠡ㊫方tri;槎luhar;楨;愞ൡ㋕㋛㋟㌬㌸㍱\0㍺㎤\0\0㏬㏰\0㐨㑈㑚㒭㒱㓊㓱\0㘖\0\0㘳cute;䅛quï➺Ԁ;Eaceinpsyᇭ㋳㋵㋿㌂㌋㌏㌟㌦㌩;檴ǰ㋺\0㋼;檸on;䅡uåᇾĀ;dᇳ㌇il;䅟rc;䅝ƀEas㌖㌘㌛;檶p;檺im;择olint;樓iíሄ;䑁otƀ;be㌴ᵇ㌵担;橦΀Aacmstx㍆㍊㍗㍛㍞㍣㍭rr;懘rĀhr㍐㍒ë∨Ā;oਸ਼਴t耻§䂧i;䀻war;椩mĀin㍩ðnuóñt;朶rĀ;o㍶⁕쀀𝔰Ȁacoy㎂㎆㎑㎠rp;景Āhy㎋㎏cy;䑉;䑈rtɭ㎙\0\0㎜iäᑤaraì⹯耻­䂭Āgm㎨㎴maƀ;fv㎱㎲㎲䏃;䏂Ѐ;deglnprካ㏅㏉㏎㏖㏞㏡㏦ot;橪Ā;q኱ኰĀ;E㏓㏔檞;檠Ā;E㏛㏜檝;檟e;扆lus;樤arr;楲aròᄽȀaeit㏸㐈㐏㐗Āls㏽㐄lsetmé㍪hp;樳parsl;槤Ādlᑣ㐔e;挣Ā;e㐜㐝檪Ā;s㐢㐣檬;쀀⪬︀ƀflp㐮㐳㑂tcy;䑌Ā;b㐸㐹䀯Ā;a㐾㐿槄r;挿f;쀀𝕤aĀdr㑍ЂesĀ;u㑔㑕晠it»㑕ƀcsu㑠㑹㒟Āau㑥㑯pĀ;sᆈ㑫;쀀⊓︀pĀ;sᆴ㑵;쀀⊔︀uĀbp㑿㒏ƀ;esᆗᆜ㒆etĀ;eᆗ㒍ñᆝƀ;esᆨᆭ㒖etĀ;eᆨ㒝ñᆮƀ;afᅻ㒦ְrť㒫ֱ»ᅼaròᅈȀcemt㒹㒾㓂㓅r;쀀𝓈tmîñiì㐕aræᆾĀar㓎㓕rĀ;f㓔ឿ昆Āan㓚㓭ightĀep㓣㓪psiloîỠhé⺯s»⡒ʀbcmnp㓻㕞ሉ㖋㖎Ҁ;Edemnprs㔎㔏㔑㔕㔞㔣㔬㔱㔶抂;櫅ot;檽Ā;dᇚ㔚ot;櫃ult;櫁ĀEe㔨㔪;櫋;把lus;檿arr;楹ƀeiu㔽㕒㕕tƀ;en㔎㕅㕋qĀ;qᇚ㔏eqĀ;q㔫㔨m;櫇Ābp㕚㕜;櫕;櫓c̀;acensᇭ㕬㕲㕹㕻㌦pproø㋺urlyeñᇾñᇳƀaes㖂㖈㌛pproø㌚qñ㌗g;晪ڀ123;Edehlmnps㖩㖬㖯ሜ㖲㖴㗀㗉㗕㗚㗟㗨㗭耻¹䂹耻²䂲耻³䂳;櫆Āos㖹㖼t;檾ub;櫘Ā;dሢ㗅ot;櫄sĀou㗏㗒l;柉b;櫗arr;楻ult;櫂ĀEe㗤㗦;櫌;抋lus;櫀ƀeiu㗴㘉㘌tƀ;enሜ㗼㘂qĀ;qሢ㖲eqĀ;q㗧㗤m;櫈Ābp㘑㘓;櫔;櫖ƀAan㘜㘠㘭rr;懙rĀhr㘦㘨ë∮Ā;oਫ਩war;椪lig耻ß䃟௡㙑㙝㙠ዎ㙳㙹\0㙾㛂\0\0\0\0\0㛛㜃\0㜉㝬\0\0\0㞇ɲ㙖\0\0㙛get;挖;䏄rë๟ƀaey㙦㙫㙰ron;䅥dil;䅣;䑂lrec;挕r;쀀𝔱Ȁeiko㚆㚝㚵㚼Dz㚋\0㚑eĀ4fኄኁaƀ;sv㚘㚙㚛䎸ym;䏑Ācn㚢㚲kĀas㚨㚮pproø዁im»ኬsðኞĀas㚺㚮ð዁rn耻þ䃾Ǭ̟㛆⋧es膀×;bd㛏㛐㛘䃗Ā;aᤏ㛕r;樱;樰ƀeps㛡㛣㜀á⩍Ȁ;bcf҆㛬㛰㛴ot;挶ir;櫱Ā;o㛹㛼쀀𝕥rk;櫚á㍢rime;怴ƀaip㜏㜒㝤dåቈ΀adempst㜡㝍㝀㝑㝗㝜㝟ngleʀ;dlqr㜰㜱㜶㝀㝂斵own»ᶻeftĀ;e⠀㜾ñम;扜ightĀ;e㊪㝋ñၚot;旬inus;樺lus;樹b;槍ime;樻ezium;揢ƀcht㝲㝽㞁Āry㝷㝻;쀀𝓉;䑆cy;䑛rok;䅧Āio㞋㞎xô᝷headĀlr㞗㞠eftarro÷ࡏightarrow»ཝऀAHabcdfghlmoprstuw㟐㟓㟗㟤㟰㟼㠎㠜㠣㠴㡑㡝㡫㢩㣌㣒㣪㣶ròϭar;楣Ācr㟜㟢ute耻ú䃺òᅐrǣ㟪\0㟭y;䑞ve;䅭Āiy㟵㟺rc耻û䃻;䑃ƀabh㠃㠆㠋ròᎭlac;䅱aòᏃĀir㠓㠘sht;楾;쀀𝔲rave耻ù䃹š㠧㠱rĀlr㠬㠮»ॗ»ႃlk;斀Āct㠹㡍ɯ㠿\0\0㡊rnĀ;e㡅㡆挜r»㡆op;挏ri;旸Āal㡖㡚cr;䅫肻¨͉Āgp㡢㡦on;䅳f;쀀𝕦̀adhlsuᅋ㡸㡽፲㢑㢠ownáᎳarpoonĀlr㢈㢌efô㠭ighô㠯iƀ;hl㢙㢚㢜䏅»ᏺon»㢚parrows;懈ƀcit㢰㣄㣈ɯ㢶\0\0㣁rnĀ;e㢼㢽挝r»㢽op;挎ng;䅯ri;旹cr;쀀𝓊ƀdir㣙㣝㣢ot;拰lde;䅩iĀ;f㜰㣨»᠓Āam㣯㣲rò㢨l耻ü䃼angle;榧ހABDacdeflnoprsz㤜㤟㤩㤭㦵㦸㦽㧟㧤㧨㧳㧹㧽㨁㨠ròϷarĀ;v㤦㤧櫨;櫩asèϡĀnr㤲㤷grt;榜΀eknprst㓣㥆㥋㥒㥝㥤㦖appá␕othinçẖƀhir㓫⻈㥙opô⾵Ā;hᎷ㥢ïㆍĀiu㥩㥭gmá㎳Ābp㥲㦄setneqĀ;q㥽㦀쀀⊊︀;쀀⫋︀setneqĀ;q㦏㦒쀀⊋︀;쀀⫌︀Āhr㦛㦟etá㚜iangleĀlr㦪㦯eft»थight»ၑy;䐲ash»ံƀelr㧄㧒㧗ƀ;beⷪ㧋㧏ar;抻q;扚lip;拮Ābt㧜ᑨaòᑩr;쀀𝔳tré㦮suĀbp㧯㧱»ജ»൙pf;쀀𝕧roð໻tré㦴Ācu㨆㨋r;쀀𝓋Ābp㨐㨘nĀEe㦀㨖»㥾nĀEe㦒㨞»㦐igzag;榚΀cefoprs㨶㨻㩖㩛㩔㩡㩪irc;䅵Ādi㩀㩑Ābg㩅㩉ar;機eĀ;qᗺ㩏;扙erp;愘r;쀀𝔴pf;쀀𝕨Ā;eᑹ㩦atèᑹcr;쀀𝓌ૣណ㪇\0㪋\0㪐㪛\0\0㪝㪨㪫㪯\0\0㫃㫎\0㫘ៜ៟tré៑r;쀀𝔵ĀAa㪔㪗ròσrò৶;䎾ĀAa㪡㪤ròθrò৫að✓is;拻ƀdptឤ㪵㪾Āfl㪺ឩ;쀀𝕩imåឲĀAa㫇㫊ròώròਁĀcq㫒ីr;쀀𝓍Āpt៖㫜ré។Ѐacefiosu㫰㫽㬈㬌㬑㬕㬛㬡cĀuy㫶㫻te耻ý䃽;䑏Āiy㬂㬆rc;䅷;䑋n耻¥䂥r;쀀𝔶cy;䑗pf;쀀𝕪cr;쀀𝓎Ācm㬦㬩y;䑎l耻ÿ䃿Ԁacdefhiosw㭂㭈㭔㭘㭤㭩㭭㭴㭺㮀cute;䅺Āay㭍㭒ron;䅾;䐷ot;䅼Āet㭝㭡træᕟa;䎶r;쀀𝔷cy;䐶grarr;懝pf;쀀𝕫cr;쀀𝓏Ājn㮅㮇;怍j;怌'.split("").map(e=>e.charCodeAt(0))),fs=new Uint16Array("Ȁaglq\tɭ\0\0p;䀦os;䀧t;䀾t;䀼uot;䀢".split("").map(e=>e.charCodeAt(0)));const ms=new Map([[0,65533],[128,8364],[130,8218],[131,402],[132,8222],[133,8230],[134,8224],[135,8225],[136,710],[137,8240],[138,352],[139,8249],[140,338],[142,381],[145,8216],[146,8217],[147,8220],[148,8221],[149,8226],[150,8211],[151,8212],[152,732],[153,8482],[154,353],[155,8250],[156,339],[158,382],[159,376]]),gs=null!==(is=String.fromCodePoint)&&void 0!==is?is:function(e){let t="";return e>65535&&(e-=65536,t+=String.fromCharCode(e>>>10&1023|55296),e=56320|1023&e),t+=String.fromCharCode(e),t};var ys;!function(e){e[e.NUM=35]="NUM",e[e.SEMI=59]="SEMI",e[e.EQUALS=61]="EQUALS",e[e.ZERO=48]="ZERO",e[e.NINE=57]="NINE",e[e.LOWER_A=97]="LOWER_A",e[e.LOWER_F=102]="LOWER_F",e[e.LOWER_X=120]="LOWER_X",e[e.LOWER_Z=122]="LOWER_Z",e[e.UPPER_A=65]="UPPER_A",e[e.UPPER_F=70]="UPPER_F",e[e.UPPER_Z=90]="UPPER_Z"}(ys||(ys={}));var ks,bs,ws;function Cs(e){return e>=ys.ZERO&&e<=ys.NINE}function xs(e){return e>=ys.UPPER_A&&e<=ys.UPPER_F||e>=ys.LOWER_A&&e<=ys.LOWER_F}function Ds(e){return e===ys.EQUALS||function(e){return e>=ys.UPPER_A&&e<=ys.UPPER_Z||e>=ys.LOWER_A&&e<=ys.LOWER_Z||Cs(e)}(e)}!function(e){e[e.VALUE_LENGTH=49152]="VALUE_LENGTH",e[e.BRANCH_LENGTH=16256]="BRANCH_LENGTH",e[e.JUMP_TABLE=127]="JUMP_TABLE"}(ks||(ks={})),function(e){e[e.EntityStart=0]="EntityStart",e[e.NumericStart=1]="NumericStart",e[e.NumericDecimal=2]="NumericDecimal",e[e.NumericHex=3]="NumericHex",e[e.NamedEntity=4]="NamedEntity"}(bs||(bs={})),function(e){e[e.Legacy=0]="Legacy",e[e.Strict=1]="Strict",e[e.Attribute=2]="Attribute"}(ws||(ws={}));class vs{constructor(e,t,n){this.decodeTree=e,this.emitCodePoint=t,this.errors=n,this.state=bs.EntityStart,this.consumed=1,this.result=0,this.treeIndex=0,this.excess=1,this.decodeMode=ws.Strict}startEntity(e){this.decodeMode=e,this.state=bs.EntityStart,this.result=0,this.treeIndex=0,this.excess=1,this.consumed=1}write(e,t){switch(this.state){case bs.EntityStart:return e.charCodeAt(t)===ys.NUM?(this.state=bs.NumericStart,this.consumed+=1,this.stateNumericStart(e,t+1)):(this.state=bs.NamedEntity,this.stateNamedEntity(e,t));case bs.NumericStart:return this.stateNumericStart(e,t);case bs.NumericDecimal:return this.stateNumericDecimal(e,t);case bs.NumericHex:return this.stateNumericHex(e,t);case bs.NamedEntity:return this.stateNamedEntity(e,t)}}stateNumericStart(e,t){return t>=e.length?-1:(32|e.charCodeAt(t))===ys.LOWER_X?(this.state=bs.NumericHex,this.consumed+=1,this.stateNumericHex(e,t+1)):(this.state=bs.NumericDecimal,this.stateNumericDecimal(e,t))}addToNumericResult(e,t,n,r){if(t!==n){const i=n-t;this.result=this.result*Math.pow(r,i)+parseInt(e.substr(t,i),r),this.consumed+=i}}stateNumericHex(e,t){const n=t;for(;t<e.length;){const r=e.charCodeAt(t);if(!Cs(r)&&!xs(r))return this.addToNumericResult(e,n,t,16),this.emitNumericEntity(r,3);t+=1}return this.addToNumericResult(e,n,t,16),-1}stateNumericDecimal(e,t){const n=t;for(;t<e.length;){const r=e.charCodeAt(t);if(!Cs(r))return this.addToNumericResult(e,n,t,10),this.emitNumericEntity(r,2);t+=1}return this.addToNumericResult(e,n,t,10),-1}emitNumericEntity(e,t){var n;if(this.consumed<=t)return null===(n=this.errors)||void 0===n||n.absenceOfDigitsInNumericCharacterReference(this.consumed),0;if(e===ys.SEMI)this.consumed+=1;else if(this.decodeMode===ws.Strict)return 0;return this.emitCodePoint(function(e){var t;return e>=55296&&e<=57343||e>1114111?65533:null!==(t=ms.get(e))&&void 0!==t?t:e}(this.result),this.consumed),this.errors&&(e!==ys.SEMI&&this.errors.missingSemicolonAfterCharacterReference(),this.errors.validateNumericCharacterReference(this.result)),this.consumed}stateNamedEntity(e,t){const{decodeTree:n}=this;let r=n[this.treeIndex],i=(r&ks.VALUE_LENGTH)>>14;for(;t<e.length;t++,this.excess++){const o=e.charCodeAt(t);if(this.treeIndex=Ss(n,r,this.treeIndex+Math.max(1,i),o),this.treeIndex<0)return 0===this.result||this.decodeMode===ws.Attribute&&(0===i||Ds(o))?0:this.emitNotTerminatedNamedEntity();if(r=n[this.treeIndex],i=(r&ks.VALUE_LENGTH)>>14,0!==i){if(o===ys.SEMI)return this.emitNamedEntityData(this.treeIndex,i,this.consumed+this.excess);this.decodeMode!==ws.Strict&&(this.result=this.treeIndex,this.consumed+=this.excess,this.excess=0)}}return-1}emitNotTerminatedNamedEntity(){var e;const{result:t,decodeTree:n}=this,r=(n[t]&ks.VALUE_LENGTH)>>14;return this.emitNamedEntityData(t,r,this.consumed),null===(e=this.errors)||void 0===e||e.missingSemicolonAfterCharacterReference(),this.consumed}emitNamedEntityData(e,t,n){const{decodeTree:r}=this;return this.emitCodePoint(1===t?r[e]&~ks.VALUE_LENGTH:r[e+1],n),3===t&&this.emitCodePoint(r[e+2],n),n}end(){var e;switch(this.state){case bs.NamedEntity:return 0===this.result||this.decodeMode===ws.Attribute&&this.result!==this.treeIndex?0:this.emitNotTerminatedNamedEntity();case bs.NumericDecimal:return this.emitNumericEntity(0,2);case bs.NumericHex:return this.emitNumericEntity(0,3);case bs.NumericStart:return null===(e=this.errors)||void 0===e||e.absenceOfDigitsInNumericCharacterReference(this.consumed),0;case bs.EntityStart:return 0}}}function _s(e){let t="";const n=new vs(e,e=>t+=gs(e));return function(e,r){let i=0,o=0;for(;(o=e.indexOf("&",o))>=0;){t+=e.slice(i,o),n.startEntity(r);const s=n.write(e,o+1);if(s<0){i=o+n.end();break}i=o+s,o=0===s?i+1:i}const s=t+e.slice(i);return t="",s}}function Ss(e,t,n,r){const i=(t&ks.BRANCH_LENGTH)>>7,o=t&ks.JUMP_TABLE;if(0===i)return 0!==o&&r===o?n:-1;if(o){const t=r-o;return t<0||t>=i?-1:e[n+t]-1}let s=n,l=s+i-1;for(;s<=l;){const t=s+l>>>1,n=e[t];if(n<r)s=t+1;else{if(!(n>r))return e[t+i];l=t-1}}return-1}const Es=_s(ds);function As(e,t=ws.Legacy){return Es(e,t)}function Ms(e){return"[object String]"===function(e){return Object.prototype.toString.call(e)}(e)}_s(fs);const Os=Object.prototype.hasOwnProperty;function Ns(e){return Array.prototype.slice.call(arguments,1).forEach(function(t){if(t){if("object"!=typeof t)throw new TypeError(t+"must be object");Object.keys(t).forEach(function(n){e[n]=t[n]})}}),e}function Fs(e,t,n){return[].concat(e.slice(0,t),n,e.slice(t+1))}function Ts(e){return!(e>=55296&&e<=57343)&&(!(e>=64976&&e<=65007)&&(!!(65535&~e&&65534!=(65535&e))&&(!(e>=0&&e<=8)&&(11!==e&&(!(e>=14&&e<=31)&&(!(e>=127&&e<=159)&&!(e>1114111)))))))}function Rs(e){if(e>65535){const t=55296+((e-=65536)>>10),n=56320+(1023&e);return String.fromCharCode(t,n)}return String.fromCharCode(e)}const zs=/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g,Is=new RegExp(zs.source+"|"+/&([a-z#][a-z0-9]{1,31});/gi.source,"gi"),Bs=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))$/i;function Ps(e){return e.indexOf("\\")<0&&e.indexOf("&")<0?e:e.replace(Is,function(e,t,n){return t||function(e,t){if(35===t.charCodeAt(0)&&Bs.test(t)){const n="x"===t[1].toLowerCase()?parseInt(t.slice(2),16):parseInt(t.slice(1),10);return Ts(n)?Rs(n):e}const n=As(e);return n!==e?n:e}(e,n)})}const Ls=/[&<>"]/,qs=/[&<>"]/g,$s={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"};function Vs(e){return $s[e]}function js(e){return Ls.test(e)?e.replace(qs,Vs):e}const Js=/[.?*+^$[\]\\(){}|-]/g;function Ws(e){switch(e){case 9:case 32:return!0}return!1}function Hs(e){if(e>=8192&&e<=8202)return!0;switch(e){case 9:case 10:case 11:case 12:case 13:case 32:case 160:case 5760:case 8239:case 8287:case 12288:return!0}return!1}function Us(e){return cs.test(e)||hs.test(e)}function Ks(e){switch(e){case 33:case 34:case 35:case 36:case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:case 47:case 58:case 59:case 60:case 61:case 62:case 63:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 124:case 125:case 126:return!0;default:return!1}}function Zs(e){return e=e.trim().replace(/\s+/g," "),"Ṿ"==="ẞ".toLowerCase()&&(e=e.replace(/ẞ/g,"ß")),e.toLowerCase().toUpperCase()}const Gs={mdurl:ss,ucmicro:ps};var Qs=Object.freeze({__proto__:null,arrayReplaceAt:Fs,assign:Ns,escapeHtml:js,escapeRE:function(e){return e.replace(Js,"\\$&")},fromCodePoint:Rs,has:function(e,t){return Os.call(e,t)},isMdAsciiPunct:Ks,isPunctChar:Us,isSpace:Ws,isString:Ms,isValidEntityCode:Ts,isWhiteSpace:Hs,lib:Gs,normalizeReference:Zs,unescapeAll:Ps,unescapeMd:function(e){return e.indexOf("\\")<0?e:e.replace(zs,"$1")}});var Xs=Object.freeze({__proto__:null,parseLinkDestination:function(e,t,n){let r,i=t;const o={ok:!1,pos:0,str:""};if(60===e.charCodeAt(i)){for(i++;i<n;){if(r=e.charCodeAt(i),10===r)return o;if(60===r)return o;if(62===r)return o.pos=i+1,o.str=Ps(e.slice(t+1,i)),o.ok=!0,o;92===r&&i+1<n?i+=2:i++}return o}let s=0;for(;i<n&&(r=e.charCodeAt(i),32!==r)&&!(r<32||127===r);)if(92===r&&i+1<n){if(32===e.charCodeAt(i+1))break;i+=2}else{if(40===r&&(s++,s>32))return o;if(41===r){if(0===s)break;s--}i++}return t===i||0!==s||(o.str=Ps(e.slice(t,i)),o.pos=i,o.ok=!0),o},parseLinkLabel:function(e,t,n){let r,i,o,s;const l=e.posMax,a=e.pos;for(e.pos=t+1,r=1;e.pos<l;){if(o=e.src.charCodeAt(e.pos),93===o&&(r--,0===r)){i=!0;break}if(s=e.pos,e.md.inline.skipToken(e),91===o)if(s===e.pos-1)r++;else if(n)return e.pos=a,-1}let c=-1;return i&&(c=e.pos),e.pos=a,c},parseLinkTitle:function(e,t,n,r){let i,o=t;const s={ok:!1,can_continue:!1,pos:0,str:"",marker:0};if(r)s.str=r.str,s.marker=r.marker;else{if(o>=n)return s;let r=e.charCodeAt(o);if(34!==r&&39!==r&&40!==r)return s;t++,o++,40===r&&(r=41),s.marker=r}for(;o<n;){if(i=e.charCodeAt(o),i===s.marker)return s.pos=o+1,s.str+=Ps(e.slice(t,o)),s.ok=!0,s;if(40===i&&41===s.marker)return s;92===i&&o+1<n&&o++,o++}return s.can_continue=!0,s.str+=Ps(e.slice(t,o)),s}});const Ys={};function el(){this.rules=Ns({},Ys)}function tl(){this.__rules__=[],this.__cache__=null}function nl(e,t,n){this.type=e,this.tag=t,this.attrs=null,this.map=null,this.nesting=n,this.level=0,this.children=null,this.content="",this.markup="",this.info="",this.meta=null,this.block=!1,this.hidden=!1}function rl(e,t,n){this.src=e,this.env=n,this.tokens=[],this.inlineMode=!1,this.md=t}Ys.code_inline=function(e,t,n,r,i){const o=e[t];return"<code"+i.renderAttrs(o)+">"+js(o.content)+"</code>"},Ys.code_block=function(e,t,n,r,i){const o=e[t];return"<pre"+i.renderAttrs(o)+"><code>"+js(e[t].content)+"</code></pre>\n"},Ys.fence=function(e,t,n,r,i){const o=e[t],s=o.info?Ps(o.info).trim():"";let l,a="",c="";if(s){const e=s.split(/(\s+)/g);a=e[0],c=e.slice(2).join("")}if(l=n.highlight&&n.highlight(o.content,a,c)||js(o.content),0===l.indexOf("<pre"))return l+"\n";if(s){const e=o.attrIndex("class"),t=o.attrs?o.attrs.slice():[];e<0?t.push(["class",n.langPrefix+a]):(t[e]=t[e].slice(),t[e][1]+=" "+n.langPrefix+a);const r={attrs:t};return`<pre><code${i.renderAttrs(r)}>${l}</code></pre>\n`}return`<pre><code${i.renderAttrs(o)}>${l}</code></pre>\n`},Ys.image=function(e,t,n,r,i){const o=e[t];return o.attrs[o.attrIndex("alt")][1]=i.renderInlineAsText(o.children,n,r),i.renderToken(e,t,n)},Ys.hardbreak=function(e,t,n){return n.xhtmlOut?"<br />\n":"<br>\n"},Ys.softbreak=function(e,t,n){return n.breaks?n.xhtmlOut?"<br />\n":"<br>\n":"\n"},Ys.text=function(e,t){return js(e[t].content)},Ys.html_block=function(e,t){return e[t].content},Ys.html_inline=function(e,t){return e[t].content},el.prototype.renderAttrs=function(e){let t,n,r;if(!e.attrs)return"";for(r="",t=0,n=e.attrs.length;t<n;t++)r+=" "+js(e.attrs[t][0])+'="'+js(e.attrs[t][1])+'"';return r},el.prototype.renderToken=function(e,t,n){const r=e[t];let i="";if(r.hidden)return"";r.block&&-1!==r.nesting&&t&&e[t-1].hidden&&(i+="\n"),i+=(-1===r.nesting?"</":"<")+r.tag,i+=this.renderAttrs(r),0===r.nesting&&n.xhtmlOut&&(i+=" /");let o=!1;if(r.block&&(o=!0,1===r.nesting&&t+1<e.length)){const n=e[t+1];("inline"===n.type||n.hidden||-1===n.nesting&&n.tag===r.tag)&&(o=!1)}return i+=o?">\n":">",i},el.prototype.renderInline=function(e,t,n){let r="";const i=this.rules;for(let o=0,s=e.length;o<s;o++){const s=e[o].type;void 0!==i[s]?r+=i[s](e,o,t,n,this):r+=this.renderToken(e,o,t)}return r},el.prototype.renderInlineAsText=function(e,t,n){let r="";for(let i=0,o=e.length;i<o;i++)switch(e[i].type){case"text":case"html_inline":case"html_block":r+=e[i].content;break;case"image":r+=this.renderInlineAsText(e[i].children,t,n);break;case"softbreak":case"hardbreak":r+="\n"}return r},el.prototype.render=function(e,t,n){let r="";const i=this.rules;for(let o=0,s=e.length;o<s;o++){const s=e[o].type;"inline"===s?r+=this.renderInline(e[o].children,t,n):void 0!==i[s]?r+=i[s](e,o,t,n,this):r+=this.renderToken(e,o,t,n)}return r},tl.prototype.__find__=function(e){for(let t=0;t<this.__rules__.length;t++)if(this.__rules__[t].name===e)return t;return-1},tl.prototype.__compile__=function(){const e=this,t=[""];e.__rules__.forEach(function(e){e.enabled&&e.alt.forEach(function(e){t.indexOf(e)<0&&t.push(e)})}),e.__cache__={},t.forEach(function(t){e.__cache__[t]=[],e.__rules__.forEach(function(n){n.enabled&&(t&&n.alt.indexOf(t)<0||e.__cache__[t].push(n.fn))})})},tl.prototype.at=function(e,t,n){const r=this.__find__(e),i=n||{};if(-1===r)throw new Error("Parser rule not found: "+e);this.__rules__[r].fn=t,this.__rules__[r].alt=i.alt||[],this.__cache__=null},tl.prototype.before=function(e,t,n,r){const i=this.__find__(e),o=r||{};if(-1===i)throw new Error("Parser rule not found: "+e);this.__rules__.splice(i,0,{name:t,enabled:!0,fn:n,alt:o.alt||[]}),this.__cache__=null},tl.prototype.after=function(e,t,n,r){const i=this.__find__(e),o=r||{};if(-1===i)throw new Error("Parser rule not found: "+e);this.__rules__.splice(i+1,0,{name:t,enabled:!0,fn:n,alt:o.alt||[]}),this.__cache__=null},tl.prototype.push=function(e,t,n){const r=n||{};this.__rules__.push({name:e,enabled:!0,fn:t,alt:r.alt||[]}),this.__cache__=null},tl.prototype.enable=function(e,t){Array.isArray(e)||(e=[e]);const n=[];return e.forEach(function(e){const r=this.__find__(e);if(r<0){if(t)return;throw new Error("Rules manager: invalid rule name "+e)}this.__rules__[r].enabled=!0,n.push(e)},this),this.__cache__=null,n},tl.prototype.enableOnly=function(e,t){Array.isArray(e)||(e=[e]),this.__rules__.forEach(function(e){e.enabled=!1}),this.enable(e,t)},tl.prototype.disable=function(e,t){Array.isArray(e)||(e=[e]);const n=[];return e.forEach(function(e){const r=this.__find__(e);if(r<0){if(t)return;throw new Error("Rules manager: invalid rule name "+e)}this.__rules__[r].enabled=!1,n.push(e)},this),this.__cache__=null,n},tl.prototype.getRules=function(e){return null===this.__cache__&&this.__compile__(),this.__cache__[e]||[]},nl.prototype.attrIndex=function(e){if(!this.attrs)return-1;const t=this.attrs;for(let n=0,r=t.length;n<r;n++)if(t[n][0]===e)return n;return-1},nl.prototype.attrPush=function(e){this.attrs?this.attrs.push(e):this.attrs=[e]},nl.prototype.attrSet=function(e,t){const n=this.attrIndex(e),r=[e,t];n<0?this.attrPush(r):this.attrs[n]=r},nl.prototype.attrGet=function(e){const t=this.attrIndex(e);let n=null;return t>=0&&(n=this.attrs[t][1]),n},nl.prototype.attrJoin=function(e,t){const n=this.attrIndex(e);n<0?this.attrPush([e,t]):this.attrs[n][1]=this.attrs[n][1]+" "+t},rl.prototype.Token=nl;const il=/\r\n?|\n/g,ol=/\0/g;function sl(e){return/^<a[>\s]/i.test(e)}function ll(e){return/^<\/a\s*>/i.test(e)}const al=/\+-|\.\.|\?\?\?\?|!!!!|,,|--/,cl=/\((c|tm|r)\)/i,hl=/\((c|tm|r)\)/gi,ul={c:"©",r:"®",tm:"™"};function pl(e,t){return ul[t.toLowerCase()]}function dl(e){let t=0;for(let n=e.length-1;n>=0;n--){const r=e[n];"text"!==r.type||t||(r.content=r.content.replace(hl,pl)),"link_open"===r.type&&"auto"===r.info&&t--,"link_close"===r.type&&"auto"===r.info&&t++}}function fl(e){let t=0;for(let n=e.length-1;n>=0;n--){const r=e[n];"text"!==r.type||t||al.test(r.content)&&(r.content=r.content.replace(/\+-/g,"±").replace(/\.{2,}/g,"…").replace(/([?!])…/g,"$1..").replace(/([?!]){4,}/g,"$1$1$1").replace(/,{2,}/g,",").replace(/(^|[^-])---(?=[^-]|$)/gm,"$1—").replace(/(^|\s)--(?=\s|$)/gm,"$1–").replace(/(^|[^-\s])--(?=[^-\s]|$)/gm,"$1–")),"link_open"===r.type&&"auto"===r.info&&t--,"link_close"===r.type&&"auto"===r.info&&t++}}const ml=/['"]/,gl=/['"]/g;function yl(e,t,n){return e.slice(0,t)+n+e.slice(t+1)}function kl(e,t){let n;const r=[];for(let i=0;i<e.length;i++){const o=e[i],s=e[i].level;for(n=r.length-1;n>=0&&!(r[n].level<=s);n--);if(r.length=n+1,"text"!==o.type)continue;let l=o.content,a=0,c=l.length;e:for(;a<c;){gl.lastIndex=a;const h=gl.exec(l);if(!h)break;let u=!0,p=!0;a=h.index+1;const d="'"===h[0];let f=32;if(h.index-1>=0)f=l.charCodeAt(h.index-1);else for(n=i-1;n>=0&&("softbreak"!==e[n].type&&"hardbreak"!==e[n].type);n--)if(e[n].content){f=e[n].content.charCodeAt(e[n].content.length-1);break}let m=32;if(a<c)m=l.charCodeAt(a);else for(n=i+1;n<e.length&&("softbreak"!==e[n].type&&"hardbreak"!==e[n].type);n++)if(e[n].content){m=e[n].content.charCodeAt(0);break}const g=Ks(f)||Us(String.fromCharCode(f)),y=Ks(m)||Us(String.fromCharCode(m)),k=Hs(f),b=Hs(m);if(b?u=!1:y&&(k||g||(u=!1)),k?p=!1:g&&(b||y||(p=!1)),34===m&&'"'===h[0]&&f>=48&&f<=57&&(p=u=!1),u&&p&&(u=g,p=y),u||p){if(p)for(n=r.length-1;n>=0;n--){let u=r[n];if(r[n].level<s)break;if(u.single===d&&r[n].level===s){let s,p;u=r[n],d?(s=t.md.options.quotes[2],p=t.md.options.quotes[3]):(s=t.md.options.quotes[0],p=t.md.options.quotes[1]),o.content=yl(o.content,h.index,p),e[u.token].content=yl(e[u.token].content,u.pos,s),a+=p.length-1,u.token===i&&(a+=s.length-1),l=o.content,c=l.length,r.length=n;continue e}}u?r.push({token:i,pos:h.index,single:d,level:s}):p&&d&&(o.content=yl(o.content,h.index,"’"))}else d&&(o.content=yl(o.content,h.index,"’"))}}}const bl=[["normalize",function(e){let t;t=e.src.replace(il,"\n"),t=t.replace(ol,"�"),e.src=t}],["block",function(e){let t;e.inlineMode?(t=new e.Token("inline","",0),t.content=e.src,t.map=[0,1],t.children=[],e.tokens.push(t)):e.md.block.parse(e.src,e.md,e.env,e.tokens)}],["inline",function(e){const t=e.tokens;for(let n=0,r=t.length;n<r;n++){const r=t[n];"inline"===r.type&&e.md.inline.parse(r.content,e.md,e.env,r.children)}}],["linkify",function(e){const t=e.tokens;if(e.md.options.linkify)for(let n=0,r=t.length;n<r;n++){if("inline"!==t[n].type||!e.md.linkify.pretest(t[n].content))continue;let r=t[n].children,i=0;for(let o=r.length-1;o>=0;o--){const s=r[o];if("link_close"!==s.type){if("html_inline"===s.type&&(sl(s.content)&&i>0&&i--,ll(s.content)&&i++),!(i>0)&&"text"===s.type&&e.md.linkify.test(s.content)){const i=s.content;let l=e.md.linkify.match(i);const a=[];let c=s.level,h=0;l.length>0&&0===l[0].index&&o>0&&"text_special"===r[o-1].type&&(l=l.slice(1));for(let t=0;t<l.length;t++){const n=l[t].url,r=e.md.normalizeLink(n);if(!e.md.validateLink(r))continue;let o=l[t].text;o=l[t].schema?"mailto:"!==l[t].schema||/^mailto:/i.test(o)?e.md.normalizeLinkText(o):e.md.normalizeLinkText("mailto:"+o).replace(/^mailto:/,""):e.md.normalizeLinkText("http://"+o).replace(/^http:\/\//,"");const s=l[t].index;if(s>h){const t=new e.Token("text","",0);t.content=i.slice(h,s),t.level=c,a.push(t)}const u=new e.Token("link_open","a",1);u.attrs=[["href",r]],u.level=c++,u.markup="linkify",u.info="auto",a.push(u);const p=new e.Token("text","",0);p.content=o,p.level=c,a.push(p);const d=new e.Token("link_close","a",-1);d.level=--c,d.markup="linkify",d.info="auto",a.push(d),h=l[t].lastIndex}if(h<i.length){const t=new e.Token("text","",0);t.content=i.slice(h),t.level=c,a.push(t)}t[n].children=r=Fs(r,o,a)}}else for(o--;r[o].level!==s.level&&"link_open"!==r[o].type;)o--}}}],["replacements",function(e){let t;if(e.md.options.typographer)for(t=e.tokens.length-1;t>=0;t--)"inline"===e.tokens[t].type&&(cl.test(e.tokens[t].content)&&dl(e.tokens[t].children),al.test(e.tokens[t].content)&&fl(e.tokens[t].children))}],["smartquotes",function(e){if(e.md.options.typographer)for(let t=e.tokens.length-1;t>=0;t--)"inline"===e.tokens[t].type&&ml.test(e.tokens[t].content)&&kl(e.tokens[t].children,e)}],["text_join",function(e){let t,n;const r=e.tokens,i=r.length;for(let e=0;e<i;e++){if("inline"!==r[e].type)continue;const i=r[e].children,o=i.length;for(t=0;t<o;t++)"text_special"===i[t].type&&(i[t].type="text");for(t=n=0;t<o;t++)"text"===i[t].type&&t+1<o&&"text"===i[t+1].type?i[t+1].content=i[t].content+i[t+1].content:(t!==n&&(i[n]=i[t]),n++);t!==n&&(i.length=n)}}]];function wl(){this.ruler=new tl;for(let e=0;e<bl.length;e++)this.ruler.push(bl[e][0],bl[e][1])}function Cl(e,t,n,r){this.src=e,this.md=t,this.env=n,this.tokens=r,this.bMarks=[],this.eMarks=[],this.tShift=[],this.sCount=[],this.bsCount=[],this.blkIndent=0,this.line=0,this.lineMax=0,this.tight=!1,this.ddIndent=-1,this.listIndent=-1,this.parentType="root",this.level=0;const i=this.src;for(let e=0,t=0,n=0,r=0,o=i.length,s=!1;t<o;t++){const l=i.charCodeAt(t);if(!s){if(Ws(l)){n++,9===l?r+=4-r%4:r++;continue}s=!0}10!==l&&t!==o-1||(10!==l&&t++,this.bMarks.push(e),this.eMarks.push(t),this.tShift.push(n),this.sCount.push(r),this.bsCount.push(0),s=!1,n=0,r=0,e=t+1)}this.bMarks.push(i.length),this.eMarks.push(i.length),this.tShift.push(0),this.sCount.push(0),this.bsCount.push(0),this.lineMax=this.bMarks.length-1}wl.prototype.process=function(e){const t=this.ruler.getRules("");for(let n=0,r=t.length;n<r;n++)t[n](e)},wl.prototype.State=rl,Cl.prototype.push=function(e,t,n){const r=new nl(e,t,n);return r.block=!0,n<0&&this.level--,r.level=this.level,n>0&&this.level++,this.tokens.push(r),r},Cl.prototype.isEmpty=function(e){return this.bMarks[e]+this.tShift[e]>=this.eMarks[e]},Cl.prototype.skipEmptyLines=function(e){for(let t=this.lineMax;e<t&&!(this.bMarks[e]+this.tShift[e]<this.eMarks[e]);e++);return e},Cl.prototype.skipSpaces=function(e){for(let t=this.src.length;e<t;e++){if(!Ws(this.src.charCodeAt(e)))break}return e},Cl.prototype.skipSpacesBack=function(e,t){if(e<=t)return e;for(;e>t;)if(!Ws(this.src.charCodeAt(--e)))return e+1;return e},Cl.prototype.skipChars=function(e,t){for(let n=this.src.length;e<n&&this.src.charCodeAt(e)===t;e++);return e},Cl.prototype.skipCharsBack=function(e,t,n){if(e<=n)return e;for(;e>n;)if(t!==this.src.charCodeAt(--e))return e+1;return e},Cl.prototype.getLines=function(e,t,n,r){if(e>=t)return"";const i=new Array(t-e);for(let o=0,s=e;s<t;s++,o++){let e=0;const l=this.bMarks[s];let a,c=l;for(a=s+1<t||r?this.eMarks[s]+1:this.eMarks[s];c<a&&e<n;){const t=this.src.charCodeAt(c);if(Ws(t))9===t?e+=4-(e+this.bsCount[s])%4:e++;else{if(!(c-l<this.tShift[s]))break;e++}c++}i[o]=e>n?new Array(e-n+1).join(" ")+this.src.slice(c,a):this.src.slice(c,a)}return i.join("")},Cl.prototype.Token=nl;function xl(e,t){const n=e.bMarks[t]+e.tShift[t],r=e.eMarks[t];return e.src.slice(n,r)}function Dl(e){const t=[],n=e.length;let r=0,i=e.charCodeAt(r),o=!1,s=0,l="";for(;r<n;)124===i&&(o?(l+=e.substring(s,r-1),s=r):(t.push(l+e.substring(s,r)),l="",s=r+1)),o=92===i,r++,i=e.charCodeAt(r);return t.push(l+e.substring(s)),t}function vl(e,t){const n=e.eMarks[t];let r=e.bMarks[t]+e.tShift[t];const i=e.src.charCodeAt(r++);if(42!==i&&45!==i&&43!==i)return-1;if(r<n){if(!Ws(e.src.charCodeAt(r)))return-1}return r}function _l(e,t){const n=e.bMarks[t]+e.tShift[t],r=e.eMarks[t];let i=n;if(i+1>=r)return-1;let o=e.src.charCodeAt(i++);if(o<48||o>57)return-1;for(;;){if(i>=r)return-1;if(o=e.src.charCodeAt(i++),!(o>=48&&o<=57)){if(41===o||46===o)break;return-1}if(i-n>=10)return-1}return i<r&&(o=e.src.charCodeAt(i),!Ws(o))?-1:i}const Sl="<[A-Za-z][A-Za-z0-9\\-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^\"'=<>`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*\\/?>",El="<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>",Al=new RegExp("^(?:"+Sl+"|"+El+"|\x3c!---?>|\x3c!--(?:[^-]|-[^-]|--[^>])*--\x3e|<[?][\\s\\S]*?[?]>|<![A-Za-z][^>]*>|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>)"),Ml=new RegExp("^(?:"+Sl+"|"+El+")"),Ol=[[/^<(script|pre|style|textarea)(?=(\s|>|$))/i,/<\/(script|pre|style|textarea)>/i,!0],[/^<!--/,/-->/,!0],[/^<\?/,/\?>/,!0],[/^<![A-Z]/,/>/,!0],[/^<!\[CDATA\[/,/\]\]>/,!0],[new RegExp("^</?("+["address","article","aside","base","basefont","blockquote","body","caption","center","col","colgroup","dd","details","dialog","dir","div","dl","dt","fieldset","figcaption","figure","footer","form","frame","frameset","h1","h2","h3","h4","h5","h6","head","header","hr","html","iframe","legend","li","link","main","menu","menuitem","nav","noframes","ol","optgroup","option","p","param","search","section","summary","table","tbody","td","tfoot","th","thead","title","tr","track","ul"].join("|")+")(?=(\\s|/?>|$))","i"),/^$/,!0],[new RegExp(Ml.source+"\\s*$"),/^$/,!1]];const Nl=[["table",function(e,t,n,r){if(t+2>n)return!1;let i=t+1;if(e.sCount[i]<e.blkIndent)return!1;if(e.sCount[i]-e.blkIndent>=4)return!1;let o=e.bMarks[i]+e.tShift[i];if(o>=e.eMarks[i])return!1;const s=e.src.charCodeAt(o++);if(124!==s&&45!==s&&58!==s)return!1;if(o>=e.eMarks[i])return!1;const l=e.src.charCodeAt(o++);if(124!==l&&45!==l&&58!==l&&!Ws(l))return!1;if(45===s&&Ws(l))return!1;for(;o<e.eMarks[i];){const t=e.src.charCodeAt(o);if(124!==t&&45!==t&&58!==t&&!Ws(t))return!1;o++}let a=xl(e,t+1),c=a.split("|");const h=[];for(let e=0;e<c.length;e++){const t=c[e].trim();if(!t){if(0===e||e===c.length-1)continue;return!1}if(!/^:?-+:?$/.test(t))return!1;58===t.charCodeAt(t.length-1)?h.push(58===t.charCodeAt(0)?"center":"right"):58===t.charCodeAt(0)?h.push("left"):h.push("")}if(a=xl(e,t).trim(),-1===a.indexOf("|"))return!1;if(e.sCount[t]-e.blkIndent>=4)return!1;c=Dl(a),c.length&&""===c[0]&&c.shift(),c.length&&""===c[c.length-1]&&c.pop();const u=c.length;if(0===u||u!==h.length)return!1;if(r)return!0;const p=e.parentType;e.parentType="table";const d=e.md.block.ruler.getRules("blockquote"),f=[t,0];e.push("table_open","table",1).map=f,e.push("thead_open","thead",1).map=[t,t+1],e.push("tr_open","tr",1).map=[t,t+1];for(let t=0;t<c.length;t++){const n=e.push("th_open","th",1);h[t]&&(n.attrs=[["style","text-align:"+h[t]]]);const r=e.push("inline","",0);r.content=c[t].trim(),r.children=[],e.push("th_close","th",-1)}let m;e.push("tr_close","tr",-1),e.push("thead_close","thead",-1);let g=0;for(i=t+2;i<n&&!(e.sCount[i]<e.blkIndent);i++){let r=!1;for(let t=0,o=d.length;t<o;t++)if(d[t](e,i,n,!0)){r=!0;break}if(r)break;if(a=xl(e,i).trim(),!a)break;if(e.sCount[i]-e.blkIndent>=4)break;if(c=Dl(a),c.length&&""===c[0]&&c.shift(),c.length&&""===c[c.length-1]&&c.pop(),g+=u-c.length,g>65536)break;if(i===t+2){e.push("tbody_open","tbody",1).map=m=[t+2,0]}e.push("tr_open","tr",1).map=[i,i+1];for(let t=0;t<u;t++){const n=e.push("td_open","td",1);h[t]&&(n.attrs=[["style","text-align:"+h[t]]]);const r=e.push("inline","",0);r.content=c[t]?c[t].trim():"",r.children=[],e.push("td_close","td",-1)}e.push("tr_close","tr",-1)}return m&&(e.push("tbody_close","tbody",-1),m[1]=i),e.push("table_close","table",-1),f[1]=i,e.parentType=p,e.line=i,!0},["paragraph","reference"]],["code",function(e,t,n){if(e.sCount[t]-e.blkIndent<4)return!1;let r=t+1,i=r;for(;r<n;)if(e.isEmpty(r))r++;else{if(!(e.sCount[r]-e.blkIndent>=4))break;r++,i=r}e.line=i;const o=e.push("code_block","code",0);return o.content=e.getLines(t,i,4+e.blkIndent,!1)+"\n",o.map=[t,e.line],!0}],["fence",function(e,t,n,r){let i=e.bMarks[t]+e.tShift[t],o=e.eMarks[t];if(e.sCount[t]-e.blkIndent>=4)return!1;if(i+3>o)return!1;const s=e.src.charCodeAt(i);if(126!==s&&96!==s)return!1;let l=i;i=e.skipChars(i,s);let a=i-l;if(a<3)return!1;const c=e.src.slice(l,i),h=e.src.slice(i,o);if(96===s&&h.indexOf(String.fromCharCode(s))>=0)return!1;if(r)return!0;let u=t,p=!1;for(;(u++,!(u>=n))&&(i=l=e.bMarks[u]+e.tShift[u],o=e.eMarks[u],!(i<o&&e.sCount[u]<e.blkIndent));)if(e.src.charCodeAt(i)===s&&!(e.sCount[u]-e.blkIndent>=4||(i=e.skipChars(i,s),i-l<a||(i=e.skipSpaces(i),i<o)))){p=!0;break}a=e.sCount[t],e.line=u+(p?1:0);const d=e.push("fence","code",0);return d.info=h,d.content=e.getLines(t+1,u,a,!0),d.markup=c,d.map=[t,e.line],!0},["paragraph","reference","blockquote","list"]],["blockquote",function(e,t,n,r){let i=e.bMarks[t]+e.tShift[t],o=e.eMarks[t];const s=e.lineMax;if(e.sCount[t]-e.blkIndent>=4)return!1;if(62!==e.src.charCodeAt(i))return!1;if(r)return!0;const l=[],a=[],c=[],h=[],u=e.md.block.ruler.getRules("blockquote"),p=e.parentType;e.parentType="blockquote";let d,f=!1;for(d=t;d<n;d++){const t=e.sCount[d]<e.blkIndent;if(i=e.bMarks[d]+e.tShift[d],o=e.eMarks[d],i>=o)break;if(62===e.src.charCodeAt(i++)&&!t){let t,n,r=e.sCount[d]+1;32===e.src.charCodeAt(i)?(i++,r++,n=!1,t=!0):9===e.src.charCodeAt(i)?(t=!0,(e.bsCount[d]+r)%4==3?(i++,r++,n=!1):n=!0):t=!1;let s=r;for(l.push(e.bMarks[d]),e.bMarks[d]=i;i<o;){const t=e.src.charCodeAt(i);if(!Ws(t))break;9===t?s+=4-(s+e.bsCount[d]+(n?1:0))%4:s++,i++}f=i>=o,a.push(e.bsCount[d]),e.bsCount[d]=e.sCount[d]+1+(t?1:0),c.push(e.sCount[d]),e.sCount[d]=s-r,h.push(e.tShift[d]),e.tShift[d]=i-e.bMarks[d];continue}if(f)break;let r=!1;for(let t=0,i=u.length;t<i;t++)if(u[t](e,d,n,!0)){r=!0;break}if(r){e.lineMax=d,0!==e.blkIndent&&(l.push(e.bMarks[d]),a.push(e.bsCount[d]),h.push(e.tShift[d]),c.push(e.sCount[d]),e.sCount[d]-=e.blkIndent);break}l.push(e.bMarks[d]),a.push(e.bsCount[d]),h.push(e.tShift[d]),c.push(e.sCount[d]),e.sCount[d]=-1}const m=e.blkIndent;e.blkIndent=0;const g=e.push("blockquote_open","blockquote",1);g.markup=">";const y=[t,0];g.map=y,e.md.block.tokenize(e,t,d),e.push("blockquote_close","blockquote",-1).markup=">",e.lineMax=s,e.parentType=p,y[1]=e.line;for(let n=0;n<h.length;n++)e.bMarks[n+t]=l[n],e.tShift[n+t]=h[n],e.sCount[n+t]=c[n],e.bsCount[n+t]=a[n];return e.blkIndent=m,!0},["paragraph","reference","blockquote","list"]],["hr",function(e,t,n,r){const i=e.eMarks[t];if(e.sCount[t]-e.blkIndent>=4)return!1;let o=e.bMarks[t]+e.tShift[t];const s=e.src.charCodeAt(o++);if(42!==s&&45!==s&&95!==s)return!1;let l=1;for(;o<i;){const t=e.src.charCodeAt(o++);if(t!==s&&!Ws(t))return!1;t===s&&l++}if(l<3)return!1;if(r)return!0;e.line=t+1;const a=e.push("hr","hr",0);return a.map=[t,e.line],a.markup=Array(l+1).join(String.fromCharCode(s)),!0},["paragraph","reference","blockquote","list"]],["list",function(e,t,n,r){let i,o,s,l,a=t,c=!0;if(e.sCount[a]-e.blkIndent>=4)return!1;if(e.listIndent>=0&&e.sCount[a]-e.listIndent>=4&&e.sCount[a]<e.blkIndent)return!1;let h,u,p,d=!1;if(r&&"paragraph"===e.parentType&&e.sCount[a]>=e.blkIndent&&(d=!0),(p=_l(e,a))>=0){if(h=!0,s=e.bMarks[a]+e.tShift[a],u=Number(e.src.slice(s,p-1)),d&&1!==u)return!1}else{if(!((p=vl(e,a))>=0))return!1;h=!1}if(d&&e.skipSpaces(p)>=e.eMarks[a])return!1;if(r)return!0;const f=e.src.charCodeAt(p-1),m=e.tokens.length;h?(l=e.push("ordered_list_open","ol",1),1!==u&&(l.attrs=[["start",u]])):l=e.push("bullet_list_open","ul",1);const g=[a,0];l.map=g,l.markup=String.fromCharCode(f);let y=!1;const k=e.md.block.ruler.getRules("list"),b=e.parentType;for(e.parentType="list";a<n;){o=p,i=e.eMarks[a];const t=e.sCount[a]+p-(e.bMarks[a]+e.tShift[a]);let r=t;for(;o<i;){const t=e.src.charCodeAt(o);if(9===t)r+=4-(r+e.bsCount[a])%4;else{if(32!==t)break;r++}o++}const u=o;let d;d=u>=i?1:r-t,d>4&&(d=1);const m=t+d;l=e.push("list_item_open","li",1),l.markup=String.fromCharCode(f);const g=[a,0];l.map=g,h&&(l.info=e.src.slice(s,p-1));const b=e.tight,w=e.tShift[a],C=e.sCount[a],x=e.listIndent;if(e.listIndent=e.blkIndent,e.blkIndent=m,e.tight=!0,e.tShift[a]=u-e.bMarks[a],e.sCount[a]=r,u>=i&&e.isEmpty(a+1)?e.line=Math.min(e.line+2,n):e.md.block.tokenize(e,a,n,!0),e.tight&&!y||(c=!1),y=e.line-a>1&&e.isEmpty(e.line-1),e.blkIndent=e.listIndent,e.listIndent=x,e.tShift[a]=w,e.sCount[a]=C,e.tight=b,l=e.push("list_item_close","li",-1),l.markup=String.fromCharCode(f),a=e.line,g[1]=a,a>=n)break;if(e.sCount[a]<e.blkIndent)break;if(e.sCount[a]-e.blkIndent>=4)break;let D=!1;for(let t=0,r=k.length;t<r;t++)if(k[t](e,a,n,!0)){D=!0;break}if(D)break;if(h){if(p=_l(e,a),p<0)break;s=e.bMarks[a]+e.tShift[a]}else if(p=vl(e,a),p<0)break;if(f!==e.src.charCodeAt(p-1))break}return l=h?e.push("ordered_list_close","ol",-1):e.push("bullet_list_close","ul",-1),l.markup=String.fromCharCode(f),g[1]=a,e.line=a,e.parentType=b,c&&function(e,t){const n=e.level+2;for(let r=t+2,i=e.tokens.length-2;r<i;r++)e.tokens[r].level===n&&"paragraph_open"===e.tokens[r].type&&(e.tokens[r+2].hidden=!0,e.tokens[r].hidden=!0,r+=2)}(e,m),!0},["paragraph","reference","blockquote"]],["reference",function(e,t,n,r){let i=e.bMarks[t]+e.tShift[t],o=e.eMarks[t],s=t+1;if(e.sCount[t]-e.blkIndent>=4)return!1;if(91!==e.src.charCodeAt(i))return!1;function l(t){const n=e.lineMax;if(t>=n||e.isEmpty(t))return null;let r=!1;if(e.sCount[t]-e.blkIndent>3&&(r=!0),e.sCount[t]<0&&(r=!0),!r){const r=e.md.block.ruler.getRules("reference"),i=e.parentType;e.parentType="reference";let o=!1;for(let i=0,s=r.length;i<s;i++)if(r[i](e,t,n,!0)){o=!0;break}if(e.parentType=i,o)return null}const i=e.bMarks[t]+e.tShift[t],o=e.eMarks[t];return e.src.slice(i,o+1)}let a=e.src.slice(i,o+1);o=a.length;let c=-1;for(i=1;i<o;i++){const e=a.charCodeAt(i);if(91===e)return!1;if(93===e){c=i;break}if(10===e){const e=l(s);null!==e&&(a+=e,o=a.length,s++)}else if(92===e&&(i++,i<o&&10===a.charCodeAt(i))){const e=l(s);null!==e&&(a+=e,o=a.length,s++)}}if(c<0||58!==a.charCodeAt(c+1))return!1;for(i=c+2;i<o;i++){const e=a.charCodeAt(i);if(10===e){const e=l(s);null!==e&&(a+=e,o=a.length,s++)}else if(!Ws(e))break}const h=e.md.helpers.parseLinkDestination(a,i,o);if(!h.ok)return!1;const u=e.md.normalizeLink(h.str);if(!e.md.validateLink(u))return!1;i=h.pos;const p=i,d=s,f=i;for(;i<o;i++){const e=a.charCodeAt(i);if(10===e){const e=l(s);null!==e&&(a+=e,o=a.length,s++)}else if(!Ws(e))break}let m,g=e.md.helpers.parseLinkTitle(a,i,o);for(;g.can_continue;){const t=l(s);if(null===t)break;a+=t,i=o,o=a.length,s++,g=e.md.helpers.parseLinkTitle(a,i,o,g)}for(i<o&&f!==i&&g.ok?(m=g.str,i=g.pos):(m="",i=p,s=d);i<o;){if(!Ws(a.charCodeAt(i)))break;i++}if(i<o&&10!==a.charCodeAt(i)&&m)for(m="",i=p,s=d;i<o;){if(!Ws(a.charCodeAt(i)))break;i++}if(i<o&&10!==a.charCodeAt(i))return!1;const y=Zs(a.slice(1,c));return!!y&&(r||(void 0===e.env.references&&(e.env.references={}),void 0===e.env.references[y]&&(e.env.references[y]={title:m,href:u}),e.line=s),!0)}],["html_block",function(e,t,n,r){let i=e.bMarks[t]+e.tShift[t],o=e.eMarks[t];if(e.sCount[t]-e.blkIndent>=4)return!1;if(!e.md.options.html)return!1;if(60!==e.src.charCodeAt(i))return!1;let s=e.src.slice(i,o),l=0;for(;l<Ol.length&&!Ol[l][0].test(s);l++);if(l===Ol.length)return!1;if(r)return Ol[l][2];let a=t+1;if(!Ol[l][1].test(s))for(;a<n&&!(e.sCount[a]<e.blkIndent);a++)if(i=e.bMarks[a]+e.tShift[a],o=e.eMarks[a],s=e.src.slice(i,o),Ol[l][1].test(s)){0!==s.length&&a++;break}e.line=a;const c=e.push("html_block","",0);return c.map=[t,a],c.content=e.getLines(t,a,e.blkIndent,!0),!0},["paragraph","reference","blockquote"]],["heading",function(e,t,n,r){let i=e.bMarks[t]+e.tShift[t],o=e.eMarks[t];if(e.sCount[t]-e.blkIndent>=4)return!1;let s=e.src.charCodeAt(i);if(35!==s||i>=o)return!1;let l=1;for(s=e.src.charCodeAt(++i);35===s&&i<o&&l<=6;)l++,s=e.src.charCodeAt(++i);if(l>6||i<o&&!Ws(s))return!1;if(r)return!0;o=e.skipSpacesBack(o,i);const a=e.skipCharsBack(o,35,i);a>i&&Ws(e.src.charCodeAt(a-1))&&(o=a),e.line=t+1;const c=e.push("heading_open","h"+String(l),1);c.markup="########".slice(0,l),c.map=[t,e.line];const h=e.push("inline","",0);return h.content=e.src.slice(i,o).trim(),h.map=[t,e.line],h.children=[],e.push("heading_close","h"+String(l),-1).markup="########".slice(0,l),!0},["paragraph","reference","blockquote"]],["lheading",function(e,t,n){const r=e.md.block.ruler.getRules("paragraph");if(e.sCount[t]-e.blkIndent>=4)return!1;const i=e.parentType;e.parentType="paragraph";let o,s=0,l=t+1;for(;l<n&&!e.isEmpty(l);l++){if(e.sCount[l]-e.blkIndent>3)continue;if(e.sCount[l]>=e.blkIndent){let t=e.bMarks[l]+e.tShift[l];const n=e.eMarks[l];if(t<n&&(o=e.src.charCodeAt(t),(45===o||61===o)&&(t=e.skipChars(t,o),t=e.skipSpaces(t),t>=n))){s=61===o?1:2;break}}if(e.sCount[l]<0)continue;let t=!1;for(let i=0,o=r.length;i<o;i++)if(r[i](e,l,n,!0)){t=!0;break}if(t)break}if(!s)return!1;const a=e.getLines(t,l,e.blkIndent,!1).trim();e.line=l+1;const c=e.push("heading_open","h"+String(s),1);c.markup=String.fromCharCode(o),c.map=[t,e.line];const h=e.push("inline","",0);return h.content=a,h.map=[t,e.line-1],h.children=[],e.push("heading_close","h"+String(s),-1).markup=String.fromCharCode(o),e.parentType=i,!0}],["paragraph",function(e,t,n){const r=e.md.block.ruler.getRules("paragraph"),i=e.parentType;let o=t+1;for(e.parentType="paragraph";o<n&&!e.isEmpty(o);o++){if(e.sCount[o]-e.blkIndent>3)continue;if(e.sCount[o]<0)continue;let t=!1;for(let i=0,s=r.length;i<s;i++)if(r[i](e,o,n,!0)){t=!0;break}if(t)break}const s=e.getLines(t,o,e.blkIndent,!1).trim();e.line=o,e.push("paragraph_open","p",1).map=[t,e.line];const l=e.push("inline","",0);return l.content=s,l.map=[t,e.line],l.children=[],e.push("paragraph_close","p",-1),e.parentType=i,!0}]];function Fl(){this.ruler=new tl;for(let e=0;e<Nl.length;e++)this.ruler.push(Nl[e][0],Nl[e][1],{alt:(Nl[e][2]||[]).slice()})}function Tl(e,t,n,r){this.src=e,this.env=n,this.md=t,this.tokens=r,this.tokens_meta=Array(r.length),this.pos=0,this.posMax=this.src.length,this.level=0,this.pending="",this.pendingLevel=0,this.cache={},this.delimiters=[],this._prev_delimiters=[],this.backticks={},this.backticksScanned=!1,this.linkLevel=0}function Rl(e){switch(e){case 10:case 33:case 35:case 36:case 37:case 38:case 42:case 43:case 45:case 58:case 60:case 61:case 62:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 125:case 126:return!0;default:return!1}}Fl.prototype.tokenize=function(e,t,n){const r=this.ruler.getRules(""),i=r.length,o=e.md.options.maxNesting;let s=t,l=!1;for(;s<n&&(e.line=s=e.skipEmptyLines(s),!(s>=n))&&!(e.sCount[s]<e.blkIndent);){if(e.level>=o){e.line=n;break}const t=e.line;let a=!1;for(let o=0;o<i;o++)if(a=r[o](e,s,n,!1),a){if(t>=e.line)throw new Error("block rule didn't increment state.line");break}if(!a)throw new Error("none of the block rules matched");e.tight=!l,e.isEmpty(e.line-1)&&(l=!0),s=e.line,s<n&&e.isEmpty(s)&&(l=!0,s++,e.line=s)}},Fl.prototype.parse=function(e,t,n,r){if(!e)return;const i=new this.State(e,t,n,r);this.tokenize(i,i.line,i.lineMax)},Fl.prototype.State=Cl,Tl.prototype.pushPending=function(){const e=new nl("text","",0);return e.content=this.pending,e.level=this.pendingLevel,this.tokens.push(e),this.pending="",e},Tl.prototype.push=function(e,t,n){this.pending&&this.pushPending();const r=new nl(e,t,n);let i=null;return n<0&&(this.level--,this.delimiters=this._prev_delimiters.pop()),r.level=this.level,n>0&&(this.level++,this._prev_delimiters.push(this.delimiters),this.delimiters=[],i={delimiters:this.delimiters}),this.pendingLevel=this.level,this.tokens.push(r),this.tokens_meta.push(i),r},Tl.prototype.scanDelims=function(e,t){const n=this.posMax,r=this.src.charCodeAt(e),i=e>0?this.src.charCodeAt(e-1):32;let o=e;for(;o<n&&this.src.charCodeAt(o)===r;)o++;const s=o-e,l=o<n?this.src.charCodeAt(o):32,a=Ks(i)||Us(String.fromCharCode(i)),c=Ks(l)||Us(String.fromCharCode(l)),h=Hs(i),u=Hs(l),p=!u&&(!c||h||a),d=!h&&(!a||u||c);return{can_open:p&&(t||!d||a),can_close:d&&(t||!p||c),length:s}},Tl.prototype.Token=nl;const zl=/(?:^|[^a-z0-9.+-])([a-z][a-z0-9.+-]*)$/i;const Il=[];for(let e=0;e<256;e++)Il.push(0);function Bl(e,t){let n;const r=[],i=t.length;for(let o=0;o<i;o++){const i=t[o];if(126!==i.marker)continue;if(-1===i.end)continue;const s=t[i.end];n=e.tokens[i.token],n.type="s_open",n.tag="s",n.nesting=1,n.markup="~~",n.content="",n=e.tokens[s.token],n.type="s_close",n.tag="s",n.nesting=-1,n.markup="~~",n.content="","text"===e.tokens[s.token-1].type&&"~"===e.tokens[s.token-1].content&&r.push(s.token-1)}for(;r.length;){const t=r.pop();let i=t+1;for(;i<e.tokens.length&&"s_close"===e.tokens[i].type;)i++;i--,t!==i&&(n=e.tokens[i],e.tokens[i]=e.tokens[t],e.tokens[t]=n)}}"\\!\"#$%&'()*+,./:;<=>?@[]^_`{|}~-".split("").forEach(function(e){Il[e.charCodeAt(0)]=1});var Pl={tokenize:function(e,t){const n=e.pos,r=e.src.charCodeAt(n);if(t)return!1;if(126!==r)return!1;const i=e.scanDelims(e.pos,!0);let o=i.length;const s=String.fromCharCode(r);if(o<2)return!1;let l;o%2&&(l=e.push("text","",0),l.content=s,o--);for(let t=0;t<o;t+=2)l=e.push("text","",0),l.content=s+s,e.delimiters.push({marker:r,length:0,token:e.tokens.length-1,end:-1,open:i.can_open,close:i.can_close});return e.pos+=i.length,!0},postProcess:function(e){const t=e.tokens_meta,n=e.tokens_meta.length;Bl(e,e.delimiters);for(let r=0;r<n;r++)t[r]&&t[r].delimiters&&Bl(e,t[r].delimiters)}};function Ll(e,t){for(let n=t.length-1;n>=0;n--){const r=t[n];if(95!==r.marker&&42!==r.marker)continue;if(-1===r.end)continue;const i=t[r.end],o=n>0&&t[n-1].end===r.end+1&&t[n-1].marker===r.marker&&t[n-1].token===r.token-1&&t[r.end+1].token===i.token+1,s=String.fromCharCode(r.marker),l=e.tokens[r.token];l.type=o?"strong_open":"em_open",l.tag=o?"strong":"em",l.nesting=1,l.markup=o?s+s:s,l.content="";const a=e.tokens[i.token];a.type=o?"strong_close":"em_close",a.tag=o?"strong":"em",a.nesting=-1,a.markup=o?s+s:s,a.content="",o&&(e.tokens[t[n-1].token].content="",e.tokens[t[r.end+1].token].content="",n--)}}var ql={tokenize:function(e,t){const n=e.pos,r=e.src.charCodeAt(n);if(t)return!1;if(95!==r&&42!==r)return!1;const i=e.scanDelims(e.pos,42===r);for(let t=0;t<i.length;t++){e.push("text","",0).content=String.fromCharCode(r),e.delimiters.push({marker:r,length:i.length,token:e.tokens.length-1,end:-1,open:i.can_open,close:i.can_close})}return e.pos+=i.length,!0},postProcess:function(e){const t=e.tokens_meta,n=e.tokens_meta.length;Ll(e,e.delimiters);for(let r=0;r<n;r++)t[r]&&t[r].delimiters&&Ll(e,t[r].delimiters)}};const $l=/^([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)$/,Vl=/^([a-zA-Z][a-zA-Z0-9+.-]{1,31}):([^<>\x00-\x20]*)$/;const jl=/^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i,Jl=/^&([a-z][a-z0-9]{1,31});/i;function Wl(e){const t={},n=e.length;if(!n)return;let r=0,i=-2;const o=[];for(let s=0;s<n;s++){const n=e[s];if(o.push(0),e[r].marker===n.marker&&i===n.token-1||(r=s),i=n.token,n.length=n.length||0,!n.close)continue;t.hasOwnProperty(n.marker)||(t[n.marker]=[-1,-1,-1,-1,-1,-1]);const l=t[n.marker][(n.open?3:0)+n.length%3];let a=r-o[r]-1,c=a;for(;a>l;a-=o[a]+1){const t=e[a];if(t.marker===n.marker&&(t.open&&t.end<0)){let r=!1;if((t.close||n.open)&&(t.length+n.length)%3==0&&(t.length%3==0&&n.length%3==0||(r=!0)),!r){const r=a>0&&!e[a-1].open?o[a-1]+1:0;o[s]=s-a+r,o[a]=r,n.open=!1,t.end=s,t.close=!1,c=-1,i=-2;break}}}-1!==c&&(t[n.marker][(n.open?3:0)+(n.length||0)%3]=c)}}const Hl=[["text",function(e,t){let n=e.pos;for(;n<e.posMax&&!Rl(e.src.charCodeAt(n));)n++;return n!==e.pos&&(t||(e.pending+=e.src.slice(e.pos,n)),e.pos=n,!0)}],["linkify",function(e,t){if(!e.md.options.linkify)return!1;if(e.linkLevel>0)return!1;const n=e.pos;if(n+3>e.posMax)return!1;if(58!==e.src.charCodeAt(n))return!1;if(47!==e.src.charCodeAt(n+1))return!1;if(47!==e.src.charCodeAt(n+2))return!1;const r=e.pending.match(zl);if(!r)return!1;const i=r[1],o=e.md.linkify.matchAtStart(e.src.slice(n-i.length));if(!o)return!1;let s=o.url;if(s.length<=i.length)return!1;s=s.replace(/\*+$/,"");const l=e.md.normalizeLink(s);if(!e.md.validateLink(l))return!1;if(!t){e.pending=e.pending.slice(0,-i.length);const t=e.push("link_open","a",1);t.attrs=[["href",l]],t.markup="linkify",t.info="auto";e.push("text","",0).content=e.md.normalizeLinkText(s);const n=e.push("link_close","a",-1);n.markup="linkify",n.info="auto"}return e.pos+=s.length-i.length,!0}],["newline",function(e,t){let n=e.pos;if(10!==e.src.charCodeAt(n))return!1;const r=e.pending.length-1,i=e.posMax;if(!t)if(r>=0&&32===e.pending.charCodeAt(r))if(r>=1&&32===e.pending.charCodeAt(r-1)){let t=r-1;for(;t>=1&&32===e.pending.charCodeAt(t-1);)t--;e.pending=e.pending.slice(0,t),e.push("hardbreak","br",0)}else e.pending=e.pending.slice(0,-1),e.push("softbreak","br",0);else e.push("softbreak","br",0);for(n++;n<i&&Ws(e.src.charCodeAt(n));)n++;return e.pos=n,!0}],["escape",function(e,t){let n=e.pos;const r=e.posMax;if(92!==e.src.charCodeAt(n))return!1;if(n++,n>=r)return!1;let i=e.src.charCodeAt(n);if(10===i){for(t||e.push("hardbreak","br",0),n++;n<r&&(i=e.src.charCodeAt(n),Ws(i));)n++;return e.pos=n,!0}let o=e.src[n];if(i>=55296&&i<=56319&&n+1<r){const t=e.src.charCodeAt(n+1);t>=56320&&t<=57343&&(o+=e.src[n+1],n++)}const s="\\"+o;if(!t){const t=e.push("text_special","",0);i<256&&0!==Il[i]?t.content=o:t.content=s,t.markup=s,t.info="escape"}return e.pos=n+1,!0}],["backticks",function(e,t){let n=e.pos;if(96!==e.src.charCodeAt(n))return!1;const r=n;n++;const i=e.posMax;for(;n<i&&96===e.src.charCodeAt(n);)n++;const o=e.src.slice(r,n),s=o.length;if(e.backticksScanned&&(e.backticks[s]||0)<=r)return t||(e.pending+=o),e.pos+=s,!0;let l,a=n;for(;-1!==(l=e.src.indexOf("`",a));){for(a=l+1;a<i&&96===e.src.charCodeAt(a);)a++;const r=a-l;if(r===s){if(!t){const t=e.push("code_inline","code",0);t.markup=o,t.content=e.src.slice(n,l).replace(/\n/g," ").replace(/^ (.+) $/,"$1")}return e.pos=a,!0}e.backticks[r]=l}return e.backticksScanned=!0,t||(e.pending+=o),e.pos+=s,!0}],["strikethrough",Pl.tokenize],["emphasis",ql.tokenize],["link",function(e,t){let n,r,i,o,s="",l="",a=e.pos,c=!0;if(91!==e.src.charCodeAt(e.pos))return!1;const h=e.pos,u=e.posMax,p=e.pos+1,d=e.md.helpers.parseLinkLabel(e,e.pos,!0);if(d<0)return!1;let f=d+1;if(f<u&&40===e.src.charCodeAt(f)){for(c=!1,f++;f<u&&(n=e.src.charCodeAt(f),Ws(n)||10===n);f++);if(f>=u)return!1;if(a=f,i=e.md.helpers.parseLinkDestination(e.src,f,e.posMax),i.ok){for(s=e.md.normalizeLink(i.str),e.md.validateLink(s)?f=i.pos:s="",a=f;f<u&&(n=e.src.charCodeAt(f),Ws(n)||10===n);f++);if(i=e.md.helpers.parseLinkTitle(e.src,f,e.posMax),f<u&&a!==f&&i.ok)for(l=i.str,f=i.pos;f<u&&(n=e.src.charCodeAt(f),Ws(n)||10===n);f++);}(f>=u||41!==e.src.charCodeAt(f))&&(c=!0),f++}if(c){if(void 0===e.env.references)return!1;if(f<u&&91===e.src.charCodeAt(f)?(a=f+1,f=e.md.helpers.parseLinkLabel(e,f),f>=0?r=e.src.slice(a,f++):f=d+1):f=d+1,r||(r=e.src.slice(p,d)),o=e.env.references[Zs(r)],!o)return e.pos=h,!1;s=o.href,l=o.title}if(!t){e.pos=p,e.posMax=d;const t=[["href",s]];e.push("link_open","a",1).attrs=t,l&&t.push(["title",l]),e.linkLevel++,e.md.inline.tokenize(e),e.linkLevel--,e.push("link_close","a",-1)}return e.pos=f,e.posMax=u,!0}],["image",function(e,t){let n,r,i,o,s,l,a,c,h="";const u=e.pos,p=e.posMax;if(33!==e.src.charCodeAt(e.pos))return!1;if(91!==e.src.charCodeAt(e.pos+1))return!1;const d=e.pos+2,f=e.md.helpers.parseLinkLabel(e,e.pos+1,!1);if(f<0)return!1;if(o=f+1,o<p&&40===e.src.charCodeAt(o)){for(o++;o<p&&(n=e.src.charCodeAt(o),Ws(n)||10===n);o++);if(o>=p)return!1;for(c=o,l=e.md.helpers.parseLinkDestination(e.src,o,e.posMax),l.ok&&(h=e.md.normalizeLink(l.str),e.md.validateLink(h)?o=l.pos:h=""),c=o;o<p&&(n=e.src.charCodeAt(o),Ws(n)||10===n);o++);if(l=e.md.helpers.parseLinkTitle(e.src,o,e.posMax),o<p&&c!==o&&l.ok)for(a=l.str,o=l.pos;o<p&&(n=e.src.charCodeAt(o),Ws(n)||10===n);o++);else a="";if(o>=p||41!==e.src.charCodeAt(o))return e.pos=u,!1;o++}else{if(void 0===e.env.references)return!1;if(o<p&&91===e.src.charCodeAt(o)?(c=o+1,o=e.md.helpers.parseLinkLabel(e,o),o>=0?i=e.src.slice(c,o++):o=f+1):o=f+1,i||(i=e.src.slice(d,f)),s=e.env.references[Zs(i)],!s)return e.pos=u,!1;h=s.href,a=s.title}if(!t){r=e.src.slice(d,f);const t=[];e.md.inline.parse(r,e.md,e.env,t);const n=e.push("image","img",0),i=[["src",h],["alt",""]];n.attrs=i,n.children=t,n.content=r,a&&i.push(["title",a])}return e.pos=o,e.posMax=p,!0}],["autolink",function(e,t){let n=e.pos;if(60!==e.src.charCodeAt(n))return!1;const r=e.pos,i=e.posMax;for(;;){if(++n>=i)return!1;const t=e.src.charCodeAt(n);if(60===t)return!1;if(62===t)break}const o=e.src.slice(r+1,n);if(Vl.test(o)){const n=e.md.normalizeLink(o);if(!e.md.validateLink(n))return!1;if(!t){const t=e.push("link_open","a",1);t.attrs=[["href",n]],t.markup="autolink",t.info="auto";e.push("text","",0).content=e.md.normalizeLinkText(o);const r=e.push("link_close","a",-1);r.markup="autolink",r.info="auto"}return e.pos+=o.length+2,!0}if($l.test(o)){const n=e.md.normalizeLink("mailto:"+o);if(!e.md.validateLink(n))return!1;if(!t){const t=e.push("link_open","a",1);t.attrs=[["href",n]],t.markup="autolink",t.info="auto";e.push("text","",0).content=e.md.normalizeLinkText(o);const r=e.push("link_close","a",-1);r.markup="autolink",r.info="auto"}return e.pos+=o.length+2,!0}return!1}],["html_inline",function(e,t){if(!e.md.options.html)return!1;const n=e.posMax,r=e.pos;if(60!==e.src.charCodeAt(r)||r+2>=n)return!1;const i=e.src.charCodeAt(r+1);if(33!==i&&63!==i&&47!==i&&!function(e){const t=32|e;return t>=97&&t<=122}(i))return!1;const o=e.src.slice(r).match(Al);if(!o)return!1;if(!t){const t=e.push("html_inline","",0);t.content=o[0],s=t.content,/^<a[>\s]/i.test(s)&&e.linkLevel++,function(e){return/^<\/a\s*>/i.test(e)}(t.content)&&e.linkLevel--}var s;return e.pos+=o[0].length,!0}],["entity",function(e,t){const n=e.pos,r=e.posMax;if(38!==e.src.charCodeAt(n))return!1;if(n+1>=r)return!1;if(35===e.src.charCodeAt(n+1)){const r=e.src.slice(n).match(jl);if(r){if(!t){const t="x"===r[1][0].toLowerCase()?parseInt(r[1].slice(1),16):parseInt(r[1],10),n=e.push("text_special","",0);n.content=Ts(t)?Rs(t):Rs(65533),n.markup=r[0],n.info="entity"}return e.pos+=r[0].length,!0}}else{const r=e.src.slice(n).match(Jl);if(r){const n=As(r[0]);if(n!==r[0]){if(!t){const t=e.push("text_special","",0);t.content=n,t.markup=r[0],t.info="entity"}return e.pos+=r[0].length,!0}}}return!1}]],Ul=[["balance_pairs",function(e){const t=e.tokens_meta,n=e.tokens_meta.length;Wl(e.delimiters);for(let e=0;e<n;e++)t[e]&&t[e].delimiters&&Wl(t[e].delimiters)}],["strikethrough",Pl.postProcess],["emphasis",ql.postProcess],["fragments_join",function(e){let t,n,r=0;const i=e.tokens,o=e.tokens.length;for(t=n=0;t<o;t++)i[t].nesting<0&&r--,i[t].level=r,i[t].nesting>0&&r++,"text"===i[t].type&&t+1<o&&"text"===i[t+1].type?i[t+1].content=i[t].content+i[t+1].content:(t!==n&&(i[n]=i[t]),n++);t!==n&&(i.length=n)}]];function Kl(){this.ruler=new tl;for(let e=0;e<Hl.length;e++)this.ruler.push(Hl[e][0],Hl[e][1]);this.ruler2=new tl;for(let e=0;e<Ul.length;e++)this.ruler2.push(Ul[e][0],Ul[e][1])}function Zl(e){return Array.prototype.slice.call(arguments,1).forEach(function(t){t&&Object.keys(t).forEach(function(n){e[n]=t[n]})}),e}function Gl(e){return Object.prototype.toString.call(e)}function Ql(e){return"[object Function]"===Gl(e)}function Xl(e){return e.replace(/[.?*+^$[\]\\(){}|-]/g,"\\$&")}Kl.prototype.skipToken=function(e){const t=e.pos,n=this.ruler.getRules(""),r=n.length,i=e.md.options.maxNesting,o=e.cache;if(void 0!==o[t])return void(e.pos=o[t]);let s=!1;if(e.level<i){for(let i=0;i<r;i++)if(e.level++,s=n[i](e,!0),e.level--,s){if(t>=e.pos)throw new Error("inline rule didn't increment state.pos");break}}else e.pos=e.posMax;s||e.pos++,o[t]=e.pos},Kl.prototype.tokenize=function(e){const t=this.ruler.getRules(""),n=t.length,r=e.posMax,i=e.md.options.maxNesting;for(;e.pos<r;){const o=e.pos;let s=!1;if(e.level<i)for(let r=0;r<n;r++)if(s=t[r](e,!1),s){if(o>=e.pos)throw new Error("inline rule didn't increment state.pos");break}if(s){if(e.pos>=r)break}else e.pending+=e.src[e.pos++]}e.pending&&e.pushPending()},Kl.prototype.parse=function(e,t,n,r){const i=new this.State(e,t,n,r);this.tokenize(i);const o=this.ruler2.getRules(""),s=o.length;for(let e=0;e<s;e++)o[e](i)},Kl.prototype.State=Tl;const Yl={fuzzyLink:!0,fuzzyEmail:!0,fuzzyIP:!1};const ea={"http:":{validate:function(e,t,n){const r=e.slice(t);return n.re.http||(n.re.http=new RegExp("^\\/\\/"+n.re.src_auth+n.re.src_host_port_strict+n.re.src_path,"i")),n.re.http.test(r)?r.match(n.re.http)[0].length:0}},"https:":"http:","ftp:":"http:","//":{validate:function(e,t,n){const r=e.slice(t);return n.re.no_http||(n.re.no_http=new RegExp("^"+n.re.src_auth+"(?:localhost|(?:(?:"+n.re.src_domain+")\\.)+"+n.re.src_domain_root+")"+n.re.src_port+n.re.src_host_terminator+n.re.src_path,"i")),n.re.no_http.test(r)?t>=3&&":"===e[t-3]||t>=3&&"/"===e[t-3]?0:r.match(n.re.no_http)[0].length:0}},"mailto:":{validate:function(e,t,n){const r=e.slice(t);return n.re.mailto||(n.re.mailto=new RegExp("^"+n.re.src_email_name+"@"+n.re.src_host_strict,"i")),n.re.mailto.test(r)?r.match(n.re.mailto)[0].length:0}}},ta="biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|рф".split("|");function na(e){const t=e.re=function(e){const t={};e=e||{},t.src_Any=ls.source,t.src_Cc=as.source,t.src_Z=us.source,t.src_P=cs.source,t.src_ZPCc=[t.src_Z,t.src_P,t.src_Cc].join("|"),t.src_ZCc=[t.src_Z,t.src_Cc].join("|");const n="[><|]";return t.src_pseudo_letter="(?:(?![><|]|"+t.src_ZPCc+")"+t.src_Any+")",t.src_ip4="(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",t.src_auth="(?:(?:(?!"+t.src_ZCc+"|[@/\\[\\]()]).)+@)?",t.src_port="(?::(?:6(?:[0-4]\\d{3}|5(?:[0-4]\\d{2}|5(?:[0-2]\\d|3[0-5])))|[1-5]?\\d{1,4}))?",t.src_host_terminator="(?=$|[><|]|"+t.src_ZPCc+")(?!"+(e["---"]?"-(?!--)|":"-|")+"_|:\\d|\\.-|\\.(?!$|"+t.src_ZPCc+"))",t.src_path="(?:[/?#](?:(?!"+t.src_ZCc+"|"+n+"|[()[\\]{}.,\"'?!\\-;]).|\\[(?:(?!"+t.src_ZCc+"|\\]).)*\\]|\\((?:(?!"+t.src_ZCc+"|[)]).)*\\)|\\{(?:(?!"+t.src_ZCc+'|[}]).)*\\}|\\"(?:(?!'+t.src_ZCc+'|["]).)+\\"|\\\'(?:(?!'+t.src_ZCc+"|[']).)+\\'|\\'(?="+t.src_pseudo_letter+"|[-])|\\.{2,}[a-zA-Z0-9%/&]|\\.(?!"+t.src_ZCc+"|[.]|$)|"+(e["---"]?"\\-(?!--(?:[^-]|$))(?:-*)|":"\\-+|")+",(?!"+t.src_ZCc+"|$)|;(?!"+t.src_ZCc+"|$)|\\!+(?!"+t.src_ZCc+"|[!]|$)|\\?(?!"+t.src_ZCc+"|[?]|$))+|\\/)?",t.src_email_name='[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]*',t.src_xn="xn--[a-z0-9\\-]{1,59}",t.src_domain_root="(?:"+t.src_xn+"|"+t.src_pseudo_letter+"{1,63})",t.src_domain="(?:"+t.src_xn+"|(?:"+t.src_pseudo_letter+")|(?:"+t.src_pseudo_letter+"(?:-|"+t.src_pseudo_letter+"){0,61}"+t.src_pseudo_letter+"))",t.src_host="(?:(?:(?:(?:"+t.src_domain+")\\.)*"+t.src_domain+"))",t.tpl_host_fuzzy="(?:"+t.src_ip4+"|(?:(?:(?:"+t.src_domain+")\\.)+(?:%TLDS%)))",t.tpl_host_no_ip_fuzzy="(?:(?:(?:"+t.src_domain+")\\.)+(?:%TLDS%))",t.src_host_strict=t.src_host+t.src_host_terminator,t.tpl_host_fuzzy_strict=t.tpl_host_fuzzy+t.src_host_terminator,t.src_host_port_strict=t.src_host+t.src_port+t.src_host_terminator,t.tpl_host_port_fuzzy_strict=t.tpl_host_fuzzy+t.src_port+t.src_host_terminator,t.tpl_host_port_no_ip_fuzzy_strict=t.tpl_host_no_ip_fuzzy+t.src_port+t.src_host_terminator,t.tpl_host_fuzzy_test="localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:"+t.src_ZPCc+"|>|$))",t.tpl_email_fuzzy='(^|[><|]|"|\\(|'+t.src_ZCc+")("+t.src_email_name+"@"+t.tpl_host_fuzzy_strict+")",t.tpl_link_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`||]|"+t.src_ZPCc+"))((?![$+<=>^`||])"+t.tpl_host_port_fuzzy_strict+t.src_path+")",t.tpl_link_no_ip_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`||]|"+t.src_ZPCc+"))((?![$+<=>^`||])"+t.tpl_host_port_no_ip_fuzzy_strict+t.src_path+")",t}(e.__opts__),n=e.__tlds__.slice();function r(e){return e.replace("%TLDS%",t.src_tlds)}e.onCompile(),e.__tlds_replaced__||n.push("a[cdefgilmnoqrstuwxz]|b[abdefghijmnorstvwyz]|c[acdfghiklmnoruvwxyz]|d[ejkmoz]|e[cegrstu]|f[ijkmor]|g[abdefghilmnpqrstuwy]|h[kmnrtu]|i[delmnoqrst]|j[emop]|k[eghimnprwyz]|l[abcikrstuvy]|m[acdeghklmnopqrstuvwxyz]|n[acefgilopruz]|om|p[aefghklmnrstwy]|qa|r[eosuw]|s[abcdeghijklmnortuvxyz]|t[cdfghjklmnortvwz]|u[agksyz]|v[aceginu]|w[fs]|y[et]|z[amw]"),n.push(t.src_xn),t.src_tlds=n.join("|"),t.email_fuzzy=RegExp(r(t.tpl_email_fuzzy),"i"),t.link_fuzzy=RegExp(r(t.tpl_link_fuzzy),"i"),t.link_no_ip_fuzzy=RegExp(r(t.tpl_link_no_ip_fuzzy),"i"),t.host_fuzzy_test=RegExp(r(t.tpl_host_fuzzy_test),"i");const i=[];function o(e,t){throw new Error('(LinkifyIt) Invalid schema "'+e+'": '+t)}e.__compiled__={},Object.keys(e.__schemas__).forEach(function(t){const n=e.__schemas__[t];if(null===n)return;const r={validate:null,link:null};if(e.__compiled__[t]=r,"[object Object]"===Gl(n))return!function(e){return"[object RegExp]"===Gl(e)}(n.validate)?Ql(n.validate)?r.validate=n.validate:o(t,n):r.validate=function(e){return function(t,n){const r=t.slice(n);return e.test(r)?r.match(e)[0].length:0}}(n.validate),void(Ql(n.normalize)?r.normalize=n.normalize:n.normalize?o(t,n):r.normalize=function(e,t){t.normalize(e)});!function(e){return"[object String]"===Gl(e)}(n)?o(t,n):i.push(t)}),i.forEach(function(t){e.__compiled__[e.__schemas__[t]]&&(e.__compiled__[t].validate=e.__compiled__[e.__schemas__[t]].validate,e.__compiled__[t].normalize=e.__compiled__[e.__schemas__[t]].normalize)}),e.__compiled__[""]={validate:null,normalize:function(e,t){t.normalize(e)}};const s=Object.keys(e.__compiled__).filter(function(t){return t.length>0&&e.__compiled__[t]}).map(Xl).join("|");e.re.schema_test=RegExp("(^|(?!_)(?:[><|]|"+t.src_ZPCc+"))("+s+")","i"),e.re.schema_search=RegExp("(^|(?!_)(?:[><|]|"+t.src_ZPCc+"))("+s+")","ig"),e.re.schema_at_start=RegExp("^"+e.re.schema_search.source,"i"),e.re.pretest=RegExp("("+e.re.schema_test.source+")|("+e.re.host_fuzzy_test.source+")|@","i"),function(e){e.__index__=-1,e.__text_cache__=""}(e)}function ra(e,t){const n=e.__index__,r=e.__last_index__,i=e.__text_cache__.slice(n,r);this.schema=e.__schema__.toLowerCase(),this.index=n+t,this.lastIndex=r+t,this.raw=i,this.text=i,this.url=i}function ia(e,t){const n=new ra(e,t);return e.__compiled__[n.schema].normalize(n,e),n}function oa(e,t){if(!(this instanceof oa))return new oa(e,t);var n;t||(n=e,Object.keys(n||{}).reduce(function(e,t){return e||Yl.hasOwnProperty(t)},!1)&&(t=e,e={})),this.__opts__=Zl({},Yl,t),this.__index__=-1,this.__last_index__=-1,this.__schema__="",this.__text_cache__="",this.__schemas__=Zl({},ea,e),this.__compiled__={},this.__tlds__=ta,this.__tlds_replaced__=!1,this.re={},na(this)}oa.prototype.add=function(e,t){return this.__schemas__[e]=t,na(this),this},oa.prototype.set=function(e){return this.__opts__=Zl(this.__opts__,e),this},oa.prototype.test=function(e){if(this.__text_cache__=e,this.__index__=-1,!e.length)return!1;let t,n,r,i,o,s,l,a,c;if(this.re.schema_test.test(e))for(l=this.re.schema_search,l.lastIndex=0;null!==(t=l.exec(e));)if(i=this.testSchemaAt(e,t[2],l.lastIndex),i){this.__schema__=t[2],this.__index__=t.index+t[1].length,this.__last_index__=t.index+t[0].length+i;break}return this.__opts__.fuzzyLink&&this.__compiled__["http:"]&&(a=e.search(this.re.host_fuzzy_test),a>=0&&(this.__index__<0||a<this.__index__)&&null!==(n=e.match(this.__opts__.fuzzyIP?this.re.link_fuzzy:this.re.link_no_ip_fuzzy))&&(o=n.index+n[1].length,(this.__index__<0||o<this.__index__)&&(this.__schema__="",this.__index__=o,this.__last_index__=n.index+n[0].length))),this.__opts__.fuzzyEmail&&this.__compiled__["mailto:"]&&(c=e.indexOf("@"),c>=0&&null!==(r=e.match(this.re.email_fuzzy))&&(o=r.index+r[1].length,s=r.index+r[0].length,(this.__index__<0||o<this.__index__||o===this.__index__&&s>this.__last_index__)&&(this.__schema__="mailto:",this.__index__=o,this.__last_index__=s))),this.__index__>=0},oa.prototype.pretest=function(e){return this.re.pretest.test(e)},oa.prototype.testSchemaAt=function(e,t,n){return this.__compiled__[t.toLowerCase()]?this.__compiled__[t.toLowerCase()].validate(e,n,this):0},oa.prototype.match=function(e){const t=[];let n=0;this.__index__>=0&&this.__text_cache__===e&&(t.push(ia(this,n)),n=this.__last_index__);let r=n?e.slice(n):e;for(;this.test(r);)t.push(ia(this,n)),r=r.slice(this.__last_index__),n+=this.__last_index__;return t.length?t:null},oa.prototype.matchAtStart=function(e){if(this.__text_cache__=e,this.__index__=-1,!e.length)return null;const t=this.re.schema_at_start.exec(e);if(!t)return null;const n=this.testSchemaAt(e,t[2],t[0].length);return n?(this.__schema__=t[2],this.__index__=t.index+t[1].length,this.__last_index__=t.index+t[0].length+n,ia(this,0)):null},oa.prototype.tlds=function(e,t){return e=Array.isArray(e)?e:[e],t?(this.__tlds__=this.__tlds__.concat(e).sort().filter(function(e,t,n){return e!==n[t-1]}).reverse(),na(this),this):(this.__tlds__=e.slice(),this.__tlds_replaced__=!0,na(this),this)},oa.prototype.normalize=function(e){e.schema||(e.url="http://"+e.url),"mailto:"!==e.schema||/^mailto:/i.test(e.url)||(e.url="mailto:"+e.url)},oa.prototype.onCompile=function(){};const sa=2147483647,la=36,aa=/^xn--/,ca=/[^\0-\x7F]/,ha=/[\x2E\u3002\uFF0E\uFF61]/g,ua={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},pa=Math.floor,da=String.fromCharCode;function fa(e){throw new RangeError(ua[e])}function ma(e,t){const n=e.split("@");let r="";n.length>1&&(r=n[0]+"@",e=n[1]);const i=function(e,t){const n=[];let r=e.length;for(;r--;)n[r]=t(e[r]);return n}((e=e.replace(ha,".")).split("."),t).join(".");return r+i}function ga(e){const t=[];let n=0;const r=e.length;for(;n<r;){const i=e.charCodeAt(n++);if(i>=55296&&i<=56319&&n<r){const r=e.charCodeAt(n++);56320==(64512&r)?t.push(((1023&i)<<10)+(1023&r)+65536):(t.push(i),n--)}else t.push(i)}return t}const ya=function(e){return e>=48&&e<58?e-48+26:e>=65&&e<91?e-65:e>=97&&e<123?e-97:la},ka=function(e,t){return e+22+75*(e<26)-((0!=t)<<5)},ba=function(e,t,n){let r=0;for(e=n?pa(e/700):e>>1,e+=pa(e/t);e>455;r+=la)e=pa(e/35);return pa(r+36*e/(e+38))},wa=function(e){const t=[],n=e.length;let r=0,i=128,o=72,s=e.lastIndexOf("-");s<0&&(s=0);for(let n=0;n<s;++n)e.charCodeAt(n)>=128&&fa("not-basic"),t.push(e.charCodeAt(n));for(let l=s>0?s+1:0;l<n;){const s=r;for(let t=1,i=la;;i+=la){l>=n&&fa("invalid-input");const s=ya(e.charCodeAt(l++));s>=la&&fa("invalid-input"),s>pa((sa-r)/t)&&fa("overflow"),r+=s*t;const a=i<=o?1:i>=o+26?26:i-o;if(s<a)break;const c=la-a;t>pa(sa/c)&&fa("overflow"),t*=c}const a=t.length+1;o=ba(r-s,a,0==s),pa(r/a)>sa-i&&fa("overflow"),i+=pa(r/a),r%=a,t.splice(r++,0,i)}return String.fromCodePoint(...t)},Ca=function(e){const t=[],n=(e=ga(e)).length;let r=128,i=0,o=72;for(const n of e)n<128&&t.push(da(n));const s=t.length;let l=s;for(s&&t.push("-");l<n;){let n=sa;for(const t of e)t>=r&&t<n&&(n=t);const a=l+1;n-r>pa((sa-i)/a)&&fa("overflow"),i+=(n-r)*a,r=n;for(const n of e)if(n<r&&++i>sa&&fa("overflow"),n===r){let e=i;for(let n=la;;n+=la){const r=n<=o?1:n>=o+26?26:n-o;if(e<r)break;const i=e-r,s=la-r;t.push(da(ka(r+i%s,0))),e=pa(i/s)}t.push(da(ka(e,0))),o=ba(i,a,l===s),i=0,++l}++i,++r}return t.join("")},xa=function(e){return ma(e,function(e){return ca.test(e)?"xn--"+Ca(e):e})},Da=function(e){return ma(e,function(e){return aa.test(e)?wa(e.slice(4).toLowerCase()):e})};const va={default:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:100},components:{core:{},block:{},inline:{}}},zero:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["paragraph"]},inline:{rules:["text"],rules2:["balance_pairs","fragments_join"]}}},commonmark:{options:{html:!0,xhtmlOut:!0,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["blockquote","code","fence","heading","hr","html_block","lheading","list","reference","paragraph"]},inline:{rules:["autolink","backticks","emphasis","entity","escape","html_inline","image","link","newline","text"],rules2:["balance_pairs","emphasis","fragments_join"]}}}},_a=/^(vbscript|javascript|file|data):/,Sa=/^data:image\/(gif|png|jpeg|webp);/;function Ea(e){const t=e.trim().toLowerCase();return!_a.test(t)||Sa.test(t)}const Aa=["http:","https:","mailto:"];function Ma(e){const t=rs(e,!0);if(t.hostname&&(!t.protocol||Aa.indexOf(t.protocol)>=0))try{t.hostname=xa(t.hostname)}catch(e){}return jo(Jo(t))}function Oa(e){const t=rs(e,!0);if(t.hostname&&(!t.protocol||Aa.indexOf(t.protocol)>=0))try{t.hostname=Da(t.hostname)}catch(e){}return $o(Jo(t),$o.defaultChars+"%")}function Na(e,t){if(!(this instanceof Na))return new Na(e,t);t||Ms(e)||(t=e||{},e="default"),this.inline=new Kl,this.block=new Fl,this.core=new wl,this.renderer=new el,this.linkify=new oa,this.validateLink=Ea,this.normalizeLink=Ma,this.normalizeLinkText=Oa,this.utils=Qs,this.helpers=Ns({},Xs),this.options={},this.configure(e),t&&this.set(t)}Na.prototype.set=function(e){return Ns(this.options,e),this},Na.prototype.configure=function(e){const t=this;if(Ms(e)){const t=e;if(!(e=va[t]))throw new Error('Wrong `markdown-it` preset "'+t+'", check name')}if(!e)throw new Error("Wrong `markdown-it` preset, can't be empty");return e.options&&t.set(e.options),e.components&&Object.keys(e.components).forEach(function(n){e.components[n].rules&&t[n].ruler.enableOnly(e.components[n].rules),e.components[n].rules2&&t[n].ruler2.enableOnly(e.components[n].rules2)}),this},Na.prototype.enable=function(e,t){let n=[];Array.isArray(e)||(e=[e]),["core","block","inline"].forEach(function(t){n=n.concat(this[t].ruler.enable(e,!0))},this),n=n.concat(this.inline.ruler2.enable(e,!0));const r=e.filter(function(e){return n.indexOf(e)<0});if(r.length&&!t)throw new Error("MarkdownIt. Failed to enable unknown rule(s): "+r);return this},Na.prototype.disable=function(e,t){let n=[];Array.isArray(e)||(e=[e]),["core","block","inline"].forEach(function(t){n=n.concat(this[t].ruler.disable(e,!0))},this),n=n.concat(this.inline.ruler2.disable(e,!0));const r=e.filter(function(e){return n.indexOf(e)<0});if(r.length&&!t)throw new Error("MarkdownIt. Failed to disable unknown rule(s): "+r);return this},Na.prototype.use=function(e){const t=[this].concat(Array.prototype.slice.call(arguments,1));return e.apply(e,t),this},Na.prototype.parse=function(e,t){if("string"!=typeof e)throw new Error("Input data should be a String");const n=new this.core.State(e,this,t);return this.core.process(n),n.tokens},Na.prototype.render=function(e,t){return t=t||{},this.renderer.render(this.parse(e,t),this.options,t)},Na.prototype.parseInline=function(e,t){const n=new this.core.State(e,this,t);return n.inlineMode=!0,this.core.process(n),n.tokens},Na.prototype.renderInline=function(e,t){return t=t||{},this.renderer.render(this.parseInline(e,t),this.options,t)};const Fa=new H({nodes:{doc:{content:"block+"},paragraph:{content:"inline*",group:"block",parseDOM:[{tag:"p"}],toDOM:()=>["p",0]},blockquote:{content:"block+",group:"block",parseDOM:[{tag:"blockquote"}],toDOM:()=>["blockquote",0]},horizontal_rule:{group:"block",parseDOM:[{tag:"hr"}],toDOM:()=>["div",["hr"]]},heading:{attrs:{level:{default:1}},content:"(text | image)*",group:"block",defining:!0,parseDOM:[{tag:"h1",attrs:{level:1}},{tag:"h2",attrs:{level:2}},{tag:"h3",attrs:{level:3}},{tag:"h4",attrs:{level:4}},{tag:"h5",attrs:{level:5}},{tag:"h6",attrs:{level:6}}],toDOM:e=>["h"+e.attrs.level,0]},code_block:{content:"text*",group:"block",code:!0,defining:!0,marks:"",attrs:{params:{default:""}},parseDOM:[{tag:"pre",preserveWhitespace:"full",getAttrs:e=>({params:e.getAttribute("data-params")||""})}],toDOM:e=>["pre",e.attrs.params?{"data-params":e.attrs.params}:{},["code",0]]},ordered_list:{content:"list_item+",group:"block",attrs:{order:{default:1},tight:{default:!1}},parseDOM:[{tag:"ol",getAttrs:e=>({order:e.hasAttribute("start")?+e.getAttribute("start"):1,tight:e.hasAttribute("data-tight")})}],toDOM:e=>["ol",{start:1==e.attrs.order?null:e.attrs.order,"data-tight":e.attrs.tight?"true":null},0]},bullet_list:{content:"list_item+",group:"block",attrs:{tight:{default:!1}},parseDOM:[{tag:"ul",getAttrs:e=>({tight:e.hasAttribute("data-tight")})}],toDOM:e=>["ul",{"data-tight":e.attrs.tight?"true":null},0]},list_item:{content:"block+",defining:!0,parseDOM:[{tag:"li"}],toDOM:()=>["li",0]},text:{group:"inline"},image:{inline:!0,attrs:{src:{},alt:{default:null},title:{default:null}},group:"inline",draggable:!0,parseDOM:[{tag:"img[src]",getAttrs:e=>({src:e.getAttribute("src"),title:e.getAttribute("title"),alt:e.getAttribute("alt")})}],toDOM:e=>["img",e.attrs]},hard_break:{inline:!0,group:"inline",selectable:!1,parseDOM:[{tag:"br"}],toDOM:()=>["br"]}},marks:{em:{parseDOM:[{tag:"i"},{tag:"em"},{style:"font-style=italic"},{style:"font-style=normal",clearMark:e=>"em"==e.type.name}],toDOM:()=>["em"]},strong:{parseDOM:[{tag:"strong"},{tag:"b",getAttrs:e=>"normal"!=e.style.fontWeight&&null},{style:"font-weight=400",clearMark:e=>"strong"==e.type.name},{style:"font-weight",getAttrs:e=>/^(bold(er)?|[5-9]\d{2,})$/.test(e)&&null}],toDOM:()=>["strong"]},link:{attrs:{href:{},title:{default:null}},inclusive:!1,parseDOM:[{tag:"a[href]",getAttrs:e=>({href:e.getAttribute("href"),title:e.getAttribute("title")})}],toDOM:e=>["a",e.attrs]},code:{code:!0,parseDOM:[{tag:"code"}],toDOM:()=>["code"]}}});class Ta{constructor(e,t){this.schema=e,this.tokenHandlers=t,this.stack=[{type:e.topNodeType,attrs:null,content:[],marks:l.none}]}top(){return this.stack[this.stack.length-1]}push(e){this.stack.length&&this.top().content.push(e)}addText(e){if(!e)return;let t,n=this.top(),r=n.content,i=r[r.length-1],o=this.schema.text(e,n.marks);i&&(t=function(e,t){if(e.isText&&t.isText&&l.sameSet(e.marks,t.marks))return e.withText(e.text+t.text)}(i,o))?r[r.length-1]=t:r.push(o)}openMark(e){let t=this.top();t.marks=e.addToSet(t.marks)}closeMark(e){let t=this.top();t.marks=e.removeFromSet(t.marks)}parseTokens(e){for(let t=0;t<e.length;t++){let n=e[t],r=this.tokenHandlers[n.type];if(!r)throw new Error("Token type `"+n.type+"` not supported by Markdown parser");r(this,n,e,t)}}addNode(e,t,n){let r=this.top(),i=e.createAndFill(t,n,r?r.marks:[]);return i?(this.push(i),i):null}openNode(e,t){this.stack.push({type:e,attrs:t,content:[],marks:l.none})}closeNode(){let e=this.stack.pop();return this.addNode(e.type,e.attrs,e.content)}}function Ra(e,t,n,r){return e.getAttrs?e.getAttrs(t,n,r):e.attrs instanceof Function?e.attrs(t):e.attrs}function za(e,t){return e.noCloseToken||"code_inline"==t||"code_block"==t||"fence"==t}function Ia(e){return"\n"==e[e.length-1]?e.slice(0,e.length-1):e}function Ba(){}function Pa(e,t){for(;++t<e.length;)if("list_item_open"!=e[t].type)return e[t].hidden;return!1}const La=new class{constructor(e,t,n){this.schema=e,this.tokenizer=t,this.tokens=n,this.tokenHandlers=function(e,t){let n=Object.create(null);for(let r in t){let i=t[r];if(i.block){let t=e.nodeType(i.block);za(i,r)?n[r]=(e,n,r,o)=>{e.openNode(t,Ra(i,n,r,o)),e.addText(Ia(n.content)),e.closeNode()}:(n[r+"_open"]=(e,n,r,o)=>e.openNode(t,Ra(i,n,r,o)),n[r+"_close"]=e=>e.closeNode())}else if(i.node){let t=e.nodeType(i.node);n[r]=(e,n,r,o)=>e.addNode(t,Ra(i,n,r,o))}else if(i.mark){let t=e.marks[i.mark];za(i,r)?n[r]=(e,n,r,o)=>{e.openMark(t.create(Ra(i,n,r,o))),e.addText(Ia(n.content)),e.closeMark(t)}:(n[r+"_open"]=(e,n,r,o)=>e.openMark(t.create(Ra(i,n,r,o))),n[r+"_close"]=e=>e.closeMark(t))}else{if(!i.ignore)throw new RangeError("Unrecognized parsing spec "+JSON.stringify(i));za(i,r)?n[r]=Ba:(n[r+"_open"]=Ba,n[r+"_close"]=Ba)}}return n.text=(e,t)=>e.addText(t.content),n.inline=(e,t)=>e.parseTokens(t.children),n.softbreak=n.softbreak||(e=>e.addText(" ")),n}(e,n)}parse(e,t={}){let n,r=new Ta(this.schema,this.tokenHandlers);r.parseTokens(this.tokenizer.parse(e,t));do{n=r.closeNode()}while(r.stack.length);return n||this.schema.topNodeType.createAndFill()}}(Fa,Na("commonmark",{html:!1}),{blockquote:{block:"blockquote"},paragraph:{block:"paragraph"},list_item:{block:"list_item"},bullet_list:{block:"bullet_list",getAttrs:(e,t,n)=>({tight:Pa(t,n)})},ordered_list:{block:"ordered_list",getAttrs:(e,t,n)=>({order:+e.attrGet("start")||1,tight:Pa(t,n)})},heading:{block:"heading",getAttrs:e=>({level:+e.tag.slice(1)})},code_block:{block:"code_block",noCloseToken:!0},fence:{block:"code_block",getAttrs:e=>({params:e.info||""}),noCloseToken:!0},hr:{node:"horizontal_rule"},image:{node:"image",getAttrs:e=>({src:e.attrGet("src"),title:e.attrGet("title")||null,alt:e.children[0]&&e.children[0].content||null})},hardbreak:{node:"hard_break"},em:{mark:"em"},strong:{mark:"strong"},link:{mark:"link",getAttrs:e=>({href:e.attrGet("href"),title:e.attrGet("title")||null})},code_inline:{mark:"code",noCloseToken:!0}});function qa(e,{allowSpaces:t=!1}){return n=>{const r=new RegExp(`\\s${e}$`),i=t?new RegExp(`${e}.*?(?=\\s${e}|$)`,"g"):new RegExp(`(?:^)?${e}[^\\s${e}]*`,"g"),o=n.before(),s=n.end(),l=n.doc.textBetween(o,s,"\0","\0");let a;for(;a=i.exec(l);){const e=a.input.slice(Math.max(0,a.index-1),a.index);if(!/^[\s\0]?$/.test(e))continue;const i=a.index+n.start();let o=i+a[0].length;if(t&&r.test(l.slice(o-1,o+1))&&(a[0]+=" ",o++),i<n.pos&&o>=n.pos)return{range:{from:i,to:o},text:a[0]}}}}function $a({matcher:e=qa("#"),suggestionClass:t="ProseMirror-suggestion",onEnter:n=()=>!1,onChange:r=()=>!1,onExit:i=()=>!1,onKeyDown:o=()=>!1,debug:s=!1}){return new mt({key:new kt("suggestions"),view(){return{update:(e,t)=>{const o=this.key.getState(t),s=this.key.getState(e.state),l=o.active&&s.active&&o.range.from!==s.range.from,a=!o.active&&s.active,c=o.active&&!s.active,h=!a&&!c&&o.text!==s.text;(c||l)&&i({view:e,range:o.range,text:o.text}),h&&!l&&r({view:e,range:s.range,text:s.text}),(a||l)&&n({view:e,range:s.range,text:s.text})}}},state:{init:()=>({active:!1,range:{},text:null}),apply(t,n){const{selection:r}=t,i={...n};if(r.from===r.to){(r.from<n.range.from||r.from>n.range.to)&&(i.active=!1);const t=r.$from,o=e(t);o?(i.active=!0,i.range=o.range,i.text=o.text):i.active=!1}else i.active=!1;return i.active||(i.range={},i.text=null),i}},props:{handleKeyDown(e,t){const{active:n}=this.getState(e.state);return!!n&&o({view:e,event:t})},decorations(e){const{active:n,range:r}=this.getState(e);return n?ao.create(e.doc,[oo.inline(r.from,r.to,{nodeName:"span",class:t,style:s?"background: rgba(0, 0, 255, 0.05); color: blue; border: 2px solid blue;":null})]):null}}})}export{K as DOMParser,ie as DOMSerializer,oo as Decoration,ao as DecorationSet,dt as EditorState,Ro as EditorView,mt as Plugin,kt as PluginKey,H as Schema,et as TextSelection,zt as baseKeymap,xn as basicSchema,La as defaultMarkdownParser,Qt as history,pn as keymap,en as redo,$a as suggestionsPlugin,qa as triggerCharacter,Yt as undo}; diff --git a/third_party/js/prosemirror/rollup.config.mjs b/third_party/js/prosemirror/rollup.config.mjs @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import terser from "@rollup/plugin-terser"; +import nodeResolve from "@rollup/plugin-node-resolve"; +import commonjs from "@rollup/plugin-commonjs"; +import json from "@rollup/plugin-json"; + +export default function (commandLineArgs = {}) { + const plugins = [ + nodeResolve({ + browser: true, + dedupe: [ + "prosemirror-commands", + "prosemirror-history", + "prosemirror-keymap", + "prosemirror-markdown", + "prosemirror-model", + "prosemirror-schema-basic", + "prosemirror-state", + "prosemirror-transform", + "prosemirror-view", + ], + }), + commonjs(), + json(), + ]; + + if (commandLineArgs.configMinify) { + plugins.push(terser()); + } + + return { + input: "bundle_entry.mjs", + output: { + file: "prosemirror.bundle.mjs", + format: "esm", + sourcemap: false, + }, + plugins, + treeshake: true, + }; +} diff --git a/toolkit/content/license.html b/toolkit/content/license.html @@ -129,6 +129,7 @@ #endif <li><a href="about:license#praton">praton License</a></li> <li><a href="about:license#praton1">praton and inet_ntop License</a></li> + <li><a href="about:license#prosemirror">ProseMirror License</a></li> <li><a href="about:license#qcms">qcms License</a></li> <li><a href="about:license#qrcode-generator">QR Code Generator License</a></li> <li><a href="about:license#react">React License</a></li> @@ -4738,6 +4739,79 @@ product. <tr> <td> + <h1><a id="prosemirror"></a>ProseMirror Licenses</h1> + </td> + <td> + <p>This entry applies to all files in <code>third_party/js/prosemirror/</code> and its subdirectories. Most ProseMirror packages are licensed under the MIT license, except where noted below.</p> + <ul> + <li> + MIT License: + <ul> + <li><code>third_party/js/prosemirror/prosemirror-commands</code></li> + <li><code>third_party/js/prosemirror/prosemirror-history</code></li> + <li><code>third_party/js/prosemirror/prosemirror-keymap</code></li> + <li><code>third_party/js/prosemirror/prosemirror-model</code></li> + <li><code>third_party/js/prosemirror/prosemirror-schema-basic</code></li> + <li><code>third_party/js/prosemirror/prosemirror-state</code></li> + <li><code>third_party/js/prosemirror/prosemirror-transform</code></li> + <li><code>third_party/js/prosemirror/prosemirror-view</code></li> + </ul> + </li> + <li> + Apache License 2.0: + <ul> + <li><code>third_party/js/prosemirror/prosemirror-suggestions</code></li> + </ul> + </li> + </ul> + </td> + <td> + <pre> + MIT License (applies to core and non-core ProseMirror packages) + + Copyright (C) 2015-2017 by Marijn Haverbeke <marijn@haverbeke.berlin> and others + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + --- + + Apache License 2.0 (applies to third-party package <code>third_party/js/prosemirror/prosemirror-suggestions</code>): + + Copyright 2017 Quartzy, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + </pre> + </td> + </tr> + + <tr> + <td> <h1><a id="qrcode-generator"></a>QR Code Generator License</h1> </td> <td>