diff --git a/.github/workflows/deploy-latest.yml b/.github/workflows/deploy-latest.yml index cf9b25a7..3fc83438 100644 --- a/.github/workflows/deploy-latest.yml +++ b/.github/workflows/deploy-latest.yml @@ -14,46 +14,24 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - name: Check deployment prerequisites - id: deployment-prerequisites - env: - GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} - LATEST_DATASETTE_SECRET: ${{ secrets.LATEST_DATASETTE_SECRET }} - run: | - missing=() - for variable in GCP_SA_KEY LATEST_DATASETTE_SECRET; do - if [[ -z "${!variable:-}" ]]; then - missing+=("$variable") - fi - done - if (( ${#missing[@]} )); then - echo "::notice::Skipping deployment because required environment variables are missing: ${missing[*]}" - echo "available=false" >> "$GITHUB_OUTPUT" - else - echo "available=true" >> "$GITHUB_OUTPUT" - fi - name: Check out datasette - if: ${{ steps.deployment-prerequisites.outputs.available == 'true' }} uses: actions/checkout@v7 - name: Set up Python - if: ${{ steps.deployment-prerequisites.outputs.available == 'true' }} uses: actions/setup-python@v6 with: python-version: "3.13" cache: pip - name: Install Python dependencies - if: ${{ steps.deployment-prerequisites.outputs.available == 'true' }} run: | python -m pip install --upgrade pip python -m pip install . --group dev - python -m pip install sphinx-to-sqlite==0.1a1 "s3-credentials>=0.17" + python -m pip install sphinx-to-sqlite==0.1a1 - name: Run tests - if: ${{ steps.deployment-prerequisites.outputs.available == 'true' && github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' }} run: | pytest -n auto -m "not serial" pytest -m "serial" - name: Build fixtures.db and other files needed to deploy the demo - if: ${{ steps.deployment-prerequisites.outputs.available == 'true' }} run: |- python tests/fixtures.py \ fixtures.db \ @@ -61,18 +39,14 @@ jobs: fixtures-metadata.json \ plugins \ --extra-db-filename extra_database.db - # Package the config with the plugins, excluding test-only plugin secrets - # that reference temporary files outside the deployed container. - jq 'del(.plugins)' fixtures-config.json > plugins/fixtures-config.json - name: Build docs.db - if: ${{ steps.deployment-prerequisites.outputs.available == 'true' && github.ref == 'refs/heads/main' }} + if: ${{ github.ref == 'refs/heads/main' }} run: |- cd docs DISABLE_SPHINX_INLINE_TABS=1 sphinx-build -b xml . _build sphinx-to-sqlite ../docs.db _build cd .. - name: Set up the alternate-route demo - if: ${{ steps.deployment-prerequisites.outputs.available == 'true' }} run: | echo ' from datasette import hookimpl @@ -84,7 +58,6 @@ jobs: ' > plugins/alternative_route.py cp fixtures.db fixtures2.db - name: And the counters writable stored query demo - if: ${{ steps.deployment-prerequisites.outputs.available == 'true' }} run: | cat > plugins/counters.py <=0.2.2' \ --service "datasette-latest$SUFFIX" \ --secret $LATEST_DATASETTE_SECRET - - name: Upload latest documentation database to S3 (only for main) - if: ${{ steps.deployment-prerequisites.outputs.available == 'true' && github.ref == 'refs/heads/main' }} - env: - AWS_ACCESS_KEY_ID: ${{ secrets.S3_DATASETTE_DOCS_ACCESS_KEY }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_DATASETTE_DOCS_SECRET_KEY }} + - name: Deploy to docs as well (only for main) + if: ${{ github.ref == 'refs/heads/main' }} run: |- - # Keep development documentation separate from the stable release database. - s3-credentials put-object datasette-docs latest/docs.db docs.db \ - --content-type application/octet-stream + # Deploy docs.db to a different service + datasette publish cloudrun docs.db \ + --branch=$GITHUB_SHA \ + --version-note=$GITHUB_SHA \ + --extra-options="--setting template_debug 1" \ + --service=datasette-docs-latest diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 85369f6c..f5b8dbf6 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -2,15 +2,9 @@ name: Playwright on: push: - branches: - - main pull_request: workflow_dispatch: -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - permissions: contents: read diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml index fa7ec6aa..d92ab82b 100644 --- a/.github/workflows/prettier.yml +++ b/.github/workflows/prettier.yml @@ -1,15 +1,6 @@ name: Check JavaScript for conformance with Prettier -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} +on: [push] permissions: contents: read diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 232a34c7..21ed4c12 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,7 @@ name: Publish Python Package on: release: - types: [published] + types: [created] permissions: contents: read @@ -51,8 +51,6 @@ jobs: - name: Publish uses: pypa/gh-action-pypi-publish@release/v1 - # After the first non-prerelease 1.0 release, disable this job on 0.65.x, - # even for later 0.65 releases, so they cannot overwrite the 1.0 stable docs. deploy_static_docs: runs-on: ubuntu-latest needs: [deploy] @@ -68,20 +66,26 @@ jobs: - name: Install dependencies run: | python -m pip install . --group dev - python -m pip install sphinx-to-sqlite==0.1a1 "s3-credentials>=0.17" + python -m pip install sphinx-to-sqlite==0.1a1 - name: Build docs.db run: |- cd docs DISABLE_SPHINX_INLINE_TABS=1 sphinx-build -b xml . _build sphinx-to-sqlite ../docs.db _build cd .. - - name: Upload stable documentation database to S3 - env: - AWS_ACCESS_KEY_ID: ${{ secrets.S3_DATASETTE_DOCS_ACCESS_KEY }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_DATASETTE_DOCS_SECRET_KEY }} + - id: auth + name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + - name: Set up Cloud SDK + uses: google-github-actions/setup-gcloud@v3 + - name: Deploy stable-docs.datasette.io to Cloud Run run: |- - s3-credentials put-object datasette-docs docs.db docs.db \ - --content-type application/octet-stream + gcloud config set run/region us-central1 + gcloud config set project datasette-222320 + datasette publish cloudrun docs.db \ + --service=datasette-docs-stable deploy_docker: runs-on: ubuntu-latest diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index aa35338f..58635025 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -1,15 +1,6 @@ name: Check spelling in documentation -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} +on: [push, pull_request] permissions: contents: read diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml new file mode 100644 index 00000000..e9bd4bab --- /dev/null +++ b/.github/workflows/test-coverage.yml @@ -0,0 +1,40 @@ +name: Calculate test coverage + +on: + push: + branches: + - main + pull_request: + branches: + - main +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Check out datasette + uses: actions/checkout@v7 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + cache: 'pip' + cache-dependency-path: '**/pyproject.toml' + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install . --group dev + python -m pip install pytest-cov + - name: Run tests + run: |- + ls -lah + cat .coveragerc + pytest -m "not serial" --cov=datasette --cov-config=.coveragerc --cov-report xml:coverage.xml --cov-report term -x + ls -lah + - name: Upload coverage report + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: coverage.xml diff --git a/.github/workflows/test-pyodide.yml b/.github/workflows/test-pyodide.yml index 449855f3..5e81ed82 100644 --- a/.github/workflows/test-pyodide.yml +++ b/.github/workflows/test-pyodide.yml @@ -2,15 +2,9 @@ name: Test in Pyodide with shot-scraper on: push: - branches: - - main pull_request: workflow_dispatch: -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} - permissions: contents: read diff --git a/.github/workflows/test-sqlite-support.yml b/.github/workflows/test-sqlite-support.yml index 700f3cce..2fdb3a40 100644 --- a/.github/workflows/test-sqlite-support.yml +++ b/.github/workflows/test-sqlite-support.yml @@ -1,15 +1,6 @@ name: Test SQLite versions -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} +on: [push, pull_request] permissions: contents: read @@ -21,10 +12,10 @@ jobs: strategy: matrix: platform: [ubuntu-latest] - python-version: ["3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] sqlite-version: [ #"3", # latest version - #"3.46", + "3.46", #"3.45", #"3.27", #"3.26", diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8176a630..751eedfd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,15 +1,6 @@ name: Test -on: - push: - branches: - - main - pull_request: - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.event_name == 'pull_request' }} +on: [push, pull_request] permissions: contents: read @@ -20,20 +11,16 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] - include: - - python-version: "3.14" - coverage: true + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v7 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v7 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip cache-dependency-path: pyproject.toml - check-latest: true - name: Build extension for --load-extension test run: |- (cd tests && gcc ext.c -fPIC -shared -o ext.so) @@ -41,27 +28,12 @@ jobs: run: | pip install . --group dev pip freeze - - name: Install pytest-cov - if: ${{ matrix.coverage }} - run: pip install pytest-cov - name: Run tests run: | - if [ "${{ matrix.coverage }}" = "true" ]; then - COV="--cov=datasette --cov-config=.coveragerc" - pytest -n auto -m "not serial" $COV --cov-report= - pytest -m "serial" $COV --cov-append --cov-report xml:coverage.xml --cov-report term - else - pytest -n auto -m "not serial" - pytest -m "serial" - fi + pytest -n auto -m "not serial" + pytest -m "serial" # And the test that exceeds a localhost HTTPS server tests/test_datasette_https_server.sh - - name: Upload coverage report - if: ${{ matrix.coverage }} - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - name: Black run: | black --version diff --git a/Dockerfile b/Dockerfile index 58287dd7..9a8f06cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim-bookworm AS build +FROM python:3.11.0-slim-bullseye as build # Version of Datasette to install, e.g. 0.55 # docker build . -t datasette --build-arg VERSION=0.55 diff --git a/Justfile b/Justfile index d1b69378..6ffff870 100644 --- a/Justfile +++ b/Justfile @@ -49,18 +49,13 @@ export DATASETTE_SECRET := "not_a_secret" uv run cog -r README.md docs/*.rst # Serve live docs on localhost:8000 -@docs: shots cog blacken-docs +@docs: cog blacken-docs uv run make -C docs livehtml # Build docs as static HTML @docs-build: cog blacken-docs rm -rf docs/_build && cd docs && uv run make html -# Take any missing documentation screenshots defined in docs/shots.yml -@shots: - uv run --group shots shot-scraper install - cd docs && uv run --group shots shot-scraper multi shots.yml --no-clobber --reduced-motion --retina - # Apply Black @black: uv run black datasette tests diff --git a/README.md b/README.md index 1f79778f..393e8e5c 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ You can also install it using `pip` or `pipx`: pip install datasette -Datasette requires Python 3.10 or higher. We also have [detailed installation instructions](https://docs.datasette.io/en/stable/installation.html) covering other options such as Docker. +Datasette requires Python 3.8 or higher. We also have [detailed installation instructions](https://docs.datasette.io/en/stable/installation.html) covering other options such as Docker. ## Basic usage diff --git a/datasette/__init__.py b/datasette/__init__.py index 982dcc79..e0022178 100644 --- a/datasette/__init__.py +++ b/datasette/__init__.py @@ -1,7 +1,6 @@ from datasette.permissions import Permission # noqa from datasette.version import __version_info__, __version__ # noqa from datasette.events import Event # noqa -from datasette.background_tasks import BackgroundTask, BackgroundTaskSupervisor # noqa from datasette.tokens import TokenHandler, TokenInvalid, TokenRestrictions # noqa from datasette.utils.asgi import ( # noqa Forbidden, diff --git a/datasette/_pytest_plugin.py b/datasette/_pytest_plugin.py index 587380ed..103c616d 100644 --- a/datasette/_pytest_plugin.py +++ b/datasette/_pytest_plugin.py @@ -89,8 +89,7 @@ def pytest_runtest_protocol(item, nextitem): continue try: ds.close() - except Exception as e: # noqa: BLE001 - # Surfaced as a pytest warning; teardown must not fail the run + except Exception as e: item.warn( pytest.PytestUnraisableExceptionWarning( f"Error closing Datasette instance: {e!r}" diff --git a/datasette/actor_auth_cookie.py b/datasette/actor_auth_cookie.py index 7503f1d5..368213af 100644 --- a/datasette/actor_auth_cookie.py +++ b/datasette/actor_auth_cookie.py @@ -1,9 +1,7 @@ -import time - -from itsdangerous import BadSignature - from datasette import hookimpl +from itsdangerous import BadSignature from datasette.utils import baseconv +import time @hookimpl diff --git a/datasette/app.py b/datasette/app.py index 3e4c5acf..3d7037ac 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -1,8 +1,8 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable, Sequence -from typing import TYPE_CHECKING, Any +import contextvars +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Sequence if TYPE_CHECKING: from datasette.permissions import Resource @@ -12,10 +12,11 @@ import dataclasses import datetime import functools import glob +import httpx import importlib.metadata import inspect +from itsdangerous import BadSignature import json -import logging import os import re import secrets @@ -27,52 +28,91 @@ import urllib.parse from concurrent import futures from pathlib import Path -import httpx2 -from itsdangerous import BadSignature, URLSafeSerializer +from markupsafe import Markup, escape +from itsdangerous import URLSafeSerializer from jinja2 import ( ChoiceLoader, Environment, FileSystemLoader, - PrefixLoader, pass_context, + PrefixLoader, ) from jinja2.environment import Template from jinja2.exceptions import TemplateNotFound -from markupsafe import Markup, escape -from . import stored_queries, write_sql -from .background_tasks import BackgroundTask, BackgroundTaskSupervisor -from .column_types import SQLiteType -from .csrf import CrossOriginProtectionMiddleware -from .database import Database, QueryInterrupted from .events import Event -from .plugins import DEFAULT_PLUGINS, get_plugins, pm -from .renderer import json_renderer -from .resources import DatabaseResource, TableResource -from .telemetry import ( - TelemetryMiddleware, - _in_datasette_client, - clamp_http_method, - register_datasette, - request_span, - tracer, - unregister_datasette, +from .column_types import SQLiteType +from . import stored_queries, write_sql +from .views import Context +from .views.database import ( + database_download, + DatabaseView, + QueryView, ) -from .telemetry_registry import HTTP_ROUTE, STARTUP -from .tokens import TokenInvalid -from .tracer import AsgiTracer +from .views.table_create_alter import ( + DatabaseForeignKeyTargetsView, + TableAlterView, + TableCreateView, + TableForeignKeySuggestionsView, +) +from .views.execute_write import ExecuteWriteAnalyzeView, ExecuteWriteView +from .views.stored_queries import ( + QueryCreateAnalyzeView, + QueryDeleteView, + QueryDefinitionView, + QueryEditView, + GlobalQueryListView, + QueryListView, + QueryParametersView, + QueryStoreView, + QueryUpdateView, +) +from .views.index import IndexView +from .views.special import ( + JsonDataView, + PatternPortfolioView, + AutocompleteDebugView, + AuthTokenView, + ApiExplorerView, + CreateTokenView, + LogoutView, + AllowDebugView, + PermissionsDebugView, + MessagesDebugView, + AllowedResourcesView, + PermissionRulesView, + PermissionCheckView, + JumpView, + InstanceSchemaView, + DatabaseSchemaView, + DatabaseEditorSchemaView, + TableSchemaView, +) +from .views.table import ( + TableAutocompleteView, + TableInsertView, + TableUpsertView, + TableSetColumnTypeView, + TableDropView, + TableFragmentView, + table_view, +) +from .views.row import RowView, RowDeleteView, RowUpdateView +from .renderer import json_renderer from .url_builder import Urls +from .database import Database, QueryInterrupted + from .utils import ( - SPATIALITE_FUNCTIONS, PaginatedResources, PrefixedUrlString, + SPATIALITE_FUNCTIONS, StartupError, - add_cors_headers, async_call_with_supported_arguments, await_me_maybe, baseconv, call_with_supported_arguments, detect_json1, + add_cors_headers, display_actor, escape_css_string, escape_sqlite, @@ -82,100 +122,50 @@ from .utils import ( move_plugins_and_allow, move_table_config, parse_metadata, - redact_keys, resolve_env_secrets, resolve_routes, - row_sql_params_pks, sha256_file, tilde_decode, tilde_encode, to_css_class, urlsafe_components, + redact_keys, + row_sql_params_pks, ) +from .tokens import TokenInvalid from .utils.asgi import ( AsgiLifespan, - AsgiRunOnFirstRequest, BadRequest, - DatabaseNotFound, Forbidden, NotFound, + DatabaseNotFound, + TableNotFound, + RowNotFound, Request, Response, - RowNotFound, - TableNotFound, + AsgiRunOnFirstRequest, + asgi_static, asgi_send, asgi_send_file, asgi_send_redirect, - asgi_static, ) +from .csrf import CrossOriginProtectionMiddleware from .utils.internal_db import init_internal_db, populate_schema_tables from .utils.sqlite import ( sqlite3, using_pysqlite3, ) +from .tracer import AsgiTracer +from .plugins import pm, DEFAULT_PLUGINS, get_plugins from .version import __version__ -from .views import Context -from .views.database import ( - DatabaseView, - QueryView, - database_download, -) -from .views.execute_write import ExecuteWriteAnalyzeView, ExecuteWriteView -from .views.index import IndexView -from .views.row import RowDeleteView, RowUpdateView, RowView -from .views.special import ( - AllowDebugView, - AllowedResourcesView, - ApiExplorerView, - AuthTokenView, - AutocompleteDebugView, - CreateTokenView, - DatabaseSchemaView, - InstanceSchemaView, - JsonDataView, - JumpView, - LogoutView, - MessagesDebugView, - PatternPortfolioView, - PermissionCheckView, - PermissionRulesView, - PermissionsDebugView, - TableSchemaView, -) -from .views.stored_queries import ( - GlobalQueryListView, - QueryCreateAnalyzeView, - QueryDefinitionView, - QueryDeleteView, - QueryEditView, - QueryListView, - QueryParametersView, - QueryStoreView, - QueryUpdateView, -) -from .views.table import ( - TableAutocompleteView, - TableCountView, - TableDropView, - TableFragmentView, - TableInsertView, - TableSetColumnTypeView, - TableUpsertView, - table_view, -) -from .views.table_create_alter import ( - DatabaseForeignKeyTargetsView, - TableAlterView, - TableCreateView, - TableForeignKeySuggestionsView, -) + +from .resources import DatabaseResource, TableResource app_root = Path(__file__).parent.parent -logger = logging.getLogger(__name__) - -# _in_datasette_client is defined in telemetry.py to avoid a circular import +# Context variable to track when code is executing within a datasette.client request +_in_datasette_client = contextvars.ContextVar("in_datasette_client", default=False) class _DatasetteClientContext: @@ -195,7 +185,7 @@ class PermissionCheck: """Represents a logged permission check for debugging purposes.""" when: str - actor: dict[str, Any] | None + actor: Dict[str, Any] | None action: str parent: str | None child: str | None @@ -325,7 +315,7 @@ def _permission_cache_key(actor, action, parent, child): actor_key = ( json.dumps(actor, sort_keys=True, default=repr) if actor is not None else None ) - return (actor_key, action.name, parent, action.normalize_child(child)) + return (actor_key, action, parent, child) async def favicon(request, send): @@ -432,7 +422,6 @@ class Datasette: default_deny=False, ): self._startup_invoked = False - self._shutdown_invoked = False self._closed = False assert config_dir is None or isinstance( config_dir, Path @@ -446,7 +435,7 @@ class Datasette: if config_dir: db_files = [] for ext in ("db", "sqlite", "sqlite3"): - db_files.extend(config_dir.glob(f"*.{ext}")) + db_files.extend(config_dir.glob("*.{}".format(ext))) self.files += tuple(str(f) for f in db_files) if ( config_dir @@ -464,11 +453,8 @@ class Datasette: self.databases = collections.OrderedDict() self.actions = {} # .invoke_startup() will populate this self._column_types = {} # .invoke_startup() will populate this - self._setup_db_done = False - self._suppress_background_tasks = False try: self._refresh_schemas_lock = asyncio.Lock() - self._startup_lock = asyncio.Lock() except RuntimeError as rex: # Workaround for intermittent test failure, see: # https://github.com/simonw/datasette/issues/1802 @@ -476,10 +462,8 @@ class Datasette: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) self._refresh_schemas_lock = asyncio.Lock() - self._startup_lock = asyncio.Lock() else: raise - self._background_tasks = BackgroundTaskSupervisor(self) self.crossdb = crossdb self.nolock = nolock if memory or crossdb or not self.files: @@ -651,8 +635,6 @@ class Datasette: self.root_enabled = False self.default_deny = default_deny self.client = DatasetteClient(self) - # Last, so metric callbacks never see a partially initialized instance - register_datasette(self) async def apply_metadata_json(self): # Apply any metadata entries from metadata.json to the internal tables @@ -694,10 +676,10 @@ class Datasette: def get_jinja_environment(self, request: Request = None) -> Environment: environment = self._jinja_env if request: - for hook_environment in pm.hook.jinja2_environment_from_request( + for environment in pm.hook.jinja2_environment_from_request( datasette=self, request=request, env=environment ): - environment = hook_environment + pass return environment def get_action(self, name_or_abbr: str): @@ -751,7 +733,7 @@ class Datasette: catalog_database_names.update( row["database_name"] for row in await internal_db.execute( - f"select distinct database_name from {table}" + "select distinct database_name from {}".format(table) ) if row["database_name"] is not None ) @@ -762,7 +744,7 @@ class Datasette: for stale_db_name in stale_databases: for table in catalog_table_names: conn.execute( - f"DELETE FROM {table} WHERE database_name = ?", + "DELETE FROM {} WHERE database_name = ?".format(table), [stale_db_name], ) @@ -772,7 +754,19 @@ class Datasette: # Compare schema versions to see if we should skip it if schema_version == current_schema_versions.get(database_name): continue - await populate_schema_tables(internal_db, db, schema_version) + placeholders = "(?, ?, ?, ?)" + values = [database_name, str(db.path), db.is_memory, schema_version] + if db.path is None: + placeholders = "(?, null, ?, ?)" + values = [database_name, db.is_memory, schema_version] + await internal_db.execute_write( + """ + INSERT OR REPLACE INTO catalog_databases (database_name, path, is_memory, schema_version) + VALUES {} + """.format(placeholders), + values, + ) + await populate_schema_tables(internal_db, db) @property def urls(self): @@ -793,61 +787,61 @@ class Datasette: # This must be called for Datasette to be in a usable state if self._startup_invoked: return - # Group spans created during startup under a single parent span - with tracer.start_as_current_span(STARTUP): - # Register event classes - event_classes = [] - for hook in pm.hook.register_events(datasette=self): - extra_classes = await await_me_maybe(hook) - if extra_classes: - event_classes.extend(extra_classes) - self.event_classes = tuple(event_classes) + # Register event classes + event_classes = [] + for hook in pm.hook.register_events(datasette=self): + extra_classes = await await_me_maybe(hook) + if extra_classes: + event_classes.extend(extra_classes) + self.event_classes = tuple(event_classes) - # Register actions, but watch out for duplicate name/abbr - action_names = {} - action_abbrs = {} - for hook in pm.hook.register_actions(datasette=self): - if hook: - for action in hook: - if ( - action.name in action_names - and action != action_names[action.name] - ): - raise StartupError(f"Duplicate action name: {action.name}") - if ( - action.abbr - and action.abbr in action_abbrs - and action != action_abbrs[action.abbr] - ): - raise StartupError(f"Duplicate action abbr: {action.abbr}") - action_names[action.name] = action - if action.abbr: - action_abbrs[action.abbr] = action - self.actions[action.name] = action + # Register actions, but watch out for duplicate name/abbr + action_names = {} + action_abbrs = {} + for hook in pm.hook.register_actions(datasette=self): + if hook: + for action in hook: + if ( + action.name in action_names + and action != action_names[action.name] + ): + raise StartupError( + "Duplicate action name: {}".format(action.name) + ) + if ( + action.abbr + and action.abbr in action_abbrs + and action != action_abbrs[action.abbr] + ): + raise StartupError( + "Duplicate action abbr: {}".format(action.abbr) + ) + action_names[action.name] = action + if action.abbr: + action_abbrs[action.abbr] = action + self.actions[action.name] = action - # Register column types (classes, not instances) - self._column_types = {} - for hook in pm.hook.register_column_types(datasette=self): - if hook: - for ct_cls in hook: - if ct_cls.name in self._column_types: - raise StartupError( - f"Duplicate column type name: {ct_cls.name}" - ) - self._column_types[ct_cls.name] = ct_cls + # Register column types (classes, not instances) + self._column_types = {} + for hook in pm.hook.register_column_types(datasette=self): + if hook: + for ct_cls in hook: + if ct_cls.name in self._column_types: + raise StartupError(f"Duplicate column type name: {ct_cls.name}") + self._column_types[ct_cls.name] = ct_cls - for hook in pm.hook.prepare_jinja2_environment( - env=self._jinja_env, datasette=self - ): - await await_me_maybe(hook) - # Ensure internal tables and metadata are populated before startup hooks - await self._refresh_schemas() - await self._save_queries_from_config() - # Load column_types from config into internal DB - await self._apply_column_types_config() - for hook in pm.hook.startup(datasette=self): - await await_me_maybe(hook) - self._startup_invoked = True + for hook in pm.hook.prepare_jinja2_environment( + env=self._jinja_env, datasette=self + ): + await await_me_maybe(hook) + # Ensure internal tables and metadata are populated before startup hooks + await self._refresh_schemas() + await self._save_queries_from_config() + # Load column_types from config into internal DB + await self._apply_column_types_config() + for hook in pm.hook.startup(datasette=self): + await await_me_maybe(hook) + self._startup_invoked = True def sign(self, value, namespace="default"): return URLSafeSerializer(self._secret, namespace).dumps(value) @@ -880,7 +874,7 @@ class Datasette: actor_id: str, *, expires_after: int | None = None, - restrictions: TokenRestrictions | None = None, + restrictions: "TokenRestrictions | None" = None, handler: str | None = None, ) -> str: """ @@ -937,7 +931,7 @@ class Datasette: raise KeyError return matches[0] if name is None: - name = next(iter(self.databases.keys())) + name = [key for key in self.databases.keys()][0] return self.databases[name] def add_database(self, db, name=None, route=None): @@ -950,7 +944,7 @@ class Datasette: suggestion = name i = 2 while name in self.databases: - name = f"{suggestion}_{i}" + name = "{}_{}".format(suggestion, i) i += 1 db.name = name db.route = route or name @@ -980,21 +974,18 @@ class Datasette: if self._closed: return self._closed = True - # Stop reporting metrics before closing databases - unregister_datasette(self) first_exception = None dbs = list(self.databases.values()) + [self._internal_database] for db in dbs: try: db.close() - except Exception as e: # noqa: BLE001 - # Collect the first failure and re-raise after every close() has run + except Exception as e: if first_exception is None: first_exception = e if self.executor is not None: try: self.executor.shutdown(wait=True, cancel_futures=True) - except Exception as e: # noqa: BLE001 + except Exception as e: if first_exception is None: first_exception = e if first_exception is not None: @@ -1343,15 +1334,24 @@ class Datasette: actual = ( actual_sqlite_type.value if actual_sqlite_type is not None - else f"unrecognized {column_detail.type!r}" + else "unrecognized {!r}".format(column_detail.type) ) raise ValueError( - f"Column type {ct_cls.name!r} is only applicable to SQLite types {allowed} but {database}.{resource}.{column} " - f"has SQLite type {actual}" + "Column type {!r} is only applicable to SQLite types {} but {}.{}.{} " + "has SQLite type {}".format( + ct_cls.name, + allowed, + database, + resource, + column, + actual, + ) ) async def _apply_column_types_config(self): """Load column_types from datasette.json config into the internal DB.""" + import logging + for db_name, db_conf in (self.config or {}).get("databases", {}).items(): for table_name, table_conf in db_conf.get("tables", {}).items(): for col_name, ct in table_conf.get("column_types", {}).items(): @@ -1361,7 +1361,7 @@ class Datasette: col_type = ct["type"] config = ct.get("config") if col_type not in self._column_types: - logger.warning( + logging.warning( "column_types config references unknown type %r " "for %s.%s.%s", col_type, @@ -1374,7 +1374,7 @@ class Datasette: db_name, table_name, col_name, col_type, config ) except ValueError as ex: - logger.warning(str(ex)) + logging.warning(str(ex)) async def get_column_type(self, database: str, resource: str, column: str): """ @@ -1427,7 +1427,7 @@ class Datasette: resource: str, column: str, column_type: str, - config: dict | None = None, + config: dict = None, ) -> None: """Assign a column type. Overwrites any existing assignment.""" ct_cls = self._column_types.get(column_type) @@ -1511,7 +1511,9 @@ class Datasette: possible_names = {plugin["name"], plugin["name"].replace("-", "_")} if plugin_name in possible_names: return _resolve_static_asset_path(plugin["static_path"], path) - raise FileNotFoundError(f"No static assets found for plugin {plugin_name}") + raise FileNotFoundError( + "No static assets found for plugin {}".format(plugin_name) + ) def _static_mounted_asset(self, mount_name, path): mount_name = mount_name.strip("/") @@ -1521,7 +1523,7 @@ class Datasette: _resolve_static_asset_path(dirname, path), self.urls.path("/{}/{}".format(mount_name, path.lstrip("/"))), ) - raise FileNotFoundError(f"No static mount found for {mount_name}") + raise FileNotFoundError("No static mount found for {}".format(mount_name)) def _static_asset_hash(self, filepath): filepath = Path(filepath) @@ -1553,28 +1555,15 @@ class Datasette: conn.row_factory = sqlite3.Row conn.text_factory = lambda x: str(x, "utf-8", "replace") if self.sqlite_extensions and database != INTERNAL_DB_NAME: - # Extension loading is only enabled for as long as it takes to - # load the configured extensions. Leaving it enabled would let - # anyone who can execute SQL call load_extension() themselves. conn.enable_load_extension(True) - try: - for extension in self.sqlite_extensions: - # "extension" is either a string path to the extension - # or a 2-item tuple that specifies which entrypoint to load. - if isinstance(extension, tuple): - path, entrypoint = extension - if sys.version_info >= (3, 12): - conn.load_extension(path, entrypoint=entrypoint) - else: - # Connection.load_extension() only gained the - # entrypoint argument in Python 3.12 - conn.execute( - "SELECT load_extension(?, ?)", [path, entrypoint] - ) - else: - conn.load_extension(extension) - finally: - conn.enable_load_extension(False) + for extension in self.sqlite_extensions: + # "extension" is either a string path to the extension + # or a 2-item tuple that specifies which entrypoint to load. + if isinstance(extension, tuple): + path, entrypoint = extension + conn.execute("SELECT load_extension(?, ?)", [path, entrypoint]) + else: + conn.execute("SELECT load_extension(?)", [extension]) if self.setting("cache_size_kb"): conn.execute(f"PRAGMA cache_size=-{self.setting('cache_size_kb')}") # pylint: disable=no-member @@ -1625,17 +1614,18 @@ class Datasette: if await self.allowed(action="view-instance", actor=actor): crumbs.append({"href": self.urls.instance(), "label": "home"}) # Database link - if database and await self.allowed( - action="view-database", - resource=DatabaseResource(database=database), - actor=actor, - ): - crumbs.append( - { - "href": self.urls.database(database), - "label": database, - } - ) + if database: + if await self.allowed( + action="view-database", + resource=DatabaseResource(database=database), + actor=actor, + ): + crumbs.append( + { + "href": self.urls.database(database), + "label": database, + } + ) # Table link if table: assert database, "table= requires database=" @@ -1654,7 +1644,7 @@ class Datasette: async def actors_from_ids( self, actor_ids: Iterable[str | int] - ) -> dict[int | str, dict]: + ) -> Dict[int | str, Dict]: result = pm.hook.actors_from_ids(datasette=self, actor_ids=actor_ids) if result is None: # Do the default thing @@ -1663,9 +1653,9 @@ class Datasette: return result async def track_event(self, event: Event): - assert isinstance( - event, self.event_classes - ), f"Invalid event type: {type(event)}" + assert isinstance(event, self.event_classes), "Invalid event type: {}".format( + type(event) + ) for hook in pm.hook.track_event(datasette=self, event=event): await await_me_maybe(hook) @@ -1702,7 +1692,7 @@ class Datasette: self, actor: dict, action: str, - resource: Resource | None = None, + resource: "Resource" | None = None, ): """ Check if actor can see a resource and if it's private. @@ -1767,145 +1757,8 @@ class Datasette: sql, params = await build_allowed_resources_sql( self, actor, action, parent=parent, include_is_private=include_is_private ) - if action == "view-table": - sql, params = await self._apply_derived_table_permissions_to_sql( - sql, - params, - actor=actor, - parent=parent, - include_is_private=include_is_private, - ) return ResourcesSQL(sql, params) - async def _allowed_derived_table_source( - self, database, source, *, actor, dependencies - ): - """Check an immediate source, denying sources that are themselves derived.""" - if any( - TableResource.normalize_child(table) - == TableResource.normalize_child(source) - for table in dependencies - ): - return False - # The source has no dependency in this map. Evaluate its own permission - # and prerequisites without starting another dependency check. - verdicts = await self._allowed_many( - actions=["view-table"], - resource=TableResource(database, source), - actor=actor, - check_derived=False, - ) - return verdicts["view-table"] - - async def _apply_derived_table_permissions_to_sql( - self, - sql, - params, - *, - actor, - parent, - include_is_private, - ): - databases = ( - [(parent, self.databases[parent])] - if parent in self.databases - else ([] if parent is not None else list(self.databases.items())) - ) - dependency_maps = dict( - zip( - (name for name, _ in databases), - await asyncio.gather( - *(db.derived_table_dependencies() for _, db in databases) - ), - ) - ) - dependencies = [ - (database_name, child, source) - for database_name, dependency_map in dependency_maps.items() - for child, source in dependency_map.items() - ] - if not dependencies: - return sql, params - - sources = sorted( - {(database_name, source) for database_name, _, source in dependencies} - ) - actor_verdicts = await asyncio.gather( - *( - self._allowed_derived_table_source( - database_name, - source, - actor=actor, - dependencies=dependency_maps[database_name], - ) - for database_name, source in sources - ) - ) - actor_allowed = dict(zip(sources, actor_verdicts)) - - anonymous_allowed = {} - if include_is_private: - anonymous_verdicts = await asyncio.gather( - *( - self._allowed_derived_table_source( - database_name, - source, - actor=None, - dependencies=dependency_maps[database_name], - ) - for database_name, source in sources - ) - ) - anonymous_allowed = dict(zip(sources, anonymous_verdicts)) - - wrapped_params = dict(params) - derived_rows = [ - [ - database_name, - child, - int(actor_allowed[(database_name, source)]), - *( - [int(anonymous_allowed[(database_name, source)])] - if include_is_private - else [] - ), - ] - for database_name, child, source in dependencies - ] - derived_param = "_datasette_derived_permissions" - while derived_param in wrapped_params: - derived_param += "_" - wrapped_params[derived_param] = json.dumps(derived_rows) - - derived_columns = "parent, child, source_allowed" - select_columns = "allowed.parent, allowed.child, allowed.reason" - if include_is_private: - derived_columns += ", source_anonymous_allowed" - select_columns += ( - ", CASE WHEN derived.source_anonymous_allowed = 0 " - "THEN 1 ELSE allowed.is_private END AS is_private" - ) - wrapped_sql = f""" -WITH derived_permissions({derived_columns}) AS ( - SELECT - json_extract(value, '$[0]'), - json_extract(value, '$[1]'), - json_extract(value, '$[2]') - {", json_extract(value, '$[3]')" if include_is_private else ""} - FROM json_each(:{derived_param}) -), -allowed AS ( -{sql} -) -SELECT {select_columns} -FROM allowed -LEFT JOIN derived_permissions AS derived - ON allowed.parent = derived.parent AND allowed.child = derived.child COLLATE NOCASE -WHERE COALESCE(derived.source_allowed, 1) = 1 -ORDER BY allowed.parent, allowed.child -""".strip() - return wrapped_sql, wrapped_params - async def allowed_resources( self, action: str, @@ -2038,7 +1891,10 @@ ORDER BY allowed.parent, allowed.child if truncated and resources: last_resource = resources[-1] # Use tilde-encoding like table pagination - next_token = f"{tilde_encode(str(last_resource.parent))},{tilde_encode(str(last_resource.child))}" + next_token = "{},{}".format( + tilde_encode(str(last_resource.parent)), + tilde_encode(str(last_resource.child)), + ) return PaginatedResources( resources=resources, @@ -2056,7 +1912,7 @@ ORDER BY allowed.parent, allowed.child self, *, action: str, - resource: Resource = None, + resource: "Resource" = None, actor: dict | None = None, ) -> bool: """ @@ -2087,7 +1943,7 @@ ORDER BY allowed.parent, allowed.child self, *, actions: Sequence[str], - resource: Resource = None, + resource: "Resource" = None, actor: dict | None = None, ) -> dict[str, bool]: """ @@ -2108,17 +1964,11 @@ ORDER BY allowed.parent, allowed.child ) # {"edit-schema": True, "drop-table": True, "insert-row": False} """ - return await self._allowed_many( - actions=actions, resource=resource, actor=actor, check_derived=True - ) - - async def _allowed_many(self, *, actions, resource, actor, check_derived): - """Evaluate permissions, optionally applying the one-hop source policy.""" + from datasette.utils.actions_sql import check_permissions_for_actions from datasette.permissions import ( _permission_check_cache, _skip_permission_checks, ) - from datasette.utils.actions_sql import check_permissions_for_actions # For global actions, resource is None parent = resource.parent if resource else None @@ -2151,7 +2001,7 @@ ORDER BY allowed.parent, allowed.child to_check = [] for name in expanded: if cache is not None: - key = _permission_cache_key(actor, self.actions[name], parent, child) + key = _permission_cache_key(actor, name, parent, child) if key in cache: final[name] = cache[key] continue @@ -2167,28 +2017,6 @@ ORDER BY allowed.parent, allowed.child child=child, ) - if ( - check_derived - and "view-table" in to_check - and raw.get("view-table") - and isinstance(resource, TableResource) - and parent in self.databases - ): - dependencies = await self.databases[parent].derived_table_dependencies() - source = next( - ( - source - for table, source in dependencies.items() - if TableResource.normalize_child(table) - == TableResource.normalize_child(child) - ), - None, - ) - if source is not None: - raw["view-table"] = await self._allowed_derived_table_source( - parent, source, actor=actor, dependencies=dependencies - ) - def resolve(name): # final verdict = own rules AND verdict of also_requires chain if name in final: @@ -2206,9 +2034,7 @@ ORDER BY allowed.parent, allowed.child # Cache the freshly computed checks if cache is not None: for name in to_check: - cache[ - _permission_cache_key(actor, self.actions[name], parent, child) - ] = final[name] + cache[_permission_cache_key(actor, name, parent, child)] = final[name] # Log every check (including cache hits) for the debug page, # dependencies before the actions that required them @@ -2231,7 +2057,7 @@ ORDER BY allowed.parent, allowed.child self, *, action: str, - resource: Resource = None, + resource: "Resource" = None, actor: dict | None = None, ): """ @@ -2285,32 +2111,18 @@ ORDER BY allowed.parent, allowed.child db = self.databases[database] foreign_keys = await db.foreign_keys_for_table(table) # Find the foreign_key for this column - fk = next( - ( + try: + fk = [ foreign_key for foreign_key in foreign_keys if foreign_key["column"] == column - ), - None, - ) - if fk is None: + ][0] + except IndexError: return {} # Ensure user has permission to view the referenced table from datasette.resources import TableResource other_table = fk["other_table"] - # Foreign key declarations can spell the target with different casing. - target_table = ( - await db.execute( - "select name from sqlite_master where type='table' and name=? collate nocase", - [other_table], - ) - ).first() - if target_table is None: - # SQLite accepts a foreign key to a table that does not exist, and - # linking to it would only lead to a 404 - return {} - other_table = target_table[0] other_column = fk["other_column"] if other_column is None: other_pks = await db.primary_keys(other_table) @@ -2393,17 +2205,16 @@ ORDER BY allowed.parent, allowed.child sqlite_extensions[extension] = result.fetchone()[0] else: sqlite_extensions[extension] = None - except Exception: # noqa: BLE001, S110 - # Probing for optional SQLite extensions - absence is the normal case + except Exception: pass # More details on SpatiaLite if "spatialite" in sqlite_extensions: spatialite_details = {} for fn in SPATIALITE_FUNCTIONS: try: - result = conn.execute(f"select {fn}()") + result = conn.execute("select {}()".format(fn)) spatialite_details[fn] = result.fetchone()[0] - except sqlite3.Error as e: + except Exception as e: spatialite_details[fn] = {"error": str(e)} sqlite_extensions["spatialite"] = spatialite_details @@ -2411,7 +2222,9 @@ ORDER BY allowed.parent, allowed.child fts_versions = [] for fts in ("FTS5", "FTS4", "FTS3"): try: - conn.execute(f"CREATE VIRTUAL TABLE v{fts} USING {fts} (data)") + conn.execute( + "CREATE VIRTUAL TABLE v{fts} USING {fts} (data)".format(fts=fts) + ) fts_versions.append(fts) except sqlite3.OperationalError: continue @@ -2470,7 +2283,7 @@ ORDER BY allowed.parent, allowed.child "static": p["static_path"] is not None, "templates": p["templates_path"] is not None, "version": p.get("version"), - "hooks": sorted(set(p["hooks"])), + "hooks": list(sorted(set(p["hooks"]))), } for p in ps ] @@ -2494,21 +2307,6 @@ ORDER BY allowed.parent, allowed.child ) return d - def _tasks(self): - return { - "tasks": [ - { - "name": t.name, - "state": t.state, - "function": t.function, - "started_at": t.started_at, - "exception": repr(t.exception) if t.exception else None, - } - for t in self._background_tasks.tasks() - ], - "launched": self._background_tasks.launched, - } - def _actor(self, request): return {"actor": request.actor} @@ -2561,15 +2359,13 @@ ORDER BY allowed.parent, allowed.child async def render_template( self, - templates: list[str] | str | Template, - context: dict[str, Any] | Context | None = None, + templates: List[str] | str | Template, + context: Dict[str, Any] | Context | None = None, request: Request | None = None, view_name: str | None = None, ): if not self._startup_invoked: - raise RuntimeError( - "render_template() called before await ds.invoke_startup()" - ) + raise Exception("render_template() called before await ds.invoke_startup()") context = context or {} if isinstance(templates, Template): template = templates @@ -2615,11 +2411,9 @@ ORDER BY allowed.parent, allowed.child datasette=self, ): extra_vars = await await_me_maybe(extra_vars) - if extra_vars is None: - continue - assert isinstance( - extra_vars, dict - ), f"extra_vars is of type {type(extra_vars)}" + assert isinstance(extra_vars, dict), "extra_vars is of type {}".format( + type(extra_vars) + ) extra_template_vars.update(extra_vars) async def menu_links(): @@ -2638,27 +2432,29 @@ ORDER BY allowed.parent, allowed.child # the contract tests fail otherwise template_context = { **context, - "request": request, - "crumb_items": self._crumb_items, - "urls": self.urls, - "actor": request.actor if request else None, - "menu_links": menu_links, - "display_actor": display_actor, - "show_logout": request is not None - and "ds_actor" in request.cookies - and request.actor, - "zip": zip, - "body_scripts": body_scripts, - "format_bytes": format_bytes, - "show_messages": lambda: self._show_messages(request), - "extra_css_urls": await self._asset_urls( - "extra_css_urls", template, context, request, view_name - ), - "extra_js_urls": await self._asset_urls( - "extra_js_urls", template, context, request, view_name - ), - "base_url": self.setting("base_url"), - "datasette_version": __version__, + **{ + "request": request, + "crumb_items": self._crumb_items, + "urls": self.urls, + "actor": request.actor if request else None, + "menu_links": menu_links, + "display_actor": display_actor, + "show_logout": request is not None + and "ds_actor" in request.cookies + and request.actor, + "zip": zip, + "body_scripts": body_scripts, + "format_bytes": format_bytes, + "show_messages": lambda: self._show_messages(request), + "extra_css_urls": await self._asset_urls( + "extra_css_urls", template, context, request, view_name + ), + "extra_js_urls": await self._asset_urls( + "extra_js_urls", template, context, request, view_name + ), + "base_url": self.setting("base_url"), + "datasette_version": __version__, + }, **extra_template_vars, } if request and request.args.get("_context") and self.setting("template_debug"): @@ -2679,7 +2475,7 @@ ORDER BY allowed.parent, allowed.child ): data = {"a": actor} if expire_after: - expires_at = int(time.time()) + expire_after + expires_at = int(time.time()) + (24 * 60 * 60) data["e"] = baseconv.base62.encode(expires_at) response.set_cookie("ds_actor", self.sign(data, "actor")) @@ -2780,7 +2576,7 @@ ORDER BY allowed.parent, allowed.child JsonDataView.as_view( self, "plugins.json", - self._plugins, + lambda request: {"plugins": self._plugins(request)}, needs_request=True, ), r"/-/plugins(\.(?Pjson))?$", @@ -2799,12 +2595,6 @@ ORDER BY allowed.parent, allowed.child ), r"/-/threads(\.(?Pjson))?$", ) - add_route( - JsonDataView.as_view( - self, "tasks.json", self._tasks, permission="permissions-debug" - ), - r"/-/tasks(\.(?Pjson))?$", - ) add_route( JsonDataView.as_view( self, @@ -2927,6 +2717,10 @@ ORDER BY allowed.parent, allowed.child DatabaseSchemaView.as_view(self), r"/(?P[^\/\.]+)/-/schema(\.(?Pjson|md))?$", ) + add_route( + DatabaseEditorSchemaView.as_view(self), + r"/(?P[^\/\.]+)/-/editor-schema\.json$", + ) add_route( QueryParametersView.as_view(self), r"/(?P[^\/\.]+)/-/query/parameters$", @@ -2979,10 +2773,6 @@ ORDER BY allowed.parent, allowed.child TableSetColumnTypeView.as_view(self), r"/(?P[^\/\.]+)/(?P[^\/\.]+)/-/set-column-type$", ) - add_route( - TableCountView.as_view(self), - r"/(?P[^\/\.]+)/(?P
[^\/\.]+)/-/count$", - ) add_route( TableFragmentView.as_view(self), r"/(?P[^\/\.]+)/(?P
[^\/\.]+)/-/fragment$", @@ -3046,130 +2836,26 @@ ORDER BY allowed.parent, allowed.child raise RowNotFound(db.name, table_name, pk_values) return ResolvedRow(db, table_name, sql, params, pks, pk_values, results.first()) - async def _startup_sequence(self): - """Idempotently run the full startup sequence: table counts for - immutable databases, then invoke_startup(). Safe to call more than - once and safe to call concurrently - callers block until whichever - call got there first has finished. - - This is the single entry point used by both AsgiLifespan (so - real deployments finish startup before accepting requests) and - AsgiRunOnFirstRequest (the fallback for hosts that never send - lifespan events, e.g. DatasetteClient's httpx2.ASGITransport), and - `datasette serve` (cli.py) calls it too. The fast path below checks - both `_startup_invoked` and `_setup_db_done` - not just the former - - so that a bare `await ds.invoke_startup()` made by a caller ahead of - `_startup_sequence()` (which only sets `_startup_invoked`) can't - make this method skip the immutable-database table-count precompute. - """ - if self._startup_invoked and self._setup_db_done: - return - async with self._startup_lock: - if self._startup_invoked and self._setup_db_done: - return - if not self._setup_db_done: - # First time server starts up, calculate table counts for - # immutable databases - for database in self.databases.values(): - if not database.is_mutable: - await database.table_counts(limit=60 * 60 * 1000) - self._setup_db_done = True - await self.invoke_startup() - - def add_background_task(self, func, name=None) -> BackgroundTask: - """Register a piece of supervised background work, typically from - a plugin's ``startup`` hook. - - ``func`` must be a coroutine function taking one positional - argument, the ``Datasette`` instance - core calls ``func(self)``. - Callable any time after ``__init__``: if background tasks haven't - launched yet (the common case - most callers are ``startup`` hooks, - which run before launch), this buffers the registration until they - do; if they've already launched (e.g. called from a request - handler after the server is up), the task starts immediately. - - Returns a :class:`~datasette.background_tasks.BackgroundTask` - handle (``.name``, ``.state``, ``.task``, ``.exception``, - ``.started_at``, ``.function``, ``.cancel()``). - - ``name`` defaults to ``func.__qualname__``; on a name collision a - ``-2``, ``-3``, ... suffix is appended, since names are how - ``/-/tasks`` and log messages identify work. - """ - return self._background_tasks.add(func, name=name) - - async def start_background_tasks(self): - """Run startup (if it hasn't run yet) and launch every registered - background task. - - Public entry point for tests, embedders, and headless CLIs (the - ``datasette-rss``-style ``fetch --due`` shape) that want supervised - background tasks without running a server - equivalent to what - happens automatically via ASGI lifespan / the first-request - fallback in a served deployment. - """ - await self.invoke_startup() - await self._background_tasks.launch_all() - - async def _launch_background_tasks(self): - """Idempotently launch every registered background task. Private: - this is the entry point wired into the lifecycle trigger lists - (the second entry in both ``AsgiLifespan`` and - ``AsgiRunOnFirstRequest``'s ``on_startup``, after - ``_startup_sequence``) - not something plugins or embedders should - call directly; use ``add_background_task`` / - ``start_background_tasks`` instead. - - Positioned after ``_startup_sequence`` in both trigger lists so - launch always happens once every plugin's ``startup`` hook has had - a chance to register work - the ordering guarantee that makes - ``add_background_task`` useful. No-ops when - ``_suppress_background_tasks`` is set (the ``--get`` CLI path: its - one-shot TestClient request flows through the full ASGI stack, - including the first-request fallback, but must never launch - long-lived background work). - """ - if self._suppress_background_tasks: - return - await self._background_tasks.launch_all() - - async def invoke_shutdown(self): - """Run the graceful teardown sequence: plugin ``shutdown`` hooks, - then cancel and drain supervised background tasks, then close - every database. - """ - if self._shutdown_invoked: - return - self._shutdown_invoked = True - for hook in pm.hook.shutdown(datasette=self): - try: - await await_me_maybe(hook) - except Exception: - logging.getLogger("datasette").exception("shutdown hook failed") - await self._background_tasks.cancel_all(grace=5.0) - self.close() - def app(self): """Returns an ASGI app function that serves the whole of Datasette""" routes = self._routes() + async def setup_db(): + # First time server starts up, calculate table counts for immutable databases + for database in self.databases.values(): + if not database.is_mutable: + await database.table_counts(limit=60 * 60 * 1000) + + async def _close_on_shutdown(): + self.close() + asgi = CrossOriginProtectionMiddleware(DatasetteRouter(self, routes), self) if self.setting("trace_debug"): asgi = AsgiTracer(asgi) - asgi = AsgiLifespan( - asgi, - on_startup=[self._startup_sequence, self._launch_background_tasks], - on_shutdown=[self.invoke_shutdown], - ) + asgi = AsgiLifespan(asgi, on_shutdown=[_close_on_shutdown]) + asgi = AsgiRunOnFirstRequest(asgi, on_startup=[setup_db, self.invoke_startup]) for wrapper in pm.hook.asgi_wrapper(datasette=self): asgi = wrapper(asgi) - asgi = AsgiRunOnFirstRequest( - asgi, - on_startup=[self._startup_sequence, self._launch_background_tasks], - ) - # Outermost, so spans from plugin middleware and first-request - # startup are children of the request span - asgi = TelemetryMiddleware(asgi) return asgi @@ -3207,50 +2893,6 @@ class DatasetteRouter: receive, max_post_body_bytes=self.ds.setting("max_post_body_bytes"), ) - match, view = resolve_routes(self.routes, path) - is_static = view is favicon or getattr(view, "_datasette_static", False) - original_send = send - - async def send(message): - if message["type"] == "http.response.start" and not ( - is_static and message["status"] in (200, 304) - ): - # Decide privacy after rendering, including for streaming responses - # and error handlers. A public primary resource can still include - # private labels, actor navigation, or cookie-dependent content. - headers = list(message.get("headers", [])) - personalized = ( - request.actor is not None - or "cookie" in request.headers - or "authorization" in request.headers - or any(key.lower() == b"set-cookie" for key, _ in headers) - ) - if personalized: - headers = [ - (key, value) - for key, value in headers - if key.lower() != b"cache-control" - ] - headers.append((b"cache-control", b"private, no-store")) - - # Anonymous responses must not be reused for credentialed requests. - # Preserve any additional variation specified by views or plugins. - vary = [ - part.strip() - for key, value in headers - if key.lower() == b"vary" - for part in value.split(b",") - if part.strip() - ] - if b"*" not in vary: - for name in (b"Cookie", b"Authorization"): - if name.lower() not in {part.lower() for part in vary}: - vary.append(name) - headers = [(k, v) for k, v in headers if k.lower() != b"vary"] - headers.append((b"vary", b", ".join(vary))) - message = dict(message, headers=headers) - await original_send(message) - # Populate request_messages if ds_messages cookie is present try: request._messages = self.ds.unsign( @@ -3290,18 +2932,12 @@ class DatasetteRouter: return await self.handle_401(request, send, token_error) scope_modifications["actor"] = actor or default_actor scope = dict(scope, **scope_modifications) - request.scope = scope + + match, view = resolve_routes(self.routes, path) if match is None: return await self.handle_404(request, send) - # Now the route is known, add it to the request span - span = request_span(scope) - if span is not None: - route = match.re.pattern - span.set_attribute(HTTP_ROUTE, route) - span.update_name(f"{clamp_http_method(request.method)} {route}") - new_scope = dict(scope, url_route={"kwargs": match.groupdict()}) request.scope = new_scope try: @@ -3322,8 +2958,7 @@ class DatasetteRouter: custom_response ), "Default forbidden() hook should have been called" return await custom_response.asgi_send(send) - except Exception as exception: # noqa: BLE001 - # This IS the top-level error handler - it must catch everything + except Exception as exception: return await self.handle_exception(request, send, exception) async def handle_401(self, request, send, exception): @@ -3345,7 +2980,7 @@ class DatasetteRouter: request.path.replace("~", "~7E").replace("%", "~").replace(".", "~2E") ) if request.query_string: - new_path += f"?{request.query_string}" + new_path += "?{}".format(request.query_string) await asgi_send_redirect(send, new_path) return # If URL has a trailing slash, redirect to URL without it @@ -3555,7 +3190,8 @@ _curly_re = re.compile(r"({.*?})") def route_pattern_from_filepath(filepath): # Drop the ".html" suffix - filepath = filepath.removesuffix(".html") + if filepath.endswith(".html"): + filepath = filepath[: -len(".html")] re_bits = ["/"] for bit in _curly_re.split(filepath): if _curly_re.match(bit): @@ -3612,14 +3248,14 @@ class DatasetteClient: with _DatasetteClientContext(): if skip_permission_checks: with SkipPermissions(): - async with httpx2.AsyncClient( - transport=httpx2.ASGITransport(app=self.app), + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=self.app), cookies=kwargs.pop("cookies", None), ) as client: return await getattr(client, method)(self._fix(path), **kwargs) else: - async with httpx2.AsyncClient( - transport=httpx2.ASGITransport(app=self.app), + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=self.app), cookies=kwargs.pop("cookies", None), ) as client: return await getattr(client, method)(self._fix(path), **kwargs) @@ -3666,10 +3302,10 @@ class DatasetteClient: method: HTTP method (e.g., "GET", "POST", "PUT") path: The path to request skip_permission_checks: If True, bypass all permission checks for this request - **kwargs: Additional arguments to pass to httpx2 + **kwargs: Additional arguments to pass to httpx Returns: - httpx2.Response: The response from the request + httpx.Response: The response from the request """ from datasette.permissions import SkipPermissions @@ -3678,16 +3314,16 @@ class DatasetteClient: with _DatasetteClientContext(): if skip_permission_checks: with SkipPermissions(): - async with httpx2.AsyncClient( - transport=httpx2.ASGITransport(app=self.app), + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=self.app), cookies=kwargs.pop("cookies", None), ) as client: return await client.request( method, self._fix(path, avoid_path_rewrites), **kwargs ) else: - async with httpx2.AsyncClient( - transport=httpx2.ASGITransport(app=self.app), + async with httpx.AsyncClient( + transport=httpx.ASGITransport(app=self.app), cookies=kwargs.pop("cookies", None), ) as client: return await client.request( diff --git a/datasette/background_tasks.py b/datasette/background_tasks.py deleted file mode 100644 index 6b34fd85..00000000 --- a/datasette/background_tasks.py +++ /dev/null @@ -1,227 +0,0 @@ -""" -Supervised background-task registration for Datasette core. - -Plugins that need long-lived background work (a polling loop, a queue -consumer, a scheduled job runner) register it with -``datasette.add_background_task(func, name=None)`` - typically from a -``startup`` plugin hook - instead of fire-and-forgetting their own -``asyncio.create_task()``. Core owns: - -- **references**: every launched ``asyncio.Task`` is kept alive on a - :class:`BackgroundTaskSupervisor`, so it can never be silently garbage - collected the way an unreferenced ``create_task()`` call can be; -- **launch timing**: registered work is buffered until - :meth:`BackgroundTaskSupervisor.launch_all` runs, which core arranges to - happen only after *every* plugin's ``startup`` hook has finished - so - a task that depends on another plugin having registered something first - doesn't need ``tryfirst=True`` ordering tricks; -- **crash surfacing**: an unhandled exception in a background task is - logged with its full traceback to the ``datasette.background_tasks`` - logger and recorded on the handle, instead of becoming an "Task - exception was never retrieved" warning nobody sees; -- **cancellation**: :meth:`BackgroundTaskSupervisor.cancel_all` cancels - every task still running and waits (with a grace period) for them to - actually stop. -""" - -from __future__ import annotations - -import asyncio -import datetime -import functools -import logging -from collections.abc import Awaitable, Callable - -logger = logging.getLogger("datasette.background_tasks") - - -def _utcnow_iso() -> str: - return datetime.datetime.now(datetime.timezone.utc).isoformat() - - -def _function_path(func: Callable) -> str: - """Describe the callable without guessing which plugin registered it.""" - while isinstance(func, functools.partial): - func = func.func - if not hasattr(func, "__qualname__"): - func = type(func).__call__ - return f"{func.__module__}.{func.__qualname__}" - - -class BackgroundTask: - """A handle to a single piece of supervised background work. - - States: ``registered`` (added but not yet launched) -> ``running`` -> - one of ``completed`` (returned cleanly), ``crashed`` (raised an - exception other than ``CancelledError`` - see ``.exception``), or - ``cancelled`` (``.cancel()`` was called, or it was still running at - shutdown). - """ - - def __init__( - self, - name: str, - func: Callable[[object], Awaitable[None]], - ): - self.name = name - self.state = "registered" - self.task: asyncio.Task | None = None - self.exception: BaseException | None = None - self.started_at: str | None = None - self.function = _function_path(func) - self._func = func - self._supervisor: BackgroundTaskSupervisor | None = None - - def cancel(self) -> None: - """Cancel this task. - - If it has already been launched, cancels the underlying - ``asyncio.Task`` - its state becomes ``cancelled`` once the - cancellation is observed (asynchronously, via the task's done - callback). If it has not been launched yet, this is a no-op as - far as asyncio is concerned (there's no task to cancel) but it - deregisters the handle from its supervisor so it never runs. - """ - if self.task is not None: - self.task.cancel() - elif self._supervisor is not None: - self._supervisor._deregister(self) - - def __repr__(self) -> str: - return f"" - - -class BackgroundTaskSupervisor: - """Owns registration and launch of every :class:`BackgroundTask` for a - single ``Datasette`` instance. - - Registration (:meth:`add`) is separate from launch - (:meth:`launch_all`): plugins register work whenever convenient - (typically from a ``startup`` hook, but request handlers can register - dynamic per-job work too), and it either sits buffered until - :meth:`launch_all` runs, or - if :meth:`launch_all` has already run - - starts immediately. - - Strong references to every :class:`BackgroundTask` (and its - ``asyncio.Task``) are kept for the life of the instance, by design - - that's what makes the enrichments-style "fire-and-forget task gets - garbage collected mid-flight" bug impossible here. There is currently - no pruning of completed/crashed/cancelled tasks, so a plugin that - dynamically registers many short-lived tasks over a long process - lifetime (a per-job registration pattern, e.g. one task per queued - job) will grow this list without bound. That's an accepted v1 - trade-off in favour of full introspection (``/-/tasks``); revisit - with a pruning or capping policy if unbounded growth is reported in - practice. - """ - - def __init__(self, datasette): - self._datasette = datasette - self._tasks: list[BackgroundTask] = [] - self._names = set() - self._launched = False - self._lock = asyncio.Lock() - - def add(self, func, name=None) -> BackgroundTask: - base_name = name or getattr(func, "__qualname__", None) or repr(func) - actual_name = self._unique_name(base_name) - handle = BackgroundTask(actual_name, func) - handle._supervisor = self - self._tasks.append(handle) - self._names.add(actual_name) - if self._launched: - self._launch_one(handle) - return handle - - def _unique_name(self, base_name: str) -> str: - if base_name not in self._names: - return base_name - n = 2 - while f"{base_name}-{n}" in self._names: - n += 1 - return f"{base_name}-{n}" - - def _deregister(self, handle: BackgroundTask) -> None: - try: - self._tasks.remove(handle) - except ValueError: - pass - self._names.discard(handle.name) - - def _launch_one(self, handle: BackgroundTask) -> None: - handle.state = "running" - handle.started_at = _utcnow_iso() - handle.task = asyncio.create_task( - handle._func(self._datasette), name=handle.name - ) - handle.task.add_done_callback(functools.partial(_on_task_done, handle)) - - async def launch_all(self) -> None: - """Launch every currently-registered task that hasn't launched - yet. Idempotent and safe to call concurrently: subsequent (or - racing) calls are no-ops once the first has set ``self._launched``. - """ - if self._launched: - return - async with self._lock: - if self._launched: - return - self._launched = True - for handle in list(self._tasks): - if handle.task is None: - self._launch_one(handle) - - async def cancel_all(self, grace: float = 5.0) -> None: - """Cancel every task that isn't already done, then wait up to - ``grace`` seconds for them to actually finish. Stragglers still - running after that are logged by name (but left to finish or not - on their own - this does not forcibly kill them, asyncio has no - mechanism for that). - """ - handles_by_task = { - handle.task: handle for handle in self._tasks if handle.task is not None - } - pending = [task for task in handles_by_task if not task.done()] - for task in pending: - task.cancel() - if not pending: - return - _done, not_done = await asyncio.wait(pending, timeout=grace) - if not_done: - names = sorted(handles_by_task[task].name for task in not_done) - logger.warning( - "%d background task(s) did not finish within the %.1fs grace " - "period after cancellation: %s", - len(names), - grace, - ", ".join(names), - ) - - def tasks(self) -> list[BackgroundTask]: - """Return every registered :class:`BackgroundTask`, launched or - not, in registration order. Used by the ``/-/tasks`` debug - endpoint. - """ - return list(self._tasks) - - @property - def launched(self) -> bool: - """Whether :meth:`launch_all` has run yet - lets ``/-/tasks`` - distinguish "no tasks registered" from "tasks registered but - nothing has armed the launch yet" without reaching for the - private ``_launched`` attribute. - """ - return self._launched - - -def _on_task_done(handle: BackgroundTask, task: asyncio.Task) -> None: - if task.cancelled(): - handle.state = "cancelled" - return - exc = task.exception() - if exc is not None: - handle.state = "crashed" - handle.exception = exc - logger.error("Background task %r crashed", handle.name, exc_info=exc) - return - handle.state = "completed" diff --git a/datasette/blob_renderer.py b/datasette/blob_renderer.py index b6c8b77f..4d8c6bea 100644 --- a/datasette/blob_renderer.py +++ b/datasette/blob_renderer.py @@ -1,8 +1,7 @@ -import hashlib - from datasette import hookimpl +from datasette.utils.asgi import Response, BadRequest from datasette.utils import to_css_class -from datasette.utils.asgi import BadRequest, Response +import hashlib _BLOB_COLUMN = "_blob_column" _BLOB_HASH = "_blob_hash" diff --git a/datasette/cli.py b/datasette/cli.py index e83de93a..90a33e80 100644 --- a/datasette/cli.py +++ b/datasette/cli.py @@ -1,45 +1,43 @@ import asyncio +import uvicorn +import click +from click import formatting +from click.types import CompositeParamType +from click_default_group import DefaultGroup import functools import json import os import pathlib +from runpy import run_module import shutil +from subprocess import call import sys import textwrap import webbrowser -from runpy import run_module -from subprocess import call - -import click -import uvicorn -from click import formatting -from click.types import CompositeParamType -from click_default_group import DefaultGroup - from .app import ( + Datasette, DEFAULT_SETTINGS, SETTINGS, SQLITE_LIMIT_ATTACHED, - Datasette, pm, ) from .inspect import inspect_tables from .utils import ( - ConnectionProblem, LoadExtension, - SpatialiteConnectionProblem, - SpatialiteNotFound, StartupError, - StaticMount, - ValueAsBooleanError, check_connection, deep_dict_update, find_spatialite, + parse_metadata, + ConnectionProblem, + SpatialiteConnectionProblem, initial_path_for_datasette, pairs_to_nested_config, - parse_metadata, temporary_docker_directory, value_as_boolean, + SpatialiteNotFound, + StaticMount, + ValueAsBooleanError, ) from .utils.sqlite import sqlite3 from .utils.testing import TestClient @@ -77,7 +75,7 @@ class Setting(CompositeParamType): # Datasette 1.0, we turn bare setting names into setting.name # Type checking for those older settings default = DEFAULT_SETTINGS[name] - name = f"settings.{name}" + name = "settings.{}".format(name) if isinstance(default, bool): try: return name, "true" if value_as_boolean(value) else "false" @@ -157,11 +155,7 @@ async def inspect_(files, sqlite_extensions): app = Datasette([], immutables=files, sqlite_extensions=sqlite_extensions) data = {} for name, database in app.databases.items(): - - def _inspect_tables(conn): - return inspect_tables(conn, {}) - - tables = await database.execute_fn(_inspect_tables) + tables = await database.execute_fn(lambda conn: inspect_tables(conn, {})) data[name] = { "hash": database.hash, "size": database.size, @@ -177,6 +171,7 @@ async def inspect_(files, sqlite_extensions): @cli.group() def publish(): """Publish specified SQLite database files to the internet along with a Datasette-powered interface and API""" + pass # Register publish plugins @@ -501,7 +496,6 @@ def uninstall(packages, yes): "--internal", type=click.Path(), help="Path to a persistent Datasette internal SQLite database", - envvar="DATASETTE_INTERNAL", ) def serve( files, @@ -584,27 +578,27 @@ def serve( # https://github.com/simonw/datasette/issues/2389 deep_dict_update(config_data, settings_updates) - kwargs = { - "immutables": immutable, - "cache_headers": not reload, - "cors": cors, - "inspect_data": inspect_data, - "config": config_data, - "metadata": metadata_data, - "sqlite_extensions": sqlite_extensions, - "template_dir": template_dir, - "plugins_dir": plugins_dir, - "static_mounts": static, - "settings": None, # These are passed in config= now - "memory": memory, - "secret": secret, - "version_note": version_note, - "pdb": pdb, - "crossdb": crossdb, - "nolock": nolock, - "internal": internal, - "default_deny": default_deny, - } + kwargs = dict( + immutables=immutable, + cache_headers=not reload, + cors=cors, + inspect_data=inspect_data, + config=config_data, + metadata=metadata_data, + sqlite_extensions=sqlite_extensions, + template_dir=template_dir, + plugins_dir=plugins_dir, + static_mounts=static, + settings=None, # These are passed in config= now + memory=memory, + secret=secret, + version_note=version_note, + pdb=pdb, + crossdb=crossdb, + nolock=nolock, + internal=internal, + default_deny=default_deny, + ) # Separate directories from files directories = [f for f in files if os.path.isdir(f)] @@ -627,7 +621,9 @@ def serve( conn.close() else: raise click.ClickException( - f"Invalid value for '[FILES]...': Path '{file}' does not exist." + "Invalid value for '[FILES]...': Path '{}' does not exist.".format( + file + ) ) # Check for duplicate files by resolving all paths to their absolute forms @@ -668,6 +664,16 @@ def serve( # Private utility mechanism for writing unit tests return ds + # Run async soundness checks before startup hooks, since invoke_startup + # now populates internal tables which requires querying each database + run_sync(lambda: check_databases(ds)) + + # Run the "startup" plugin hooks + try: + run_sync(ds.invoke_startup) + except StartupError as e: + raise click.ClickException(e.args[0]) + if headers and not get: raise click.ClickException("--headers can only be used with --get") @@ -675,23 +681,10 @@ def serve( raise click.ClickException("--token can only be used with --get") if get: - # --get means we don't run Uvicorn at all - run_sync(lambda: check_databases(ds)) - - try: - run_sync(ds.invoke_startup) - except StartupError as e: - raise click.ClickException(e.args[0]) - - # --get never launches background tasks: TestClient's request below - # flows through the full ASGI stack, including the - # AsgiRunOnFirstRequest fallback, which would otherwise launch them. - ds._suppress_background_tasks = True - client = TestClient(ds) request_headers = {} if token: - request_headers["Authorization"] = f"Bearer {token}" + request_headers["Authorization"] = "Bearer {}".format(token) cookies = {} if actor: cookies["ds_actor"] = client.actor_cookie(json.loads(actor)) @@ -712,54 +705,30 @@ def serve( sys.exit(exit_code) return - # check_databases, invoke_startup() and the uvicorn server all run on a - # single event loop, so that anything a plugin's "startup" hook schedules - # on the loop (asyncio.create_task, Lock/Queue/Event objects, ...) is - # still alive when the server starts handling requests. - async def _serve_async(): - # Populate internal catalog tables before invoke_startup - await check_databases(ds) - - # Run the full startup sequence (immutable-database table-count - # precompute + the "startup" plugin hooks) via the same entry point - # AsgiLifespan/AsgiRunOnFirstRequest use, so it's not skipped when - # uvicorn's lifespan.startup fires moments later. - try: - await ds._startup_sequence() - except StartupError as e: - raise click.ClickException(e.args[0]) - - # Start the server - url = None - if root: - ds.root_enabled = True - url = "http://{}:{}{}?token={}".format( - host, port, ds.urls.path("-/auth-token"), ds._root_token - ) - click.echo(url) - if open_browser: - if url is None: - # Figure out most convenient URL - to table, database or homepage - path = await initial_path_for_datasette(ds) - url = f"http://{host}:{port}{path}" - webbrowser.open(url) - uvicorn_kwargs = { - "host": host, - "port": port, - "log_level": "info", - "lifespan": "on", - "workers": 1, - } - if uds: - uvicorn_kwargs["uds"] = uds - if ssl_keyfile: - uvicorn_kwargs["ssl_keyfile"] = ssl_keyfile - if ssl_certfile: - uvicorn_kwargs["ssl_certfile"] = ssl_certfile - server = uvicorn.Server(uvicorn.Config(ds.app(), **uvicorn_kwargs)) - await server.serve() - - asyncio.run(_serve_async()) + # Start the server + url = None + if root: + ds.root_enabled = True + url = "http://{}:{}{}?token={}".format( + host, port, ds.urls.path("-/auth-token"), ds._root_token + ) + click.echo(url) + if open_browser: + if url is None: + # Figure out most convenient URL - to table, database or homepage + path = run_sync(lambda: initial_path_for_datasette(ds)) + url = f"http://{host}:{port}{path}" + webbrowser.open(url) + uvicorn_kwargs = dict( + host=host, port=port, log_level="info", lifespan="on", workers=1 + ) + if uds: + uvicorn_kwargs["uds"] = uds + if ssl_keyfile: + uvicorn_kwargs["ssl_keyfile"] = ssl_keyfile + if ssl_certfile: + uvicorn_kwargs["ssl_certfile"] = ssl_certfile + uvicorn.run(ds.app(), **uvicorn_kwargs) @cli.command() @@ -916,7 +885,7 @@ async def check_databases(ds): ) except ConnectionProblem as e: raise click.UsageError( - f"Connection to {database.path} failed check: {e.args[0]!s}" + f"Connection to {database.path} failed check: {str(e.args[0])}" ) # If --crossdb and more than SQLITE_LIMIT_ATTACHED show warning if ( @@ -924,5 +893,9 @@ async def check_databases(ds): and len([db for db in ds.databases.values() if not db.is_memory]) > SQLITE_LIMIT_ATTACHED ): - msg = f"Warning: --crossdb only works with the first {SQLITE_LIMIT_ATTACHED} attached databases" + msg = ( + "Warning: --crossdb only works with the first {} attached databases".format( + SQLITE_LIMIT_ATTACHED + ) + ) click.echo(click.style(msg, bold=True, fg="yellow"), err=True) diff --git a/datasette/column_types.py b/datasette/column_types.py index 92fdd969..11a14ec0 100644 --- a/datasette/column_types.py +++ b/datasette/column_types.py @@ -64,14 +64,14 @@ class ColumnType: Return an HTML string to render this cell value, or None to fall through to the default render_cell plugin hook chain. """ - return + return None async def validate(self, value, datasette): """ Validate a value before it is written. Return None if valid, or a string error message if invalid. """ - return + return None async def transform_value(self, value, datasette): """ diff --git a/datasette/csrf.py b/datasette/csrf.py index a62f9473..df239aee 100644 --- a/datasette/csrf.py +++ b/datasette/csrf.py @@ -40,12 +40,12 @@ def _origin_tuple(value): scheme = (parsed.scheme or "").lower() host = (parsed.hostname or "").lower() if not scheme or not host: - raise ValueError(f"missing scheme or host in {value!r}") + raise ValueError("missing scheme or host in {!r}".format(value)) port = parsed.port # may raise ValueError on bad ports if port is None: port = DEFAULT_PORTS.get(scheme) if port is None: - raise ValueError(f"unknown default port for scheme {scheme!r}") + raise ValueError("unknown default port for scheme {!r}".format(scheme)) return scheme, host, port @@ -125,7 +125,9 @@ class CrossOriginProtectionMiddleware: return await self._forbid( send, - f"Sec-Fetch-Site was {sec_fetch_site!r}, expected 'same-origin' or 'none'", + "Sec-Fetch-Site was {!r}, expected 'same-origin' or 'none'".format( + sec_fetch_site + ), ) return @@ -139,11 +141,11 @@ class CrossOriginProtectionMiddleware: request_scheme = self._request_scheme(scope) try: origin_tuple = _origin_tuple(origin) - expected_tuple = _origin_tuple(f"{request_scheme}://{host}") + expected_tuple = _origin_tuple("{}://{}".format(request_scheme, host)) except ValueError: await self._forbid( send, - f"Malformed Origin {origin!r} or Host {host!r}", + "Malformed Origin {!r} or Host {!r}".format(origin, host), ) return @@ -153,7 +155,7 @@ class CrossOriginProtectionMiddleware: await self._forbid( send, - f"Origin {origin!r} does not match Host {host!r}", + "Origin {!r} does not match Host {!r}".format(origin, host), ) def _request_scheme(self, scope): @@ -161,8 +163,7 @@ class CrossOriginProtectionMiddleware: try: if self.datasette.setting("force_https_urls"): return "https" - except Exception: # noqa: BLE001, S110 - # Settings may not be readable this early; fall back to the ASGI scheme + except Exception: pass return scope.get("scheme") or "http" diff --git a/datasette/database.py b/datasette/database.py index 542b3012..e7fe1ed9 100644 --- a/datasette/database.py +++ b/datasette/database.py @@ -1,71 +1,33 @@ import asyncio import atexit -import contextvars +from collections import namedtuple import inspect import os +from pathlib import Path import queue +import sqlite_utils import sys import tempfile import threading -import time import uuid -from collections import namedtuple -from pathlib import Path -import sqlite_utils -from opentelemetry import context as otel_context_api -from opentelemetry.trace import Status, StatusCode - -from .inspect import inspect_hash -from .telemetry import ( - callback_name, - linked_root_span_kwargs, - record_operation_duration, - record_query_interrupted, - record_write_queue_wait, - sql_attribute, - sql_operation_name, - tracer, -) -from .telemetry_registry import ( - CALLBACK, - DB_NAMESPACE, - DB_OPERATION_NAME, - DB_QUERY, - DB_QUERY_EXECUTE, - DB_QUERY_TEXT, - DB_SYSTEM, - DB_WRITE_EXECUTE, - DB_WRITE_QUEUE_WAIT, - EXECUTEMANY, - EXECUTESCRIPT, - INTERRUPTED, - ISOLATED_CONNECTION, - PARAM_COUNT, - PARAM_SETS, - ROWS_RETURNED, - SQL_ERROR_SUPPRESSED, - TIME_LIMIT_MS, - TRANSACTION, - TRUNCATED, -) from .tracer import trace from .utils import ( call_with_supported_arguments, detect_fts, detect_primary_keys, detect_spatialite, - escape_sqlite, get_all_foreign_keys, get_outbound_foreign_keys, md5_not_usedforsecurity, - sqlite3, sqlite_timelimit, - table_column_details, + sqlite3, table_columns, + table_column_details, ) from .utils.sql_analysis import SQLAnalysis, analyze_sql_tables -from .utils.sqlite import sqlite_derived_table_dependencies, sqlite_hidden_table_names +from .utils.sqlite import sqlite_hidden_table_names +from .inspect import inspect_hash connections = threading.local() @@ -121,7 +83,6 @@ class Database: self.cached_hash = None self.cached_size = None self._cached_table_counts = None - self._cached_derived_table_dependencies = None self._write_thread = None self._write_queue = None self._closed = False @@ -130,15 +91,16 @@ class Database: # These are used when in non-threaded mode: self._read_connection = None self._write_connection = None - # Track file and memory connections, including reads on worker threads, - # so close() can release all of them from the calling thread. - self._all_connections = [] + # This is used to track all file connections so they can be closed + self._all_file_connections = [] if not is_temp_disk: self.mode = mode def _check_not_closed(self): if self._closed: - raise DatasetteClosedError(f"Database {self.name!r} has been closed") + raise DatasetteClosedError( + "Database {!r} has been closed".format(self.name) + ) def _remove_pending_execute_future(self, future): with self._pending_execute_futures_lock: @@ -177,18 +139,15 @@ class Database: if write: extra_kwargs["isolation_level"] = "IMMEDIATE" if self.memory_name: - uri = f"file:{self.memory_name}?mode=memory&cache=shared" + uri = "file:{}?mode=memory&cache=shared".format(self.memory_name) conn = sqlite3.connect( uri, uri=True, check_same_thread=False, **extra_kwargs ) if not write: conn.execute("PRAGMA query_only=1") - self._all_connections.append(conn) return conn if self.is_memory: - conn = sqlite3.connect(":memory:", uri=True, check_same_thread=False) - self._all_connections.append(conn) - return conn + return sqlite3.connect(":memory:", uri=True) # mode=ro or immutable=1? if self.is_mutable: @@ -205,7 +164,7 @@ class Database: conn = sqlite3.connect( f"file:{self.path}{qs}", uri=True, check_same_thread=False, **extra_kwargs ) - self._all_connections.append(conn) + self._all_file_connections.append(conn) if self.is_temp_disk and not self._wal_enabled: conn.execute("PRAGMA journal_mode=WAL") self._wal_enabled = True @@ -233,22 +192,23 @@ class Database: write_thread.join(timeout=10) if write_thread.is_alive(): sys.stderr.write( - f"Datasette: write thread for {self.name!r} did not exit within 10s\n" + "Datasette: write thread for {!r} did not exit within 10s\n".format( + self.name + ) ) sys.stderr.flush() for future in pending_execute_futures: try: future.result() - except Exception: # noqa: BLE001, S110 - # Shutdown teardown - a failed pending write must not block close() + except Exception: pass - # Close anything still tracked in _all_connections - for connection in self._all_connections: + # Close anything still tracked in _all_file_connections + for connection in self._all_file_connections: try: connection.close() - except Exception: # noqa: BLE001, S110 + except Exception: pass - self._all_connections = [] + self._all_file_connections = [] # Drop per-thread cached read connections we can reach try: delattr(connections, self._thread_local_id) @@ -258,13 +218,13 @@ class Database: if self._read_connection is not None: try: self._read_connection.close() - except Exception: # noqa: BLE001, S110 + except Exception: pass self._read_connection = None if self._write_connection is not None: try: self._write_connection.close() - except Exception: # noqa: BLE001, S110 + except Exception: pass self._write_connection = None if self.is_temp_disk: @@ -286,46 +246,19 @@ class Database: request=None, return_all=False, returning_limit=EXECUTE_WRITE_RETURNING_LIMIT, - transaction=True, - time_limit_ms=2000, ): self._check_not_closed() if returning_limit < 0: raise ValueError("returning_limit must be >= 0") - def execute_sql(conn): + def _inner(conn): cursor = conn.execute(sql, params or []) return ExecuteWriteResult.from_cursor( cursor, return_all=return_all, returning_limit=returning_limit ) - def _inner(conn): - try: - if time_limit_ms is None: - return execute_sql(conn) - with sqlite_timelimit(conn, time_limit_ms): - return execute_sql(conn) - except (sqlite3.OperationalError, sqlite3.DatabaseError) as e: - if e.args == ("interrupted",): - raise QueryInterrupted(e, sql, params) - raise - - with trace( # noqa: SIM117 - "sql", database=self.name, sql=sql.strip(), params=params - ): - with tracer.start_as_current_span(DB_QUERY, kind=DB_QUERY.kind) as span: - span.set_attribute(DB_SYSTEM, "sqlite") - span.set_attribute(DB_NAMESPACE, self.name) - span.set_attribute(DB_QUERY_TEXT, sql_attribute(sql)) - operation_name = sql_operation_name(sql) - if operation_name: - span.set_attribute(DB_OPERATION_NAME, operation_name) - if params: - span.set_attribute(PARAM_COUNT, len(params)) - with record_operation_duration(self.name, "write"): - results = await self._execute_write_fn( - _inner, block=block, request=request, transaction=transaction - ) + with trace("sql", database=self.name, sql=sql.strip(), params=params): + results = await self.execute_write_fn(_inner, block=block, request=request) return results async def execute_write_script(self, sql, block=True, request=None): @@ -334,19 +267,10 @@ class Database: def _inner(conn): return conn.executescript(sql) - with trace( # noqa: SIM117 - "sql", database=self.name, sql=sql.strip(), executescript=True - ): - # No db.operation.name, since the script can contain multiple statements - with tracer.start_as_current_span(DB_QUERY, kind=DB_QUERY.kind) as span: - span.set_attribute(DB_SYSTEM, "sqlite") - span.set_attribute(DB_NAMESPACE, self.name) - span.set_attribute(DB_QUERY_TEXT, sql_attribute(sql)) - span.set_attribute(EXECUTESCRIPT, True) - with record_operation_duration(self.name, "write"): - results = await self._execute_write_fn( - _inner, block=block, transaction=False, request=request - ) + with trace("sql", database=self.name, sql=sql.strip(), executescript=True): + results = await self.execute_write_fn( + _inner, block=block, transaction=False, request=request + ) return results async def execute_write_many(self, sql, params_seq, block=True, request=None): @@ -366,19 +290,9 @@ class Database: with trace( "sql", database=self.name, sql=sql.strip(), executemany=True ) as kwargs: - with tracer.start_as_current_span(DB_QUERY, kind=DB_QUERY.kind) as span: - span.set_attribute(DB_SYSTEM, "sqlite") - span.set_attribute(DB_NAMESPACE, self.name) - span.set_attribute(DB_QUERY_TEXT, sql_attribute(sql)) - span.set_attribute(EXECUTEMANY, True) - operation_name = sql_operation_name(sql) - if operation_name: - span.set_attribute(DB_OPERATION_NAME, operation_name) - with record_operation_duration(self.name, "write"): - results, count = await self._execute_write_fn( - _inner, block=block, request=request - ) - span.set_attribute(PARAM_SETS, count) + results, count = await self.execute_write_fn( + _inner, block=block, request=request + ) kwargs["count"] = count return results @@ -395,58 +309,31 @@ class Database: finally: isolated_connection.close() try: - self._all_connections.remove(isolated_connection) + self._all_file_connections.remove(isolated_connection) except ValueError: - # May already have been cleared by close(). + # Was probably a memory connection pass - with tracer.start_as_current_span(DB_QUERY, kind=DB_QUERY.kind) as span: - span.set_attribute(DB_SYSTEM, "sqlite") - span.set_attribute(DB_NAMESPACE, self.name) - span.set_attribute(CALLBACK, callback_name(fn)) - # Immutable databases run this on the read pool, not the write queue - with record_operation_duration(self.name, "write" if write else "read"): - if self.ds.executor is None: - # non-threaded mode - return _run() - if not write: - # Immutable database - no writes can ever occur, so there - # is no write queue to block; run against a fresh - # read-only connection - ctx = contextvars.copy_context() - return await asyncio.get_running_loop().run_in_executor( - self.ds.executor, ctx.run, _run - ) - # Threaded mode - send to write thread - return await self._send_to_write_thread(fn, isolated_connection=True) + if self.ds.executor is None: + # non-threaded mode + return _run() + if not write: + # Immutable database - no writes can ever occur, so there is no + # write queue to block; run against a fresh read-only connection + return await asyncio.get_running_loop().run_in_executor( + self.ds.executor, _run + ) + # Threaded mode - send to write thread + return await self._send_to_write_thread(fn, isolated_connection=True) async def analyze_sql(self, sql, params=None) -> SQLAnalysis: self._check_not_closed() - def _analyze_sql(conn): - return analyze_sql_tables(conn, sql, params, database_name=self.name) - - return await self.execute_isolated_fn(_analyze_sql) + return await self.execute_isolated_fn( + lambda conn: analyze_sql_tables(conn, sql, params, database_name=self.name) + ) async def execute_write_fn(self, fn, block=True, transaction=True, request=None): - """Run `fn(conn)` on the write connection, traced as a `db.query` span. - - The SQL-string write methods call `_execute_write_fn()` directly to - avoid creating a second span. - """ - self._check_not_closed() - # Record the name before _wrap_fn_with_hooks() wraps fn - name = callback_name(fn) - with tracer.start_as_current_span(DB_QUERY, kind=DB_QUERY.kind) as span: - span.set_attribute(DB_SYSTEM, "sqlite") - span.set_attribute(DB_NAMESPACE, self.name) - span.set_attribute(CALLBACK, name) - with record_operation_duration(self.name, "write"): - return await self._execute_write_fn( - fn, block=block, transaction=transaction, request=request - ) - - async def _execute_write_fn(self, fn, block=True, transaction=True, request=None): self._check_not_closed() pending_events = [] @@ -461,19 +348,9 @@ class Database: self.ds._prepare_connection(self._write_connection, self.name) if transaction: with self._write_connection: - self._write_connection.execute("BEGIN IMMEDIATE") result = fn(self._write_connection) else: result = fn(self._write_connection) - if not block: - # There is no write thread here, so the write has already - # finished. Hand back the same (task_id, reply_future) shape - # _send_to_write_thread() returns, with the future already - # resolved, so the block=False path below is identical in - # both modes. - reply_future = asyncio.get_running_loop().create_future() - reply_future.set_result(result) - result = (uuid.uuid4(), reply_future) else: result = await self._send_to_write_thread( fn, block=block, transaction=transaction @@ -489,8 +366,7 @@ class Database: async def _dispatch_events_after_write(): try: await reply_future - except Exception: # noqa: BLE001 - # The write failed; skip success events regardless of why + except Exception: # if the write failed, don't emit success events return for event in pending_events: @@ -543,24 +419,15 @@ class Database: self._write_thread = threading.Thread( target=self._execute_writes, daemon=True ) - self._write_thread.name = f"_execute_writes for database {self.name}" + self._write_thread.name = "_execute_writes for database {}".format( + self.name + ) self._write_thread.start() - task_id = uuid.uuid4() + task_id = uuid.uuid5(uuid.NAMESPACE_DNS, "datasette.io") loop = asyncio.get_running_loop() reply_future = loop.create_future() - # Capture the OpenTelemetry context and enqueue time for the write thread self._write_queue.put( - WriteTask( - fn, - task_id, - loop, - reply_future, - isolated_connection, - transaction, - otel_context_api.get_current(), - time.time_ns(), - block, - ) + WriteTask(fn, task_id, loop, reply_future, isolated_connection, transaction) ) if block: return await reply_future @@ -574,11 +441,8 @@ class Database: conn = None try: conn = self.connect(write=True) - # Threads do not inherit the caller's context, so any spans - # created by prepare_connection hooks here are root spans self.ds._prepare_connection(conn, self.name) - except Exception as e: # noqa: BLE001 - # Stored and re-raised to whoever queues the next write + except Exception as e: conn_exception = e while True: task = self._write_queue.get() @@ -586,105 +450,43 @@ class Database: if conn is not None: try: conn.close() - except Exception: # noqa: BLE001, S110 - # Best-effort close as the write thread exits + except Exception: pass return - # block=True: the caller awaits the result, so the write spans - # are children of the caller's span. The token must be detached - # in the finally block or the context leaks into later writes. - # block=False: the caller may finish first, so the write spans - # are root spans with a link back to the caller's span. - token = None - write_span_kwargs = {} - if task.block: - token = otel_context_api.attach(task.otel_context) + exception = None + result = None + if conn_exception is not None: + exception = conn_exception + elif task.isolated_connection: + try: + isolated_connection = self.connect(write=True) + try: + result = task.fn(isolated_connection) + finally: + isolated_connection.close() + try: + self._all_file_connections.remove(isolated_connection) + except ValueError: + # Was probably a memory connection + pass + except Exception as e: + sys.stderr.write("{}\n".format(e)) + sys.stderr.flush() + exception = e else: - write_span_kwargs = linked_root_span_kwargs(task.otel_context) - try: - exception = None - result = None - # Span covers the time from enqueue to dequeue - dequeued_at_ns = time.time_ns() - tracer.start_span( - DB_WRITE_QUEUE_WAIT, - start_time=task.enqueued_at_ns, - **write_span_kwargs, - ).end(end_time=dequeued_at_ns) - record_write_queue_wait(self.name, dequeued_at_ns - task.enqueued_at_ns) - if conn_exception is not None: - exception = conn_exception - elif task.isolated_connection: - try: - with tracer.start_as_current_span( - DB_WRITE_EXECUTE, **write_span_kwargs - ) as span: - span.set_attribute( - ISOLATED_CONNECTION, - task.isolated_connection, - ) - span.set_attribute(TRANSACTION, task.transaction) - isolated_connection = self.connect(write=True) - try: - result = task.fn(isolated_connection) - finally: - isolated_connection.close() - try: - self._all_connections.remove(isolated_connection) - except ValueError: - # May already have been cleared by close(). - pass - except Exception as e: # noqa: BLE001 - # Write thread must survive any task failure or the database wedges - sys.stderr.write(f"{e}\n") - sys.stderr.flush() - exception = e - else: - try: - with tracer.start_as_current_span( - DB_WRITE_EXECUTE, **write_span_kwargs - ) as span: - span.set_attribute( - ISOLATED_CONNECTION, - task.isolated_connection, - ) - span.set_attribute(TRANSACTION, task.transaction) - if task.transaction: - with conn: - conn.execute("BEGIN IMMEDIATE") - result = task.fn(conn) - else: - result = task.fn(conn) - except Exception as e: # noqa: BLE001 - sys.stderr.write(f"{e}\n") - sys.stderr.flush() - exception = e - _deliver_write_result(task, result, exception) - finally: - if token is not None: - otel_context_api.detach(token) + try: + if task.transaction: + with conn: + result = task.fn(conn) + else: + result = task.fn(conn) + except Exception as e: + sys.stderr.write("{}\n".format(e)) + sys.stderr.flush() + exception = e + _deliver_write_result(task, result, exception) async def execute_fn(self, fn): - """Run `fn(conn)` on a read connection, traced as a `db.query` span. - - `execute()` calls `_execute_fn()` directly to avoid creating a second - span. - """ - self._check_not_closed() - - def fn_in_execute_span(conn): - # Runs on the worker thread - with tracer.start_as_current_span(DB_QUERY_EXECUTE): - return fn(conn) - - with tracer.start_as_current_span(DB_QUERY, kind=DB_QUERY.kind) as span: - span.set_attribute(DB_SYSTEM, "sqlite") - span.set_attribute(DB_NAMESPACE, self.name) - span.set_attribute(CALLBACK, callback_name(fn)) - with record_operation_duration(self.name, "read"): - return await self._execute_fn(fn_in_execute_span) - - async def _execute_fn(self, fn): self._check_not_closed() if self.ds.executor is None: # non-threaded mode @@ -704,11 +506,7 @@ class Database: with self._pending_execute_futures_lock: self._check_not_closed() - # Run in a copy of the caller's context so spans created in the - # thread have the correct parent. This needs a fresh copy for - # each submit, since a Context cannot be entered concurrently. - ctx = contextvars.copy_context() - future = self.ds.executor.submit(ctx.run, in_thread) + future = self.ds.executor.submit(in_thread) self._pending_execute_futures.add(future) future.add_done_callback(self._remove_pending_execute_future) return await asyncio.wrap_future(future) @@ -725,101 +523,46 @@ class Database: """Executes sql against db_name in a thread""" self._check_not_closed() page_size = page_size or self.ds.page_size - time_limit_ms = self.ds.sql_time_limit_ms - # Callers that pass a shorter custom_time_limit, such as table counts - # and facet suggestions, expect timeouts, so they are not span errors - timeout_expected = bool(custom_time_limit) and custom_time_limit < time_limit_ms - if timeout_expected: - time_limit_ms = custom_time_limit def sql_operation_in_thread(conn): - # Expected timeouts and errors with log_sql_errors=False are not - # recorded as span errors, so exceptions are handled explicitly - with tracer.start_as_current_span( - DB_QUERY_EXECUTE, - record_exception=False, - set_status_on_exception=False, - ) as execute_span: + time_limit_ms = self.ds.sql_time_limit_ms + if custom_time_limit and custom_time_limit < time_limit_ms: + time_limit_ms = custom_time_limit + + with sqlite_timelimit(conn, time_limit_ms): try: - with sqlite_timelimit(conn, time_limit_ms): - try: - cursor = conn.cursor() - cursor.execute(sql, params if params is not None else {}) - max_returned_rows = self.ds.max_returned_rows - if max_returned_rows == page_size: - max_returned_rows += 1 - if max_returned_rows and truncate: - rows = cursor.fetchmany(max_returned_rows + 1) - truncated = len(rows) > max_returned_rows - rows = rows[:max_returned_rows] - else: - rows = cursor.fetchall() - truncated = False - except (sqlite3.OperationalError, sqlite3.DatabaseError) as e: - if e.args == ("interrupted",): - raise QueryInterrupted(e, sql, params) - if log_sql_errors: - sys.stderr.write( - f"ERROR: conn={conn}, sql = {sql!r}, params = {params}: {e}\n" - ) - sys.stderr.flush() - raise - except QueryInterrupted as e: - if not timeout_expected: - execute_span.record_exception(e) - execute_span.set_status(Status(StatusCode.ERROR, str(e))) - raise - except Exception as e: - if log_sql_errors: - execute_span.record_exception(e) - execute_span.set_status(Status(StatusCode.ERROR, str(e))) - raise - - if truncate: - return Results(rows, truncated, cursor.description) - - else: - return Results(rows, False, cursor.description) - - with trace( # noqa: SIM117 - "sql", database=self.name, sql=sql.strip(), params=params - ): - with tracer.start_as_current_span( - DB_QUERY, - kind=DB_QUERY.kind, - record_exception=False, - set_status_on_exception=False, - ) as span: - span.set_attribute(DB_SYSTEM, "sqlite") - span.set_attribute(DB_NAMESPACE, self.name) - span.set_attribute(DB_QUERY_TEXT, sql_attribute(sql)) - span.set_attribute(TIME_LIMIT_MS, time_limit_ms) - operation_name = sql_operation_name(sql) - if operation_name: - span.set_attribute(DB_OPERATION_NAME, operation_name) - if params: - span.set_attribute(PARAM_COUNT, len(params)) - try: - with record_operation_duration(self.name, "read"): - results = await self._execute_fn(sql_operation_in_thread) - except QueryInterrupted as e: - span.set_attribute(INTERRUPTED, True) - if not timeout_expected: - span.set_status(Status(StatusCode.ERROR, str(e))) - span.record_exception(e) - record_query_interrupted(self.name) - raise - except Exception as e: - # log_sql_errors=False callers, such as facet suggestion, - # expect some queries to fail - if log_sql_errors: - span.record_exception(e) - span.set_status(Status(StatusCode.ERROR, str(e))) + cursor = conn.cursor() + cursor.execute(sql, params if params is not None else {}) + max_returned_rows = self.ds.max_returned_rows + if max_returned_rows == page_size: + max_returned_rows += 1 + if max_returned_rows and truncate: + rows = cursor.fetchmany(max_returned_rows + 1) + truncated = len(rows) > max_returned_rows + rows = rows[:max_returned_rows] else: - span.set_attribute(SQL_ERROR_SUPPRESSED, True) + rows = cursor.fetchall() + truncated = False + except (sqlite3.OperationalError, sqlite3.DatabaseError) as e: + if e.args == ("interrupted",): + raise QueryInterrupted(e, sql, params) + if log_sql_errors: + sys.stderr.write( + "ERROR: conn={}, sql = {}, params = {}: {}\n".format( + conn, repr(sql), params, e + ) + ) + sys.stderr.flush() raise - span.set_attribute(TRUNCATED, results.truncated) - span.set_attribute(ROWS_RETURNED, len(results.rows)) + + if truncate: + return Results(rows, truncated, cursor.description) + + else: + return Results(rows, False, cursor.description) + + with trace("sql", database=self.name, sql=sql.strip(), params=params): + results = await self.execute_fn(sql_operation_in_thread) return results @property @@ -860,7 +603,7 @@ class Database: try: table_count = ( await self.execute( - f"select count(*) from (select * from {escape_sqlite(table)} limit {self.count_limit + 1})", + f"select count(*) from (select * from [{table}] limit {self.count_limit + 1})", custom_time_limit=limit, ) ).rows[0][0] @@ -910,32 +653,17 @@ class Database: ) return [r[0] for r in results.rows] - # Named functions rather than lambdas give more useful datasette.callback - # span attributes - async def table_columns(self, table): - def _table_columns(conn): - return table_columns(conn, table) - - return await self.execute_fn(_table_columns) + return await self.execute_fn(lambda conn: table_columns(conn, table)) async def table_column_details(self, table): - def _table_column_details(conn): - return table_column_details(conn, table) - - return await self.execute_fn(_table_column_details) + return await self.execute_fn(lambda conn: table_column_details(conn, table)) async def primary_keys(self, table): - def _primary_keys(conn): - return detect_primary_keys(conn, table) - - return await self.execute_fn(_primary_keys) + return await self.execute_fn(lambda conn: detect_primary_keys(conn, table)) async def fts_table(self, table): - def _fts_table(conn): - return detect_fts(conn, table) - - return await self.execute_fn(_fts_table) + return await self.execute_fn(lambda conn: detect_fts(conn, table)) async def label_column_for_table(self, table): explicit_label_column = (await self.ds.table_config(self.name, table)).get( @@ -979,9 +707,9 @@ class Database: column_names and len(column_names) == 2 and ("id" in column_names or "pk" in column_names) - and set(column_names) != {"id", "pk"} + and not set(column_names) == {"id", "pk"} ): - return next(c for c in column_names if c not in ("id", "pk")) + return [c for c in column_names if c not in ("id", "pk")][0] # Couldn't find a label: return None @@ -1027,17 +755,6 @@ class Database: return hidden_tables - async def derived_table_dependencies(self): - """Return implementation tables and the tables they derive from.""" - schema_version = (await self.execute("PRAGMA schema_version")).first()[0] - if ( - self._cached_derived_table_dependencies is None - or self._cached_derived_table_dependencies[0] != schema_version - ): - dependencies = await self.execute_fn(sqlite_derived_table_dependencies) - self._cached_derived_table_dependencies = (schema_version, dependencies) - return self._cached_derived_table_dependencies[1] - async def view_names(self): results = await self.execute("select name from sqlite_master where type='view'") return [r[0] for r in results.rows] @@ -1133,28 +850,16 @@ def _apply_write_wrapper(fn, wrapper_factory, track_event): class WriteTask: __slots__ = ( - "block", - "enqueued_at_ns", "fn", - "isolated_connection", - "loop", - "otel_context", - "reply_future", "task_id", + "loop", + "reply_future", + "isolated_connection", "transaction", ) def __init__( - self, - fn, - task_id, - loop, - reply_future, - isolated_connection, - transaction, - otel_context, - enqueued_at_ns, - block, + self, fn, task_id, loop, reply_future, isolated_connection, transaction ): self.fn = fn self.task_id = task_id @@ -1162,9 +867,6 @@ class WriteTask: self.reply_future = reply_future self.isolated_connection = isolated_connection self.transaction = transaction - self.otel_context = otel_context - self.enqueued_at_ns = enqueued_at_ns - self.block = block def _deliver_write_result(task, result, exception): @@ -1193,7 +895,7 @@ class QueryInterrupted(Exception): self.params = params def __str__(self): - return f"QueryInterrupted: {self.e}" + return "QueryInterrupted: {}".format(self.e) class MultipleValues(Exception): diff --git a/datasette/default_actions.py b/datasette/default_actions.py index ee165ae5..602e0df4 100644 --- a/datasette/default_actions.py +++ b/datasette/default_actions.py @@ -2,8 +2,8 @@ from datasette import hookimpl from datasette.permissions import Action from datasette.resources import ( DatabaseResource, - QueryResource, TableResource, + QueryResource, ) diff --git a/datasette/default_column_types.py b/datasette/default_column_types.py index 6def3698..f90a733e 100644 --- a/datasette/default_column_types.py +++ b/datasette/default_column_types.py @@ -6,17 +6,6 @@ import markupsafe from datasette import hookimpl from datasette.column_types import ColumnType, SQLiteType -_HTTP_URL_RE = re.compile(r"https?://\S+", re.IGNORECASE) - - -def _normalize_http_url(value): - if not isinstance(value, str): - return None - normalized = value.strip() - if not _HTTP_URL_RE.fullmatch(normalized): - return None - return normalized - class UrlColumnType(ColumnType): name = "url" @@ -26,10 +15,7 @@ class UrlColumnType(ColumnType): async def render_cell(self, value, column, table, database, datasette, request): if not value or not isinstance(value, str): return None - normalized = _normalize_http_url(value) - if normalized is None: - return markupsafe.escape(value.strip()) - escaped = markupsafe.escape(normalized) + escaped = markupsafe.escape(value.strip()) return markupsafe.Markup(f'{escaped}') async def validate(self, value, datasette): @@ -37,7 +23,7 @@ class UrlColumnType(ColumnType): return None if not isinstance(value, str): return "URL must be a string" - if _normalize_http_url(value) is None: + if not re.match(r"^https?://\S+$", value.strip()): return "Invalid URL" return None diff --git a/datasette/default_magic_parameters.py b/datasette/default_magic_parameters.py index bff5f0a7..91c1c5aa 100644 --- a/datasette/default_magic_parameters.py +++ b/datasette/default_magic_parameters.py @@ -1,9 +1,8 @@ +from datasette import hookimpl import datetime import os import time -from datasette import hookimpl - def header(key, request): key = key.replace("_", "-").encode("utf-8") diff --git a/datasette/default_permissions/__init__.py b/datasette/default_permissions/__init__.py index dee5df42..6cd46f04 100644 --- a/datasette/default_permissions/__init__.py +++ b/datasette/default_permissions/__init__.py @@ -17,29 +17,18 @@ UNION/INTERSECT operations. The order of evaluation is: from __future__ import annotations -from .config import config_permissions_sql as config_permissions_sql -from .defaults import ( - DEFAULT_ALLOW_ACTIONS as DEFAULT_ALLOW_ACTIONS, -) -from .defaults import ( - default_action_permissions_sql as default_action_permissions_sql, -) -from .defaults import ( - # Avoid "datasette.default_permissions" does not explicitly export attribute - default_allow_sql_check as default_allow_sql_check, -) -from .defaults import ( - default_query_permissions_sql as default_query_permissions_sql, -) -from .restrictions import ( - ActorRestrictions as ActorRestrictions, -) - # Re-export all hooks and public utilities from .restrictions import ( actor_restrictions_sql as actor_restrictions_sql, -) -from .restrictions import ( restrictions_allow_action as restrictions_allow_action, + ActorRestrictions as ActorRestrictions, ) from .root import root_user_permissions_sql as root_user_permissions_sql +from .config import config_permissions_sql as config_permissions_sql +from .defaults import ( + # Avoid "datasette.default_permissions" does not explicitly export attribute + default_allow_sql_check as default_allow_sql_check, + default_action_permissions_sql as default_action_permissions_sql, + default_query_permissions_sql as default_query_permissions_sql, + DEFAULT_ALLOW_ACTIONS as DEFAULT_ALLOW_ACTIONS, +) diff --git a/datasette/default_permissions/config.py b/datasette/default_permissions/config.py index a4f5a4de..aab87c1c 100644 --- a/datasette/default_permissions/config.py +++ b/datasette/default_permissions/config.py @@ -6,7 +6,7 @@ Applies permission rules from datasette.yaml configuration. from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, List, Optional, Set, Tuple if TYPE_CHECKING: from datasette.app import Datasette @@ -55,8 +55,8 @@ class ConfigPermissionProcessor: def __init__( self, - datasette: Datasette, - actor: dict | None, + datasette: "Datasette", + actor: Optional[dict], action: str, ): self.datasette = datasette @@ -74,8 +74,8 @@ class ConfigPermissionProcessor: self.restrictions = actor.get("_r", {}) if actor else {} # Pre-compute restriction info for efficiency - self.restricted_databases: set[str] = set() - self.restricted_tables: set[tuple[str, str]] = set() + self.restricted_databases: Set[str] = set() + self.restricted_tables: Set[Tuple[str, str]] = set() if self.has_restrictions: self.restricted_databases = { @@ -92,27 +92,16 @@ class ConfigPermissionProcessor: # Tables implicitly reference their parent databases self.restricted_databases.update(db for db, _ in self.restricted_tables) - # Resolve identity keys once per action, rather than scanning the - # restriction allowlist for every configured table's allow block. - self.restricted_table_keys = { - (db, self.action_obj.normalize_child(table) if self.action_obj else table) - for db, table in self.restricted_tables - } - - def evaluate_allow_block(self, allow_block: Any) -> bool | None: + def evaluate_allow_block(self, allow_block: Any) -> Optional[bool]: """Evaluate an allow block against the current actor.""" if allow_block is None: return None - # Values passed using ``-s permissions.* 1`` or ``0`` are parsed as - # integers, but should retain the CLI's boolean 1/0 behavior. - if isinstance(allow_block, int) and allow_block in (0, 1): - return bool(allow_block) return actor_matches_allow(self.actor, allow_block) def is_in_restriction_allowlist( self, - parent: str | None, - child: str | None, + parent: Optional[str], + child: Optional[str], ) -> bool: """Check if resource is allowed by actor restrictions.""" if not self.has_restrictions: @@ -132,10 +121,8 @@ class ConfigPermissionProcessor: if parent: table_restrictions = (self.restrictions.get("r", {}) or {}).get(parent, {}) if child: - child_key = ( - self.action_obj.normalize_child(child) if self.action_obj else child - ) - if (parent, child_key) in self.restricted_table_keys: + table_actions = table_restrictions.get(child, []) + if self.action_checks.intersection(table_actions): return True else: # Parent query should proceed if any child in this database is allowlisted @@ -156,9 +143,9 @@ class ConfigPermissionProcessor: def add_permissions_rule( self, - parent: str | None, - child: str | None, - permissions_block: dict | None, + parent: Optional[str], + child: Optional[str], + permissions_block: Optional[dict], scope_desc: str, ) -> None: """Add a rule from a permissions:{action} block.""" @@ -178,8 +165,8 @@ class ConfigPermissionProcessor: def add_allow_block_rule( self, - parent: str | None, - child: str | None, + parent: Optional[str], + child: Optional[str], allow_block: Any, scope_desc: str, ) -> None: @@ -211,8 +198,8 @@ class ConfigPermissionProcessor: def _add_restriction_gate_denies( self, - parent: str | None, - child: str | None, + parent: Optional[str], + child: Optional[str], is_allowed: bool, scope_desc: str, ) -> None: @@ -244,7 +231,7 @@ class ConfigPermissionProcessor: if db_name == parent: self.collector.add(db_name, table_name, False, reason) - def process(self) -> PermissionSQL | None: + def process(self) -> Optional[PermissionSQL]: """Process all config rules and return combined PermissionSQL.""" self._process_root_permissions() self._process_databases() @@ -434,10 +421,10 @@ class ConfigPermissionProcessor: @hookimpl(specname="permission_resources_sql") async def config_permissions_sql( - datasette: Datasette, - actor: dict | None, + datasette: "Datasette", + actor: Optional[dict], action: str, -) -> list[PermissionSQL] | None: +) -> Optional[List[PermissionSQL]]: """ Apply permission rules from datasette.yaml configuration. diff --git a/datasette/default_permissions/defaults.py b/datasette/default_permissions/defaults.py index 6f97812b..5bc74425 100644 --- a/datasette/default_permissions/defaults.py +++ b/datasette/default_permissions/defaults.py @@ -6,7 +6,7 @@ Provides default allow rules for standard view/execute actions. from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from datasette.app import Datasette @@ -29,28 +29,29 @@ DEFAULT_ALLOW_ACTIONS = frozenset( @hookimpl(specname="permission_resources_sql") async def default_allow_sql_check( - datasette: Datasette, - actor: dict | None, + datasette: "Datasette", + actor: Optional[dict], action: str, -) -> PermissionSQL | None: +) -> Optional[PermissionSQL]: """ Enforce the default_allow_sql setting. When default_allow_sql is false (the default), execute-sql is denied unless explicitly allowed by config or other rules. """ - if action == "execute-sql" and not datasette.setting("default_allow_sql"): - return PermissionSQL.deny(reason="default_allow_sql is false") + if action == "execute-sql": + if not datasette.setting("default_allow_sql"): + return PermissionSQL.deny(reason="default_allow_sql is false") return None @hookimpl(specname="permission_resources_sql") async def default_action_permissions_sql( - datasette: Datasette, - actor: dict | None, + datasette: "Datasette", + actor: Optional[dict], action: str, -) -> PermissionSQL | None: +) -> Optional[PermissionSQL]: """ Provide default allow rules for standard view/execute actions. @@ -70,10 +71,10 @@ async def default_action_permissions_sql( @hookimpl(specname="permission_resources_sql") async def default_query_permissions_sql( - datasette: Datasette, - actor: dict | None, + datasette: "Datasette", + actor: Optional[dict], action: str, -) -> PermissionSQL | None: +) -> Optional[PermissionSQL]: actor_id = actor.get("id") if isinstance(actor, dict) else None if action not in {"view-query", "update-query", "delete-query"}: diff --git a/datasette/default_permissions/helpers.py b/datasette/default_permissions/helpers.py index 5e59b7b4..47e03569 100644 --- a/datasette/default_permissions/helpers.py +++ b/datasette/default_permissions/helpers.py @@ -5,7 +5,7 @@ Shared helper utilities for default permission implementations. from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional, Set if TYPE_CHECKING: from datasette.app import Datasette @@ -13,7 +13,7 @@ if TYPE_CHECKING: from datasette.permissions import PermissionSQL -def get_action_name_variants(datasette: Datasette, action: str) -> set[str]: +def get_action_name_variants(datasette: "Datasette", action: str) -> Set[str]: """ Get all name variants for an action (full name and abbreviation). @@ -27,7 +27,7 @@ def get_action_name_variants(datasette: Datasette, action: str) -> set[str]: return variants -def action_in_list(datasette: Datasette, action: str, action_list: list) -> bool: +def action_in_list(datasette: "Datasette", action: str, action_list: list) -> bool: """Check if an action (or its abbreviation) is in a list.""" return bool(get_action_name_variants(datasette, action).intersection(action_list)) @@ -36,8 +36,8 @@ def action_in_list(datasette: Datasette, action: str, action_list: list) -> bool class PermissionRow: """A single permission rule row.""" - parent: str | None - child: str | None + parent: Optional[str] + child: Optional[str] allow: bool reason: str @@ -46,14 +46,14 @@ class PermissionRowCollector: """Collects permission rows and converts them to PermissionSQL.""" def __init__(self, prefix: str = "row"): - self.rows: list[PermissionRow] = [] + self.rows: List[PermissionRow] = [] self.prefix = prefix def add( self, - parent: str | None, - child: str | None, - allow: bool | None, + parent: Optional[str], + child: Optional[str], + allow: Optional[bool], reason: str, if_not_none: bool = False, ) -> None: @@ -62,7 +62,7 @@ class PermissionRowCollector: return self.rows.append(PermissionRow(parent, child, allow, reason)) - def to_permission_sql(self) -> PermissionSQL | None: + def to_permission_sql(self) -> Optional[PermissionSQL]: """Convert collected rows to a PermissionSQL object.""" if not self.rows: return None diff --git a/datasette/default_permissions/restrictions.py b/datasette/default_permissions/restrictions.py index d30ebd3f..a22cd7e5 100644 --- a/datasette/default_permissions/restrictions.py +++ b/datasette/default_permissions/restrictions.py @@ -8,7 +8,7 @@ contains allowlists of resources the actor can access. from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional, Set, Tuple if TYPE_CHECKING: from datasette.app import Datasette @@ -23,12 +23,12 @@ from .helpers import action_in_list, get_action_name_variants class ActorRestrictions: """Parsed actor restrictions from the _r key.""" - global_actions: list[str] # _r.a - globally allowed actions + global_actions: List[str] # _r.a - globally allowed actions database_actions: dict # _r.d - {db_name: [actions]} table_actions: dict # _r.r - {db_name: {table: [actions]}} @classmethod - def from_actor(cls, actor: dict | None) -> ActorRestrictions | None: + def from_actor(cls, actor: Optional[dict]) -> Optional["ActorRestrictions"]: """Parse restrictions from actor dict. Returns None if no restrictions.""" if not actor: return None @@ -44,11 +44,11 @@ class ActorRestrictions: table_actions=restrictions.get("r", {}), ) - def is_action_globally_allowed(self, datasette: Datasette, action: str) -> bool: + def is_action_globally_allowed(self, datasette: "Datasette", action: str) -> bool: """Check if action is in the global allowlist.""" return action_in_list(datasette, action, self.global_actions) - def get_allowed_databases(self, datasette: Datasette, action: str) -> set[str]: + def get_allowed_databases(self, datasette: "Datasette", action: str) -> Set[str]: """Get database names where this action is allowed.""" allowed = set() for db_name, db_actions in self.database_actions.items(): @@ -57,8 +57,8 @@ class ActorRestrictions: return allowed def get_allowed_tables( - self, datasette: Datasette, action: str - ) -> set[tuple[str, str]]: + self, datasette: "Datasette", action: str + ) -> Set[Tuple[str, str]]: """Get (database, table) pairs where this action is allowed.""" allowed = set() for db_name, tables in self.table_actions.items(): @@ -70,10 +70,10 @@ class ActorRestrictions: @hookimpl(specname="permission_resources_sql") async def actor_restrictions_sql( - datasette: Datasette, - actor: dict | None, + datasette: "Datasette", + actor: Optional[dict], action: str, -) -> list[PermissionSQL] | None: +) -> Optional[List[PermissionSQL]]: """ Handle actor restriction-based permission rules. @@ -140,10 +140,10 @@ async def actor_restrictions_sql( def restrictions_allow_action( - datasette: Datasette, + datasette: "Datasette", restrictions: dict, action: str, - resource: str | tuple[str, str] | None, + resource: Optional[str | Tuple[str, str]], ) -> bool: """ Check if restrictions allow the requested action on the requested resource. @@ -185,15 +185,11 @@ def restrictions_allow_action( # Check table/resource level if resource is not None and not isinstance(resource, str) and len(resource) == 2: database, table = resource - action_obj = datasette.actions.get(action) - normalize = action_obj.normalize_child if action_obj else lambda name: name - for table_name, table_allowed in ( - restrictions.get("r", {}).get(database, {}).items() - ): - if normalize(table_name) == normalize(table): - assert isinstance(table_allowed, list) - if to_check.intersection(table_allowed): - return True + table_allowed = restrictions.get("r", {}).get(database, {}).get(table) + if table_allowed is not None: + assert isinstance(table_allowed, list) + if to_check.intersection(table_allowed): + return True # This action is not explicitly allowed, so reject it return False diff --git a/datasette/default_permissions/root.py b/datasette/default_permissions/root.py index 22d13f65..4931f7ff 100644 --- a/datasette/default_permissions/root.py +++ b/datasette/default_permissions/root.py @@ -6,7 +6,7 @@ Grants full permissions to the root user when --root flag is used. from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from datasette.app import Datasette @@ -17,9 +17,9 @@ from datasette.permissions import PermissionSQL @hookimpl(specname="permission_resources_sql") async def root_user_permissions_sql( - datasette: Datasette, - actor: dict | None, -) -> PermissionSQL | None: + datasette: "Datasette", + actor: Optional[dict], +) -> Optional[PermissionSQL]: """ Grant root user full permissions when --root flag is used. """ diff --git a/datasette/default_permissions/sqlite_statistics.py b/datasette/default_permissions/sqlite_statistics.py deleted file mode 100644 index 11fd4008..00000000 --- a/datasette/default_permissions/sqlite_statistics.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Default table-access policy for SQLite optimizer statistics.""" - -import json - -from datasette import hookimpl -from datasette.permissions import PermissionSQL - - -@hookimpl -def permission_resources_sql(action): - if action != "view-table": - return None - return PermissionSQL( - sql=""" - SELECT database_name AS parent, value AS child, 0 AS allow, - 'SQLite statistics tables are denied by default' AS reason - FROM catalog_databases - CROSS JOIN json_each(:sqlite_statistics_names) - """, - params={ - "sqlite_statistics_names": json.dumps( - ["sqlite_stat1", "sqlite_stat2", "sqlite_stat3", "sqlite_stat4"] - ) - }, - ) diff --git a/datasette/default_permissions/tokens.py b/datasette/default_permissions/tokens.py index 52daf8a2..7a359dc6 100644 --- a/datasette/default_permissions/tokens.py +++ b/datasette/default_permissions/tokens.py @@ -7,7 +7,7 @@ to datasette.verify_token() so all registered handlers are tried. from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from datasette.app import Datasette @@ -17,13 +17,15 @@ from datasette.tokens import SignedTokenHandler @hookimpl -def register_token_handler(datasette: Datasette): +def register_token_handler(datasette: "Datasette"): """Register the default signed token handler.""" return SignedTokenHandler() @hookimpl(specname="actor_from_request") -async def actor_from_signed_api_token(datasette: Datasette, request) -> dict | None: +async def actor_from_signed_api_token( + datasette: "Datasette", request +) -> Optional[dict]: """ Authenticate requests using API tokens by delegating to all registered token handlers via datasette.verify_token(). diff --git a/datasette/default_table_actions.py b/datasette/default_table_actions.py index 0f2f32ef..e41434ef 100644 --- a/datasette/default_table_actions.py +++ b/datasette/default_table_actions.py @@ -20,7 +20,7 @@ def table_actions(datasette, actor, database, table, request): "label": "Alter table", "description": "Change columns and primary key for this table.", "attrs": { - "aria-label": f"Alter table {table}", + "aria-label": "Alter table {}".format(table), "data-table-action": "alter-table", }, } diff --git a/datasette/events.py b/datasette/events.py index 5f3fd06e..e8786da9 100644 --- a/datasette/events.py +++ b/datasette/events.py @@ -1,8 +1,7 @@ from abc import ABC, abstractproperty from dataclasses import asdict, dataclass, field -from datetime import datetime, timezone - from datasette.hookspecs import hookimpl +from datetime import datetime, timezone @dataclass diff --git a/datasette/facets.py b/datasette/facets.py index 69ac2c42..abe0605e 100644 --- a/datasette/facets.py +++ b/datasette/facets.py @@ -1,13 +1,12 @@ import json import urllib - from datasette import hookimpl from datasette.database import QueryInterrupted from datasette.utils import ( - detect_json1, escape_sqlite, path_with_added_args, path_with_removed_args, + detect_json1, sqlite3, ) @@ -31,7 +30,7 @@ def load_facet_configs(request, table_config): assert ( len(facet_config.values()) == 1 ), "Metadata config dicts should be {type: config}" - type, facet_config = next(iter(facet_config.items())) + type, facet_config = list(facet_config.items())[0] if isinstance(facet_config, str): facet_config = {"simple": facet_config} facet_configs.setdefault(type, []).append( @@ -39,7 +38,7 @@ def load_facet_configs(request, table_config): ) qs_pairs = urllib.parse.parse_qs(request.query_string, keep_blank_values=True) for key, values in qs_pairs.items(): - if key == "_facet" or key.startswith("_facet_"): + if key.startswith("_facet"): # Figure out the facet type if key == "_facet": type = "column" @@ -86,7 +85,7 @@ class Facet: self.database = database # For foreign key expansion. Can be None for e.g. stored SQL queries: self.table = table - self.sql = sql or f"select * from {escape_sqlite(table)}" + self.sql = sql or f"select * from [{table}]" self.params = params or [] self.table_config = table_config # row_count can be None, in which case we calculate it ourselves: @@ -161,13 +160,18 @@ class ColumnFacet(Facet): for column in columns: if column in already_enabled: continue - suggested_facet_sql = f""" - with limited as (select * from ({self.sql}) limit {self.suggest_consider}) - select {escape_sqlite(column)} as value, count(*) as n from limited + suggested_facet_sql = """ + with limited as (select * from ({sql}) limit {suggest_consider}) + select {column} as value, count(*) as n from limited where value is not null group by value - limit {facet_size + 1} - """ + limit {limit} + """.format( + column=escape_sqlite(column), + sql=self.sql, + limit=facet_size + 1, + suggest_consider=self.suggest_consider, + ) distinct_values = None try: distinct_values = await self.ds.execute( @@ -263,16 +267,11 @@ class ColumnFacet(Facet): for row in facet_rows: column_qs = column if column.startswith("_"): - column_qs = f"{column}__exact" - selected_args = { - key: str(row["value"]) - for key in (column_qs, f"{column}__exact") - if (key, str(row["value"])) in qs_pairs - } - selected = bool(selected_args) + column_qs = "{}__exact".format(column) + selected = (column_qs, str(row["value"])) in qs_pairs if selected: toggle_path = path_with_removed_args( - self.request, selected_args + self.request, {column_qs: str(row["value"])} ) else: toggle_path = path_with_added_args( @@ -343,12 +342,12 @@ class ArrayFacet(Facet): for v in await self.ds.execute( self.database, ( - f"select {escape_sqlite(column)} from ({self.sql}) " - f"where {escape_sqlite(column)} is not null " - f"and {escape_sqlite(column)} != '' " - f"and json_array_length({escape_sqlite(column)}) > 0 " + "select {column} from ({sql}) " + "where {column} is not null " + "and {column} != '' " + "and json_array_length({column}) > 0 " "limit 100" - ), + ).format(column=escape_sqlite(column), sql=self.sql), self.params, truncate=False, custom_time_limit=self.ds.setting( @@ -389,14 +388,14 @@ class ArrayFacet(Facet): source = source_and_config["source"] column = config.get("column") or config["simple"] # https://github.com/simonw/datasette/issues/448 - facet_sql = f""" - with inner as ({self.sql}), + facet_sql = """ + with inner as ({sql}), deduped_array_items as ( select distinct j.value, inner.* from - json_each([inner].{escape_sqlite(column)}) j + json_each([inner].{col}) j join inner ) select @@ -407,8 +406,12 @@ class ArrayFacet(Facet): group by value order by - count(*) desc, value limit {facet_size + 1} - """ + count(*) desc, value limit {limit} + """.format( + col=escape_sqlite(column), + sql=self.sql, + limit=facet_size + 1, + ) try: facet_rows_results = await self.ds.execute( self.database, diff --git a/datasette/filters.py b/datasette/filters.py index 0499d086..95cc5f37 100644 --- a/datasette/filters.py +++ b/datasette/filters.py @@ -1,12 +1,8 @@ -import json -import math -from typing import ClassVar - from datasette import hookimpl -from datasette.resources import DatabaseResource, TableResource -from datasette.utils.asgi import BadRequest +from datasette.resources import DatabaseResource from datasette.views.base import DatasetteError - +from datasette.utils.asgi import BadRequest +import json from .utils import detect_json1, escape_sqlite, path_with_removed_args @@ -52,20 +48,13 @@ def search_filters(request, database, table, datasette): human_descriptions = [] extra_context = {} - # Figure out which trusted fts_table to use. Query string parameters can - # repeat this mapping (for backwards compatibility), but must not select - # a different table or primary key. + # Figure out which fts_table to use table_metadata = await datasette.table_config(database, table) db = datasette.get_database(database) - fts_table = table_metadata.get("fts_table") + fts_table = request.args.get("_fts_table") + fts_table = fts_table or table_metadata.get("fts_table") fts_table = fts_table or await db.fts_table(table) - fts_pk = table_metadata.get("fts_pk", "rowid") - requested_fts_table = request.args.get("_fts_table") - requested_fts_pk = request.args.get("_fts_pk") - if (requested_fts_table and requested_fts_table != fts_table) or ( - requested_fts_pk and requested_fts_pk != fts_pk - ): - raise BadRequest("Invalid _fts_table or _fts_pk") + fts_pk = request.args.get("_fts_pk", table_metadata.get("fts_pk", "rowid")) search_args = { key: request.args[key] for key in request.args @@ -83,11 +72,6 @@ def search_filters(request, database, table, datasette): extra_context["supports_search"] = bool(fts_table) if fts_table and search_args: - await datasette.ensure_permission( - action="view-table", - resource=TableResource(database=database, table=fts_table), - actor=request.actor, - ) if "_search" in search_args: # Simple ?_search=xxx search = search_args["_search"] @@ -115,9 +99,9 @@ def search_filters(request, database, table, datasette): fts_table=escape_sqlite(fts_table), search_col=escape_sqlite(search_col), match_clause=( - f":search_{i}" + ":search_{}".format(i) if search_mode_raw - else f"escape_fts(:search_{i})" + else "escape_fts(:search_{})".format(i) ), ) ) @@ -148,18 +132,13 @@ def through_filters(request, database, table, datasette): through_table = through_data["table"] other_column = through_data["column"] value = through_data["value"] - await datasette.ensure_permission( - action="view-table", - resource=TableResource(database=database, table=through_table), - actor=request.actor, - ) db = datasette.get_database(database) outgoing_foreign_keys = await db.foreign_keys_for_table(through_table) - fk_to_us = next( - (fk for fk in outgoing_foreign_keys if fk["other_table"] == table), - None, - ) - if fk_to_us is None: + try: + fk_to_us = [ + fk for fk in outgoing_foreign_keys if fk["other_table"] == table + ][0] + except IndexError: raise DatasetteError( "Invalid _through - could not find corresponding foreign key" ) @@ -203,17 +182,6 @@ class Filter: raise NotImplementedError -def _coerce_numeric_filter_value(value): - try: - return int(value) - except ValueError: - try: - converted = float(value) - except ValueError: - return value - return converted if math.isfinite(converted) else value - - class TemplatedFilter(Filter): def __init__( self, @@ -235,17 +203,13 @@ class TemplatedFilter(Filter): def where_clause(self, table, column, value, param_counter): converted = self.format.format(value) - if self.numeric: - converted = _coerce_numeric_filter_value(converted) + if self.numeric and converted.isdigit(): + converted = int(converted) if self.no_argument: - kwargs = {"c": _quote_sqlite_identifier(column)} + kwargs = {"c": column} converted = None else: - kwargs = { - "c": _quote_sqlite_identifier(column), - "p": f"p{param_counter}", - "t": _quote_sqlite_identifier(table), - } + kwargs = {"c": column, "p": f"p{param_counter}", "t": table} return self.sql_template.format(**kwargs), converted def human_clause(self, column, value): @@ -259,14 +223,6 @@ class TemplatedFilter(Filter): return template.format(c=column, v=value) -def _quote_sqlite_identifier(identifier): - # Preserve the historic always-quoted SQL generated by TemplatedFilter. - escaped = escape_sqlite(identifier) - if escaped == identifier: - return f'"{identifier}"' - return escaped - - class InFilter(Filter): key = "in" display = "in" @@ -308,56 +264,56 @@ class Filters: TemplatedFilter( "exact", "=", - "{c} = :{p}", + '"{c}" = :{p}', lambda c, v: "{c} = {v}" if v.isdigit() else '{c} = "{v}"', ), TemplatedFilter( "not", "!=", - "{c} != :{p}", + '"{c}" != :{p}', lambda c, v: "{c} != {v}" if v.isdigit() else '{c} != "{v}"', ), TemplatedFilter( "contains", "contains", - "{c} like :{p}", + '"{c}" like :{p}', '{c} contains "{v}"', format="%{}%", ), TemplatedFilter( "notcontains", "does not contain", - "{c} not like :{p}", + '"{c}" not like :{p}', '{c} does not contain "{v}"', format="%{}%", ), TemplatedFilter( "endswith", "ends with", - "{c} like :{p}", + '"{c}" like :{p}', '{c} ends with "{v}"', format="%{}", ), TemplatedFilter( "startswith", "starts with", - "{c} like :{p}", + '"{c}" like :{p}', '{c} starts with "{v}"', format="{}%", ), - TemplatedFilter("gt", ">", "{c} > :{p}", "{c} > {v}", numeric=True), + TemplatedFilter("gt", ">", '"{c}" > :{p}', "{c} > {v}", numeric=True), TemplatedFilter( - "gte", "\u2265", "{c} >= :{p}", "{c} \u2265 {v}", numeric=True + "gte", "\u2265", '"{c}" >= :{p}', "{c} \u2265 {v}", numeric=True ), - TemplatedFilter("lt", "<", "{c} < :{p}", "{c} < {v}", numeric=True), + TemplatedFilter("lt", "<", '"{c}" < :{p}', "{c} < {v}", numeric=True), TemplatedFilter( - "lte", "\u2264", "{c} <= :{p}", "{c} \u2264 {v}", numeric=True + "lte", "\u2264", '"{c}" <= :{p}', "{c} \u2264 {v}", numeric=True ), - TemplatedFilter("like", "like", "{c} like :{p}", '{c} like "{v}"'), + TemplatedFilter("like", "like", '"{c}" like :{p}', '{c} like "{v}"'), TemplatedFilter( - "notlike", "not like", "{c} not like :{p}", '{c} not like "{v}"' + "notlike", "not like", '"{c}" not like :{p}', '{c} not like "{v}"' ), - TemplatedFilter("glob", "glob", "{c} glob :{p}", '{c} glob "{v}"'), + TemplatedFilter("glob", "glob", '"{c}" glob :{p}', '{c} glob "{v}"'), InFilter(), NotInFilter(), ] @@ -366,13 +322,13 @@ class Filters: TemplatedFilter( "arraycontains", "array contains", - """:{p} in (select value from json_each({t}.{c}))""", + """:{p} in (select value from json_each([{t}].[{c}]))""", '{c} contains "{v}"', ), TemplatedFilter( "arraynotcontains", "array does not contain", - """:{p} not in (select value from json_each({t}.{c}))""", + """:{p} not in (select value from json_each([{t}].[{c}]))""", '{c} does not contain "{v}"', ), ] @@ -380,34 +336,36 @@ class Filters: else [] ) + [ - TemplatedFilter("date", "date", "date({c}) = :{p}", '"{c}" is on date {v}'), TemplatedFilter( - "isnull", "is null", "{c} is null", "{c} is null", no_argument=True + "date", "date", 'date("{c}") = :{p}', '"{c}" is on date {v}' + ), + TemplatedFilter( + "isnull", "is null", '"{c}" is null', "{c} is null", no_argument=True ), TemplatedFilter( "notnull", "is not null", - "{c} is not null", + '"{c}" is not null', "{c} is not null", no_argument=True, ), TemplatedFilter( "isblank", "is blank", - "({c} is null or {c} = '')", + '("{c}" is null or "{c}" = "")', "{c} is blank", no_argument=True, ), TemplatedFilter( "notblank", "is not blank", - "({c} is not null and {c} != '')", + '("{c}" is not null and "{c}" != "")', "{c} is not blank", no_argument=True, ), ] ) - _filters_by_key: ClassVar[dict[str, Filter]] = {f.key: f for f in _filters} + _filters_by_key = {f.key: f for f in _filters} def __init__(self, pairs): self.pairs = pairs diff --git a/datasette/fixtures.py b/datasette/fixtures.py index 049e35ed..7c85e16a 100644 --- a/datasette/fixtures.py +++ b/datasette/fixtures.py @@ -1,10 +1,9 @@ +from datasette.utils.sqlite import sqlite3 +from datasette.utils import documented import itertools import random import string -from datasette.utils import documented -from datasette.utils.sqlite import sqlite3 - __all__ = [ "EXTRA_DATABASE_SQL", "TABLES", @@ -347,7 +346,9 @@ CREATE VIEW searchable_view_configured_by_metadata AS + '\nINSERT INTO no_primary_key VALUES ("RENDER_CELL_DEMO", "a202", "b202", "c202");\n' + "\n".join( [ - f'INSERT INTO compound_three_primary_keys VALUES ("{a}", "{b}", "{c}", "{content}");' + 'INSERT INTO compound_three_primary_keys VALUES ("{a}", "{b}", "{c}", "{content}");'.format( + a=a, b=b, c=c, content=content + ) for a, b, c, content in generate_compound_rows(1001) ] ) diff --git a/datasette/forbidden.py b/datasette/forbidden.py index 67bf0d8b..91b1ff96 100644 --- a/datasette/forbidden.py +++ b/datasette/forbidden.py @@ -1,5 +1,4 @@ -from datasette import Response, hookimpl - +from datasette import hookimpl, Response from .utils import add_cors_headers diff --git a/datasette/handle_exception.py b/datasette/handle_exception.py index ef6c7b7e..e255ddf2 100644 --- a/datasette/handle_exception.py +++ b/datasette/handle_exception.py @@ -1,21 +1,16 @@ -import traceback - -from markupsafe import Markup - -from datasette import Response, hookimpl - +from datasette import hookimpl, Response from .utils import add_cors_headers, error_body from .utils.asgi import ( Base400, ) from .views.base import DatasetteError +from markupsafe import Markup +import traceback -# Debugger imports are deliberate - they back the "pdb" setting, which drops -# into a debugger on unhandled exceptions try: - import ipdb as pdb # noqa: T100 + import ipdb as pdb except ImportError: - import pdb # noqa: T100 + import pdb try: import rich @@ -59,10 +54,6 @@ def handle_exception(datasette, request, exception): body = dict(info) body.update(error_body(plain_message or message, status)) return Response.json(body, status=status, headers=headers) - if request.path.split("?")[0].endswith(".csv"): - return Response.text( - plain_message or message, status=status, headers=headers - ) info.update( { "ok": False, @@ -78,7 +69,7 @@ def handle_exception(datasette, request, exception): dict( info, urls=datasette.urls, - menu_links=list, + menu_links=lambda: [], ) ), status=status, diff --git a/datasette/hookspecs.py b/datasette/hookspecs.py index 49d8e8ea..7c56f882 100644 --- a/datasette/hookspecs.py +++ b/datasette/hookspecs.py @@ -1,4 +1,5 @@ -from pluggy import HookimplMarker, HookspecMarker +from pluggy import HookimplMarker +from pluggy import HookspecMarker hookspec = HookspecMarker("datasette") hookimpl = HookimplMarker("datasette") @@ -9,11 +10,6 @@ def startup(datasette): """Fires directly after Datasette first starts running""" -@hookspec -def shutdown(datasette): - """Called once when the Datasette server is shutting down""" - - @hookspec def asgi_wrapper(datasette): """Returns an ASGI middleware callable to wrap our ASGI application with""" @@ -50,7 +46,7 @@ def extra_body_script( def extra_template_vars( template, database, table, columns, view_name, request, datasette ): - """Extra template variables to be made available to the template - can return dict, None, callable or awaitable""" + """Extra template variables to be made available to the template - can return dict or callable or awaitable""" @hookspec diff --git a/datasette/inspect.py b/datasette/inspect.py index b126ce5c..5e681e03 100644 --- a/datasette/inspect.py +++ b/datasette/inspect.py @@ -1,13 +1,13 @@ import hashlib from .utils import ( + detect_spatialite, detect_fts, detect_primary_keys, - detect_spatialite, escape_sqlite, get_all_foreign_keys, - sqlite3, table_columns, + sqlite3, ) HASH_BLOCK_SIZE = 1024 * 1024 @@ -95,10 +95,10 @@ def inspect_tables(conn, database_metadata): """) ] - for t, table_info in tables.items(): + for t in tables.keys(): for hidden_table in hidden_tables: if t == hidden_table or t.startswith(hidden_table): - table_info["hidden"] = True + tables[t]["hidden"] = True continue return tables diff --git a/datasette/jump.py b/datasette/jump.py index d70d33df..d138e827 100644 --- a/datasette/jump.py +++ b/datasette/jump.py @@ -21,7 +21,7 @@ class JumpSQL: search_text: str | None = None, display_name: str | None = None, item_type: str = "menu", - ) -> JumpSQL: + ) -> "JumpSQL": if search_text is None: search_text = " ".join( text for text in (label, display_name, description) if text is not None diff --git a/datasette/permissions.py b/datasette/permissions.py index 2d242560..786dc026 100644 --- a/datasette/permissions.py +++ b/datasette/permissions.py @@ -1,11 +1,7 @@ -import contextvars from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any, NamedTuple - -_SQLITE_IDENTIFIER_CASE = str.maketrans( - "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz" -) +import contextvars # Context variable to track when permission checks should be skipped _skip_permission_checks = contextvars.ContextVar( @@ -53,15 +49,6 @@ class Resource(ABC): # Class-level metadata (subclasses must define these) name: str = None # e.g., "table", "database", "model" parent_class: type["Resource"] | None = None # e.g., DatabaseResource for tables - case_insensitive_child: bool = False - - @classmethod - def normalize_child(cls, child: str | None) -> str | None: - """Return a comparison key without changing the resource's display name.""" - if cls.case_insensitive_child and child is not None: - # Match SQLite NOCASE: fold ASCII only, not Unicode lower/casefold. - return child.translate(_SQLITE_IDENTIFIER_CASE) - return child # Instance-level optional extra attributes reasons: list[str] | None = None @@ -85,8 +72,8 @@ class Resource(ABC): ) def __repr__(self) -> str: - return ( - f"{self.__class__.__name__}(parent={self.parent!r}, child={self.child!r})" + return "{}(parent={!r}, child={!r})".format( + self.__class__.__name__, self.parent, self.child ) @property @@ -142,6 +129,7 @@ class Resource(ABC): Must return two columns: parent, child """ + pass class AllowedResource(NamedTuple): @@ -159,11 +147,6 @@ class Action: resource_class: type[Resource] | None = None also_requires: str | None = None # Optional action name that must also be allowed - def normalize_child(self, child: str | None) -> str | None: - if self.resource_class is None: - return child - return self.resource_class.normalize_child(child) - @property def takes_parent(self) -> bool: """ diff --git a/datasette/plugins.py b/datasette/plugins.py index 6a4d7da7..ae2cb17d 100644 --- a/datasette/plugins.py +++ b/datasette/plugins.py @@ -1,14 +1,20 @@ import importlib -import importlib.metadata as importlib_metadata -import importlib.resources as importlib_resources import os -import sys -from pprint import pprint - import pluggy - +from pprint import pprint +import sys from . import hookspecs +if sys.version_info >= (3, 9): + import importlib.resources as importlib_resources +else: + import importlib_resources +if sys.version_info >= (3, 10): + import importlib.metadata as importlib_metadata +else: + import importlib_metadata + + DEFAULT_PLUGINS = ( "datasette.publish.heroku", "datasette.publish.cloudrun", @@ -18,7 +24,6 @@ DEFAULT_PLUGINS = ( "datasette.actor_auth_cookie", "datasette.default_permissions", "datasette.default_permissions.tokens", - "datasette.default_permissions.sqlite_statistics", "datasette.default_actions", "datasette.default_column_types", "datasette.default_magic_parameters", @@ -80,7 +85,7 @@ if DATASETTE_LOAD_PLUGINS is not None: # Ensure name can be found in plugin_to_distinfo later: pm._plugin_distinfo.append((mod, distribution)) except importlib_metadata.PackageNotFoundError: - sys.stderr.write(f"Plugin {package_name} could not be found\n") + sys.stderr.write("Plugin {} could not be found\n".format(package_name)) # Load default plugins diff --git a/datasette/publish/cloudrun.py b/datasette/publish/cloudrun.py index 9ace865b..63d22fe8 100644 --- a/datasette/publish/cloudrun.py +++ b/datasette/publish/cloudrun.py @@ -1,17 +1,15 @@ +from datasette import hookimpl +import click import json import os import re from subprocess import CalledProcessError, check_call, check_output -import click - -from datasette import hookimpl - -from ..utils import temporary_docker_directory from .common import ( add_common_publish_arguments_and_options, fail_if_publish_binary_not_installed, ) +from ..utils import temporary_docker_directory @hookimpl @@ -221,7 +219,7 @@ def publish_subcommand(publish): check_call( "gcloud builds submit --tag {}{}".format( - image_id, f" --timeout {timeout}" if timeout else "" + image_id, " --timeout {}".format(timeout) if timeout else "" ), shell=True, ) @@ -233,7 +231,7 @@ def publish_subcommand(publish): ("--min-instances", min_instances), ): if value is not None: - extra_deploy_options.append(f"{option} {value}") + extra_deploy_options.append("{} {}".format(option, value)) check_call( "gcloud run deploy --allow-unauthenticated --platform=managed --image {} {}{}".format( image_id, @@ -260,16 +258,24 @@ def _ensure_artifact_registry(artifact_project, artifact_region, artifact_reposi ) from exc describe_cmd = ( - f"gcloud artifacts repositories describe {artifact_repository} --project {artifact_project} " - f"--location {artifact_region} --quiet" + "gcloud artifacts repositories describe {repo} --project {project} " + "--location {location} --quiet" + ).format( + repo=artifact_repository, + project=artifact_project, + location=artifact_region, ) try: check_call(describe_cmd, shell=True) return except CalledProcessError: create_cmd = ( - f"gcloud artifacts repositories create {artifact_repository} --repository-format=docker " - f'--location {artifact_region} --project {artifact_project} --description "Datasette Cloud Run images" --quiet' + "gcloud artifacts repositories create {repo} --repository-format=docker " + '--location {location} --project {project} --description "Datasette Cloud Run images" --quiet' + ).format( + repo=artifact_repository, + location=artifact_region, + project=artifact_project, ) try: check_call(create_cmd, shell=True) diff --git a/datasette/publish/common.py b/datasette/publish/common.py index 27dfd4bf..29665eb3 100644 --- a/datasette/publish/common.py +++ b/datasette/publish/common.py @@ -1,11 +1,9 @@ +from ..utils import StaticMount +import click import os import shutil import sys -import click - -from ..utils import StaticMount - def add_common_publish_arguments_and_options(subcommand): for decorator in reversed( @@ -78,7 +76,9 @@ def fail_if_publish_binary_not_installed(binary, publish_target, install_link): """Exit (with error message) if ``binary` isn't installed""" if not shutil.which(binary): click.secho( - f"Publishing to {publish_target} requires {binary} to be installed and configured", + "Publishing to {publish_target} requires {binary} to be installed and configured".format( + publish_target=publish_target, binary=binary + ), bg="red", fg="white", bold=True, diff --git a/datasette/publish/heroku.py b/datasette/publish/heroku.py index b0290833..f576a346 100644 --- a/datasette/publish/heroku.py +++ b/datasette/publish/heroku.py @@ -1,21 +1,19 @@ +from contextlib import contextmanager +from datasette import hookimpl +import click import json import os import pathlib import shlex import shutil -import tempfile -from contextlib import contextmanager from subprocess import call, check_output - -import click - -from datasette import hookimpl -from datasette.utils import link_or_copy, link_or_copy_directory, parse_metadata +import tempfile from .common import ( add_common_publish_arguments_and_options, fail_if_publish_binary_not_installed, ) +from datasette.utils import link_or_copy, link_or_copy_directory, parse_metadata @hookimpl @@ -236,7 +234,7 @@ def temporary_heroku_directory( extras.extend(["--static", f"{mount_point}:{mount_point}"]) quoted_files = " ".join( - [f"-i {shlex.quote(file_name)}" for file_name in file_names] + ["-i {}".format(shlex.quote(file_name)) for file_name in file_names] ) procfile_cmd = "web: datasette serve --host 0.0.0.0 {quoted_files} --cors --port $PORT --inspect-file inspect-data.json {extras}".format( quoted_files=quoted_files, extras=" ".join(extras) diff --git a/datasette/renderer.py b/datasette/renderer.py index 0e01f52f..7c94f6ee 100644 --- a/datasette/renderer.py +++ b/datasette/renderer.py @@ -1,13 +1,12 @@ import json - from datasette.extras import extra_names_from_request from datasette.utils import ( - CustomJSONEncoder, error_body, - path_from_row_pks, - remove_infinites, - sqlite3, value_as_boolean, + remove_infinites, + CustomJSONEncoder, + path_from_row_pks, + sqlite3, ) from datasette.utils.asgi import Response diff --git a/datasette/resources.py b/datasette/resources.py index 29bf7b1e..ee2e6d98 100644 --- a/datasette/resources.py +++ b/datasette/resources.py @@ -25,7 +25,6 @@ class TableResource(Resource): name = "table" parent_class = DatabaseResource - case_insensitive_child = True def __init__(self, database: str, table: str): super().__init__(parent=database, child=table) diff --git a/datasette/static/app.css b/datasette/static/app.css index 0297371c..d101e4b7 100644 --- a/datasette/static/app.css +++ b/datasette/static/app.css @@ -1,144 +1,3 @@ -/* Shared modal styles. */ -datasette-modal { - display: contents; -} - -dialog.datasette-modal { - --ink: #0f0f0f; - --paper: #eef6ff; - --muted: #6b6b6b; - --rule: #d8e6f5; - --accent: #1a56db; - --card: #ffffff; - border: none; - border-radius: var(--modal-border-radius, 0.75rem); - padding: 0; - margin: auto; - width: min(520px, calc(100vw - 32px)); - max-width: 95vw; - max-height: calc(100dvh - 32px); - box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)); - animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out; - overflow: hidden; - font-family: system-ui, -apple-system, sans-serif; - background: var(--card); -} - -dialog.datasette-modal[open] { - display: flex; - flex-direction: column; -} - -dialog.datasette-modal::backdrop { - background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5)); - backdrop-filter: var(--modal-backdrop-blur, blur(4px)); - -webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px)); - animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out; -} - -@keyframes datasette-modal-slide-in { - from { opacity: 0; transform: translateY(-20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -@keyframes datasette-modal-fade-in { - from { opacity: 0; } - to { opacity: 1; } -} - -:where(.datasette-modal) .modal-header { - padding: 20px 24px 12px; - border-bottom: 1px solid var(--rule); - display: flex; - align-items: center; - gap: 12px; - flex-shrink: 0; - min-width: 0; -} - -:where(.datasette-modal) .modal-title { - font-size: 1rem; - font-weight: 600; - color: var(--ink); -} - -:where(.datasette-modal) .modal-meta { - font-family: ui-monospace, monospace; - font-size: 0.7rem; - color: var(--muted); - background: var(--paper); - padding: 3px 9px; - border-radius: 20px; -} - -:where(.datasette-modal) .modal-body { - min-height: 0; - overflow: auto; - padding: 16px 24px 24px; -} - -:where(.datasette-modal) .modal-footer { - padding: 14px 20px; - border-top: 1px solid var(--rule); - display: flex; - align-items: center; - justify-content: flex-end; - gap: 10px; - flex-shrink: 0; - background: var(--paper); -} - -:where(.datasette-modal) .footer-info { - flex: 1; - font-family: ui-monospace, monospace; - font-size: 0.68rem; - color: var(--muted); -} - -:where(.datasette-modal) .modal-btn { - border: none; - border-radius: 5px; - padding: 9px 20px; - font-size: 0.85rem; - font-weight: 500; - cursor: pointer; - touch-action: manipulation; - font-family: inherit; - transition: background 0.12s; -} - -:where(.datasette-modal) .modal-btn-ghost { - background: transparent; - color: var(--muted); - border: 1px solid var(--rule); -} - -:where(.datasette-modal) .modal-btn-ghost:hover { - background: var(--rule); - color: var(--ink); -} - -:where(.datasette-modal) .modal-btn-primary { - background: var(--accent); - color: #fff; -} - -:where(.datasette-modal) .modal-btn-primary:hover { - background: #1949b8; -} - -:where(.datasette-modal) .modal-btn:disabled { - opacity: 0.65; - cursor: wait; -} - -@media (prefers-reduced-motion: reduce) { - dialog.datasette-modal, - dialog.datasette-modal::backdrop { - animation: none; - } -} - /* Reset and Page Setup ==================================================== */ /* Reset from http://meyerweb.com/eric/tools/css/reset/ @@ -204,7 +63,7 @@ em { } /* end reset */ -/* Shared modal CSS variables */ +/* Modal CSS variables (shared by web components via Shadow DOM) */ :root { --modal-backdrop-bg: rgba(0, 0, 0, 0.5); --modal-backdrop-blur: blur(4px); @@ -357,49 +216,6 @@ a:active { text-decoration: underline; } -.table-summary .count-all ~ .table-summary-description { - margin-left: 0.5rem; -} - -.table-summary .count-error:not(:empty) { - display: block; - margin-top: 0.25rem; - font-size: 0.875rem; - font-weight: 400; - line-height: 1.5; -} - -button.count-all { - background: none; - border: none; - padding: 3px 0; - margin-left: 0.25rem; - color: #276890; - font-family: inherit; - font-size: 0.8125rem; - font-weight: 400; - line-height: 1.5; - cursor: pointer; -} - -button.count-all:hover, -button.count-all:focus-visible { - text-decoration: underline; -} - -button.count-all:disabled { - color: #596478; - cursor: wait; -} - -@media (pointer: coarse) { - button.count-all { - min-height: 44px; - padding-left: 7px; - padding-right: 7px; - } -} - button.button-as-link { background: none; border: none; @@ -1122,552 +938,84 @@ p.zero-results { display: none; } -/* navigation-search */ -navigation-search { - display: contents; -} - -navigation-search dialog.datasette-modal { - max-width: 90vw; - width: 600px; - max-height: 80vh; -} - -navigation-search .search-container { - display: flex; - flex-direction: column; -} - -navigation-search .search-input-wrapper { - padding: 1.25rem; - border-bottom: 1px solid #e5e7eb; - display: flex; - gap: 0.5rem; - align-items: center; -} - -navigation-search .search-input { - width: 100%; - flex: 1; - min-width: 0; - padding: 0.75rem 1rem; - font-size: 1rem; - border: 2px solid #e5e7eb; - border-radius: 0.5rem; - outline: none; - transition: border-color 0.2s; - box-sizing: border-box; -} - -navigation-search .search-input:focus { - border-color: #2563eb; -} - -navigation-search .close-search { - background: transparent; - border: 1px solid transparent; - border-radius: 0.375rem; - color: #4b5563; - cursor: pointer; - flex: 0 0 auto; - font: inherit; - font-size: 1.5rem; - height: 2.75rem; - line-height: 1; - width: 2.75rem; -} - -navigation-search .close-search:hover, -navigation-search .close-search:focus { - background-color: #f3f4f6; - border-color: #d1d5db; -} - -navigation-search .results-container { - box-sizing: content-box; - height: calc(80vh - 180px); - padding: 0.5rem; -} - -navigation-search .results-list:empty { - display: none; -} - -navigation-search .result-item { - padding: 0.875rem 1rem; - cursor: pointer; - border-radius: 0.5rem; - transition: background-color 0.15s; - display: flex; - align-items: center; - gap: 0.75rem; -} - -navigation-search .result-item:hover { - background-color: #f3f4f6; -} - -navigation-search .result-item.selected { - background-color: #dbeafe; -} - -navigation-search .result-item > div { - flex: 1; - min-width: 0; -} - -navigation-search .jump-start-content { - border-bottom: 1px solid #e5e7eb; - margin-bottom: 0.5rem; - padding: 0.5rem 0.5rem 1rem; -} - -navigation-search .jump-start-content:empty { - display: none; -} - -navigation-search .result-name { - font-weight: 500; - color: #111827; -} - -navigation-search .result-label { - font-size: 0.875rem; - color: #4b5563; -} - -navigation-search .result-type { - color: #4b5563; - font-size: 0.75rem; - font-weight: 600; - text-transform: uppercase; -} - -navigation-search .result-url { - font-size: 0.875rem; - color: #6b7280; -} - -navigation-search .result-description { - color: #374151; - display: -webkit-box; - font-size: 0.8125rem; - line-height: 1.35; - margin-top: 0.35rem; - overflow: hidden; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; -} - -navigation-search .results-heading { - color: #4b5563; - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0; - padding: 0.5rem 1rem 0.25rem; - text-transform: uppercase; -} - -navigation-search .recent-actions { - padding: 0.25rem 1rem 0.75rem; -} - -navigation-search .clear-recent { - background: transparent; - border: 0; - color: #2563eb; - cursor: pointer; - font: inherit; - font-size: 0.875rem; - padding: 0; -} - -navigation-search .clear-recent:hover { - text-decoration: underline; -} - -navigation-search .no-results { - padding: 2rem; - text-align: center; - color: #6b7280; -} - -navigation-search .hint-text { - padding: 0.75rem 1.25rem; - font-size: 0.875rem; - color: #6b7280; - border-top: 1px solid #e5e7eb; - display: flex; - gap: 1rem; - flex-wrap: wrap; -} - -navigation-search .hint-text kbd { - background: #f3f4f6; - padding: 0.125rem 0.375rem; - border-radius: 0.25rem; - font-size: 0.75rem; - border: 1px solid #d1d5db; - font-family: monospace; -} - -navigation-search .visually-hidden { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - white-space: nowrap; - width: 1px; -} - -@media (max-width: 640px) { - navigation-search dialog.datasette-modal { - width: 95vw; - max-height: 85vh; - border-radius: 0.5rem; +@keyframes datasette-modal-slide-in { + from { + opacity: 0; + transform: translateY(-20px) scale(0.95); } - - navigation-search .search-input-wrapper { - padding: 1rem; - } - - navigation-search .search-input { - font-size: 16px; - } - - navigation-search .result-item { - padding: 1rem 0.75rem; - } - - navigation-search .hint-text { - font-size: 0.8rem; - padding: 0.5rem 1rem; + to { + opacity: 1; + transform: translateY(0) scale(1); } } +@keyframes datasette-modal-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} -/* column-chooser */ -column-chooser { - display: contents; +dialog.mobile-column-actions-dialog { --ink: #0f0f0f; --paper: #eef6ff; --muted: #6b6b6b; --rule: #d8e6f5; --accent: #1a56db; - --accent-light: #e8effd; --card: #ffffff; -} - -column-chooser * { - box-sizing: border-box; - margin: 0; + border: none; + border-radius: var(--modal-border-radius, 0.75rem); padding: 0; -} - -column-chooser dialog.datasette-modal { - width: 100%; - max-width: 420px; - max-height: min(640px, calc(100vh - 32px)); - -webkit-user-select: none; - -webkit-touch-callout: none; - -webkit-tap-highlight-color: transparent; -} - -column-chooser dialog.datasette-modal[open] { - height: min(640px, calc(100vh - 32px)); -} - -column-chooser .modal-header { - padding: 20px 24px 16px; - justify-content: space-between; -} - -column-chooser .list-toolbar { - padding: 6px 24px; - border-bottom: 1px solid var(--rule); - display: flex; - gap: 12px; - flex-shrink: 0; -} - -column-chooser .list-toolbar button { - background: var(--accent-light); - border: 1px solid var(--rule); - border-radius: 4px; - font-family: inherit; - font-size: 0.75rem; - color: var(--accent); - cursor: pointer; - padding: 3px 10px; - transition: - background 0.12s, - color 0.12s; -} - -column-chooser .list-toolbar button:hover { - background: var(--accent); - color: white; -} - -column-chooser .list-wrap { - flex: 1; - padding: 0; - overflow-x: hidden; - position: relative; - overscroll-behavior: contain; - -webkit-overflow-scrolling: touch; -} - -column-chooser .list-wrap::before, -column-chooser .list-wrap::after { - content: ""; - position: sticky; - display: block; - left: 0; - right: 0; - height: 20px; - pointer-events: none; - z-index: 5; - transition: opacity 0.2s; -} - -column-chooser .list-wrap::before { - top: 0; - background: linear-gradient( - to bottom, - rgba(255, 255, 255, 0.9), - transparent - ); -} - -column-chooser .list-wrap::after { - bottom: 0; - background: linear-gradient(to top, rgba(255, 255, 255, 0.9), transparent); - margin-top: -20px; -} - -column-chooser .scroll-zone { - position: absolute; - left: 0; - right: 0; - height: 72px; - pointer-events: none; - z-index: 10; -} - -column-chooser .scroll-zone-top { - top: 0; -} - -column-chooser .scroll-zone-bot { - bottom: 0; -} - -column-chooser .drag-list { - list-style: none; - padding: 4px 0; -} - -column-chooser .drag-item { - display: flex; - align-items: center; - background: white; - border-bottom: 1px solid var(--rule); - user-select: none; - -webkit-user-select: none; - -webkit-touch-callout: none; - position: relative; - transition: background 0.08s; -} - -column-chooser .drag-item:last-child { - border-bottom: none; -} - -column-chooser .drag-handle { - display: flex; - align-items: center; - justify-content: center; - width: 48px; - height: 48px; - flex-shrink: 0; - cursor: grab; - color: #c8c4bc; - touch-action: none; - transition: color 0.15s; -} - -column-chooser .drag-handle:hover { - color: var(--accent); -} - -column-chooser .drag-handle svg { - pointer-events: none; - display: block; -} - -column-chooser .drag-item-content { - display: flex; - align-items: center; - flex: 1; - min-width: 0; - cursor: pointer; -} - -column-chooser .drag-item-check { - display: flex; - align-items: center; - width: 32px; - height: 48px; - flex-shrink: 0; -} - -column-chooser .drag-item-check input[type="checkbox"] { - width: 16px; - height: 16px; - accent-color: var(--accent); - cursor: pointer; -} - -column-chooser .drag-item-label { - flex: 1; - font-size: 0.9rem; - line-height: 48px; - padding-right: 16px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - cursor: default; -} - -column-chooser .drag-item.is-dragging { - opacity: 0; -} - -column-chooser .drop-indicator { - position: absolute; - left: 48px; - right: 0; - height: 2px; - background: var(--accent); - border-radius: 99px; - pointer-events: none; - z-index: 20; - display: none; -} - -column-chooser .drop-indicator.top { - top: -1px; - display: block; -} - -column-chooser .drop-indicator.bottom { - bottom: -1px; - display: block; -} - -column-chooser .drag-ghost { - position: fixed; - pointer-events: none; - z-index: 9999; - background: white; - border-radius: 6px; - box-shadow: - 0 8px 32px rgba(0, 0, 0, 0.18), - 0 2px 8px rgba(0, 0, 0, 0.1); - display: flex; - align-items: center; - border: 1.5px solid var(--accent-light); - opacity: 0.97; - will-change: transform; - font-family: - system-ui, - -apple-system, - sans-serif; -} - -column-chooser .scroll-pulse { - position: absolute; - left: 50%; - transform: translateX(-50%); - width: 32px; - height: 32px; - border-radius: 50%; - background: var(--accent); - opacity: 0; - pointer-events: none; - z-index: 10; - transition: opacity 0.15s; -} - -column-chooser .scroll-pulse.top { - top: 8px; -} - -column-chooser .scroll-pulse.bot { - bottom: 8px; -} - -column-chooser .scroll-pulse.active { - opacity: 0.18; - animation: column-chooser-pulse 0.8s ease-in-out infinite; -} - -@keyframes column-chooser-pulse { - 0%, - 100% { - transform: translateX(-50%) scale(1); - opacity: 0.18; - } - 50% { - transform: translateX(-50%) scale(1.5); - opacity: 0.07; - } -} - -column-chooser .modal-btn-primary { - color: white; -} - -column-chooser .modal-btn-primary:hover { - background: #1448c0; -} - -column-chooser .list-wrap::-webkit-scrollbar { - width: 5px; -} - -column-chooser .list-wrap::-webkit-scrollbar-track { - background: transparent; -} - -column-chooser .list-wrap::-webkit-scrollbar-thumb { - background: var(--rule); - border-radius: 99px; -} - -column-chooser input, -column-chooser textarea { - -webkit-user-select: auto; - user-select: auto; -} - -dialog.mobile-column-actions-dialog { + margin: auto; width: min(420px, calc(100vw - 32px)); + max-width: 95vw; max-height: min(640px, calc(100vh - 32px)); + box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)); + animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out; + overflow: hidden; + font-family: system-ui, -apple-system, sans-serif; + background: var(--card); +} + +dialog.mobile-column-actions-dialog[open] { + display: flex; + flex-direction: column; +} + +dialog.mobile-column-actions-dialog::backdrop { + background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5)); + backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + -webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out; } .mobile-column-actions-dialog .modal-header { padding: 20px 24px 16px; + border-bottom: 1px solid var(--rule); + display: flex; + align-items: center; justify-content: space-between; + gap: 12px; + flex-shrink: 0; +} + +.mobile-column-actions-dialog .modal-title { + font-size: 1rem; + font-weight: 600; + color: var(--ink); +} + +.mobile-column-actions-dialog .modal-meta { + font-family: ui-monospace, monospace; + font-size: 0.7rem; + color: var(--muted); + background: var(--paper); + padding: 3px 9px; + border-radius: 20px; } .mobile-column-actions-dialog .list-wrap { flex: 1 1 auto; - padding: 0; + min-height: 0; + overflow-y: auto; overflow-x: hidden; position: relative; overscroll-behavior: contain; @@ -1794,12 +1142,102 @@ dialog.mobile-column-actions-dialog { font-size: 0.85em; } +.mobile-column-actions-dialog .modal-footer { + padding: 14px 20px; + border-top: 1px solid var(--rule); + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + background: var(--paper); +} + +.mobile-column-actions-dialog .footer-info { + flex: 1; + font-family: ui-monospace, monospace; + font-size: 0.68rem; + color: var(--muted); +} + +.mobile-column-actions-dialog .btn { + border: none; + border-radius: 5px; + padding: 9px 20px; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + touch-action: manipulation; + font-family: inherit; + transition: background 0.12s; +} + +.mobile-column-actions-dialog .btn-ghost { + background: transparent; + color: var(--muted); + border: 1px solid var(--rule); +} + +.mobile-column-actions-dialog .btn-ghost:hover { + background: var(--rule); + color: var(--ink); +} + dialog.set-column-type-dialog { + --ink: #0f0f0f; + --paper: #eef6ff; + --muted: #6b6b6b; + --rule: #d8e6f5; + --accent: #1a56db; + --card: #ffffff; + border: none; + border-radius: var(--modal-border-radius, 0.75rem); + padding: 0; + margin: auto; + width: min(520px, calc(100vw - 32px)); + max-width: 95vw; max-height: min(720px, calc(100vh - 32px)); + box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)); + animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out; + overflow: hidden; + font-family: system-ui, -apple-system, sans-serif; + background: var(--card); +} + +dialog.set-column-type-dialog[open] { + display: flex; + flex-direction: column; +} + +dialog.set-column-type-dialog::backdrop { + background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5)); + backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + -webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out; } .set-column-type-dialog .modal-header { + padding: 20px 24px 12px; + border-bottom: 1px solid var(--rule); + display: flex; + align-items: center; justify-content: space-between; + gap: 12px; + flex-shrink: 0; +} + +.set-column-type-dialog .modal-title { + font-size: 1rem; + font-weight: 600; + color: var(--ink); +} + +.set-column-type-dialog .modal-meta { + font-family: ui-monospace, monospace; + font-size: 0.7rem; + color: var(--muted); + background: var(--paper); + padding: 3px 9px; + border-radius: 20px; } .set-column-type-status, @@ -1821,6 +1259,8 @@ dialog.set-column-type-dialog { } .set-column-type-options { + padding: 16px 24px 24px; + overflow-y: auto; display: grid; gap: 12px; } @@ -1862,6 +1302,60 @@ dialog.set-column-type-dialog { font-size: 0.9rem; } +.set-column-type-dialog .modal-footer { + padding: 14px 20px; + border-top: 1px solid var(--rule); + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + background: var(--paper); +} + +.set-column-type-dialog .footer-info { + flex: 1; + font-family: ui-monospace, monospace; + font-size: 0.68rem; + color: var(--muted); +} + +.set-column-type-dialog .btn { + border: none; + border-radius: 5px; + padding: 9px 20px; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + touch-action: manipulation; + font-family: inherit; + transition: background 0.12s; +} + +.set-column-type-dialog .btn-ghost { + background: transparent; + color: var(--muted); + border: 1px solid var(--rule); +} + +.set-column-type-dialog .btn-ghost:hover { + background: var(--rule); + color: var(--ink); +} + +.set-column-type-dialog .btn-primary { + background: var(--accent); + color: #fff; +} + +.set-column-type-dialog .btn-primary:hover { + background: #1949b8; +} + +.set-column-type-dialog .btn:disabled { + opacity: 0.65; + cursor: wait; +} + .row-mutation-status { margin: 0 0 0.75rem; padding: 8px 10px; @@ -1895,11 +1389,46 @@ button.table-insert-row svg { } dialog.row-delete-dialog { + --ink: #0f0f0f; + --paper: #eef6ff; + --muted: #6b6b6b; + --rule: #d8e6f5; + --accent: #1a56db; + --card: #ffffff; + border: none; + border-radius: var(--modal-border-radius, 0.75rem); + padding: 0; + margin: auto; width: min(440px, calc(100vw - 32px)); + max-width: 95vw; + box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)); + animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out; + overflow: hidden; + font-family: system-ui, -apple-system, sans-serif; + background: var(--card); +} + +dialog.row-delete-dialog[open] { + display: flex; + flex-direction: column; +} + +dialog.row-delete-dialog::backdrop { + background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5)); + backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + -webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out; } .row-delete-dialog .modal-header { + padding: 20px 24px 12px; + border-bottom: 1px solid var(--rule); + display: flex; + align-items: center; justify-content: flex-start; + gap: 12px; + flex-shrink: 0; + min-width: 0; } .row-delete-dialog .modal-title { @@ -1908,6 +1437,9 @@ dialog.row-delete-dialog { gap: 0.35rem; min-width: 0; max-width: 100%; + font-size: 1rem; + font-weight: 600; + color: var(--ink); } .row-delete-message, @@ -1939,12 +1471,94 @@ dialog.row-delete-dialog { .row-delete-dialog .modal-footer { padding: 18px 20px 14px; + border-top: 1px solid var(--rule); + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + flex-shrink: 0; + background: var(--paper); margin-top: 18px; } +.row-delete-dialog .btn { + border: none; + border-radius: 5px; + padding: 9px 20px; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + touch-action: manipulation; + font-family: inherit; + transition: background 0.12s; +} + +.row-delete-dialog .btn-ghost { + background: transparent; + color: var(--muted); + border: 1px solid var(--rule); +} + +.row-delete-dialog .btn-ghost:hover { + background: var(--rule); + color: var(--ink); +} + +.row-delete-dialog .btn-primary { + background: var(--accent); + color: #fff; +} + +.row-delete-dialog .btn-primary:hover { + background: #1949b8; +} + +.row-delete-dialog .btn:disabled { + opacity: 0.65; + cursor: wait; +} + dialog.row-edit-dialog { + --ink: #0f0f0f; + --paper: #eef6ff; + --muted: #6b6b6b; + --rule: #d8e6f5; + --accent: #1a56db; + --card: #ffffff; + border: none; + border-radius: var(--modal-border-radius, 0.75rem); + padding: 0; + margin: auto; width: min(720px, calc(100vw - 32px)); + max-width: 95vw; max-height: min(780px, calc(100vh - 32px)); + box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)); + animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out; + overflow: hidden; + font-family: system-ui, -apple-system, sans-serif; + background: var(--card); +} + +dialog.row-edit-dialog[open] { + display: flex; + flex-direction: column; +} + +dialog.row-edit-dialog::backdrop { + background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5)); + backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + -webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out; +} + +.row-edit-dialog .modal-header { + padding: 20px 24px 12px; + border-bottom: 1px solid var(--rule); + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; + min-width: 0; } .row-edit-dialog .modal-title { @@ -1953,6 +1567,9 @@ dialog.row-edit-dialog { gap: 0.35rem; min-width: 0; max-width: 100%; + font-size: 1rem; + font-weight: 600; + color: var(--ink); } .row-edit-dialog .modal-title .row-dialog-action, @@ -2020,6 +1637,8 @@ dialog.row-edit-dialog { .row-edit-fields { display: grid; gap: 14px; + padding: 16px 24px 24px; + overflow-y: auto; } .row-edit-fields[hidden], @@ -2299,6 +1918,8 @@ textarea.row-edit-input { .row-edit-bulk { display: grid; gap: 8px; + padding: 16px 24px 24px; + overflow-y: auto; } .row-edit-bulk-editor { @@ -2318,7 +1939,7 @@ textarea.row-edit-input { justify-content: flex-start; } -.row-edit-bulk-actions .modal-btn { +.row-edit-bulk-actions .btn { padding-left: 12px; padding-right: 12px; } @@ -2562,6 +2183,17 @@ datasette-autocomplete input[type="text"], max-width: 46rem; } +.row-edit-dialog .modal-footer { + padding: 14px 20px; + border-top: 1px solid var(--rule); + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + flex-shrink: 0; + background: var(--paper); +} + .row-edit-mode-link { color: var(--accent); font-size: 0.9rem; @@ -2572,14 +2204,84 @@ datasette-autocomplete input[type="text"], display: none; } -.row-edit-dialog .modal-btn:disabled { +.row-edit-dialog .btn { + border: none; + border-radius: 5px; + padding: 9px 20px; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + touch-action: manipulation; + font-family: inherit; + transition: background 0.12s; +} + +.row-edit-dialog .btn-ghost { + background: transparent; + color: var(--muted); + border: 1px solid var(--rule); +} + +.row-edit-dialog .btn-ghost:hover { + background: var(--rule); + color: var(--ink); +} + +.row-edit-dialog .btn-primary { + background: var(--accent); + color: #fff; +} + +.row-edit-dialog .btn-primary:hover { + background: #1949b8; +} + +.row-edit-dialog .btn:disabled { opacity: 0.55; cursor: not-allowed; } dialog.table-create-dialog { + --ink: #0f0f0f; + --paper: #eef6ff; + --muted: #6b6b6b; + --rule: #d8e6f5; + --accent: #1a56db; + --card: #ffffff; + border: none; + border-radius: var(--modal-border-radius, 0.75rem); + padding: 0; + margin: auto; width: min(980px, calc(100vw - 32px)); + max-width: 95vw; max-height: min(780px, calc(100vh - 32px)); + box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)); + animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out; + overflow: hidden; + font-family: system-ui, -apple-system, sans-serif; + background: var(--card); +} + +dialog.table-create-dialog[open] { + display: flex; + flex-direction: column; +} + +dialog.table-create-dialog::backdrop { + background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5)); + backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + -webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out; +} + +.table-create-dialog .modal-header { + padding: 20px 24px 12px; + border-bottom: 1px solid var(--rule); + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; + min-width: 0; } .table-create-dialog .modal-title { @@ -2587,6 +2289,9 @@ dialog.table-create-dialog { align-items: center; min-width: 0; max-width: 100%; + font-size: 1rem; + font-weight: 600; + color: var(--ink); } .table-create-form { @@ -2614,6 +2319,8 @@ dialog.table-create-dialog { .table-create-fields { display: grid; gap: 18px; + padding: 16px 24px 24px; + overflow-y: auto; } .table-create-field { @@ -3023,6 +2730,17 @@ select.table-create-input { outline-offset: 1px; } +.table-create-dialog .modal-footer { + padding: 14px 20px; + border-top: 1px solid var(--rule); + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + flex-shrink: 0; + background: var(--paper); +} + .table-create-mode-link { color: var(--accent); font-size: 0.9rem; @@ -3033,7 +2751,39 @@ select.table-create-input { display: none; } -.table-create-dialog .modal-btn:disabled, +.table-create-dialog .btn { + border: none; + border-radius: 5px; + padding: 9px 20px; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + touch-action: manipulation; + font-family: inherit; + transition: background 0.12s; +} + +.table-create-dialog .btn-ghost { + background: transparent; + color: var(--muted); + border: 1px solid var(--rule); +} + +.table-create-dialog .btn-ghost:hover { + background: var(--rule); + color: var(--ink); +} + +.table-create-dialog .btn-primary { + background: var(--accent); + color: #fff; +} + +.table-create-dialog .btn-primary:hover { + background: #1949b8; +} + +.table-create-dialog .btn:disabled, .table-create-add-column:disabled, .table-create-icon-button:disabled { opacity: 0.55; @@ -3041,8 +2791,46 @@ select.table-create-input { } dialog.table-alter-dialog { + --ink: #0f0f0f; + --paper: #eef6ff; + --muted: #6b6b6b; + --rule: #d8e6f5; + --accent: #1a56db; + --card: #ffffff; + border: none; + border-radius: var(--modal-border-radius, 0.75rem); + padding: 0; + margin: auto; width: min(980px, calc(100vw - 32px)); + max-width: 95vw; max-height: min(780px, calc(100vh - 32px)); + box-shadow: var(--modal-shadow, 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)); + animation: datasette-modal-slide-in var(--modal-animation-duration, 0.2s) ease-out; + overflow: hidden; + font-family: system-ui, -apple-system, sans-serif; + background: var(--card); +} + +dialog.table-alter-dialog[open] { + display: flex; + flex-direction: column; +} + +dialog.table-alter-dialog::backdrop { + background: var(--modal-backdrop-bg, rgba(0, 0, 0, 0.5)); + backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + -webkit-backdrop-filter: var(--modal-backdrop-blur, blur(4px)); + animation: datasette-modal-fade-in var(--modal-animation-duration, 0.2s) ease-out; +} + +.table-alter-dialog .modal-header { + padding: 20px 24px 12px; + border-bottom: 1px solid var(--rule); + display: flex; + align-items: center; + gap: 12px; + flex-shrink: 0; + min-width: 0; } .table-alter-dialog .modal-title { @@ -3050,6 +2838,9 @@ dialog.table-alter-dialog { align-items: center; min-width: 0; max-width: 100%; + font-size: 1rem; + font-weight: 600; + color: var(--ink); } .table-alter-form { @@ -3077,6 +2868,8 @@ dialog.table-alter-dialog { .table-alter-fields { display: grid; gap: 18px; + padding: 16px 24px 24px; + overflow-y: auto; } .table-alter-table-options { @@ -3110,6 +2903,8 @@ dialog.table-alter-dialog { .table-alter-review { display: grid; gap: 12px; + overflow-y: auto; + padding: 16px 24px 24px; } .table-alter-review[hidden] { @@ -3403,29 +3198,72 @@ select.table-alter-input { outline-offset: 1px; } -.table-alter-dialog .modal-btn-danger { +.table-alter-dialog .modal-footer { + padding: 14px 20px; + border-top: 1px solid var(--rule); + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + flex-shrink: 0; + background: var(--paper); +} + +.table-alter-dialog .btn { + border: none; + border-radius: 5px; + padding: 9px 20px; + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + touch-action: manipulation; + font-family: inherit; + transition: background 0.12s; +} + +.table-alter-dialog .btn-ghost { + background: transparent; + color: var(--muted); + border: 1px solid var(--rule); +} + +.table-alter-dialog .btn-ghost:hover { + background: var(--rule); + color: var(--ink); +} + +.table-alter-dialog .btn-danger { background: #b91c1c; color: #fff; margin-right: auto; } -.table-alter-dialog .modal-btn-danger:hover { +.table-alter-dialog .btn-danger:hover { background: #991b1b; } -.table-alter-dialog .modal-btn-danger:disabled, -.table-alter-dialog .modal-btn-danger:disabled:hover { +.table-alter-dialog .btn-danger:disabled, +.table-alter-dialog .btn-danger:disabled:hover { background: #d98c8c; color: #fff; } -.table-alter-dialog .modal-btn-primary:disabled, -.table-alter-dialog .modal-btn-primary:disabled:hover { +.table-alter-dialog .btn-primary { + background: var(--accent); + color: #fff; +} + +.table-alter-dialog .btn-primary:hover { + background: #1949b8; +} + +.table-alter-dialog .btn-primary:disabled, +.table-alter-dialog .btn-primary:disabled:hover { background: #a0aec0; color: #fff; } -.table-alter-dialog .modal-btn:disabled, +.table-alter-dialog .btn:disabled, .table-alter-add-column:disabled, .table-alter-icon-button:disabled { opacity: 0.55; diff --git a/datasette/static/cm-editor-6.0.1.bundle.js b/datasette/static/cm-editor-6.0.1.bundle.js deleted file mode 100644 index 21b5f461..00000000 --- a/datasette/static/cm-editor-6.0.1.bundle.js +++ /dev/null @@ -1 +0,0 @@ -var cm=function(t){"use strict";class e{constructor(){}lineAt(t){if(t<0||t>this.length)throw new RangeError(`Invalid position ${t} in document of length ${this.length}`);return this.lineInner(t,!1,1,0)}line(t){if(t<1||t>this.lines)throw new RangeError(`Invalid line number ${t} in ${this.lines}-line document`);return this.lineInner(t,!0,1,0)}replace(t,e,i){let s=[];return this.decompose(0,t,s,2),i.length&&i.decompose(0,i.length,s,3),this.decompose(e,this.length,s,1),n.from(s,this.length-(e-t)+i.length)}append(t){return this.replace(this.length,this.length,t)}slice(t,e=this.length){let i=[];return this.decompose(t,e,i,0),n.from(i,e-t)}eq(t){if(t==this)return!0;if(t.length!=this.length||t.lines!=this.lines)return!1;let e=this.scanIdentical(t,1),i=this.length-this.scanIdentical(t,-1),n=new o(this),s=new o(t);for(let t=e,r=e;;){if(n.next(t),s.next(t),t=0,n.lineBreak!=s.lineBreak||n.done!=s.done||n.value!=s.value)return!1;if(r+=n.value.length,n.done||r>=i)return!0}}iter(t=1){return new o(this,t)}iterRange(t,e=this.length){return new l(this,t,e)}iterLines(t,e){let i;if(null==t)i=this.iter();else{null==e&&(e=this.lines+1);let n=this.line(t).from;i=this.iterRange(n,Math.max(n,e==this.lines+1?this.length:e<=1?0:this.line(e-1).to))}return new a(i)}toString(){return this.sliceString(0)}toJSON(){let t=[];return this.flatten(t),t}static of(t){if(0==t.length)throw new RangeError("A document must have at least one line");return 1!=t.length||t[0]?t.length<=32?new i(t):n.from(i.split(t,[])):e.empty}}class i extends e{constructor(t,e=function(t){let e=-1;for(let i of t)e+=i.length+1;return e}(t)){super(),this.text=t,this.length=e}get lines(){return this.text.length}get children(){return null}lineInner(t,e,i,n){for(let s=0;;s++){let r=this.text[s],o=n+r.length;if((e?i:o)>=t)return new h(n,o,i,r);n=o+1,i++}}decompose(t,e,n,o){let l=t<=0&&e>=this.length?this:new i(r(this.text,t,e),Math.min(e,this.length)-Math.max(0,t));if(1&o){let t=n.pop(),e=s(l.text,t.text.slice(),0,l.length);if(e.length<=32)n.push(new i(e,t.length+l.length));else{let t=e.length>>1;n.push(new i(e.slice(0,t)),new i(e.slice(t)))}}else n.push(l)}replace(t,e,o){if(!(o instanceof i))return super.replace(t,e,o);let l=s(this.text,s(o.text,r(this.text,0,t)),e),a=this.length+o.length-(e-t);return l.length<=32?new i(l,a):n.from(i.split(l,[]),a)}sliceString(t,e=this.length,i="\n"){let n="";for(let s=0,r=0;s<=e&&rt&&r&&(n+=i),ts&&(n+=o.slice(Math.max(0,t-s),e-s)),s=l+1}return n}flatten(t){for(let e of this.text)t.push(e)}scanIdentical(){return 0}static split(t,e){let n=[],s=-1;for(let r of t)n.push(r),s+=r.length+1,32==n.length&&(e.push(new i(n,s)),n=[],s=-1);return s>-1&&e.push(new i(n,s)),e}}class n extends e{constructor(t,e){super(),this.children=t,this.length=e,this.lines=0;for(let e of t)this.lines+=e.lines}lineInner(t,e,i,n){for(let s=0;;s++){let r=this.children[s],o=n+r.length,l=i+r.lines-1;if((e?l:o)>=t)return r.lineInner(t,e,i,n);n=o+1,i=l+1}}decompose(t,e,i,n){for(let s=0,r=0;r<=e&&s=r){let s=n&((r<=t?1:0)|(l>=e?2:0));r>=t&&l<=e&&!s?i.push(o):o.decompose(t-r,e-r,i,s)}r=l+1}}replace(t,e,i){if(i.lines=r&&e<=l){let a=o.replace(t-r,e-r,i),h=this.lines-o.lines+a.lines;if(a.lines>4&&a.lines>h>>6){let r=this.children.slice();return r[s]=a,new n(r,this.length-(e-t)+i.length)}return super.replace(r,l,a)}r=l+1}return super.replace(t,e,i)}sliceString(t,e=this.length,i="\n"){let n="";for(let s=0,r=0;st&&s&&(n+=i),tr&&(n+=o.sliceString(t-r,e-r,i)),r=l+1}return n}flatten(t){for(let e of this.children)e.flatten(t)}scanIdentical(t,e){if(!(t instanceof n))return 0;let i=0,[s,r,o,l]=e>0?[0,0,this.children.length,t.children.length]:[this.children.length-1,t.children.length-1,-1,-1];for(;;s+=e,r+=e){if(s==o||r==l)return i;let n=this.children[s],a=t.children[r];if(n!=a)return i+n.scanIdentical(a,e);i+=n.length+1}}static from(t,e=t.reduce(((t,e)=>t+e.length+1),-1)){let s=0;for(let e of t)s+=e.lines;if(s<32){let n=[];for(let e of t)e.flatten(n);return new i(n,e)}let r=Math.max(32,s>>5),o=r<<1,l=r>>1,a=[],h=0,c=-1,u=[];function f(t){let e;if(t.lines>o&&t instanceof n)for(let e of t.children)f(e);else t.lines>l&&(h>l||!h)?(d(),a.push(t)):t instanceof i&&h&&(e=u[u.length-1])instanceof i&&t.lines+e.lines<=32?(h+=t.lines,c+=t.length+1,u[u.length-1]=new i(e.text.concat(t.text),e.length+1+t.length)):(h+t.lines>r&&d(),h+=t.lines,c+=t.length+1,u.push(t))}function d(){0!=h&&(a.push(1==u.length?u[0]:n.from(u,c)),c=-1,h=u.length=0)}for(let e of t)f(e);return d(),1==a.length?a[0]:new n(a,e)}}function s(t,e,i=0,n=1e9){for(let s=0,r=0,o=!0;r=i&&(a>n&&(l=l.slice(0,n-s)),s0?1:(t instanceof i?t.text.length:t.children.length)<<1]}nextInner(t,e){for(this.done=this.lineBreak=!1;;){let n=this.nodes.length-1,s=this.nodes[n],r=this.offsets[n],o=r>>1,l=s instanceof i?s.text.length:s.children.length;if(o==(e>0?l:0)){if(0==n)return this.done=!0,this.value="",this;e>0&&this.offsets[n-1]++,this.nodes.pop(),this.offsets.pop()}else if((1&r)==(e>0?0:1)){if(this.offsets[n]+=e,0==t)return this.lineBreak=!0,this.value="\n",this;t--}else if(s instanceof i){let i=s.text[o+(e<0?-1:0)];if(this.offsets[n]+=e,i.length>Math.max(0,t))return this.value=0==t?i:e>0?i.slice(t):i.slice(0,i.length-t),this;t-=i.length}else{let r=s.children[o+(e<0?-1:0)];t>r.length?(t-=r.length,this.offsets[n]+=e):(e<0&&this.offsets[n]--,this.nodes.push(r),this.offsets.push(e>0?1:(r instanceof i?r.text.length:r.children.length)<<1))}}}next(t=0){return t<0&&(this.nextInner(-t,-this.dir),t=this.value.length),this.nextInner(t,this.dir)}}class l{constructor(t,e,i){this.value="",this.done=!1,this.cursor=new o(t,e>i?-1:1),this.pos=e>i?t.length:0,this.from=Math.min(e,i),this.to=Math.max(e,i)}nextInner(t,e){if(e<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;t+=Math.max(0,e<0?this.pos-this.to:this.from-this.pos);let i=e<0?this.pos-this.from:this.to-this.pos;t>i&&(t=i),i-=t;let{value:n}=this.cursor.next(t);return this.pos+=(n.length+t)*e,this.value=n.length<=i?n:e<0?n.slice(n.length-i):n.slice(0,i),this.done=!this.value,this}next(t=0){return t<0?t=Math.max(t,this.from-this.pos):t>0&&(t=Math.min(t,this.to-this.pos)),this.nextInner(t,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&""!=this.value}}class a{constructor(t){this.inner=t,this.afterBreak=!0,this.value="",this.done=!1}next(t=0){let{done:e,lineBreak:i,value:n}=this.inner.next(t);return e?(this.done=!0,this.value=""):i?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=n,this.afterBreak=!1),this}get lineBreak(){return!1}}"undefined"!=typeof Symbol&&(e.prototype[Symbol.iterator]=function(){return this.iter()},o.prototype[Symbol.iterator]=l.prototype[Symbol.iterator]=a.prototype[Symbol.iterator]=function(){return this});class h{constructor(t,e,i,n){this.from=t,this.to=e,this.number=i,this.text=n}get length(){return this.to-this.from}}let c="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map((t=>t?parseInt(t,36):1));for(let t=1;tt)return c[e-1]<=t;return!1}function f(t){return t>=127462&&t<=127487}function d(t,e,i=!0,n=!0){return(i?p:m)(t,e,n)}function p(t,e,i){if(e==t.length)return e;e&&g(t.charCodeAt(e))&&v(t.charCodeAt(e-1))&&e--;let n=w(t,e);for(e+=b(n);e=0&&f(w(t,n));)i++,n-=2;if(i%2==0)break;e+=2}}}return e}function m(t,e,i){for(;e>0;){let n=p(t,e-2,i);if(n=56320&&t<57344}function v(t){return t>=55296&&t<56320}function w(t,e){let i=t.charCodeAt(e);if(!v(i)||e+1==t.length)return i;let n=t.charCodeAt(e+1);return g(n)?n-56320+(i-55296<<10)+65536:i}function y(t){return t<=65535?String.fromCharCode(t):(t-=65536,String.fromCharCode(55296+(t>>10),56320+(1023&t)))}function b(t){return t<65536?1:2}const x=/\r\n?|\n/;var k=function(t){return t[t.Simple=0]="Simple",t[t.TrackDel=1]="TrackDel",t[t.TrackBefore=2]="TrackBefore",t[t.TrackAfter=3]="TrackAfter",t}(k||(k={}));class S{constructor(t){this.sections=t}get length(){let t=0;for(let e=0;et)return s+(t-n);s+=o}else{if(i!=k.Simple&&a>=t&&(i==k.TrackDel&&nt||i==k.TrackBefore&&nt))return null;if(a>t||a==t&&e<0&&!o)return t==n||e<0?s:s+l;s+=l}n=a}if(t>n)throw new RangeError(`Position ${t} is out of range for changeset of length ${n}`);return s}touchesRange(t,e=t){for(let i=0,n=0;i=0&&n<=e&&s>=t)return!(ne)||"cover";n=s}return!1}toString(){let t="";for(let e=0;e=0?":"+n:"")}return t}toJSON(){return this.sections}static fromJSON(t){if(!Array.isArray(t)||t.length%2||t.some((t=>"number"!=typeof t)))throw new RangeError("Invalid JSON representation of ChangeDesc");return new S(t)}static create(t){return new S(t)}}class C extends S{constructor(t,e){super(t),this.inserted=e}apply(t){if(this.length!=t.length)throw new RangeError("Applying change set to a document with the wrong length");return M(this,((e,i,n,s,r)=>t=t.replace(n,n+(i-e),r)),!1),t}mapDesc(t,e=!1){return D(this,t,e,!0)}invert(t){let i=this.sections.slice(),n=[];for(let s=0,r=0;s=0){i[s]=l,i[s+1]=o;let a=s>>1;for(;n.length0&&O(i,e,s.text),s.forward(t),o+=t}let a=t[r++];for(;o>1].toJSON()))}return t}static of(t,i,n){let s=[],r=[],o=0,l=null;function a(t=!1){if(!t&&!s.length)return;ol||t<0||l>i)throw new RangeError(`Invalid change range ${t} to ${l} (in doc of length ${i})`);let u=c?"string"==typeof c?e.of(c.split(n||x)):c:e.empty,f=u.length;if(t==l&&0==f)return;to&&A(s,t-o,-1),A(s,l-t,f),O(r,s,u),o=l}}(t),a(!l),l}static empty(t){return new C(t?[t,-1]:[],[])}static fromJSON(t){if(!Array.isArray(t))throw new RangeError("Invalid JSON representation of ChangeSet");let i=[],n=[];for(let s=0;se&&"string"!=typeof t)))throw new RangeError("Invalid JSON representation of ChangeSet");if(1==r.length)i.push(r[0],0);else{for(;n.length=0&&i<=0&&i==t[s+1]?t[s]+=e:0==e&&0==t[s]?t[s+1]+=i:n?(t[s]+=e,t[s+1]+=i):t.push(e,i)}function O(t,i,n){if(0==n.length)return;let s=i.length-2>>1;if(s>1])),!(n||l==t.sections.length||t.sections[l+1]<0);)a=t.sections[l++],h=t.sections[l++];i(r,c,o,u,f),r=c,o=u}}}function D(t,e,i,n=!1){let s=[],r=n?[]:null,o=new P(t),l=new P(e);for(let t=-1;;)if(-1==o.ins&&-1==l.ins){let t=Math.min(o.len,l.len);A(s,t,-1),o.forward(t),l.forward(t)}else if(l.ins>=0&&(o.ins<0||t==o.i||0==o.off&&(l.len=0&&t=0)){if(o.done&&l.done)return r?C.createSet(s,r):S.create(s);throw new Error("Mismatched change set lengths")}{let e=0,i=o.len;for(;i;)if(-1==l.ins){let t=Math.min(i,l.len);e+=t,i-=t,l.forward(t)}else{if(!(0==l.ins&&l.lene||o.ins>=0&&o.len>e)&&(t||n.length>i),r.forward2(e),o.forward(e)}}else A(n,0,o.ins,t),s&&O(s,n,o.text),o.next()}}class P{constructor(t){this.set=t,this.i=0,this.next()}next(){let{sections:t}=this.set;this.i>1;return i>=t.length?e.empty:t[i]}textBit(t){let{inserted:i}=this.set,n=this.i-2>>1;return n>=i.length&&!t?e.empty:i[n].slice(this.off,null==t?void 0:this.off+t)}forward(t){t==this.len?this.next():(this.len-=t,this.off+=t)}forward2(t){-1==this.ins?this.forward(t):t==this.ins?this.next():(this.ins-=t,this.off+=t)}}class R{constructor(t,e,i){this.from=t,this.to=e,this.flags=i}get anchor(){return 16&this.flags?this.to:this.from}get head(){return 16&this.flags?this.from:this.to}get empty(){return this.from==this.to}get assoc(){return 4&this.flags?-1:8&this.flags?1:0}get bidiLevel(){let t=3&this.flags;return 3==t?null:t}get goalColumn(){let t=this.flags>>5;return 33554431==t?void 0:t}map(t,e=-1){let i,n;return this.empty?i=n=t.mapPos(this.from,e):(i=t.mapPos(this.from,1),n=t.mapPos(this.to,-1)),i==this.from&&n==this.to?this:new R(i,n,this.flags)}extend(t,e=t){if(t<=this.anchor&&e>=this.anchor)return E.range(t,e);let i=Math.abs(t-this.anchor)>Math.abs(e-this.anchor)?t:e;return E.range(this.anchor,i)}eq(t){return this.anchor==t.anchor&&this.head==t.head}toJSON(){return{anchor:this.anchor,head:this.head}}static fromJSON(t){if(!t||"number"!=typeof t.anchor||"number"!=typeof t.head)throw new RangeError("Invalid JSON representation for SelectionRange");return E.range(t.anchor,t.head)}static create(t,e,i){return new R(t,e,i)}}class E{constructor(t,e){this.ranges=t,this.mainIndex=e}map(t,e=-1){return t.empty?this:E.create(this.ranges.map((i=>i.map(t,e))),this.mainIndex)}eq(t){if(this.ranges.length!=t.ranges.length||this.mainIndex!=t.mainIndex)return!1;for(let e=0;et.toJSON())),main:this.mainIndex}}static fromJSON(t){if(!t||!Array.isArray(t.ranges)||"number"!=typeof t.main||t.main>=t.ranges.length)throw new RangeError("Invalid JSON representation for EditorSelection");return new E(t.ranges.map((t=>R.fromJSON(t))),t.main)}static single(t,e=t){return new E([E.range(t,e)],0)}static create(t,e=0){if(0==t.length)throw new RangeError("A selection needs at least one range");for(let i=0,n=0;nt?4:0))}static normalized(t,e=0){let i=t[e];t.sort(((t,e)=>t.from-e.from)),e=t.indexOf(i);for(let i=1;in.head?E.range(o,r):E.range(r,o))}}return new E(t,e)}}function B(t,e){for(let i of t.ranges)if(i.to>e)throw new RangeError("Selection points outside of document")}let L=0;class N{constructor(t,e,i,n,s){this.combine=t,this.compareInput=e,this.compare=i,this.isStatic=n,this.id=L++,this.default=t([]),this.extensions="function"==typeof s?s(this):s}static define(t={}){return new N(t.combine||(t=>t),t.compareInput||((t,e)=>t===e),t.compare||(t.combine?(t,e)=>t===e:I),!!t.static,t.enables)}of(t){return new V([],this,0,t)}compute(t,e){if(this.isStatic)throw new Error("Can't compute a static facet");return new V(t,this,1,e)}computeN(t,e){if(this.isStatic)throw new Error("Can't compute a static facet");return new V(t,this,2,e)}from(t,e){return e||(e=t=>t),this.compute([t],(i=>e(i.field(t))))}}function I(t,e){return t==e||t.length==e.length&&t.every(((t,i)=>t===e[i]))}class V{constructor(t,e,i,n){this.dependencies=t,this.facet=e,this.type=i,this.value=n,this.id=L++}dynamicSlot(t){var e;let i=this.value,n=this.facet.compareInput,s=this.id,r=t[s]>>1,o=2==this.type,l=!1,a=!1,h=[];for(let i of this.dependencies)"doc"==i?l=!0:"selection"==i?a=!0:0==(1&(null!==(e=t[i.id])&&void 0!==e?e:1))&&h.push(t[i.id]);return{create:t=>(t.values[r]=i(t),1),update(t,e){if(l&&e.docChanged||a&&(e.docChanged||e.selection)||z(t,h)){let e=i(t);if(o?!W(e,t.values[r],n):!n(e,t.values[r]))return t.values[r]=e,1}return 0},reconfigure:(t,e)=>{let l,a=e.config.address[s];if(null!=a){let s=tt(e,a);if(this.dependencies.every((i=>i instanceof N?e.facet(i)===t.facet(i):!(i instanceof q)||e.field(i,!1)==t.field(i,!1)))||(o?W(l=i(t),s,n):n(l=i(t),s)))return t.values[r]=s,0}else l=i(t);return t.values[r]=l,1}}}}function W(t,e,i){if(t.length!=e.length)return!1;for(let n=0;nt[e.id])),s=i.map((t=>t.type)),r=n.filter((t=>!(1&t))),o=t[e.id]>>1;function l(t){let i=[];for(let e=0;et===e),t);return t.provide&&(e.provides=t.provide(e)),e}create(t){let e=t.facet(F).find((t=>t.field==this));return((null==e?void 0:e.create)||this.createF)(t)}slot(t){let e=t[this.id]>>1;return{create:t=>(t.values[e]=this.create(t),1),update:(t,i)=>{let n=t.values[e],s=this.updateF(n,i);return this.compareF(n,s)?0:(t.values[e]=s,1)},reconfigure:(t,i)=>null!=i.config.address[this.id]?(t.values[e]=i.field(this),0):(t.values[e]=this.create(t),1)}}init(t){return[this,F.of({field:this,create:t})]}get extension(){return this}}const _=4,j=3,U=2,$=1;function Q(t){return e=>new G(e,t)}const K={highest:Q(0),high:Q($),default:Q(U),low:Q(j),lowest:Q(_)};class G{constructor(t,e){this.inner=t,this.prec=e}}class J{of(t){return new X(this,t)}reconfigure(t){return J.reconfigure.of({compartment:this,extension:t})}get(t){return t.config.compartments.get(this)}}class X{constructor(t,e){this.compartment=t,this.inner=e}}class Z{constructor(t,e,i,n,s,r){for(this.base=t,this.compartments=e,this.dynamicSlots=i,this.address=n,this.staticValues=s,this.facets=r,this.statusTemplate=[];this.statusTemplate.length>1]}static resolve(t,e,i){let n=[],s=Object.create(null),r=new Map;for(let i of function(t,e,i){let n=[[],[],[],[],[]],s=new Map;function r(t,o){let l=s.get(t);if(null!=l){if(l<=o)return;let e=n[l].indexOf(t);e>-1&&n[l].splice(e,1),t instanceof X&&i.delete(t.compartment)}if(s.set(t,o),Array.isArray(t))for(let e of t)r(e,o);else if(t instanceof X){if(i.has(t.compartment))throw new RangeError("Duplicate use of compartment in extensions");let n=e.get(t.compartment)||t.inner;i.set(t.compartment,n),r(n,o)}else if(t instanceof G)r(t.inner,t.prec);else if(t instanceof q)n[o].push(t),t.provides&&r(t.provides,o);else if(t instanceof V)n[o].push(t),t.facet.extensions&&r(t.facet.extensions,U);else{let e=t.extension;if(!e)throw new Error(`Unrecognized extension value in extension set (${t}). This sometimes happens because multiple instances of @codemirror/state are loaded, breaking instanceof checks.`);r(e,o)}}return r(t,U),n.reduce(((t,e)=>t.concat(e)))}(t,e,r))i instanceof q?n.push(i):(s[i.facet.id]||(s[i.facet.id]=[])).push(i);let o=Object.create(null),l=[],a=[];for(let t of n)o[t.id]=a.length<<1,a.push((e=>t.slot(e)));let h=null==i?void 0:i.config.facets;for(let t in s){let e=s[t],n=e[0].facet,r=h&&h[t]||[];if(e.every((t=>0==t.type)))if(o[n.id]=l.length<<1|1,I(r,e))l.push(i.facet(n));else{let t=n.combine(e.map((t=>t.value)));l.push(i&&n.compare(t,i.facet(n))?i.facet(n):t)}else{for(let t of e)0==t.type?(o[t.id]=l.length<<1|1,l.push(t.value)):(o[t.id]=a.length<<1,a.push((e=>t.dynamicSlot(e))));o[n.id]=a.length<<1,a.push((t=>H(t,n,e)))}}let c=a.map((t=>t(o)));return new Z(t,r,c,o,l,s)}}function Y(t,e){if(1&e)return 2;let i=e>>1,n=t.status[i];if(4==n)throw new Error("Cyclic dependency between fields and/or facets");if(2&n)return n;t.status[i]=4;let s=t.computeSlot(t,t.config.dynamicSlots[i]);return t.status[i]=2|s}function tt(t,e){return 1&e?t.config.staticValues[e>>1]:t.values[e>>1]}const et=N.define(),it=N.define({combine:t=>t.some((t=>t)),static:!0}),nt=N.define({combine:t=>t.length?t[0]:void 0,static:!0}),st=N.define(),rt=N.define(),ot=N.define(),lt=N.define({combine:t=>!!t.length&&t[0]});class at{constructor(t,e){this.type=t,this.value=e}static define(){return new ht}}class ht{of(t){return new at(this,t)}}class ct{constructor(t){this.map=t}of(t){return new ut(this,t)}}class ut{constructor(t,e){this.type=t,this.value=e}map(t){let e=this.type.map(this.value,t);return void 0===e?void 0:e==this.value?this:new ut(this.type,e)}is(t){return this.type==t}static define(t={}){return new ct(t.map||(t=>t))}static mapEffects(t,e){if(!t.length)return t;let i=[];for(let n of t){let t=n.map(e);t&&i.push(t)}return i}}ut.reconfigure=ut.define(),ut.appendConfig=ut.define();class ft{constructor(t,e,i,n,s,r){this.startState=t,this.changes=e,this.selection=i,this.effects=n,this.annotations=s,this.scrollIntoView=r,this._doc=null,this._state=null,i&&B(i,e.newLength),s.some((t=>t.type==ft.time))||(this.annotations=s.concat(ft.time.of(Date.now())))}static create(t,e,i,n,s,r){return new ft(t,e,i,n,s,r)}get newDoc(){return this._doc||(this._doc=this.changes.apply(this.startState.doc))}get newSelection(){return this.selection||this.startState.selection.map(this.changes)}get state(){return this._state||this.startState.applyTransaction(this),this._state}annotation(t){for(let e of this.annotations)if(e.type==t)return e.value}get docChanged(){return!this.changes.empty}get reconfigured(){return this.startState.config!=this.state.config}isUserEvent(t){let e=this.annotation(ft.userEvent);return!(!e||!(e==t||e.length>t.length&&e.slice(0,t.length)==t&&"."==e[t.length]))}}function dt(t,e){let i=[];for(let n=0,s=0;;){let r,o;if(n=t[n]))r=t[n++],o=t[n++];else{if(!(s=0;s--){let r=i[s](t);r&&Object.keys(r).length&&(n=pt(n,mt(e,r,t.changes.newLength),!0))}return n==t?t:ft.create(e,t.changes,t.selection,n.effects,n.annotations,n.scrollIntoView)}(i?function(t){let e=t.startState,i=!0;for(let n of e.facet(st)){let e=n(t);if(!1===e){i=!1;break}Array.isArray(e)&&(i=!0===i?e:dt(i,e))}if(!0!==i){let n,s;if(!1===i)s=t.changes.invertedDesc,n=C.empty(e.doc.length);else{let e=t.changes.filter(i);n=e.changes,s=e.filtered.mapDesc(e.changes).invertedDesc}t=ft.create(e,n,t.selection&&t.selection.map(s),ut.mapEffects(t.effects,s),t.annotations,t.scrollIntoView)}let n=e.facet(rt);for(let i=n.length-1;i>=0;i--){let s=n[i](t);t=s instanceof ft?s:Array.isArray(s)&&1==s.length&&s[0]instanceof ft?s[0]:gt(e,wt(s),!1)}return t}(s):s)}ft.time=at.define(),ft.userEvent=at.define(),ft.addToHistory=at.define(),ft.remote=at.define();const vt=[];function wt(t){return null==t?vt:Array.isArray(t)?t:[t]}var yt=function(t){return t[t.Word=0]="Word",t[t.Space=1]="Space",t[t.Other=2]="Other",t}(yt||(yt={}));const bt=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;let xt;try{xt=new RegExp("[\\p{Alphabetic}\\p{Number}_]","u")}catch(t){}function kt(t){return e=>{if(!/\S/.test(e))return yt.Space;if(function(t){if(xt)return xt.test(t);for(let e=0;e"€"&&(i.toUpperCase()!=i.toLowerCase()||bt.test(i)))return!0}return!1}(e))return yt.Word;for(let i=0;i-1)return yt.Word;return yt.Other}}class St{constructor(t,e,i,n,s,r){this.config=t,this.doc=e,this.selection=i,this.values=n,this.status=t.statusTemplate.slice(),this.computeSlot=s,r&&(r._state=this);for(let t=0;ts.set(e,t))),i=null),s.set(e.value.compartment,e.value.extension)):e.is(ut.reconfigure)?(i=null,n=e.value):e.is(ut.appendConfig)&&(i=null,n=wt(n).concat(e.value));if(i)e=t.startState.values.slice();else{i=Z.resolve(n,s,this),e=new St(i,this.doc,this.selection,i.dynamicSlots.map((()=>null)),((t,e)=>e.reconfigure(t,this)),null).values}new St(i,t.newDoc,t.newSelection,e,((e,i)=>i.update(e,t)),t)}replaceSelection(t){return"string"==typeof t&&(t=this.toText(t)),this.changeByRange((e=>({changes:{from:e.from,to:e.to,insert:t},range:E.cursor(e.from+t.length)})))}changeByRange(t){let e=this.selection,i=t(e.ranges[0]),n=this.changes(i.changes),s=[i.range],r=wt(i.effects);for(let i=1;is.spec.fromJSON(r,t))))}return St.create({doc:t.doc,selection:E.fromJSON(t.selection),extensions:e.extensions?n.concat([e.extensions]):n})}static create(t={}){let i=Z.resolve(t.extensions||[],new Map),n=t.doc instanceof e?t.doc:e.of((t.doc||"").split(i.staticFacet(St.lineSeparator)||x)),s=t.selection?t.selection instanceof E?t.selection:E.single(t.selection.anchor,t.selection.head):E.single(0);return B(s,n.length),i.staticFacet(it)||(s=s.asSingle()),new St(i,n,s,i.dynamicSlots.map((()=>null)),((t,e)=>e.create(t)),null)}get tabSize(){return this.facet(St.tabSize)}get lineBreak(){return this.facet(St.lineSeparator)||"\n"}get readOnly(){return this.facet(lt)}phrase(t,...e){for(let e of this.facet(St.phrases))if(Object.prototype.hasOwnProperty.call(e,t)){t=e[t];break}return e.length&&(t=t.replace(/\$(\$|\d*)/g,((t,i)=>{if("$"==i)return"$";let n=+(i||1);return!n||n>e.length?t:e[n-1]}))),t}languageDataAt(t,e,i=-1){let n=[];for(let s of this.facet(et))for(let r of s(this,e,i))Object.prototype.hasOwnProperty.call(r,t)&&n.push(r[t]);return n}charCategorizer(t){return kt(this.languageDataAt("wordChars",t).join(""))}wordAt(t){let{text:e,from:i,length:n}=this.doc.lineAt(t),s=this.charCategorizer(t),r=t-i,o=t-i;for(;r>0;){let t=d(e,r,!1);if(s(e.slice(t,r))!=yt.Word)break;r=t}for(;ot.length?t[0]:4}),St.lineSeparator=nt,St.readOnly=lt,St.phrases=N.define({compare(t,e){let i=Object.keys(t),n=Object.keys(e);return i.length==n.length&&i.every((i=>t[i]==e[i]))}}),St.languageData=et,St.changeFilter=st,St.transactionFilter=rt,St.transactionExtender=ot,J.reconfigure=ut.define();class At{eq(t){return this==t}range(t,e=t){return Ot.create(t,e,this)}}At.prototype.startSide=At.prototype.endSide=0,At.prototype.point=!1,At.prototype.mapMode=k.TrackDel;let Ot=class{constructor(t,e,i){this.from=t,this.to=e,this.value=i}static create(t,e,i){return new Ot(t,e,i)}};function Mt(t,e){return t.from-e.from||t.value.startSide-e.value.startSide}class Dt{constructor(t,e,i,n){this.from=t,this.to=e,this.value=i,this.maxPoint=n}get length(){return this.to[this.to.length-1]}findIndex(t,e,i,n=0){let s=i?this.to:this.from;for(let r=n,o=s.length;;){if(r==o)return r;let n=r+o>>1,l=s[n]-t||(i?this.value[n].endSide:this.value[n].startSide)-e;if(n==r)return l>=0?r:o;l>=0?o=n:r=n+1}}between(t,e,i,n){for(let s=this.findIndex(e,-1e9,!0),r=this.findIndex(i,1e9,!1,s);sh||a==h&&c.startSide>0&&c.endSide<=0)continue;(h-a||c.endSide-c.startSide)<0||(r<0&&(r=a),c.point&&(o=Math.max(o,h-a)),i.push(c),n.push(a-r),s.push(h-r))}return{mapped:i.length?new Dt(n,s,i,o):null,pos:r}}}class Tt{constructor(t,e,i,n){this.chunkPos=t,this.chunk=e,this.nextLayer=i,this.maxPoint=n}static create(t,e,i,n){return new Tt(t,e,i,n)}get length(){let t=this.chunk.length-1;return t<0?0:Math.max(this.chunkEnd(t),this.nextLayer.length)}get size(){if(this.isEmpty)return 0;let t=this.nextLayer.size;for(let e of this.chunk)t+=e.value.length;return t}chunkEnd(t){return this.chunkPos[t]+this.chunk[t].length}update(t){let{add:e=[],sort:i=!1,filterFrom:n=0,filterTo:s=this.length}=t,r=t.filter;if(0==e.length&&!r)return this;if(i&&(e=e.slice().sort(Mt)),this.isEmpty)return e.length?Tt.of(e):this;let o=new Et(this,null,-1).goto(0),l=0,a=[],h=new Pt;for(;o.value||l=0){let t=e[l++];h.addInner(t.from,t.to,t.value)||a.push(t)}else 1==o.rangeIndex&&o.chunkIndexthis.chunkEnd(o.chunkIndex)||so.to||s=s&&t<=s+r.length&&!1===r.between(s,t-s,e-s,i))return}this.nextLayer.between(t,e,i)}}iter(t=0){return Bt.from([this]).goto(t)}get isEmpty(){return this.nextLayer==this}static iter(t,e=0){return Bt.from(t).goto(e)}static compare(t,e,i,n,s=-1){let r=t.filter((t=>t.maxPoint>0||!t.isEmpty&&t.maxPoint>=s)),o=e.filter((t=>t.maxPoint>0||!t.isEmpty&&t.maxPoint>=s)),l=Rt(r,o,i),a=new Nt(r,l,s),h=new Nt(o,l,s);i.iterGaps(((t,e,i)=>It(a,t,h,e,i,n))),i.empty&&0==i.length&&It(a,0,h,0,0,n)}static eq(t,e,i=0,n){null==n&&(n=999999999);let s=t.filter((t=>!t.isEmpty&&e.indexOf(t)<0)),r=e.filter((e=>!e.isEmpty&&t.indexOf(e)<0));if(s.length!=r.length)return!1;if(!s.length)return!0;let o=Rt(s,r),l=new Nt(s,o,0).goto(i),a=new Nt(r,o,0).goto(i);for(;;){if(l.to!=a.to||!Vt(l.active,a.active)||l.point&&(!a.point||!l.point.eq(a.point)))return!1;if(l.to>n)return!0;l.next(),a.next()}}static spans(t,e,i,n,s=-1){let r=new Nt(t,null,s).goto(e),o=e,l=r.openStart;for(;;){let t=Math.min(r.to,i);if(r.point){let i=r.activeForPoint(r.to),s=r.pointFromo&&(n.span(o,t,r.active,l),l=r.openEnd(t));if(r.to>i)return l+(r.point&&r.to>i?1:0);o=r.to,r.next()}}static of(t,e=!1){let i=new Pt;for(let n of t instanceof Ot?[t]:e?function(t){if(t.length>1)for(let e=t[0],i=1;i0)return t.slice().sort(Mt);e=n}return t}(t):t)i.add(n.from,n.to,n.value);return i.finish()}}Tt.empty=new Tt([],[],null,-1),Tt.empty.nextLayer=Tt.empty;class Pt{constructor(){this.chunks=[],this.chunkPos=[],this.chunkStart=-1,this.last=null,this.lastFrom=-1e9,this.lastTo=-1e9,this.from=[],this.to=[],this.value=[],this.maxPoint=-1,this.setMaxPoint=-1,this.nextLayer=null}finishChunk(t){this.chunks.push(new Dt(this.from,this.to,this.value,this.maxPoint)),this.chunkPos.push(this.chunkStart),this.chunkStart=-1,this.setMaxPoint=Math.max(this.setMaxPoint,this.maxPoint),this.maxPoint=-1,t&&(this.from=[],this.to=[],this.value=[])}add(t,e,i){this.addInner(t,e,i)||(this.nextLayer||(this.nextLayer=new Pt)).add(t,e,i)}addInner(t,e,i){let n=t-this.lastTo||i.startSide-this.last.endSide;if(n<=0&&(t-this.lastFrom||i.startSide-this.last.startSide)<0)throw new Error("Ranges must be added sorted by `from` position and `startSide`");return!(n<0)&&(250==this.from.length&&this.finishChunk(!0),this.chunkStart<0&&(this.chunkStart=t),this.from.push(t-this.chunkStart),this.to.push(e-this.chunkStart),this.last=i,this.lastFrom=t,this.lastTo=e,this.value.push(i),i.point&&(this.maxPoint=Math.max(this.maxPoint,e-t)),!0)}addChunk(t,e){if((t-this.lastTo||e.value[0].startSide-this.last.endSide)<0)return!1;this.from.length&&this.finishChunk(!0),this.setMaxPoint=Math.max(this.setMaxPoint,e.maxPoint),this.chunks.push(e),this.chunkPos.push(t);let i=e.value.length-1;return this.last=e.value[i],this.lastFrom=e.from[i]+t,this.lastTo=e.to[i]+t,!0}finish(){return this.finishInner(Tt.empty)}finishInner(t){if(this.from.length&&this.finishChunk(!1),0==this.chunks.length)return t;let e=Tt.create(this.chunkPos,this.chunks,this.nextLayer?this.nextLayer.finishInner(t):t,this.setMaxPoint);return this.from=null,e}}function Rt(t,e,i){let n=new Map;for(let e of t)for(let t=0;t=this.minPoint)break}}}setRangeIndex(t){if(t==this.layer.chunk[this.chunkIndex].value.length){if(this.chunkIndex++,this.skip)for(;this.chunkIndex=i&&n.push(new Et(r,e,i,s));return 1==n.length?n[0]:new Bt(n)}get startSide(){return this.value?this.value.startSide:0}goto(t,e=-1e9){for(let i of this.heap)i.goto(t,e);for(let t=this.heap.length>>1;t>=0;t--)Lt(this.heap,t);return this.next(),this}forward(t,e){for(let i of this.heap)i.forward(t,e);for(let t=this.heap.length>>1;t>=0;t--)Lt(this.heap,t);(this.to-t||this.value.endSide-e)<0&&this.next()}next(){if(0==this.heap.length)this.from=this.to=1e9,this.value=null,this.rank=-1;else{let t=this.heap[0];this.from=t.from,this.to=t.to,this.value=t.value,this.rank=t.rank,t.value&&t.next(),Lt(this.heap,0)}}}function Lt(t,e){for(let i=t[e];;){let n=1+(e<<1);if(n>=t.length)break;let s=t[n];if(n+1=0&&(s=t[n+1],n++),i.compare(s)<0)break;t[n]=i,t[e]=s,e=n}}class Nt{constructor(t,e,i){this.minPoint=i,this.active=[],this.activeTo=[],this.activeRank=[],this.minActive=-1,this.point=null,this.pointFrom=0,this.pointRank=0,this.to=-1e9,this.endSide=0,this.openStart=-1,this.cursor=Bt.from(t,e,i)}goto(t,e=-1e9){return this.cursor.goto(t,e),this.active.length=this.activeTo.length=this.activeRank.length=0,this.minActive=-1,this.to=t,this.endSide=e,this.openStart=-1,this.next(),this}forward(t,e){for(;this.minActive>-1&&(this.activeTo[this.minActive]-t||this.active[this.minActive].endSide-e)<0;)this.removeActive(this.minActive);this.cursor.forward(t,e)}removeActive(t){Wt(this.active,t),Wt(this.activeTo,t),Wt(this.activeRank,t),this.minActive=Ht(this.active,this.activeTo)}addActive(t){let e=0,{value:i,to:n,rank:s}=this.cursor;for(;e-1&&(this.activeTo[n]-this.cursor.from||this.active[n].endSide-this.cursor.startSide)<0){if(this.activeTo[n]>t){this.to=this.activeTo[n],this.endSide=this.active[n].endSide;break}this.removeActive(n),i&&Wt(i,n)}else{if(!this.cursor.value){this.to=this.endSide=1e9;break}if(this.cursor.from>t){this.to=this.cursor.from,this.endSide=this.cursor.startSide;break}{let t=this.cursor.value;if(t.point){if(!(e&&this.cursor.to==this.to&&this.cursor.from=0&&i[e]=0&&!(this.activeRank[i]t||this.activeTo[i]==t&&this.active[i].endSide>=this.point.endSide)&&e.push(this.active[i]);return e.reverse()}openEnd(t){let e=0;for(let i=this.activeTo.length-1;i>=0&&this.activeTo[i]>t;i--)e++;return e}}function It(t,e,i,n,s,r){t.goto(e),i.goto(n);let o=n+s,l=n,a=n-e;for(;;){let e=t.to+a-i.to||t.endSide-i.endSide,n=e<0?t.to+a:i.to,s=Math.min(n,o);if(t.point||i.point?t.point&&i.point&&(t.point==i.point||t.point.eq(i.point))&&Vt(t.activeForPoint(t.to+a),i.activeForPoint(i.to))||r.comparePoint(l,s,t.point,i.point):s>l&&!Vt(t.active,i.active)&&r.compareRange(l,s,t.active,i.active),n>o)break;l=n,e<=0&&t.next(),e>=0&&i.next()}}function Vt(t,e){if(t.length!=e.length)return!1;for(let i=0;i=e;i--)t[i+1]=t[i];t[e]=i}function Ht(t,e){let i=-1,n=1e9;for(let s=0;s=e)return n;if(n==t.length)break;s+=9==t.charCodeAt(n)?i-s%i:1,n=d(t,n)}return!0===n?-1:t.length}const _t="undefined"==typeof Symbol?"__ͼ":Symbol.for("ͼ"),jt="undefined"==typeof Symbol?"__styleSet"+Math.floor(1e8*Math.random()):Symbol("styleSet"),Ut="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:{};class $t{constructor(t,e){this.rules=[];let{finish:i}=e||{};function n(t){return/^@/.test(t)?[t]:t.split(/,\s*/)}function s(t,e,r,o){let l=[],a=/^@(\w+)\b/.exec(t[0]),h=a&&"keyframes"==a[1];if(a&&null==e)return r.push(t[0]+";");for(let i in e){let o=e[i];if(/&/.test(i))s(i.split(/,\s*/).map((e=>t.map((t=>e.replace(/&/,t))))).reduce(((t,e)=>t.concat(e))),o,r);else if(o&&"object"==typeof o){if(!a)throw new RangeError("The value of a property ("+i+") should be a primitive value.");s(n(i),o,l,h)}else null!=o&&l.push(i.replace(/_.*/,"").replace(/[A-Z]/g,(t=>"-"+t.toLowerCase()))+": "+o+";")}(l.length||h)&&r.push((!i||a||o?t:t.map(i)).join(", ")+" {"+l.join(" ")+"}")}for(let e in t)s(n(e),t[e],this.rules)}getRules(){return this.rules.join("\n")}static newName(){let t=Ut[_t]||1;return Ut[_t]=t+1,"ͼ"+t.toString(36)}static mount(t,e){(t[jt]||new Kt(t)).mount(Array.isArray(e)?e:[e])}}let Qt=null;class Kt{constructor(t){if(!t.head&&t.adoptedStyleSheets&&"undefined"!=typeof CSSStyleSheet){if(Qt)return t.adoptedStyleSheets=[Qt.sheet].concat(t.adoptedStyleSheets),t[jt]=Qt;this.sheet=new CSSStyleSheet,t.adoptedStyleSheets=[this.sheet].concat(t.adoptedStyleSheets),Qt=this}else{this.styleTag=(t.ownerDocument||t).createElement("style");let e=t.head||t;e.insertBefore(this.styleTag,e.firstChild)}this.modules=[],t[jt]=this}mount(t){let e=this.sheet,i=0,n=0;for(let s=0;s-1&&(this.modules.splice(o,1),n--,o=-1),-1==o){if(this.modules.splice(n++,0,r),e)for(let t=0;t",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'},Xt="undefined"!=typeof navigator&&/Chrome\/(\d+)/.exec(navigator.userAgent);"undefined"!=typeof navigator&&/Gecko\/\d+/.test(navigator.userAgent);for(var Zt="undefined"!=typeof navigator&&/Mac/.test(navigator.platform),Yt="undefined"!=typeof navigator&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),te=Zt||Xt&&+Xt[1]<57,ee=0;ee<10;ee++)Gt[48+ee]=Gt[96+ee]=String(ee);for(ee=1;ee<=24;ee++)Gt[ee+111]="F"+ee;for(ee=65;ee<=90;ee++)Gt[ee]=String.fromCharCode(ee+32),Jt[ee]=String.fromCharCode(ee);for(var ie in Gt)Jt.hasOwnProperty(ie)||(Jt[ie]=Gt[ie]);function ne(t){let e;return e=11==t.nodeType?t.getSelection?t:t.ownerDocument:t,e.getSelection()}function se(t,e){return!!e&&(t==e||t.contains(1!=e.nodeType?e.parentNode:e))}function re(t,e){if(!e.anchorNode)return!1;try{return se(t,e.anchorNode)}catch(t){return!1}}function oe(t){return 3==t.nodeType?we(t,0,t.nodeValue.length).getClientRects():1==t.nodeType?t.getClientRects():[]}function le(t,e,i,n){return!!i&&(he(t,e,i,n,-1)||he(t,e,i,n,1))}function ae(t){for(var e=0;;e++)if(!(t=t.previousSibling))return e}function he(t,e,i,n,s){for(;;){if(t==i&&e==n)return!0;if(e==(s<0?0:ce(t))){if("DIV"==t.nodeName)return!1;let i=t.parentNode;if(!i||1!=i.nodeType)return!1;e=ae(t)+(s<0?0:1),t=i}else{if(1!=t.nodeType)return!1;if(1==(t=t.childNodes[e+(s<0?-1:0)]).nodeType&&"false"==t.contentEditable)return!1;e=s<0?ce(t):0}}}function ce(t){return 3==t.nodeType?t.nodeValue.length:t.childNodes.length}const ue={left:0,right:0,top:0,bottom:0};function fe(t,e){let i=e?t.left:t.right;return{left:i,right:i,top:t.top,bottom:t.bottom}}function de(t){return{left:0,right:t.innerWidth,top:0,bottom:t.innerHeight}}class pe{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}eq(t){return this.anchorNode==t.anchorNode&&this.anchorOffset==t.anchorOffset&&this.focusNode==t.focusNode&&this.focusOffset==t.focusOffset}setRange(t){this.set(t.anchorNode,t.anchorOffset,t.focusNode,t.focusOffset)}set(t,e,i,n){this.anchorNode=t,this.anchorOffset=e,this.focusNode=i,this.focusOffset=n}}let me,ge=null;function ve(t){if(t.setActive)return t.setActive();if(ge)return t.focus(ge);let e=[];for(let i=t;i&&(e.push(i,i.scrollTop,i.scrollLeft),i!=i.ownerDocument);i=i.parentNode);if(t.focus(null==ge?{get preventScroll(){return ge={preventScroll:!0},!0}}:void 0),!ge){ge=!1;for(let t=0;te)return i.domBoundsAround(t,e,a);if(c>=t&&-1==n&&(n=l,s=a),a>e&&i.dom.parentNode==this.dom){r=l,o=h;break}h=c,a=c+i.breakAfter}return{from:s,to:o<0?i+this.length:o,startDOM:(n?this.children[n-1].dom.nextSibling:null)||this.dom.firstChild,endDOM:r=0?this.children[r].dom:null}}markDirty(t=!1){this.dirty|=2,this.markParentsDirty(t)}markParentsDirty(t){for(let e=this.parent;e;e=e.parent){if(t&&(e.dirty|=2),1&e.dirty)return;e.dirty|=1,t=!1}}setParent(t){this.parent!=t&&(this.parent=t,this.dirty&&this.markParentsDirty(!0))}setDOM(t){this.dom&&(this.dom.cmView=null),this.dom=t,t.cmView=this}get rootView(){for(let t=this;;){let e=t.parent;if(!e)return t;t=e}}replaceChildren(t,e,i=ke){this.markDirty();for(let i=t;ithis.pos||t==this.pos&&(e>0||0==this.i||this.children[this.i-1].breakAfter))return this.off=t-this.pos,this;let i=this.children[--this.i];this.pos-=i.length+i.breakAfter}}}function Oe(t,e,i,n,s,r,o,l,a){let{children:h}=t,c=h.length?h[e]:null,u=r.length?r[r.length-1]:null,f=u?u.breakAfter:o;if(!(e==n&&c&&!o&&!f&&r.length<2&&c.merge(i,s,r.length?u:null,0==i,l,a))){if(n0&&(!o&&r.length&&c.merge(i,c.length,r[0],!1,l,0)?c.breakAfter=r.shift().breakAfter:(i2);var ze={mac:We||/Mac/.test(De.platform),windows:/Win/.test(De.platform),linux:/Linux|X11/.test(De.platform),ie:Be,ie_version:Re?Te.documentMode||6:Ee?+Ee[1]:Pe?+Pe[1]:0,gecko:Le,gecko_version:Le?+(/Firefox\/(\d+)/.exec(De.userAgent)||[0,0])[1]:0,chrome:!!Ne,chrome_version:Ne?+Ne[1]:0,ios:We,android:/Android\b/.test(De.userAgent),webkit:Ie,safari:Ve,webkit_version:Ie?+(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent)||[0,0])[1]:0,tabSize:null!=Te.documentElement.style.tabSize?"tab-size":"-moz-tab-size"};class He extends Se{constructor(t){super(),this.text=t}get length(){return this.text.length}createDOM(t){this.setDOM(t||document.createTextNode(this.text))}sync(t){this.dom||this.createDOM(),this.dom.nodeValue!=this.text&&(t&&t.node==this.dom&&(t.written=!0),this.dom.nodeValue=this.text)}reuseDOM(t){3==t.nodeType&&this.createDOM(t)}merge(t,e,i){return(!i||i instanceof He&&!(this.length-(e-t)+i.length>256))&&(this.text=this.text.slice(0,t)+(i?i.text:"")+this.text.slice(e),this.markDirty(),!0)}split(t){let e=new He(this.text.slice(t));return this.text=this.text.slice(0,t),this.markDirty(),e}localPosFromDOM(t,e){return t==this.dom?e:e?this.text.length:0}domAtPos(t){return new xe(this.dom,t)}domBoundsAround(t,e,i){return{from:i,to:i+this.length,startDOM:this.dom,endDOM:this.dom.nextSibling}}coordsAt(t,e){return qe(this.dom,t,e)}}class Fe extends Se{constructor(t,e=[],i=0){super(),this.mark=t,this.children=e,this.length=i;for(let t of e)t.setParent(this)}setAttrs(t){if(be(t),this.mark.class&&(t.className=this.mark.class),this.mark.attrs)for(let e in this.mark.attrs)t.setAttribute(e,this.mark.attrs[e]);return t}reuseDOM(t){t.nodeName==this.mark.tagName.toUpperCase()&&(this.setDOM(t),this.dirty|=6)}sync(t){this.dom?4&this.dirty&&this.setAttrs(this.dom):this.setDOM(this.setAttrs(document.createElement(this.mark.tagName))),super.sync(t)}merge(t,e,i,n,s,r){return(!i||!(!(i instanceof Fe&&i.mark.eq(this.mark))||t&&s<=0||et&&e.push(i=t&&(n=s),i=o,s++}let r=this.length-t;return this.length=t,n>-1&&(this.children.length=n,this.markDirty()),new Fe(this.mark,e,r)}domAtPos(t){return Ke(this,t)}coordsAt(t,e){return Je(this,t,e)}}function qe(t,e,i){let n=t.nodeValue.length;e>n&&(e=n);let s=e,r=e,o=0;0==e&&i<0||e==n&&i>=0?ze.chrome||ze.gecko||(e?(s--,o=1):r=0)?0:l.length-1];return ze.safari&&!o&&0==a.width&&(a=Array.prototype.find.call(l,(t=>t.width))||a),o?fe(a,o<0):a||null}class _e extends Se{constructor(t,e,i){super(),this.widget=t,this.length=e,this.side=i,this.prevWidget=null}static create(t,e,i){return new(t.customView||_e)(t,e,i)}split(t){let e=_e.create(this.widget,this.length-t,this.side);return this.length-=t,e}sync(){this.dom&&this.widget.updateDOM(this.dom)||(this.dom&&this.prevWidget&&this.prevWidget.destroy(this.dom),this.prevWidget=null,this.setDOM(this.widget.toDOM(this.editorView)),this.dom.contentEditable="false")}getSide(){return this.side}merge(t,e,i,n,s,r){return!(i&&(!(i instanceof _e&&this.widget.compare(i.widget))||t>0&&s<=0||e0?i.length-1:0;n=i[e],!(t>0?0==e:e==i.length-1||n.top0?-1:1);return this.length?n:fe(n,this.side>0)}get isEditable(){return!1}destroy(){super.destroy(),this.dom&&this.widget.destroy(this.dom)}}class je extends _e{domAtPos(t){let{topView:e,text:i}=this.widget;return e?Ue(t,0,e,i,((t,e)=>t.domAtPos(e)),(t=>new xe(i,Math.min(t,i.nodeValue.length)))):new xe(i,Math.min(t,i.nodeValue.length))}sync(){this.setDOM(this.widget.toDOM())}localPosFromDOM(t,e){let{topView:i,text:n}=this.widget;return i?$e(t,e,i,n):Math.min(e,this.length)}ignoreMutation(){return!1}get overrideDOMText(){return null}coordsAt(t,e){let{topView:i,text:n}=this.widget;return i?Ue(t,e,i,n,((t,e,i)=>t.coordsAt(e,i)),((t,e)=>qe(n,t,e))):qe(n,t,e)}destroy(){var t;super.destroy(),null===(t=this.widget.topView)||void 0===t||t.destroy()}get isEditable(){return!0}canReuseDOM(){return!0}}function Ue(t,e,i,n,s,r){if(i instanceof Fe){for(let o=i.dom.firstChild;o;o=o.nextSibling){let i=Se.get(o);if(!i)return r(t,e);let l=se(o,n),a=i.length+(l?n.nodeValue.length:0);if(t=0;)if(e<0?n>0:n0?-1:1);return i&&i.tope.top?{left:e.left,right:e.right,top:i.top,bottom:i.bottom}:e}get overrideDOMText(){return e.empty}}function Ke(t,e){let i=t.dom,{children:n}=t,s=0;for(let t=0;st&&e0;t--){let e=n[t-1];if(e.dom.parentNode==i)return e.domAtPos(e.length)}for(let t=s;t0&&e instanceof Fe&&s.length&&(n=s[s.length-1])instanceof Fe&&n.mark.eq(e.mark)?Ge(n,e.children[0],i-1):(s.push(e),e.setParent(t)),t.length+=e.length}function Je(t,e,i){let n=null,s=-1,r=null,o=-1;!function t(e,i){for(let l=0,a=0;l=i&&(h.children.length?t(h,i-a):!r&&(c>i||a==c&&h.getSide()>0)?(r=h,o=i-a):(a0?3e8:-4e8:e>0?1e8:-1e8,new ri(t,e,e,i,t.widget||null,!1)}static replace(t){let e,i,n=!!t.block;if(t.isBlockGap)e=-5e8,i=4e8;else{let{start:s,end:r}=oi(t,n);e=(s?n?-3e8:-1:5e8)-1,i=1+(r?n?2e8:1:-6e8)}return new ri(t,e,i,n,t.widget||null,!0)}static line(t){return new si(t)}static set(t,e=!1){return Tt.of(t,e)}hasHeight(){return!!this.widget&&this.widget.estimatedHeight>-1}}ii.none=Tt.empty;class ni extends ii{constructor(t){let{start:e,end:i}=oi(t);super(e?-1:5e8,i?1:-6e8,null,t),this.tagName=t.tagName||"span",this.class=t.class||"",this.attrs=t.attributes||null}eq(t){return this==t||t instanceof ni&&this.tagName==t.tagName&&this.class==t.class&&Ze(this.attrs,t.attrs)}range(t,e=t){if(t>=e)throw new RangeError("Mark decorations may not be empty");return super.range(t,e)}}ni.prototype.point=!1;class si extends ii{constructor(t){super(-2e8,-2e8,null,t)}eq(t){return t instanceof si&&Ze(this.spec.attributes,t.spec.attributes)}range(t,e=t){if(e!=t)throw new RangeError("Line decoration ranges must be zero-length");return super.range(t,e)}}si.prototype.mapMode=k.TrackBefore,si.prototype.point=!0;class ri extends ii{constructor(t,e,i,n,s,r){super(e,i,s,t),this.block=n,this.isReplace=r,this.mapMode=n?e<=0?k.TrackBefore:k.TrackAfter:k.TrackDel}get type(){return this.startSide=5}eq(t){return t instanceof ri&&(e=this.widget,i=t.widget,e==i||!!(e&&i&&e.compare(i)))&&this.block==t.block&&this.startSide==t.startSide&&this.endSide==t.endSide;var e,i}range(t,e=t){if(this.isReplace&&(t>e||t==e&&this.startSide>0&&this.endSide<=0))throw new RangeError("Invalid range for replacement decoration");if(!this.isReplace&&e!=t)throw new RangeError("Widget decorations can only have zero-length ranges");return super.range(t,e)}}function oi(t,e=!1){let{inclusiveStart:i,inclusiveEnd:n}=t;return null==i&&(i=t.inclusive),null==n&&(n=t.inclusive),{start:null!=i?i:e,end:null!=n?n:e}}function li(t,e,i,n=0){let s=i.length-1;s>=0&&i[s]+n>=t?i[s]=Math.max(i[s],e):i.push(t,e)}ri.prototype.point=!0;class ai extends Se{constructor(){super(...arguments),this.children=[],this.length=0,this.prevAttrs=void 0,this.attrs=null,this.breakAfter=0}merge(t,e,i,n,s,r){if(i){if(!(i instanceof ai))return!1;this.dom||i.transferDOM(this)}return n&&this.setDeco(i?i.attrs:null),Me(this,t,e,i?i.children:[],s,r),!0}split(t){let e=new ai;if(e.breakAfter=this.breakAfter,0==this.length)return e;let{i:i,off:n}=this.childPos(t);n&&(e.append(this.children[i].split(n),0),this.children[i].merge(n,this.children[i].length,null,!1,0,0),i++);for(let t=i;t0&&0==this.children[i-1].length;)this.children[--i].destroy();return this.children.length=i,this.markDirty(),this.length=t,e}transferDOM(t){this.dom&&(this.markDirty(),t.setDOM(this.dom),t.prevAttrs=void 0===this.prevAttrs?this.attrs:this.prevAttrs,this.prevAttrs=void 0,this.dom=null)}setDeco(t){Ze(this.attrs,t)||(this.dom&&(this.prevAttrs=this.attrs,this.markDirty()),this.attrs=t)}append(t,e){Ge(this,t,e)}addLineDeco(t){let e=t.spec.attributes,i=t.spec.class;e&&(this.attrs=Xe(e,this.attrs||{})),i&&(this.attrs=Xe({class:i},this.attrs||{}))}domAtPos(t){return Ke(this,t)}reuseDOM(t){"DIV"==t.nodeName&&(this.setDOM(t),this.dirty|=6)}sync(t){var e;this.dom?4&this.dirty&&(be(this.dom),this.dom.className="cm-line",this.prevAttrs=this.attrs?null:void 0):(this.setDOM(document.createElement("div")),this.dom.className="cm-line",this.prevAttrs=this.attrs?null:void 0),void 0!==this.prevAttrs&&(Ye(this.dom,this.prevAttrs,this.attrs),this.dom.classList.add("cm-line"),this.prevAttrs=void 0),super.sync(t);let i=this.dom.lastChild;for(;i&&Se.get(i)instanceof Fe;)i=i.lastChild;if(!(i&&this.length&&("BR"==i.nodeName||0!=(null===(e=Se.get(i))||void 0===e?void 0:e.isEditable)||ze.ios&&this.children.some((t=>t instanceof He))))){let t=document.createElement("BR");t.cmIgnore=!0,this.dom.appendChild(t)}}measureTextSize(){if(0==this.children.length||this.length>20)return null;let t=0;for(let e of this.children){if(!(e instanceof He)||/[^ -~]/.test(e.text))return null;let i=oe(e.dom);if(1!=i.length)return null;t+=i[0].width}return t?{lineHeight:this.dom.getBoundingClientRect().height,charWidth:t/this.length}:null}coordsAt(t,e){return Je(this,t,e)}become(t){return!1}get type(){return ei.Text}static find(t,e){for(let i=0,n=0;i=e){if(s instanceof ai)return s;if(r>e)break}n=r+s.breakAfter}return null}}class hi extends Se{constructor(t,e,i){super(),this.widget=t,this.length=e,this.type=i,this.breakAfter=0,this.prevWidget=null}merge(t,e,i,n,s,r){return!(i&&(!(i instanceof hi&&this.widget.compare(i.widget))||t>0&&s<=0||e0;){if(this.textOff==this.text.length){let{value:e,lineBreak:i,done:n}=this.cursor.next(this.skip);if(this.skip=0,n)throw new Error("Ran out of text content when drawing inline views");if(i){this.posCovered()||this.getLine(),this.content.length?this.content[this.content.length-1].breakAfter=1:this.breakAtStart=1,this.flushBuffer([]),this.curLine=null,t--;continue}this.text=e,this.textOff=0}let n=Math.min(this.text.length-this.textOff,t,512);this.flushBuffer(e.slice(e.length-i)),this.getLine().append(ui(new He(this.text.slice(this.textOff,this.textOff+n)),e),i),this.atCursorPos=!0,this.textOff+=n,t-=n,i=0}}span(t,e,i,n){this.buildText(e-t,i,n),this.pos=e,this.openStart<0&&(this.openStart=n)}point(t,e,i,n,s,r){if(this.disallowBlockEffectsFor[r]&&i instanceof ri){if(i.block)throw new RangeError("Block decorations may not be specified via plugins");if(e>this.doc.lineAt(this.pos).to)throw new RangeError("Decorations that replace line breaks may not be specified via plugins")}let o=e-t;if(i instanceof ri)if(i.block){let{type:t}=i;t!=ei.WidgetAfter||this.posCovered()||this.getLine(),this.addBlockWidget(new hi(i.widget||new fi("div"),o,t))}else{let r=_e.create(i.widget||new fi("span"),o,o?0:i.startSide),l=this.atCursorPos&&!r.isEditable&&s<=n.length&&(t0),a=!r.isEditable&&(tt.some((t=>t))}),bi=N.define({combine:t=>t.some((t=>t))});class xi{constructor(t,e="nearest",i="nearest",n=5,s=5){this.range=t,this.y=e,this.x=i,this.yMargin=n,this.xMargin=s}map(t){return t.empty?this:new xi(this.range.map(t),this.y,this.x,this.yMargin,this.xMargin)}}const ki=ut.define({map:(t,e)=>t.map(e)});function Si(t,e,i){let n=t.facet(gi);n.length?n[0](e):window.onerror?window.onerror(String(e),i,void 0,void 0,e):i?console.error(i+":",e):console.error(e)}const Ci=N.define({combine:t=>!t.length||t[0]});let Ai=0;const Oi=N.define();class Mi{constructor(t,e,i,n){this.id=t,this.create=e,this.domEventHandlers=i,this.extension=n(this)}static define(t,e){const{eventHandlers:i,provide:n,decorations:s}=e||{};return new Mi(Ai++,t,i,(t=>{let e=[Oi.of(t)];return s&&e.push(Ri.of((e=>{let i=e.plugin(t);return i?s(i):ii.none}))),n&&e.push(n(t)),e}))}static fromClass(t,e){return Mi.define((e=>new t(e)),e)}}class Di{constructor(t){this.spec=t,this.mustUpdate=null,this.value=null}update(t){if(this.value){if(this.mustUpdate){let t=this.mustUpdate;if(this.mustUpdate=null,this.value.update)try{this.value.update(t)}catch(e){if(Si(t.state,e,"CodeMirror plugin crashed"),this.value.destroy)try{this.value.destroy()}catch(t){}this.deactivate()}}}else if(this.spec)try{this.value=this.spec.create(t)}catch(e){Si(t.state,e,"CodeMirror plugin crashed"),this.deactivate()}return this}destroy(t){var e;if(null===(e=this.value)||void 0===e?void 0:e.destroy)try{this.value.destroy()}catch(e){Si(t.state,e,"CodeMirror plugin crashed")}}deactivate(){this.spec=this.value=null}}const Ti=N.define(),Pi=N.define(),Ri=N.define(),Ei=N.define(),Bi=N.define(),Li=N.define();class Ni{constructor(t,e,i,n){this.fromA=t,this.toA=e,this.fromB=i,this.toB=n}join(t){return new Ni(Math.min(this.fromA,t.fromA),Math.max(this.toA,t.toA),Math.min(this.fromB,t.fromB),Math.max(this.toB,t.toB))}addToSet(t){let e=t.length,i=this;for(;e>0;e--){let n=t[e-1];if(!(n.fromA>i.toA)){if(n.toAh)break;s+=2}if(!l)return i;new Ni(l.fromA,l.toA,l.fromB,l.toB).addToSet(i),r=l.toA,o=l.toB}}}class Ii{constructor(t,e,i){this.view=t,this.state=e,this.transactions=i,this.flags=0,this.startState=t.state,this.changes=C.empty(this.startState.doc.length);for(let t of i)this.changes=this.changes.compose(t.changes);let n=[];this.changes.iterChangedRanges(((t,e,i,s)=>n.push(new Ni(t,e,i,s)))),this.changedRanges=n;let s=t.hasFocus;s!=t.inputState.notifiedFocused&&(t.inputState.notifiedFocused=s,this.flags|=1)}static create(t,e,i){return new Ii(t,e,i)}get viewportChanged(){return(4&this.flags)>0}get heightChanged(){return(2&this.flags)>0}get geometryChanged(){return this.docChanged||(10&this.flags)>0}get focusChanged(){return(1&this.flags)>0}get docChanged(){return!this.changes.empty}get selectionSet(){return this.transactions.some((t=>t.selection))}get empty(){return 0==this.flags&&0==this.transactions.length}}var Vi=function(t){return t[t.LTR=0]="LTR",t[t.RTL=1]="RTL",t}(Vi||(Vi={}));const Wi=Vi.LTR,zi=Vi.RTL;function Hi(t){let e=[];for(let i=0;i=e){if(o.level==i)return r;(s<0||(0!=n?n<0?o.frome:t[s].level>o.level))&&(s=r)}}if(s<0)throw new RangeError("Index out of range");return s}}const Qi=[];function Ki(t){return[new $i(0,t,0)]}let Gi="";function Ji(t,e,i,n,s){var r;let o=n.head-t.from,l=-1;if(0==o){if(!s||!t.length)return null;e[0].level!=i&&(o=e[0].side(!1,i),l=0)}else if(o==t.length){if(s)return null;let t=e[e.length-1];t.level!=i&&(o=t.side(!0,i),l=e.length-1)}l<0&&(l=$i.find(e,o,null!==(r=n.bidiLevel)&&void 0!==r?r:-1,n.assoc));let a=e[l];o==a.side(s,i)&&(a=e[l+=s?1:-1],o=a.side(!s,i));let h=s==(a.dir==i),c=d(t.text,o,h);if(Gi=t.text.slice(Math.min(o,c),Math.max(o,c)),c!=a.side(s,i))return E.cursor(c+t.from,h?-1:1,a.level);let u=l==(s?e.length-1:0)?null:e[l+(s?1:-1)];return u||a.level==i?u&&u.level1)for(let e of this.points)e.node==t&&e.pos>this.text.length&&(e.pos-=o-1);i=r+o}}readNode(t){if(t.cmIgnore)return;let e=Se.get(t),i=e&&e.overrideDOMText;if(null!=i){this.findPointInside(t,i.length);for(let t=i.iter();!t.next().done;)t.lineBreak?this.lineBreak():this.append(t.value)}else 3==t.nodeType?this.readTextNode(t):"BR"==t.nodeName?t.nextSibling&&this.lineBreak():1==t.nodeType&&this.readRange(t.firstChild,null)}findPointBefore(t,e){for(let i of this.points)i.node==t&&t.childNodes[i.offset]==e&&(i.pos=this.text.length)}findPointInside(t,e){for(let i of this.points)(3==t.nodeType?i.node==t:t.contains(i.node))&&(i.pos=this.text.length+Math.min(e,i.offset))}}function Yi(t){return 1==t.nodeType&&/^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(t.nodeName)}class tn{constructor(t,e){this.node=t,this.offset=e,this.pos=-1}}class en extends Se{constructor(t){super(),this.view=t,this.compositionDeco=ii.none,this.decorations=[],this.dynamicDecorationMap=[],this.minWidth=0,this.minWidthFrom=0,this.minWidthTo=0,this.impreciseAnchor=null,this.impreciseHead=null,this.forceSelection=!1,this.lastUpdate=Date.now(),this.setDOM(t.contentDOM),this.children=[new ai],this.children[0].setParent(this),this.updateDeco(),this.updateInner([new Ni(0,0,0,t.state.doc.length)],0)}get editorView(){return this.view}get length(){return this.view.state.doc.length}update(t){let e=t.changedRanges;this.minWidth>0&&e.length&&(e.every((({fromA:t,toA:e})=>ethis.minWidthTo))?(this.minWidthFrom=t.changes.mapPos(this.minWidthFrom,1),this.minWidthTo=t.changes.mapPos(this.minWidthTo,1)):this.minWidth=this.minWidthFrom=this.minWidthTo=0),this.view.inputState.composing<0?this.compositionDeco=ii.none:(t.transactions.length||this.dirty)&&(this.compositionDeco=function(t,e){let i=sn(t);if(!i)return ii.none;let{from:n,to:s,node:r,text:o}=i,l=e.mapPos(n,1),a=Math.max(l,e.mapPos(s,-1)),{state:h}=t,c=3==r.nodeType?r.nodeValue:new Zi([],h).readRange(r.firstChild,null).text;if(a-l{this.dom.style.height=this.view.viewState.contentHeight+"px",this.dom.style.flexBasis=this.minWidth?this.minWidth+"px":"";let t=ze.chrome||ze.ios?{node:i.selectionRange.focusNode,written:!1}:void 0;this.sync(t),this.dirty=0,t&&(t.written||i.selectionRange.focusNode!=t.node)&&(this.forceSelection=!0),this.dom.style.height=""}));let n=[];if(this.view.viewport.from||this.view.viewport.to=0?t[e]:null;if(!n)break;let{fromA:s,toA:r,fromB:o,toB:l}=n,{content:a,breakAtStart:h,openStart:c,openEnd:u}=ci.build(this.view.state.doc,o,l,this.decorations,this.dynamicDecorationMap),{i:f,off:d}=i.findPos(r,1),{i:p,off:m}=i.findPos(s,-1);Oe(this,p,m,f,d,a,h,c,u)}}updateSelection(t=!1,e=!1){if(!t&&this.view.observer.selectionRange.focusNode||this.view.observer.readSelectionRange(),!e&&!this.mayControlSelection())return;let i=this.forceSelection;this.forceSelection=!1;let n=this.view.state.selection.main,s=this.domAtPos(n.anchor),r=n.empty?s:this.domAtPos(n.head);if(ze.gecko&&n.empty&&(1==(o=s).node.nodeType&&o.node.firstChild&&(0==o.offset||"false"==o.node.childNodes[o.offset-1].contentEditable)&&(o.offset==o.node.childNodes.length||"false"==o.node.childNodes[o.offset].contentEditable))){let t=document.createTextNode("");this.view.observer.ignore((()=>s.node.insertBefore(t,s.node.childNodes[s.offset]||null))),s=r=new xe(t,0),i=!0}var o;let l=this.view.observer.selectionRange;!i&&l.focusNode&&le(s.node,s.offset,l.anchorNode,l.anchorOffset)&&le(r.node,r.offset,l.focusNode,l.focusOffset)||(this.view.observer.ignore((()=>{ze.android&&ze.chrome&&this.dom.contains(l.focusNode)&&function(t,e){for(let i=t;i&&i!=e;i=i.assignedSlot||i.parentNode)if(1==i.nodeType&&"false"==i.contentEditable)return!0;return!1}(l.focusNode,this.dom)&&(this.dom.blur(),this.dom.focus({preventScroll:!0}));let t=ne(this.view.root);if(t)if(n.empty){if(ze.gecko){let t=(e=s.node,i=s.offset,1!=e.nodeType?0:(i&&"false"==e.childNodes[i-1].contentEditable?1:0)|(in.head&&([s,r]=[r,s]),e.setEnd(r.node,r.offset),e.setStart(s.node,s.offset),t.removeAllRanges(),t.addRange(e)}else;var e,i})),this.view.observer.setSelectionRange(s,r)),this.impreciseAnchor=s.precise?null:new xe(l.anchorNode,l.anchorOffset),this.impreciseHead=r.precise?null:new xe(l.focusNode,l.focusOffset)}enforceCursorAssoc(){if(this.compositionDeco.size)return;let{view:t}=this,e=t.state.selection.main,i=ne(t.root),{anchorNode:n,anchorOffset:s}=t.observer.selectionRange;if(!(i&&e.empty&&e.assoc&&i.modify))return;let r=ai.find(this,e.head);if(!r)return;let o=r.posAtStart;if(e.head==o||e.head==o+r.length)return;let l=this.coordsAt(e.head,-1),a=this.coordsAt(e.head,1);if(!l||!a||l.bottom>a.top)return;let h=this.domAtPos(e.head+e.assoc);i.collapse(h.node,h.offset),i.modify("move",e.assoc<0?"forward":"backward","lineboundary"),t.observer.readSelectionRange();let c=t.observer.selectionRange;t.docView.posFromDOM(c.anchorNode,c.anchorOffset)!=e.from&&i.collapse(n,s)}mayControlSelection(){let t=this.view.root.activeElement;return t==this.dom||re(this.dom,this.view.observer.selectionRange)&&!(t&&this.dom.contains(t))}nearest(t){for(let e=t;e;){let t=Se.get(e);if(t&&t.rootView==this)return t;e=e.parentNode}return null}posFromDOM(t,e){let i=this.nearest(t);if(!i)throw new RangeError("Trying to find position for a DOM position outside of the document");return i.localPosFromDOM(t,e)+i.posAtStart}domAtPos(t){let{i:e,off:i}=this.childCursor().findPos(t,-1);for(;er||t==r&&s.type!=ei.WidgetBefore&&s.type!=ei.WidgetAfter&&(!n||2==e||this.children[n-1].breakAfter||this.children[n-1].type==ei.WidgetBefore&&e>-2))return s.coordsAt(t-r,e);i=r}}measureVisibleLineHeights(t){let e=[],{from:i,to:n}=t,s=this.view.contentDOM.clientWidth,r=s>Math.max(this.view.scrollDOM.clientWidth,this.minWidth)+1,o=-1,l=this.view.textDirection==Vi.LTR;for(let t=0,a=0;an)break;if(t>=i){let i=h.dom.getBoundingClientRect();if(e.push(i.height),r){let e=h.dom.lastChild,n=e?oe(e):[];if(n.length){let e=n[n.length-1],r=l?e.right-i.left:i.right-e.left;r>o&&(o=r,this.minWidth=s,this.minWidthFrom=t,this.minWidthTo=c)}}}t=c+h.breakAfter}return e}textDirectionAt(t){let{i:e}=this.childPos(t,1);return"rtl"==getComputedStyle(this.children[e].dom).direction?Vi.RTL:Vi.LTR}measureTextSize(){for(let t of this.children)if(t instanceof ai){let e=t.measureTextSize();if(e)return e}let t,e,i=document.createElement("div");return i.className="cm-line",i.style.width="99999px",i.textContent="abc def ghi jkl mno pqr stu",this.view.observer.ignore((()=>{this.dom.appendChild(i);let n=oe(i.firstChild)[0];t=i.getBoundingClientRect().height,e=n?n.width/27:7,i.remove()})),{lineHeight:t,charWidth:e}}childCursor(t=this.length){let e=this.children.length;return e&&(t-=this.children[--e].length),new Ae(this.children,t,e)}computeBlockGapDeco(){let t=[],e=this.view.viewState;for(let i=0,n=0;;n++){let s=n==e.viewports.length?null:e.viewports[n],r=s?s.from-1:this.length;if(r>i){let n=e.lineBlockAt(r).bottom-e.lineBlockAt(i).top;t.push(ii.replace({widget:new nn(n),block:!0,inclusive:!0,isBlockGap:!0}).range(i,r))}if(!s)break;i=s.to+1}return ii.set(t)}updateDeco(){let t=this.view.state.facet(Ri).map(((t,e)=>(this.dynamicDecorationMap[e]="function"==typeof t)?t(this.view):t));for(let e=t.length;ei.anchor?-1:1);if(!n)return;!i.empty&&(e=this.coordsAt(i.anchor,i.anchor>i.head?-1:1))&&(n={left:Math.min(n.left,e.left),top:Math.min(n.top,e.top),right:Math.max(n.right,e.right),bottom:Math.max(n.bottom,e.bottom)});let s=0,r=0,o=0,l=0;for(let t of this.view.state.facet(Bi).map((t=>t(this.view))))if(t){let{left:e,right:i,top:n,bottom:a}=t;null!=e&&(s=Math.max(s,e)),null!=i&&(r=Math.max(r,i)),null!=n&&(o=Math.max(o,n)),null!=a&&(l=Math.max(l,a))}let a={left:n.left-s,top:n.top-o,right:n.right+r,bottom:n.bottom+l};!function(t,e,i,n,s,r,o,l){let a=t.ownerDocument,h=a.defaultView||window;for(let c=t;c;)if(1==c.nodeType){let t,u=c==a.body;if(u)t=de(h);else{if(c.scrollHeight<=c.clientHeight&&c.scrollWidth<=c.clientWidth){c=c.assignedSlot||c.parentNode;continue}let e=c.getBoundingClientRect();t={left:e.left,right:e.left+c.clientWidth,top:e.top,bottom:e.top+c.clientHeight}}let f=0,d=0;if("nearest"==s)e.top0&&e.bottom>t.bottom+d&&(d=e.bottom-t.bottom+d+o)):e.bottom>t.bottom&&(d=e.bottom-t.bottom+o,i<0&&e.top-d0&&e.right>t.right+f&&(f=e.right-t.right+f+r)):e.right>t.right&&(f=e.right-t.right+r,i<0&&e.left0&&i<=0)e=ce(t=t.childNodes[e-1]);else{if(!(1==t.nodeType&&e=0))return null;t=t.childNodes[e],e=0}}}class ln{constructor(){this.changes=[]}compareRange(t,e){li(t,e,this.changes)}comparePoint(t,e){li(t,e,this.changes)}}function an(t,e){return e.left>t?e.left-t:Math.max(0,t-e.right)}function hn(t,e){return e.top>t?e.top-t:Math.max(0,t-e.bottom)}function cn(t,e){return t.tope.top+1}function un(t,e){return et.bottom?{top:t.top,left:t.left,right:t.right,bottom:e}:t}function dn(t,e,i){let n,s,r,o,l,a,h,c,u=!1;for(let f=t.firstChild;f;f=f.nextSibling){let t=oe(f);for(let d=0;dg||o==g&&r>m)&&(n=f,s=p,r=m,o=g,u=!m||(m>0?d0)),0==m?i>p.bottom&&(!h||h.bottomp.top)&&(a=f,c=p):h&&cn(h,p)?h=fn(h,p.bottom):c&&cn(c,p)&&(c=un(c,p.top))}}if(h&&h.bottom>=i?(n=l,s=h):c&&c.top<=i&&(n=a,s=c),!n)return{node:t,offset:0};let f=Math.max(s.left,Math.min(s.right,e));return 3==n.nodeType?pn(n,f,i):u&&"false"!=n.contentEditable?dn(n,f,i):{node:t,offset:Array.prototype.indexOf.call(t.childNodes,n)+(e>=(s.left+s.right)/2?1:0)}}function pn(t,e,i){let n=t.nodeValue.length,s=-1,r=1e9,o=0;for(let l=0;li?h.top-i:i-h.bottom)-1;if(h.left-1<=e&&h.right+1>=e&&c=(h.left+h.right)/2,n=i;if(ze.chrome||ze.gecko){we(t,l).getBoundingClientRect().left==h.right&&(n=!i)}if(c<=0)return{node:t,offset:l+(n?1:0)};s=l+(n?1:0),r=c}}}return{node:t,offset:s>-1?s:o>0?t.nodeValue.length:0}}function mn(t,{x:e,y:i},n,s=-1){var r;let o,l=t.contentDOM.getBoundingClientRect(),a=l.top+t.viewState.paddingTop,{docHeight:h}=t.viewState,c=i-a;if(c<0)return 0;if(c>h)return t.state.doc.length;for(let e=t.defaultLineHeight/2,i=!1;o=t.elementAtHeight(c),o.type!=ei.Text;)for(;c=s>0?o.bottom+e:o.top-e,!(c>=0&&c<=h);){if(i)return n?null:0;i=!0,s=-s}i=a+c;let u=o.from;if(ut.viewport.to)return t.viewport.to==t.state.doc.length?t.state.doc.length:n?null:gn(t,l,o,e,i);let f=t.dom.ownerDocument,d=t.root.elementFromPoint?t.root:f,p=d.elementFromPoint(e,i);p&&!t.contentDOM.contains(p)&&(p=null),p||(e=Math.max(l.left+1,Math.min(l.right-1,e)),p=d.elementFromPoint(e,i),p&&!t.contentDOM.contains(p)&&(p=null));let m,g=-1;if(p&&0!=(null===(r=t.docView.nearest(p))||void 0===r?void 0:r.isEditable))if(f.caretPositionFromPoint){let t=f.caretPositionFromPoint(e,i);t&&({offsetNode:m,offset:g}=t)}else if(f.caretRangeFromPoint){let n=f.caretRangeFromPoint(e,i);n&&(({startContainer:m,startOffset:g}=n),(!t.contentDOM.contains(m)||ze.safari&&function(t,e,i){let n;if(3!=t.nodeType||e!=(n=t.nodeValue.length))return!1;for(let e=t.nextSibling;e;e=e.nextSibling)if(1!=e.nodeType||"BR"!=e.nodeName)return!1;return we(t,n-1,n).getBoundingClientRect().left>i}(m,g,e)||ze.chrome&&function(t,e,i){if(0!=e)return!1;for(let e=t;;){let t=e.parentNode;if(!t||1!=t.nodeType||t.firstChild!=e)return!1;if(t.classList.contains("cm-line"))break;e=t}let n=1==t.nodeType?t.getBoundingClientRect():we(t,0,Math.max(t.nodeValue.length,1)).getBoundingClientRect();return i-n.left>5}(m,g,e))&&(m=void 0))}if(!m||!t.docView.dom.contains(m)){let n=ai.find(t.docView,u);if(!n)return c>o.top+o.height/2?o.to:o.from;({node:m,offset:g}=dn(n.dom,e,i))}return t.docView.posFromDOM(m,g)}function gn(t,e,i,n,s){let r=Math.round((n-e.left)*t.defaultCharacterWidth);if(t.lineWrapping&&i.height>1.5*t.defaultLineHeight){r+=Math.floor((s-i.top)/t.defaultLineHeight)*t.viewState.heightOracle.lineLength}let o=t.state.sliceDoc(i.from,i.to);return i.from+qt(o,r,t.state.tabSize)}function vn(t,e,i,n){let s=t.state.doc.lineAt(e.head),r=t.bidiSpans(s),o=t.textDirectionAt(s.from);for(let l=e,a=null;;){let e=Ji(s,r,o,l,i),h=Gi;if(!e){if(s.number==(i?t.state.doc.lines:1))return l;h="\n",s=t.state.doc.line(s.number+(i?1:-1)),r=t.bidiSpans(s),e=E.cursor(i?s.from:s.to)}if(a){if(!a(h))return l}else{if(!n)return e;a=n(h)}l=e}}function wn(t,e,i){let n=t.state.facet(Ei).map((e=>e(t)));for(;;){let t=!1;for(let s of n)s.between(i.from-1,i.from+1,((n,s,r)=>{i.from>n&&i.fromi.from?E.cursor(n,1):E.cursor(s,-1),t=!0)}));if(!t)return i}}class yn{constructor(t){this.lastKeyCode=0,this.lastKeyTime=0,this.lastTouchTime=0,this.lastFocusTime=0,this.lastScrollTop=0,this.lastScrollLeft=0,this.chromeScrollHack=-1,this.pendingIOSKey=void 0,this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastEscPress=0,this.lastContextMenu=0,this.scrollHandlers=[],this.registeredEvents=[],this.customHandlers=[],this.composing=-1,this.compositionFirstChange=null,this.compositionEndedAt=0,this.mouseSelection=null;for(let e in An){let i=An[e];t.contentDOM.addEventListener(e,(n=>{Cn(t,n)&&!this.ignoreDuringComposition(n)&&("keydown"==e&&this.keydown(t,n)||(this.mustFlushObserver(n)&&t.observer.forceFlush(),this.runCustomHandlers(e,t,n)?n.preventDefault():i(t,n)))}),On[e]),this.registeredEvents.push(e)}ze.chrome&&102==ze.chrome_version&&t.scrollDOM.addEventListener("wheel",(()=>{this.chromeScrollHack<0?t.contentDOM.style.pointerEvents="none":window.clearTimeout(this.chromeScrollHack),this.chromeScrollHack=setTimeout((()=>{this.chromeScrollHack=-1,t.contentDOM.style.pointerEvents=""}),100)}),{passive:!0}),this.notifiedFocused=t.hasFocus,ze.safari&&t.contentDOM.addEventListener("input",(()=>null))}setSelectionOrigin(t){this.lastSelectionOrigin=t,this.lastSelectionTime=Date.now()}ensureHandlers(t,e){var i;let n;this.customHandlers=[];for(let s of e)if(n=null===(i=s.update(t).spec)||void 0===i?void 0:i.domEventHandlers){this.customHandlers.push({plugin:s.value,handlers:n});for(let e in n)this.registeredEvents.indexOf(e)<0&&"scroll"!=e&&(this.registeredEvents.push(e),t.contentDOM.addEventListener(e,(i=>{Cn(t,i)&&this.runCustomHandlers(e,t,i)&&i.preventDefault()})))}}runCustomHandlers(t,e,i){for(let n of this.customHandlers){let s=n.handlers[t];if(s)try{if(s.call(n.plugin,i,e)||i.defaultPrevented)return!0}catch(t){Si(e.state,t)}}return!1}runScrollHandlers(t,e){this.lastScrollTop=t.scrollDOM.scrollTop,this.lastScrollLeft=t.scrollDOM.scrollLeft;for(let i of this.customHandlers){let n=i.handlers.scroll;if(n)try{n.call(i.plugin,e,t)}catch(e){Si(t.state,e)}}}keydown(t,e){if(this.lastKeyCode=e.keyCode,this.lastKeyTime=Date.now(),9==e.keyCode&&Date.now()t.keyCode==e.keyCode)))&&!e.ctrlKey||xn.indexOf(e.key)>-1&&e.ctrlKey&&!e.shiftKey))&&(this.pendingIOSKey=i||e,setTimeout((()=>this.flushIOSKey(t)),250),!0)}flushIOSKey(t){let e=this.pendingIOSKey;return!!e&&(this.pendingIOSKey=void 0,ye(t.contentDOM,e.key,e.keyCode))}ignoreDuringComposition(t){return!!/^key/.test(t.type)&&(this.composing>0||!!(ze.safari&&!ze.ios&&Date.now()-this.compositionEndedAt<100)&&(this.compositionEndedAt=0,!0))}mustFlushObserver(t){return"keydown"==t.type&&229!=t.keyCode}startMouseSelection(t){this.mouseSelection&&this.mouseSelection.destroy(),this.mouseSelection=t}update(t){this.mouseSelection&&this.mouseSelection.update(t),t.transactions.length&&(this.lastKeyCode=this.lastSelectionTime=0)}destroy(){this.mouseSelection&&this.mouseSelection.destroy()}}const bn=[{key:"Backspace",keyCode:8,inputType:"deleteContentBackward"},{key:"Enter",keyCode:13,inputType:"insertParagraph"},{key:"Delete",keyCode:46,inputType:"deleteContentForward"}],xn="dthko",kn=[16,17,18,20,91,92,224,225];class Sn{constructor(t,e,i,n){this.view=t,this.style=i,this.mustSelect=n,this.lastEvent=e;let s=t.contentDOM.ownerDocument;s.addEventListener("mousemove",this.move=this.move.bind(this)),s.addEventListener("mouseup",this.up=this.up.bind(this)),this.extend=e.shiftKey,this.multiple=t.state.facet(St.allowMultipleSelections)&&function(t,e){let i=t.state.facet(di);return i.length?i[0](e):ze.mac?e.metaKey:e.ctrlKey}(t,e),this.dragMove=function(t,e){let i=t.state.facet(pi);return i.length?i[0](e):ze.mac?!e.altKey:!e.ctrlKey}(t,e),this.dragging=!(!function(t,e){let{main:i}=t.state.selection;if(i.empty)return!1;let n=ne(t.root);if(!n||0==n.rangeCount)return!0;let s=n.getRangeAt(0).getClientRects();for(let t=0;t=e.clientX&&i.top<=e.clientY&&i.bottom>=e.clientY)return!0}return!1}(t,e)||1!=Wn(e))&&null,!1===this.dragging&&(e.preventDefault(),this.select(e))}move(t){if(0==t.buttons)return this.destroy();!1===this.dragging&&this.select(this.lastEvent=t)}up(t){null==this.dragging&&this.select(this.lastEvent),this.dragging||t.preventDefault(),this.destroy()}destroy(){let t=this.view.contentDOM.ownerDocument;t.removeEventListener("mousemove",this.move),t.removeEventListener("mouseup",this.up),this.view.inputState.mouseSelection=null}select(t){let e=this.style.get(t,this.extend,this.multiple);!this.mustSelect&&e.eq(this.view.state.selection)&&e.main.assoc==this.view.state.selection.main.assoc||this.view.dispatch({selection:e,userEvent:"select.pointer",scrollIntoView:!0}),this.mustSelect=!1}update(t){t.docChanged&&this.dragging&&(this.dragging=this.dragging.map(t.changes)),this.style.update(t)&&setTimeout((()=>this.select(this.lastEvent)),20)}}function Cn(t,e){if(!e.bubbles)return!0;if(e.defaultPrevented)return!1;for(let i,n=e.target;n!=t.contentDOM;n=n.parentNode)if(!n||11==n.nodeType||(i=Se.get(n))&&i.ignoreEvent(e))return!1;return!0}const An=Object.create(null),On=Object.create(null),Mn=ze.ie&&ze.ie_version<15||ze.ios&&ze.webkit_version<604;function Dn(t,e){let i,{state:n}=t,s=1,r=n.toText(e),o=r.lines==n.selection.ranges.length;if(null!=Hn&&n.selection.ranges.every((t=>t.empty))&&Hn==r.toString()){let t=-1;i=n.changeByRange((i=>{let l=n.doc.lineAt(i.from);if(l.from==t)return{range:i};t=l.from;let a=n.toText((o?r.line(s++).text:e)+n.lineBreak);return{changes:{from:l.from,insert:a},range:E.cursor(i.from+a.length)}}))}else i=o?n.changeByRange((t=>{let e=r.line(s++);return{changes:{from:t.from,to:t.to,insert:e.text},range:E.cursor(t.from+e.length)}})):n.replaceSelection(r);t.dispatch(i,{userEvent:"input.paste",scrollIntoView:!0})}function Tn(t,e,i,n){if(1==n)return E.cursor(e,i);if(2==n)return function(t,e,i=1){let n=t.charCategorizer(e),s=t.doc.lineAt(e),r=e-s.from;if(0==s.length)return E.cursor(e);0==r?i=1:r==s.length&&(i=-1);let o=r,l=r;i<0?o=d(s.text,r,!1):l=d(s.text,r);let a=n(s.text.slice(o,l));for(;o>0;){let t=d(s.text,o,!1);if(n(s.text.slice(t,o))!=a)break;o=t}for(;l{t.inputState.setSelectionOrigin("select"),27==e.keyCode?t.inputState.lastEscPress=Date.now():kn.indexOf(e.keyCode)<0&&(t.inputState.lastEscPress=0)},An.touchstart=(t,e)=>{t.inputState.lastTouchTime=Date.now(),t.inputState.setSelectionOrigin("select.pointer")},An.touchmove=t=>{t.inputState.setSelectionOrigin("select.pointer")},On.touchstart=On.touchmove={passive:!0},An.mousedown=(t,e)=>{if(t.observer.flush(),t.inputState.lastTouchTime>Date.now()-2e3)return;let i=null;for(let n of t.state.facet(mi))if(i=n(t,e),i)break;if(i||0!=e.button||(i=function(t,e){let i=Bn(t,e),n=Wn(e),s=t.state.selection,r=i,o=e;return{update(t){t.docChanged&&(i.pos=t.changes.mapPos(i.pos),s=s.map(t.changes),o=null)},get(e,l,a){let h;o&&e.clientX==o.clientX&&e.clientY==o.clientY?h=r:(h=r=Bn(t,e),o=e);let c=Tn(t,h.pos,h.bias,n);if(i.pos!=h.pos&&!l){let e=Tn(t,i.pos,i.bias,n),s=Math.min(e.from,c.from),r=Math.max(e.to,c.to);c=s1&&s.ranges.some((t=>t.eq(c)))?function(t,e){for(let i=0;;i++)if(t.ranges[i].eq(e))return E.create(t.ranges.slice(0,i).concat(t.ranges.slice(i+1)),t.mainIndex==i?0:t.mainIndex-(t.mainIndex>i?1:0))}(s,c):a?s.addRange(c):E.create([c])}}}(t,e)),i){let n=t.root.activeElement!=t.contentDOM;n&&t.observer.ignore((()=>ve(t.contentDOM))),t.inputState.startMouseSelection(new Sn(t,e,i,n))}};let Pn=(t,e)=>t>=e.top&&t<=e.bottom,Rn=(t,e,i)=>Pn(e,i)&&t>=i.left&&t<=i.right;function En(t,e,i,n){let s=ai.find(t.docView,e);if(!s)return 1;let r=e-s.posAtStart;if(0==r)return 1;if(r==s.length)return-1;let o=s.coordsAt(r,-1);if(o&&Rn(i,n,o))return-1;let l=s.coordsAt(r,1);return l&&Rn(i,n,l)?1:o&&Pn(n,o)?-1:1}function Bn(t,e){let i=t.posAtCoords({x:e.clientX,y:e.clientY},!1);return{pos:i,bias:En(t,i,e.clientX,e.clientY)}}const Ln=ze.ie&&ze.ie_version<=11;let Nn=null,In=0,Vn=0;function Wn(t){if(!Ln)return t.detail;let e=Nn,i=Vn;return Nn=t,Vn=Date.now(),In=!e||i>Date.now()-400&&Math.abs(e.clientX-t.clientX)<2&&Math.abs(e.clientY-t.clientY)<2?(In+1)%3:1}function zn(t,e,i,n){if(!i)return;let s=t.posAtCoords({x:e.clientX,y:e.clientY},!1);e.preventDefault();let{mouseSelection:r}=t.inputState,o=n&&r&&r.dragging&&r.dragMove?{from:r.dragging.from,to:r.dragging.to}:null,l={from:s,insert:i},a=t.state.changes(o?[o,l]:l);t.focus(),t.dispatch({changes:a,selection:{anchor:a.mapPos(s,-1),head:a.mapPos(s,1)},userEvent:o?"move.drop":"input.drop"})}An.dragstart=(t,e)=>{let{selection:{main:i}}=t.state,{mouseSelection:n}=t.inputState;n&&(n.dragging=i),e.dataTransfer&&(e.dataTransfer.setData("Text",t.state.sliceDoc(i.from,i.to)),e.dataTransfer.effectAllowed="copyMove")},An.drop=(t,e)=>{if(!e.dataTransfer)return;if(t.state.readOnly)return e.preventDefault();let i=e.dataTransfer.files;if(i&&i.length){e.preventDefault();let n=Array(i.length),s=0,r=()=>{++s==i.length&&zn(t,e,n.filter((t=>null!=t)).join(t.state.lineBreak),!1)};for(let t=0;t{/[\x00-\x08\x0e-\x1f]{2}/.test(e.result)||(n[t]=e.result),r()},e.readAsText(i[t])}}else zn(t,e,e.dataTransfer.getData("Text"),!0)},An.paste=(t,e)=>{if(t.state.readOnly)return e.preventDefault();t.observer.flush();let i=Mn?null:e.clipboardData;i?(Dn(t,i.getData("text/plain")),e.preventDefault()):function(t){let e=t.dom.parentNode;if(!e)return;let i=e.appendChild(document.createElement("textarea"));i.style.cssText="position: fixed; left: -10000px; top: 10px",i.focus(),setTimeout((()=>{t.focus(),i.remove(),Dn(t,i.value)}),50)}(t)};let Hn=null;function Fn(t){setTimeout((()=>{t.hasFocus!=t.inputState.notifiedFocused&&t.update([])}),10)}An.copy=An.cut=(t,e)=>{let{text:i,ranges:n,linewise:s}=function(t){let e=[],i=[],n=!1;for(let n of t.selection.ranges)n.empty||(e.push(t.sliceDoc(n.from,n.to)),i.push(n));if(!e.length){let s=-1;for(let{from:n}of t.selection.ranges){let r=t.doc.lineAt(n);r.number>s&&(e.push(r.text),i.push({from:r.from,to:Math.min(t.doc.length,r.to+1)})),s=r.number}n=!0}return{text:e.join(t.lineBreak),ranges:i,linewise:n}}(t.state);if(!i&&!s)return;Hn=s?i:null;let r=Mn?null:e.clipboardData;r?(e.preventDefault(),r.clearData(),r.setData("text/plain",i)):function(t,e){let i=t.dom.parentNode;if(!i)return;let n=i.appendChild(document.createElement("textarea"));n.style.cssText="position: fixed; left: -10000px; top: 10px",n.value=e,n.focus(),n.selectionEnd=e.length,n.selectionStart=0,setTimeout((()=>{n.remove(),t.focus()}),50)}(t,i),"cut"!=e.type||t.state.readOnly||t.dispatch({changes:n,scrollIntoView:!0,userEvent:"delete.cut"})},An.focus=t=>{t.inputState.lastFocusTime=Date.now(),t.scrollDOM.scrollTop||!t.inputState.lastScrollTop&&!t.inputState.lastScrollLeft||(t.scrollDOM.scrollTop=t.inputState.lastScrollTop,t.scrollDOM.scrollLeft=t.inputState.lastScrollLeft),Fn(t)},An.blur=t=>{t.observer.clearSelectionRange(),Fn(t)},An.compositionstart=An.compositionupdate=t=>{null==t.inputState.compositionFirstChange&&(t.inputState.compositionFirstChange=!0),t.inputState.composing<0&&(t.inputState.composing=0)},An.compositionend=t=>{t.inputState.composing=-1,t.inputState.compositionEndedAt=Date.now(),t.inputState.compositionFirstChange=null,ze.chrome&&ze.android&&t.observer.flushSoon(),setTimeout((()=>{t.inputState.composing<0&&t.docView.compositionDeco.size&&t.update([])}),50)},An.contextmenu=t=>{t.inputState.lastContextMenu=Date.now()},An.beforeinput=(t,e)=>{var i;let n;if(ze.chrome&&ze.android&&(n=bn.find((t=>t.inputType==e.inputType)))&&(t.observer.delayAndroidKey(n.key,n.keyCode),"Backspace"==n.key||"Delete"==n.key)){let e=(null===(i=window.visualViewport)||void 0===i?void 0:i.height)||0;setTimeout((()=>{var i;((null===(i=window.visualViewport)||void 0===i?void 0:i.height)||0)>e+10&&t.hasFocus&&(t.contentDOM.blur(),t.focus())}),100)}};const qn=["pre-wrap","normal","pre-line","break-spaces"];class _n{constructor(t){this.lineWrapping=t,this.doc=e.empty,this.heightSamples={},this.lineHeight=14,this.charWidth=7,this.lineLength=30,this.heightChanged=!1}heightForGap(t,e){let i=this.doc.lineAt(e).number-this.doc.lineAt(t).number+1;return this.lineWrapping&&(i+=Math.ceil((e-t-i*this.lineLength*.5)/this.lineLength)),this.lineHeight*i}heightForLine(t){if(!this.lineWrapping)return this.lineHeight;return(1+Math.max(0,Math.ceil((t-this.lineLength)/(this.lineLength-5))))*this.lineHeight}setDoc(t){return this.doc=t,this}mustRefreshForWrapping(t){return qn.indexOf(t)>-1!=this.lineWrapping}mustRefreshForHeights(t){let e=!1;for(let i=0;i-1,o=Math.round(e)!=Math.round(this.lineHeight)||this.lineWrapping!=r;if(this.lineWrapping=r,this.lineHeight=e,this.charWidth=i,this.lineLength=n,o){this.heightSamples={};for(let t=0;t0}set outdated(t){this.flags=(t?2:0)|-3&this.flags}setHeight(t,e){this.height!=e&&(Math.abs(this.height-e)>Qn&&(t.heightChanged=!0),this.height=e)}replace(t,e,i){return Kn.of(i)}decomposeLeft(t,e){e.push(this)}decomposeRight(t,e){e.push(this)}applyChanges(t,e,i,n){let s=this;for(let r=n.length-1;r>=0;r--){let{fromA:o,toA:l,fromB:a,toB:h}=n[r],c=s.lineAt(o,$n.ByPosNoHeight,e,0,0),u=c.to>=l?c:s.lineAt(l,$n.ByPosNoHeight,e,0,0);for(h+=u.to-l,l=u.to;r>0&&c.from<=n[r-1].toA;)o=n[r-1].fromA,a=n[r-1].fromB,r--,o2*s){let s=t[e-1];s.break?t.splice(--e,1,s.left,null,s.right):t.splice(--e,1,s.left,s.right),i+=1+s.break,n-=s.size}else{if(!(s>2*n))break;{let e=t[i];e.break?t.splice(i,1,e.left,null,e.right):t.splice(i,1,e.left,e.right),i+=2+e.break,s-=e.size}}else if(n=s&&r(this.blockAt(0,i,n,s))}updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more&&this.setHeight(t,n.heights[n.index++]),this.outdated=!1,this}toString(){return`block(${this.length})`}}class Jn extends Gn{constructor(t,e){super(t,e,ei.Text),this.collapsed=0,this.widgetHeight=0}replace(t,e,i){let n=i[0];return 1==i.length&&(n instanceof Jn||n instanceof Xn&&4&n.flags)&&Math.abs(this.length-n.length)<10?(n instanceof Xn?n=new Jn(n.length,this.height):n.height=this.height,this.outdated||(n.outdated=!1),n):Kn.of(i)}updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more?this.setHeight(t,n.heights[n.index++]):(i||this.outdated)&&this.setHeight(t,Math.max(this.widgetHeight,t.heightForLine(this.length-this.collapsed))),this.outdated=!1,this}toString(){return`line(${this.length}${this.collapsed?-this.collapsed:""}${this.widgetHeight?":"+this.widgetHeight:""})`}}class Xn extends Kn{constructor(t){super(t,0)}lines(t,e){let i=t.lineAt(e).number,n=t.lineAt(e+this.length).number;return{firstLine:i,lastLine:n,lineHeight:this.height/(n-i+1)}}blockAt(t,e,i,n){let{firstLine:s,lastLine:r,lineHeight:o}=this.lines(e,n),l=Math.max(0,Math.min(r-s,Math.floor((t-i)/o))),{from:a,length:h}=e.line(s+l);return new Un(a,h,i+o*l,o,ei.Text)}lineAt(t,e,i,n,s){if(e==$n.ByHeight)return this.blockAt(t,i,n,s);if(e==$n.ByPosNoHeight){let{from:e,to:n}=i.lineAt(t);return new Un(e,n-e,0,0,ei.Text)}let{firstLine:r,lineHeight:o}=this.lines(i,s),{from:l,length:a,number:h}=i.lineAt(t);return new Un(l,a,n+o*(h-r),o,ei.Text)}forEachLine(t,e,i,n,s,r){let{firstLine:o,lineHeight:l}=this.lines(i,s);for(let a=Math.max(t,s),h=Math.min(s+this.length,e);a<=h;){let e=i.lineAt(a);a==t&&(n+=l*(e.number-o)),r(new Un(e.from,e.length,n,l,ei.Text)),n+=l,a=e.to+1}}replace(t,e,i){let n=this.length-e;if(n>0){let t=i[i.length-1];t instanceof Xn?i[i.length-1]=new Xn(t.length+n):i.push(null,new Xn(n-1))}if(t>0){let e=i[0];e instanceof Xn?i[0]=new Xn(t+e.length):i.unshift(new Xn(t-1),null)}return Kn.of(i)}decomposeLeft(t,e){e.push(new Xn(t-1),null)}decomposeRight(t,e){e.push(null,new Xn(this.length-t-1))}updateHeight(t,e=0,i=!1,n){let s=e+this.length;if(n&&n.from<=e+this.length&&n.more){let i=[],r=Math.max(e,n.from),o=-1,l=t.heightChanged;for(n.from>e&&i.push(new Xn(n.from-e-1).updateHeight(t,e));r<=s&&n.more;){let e=t.doc.lineAt(r).length;i.length&&i.push(null);let s=n.heights[n.index++];-1==o?o=s:Math.abs(s-o)>=Qn&&(o=-2);let l=new Jn(e,s);l.outdated=!1,i.push(l),r+=e+1}r<=s&&i.push(null,new Xn(s-r).updateHeight(t,r));let a=Kn.of(i);return t.heightChanged=l||o<0||Math.abs(a.height-this.height)>=Qn||Math.abs(o-this.lines(t.doc,e).lineHeight)>=Qn,a}return(i||this.outdated)&&(this.setHeight(t,t.heightForGap(e,e+this.length)),this.outdated=!1),this}toString(){return`gap(${this.length})`}}class Zn extends Kn{constructor(t,e,i){super(t.length+e+i.length,t.height+i.height,e|(t.outdated||i.outdated?2:0)),this.left=t,this.right=i,this.size=t.size+i.size}get break(){return 1&this.flags}blockAt(t,e,i,n){let s=i+this.left.height;return to))return a;let h=e==$n.ByPosNoHeight?$n.ByPosNoHeight:$n.ByPos;return l?a.join(this.right.lineAt(o,h,i,r,o)):this.left.lineAt(o,h,i,n,s).join(a)}forEachLine(t,e,i,n,s,r){let o=n+this.left.height,l=s+this.left.length+this.break;if(this.break)t=l&&this.right.forEachLine(t,e,i,o,l,r);else{let a=this.lineAt(l,$n.ByPos,i,n,s);t=t&&a.from<=e&&r(a),e>a.to&&this.right.forEachLine(a.to+1,e,i,o,l,r)}}replace(t,e,i){let n=this.left.length+this.break;if(ethis.left.length)return this.balanced(this.left,this.right.replace(t-n,e-n,i));let s=[];t>0&&this.decomposeLeft(t,s);let r=s.length;for(let t of i)s.push(t);if(t>0&&Yn(s,r-1),e=i&&e.push(null)),t>i&&this.right.decomposeLeft(t-i,e)}decomposeRight(t,e){let i=this.left.length,n=i+this.break;if(t>=n)return this.right.decomposeRight(t-n,e);t2*e.size||e.size>2*t.size?Kn.of(this.break?[t,null,e]:[t,e]):(this.left=t,this.right=e,this.height=t.height+e.height,this.outdated=t.outdated||e.outdated,this.size=t.size+e.size,this.length=t.length+this.break+e.length,this)}updateHeight(t,e=0,i=!1,n){let{left:s,right:r}=this,o=e+s.length+this.break,l=null;return n&&n.from<=e+s.length&&n.more?l=s=s.updateHeight(t,e,i,n):s.updateHeight(t,e,i),n&&n.from<=o+r.length&&n.more?l=r=r.updateHeight(t,o,i,n):r.updateHeight(t,o,i),l?this.balanced(s,r):(this.height=this.left.height+this.right.height,this.outdated=!1,this)}toString(){return this.left+(this.break?" ":"-")+this.right}}function Yn(t,e){let i,n;null==t[e]&&(i=t[e-1])instanceof Xn&&(n=t[e+1])instanceof Xn&&t.splice(e-1,3,new Xn(i.length+1+n.length))}class ts{constructor(t,e){this.pos=t,this.oracle=e,this.nodes=[],this.lineStart=-1,this.lineEnd=-1,this.covering=null,this.writtenTo=t}get isCovered(){return this.covering&&this.nodes[this.nodes.length-1]==this.covering}span(t,e){if(this.lineStart>-1){let t=Math.min(e,this.lineEnd),i=this.nodes[this.nodes.length-1];i instanceof Jn?i.length+=t-this.pos:(t>this.pos||!this.isCovered)&&this.nodes.push(new Jn(t-this.pos,-1)),this.writtenTo=t,e>t&&(this.nodes.push(null),this.writtenTo++,this.lineStart=-1)}this.pos=e}point(t,e,i){if(t=5)&&this.addLineDeco(n,s)}else e>t&&this.span(t,e);this.lineEnd>-1&&this.lineEnd-1)return;let{from:t,to:e}=this.oracle.doc.lineAt(this.pos);this.lineStart=t,this.lineEnd=e,this.writtenTot&&this.nodes.push(new Jn(this.pos-t,-1)),this.writtenTo=this.pos}blankContent(t,e){let i=new Xn(e-t);return this.oracle.doc.lineAt(t).to==e&&(i.flags|=4),i}ensureLine(){this.enterLine();let t=this.nodes.length?this.nodes[this.nodes.length-1]:null;if(t instanceof Jn)return t;let e=new Jn(0,-1);return this.nodes.push(e),e}addBlock(t){this.enterLine(),t.type!=ei.WidgetAfter||this.isCovered||this.ensureLine(),this.nodes.push(t),this.writtenTo=this.pos=this.pos+t.length,t.type!=ei.WidgetBefore&&(this.covering=t)}addLineDeco(t,e){let i=this.ensureLine();i.length+=e,i.collapsed+=e,i.widgetHeight=Math.max(i.widgetHeight,t),this.writtenTo=this.pos=this.pos+e}finish(t){let e=0==this.nodes.length?null:this.nodes[this.nodes.length-1];!(this.lineStart>-1)||e instanceof Jn||this.isCovered?(this.writtenToi.clientHeight||i.scrollWidth>i.clientWidth)&&"visible"!=n.overflow){let n=i.getBoundingClientRect();r=Math.max(r,n.left),o=Math.min(o,n.right),l=Math.max(l,n.top),a=e==t.parentNode?n.bottom:Math.min(a,n.bottom)}e="absolute"==n.position||"fixed"==n.position?i.offsetParent:i.parentNode}else{if(11!=e.nodeType)break;e=e.host}return{left:r-i.left,right:Math.max(r,o)-i.left,top:l-(i.top+e),bottom:Math.max(l,a)-(i.top+e)}}function ns(t,e){let i=t.getBoundingClientRect();return{left:0,right:i.right-i.left,top:e,bottom:i.bottom-(i.top+e)}}class ss{constructor(t,e,i){this.from=t,this.to=e,this.size=i}static same(t,e){if(t.length!=e.length)return!1;for(let i=0;i"function"!=typeof t&&"cm-lineWrapping"==t.class));this.heightOracle=new _n(i),this.stateDeco=t.facet(Ri).filter((t=>"function"!=typeof t)),this.heightMap=Kn.empty().applyChanges(this.stateDeco,e.empty,this.heightOracle.setDoc(t.doc),[new Ni(0,0,0,t.doc.length)]),this.viewport=this.getViewport(0,null),this.updateViewportLines(),this.updateForViewport(),this.lineGaps=this.ensureLineGaps([]),this.lineGapDeco=ii.set(this.lineGaps.map((t=>t.draw(!1)))),this.computeVisibleRanges()}updateForViewport(){let t=[this.viewport],{main:e}=this.state.selection;for(let i=0;i<=1;i++){let n=i?e.head:e.anchor;if(!t.some((({from:t,to:e})=>n>=t&&n<=e))){let{from:e,to:i}=this.lineBlockAt(n);t.push(new ls(e,i))}}this.viewports=t.sort(((t,e)=>t.from-e.from)),this.scaler=this.heightMap.height<=7e6?us:new fs(this.heightOracle.doc,this.heightMap,this.viewports)}updateViewportLines(){this.viewportLines=[],this.heightMap.forEachLine(this.viewport.from,this.viewport.to,this.state.doc,0,0,(t=>{this.viewportLines.push(1==this.scaler.scale?t:ds(t,this.scaler))}))}update(t,e=null){this.state=t.state;let i=this.stateDeco;this.stateDeco=this.state.facet(Ri).filter((t=>"function"!=typeof t));let n=t.changedRanges,s=Ni.extendWithRanges(n,function(t,e,i){let n=new es;return Tt.compare(t,e,i,n,0),n.changes}(i,this.stateDeco,t?t.changes:C.empty(this.state.doc.length))),r=this.heightMap.height;this.heightMap=this.heightMap.applyChanges(this.stateDeco,t.startState.doc,this.heightOracle.setDoc(this.state.doc),s),this.heightMap.height!=r&&(t.flags|=2);let o=s.length?this.mapViewport(this.viewport,t.changes):this.viewport;(e&&(e.range.heado.to)||!this.viewportIsAppropriate(o))&&(o=this.getViewport(0,e));let l=!t.changes.empty||2&t.flags||o.from!=this.viewport.from||o.to!=this.viewport.to;this.viewport=o,this.updateForViewport(),l&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps,t.changes))),t.flags|=this.computeVisibleRanges(),e&&(this.scrollTarget=e),!this.mustEnforceCursorAssoc&&t.selectionSet&&t.view.lineWrapping&&t.state.selection.main.empty&&t.state.selection.main.assoc&&!t.state.facet(bi)&&(this.mustEnforceCursorAssoc=!0)}measure(t){let i=t.contentDOM,n=window.getComputedStyle(i),s=this.heightOracle,r=n.whiteSpace;this.defaultTextDirection="rtl"==n.direction?Vi.RTL:Vi.LTR;let o=this.heightOracle.mustRefreshForWrapping(r),l=o||this.mustMeasureContent||this.contentDOMHeight!=i.clientHeight;this.contentDOMHeight=i.clientHeight,this.mustMeasureContent=!1;let a=0,h=0,c=parseInt(n.paddingTop)||0,u=parseInt(n.paddingBottom)||0;this.paddingTop==c&&this.paddingBottom==u||(this.paddingTop=c,this.paddingBottom=u,a|=10),this.editorWidth!=t.scrollDOM.clientWidth&&(s.lineWrapping&&(l=!0),this.editorWidth=t.scrollDOM.clientWidth,a|=8);let f=(this.printing?ns:is)(i,this.paddingTop),d=f.top-this.pixelViewport.top,p=f.bottom-this.pixelViewport.bottom;this.pixelViewport=f;let m=this.pixelViewport.bottom>this.pixelViewport.top&&this.pixelViewport.right>this.pixelViewport.left;if(m!=this.inView&&(this.inView=m,m&&(l=!0)),!this.inView&&!this.scrollTarget)return 0;let g=i.clientWidth;if(this.contentDOMWidth==g&&this.editorHeight==t.scrollDOM.clientHeight||(this.contentDOMWidth=g,this.editorHeight=t.scrollDOM.clientHeight,a|=8),l){let i=t.docView.measureVisibleLineHeights(this.viewport);if(s.mustRefreshForHeights(i)&&(o=!0),o||s.lineWrapping&&Math.abs(g-this.contentDOMWidth)>s.charWidth){let{lineHeight:e,charWidth:n}=t.docView.measureTextSize();o=e>0&&s.refresh(r,e,n,g/n,i),o&&(t.docView.minWidth=0,a|=8)}d>0&&p>0?h=Math.max(d,p):d<0&&p<0&&(h=Math.min(d,p)),s.heightChanged=!1;for(let n of this.viewports){let r=n.from==this.viewport.from?i:t.docView.measureVisibleLineHeights(n);this.heightMap=(o?Kn.empty().applyChanges(this.stateDeco,e.empty,this.heightOracle,[new Ni(0,0,0,t.state.doc.length)]):this.heightMap).updateHeight(s,0,o,new jn(n.from,r))}s.heightChanged&&(a|=2)}let v=!this.viewportIsAppropriate(this.viewport,h)||this.scrollTarget&&(this.scrollTarget.range.headthis.viewport.to);return v&&(this.viewport=this.getViewport(h,this.scrollTarget)),this.updateForViewport(),(2&a||v)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(o?[]:this.lineGaps,t)),a|=this.computeVisibleRanges(),this.mustEnforceCursorAssoc&&(this.mustEnforceCursorAssoc=!1,t.docView.enforceCursorAssoc()),a}get visibleTop(){return this.scaler.fromDOM(this.pixelViewport.top)}get visibleBottom(){return this.scaler.fromDOM(this.pixelViewport.bottom)}getViewport(t,e){let i=.5-Math.max(-.5,Math.min(.5,t/1e3/2)),n=this.heightMap,s=this.state.doc,{visibleTop:r,visibleBottom:o}=this,l=new ls(n.lineAt(r-1e3*i,$n.ByHeight,s,0,0).from,n.lineAt(o+1e3*(1-i),$n.ByHeight,s,0,0).to);if(e){let{head:t}=e.range;if(tl.to){let i,r=Math.min(this.editorHeight,this.pixelViewport.bottom-this.pixelViewport.top),o=n.lineAt(t,$n.ByPos,s,0,0);i="center"==e.y?(o.top+o.bottom)/2-r/2:"start"==e.y||"nearest"==e.y&&t=o+Math.max(10,Math.min(i,250)))&&n>r-2e3&&s>1,r=n<<1;if(this.defaultTextDirection!=Vi.LTR&&!i)return[];let o=[],l=(n,r,a,h)=>{if(r-nn&&tt.from>=a.from&&t.to<=a.to&&Math.abs(t.from-n)t.frome))));if(!f){if(rt.from<=r&&t.to>=r))){let t=e.moveToLineBoundary(E.cursor(r),!1,!0).head;t>n&&(r=t)}f=new ss(n,r,this.gapSize(a,n,r,h))}o.push(f)};for(let t of this.viewportLines){if(t.lengtht.from&&l(t.from,s,t,e),ot.draw(this.heightOracle.lineWrapping)))))}computeVisibleRanges(){let t=this.stateDeco;this.lineGaps.length&&(t=t.concat(this.lineGapDeco));let e=[];Tt.spans(t,this.viewport.from,this.viewport.to,{span(t,i){e.push({from:t,to:i})},point(){}},20);let i=e.length!=this.visibleRanges.length||this.visibleRanges.some(((t,i)=>t.from!=e[i].from||t.to!=e[i].to));return this.visibleRanges=e,i?4:0}lineBlockAt(t){return t>=this.viewport.from&&t<=this.viewport.to&&this.viewportLines.find((e=>e.from<=t&&e.to>=t))||ds(this.heightMap.lineAt(t,$n.ByPos,this.state.doc,0,0),this.scaler)}lineBlockAtHeight(t){return ds(this.heightMap.lineAt(this.scaler.fromDOM(t),$n.ByHeight,this.state.doc,0,0),this.scaler)}elementAtHeight(t){return ds(this.heightMap.blockAt(this.scaler.fromDOM(t),this.state.doc,0,0),this.scaler)}get docHeight(){return this.scaler.toDOM(this.heightMap.height)}get contentHeight(){return this.docHeight+this.paddingTop+this.paddingBottom}}class ls{constructor(t,e){this.from=t,this.to=e}}function as(t,e,i){let n=[],s=t,r=0;return Tt.spans(i,t,e,{span(){},point(t,e){t>s&&(n.push({from:s,to:t}),r+=t-s),s=e}},20),s=1)return e[e.length-1].to;let n=Math.floor(t*i);for(let t=0;;t++){let{from:i,to:s}=e[t],r=s-i;if(n<=r)return i+n;n-=r}}function cs(t,e){let i=0;for(let{from:n,to:s}of t.ranges){if(e<=s){i+=e-n;break}i+=s-n}return i/t.total}const us={toDOM:t=>t,fromDOM:t=>t,scale:1};class fs{constructor(t,e,i){let n=0,s=0,r=0;this.viewports=i.map((({from:i,to:s})=>{let r=e.lineAt(i,$n.ByPos,t,0,0).top,o=e.lineAt(s,$n.ByPos,t,0,0).bottom;return n+=o-r,{from:i,to:s,top:r,bottom:o,domTop:0,domBottom:0}})),this.scale=(7e6-n)/(e.height-n);for(let t of this.viewports)t.domTop=r+(t.top-s)*this.scale,r=t.domBottom=t.domTop+(t.bottom-t.top),s=t.bottom}toDOM(t){for(let e=0,i=0,n=0;;e++){let s=eds(t,e))):t.type)}const ps=N.define({combine:t=>t.join(" ")}),ms=N.define({combine:t=>t.indexOf(!0)>-1}),gs=$t.newName(),vs=$t.newName(),ws=$t.newName(),ys={"&light":"."+vs,"&dark":"."+ws};function bs(t,e,i){return new $t(e,{finish:e=>/&/.test(e)?e.replace(/&\w*/,(e=>{if("&"==e)return t;if(!i||!i[e])throw new RangeError(`Unsupported selector: ${e}`);return i[e]})):t+" "+e})}const xs=bs("."+gs,{"&.cm-editor":{position:"relative !important",boxSizing:"border-box","&.cm-focused":{outline:"1px dotted #212121"},display:"flex !important",flexDirection:"column"},".cm-scroller":{display:"flex !important",alignItems:"flex-start !important",fontFamily:"monospace",lineHeight:1.4,height:"100%",overflowX:"auto",position:"relative",zIndex:0},".cm-content":{margin:0,flexGrow:2,flexShrink:0,minHeight:"100%",display:"block",whiteSpace:"pre",wordWrap:"normal",boxSizing:"border-box",padding:"4px 0",outline:"none","&[contenteditable=true]":{WebkitUserModify:"read-write-plaintext-only"}},".cm-lineWrapping":{whiteSpace_fallback:"pre-wrap",whiteSpace:"break-spaces",wordBreak:"break-word",overflowWrap:"anywhere",flexShrink:1},"&light .cm-content":{caretColor:"black"},"&dark .cm-content":{caretColor:"white"},".cm-line":{display:"block",padding:"0 2px 0 4px"},".cm-selectionLayer":{zIndex:-1,contain:"size style"},".cm-selectionBackground":{position:"absolute"},"&light .cm-selectionBackground":{background:"#d9d9d9"},"&dark .cm-selectionBackground":{background:"#222"},"&light.cm-focused .cm-selectionBackground":{background:"#d7d4f0"},"&dark.cm-focused .cm-selectionBackground":{background:"#233"},".cm-cursorLayer":{zIndex:100,contain:"size style",pointerEvents:"none"},"&.cm-focused .cm-cursorLayer":{animation:"steps(1) cm-blink 1.2s infinite"},"@keyframes cm-blink":{"0%":{},"50%":{opacity:0},"100%":{}},"@keyframes cm-blink2":{"0%":{},"50%":{opacity:0},"100%":{}},".cm-cursor, .cm-dropCursor":{position:"absolute",borderLeft:"1.2px solid black",marginLeft:"-0.6px",pointerEvents:"none"},".cm-cursor":{display:"none"},"&dark .cm-cursor":{borderLeftColor:"#444"},"&.cm-focused .cm-cursor":{display:"block"},"&light .cm-activeLine":{backgroundColor:"#cceeff44"},"&dark .cm-activeLine":{backgroundColor:"#99eeff33"},"&light .cm-specialChar":{color:"red"},"&dark .cm-specialChar":{color:"#f78"},".cm-gutters":{flexShrink:0,display:"flex",height:"100%",boxSizing:"border-box",left:0,zIndex:200},"&light .cm-gutters":{backgroundColor:"#f5f5f5",color:"#6c6c6c",borderRight:"1px solid #ddd"},"&dark .cm-gutters":{backgroundColor:"#333338",color:"#ccc"},".cm-gutter":{display:"flex !important",flexDirection:"column",flexShrink:0,boxSizing:"border-box",minHeight:"100%",overflow:"hidden"},".cm-gutterElement":{boxSizing:"border-box"},".cm-lineNumbers .cm-gutterElement":{padding:"0 3px 0 5px",minWidth:"20px",textAlign:"right",whiteSpace:"nowrap"},"&light .cm-activeLineGutter":{backgroundColor:"#e2f2ff"},"&dark .cm-activeLineGutter":{backgroundColor:"#222227"},".cm-panels":{boxSizing:"border-box",position:"sticky",left:0,right:0},"&light .cm-panels":{backgroundColor:"#f5f5f5",color:"black"},"&light .cm-panels-top":{borderBottom:"1px solid #ddd"},"&light .cm-panels-bottom":{borderTop:"1px solid #ddd"},"&dark .cm-panels":{backgroundColor:"#333338",color:"white"},".cm-tab":{display:"inline-block",overflow:"hidden",verticalAlign:"bottom"},".cm-widgetBuffer":{verticalAlign:"text-top",height:"1em",width:0,display:"inline"},".cm-placeholder":{color:"#888",display:"inline-block",verticalAlign:"top"},".cm-button":{verticalAlign:"middle",color:"inherit",fontSize:"70%",padding:".2em 1em",borderRadius:"1px"},"&light .cm-button":{backgroundImage:"linear-gradient(#eff1f5, #d9d9df)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#b4b4b4, #d0d3d6)"}},"&dark .cm-button":{backgroundImage:"linear-gradient(#393939, #111)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#111, #333)"}},".cm-textfield":{verticalAlign:"middle",color:"inherit",fontSize:"70%",border:"1px solid silver",padding:".2em .5em"},"&light .cm-textfield":{backgroundColor:"white"},"&dark .cm-textfield":{border:"1px solid #555",backgroundColor:"inherit"}},ys);class ks{constructor(t,e,i,n){this.typeOver=n,this.bounds=null,this.text="";let{impreciseHead:s,impreciseAnchor:r}=t.docView;if(t.state.readOnly&&e>-1)this.newSel=null;else if(e>-1&&(this.bounds=t.docView.domBoundsAround(e,i,0))){let e=s||r?[]:function(t){let e=[];if(t.root.activeElement!=t.contentDOM)return e;let{anchorNode:i,anchorOffset:n,focusNode:s,focusOffset:r}=t.observer.selectionRange;i&&(e.push(new tn(i,n)),s==i&&r==n||e.push(new tn(s,r)));return e}(t),i=new Zi(e,t.state);i.readRange(this.bounds.startDOM,this.bounds.endDOM),this.text=i.text,this.newSel=function(t,e){if(0==t.length)return null;let i=t[0].pos,n=2==t.length?t[1].pos:i;return i>-1&&n>-1?E.single(i+e,n+e):null}(e,this.bounds.from)}else{let e=t.observer.selectionRange,i=s&&s.node==e.focusNode&&s.offset==e.focusOffset||!se(t.contentDOM,e.focusNode)?t.state.selection.main.head:t.docView.posFromDOM(e.focusNode,e.focusOffset),n=r&&r.node==e.anchorNode&&r.offset==e.anchorOffset||!se(t.contentDOM,e.anchorNode)?t.state.selection.main.anchor:t.docView.posFromDOM(e.anchorNode,e.anchorOffset);this.newSel=E.single(n,i)}}}function Ss(t,i){let n,{newSel:s}=i,r=t.state.selection.main;if(i.bounds){let{from:s,to:o}=i.bounds,l=r.from,a=null;(8===t.inputState.lastKeyCode&&t.inputState.lastKeyTime>Date.now()-100||ze.android&&i.text.length0&&l>0&&t.charCodeAt(o-1)==e.charCodeAt(l-1);)o--,l--;if("end"==n){i-=o+Math.max(0,r-Math.min(o,l))-r}if(o=o?r-i:0,l=r+(l-o),o=r}else if(l=l?r-i:0,o=r+(o-l),l=r}return{from:r,toA:o,toB:l}}(t.state.doc.sliceString(s,o,Xi),i.text,l-s,a);h&&(ze.chrome&&13==t.inputState.lastKeyCode&&h.toB==h.from+2&&"￿￿"==i.text.slice(h.from,h.toB)&&h.toB--,n={from:s+h.from,to:s+h.toA,insert:e.of(i.text.slice(h.from,h.toB).split(Xi))})}else!s||t.hasFocus&&t.state.facet(Ci)&&!s.main.eq(r)||(s=null);if(!n&&!s)return!1;if(!n&&i.typeOver&&!r.empty&&s&&s.main.empty?n={from:r.from,to:r.to,insert:t.state.doc.slice(r.from,r.to)}:n&&n.from>=r.from&&n.to<=r.to&&(n.from!=r.from||n.to!=r.to)&&r.to-r.from-(n.to-n.from)<=4?n={from:r.from,to:r.to,insert:t.state.doc.slice(r.from,n.from).append(n.insert).append(t.state.doc.slice(n.to,r.to))}:(ze.mac||ze.android)&&n&&n.from==n.to&&n.from==r.head-1&&/^\. ?$/.test(n.insert.toString())?(s&&2==n.insert.length&&(s=E.single(s.main.anchor-1,s.main.head-1)),n={from:r.from,to:r.to,insert:e.of([" "])}):ze.chrome&&n&&n.from==n.to&&n.from==r.head&&"\n "==n.insert.toString()&&t.lineWrapping&&(s&&(s=E.single(s.main.anchor-1,s.main.head-1)),n={from:r.from,to:r.to,insert:e.of([" "])}),n){let e=t.state;if(ze.ios&&t.inputState.flushIOSKey(t))return!0;if(ze.android&&(n.from==r.from&&n.to==r.to&&1==n.insert.length&&2==n.insert.lines&&ye(t.contentDOM,"Enter",13)||n.from==r.from-1&&n.to==r.to&&0==n.insert.length&&ye(t.contentDOM,"Backspace",8)||n.from==r.from&&n.to==r.to+1&&0==n.insert.length&&ye(t.contentDOM,"Delete",46)))return!0;let i,o=n.insert.toString();if(t.state.facet(wi).some((e=>e(t,n.from,n.to,o))))return!0;if(t.inputState.composing>=0&&t.inputState.composing++,n.from>=r.from&&n.to<=r.to&&n.to-n.from>=(r.to-r.from)/3&&(!s||s.main.empty&&s.main.from==n.from+n.insert.length)&&t.inputState.composing<0){let s=r.fromn.to?e.sliceDoc(n.to,r.to):"";i=e.replaceSelection(t.state.toText(s+n.insert.sliceString(0,void 0,t.state.lineBreak)+o))}else{let o=e.changes(n),l=s&&!e.selection.main.eq(s.main)&&s.main.to<=o.newLength?s.main:void 0;if(e.selection.ranges.length>1&&t.inputState.composing>=0&&n.to<=r.to&&n.to>=r.to-10){let s=t.state.sliceDoc(n.from,n.to),a=sn(t)||t.state.doc.lineAt(r.head),h=r.to-n.to,c=r.to-r.from;i=e.changeByRange((i=>{if(i.from==r.from&&i.to==r.to)return{changes:o,range:l||i.map(o)};let u=i.to-h,f=u-s.length;if(i.to-i.from!=c||t.state.sliceDoc(f,u)!=s||a&&i.to>=a.from&&i.from<=a.to)return{range:i};let d=e.changes({from:f,to:u,insert:n.insert}),p=i.to-r.to;return{changes:d,range:l?E.range(Math.max(0,l.anchor+p),Math.max(0,l.head+p)):i.map(d)}}))}else i={changes:o,selection:l&&e.selection.replaceRange(l)}}let l="input.type";return t.composing&&(l+=".compose",t.inputState.compositionFirstChange&&(l+=".start",t.inputState.compositionFirstChange=!1)),t.dispatch(i,{scrollIntoView:!0,userEvent:l}),!0}if(s&&!s.main.eq(r)){let e=!1,i="select";return t.inputState.lastSelectionTime>Date.now()-50&&("select"==t.inputState.lastSelectionOrigin&&(e=!0),i=t.inputState.lastSelectionOrigin),t.dispatch({selection:s,scrollIntoView:e,userEvent:i}),!0}return!1}const Cs={childList:!0,characterData:!0,subtree:!0,attributes:!0,characterDataOldValue:!0},As=ze.ie&&ze.ie_version<=11;class Os{constructor(t){this.view=t,this.active=!1,this.selectionRange=new pe,this.selectionChanged=!1,this.delayedFlush=-1,this.resizeTimeout=-1,this.queue=[],this.delayedAndroidKey=null,this.flushingAndroidKey=-1,this.lastChange=0,this.scrollTargets=[],this.intersection=null,this.resize=null,this.intersecting=!1,this.gapIntersection=null,this.gaps=[],this.parentCheck=-1,this.dom=t.contentDOM,this.observer=new MutationObserver((e=>{for(let t of e)this.queue.push(t);(ze.ie&&ze.ie_version<=11||ze.ios&&t.composing)&&e.some((t=>"childList"==t.type&&t.removedNodes.length||"characterData"==t.type&&t.oldValue.length>t.target.nodeValue.length))?this.flushSoon():this.flush()})),As&&(this.onCharData=t=>{this.queue.push({target:t.target,type:"characterData",oldValue:t.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this),this.onResize=this.onResize.bind(this),this.onPrint=this.onPrint.bind(this),this.onScroll=this.onScroll.bind(this),"function"==typeof ResizeObserver&&(this.resize=new ResizeObserver((()=>{var t;(null===(t=this.view.docView)||void 0===t?void 0:t.lastUpdate){this.parentCheck<0&&(this.parentCheck=setTimeout(this.listenForScroll.bind(this),1e3)),t.length>0&&t[t.length-1].intersectionRatio>0!=this.intersecting&&(this.intersecting=!this.intersecting,this.intersecting!=this.view.inView&&this.onScrollChanged(document.createEvent("Event")))}),{}),this.intersection.observe(this.dom),this.gapIntersection=new IntersectionObserver((t=>{t.length>0&&t[t.length-1].intersectionRatio>0&&this.onScrollChanged(document.createEvent("Event"))}),{})),this.listenForScroll(),this.readSelectionRange()}onScrollChanged(t){this.view.inputState.runScrollHandlers(this.view,t),this.intersecting&&this.view.measure()}onScroll(t){this.intersecting&&this.flush(!1),this.onScrollChanged(t)}onResize(){this.resizeTimeout<0&&(this.resizeTimeout=setTimeout((()=>{this.resizeTimeout=-1,this.view.requestMeasure()}),50))}onPrint(){this.view.viewState.printing=!0,this.view.measure(),setTimeout((()=>{this.view.viewState.printing=!1,this.view.requestMeasure()}),500)}updateGaps(t){if(this.gapIntersection&&(t.length!=this.gaps.length||this.gaps.some(((e,i)=>e!=t[i])))){this.gapIntersection.disconnect();for(let e of t)this.gapIntersection.observe(e);this.gaps=t}}onSelectionChange(t){let e=this.selectionChanged;if(!this.readSelectionRange()||this.delayedAndroidKey)return;let{view:i}=this,n=this.selectionRange;if(i.state.facet(Ci)?i.root.activeElement!=this.dom:!re(i.dom,n))return;let s=n.anchorNode&&i.docView.nearest(n.anchorNode);s&&s.ignoreEvent(t)?e||(this.selectionChanged=!1):(ze.ie&&ze.ie_version<=11||ze.android&&ze.chrome)&&!i.state.selection.main.empty&&n.focusNode&&le(n.focusNode,n.focusOffset,n.anchorNode,n.anchorOffset)?this.flushSoon():this.flush(!1)}readSelectionRange(){let{view:t}=this,e=ze.safari&&11==t.root.nodeType&&function(t){let e=t.activeElement;for(;e&&e.shadowRoot;)e=e.shadowRoot.activeElement;return e}(this.dom.ownerDocument)==this.dom&&function(t){let e=null;function i(t){t.preventDefault(),t.stopImmediatePropagation(),e=t.getTargetRanges()[0]}if(t.contentDOM.addEventListener("beforeinput",i,!0),t.dom.ownerDocument.execCommand("indent"),t.contentDOM.removeEventListener("beforeinput",i,!0),!e)return null;let n=e.startContainer,s=e.startOffset,r=e.endContainer,o=e.endOffset,l=t.docView.domAtPos(t.state.selection.main.anchor);le(l.node,l.offset,r,o)&&([n,s,r,o]=[r,o,n,s]);return{anchorNode:n,anchorOffset:s,focusNode:r,focusOffset:o}}(this.view)||ne(t.root);if(!e||this.selectionRange.eq(e))return!1;let i=re(this.dom,e);return i&&!this.selectionChanged&&t.inputState.lastFocusTime>Date.now()-200&&t.inputState.lastTouchTime{let t=this.delayedAndroidKey;t&&(this.clearDelayedAndroidKey(),!this.flush()&&t.force&&ye(this.dom,t.key,t.keyCode))};this.flushingAndroidKey=this.view.win.requestAnimationFrame(t)}this.delayedAndroidKey&&"Enter"!=t||(this.delayedAndroidKey={key:t,keyCode:e,force:this.lastChange{this.delayedFlush=-1,this.flush()})))}forceFlush(){this.delayedFlush>=0&&(this.view.win.cancelAnimationFrame(this.delayedFlush),this.delayedFlush=-1),this.flush()}processRecords(){let t=this.queue;for(let e of this.observer.takeRecords())t.push(e);t.length&&(this.queue=[]);let e=-1,i=-1,n=!1;for(let s of t){let t=this.readMutation(s);t&&(t.typeOver&&(n=!0),-1==e?({from:e,to:i}=t):(e=Math.min(t.from,e),i=Math.max(t.to,i)))}return{from:e,to:i,typeOver:n}}readChange(){let{from:t,to:e,typeOver:i}=this.processRecords(),n=this.selectionChanged&&re(this.dom,this.selectionRange);return t<0&&!n?null:(t>-1&&(this.lastChange=Date.now()),this.view.inputState.lastFocusTime=0,this.selectionChanged=!1,new ks(this.view,t,e,i))}flush(t=!0){if(this.delayedFlush>=0||this.delayedAndroidKey)return!1;t&&this.readSelectionRange();let e=this.readChange();if(!e)return!1;let i=this.view.state,n=Ss(this.view,e);return this.view.state==i&&this.view.update([]),n}readMutation(t){let e=this.view.docView.nearest(t.target);if(!e||e.ignoreMutation(t))return null;if(e.markDirty("attributes"==t.type),"attributes"==t.type&&(e.dirty|=4),"childList"==t.type){let i=Ms(e,t.previousSibling||t.target.previousSibling,-1),n=Ms(e,t.nextSibling||t.target.nextSibling,1);return{from:i?e.posAfter(i):e.posAtStart,to:n?e.posBefore(n):e.posAtEnd,typeOver:!1}}return"characterData"==t.type?{from:e.posAtStart,to:e.posAtEnd,typeOver:t.target.nodeValue==t.oldValue}:null}setWindow(t){t!=this.win&&(this.removeWindowListeners(this.win),this.win=t,this.addWindowListeners(this.win))}addWindowListeners(t){t.addEventListener("resize",this.onResize),t.addEventListener("beforeprint",this.onPrint),t.addEventListener("scroll",this.onScroll),t.document.addEventListener("selectionchange",this.onSelectionChange)}removeWindowListeners(t){t.removeEventListener("scroll",this.onScroll),t.removeEventListener("resize",this.onResize),t.removeEventListener("beforeprint",this.onPrint),t.document.removeEventListener("selectionchange",this.onSelectionChange)}destroy(){var t,e,i;this.stop(),null===(t=this.intersection)||void 0===t||t.disconnect(),null===(e=this.gapIntersection)||void 0===e||e.disconnect(),null===(i=this.resize)||void 0===i||i.disconnect();for(let t of this.scrollTargets)t.removeEventListener("scroll",this.onScroll);this.removeWindowListeners(this.win),clearTimeout(this.parentCheck),clearTimeout(this.resizeTimeout),this.win.cancelAnimationFrame(this.delayedFlush),this.win.cancelAnimationFrame(this.flushingAndroidKey)}}function Ms(t,e,i){for(;e;){let n=Se.get(e);if(n&&n.parent==t)return n;let s=e.parentNode;e=s!=t.dom?s:i>0?e.nextSibling:e.previousSibling}return null}class Ds{constructor(t={}){this.plugins=[],this.pluginMap=new Map,this.editorAttrs={},this.contentAttrs={},this.bidiCache=[],this.destroyed=!1,this.updateState=2,this.measureScheduled=-1,this.measureRequests=[],this.contentDOM=document.createElement("div"),this.scrollDOM=document.createElement("div"),this.scrollDOM.tabIndex=-1,this.scrollDOM.className="cm-scroller",this.scrollDOM.appendChild(this.contentDOM),this.announceDOM=document.createElement("div"),this.announceDOM.style.cssText="position: absolute; top: -10000px",this.announceDOM.setAttribute("aria-live","polite"),this.dom=document.createElement("div"),this.dom.appendChild(this.announceDOM),this.dom.appendChild(this.scrollDOM),this._dispatch=t.dispatch||(t=>this.update([t])),this.dispatch=this.dispatch.bind(this),this._root=t.root||function(t){for(;t;){if(t&&(9==t.nodeType||11==t.nodeType&&t.host))return t;t=t.assignedSlot||t.parentNode}return null}(t.parent)||document,this.viewState=new os(t.state||St.create(t)),this.plugins=this.state.facet(Oi).map((t=>new Di(t)));for(let t of this.plugins)t.update(this);this.observer=new Os(this),this.inputState=new yn(this),this.inputState.ensureHandlers(this,this.plugins),this.docView=new en(this),this.mountStyles(),this.updateAttrs(),this.updateState=0,this.requestMeasure(),t.parent&&t.parent.appendChild(this.dom)}get state(){return this.viewState.state}get viewport(){return this.viewState.viewport}get visibleRanges(){return this.viewState.visibleRanges}get inView(){return this.viewState.inView}get composing(){return this.inputState.composing>0}get compositionStarted(){return this.inputState.composing>=0}get root(){return this._root}get win(){return this.dom.ownerDocument.defaultView||window}dispatch(...t){this._dispatch(1==t.length&&t[0]instanceof ft?t[0]:this.state.update(...t))}update(t){if(0!=this.updateState)throw new Error("Calls to EditorView.update are not allowed while an update is in progress");let e,i=!1,n=!1,s=this.state;for(let e of t){if(e.startState!=s)throw new RangeError("Trying to update state with a transaction that doesn't start from the previous state.");s=e.state}if(this.destroyed)return void(this.viewState.state=s);let r=this.observer.delayedAndroidKey,o=null;if(r?(this.observer.clearDelayedAndroidKey(),o=this.observer.readChange(),(o&&!this.state.doc.eq(s.doc)||!this.state.selection.eq(s.selection))&&(o=null)):this.observer.clear(),s.facet(St.phrases)!=this.state.facet(St.phrases))return this.setState(s);e=Ii.create(this,s,t);let l=this.viewState.scrollTarget;try{this.updateState=2;for(let e of t){if(l&&(l=l.map(e.changes)),e.scrollIntoView){let{main:t}=e.state.selection;l=new xi(t.empty?t:E.cursor(t.head,t.head>t.anchor?-1:1))}for(let t of e.effects)t.is(ki)&&(l=t.value)}this.viewState.update(e,l),this.bidiCache=Rs.update(this.bidiCache,e.changes),e.empty||(this.updatePlugins(e),this.inputState.update(e)),i=this.docView.update(e),this.state.facet(Li)!=this.styleModules&&this.mountStyles(),n=this.updateAttrs(),this.showAnnouncements(t),this.docView.updateSelection(i,t.some((t=>t.isUserEvent("select.pointer"))))}finally{this.updateState=0}if(e.startState.facet(ps)!=e.state.facet(ps)&&(this.viewState.mustMeasureContent=!0),(i||n||l||this.viewState.mustEnforceCursorAssoc||this.viewState.mustMeasureContent)&&this.requestMeasure(),!e.empty)for(let t of this.state.facet(vi))t(e);o&&!Ss(this,o)&&r.force&&ye(this.contentDOM,r.key,r.keyCode)}setState(t){if(0!=this.updateState)throw new Error("Calls to EditorView.setState are not allowed while an update is in progress");if(this.destroyed)return void(this.viewState.state=t);this.updateState=2;let e=this.hasFocus;try{for(let t of this.plugins)t.destroy(this);this.viewState=new os(t),this.plugins=t.facet(Oi).map((t=>new Di(t))),this.pluginMap.clear();for(let t of this.plugins)t.update(this);this.docView=new en(this),this.inputState.ensureHandlers(this,this.plugins),this.mountStyles(),this.updateAttrs(),this.bidiCache=[]}finally{this.updateState=0}e&&this.focus(),this.requestMeasure()}updatePlugins(t){let e=t.startState.facet(Oi),i=t.state.facet(Oi);if(e!=i){let n=[];for(let s of i){let i=e.indexOf(s);if(i<0)n.push(new Di(s));else{let e=this.plugins[i];e.mustUpdate=t,n.push(e)}}for(let e of this.plugins)e.mustUpdate!=t&&e.destroy(this);this.plugins=n,this.pluginMap.clear(),this.inputState.ensureHandlers(this,this.plugins)}else for(let e of this.plugins)e.mustUpdate=t;for(let t=0;t-1&&cancelAnimationFrame(this.measureScheduled),this.measureScheduled=0,t&&this.observer.forceFlush();let e=null,{scrollHeight:i,scrollTop:n,clientHeight:s}=this.scrollDOM,r=n>i-s-4?i:n;try{for(let t=0;;t++){this.updateState=1;let i=this.viewport,n=this.viewState.lineBlockAtHeight(r),s=this.viewState.measure(this);if(!s&&!this.measureRequests.length&&null==this.viewState.scrollTarget)break;if(t>5){console.warn(this.measureRequests.length?"Measure loop restarted more than 5 times":"Viewport failed to stabilize");break}let o=[];4&s||([this.measureRequests,o]=[o,this.measureRequests]);let l=o.map((t=>{try{return t.read(this)}catch(t){return Si(this.state,t),Ps}})),a=Ii.create(this,this.state,[]),h=!1,c=!1;a.flags|=s,e?e.flags|=s:e=a,this.updateState=2,a.empty||(this.updatePlugins(a),this.inputState.update(a),this.updateAttrs(),h=this.docView.update(a));for(let t=0;t1||t<-1)&&(this.scrollDOM.scrollTop+=t,c=!0)}if(h&&this.docView.updateSelection(!0),this.viewport.from==i.from&&this.viewport.to==i.to&&!c&&0==this.measureRequests.length)break}}finally{this.updateState=0,this.measureScheduled=-1}if(e&&!e.empty)for(let t of this.state.facet(vi))t(e)}get themeClasses(){return gs+" "+(this.state.facet(ms)?ws:vs)+" "+this.state.facet(ps)}updateAttrs(){let t=Es(this,Ti,{class:"cm-editor"+(this.hasFocus?" cm-focused ":" ")+this.themeClasses}),e={spellcheck:"false",autocorrect:"off",autocapitalize:"off",translate:"no",contenteditable:this.state.facet(Ci)?"true":"false",class:"cm-content",style:`${ze.tabSize}: ${this.state.tabSize}`,role:"textbox","aria-multiline":"true"};this.state.readOnly&&(e["aria-readonly"]="true"),Es(this,Pi,e);let i=this.observer.ignore((()=>{let i=Ye(this.contentDOM,this.contentAttrs,e),n=Ye(this.dom,this.editorAttrs,t);return i||n}));return this.editorAttrs=t,this.contentAttrs=e,i}showAnnouncements(t){let e=!0;for(let i of t)for(let t of i.effects)if(t.is(Ds.announce)){e&&(this.announceDOM.textContent=""),e=!1,this.announceDOM.appendChild(document.createElement("div")).textContent=t.value}}mountStyles(){this.styleModules=this.state.facet(Li),$t.mount(this.root,this.styleModules.concat(xs).reverse())}readMeasured(){if(2==this.updateState)throw new Error("Reading the editor layout isn't allowed during an update");0==this.updateState&&this.measureScheduled>-1&&this.measure(!1)}requestMeasure(t){if(this.measureScheduled<0&&(this.measureScheduled=this.win.requestAnimationFrame((()=>this.measure()))),t){if(null!=t.key)for(let e=0;ee.spec==t))||null),e&&e.update(this).value}get documentTop(){return this.contentDOM.getBoundingClientRect().top+this.viewState.paddingTop}get documentPadding(){return{top:this.viewState.paddingTop,bottom:this.viewState.paddingBottom}}elementAtHeight(t){return this.readMeasured(),this.viewState.elementAtHeight(t)}lineBlockAtHeight(t){return this.readMeasured(),this.viewState.lineBlockAtHeight(t)}get viewportLineBlocks(){return this.viewState.viewportLines}lineBlockAt(t){return this.viewState.lineBlockAt(t)}get contentHeight(){return this.viewState.contentHeight}moveByChar(t,e,i){return wn(this,t,vn(this,t,e,i))}moveByGroup(t,e){return wn(this,t,vn(this,t,e,(e=>function(t,e,i){let n=t.state.charCategorizer(e),s=n(i);return t=>{let e=n(t);return s==yt.Space&&(s=e),s==e}}(this,t.head,e))))}moveToLineBoundary(t,e,i=!0){return function(t,e,i,n){let s=t.state.doc.lineAt(e.head),r=n&&t.lineWrapping?t.coordsAtPos(e.assoc<0&&e.head>s.from?e.head-1:e.head):null;if(r){let e=t.dom.getBoundingClientRect(),n=t.textDirectionAt(s.from),o=t.posAtCoords({x:i==(n==Vi.LTR)?e.right-1:e.left+1,y:(r.top+r.bottom)/2});if(null!=o)return E.cursor(o,i?-1:1)}let o=ai.find(t.docView,e.head),l=o?i?o.posAtEnd:o.posAtStart:i?s.to:s.from;return E.cursor(l,i?-1:1)}(this,t,e,i)}moveVertically(t,e,i){return wn(this,t,function(t,e,i,n){let s=e.head,r=i?1:-1;if(s==(i?t.state.doc.length:0))return E.cursor(s,e.assoc);let o,l=e.goalColumn,a=t.contentDOM.getBoundingClientRect(),h=t.coordsAtPos(s),c=t.documentTop;if(h)null==l&&(l=h.left-a.left),o=r<0?h.top:h.bottom;else{let e=t.viewState.lineBlockAt(s);null==l&&(l=Math.min(a.right-a.left,t.defaultCharacterWidth*(s-e.from))),o=(r<0?e.top:e.bottom)+c}let u=a.left+l,f=null!=n?n:t.defaultLineHeight>>1;for(let i=0;;i+=10){let n=o+(f+i)*r,h=mn(t,{x:u,y:n},!1,r);if(na.bottom||(r<0?hs))return E.cursor(h,e.assoc,void 0,l)}}(this,t,e,i))}domAtPos(t){return this.docView.domAtPos(t)}posAtDOM(t,e=0){return this.docView.posFromDOM(t,e)}posAtCoords(t,e=!0){return this.readMeasured(),mn(this,t,e)}coordsAtPos(t,e=1){this.readMeasured();let i=this.docView.coordsAt(t,e);if(!i||i.left==i.right)return i;let n=this.state.doc.lineAt(t),s=this.bidiSpans(n);return fe(i,s[$i.find(s,t-n.from,-1,e)].dir==Vi.LTR==e>0)}get defaultCharacterWidth(){return this.viewState.heightOracle.charWidth}get defaultLineHeight(){return this.viewState.heightOracle.lineHeight}get textDirection(){return this.viewState.defaultTextDirection}textDirectionAt(t){return!this.state.facet(yi)||tthis.viewport.to?this.textDirection:(this.readMeasured(),this.docView.textDirectionAt(t))}get lineWrapping(){return this.viewState.heightOracle.lineWrapping}bidiSpans(t){if(t.length>Ts)return Ki(t.length);let e=this.textDirectionAt(t.from);for(let i of this.bidiCache)if(i.from==t.from&&i.dir==e)return i.order;let i=function(t,e){let i=t.length,n=e==Wi?1:2,s=e==Wi?2:1;if(!t||1==n&&!Ui.test(t))return Ki(i);for(let e=0,s=n,o=n;e=0;t-=3)if(ji[t+1]==-r){let e=ji[t+2],i=2&e?n:4&e?1&e?s:n:0;i&&(Qi[l]=Qi[ji[t]]=i),a=t;break}}else{if(189==ji.length)break;ji[a++]=l,ji[a++]=e,ji[a++]=h}else if(2==(o=Qi[l])||1==o){let t=o==n;h=t?0:1;for(let e=a-3;e>=0;e-=3){let i=ji[e+2];if(2&i)break;if(t)ji[e+2]|=2;else{if(4&i)break;ji[e+2]|=4}}}for(let t=0;te;){let t=i,n=2!=Qi[--i];for(;i>e&&n==(2!=Qi[i-1]);)i--;o.push(new $i(i,t,n?2:1))}else o.push(new $i(e,t,0))}else for(let t=0;tDate.now()-3e4)&&this.root.activeElement==this.contentDOM}focus(){this.observer.ignore((()=>{ve(this.contentDOM),this.docView.updateSelection()}))}setRoot(t){this._root!=t&&(this._root=t,this.observer.setWindow((9==t.nodeType?t:t.ownerDocument).defaultView||window),this.mountStyles())}destroy(){for(let t of this.plugins)t.destroy(this);this.plugins=[],this.inputState.destroy(),this.dom.remove(),this.observer.destroy(),this.measureScheduled>-1&&cancelAnimationFrame(this.measureScheduled),this.destroyed=!0}static scrollIntoView(t,e={}){return ki.of(new xi("number"==typeof t?E.cursor(t):t,e.y,e.x,e.yMargin,e.xMargin))}static domEventHandlers(t){return Mi.define((()=>({})),{eventHandlers:t})}static theme(t,e){let i=$t.newName(),n=[ps.of(i),Li.of(bs(`.${i}`,t))];return e&&e.dark&&n.push(ms.of(!0)),n}static baseTheme(t){return K.lowest(Li.of(bs("."+gs,t,ys)))}static findFromDOM(t){var e;let i=t.querySelector(".cm-content"),n=i&&Se.get(i)||Se.get(t);return(null===(e=null==n?void 0:n.rootView)||void 0===e?void 0:e.view)||null}}Ds.styleModule=Li,Ds.inputHandler=wi,Ds.perLineTextDirection=yi,Ds.exceptionSink=gi,Ds.updateListener=vi,Ds.editable=Ci,Ds.mouseSelectionStyle=mi,Ds.dragMovesSelection=pi,Ds.clickAddsSelectionRange=di,Ds.decorations=Ri,Ds.atomicRanges=Ei,Ds.scrollMargins=Bi,Ds.darkTheme=ms,Ds.contentAttributes=Pi,Ds.editorAttributes=Ti,Ds.lineWrapping=Ds.contentAttributes.of({class:"cm-lineWrapping"}),Ds.announce=ut.define();const Ts=4096,Ps={};class Rs{constructor(t,e,i,n){this.from=t,this.to=e,this.dir=i,this.order=n}static update(t,e){if(e.empty)return t;let i=[],n=t.length?t[t.length-1].dir:Vi.LTR;for(let s=Math.max(0,t.length-10);s=0;s--){let e=n[s],r="function"==typeof e?e(t):e;r&&Xe(r,i)}return i}const Bs=ze.mac?"mac":ze.windows?"win":ze.linux?"linux":"key";function Ls(t,e,i){return e.altKey&&(t="Alt-"+t),e.ctrlKey&&(t="Ctrl-"+t),e.metaKey&&(t="Meta-"+t),!1!==i&&e.shiftKey&&(t="Shift-"+t),t}const Ns=K.default(Ds.domEventHandlers({keydown:(t,e)=>Hs(Ws(e.state),t,e,"editor")})),Is=N.define({enables:Ns}),Vs=new WeakMap;function Ws(t){let e=t.facet(Is),i=Vs.get(e);return i||Vs.set(e,i=function(t,e=Bs){let i=Object.create(null),n=Object.create(null),s=(t,e)=>{let i=n[t];if(null==i)n[t]=e;else if(i!=e)throw new Error("Key binding "+t+" is used both as a regular binding and as a multi-stroke prefix")},r=(t,n,r,o)=>{var l,a;let h=i[t]||(i[t]=Object.create(null)),c=n.split(/ (?!$)/).map((t=>function(t,e){const i=t.split(/-(?!$)/);let n,s,r,o,l=i[i.length-1];"Space"==l&&(l=" ");for(let t=0;t{let n=zs={view:e,prefix:i,scope:t};return setTimeout((()=>{zs==n&&(zs=null)}),4e3),!0}]})}let u=c.join(" ");s(u,!1);let f=h[u]||(h[u]={preventDefault:!1,run:(null===(a=null===(l=h._any)||void 0===l?void 0:l.run)||void 0===a?void 0:a.slice())||[]});r&&f.run.push(r),o&&(f.preventDefault=!0)};for(let n of t){let t=n.scope?n.scope.split(" "):["editor"];if(n.any)for(let e of t){let t=i[e]||(i[e]=Object.create(null));t._any||(t._any={preventDefault:!1,run:[]});for(let e in t)t[e].run.push(n.any)}let s=n[e]||n.key;if(s)for(let e of t)r(e,s,n.run,n.preventDefault),n.shift&&r(e,"Shift-"+s,n.shift,n.preventDefault)}return i}(e.reduce(((t,e)=>t.concat(e)),[]))),i}let zs=null;function Hs(t,e,i,n){let s=function(t){var e=!(te&&(t.ctrlKey||t.altKey||t.metaKey)||Yt&&t.shiftKey&&t.key&&1==t.key.length||"Unidentified"==t.key)&&t.key||(t.shiftKey?Jt:Gt)[t.keyCode]||t.key||"Unidentified";return"Esc"==e&&(e="Escape"),"Del"==e&&(e="Delete"),"Left"==e&&(e="ArrowLeft"),"Up"==e&&(e="ArrowUp"),"Right"==e&&(e="ArrowRight"),"Down"==e&&(e="ArrowDown"),e}(e),r=b(w(s,0))==s.length&&" "!=s,o="",l=!1;zs&&zs.view==i&&zs.scope==n&&(o=zs.prefix+" ",(l=kn.indexOf(e.keyCode)<0)&&(zs=null));let a,h,c=new Set,u=t=>{if(t){for(let n of t.run)if(!c.has(n)&&(c.add(n),n(i,e)))return!0;t.preventDefault&&(l=!0)}return!1},f=t[n];if(f){if(u(f[o+Ls(s,e,!r)]))return!0;if(r&&(e.altKey||e.metaKey||e.ctrlKey)&&(a=Gt[e.keyCode])&&a!=s){if(u(f[o+Ls(a,e,!0)]))return!0;if(e.shiftKey&&(h=Jt[e.keyCode])!=s&&h!=a&&u(f[o+Ls(h,e,!1)]))return!0}else if(r&&e.shiftKey&&u(f[o+Ls(s,e,!0)]))return!0;if(u(f._any))return!0}return l}const Fs=!ze.ios,qs=N.define({combine:t=>Ct(t,{cursorBlinkRate:1200,drawRangeCursor:!0},{cursorBlinkRate:(t,e)=>Math.min(t,e),drawRangeCursor:(t,e)=>t||e})});function _s(t={}){return[qs.of(t),Us,Qs,bi.of(!0)]}class js{constructor(t,e,i,n,s){this.left=t,this.top=e,this.width=i,this.height=n,this.className=s}draw(){let t=document.createElement("div");return t.className=this.className,this.adjust(t),t}adjust(t){t.style.left=this.left+"px",t.style.top=this.top+"px",this.width>=0&&(t.style.width=this.width+"px"),t.style.height=this.height+"px"}eq(t){return this.left==t.left&&this.top==t.top&&this.width==t.width&&this.height==t.height&&this.className==t.className}}const Us=Mi.fromClass(class{constructor(t){this.view=t,this.rangePieces=[],this.cursors=[],this.measureReq={read:this.readPos.bind(this),write:this.drawSel.bind(this)},this.selectionLayer=t.scrollDOM.appendChild(document.createElement("div")),this.selectionLayer.className="cm-selectionLayer",this.selectionLayer.setAttribute("aria-hidden","true"),this.cursorLayer=t.scrollDOM.appendChild(document.createElement("div")),this.cursorLayer.className="cm-cursorLayer",this.cursorLayer.setAttribute("aria-hidden","true"),t.requestMeasure(this.measureReq),this.setBlinkRate()}setBlinkRate(){this.cursorLayer.style.animationDuration=this.view.state.facet(qs).cursorBlinkRate+"ms"}update(t){let e=t.startState.facet(qs)!=t.state.facet(qs);(e||t.selectionSet||t.geometryChanged||t.viewportChanged)&&this.view.requestMeasure(this.measureReq),t.transactions.some((t=>t.scrollIntoView))&&(this.cursorLayer.style.animationName="cm-blink"==this.cursorLayer.style.animationName?"cm-blink2":"cm-blink"),e&&this.setBlinkRate()}readPos(){let{state:t}=this.view,e=t.facet(qs),i=t.selection.ranges.map((t=>t.empty?[]:function(t,e){if(e.to<=t.viewport.from||e.from>=t.viewport.to)return[];let i=Math.max(e.from,t.viewport.from),n=Math.min(e.to,t.viewport.to),s=t.textDirection==Vi.LTR,r=t.contentDOM,o=r.getBoundingClientRect(),l=Ks(t),a=window.getComputedStyle(r.firstChild),h=o.left+parseInt(a.paddingLeft)+Math.min(0,parseInt(a.textIndent)),c=o.right-parseInt(a.paddingRight),u=Js(t,i),f=Js(t,n),d=u.type==ei.Text?u:null,p=f.type==ei.Text?f:null;t.lineWrapping&&(d&&(d=Gs(t,i,d)),p&&(p=Gs(t,n,p)));if(d&&p&&d.from==p.from)return g(v(e.from,e.to,d));{let i=d?v(e.from,null,d):w(u,!1),n=p?v(null,e.to,p):w(f,!0),s=[];return(d||u).to<(p||f).from-1?s.push(m(h,i.bottom,c,n.top)):i.bottomu&&n.from=r)break;l>s&&a(Math.max(t,s),null==e&&t<=u,Math.min(l,r),null==i&&l>=f,o.dir)}if(s=n.to+1,s>=r)break}return 0==l.length&&a(u,null==e,f,null==i,t.textDirection),{top:r,bottom:o,horizontal:l}}function w(t,e){let i=o.top+(e?t.top:t.bottom);return{top:i,bottom:i,horizontal:[]}}}(this.view,t))).reduce(((t,e)=>t.concat(e))),n=[];for(let i of t.selection.ranges){let s=i==t.selection.main;if(i.empty?!s||Fs:e.drawRangeCursor){let t=Xs(this.view,i,s);t&&n.push(t)}}return{rangePieces:i,cursors:n}}drawSel({rangePieces:t,cursors:e}){if(t.length!=this.rangePieces.length||t.some(((t,e)=>!t.eq(this.rangePieces[e])))){this.selectionLayer.textContent="";for(let e of t)this.selectionLayer.appendChild(e.draw());this.rangePieces=t}if(e.length!=this.cursors.length||e.some(((t,e)=>!t.eq(this.cursors[e])))){let t=this.cursorLayer.children;if(t.length!==e.length){this.cursorLayer.textContent="";for(const t of e)this.cursorLayer.appendChild(t.draw())}else e.forEach(((e,i)=>e.adjust(t[i])));this.cursors=e}}destroy(){this.selectionLayer.remove(),this.cursorLayer.remove()}}),$s={".cm-line":{"& ::selection":{backgroundColor:"transparent !important"},"&::selection":{backgroundColor:"transparent !important"}}};Fs&&($s[".cm-line"].caretColor="transparent !important");const Qs=K.highest(Ds.theme($s));function Ks(t){let e=t.scrollDOM.getBoundingClientRect();return{left:(t.textDirection==Vi.LTR?e.left:e.right-t.scrollDOM.clientWidth)-t.scrollDOM.scrollLeft,top:e.top-t.scrollDOM.scrollTop}}function Gs(t,e,i){let n=E.cursor(e);return{from:Math.max(i.from,t.moveToLineBoundary(n,!1,!0).from),to:Math.min(i.to,t.moveToLineBoundary(n,!0,!0).from),type:ei.Text}}function Js(t,e){let i=t.lineBlockAt(e);if(Array.isArray(i.type))for(let t of i.type)if(t.to>e||t.to==e&&(t.to==i.to||t.type==ei.Text))return t;return i}function Xs(t,e,i){let n=t.coordsAtPos(e.head,e.assoc||1);if(!n)return null;let s=Ks(t);return new js(n.left-s.left,n.top-s.top,-1,n.bottom-n.top,i?"cm-cursor cm-cursor-primary":"cm-cursor cm-cursor-secondary")}const Zs=ut.define({map:(t,e)=>null==t?null:e.mapPos(t)}),Ys=q.define({create:()=>null,update:(t,e)=>(null!=t&&(t=e.changes.mapPos(t)),e.effects.reduce(((t,e)=>e.is(Zs)?e.value:t),t))}),tr=Mi.fromClass(class{constructor(t){this.view=t,this.cursor=null,this.measureReq={read:this.readPos.bind(this),write:this.drawCursor.bind(this)}}update(t){var e;let i=t.state.field(Ys);null==i?null!=this.cursor&&(null===(e=this.cursor)||void 0===e||e.remove(),this.cursor=null):(this.cursor||(this.cursor=this.view.scrollDOM.appendChild(document.createElement("div")),this.cursor.className="cm-dropCursor"),(t.startState.field(Ys)!=i||t.docChanged||t.geometryChanged)&&this.view.requestMeasure(this.measureReq))}readPos(){let t=this.view.state.field(Ys),e=null!=t&&this.view.coordsAtPos(t);if(!e)return null;let i=this.view.scrollDOM.getBoundingClientRect();return{left:e.left-i.left+this.view.scrollDOM.scrollLeft,top:e.top-i.top+this.view.scrollDOM.scrollTop,height:e.bottom-e.top}}drawCursor(t){this.cursor&&(t?(this.cursor.style.left=t.left+"px",this.cursor.style.top=t.top+"px",this.cursor.style.height=t.height+"px"):this.cursor.style.left="-100000px")}destroy(){this.cursor&&this.cursor.remove()}setDropPos(t){this.view.state.field(Ys)!=t&&this.view.dispatch({effects:Zs.of(t)})}},{eventHandlers:{dragover(t){this.setDropPos(this.view.posAtCoords({x:t.clientX,y:t.clientY}))},dragleave(t){t.target!=this.view.contentDOM&&this.view.contentDOM.contains(t.relatedTarget)||this.setDropPos(null)},dragend(){this.setDropPos(null)},drop(){this.setDropPos(null)}}});function er(t,e,i,n,s){e.lastIndex=0;for(let r,o=t.iterRange(i,n),l=i;!o.next().done;l+=o.value.length)if(!o.lineBreak)for(;r=e.exec(o.value);)s(l+r.index,r)}class ir{constructor(t){const{regexp:e,decoration:i,decorate:n,boundary:s,maxLength:r=1e3}=t;if(!e.global)throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");if(this.regexp=e,n)this.addMatch=(t,e,i,s)=>n(s,i,i+t[0].length,t,e);else if("function"==typeof i)this.addMatch=(t,e,n,s)=>{let r=i(t,e,n);r&&s(n,n+t[0].length,r)};else{if(!i)throw new RangeError("Either 'decorate' or 'decoration' should be provided to MatchDecorator");this.addMatch=(t,e,n,s)=>s(n,n+t[0].length,i)}this.boundary=s,this.maxLength=r}createDeco(t){let e=new Pt,i=e.add.bind(e);for(let{from:e,to:n}of function(t,e){let i=t.visibleRanges;if(1==i.length&&i[0].from==t.viewport.from&&i[0].to==t.viewport.to)return i;let n=[];for(let{from:s,to:r}of i)s=Math.max(t.state.doc.lineAt(s).from,s-e),r=Math.min(t.state.doc.lineAt(r).to,r+e),n.length&&n[n.length-1].to>=s?n[n.length-1].to=r:n.push({from:s,to:r});return n}(t,this.maxLength))er(t.state.doc,this.regexp,e,n,((e,n)=>this.addMatch(n,t,e,i)));return e.finish()}updateDeco(t,e){let i=1e9,n=-1;return t.docChanged&&t.changes.iterChanges(((e,s,r,o)=>{o>t.view.viewport.from&&r1e3?this.createDeco(t.view):n>-1?this.updateRange(t.view,e.map(t.changes),i,n):e}updateRange(t,e,i,n){for(let s of t.visibleRanges){let r=Math.max(s.from,i),o=Math.min(s.to,n);if(o>r){let i=t.state.doc.lineAt(r),n=i.toi.from;r--)if(this.boundary.test(i.text[r-1-i.from])){l=r;break}for(;oc.push(i.range(t,e));if(i==n)for(this.regexp.lastIndex=l-i.from;(h=this.regexp.exec(i.text))&&h.indexthis.addMatch(i,t,e,u)));e=e.update({filterFrom:l,filterTo:a,filter:(t,e)=>ta,add:c})}}return e}}const nr=null!=/x/.unicode?"gu":"g",sr=new RegExp("[\0-\b\n--Ÿ­؜​‎‏\u2028\u2029‭‮⁦⁧⁩\ufeff-]",nr),rr={0:"null",7:"bell",8:"backspace",10:"newline",11:"vertical tab",13:"carriage return",27:"escape",8203:"zero width space",8204:"zero width non-joiner",8205:"zero width joiner",8206:"left-to-right mark",8207:"right-to-left mark",8232:"line separator",8237:"left-to-right override",8238:"right-to-left override",8294:"left-to-right isolate",8295:"right-to-left isolate",8297:"pop directional isolate",8233:"paragraph separator",65279:"zero width no-break space",65532:"object replacement"};let or=null;const lr=N.define({combine(t){let e=Ct(t,{render:null,specialChars:sr,addSpecialChars:null});return(e.replaceTabs=!function(){var t;if(null==or&&"undefined"!=typeof document&&document.body){let e=document.body.style;or=null!=(null!==(t=e.tabSize)&&void 0!==t?t:e.MozTabSize)}return or||!1}())&&(e.specialChars=new RegExp("\t|"+e.specialChars.source,nr)),e.addSpecialChars&&(e.specialChars=new RegExp(e.specialChars.source+"|"+e.addSpecialChars.source,nr)),e}});function ar(t={}){return[lr.of(t),hr||(hr=Mi.fromClass(class{constructor(t){this.view=t,this.decorations=ii.none,this.decorationCache=Object.create(null),this.decorator=this.makeDecorator(t.state.facet(lr)),this.decorations=this.decorator.createDeco(t)}makeDecorator(t){return new ir({regexp:t.specialChars,decoration:(e,i,n)=>{let{doc:s}=i.state,r=w(e[0],0);if(9==r){let t=s.lineAt(n),e=i.state.tabSize,r=Ft(t.text,e,n-t.from);return ii.replace({widget:new ur((e-r%e)*this.view.defaultCharacterWidth)})}return this.decorationCache[r]||(this.decorationCache[r]=ii.replace({widget:new cr(t,r)}))},boundary:t.replaceTabs?void 0:/[^]/})}update(t){let e=t.state.facet(lr);t.startState.facet(lr)!=e?(this.decorator=this.makeDecorator(e),this.decorations=this.decorator.createDeco(t.view)):this.decorations=this.decorator.updateDeco(t,this.decorations)}},{decorations:t=>t.decorations}))]}let hr=null;class cr extends ti{constructor(t,e){super(),this.options=t,this.code=e}eq(t){return t.code==this.code}toDOM(t){let e=function(t){return t>=32?"•":10==t?"␤":String.fromCharCode(9216+t)}(this.code),i=t.state.phrase("Control character")+" "+(rr[this.code]||"0x"+this.code.toString(16)),n=this.options.render&&this.options.render(this.code,i,e);if(n)return n;let s=document.createElement("span");return s.textContent=e,s.title=i,s.setAttribute("aria-label",i),s.className="cm-specialChar",s}ignoreEvent(){return!1}}class ur extends ti{constructor(t){super(),this.width=t}eq(t){return t.width==this.width}toDOM(){let t=document.createElement("span");return t.textContent="\t",t.className="cm-tab",t.style.width=this.width+"px",t}ignoreEvent(){return!1}}const fr=ii.line({class:"cm-activeLine"}),dr=Mi.fromClass(class{constructor(t){this.decorations=this.getDeco(t)}update(t){(t.docChanged||t.selectionSet)&&(this.decorations=this.getDeco(t.view))}getDeco(t){let e=-1,i=[];for(let n of t.state.selection.ranges){let s=t.lineBlockAt(n.head);s.from>e&&(i.push(fr.range(s.from)),e=s.from)}return ii.set(i)}},{decorations:t=>t.decorations}),pr=2e3;function mr(t,e){let i=t.posAtCoords({x:e.clientX,y:e.clientY},!1),n=t.state.doc.lineAt(i),s=i-n.from,r=s>pr?-1:s==n.length?function(t,e){let i=t.coordsAtPos(t.viewport.from);return i?Math.round(Math.abs((i.left-e)/t.defaultCharacterWidth)):-1}(t,e.clientX):Ft(n.text,t.state.tabSize,i-n.from);return{line:n.number,col:r,off:s}}function gr(t,e){let i=mr(t,e),n=t.state.selection;return i?{update(t){if(t.docChanged){let e=t.changes.mapPos(t.startState.doc.line(i.line).from),s=t.state.doc.lineAt(e);i={line:s.number,col:i.col,off:Math.min(i.off,s.length)},n=n.map(t.changes)}},get(e,s,r){let o=mr(t,e);if(!o)return n;let l=function(t,e,i){let n=Math.min(e.line,i.line),s=Math.max(e.line,i.line),r=[];if(e.off>pr||i.off>pr||e.col<0||i.col<0){let o=Math.min(e.off,i.off),l=Math.max(e.off,i.off);for(let e=n;e<=s;e++){let i=t.doc.line(e);i.length<=l&&r.push(E.range(i.from+o,i.to+l))}}else{let o=Math.min(e.col,i.col),l=Math.max(e.col,i.col);for(let e=n;e<=s;e++){let i=t.doc.line(e),n=qt(i.text,o,t.tabSize,!0);if(n<0)r.push(E.cursor(i.to));else{let e=qt(i.text,l,t.tabSize);r.push(E.range(i.from+n,i.from+e))}}}return r}(t.state,i,o);return l.length?r?E.create(l.concat(n.ranges)):E.create(l):n}}:null}function vr(t){let e=(null==t?void 0:t.eventFilter)||(t=>t.altKey&&0==t.button);return Ds.mouseSelectionStyle.of(((t,i)=>e(i)?gr(t,i):null))}const wr={Alt:[18,t=>t.altKey],Control:[17,t=>t.ctrlKey],Shift:[16,t=>t.shiftKey],Meta:[91,t=>t.metaKey]},yr={style:"cursor: crosshair"};function br(t={}){let[e,i]=wr[t.key||"Alt"],n=Mi.fromClass(class{constructor(t){this.view=t,this.isDown=!1}set(t){this.isDown!=t&&(this.isDown=t,this.view.update([]))}},{eventHandlers:{keydown(t){this.set(t.keyCode==e||i(t))},keyup(t){t.keyCode!=e&&i(t)||this.set(!1)},mousemove(t){this.set(i(t))}}});return[n,Ds.contentAttributes.of((t=>{var e;return(null===(e=t.plugin(n))||void 0===e?void 0:e.isDown)?yr:null}))]}const xr="-10000px";class kr{constructor(t,e,i){this.facet=e,this.createTooltipView=i,this.input=t.state.facet(e),this.tooltips=this.input.filter((t=>t)),this.tooltipViews=this.tooltips.map(i)}update(t){var e;let i=t.state.facet(this.facet),n=i.filter((t=>t));if(i===this.input){for(let e of this.tooltipViews)e.update&&e.update(t);return!1}let s=[];for(let e=0;e{var e,i,n;return{position:ze.ios?"absolute":(null===(e=t.find((t=>t.position)))||void 0===e?void 0:e.position)||"fixed",parent:(null===(i=t.find((t=>t.parent)))||void 0===i?void 0:i.parent)||null,tooltipSpace:(null===(n=t.find((t=>t.tooltipSpace)))||void 0===n?void 0:n.tooltipSpace)||Sr}}}),Ar=Mi.fromClass(class{constructor(t){this.view=t,this.inView=!0,this.lastTransaction=0,this.measureTimeout=-1;let e=t.state.facet(Cr);this.position=e.position,this.parent=e.parent,this.classes=t.themeClasses,this.createContainer(),this.measureReq={read:this.readMeasure.bind(this),write:this.writeMeasure.bind(this),key:this},this.manager=new kr(t,Dr,(t=>this.createTooltip(t))),this.intersectionObserver="function"==typeof IntersectionObserver?new IntersectionObserver((t=>{Date.now()>this.lastTransaction-50&&t.length>0&&t[t.length-1].intersectionRatio<1&&this.measureSoon()}),{threshold:[1]}):null,this.observeIntersection(),t.win.addEventListener("resize",this.measureSoon=this.measureSoon.bind(this)),this.maybeMeasure()}createContainer(){this.parent?(this.container=document.createElement("div"),this.container.style.position="relative",this.container.className=this.view.themeClasses,this.parent.appendChild(this.container)):this.container=this.view.dom}observeIntersection(){if(this.intersectionObserver){this.intersectionObserver.disconnect();for(let t of this.manager.tooltipViews)this.intersectionObserver.observe(t.dom)}}measureSoon(){this.measureTimeout<0&&(this.measureTimeout=setTimeout((()=>{this.measureTimeout=-1,this.maybeMeasure()}),50))}update(t){t.transactions.length&&(this.lastTransaction=Date.now());let e=this.manager.update(t);e&&this.observeIntersection();let i=e||t.geometryChanged,n=t.state.facet(Cr);if(n.position!=this.position){this.position=n.position;for(let t of this.manager.tooltipViews)t.dom.style.position=this.position;i=!0}if(n.parent!=this.parent){this.parent&&this.container.remove(),this.parent=n.parent,this.createContainer();for(let t of this.manager.tooltipViews)this.container.appendChild(t.dom);i=!0}else this.parent&&this.view.themeClasses!=this.classes&&(this.classes=this.container.className=this.view.themeClasses);i&&this.maybeMeasure()}createTooltip(t){let e=t.create(this.view);if(e.dom.classList.add("cm-tooltip"),t.arrow&&!e.dom.querySelector(".cm-tooltip > .cm-tooltip-arrow")){let t=document.createElement("div");t.className="cm-tooltip-arrow",e.dom.appendChild(t)}return e.dom.style.position=this.position,e.dom.style.top=xr,this.container.appendChild(e.dom),e.mount&&e.mount(this.view),e}destroy(){var t,e;this.view.win.removeEventListener("resize",this.measureSoon);for(let e of this.manager.tooltipViews)e.dom.remove(),null===(t=e.destroy)||void 0===t||t.call(e);null===(e=this.intersectionObserver)||void 0===e||e.disconnect(),clearTimeout(this.measureTimeout)}readMeasure(){let t=this.view.dom.getBoundingClientRect();return{editor:t,parent:this.parent?this.container.getBoundingClientRect():t,pos:this.manager.tooltips.map(((t,e)=>{let i=this.manager.tooltipViews[e];return i.getCoords?i.getCoords(t.pos):this.view.coordsAtPos(t.pos)})),size:this.manager.tooltipViews.map((({dom:t})=>t.getBoundingClientRect())),space:this.view.state.facet(Cr).tooltipSpace(this.view)}}writeMeasure(t){let{editor:e,space:i}=t,n=[];for(let s=0;s=Math.min(e.bottom,i.bottom)||a.rightMath.min(e.right,i.right)+.1){l.style.top=xr;continue}let c=r.arrow?o.dom.querySelector(".cm-tooltip-arrow"):null,u=c?7:0,f=h.right-h.left,d=h.bottom-h.top,p=o.offset||Mr,m=this.view.textDirection==Vi.LTR,g=h.width>i.right-i.left?m?i.left:i.right-h.width:m?Math.min(a.left-(c?14:0)+p.x,i.right-f):Math.max(i.left,a.left-f+(c?14:0)-p.x),v=!!r.above;!r.strictSide&&(v?a.top-(h.bottom-h.top)-p.yi.bottom)&&v==i.bottom-a.bottom>a.top-i.top&&(v=!v);let w=v?a.top-d-u-p.y:a.bottom+u+p.y,y=g+f;if(!0!==o.overlap)for(let t of n)t.leftg&&t.topw&&(w=v?t.top-d-2-u:t.bottom+u+2);"absolute"==this.position?(l.style.top=w-t.parent.top+"px",l.style.left=g-t.parent.left+"px"):(l.style.top=w+"px",l.style.left=g+"px"),c&&(c.style.left=a.left+(m?p.x:-p.x)-(g+14-7)+"px"),!0!==o.overlap&&n.push({left:g,top:w,right:y,bottom:w+d}),l.classList.toggle("cm-tooltip-above",v),l.classList.toggle("cm-tooltip-below",!v),o.positioned&&o.positioned(t.space)}}maybeMeasure(){if(this.manager.tooltips.length&&(this.view.inView&&this.view.requestMeasure(this.measureReq),this.inView!=this.view.inView&&(this.inView=this.view.inView,!this.inView)))for(let t of this.manager.tooltipViews)t.dom.style.top=xr}},{eventHandlers:{scroll(){this.maybeMeasure()}}}),Or=Ds.baseTheme({".cm-tooltip":{zIndex:100},"&light .cm-tooltip":{border:"1px solid #bbb",backgroundColor:"#f5f5f5"},"&light .cm-tooltip-section:not(:first-child)":{borderTop:"1px solid #bbb"},"&dark .cm-tooltip":{backgroundColor:"#333338",color:"white"},".cm-tooltip-arrow":{height:"7px",width:"14px",position:"absolute",zIndex:-1,overflow:"hidden","&:before, &:after":{content:"''",position:"absolute",width:0,height:0,borderLeft:"7px solid transparent",borderRight:"7px solid transparent"},".cm-tooltip-above &":{bottom:"-7px","&:before":{borderTop:"7px solid #bbb"},"&:after":{borderTop:"7px solid #f5f5f5",bottom:"1px"}},".cm-tooltip-below &":{top:"-7px","&:before":{borderBottom:"7px solid #bbb"},"&:after":{borderBottom:"7px solid #f5f5f5",top:"1px"}}},"&dark .cm-tooltip .cm-tooltip-arrow":{"&:before":{borderTopColor:"#333338",borderBottomColor:"#333338"},"&:after":{borderTopColor:"transparent",borderBottomColor:"transparent"}}}),Mr={x:0,y:0},Dr=N.define({enables:[Ar,Or]}),Tr=N.define();class Pr{constructor(t){this.view=t,this.mounted=!1,this.dom=document.createElement("div"),this.dom.classList.add("cm-tooltip-hover"),this.manager=new kr(t,Tr,(t=>this.createHostedView(t)))}static create(t){return new Pr(t)}createHostedView(t){let e=t.create(this.view);return e.dom.classList.add("cm-tooltip-section"),this.dom.appendChild(e.dom),this.mounted&&e.mount&&e.mount(this.view),e}mount(t){for(let e of this.manager.tooltipViews)e.mount&&e.mount(t);this.mounted=!0}positioned(t){for(let e of this.manager.tooltipViews)e.positioned&&e.positioned(t)}update(t){this.manager.update(t)}}const Rr=Dr.compute([Tr],(t=>{let e=t.facet(Tr).filter((t=>t));return 0===e.length?null:{pos:Math.min(...e.map((t=>t.pos))),end:Math.max(...e.filter((t=>null!=t.end)).map((t=>t.end))),create:Pr.create,above:e[0].above,arrow:e.some((t=>t.arrow))}}));class Er{constructor(t,e,i,n,s){this.view=t,this.source=e,this.field=i,this.setHover=n,this.hoverTime=s,this.hoverTimeout=-1,this.restartTimeout=-1,this.pending=null,this.lastMove={x:0,y:0,target:t.dom,time:0},this.checkHover=this.checkHover.bind(this),t.dom.addEventListener("mouseleave",this.mouseleave=this.mouseleave.bind(this)),t.dom.addEventListener("mousemove",this.mousemove=this.mousemove.bind(this))}update(){this.pending&&(this.pending=null,clearTimeout(this.restartTimeout),this.restartTimeout=setTimeout((()=>this.startHover()),20))}get active(){return this.view.state.field(this.field)}checkHover(){if(this.hoverTimeout=-1,this.active)return;let t=Date.now()-this.lastMove.time;ti.bottom||t.xi.right+this.view.defaultCharacterWidth)return;let n=this.view.bidiSpans(this.view.state.doc.lineAt(e)).find((t=>t.from<=e&&t.to>=e)),s=n&&n.dir==Vi.RTL?-1:1,r=this.source(this.view,e,t.x{this.pending==t&&(this.pending=null,e&&this.view.dispatch({effects:this.setHover.of(e)}))}),(t=>Si(this.view.state,t,"hover tooltip")))}else r&&this.view.dispatch({effects:this.setHover.of(r)})}mousemove(t){var e;this.lastMove={x:t.clientX,y:t.clientY,target:t.target,time:Date.now()},this.hoverTimeout<0&&(this.hoverTimeout=setTimeout(this.checkHover,this.hoverTime));let i=this.active;if(i&&!Br(this.lastMove.target)||this.pending){let{pos:n}=i||this.pending,s=null!==(e=null==i?void 0:i.end)&&void 0!==e?e:n;(n==s?this.view.posAtCoords(this.lastMove)==n:function(t,e,i,n,s,r){let o=document.createRange(),l=t.domAtPos(e),a=t.domAtPos(i);o.setEnd(a.node,a.offset),o.setStart(l.node,l.offset);let h=o.getClientRects();o.detach();for(let t=0;tnull,update(t,n){if(t&&(e.hideOnChange&&(n.docChanged||n.selection)||e.hideOn&&e.hideOn(n,t)))return null;if(t&&n.docChanged){let e=n.changes.mapPos(t.pos,-1,k.TrackDel);if(null==e)return null;let i=Object.assign(Object.create(null),t);i.pos=e,null!=t.end&&(i.end=n.changes.mapPos(t.end)),t=i}for(let e of n.effects)e.is(i)&&(t=e.value),e.is(Nr)&&(t=null);return t},provide:t=>Tr.from(t)});return[n,Mi.define((s=>new Er(s,t,n,i,e.hoverTime||300))),Rr]}const Nr=ut.define(),Ir=N.define({combine(t){let e,i;for(let n of t)e=e||n.topContainer,i=i||n.bottomContainer;return{topContainer:e,bottomContainer:i}}});function Vr(t,e){let i=t.plugin(Wr),n=i?i.specs.indexOf(e):-1;return n>-1?i.panels[n]:null}const Wr=Mi.fromClass(class{constructor(t){this.input=t.state.facet(Fr),this.specs=this.input.filter((t=>t)),this.panels=this.specs.map((e=>e(t)));let e=t.state.facet(Ir);this.top=new zr(t,!0,e.topContainer),this.bottom=new zr(t,!1,e.bottomContainer),this.top.sync(this.panels.filter((t=>t.top))),this.bottom.sync(this.panels.filter((t=>!t.top)));for(let t of this.panels)t.dom.classList.add("cm-panel"),t.mount&&t.mount()}update(t){let e=t.state.facet(Ir);this.top.container!=e.topContainer&&(this.top.sync([]),this.top=new zr(t.view,!0,e.topContainer)),this.bottom.container!=e.bottomContainer&&(this.bottom.sync([]),this.bottom=new zr(t.view,!1,e.bottomContainer)),this.top.syncClasses(),this.bottom.syncClasses();let i=t.state.facet(Fr);if(i!=this.input){let e=i.filter((t=>t)),n=[],s=[],r=[],o=[];for(let i of e){let e,l=this.specs.indexOf(i);l<0?(e=i(t.view),o.push(e)):(e=this.panels[l],e.update&&e.update(t)),n.push(e),(e.top?s:r).push(e)}this.specs=e,this.panels=n,this.top.sync(s),this.bottom.sync(r);for(let t of o)t.dom.classList.add("cm-panel"),t.mount&&t.mount()}else for(let e of this.panels)e.update&&e.update(t)}destroy(){this.top.sync([]),this.bottom.sync([])}},{provide:t=>Ds.scrollMargins.of((e=>{let i=e.plugin(t);return i&&{top:i.top.scrollMargin(),bottom:i.bottom.scrollMargin()}}))});class zr{constructor(t,e,i){this.view=t,this.top=e,this.container=i,this.dom=void 0,this.classes="",this.panels=[],this.syncClasses()}sync(t){for(let e of this.panels)e.destroy&&t.indexOf(e)<0&&e.destroy();this.panels=t,this.syncDOM()}syncDOM(){if(0==this.panels.length)return void(this.dom&&(this.dom.remove(),this.dom=void 0));if(!this.dom){this.dom=document.createElement("div"),this.dom.className=this.top?"cm-panels cm-panels-top":"cm-panels cm-panels-bottom",this.dom.style[this.top?"top":"bottom"]="0";let t=this.container||this.view.dom;t.insertBefore(this.dom,this.top?t.firstChild:null)}let t=this.dom.firstChild;for(let e of this.panels)if(e.dom.parentNode==this.dom){for(;t!=e.dom;)t=Hr(t);t=t.nextSibling}else this.dom.insertBefore(e.dom,t);for(;t;)t=Hr(t)}scrollMargin(){return!this.dom||this.container?0:Math.max(0,this.top?this.dom.getBoundingClientRect().bottom-Math.max(0,this.view.scrollDOM.getBoundingClientRect().top):Math.min(innerHeight,this.view.scrollDOM.getBoundingClientRect().bottom)-this.dom.getBoundingClientRect().top)}syncClasses(){if(this.container&&this.classes!=this.view.themeClasses){for(let t of this.classes.split(" "))t&&this.container.classList.remove(t);for(let t of(this.classes=this.view.themeClasses).split(" "))t&&this.container.classList.add(t)}}}function Hr(t){let e=t.nextSibling;return t.remove(),e}const Fr=N.define({enables:Wr});class qr extends At{compare(t){return this==t||this.constructor==t.constructor&&this.eq(t)}eq(t){return!1}destroy(t){}}qr.prototype.elementClass="",qr.prototype.toDOM=void 0,qr.prototype.mapMode=k.TrackBefore,qr.prototype.startSide=qr.prototype.endSide=-1,qr.prototype.point=!0;const _r=N.define(),jr={class:"",renderEmptyElements:!1,elementStyle:"",markers:()=>Tt.empty,lineMarker:()=>null,lineMarkerChange:null,initialSpacer:null,updateSpacer:null,domEventHandlers:{}},Ur=N.define();function $r(t){return[Kr(),Ur.of(Object.assign(Object.assign({},jr),t))]}const Qr=N.define({combine:t=>t.some((t=>t))});function Kr(t){let e=[Gr];return t&&!1===t.fixed&&e.push(Qr.of(!0)),e}const Gr=Mi.fromClass(class{constructor(t){this.view=t,this.prevViewport=t.viewport,this.dom=document.createElement("div"),this.dom.className="cm-gutters",this.dom.setAttribute("aria-hidden","true"),this.dom.style.minHeight=this.view.contentHeight+"px",this.gutters=t.state.facet(Ur).map((e=>new Yr(t,e)));for(let t of this.gutters)this.dom.appendChild(t.dom);this.fixed=!t.state.facet(Qr),this.fixed&&(this.dom.style.position="sticky"),this.syncGutters(!1),t.scrollDOM.insertBefore(this.dom,t.contentDOM)}update(t){if(this.updateGutters(t)){let e=this.prevViewport,i=t.view.viewport,n=Math.min(e.to,i.to)-Math.max(e.from,i.from);this.syncGutters(n<.8*(i.to-i.from))}t.geometryChanged&&(this.dom.style.minHeight=this.view.contentHeight+"px"),this.view.state.facet(Qr)!=!this.fixed&&(this.fixed=!this.fixed,this.dom.style.position=this.fixed?"sticky":""),this.prevViewport=t.view.viewport}syncGutters(t){let e=this.dom.nextSibling;t&&this.dom.remove();let i=Tt.iter(this.view.state.facet(_r),this.view.viewport.from),n=[],s=this.gutters.map((t=>new Zr(t,this.view.viewport,-this.view.documentPadding.top)));for(let t of this.view.viewportLineBlocks){let e;if(Array.isArray(t.type)){for(let i of t.type)if(i.type==ei.Text){e=i;break}}else e=t.type==ei.Text?t:void 0;if(e){n.length&&(n=[]),Xr(i,n,t.from);for(let t of s)t.line(this.view,e,n)}}for(let t of s)t.finish();t&&this.view.scrollDOM.insertBefore(this.dom,e)}updateGutters(t){let e=t.startState.facet(Ur),i=t.state.facet(Ur),n=t.docChanged||t.heightChanged||t.viewportChanged||!Tt.eq(t.startState.facet(_r),t.state.facet(_r),t.view.viewport.from,t.view.viewport.to);if(e==i)for(let e of this.gutters)e.update(t)&&(n=!0);else{n=!0;let s=[];for(let n of i){let i=e.indexOf(n);i<0?s.push(new Yr(this.view,n)):(this.gutters[i].update(t),s.push(this.gutters[i]))}for(let t of this.gutters)t.dom.remove(),s.indexOf(t)<0&&t.destroy();for(let t of s)this.dom.appendChild(t.dom);this.gutters=s}return n}destroy(){for(let t of this.gutters)t.destroy();this.dom.remove()}},{provide:t=>Ds.scrollMargins.of((e=>{let i=e.plugin(t);return i&&0!=i.gutters.length&&i.fixed?e.textDirection==Vi.LTR?{left:i.dom.offsetWidth}:{right:i.dom.offsetWidth}:null}))});function Jr(t){return Array.isArray(t)?t:[t]}function Xr(t,e,i){for(;t.value&&t.from<=i;)t.from==i&&e.push(t.value),t.next()}class Zr{constructor(t,e,i){this.gutter=t,this.height=i,this.localMarkers=[],this.i=0,this.cursor=Tt.iter(t.markers,e.from)}line(t,e,i){this.localMarkers.length&&(this.localMarkers=[]),Xr(this.cursor,this.localMarkers,e.from);let n=i.length?this.localMarkers.concat(i):this.localMarkers,s=this.gutter.config.lineMarker(t,e,n);s&&n.unshift(s);let r=this.gutter;if(0==n.length&&!r.config.renderEmptyElements)return;let o=e.top-this.height;if(this.i==r.elements.length){let i=new to(t,e.height,o,n);r.elements.push(i),r.dom.appendChild(i.dom)}else r.elements[this.i].update(t,e.height,o,n);this.height=e.bottom,this.i++}finish(){let t=this.gutter;for(;t.elements.length>this.i;){let e=t.elements.pop();t.dom.removeChild(e.dom),e.destroy()}}}class Yr{constructor(t,e){this.view=t,this.config=e,this.elements=[],this.spacer=null,this.dom=document.createElement("div"),this.dom.className="cm-gutter"+(this.config.class?" "+this.config.class:"");for(let i in e.domEventHandlers)this.dom.addEventListener(i,(n=>{let s=t.lineBlockAtHeight(n.clientY-t.documentTop);e.domEventHandlers[i](t,s,n)&&n.preventDefault()}));this.markers=Jr(e.markers(t)),e.initialSpacer&&(this.spacer=new to(t,0,0,[e.initialSpacer(t)]),this.dom.appendChild(this.spacer.dom),this.spacer.dom.style.cssText+="visibility: hidden; pointer-events: none")}update(t){let e=this.markers;if(this.markers=Jr(this.config.markers(t.view)),this.spacer&&this.config.updateSpacer){let e=this.config.updateSpacer(this.spacer.markers[0],t);e!=this.spacer.markers[0]&&this.spacer.update(t.view,0,0,[e])}let i=t.view.viewport;return!Tt.eq(this.markers,e,i.from,i.to)||!!this.config.lineMarkerChange&&this.config.lineMarkerChange(t)}destroy(){for(let t of this.elements)t.destroy()}}class to{constructor(t,e,i,n){this.height=-1,this.above=0,this.markers=[],this.dom=document.createElement("div"),this.dom.className="cm-gutterElement",this.update(t,e,i,n)}update(t,e,i,n){this.height!=e&&(this.dom.style.height=(this.height=e)+"px"),this.above!=i&&(this.dom.style.marginTop=(this.above=i)?i+"px":""),function(t,e){if(t.length!=e.length)return!1;for(let i=0;iCt(t,{formatNumber:String,domEventHandlers:{}},{domEventHandlers(t,e){let i=Object.assign({},t);for(let t in e){let n=i[t],s=e[t];i[t]=n?(t,e,i)=>n(t,e,i)||s(t,e,i):s}return i}})});class no extends qr{constructor(t){super(),this.number=t}eq(t){return this.number==t.number}toDOM(){return document.createTextNode(this.number)}}function so(t,e){return t.state.facet(io).formatNumber(e,t.state)}const ro=Ur.compute([io],(t=>({class:"cm-lineNumbers",renderEmptyElements:!1,markers:t=>t.state.facet(eo),lineMarker:(t,e,i)=>i.some((t=>t.toDOM))?null:new no(so(t,t.state.doc.lineAt(e.from).number)),lineMarkerChange:t=>t.startState.facet(io)!=t.state.facet(io),initialSpacer:t=>new no(so(t,lo(t.state.doc.lines))),updateSpacer(t,e){let i=so(e.view,lo(e.view.state.doc.lines));return i==t.number?t:new no(i)},domEventHandlers:t.facet(io).domEventHandlers})));function oo(t={}){return[io.of(t),Kr(),ro]}function lo(t){let e=9;for(;e{let e=[],i=-1;for(let n of t.selection.ranges){let s=t.doc.lineAt(n.head).from;s>i&&(i=s,e.push(ao.range(s)))}return Tt.of(e)}));const co=1024;let uo=0;class fo{constructor(t,e){this.from=t,this.to=e}}class po{constructor(t={}){this.id=uo++,this.perNode=!!t.perNode,this.deserialize=t.deserialize||(()=>{throw new Error("This node type doesn't define a deserialize function")})}add(t){if(this.perNode)throw new RangeError("Can't add per-node props to node types");return"function"!=typeof t&&(t=go.match(t)),e=>{let i=t(e);return void 0===i?null:[this,i]}}}po.closedBy=new po({deserialize:t=>t.split(" ")}),po.openedBy=new po({deserialize:t=>t.split(" ")}),po.group=new po({deserialize:t=>t.split(" ")}),po.contextHash=new po({perNode:!0}),po.lookAhead=new po({perNode:!0}),po.mounted=new po({perNode:!0});const mo=Object.create(null);class go{constructor(t,e,i,n=0){this.name=t,this.props=e,this.id=i,this.flags=n}static define(t){let e=t.props&&t.props.length?Object.create(null):mo,i=(t.top?1:0)|(t.skipped?2:0)|(t.error?4:0)|(null==t.name?8:0),n=new go(t.name||"",e,t.id,i);if(t.props)for(let i of t.props)if(Array.isArray(i)||(i=i(n)),i){if(i[0].perNode)throw new RangeError("Can't store a per-node prop on a node type");e[i[0].id]=i[1]}return n}prop(t){return this.props[t.id]}get isTop(){return(1&this.flags)>0}get isSkipped(){return(2&this.flags)>0}get isError(){return(4&this.flags)>0}get isAnonymous(){return(8&this.flags)>0}is(t){if("string"==typeof t){if(this.name==t)return!0;let e=this.prop(po.group);return!!e&&e.indexOf(t)>-1}return this.id==t}static match(t){let e=Object.create(null);for(let i in t)for(let n of i.split(" "))e[n]=t[i];return t=>{for(let i=t.prop(po.group),n=-1;n<(i?i.length:0);n++){let s=e[n<0?t.name:i[n]];if(s)return s}}}}go.none=new go("",Object.create(null),0,8);class vo{constructor(t){this.types=t;for(let e=0;e=n&&(r.type.isAnonymous||!1!==e(r))){if(r.firstChild())continue;t=!0}for(;t&&i&&!r.type.isAnonymous&&i(r),!r.nextSibling();){if(!r.parent())return;t=!0}}}prop(t){return t.perNode?this.props?this.props[t.id]:void 0:this.type.prop(t)}get propValues(){let t=[];if(this.props)for(let e in this.props)t.push([+e,this.props[e]]);return t}balance(t={}){return this.children.length<=8?this:Io(go.none,this.children,this.positions,0,this.children.length,0,this.length,((t,e,i)=>new xo(this.type,t,e,i,this.propValues)),t.makeTree||((t,e,i)=>new xo(go.none,t,e,i)))}static build(t){return function(t){var e;let{buffer:i,nodeSet:n,maxBufferLength:s=co,reused:r=[],minRepeatType:o=n.types.length}=t,l=Array.isArray(i)?new ko(i,i.length):i,a=n.types,h=0,c=0;function u(t,e,i,v,w){let{id:y,start:b,end:x,size:k}=l,S=c;for(;k<0;){if(l.next(),-1==k){let e=r[y];return i.push(e),void v.push(b-t)}if(-3==k)return void(h=y);if(-4==k)return void(c=y);throw new RangeError(`Unrecognized record size: ${k}`)}let C,A,O=a[y],M=b-t;if(x-b<=s&&(A=m(l.pos-e,w))){let e=new Uint16Array(A.size-A.skip),i=l.pos-A.size,s=e.length;for(;l.pos>i;)s=g(A.start,e,s);C=new So(e,x-A.start,n),M=A.start-t}else{let t=l.pos-k;l.next();let e=[],i=[],n=y>=o?y:-1,r=0,a=x;for(;l.pos>t;)n>=0&&l.id==n&&l.size>=0?(l.end<=a-s&&(d(e,i,b,r,l.end,a,n,S),r=e.length,a=l.end),l.next()):u(b,t,e,i,n);if(n>=0&&r>0&&r-1&&r>0){let t=f(O);C=Io(O,e,i,0,e.length,0,x-b,t,t)}else C=p(O,e,i,x-b,S-x)}i.push(C),v.push(M)}function f(t){return(e,i,n)=>{let s,r,o=0,l=e.length-1;if(l>=0&&(s=e[l])instanceof xo){if(!l&&s.type==t&&s.length==n)return s;(r=s.prop(po.lookAhead))&&(o=i[l]+s.length+r)}return p(t,e,i,n,o)}}function d(t,e,i,s,r,o,l,a){let h=[],c=[];for(;t.length>s;)h.push(t.pop()),c.push(e.pop()+i-r);t.push(p(n.types[l],h,c,o-r,a-o)),e.push(r-i)}function p(t,e,i,n,s=0,r){if(h){let t=[po.contextHash,h];r=r?[t].concat(r):[t]}if(s>25){let t=[po.lookAhead,s];r=r?[t].concat(r):[t]}return new xo(t,e,i,n,r)}function m(t,e){let i=l.fork(),n=0,r=0,a=0,h=i.end-s,c={size:0,start:0,skip:0};t:for(let s=i.pos-t;i.pos>s;){let t=i.size;if(i.id==e&&t>=0){c.size=n,c.start=r,c.skip=a,a+=4,n+=4,i.next();continue}let l=i.pos-t;if(t<0||l=o?4:0,f=i.start;for(i.next();i.pos>l;){if(i.size<0){if(-3!=i.size)break t;u+=4}else i.id>=o&&(u+=4);i.next()}r=f,n+=t,a+=u}return(e<0||n==t)&&(c.size=n,c.start=r,c.skip=a),c.size>4?c:void 0}function g(t,e,i){let{id:n,start:s,end:r,size:a}=l;if(l.next(),a>=0&&n4){let n=l.pos-(a-4);for(;l.pos>n;)i=g(t,e,i)}e[--i]=o,e[--i]=r-t,e[--i]=s-t,e[--i]=n}else-3==a?h=n:-4==a&&(c=n);return i}let v=[],w=[];for(;l.pos>0;)u(t.start||0,t.bufferStart||0,v,w,-1);let y=null!==(e=t.length)&&void 0!==e?e:v.length?w[0]+v[0].length:0;return new xo(a[t.topID],v.reverse(),w.reverse(),y)}(t)}}xo.empty=new xo(go.none,[],[],0);class ko{constructor(t,e){this.buffer=t,this.index=e}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}get pos(){return this.index}next(){this.index-=4}fork(){return new ko(this.buffer,this.index)}}class So{constructor(t,e,i){this.buffer=t,this.length=e,this.set=i}get type(){return go.none}toString(){let t=[];for(let e=0;e0));l=r[l+3]);return o}slice(t,e,i,n){let s=this.buffer,r=new Uint16Array(e-t);for(let n=t,o=0;n=e&&ie;case 1:return i<=e&&n>e;case 2:return n>e;case 4:return!0}}function Ao(t,e){let i=t.childBefore(e);for(;i;){let e=i.lastChild;if(!e||e.to!=i.to)break;e.type.isError&&e.from==e.to?(t=i,i=e.prevSibling):i=e}return t}function Oo(t,e,i,n){for(var s;t.from==t.to||(i<1?t.from>=e:t.from>e)||(i>-1?t.to<=e:t.to0?o.length:-1;t!=a;t+=e){let a=o[t],h=l[t]+r.from;if(Co(n,i,h,h+a.length))if(a instanceof So){if(s&bo.ExcludeBuffers)continue;let o=a.findChild(0,a.buffer.length,e,i-h,n);if(o>-1)return new Ro(new Po(r,a,t,h),null,o)}else if(s&bo.IncludeAnonymous||!a.type.isAnonymous||Bo(a)){let o;if(!(s&bo.IgnoreMounts)&&a.props&&(o=a.prop(po.mounted))&&!o.overlay)return new Mo(o.tree,h,t,r);let l=new Mo(a,h,t,r);return s&bo.IncludeAnonymous||!l.type.isAnonymous?l:l.nextChild(e<0?a.children.length-1:0,e,i,n)}}if(s&bo.IncludeAnonymous||!r.type.isAnonymous)return null;if(t=r.index>=0?r.index+e:e<0?-1:r._parent._tree.children.length,r=r._parent,!r)return null}}get firstChild(){return this.nextChild(0,1,0,4)}get lastChild(){return this.nextChild(this._tree.children.length-1,-1,0,4)}childAfter(t){return this.nextChild(0,1,t,2)}childBefore(t){return this.nextChild(this._tree.children.length-1,-1,t,-2)}enter(t,e,i=0){let n;if(!(i&bo.IgnoreOverlays)&&(n=this._tree.prop(po.mounted))&&n.overlay){let i=t-this.from;for(let{from:t,to:s}of n.overlay)if((e>0?t<=i:t=i:s>i))return new Mo(n.tree,n.overlay[0].from+this.from,-1,this)}return this.nextChild(0,1,t,e,i)}nextSignificantParent(){let t=this;for(;t.type.isAnonymous&&t._parent;)t=t._parent;return t}get parent(){return this._parent?this._parent.nextSignificantParent():null}get nextSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index+1,1,0,4):null}get prevSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index-1,-1,0,4):null}cursor(t=0){return new Eo(this,t)}get tree(){return this._tree}toTree(){return this._tree}resolve(t,e=0){return Oo(this,t,e,!1)}resolveInner(t,e=0){return Oo(this,t,e,!0)}enterUnfinishedNodesBefore(t){return Ao(this,t)}getChild(t,e=null,i=null){let n=Do(this,t,e,i);return n.length?n[0]:null}getChildren(t,e=null,i=null){return Do(this,t,e,i)}toString(){return this._tree.toString()}get node(){return this}matchContext(t){return To(this,t)}}function Do(t,e,i,n){let s=t.cursor(),r=[];if(!s.firstChild())return r;if(null!=i)for(;!s.type.is(i);)if(!s.nextSibling())return r;for(;;){if(null!=n&&s.type.is(n))return r;if(s.type.is(e)&&r.push(s.node),!s.nextSibling())return null==n?r:[]}}function To(t,e,i=e.length-1){for(let n=t.parent;i>=0;n=n.parent){if(!n)return!1;if(!n.type.isAnonymous){if(e[i]&&e[i]!=n.name)return!1;i--}}return!0}class Po{constructor(t,e,i,n){this.parent=t,this.buffer=e,this.index=i,this.start=n}}class Ro{constructor(t,e,i){this.context=t,this._parent=e,this.index=i,this.type=t.buffer.set.types[t.buffer.buffer[i]]}get name(){return this.type.name}get from(){return this.context.start+this.context.buffer.buffer[this.index+1]}get to(){return this.context.start+this.context.buffer.buffer[this.index+2]}child(t,e,i){let{buffer:n}=this.context,s=n.findChild(this.index+4,n.buffer[this.index+3],t,e-this.context.start,i);return s<0?null:new Ro(this.context,this,s)}get firstChild(){return this.child(1,0,4)}get lastChild(){return this.child(-1,0,4)}childAfter(t){return this.child(1,t,2)}childBefore(t){return this.child(-1,t,-2)}enter(t,e,i=0){if(i&bo.ExcludeBuffers)return null;let{buffer:n}=this.context,s=n.findChild(this.index+4,n.buffer[this.index+3],e>0?1:-1,t-this.context.start,e);return s<0?null:new Ro(this.context,this,s)}get parent(){return this._parent||this.context.parent.nextSignificantParent()}externalSibling(t){return this._parent?null:this.context.parent.nextChild(this.context.index+t,t,0,4)}get nextSibling(){let{buffer:t}=this.context,e=t.buffer[this.index+3];return e<(this._parent?t.buffer[this._parent.index+3]:t.buffer.length)?new Ro(this.context,this._parent,e):this.externalSibling(1)}get prevSibling(){let{buffer:t}=this.context,e=this._parent?this._parent.index+4:0;return this.index==e?this.externalSibling(-1):new Ro(this.context,this._parent,t.findChild(e,this.index,-1,0,4))}cursor(t=0){return new Eo(this,t)}get tree(){return null}toTree(){let t=[],e=[],{buffer:i}=this.context,n=this.index+4,s=i.buffer[this.index+3];if(s>n){let r=i.buffer[this.index+1],o=i.buffer[this.index+2];t.push(i.slice(n,s,r,o)),e.push(0)}return new xo(this.type,t,e,this.to-this.from)}resolve(t,e=0){return Oo(this,t,e,!1)}resolveInner(t,e=0){return Oo(this,t,e,!0)}enterUnfinishedNodesBefore(t){return Ao(this,t)}toString(){return this.context.buffer.childString(this.index)}getChild(t,e=null,i=null){let n=Do(this,t,e,i);return n.length?n[0]:null}getChildren(t,e=null,i=null){return Do(this,t,e,i)}get node(){return this}matchContext(t){return To(this,t)}}class Eo{constructor(t,e=0){if(this.mode=e,this.buffer=null,this.stack=[],this.index=0,this.bufferNode=null,t instanceof Mo)this.yieldNode(t);else{this._tree=t.context.parent,this.buffer=t.context;for(let e=t._parent;e;e=e._parent)this.stack.unshift(e.index);this.bufferNode=t,this.yieldBuf(t.index)}}get name(){return this.type.name}yieldNode(t){return!!t&&(this._tree=t,this.type=t.type,this.from=t.from,this.to=t.to,!0)}yieldBuf(t,e){this.index=t;let{start:i,buffer:n}=this.buffer;return this.type=e||n.set.types[n.buffer[t]],this.from=i+n.buffer[t+1],this.to=i+n.buffer[t+2],!0}yield(t){return!!t&&(t instanceof Mo?(this.buffer=null,this.yieldNode(t)):(this.buffer=t.context,this.yieldBuf(t.index,t.type)))}toString(){return this.buffer?this.buffer.buffer.childString(this.index):this._tree.toString()}enterChild(t,e,i){if(!this.buffer)return this.yield(this._tree.nextChild(t<0?this._tree._tree.children.length-1:0,t,e,i,this.mode));let{buffer:n}=this.buffer,s=n.findChild(this.index+4,n.buffer[this.index+3],t,e-this.buffer.start,i);return!(s<0)&&(this.stack.push(this.index),this.yieldBuf(s))}firstChild(){return this.enterChild(1,0,4)}lastChild(){return this.enterChild(-1,0,4)}childAfter(t){return this.enterChild(1,t,2)}childBefore(t){return this.enterChild(-1,t,-2)}enter(t,e,i=this.mode){return this.buffer?!(i&bo.ExcludeBuffers)&&this.enterChild(1,t,e):this.yield(this._tree.enter(t,e,i))}parent(){if(!this.buffer)return this.yieldNode(this.mode&bo.IncludeAnonymous?this._tree._parent:this._tree.parent);if(this.stack.length)return this.yieldBuf(this.stack.pop());let t=this.mode&bo.IncludeAnonymous?this.buffer.parent:this.buffer.parent.nextSignificantParent();return this.buffer=null,this.yieldNode(t)}sibling(t){if(!this.buffer)return!!this._tree._parent&&this.yield(this._tree.index<0?null:this._tree._parent.nextChild(this._tree.index+t,t,0,4,this.mode));let{buffer:e}=this.buffer,i=this.stack.length-1;if(t<0){let t=i<0?0:this.stack[i]+4;if(this.index!=t)return this.yieldBuf(e.findChild(t,this.index,-1,0,4))}else{let t=e.buffer[this.index+3];if(t<(i<0?e.buffer.length:e.buffer[this.stack[i]+3]))return this.yieldBuf(t)}return i<0&&this.yield(this.buffer.parent.nextChild(this.buffer.index+t,t,0,4,this.mode))}nextSibling(){return this.sibling(1)}prevSibling(){return this.sibling(-1)}atLastNode(t){let e,i,{buffer:n}=this;if(n){if(t>0){if(this.index-1)for(let n=e+t,s=t<0?-1:i._tree.children.length;n!=s;n+=t){let t=i._tree.children[n];if(this.mode&bo.IncludeAnonymous||t instanceof So||!t.type.isAnonymous||Bo(t))return!1}return!0}move(t,e){if(e&&this.enterChild(t,0,4))return!0;for(;;){if(this.sibling(t))return!0;if(this.atLastNode(t)||!this.parent())return!1}}next(t=!0){return this.move(1,t)}prev(t=!0){return this.move(-1,t)}moveTo(t,e=0){for(;(this.from==this.to||(e<1?this.from>=t:this.from>t)||(e>-1?this.to<=t:this.to=0;){for(let r=t;r;r=r._parent)if(r.index==n){if(n==this.index)return r;e=r,i=s+1;break t}n=this.stack[--s]}for(let t=i;t=0;s--){if(s<0)return To(this.node,t,n);let r=i[e.buffer[this.stack[s]]];if(!r.isAnonymous){if(t[n]&&t[n]!=r.name)return!1;n--}}return!0}}function Bo(t){return t.children.some((t=>t instanceof So||!t.type.isAnonymous||Bo(t)))}const Lo=new WeakMap;function No(t,e){if(!t.isAnonymous||e instanceof So||e.type!=t)return 1;let i=Lo.get(e);if(null==i){i=1;for(let n of e.children){if(n.type!=t||!(n instanceof xo)){i=1;break}i+=No(t,n)}Lo.set(e,i)}return i}function Io(t,e,i,n,s,r,o,l,a){let h=0;for(let i=n;i=c)break;p+=e}if(h==s+1){if(p>c){let t=i[s];e(t.children,t.positions,0,t.children.length,n[s]+l);continue}u.push(i[s])}else{let e=n[h-1]+i[h-1].length-d;u.push(Io(t,i,n,s,h,d,e,null,a))}f.push(d+l-r)}}(e,i,n,s,0),(l||a)(u,f,o)}class Vo{constructor(t,e,i,n,s=!1,r=!1){this.from=t,this.to=e,this.tree=i,this.offset=n,this.open=(s?1:0)|(r?2:0)}get openStart(){return(1&this.open)>0}get openEnd(){return(2&this.open)>0}static addTree(t,e=[],i=!1){let n=[new Vo(0,t.length,t,0,!1,i)];for(let i of e)i.to>t.length&&n.push(i);return n}static applyChanges(t,e,i=128){if(!e.length)return t;let n=[],s=1,r=t.length?t[0]:null;for(let o=0,l=0,a=0;;o++){let h=o=i)for(;r&&r.from=e.from||c<=e.to||a){let t=Math.max(e.from,l)-a,i=Math.min(e.to,c)-a;e=t>=i?null:new Vo(t,i,e.tree,e.offset+a,o>0,!!h)}if(e&&n.push(e),r.to>c)break;r=snew fo(t.from,t.to))):[new fo(0,0)]:[new fo(0,t.length)],this.createParse(t,e||[],i)}parse(t,e,i){let n=this.startParse(t,e,i);for(;;){let t=n.advance();if(t)return t}}}class zo{constructor(t){this.string=t}get length(){return this.string.length}chunk(t){return this.string.slice(t)}get lineChunks(){return!1}read(t,e){return this.string.slice(t,e)}}new po({perNode:!0});let Ho=0;class Fo{constructor(t,e,i){this.set=t,this.base=e,this.modified=i,this.id=Ho++}static define(t){if(null==t?void 0:t.base)throw new Error("Can not derive from a modified tag");let e=new Fo([],null,[]);if(e.set.push(e),t)for(let i of t.set)e.set.push(i);return e}static defineModifier(){let t=new _o;return e=>e.modified.indexOf(t)>-1?e:_o.get(e.base||e,e.modified.concat(t).sort(((t,e)=>t.id-e.id)))}}let qo=0;class _o{constructor(){this.instances=[],this.id=qo++}static get(t,e){if(!e.length)return t;let i=e[0].instances.find((i=>{return i.base==t&&(n=e,s=i.modified,n.length==s.length&&n.every(((t,e)=>t==s[e])));var n,s}));if(i)return i;let n=[],s=new Fo(n,t,e);for(let t of e)t.instances.push(s);let r=function(t){let e=[[]];for(let i=0;ie.length-t.length))}(e);for(let e of t.set)if(!e.modified.length)for(let t of r)n.push(_o.get(e,t));return s}}function jo(t){let e=Object.create(null);for(let i in t){let n=t[i];Array.isArray(n)||(n=[n]);for(let t of i.split(" "))if(t){let i=[],s=2,r=t;for(let e=0;;){if("..."==r&&e>0&&e+3==t.length){s=1;break}let n=/^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(r);if(!n)throw new RangeError("Invalid path: "+t);if(i.push("*"==n[0]?"":'"'==n[0][0]?JSON.parse(n[0]):n[0]),e+=n[0].length,e==t.length)break;let o=t[e++];if(e==t.length&&"!"==o){s=0;break}if("/"!=o)throw new RangeError("Invalid path: "+t);r=t.slice(e)}let o=i.length-1,l=i[o];if(!l)throw new RangeError("Invalid path: "+t);let a=new $o(n,s,o>0?i.slice(0,o):null);e[l]=a.sort(e[l])}}return Uo.add(e)}const Uo=new po;class $o{constructor(t,e,i,n){this.tags=t,this.mode=e,this.context=i,this.next=n}get opaque(){return 0==this.mode}get inherit(){return 1==this.mode}sort(t){return!t||t.depth{let e=s;for(let n of t)for(let t of n.set){let n=i[t.id];if(n){e=e?e+" "+n:n;break}}return e},scope:n}}function Ko(t,e,i,n=0,s=t.length){let r=new Go(n,Array.isArray(e)?e:[e],i);r.highlightRange(t.cursor(),n,s,"",r.highlighters),r.flush(s)}$o.empty=new $o([],2,null);class Go{constructor(t,e,i){this.at=t,this.highlighters=e,this.span=i,this.class=""}startSpan(t,e){e!=this.class&&(this.flush(t),t>this.at&&(this.at=t),this.class=e)}flush(t){t>this.at&&this.class&&this.span(this.at,t,this.class)}highlightRange(t,e,i,n,s){let{type:r,from:o,to:l}=t;if(o>=i||l<=e)return;r.isTop&&(s=this.highlighters.filter((t=>!t.scope||t.scope(r))));let a=n,h=function(t){let e=t.type.prop(Uo);for(;e&&e.context&&!t.matchContext(e.context);)e=e.next;return e||null}(t)||$o.empty,c=function(t,e){let i=null;for(let n of t){let t=n.style(e);t&&(i=i?i+" "+t:t)}return i}(s,h.tags);if(c&&(a&&(a+=" "),a+=c,1==h.mode&&(n+=(n?" ":"")+c)),this.startSpan(t.from,a),h.opaque)return;let u=t.tree&&t.tree.prop(po.mounted);if(u&&u.overlay){let r=t.node.enter(u.overlay[0].from+o,1),h=this.highlighters.filter((t=>!t.scope||t.scope(u.tree.type))),c=t.firstChild();for(let f=0,d=o;;f++){let p=f=m)&&t.nextSibling()););if(!p||m>i)break;d=p.to+o,d>e&&(this.highlightRange(r.cursor(),Math.max(e,p.from+o),Math.min(i,d),n,h),this.startSpan(d,a))}c&&t.parent()}else if(t.firstChild()){do{if(!(t.to<=e)){if(t.from>=i)break;this.highlightRange(t,e,i,n,s),this.startSpan(Math.min(i,t.to),a)}}while(t.nextSibling());t.parent()}}}const Jo=Fo.define,Xo=Jo(),Zo=Jo(),Yo=Jo(Zo),tl=Jo(Zo),el=Jo(),il=Jo(el),nl=Jo(el),sl=Jo(),rl=Jo(sl),ol=Jo(),ll=Jo(),al=Jo(),hl=Jo(al),cl=Jo(),ul={comment:Xo,lineComment:Jo(Xo),blockComment:Jo(Xo),docComment:Jo(Xo),name:Zo,variableName:Jo(Zo),typeName:Yo,tagName:Jo(Yo),propertyName:tl,attributeName:Jo(tl),className:Jo(Zo),labelName:Jo(Zo),namespace:Jo(Zo),macroName:Jo(Zo),literal:el,string:il,docString:Jo(il),character:Jo(il),attributeValue:Jo(il),number:nl,integer:Jo(nl),float:Jo(nl),bool:Jo(el),regexp:Jo(el),escape:Jo(el),color:Jo(el),url:Jo(el),keyword:ol,self:Jo(ol),null:Jo(ol),atom:Jo(ol),unit:Jo(ol),modifier:Jo(ol),operatorKeyword:Jo(ol),controlKeyword:Jo(ol),definitionKeyword:Jo(ol),moduleKeyword:Jo(ol),operator:ll,derefOperator:Jo(ll),arithmeticOperator:Jo(ll),logicOperator:Jo(ll),bitwiseOperator:Jo(ll),compareOperator:Jo(ll),updateOperator:Jo(ll),definitionOperator:Jo(ll),typeOperator:Jo(ll),controlOperator:Jo(ll),punctuation:al,separator:Jo(al),bracket:hl,angleBracket:Jo(hl),squareBracket:Jo(hl),paren:Jo(hl),brace:Jo(hl),content:sl,heading:rl,heading1:Jo(rl),heading2:Jo(rl),heading3:Jo(rl),heading4:Jo(rl),heading5:Jo(rl),heading6:Jo(rl),contentSeparator:Jo(sl),list:Jo(sl),quote:Jo(sl),emphasis:Jo(sl),strong:Jo(sl),link:Jo(sl),monospace:Jo(sl),strikethrough:Jo(sl),inserted:Jo(),deleted:Jo(),changed:Jo(),invalid:Jo(),meta:cl,documentMeta:Jo(cl),annotation:Jo(cl),processingInstruction:Jo(cl),definition:Fo.defineModifier(),constant:Fo.defineModifier(),function:Fo.defineModifier(),standard:Fo.defineModifier(),local:Fo.defineModifier(),special:Fo.defineModifier()};var fl;Qo([{tag:ul.link,class:"tok-link"},{tag:ul.heading,class:"tok-heading"},{tag:ul.emphasis,class:"tok-emphasis"},{tag:ul.strong,class:"tok-strong"},{tag:ul.keyword,class:"tok-keyword"},{tag:ul.atom,class:"tok-atom"},{tag:ul.bool,class:"tok-bool"},{tag:ul.url,class:"tok-url"},{tag:ul.labelName,class:"tok-labelName"},{tag:ul.inserted,class:"tok-inserted"},{tag:ul.deleted,class:"tok-deleted"},{tag:ul.literal,class:"tok-literal"},{tag:ul.string,class:"tok-string"},{tag:ul.number,class:"tok-number"},{tag:[ul.regexp,ul.escape,ul.special(ul.string)],class:"tok-string2"},{tag:ul.variableName,class:"tok-variableName"},{tag:ul.local(ul.variableName),class:"tok-variableName tok-local"},{tag:ul.definition(ul.variableName),class:"tok-variableName tok-definition"},{tag:ul.special(ul.variableName),class:"tok-variableName2"},{tag:ul.definition(ul.propertyName),class:"tok-propertyName tok-definition"},{tag:ul.typeName,class:"tok-typeName"},{tag:ul.namespace,class:"tok-namespace"},{tag:ul.className,class:"tok-className"},{tag:ul.macroName,class:"tok-macroName"},{tag:ul.propertyName,class:"tok-propertyName"},{tag:ul.operator,class:"tok-operator"},{tag:ul.comment,class:"tok-comment"},{tag:ul.meta,class:"tok-meta"},{tag:ul.invalid,class:"tok-invalid"},{tag:ul.punctuation,class:"tok-punctuation"}]);const dl=new po;class pl{constructor(t,e,i=[],n=""){this.data=t,this.name=n,St.prototype.hasOwnProperty("tree")||Object.defineProperty(St.prototype,"tree",{get(){return vl(this)}}),this.parser=e,this.extension=[Ol.of(this),St.languageData.of(((t,e,i)=>t.facet(ml(t,e,i))))].concat(i)}isActiveAt(t,e,i=-1){return ml(t,e,i)==this.data}findRegions(t){let e=t.facet(Ol);if((null==e?void 0:e.data)==this.data)return[{from:0,to:t.doc.length}];if(!e||!e.allowsNesting)return[];let i=[],n=(t,e)=>{if(t.prop(dl)==this.data)return void i.push({from:e,to:e+t.length});let s=t.prop(po.mounted);if(s){if(s.tree.prop(dl)==this.data){if(s.overlay)for(let t of s.overlay)i.push({from:t.from+e,to:t.to+e});else i.push({from:e,to:e+t.length});return}if(s.overlay){let t=i.length;if(n(s.tree,s.overlay[0].from+e),i.length>t)return}}for(let i=0;it.concat(i):void 0}));var i;return new gl(e,t.parser.configure({props:[dl.add((t=>t.isTop?e:void 0))]}),t.name)}configure(t,e){return new gl(this.data,this.parser.configure(t),e||this.name)}get allowsNesting(){return this.parser.hasWrappers()}}function vl(t){let e=t.field(pl.state,!1);return e?e.tree:xo.empty}class wl{constructor(t,e=t.length){this.doc=t,this.length=e,this.cursorPos=0,this.string="",this.cursor=t.iter()}syncTo(t){return this.string=this.cursor.next(t-this.cursorPos).value,this.cursorPos=t+this.string.length,this.cursorPos-this.string.length}chunk(t){return this.syncTo(t),this.string}get lineChunks(){return!0}read(t,e){let i=this.cursorPos-this.string.length;return t=this.cursorPos?this.doc.sliceString(t,e):this.string.slice(t-i,e-i)}}let yl=null;class bl{constructor(t,e,i=[],n,s,r,o,l){this.parser=t,this.state=e,this.fragments=i,this.tree=n,this.treeLen=s,this.viewport=r,this.skipped=o,this.scheduleOn=l,this.parse=null,this.tempSkipped=[]}static create(t,e,i){return new bl(t,e,[],xo.empty,0,i,[],null)}startParse(){return this.parser.startParse(new wl(this.state.doc),this.fragments)}work(t,e){return null!=e&&e>=this.state.doc.length&&(e=void 0),this.tree!=xo.empty&&this.isDone(null!=e?e:this.state.doc.length)?(this.takeTree(),!0):this.withContext((()=>{var i;if("number"==typeof t){let e=Date.now()+t;t=()=>Date.now()>e}for(this.parse||(this.parse=this.startParse()),null!=e&&(null==this.parse.stoppedAt||this.parse.stoppedAt>e)&&e=this.treeLen&&((null==this.parse.stoppedAt||this.parse.stoppedAt>t)&&this.parse.stopAt(t),this.withContext((()=>{for(;!(e=this.parse.advance()););})),this.treeLen=t,this.tree=e,this.fragments=this.withoutTempSkipped(Vo.addTree(this.tree,this.fragments,!0)),this.parse=null)}withContext(t){let e=yl;yl=this;try{return t()}finally{yl=e}}withoutTempSkipped(t){for(let e;e=this.tempSkipped.pop();)t=xl(t,e.from,e.to);return t}changes(t,e){let{fragments:i,tree:n,treeLen:s,viewport:r,skipped:o}=this;if(this.takeTree(),!t.empty){let e=[];if(t.iterChangedRanges(((t,i,n,s)=>e.push({fromA:t,toA:i,fromB:n,toB:s}))),i=Vo.applyChanges(i,e),n=xo.empty,s=0,r={from:t.mapPos(r.from,-1),to:t.mapPos(r.to,1)},this.skipped.length){o=[];for(let e of this.skipped){let i=t.mapPos(e.from,1),n=t.mapPos(e.to,-1);it.from&&(this.fragments=xl(this.fragments,i,n),this.skipped.splice(e--,1))}return!(this.skipped.length>=e)&&(this.reset(),!0)}reset(){this.parse&&(this.takeTree(),this.parse=null)}skipUntilInView(t,e){this.skipped.push({from:t,to:e})}static getSkippingParser(t){return new class extends Wo{createParse(e,i,n){let s=n[0].from,r=n[n.length-1].to;return{parsedPos:s,advance(){let e=yl;if(e){for(let t of n)e.tempSkipped.push(t);t&&(e.scheduleOn=e.scheduleOn?Promise.all([e.scheduleOn,t]):t)}return this.parsedPos=r,new xo(go.none,[],[],r-s)},stoppedAt:null,stopAt(){}}}}}isDone(t){t=Math.min(t,this.state.doc.length);let e=this.fragments;return this.treeLen>=t&&e.length&&0==e[0].from&&e[0].to>=t}static get(){return yl}}function xl(t,e,i){return Vo.applyChanges(t,[{fromA:e,toA:i,fromB:e,toB:i}])}class kl{constructor(t){this.context=t,this.tree=t.tree}apply(t){if(!t.docChanged&&this.tree==this.context.tree)return this;let e=this.context.changes(t.changes,t.state),i=this.context.treeLen==t.startState.doc.length?void 0:Math.max(t.changes.mapPos(this.context.treeLen),e.viewport.to);return e.work(20,i)||e.takeTree(),new kl(e)}static init(t){let e=Math.min(3e3,t.doc.length),i=bl.create(t.facet(Ol).parser,t,{from:0,to:e});return i.work(20,e)||i.takeTree(),new kl(i)}}pl.state=q.define({create:kl.init,update(t,e){for(let t of e.effects)if(t.is(pl.setState))return t.value;return e.startState.facet(Ol)!=e.state.facet(Ol)?kl.init(e.state):t.apply(e)}});let Sl=t=>{let e=setTimeout((()=>t()),500);return()=>clearTimeout(e)};"undefined"!=typeof requestIdleCallback&&(Sl=t=>{let e=-1,i=setTimeout((()=>{e=requestIdleCallback(t,{timeout:400})}),100);return()=>e<0?clearTimeout(i):cancelIdleCallback(e)});const Cl="undefined"!=typeof navigator&&(null===(fl=navigator.scheduling)||void 0===fl?void 0:fl.isInputPending)?()=>navigator.scheduling.isInputPending():null,Al=Mi.fromClass(class{constructor(t){this.view=t,this.working=null,this.workScheduled=0,this.chunkEnd=-1,this.chunkBudget=-1,this.work=this.work.bind(this),this.scheduleWork()}update(t){let e=this.view.state.field(pl.state).context;(e.updateViewport(t.view.viewport)||this.view.viewport.to>e.treeLen)&&this.scheduleWork(),t.docChanged&&(this.view.hasFocus&&(this.chunkBudget+=50),this.scheduleWork()),this.checkAsyncSchedule(e)}scheduleWork(){if(this.working)return;let{state:t}=this.view,e=t.field(pl.state);e.tree==e.context.tree&&e.context.isDone(t.doc.length)||(this.working=Sl(this.work))}work(t){this.working=null;let e=Date.now();if(this.chunkEndn+1e3,l=s.context.work((()=>Cl&&Cl()||Date.now()>r),n+(o?0:1e5));this.chunkBudget-=Date.now()-e,(l||this.chunkBudget<=0)&&(s.context.takeTree(),this.view.dispatch({effects:pl.setState.of(new kl(s.context))})),this.chunkBudget>0&&(!l||o)&&this.scheduleWork(),this.checkAsyncSchedule(s.context)}checkAsyncSchedule(t){t.scheduleOn&&(this.workScheduled++,t.scheduleOn.then((()=>this.scheduleWork())).catch((t=>Si(this.view.state,t))).then((()=>this.workScheduled--)),t.scheduleOn=null)}destroy(){this.working&&this.working()}isWorking(){return!!(this.working||this.workScheduled>0)}},{eventHandlers:{focus(){this.scheduleWork()}}}),Ol=N.define({combine:t=>t.length?t[0]:null,enables:t=>[pl.state,Al,Ds.contentAttributes.compute([t],(e=>{let i=e.facet(t);return i&&i.name?{"data-language":i.name}:{}}))]});class Ml{constructor(t,e=[]){this.language=t,this.support=e,this.extension=[t,e]}}const Dl=N.define(),Tl=N.define({combine:t=>{if(!t.length)return" ";if(!/^(?: +|\t+)$/.test(t[0]))throw new Error("Invalid indent unit: "+JSON.stringify(t[0]));return t[0]}});function Pl(t){let e=t.facet(Tl);return 9==e.charCodeAt(0)?t.tabSize*e.length:e.length}function Rl(t,e){let i="",n=t.tabSize;if(9==t.facet(Tl).charCodeAt(0))for(;e>=n;)i+="\t",e-=n;for(let t=0;t=i.from&&n<=i.to?s&&n==t?{text:"",from:t}:(e<0?n-1&&(s+=r-this.countColumn(i,i.search(/\S|$/))),s}countColumn(t,e=t.length){return Ft(t,this.state.tabSize,e)}lineIndent(t,e=1){let{text:i,from:n}=this.lineAt(t,e),s=this.options.overrideIndentation;if(s){let t=s(n);if(t>-1)return t}return this.countColumn(i,i.search(/\S|$/))}get simulatedBreak(){return this.options.simulateBreak||null}}const Ll=new po;function Nl(t){let e=t.type.prop(Ll);if(e)return e;let i,n=t.firstChild;if(n&&(i=n.type.prop(po.closedBy))){let e=t.lastChild,n=e&&i.indexOf(e.name)>-1;return t=>function(t,e,i,n,s){let r=t.textAfter,o=r.match(/^\s*/)[0].length,l=n&&r.slice(o,o+n.length)==n||s==t.pos+o,a=e?function(t){let e=t.node,i=e.childAfter(e.from),n=e.lastChild;if(!i)return null;let s=t.options.simulateBreak,r=t.state.doc.lineAt(i.from),o=null==s||s<=r.from?r.to:Math.min(r.to,s);for(let t=i.to;;){let s=e.childAfter(t);if(!s||s==n)return null;if(!s.type.isSkipped)return s.from{let n=t&&t.test(i.textAfter);return i.baseIndent+(n?0:e*i.unit)}}const Fl=N.define(),ql=new po;function _l(t){let e=t.lastChild;return e&&e.to==t.to&&e.type.isError}function jl(t,e,i){for(let n of t.facet(Fl)){let s=n(t,e,i);if(s)return s}return function(t,e,i){let n=vl(t);if(n.lengthi)continue;if(s&&r.from=e&&n.to>i&&(s=n)}}return s}(t,e,i)}function Ul(t,e){let i=e.mapPos(t.from,1),n=e.mapPos(t.to,-1);return i>=n?void 0:{from:i,to:n}}const $l=ut.define({map:Ul}),Ql=ut.define({map:Ul});function Kl(t){let e=[];for(let{head:i}of t.state.selection.ranges)e.some((t=>t.from<=i&&t.to>=i))||e.push(t.lineBlockAt(i));return e}const Gl=q.define({create:()=>ii.none,update(t,e){t=t.map(e.changes);for(let i of e.effects)i.is($l)&&!Xl(t,i.value.from,i.value.to)?t=t.update({add:[sa.range(i.value.from,i.value.to)]}):i.is(Ql)&&(t=t.update({filter:(t,e)=>i.value.from!=t||i.value.to!=e,filterFrom:i.value.from,filterTo:i.value.to}));if(e.selection){let i=!1,{head:n}=e.selection.main;t.between(n,n,((t,e)=>{tn&&(i=!0)})),i&&(t=t.update({filterFrom:n,filterTo:n,filter:(t,e)=>e<=n||t>=n}))}return t},provide:t=>Ds.decorations.from(t),toJSON(t,e){let i=[];return t.between(0,e.doc.length,((t,e)=>{i.push(t,e)})),i},fromJSON(t){if(!Array.isArray(t)||t.length%2)throw new RangeError("Invalid JSON for fold state");let e=[];for(let i=0;i{(!s||s.from>t)&&(s={from:t,to:e})})),s}function Xl(t,e,i){let n=!1;return t.between(e,e,((t,s)=>{t==e&&s==i&&(n=!0)})),n}function Zl(t,e){return t.field(Gl,!1)?e:e.concat(ut.appendConfig.of(na()))}function Yl(t,e,i=!0){let n=t.state.doc.lineAt(e.from).number,s=t.state.doc.lineAt(e.to).number;return Ds.announce.of(`${t.state.phrase(i?"Folded lines":"Unfolded lines")} ${n} ${t.state.phrase("to")} ${s}.`)}const ta=[{key:"Ctrl-Shift-[",mac:"Cmd-Alt-[",run:t=>{for(let e of Kl(t)){let i=jl(t.state,e.from,e.to);if(i)return t.dispatch({effects:Zl(t.state,[$l.of(i),Yl(t,i)])}),!0}return!1}},{key:"Ctrl-Shift-]",mac:"Cmd-Alt-]",run:t=>{if(!t.state.field(Gl,!1))return!1;let e=[];for(let i of Kl(t)){let n=Jl(t.state,i.from,i.to);n&&e.push(Ql.of(n),Yl(t,n,!1))}return e.length&&t.dispatch({effects:e}),e.length>0}},{key:"Ctrl-Alt-[",run:t=>{let{state:e}=t,i=[];for(let n=0;n{let e=t.state.field(Gl,!1);if(!e||!e.size)return!1;let i=[];return e.between(0,t.state.doc.length,((t,e)=>{i.push(Ql.of({from:t,to:e}))})),t.dispatch({effects:i}),!0}}],ea={placeholderDOM:null,placeholderText:"…"},ia=N.define({combine:t=>Ct(t,ea)});function na(t){let e=[Gl,aa];return t&&e.push(ia.of(t)),e}const sa=ii.replace({widget:new class extends ti{toDOM(t){let{state:e}=t,i=e.facet(ia),n=e=>{let i=t.lineBlockAt(t.posAtDOM(e.target)),n=Jl(t.state,i.from,i.to);n&&t.dispatch({effects:Ql.of(n)}),e.preventDefault()};if(i.placeholderDOM)return i.placeholderDOM(t,n);let s=document.createElement("span");return s.textContent=i.placeholderText,s.setAttribute("aria-label",e.phrase("folded code")),s.title=e.phrase("unfold"),s.className="cm-foldPlaceholder",s.onclick=n,s}}}),ra={openText:"⌄",closedText:"›",markerDOM:null,domEventHandlers:{},foldingChanged:()=>!1};class oa extends qr{constructor(t,e){super(),this.config=t,this.open=e}eq(t){return this.config==t.config&&this.open==t.open}toDOM(t){if(this.config.markerDOM)return this.config.markerDOM(this.open);let e=document.createElement("span");return e.textContent=this.open?this.config.openText:this.config.closedText,e.title=t.state.phrase(this.open?"Fold line":"Unfold line"),e}}function la(t={}){let e=Object.assign(Object.assign({},ra),t),i=new oa(e,!0),n=new oa(e,!1),s=Mi.fromClass(class{constructor(t){this.from=t.viewport.from,this.markers=this.buildMarkers(t)}update(t){(t.docChanged||t.viewportChanged||t.startState.facet(Ol)!=t.state.facet(Ol)||t.startState.field(Gl,!1)!=t.state.field(Gl,!1)||vl(t.startState)!=vl(t.state)||e.foldingChanged(t))&&(this.markers=this.buildMarkers(t.view))}buildMarkers(t){let e=new Pt;for(let s of t.viewportLineBlocks){let r=Jl(t.state,s.from,s.to)?n:jl(t.state,s.from,s.to)?i:null;r&&e.add(s.from,s.from,r)}return e.finish()}}),{domEventHandlers:r}=e;return[s,$r({class:"cm-foldGutter",markers(t){var e;return(null===(e=t.plugin(s))||void 0===e?void 0:e.markers)||Tt.empty},initialSpacer:()=>new oa(e,!1),domEventHandlers:Object.assign(Object.assign({},r),{click:(t,e,i)=>{if(r.click&&r.click(t,e,i))return!0;let n=Jl(t.state,e.from,e.to);if(n)return t.dispatch({effects:Ql.of(n)}),!0;let s=jl(t.state,e.from,e.to);return!!s&&(t.dispatch({effects:$l.of(s)}),!0)}})}),na()]}const aa=Ds.baseTheme({".cm-foldPlaceholder":{backgroundColor:"#eee",border:"1px solid #ddd",color:"#888",borderRadius:".2em",margin:"0 1px",padding:"0 1px",cursor:"pointer"},".cm-foldGutter span":{padding:"0 1px",cursor:"pointer"}});class ha{constructor(t,e){let i;function n(t){let e=$t.newName();return(i||(i=Object.create(null)))["."+e]=t,e}this.specs=t;const s="string"==typeof e.all?e.all:e.all?n(e.all):void 0,r=e.scope;this.scope=r instanceof pl?t=>t.prop(dl)==r.data:r?t=>t==r:void 0,this.style=Qo(t.map((t=>({tag:t.tag,class:t.class||n(Object.assign({},t,{tag:null}))}))),{all:s}).style,this.module=i?new $t(i):null,this.themeType=e.themeType}static define(t,e){return new ha(t,e||{})}}const ca=N.define(),ua=N.define({combine:t=>t.length?[t[0]]:null});function fa(t){let e=t.facet(ca);return e.length?e:t.facet(ua)}function da(t,e){let i,n=[ma];return t instanceof ha&&(t.module&&n.push(Ds.styleModule.of(t.module)),i=t.themeType),(null==e?void 0:e.fallback)?n.push(ua.of(t)):i?n.push(ca.computeN([Ds.darkTheme],(e=>e.facet(Ds.darkTheme)==("dark"==i)?[t]:[]))):n.push(ca.of(t)),n}class pa{constructor(t){this.markCache=Object.create(null),this.tree=vl(t.state),this.decorations=this.buildDeco(t,fa(t.state))}update(t){let e=vl(t.state),i=fa(t.state),n=i!=fa(t.startState);e.length{i.add(t,e,this.markCache[n]||(this.markCache[n]=ii.mark({class:n})))}),n,s);return i.finish()}}const ma=K.high(Mi.fromClass(pa,{decorations:t=>t.decorations})),ga=ha.define([{tag:ul.meta,color:"#7a757a"},{tag:ul.link,textDecoration:"underline"},{tag:ul.heading,textDecoration:"underline",fontWeight:"bold"},{tag:ul.emphasis,fontStyle:"italic"},{tag:ul.strong,fontWeight:"bold"},{tag:ul.strikethrough,textDecoration:"line-through"},{tag:ul.keyword,color:"#708"},{tag:[ul.atom,ul.bool,ul.url,ul.contentSeparator,ul.labelName],color:"#219"},{tag:[ul.literal,ul.inserted],color:"#164"},{tag:[ul.string,ul.deleted],color:"#a11"},{tag:[ul.regexp,ul.escape,ul.special(ul.string)],color:"#e40"},{tag:ul.definition(ul.variableName),color:"#00f"},{tag:ul.local(ul.variableName),color:"#30a"},{tag:[ul.typeName,ul.namespace],color:"#085"},{tag:ul.className,color:"#167"},{tag:[ul.special(ul.variableName),ul.macroName],color:"#256"},{tag:ul.definition(ul.propertyName),color:"#00c"},{tag:ul.comment,color:"#940"},{tag:ul.invalid,color:"#f00"}]),va=Ds.baseTheme({"&.cm-focused .cm-matchingBracket":{backgroundColor:"#328c8252"},"&.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bb555544"}}),wa="()[]{}",ya=N.define({combine:t=>Ct(t,{afterCursor:!0,brackets:wa,maxScanDistance:1e4,renderMatch:ka})}),ba=ii.mark({class:"cm-matchingBracket"}),xa=ii.mark({class:"cm-nonmatchingBracket"});function ka(t){let e=[],i=t.matched?ba:xa;return e.push(i.range(t.start.from,t.start.to)),t.end&&e.push(i.range(t.end.from,t.end.to)),e}const Sa=q.define({create:()=>ii.none,update(t,e){if(!e.docChanged&&!e.selection)return t;let i=[],n=e.state.facet(ya);for(let t of e.state.selection.ranges){if(!t.empty)continue;let s=Ma(e.state,t.head,-1,n)||t.head>0&&Ma(e.state,t.head-1,1,n)||n.afterCursor&&(Ma(e.state,t.head,1,n)||t.headDs.decorations.from(t)}),Ca=[Sa,va];function Aa(t={}){return[ya.of(t),Ca]}function Oa(t,e,i){let n=t.prop(e<0?po.openedBy:po.closedBy);if(n)return n;if(1==t.name.length){let n=i.indexOf(t.name);if(n>-1&&n%2==(e<0?1:0))return[i[n+e]]}return null}function Ma(t,e,i,n={}){let s=n.maxScanDistance||1e4,r=n.brackets||wa,o=vl(t),l=o.resolveInner(e,i);for(let n=l;n;n=n.parent){let s=Oa(n.type,i,r);if(s&&n.from0)return null;let h={from:i<0?e-1:e,to:i>0?e+1:e},c=t.doc.iterRange(e,i>0?t.doc.length:0),u=0;for(let t=0;!c.next().done&&t<=r;){let r=c.value;i<0&&(t+=r.length);let l=e+t*i;for(let t=i>0?0:r.length-1,e=i>0?r.length:-1;t!=e;t+=i){let e=o.indexOf(r[t]);if(!(e<0||n.resolveInner(l+t,1).type!=s))if(e%2==0==i>0)u++;else{if(1==u)return{start:h,end:{from:l+t,to:l+t+1},matched:e>>1==a>>1};u--}}i>0&&(t+=r.length)}return c.done?{start:h,matched:!1}:null}(t,e,i,o,l.type,s,r)}function Da(t,e,i,n,s,r){let o=n.parent,l={from:n.from,to:n.to},a=0,h=null==o?void 0:o.cursor();if(h&&(i<0?h.childBefore(n.from):h.childAfter(n.to)))do{if(i<0?h.to<=n.from:h.from>=n.to){if(0==a&&s.indexOf(h.type.name)>-1&&h.from-1||(Ra.push(t),console.warn(e))}function La(t,e){let i=null;for(let n of e.split(".")){let e=t[n]||ul[n];e?"function"==typeof e?i?i=e(i):Ba(n,`Modifier ${n} used at start of tag`):i?Ba(n,`Tag ${n} used as modifier`):i=e:Ba(n,`Unknown highlighting tag ${n}`)}if(!i)return 0;let n=e.replace(/ /g,"_"),s=go.define({id:Pa.length,name:n,props:[jo({[n]:i})]});return Pa.push(s),s.id}function Na(t,e){return({state:i,dispatch:n})=>{if(i.readOnly)return!1;let s=t(e,i);return!!s&&(n(i.update(s)),!0)}}const Ia=Na(Fa,0),Va=Na(Ha,0),Wa=Na(((t,e)=>Ha(t,e,function(t){let e=[];for(let i of t.selection.ranges){let n=t.doc.lineAt(i.from),s=i.to<=n.to?n:t.doc.lineAt(i.to),r=e.length-1;r>=0&&e[r].to>n.from?e[r].to=s.to:e.push({from:n.from,to:s.to})}return e}(e))),0);function za(t,e=t.selection.main.head){let i=t.languageDataAt("commentTokens",e);return i.length?i[0]:{}}function Ha(t,e,i=e.selection.ranges){let n=i.map((t=>za(e,t.from).block));if(!n.every((t=>t)))return null;let s=i.map(((t,i)=>function(t,{open:e,close:i},n,s){let r,o,l=t.sliceDoc(n-50,n),a=t.sliceDoc(s,s+50),h=/\s*$/.exec(l)[0].length,c=/^\s*/.exec(a)[0].length,u=l.length-h;if(l.slice(u-e.length,u)==e&&a.slice(c,c+i.length)==i)return{open:{pos:n-h,margin:h&&1},close:{pos:s+c,margin:c&&1}};s-n<=100?r=o=t.sliceDoc(n,s):(r=t.sliceDoc(n,n+50),o=t.sliceDoc(s-50,s));let f=/^\s*/.exec(r)[0].length,d=/\s*$/.exec(o)[0].length,p=o.length-d-i.length;return r.slice(f,f+e.length)==e&&o.slice(p,p+i.length)==i?{open:{pos:n+f+e.length,margin:/\s/.test(r.charAt(f+e.length))?1:0},close:{pos:s-d-i.length,margin:/\s/.test(o.charAt(p-1))?1:0}}:null}(e,n[i],t.from,t.to)));if(2!=t&&!s.every((t=>t)))return{changes:e.changes(i.map(((t,e)=>s[e]?[]:[{from:t.from,insert:n[e].open+" "},{from:t.to,insert:" "+n[e].close}])))};if(1!=t&&s.some((t=>t))){let t=[];for(let e,i=0;is&&(t==r||r>l.from)){s=l.from;let t=za(e,i).line;if(!t)continue;let r=/^\s*/.exec(l.text)[0].length,a=r==l.length,h=l.text.slice(r,r+t.length)==t?r:-1;rt.comment<0&&(!t.empty||t.single)))){let t=[];for(let{line:e,token:i,indent:s,empty:r,single:o}of n)!o&&r||t.push({from:e.from+s,insert:i+" "});let i=e.changes(t);return{changes:i,selection:e.selection.map(i,1)}}if(1!=t&&n.some((t=>t.comment>=0))){let t=[];for(let{line:e,comment:i,token:s}of n)if(i>=0){let n=e.from+i,r=n+s.length;" "==e.text[r-e.from]&&r++,t.push({from:n,to:r})}return{changes:t}}return null}const qa=at.define(),_a=at.define(),ja=N.define(),Ua=N.define({combine:t=>Ct(t,{minDepth:100,newGroupDelay:500},{minDepth:Math.max,newGroupDelay:Math.min})});const $a=q.define({create:()=>ah.empty,update(t,e){let i=e.state.facet(Ua),n=e.annotation(qa);if(n){let s=e.docChanged?E.single(function(t){let e=0;return t.iterChangedRanges(((t,i)=>e=i)),e}(e.changes)):void 0,r=Ya.fromTransaction(e,s),o=n.side,l=0==o?t.undone:t.done;return l=r?th(l,l.length,i.minDepth,r):nh(l,e.startState.selection),new ah(0==o?n.rest:l,0==o?l:n.rest)}let s=e.annotation(_a);if("full"!=s&&"before"!=s||(t=t.isolate()),!1===e.annotation(ft.addToHistory))return e.changes.empty?t:t.addMapping(e.changes.desc);let r=Ya.fromTransaction(e),o=e.annotation(ft.time),l=e.annotation(ft.userEvent);return r?t=t.addChanges(r,o,l,i.newGroupDelay,i.minDepth):e.selection&&(t=t.addSelection(e.startState.selection,o,l,i.newGroupDelay)),"full"!=s&&"after"!=s||(t=t.isolate()),t},toJSON:t=>({done:t.done.map((t=>t.toJSON())),undone:t.undone.map((t=>t.toJSON()))}),fromJSON:t=>new ah(t.done.map(Ya.fromJSON),t.undone.map(Ya.fromJSON))});function Qa(t={}){return[$a,Ua.of(t),Ds.domEventHandlers({beforeinput(t,e){let i="historyUndo"==t.inputType?Ga:"historyRedo"==t.inputType?Ja:null;return!!i&&(t.preventDefault(),i(e))}})]}function Ka(t,e){return function({state:i,dispatch:n}){if(!e&&i.readOnly)return!1;let s=i.field($a,!1);if(!s)return!1;let r=s.pop(t,i,e);return!!r&&(n(r),!0)}}const Ga=Ka(0,!1),Ja=Ka(1,!1),Xa=Ka(0,!0),Za=Ka(1,!0);class Ya{constructor(t,e,i,n,s){this.changes=t,this.effects=e,this.mapped=i,this.startSelection=n,this.selectionsAfter=s}setSelAfter(t){return new Ya(this.changes,this.effects,this.mapped,this.startSelection,t)}toJSON(){var t,e,i;return{changes:null===(t=this.changes)||void 0===t?void 0:t.toJSON(),mapped:null===(e=this.mapped)||void 0===e?void 0:e.toJSON(),startSelection:null===(i=this.startSelection)||void 0===i?void 0:i.toJSON(),selectionsAfter:this.selectionsAfter.map((t=>t.toJSON()))}}static fromJSON(t){return new Ya(t.changes&&C.fromJSON(t.changes),[],t.mapped&&S.fromJSON(t.mapped),t.startSelection&&E.fromJSON(t.startSelection),t.selectionsAfter.map(E.fromJSON))}static fromTransaction(t,e){let i=ih;for(let e of t.startState.facet(ja)){let n=e(t);n.length&&(i=i.concat(n))}return!i.length&&t.changes.empty?null:new Ya(t.changes.invert(t.startState.doc),i,void 0,e||t.startState.selection,ih)}static selection(t){return new Ya(void 0,ih,void 0,void 0,t)}}function th(t,e,i,n){let s=e+1>i+20?e-i-1:0,r=t.slice(s,e);return r.push(n),r}function eh(t,e){return t.length?e.length?t.concat(e):t:e}const ih=[];function nh(t,e){if(t.length){let i=t[t.length-1],n=i.selectionsAfter.slice(Math.max(0,i.selectionsAfter.length-200));return n.length&&n[n.length-1].eq(e)?t:(n.push(e),th(t,t.length-1,1e9,i.setSelAfter(n)))}return[Ya.selection([e])]}function sh(t){let e=t[t.length-1],i=t.slice();return i[t.length-1]=e.setSelAfter(e.selectionsAfter.slice(0,e.selectionsAfter.length-1)),i}function rh(t,e){if(!t.length)return t;let i=t.length,n=ih;for(;i;){let s=oh(t[i-1],e,n);if(s.changes&&!s.changes.empty||s.effects.length){let e=t.slice(0,i);return e[i-1]=s,e}e=s.mapped,i--,n=s.selectionsAfter}return n.length?[Ya.selection(n)]:ih}function oh(t,e,i){let n=eh(t.selectionsAfter.length?t.selectionsAfter.map((t=>t.map(e))):ih,i);if(!t.changes)return Ya.selection(n);let s=t.changes.map(e),r=e.mapDesc(t.changes,!0),o=t.mapped?t.mapped.composeDesc(r):r;return new Ya(s,ut.mapEffects(t.effects,e),o,t.startSelection.map(r),n)}const lh=/^(input\.type|delete)($|\.)/;class ah{constructor(t,e,i=0,n){this.done=t,this.undone=e,this.prevTime=i,this.prevUserEvent=n}isolate(){return this.prevTime?new ah(this.done,this.undone):this}addChanges(t,e,i,n,s){let r=this.done,o=r[r.length-1];return r=o&&o.changes&&!o.changes.empty&&t.changes&&(!i||lh.test(i))&&(!o.selectionsAfter.length&&e-this.prevTimei.push(t,e))),e.iterChangedRanges(((t,e,s,r)=>{for(let t=0;t=e&&s<=o&&(n=!0)}})),n}(o.changes,t.changes)||"input.type.compose"==i)?th(r,r.length-1,s,new Ya(t.changes.compose(o.changes),eh(t.effects,o.effects),o.mapped,o.startSelection,ih)):th(r,r.length,s,t),new ah(r,ih,e,i)}addSelection(t,e,i,n){let s=this.done.length?this.done[this.done.length-1].selectionsAfter:ih;return s.length>0&&e-this.prevTimet.empty!=o.ranges[e].empty)).length)?this:new ah(nh(this.done,t),this.undone,e,i);var r,o}addMapping(t){return new ah(rh(this.done,t),rh(this.undone,t),this.prevTime,this.prevUserEvent)}pop(t,e,i){let n=0==t?this.done:this.undone;if(0==n.length)return null;let s=n[n.length-1];if(i&&s.selectionsAfter.length)return e.update({selection:s.selectionsAfter[s.selectionsAfter.length-1],annotations:qa.of({side:t,rest:sh(n)}),userEvent:0==t?"select.undo":"select.redo",scrollIntoView:!0});if(s.changes){let i=1==n.length?ih:n.slice(0,n.length-1);return s.mapped&&(i=rh(i,s.mapped)),e.update({changes:s.changes,selection:s.startSelection,effects:s.effects,annotations:qa.of({side:t,rest:i}),filter:!1,userEvent:0==t?"undo":"redo",scrollIntoView:!0})}return null}}ah.empty=new ah(ih,ih);const hh=[{key:"Mod-z",run:Ga,preventDefault:!0},{key:"Mod-y",mac:"Mod-Shift-z",run:Ja,preventDefault:!0},{linux:"Ctrl-Shift-z",run:Ja,preventDefault:!0},{key:"Mod-u",run:Xa,preventDefault:!0},{key:"Alt-u",mac:"Mod-Shift-u",run:Za,preventDefault:!0}];function ch(t,e){return E.create(t.ranges.map(e),t.mainIndex)}function uh(t,e){return t.update({selection:e,scrollIntoView:!0,userEvent:"select"})}function fh({state:t,dispatch:e},i){let n=ch(t.selection,i);return!n.eq(t.selection)&&(e(uh(t,n)),!0)}function dh(t,e){return E.cursor(e?t.to:t.from)}function ph(t,e){return fh(t,(i=>i.empty?t.moveByChar(i,e):dh(i,e)))}function mh(t){return t.textDirectionAt(t.state.selection.main.head)==Vi.LTR}const gh=t=>ph(t,!mh(t)),vh=t=>ph(t,mh(t));function wh(t,e){return fh(t,(i=>i.empty?t.moveByGroup(i,e):dh(i,e)))}function yh(t,e,i){if(e.type.prop(i))return!0;let n=e.to-e.from;return n&&(n>2||/[^\s,.;:]/.test(t.sliceDoc(e.from,e.to)))||e.firstChild}function bh(t,e,i){let n,s,r=vl(t).resolveInner(e.head),o=i?po.closedBy:po.openedBy;for(let n=e.head;;){let e=i?r.childAfter(n):r.childBefore(n);if(!e)break;yh(t,e,o)?r=e:n=i?e.to:e.from}return s=r.type.prop(o)&&(n=i?Ma(t,r.from,1):Ma(t,r.to,-1))&&n.matched?i?n.end.to:n.end.from:i?r.to:r.from,E.cursor(s,i?-1:1)}function xh(t,e){return fh(t,(i=>{if(!i.empty)return dh(i,e);let n=t.moveVertically(i,e);return n.head!=i.head?n:t.moveToLineBoundary(i,e)}))}const kh=t=>xh(t,!1),Sh=t=>xh(t,!0);function Ch(t){return Math.max(t.defaultLineHeight,Math.min(t.dom.clientHeight,innerHeight)-5)}function Ah(t,e){let{state:i}=t,n=ch(i.selection,(i=>i.empty?t.moveVertically(i,e,Ch(t)):dh(i,e)));if(n.eq(i.selection))return!1;let s,r=t.coordsAtPos(i.selection.main.head),o=t.scrollDOM.getBoundingClientRect();return r&&r.top>o.top&&r.bottomAh(t,!1),Mh=t=>Ah(t,!0);function Dh(t,e,i){let n=t.lineBlockAt(e.head),s=t.moveToLineBoundary(e,i);if(s.head==e.head&&s.head!=(i?n.to:n.from)&&(s=t.moveToLineBoundary(e,i,!1)),!i&&s.head==n.from&&n.length){let i=/^\s*/.exec(t.state.sliceDoc(n.from,Math.min(n.from+100,n.to)))[0].length;i&&e.head!=n.from+i&&(s=E.cursor(n.from+i))}return s}function Th(t,e){let i=ch(t.state.selection,(t=>{let i=e(t);return E.range(t.anchor,i.head,i.goalColumn)}));return!i.eq(t.state.selection)&&(t.dispatch(uh(t.state,i)),!0)}function Ph(t,e){return Th(t,(i=>t.moveByChar(i,e)))}const Rh=t=>Ph(t,!mh(t)),Eh=t=>Ph(t,mh(t));function Bh(t,e){return Th(t,(i=>t.moveByGroup(i,e)))}function Lh(t,e){return Th(t,(i=>t.moveVertically(i,e)))}const Nh=t=>Lh(t,!1),Ih=t=>Lh(t,!0);function Vh(t,e){return Th(t,(i=>t.moveVertically(i,e,Ch(t))))}const Wh=t=>Vh(t,!1),zh=t=>Vh(t,!0),Hh=({state:t,dispatch:e})=>(e(uh(t,{anchor:0})),!0),Fh=({state:t,dispatch:e})=>(e(uh(t,{anchor:t.doc.length})),!0),qh=({state:t,dispatch:e})=>(e(uh(t,{anchor:t.selection.main.anchor,head:0})),!0),_h=({state:t,dispatch:e})=>(e(uh(t,{anchor:t.selection.main.anchor,head:t.doc.length})),!0);function jh(t,e){if(t.state.readOnly)return!1;let i="delete.selection",{state:n}=t,s=n.changeByRange((n=>{let{from:s,to:r}=n;if(s==r){let n=e(s);ns&&(i="delete.forward",n=Uh(t,n,!0)),s=Math.min(s,n),r=Math.max(r,n)}else s=Uh(t,s,!1),r=Uh(t,r,!0);return s==r?{range:n}:{changes:{from:s,to:r},range:E.cursor(s)}}));return!s.changes.empty&&(t.dispatch(n.update(s,{scrollIntoView:!0,userEvent:i,effects:"delete.selection"==i?Ds.announce.of(n.phrase("Selection deleted")):void 0})),!0)}function Uh(t,e,i){if(t instanceof Ds)for(let n of t.state.facet(Ds.atomicRanges).map((e=>e(t))))n.between(e,e,((t,n)=>{te&&(e=i?n:t)}));return e}const $h=(t,e)=>jh(t,(i=>{let n,s,{state:r}=t,o=r.doc.lineAt(i);if(!e&&i>o.from&&i$h(t,!1),Kh=t=>$h(t,!0),Gh=(t,e)=>jh(t,(i=>{let n=i,{state:s}=t,r=s.doc.lineAt(n),o=s.charCategorizer(n);for(let t=null;;){if(n==(e?r.to:r.from)){n==i&&r.number!=(e?s.doc.lines:1)&&(n+=e?1:-1);break}let l=d(r.text,n-r.from,e)+r.from,a=r.text.slice(Math.min(n,l)-r.from,Math.max(n,l)-r.from),h=o(a);if(null!=t&&h!=t)break;" "==a&&n==i||(t=h),n=l}return n})),Jh=t=>Gh(t,!1),Xh=t=>jh(t,(e=>{let i=t.lineBlockAt(e).to;return e=s.number){let t=e[e.length-1];t.to=r.to,t.ranges.push(n)}else e.push({from:s.from,to:r.to,ranges:[n]});i=r.number+1}return e}function Yh(t,e,i){if(t.readOnly)return!1;let n=[],s=[];for(let e of Zh(t)){if(i?e.to==t.doc.length:0==e.from)continue;let r=t.doc.lineAt(i?e.to+1:e.from-1),o=r.length+1;if(i){n.push({from:e.to,to:r.to},{from:e.from,insert:r.text+t.lineBreak});for(let i of e.ranges)s.push(E.range(Math.min(t.doc.length,i.anchor+o),Math.min(t.doc.length,i.head+o)))}else{n.push({from:r.from,to:e.from},{from:e.to,insert:t.lineBreak+r.text});for(let t of e.ranges)s.push(E.range(t.anchor-o,t.head-o))}}return!!n.length&&(e(t.update({changes:n,scrollIntoView:!0,selection:E.create(s,t.selection.mainIndex),userEvent:"move.line"})),!0)}function tc(t,e,i){if(t.readOnly)return!1;let n=[];for(let e of Zh(t))i?n.push({from:e.from,insert:t.doc.slice(e.from,e.to)+t.lineBreak}):n.push({from:e.to,insert:t.lineBreak+t.doc.slice(e.from,e.to)});return e(t.update({changes:n,scrollIntoView:!0,userEvent:"input.copyline"})),!0}const ec=ic(!1);function ic(t){return({state:i,dispatch:n})=>{if(i.readOnly)return!1;let s=i.changeByRange((n=>{let{from:s,to:r}=n,o=i.doc.lineAt(s),l=!t&&s==r&&function(t,e){if(/\(\)|\[\]|\{\}/.test(t.sliceDoc(e-1,e+1)))return{from:e,to:e};let i,n=vl(t).resolveInner(e),s=n.childBefore(e),r=n.childAfter(e);return s&&r&&s.to<=e&&r.from>=e&&(i=s.type.prop(po.closedBy))&&i.indexOf(r.name)>-1&&t.doc.lineAt(s.to).from==t.doc.lineAt(r.from).from?{from:s.to,to:r.from}:null}(i,s);t&&(s=r=(r<=o.to?o:i.doc.lineAt(r)).to);let a=new Bl(i,{simulateBreak:s,simulateDoubleBreak:!!l}),h=El(a,s);for(null==h&&(h=/^\s*/.exec(i.doc.lineAt(s).text)[0].length);ro.from&&s{let s=[];for(let r=n.from;r<=n.to;){let o=t.doc.lineAt(r);o.number>i&&(n.empty||n.to>o.from)&&(e(o,s,n),i=o.number),r=o.to+1}let r=t.changes(s);return{changes:s,range:E.range(r.mapPos(n.anchor,1),r.mapPos(n.head,1))}}))}const sc=[{key:"Alt-ArrowLeft",mac:"Ctrl-ArrowLeft",run:t=>fh(t,(e=>bh(t.state,e,!mh(t)))),shift:t=>Th(t,(e=>bh(t.state,e,!mh(t))))},{key:"Alt-ArrowRight",mac:"Ctrl-ArrowRight",run:t=>fh(t,(e=>bh(t.state,e,mh(t)))),shift:t=>Th(t,(e=>bh(t.state,e,mh(t))))},{key:"Alt-ArrowUp",run:({state:t,dispatch:e})=>Yh(t,e,!1)},{key:"Shift-Alt-ArrowUp",run:({state:t,dispatch:e})=>tc(t,e,!1)},{key:"Alt-ArrowDown",run:({state:t,dispatch:e})=>Yh(t,e,!0)},{key:"Shift-Alt-ArrowDown",run:({state:t,dispatch:e})=>tc(t,e,!0)},{key:"Escape",run:({state:t,dispatch:e})=>{let i=t.selection,n=null;return i.ranges.length>1?n=E.create([i.main]):i.main.empty||(n=E.create([E.cursor(i.main.head)])),!!n&&(e(uh(t,n)),!0)}},{key:"Mod-Enter",run:ic(!0)},{key:"Alt-l",mac:"Ctrl-l",run:({state:t,dispatch:e})=>{let i=Zh(t).map((({from:e,to:i})=>E.range(e,Math.min(i+1,t.doc.length))));return e(t.update({selection:E.create(i),userEvent:"select"})),!0}},{key:"Mod-i",run:({state:t,dispatch:e})=>{let i=ch(t.selection,(e=>{var i;let n=vl(t).resolveInner(e.head,1);for(;!(n.from=e.to||n.to>e.to&&n.from<=e.from)&&(null===(i=n.parent)||void 0===i?void 0:i.parent);)n=n.parent;return E.range(n.to,n.from)}));return e(uh(t,i)),!0},preventDefault:!0},{key:"Mod-[",run:({state:t,dispatch:e})=>!t.readOnly&&(e(t.update(nc(t,((e,i)=>{let n=/^\s*/.exec(e.text)[0];if(!n)return;let s=Ft(n,t.tabSize),r=0,o=Rl(t,Math.max(0,s-Pl(t)));for(;r!t.readOnly&&(e(t.update(nc(t,((e,i)=>{i.push({from:e.from,insert:t.facet(Tl)})})),{userEvent:"input.indent"})),!0)},{key:"Mod-Alt-\\",run:({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=Object.create(null),n=new Bl(t,{overrideIndentation:t=>{let e=i[t];return null==e?-1:e}}),s=nc(t,((e,s,r)=>{let o=El(n,e.from);if(null==o)return;/\S/.test(e.text)||(o=0);let l=/^\s*/.exec(e.text)[0],a=Rl(t,o);(l!=a||r.from{if(t.state.readOnly)return!1;let{state:e}=t,i=e.changes(Zh(e).map((({from:t,to:i})=>(t>0?t--:it.moveVertically(e,!0))).map(i);return t.dispatch({changes:i,selection:n,scrollIntoView:!0,userEvent:"delete.line"}),!0}},{key:"Shift-Mod-\\",run:({state:t,dispatch:e})=>function(t,e,i){let n=!1,s=ch(t.selection,(e=>{let s=Ma(t,e.head,-1)||Ma(t,e.head,1)||e.head>0&&Ma(t,e.head-1,1)||e.head{let e=za(t.state);return e.line?Ia(t):!!e.block&&Wa(t)}},{key:"Alt-A",run:Va}].concat([{key:"ArrowLeft",run:gh,shift:Rh,preventDefault:!0},{key:"Mod-ArrowLeft",mac:"Alt-ArrowLeft",run:t=>wh(t,!mh(t)),shift:t=>Bh(t,!mh(t)),preventDefault:!0},{mac:"Cmd-ArrowLeft",run:t=>fh(t,(e=>Dh(t,e,!mh(t)))),shift:t=>Th(t,(e=>Dh(t,e,!mh(t)))),preventDefault:!0},{key:"ArrowRight",run:vh,shift:Eh,preventDefault:!0},{key:"Mod-ArrowRight",mac:"Alt-ArrowRight",run:t=>wh(t,mh(t)),shift:t=>Bh(t,mh(t)),preventDefault:!0},{mac:"Cmd-ArrowRight",run:t=>fh(t,(e=>Dh(t,e,mh(t)))),shift:t=>Th(t,(e=>Dh(t,e,mh(t)))),preventDefault:!0},{key:"ArrowUp",run:kh,shift:Nh,preventDefault:!0},{mac:"Cmd-ArrowUp",run:Hh,shift:qh},{mac:"Ctrl-ArrowUp",run:Oh,shift:Wh},{key:"ArrowDown",run:Sh,shift:Ih,preventDefault:!0},{mac:"Cmd-ArrowDown",run:Fh,shift:_h},{mac:"Ctrl-ArrowDown",run:Mh,shift:zh},{key:"PageUp",run:Oh,shift:Wh},{key:"PageDown",run:Mh,shift:zh},{key:"Home",run:t=>fh(t,(e=>Dh(t,e,!1))),shift:t=>Th(t,(e=>Dh(t,e,!1))),preventDefault:!0},{key:"Mod-Home",run:Hh,shift:qh},{key:"End",run:t=>fh(t,(e=>Dh(t,e,!0))),shift:t=>Th(t,(e=>Dh(t,e,!0))),preventDefault:!0},{key:"Mod-End",run:Fh,shift:_h},{key:"Enter",run:ec},{key:"Mod-a",run:({state:t,dispatch:e})=>(e(t.update({selection:{anchor:0,head:t.doc.length},userEvent:"select"})),!0)},{key:"Backspace",run:Qh,shift:Qh},{key:"Delete",run:Kh},{key:"Mod-Backspace",mac:"Alt-Backspace",run:Jh},{key:"Mod-Delete",mac:"Alt-Delete",run:t=>Gh(t,!0)},{mac:"Mod-Backspace",run:t=>jh(t,(e=>{let i=t.lineBlockAt(e).from;return e>i?i:Math.max(0,e-1)}))},{mac:"Mod-Delete",run:Xh}].concat([{key:"Ctrl-b",run:gh,shift:Rh,preventDefault:!0},{key:"Ctrl-f",run:vh,shift:Eh},{key:"Ctrl-p",run:kh,shift:Nh},{key:"Ctrl-n",run:Sh,shift:Ih},{key:"Ctrl-a",run:t=>fh(t,(e=>E.cursor(t.lineBlockAt(e.head).from,1))),shift:t=>Th(t,(e=>E.cursor(t.lineBlockAt(e.head).from)))},{key:"Ctrl-e",run:t=>fh(t,(e=>E.cursor(t.lineBlockAt(e.head).to,-1))),shift:t=>Th(t,(e=>E.cursor(t.lineBlockAt(e.head).to)))},{key:"Ctrl-d",run:Kh},{key:"Ctrl-h",run:Qh},{key:"Ctrl-k",run:Xh},{key:"Ctrl-Alt-h",run:Jh},{key:"Ctrl-o",run:({state:t,dispatch:i})=>{if(t.readOnly)return!1;let n=t.changeByRange((t=>({changes:{from:t.from,to:t.to,insert:e.of(["",""])},range:E.cursor(t.from)})));return i(t.update(n,{scrollIntoView:!0,userEvent:"input"})),!0}},{key:"Ctrl-t",run:({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=t.changeByRange((e=>{if(!e.empty||0==e.from||e.from==t.doc.length)return{range:e};let i=e.from,n=t.doc.lineAt(i),s=i==n.from?i-1:d(n.text,i-n.from,!1)+n.from,r=i==n.to?i+1:d(n.text,i-n.from,!0)+n.from;return{changes:{from:s,to:r,insert:t.doc.slice(i,r).append(t.doc.slice(s,i))},range:E.cursor(r)}}));return!i.changes.empty&&(e(t.update(i,{scrollIntoView:!0,userEvent:"move.character"})),!0)}},{key:"Ctrl-v",run:Mh}].map((t=>({mac:t.key,run:t.run,shift:t.shift})))));function rc(){var t=arguments[0];"string"==typeof t&&(t=document.createElement(t));var e=1,i=arguments[1];if(i&&"object"==typeof i&&null==i.nodeType&&!Array.isArray(i)){for(var n in i)if(Object.prototype.hasOwnProperty.call(i,n)){var s=i[n];"string"==typeof s?t.setAttribute(n,s):null!=s&&(t[n]=s)}e++}for(;et.normalize("NFKD"):t=>t;class ac{constructor(t,e,i=0,n=t.length,s,r){this.test=r,this.value={from:0,to:0},this.done=!1,this.matches=[],this.buffer="",this.bufferPos=0,this.iter=t.iterRange(i,n),this.bufferStart=i,this.normalize=s?t=>s(lc(t)):lc,this.query=this.normalize(e)}peek(){if(this.bufferPos==this.buffer.length){if(this.bufferStart+=this.buffer.length,this.iter.next(),this.iter.done)return-1;this.bufferPos=0,this.buffer=this.iter.value}return w(this.buffer,this.bufferPos)}next(){for(;this.matches.length;)this.matches.pop();return this.nextOverlapping()}nextOverlapping(){for(;;){let t=this.peek();if(t<0)return this.done=!0,this;let e=y(t),i=this.bufferStart+this.bufferPos;this.bufferPos+=b(t);let n=this.normalize(e);for(let t=0,s=i;;t++){let r=n.charCodeAt(t),o=this.match(r,s);if(o)return this.value=o,this;if(t==n.length-1)break;s==i&&tthis.to&&(this.curLine=this.curLine.slice(0,this.to-this.curLineStart)),this.iter.next())}nextLine(){this.curLineStart=this.curLineStart+this.curLine.length+1,this.curLineStart>this.to?this.curLine="":this.getLine(0)}next(){for(let t=this.matchPos-this.curLineStart;;){this.re.lastIndex=t;let e=this.matchPos<=this.to&&this.re.exec(this.curLine);if(e){let i=this.curLineStart+e.index,n=i+e[0].length;if(this.matchPos=mc(this.text,n+(i==n?1:0)),i==this.curLineStart+this.curLine.length&&this.nextLine(),(ithis.value.to)&&(!this.test||this.test(i,n,e)))return this.value={from:i,to:n,match:e},this;t=this.matchPos-this.curLineStart}else{if(!(this.curLineStart+this.curLine.length=i||n.to<=e){let n=new dc(e,t.sliceString(e,i));return fc.set(t,n),n}if(n.from==e&&n.to==i)return n;let{text:s,from:r}=n;return r>e&&(s=t.sliceString(e,r)+s,r=e),n.to=this.to?this.to:this.text.lineAt(t).to}next(){for(;;){let t=this.re.lastIndex=this.matchPos-this.flat.from,e=this.re.exec(this.flat.text);if(e&&!e[0]&&e.index==t&&(this.re.lastIndex=t+1,e=this.re.exec(this.flat.text)),e){let t=this.flat.from+e.index,i=t+e[0].length;if((this.flat.to>=this.to||e.index+e[0].length<=this.flat.text.length-10)&&(!this.test||this.test(t,i,e)))return this.value={from:t,to:i,match:e},this.matchPos=mc(this.text,i+(t==i?1:0)),this}if(this.flat.to==this.to)return this.done=!0,this;this.flat=dc.get(this.text,this.flat.from,this.chunkEnd(this.flat.from+2*this.flat.text.length))}}}function mc(t,e){if(e>=t.length)return e;let i,n=t.lineAt(e);for(;e=56320&&i<57344;)e++;return e}function gc(t){let e=rc("input",{class:"cm-textfield",name:"line"});function i(){let i=/^([+-])?(\d+)?(:\d+)?(%)?$/.exec(e.value);if(!i)return;let{state:n}=t,s=n.doc.lineAt(n.selection.main.head),[,r,o,l,a]=i,h=l?+l.slice(1):0,c=o?+o:s.number;if(o&&a){let t=c/100;r&&(t=t*("-"==r?-1:1)+s.number/n.doc.lines),c=Math.round(n.doc.lines*t)}else o&&r&&(c=c*("-"==r?-1:1)+s.number);let u=n.doc.line(Math.max(1,Math.min(n.doc.lines,c)));t.dispatch({effects:vc.of(!1),selection:E.cursor(u.from+Math.max(0,Math.min(h,u.length))),scrollIntoView:!0}),t.focus()}return{dom:rc("form",{class:"cm-gotoLine",onkeydown:e=>{27==e.keyCode?(e.preventDefault(),t.dispatch({effects:vc.of(!1)}),t.focus()):13==e.keyCode&&(e.preventDefault(),i())},onsubmit:t=>{t.preventDefault(),i()}},rc("label",t.state.phrase("Go to line"),": ",e)," ",rc("button",{class:"cm-button",type:"submit"},t.state.phrase("go")))}}"undefined"!=typeof Symbol&&(uc.prototype[Symbol.iterator]=pc.prototype[Symbol.iterator]=function(){return this});const vc=ut.define(),wc=q.define({create:()=>!0,update(t,e){for(let i of e.effects)i.is(vc)&&(t=i.value);return t},provide:t=>Fr.from(t,(t=>t?gc:null))}),yc=Ds.baseTheme({".cm-panel.cm-gotoLine":{padding:"2px 6px 4px","& label":{fontSize:"80%"}}}),bc={highlightWordAroundCursor:!1,minSelectionLength:1,maxMatches:100,wholeWords:!1},xc=N.define({combine:t=>Ct(t,bc,{highlightWordAroundCursor:(t,e)=>t||e,minSelectionLength:Math.min,maxMatches:Math.min})});function kc(t){let e=[Mc,Oc];return t&&e.push(xc.of(t)),e}const Sc=ii.mark({class:"cm-selectionMatch"}),Cc=ii.mark({class:"cm-selectionMatch cm-selectionMatch-main"});function Ac(t,e,i,n){return!(0!=i&&t(e.sliceDoc(i-1,i))==yt.Word||n!=e.doc.length&&t(e.sliceDoc(n,n+1))==yt.Word)}const Oc=Mi.fromClass(class{constructor(t){this.decorations=this.getDeco(t)}update(t){(t.selectionSet||t.docChanged||t.viewportChanged)&&(this.decorations=this.getDeco(t.view))}getDeco(t){let e=t.state.facet(xc),{state:i}=t,n=i.selection;if(n.ranges.length>1)return ii.none;let s,r=n.main,o=null;if(r.empty){if(!e.highlightWordAroundCursor)return ii.none;let t=i.wordAt(r.head);if(!t)return ii.none;o=i.charCategorizer(r.head),s=i.sliceDoc(t.from,t.to)}else{let t=r.to-r.from;if(t200)return ii.none;if(e.wholeWords){if(s=i.sliceDoc(r.from,r.to),o=i.charCategorizer(r.head),!Ac(o,i,r.from,r.to)||!function(t,e,i,n){return t(e.sliceDoc(i,i+1))==yt.Word&&t(e.sliceDoc(n-1,n))==yt.Word}(o,i,r.from,r.to))return ii.none}else if(s=i.sliceDoc(r.from,r.to).trim(),!s)return ii.none}let l=[];for(let n of t.visibleRanges){let t=new ac(i.doc,s,n.from,n.to);for(;!t.next().done;){let{from:n,to:s}=t.value;if((!o||Ac(o,i,n,s))&&(r.empty&&n<=r.from&&s>=r.to?l.push(Cc.range(n,s)):(n>=r.to||s<=r.from)&&l.push(Sc.range(n,s)),l.length>e.maxMatches))return ii.none}}return ii.set(l)}},{decorations:t=>t.decorations}),Mc=Ds.baseTheme({".cm-selectionMatch":{backgroundColor:"#99ff7780"},".cm-searchMatch .cm-selectionMatch":{backgroundColor:"transparent"}});const Dc=N.define({combine:t=>Ct(t,{top:!1,caseSensitive:!1,literal:!1,wholeWord:!1,createPanel:t=>new eu(t)})});class Tc{constructor(t){this.search=t.search,this.caseSensitive=!!t.caseSensitive,this.literal=!!t.literal,this.regexp=!!t.regexp,this.replace=t.replace||"",this.valid=!!this.search&&(!this.regexp||function(t){try{return new RegExp(t,cc),!0}catch(t){return!1}}(this.search)),this.unquoted=this.unquote(this.search),this.wholeWord=!!t.wholeWord}unquote(t){return this.literal?t:t.replace(/\\([nrt\\])/g,((t,e)=>"n"==e?"\n":"r"==e?"\r":"t"==e?"\t":"\\"))}eq(t){return this.search==t.search&&this.replace==t.replace&&this.caseSensitive==t.caseSensitive&&this.regexp==t.regexp&&this.wholeWord==t.wholeWord}create(){return this.regexp?new Ic(this):new Ec(this)}getCursor(t,e=0,i){let n=t.doc?t:St.create({doc:t});return null==i&&(i=n.doc.length),this.regexp?Bc(this,n,e,i):Rc(this,n,e,i)}}class Pc{constructor(t){this.spec=t}}function Rc(t,e,i,n){return new ac(e.doc,t.unquoted,i,n,t.caseSensitive?void 0:t=>t.toLowerCase(),t.wholeWord?function(t,e){return(i,n,s,r)=>((r>i||r+s.length=e)return null;n.push(i.value)}return n}highlight(t,e,i,n){let s=Rc(this.spec,t,Math.max(0,e-this.spec.unquoted.length),Math.min(i+this.spec.unquoted.length,t.doc.length));for(;!s.next().done;)n(s.value.from,s.value.to)}}function Bc(t,e,i,n){return new uc(e.doc,t.search,{ignoreCase:!t.caseSensitive,test:t.wholeWord?(s=e.charCategorizer(e.selection.main.head),(t,e,i)=>!i[0].length||(s(Lc(i.input,i.index))!=yt.Word||s(Nc(i.input,i.index))!=yt.Word)&&(s(Nc(i.input,i.index+i[0].length))!=yt.Word||s(Lc(i.input,i.index+i[0].length))!=yt.Word)):void 0},i,n);var s}function Lc(t,e){return t.slice(d(t,e,!1),e)}function Nc(t,e){return t.slice(e,d(t,e))}class Ic extends Pc{nextMatch(t,e,i){let n=Bc(this.spec,t,i,t.doc.length).next();return n.done&&(n=Bc(this.spec,t,0,e).next()),n.done?null:n.value}prevMatchInRange(t,e,i){for(let n=1;;n++){let s=Math.max(e,i-1e4*n),r=Bc(this.spec,t,s,i),o=null;for(;!r.next().done;)o=r.value;if(o&&(s==e||o.from>s+10))return o;if(s==e)return null}}prevMatch(t,e,i){return this.prevMatchInRange(t,0,e)||this.prevMatchInRange(t,i,t.doc.length)}getReplacement(t){return this.spec.unquote(this.spec.replace.replace(/\$([$&\d+])/g,((e,i)=>"$"==i?"$":"&"==i?t.match[0]:"0"!=i&&+i=e)return null;n.push(i.value)}return n}highlight(t,e,i,n){let s=Bc(this.spec,t,Math.max(0,e-250),Math.min(i+250,t.doc.length));for(;!s.next().done;)n(s.value.from,s.value.to)}}const Vc=ut.define(),Wc=ut.define(),zc=q.define({create:t=>new Hc(Xc(t).create(),null),update(t,e){for(let i of e.effects)i.is(Vc)?t=new Hc(i.value.create(),t.panel):i.is(Wc)&&(t=new Hc(t.query,i.value?Jc:null));return t},provide:t=>Fr.from(t,(t=>t.panel))});class Hc{constructor(t,e){this.query=t,this.panel=e}}const Fc=ii.mark({class:"cm-searchMatch"}),qc=ii.mark({class:"cm-searchMatch cm-searchMatch-selected"}),_c=Mi.fromClass(class{constructor(t){this.view=t,this.decorations=this.highlight(t.state.field(zc))}update(t){let e=t.state.field(zc);(e!=t.startState.field(zc)||t.docChanged||t.selectionSet||t.viewportChanged)&&(this.decorations=this.highlight(e))}highlight({query:t,panel:e}){if(!e||!t.spec.valid)return ii.none;let{view:i}=this,n=new Pt;for(let e=0,s=i.visibleRanges,r=s.length;es[e+1].from-500;)l=s[++e].to;t.highlight(i.state,o,l,((t,e)=>{let s=i.state.selection.ranges.some((i=>i.from==t&&i.to==e));n.add(t,e,s?qc:Fc)}))}return n.finish()}},{decorations:t=>t.decorations});function jc(t){return e=>{let i=e.state.field(zc,!1);return i&&i.query.spec.valid?t(e,i):Zc(e)}}const Uc=jc(((t,{query:e})=>{let{to:i}=t.state.selection.main,n=e.nextMatch(t.state,i,i);return!!n&&(t.dispatch({selection:{anchor:n.from,head:n.to},scrollIntoView:!0,effects:su(t,n),userEvent:"select.search"}),!0)})),$c=jc(((t,{query:e})=>{let{state:i}=t,{from:n}=i.selection.main,s=e.prevMatch(i,n,n);return!!s&&(t.dispatch({selection:{anchor:s.from,head:s.to},scrollIntoView:!0,effects:su(t,s),userEvent:"select.search"}),!0)})),Qc=jc(((t,{query:e})=>{let i=e.matchAll(t.state,1e3);return!(!i||!i.length)&&(t.dispatch({selection:E.create(i.map((t=>E.range(t.from,t.to)))),userEvent:"select.search.matches"}),!0)})),Kc=jc(((t,{query:e})=>{let{state:i}=t,{from:n,to:s}=i.selection.main;if(i.readOnly)return!1;let r=e.nextMatch(i,n,n);if(!r)return!1;let o,l,a=[],h=[];if(r.from==n&&r.to==s&&(l=i.toText(e.getReplacement(r)),a.push({from:r.from,to:r.to,insert:l}),r=e.nextMatch(i,r.from,r.to),h.push(Ds.announce.of(i.phrase("replaced match on line $",i.doc.lineAt(n).number)+"."))),r){let e=0==a.length||a[0].from>=r.to?0:r.to-r.from-l.length;o={anchor:r.from-e,head:r.to-e},h.push(su(t,r))}return t.dispatch({changes:a,selection:o,scrollIntoView:!!o,effects:h,userEvent:"input.replace"}),!0})),Gc=jc(((t,{query:e})=>{if(t.state.readOnly)return!1;let i=e.matchAll(t.state,1e9).map((t=>{let{from:i,to:n}=t;return{from:i,to:n,insert:e.getReplacement(t)}}));if(!i.length)return!1;let n=t.state.phrase("replaced $ matches",i.length)+".";return t.dispatch({changes:i,effects:Ds.announce.of(n),userEvent:"input.replace.all"}),!0}));function Jc(t){return t.state.facet(Dc).createPanel(t)}function Xc(t,e){var i,n,s,r;let o=t.selection.main,l=o.empty||o.to>o.from+100?"":t.sliceDoc(o.from,o.to);if(e&&!l)return e;let a=t.facet(Dc);return new Tc({search:(null!==(i=null==e?void 0:e.literal)&&void 0!==i?i:a.literal)?l:l.replace(/\n/g,"\\n"),caseSensitive:null!==(n=null==e?void 0:e.caseSensitive)&&void 0!==n?n:a.caseSensitive,literal:null!==(s=null==e?void 0:e.literal)&&void 0!==s?s:a.literal,wholeWord:null!==(r=null==e?void 0:e.wholeWord)&&void 0!==r?r:a.wholeWord})}const Zc=t=>{let e=t.state.field(zc,!1);if(e&&e.panel){let i=Vr(t,Jc);if(!i)return!1;let n=i.dom.querySelector("[main-field]");if(n&&n!=t.root.activeElement){let i=Xc(t.state,e.query.spec);i.valid&&t.dispatch({effects:Vc.of(i)}),n.focus(),n.select()}}else t.dispatch({effects:[Wc.of(!0),e?Vc.of(Xc(t.state,e.query.spec)):ut.appendConfig.of(ou)]});return!0},Yc=t=>{let e=t.state.field(zc,!1);if(!e||!e.panel)return!1;let i=Vr(t,Jc);return i&&i.dom.contains(t.root.activeElement)&&t.focus(),t.dispatch({effects:Wc.of(!1)}),!0},tu=[{key:"Mod-f",run:Zc,scope:"editor search-panel"},{key:"F3",run:Uc,shift:$c,scope:"editor search-panel",preventDefault:!0},{key:"Mod-g",run:Uc,shift:$c,scope:"editor search-panel",preventDefault:!0},{key:"Escape",run:Yc,scope:"editor search-panel"},{key:"Mod-Shift-l",run:({state:t,dispatch:e})=>{let i=t.selection;if(i.ranges.length>1||i.main.empty)return!1;let{from:n,to:s}=i.main,r=[],o=0;for(let e=new ac(t.doc,t.sliceDoc(n,s));!e.next().done;){if(r.length>1e3)return!1;e.value.from==n&&(o=r.length),r.push(E.range(e.value.from,e.value.to))}return e(t.update({selection:E.create(r,o),userEvent:"select.search.matches"})),!0}},{key:"Alt-g",run:t=>{let e=Vr(t,gc);if(!e){let i=[vc.of(!0)];null==t.state.field(wc,!1)&&i.push(ut.appendConfig.of([wc,yc])),t.dispatch({effects:i}),e=Vr(t,gc)}return e&&e.dom.querySelector("input").focus(),!0}},{key:"Mod-d",run:({state:t,dispatch:e})=>{let{ranges:i}=t.selection;if(i.some((t=>t.from===t.to)))return(({state:t,dispatch:e})=>{let{selection:i}=t,n=E.create(i.ranges.map((e=>t.wordAt(e.head)||E.cursor(e.head))),i.mainIndex);return!n.eq(i)&&(e(t.update({selection:n})),!0)})({state:t,dispatch:e});let n=t.sliceDoc(i[0].from,i[0].to);if(t.selection.ranges.some((e=>t.sliceDoc(e.from,e.to)!=n)))return!1;let s=function(t,e){let{main:i,ranges:n}=t.selection,s=t.wordAt(i.head),r=s&&s.from==i.from&&s.to==i.to;for(let i=!1,s=new ac(t.doc,e,n[n.length-1].to);;){if(s.next(),!s.done){if(i&&n.some((t=>t.from==s.value.from)))continue;if(r){let e=t.wordAt(s.value.from);if(!e||e.from!=s.value.from||e.to!=s.value.to)continue}return s.value}if(i)return null;s=new ac(t.doc,e,0,Math.max(0,n[n.length-1].from-1)),i=!0}}(t,n);return!!s&&(e(t.update({selection:t.selection.addRange(E.range(s.from,s.to),!1),effects:Ds.scrollIntoView(s.to)})),!0)},preventDefault:!0}];class eu{constructor(t){this.view=t;let e=this.query=t.state.field(zc).query.spec;function i(t,e,i){return rc("button",{class:"cm-button",name:t,onclick:e,type:"button"},i)}this.commit=this.commit.bind(this),this.searchField=rc("input",{value:e.search,placeholder:iu(t,"Find"),"aria-label":iu(t,"Find"),class:"cm-textfield",name:"search",form:"","main-field":"true",onchange:this.commit,onkeyup:this.commit}),this.replaceField=rc("input",{value:e.replace,placeholder:iu(t,"Replace"),"aria-label":iu(t,"Replace"),class:"cm-textfield",name:"replace",form:"",onchange:this.commit,onkeyup:this.commit}),this.caseField=rc("input",{type:"checkbox",name:"case",form:"",checked:e.caseSensitive,onchange:this.commit}),this.reField=rc("input",{type:"checkbox",name:"re",form:"",checked:e.regexp,onchange:this.commit}),this.wordField=rc("input",{type:"checkbox",name:"word",form:"",checked:e.wholeWord,onchange:this.commit}),this.dom=rc("div",{onkeydown:t=>this.keydown(t),class:"cm-search"},[this.searchField,i("next",(()=>Uc(t)),[iu(t,"next")]),i("prev",(()=>$c(t)),[iu(t,"previous")]),i("select",(()=>Qc(t)),[iu(t,"all")]),rc("label",null,[this.caseField,iu(t,"match case")]),rc("label",null,[this.reField,iu(t,"regexp")]),rc("label",null,[this.wordField,iu(t,"by word")]),...t.state.readOnly?[]:[rc("br"),this.replaceField,i("replace",(()=>Kc(t)),[iu(t,"replace")]),i("replaceAll",(()=>Gc(t)),[iu(t,"replace all")])],rc("button",{name:"close",onclick:()=>Yc(t),"aria-label":iu(t,"close"),type:"button"},["×"])])}commit(){let t=new Tc({search:this.searchField.value,caseSensitive:this.caseField.checked,regexp:this.reField.checked,wholeWord:this.wordField.checked,replace:this.replaceField.value});t.eq(this.query)||(this.query=t,this.view.dispatch({effects:Vc.of(t)}))}keydown(t){var e,i,n;e=this.view,i=t,n="search-panel",Hs(Ws(e.state),i,e,n)?t.preventDefault():13==t.keyCode&&t.target==this.searchField?(t.preventDefault(),(t.shiftKey?$c:Uc)(this.view)):13==t.keyCode&&t.target==this.replaceField&&(t.preventDefault(),Kc(this.view))}update(t){for(let e of t.transactions)for(let t of e.effects)t.is(Vc)&&!t.value.eq(this.query)&&this.setQuery(t.value)}setQuery(t){this.query=t,this.searchField.value=t.search,this.replaceField.value=t.replace,this.caseField.checked=t.caseSensitive,this.reField.checked=t.regexp,this.wordField.checked=t.wholeWord}mount(){this.searchField.select()}get pos(){return 80}get top(){return this.view.state.facet(Dc).top}}function iu(t,e){return t.state.phrase(e)}const nu=/[\s\.,:;?!]/;function su(t,{from:e,to:i}){let n=t.state.doc.lineAt(e),s=t.state.doc.lineAt(i).to,r=Math.max(n.from,e-30),o=Math.min(s,i+30),l=t.state.sliceDoc(r,o);if(r!=n.from)for(let t=0;t<30;t++)if(!nu.test(l[t+1])&&nu.test(l[t])){l=l.slice(t);break}if(o!=s)for(let t=l.length-1;t>l.length-30;t--)if(!nu.test(l[t-1])&&nu.test(l[t])){l=l.slice(0,t);break}return Ds.announce.of(`${t.state.phrase("current match")}. ${l} ${t.state.phrase("on line")} ${n.number}.`)}const ru=Ds.baseTheme({".cm-panel.cm-search":{padding:"2px 6px 4px",position:"relative","& [name=close]":{position:"absolute",top:"0",right:"4px",backgroundColor:"inherit",border:"none",font:"inherit",padding:0,margin:0},"& input, & button, & label":{margin:".2em .6em .2em 0"},"& input[type=checkbox]":{marginRight:".2em"},"& label":{fontSize:"80%",whiteSpace:"pre"}},"&light .cm-searchMatch":{backgroundColor:"#ffff0054"},"&dark .cm-searchMatch":{backgroundColor:"#00ffff8a"},"&light .cm-searchMatch-selected":{backgroundColor:"#ff6a0054"},"&dark .cm-searchMatch-selected":{backgroundColor:"#ff00ff8a"}}),ou=[zc,K.lowest(_c),ru];class lu{constructor(t,e,i){this.state=t,this.pos=e,this.explicit=i,this.abortListeners=[]}tokenBefore(t){let e=vl(this.state).resolveInner(this.pos,-1);for(;e&&t.indexOf(e.name)<0;)e=e.parent;return e?{from:e.from,to:this.pos,text:this.state.sliceDoc(e.from,this.pos),type:e.type}:null}matchBefore(t){let e=this.state.doc.lineAt(this.pos),i=Math.max(e.from,this.pos-250),n=e.text.slice(i-e.from,this.pos-e.from),s=n.search(fu(t,!1));return s<0?null:{from:i+s,to:this.pos,text:n.slice(s)}}get aborted(){return null==this.abortListeners}addEventListener(t,e){"abort"==t&&this.abortListeners&&this.abortListeners.push(e)}}function au(t){let e=Object.keys(t).join(""),i=/\w/.test(e);return i&&(e=e.replace(/\w/g,"")),`[${i?"\\w":""}${e.replace(/[^\w\s]/g,"\\$&")}]`}function hu(t){let e=t.map((t=>"string"==typeof t?{label:t}:t)),[i,n]=e.every((t=>/^\w+$/.test(t.label)))?[/\w*$/,/\w+$/]:function(t){let e=Object.create(null),i=Object.create(null);for(let{label:n}of t){e[n[0]]=!0;for(let t=1;t{let s=t.matchBefore(n);return s||t.explicit?{from:s?s.from:t.pos,options:e,validFor:i}:null}}class cu{constructor(t,e,i){this.completion=t,this.source=e,this.match=i}}function uu(t){return t.selection.main.head}function fu(t,e){var i;let{source:n}=t,s=e&&"^"!=n[0],r="$"!=n[n.length-1];return s||r?new RegExp(`${s?"^":""}(?:${n})${r?"$":""}`,null!==(i=t.flags)&&void 0!==i?i:t.ignoreCase?"i":""):t}const du=at.define();function pu(t,e){const i=e.completion.apply||e.completion.label;let n=e.source;var s,r,o,l;"string"==typeof i?t.dispatch(Object.assign(Object.assign({},(s=t.state,r=i,o=n.from,l=n.to,Object.assign(Object.assign({},s.changeByRange((t=>{if(t==s.selection.main)return{changes:{from:o,to:l,insert:r},range:E.cursor(o+r.length)};let e=l-o;return!t.empty||e&&s.sliceDoc(t.from-e,t.from)!=s.sliceDoc(o,l)?{range:t}:{changes:{from:t.from-e,to:t.from,insert:r},range:E.cursor(t.from-e+r.length)}}))),{userEvent:"input.complete"}))),{annotations:du.of(e.completion)})):i(t,e.completion,n.from,n.to)}const mu=new WeakMap;function gu(t){if(!Array.isArray(t))return t;let e=mu.get(t);return e||mu.set(t,e=hu(t)),e}class vu{constructor(t){this.pattern=t,this.chars=[],this.folded=[],this.any=[],this.precise=[],this.byWord=[];for(let e=0;e=48&&a<=57||a>=97&&a<=122?2:a>=65&&a<=90?1:0:(x=y(a))!=x.toLowerCase()?1:x!=x.toUpperCase()?2:0;(!n||1==k&&m||0==v&&0!=k)&&(e[c]==a||i[c]==a&&(u=!0)?r[c++]=n:r.length&&(g=!1)),v=k,n+=b(a)}return c==l&&0==r[0]&&g?this.result((u?-200:0)-100,r,t):f==l&&0==d?[-200-t.length,0,p]:o>-1?[-700-t.length,o,o+this.pattern.length]:f==l?[-900-t.length,d,p]:c==l?this.result((u?-200:0)-100-700+(g?0:-1100),r,t):2==e.length?null:this.result((n[0]?-700:0)-200-1100,n,t)}result(t,e,i){let n=[t-i.length],s=1;for(let t of e){let e=t+(this.astral?b(w(i,t)):1);s>1&&n[s-1]==t?n[s-1]=e:(n[s++]=t,n[s++]=e)}return n}}const wu=N.define({combine:t=>Ct(t,{activateOnTyping:!0,selectOnOpen:!0,override:null,closeOnBlur:!0,maxRenderedOptions:100,defaultKeymap:!0,optionClass:()=>"",aboveCursor:!1,icons:!0,addToOptions:[],compareCompletions:(t,e)=>t.label.localeCompare(e.label),interactionDelay:75},{defaultKeymap:(t,e)=>t&&e,closeOnBlur:(t,e)=>t&&e,icons:(t,e)=>t&&e,optionClass:(t,e)=>i=>function(t,e){return t?e?t+" "+e:t:e}(t(i),e(i)),addToOptions:(t,e)=>t.concat(e)})});function yu(t,e,i){if(t<=i)return{from:0,to:t};if(e<0&&(e=0),e<=t>>1){let t=Math.floor(e/i);return{from:t*i,to:(t+1)*i}}let n=Math.floor((t-e)/i);return{from:t-(n+1)*i,to:t-n*i}}class bu{constructor(t,e){this.view=t,this.stateField=e,this.info=null,this.placeInfo={read:()=>this.measureInfo(),write:t=>this.positionInfo(t),key:this},this.space=null;let i=t.state.field(e),{options:n,selected:s}=i.open,r=t.state.facet(wu);this.optionContent=function(t){let e=t.addToOptions.slice();return t.icons&&e.push({render(t){let e=document.createElement("div");return e.classList.add("cm-completionIcon"),t.type&&e.classList.add(...t.type.split(/\s+/g).map((t=>"cm-completionIcon-"+t))),e.setAttribute("aria-hidden","true"),e},position:20}),e.push({render(t,e,i){let n=document.createElement("span");n.className="cm-completionLabel";let{label:s}=t,r=0;for(let t=1;tr&&n.appendChild(document.createTextNode(s.slice(r,e)));let l=n.appendChild(document.createElement("span"));l.appendChild(document.createTextNode(s.slice(e,o))),l.className="cm-completionMatchedText",r=o}return rt.position-e.position)).map((t=>t.render))}(r),this.optionClass=r.optionClass,this.range=yu(n.length,s,r.maxRenderedOptions),this.dom=document.createElement("div"),this.dom.className="cm-tooltip-autocomplete",this.dom.addEventListener("mousedown",(e=>{for(let i,s=e.target;s&&s!=this.dom;s=s.parentNode)if("LI"==s.nodeName&&(i=/-(\d+)$/.exec(s.id))&&+i[1]{this.info&&this.view.requestMeasure(this.placeInfo)}))}mount(){this.updateSel()}update(t){var e,i,n;let s=t.state.field(this.stateField),r=t.startState.field(this.stateField);s!=r&&(this.updateSel(),(null===(e=s.open)||void 0===e?void 0:e.disabled)!=(null===(i=r.open)||void 0===i?void 0:i.disabled)&&this.dom.classList.toggle("cm-tooltip-autocomplete-disabled",!!(null===(n=s.open)||void 0===n?void 0:n.disabled)))}positioned(t){this.space=t,this.info&&this.view.requestMeasure(this.placeInfo)}updateSel(){let t=this.view.state.field(this.stateField),e=t.open;if((e.selected>-1&&e.selected=this.range.to)&&(this.range=yu(e.options.length,e.selected,this.view.state.facet(wu).maxRenderedOptions),this.list.remove(),this.list=this.dom.appendChild(this.createListBox(e.options,t.id,this.range)),this.list.addEventListener("scroll",(()=>{this.info&&this.view.requestMeasure(this.placeInfo)}))),this.updateSelectedOption(e.selected)){this.info&&(this.info.remove(),this.info=null);let{completion:i}=e.options[e.selected],{info:n}=i;if(!n)return;let s="string"==typeof n?document.createTextNode(n):n(i);if(!s)return;"then"in s?s.then((e=>{e&&this.view.state.field(this.stateField,!1)==t&&this.addInfoPane(e)})).catch((t=>Si(this.view.state,t,"completion info"))):this.addInfoPane(s)}}addInfoPane(t){let e=this.info=document.createElement("div");e.className="cm-tooltip cm-completionInfo",e.appendChild(t),this.dom.appendChild(e),this.view.requestMeasure(this.placeInfo)}updateSelectedOption(t){let e=null;for(let i=this.list.firstChild,n=this.range.from;i;i=i.nextSibling,n++)n==t?i.hasAttribute("aria-selected")||(i.setAttribute("aria-selected","true"),e=i):i.hasAttribute("aria-selected")&&i.removeAttribute("aria-selected");return e&&function(t,e){let i=t.getBoundingClientRect(),n=e.getBoundingClientRect();n.topi.bottom&&(t.scrollTop+=n.bottom-i.bottom)}(this.list,e),e}measureInfo(){let t=this.dom.querySelector("[aria-selected]");if(!t||!this.info)return null;let e=this.dom.getBoundingClientRect(),i=this.info.getBoundingClientRect(),n=t.getBoundingClientRect(),s=this.space;if(!s){let t=this.dom.ownerDocument.defaultView||window;s={left:0,top:0,right:t.innerWidth,bottom:t.innerHeight}}if(n.top>Math.min(s.bottom,e.bottom)-10||n.bottom=i.height||t>e.top?h=n.bottom-e.top+"px":c=e.bottom-n.top+"px"}return{top:h,bottom:c,maxWidth:r,class:a?o?"left-narrow":"right-narrow":l?"left":"right"}}positionInfo(t){this.info&&(t?(this.info.style.top=t.top,this.info.style.bottom=t.bottom,this.info.style.maxWidth=t.maxWidth,this.info.className="cm-tooltip cm-completionInfo cm-completionInfo-"+t.class):this.info.style.top="-1e6px")}createListBox(t,e,i){const n=document.createElement("ul");n.id=e,n.setAttribute("role","listbox"),n.setAttribute("aria-expanded","true"),n.setAttribute("aria-label",this.view.state.phrase("Completions"));for(let s=i.from;s=this.options.length?this:new ku(this.options,Au(e,t),this.tooltip,this.timestamp,t,this.disabled)}static build(t,e,i,n,s){let r=function(t,e){let i=[],n=0;for(let s of t)if(s.hasResult())if(!1===s.result.filter){let t=s.result.getMatch;for(let e of s.result.options){let r=[1e9-n++];if(t)for(let i of t(e))r.push(i);i.push(new cu(e,s,r))}}else{let t,n=new vu(e.sliceDoc(s.from,s.to));for(let e of s.result.options)(t=n.match(e.label))&&(null!=e.boost&&(t[0]+=e.boost),i.push(new cu(e,s,t)))}let s=[],r=null,o=e.facet(wu).compareCompletions;for(let t of i.sort(((t,e)=>e.match[0]-t.match[0]||o(t.completion,e.completion))))!r||r.label!=t.completion.label||r.detail!=t.completion.detail||null!=r.type&&null!=t.completion.type&&r.type!=t.completion.type||r.apply!=t.completion.apply?s.push(t):xu(t.completion)>xu(r)&&(s[s.length-1]=t),r=t.completion;return s}(t,e);if(!r.length)return n&&t.some((t=>1==t.state))?new ku(n.options,n.attrs,n.tooltip,n.timestamp,n.selected,!0):null;let o=e.facet(wu).selectOnOpen?0:-1;if(n&&n.selected!=o&&-1!=n.selected){let t=n.options[n.selected].completion;for(let e=0;ee.hasResult()?Math.min(t,e.from):t),1e8),create:(l=Lu,t=>new bu(t,l)),above:s.aboveCursor},n?n.timestamp:Date.now(),o,!1);var l}map(t){return new ku(this.options,this.attrs,Object.assign(Object.assign({},this.tooltip),{pos:t.mapPos(this.tooltip.pos)}),this.timestamp,this.selected,this.disabled)}}class Su{constructor(t,e,i){this.active=t,this.id=e,this.open=i}static start(){return new Su(Ou,"cm-ac-"+Math.floor(2e6*Math.random()).toString(36),null)}update(t){let{state:e}=t,i=e.facet(wu),n=(i.override||e.languageDataAt("autocomplete",uu(e)).map(gu)).map((e=>(this.active.find((t=>t.source==e))||new Du(e,this.active.some((t=>0!=t.state))?1:0)).update(t,i)));n.length==this.active.length&&n.every(((t,e)=>t==this.active[e]))&&(n=this.active);let s=this.open;t.selection||n.some((e=>e.hasResult()&&t.changes.touchesRange(e.from,e.to)))||!function(t,e){if(t==e)return!0;for(let i=0,n=0;;){for(;i1==t.state))?s=null:s&&t.docChanged&&(s=s.map(t.changes)),!s&&n.every((t=>1!=t.state))&&n.some((t=>t.hasResult()))&&(n=n.map((t=>t.hasResult()?new Du(t.source,0):t)));for(let e of t.effects)e.is(Bu)&&(s=s&&s.setSelected(e.value,this.id));return n==this.active&&s==this.open?this:new Su(n,this.id,s)}get tooltip(){return this.open?this.open.tooltip:null}get attrs(){return this.open?this.open.attrs:Cu}}const Cu={"aria-autocomplete":"list"};function Au(t,e){let i={"aria-autocomplete":"list","aria-haspopup":"listbox","aria-controls":t};return e>-1&&(i["aria-activedescendant"]=t+"-"+e),i}const Ou=[];function Mu(t){return t.isUserEvent("input.type")?"input":t.isUserEvent("delete.backward")?"delete":null}class Du{constructor(t,e,i=-1){this.source=t,this.state=e,this.explicitPos=i}hasResult(){return!1}update(t,e){let i=Mu(t),n=this;i?n=n.handleUserEvent(t,i,e):t.docChanged?n=n.handleChange(t):t.selection&&0!=n.state&&(n=new Du(n.source,0));for(let e of t.effects)if(e.is(Pu))n=new Du(n.source,1,e.value?uu(t.state):-1);else if(e.is(Ru))n=new Du(n.source,0);else if(e.is(Eu))for(let t of e.value)t.source==n.source&&(n=t);return n}handleUserEvent(t,e,i){return"delete"!=e&&i.activateOnTyping?new Du(this.source,1):this.map(t.changes)}handleChange(t){return t.changes.touchesRange(uu(t.startState))?new Du(this.source,0):this.map(t.changes)}map(t){return t.empty||this.explicitPos<0?this:new Du(this.source,this.state,t.mapPos(this.explicitPos))}}class Tu extends Du{constructor(t,e,i,n,s){super(t,2,e),this.result=i,this.from=n,this.to=s}hasResult(){return!0}handleUserEvent(t,e,i){var n;let s=t.changes.mapPos(this.from),r=t.changes.mapPos(this.to,1),o=uu(t.state);if((this.explicitPos<0?o<=s:or||"delete"==e&&uu(t.startState)==this.from)return new Du(this.source,"input"==e&&i.activateOnTyping?1:0);let l,a=this.explicitPos<0?-1:t.changes.mapPos(this.explicitPos);return function(t,e,i,n){if(!t)return!1;let s=e.sliceDoc(i,n);return"function"==typeof t?t(s,i,n,e):fu(t,!0).test(s)}(this.result.validFor,t.state,s,r)?new Tu(this.source,a,this.result,s,r):this.result.update&&(l=this.result.update(this.result,s,r,new lu(t.state,o,a>=0)))?new Tu(this.source,a,l,l.from,null!==(n=l.to)&&void 0!==n?n:uu(t.state)):new Du(this.source,1,a)}handleChange(t){return t.changes.touchesRange(this.from,this.to)?new Du(this.source,0):this.map(t.changes)}map(t){return t.empty?this:new Tu(this.source,this.explicitPos<0?-1:t.mapPos(this.explicitPos),this.result,t.mapPos(this.from),t.mapPos(this.to,1))}}const Pu=ut.define(),Ru=ut.define(),Eu=ut.define({map:(t,e)=>t.map((t=>t.map(e)))}),Bu=ut.define(),Lu=q.define({create:()=>Su.start(),update:(t,e)=>t.update(e),provide:t=>[Dr.from(t,(t=>t.tooltip)),Ds.contentAttributes.from(t,(t=>t.attrs))]});function Nu(t,e="option"){return i=>{let n=i.state.field(Lu,!1);if(!n||!n.open||n.open.disabled||Date.now()-n.open.timestamp-1?n.open.selected+r*(t?1:-1):t?0:o-1;return l<0?l="page"==e?0:o-1:l>=o&&(l="page"==e?o-1:0),i.dispatch({effects:Bu.of(l)}),!0}}class Iu{constructor(t,e){this.active=t,this.context=e,this.time=Date.now(),this.updates=[],this.done=void 0}}const Vu=Mi.fromClass(class{constructor(t){this.view=t,this.debounceUpdate=-1,this.running=[],this.debounceAccept=-1,this.composing=0;for(let e of t.state.field(Lu).active)1==e.state&&this.startQuery(e)}update(t){let e=t.state.field(Lu);if(!t.selectionSet&&!t.docChanged&&t.startState.field(Lu)==e)return;let i=t.transactions.some((t=>(t.selection||t.docChanged)&&!Mu(t)));for(let e=0;e50&&Date.now()-n.time>1e3){for(let t of n.context.abortListeners)try{t()}catch(t){Si(this.view.state,t)}n.context.abortListeners=null,this.running.splice(e--,1)}else n.updates.push(...t.transactions)}if(this.debounceUpdate>-1&&clearTimeout(this.debounceUpdate),this.debounceUpdate=e.active.some((t=>1==t.state&&!this.running.some((e=>e.active.source==t.source))))?setTimeout((()=>this.startUpdate()),50):-1,0!=this.composing)for(let e of t.transactions)"input"==Mu(e)?this.composing=2:2==this.composing&&e.selection&&(this.composing=3)}startUpdate(){this.debounceUpdate=-1;let{state:t}=this.view,e=t.field(Lu);for(let t of e.active)1!=t.state||this.running.some((e=>e.active.source==t.source))||this.startQuery(t)}startQuery(t){let{state:e}=this.view,i=uu(e),n=new lu(e,i,t.explicitPos==i),s=new Iu(t,n);this.running.push(s),Promise.resolve(t.source(n)).then((t=>{s.context.aborted||(s.done=t||null,this.scheduleAccept())}),(t=>{this.view.dispatch({effects:Ru.of(null)}),Si(this.view.state,t)}))}scheduleAccept(){this.running.every((t=>void 0!==t.done))?this.accept():this.debounceAccept<0&&(this.debounceAccept=setTimeout((()=>this.accept()),50))}accept(){var t;this.debounceAccept>-1&&clearTimeout(this.debounceAccept),this.debounceAccept=-1;let e=[],i=this.view.state.facet(wu);for(let n=0;nt.source==s.active.source));if(r&&1==r.state)if(null==s.done){let t=new Du(s.active.source,0);for(let e of s.updates)t=t.update(e,i);1!=t.state&&e.push(t)}else this.startQuery(r)}e.length&&this.view.dispatch({effects:Eu.of(e)})}},{eventHandlers:{blur(){let t=this.view.state.field(Lu,!1);t&&t.tooltip&&this.view.state.facet(wu).closeOnBlur&&this.view.dispatch({effects:Ru.of(null)})},compositionstart(){this.composing=1},compositionend(){3==this.composing&&setTimeout((()=>this.view.dispatch({effects:Pu.of(!1)})),20),this.composing=0}}}),Wu=Ds.baseTheme({".cm-tooltip.cm-tooltip-autocomplete":{"& > ul":{fontFamily:"monospace",whiteSpace:"nowrap",overflow:"hidden auto",maxWidth_fallback:"700px",maxWidth:"min(700px, 95vw)",minWidth:"250px",maxHeight:"10em",listStyle:"none",margin:0,padding:0,"& > li":{overflowX:"hidden",textOverflow:"ellipsis",cursor:"pointer",padding:"1px 3px",lineHeight:1.2}}},"&light .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#17c",color:"white"},"&light .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#777"},"&dark .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#347",color:"white"},"&dark .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#444"},".cm-completionListIncompleteTop:before, .cm-completionListIncompleteBottom:after":{content:'"···"',opacity:.5,display:"block",textAlign:"center"},".cm-tooltip.cm-completionInfo":{position:"absolute",padding:"3px 9px",width:"max-content",maxWidth:"400px",boxSizing:"border-box"},".cm-completionInfo.cm-completionInfo-left":{right:"100%"},".cm-completionInfo.cm-completionInfo-right":{left:"100%"},".cm-completionInfo.cm-completionInfo-left-narrow":{right:"30px"},".cm-completionInfo.cm-completionInfo-right-narrow":{left:"30px"},"&light .cm-snippetField":{backgroundColor:"#00000022"},"&dark .cm-snippetField":{backgroundColor:"#ffffff22"},".cm-snippetFieldPosition":{verticalAlign:"text-top",width:0,height:"1.15em",display:"inline-block",margin:"0 -0.7px -.7em",borderLeft:"1.4px dotted #888"},".cm-completionMatchedText":{textDecoration:"underline"},".cm-completionDetail":{marginLeft:"0.5em",fontStyle:"italic"},".cm-completionIcon":{fontSize:"90%",width:".8em",display:"inline-block",textAlign:"center",paddingRight:".6em",opacity:"0.6"},".cm-completionIcon-function, .cm-completionIcon-method":{"&:after":{content:"'ƒ'"}},".cm-completionIcon-class":{"&:after":{content:"'○'"}},".cm-completionIcon-interface":{"&:after":{content:"'◌'"}},".cm-completionIcon-variable":{"&:after":{content:"'𝑥'"}},".cm-completionIcon-constant":{"&:after":{content:"'𝐶'"}},".cm-completionIcon-type":{"&:after":{content:"'𝑡'"}},".cm-completionIcon-enum":{"&:after":{content:"'∪'"}},".cm-completionIcon-property":{"&:after":{content:"'□'"}},".cm-completionIcon-keyword":{"&:after":{content:"'🔑︎'"}},".cm-completionIcon-namespace":{"&:after":{content:"'▢'"}},".cm-completionIcon-text":{"&:after":{content:"'abc'",fontSize:"50%",verticalAlign:"middle"}}}),zu={brackets:["(","[","{","'",'"'],before:")]}:;>",stringPrefixes:[]},Hu=ut.define({map(t,e){let i=e.mapPos(t,-1,k.TrackAfter);return null==i?void 0:i}}),Fu=ut.define({map:(t,e)=>e.mapPos(t)}),qu=new class extends At{};qu.startSide=1,qu.endSide=-1;const _u=q.define({create:()=>Tt.empty,update(t,e){if(e.selection){let i=e.state.doc.lineAt(e.selection.main.head).from,n=e.startState.doc.lineAt(e.startState.selection.main.head).from;i!=e.changes.mapPos(n,-1)&&(t=Tt.empty)}t=t.map(e.changes);for(let i of e.effects)i.is(Hu)?t=t.update({add:[qu.range(i.value,i.value+1)]}):i.is(Fu)&&(t=t.update({filter:t=>t!=i.value}));return t}});const ju="()[]{}<>";function Uu(t){for(let e=0;e{if((Qu?t.composing:t.compositionStarted)||t.state.readOnly)return!1;let s=t.state.selection.main;if(n.length>2||2==n.length&&1==b(w(n,0))||e!=s.from||i!=s.to)return!1;let r=function(t,e){let i=$u(t,t.selection.main.head),n=i.brackets||zu.brackets;for(let s of n){let r=Uu(w(s,0));if(e==s)return r==s?tf(t,s,n.indexOf(s+s+s)>-1,i):Zu(t,s,r,i.before||zu.before);if(e==r&&Ju(t,t.selection.main.from))return Yu(t,s,r)}return null}(t.state,n);return!!r&&(t.dispatch(r),!0)})),Gu=[{key:"Backspace",run:({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=$u(t,t.selection.main.head).brackets||zu.brackets,n=null,s=t.changeByRange((e=>{if(e.empty){let n=function(t,e){let i=t.sliceString(e-2,e);return b(w(i,0))==i.length?i:i.slice(1)}(t.doc,e.head);for(let s of i)if(s==n&&Xu(t.doc,e.head)==Uu(w(s,0)))return{changes:{from:e.head-s.length,to:e.head+s.length},range:E.cursor(e.head-s.length)}}return{range:n=e}}));return n||e(t.update(s,{scrollIntoView:!0,userEvent:"delete.backward"})),!n}}];function Ju(t,e){let i=!1;return t.field(_u).between(0,t.doc.length,(t=>{t==e&&(i=!0)})),i}function Xu(t,e){let i=t.sliceString(e,e+2);return i.slice(0,b(w(i,0)))}function Zu(t,e,i,n){let s=null,r=t.changeByRange((r=>{if(!r.empty)return{changes:[{insert:e,from:r.from},{insert:i,from:r.to}],effects:Hu.of(r.to+e.length),range:E.range(r.anchor+e.length,r.head+e.length)};let o=Xu(t.doc,r.head);return!o||/\s/.test(o)||n.indexOf(o)>-1?{changes:{insert:e+i,from:r.head},effects:Hu.of(r.head+e.length),range:E.cursor(r.head+e.length)}:{range:s=r}}));return s?null:t.update(r,{scrollIntoView:!0,userEvent:"input.type"})}function Yu(t,e,i){let n=null,s=t.selection.ranges.map((e=>e.empty&&Xu(t.doc,e.head)==i?E.cursor(e.head+i.length):n=e));return n?null:t.update({selection:E.create(s,t.selection.mainIndex),scrollIntoView:!0,effects:t.selection.ranges.map((({from:t})=>Fu.of(t)))})}function tf(t,e,i,n){let s=n.stringPrefixes||zu.stringPrefixes,r=null,o=t.changeByRange((n=>{if(!n.empty)return{changes:[{insert:e,from:n.from},{insert:e,from:n.to}],effects:Hu.of(n.to+e.length),range:E.range(n.anchor+e.length,n.head+e.length)};let o,l=n.head,a=Xu(t.doc,l);if(a==e){if(ef(t,l))return{changes:{insert:e+e,from:l},effects:Hu.of(l+e.length),range:E.cursor(l+e.length)};if(Ju(t,l)){let n=i&&t.sliceDoc(l,l+3*e.length)==e+e+e;return{range:E.cursor(l+e.length*(n?3:1)),effects:Fu.of(l)}}}else{if(i&&t.sliceDoc(l-2*e.length,l)==e+e&&(o=nf(t,l-2*e.length,s))>-1&&ef(t,o))return{changes:{insert:e+e+e+e,from:l},effects:Hu.of(l+e.length),range:E.cursor(l+e.length)};if(t.charCategorizer(l)(a)!=yt.Word&&nf(t,l,s)>-1&&!function(t,e,i,n){let s=vl(t).resolveInner(e,-1),r=n.reduce(((t,e)=>Math.max(t,e.length)),0);for(let o=0;o<5;o++){let o=t.sliceDoc(s.from,Math.min(s.to,s.from+i.length+r)),l=o.indexOf(i);if(!l||l>-1&&n.indexOf(o.slice(0,l))>-1){let e=s.firstChild;for(;e&&e.from==s.from&&e.to-e.from>i.length+l;){if(t.sliceDoc(e.to-i.length,e.to)==i)return!1;e=e.firstChild}return!0}let a=s.to==e&&s.parent;if(!a)break;s=a}return!1}(t,l,e,s))return{changes:{insert:e+e,from:l},effects:Hu.of(l+e.length),range:E.cursor(l+e.length)}}return{range:r=n}}));return r?null:t.update(o,{scrollIntoView:!0,userEvent:"input.type"})}function ef(t,e){let i=vl(t).resolveInner(e+1);return i.parent&&i.from==e}function nf(t,e,i){let n=t.charCategorizer(e);if(n(t.sliceDoc(e-1,e))!=yt.Word)return e;for(let s of i){let i=e-s.length;if(t.sliceDoc(i,e)==s&&n(t.sliceDoc(i-1,i))!=yt.Word)return i}return-1}function sf(t={}){return[Lu,wu.of(t),Vu,of,Wu]}const rf=[{key:"Ctrl-Space",run:t=>!!t.state.field(Lu,!1)&&(t.dispatch({effects:Pu.of(!0)}),!0)},{key:"Escape",run:t=>{let e=t.state.field(Lu,!1);return!(!e||!e.active.some((t=>0!=t.state)))&&(t.dispatch({effects:Ru.of(null)}),!0)}},{key:"ArrowDown",run:Nu(!0)},{key:"ArrowUp",run:Nu(!1)},{key:"PageDown",run:Nu(!0,"page")},{key:"PageUp",run:Nu(!1,"page")},{key:"Enter",run:t=>{let e=t.state.field(Lu,!1);return!(t.state.readOnly||!e||!e.open||e.open.selected<0||Date.now()-e.open.timestampt.facet(wu).defaultKeymap?[rf]:[])));class lf{constructor(t,e,i){this.from=t,this.to=e,this.diagnostic=i}}class af{constructor(t,e,i){this.diagnostics=t,this.panel=e,this.selected=i}static init(t,e,i){let n=t,s=i.facet(kf).markerFilter;s&&(n=s(n));let r=ii.set(n.map((t=>t.from==t.to||t.from==t.to-1&&i.doc.lineAt(t.from).to==t.from?ii.widget({widget:new Af(t),diagnostic:t}).range(t.from):ii.mark({attributes:{class:"cm-lintRange cm-lintRange-"+t.severity},diagnostic:t}).range(t.from,t.to))),!0);return new af(r,e,hf(r))}}function hf(t,e=null,i=0){let n=null;return t.between(i,1e9,((t,i,{spec:s})=>{if(!e||s.diagnostic==e)return n=new lf(t,i,s.diagnostic),!1})),n}function cf(t,e){return!(!t.effects.some((t=>t.is(ff)))&&!t.changes.touchesRange(e.pos))}function uf(t,e){return t.field(mf,!1)?e:e.concat(ut.appendConfig.of([mf,Ds.decorations.compute([mf],(t=>{let{selected:e,panel:i}=t.field(mf);return e&&i&&e.from!=e.to?ii.set([gf.range(e.from,e.to)]):ii.none})),Lr(vf,{hideOn:cf}),Tf]))}const ff=ut.define(),df=ut.define(),pf=ut.define(),mf=q.define({create:()=>new af(ii.none,null,null),update(t,e){if(e.docChanged){let i=t.diagnostics.map(e.changes),n=null;if(t.selected){let s=e.changes.mapPos(t.selected.from,1);n=hf(i,t.selected.diagnostic,s)||hf(i,null,s)}t=new af(i,t.panel,n)}for(let i of e.effects)i.is(ff)?t=af.init(i.value,t.panel,e.state):i.is(df)?t=new af(t.diagnostics,i.value?Mf.open:null,t.selected):i.is(pf)&&(t=new af(t.diagnostics,t.panel,i.value));return t},provide:t=>[Fr.from(t,(t=>t.panel)),Ds.decorations.from(t,(t=>t.diagnostics))]}),gf=ii.mark({class:"cm-lintRange cm-lintRange-active"});function vf(t,e,i){let{diagnostics:n}=t.state.field(mf),s=[],r=2e8,o=0;n.between(e-(i<0?1:0),e+(i>0?1:0),((t,n,{spec:l})=>{e>=t&&e<=n&&(t==n||(e>t||i>0)&&(e({dom:wf(t,s)})}:null}function wf(t,e){return rc("ul",{class:"cm-tooltip-lint"},e.map((e=>Cf(t,e,!1))))}const yf=t=>{let e=t.state.field(mf,!1);return!(!e||!e.panel)&&(t.dispatch({effects:df.of(!1)}),!0)},bf=[{key:"Mod-Shift-m",run:t=>{let e=t.state.field(mf,!1);e&&e.panel||t.dispatch({effects:uf(t.state,[df.of(!0)])});let i=Vr(t,Mf.open);return i&&i.dom.querySelector(".cm-panel-lint ul").focus(),!0},preventDefault:!0},{key:"F8",run:t=>{let e=t.state.field(mf,!1);if(!e)return!1;let i=t.state.selection.main,n=e.diagnostics.iter(i.to+1);return!(!n.value&&(n=e.diagnostics.iter(0),!n.value||n.from==i.from&&n.to==i.to))&&(t.dispatch({selection:{anchor:n.from,head:n.to},scrollIntoView:!0}),!0)}}],xf=Mi.fromClass(class{constructor(t){this.view=t,this.timeout=-1,this.set=!0;let{delay:e}=t.state.facet(kf);this.lintTime=Date.now()+e,this.run=this.run.bind(this),this.timeout=setTimeout(this.run,e)}run(){let t=Date.now();if(tPromise.resolve(t(this.view))))).then((e=>{let i=e.reduce(((t,e)=>t.concat(e)));this.view.state.doc==t.doc&&this.view.dispatch(function(t,e){return{effects:uf(t,[ff.of(e)])}}(this.view.state,i))}),(t=>{Si(this.view.state,t)}))}}update(t){let e=t.state.facet(kf);(t.docChanged||e!=t.startState.facet(kf))&&(this.lintTime=Date.now()+e.delay,this.set||(this.set=!0,this.timeout=setTimeout(this.run,e.delay)))}force(){this.set&&(this.lintTime=Date.now(),this.run())}destroy(){clearTimeout(this.timeout)}}),kf=N.define({combine:t=>Object.assign({sources:t.map((t=>t.source))},Ct(t.map((t=>t.config)),{delay:750,markerFilter:null,tooltipFilter:null})),enables:xf});function Sf(t){let e=[];if(t)t:for(let{name:i}of t){for(let t=0;tt.toLowerCase()==n.toLowerCase()))){e.push(n);continue t}}e.push("")}return e}function Cf(t,e,i){var n;let s=i?Sf(e.actions):[];return rc("li",{class:"cm-diagnostic cm-diagnostic-"+e.severity},rc("span",{class:"cm-diagnosticText"},e.renderMessage?e.renderMessage():e.message),null===(n=e.actions)||void 0===n?void 0:n.map(((i,n)=>{let r=n=>{n.preventDefault();let s=hf(t.state.field(mf).diagnostics,e);s&&i.apply(t,s.from,s.to)},{name:o}=i,l=s[n]?o.indexOf(s[n]):-1,a=l<0?o:[o.slice(0,l),rc("u",o.slice(l,l+1)),o.slice(l+1)];return rc("button",{type:"button",class:"cm-diagnosticAction",onclick:r,onmousedown:r,"aria-label":` Action: ${o}${l<0?"":` (access key "${s[n]})"`}.`},a)})),e.source&&rc("div",{class:"cm-diagnosticSource"},e.source))}class Af extends ti{constructor(t){super(),this.diagnostic=t}eq(t){return t.diagnostic==this.diagnostic}toDOM(){return rc("span",{class:"cm-lintPoint cm-lintPoint-"+this.diagnostic.severity})}}class Of{constructor(t,e){this.diagnostic=e,this.id="item_"+Math.floor(4294967295*Math.random()).toString(16),this.dom=Cf(t,e,!0),this.dom.id=this.id,this.dom.setAttribute("role","option")}}class Mf{constructor(t){this.view=t,this.items=[];this.list=rc("ul",{tabIndex:0,role:"listbox","aria-label":this.view.state.phrase("Diagnostics"),onkeydown:e=>{if(27==e.keyCode)yf(this.view),this.view.focus();else if(38==e.keyCode||33==e.keyCode)this.moveSelection((this.selectedIndex-1+this.items.length)%this.items.length);else if(40==e.keyCode||34==e.keyCode)this.moveSelection((this.selectedIndex+1)%this.items.length);else if(36==e.keyCode)this.moveSelection(0);else if(35==e.keyCode)this.moveSelection(this.items.length-1);else if(13==e.keyCode)this.view.focus();else{if(!(e.keyCode>=65&&e.keyCode<=90&&this.selectedIndex>=0))return;{let{diagnostic:i}=this.items[this.selectedIndex],n=Sf(i.actions);for(let s=0;s{for(let e=0;eyf(this.view)},"×")),this.update()}get selectedIndex(){let t=this.view.state.field(mf).selected;if(!t)return-1;for(let e=0;e{let l,a=-1;for(let t=i;ti&&(this.items.splice(i,a-i),n=!0)),e&&l.diagnostic==e.diagnostic?l.dom.hasAttribute("aria-selected")||(l.dom.setAttribute("aria-selected","true"),s=l):l.dom.hasAttribute("aria-selected")&&l.dom.removeAttribute("aria-selected"),i++}));i({sel:s.dom.getBoundingClientRect(),panel:this.list.getBoundingClientRect()}),write:({sel:t,panel:e})=>{t.tope.bottom&&(this.list.scrollTop+=t.bottom-e.bottom)}})):this.selectedIndex<0&&this.list.removeAttribute("aria-activedescendant"),n&&this.sync()}sync(){let t=this.list.firstChild;function e(){let e=t;t=e.nextSibling,e.remove()}for(let i of this.items)if(i.dom.parentNode==this.list){for(;t!=i.dom;)e();t=i.dom.nextSibling}else this.list.insertBefore(i.dom,t);for(;t;)e()}moveSelection(t){if(this.selectedIndex<0)return;let e=hf(this.view.state.field(mf).diagnostics,this.items[t].diagnostic);e&&this.view.dispatch({selection:{anchor:e.from,head:e.to},scrollIntoView:!0,effects:pf.of(e)})}static open(t){return new Mf(t)}}function Df(t){return function(t,e='viewBox="0 0 40 40"'){return`url('data:image/svg+xml,${encodeURIComponent(t)}')`}(``,'width="6" height="3"')}const Tf=Ds.baseTheme({".cm-diagnostic":{padding:"3px 6px 3px 8px",marginLeft:"-1px",display:"block",whiteSpace:"pre-wrap"},".cm-diagnostic-error":{borderLeft:"5px solid #d11"},".cm-diagnostic-warning":{borderLeft:"5px solid orange"},".cm-diagnostic-info":{borderLeft:"5px solid #999"},".cm-diagnosticAction":{font:"inherit",border:"none",padding:"2px 4px",backgroundColor:"#444",color:"white",borderRadius:"3px",marginLeft:"8px"},".cm-diagnosticSource":{fontSize:"70%",opacity:.7},".cm-lintRange":{backgroundPosition:"left bottom",backgroundRepeat:"repeat-x",paddingBottom:"0.7px"},".cm-lintRange-error":{backgroundImage:Df("#d11")},".cm-lintRange-warning":{backgroundImage:Df("orange")},".cm-lintRange-info":{backgroundImage:Df("#999")},".cm-lintRange-active":{backgroundColor:"#ffdd9980"},".cm-tooltip-lint":{padding:0,margin:0},".cm-lintPoint":{position:"relative","&:after":{content:'""',position:"absolute",bottom:0,left:"-2px",borderLeft:"3px solid transparent",borderRight:"3px solid transparent",borderBottom:"4px solid #d11"}},".cm-lintPoint-warning":{"&:after":{borderBottomColor:"orange"}},".cm-lintPoint-info":{"&:after":{borderBottomColor:"#999"}},".cm-panel.cm-panel-lint":{position:"relative","& ul":{maxHeight:"100px",overflowY:"auto","& [aria-selected]":{backgroundColor:"#ddd","& u":{textDecoration:"underline"}},"&:focus [aria-selected]":{background_fallback:"#bdf",backgroundColor:"Highlight",color_fallback:"white",color:"HighlightText"},"& u":{textDecoration:"none"},padding:0,margin:0},"& [name=close]":{position:"absolute",top:"0",right:"2px",background:"inherit",border:"none",font:"inherit",padding:0,margin:0}}}),Pf=(()=>[oo(),ho,ar(),Qa(),la(),_s(),[Ys,tr],St.allowMultipleSelections.of(!0),St.transactionFilter.of((t=>{if(!t.docChanged||!t.isUserEvent("input.type")&&!t.isUserEvent("input.complete"))return t;let e=t.startState.languageDataAt("indentOnInput",t.startState.selection.main.head);if(!e.length)return t;let i=t.newDoc,{head:n}=t.newSelection.main,s=i.lineAt(n);if(n>s.from+200)return t;let r=i.sliceString(s.from,n);if(!e.some((t=>t.test(r))))return t;let{state:o}=t,l=-1,a=[];for(let{head:t}of o.selection.ranges){let e=o.doc.lineAt(t);if(e.from==l)continue;l=e.from;let i=El(o,e.from);if(null==i)continue;let n=/^\s*/.exec(e.text)[0],s=Rl(o,i);n!=s&&a.push({from:e.from,to:e.from+n.length,insert:s})}return a.length?[t,{changes:a,sequential:!0}]:t})),da(ga,{fallback:!0}),Aa(),[Ku,_u],sf(),vr(),br(),dr,kc(),Is.of([...Gu,...sc,...tu,...hh,...ta,...rf,...bf])])();class Rf{constructor(t,e,i,n,s,r,o,l,a,h=0,c){this.p=t,this.stack=e,this.state=i,this.reducePos=n,this.pos=s,this.score=r,this.buffer=o,this.bufferBase=l,this.curContext=a,this.lookAhead=h,this.parent=c}toString(){return`[${this.stack.filter(((t,e)=>e%3==0)).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(t,e,i=0){let n=t.parser.context;return new Rf(t,[],e,i,i,0,[],0,n?new Ef(n,n.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(t,e){this.stack.push(this.state,e,this.bufferBase+this.buffer.length),this.state=t}reduce(t){let e=t>>19,i=65535&t,{parser:n}=this.p,s=n.dynamicPrecedence(i);if(s&&(this.score+=s),0==e)return this.pushState(n.getGoto(this.state,i,!0),this.reducePos),ir;)this.stack.pop();this.reduceContext(i,o)}storeNode(t,e,i,n=4,s=!1){if(0==t&&(!this.stack.length||this.stack[this.stack.length-1]0&&0==t.buffer[n-4]&&t.buffer[n-1]>-1){if(e==i)return;if(t.buffer[n-2]>=e)return void(t.buffer[n-2]=i)}}if(s&&this.pos!=i){let s=this.buffer.length;if(s>0&&0!=this.buffer[s-4])for(;s>0&&this.buffer[s-2]>i;)this.buffer[s]=this.buffer[s-4],this.buffer[s+1]=this.buffer[s-3],this.buffer[s+2]=this.buffer[s-2],this.buffer[s+3]=this.buffer[s-1],s-=4,n>4&&(n-=4);this.buffer[s]=t,this.buffer[s+1]=e,this.buffer[s+2]=i,this.buffer[s+3]=n}else this.buffer.push(t,e,i,n)}shift(t,e,i){let n=this.pos;if(131072&t)this.pushState(65535&t,this.pos);else if(0==(262144&t)){let s=t,{parser:r}=this.p;(i>this.pos||e<=r.maxNode)&&(this.pos=i,r.stateFlag(s,1)||(this.reducePos=i)),this.pushState(s,n),this.shiftContext(e,n),e<=r.maxNode&&this.buffer.push(e,n,i,4)}else this.pos=i,this.shiftContext(e,n),e<=this.p.parser.maxNode&&this.buffer.push(e,n,i,4)}apply(t,e,i){65536&t?this.reduce(t):this.shift(t,e,i)}useNode(t,e){let i=this.p.reused.length-1;(i<0||this.p.reused[i]!=t)&&(this.p.reused.push(t),i++);let n=this.pos;this.reducePos=this.pos=n+t.length,this.pushState(e,n),this.buffer.push(i,n,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,t,this,this.p.stream.reset(this.pos-t.length)))}split(){let t=this,e=t.buffer.length;for(;e>0&&t.buffer[e-2]>t.reducePos;)e-=4;let i=t.buffer.slice(e),n=t.bufferBase+e;for(;t&&n==t.bufferBase;)t=t.parent;return new Rf(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,i,n,this.curContext,this.lookAhead,t)}recoverByDelete(t,e){let i=t<=this.p.parser.maxNode;i&&this.storeNode(t,this.pos,e,4),this.storeNode(0,this.pos,e,i?8:4),this.pos=this.reducePos=e,this.score-=190}canShift(t){for(let e=new Lf(this);;){let i=this.p.parser.stateSlot(e.state,4)||this.p.parser.hasAction(e.state,t);if(0==(65536&i))return!0;if(0==i)return!1;e.reduce(i)}}recoverByInsert(t){if(this.stack.length>=300)return[];let e=this.p.parser.nextStates(this.state);if(e.length>8||this.stack.length>=120){let i=[];for(let n,s=0;s1&e&&t==n))||i.push(e[t],n)}e=i}let i=[];for(let t=0;t>19,n=65535&t,s=this.stack.length-3*i;if(s<0||e.getGoto(this.stack[s],n,!1)<0)return!1;this.storeNode(0,this.reducePos,this.reducePos,4,!0),this.score-=100}return this.reducePos=this.pos,this.reduce(t),!0}forceAll(){for(;!this.p.parser.stateFlag(this.state,2);)if(!this.forceReduce()){this.storeNode(0,this.pos,this.pos,4,!0);break}return this}get deadEnd(){if(3!=this.stack.length)return!1;let{parser:t}=this.p;return 65535==t.data[t.stateSlot(this.state,1)]&&!t.stateSlot(this.state,4)}restart(){this.state=this.stack[0],this.stack.length=0}sameState(t){if(this.state!=t.state||this.stack.length!=t.stack.length)return!1;for(let e=0;ethis.lookAhead&&(this.emitLookAhead(),this.lookAhead=t)}close(){this.curContext&&this.curContext.tracker.strict&&this.emitContext(),this.lookAhead>0&&this.emitLookAhead()}}class Ef{constructor(t,e){this.tracker=t,this.context=e,this.hash=t.strict?t.hash(e):0}}var Bf;!function(t){t[t.Insert=200]="Insert",t[t.Delete=190]="Delete",t[t.Reduce=100]="Reduce",t[t.MaxNext=4]="MaxNext",t[t.MaxInsertStackDepth=300]="MaxInsertStackDepth",t[t.DampenInsertStackDepth=120]="DampenInsertStackDepth"}(Bf||(Bf={}));class Lf{constructor(t){this.start=t,this.state=t.state,this.stack=t.stack,this.base=this.stack.length}reduce(t){let e=65535&t,i=t>>19;0==i?(this.stack==this.start.stack&&(this.stack=this.stack.slice()),this.stack.push(this.state,0,0),this.base+=3):this.base-=3*(i-1);let n=this.start.p.parser.getGoto(this.stack[this.base-3],e,!0);this.state=n}}class Nf{constructor(t,e,i){this.stack=t,this.pos=e,this.index=i,this.buffer=t.buffer,0==this.index&&this.maybeNext()}static create(t,e=t.bufferBase+t.buffer.length){return new Nf(t,e,e-t.bufferBase)}maybeNext(){let t=this.stack.parent;null!=t&&(this.index=this.stack.bufferBase-t.bufferBase,this.stack=t,this.buffer=t.buffer)}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}next(){this.index-=4,this.pos-=4,0==this.index&&this.maybeNext()}fork(){return new Nf(this.stack,this.pos,this.index)}}class If{constructor(){this.start=-1,this.value=-1,this.end=-1,this.extended=-1,this.lookAhead=0,this.mask=0,this.context=0}}const Vf=new If;class Wf{constructor(t,e){this.input=t,this.ranges=e,this.chunk="",this.chunkOff=0,this.chunk2="",this.chunk2Pos=0,this.next=-1,this.token=Vf,this.rangeIndex=0,this.pos=this.chunkPos=e[0].from,this.range=e[0],this.end=e[e.length-1].to,this.readNext()}resolveOffset(t,e){let i=this.range,n=this.rangeIndex,s=this.pos+t;for(;si.to:s>=i.to;){if(n==this.ranges.length-1)return null;let t=this.ranges[++n];s+=t.from-i.to,i=t}return s}clipPos(t){if(t>=this.range.from&&tt)return Math.max(t,e.from);return this.end}peek(t){let e,i,n=this.chunkOff+t;if(n>=0&&n=this.chunk2Pos&&en.to&&(this.chunk2=this.chunk2.slice(0,n.to-e)),i=this.chunk2.charCodeAt(0)}}return e>=this.token.lookAhead&&(this.token.lookAhead=e+1),i}acceptToken(t,e=0){let i=e?this.resolveOffset(e,-1):this.pos;if(null==i||i=this.chunk2Pos&&this.posthis.range.to?t.slice(0,this.range.to-this.pos):t,this.chunkPos=this.pos,this.chunkOff=0}}readNext(){return this.chunkOff>=this.chunk.length&&(this.getChunk(),this.chunkOff==this.chunk.length)?this.next=-1:this.next=this.chunk.charCodeAt(this.chunkOff)}advance(t=1){for(this.chunkOff+=t;this.pos+t>=this.range.to;){if(this.rangeIndex==this.ranges.length-1)return this.setDone();t-=this.range.to-this.pos,this.range=this.ranges[++this.rangeIndex],this.pos=this.range.from}return this.pos+=t,this.pos>=this.token.lookAhead&&(this.token.lookAhead=this.pos+1),this.readNext()}setDone(){return this.pos=this.chunkPos=this.end,this.range=this.ranges[this.rangeIndex=this.ranges.length-1],this.chunk="",this.next=-1}reset(t,e){if(e?(this.token=e,e.start=t,e.lookAhead=t+1,e.value=e.extended=-1):this.token=Vf,this.pos!=t){if(this.pos=t,t==this.end)return this.setDone(),this;for(;t=this.range.to;)this.range=this.ranges[++this.rangeIndex];t>=this.chunkPos&&t=this.chunkPos&&e<=this.chunkPos+this.chunk.length)return this.chunk.slice(t-this.chunkPos,e-this.chunkPos);if(t>=this.chunk2Pos&&e<=this.chunk2Pos+this.chunk2.length)return this.chunk2.slice(t-this.chunk2Pos,e-this.chunk2Pos);if(t>=this.range.from&&e<=this.range.to)return this.input.read(t,e);let i="";for(let n of this.ranges){if(n.from>=e)break;n.to>t&&(i+=this.input.read(Math.max(n.from,t),Math.min(n.to,e)))}return i}}class zf{constructor(t,e){this.data=t,this.id=e}token(t,e){!function(t,e,i,n){let s=0,r=1<0){let i=t[n];if(l.allows(i)&&(-1==e.token.value||e.token.value==i||o.overrides(i,e.token.value))){e.acceptToken(i);break}}let n=e.next,a=0,h=t[s+2];if(!(e.next<0&&h>a&&65535==t[i+3*h-3]&&65535==t[i+3*h-3])){for(;a>1,o=i+r+(r<<1),l=t[o],c=t[o+1]||65536;if(n=c)){s=t[o+2],e.advance();continue t}a=r+1}}break}s=t[i+3*h-1]}}(this.data,t,e,this.id)}}zf.prototype.contextual=zf.prototype.fallback=zf.prototype.extend=!1;class Hf{constructor(t,e={}){this.token=t,this.contextual=!!e.contextual,this.fallback=!!e.fallback,this.extend=!!e.extend}}function Ff(t,e=Uint16Array){if("string"!=typeof t)return t;let i=null;for(let n=0,s=0;n=92&&e--,e>=34&&e--;let s=e-32;if(s>=46&&(s-=46,i=!0),r+=s,i)break;r*=46}i?i[s++]=r:i=new e(r)}return i}const qf="undefined"!=typeof process&&process.env&&/\bparse\b/.test(process.env.LOG);let _f=null;var jf,Uf;function $f(t,e,i){let n=t.cursor(bo.IncludeAnonymous);for(n.moveTo(e);;)if(!(i<0?n.childBefore(e):n.childAfter(e)))for(;;){if((i<0?n.toe)&&!n.type.isError)return i<0?Math.max(0,Math.min(n.to-1,e-25)):Math.min(t.length,Math.max(n.from+1,e+25));if(i<0?n.prevSibling():n.nextSibling())break;if(!n.parent())return i<0?0:t.length}}!function(t){t[t.Margin=25]="Margin"}(jf||(jf={}));class Qf{constructor(t,e){this.fragments=t,this.nodeSet=e,this.i=0,this.fragment=null,this.safeFrom=-1,this.safeTo=-1,this.trees=[],this.start=[],this.index=[],this.nextFragment()}nextFragment(){let t=this.fragment=this.i==this.fragments.length?null:this.fragments[this.i++];if(t){for(this.safeFrom=t.openStart?$f(t.tree,t.from+t.offset,1)-t.offset:t.from,this.safeTo=t.openEnd?$f(t.tree,t.to+t.offset,-1)-t.offset:t.to;this.trees.length;)this.trees.pop(),this.start.pop(),this.index.pop();this.trees.push(t.tree),this.start.push(-t.offset),this.index.push(0),this.nextStart=this.safeFrom}else this.nextStart=1e9}nodeAt(t){if(tt)return this.nextStart=r,null;if(s instanceof xo){if(r==t){if(r=Math.max(this.safeFrom,t)&&(this.trees.push(s),this.start.push(r),this.index.push(0))}else this.index[e]++,this.nextStart=r+s.length}}}class Kf{constructor(t,e){this.stream=e,this.tokens=[],this.mainToken=null,this.actions=[],this.tokens=t.tokenizers.map((t=>new If))}getActions(t){let e=0,i=null,{parser:n}=t.p,{tokenizers:s}=n,r=n.stateSlot(t.state,3),o=t.curContext?t.curContext.hash:0,l=0;for(let n=0;nh.end+25&&(l=Math.max(h.lookAhead,l)),0!=h.value)){let n=e;if(h.extended>-1&&(e=this.addActions(t,h.extended,h.end,e)),e=this.addActions(t,h.value,h.end,e),!a.extend&&(i=h,e>n))break}}for(;this.actions.length>e;)this.actions.pop();return l&&t.setLookAhead(l),i||t.pos!=this.stream.end||(i=new If,i.value=t.p.parser.eofTerm,i.start=i.end=t.pos,e=this.addActions(t,i.value,i.end,e)),this.mainToken=i,this.actions}getMainToken(t){if(this.mainToken)return this.mainToken;let e=new If,{pos:i,p:n}=t;return e.start=i,e.end=Math.min(i+1,n.stream.end),e.value=i==n.stream.end?n.parser.eofTerm:0,e}updateCachedToken(t,e,i){let n=this.stream.clipPos(i.pos);if(e.token(this.stream.reset(n,t),i),t.value>-1){let{parser:e}=i.p;for(let n=0;n=0&&i.p.parser.dialect.allows(s>>1)){0==(1&s)?t.value=s>>1:t.extended=s>>1;break}}}else t.value=0,t.end=this.stream.clipPos(n+1)}putAction(t,e,i,n){for(let e=0;e4*t.bufferLength?new Qf(i,t.nodeSet):null}get parsedPos(){return this.minStackPos}advance(){let t,e,i=this.stacks,n=this.minStackPos,s=this.stacks=[];for(let r=0;rn)s.push(o);else{if(this.advanceStack(o,s,i))continue;{t||(t=[],e=[]),t.push(o);let i=this.tokens.getMainToken(o);e.push(i.value,i.end)}}break}}if(!s.length){let e=t&&function(t){let e=null;for(let i of t){let t=i.p.stoppedAt;(i.pos==i.p.stream.end||null!=t&&i.pos>t)&&i.p.parser.stateFlag(i.state,2)&&(!e||e.scorethis.stoppedAt?t[0]:this.runRecovery(t,e,s);if(i)return this.stackToTree(i.forceAll())}if(this.recovering){let t=1==this.recovering?1:3*this.recovering;if(s.length>t)for(s.sort(((t,e)=>e.score-t.score));s.length>t;)s.pop();s.some((t=>t.reducePos>n))&&this.recovering--}else if(s.length>1)t:for(let t=0;t500&&n.buffer.length>500){if(!((e.score-n.score||e.buffer.length-n.buffer.length)>0)){s.splice(t--,1);continue t}s.splice(i--,1)}}}this.minStackPos=s[0].pos;for(let t=1;t ":"";if(null!=this.stoppedAt&&n>this.stoppedAt)return t.forceReduce()?t:null;if(this.fragments){let e=t.curContext&&t.curContext.tracker.strict,i=e?t.curContext.hash:0;for(let o=this.fragments.nodeAt(n);o;){let n=this.parser.nodeSet.types[o.type.id]==o.type?s.getGoto(t.state,o.type.id):-1;if(n>-1&&o.length&&(!e||(o.prop(po.contextHash)||0)==i))return t.useNode(o,n),qf&&console.log(r+this.stackID(t)+` (via reuse of ${s.getName(o.type.id)})`),!0;if(!(o instanceof xo)||0==o.children.length||o.positions[0]>0)break;let l=o.children[0];if(!(l instanceof xo&&0==o.positions[0]))break;o=l}}let o=s.stateSlot(t.state,4);if(o>0)return t.reduce(o),qf&&console.log(r+this.stackID(t)+` (via always-reduce ${s.getName(65535&o)})`),!0;if(t.stack.length>=15e3)for(;t.stack.length>9e3&&t.forceReduce(););let l=this.tokens.getActions(t);for(let o=0;on?e.push(f):i.push(f)}return!1}advanceFully(t,e){let i=t.pos;for(;;){if(!this.advanceStack(t,null,null))return!1;if(t.pos>i)return Jf(t,e),!0}}runRecovery(t,e,i){let n=null,s=!1;for(let r=0;r ":"";if(o.deadEnd){if(s)continue;if(s=!0,o.restart(),qf&&console.log(h+this.stackID(o)+" (restarted)"),this.advanceFully(o,i))continue}let c=o.split(),u=h;for(let t=0;c.forceReduce()&&t<10;t++){if(qf&&console.log(u+this.stackID(c)+" (via force-reduce)"),this.advanceFully(c,i))break;qf&&(u=this.stackID(c)+" -> ")}for(let t of o.recoverByInsert(l))qf&&console.log(h+this.stackID(t)+" (via recover-insert)"),this.advanceFully(t,i);this.stream.end>o.pos?(a==o.pos&&(a++,l=0),o.recoverByDelete(l,a),qf&&console.log(h+this.stackID(o)+` (via recover-delete ${this.parser.getName(l)})`),Jf(o,i)):(!n||n.scoret.topRules[e][1])),n=[];for(let t=0;t=0)s(n,t,e[i++]);else{let r=e[i+-n];for(let o=-n;o>0;o--)s(e[i++],t,r);i++}}}this.nodeSet=new vo(e.map(((e,s)=>go.define({name:s>=this.minRepeatTerm?void 0:e,id:s,props:n[s],top:i.indexOf(s)>-1,error:0==s,skipped:t.skippedNodes&&t.skippedNodes.indexOf(s)>-1})))),t.propSources&&(this.nodeSet=this.nodeSet.extend(...t.propSources)),this.strict=!1,this.bufferLength=co;let r=Ff(t.tokenData);this.context=t.context,this.specializerSpecs=t.specialized||[],this.specialized=new Uint16Array(this.specializerSpecs.length);for(let t=0;t"number"==typeof t?new zf(r,t):t)),this.topRules=t.topRules,this.dialects=t.dialects||{},this.dynamicPrecedences=t.dynamicPrecedences||null,this.tokenPrecTable=t.tokenPrec,this.termNames=t.termNames||null,this.maxNode=this.nodeSet.types.length-1,this.dialect=this.parseDialect(),this.top=this.topRules[Object.keys(this.topRules)[0]]}createParse(t,e,i){let n=new Gf(this,t,e,i);for(let s of this.wrappers)n=s(n,t,e,i);return n}getGoto(t,e,i=!1){let n=this.goto;if(e>=n[0])return-1;for(let s=n[e+1];;){let e=n[s++],r=1&e,o=n[s++];if(r&&i)return o;for(let i=s+(e>>1);s0}validAction(t,e){if(e==this.stateSlot(t,4))return!0;for(let i=this.stateSlot(t,1);;i+=3){if(65535==this.data[i]){if(1!=this.data[i+1])return!1;i=Yf(this.data,i+2)}if(e==Yf(this.data,i+1))return!0}}nextStates(t){let e=[];for(let i=this.stateSlot(t,1);;i+=3){if(65535==this.data[i]){if(1!=this.data[i+1])break;i=Yf(this.data,i+2)}if(0==(1&this.data[i+2])){let t=this.data[i+1];e.some(((e,i)=>1&i&&e==t))||e.push(this.data[i],t)}}return e}overrides(t,e){let i=td(this.data,this.tokenPrecTable,e);return i<0||td(this.data,this.tokenPrecTable,t){let i=t.tokenizers.find((t=>t.from==e));return i?i.to:e}))),t.specializers&&(e.specializers=this.specializers.slice(),e.specializerSpecs=this.specializerSpecs.map(((i,n)=>{let s=t.specializers.find((t=>t.from==i.external));if(!s)return i;let r=Object.assign(Object.assign({},i),{external:s.to});return e.specializers[n]=ed(r),r}))),t.contextTracker&&(e.context=t.contextTracker),t.dialect&&(e.dialect=this.parseDialect(t.dialect)),null!=t.strict&&(e.strict=t.strict),t.wrap&&(e.wrappers=e.wrappers.concat(t.wrap)),null!=t.bufferLength&&(e.bufferLength=t.bufferLength),e}hasWrappers(){return this.wrappers.length>0}getName(t){return this.termNames?this.termNames[t]:String(t<=this.maxNode&&this.nodeSet.types[t].name||t)}get eofTerm(){return this.maxNode+1}get topNode(){return this.nodeSet.types[this.top[1]]}dynamicPrecedence(t){let e=this.dynamicPrecedences;return null==e?0:e[t]||0}parseDialect(t){let e=Object.keys(this.dialects),i=e.map((()=>!1));if(t)for(let n of t.split(" ")){let t=e.indexOf(n);t>=0&&(i[t]=!0)}let n=null;for(let t=0;tt.external(i,n)<<1|e}return t.get}function id(t){return t>=65&&t<=90||t>=97&&t<=122||t>=48&&t<=57}function nd(t,e,i){for(let n=!1;;){if(t.next<0)return;if(t.next==e&&!n)return void t.advance();n=i&&!n&&92==t.next,t.advance()}}function sd(t,e){for(;95==t.next||id(t.next);)null!=e&&(e+=String.fromCharCode(t.next)),t.advance();return e}function rd(t,e){for(;48==t.next||49==t.next;)t.advance();e&&t.next==e&&t.advance()}function od(t,e){for(;;){if(46==t.next){if(e)break;e=!0}else if(t.next<48||t.next>57)break;t.advance()}if(69==t.next||101==t.next)for(t.advance(),43!=t.next&&45!=t.next||t.advance();t.next>=48&&t.next<=57;)t.advance()}function ld(t){for(;!(t.next<0||10==t.next);)t.advance()}function ad(t,e){for(let i=0;i!=&|~^/",specialVar:"?",identifierQuotes:'"',words:cd("absolute action add after all allocate alter and any are as asc assertion at authorization before begin between both breadth by call cascade cascaded case cast catalog check close collate collation column commit condition connect connection constraint constraints constructor continue corresponding count create cross cube current current_date current_default_transform_group current_transform_group_for_type current_path current_role current_time current_timestamp current_user cursor cycle data day deallocate declare default deferrable deferred delete depth deref desc describe descriptor deterministic diagnostics disconnect distinct do domain drop dynamic each else elseif end end-exec equals escape except exception exec execute exists exit external fetch first for foreign found from free full function general get global go goto grant group grouping handle having hold hour identity if immediate in indicator initially inner inout input insert intersect into is isolation join key language last lateral leading leave left level like limit local localtime localtimestamp locator loop map match method minute modifies module month names natural nesting new next no none not of old on only open option or order ordinality out outer output overlaps pad parameter partial path prepare preserve primary prior privileges procedure public read reads recursive redo ref references referencing relative release repeat resignal restrict result return returns revoke right role rollback rollup routine row rows savepoint schema scroll search second section select session session_user set sets signal similar size some space specific specifictype sql sqlexception sqlstate sqlwarning start state static system_user table temporary then timezone_hour timezone_minute to trailing transaction translation treat trigger under undo union unique unnest until update usage user using value values view when whenever where while with without work write year zone ","array binary bit boolean char character clob date decimal double float int integer interval large national nchar nclob numeric object precision real smallint time timestamp varchar varying ")};function fd(t){return new Hf((e=>{var i;let{next:n}=e;if(e.advance(),ad(n,hd)){for(;ad(e.next,hd);)e.advance();e.acceptToken(36)}else if(36==n&&36==e.next&&t.doubleDollarQuotedStrings)!function(t){for(;;){if(t.next<0||t.peek(1)<0)return;if(36==t.next&&36==t.peek(1))return void t.advance(2);t.advance()}}(e),e.acceptToken(3);else if(39==n||34==n&&t.doubleQuotedStrings)nd(e,n,t.backslashEscapes),e.acceptToken(3);else if(35==n&&t.hashComments||47==n&&47==e.next&&t.slashComments)ld(e),e.acceptToken(1);else if(45!=n||45!=e.next||t.spaceAfterDashes&&32!=e.peek(1))if(47==n&&42==e.next){e.advance();for(let t=-1,i=1;!(e.next<0);)if(e.advance(),42==t&&47==e.next){if(i--,!i){e.advance();break}t=-1}else 47==t&&42==e.next?(i++,t=-1):t=e.next;e.acceptToken(2)}else if(101!=n&&69!=n||39!=e.next)if(110!=n&&78!=n||39!=e.next||!t.charSetCasts)if(95==n&&t.charSetCasts)for(let i=0;;i++){if(39==e.next&&i>1){e.advance(),nd(e,39,t.backslashEscapes),e.acceptToken(3);break}if(!id(e.next))break;e.advance()}else if(40==n)e.acceptToken(7);else if(41==n)e.acceptToken(8);else if(123==n)e.acceptToken(9);else if(125==n)e.acceptToken(10);else if(91==n)e.acceptToken(11);else if(93==n)e.acceptToken(12);else if(59==n)e.acceptToken(13);else if(t.unquotedBitLiterals&&48==n&&98==e.next)e.advance(),rd(e),e.acceptToken(22);else if(98!=n&&66!=n||39!=e.next&&34!=e.next){if(48==n&&(120==e.next||88==e.next)||(120==n||88==n)&&39==e.next){let t=39==e.next;for(e.advance();(s=e.next)>=48&&s<=57||s>=97&&s<=102||s>=65&&s<=70;)e.advance();t&&39==e.next&&e.advance(),e.acceptToken(4)}else if(46==n&&e.next>=48&&e.next<=57)od(e,!0),e.acceptToken(4);else if(46==n)e.acceptToken(14);else if(n>=48&&n<=57)od(e,!1),e.acceptToken(4);else if(ad(n,t.operatorChars)){for(;ad(e.next,t.operatorChars);)e.advance();e.acceptToken(15)}else if(ad(n,t.specialVar))e.next==n&&e.advance(),function(t){if(39==t.next||34==t.next||96==t.next){let e=t.next;t.advance(),nd(t,e,!1)}else sd(t)}(e),e.acceptToken(17);else if(ad(n,t.identifierQuotes))nd(e,n,!1),e.acceptToken(19);else if(58==n||44==n)e.acceptToken(16);else if(id(n)){let s=sd(e,String.fromCharCode(n));e.acceptToken(46==e.next?18:null!==(i=t.words[s.toLowerCase()])&&void 0!==i?i:18)}}else{const i=e.next;e.advance(),t.treatBitsAsBytes?(nd(e,i,t.backslashEscapes),e.acceptToken(23)):(rd(e,i),e.acceptToken(22))}else e.advance(),nd(e,39,t.backslashEscapes),e.acceptToken(3);else e.advance(),nd(e,39,!0);else ld(e),e.acceptToken(1);var s}))}const dd=fd(ud),pd=Zf.deserialize({version:14,states:"%vQ]QQOOO#wQRO'#DSO$OQQO'#CwO%eQQO'#CxO%lQQO'#CyO%sQQO'#CzOOQQ'#DS'#DSOOQQ'#C}'#C}O'UQRO'#C{OOQQ'#Cv'#CvOOQQ'#C|'#C|Q]QQOOQOQQOOO'`QQO'#DOO(xQRO,59cO)PQQO,59cO)UQQO'#DSOOQQ,59d,59dO)cQQO,59dOOQQ,59e,59eO)jQQO,59eOOQQ,59f,59fO)qQQO,59fOOQQ-E6{-E6{OOQQ,59b,59bOOQQ-E6z-E6zOOQQ,59j,59jOOQQ-E6|-E6|O+VQRO1G.}O+^QQO,59cOOQQ1G/O1G/OOOQQ1G/P1G/POOQQ1G/Q1G/QP+kQQO'#C}O+rQQO1G.}O)PQQO,59cO,PQQO'#Cw",stateData:",[~OtOSPOSQOS~ORUOSUOTUOUUOVROXSOZTO]XO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O^]ORvXSvXTvXUvXVvXXvXZvX]vX_vX`vXavXbvXcvXdvXevXfvXgvXhvX~OsvX~P!jOa_Ob_Oc_O~ORUOSUOTUOUUOVROXSOZTO^tO_UO`UOa`Ob`Oc`OdUOeUOfUOgUOhUO~OWaO~P$ZOYcO~P$ZO[eO~P$ZORUOSUOTUOUUOVROXSOZTO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O]hOsoX~P%zOajObjOcjO~O^]ORkaSkaTkaUkaVkaXkaZka]ka_ka`kaakabkackadkaekafkagkahka~Oska~P'kO^]O~OWvXYvX[vX~P!jOWnO~P$ZOYoO~P$ZO[pO~P$ZO^]ORkiSkiTkiUkiVkiXkiZki]ki_ki`kiakibkickidkiekifkigkihki~Oski~P)xOWkaYka[ka~P'kO]hO~P$ZOWkiYki[ki~P)xOasObsOcsO~O",goto:"#hwPPPPPPPPPPPPPPPPPPPPPPPPPPx||||!Y!^!d!xPPP#[TYOZeUORSTWZbdfqT[OZQZORiZSWOZQbRQdSQfTZgWbdfqQ^PWk^lmrQl_Qm`RrseVORSTWZbdfq",nodeNames:"⚠ LineComment BlockComment String Number Bool Null ( ) [ ] { } ; . Operator Punctuation SpecialVar Identifier QuotedIdentifier Keyword Type Bits Bytes Builtin Script Statement CompositeIdentifier Parens Braces Brackets Statement",maxTerm:38,skippedNodes:[0,1,2],repeatNodeCount:3,tokenData:"RORO",tokenizers:[0,dd],topRules:{Script:[0,25]},tokenPrec:0});function md(t){let e=t.cursor().moveTo(t.from,-1);for(;/Comment/.test(e.name);)e.moveTo(e.from,-1);return e.node}function gd(t,e){let i=t.sliceString(e.from,e.to),n=/^([`'"])(.*)\1$/.exec(i);return n?n[2]:i}function vd(t){return t&&("Identifier"==t.name||"QuotedIdentifier"==t.name)}function wd(t,e){if("CompositeIdentifier"==e.name){let i=[];for(let n=e.firstChild;n;n=n.nextSibling)vd(n)&&i.push(gd(t,n));return i}return[gd(t,e)]}function yd(t,e){for(let i=[];;){if(!e||"."!=e.name)return i;let n=md(e);if(!vd(n))return i;i.unshift(gd(t,n)),e=md(n)}}function bd(t,e){let i=vl(t).resolveInner(e,-1),n=function(t,e){let i;for(let t=e;!i;t=t.parent){if(!t)return null;"Statement"==t.name&&(i=t)}let n=null;for(let e=i.firstChild,s=!1,r=null;e;e=e.nextSibling){let i="Keyword"==e.name?t.sliceString(e.from,e.to).toLowerCase():null,o=null;if(s)if("as"==i&&r&&vd(e.nextSibling))o=gd(t,e.nextSibling);else{if(i&&xd.has(i))break;r&&vd(e)&&(o=gd(t,e))}else s="from"==i;o&&(n||(n=Object.create(null)),n[o]=wd(t,r)),r=/Identifier$/.test(e.name)?e:null}return n}(t.doc,i);return"Identifier"==i.name||"QuotedIdentifier"==i.name||"Keyword"==i.name?{from:i.from,quoted:"QuotedIdentifier"==i.name?t.doc.sliceString(i.from,i.from+1):null,parents:yd(t.doc,md(i)),aliases:n}:"."==i.name?{from:e,quoted:null,parents:yd(t.doc,i),aliases:n}:{from:e,quoted:null,parents:[],empty:!0,aliases:n}}const xd=new Set("where group having order union intersect except all distinct limit offset fetch for".split(" "));const kd=/^\w*$/,Sd=/^[`'"]?\w*[`'"]?$/;class Cd{constructor(){this.list=[],this.children=void 0}child(t){let e=this.children||(this.children=Object.create(null));return e[t]||(e[t]=new Cd)}childCompletions(t){return this.children?Object.keys(this.children).filter((t=>t)).map((e=>({label:e,type:t}))):[]}}function Ad(t,e){let i=Object.keys(t).map((i=>({label:e?i.toUpperCase():i,type:21==t[i]?"type":20==t[i]?"keyword":"variable",boost:-1})));return n=["QuotedIdentifier","SpecialVar","String","LineComment","BlockComment","."],s=hu(i),t=>{for(let e=vl(t.state).resolveInner(t.pos,-1);e;e=e.parent)if(n.indexOf(e.name)>-1)return null;return s(t)};var n,s}let Od=pd.configure({props:[Ll.add({Statement:Hl()}),ql.add({Statement:t=>({from:t.firstChild.to,to:t.to}),BlockComment:t=>({from:t.from+2,to:t.to-2})}),jo({Keyword:ul.keyword,Type:ul.typeName,Builtin:ul.standard(ul.name),Bits:ul.number,Bytes:ul.string,Bool:ul.bool,Null:ul.null,Number:ul.number,String:ul.string,Identifier:ul.name,QuotedIdentifier:ul.special(ul.string),SpecialVar:ul.special(ul.name),LineComment:ul.lineComment,BlockComment:ul.blockComment,Operator:ul.operator,"Semi Punctuation":ul.punctuation,"( )":ul.paren,"{ }":ul.brace,"[ ]":ul.squareBracket})]});class Md{constructor(t,e){this.dialect=t,this.language=e}get extension(){return this.language.extension}static define(t){let e=function(t,e,i,n){let s={};for(let e in ud)s[e]=(t.hasOwnProperty(e)?t:ud)[e];return e&&(s.words=cd(e,i||"",n)),s}(t,t.keywords,t.types,t.builtin),i=gl.define({name:"sql",parser:Od.configure({tokenizers:[{from:dd,to:fd(e)}]}),languageData:{commentTokens:{line:"--",block:{open:"/*",close:"*/"}},closeBrackets:{brackets:["(","[","{","'",'"',"`"]}}});return new Md(e,i)}}function Dd(t,e=!1){return Ad(t.dialect.words,e)}function Td(t,e=!1){return t.language.data.of({autocomplete:Dd(t,e)})}function Pd(t){return t.schema?function(t,e,i,n){let s=new Cd,r=s.child(n||"");for(let e in t){let i=e.indexOf(".");(i>-1?s.child(e.slice(0,i)):r).child(i>-1?e.slice(i+1):e).list=t[e].map((t=>"string"==typeof t?{label:t,type:"property"}:t))}r.list=(e||r.childCompletions("type")).concat(i?r.child(i).list:[]);for(let t in s.children){let e=s.child(t);e.list.length||(e.list=e.childCompletions("type"))}return s.list=r.list.concat(s.childCompletions("type")),t=>{let{parents:e,from:n,quoted:o,empty:l,aliases:a}=bd(t.state,t.pos);if(l&&!t.explicit)return null;a&&1==e.length&&(e=a[e[0]]||e);let h=s;for(let t of e){for(;!h.children||!h.children[t];)if(h==s)h=r;else{if(h!=r||!i)return null;h=h.child(i)}h=h.child(t)}let c=o&&t.state.sliceDoc(t.pos,t.pos+1)==o,u=h.list;return h==s&&a&&(u=u.concat(Object.keys(a).map((t=>({label:t,type:"constant"}))))),{from:n,to:c?t.pos+1:void 0,options:(f=o,d=u,f?d.map((t=>Object.assign(Object.assign({},t),{label:f+t.label+f,apply:void 0}))):d),validFor:o?Sd:kd};var f,d}}(t.schema,t.tables,t.defaultTable,t.defaultSchema):()=>null}function Rd(t){return t.schema?(t.dialect||Bd).language.data.of({autocomplete:Pd(t)}):[]}function Ed(t={}){let e=t.dialect||Bd;return new Ml(e.language,[Rd(t),Td(e,!!t.upperCaseKeywords)])}const Bd=Md.define({}),Ld=Md.define({keywords:"and as asc between by case cast count current_date current_time current_timestamp desc distinct each else escape except exists explain filter first for from full generated group having if in index inner intersect into isnull join last left like limit not null or order outer over pragma primary query raise range regexp right rollback row select set table then to union unique using values view virtual when where",types:"null integer real text blob",builtin:"",operatorChars:"*+-%<>!=&|/~",identifierQuotes:'`"',specialVar:"@:?$"});return t.editorFromTextArea=function(t,e={}){let i=new Ds({doc:t.value,extensions:[Is.of([{key:"Shift-Enter",run:function(){return t.value=i.state.doc.toString(),t.form.submit(),!0}},{key:"Meta-Enter",run:function(){return t.value=i.state.doc.toString(),t.form.submit(),!0}}]),Pf,Ds.lineWrapping,Ed({dialect:Ld,schema:e.schema,tables:e.tables,defaultTableName:e.defaultTableName,defaultSchemaName:e.defaultSchemaName})]}),n=i.contentDOM.closest(".cm-editor");return new ResizeObserver((function(){i.requestMeasure()})).observe(n,{attributes:!0}),t.parentNode.insertBefore(i.dom,t),t.style.display="none",t.form&&t.form.addEventListener("submit",(()=>{t.value=i.state.doc.toString()})),i},t}({}); diff --git a/datasette/static/cm-editor-6.0.1.js b/datasette/static/cm-editor-6.0.1.js deleted file mode 100644 index c1fd2ab5..00000000 --- a/datasette/static/cm-editor-6.0.1.js +++ /dev/null @@ -1,74 +0,0 @@ -import { EditorView, basicSetup } from "codemirror"; -import { keymap } from "@codemirror/view"; -import { sql, SQLDialect } from "@codemirror/lang-sql"; - -// A variation of SQLite from lang-sql https://github.com/codemirror/lang-sql/blob/ebf115fffdbe07f91465ccbd82868c587f8182bc/src/sql.ts#L231 -const SQLite = SQLDialect.define({ - // Based on https://www.sqlite.org/lang_keywords.html based on likely keywords to be used in select queries - // https://github.com/simonw/datasette/pull/1893#issuecomment-1316401895: - keywords: - "and as asc between by case cast count current_date current_time current_timestamp desc distinct each else escape except exists explain filter first for from full generated group having if in index inner intersect into isnull join last left like limit not null or order outer over pragma primary query raise range regexp right rollback row select set table then to union unique using values view virtual when where", - // https://www.sqlite.org/datatype3.html - types: "null integer real text blob", - builtin: "", - operatorChars: "*+-%<>!=&|/~", - identifierQuotes: '`"', - specialVar: "@:?$", -}); - -// Utility function from https://codemirror.net/docs/migration/ -export function editorFromTextArea(textarea, conf = {}) { - // This could also be configured with a set of tables and columns for better autocomplete: - // https://github.com/codemirror/lang-sql#user-content-sqlconfig.tables - let view = new EditorView({ - doc: textarea.value, - extensions: [ - keymap.of([ - { - key: "Shift-Enter", - run: function () { - textarea.value = view.state.doc.toString(); - textarea.form.submit(); - return true; - }, - }, - { - key: "Meta-Enter", - run: function () { - textarea.value = view.state.doc.toString(); - textarea.form.submit(); - return true; - }, - }, - ]), - // This has to be after the keymap or else the basicSetup keys will prevent - // Meta-Enter from running - basicSetup, - EditorView.lineWrapping, - sql({ - dialect: SQLite, - schema: conf.schema, - tables: conf.tables, - defaultTableName: conf.defaultTableName, - defaultSchemaName: conf.defaultSchemaName, - }), - ], - }); - - // Idea taken from https://discuss.codemirror.net/t/resizing-codemirror-6/3265. - // Using CSS resize: both and scheduling a measurement when the element changes. - let editorDOM = view.contentDOM.closest(".cm-editor"); - let observer = new ResizeObserver(function () { - view.requestMeasure(); - }); - observer.observe(editorDOM, { attributes: true }); - - textarea.parentNode.insertBefore(view.dom, textarea); - textarea.style.display = "none"; - if (textarea.form) { - textarea.form.addEventListener("submit", () => { - textarea.value = view.state.doc.toString(); - }); - } - return view; -} diff --git a/datasette/static/cm-editor.bundle.js b/datasette/static/cm-editor.bundle.js new file mode 100644 index 00000000..e329f14d --- /dev/null +++ b/datasette/static/cm-editor.bundle.js @@ -0,0 +1 @@ +var cm=function(t){"use strict";let e=[],i=[];function n(t){if(t<768)return!1;for(let n=0,s=e.length;;){let r=n+s>>1;if(t=i[r]))return!0;n=r+1}if(n==s)return!1}}function s(t){return t>=127462&&t<=127487}(()=>{let t="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map(t=>t?parseInt(t,36):1);for(let n=0,s=0;n=0&&s(a(t,n));)i++,n-=2;if(i%2==0)break;e+=2}}}return e}function l(t,e,i){for(;e>1;){let n=o(t,e-2,i);if(n=56320&&t<57344}function c(t){return t>=55296&&t<56320}function u(t){return t<65536?1:2}class f{lineAt(t){if(t<0||t>this.length)throw new RangeError(`Invalid position ${t} in document of length ${this.length}`);return this.lineInner(t,!1,1,0)}line(t){if(t<1||t>this.lines)throw new RangeError(`Invalid line number ${t} in ${this.lines}-line document`);return this.lineInner(t,!0,1,0)}replace(t,e,i){[t,e]=x(this,t,e);let n=[];return this.decompose(0,t,n,2),i.length&&i.decompose(0,i.length,n,3),this.decompose(e,this.length,n,1),p.from(n,this.length-(e-t)+i.length)}append(t){return this.replace(this.length,this.length,t)}slice(t,e=this.length){[t,e]=x(this,t,e);let i=[];return this.decompose(t,e,i,0),p.from(i,e-t)}eq(t){if(t==this)return!0;if(t.length!=this.length||t.lines!=this.lines)return!1;let e=this.scanIdentical(t,1),i=this.length-this.scanIdentical(t,-1),n=new v(this),s=new v(t);for(let t=e,r=e;;){if(n.next(t),s.next(t),t=0,n.lineBreak!=s.lineBreak||n.done!=s.done||n.value!=s.value)return!1;if(r+=n.value.length,n.done||r>=i)return!0}}iter(t=1){return new v(this,t)}iterRange(t,e=this.length){return new w(this,t,e)}iterLines(t,e){let i;if(null==t)i=this.iter();else{null==e&&(e=this.lines+1);let n=this.line(t).from;i=this.iterRange(n,Math.max(n,e==this.lines+1?this.length:e<=1?0:this.line(e-1).to))}return new b(i)}toString(){return this.sliceString(0)}toJSON(){let t=[];return this.flatten(t),t}constructor(){}static of(t){if(0==t.length)throw new RangeError("A document must have at least one line");return 1!=t.length||t[0]?t.length<=32?new d(t):p.from(d.split(t,[])):f.empty}}class d extends f{constructor(t,e=function(t){let e=-1;for(let i of t)e+=i.length+1;return e}(t)){super(),this.text=t,this.length=e}get lines(){return this.text.length}get children(){return null}lineInner(t,e,i,n){for(let s=0;;s++){let r=this.text[s],o=n+r.length;if((e?i:o)>=t)return new y(n,o,i,r);n=o+1,i++}}decompose(t,e,i,n){let s=t<=0&&e>=this.length?this:new d(g(this.text,t,e),Math.min(e,this.length)-Math.max(0,t));if(1&n){let t=i.pop(),e=m(s.text,t.text.slice(),0,s.length);if(e.length<=32)i.push(new d(e,t.length+s.length));else{let t=e.length>>1;i.push(new d(e.slice(0,t)),new d(e.slice(t)))}}else i.push(s)}replace(t,e,i){if(!(i instanceof d))return super.replace(t,e,i);[t,e]=x(this,t,e);let n=m(this.text,m(i.text,g(this.text,0,t)),e),s=this.length+i.length-(e-t);return n.length<=32?new d(n,s):p.from(d.split(n,[]),s)}sliceString(t,e=this.length,i="\n"){[t,e]=x(this,t,e);let n="";for(let s=0,r=0;s<=e&&rt&&r&&(n+=i),ts&&(n+=o.slice(Math.max(0,t-s),e-s)),s=l+1}return n}flatten(t){for(let e of this.text)t.push(e)}scanIdentical(){return 0}static split(t,e){let i=[],n=-1;for(let s of t)i.push(s),n+=s.length+1,32==i.length&&(e.push(new d(i,n)),i=[],n=-1);return n>-1&&e.push(new d(i,n)),e}}class p extends f{constructor(t,e){super(),this.children=t,this.length=e,this.lines=0;for(let e of t)this.lines+=e.lines}lineInner(t,e,i,n){for(let s=0;;s++){let r=this.children[s],o=n+r.length,l=i+r.lines-1;if((e?l:o)>=t)return r.lineInner(t,e,i,n);n=o+1,i=l+1}}decompose(t,e,i,n){for(let s=0,r=0;r<=e&&s=r){let s=n&((r<=t?1:0)|(l>=e?2:0));r>=t&&l<=e&&!s?i.push(o):o.decompose(t-r,e-r,i,s)}r=l+1}}replace(t,e,i){if([t,e]=x(this,t,e),i.lines=s&&e<=o){let l=r.replace(t-s,e-s,i),a=this.lines-r.lines+l.lines;if(l.lines>4&&l.lines>a>>6){let s=this.children.slice();return s[n]=l,new p(s,this.length-(e-t)+i.length)}return super.replace(s,o,l)}s=o+1}return super.replace(t,e,i)}sliceString(t,e=this.length,i="\n"){[t,e]=x(this,t,e);let n="";for(let s=0,r=0;st&&s&&(n+=i),tr&&(n+=o.sliceString(t-r,e-r,i)),r=l+1}return n}flatten(t){for(let e of this.children)e.flatten(t)}scanIdentical(t,e){if(!(t instanceof p))return 0;let i=0,[n,s,r,o]=e>0?[0,0,this.children.length,t.children.length]:[this.children.length-1,t.children.length-1,-1,-1];for(;;n+=e,s+=e){if(n==r||s==o)return i;let l=this.children[n],a=t.children[s];if(l!=a)return i+l.scanIdentical(a,e);i+=l.length+1}}static from(t,e=t.reduce((t,e)=>t+e.length+1,-1)){let i=0;for(let e of t)i+=e.lines;if(i<32){let i=[];for(let e of t)e.flatten(i);return new d(i,e)}let n=Math.max(32,i>>5),s=n<<1,r=n>>1,o=[],l=0,a=-1,h=[];function c(t){let e;if(t.lines>s&&t instanceof p)for(let e of t.children)c(e);else t.lines>r&&(l>r||!l)?(u(),o.push(t)):t instanceof d&&l&&(e=h[h.length-1])instanceof d&&t.lines+e.lines<=32?(l+=t.lines,a+=t.length+1,h[h.length-1]=new d(e.text.concat(t.text),e.length+1+t.length)):(l+t.lines>n&&u(),l+=t.lines,a+=t.length+1,h.push(t))}function u(){0!=l&&(o.push(1==h.length?h[0]:p.from(h,a)),a=-1,l=h.length=0)}for(let e of t)c(e);return u(),1==o.length?o[0]:new p(o,e)}}function m(t,e,i=0,n=1e9){for(let s=0,r=0,o=!0;r=i&&(a>n&&(l=l.slice(0,n-s)),s0?1:(t instanceof d?t.text.length:t.children.length)<<1]}nextInner(t,e){for(this.done=this.lineBreak=!1;;){let i=this.nodes.length-1,n=this.nodes[i],s=this.offsets[i],r=s>>1,o=n instanceof d?n.text.length:n.children.length;if(r==(e>0?o:0)){if(0==i)return this.done=!0,this.value="",this;e>0&&this.offsets[i-1]++,this.nodes.pop(),this.offsets.pop()}else if((1&s)==(e>0?0:1)){if(this.offsets[i]+=e,0==t)return this.lineBreak=!0,this.value="\n",this;t--}else if(n instanceof d){let s=n.text[r+(e<0?-1:0)];if(this.offsets[i]+=e,s.length>Math.max(0,t))return this.value=0==t?s:e>0?s.slice(t):s.slice(0,s.length-t),this;t-=s.length}else{let s=n.children[r+(e<0?-1:0)];t>s.length?(t-=s.length,this.offsets[i]+=e):(e<0&&this.offsets[i]--,this.nodes.push(s),this.offsets.push(e>0?1:(s instanceof d?s.text.length:s.children.length)<<1))}}}next(t=0){return t<0&&(this.nextInner(-t,-this.dir),t=this.value.length),this.nextInner(t,this.dir)}}class w{constructor(t,e,i){this.value="",this.done=!1,this.cursor=new v(t,e>i?-1:1),this.pos=e>i?t.length:0,this.from=Math.min(e,i),this.to=Math.max(e,i)}nextInner(t,e){if(e<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;t+=Math.max(0,e<0?this.pos-this.to:this.from-this.pos);let i=e<0?this.pos-this.from:this.to-this.pos;t>i&&(t=i),i-=t;let{value:n}=this.cursor.next(t);return this.pos+=(n.length+t)*e,this.value=n.length<=i?n:e<0?n.slice(n.length-i):n.slice(0,i),this.done=!this.value,this}next(t=0){return t<0?t=Math.max(t,this.from-this.pos):t>0&&(t=Math.min(t,this.to-this.pos)),this.nextInner(t,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&""!=this.value}}class b{constructor(t){this.inner=t,this.afterBreak=!0,this.value="",this.done=!1}next(t=0){let{done:e,lineBreak:i,value:n}=this.inner.next(t);return e&&this.afterBreak?(this.value="",this.afterBreak=!1):e?(this.done=!0,this.value=""):i?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=n,this.afterBreak=!1),this}get lineBreak(){return!1}}"undefined"!=typeof Symbol&&(f.prototype[Symbol.iterator]=function(){return this.iter()},v.prototype[Symbol.iterator]=w.prototype[Symbol.iterator]=b.prototype[Symbol.iterator]=function(){return this});class y{constructor(t,e,i,n){this.from=t,this.to=e,this.number=i,this.text=n}get length(){return this.to-this.from}}function x(t,e,i){return[e=Math.max(0,Math.min(t.length,e)),Math.max(e,Math.min(t.length,i))]}function k(t,e,i=!0,n=!0){return r(t,e,i,n)}function S(t,e){let i=t.charCodeAt(e);if(!(n=i,n>=55296&&n<56320&&e+1!=t.length))return i;var n;let s=t.charCodeAt(e+1);return function(t){return t>=56320&&t<57344}(s)?s-56320+(i-55296<<10)+65536:i}function C(t){return t<=65535?String.fromCharCode(t):(t-=65536,String.fromCharCode(55296+(t>>10),56320+(1023&t)))}function A(t){return t<65536?1:2}const M=/\r\n?|\n/;var O=function(t){return t[t.Simple=0]="Simple",t[t.TrackDel=1]="TrackDel",t[t.TrackBefore=2]="TrackBefore",t[t.TrackAfter=3]="TrackAfter",t}(O||(O={}));class T{constructor(t){this.sections=t}get length(){let t=0;for(let e=0;et)return s+(t-n);s+=o}else{if(i!=O.Simple&&a>=t&&(i==O.TrackDel&&nt||i==O.TrackBefore&&nt))return null;if(a>t||a==t&&e<0&&!o)return t==n||e<0?s:s+l;s+=l}n=a}if(t>n)throw new RangeError(`Position ${t} is out of range for changeset of length ${n}`);return s}touchesRange(t,e=t){for(let i=0,n=0;i=0&&n<=e&&s>=t)return!(ne)||"cover";n=s}return!1}toString(){let t="";for(let e=0;e=0?":"+n:"")}return t}toJSON(){return this.sections}static fromJSON(t){if(!Array.isArray(t)||t.length%2||t.some(t=>"number"!=typeof t))throw new RangeError("Invalid JSON representation of ChangeDesc");return new T(t)}static create(t){return new T(t)}}class D extends T{constructor(t,e){super(t),this.inserted=e}apply(t){if(this.length!=t.length)throw new RangeError("Applying change set to a document with the wrong length");return B(this,(e,i,n,s,r)=>t=t.replace(n,n+(i-e),r),!1),t}mapDesc(t,e=!1){return E(this,t,e,!0)}invert(t){let e=this.sections.slice(),i=[];for(let n=0,s=0;n=0){e[n]=o,e[n+1]=r;let l=n>>1;for(;i.length0&&P(i,e,s.text),s.forward(t),o+=t}let a=t[r++];for(;o>1].toJSON()))}return t}static of(t,e,i){let n=[],s=[],r=0,o=null;function l(t=!1){if(!t&&!n.length)return;ro||t<0||o>e)throw new RangeError(`Invalid change range ${t} to ${o} (in doc of length ${e})`);let c=h?"string"==typeof h?f.of(h.split(i||M)):h:f.empty,u=c.length;if(t==o&&0==u)return;tr&&R(n,t-r,-1),R(n,o-t,u),P(s,n,c),r=o}}(t),l(!o),o}static empty(t){return new D(t?[t,-1]:[],[])}static fromJSON(t){if(!Array.isArray(t))throw new RangeError("Invalid JSON representation of ChangeSet");let e=[],i=[];for(let n=0;ne&&"string"!=typeof t))throw new RangeError("Invalid JSON representation of ChangeSet");if(1==s.length)e.push(s[0],0);else{for(;i.length=0&&i<=0&&i==t[s+1]?t[s]+=e:s>=0&&0==e&&0==t[s]?t[s+1]+=i:n?(t[s]+=e,t[s+1]+=i):t.push(e,i)}function P(t,e,i){if(0==i.length)return;let n=e.length-2>>1;if(n>1])),!(i||o==t.sections.length||t.sections[o+1]<0);)l=t.sections[o++],a=t.sections[o++];e(s,h,r,c,u),s=h,r=c}}}function E(t,e,i,n=!1){let s=[],r=n?[]:null,o=new I(t),l=new I(e);for(let t=-1;;){if(o.done&&l.len||l.done&&o.len)throw new Error("Mismatched change set lengths");if(-1==o.ins&&-1==l.ins){let t=Math.min(o.len,l.len);R(s,t,-1),o.forward(t),l.forward(t)}else if(l.ins>=0&&(o.ins<0||t==o.i||0==o.off&&(l.len=0&&t=0)){if(o.done&&l.done)return r?D.createSet(s,r):T.create(s);throw new Error("Mismatched change set lengths")}{let e=0,i=o.len;for(;i;)if(-1==l.ins){let t=Math.min(i,l.len);e+=t,i-=t,l.forward(t)}else{if(!(0==l.ins&&l.lene||o.ins>=0&&o.len>e)&&(t||n.length>i),r.forward2(e),o.forward(e)}}else R(n,0,o.ins,t),s&&P(s,n,o.text),o.next()}}class I{constructor(t){this.set=t,this.i=0,this.next()}next(){let{sections:t}=this.set;this.i>1;return e>=t.length?f.empty:t[e]}textBit(t){let{inserted:e}=this.set,i=this.i-2>>1;return i>=e.length&&!t?f.empty:e[i].slice(this.off,null==t?void 0:this.off+t)}forward(t){t==this.len?this.next():(this.len-=t,this.off+=t)}forward2(t){-1==this.ins?this.forward(t):t==this.ins?this.next():(this.ins-=t,this.off+=t)}}class N{constructor(t,e,i,n){this.from=t,this.to=e,this.flags=i,this.goalColumn=n}get anchor(){return 32&this.flags?this.to:this.from}get head(){return 32&this.flags?this.from:this.to}get empty(){return this.from==this.to}get assoc(){return 8&this.flags?-1:16&this.flags?1:0}get undirectional(){return(64&this.flags)>0}get bidiLevel(){let t=7&this.flags;return 7==t?null:t}map(t,e=-1){let i,n;return this.empty?i=n=t.mapPos(this.from,e):(i=t.mapPos(this.from,1),n=t.mapPos(this.to,-1)),i==this.from&&n==this.to?this:new N(i,n,this.flags,this.goalColumn)}extend(t,e=t,i=0){if(t<=this.anchor&&e>=this.anchor)return W.range(t,e,void 0,void 0,i);let n=Math.abs(t-this.anchor)>Math.abs(e-this.anchor)?t:e;return W.range(this.anchor,n,void 0,void 0,i)}eq(t,e=!1){return!(this.anchor!=t.anchor||this.head!=t.head||this.goalColumn!=t.goalColumn||e&&this.empty&&this.assoc!=t.assoc)}toJSON(){return{anchor:this.anchor,head:this.head}}static fromJSON(t){if(!t||"number"!=typeof t.anchor||"number"!=typeof t.head)throw new RangeError("Invalid JSON representation for SelectionRange");return W.range(t.anchor,t.head)}static create(t,e,i,n){return new N(t,e,i,n)}}class W{constructor(t,e){this.ranges=t,this.mainIndex=e}map(t,e=-1){return t.empty?this:W.create(this.ranges.map(i=>i.map(t,e)),this.mainIndex)}eq(t,e=!1){if(this.ranges.length!=t.ranges.length||this.mainIndex!=t.mainIndex)return!1;for(let i=0;it.toJSON()),main:this.mainIndex}}static fromJSON(t){if(!t||!Array.isArray(t.ranges)||"number"!=typeof t.main||t.main>=t.ranges.length)throw new RangeError("Invalid JSON representation for EditorSelection");return new W(t.ranges.map(t=>N.fromJSON(t)),t.main)}static single(t,e=t){return new W([W.range(t,e)],0)}static create(t,e=0){if(0==t.length)throw new RangeError("A selection needs at least one range");for(let i=0,n=0;nt.from-e.from),e=t.indexOf(i);for(let i=1;in.head?W.range(o,r):W.range(r,o))}}return new W(t,e)}}function H(t,e){for(let i of t.ranges)if(i.to>e)throw new RangeError("Selection points outside of document")}let V=0;class z{constructor(t,e,i,n,s){this.combine=t,this.compareInput=e,this.compare=i,this.isStatic=n,this.id=V++,this.default=t([]),this.extensions="function"==typeof s?s(this):s}get reader(){return this}static define(t={}){return new z(t.combine||(t=>t),t.compareInput||((t,e)=>t===e),t.compare||(t.combine?(t,e)=>t===e:F),!!t.static,t.enables)}of(t){return new q([],this,0,t)}compute(t,e){if(this.isStatic)throw new Error("Can't compute a static facet");return new q(t,this,1,e)}computeN(t,e){if(this.isStatic)throw new Error("Can't compute a static facet");return new q(t,this,2,e)}from(t,e){return e||(e=t=>t),this.compute([t],i=>e(i.field(t)))}}function F(t,e){return t==e||t.length==e.length&&t.every((t,i)=>t===e[i])}class q{constructor(t,e,i,n){this.dependencies=t,this.facet=e,this.type=i,this.value=n,this.id=V++}dynamicSlot(t){var e;let i=this.value,n=this.facet.compareInput,s=this.id,r=t[s]>>1,o=2==this.type,l=!1,a=!1,h=[];for(let i of this.dependencies)"doc"==i?l=!0:"selection"==i?a=!0:1&(null!==(e=t[i.id])&&void 0!==e?e:1)||h.push(t[i.id]);return{create:t=>(t.values[r]=i(t),1),update(t,e){if(l&&e.docChanged||a&&(e.docChanged||e.selection)||U(t,h)){let e=i(t);if(o?!_(e,t.values[r],n):!n(e,t.values[r]))return t.values[r]=e,1}return 0},reconfigure:(t,e)=>{let l,a=e.config.address[s];if(null!=a){let s=rt(e,a);if(this.dependencies.every(i=>i instanceof z?e.facet(i)===t.facet(i):!(i instanceof K)||e.field(i,!1)==t.field(i,!1))||(o?_(l=i(t),s,n):n(l=i(t),s)))return t.values[r]=s,0}else l=i(t);return t.values[r]=l,1}}}get extension(){return this}}function _(t,e,i){if(t.length!=e.length)return!1;for(let n=0;nt[e.id]),s=i.map(t=>t.type),r=n.filter(t=>!(1&t)),o=t[e.id]>>1;function l(t){let i=[];for(let e=0;et===e),t);return t.provide&&(e.provides=t.provide(e)),e}create(t){let e=t.facet($).find(t=>t.field==this);return((null==e?void 0:e.create)||this.createF)(t)}slot(t){let e=t[this.id]>>1;return{create:t=>(t.values[e]=this.create(t),1),update:(t,i)=>{let n=t.values[e],s=this.updateF(n,i);return this.compareF(n,s)?0:(t.values[e]=s,1)},reconfigure:(t,i)=>{let n,s=t.facet($),r=i.facet($);return(n=s.find(t=>t.field==this))&&n!=r.find(t=>t.field==this)?(t.values[e]=n.create(t),1):null!=i.config.address[this.id]?(t.values[e]=i.field(this),0):(t.values[e]=this.create(t),1)}}}init(t){return[this,$.of({field:this,create:t})]}get extension(){return this}}const j=4,X=3,G=2,Y=1;function J(t){return e=>new tt(e,t)}const Z={highest:J(0),high:J(Y),default:J(G),low:J(X),lowest:J(j)};class tt{constructor(t,e){this.inner=t,this.prec=e}get extension(){return this}}class et{of(t){return new it(this,t)}reconfigure(t){return et.reconfigure.of({compartment:this,extension:t})}get(t){return t.config.compartments.get(this)}}class it{constructor(t,e){this.compartment=t,this.inner=e}get extension(){return this}}class nt{constructor(t,e,i,n,s,r){for(this.base=t,this.compartments=e,this.dynamicSlots=i,this.address=n,this.staticValues=s,this.facets=r,this.statusTemplate=[];this.statusTemplate.length>1]}static resolve(t,e,i){let n=[],s=Object.create(null),r=new Map;for(let i of function(t,e,i){let n=[[],[],[],[],[]],s=new Map;function r(t,o){let l=s.get(t);if(null!=l){if(l<=o)return;let e=n[l].indexOf(t);e>-1&&n[l].splice(e,1),t instanceof it&&i.delete(t.compartment)}if(s.set(t,o),Array.isArray(t))for(let e of t)r(e,o);else if(t instanceof it){if(i.has(t.compartment))throw new RangeError("Duplicate use of compartment in extensions");let n=e.get(t.compartment)||t.inner;i.set(t.compartment,n),r(n,o)}else if(t instanceof tt)r(t.inner,t.prec);else if(t instanceof K)n[o].push(t),t.provides&&r(t.provides,o);else if(t instanceof q)n[o].push(t),t.facet.extensions&&r(t.facet.extensions,G);else{let e=t.extension;if(!e)throw new Error(`Unrecognized extension value in extension set (${t}).`);if(e==t)throw new Error(`Unrecognized extension value in extension set (${t}). This sometimes happens because multiple instances of @codemirror/state are loaded, breaking instanceof checks.`);r(e,o)}}return r(t,G),n.reduce((t,e)=>t.concat(e))}(t,e,r))i instanceof K?n.push(i):(s[i.facet.id]||(s[i.facet.id]=[])).push(i);let o=Object.create(null),l=[],a=[];for(let t of n)o[t.id]=a.length<<1,a.push(e=>t.slot(e));let h=null==i?void 0:i.config.facets;for(let t in s){let e=s[t],n=e[0].facet,r=h&&h[t]||[];if(e.every(t=>0==t.type))if(o[n.id]=l.length<<1|1,F(r,e))l.push(i.facet(n));else{let t=n.combine(e.map(t=>t.value));l.push(i&&n.compare(t,i.facet(n))?i.facet(n):t)}else{for(let t of e)0==t.type?(o[t.id]=l.length<<1|1,l.push(t.value)):(o[t.id]=a.length<<1,a.push(e=>t.dynamicSlot(e)));o[n.id]=a.length<<1,a.push(t=>Q(t,n,e))}}let c=a.map(t=>t(o));return new nt(t,r,c,o,l,s)}}function st(t,e){if(1&e)return 2;let i=e>>1,n=t.status[i];if(4==n)throw new Error("Cyclic dependency between fields and/or facets");if(2&n)return n;t.status[i]=4;let s=t.computeSlot(t,t.config.dynamicSlots[i]);return t.status[i]=2|s}function rt(t,e){return 1&e?t.config.staticValues[e>>1]:t.values[e>>1]}const ot=z.define(),lt=z.define({combine:t=>t.some(t=>t),static:!0}),at=z.define({combine:t=>t.length?t[0]:void 0,static:!0}),ht=z.define(),ct=z.define(),ut=z.define(),ft=z.define({combine:t=>!!t.length&&t[0]});class dt{constructor(t,e){this.type=t,this.value=e}static define(){return new pt}}class pt{of(t){return new dt(this,t)}}class mt{constructor(t){this.map=t}of(t){return new gt(this,t)}}class gt{constructor(t,e){this.type=t,this.value=e}map(t){let e=this.type.map(this.value,t);return void 0===e?void 0:e==this.value?this:new gt(this.type,e)}is(t){return this.type==t}static define(t={}){return new mt(t.map||(t=>t))}static mapEffects(t,e){if(!t.length)return t;let i=[];for(let n of t){let t=n.map(e);t&&i.push(t)}return i}}gt.reconfigure=gt.define(),gt.appendConfig=gt.define();class vt{constructor(t,e,i,n,s,r){this.startState=t,this.changes=e,this.selection=i,this.effects=n,this.annotations=s,this.scrollIntoView=r,this._doc=null,this._state=null,i&&H(i,e.newLength),s.some(t=>t.type==vt.time)||(this.annotations=s.concat(vt.time.of(Date.now())))}static create(t,e,i,n,s,r){return new vt(t,e,i,n,s,r)}get newDoc(){return this._doc||(this._doc=this.changes.apply(this.startState.doc))}get newSelection(){return this.selection||this.startState.selection.map(this.changes)}get state(){return this._state||this.startState.applyTransaction(this),this._state}annotation(t){for(let e of this.annotations)if(e.type==t)return e.value}get docChanged(){return!this.changes.empty}get reconfigured(){return this.startState.config!=this.state.config}isUserEvent(t){let e=this.annotation(vt.userEvent);return!(!e||!(e==t||e.length>t.length&&e.slice(0,t.length)==t&&"."==e[t.length]))}}function wt(t,e){let i=[];for(let n=0,s=0;;){let r,o;if(n=t[n]))r=t[n++],o=t[n++];else{if(!(s=0;s--){let r=i[s](t);r&&Object.keys(r).length&&(n=bt(n,yt(e,r,t.changes.newLength),!0))}return n==t?t:vt.create(e,t.changes,t.selection,n.effects,n.annotations,n.scrollIntoView)}(i?function(t){let e=t.startState,i=!0;for(let n of e.facet(ht)){let e=n(t);if(!1===e){i=!1;break}Array.isArray(e)&&(i=!0===i?e:wt(i,e))}if(!0!==i){let n,s;if(!1===i)s=t.changes.invertedDesc,n=D.empty(e.doc.length);else{let e=t.changes.filter(i);n=e.changes,s=e.filtered.mapDesc(e.changes).invertedDesc}t=vt.create(e,n,t.selection&&t.selection.map(s),gt.mapEffects(t.effects,s),t.annotations,t.scrollIntoView)}let n=e.facet(ct);for(let i=n.length-1;i>=0;i--){let s=n[i](t);t=s instanceof vt?s:Array.isArray(s)&&1==s.length&&s[0]instanceof vt?s[0]:xt(e,St(s),!1)}return t}(s):s)}vt.time=dt.define(),vt.userEvent=dt.define(),vt.addToHistory=dt.define(),vt.remote=dt.define();const kt=[];function St(t){return null==t?kt:Array.isArray(t)?t:[t]}var Ct=function(t){return t[t.Word=0]="Word",t[t.Space=1]="Space",t[t.Other=2]="Other",t}(Ct||(Ct={}));const At=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;let Mt;try{Mt=new RegExp("[\\p{Alphabetic}\\p{Number}_]","u")}catch(t){}function Ot(t){return e=>{if(!/\S/.test(e))return Ct.Space;if(function(t){if(Mt)return Mt.test(t);for(let e=0;e"€"&&(i.toUpperCase()!=i.toLowerCase()||At.test(i)))return!0}return!1}(e))return Ct.Word;for(let i=0;i-1)return Ct.Word;return Ct.Other}}class Tt{constructor(t,e,i,n,s,r){this.config=t,this.doc=e,this.selection=i,this.values=n,this.status=t.statusTemplate.slice(),this.computeSlot=s,r&&(r._state=this);for(let t=0;ts.set(e,t)),i=null),s.set(e.value.compartment,e.value.extension)):e.is(gt.reconfigure)?(i=null,n=e.value):e.is(gt.appendConfig)&&(i=null,n=St(n).concat(e.value));if(i)e=t.startState.values.slice();else{i=nt.resolve(n,s,this),e=new Tt(i,this.doc,this.selection,i.dynamicSlots.map(()=>null),(t,e)=>e.reconfigure(t,this),null).values}let r=t.startState.facet(lt)?t.newSelection:t.newSelection.asSingle();new Tt(i,t.newDoc,r,e,(e,i)=>i.update(e,t),t)}replaceSelection(t){return"string"==typeof t&&(t=this.toText(t)),this.changeByRange(e=>({changes:{from:e.from,to:e.to,insert:t},range:W.cursor(e.from+t.length)}))}changeByRange(t){let e=this.selection,i=t(e.ranges[0]),n=this.changes(i.changes),s=[i.range],r=St(i.effects);for(let i=1;is.spec.fromJSON(r,t)))}return Tt.create({doc:t.doc,selection:W.fromJSON(t.selection),extensions:e.extensions?n.concat([e.extensions]):n})}static create(t={}){let e=nt.resolve(t.extensions||[],new Map),i=t.doc instanceof f?t.doc:f.of((t.doc||"").split(e.staticFacet(Tt.lineSeparator)||M)),n=t.selection?t.selection instanceof W?t.selection:W.single(t.selection.anchor,t.selection.head):W.single(0);return H(n,i.length),e.staticFacet(lt)||(n=n.asSingle()),new Tt(e,i,n,e.dynamicSlots.map(()=>null),(t,e)=>e.create(t),null)}get tabSize(){return this.facet(Tt.tabSize)}get lineBreak(){return this.facet(Tt.lineSeparator)||"\n"}get readOnly(){return this.facet(ft)}phrase(t,...e){for(let e of this.facet(Tt.phrases))if(Object.prototype.hasOwnProperty.call(e,t)){t=e[t];break}return e.length&&(t=t.replace(/\$(\$|\d*)/g,(t,i)=>{if("$"==i)return"$";let n=+(i||1);return!n||n>e.length?t:e[n-1]})),t}languageDataAt(t,e,i=-1){let n=[];for(let s of this.facet(ot))for(let r of s(this,e,i))Object.prototype.hasOwnProperty.call(r,t)&&n.push(r[t]);return n}charCategorizer(t){let e=this.languageDataAt("wordChars",t);return Ot(e.length?e[0]:"")}wordAt(t){let{text:e,from:i,length:n}=this.doc.lineAt(t),s=this.charCategorizer(t),r=t-i,o=t-i;for(;r>0;){let t=k(e,r,!1);if(s(e.slice(t,r))!=Ct.Word)break;r=t}for(;ot.length?t[0]:4}),Tt.lineSeparator=at,Tt.readOnly=ft,Tt.phrases=z.define({compare(t,e){let i=Object.keys(t),n=Object.keys(e);return i.length==n.length&&i.every(i=>t[i]==e[i])}}),Tt.languageData=ot,Tt.changeFilter=ht,Tt.transactionFilter=ct,Tt.transactionExtender=ut,et.reconfigure=gt.define();class Rt{eq(t){return this==t}range(t,e=t){return Bt.create(t,e,this)}}function Pt(t,e){return t==e||t.constructor==e.constructor&&t.eq(e)}Rt.prototype.startSide=Rt.prototype.endSide=0,Rt.prototype.point=!1,Rt.prototype.mapMode=O.TrackDel;let Bt=class t{constructor(t,e,i){this.from=t,this.to=e,this.value=i}static create(e,i,n){return new t(e,i,n)}};function Et(t,e){return t.from-e.from||t.value.startSide-e.value.startSide}class Lt{constructor(t,e,i,n){this.from=t,this.to=e,this.value=i,this.maxPoint=n}get length(){return this.to[this.to.length-1]}findIndex(t,e,i,n=0){let s=i?this.to:this.from;for(let r=n,o=s.length;;){if(r==o)return r;let n=r+o>>1,l=s[n]-t||(i?this.value[n].endSide:this.value[n].startSide)-e;if(n==r)return l>=0?r:o;l>=0?o=n:r=n+1}}between(t,e,i,n){for(let s=this.findIndex(e,-1e9,!0),r=this.findIndex(i,1e9,!1,s);sh||a==h&&c.startSide>0&&c.endSide<=0)continue;(h-a||c.endSide-c.startSide)<0||(r<0&&(r=a),c.point&&(o=Math.max(o,h-a)),i.push(c),n.push(a-r),s.push(h-r))}return{mapped:i.length?new Lt(n,s,i,o):null,pos:r}}}class It{constructor(t,e,i,n){this.chunkPos=t,this.chunk=e,this.nextLayer=i,this.maxPoint=n}static create(t,e,i,n){return new It(t,e,i,n)}get length(){let t=this.chunk.length-1;return t<0?0:Math.max(this.chunkEnd(t),this.nextLayer.length)}get size(){if(this.isEmpty)return 0;let t=this.nextLayer.size;for(let e of this.chunk)t+=e.value.length;return t}chunkEnd(t){return this.chunkPos[t]+this.chunk[t].length}update(t){let{add:e=[],sort:i=!1,filterFrom:n=0,filterTo:s=this.length}=t,r=t.filter;if(0==e.length&&!r)return this;if(i&&(e=e.slice().sort(Et)),this.isEmpty)return e.length?It.of(e):this;let o=new Ht(this,null,-1).goto(0),l=0,a=[],h=new Nt;for(;o.value||l=0){let t=e[l++];h.addInner(t.from,t.to,t.value)||a.push(t)}else 1==o.rangeIndex&&o.chunkIndexthis.chunkEnd(o.chunkIndex)||so.to||s=s&&t<=s+r.length&&!1===r.between(s,t-s,e-s,i))return}this.nextLayer.between(t,e,i)}}iter(t=0){return Vt.from([this]).goto(t)}get isEmpty(){return this.nextLayer==this}static iter(t,e=0){return Vt.from(t).goto(e)}static compare(t,e,i,n,s=-1){let r=t.filter(t=>t.maxPoint>0||!t.isEmpty&&t.maxPoint>=s),o=e.filter(t=>t.maxPoint>0||!t.isEmpty&&t.maxPoint>=s),l=Wt(r,o,i),a=new Ft(r,l,s),h=new Ft(o,l,s);i.iterGaps((t,e,i)=>qt(a,t,h,e,i,n)),i.empty&&0==i.length&&qt(a,0,h,0,0,n)}static eq(t,e,i=0,n){null==n&&(n=999999999);let s=t.filter(t=>!t.isEmpty&&e.indexOf(t)<0),r=e.filter(e=>!e.isEmpty&&t.indexOf(e)<0);if(s.length!=r.length)return!1;if(!s.length)return!0;let o=Wt(s,r),l=new Ft(s,o,0).goto(i),a=new Ft(r,o,0).goto(i);for(;;){if(l.to!=a.to||!_t(l.active,a.active)||l.point&&(!a.point||!Pt(l.point,a.point)))return!1;if(l.to>n)return!0;l.next(),a.next()}}static spans(t,e,i,n,s=-1){let r=new Ft(t,null,s).goto(e),o=e,l=r.openStart;for(;;){let t=Math.min(r.to,i);if(r.point){let i=r.activeForPoint(r.to),s=r.pointFromo&&(n.span(o,t,r.active,l),l=r.openEnd(t));if(r.to>i)return l+(r.point&&r.to>i?1:0);o=r.to,r.next()}}static of(t,e=!1){let i=new Nt;for(let n of t instanceof Bt?[t]:e?function(t){if(t.length>1)for(let e=t[0],i=1;i0)return t.slice().sort(Et);e=n}return t}(t):t)i.add(n.from,n.to,n.value);return i.finish()}static join(t){if(!t.length)return It.empty;let e=t[t.length-1];for(let i=t.length-2;i>=0;i--)for(let n=t[i];n!=It.empty;n=n.nextLayer)e=new It(n.chunkPos,n.chunk,e,Math.max(n.maxPoint,e.maxPoint));return e}}It.empty=new It([],[],null,-1),It.empty.nextLayer=It.empty;class Nt{finishChunk(t){this.chunks.push(new Lt(this.from,this.to,this.value,this.maxPoint)),this.chunkPos.push(this.chunkStart),this.chunkStart=-1,this.setMaxPoint=Math.max(this.setMaxPoint,this.maxPoint),this.maxPoint=-1,t&&(this.from=[],this.to=[],this.value=[])}constructor(){this.chunks=[],this.chunkPos=[],this.chunkStart=-1,this.last=null,this.lastFrom=-1e9,this.lastTo=-1e9,this.from=[],this.to=[],this.value=[],this.maxPoint=-1,this.setMaxPoint=-1,this.nextLayer=null}add(t,e,i){this.addInner(t,e,i)||(this.nextLayer||(this.nextLayer=new Nt)).add(t,e,i)}addInner(t,e,i){let n=t-this.lastTo||i.startSide-this.last.endSide;if(n<=0&&(t-this.lastFrom||i.startSide-this.last.startSide)<0)throw new Error("Ranges must be added sorted by `from` position and `startSide`");return!(n<0)&&(250==this.from.length&&this.finishChunk(!0),this.chunkStart<0&&(this.chunkStart=t),this.from.push(t-this.chunkStart),this.to.push(e-this.chunkStart),this.last=i,this.lastFrom=t,this.lastTo=e,this.value.push(i),i.point&&(this.maxPoint=Math.max(this.maxPoint,e-t)),!0)}addChunk(t,e){if((t-this.lastTo||e.value[0].startSide-this.last.endSide)<0)return!1;this.from.length&&this.finishChunk(!0),this.setMaxPoint=Math.max(this.setMaxPoint,e.maxPoint),this.chunks.push(e),this.chunkPos.push(t);let i=e.value.length-1;return this.last=e.value[i],this.lastFrom=e.from[i]+t,this.lastTo=e.to[i]+t,!0}finish(){return this.finishInner(It.empty)}finishInner(t){if(this.from.length&&this.finishChunk(!1),0==this.chunks.length)return t;let e=It.create(this.chunkPos,this.chunks,this.nextLayer?this.nextLayer.finishInner(t):t,this.setMaxPoint);return this.from=null,e}}function Wt(t,e,i){let n=new Map;for(let e of t)for(let t=0;t=this.minPoint)break}}}setRangeIndex(t){if(t==this.layer.chunk[this.chunkIndex].value.length){if(this.chunkIndex++,this.skip)for(;this.chunkIndex=i&&n.push(new Ht(r,e,i,s));return 1==n.length?n[0]:new Vt(n)}get startSide(){return this.value?this.value.startSide:0}goto(t,e=-1e9){for(let i of this.heap)i.goto(t,e);for(let t=this.heap.length>>1;t>=0;t--)zt(this.heap,t);return this.next(),this}forward(t,e){for(let i of this.heap)i.forward(t,e);for(let t=this.heap.length>>1;t>=0;t--)zt(this.heap,t);(this.to-t||this.value.endSide-e)<0&&this.next()}next(){if(0==this.heap.length)this.from=this.to=1e9,this.value=null,this.rank=-1;else{let t=this.heap[0];this.from=t.from,this.to=t.to,this.value=t.value,this.rank=t.rank,t.value&&t.next(),zt(this.heap,0)}}}function zt(t,e){for(let i=t[e];;){let n=1+(e<<1);if(n>=t.length)break;let s=t[n];if(n+1=0&&(s=t[n+1],n++),i.compare(s)<0)break;t[n]=i,t[e]=s,e=n}}class Ft{constructor(t,e,i){this.minPoint=i,this.active=[],this.activeTo=[],this.activeRank=[],this.minActive=-1,this.point=null,this.pointFrom=0,this.pointRank=0,this.to=-1e9,this.endSide=0,this.openStart=-1,this.cursor=Vt.from(t,e,i)}goto(t,e=-1e9){return this.cursor.goto(t,e),this.active.length=this.activeTo.length=this.activeRank.length=0,this.minActive=-1,this.to=t,this.endSide=e,this.openStart=-1,this.next(),this}forward(t,e){for(;this.minActive>-1&&(this.activeTo[this.minActive]-t||this.active[this.minActive].endSide-e)<0;)this.removeActive(this.minActive);this.cursor.forward(t,e)}removeActive(t){Ut(this.active,t),Ut(this.activeTo,t),Ut(this.activeRank,t),this.minActive=$t(this.active,this.activeTo)}addActive(t){let e=0,{value:i,to:n,rank:s}=this.cursor;for(;e0;)e++;Qt(this.active,e,i),Qt(this.activeTo,e,n),Qt(this.activeRank,e,s),t&&Qt(t,e,this.cursor.from),this.minActive=$t(this.active,this.activeTo)}next(){let t=this.to,e=this.point;this.point=null;let i=this.openStart<0?[]:null;for(;;){let n=this.minActive;if(n>-1&&(this.activeTo[n]-this.cursor.from||this.active[n].endSide-this.cursor.startSide)<0){if(this.activeTo[n]>t){this.to=this.activeTo[n],this.endSide=this.active[n].endSide;break}this.removeActive(n),i&&Ut(i,n)}else{if(!this.cursor.value){this.to=this.endSide=1e9;break}if(this.cursor.from>t){this.to=this.cursor.from,this.endSide=this.cursor.startSide;break}{let t=this.cursor.value;if(t.point){if(!(e&&this.cursor.to==this.to&&this.cursor.from=0&&i[e]=0&&!(this.activeRank[i]t||this.activeTo[i]==t&&this.active[i].endSide>=this.point.endSide)&&e.push(this.active[i]);return e.reverse()}openEnd(t){let e=0;for(let i=this.activeTo.length-1;i>=0&&this.activeTo[i]>t;i--)e++;return e}}function qt(t,e,i,n,s,r){t.goto(e),i.goto(n);let o=n+s,l=n,a=n-e,h=!!r.boundChange;for(let e=!1;;){let n=t.to+a-i.to,s=n||t.endSide-i.endSide,c=s<0?t.to+a:i.to,u=Math.min(c,o);if(t.point||i.point?(t.point&&i.point&&Pt(t.point,i.point)&&_t(t.activeForPoint(t.to),i.activeForPoint(i.to))||r.comparePoint(l,u,t.point,i.point),e=!1):(e&&r.boundChange(l),u>l&&!_t(t.active,i.active)&&r.compareRange(l,u,t.active,i.active),h&&uo)break;l=c,s<=0&&t.next(),s>=0&&i.next()}}function _t(t,e){if(t.length!=e.length)return!1;for(let i=0;i=e;i--)t[i+1]=t[i];t[e]=i}function $t(t,e){let i=-1,n=1e9;for(let s=0;s=e)return n;if(n==t.length)break;s+=9==t.charCodeAt(n)?i-s%i:1,n=k(t,n)}return!0===n?-1:t.length}const Xt="undefined"==typeof Symbol?"__ͼ":Symbol.for("ͼ"),Gt="undefined"==typeof Symbol?"__styleSet"+Math.floor(1e8*Math.random()):Symbol("styleSet"),Yt="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:{};class Jt{constructor(t,e){this.rules=[];let{finish:i}=e||{};function n(t){return/^@/.test(t)?[t]:t.split(/,\s*/)}function s(t,e,r,o){let l=[],a=/^@(\w+)\b/.exec(t[0]),h=a&&"keyframes"==a[1];if(a&&null==e)return r.push(t[0]+";");for(let i in e){let o=e[i];if(/&/.test(i))s(i.split(/,\s*/).map(e=>t.map(t=>e.replace(/&/,t))).reduce((t,e)=>t.concat(e)),o,r);else if(o&&"object"==typeof o){if(!a)throw new RangeError("The value of a property ("+i+") should be a primitive value.");s(n(i),o,l,h)}else null!=o&&l.push(i.replace(/_.*/,"").replace(/[A-Z]/g,t=>"-"+t.toLowerCase())+": "+o+";")}(l.length||h)&&r.push((!i||a||o?t:t.map(i)).join(", ")+" {"+l.join(" ")+"}")}for(let e in t)s(n(e),t[e],this.rules)}getRules(){return this.rules.join("\n")}static newName(){let t=Yt[Xt]||1;return Yt[Xt]=t+1,"ͼ"+t.toString(36)}static mount(t,e,i){let n=t[Gt],s=i&&i.nonce;n?s&&n.setNonce(s):n=new te(t,s),n.mount(Array.isArray(e)?e:[e],t)}}let Zt=new Map;class te{constructor(t,e){let i=t.ownerDocument||t,n=i.defaultView;if(!t.head&&t.adoptedStyleSheets&&n.CSSStyleSheet){let e=Zt.get(i);if(e)return t[Gt]=e;this.sheet=new n.CSSStyleSheet,Zt.set(i,this)}else this.styleTag=i.createElement("style"),e&&this.styleTag.setAttribute("nonce",e);this.modules=[],t[Gt]=this}mount(t,e){let i=this.sheet,n=0,s=0;for(let e=0;e-1&&(this.modules.splice(o,1),s--,o=-1),-1==o){if(this.modules.splice(s++,0,r),i)for(let t=0;t",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'},ne="undefined"!=typeof navigator&&/Mac/.test(navigator.platform),se="undefined"!=typeof navigator&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),re=0;re<10;re++)ee[48+re]=ee[96+re]=String(re);for(re=1;re<=24;re++)ee[re+111]="F"+re;for(re=65;re<=90;re++)ee[re]=String.fromCharCode(re+32),ie[re]=String.fromCharCode(re);for(var oe in ee)ie.hasOwnProperty(oe)||(ie[oe]=ee[oe]);function le(){var t=arguments[0];"string"==typeof t&&(t=document.createElement(t));var e=1,i=arguments[1];if(i&&"object"==typeof i&&null==i.nodeType&&!Array.isArray(i)){for(var n in i)if(Object.prototype.hasOwnProperty.call(i,n)){var s=i[n];"string"==typeof s?t.setAttribute(n,s):null!=s&&(t[n]=s)}e++}for(;e2);var ye={mac:be||/Mac/.test(he.platform),windows:/Win/.test(he.platform),linux:/Linux|X11/.test(he.platform),ie:pe,ie_version:fe?ce.documentMode||6:de?+de[1]:ue?+ue[1]:0,gecko:me,gecko_version:me?+(/Firefox\/(\d+)/.exec(he.userAgent)||[0,0])[1]:0,chrome:!!ge,chrome_version:ge?+ge[1]:0,ios:be,android:/Android\b/.test(he.userAgent),webkit:ve,webkit_version:ve?+(/\bAppleWebKit\/(\d+)/.exec(he.userAgent)||[0,0])[1]:0,safari:we,safari_version:we?+(/\bVersion\/(\d+(\.\d+)?)/.exec(he.userAgent)||[0,0])[1]:0,tabSize:null!=ce.documentElement.style.tabSize?"tab-size":"-moz-tab-size"};function xe(t,e){for(let i in t)"class"==i&&e.class?e.class+=" "+t.class:"style"==i&&e.style?e.style+=";"+t.style:e[i]=t[i];return e}const ke=Object.create(null);function Se(t,e,i){if(t==e)return!0;t||(t=ke),e||(e=ke);let n=Object.keys(t),s=Object.keys(e);if(n.length-(i&&n.indexOf(i)>-1?1:0)!=s.length-(i&&s.indexOf(i)>-1?1:0))return!1;for(let r of n)if(r!=i&&(-1==s.indexOf(r)||t[r]!==e[r]))return!1;return!0}function Ce(t,e,i){let n=!1;if(e)for(let s in e)i&&s in i||(n=!0,"style"==s?t.style.cssText="":t.removeAttribute(s));if(i)for(let s in i)e&&e[s]==i[s]||(n=!0,"style"==s?t.style.cssText=i[s]:t.setAttribute(s,i[s]));return n}function Ae(t){let e=Object.create(null);for(let i=0;i0?3e8:-4e8:e>0?1e8:-1e8,new Pe(t,e,e,i,t.widget||null,!1)}static replace(t){let e,i,n=!!t.block;if(t.isBlockGap)e=-5e8,i=4e8;else{let{start:s,end:r}=Be(t,n);e=(s?n?-3e8:-1:5e8)-1,i=1+(r?n?2e8:1:-6e8)}return new Pe(t,e,i,n,t.widget||null,!0)}static line(t){return new Re(t)}static set(t,e=!1){return It.of(t,e)}hasHeight(){return!!this.widget&&this.widget.estimatedHeight>-1}}Te.none=It.empty;class De extends Te{constructor(t){let{start:e,end:i}=Be(t);super(e?-1:5e8,i?1:-6e8,null,t),this.tagName=t.tagName||"span",this.attrs=t.class&&t.attributes?xe(t.attributes,{class:t.class}):t.class?{class:t.class}:t.attributes||ke}eq(t){return this==t||t instanceof De&&this.tagName==t.tagName&&Se(this.attrs,t.attrs)}range(t,e=t){if(t>=e)throw new RangeError("Mark decorations may not be empty");return super.range(t,e)}}De.prototype.point=!1;class Re extends Te{constructor(t){super(-2e8,-2e8,null,t)}eq(t){return t instanceof Re&&this.spec.class==t.spec.class&&Se(this.spec.attributes,t.spec.attributes)}range(t,e=t){if(e!=t)throw new RangeError("Line decoration ranges must be zero-length");return super.range(t,e)}}Re.prototype.mapMode=O.TrackBefore,Re.prototype.point=!0;class Pe extends Te{constructor(t,e,i,n,s,r){super(e,i,s,t),this.block=n,this.isReplace=r,this.mapMode=n?e<=0?O.TrackBefore:O.TrackAfter:O.TrackDel}get type(){return this.startSide!=this.endSide?Oe.WidgetRange:this.startSide<=0?Oe.WidgetBefore:Oe.WidgetAfter}get heightRelevant(){return this.block||!!this.widget&&(this.widget.estimatedHeight>=5||this.widget.lineBreaks>0)}eq(t){return t instanceof Pe&&(e=this.widget,i=t.widget,e==i||!!(e&&i&&e.compare(i)))&&this.block==t.block&&this.startSide==t.startSide&&this.endSide==t.endSide;var e,i}range(t,e=t){if(this.isReplace&&(t>e||t==e&&this.startSide>0&&this.endSide<=0))throw new RangeError("Invalid range for replacement decoration");if(!this.isReplace&&e!=t)throw new RangeError("Widget decorations can only have zero-length ranges");return super.range(t,e)}}function Be(t,e=!1){let{inclusiveStart:i,inclusiveEnd:n}=t;return null==i&&(i=t.inclusive),null==n&&(n=t.inclusive),{start:null!=i?i:e,end:null!=n?n:e}}function Ee(t,e,i,n=0){let s=i.length-1;s>=0&&i[s]+n>=t?i[s]=Math.max(i[s],e):i.push(t,e)}Pe.prototype.point=!0;class Le extends Rt{constructor(t,e,i){super(),this.tagName=t,this.attributes=e,this.rank=i}eq(t){return t==this||t instanceof Le&&this.tagName==t.tagName&&Se(this.attributes,t.attributes)}static create(t){return new Le(t.tagName,t.attributes||ke,null==t.rank?50:Math.max(0,Math.min(t.rank,100)))}static set(t,e=!1){return It.of(t,e)}}function Ie(t){let e;return e=11==t.nodeType?t.getSelection?t:t.ownerDocument:t,e.getSelection()}function Ne(t,e){return!!e&&(t==e||t.contains(1!=e.nodeType?e.parentNode:e))}function We(t,e){if(!e.anchorNode)return!1;try{return Ne(t,e.anchorNode)}catch(t){return!1}}function He(t){return 3==t.nodeType?Je(t,0,t.nodeValue.length).getClientRects():1==t.nodeType?t.getClientRects():[]}function Ve(t,e,i,n){return!!i&&(qe(t,e,i,n,-1)||qe(t,e,i,n,1))}function ze(t){for(var e=0;;e++)if(!(t=t.previousSibling))return e}function Fe(t){return 1==t.nodeType&&/^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(t.nodeName)}function qe(t,e,i,n,s){for(;;){if(t==i&&e==n)return!0;if(e==(s<0?0:_e(t))){if("DIV"==t.nodeName)return!1;let i=t.parentNode;if(!i||1!=i.nodeType)return!1;e=ze(t)+(s<0?0:1),t=i}else{if(1!=t.nodeType)return!1;if(1==(t=t.childNodes[e+(s<0?-1:0)]).nodeType&&"false"==t.contentEditable)return!1;e=s<0?_e(t):0}}}function _e(t){return 3==t.nodeType?t.nodeValue.length:t.childNodes.length}function Ue(t,e){let{left:i,right:n}=t;if(i==n)return t;let s=e?i:n;return{left:s,right:s,top:t.top,bottom:t.bottom}}function Qe(t){let e=t.visualViewport;return e?{left:0,right:e.width,top:0,bottom:e.height}:{left:0,right:t.innerWidth,top:0,bottom:t.innerHeight}}function $e(t,e){let i=e.width/t.offsetWidth,n=e.height/t.offsetHeight;return(i>.995&&i<1.005||!isFinite(i)||Math.abs(e.width-t.offsetWidth)<1)&&(i=1),(n>.995&&n<1.005||!isFinite(n)||Math.abs(e.height-t.offsetHeight)<1)&&(n=1),{scaleX:i,scaleY:n}}function Ke(t,e=!0){let i=t.ownerDocument,n=null,s=null;for(let r=t.parentNode;r&&(r!=i.body&&(e&&!n||!s));)if(1==r.nodeType)!s&&r.scrollHeight>r.clientHeight&&(s=r),e&&!n&&r.scrollWidth>r.clientWidth&&(n=r),r=r.assignedSlot||r.parentNode;else{if(11!=r.nodeType)break;r=r.host}return{x:n,y:s}}Le.prototype.startSide=Le.prototype.endSide=-1;class je{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}eq(t){return this.anchorNode==t.anchorNode&&this.anchorOffset==t.anchorOffset&&this.focusNode==t.focusNode&&this.focusOffset==t.focusOffset}setRange(t){let{anchorNode:e,focusNode:i}=t;this.set(e,Math.min(t.anchorOffset,e?_e(e):0),i,Math.min(t.focusOffset,i?_e(i):0))}set(t,e,i,n){this.anchorNode=t,this.anchorOffset=e,this.focusNode=i,this.focusOffset=n}}let Xe,Ge=null;function Ye(t){if(t.setActive)return t.setActive();if(Ge)return t.focus(Ge);let e=[];for(let i=t;i&&(e.push(i,i.scrollTop,i.scrollLeft),i!=i.ownerDocument);i=i.parentNode);if(t.focus(null==Ge?{get preventScroll(){return Ge={preventScroll:!0},!0}}:void 0),!Ge){Ge=!1;for(let t=0;tMath.max(0,t.document.documentElement.scrollHeight-t.innerHeight-4):t.scrollTop>Math.max(1,t.scrollHeight-t.clientHeight-4)}function ei(t,e){for(let i=t,n=e;;){if(3==i.nodeType&&n>0)return{node:i,offset:n};if(1==i.nodeType&&n>0){if("false"==i.contentEditable)return null;i=i.childNodes[n-1],n=_e(i)}else{if(!i.parentNode||Fe(i))return null;n=ze(i),i=i.parentNode}}}function ii(t,e){for(let i=t,n=e;;){if(3==i.nodeType&&n=26&&(Ge=!1);class ni{constructor(t,e,i=!0){this.node=t,this.offset=e,this.precise=i}static before(t,e){return new ni(t.parentNode,ze(t),e)}static after(t,e){return new ni(t.parentNode,ze(t)+1,e)}}var si=function(t){return t[t.LTR=0]="LTR",t[t.RTL=1]="RTL",t}(si||(si={}));const ri=si.LTR,oi=si.RTL;function li(t){let e=[];for(let i=0;i=e){if(o.level==i)return r;(s<0||(0!=n?n<0?o.frome:t[s].level>o.level))&&(s=r)}}if(s<0)throw new RangeError("Index out of range");return s}}function mi(t,e){if(t.length!=e.length)return!1;for(let i=0;ia&&o.push(new pi(a,p.from,f)),wi(t,p.direction==ri!=!(f%2)?n+1:n,s,p.inner,p.from,p.to,o),a=p.to}d=p.to}else{if(d==i||(e?gi[d]!=l:gi[d]==l))break;d++}u?vi(t,a,d,n+1,s,u,o):ae;){let i=!0,c=!1;if(!h||a>r[h-1].to){let t=gi[a-1];t!=l&&(i=!1,c=16==t)}let u=i||1!=l?null:[],f=i?n:n+1,d=a;t:for(;;)if(h&&d==r[h-1].to){if(c)break t;let p=r[--h];if(!i)for(let t=p.from,i=h;;){if(t==e)break t;if(!i||r[i-1].to!=t){if(gi[t-1]==l)break t;break}t=r[--i].from}if(u)u.push(p);else{p.to=0;t-=3)if(ui[t+1]==-i){let e=ui[t+2],i=2&e?s:4&e?1&e?r:s:0;i&&(gi[o]=gi[ui[t]]=i),l=t;break}}else{if(189==ui.length)break;ui[l++]=o,ui[l++]=e,ui[l++]=a}else if(2==(n=gi[o])||1==n){let t=n==s;a=t?0:1;for(let e=l-3;e>=0;e-=3){let i=ui[e+2];if(2&i)break;if(t)ui[e+2]|=2;else{if(4&i)break;ui[e+2]|=4}}}}}(t,s,r,n,l),function(t,e,i,n){for(let s=0,r=n;s<=i.length;s++){let o=s?i[s-1].to:t,l=sa;)e==r&&(e=i[--n].from,r=n?i[n-1].to:t),gi[--e]=c;a=o}else r=o,a++}}}(s,r,n,l),vi(t,s,r,e,i,n,o)}function bi(t){return[new pi(0,t,0)]}let yi="";function xi(t,e,i,n,s){var r;let o=n.head-t.from,l=pi.find(e,o,null!==(r=n.bidiLevel)&&void 0!==r?r:-1,n.assoc),a=e[l],h=a.side(s,i);if(o==h){let t=l+=s?1:-1;if(t<0||t>=e.length)return null;a=e[l=t],o=a.side(!s,i),h=a.side(s,i)}let c=k(t.text,o,a.forward(s,i));(ca.to)&&(c=h),yi=t.text.slice(Math.min(o,c),Math.max(o,c));let u=l==(s?e.length-1:0)?null:e[l+(s?1:-1)];return u&&c==h&&u.level+(s?0:1)t.some(t=>t)}),Ei=z.define({combine:t=>t.some(t=>t)}),Li=z.define();class Ii{constructor(t,e,i,n,s,r=!1){this.range=t,this.y=e,this.x=i,this.yMargin=n,this.xMargin=s,this.isSnapshot=r}map(t){return t.empty?this:new Ii(this.range.map(t),this.y,this.x,this.yMargin,this.xMargin,this.isSnapshot)}clip(t){return this.range.to<=t.doc.length?this:new Ii(W.cursor(t.doc.length),this.y,this.x,this.yMargin,this.xMargin,this.isSnapshot)}}const Ni=gt.define({map:(t,e)=>t.map(e)}),Wi=gt.define();function Hi(t,e,i){let n=t.facet(Mi);n.length?n[0](e):window.onerror&&window.onerror(String(e),i,void 0,void 0,e)||(i?console.error(i+":",e):console.error(e))}const Vi=z.define({combine:t=>!t.length||t[0]});let zi=0;const Fi=z.define({combine:t=>t.filter((e,i)=>{for(let n=0;n{let e=[];return r&&e.push($i.of(e=>{let i=e.plugin(t);return i?r(i):Te.none})),s&&e.push(s(t)),e})}static fromClass(t,e){return qi.define((e,i)=>new t(e,i),e)}}class _i{constructor(t){this.spec=t,this.mustUpdate=null,this.value=null}get plugin(){return this.spec&&this.spec.plugin}update(t){if(this.value){if(this.mustUpdate){let t=this.mustUpdate;if(this.mustUpdate=null,this.value.update)try{this.value.update(t)}catch(e){if(Hi(t.state,e,"CodeMirror plugin crashed"),this.value.destroy)try{this.value.destroy()}catch(t){}this.deactivate()}}}else if(this.spec)try{this.value=this.spec.plugin.create(t,this.spec.arg)}catch(e){Hi(t.state,e,"CodeMirror plugin crashed"),this.deactivate()}return this}destroy(t){var e;if(null===(e=this.value)||void 0===e?void 0:e.destroy)try{this.value.destroy()}catch(e){Hi(t.state,e,"CodeMirror plugin crashed")}}deactivate(){this.spec=this.value=null}}const Ui=z.define(),Qi=z.define(),$i=z.define(),Ki=z.define(),ji=z.define(),Xi=z.define(),Gi=z.define();function Yi(t,e){let i=t.state.facet(Gi);if(!i.length)return i;let n=i.map(e=>e instanceof Function?e(t):e),s=[];return It.spans(n,e.from,e.to,{point(){},span(t,i,n,r){let o=t-e.from,l=i-e.from,a=s;for(let t=n.length-1;t>=0;t--,r--){let i,s=n[t].spec.bidiIsolate;if(null==s&&(s=ki(e.text,o,l)),r>0&&a.length&&(i=a[a.length-1]).to==o&&i.direction==s)i.to=l,a=i.inner;else{let t={from:o,to:l,direction:s,inner:[]};a.push(t),a=t.inner}}}}),s}const Ji=z.define();function Zi(t){let e=0,i=0,n=0,s=0;for(let r of t.state.facet(Ji)){let o=r(t);o&&(null!=o.left&&(e=Math.max(e,o.left)),null!=o.right&&(i=Math.max(i,o.right)),null!=o.top&&(n=Math.max(n,o.top)),null!=o.bottom&&(s=Math.max(s,o.bottom)))}return{left:e,right:i,top:n,bottom:s}}const tn=z.define();class en{constructor(t,e,i,n){this.fromA=t,this.toA=e,this.fromB=i,this.toB=n}join(t){return new en(Math.min(this.fromA,t.fromA),Math.max(this.toA,t.toA),Math.min(this.fromB,t.fromB),Math.max(this.toB,t.toB))}addToSet(t){let e=t.length,i=this;for(;e>0;e--){let n=t[e-1];if(!(n.fromA>i.toA)){if(n.toAn.push(new en(t,e,i,s))),this.changedRanges=n}static create(t,e,i){return new nn(t,e,i)}get viewportChanged(){return(4&this.flags)>0}get viewportMoved(){return(8&this.flags)>0}get heightChanged(){return(2&this.flags)>0}get geometryChanged(){return this.docChanged||(18&this.flags)>0}get focusChanged(){return(1&this.flags)>0}get docChanged(){return!this.changes.empty}get selectionSet(){return this.transactions.some(t=>t.selection)}get empty(){return 0==this.flags&&0==this.transactions.length}}const sn=[];class rn{constructor(t,e,i=0){this.dom=t,this.length=e,this.flags=i,this.parent=null,t.cmTile=this}get breakAfter(){return 1&this.flags}get children(){return sn}isWidget(){return!1}get isHidden(){return!1}isComposite(){return!1}isLine(){return!1}isText(){return!1}isBlock(){return!1}get domAttrs(){return null}sync(t){if(this.flags|=2,4&this.flags){this.flags&=-5;let t=this.domAttrs;t&&function(t,e){for(let i=t.attributes.length-1;i>=0;i--){let n=t.attributes[i].name;null==e[n]&&t.removeAttribute(n)}for(let i in e){let n=e[i];"style"==i?t.style.cssText=n:t.getAttribute(i)!=n&&t.setAttribute(i,n)}}(this.dom,t)}}toString(){return this.constructor.name+(this.children.length?`(${this.children})`:"")+(this.breakAfter?"#":"")}destroy(){this.parent=null}setDOM(t){this.dom=t,t.cmTile=this}get posAtStart(){return this.parent?this.parent.posBefore(this):0}get posAtEnd(){return this.posAtStart+this.length}posBefore(t,e=this.posAtStart){let i=e;for(let e of this.children){if(e==t)return i;i+=e.length+e.breakAfter}throw new RangeError("Invalid child in posBefore")}posAfter(t){return this.posBefore(t)+t.length}covers(t){return!0}coordsIn(t,e,i){return null}domPosFor(t,e){let i=ze(this.dom),n=this.length?t>0:e>0;return new ni(this.parent.dom,i+(n?1:0),0==t||t==this.length)}markDirty(t){this.flags&=-3,t&&(this.flags|=4),this.parent&&2&this.parent.flags&&this.parent.markDirty(!1)}get overrideDOMText(){return null}get root(){for(let t=this;t;t=t.parent)if(t instanceof an)return t;return null}static get(t){return t.cmTile}}class on extends rn{constructor(t){super(t,0),this._children=[]}isComposite(){return!0}get children(){return this._children}get lastChild(){return this.children.length?this.children[this.children.length-1]:null}append(t){this.children.push(t),t.parent=this}sync(t){if(2&this.flags)return;super.sync(t);let e,i=this.dom,n=null,s=(null==t?void 0:t.node)==i?t:null,r=0;for(let o of this.children){if(o.sync(t),r+=o.length+o.breakAfter,e=n?n.nextSibling:i.firstChild,s&&e!=o.dom&&(s.written=!0),o.dom.parentNode==i)for(;e&&e!=o.dom;)e=ln(e);else i.insertBefore(o.dom,e);n=o.dom}for(e=n?n.nextSibling:i.firstChild,s&&e&&(s.written=!0);e;)e=ln(e);this.length=r}}function ln(t){let e=t.nextSibling;return t.parentNode.removeChild(t),e}class an extends on{constructor(t,e){super(e),this.view=t}owns(t){for(;t;t=t.parent)if(t==this)return!0;return!1}isBlock(){return!0}nearest(t){for(;;){if(!t)return null;let e=rn.get(t);if(e&&this.owns(e))return e;t=t.parentNode}}blockTiles(t){for(let e=[],i=this,n=0,s=0;;)if(n==i.children.length){if(!e.length)return;i=i.parent,i.breakAfter&&s++,n=e.pop()}else{let r=i.children[n++];if(r instanceof hn)e.push(n),i=r,n=0;else{let e=s+r.length,i=t(r,s);if(void 0!==i)return i;s=e+r.breakAfter}}}resolveBlock(t,e){let i,n,s=-1,r=-1;if(this.blockTiles((o,l)=>{let a=l+o.length;if(t>=l&&t<=a){if(o.isWidget()&&e>=-1&&e<=1){if(32&o.flags)return!0;16&o.flags&&(i=void 0)}(lt||t==l&&(e>1?o.length:o.covers(-1)))&&(!n||!o.isWidget()&&n.isWidget())&&(n=o,r=t-l)}}),!i&&!n)throw new Error("No tile at position "+t);return i&&e<0||!n?{tile:i,offset:s}:{tile:n,offset:r}}}class hn extends on{constructor(t,e){super(t),this.wrapper=e}isBlock(){return!0}covers(t){return!!this.children.length&&(t<0?this.children[0].covers(-1):this.lastChild.covers(1))}get domAttrs(){return this.wrapper.attributes}static of(t,e){let i=new hn(e||document.createElement(t.tagName),t);return e||(i.flags|=4),i}}class cn extends on{constructor(t,e){super(t),this.attrs=e}isLine(){return!0}static start(t,e,i){let n=new cn(e||document.createElement("div"),t);return e&&i||(n.flags|=4),n}get domAttrs(){return this.attrs}resolveInline(t,e,i){let n=null,s=-1,r=null,o=-1;!function t(l,a){for(let h=0,c=0;h=a&&(u.isComposite()?t(u,a-c):(!r||r.isHidden&&(e>0&&!(32&r.flags)||i&&un(r,u)))&&(f>a||32&u.flags)?(r=u,o=a-c):(cn&&(t=n);let s=t,r=t,o=0;0==t&&e<0||t==n&&e>=0?ye.chrome||ye.gecko||(t?(s--,o=1):r=0)?0:l.length-1];return ye.safari&&!o&&0==a.width&&(a=Array.prototype.find.call(l,t=>t.width)||a),null==i?a:Ue(a,(o?o>0:e<0)==i)}static of(t,e){let i=new dn(e||document.createTextNode(t),t);return e||(i.flags|=2),i}}class pn extends rn{constructor(t,e,i,n){super(t,e,n),this.widget=i}isWidget(){return!0}get isHidden(){return this.widget.isHidden}covers(t){return!(48&this.flags)&&(this.flags&(t<0?64:128))>0}coordsIn(t,e){return this.coordsInWidget(t,e,!1)}coordsInWidget(t,e,i){let n=this.widget.coordsAt(this.dom,t,e);if(n)return n;if(i)return Ue(this.dom.getBoundingClientRect(),this.length?0==t:e<=0);{let e=this.dom.getClientRects(),i=null;if(!e.length)return null;let n=!!(16&this.flags)||!(32&this.flags)&&t>0;for(let s=n?e.length-1:0;i=e[s],!(t>0?0==s:s==e.length-1||i.top0==i)}}class gn{constructor(t){this.index=0,this.beforeBreak=!1,this.parents=[],this.tile=t}advance(t,e,i){let{tile:n,index:s,beforeBreak:r,parents:o}=this;for(;t||e>0;)if(n.isComposite())if(r){if(!t)break;i&&i.break(),t--,r=!1}else if(s==n.children.length){if(!t&&!o.length)break;i&&i.leave(n),r=!!n.breakAfter,({tile:n,index:s}=o.pop()),s++}else{let l=n.children[s],a=l.breakAfter;!(e>0?l.length<=t:l.length=0;t--){let i=e.marks[t],s=n.lastChild;if(s instanceof fn&&s.mark.eq(i.mark))s.dom!=i.dom&&s.setDOM(An(i.dom)),n=s;else{if(this.cache.reused.get(i)){let t=rn.get(i.dom);t&&t.setDOM(An(i.dom))}let t=fn.of(i.mark,i.dom);n.append(t),n=t}this.cache.reused.set(i,2)}let s=rn.get(t.text);s&&this.cache.reused.set(s,2);let r=new dn(t.text,t.text.nodeValue);r.flags|=8,this.pos=t.range.toB,n.append(r)}addInlineWidget(t,e,i){let n=this.afterWidget&&48&t.flags&&(48&this.afterWidget.flags)==(48&t.flags);n||this.flushBuffer();let s=this.ensureMarks(e,i);n||16&t.flags||s.append(this.getBuffer(1)),s.append(t),this.pos+=t.length,this.afterWidget=t}addMark(t,e,i){this.flushBuffer(),this.ensureMarks(e,i).append(t),this.pos+=t.length,this.afterWidget=null}addBlockWidget(t){this.getBlockPos().append(t),this.pos+=t.length,this.lastBlock=t,this.endLine()}continueWidget(t){(this.afterWidget||this.lastBlock).length+=t,this.pos+=t}addLineStart(t,e){var i;t||(t=Cn);let n=cn.start(t,e||(null===(i=this.cache.find(cn))||void 0===i?void 0:i.dom),!!e);this.getBlockPos().append(this.lastBlock=this.curLine=n)}addLine(t){this.getBlockPos().append(t),this.pos+=t.length,this.lastBlock=t,this.endLine()}addBreak(){this.lastBlock.flags|=1,this.endLine(),this.pos++}addLineStartIfNotCovered(t){this.blockPosCovered()||this.addLineStart(t)}ensureLine(t){this.curLine||this.addLineStart(t)}ensureMarks(t,e){var i;let n=this.curLine;for(let s=t.length-1;s>=0;s--){let r,o=t[s];if(e>0&&(r=n.lastChild)&&r instanceof fn&&r.mark.eq(o))n=r,e--;else{let t=fn.of(o,null===(i=this.cache.find(fn,t=>t.mark.eq(o)))||void 0===i?void 0:i.dom);n.append(t),n=t,e=0}}return n}endLine(){if(this.curLine){this.flushBuffer();let t=this.curLine.lastChild;t&&Sn(this.curLine,!1)&&("BR"==t.dom.nodeName||!t.isWidget()||ye.ios&&Sn(this.curLine,!0))||this.curLine.append(this.cache.findWidget(On,0,32)||new pn(On.toDOM(),0,On,32)),this.curLine=this.afterWidget=null}}updateBlockWrappers(){this.wrapperPos>this.pos+1e4&&(this.blockWrappers.goto(this.pos),this.wrappers.length=0);for(let t=this.wrappers.length-1;t>=0;t--)this.wrappers[t].to=this.pos){let e=102*t.rank+t.value.rank,i=new vn(t.from,t.to,t.value,e),n=this.wrappers.length;for(;n>0&&(this.wrappers[n-1].rank-i.rank||this.wrappers[n-1].to-i.to)<0;)n--;this.wrappers.splice(n,0,i)}this.wrapperPos=this.pos}getBlockPos(){var t;this.updateBlockWrappers();let e=this.root;for(let i of this.wrappers){let n=e.lastChild;if(i.fromt.wrapper.eq(i.wrapper)))||void 0===t?void 0:t.dom);e.append(n),e=n}}return e}blockPosCovered(){let t=this.lastBlock;return null!=t&&!t.breakAfter&&(!t.isWidget()||(160&t.flags)>0)}getBuffer(t){let e=2|(t<0?16:32),i=this.cache.find(mn,void 0,1);return i&&(i.flags=e),i||new mn(e)}flushBuffer(){!this.afterWidget||32&this.afterWidget.flags||(this.afterWidget.parent.append(this.getBuffer(-1)),this.afterWidget=null)}}class bn{constructor(t){this.skipCount=0,this.text="",this.textOff=0,this.cursor=t.iter()}skip(t){this.textOff+t<=this.text.length?this.textOff+=t:(this.skipCount+=t-(this.text.length-this.textOff),this.text="",this.textOff=0)}next(t){if(this.textOff==this.text.length){let{value:e,lineBreak:i,done:n}=this.cursor.next(this.skipCount);if(this.skipCount=0,n)throw new Error("Ran out of text content when drawing inline views");this.text=e;let s=this.textOff=Math.min(t,e.length);return i?null:e.slice(0,s)}let e=Math.min(this.text.length,this.textOff+t),i=this.text.slice(this.textOff,e);return this.textOff=e,i}}const yn=[pn,cn,dn,fn,mn,hn,an];for(let t=0;t[]),this.index=yn.map(()=>0),this.reused=new Map}add(t){let e=t.constructor.bucket,i=this.buckets[e];i.length<6?i.push(t):i[this.index[e]=(this.index[e]+1)%6]=t}find(t,e,i=2){let n=t.bucket,s=this.buckets[n],r=this.index[n];for(let t=0;t{if(this.cache.add(t),t.isComposite())return!1},enter:t=>this.cache.add(t),leave:()=>{},break:()=>{}}}run(t,e){let i=e&&this.getCompositionContext(e.text);for(let n=0,s=0,r=0;;){let o=rn){let t=l-n;this.preserve(t,!r,!o),n=l,s+=t}if(!o)break;e&&o.fromA<=e.range.fromA&&o.toA>=e.range.toA?(this.forward(o.fromA,e.range.fromA,e.range.fromA1;i--){let n=i==t.parents.length?t.tile:t.parents[i].tile;n instanceof fn&&e.push(n.mark)}return e}(this.old),s=this.openMarks;this.old.advance(t,i?1:-1,{skip:(t,e,i)=>{if(t.isWidget())if(this.openWidget)this.builder.continueWidget(i-e);else{let r=i>0||e{t.isLine()?this.builder.addLineStart(t.attrs,this.cache.maybeReuse(t)):(this.cache.add(t),t instanceof fn&&n.unshift(t.mark)),this.openWidget=!1},leave:t=>{t.isLine()?n.length&&(n.length=s=0):t instanceof fn&&(n.shift(),s=Math.min(s,n.length))},break:()=>{this.builder.addBreak(),this.openWidget=!1}}),this.text.skip(t)}emit(t,e){let i=null,n=this.builder,s=-1,r=It.spans(this.decorations,t,e,{point:(t,e,r,o,l,a)=>{if(r instanceof Pe){if(this.disallowBlockEffectsFor[a]){if(r.block)throw new RangeError("Block decorations may not be specified via plugins");if(e>this.view.state.doc.lineAt(t).to)throw new RangeError("Decorations that replace line breaks may not be specified via plugins")}if(s=o.length,l>o.length)n.continueWidget(e-t);else{let s=r.widget||(r.block?Mn.block:Mn.inline),a=function(t){let e=t.isReplace?(t.startSide<0?64:0)|(t.endSide>0?128:0):t.startSide>0?32:16;t.block&&(e|=256);return e}(r),h=this.cache.findWidget(s,e-t,a)||pn.of(s,this.view,e-t,a);r.block?(r.startSide>0&&n.addLineStartIfNotCovered(i),n.addBlockWidget(h)):(n.ensureLine(i),n.addInlineWidget(h,o,l))}i=null}else i=function(t,e){let i=e.spec.attributes,n=e.spec.class;if(!i&&!n)return t;t||(t={class:"cm-line"});i&&xe(i,t);n&&(t.class+=" "+n);return t}(i,r);e>t&&this.text.skip(e-t)},span:(t,e,r,o)=>{for(let s=t;s-1&&(this.openWidget=r>s),this.openWidget||n.addLineStartIfNotCovered(i),this.openMarks=r}forward(t,e,i=1){e-t<=10?this.old.advance(e-t,i,this.reuseWalker):(this.old.advance(5,-1,this.reuseWalker),this.old.advance(e-t-10,-1),this.old.advance(5,i,this.reuseWalker))}getCompositionContext(t){let e=[],i=null;for(let n=t.parentNode;;n=n.parentNode){let t=rn.get(n);if(n==this.view.contentDOM)break;t instanceof fn?e.push(t):(null==t?void 0:t.isLine())?i=t:t instanceof hn||("DIV"!=n.nodeName||i||n==this.view.contentDOM?i||e.push(fn.of(new De({tagName:n.nodeName.toLowerCase(),attributes:Ae(n)}),n)):i=new cn(n,Cn))}return{line:i,marks:e}}}function Sn(t,e){let i=t=>{for(let n of t.children)if((e?n.isText():n.length)||i(n))return!0;return!1};return i(t)}const Cn={class:"cm-line"};function An(t){let e=rn.get(t);return e&&e.setDOM(t.cloneNode()),t}class Mn extends Me{constructor(t){super(),this.tag=t}eq(t){return t.tag==this.tag}toDOM(){return document.createElement(this.tag)}updateDOM(t){return t.nodeName.toLowerCase()==this.tag}get isHidden(){return!0}}Mn.inline=new Mn("span"),Mn.block=new Mn("div");const On=new class extends Me{toDOM(){return document.createElement("br")}get isHidden(){return!0}get editable(){return!0}};class Tn{constructor(t){this.view=t,this.decorations=[],this.blockWrappers=[],this.dynamicDecorationMap=[!1],this.domChanged=null,this.hasComposition=null,this.editContextFormatting=Te.none,this.lastCompositionAfterCursor=!1,this.minWidth=0,this.minWidthFrom=0,this.minWidthTo=0,this.impreciseAnchor=null,this.impreciseHead=null,this.forceSelection=!1,this.lastUpdate=Date.now(),this.updateDeco(),this.tile=new an(t,t.contentDOM),this.updateInner([new en(0,0,0,t.state.doc.length)],null)}update(t){var e;let i=t.changedRanges;this.minWidth>0&&i.length&&(i.every(({fromA:t,toA:e})=>ethis.minWidthTo)?(this.minWidthFrom=t.changes.mapPos(this.minWidthFrom,1),this.minWidthTo=t.changes.mapPos(this.minWidthTo,1)):this.minWidth=this.minWidthFrom=this.minWidthTo=0),this.updateEditContextFormatting(t);let n=-1;this.view.inputState.composing>=0&&!this.view.observer.editContext&&((null===(e=this.domChanged)||void 0===e?void 0:e.newSel)?n=this.domChanged.newSel.head:function(t,e){let i=!1;e&&t.iterChangedRanges((t,n)=>{te.from&&(i=!0)});return i}(t.changes,this.hasComposition)||t.selectionSet||(n=t.state.selection.main.head));let s=n>-1?function(t,e,i){let n=Rn(t,i);if(!n)return null;let{node:s,from:r,to:o}=n,l=s.nodeValue;if(/[\n\r]/.test(l))return null;if(t.state.doc.sliceString(n.from,n.to)!=l)return null;let a=e.invertedDesc;return{range:new en(a.mapPos(r),a.mapPos(o),r,o),text:s}}(this.view,t.changes,n):null;if(this.domChanged=null,this.hasComposition){let{from:e,to:n}=this.hasComposition;i=new en(e,n,t.changes.mapPos(e,-1),t.changes.mapPos(n,1)).addToSet(i.slice())}this.hasComposition=s?{from:s.range.fromB,to:s.range.toB}:null,(ye.ie||ye.chrome)&&!s&&t&&t.state.doc.lines!=t.startState.doc.lines&&(this.forceSelection=!0);let r=this.decorations,o=this.blockWrappers;this.updateDeco();let l=function(t,e,i){let n=new Pn;return It.compare(t,e,i,n),n.changes}(r,this.decorations,t.changes);l.length&&(i=en.extendWithRanges(i,l));let a=function(t,e,i){let n=new Bn;return It.compare(t,e,i,n),n.changes}(o,this.blockWrappers,t.changes);return a.length&&(i=en.extendWithRanges(i,a)),s&&!i.some(t=>t.fromA<=s.range.fromA&&t.toA>=s.range.toA)&&(i=s.range.addToSet(i.slice())),!(2&this.tile.flags&&0==i.length)&&(this.updateInner(i,s),t.transactions.length&&(this.lastUpdate=Date.now()),!0)}updateInner(t,e){this.view.viewState.mustMeasureContent=!0;let{observer:i}=this.view;i.ignore(()=>{if(e||t.length){let i=this.tile,n=new kn(this.view,i,this.blockWrappers,this.decorations,this.dynamicDecorationMap);e&&rn.get(e.text)&&n.cache.reused.set(rn.get(e.text),2),this.tile=n.run(t,e),Dn(i,n.cache.reused)}this.tile.dom.style.height=this.view.viewState.contentHeight/this.view.scaleY+"px",this.tile.dom.style.flexBasis=this.minWidth?this.minWidth+"px":"";let n=ye.chrome||ye.ios?{node:i.selectionRange.focusNode,written:!1}:void 0;this.tile.sync(n),!n||!n.written&&i.selectionRange.focusNode==n.node&&this.tile.dom.contains(n.node)||(this.forceSelection=!0),this.tile.dom.style.height=""});let n=[];if(this.view.viewport.from||this.view.viewport.to-1)&&We(i,this.view.observer.selectionRange)&&!(n&&i.contains(n));if(!(s||e||r))return;let o=this.forceSelection;this.forceSelection=!1;let l,a,h=this.view.state.selection.main;if(h.empty?a=l=this.inlineDOMNearPos(h.anchor,h.assoc||1):(a=this.inlineDOMNearPos(h.head,h.head==h.from?1:-1),l=this.inlineDOMNearPos(h.anchor,h.anchor==h.from?1:-1)),ye.gecko&&h.empty&&!this.hasComposition&&(1==(c=l).node.nodeType&&c.node.firstChild&&(0==c.offset||"false"==c.node.childNodes[c.offset-1].contentEditable)&&(c.offset==c.node.childNodes.length||"false"==c.node.childNodes[c.offset].contentEditable))){let t=document.createTextNode("");this.view.observer.ignore(()=>l.node.insertBefore(t,l.node.childNodes[l.offset]||null)),l=a=new ni(t,0),o=!0}var c;let u=this.view.observer.selectionRange;!o&&u.focusNode&&(Ve(l.node,l.offset,u.anchorNode,u.anchorOffset)&&Ve(a.node,a.offset,u.focusNode,u.focusOffset)||this.suppressWidgetCursorChange(u,h))||(this.view.observer.ignore(()=>{ye.android&&ye.chrome&&i.contains(u.focusNode)&&function(t,e){for(let i=t;i&&i!=e;i=i.assignedSlot||i.parentNode)if(1==i.nodeType&&"false"==i.contentEditable)return!0;return!1}(u.focusNode,i)&&(i.blur(),i.focus({preventScroll:!0}));let t=Ie(this.view.root);if(t)if(h.empty){if(ye.gecko){let t=(e=l.node,s=l.offset,1!=e.nodeType?0:(s&&"false"==e.childNodes[s-1].contentEditable?1:0)|(sh.head&&([l,a]=[a,l]),e.setEnd(a.node,a.offset),e.setStart(l.node,l.offset),t.removeAllRanges(),t.addRange(e)}else;var e,s;r&&this.view.root.activeElement==i&&(i.blur(),n&&n.focus())}),this.view.observer.setSelectionRange(l,a)),this.impreciseAnchor=l.precise?null:new ni(u.anchorNode,u.anchorOffset),this.impreciseHead=a.precise?null:new ni(u.focusNode,u.focusOffset)}suppressWidgetCursorChange(t,e){return this.hasComposition&&e.empty&&Ve(t.focusNode,t.focusOffset,t.anchorNode,t.anchorOffset)&&this.posFromDOM(t.focusNode,t.focusOffset)==e.head}enforceCursorAssoc(){if(this.hasComposition)return;let{view:t}=this,e=t.state.selection.main,i=Ie(t.root),{anchorNode:n,anchorOffset:s}=t.observer.selectionRange;if(!(i&&e.empty&&e.assoc&&i.modify))return;let r=this.lineAt(e.head,e.assoc);if(!r)return;let o=r.posAtStart;if(e.head==o||e.head==o+r.length)return;let l=this.coordsAt(e.head,-1),a=this.coordsAt(e.head,1);if(!l||!a||l.bottom>a.top)return;let h=this.domAtPos(e.head+e.assoc,e.assoc);i.collapse(h.node,h.offset),i.modify("move",e.assoc<0?"forward":"backward","lineboundary"),t.observer.readSelectionRange();let c=t.observer.selectionRange;t.docView.posFromDOM(c.anchorNode,c.anchorOffset)!=e.from&&i.collapse(n,s)}posFromDOM(t,e){let i=this.tile.nearest(t);if(!i)return 2&this.tile.dom.compareDocumentPosition(t)?0:this.view.state.doc.length;let n=i.posAtStart;if(!i.isComposite())return i.isText()?t==i.dom?n+e:n+(e?i.length:0):n;{let s;if(t==i.dom)s=i.dom.childNodes[e];else{let n=0==_e(t)?0:0==e?-1:1;for(;;){let e=t.parentNode;if(e==i.dom)break;0==n&&e.firstChild!=e.lastChild&&(n=t==e.firstChild?-1:1),t=e}s=n<0?t:t.nextSibling}if(s==i.dom.firstChild)return n;for(;s&&!rn.get(s);)s=s.nextSibling;if(!s)return n+i.length;for(let t=0,e=n;;t++){let n=i.children[t];if(n.dom==s)return e;e+=n.length+n.breakAfter}}}domAtPos(t,e){let{tile:i,offset:n}=this.tile.resolveBlock(t,e);return i.isWidget()?i.domPosFor(n,e):i.domIn(n,e)}inlineDOMNearPos(t,e){let i,n,s=-1,r=!1,o=-1,l=!1;return this.tile.blockTiles((e,a)=>{if(e.isWidget()){if(32&e.flags&&a>=t)return!0;16&e.flags&&(r=!0)}else{let h=a+e.length;if(a<=t&&(i=e,s=t-a,r=h=t&&!n&&(n=e,o=t-a,l=a>t),a>t&&n)return!0}}),i||n?(r&&n?i=null:l&&i&&(n=null),i&&e<0||!n?i.domIn(s,e):n.domIn(o,e)):this.domAtPos(t,e)}coordsAt(t,e,i){let{tile:n,offset:s}=this.tile.resolveBlock(t,e);return n.isWidget()?n.widget instanceof En?null:n.coordsInWidget(s,e,!0):n.coordsIn(s,e,i)}lineAt(t,e){let{tile:i}=this.tile.resolveBlock(t,e);return i.isLine()?i:null}coordsForChar(t){let{tile:e,offset:i}=this.tile.resolveBlock(t,1);if(!e.isLine())return null;return function t(e,i){if(e.isComposite())for(let n of e.children){if(n.length>=i){let e=t(n,i);if(e)return e}if((i-=n.length)<0)break}else if(e.isText()&&iMath.max(this.view.scrollDOM.clientWidth,this.minWidth)+1,o=-1,l=this.view.textDirection==si.LTR,a=0,h=(t,c,u)=>{for(let f=0;fn);f++){let n=t.children[f],d=c+n.length,p=n.dom.getBoundingClientRect(),{height:m}=p;if(u&&!f&&(a+=p.top-u.top),n instanceof hn)d>i&&h(n,c,p);else if(c>=i&&(a>0&&e.push(-a),e.push(m+a),a=0,r)){let t=n.dom.lastChild,e=t?He(t):[];if(e.length){let t=e[e.length-1],i=l?t.right-p.left:p.right-t.left;i>o&&(o=i,this.minWidth=s,this.minWidthFrom=c,this.minWidthTo=d)}}u&&f==t.children.length-1&&(a+=u.bottom-p.bottom),c=d+n.breakAfter}};return h(this.tile,0,null),e}textDirectionAt(t){let{tile:e}=this.tile.resolveBlock(t,1);return"rtl"==getComputedStyle(e.dom).direction?si.RTL:si.LTR}measureTextSize(){let t=this.tile.blockTiles(t=>{if(t.isLine()&&t.children.length&&t.length<=20){let e,i=0;for(let n of t.children){if(!n.isText()||/[^ -~]/.test(n.text))return;let t=He(n.dom);if(1!=t.length)return;i+=t[0].width,e=t[0].height}if(i)return{lineHeight:t.dom.getBoundingClientRect().height,charWidth:i/t.length,textHeight:e}}});if(t)return t;let e,i,n,s=document.createElement("div");return s.className="cm-line",s.style.width="99999px",s.style.position="absolute",s.textContent="abc def ghi jkl mno pqr stu",this.view.observer.ignore(()=>{this.tile.dom.appendChild(s);let t=He(s.firstChild)[0];e=s.getBoundingClientRect().height,i=t&&t.width?t.width/27:7,n=t&&t.height?t.height:e,s.remove()}),{lineHeight:e,charWidth:i,textHeight:n}}computeBlockGapDeco(){let t=[],e=this.view.viewState;for(let i=0,n=0;;n++){let s=n==e.viewports.length?null:e.viewports[n],r=s?s.from-1:this.view.state.doc.length;if(r>i){let n=(e.lineBlockAt(r).bottom-e.lineBlockAt(i).top)/this.view.scaleY;t.push(Te.replace({widget:new En(n),block:!0,inclusive:!0,isBlockGap:!0}).range(i,r))}if(!s)break;i=s.to+1}return Te.set(t)}updateDeco(){let t=1,e=this.view.state.facet($i).map(e=>(this.dynamicDecorationMap[t++]="function"==typeof e)?e(this.view):e),i=!1,n=this.view.state.facet(ji).map((t,e)=>{let n="function"==typeof t;return n&&(i=!0),n?t(this.view):t});for(n.length&&(this.dynamicDecorationMap[t++]=i,e.push(It.join(n))),this.decorations=[this.editContextFormatting,...e,this.computeBlockGapDeco(),this.view.viewState.lineGapDeco];t"function"==typeof t?t(this.view):t)}scrollIntoView(t){if(t.isSnapshot){let e=this.view.viewState.lineBlockAt(t.range.head);return this.view.scrollDOM.scrollTop=e.top-t.yMargin,void(this.view.scrollDOM.scrollLeft=t.xMargin)}for(let e of this.view.state.facet(Li))try{if(e(this.view,t.range,t))return!0}catch(t){Hi(this.view.state,t,"scroll handler")}let e,{range:i}=t,n=this.coordsAt(i.head,i.assoc||(i.head>i.anchor?-1:1));if(!n)return;!i.empty&&(e=this.coordsAt(i.anchor,i.anchor>i.head?-1:1))&&(n={left:Math.min(n.left,e.left),top:Math.min(n.top,e.top),right:Math.max(n.right,e.right),bottom:Math.max(n.bottom,e.bottom)});let s=Zi(this.view),r={left:n.left-s.left,top:n.top-s.top,right:n.right+s.right,bottom:n.bottom+s.bottom},{offsetWidth:o,offsetHeight:l}=this.view.scrollDOM;if(function(t,e,i,n,s,r,o,l){let a=t.ownerDocument,h=a.defaultView||window;for(let c=t,u=!1;c&&!u;)if(1==c.nodeType){let t,f=c==a.body,d=1,p=1;if(f)t=Qe(h);else{if(/^(fixed|sticky)$/.test(getComputedStyle(c).position)&&(u=!0),c.scrollHeight<=c.clientHeight&&c.scrollWidth<=c.clientWidth){c=c.assignedSlot||c.parentNode;continue}let e=c.getBoundingClientRect();({scaleX:d,scaleY:p}=$e(c,e)),t={left:e.left,right:e.left+c.clientWidth*d,top:e.top,bottom:e.top+c.clientHeight*p}}let m=0,g=0;if("nearest"==s)e.top0&&e.bottom>t.bottom+g&&(g=e.bottom-t.bottom+o)):e.bottom>t.bottom-o&&(g=e.bottom-t.bottom+o,i<0&&e.top-g0&&e.right>t.right+m&&(m=e.right-t.right+r)):e.right>t.right-r&&(m=e.right-t.right+r,i<0&&e.leftt.bottom||e.leftt.right)&&(e={left:Math.max(e.left,t.left),right:Math.min(e.right,t.right),top:Math.max(e.top,t.top),bottom:Math.min(e.bottom,t.bottom)}),c=c.assignedSlot||c.parentNode}else{if(11!=c.nodeType)break;c=c.host}}(this.view.scrollDOM,r,i.head1&&(n.top>window.pageYOffset+window.visualViewport.offsetTop+window.visualViewport.height||n.bottomt.isWidget()||t.children.some(e);return e(this.tile.resolveBlock(t,1).tile)}destroy(){Dn(this.tile)}}function Dn(t,e){let i=null==e?void 0:e.get(t);if(1!=i){null==i&&t.destroy();for(let i of t.children)Dn(i,e)}}function Rn(t,e){let i=t.observer.selectionRange;if(!i.focusNode)return null;let n=ei(i.focusNode,i.focusOffset),s=ii(i.focusNode,i.focusOffset),r=n||s;if(s&&n&&s.node!=n.node){let e=rn.get(s.node);if(!e||e.isText()&&e.text!=s.node.nodeValue)r=s;else if(t.docView.lastCompositionAfterCursor){let t=rn.get(n.node);!t||t.isText()&&t.text!=n.node.nodeValue||(r=s)}}if(t.docView.lastCompositionAfterCursor=r!=n,!r)return null;let o=e-r.offset;return{from:o,to:o+r.node.nodeValue.length,node:r.node}}let Pn=class{constructor(){this.changes=[]}compareRange(t,e){Ee(t,e,this.changes)}comparePoint(t,e){Ee(t,e,this.changes)}boundChange(t){Ee(t,t,this.changes)}};class Bn{constructor(){this.changes=[]}compareRange(t,e){Ee(t,e,this.changes)}comparePoint(){}boundChange(t){Ee(t,t,this.changes)}}class En extends Me{constructor(t){super(),this.height=t}toDOM(){let t=document.createElement("div");return t.className="cm-gap",this.updateDOM(t),t}eq(t){return t.height==this.height}updateDOM(t){return t.style.height=this.height+"px",!0}get editable(){return!0}get estimatedHeight(){return this.height}ignoreEvent(){return!1}}function Ln(t,e,i){let n=t.lineBlockAt(e);if(Array.isArray(n.type)){let t;for(let s of n.type){if(s.from>e)break;if(!(s.toe)return s;t&&(s.type!=Oe.Text||t.type==s.type&&!(i<0?s.frome))||(t=s)}}return t||n}return n}function In(t,e,i,n){let s=t.state.doc.lineAt(e.head),r=t.bidiSpans(s),o=t.textDirectionAt(s.from);for(let l=e,a=null;;){let e=xi(s,r,o,l,i),h=yi;if(!e){if(s.number==(i?t.state.doc.lines:1))return l;h="\n",s=t.state.doc.line(s.number+(i?1:-1)),r=t.bidiSpans(s),e=t.visualLineSide(s,!i)}if(a){if(!a(h))return l}else{if(!n)return e;a=n(h)}l=e}}function Nn(t,e,i){for(;;){let n=0;for(let s of t)s.between(e-1,e+1,(t,s,r)=>{if(e>t&&ee(t)),i.from,e.head>i.from?-1:1);return n==i.from?i:W.cursor(n,nt.viewState.docHeight)return new Vn(t.state.doc.length,-1);if(s=t.elementAtHeight(h),null==n)break;if(s.type==Oe.Text){if(n<0?s.tot.viewport.to)break;let e=t.docView.coordsAt(n<0?s.from:s.to,n>0?-1:1);if(e&&(n<0?e.top<=h+o:e.bottom>=h+o))break}let e=t.viewState.heightOracle.textHeight/2;h=n>0?s.bottom+e:s.top-e}if(t.viewport.from>=s.to||t.viewport.to<=s.from){if(i)return null;if(s.type==Oe.Text){let e=function(t,e,i,n,s){let r=Math.round((n-e.left)*t.defaultCharacterWidth);if(t.lineWrapping&&i.height>1.5*t.defaultLineHeight){let e=t.viewState.heightOracle.textHeight;r+=Math.floor((s-i.top-.5*(t.defaultLineHeight-e))/e)*t.viewState.heightOracle.lineLength}let o=t.state.sliceDoc(i.from,i.to);return i.from+jt(o,r,t.state.tabSize)}(t,r,s,l,a);return new Vn(e,e==s.from?1:-1)}}if(s.type!=Oe.Text)return h<(s.top+s.bottom)/2?new Vn(s.from,1):new Vn(s.to,-1);let c=t.docView.lineAt(s.from,2);return c&&c.length==s.length||(c=t.docView.lineAt(s.from,-2)),new Fn(t,l,a,t.textDirectionAt(s.from)).scanTile(c,s.from)}class Fn{constructor(t,e,i,n){this.view=t,this.x=e,this.y=i,this.baseDir=n,this.line=null,this.spans=null}bidiSpansAt(t){return(!this.line||this.line.from>t||this.line.to1||i.length&&(i[0].level!=this.baseDir||i[0].to+n.from>1;e:if(a.has(f)){let t=o+Math.floor(Math.random()*i);for(let e=0;e1)){if(i.bottomthis.y)(!s||s.top>i.top)&&(s=i),a=-1;else{let t=i.left>this.x?this.x-i.left:i.right(i+i+o)/3)return this.y=n.bottom-1,this.scan(t,e,!0);if(s&&s.top<(i+o+o)/3)return this.y=s.top+1,this.scan(t,e,!0)}let f=(h?this.dirAt(t[c],1):this.baseDir)==si.LTR;return{i:c,after:this.x>(r.left+r.right)/2==f}}scanText(t,e){let i=[];for(let n=0;n{let s=i[n]-e,r=i[n+1]-e;return Je(t.dom,s,r).getClientRects()});return n.after?new Vn(i[n.i+1],-1):new Vn(i[n.i],1)}scanTile(t,e){if(!t.length)return new Vn(e,1);if(1==t.children.length){let i=t.children[0];if(i.isText())return this.scanText(i,e);if(i.isComposite())return this.scanTile(i,e)}let i=[e];for(let n=0,s=e;n{let i=t.children[e];return 48&i.flags?null:(1==i.dom.nodeType?i.dom:Je(i.dom,0,i.length)).getClientRects()}),s=t.children[n.i],r=i[n.i];return s.isText()?this.scanText(s,r):s.isComposite()?this.scanTile(s,r):n.after?new Vn(i[n.i+1],-1):new Vn(r,1)}}const qn="￿";class _n{constructor(t,e){this.points=t,this.view=e,this.text="",this.lineSeparator=e.state.facet(Tt.lineSeparator)}append(t){this.text+=t}lineBreak(){this.text+=qn}readRange(t,e){if(!t)return this;let i=t.parentNode;for(let n=t;;){this.findPointBefore(i,n);let t=this.text.length;this.readNode(n);let s=rn.get(n),r=n.nextSibling;if(r==e){(null==s?void 0:s.breakAfter)&&!r&&i!=this.view.contentDOM&&this.lineBreak();break}let o=rn.get(r);(s&&o?s.breakAfter:(s?s.breakAfter:Fe(n))||Fe(r)&&("BR"!=n.nodeName||(null==s?void 0:s.isWidget()))&&this.text.length>t)&&!Qn(r,e)&&this.lineBreak(),n=r}return this.findPointBefore(i,e),this}readTextNode(t){let e=t.nodeValue;for(let i of this.points)i.node==t&&(i.pos=this.text.length+Math.min(i.offset,e.length));for(let i=0,n=this.lineSeparator?null:/\r\n?|\n/g;;){let s,r=-1,o=1;if(this.lineSeparator?(r=e.indexOf(this.lineSeparator,i),o=this.lineSeparator.length):(s=n.exec(e))&&(r=s.index,o=s[0].length),this.append(e.slice(i,r<0?e.length:r)),r<0)break;if(this.lineBreak(),o>1)for(let e of this.points)e.node==t&&e.pos>this.text.length&&(e.pos-=o-1);i=r+o}}readNode(t){let e=rn.get(t),i=e&&e.overrideDOMText;if(null!=i){this.findPointInside(t,i.length);for(let t=i.iter();!t.next().done;)t.lineBreak?this.lineBreak():this.append(t.value)}else 3==t.nodeType?this.readTextNode(t):"BR"==t.nodeName?t.nextSibling&&this.lineBreak():1==t.nodeType&&this.readRange(t.firstChild,null)}findPointBefore(t,e){for(let i of this.points)i.node==t&&t.childNodes[i.offset]==e&&(i.pos=this.text.length)}findPointInside(t,e){for(let i of this.points)(3==t.nodeType?i.node==t:t.contains(i.node))&&(i.pos=this.text.length+(Un(t,i.node,i.offset)?e:0))}}function Un(t,e,i){for(;;){if(!e||i<_e(e))return!1;if(e==t)return!0;i=ze(e)+1,e=e.parentNode}}function Qn(t,e){let i;for(;t!=e&&t;t=t.nextSibling){let e=rn.get(t);if(!(null==e?void 0:e.isWidget()))return!1;e&&(i||(i=[])).push(e)}if(i)for(let t of i){let e=t.overrideDOMText;if(null==e?void 0:e.length)return!1}return!0}class $n{constructor(t,e){this.node=t,this.offset=e,this.pos=-1}}class Kn{constructor(t,e,i,n){this.typeOver=n,this.bounds=null,this.text="",this.domChanged=e>-1;let{impreciseHead:s,impreciseAnchor:r}=t.docView,o=t.state.selection;if(t.state.readOnly&&e>-1)this.newSel=null;else if(e>-1&&(this.bounds=jn(t.docView.tile,e,i,0))){let e=s||r?[]:function(t){let e=[];if(t.root.activeElement!=t.contentDOM)return e;let{anchorNode:i,anchorOffset:n,focusNode:s,focusOffset:r}=t.observer.selectionRange;i&&(e.push(new $n(i,n)),s==i&&r==n||e.push(new $n(s,r)));return e}(t),i=new _n(e,t);i.readRange(this.bounds.startDOM,this.bounds.endDOM),this.text=i.text,this.newSel=function(t,e){if(0==t.length)return null;let i=t[0].pos,n=2==t.length?t[1].pos:i;return i>-1&&n>-1?W.single(i+e,n+e):null}(e,this.bounds.from)}else{let e=t.observer.selectionRange,i=s&&s.node==e.focusNode&&s.offset==e.focusOffset||!Ne(t.contentDOM,e.focusNode)?o.main.head:t.docView.posFromDOM(e.focusNode,e.focusOffset),n=r&&r.node==e.anchorNode&&r.offset==e.anchorOffset||!Ne(t.contentDOM,e.anchorNode)?o.main.anchor:t.docView.posFromDOM(e.anchorNode,e.anchorOffset),l=t.viewport;if((ye.ios||ye.chrome)&&i!=n&&Math.min(i,n)<=o.main.from&&Math.max(i,n)>=o.main.to&&(l.from>0||l.to-1&&o.ranges.length>1)this.newSel=o.replaceRange(W.range(n,i));else if(t.lineWrapping&&n==i&&(!o.main.empty||o.main.head!=i)&&t.inputState.lastTouchTime>Date.now()-100){let e=t.coordsAtPos(i,-1),n=0;e&&(n=t.inputState.lastTouchY<=e.bottom?-1:1),this.newSel=W.create([W.cursor(i,n)])}else this.newSel=W.single(n,i)}}}function jn(t,e,i,n){if(t.isComposite()){let s=-1,r=-1,o=-1,l=-1;for(let a=0,h=n,c=n;ai)return jn(n,e,i,h);if(u>=e&&-1==s&&(s=a,r=h),h>i&&n.dom.parentNode==t.dom){o=a,l=c;break}c=u,h=u+n.breakAfter}return{from:r,to:l<0?n+t.length:l,startDOM:(s?t.children[s-1].dom.nextSibling:null)||t.dom.firstChild,endDOM:o=0?t.children[o].dom:null}}return t.isText()?{from:n,to:n+t.length,startDOM:t.dom,endDOM:t.dom.nextSibling}:null}function Xn(t,e){let i,{newSel:n}=e,{state:s}=t,r=s.selection.main,o=t.inputState.lastKeyTime>Date.now()-100?t.inputState.lastKeyCode:-1;if(e.bounds){let{from:t,to:n}=e.bounds,l=r.from,a=null;(8===o||ye.android&&e.text.length=t&&r.to<=n&&(e.typeOver||u!=e.text)&&u.slice(0,r.from-t)==e.text.slice(0,r.from-t)&&u.slice(r.to-t)==e.text.slice(h=e.text.length-(u.length-(r.to-t)))?i={from:r.from,to:r.to,insert:f.of(e.text.slice(r.from-t,h).split(qn))}:(c=Yn(u,e.text,l-t,a))&&(ye.chrome&&13==o&&c.toB==c.from+2&&e.text.slice(c.from,c.toB)==qn+qn&&c.toB--,i={from:t+c.from,to:t+c.toA,insert:f.of(e.text.slice(c.from,c.toB).split(qn))})}else n&&(!t.hasFocus&&s.facet(Vi)||Jn(n,r))&&(n=null);if(!i&&!n)return!1;if((ye.mac||ye.android)&&i&&i.from==i.to&&i.from==r.head-1&&/^\. ?$/.test(i.insert.toString())&&"off"==t.contentDOM.getAttribute("autocorrect")?(n&&2==i.insert.length&&(n=W.single(n.main.anchor-1,n.main.head-1)),i={from:i.from,to:i.to,insert:f.of([i.insert.toString().replace("."," ")])}):s.doc.lineAt(r.from).toDate.now()-50?i={from:r.from,to:r.to,insert:s.toText(t.inputState.insertingText)}:ye.chrome&&i&&i.from==i.to&&i.from==r.head&&"\n "==i.insert.toString()&&t.lineWrapping&&(n&&(n=W.single(n.main.anchor-1,n.main.head-1)),i={from:r.from,to:r.to,insert:f.of([" "])}),i)return Gn(t,i,n,o);if(n&&!Jn(n,r)){let e=!1,i="select";return t.inputState.lastSelectionTime>Date.now()-50&&("select"==t.inputState.lastSelectionOrigin&&(e=!0),i=t.inputState.lastSelectionOrigin,"select.pointer"==i&&(n=Wn(s.facet(Xi).map(e=>e(t)),n))),t.dispatch({selection:n,scrollIntoView:e,userEvent:i}),!0}return!1}function Gn(t,e,i,n=-1){if(ye.ios&&t.inputState.flushIOSKey(e))return!0;let s=t.state.selection.main;if(ye.android&&(e.to==s.to&&(e.from==s.from||e.from==s.from-1&&" "==t.state.sliceDoc(e.from,s.from))&&1==e.insert.length&&2==e.insert.lines&&Ze(t.contentDOM,"Enter",13)||(e.from==s.from-1&&e.to==s.to&&0==e.insert.length||8==n&&e.insert.lengths.head)&&Ze(t.contentDOM,"Backspace",8)||e.from==s.from&&e.to==s.to+1&&0==e.insert.length&&Ze(t.contentDOM,"Delete",46)))return!0;let r,o=e.insert.toString();t.inputState.composing>=0&&t.inputState.composing++;let l=()=>r||(r=function(t,e,i){let n,s=t.state,r=s.selection.main,o=-1;if(e.from==e.to&&e.fromr.to){let i=e.frome(t)),n,i);e.from==l&&(o=l)}if(o>-1)n={changes:e,selection:W.cursor(e.from+e.insert.length,-1)};else if(e.from>=r.from&&e.to<=r.to&&e.to-e.from>=(r.to-r.from)/3&&(!i||i.main.empty&&i.main.from==e.from+e.insert.length)&&t.inputState.composing<0){let i=r.frome.to?s.sliceDoc(e.to,r.to):"";n=s.replaceSelection(t.state.toText(i+e.insert.sliceString(0,void 0,t.state.lineBreak)+o))}else{let o=s.changes(e),l=i&&i.main.to<=o.newLength?i.main:void 0;if(s.selection.ranges.length>1&&(t.inputState.composing>=0||t.inputState.compositionPendingChange)&&e.to<=r.to+10&&e.to>=r.to-10){let a,h=t.state.sliceDoc(e.from,e.to),c=i&&Rn(t,i.main.head);if(c){let t=e.insert.length-(e.to-e.from);a={from:c.from,to:c.to-t}}else a=t.state.doc.lineAt(r.head);let u=r.to-e.to;n=s.changeByRange(i=>{if(i.from==r.from&&i.to==r.to)return{changes:o,range:l||i.map(o)};let n=i.to-u,c=n-h.length;if(t.state.sliceDoc(c,n)!=h||n>=a.from&&c<=a.to)return{range:i};let f=s.changes({from:c,to:n,insert:e.insert}),d=i.to-r.to;return{changes:f,range:l?W.range(Math.max(0,l.anchor+d),Math.max(0,l.head+d)):i.map(f)}})}else n={changes:o,selection:l&&s.selection.replaceRange(l)}}let l="input.type";(t.composing||t.inputState.compositionPendingChange&&t.inputState.compositionEndedAt>Date.now()-50)&&(t.inputState.compositionPendingChange=!1,l+=".compose",t.inputState.compositionFirstChange&&(l+=".start",t.inputState.compositionFirstChange=!1));return s.update(n,{userEvent:l,scrollIntoView:!0})}(t,e,i));return t.state.facet(Ti).some(i=>i(t,e.from,e.to,o,l))||t.dispatch(l()),!0}function Yn(t,e,i,n){let s=Math.min(t.length,e.length),r=0;for(;r0&&l>0&&t.charCodeAt(o-1)==e.charCodeAt(l-1);)o--,l--;if("end"==n){i-=o+Math.max(0,r-Math.min(o,l))-r}if(o=o?r-i:0,l=r+(l-o),o=r}else if(l=l?r-i:0,o=r+(o-l),l=r}return{from:r,toA:o,toB:l}}function Jn(t,e){return e.head==t.main.head&&e.anchor==t.main.anchor}class Zn{setSelectionOrigin(t){this.lastSelectionOrigin=t,this.lastSelectionTime=Date.now()}constructor(t){this.view=t,this.lastKeyCode=0,this.lastKeyTime=0,this.touchActive=!1,this.lastTouchTime=0,this.lastTouchX=0,this.lastTouchY=0,this.lastFocusTime=0,this.lastScrollTop=0,this.lastScrollLeft=0,this.lastWheelEvent=0,this.pendingIOSKey=void 0,this.lastIOSMomentumScroll=0,this.tabFocusMode=-1,this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastContextMenu=0,this.scrollHandlers=[],this.handlers=Object.create(null),this.composing=-1,this.compositionFirstChange=null,this.compositionEndedAt=0,this.compositionPendingKey=!1,this.compositionPendingChange=!1,this.insertingText="",this.insertingTextAt=0,this.mouseSelection=null,this.draggedContent=null,this.handleEvent=this.handleEvent.bind(this),this.notifiedFocused=t.hasFocus,ye.safari&&t.contentDOM.addEventListener("input",()=>null),ye.gecko&&function(t){Ss.has(t)||(Ss.add(t),t.addEventListener("copy",()=>{}),t.addEventListener("cut",()=>{}))}(t.contentDOM.ownerDocument)}handleEvent(t){(function(t,e){if(!e.bubbles)return!0;if(e.defaultPrevented)return!1;for(let i,n=e.target;n!=t.contentDOM;n=n.parentNode)if(!n||11==n.nodeType||(i=rn.get(n))&&i.isWidget()&&!i.isHidden&&i.widget.ignoreEvent(e))return!1;return!0})(this.view,t)&&!this.ignoreDuringComposition(t)&&("keydown"==t.type&&this.keydown(t)||(0!=this.view.updateState?Promise.resolve().then(()=>this.runHandlers(t.type,t)):this.runHandlers(t.type,t)))}runHandlers(t,e){let i=this.handlers[t];if(i){for(let t of i.observers)t(this.view,e);for(let t of i.handlers){if(e.defaultPrevented)break;if(t(this.view,e)){e.preventDefault();break}}}}ensureHandlers(t){let e=es(t),i=this.handlers,n=this.view.contentDOM;for(let t in e)if("scroll"!=t){let s=!e[t].handlers.length,r=i[t];r&&s!=!r.handlers.length&&(n.removeEventListener(t,this.handleEvent),r=null),r||n.addEventListener(t,this.handleEvent,{passive:s})}for(let t in i)"scroll"==t||e[t]||n.removeEventListener(t,this.handleEvent);this.handlers=e}keydown(t){if(this.lastKeyCode=t.keyCode,this.lastKeyTime=Date.now(),9==t.keyCode&&this.tabFocusMode>-1&&(!this.tabFocusMode||Date.now()<=this.tabFocusMode))return!0;if(this.tabFocusMode>0&&27!=t.keyCode&&ss.indexOf(t.keyCode)<0&&(this.tabFocusMode=-1),ye.android&&ye.chrome&&!t.synthetic&&(13==t.keyCode||8==t.keyCode))return this.view.observer.delayAndroidKey(t.key,t.keyCode),!0;if(ye.ios&&!t.synthetic&&!t.altKey&&!t.metaKey&&(is.some(e=>e.keyCode==t.keyCode)&&!t.ctrlKey||ns.indexOf(t.key)>-1&&t.ctrlKey)){let i={ctrlKey:t.ctrlKey,altKey:t.altKey,metaKey:t.metaKey,shiftKey:t.shiftKey};return i.shiftKey&&ye.ios&&!/^(off|none)$/.test(this.view.contentDOM.autocapitalize)&&((e=this.view.win).visualViewport&&e.visualViewport.height*e.visualViewport.scale/e.document.documentElement.clientHeight<.85)&&(i.shiftKey=!1),this.pendingIOSKey={key:t.key,keyCode:t.keyCode,mods:i},setTimeout(()=>this.flushIOSKey(),250),!0}var e;return 229!=t.keyCode&&this.view.observer.forceFlush(),!1}flushIOSKey(t){let e=this.pendingIOSKey;return!!e&&(!("Enter"==e.key&&t&&t.from0||!!(ye.safari&&!ye.ios&&this.compositionPendingKey&&Date.now()-this.compositionEndedAt<100)&&(this.compositionPendingKey=!1,!0))}startMouseSelection(t){this.mouseSelection&&this.mouseSelection.destroy(),this.mouseSelection=t}update(t){this.view.observer.update(t),this.mouseSelection&&this.mouseSelection.update(t),this.draggedContent&&t.docChanged&&(this.draggedContent=this.draggedContent.map(t.changes)),t.transactions.length&&(this.lastKeyCode=this.lastSelectionTime=0)}destroy(){this.mouseSelection&&this.mouseSelection.destroy()}}function ts(t,e){return(i,n)=>{try{return e.call(t,n,i)}catch(t){Hi(i.state,t)}}}function es(t){let e=Object.create(null);function i(t){return e[t]||(e[t]={observers:[],handlers:[]})}for(let e of t){let t=e.spec,n=t&&t.plugin.domEventHandlers,s=t&&t.plugin.domEventObservers;if(n)for(let t in n){let s=n[t];s&&i(t).handlers.push(ts(e.value,s))}if(s)for(let t in s){let n=s[t];n&&i(t).observers.push(ts(e.value,n))}}for(let t in ls)i(t).handlers.push(ls[t]);for(let t in as)i(t).observers.push(as[t]);return e}const is=[{key:"Backspace",keyCode:8,inputType:"deleteContentBackward"},{key:"Enter",keyCode:13,inputType:"insertParagraph"},{key:"Enter",keyCode:13,inputType:"insertLineBreak"},{key:"Delete",keyCode:46,inputType:"deleteContentForward"}],ns="dthko",ss=[16,17,18,20,91,92,224,225];function rs(t){return.7*Math.max(0,t)+8}class os{constructor(t,e,i,n){this.view=t,this.startEvent=e,this.style=i,this.mustSelect=n,this.scrollSpeed={x:0,y:0},this.scrolling=-1,this.lastEvent=e,this.scrollParents=Ke(t.contentDOM),this.atoms=t.state.facet(Xi).map(e=>e(t));let s=t.contentDOM.ownerDocument;s.addEventListener("mousemove",this.move=this.move.bind(this)),s.addEventListener("mouseup",this.up=this.up.bind(this)),this.extend=e.shiftKey,this.multiple=t.state.facet(Tt.allowMultipleSelections)&&function(t,e){let i=t.state.facet(Si);return i.length?i[0](e):ye.mac?e.metaKey:e.ctrlKey}(t,e),this.dragging=!(!function(t,e){let{main:i}=t.state.selection;if(i.empty)return!1;let n=Ie(t.root);if(!n||0==n.rangeCount)return!0;let s=n.getRangeAt(0).getClientRects();for(let t=0;t=e.clientX&&i.top<=e.clientY&&i.bottom>=e.clientY)return!0}return!1}(t,e)||1!=vs(e))&&null}start(t){!1===this.dragging&&this.select(t)}move(t){if(0==t.buttons)return this.destroy();if(this.dragging||null==this.dragging&&(e=this.startEvent,i=t,Math.max(Math.abs(e.clientX-i.clientX),Math.abs(e.clientY-i.clientY))<10))return;var e,i;this.select(this.lastEvent=t);let n=0,s=0,r=0,o=0,l=this.view.win.innerWidth,a=this.view.win.innerHeight;this.scrollParents.x&&({left:r,right:l}=this.scrollParents.x.getBoundingClientRect()),this.scrollParents.y&&({top:o,bottom:a}=this.scrollParents.y.getBoundingClientRect());let h=Zi(this.view);t.clientX-h.left<=r+6?n=-rs(r-t.clientX):t.clientX+h.right>=l-6&&(n=rs(t.clientX-l)),t.clientY-h.top<=o+6?s=-rs(o-t.clientY):t.clientY+h.bottom>=a-6&&(s=rs(t.clientY-a)),this.setScrollSpeed(n,s)}up(t){null==this.dragging&&this.select(this.lastEvent),this.dragging||t.preventDefault(),this.destroy()}destroy(){this.setScrollSpeed(0,0);let t=this.view.contentDOM.ownerDocument;t.removeEventListener("mousemove",this.move),t.removeEventListener("mouseup",this.up),this.view.inputState.mouseSelection=this.view.inputState.draggedContent=null}setScrollSpeed(t,e){this.scrollSpeed={x:t,y:e},t||e?this.scrolling<0&&(this.scrolling=setInterval(()=>this.scroll(),50)):this.scrolling>-1&&(clearInterval(this.scrolling),this.scrolling=-1)}scroll(){let{x:t,y:e}=this.scrollSpeed;t&&this.scrollParents.x&&(this.scrollParents.x.scrollLeft+=t,t=0),e&&this.scrollParents.y&&(this.scrollParents.y.scrollTop+=e,e=0),(t||e)&&this.view.win.scrollBy(t,e),!1===this.dragging&&this.select(this.lastEvent)}select(t){let{view:e}=this,i=Wn(this.atoms,this.style.get(t,this.extend,this.multiple));!this.mustSelect&&i.eq(e.state.selection,!1===this.dragging)||this.view.dispatch({selection:i,userEvent:"select.pointer"}),this.mustSelect=!1}update(t){t.transactions.some(t=>t.isUserEvent("input.type"))?this.destroy():this.style.update(t)&&setTimeout(()=>this.select(this.lastEvent),20)}}const ls=Object.create(null),as=Object.create(null),hs=ye.ie&&ye.ie_version<15||ye.ios&&ye.webkit_version<604;function cs(t,e,i){for(let n of t.facet(e))i=n(i,t);return i}function us(t,e){e=cs(t.state,Ri,e);let i,{state:n}=t,s=1,r=n.toText(e),o=r.lines==n.selection.ranges.length;if(null!=bs&&n.selection.ranges.every(t=>t.empty)&&bs==r.toString()){let t=-1;i=n.changeByRange(i=>{let l=n.doc.lineAt(i.from);if(l.from==t)return{range:i};t=l.from;let a=n.toText((o?r.line(s++).text:e)+n.lineBreak);return{changes:{from:l.from,insert:a},range:W.cursor(i.from+a.length)}})}else i=o?n.changeByRange(t=>{let e=r.line(s++);return{changes:{from:t.from,to:t.to,insert:e.text},range:W.cursor(t.from+e.length)}}):n.replaceSelection(r);t.dispatch(i,{userEvent:"input.paste",scrollIntoView:!0})}function fs(t,e,i,n){if(1==n)return W.cursor(e,i);if(2==n)return function(t,e,i=1){let n=t.charCategorizer(e),s=t.doc.lineAt(e),r=e-s.from;if(0==s.length)return W.cursor(e);0==r?i=1:r==s.length&&(i=-1);let o=r,l=r;i<0?o=k(s.text,r,!1):l=k(s.text,r);let a=n(s.text.slice(o,l));for(;o>0;){let t=k(s.text,o,!1);if(n(s.text.slice(t,o))!=a)break;o=t}for(;l{let e=t.inputState;e.lastScrollTop=t.scrollDOM.scrollTop,e.lastScrollLeft=t.scrollDOM.scrollLeft,ye.ios&&!e.touchActive&&(e.lastIOSMomentumScroll=Date.now())},as.wheel=as.mousewheel=t=>{t.inputState.lastWheelEvent=Date.now()},ls.keydown=(t,e)=>(t.inputState.setSelectionOrigin("select"),27==e.keyCode&&0!=t.inputState.tabFocusMode&&(t.inputState.tabFocusMode=Date.now()+2e3),!1),as.touchstart=(t,e)=>{let i=t.inputState,n=e.targetTouches[0];i.touchActive=!0,i.lastTouchTime=Date.now(),n&&(i.lastTouchX=n.clientX,i.lastTouchY=n.clientY),i.setSelectionOrigin("select.pointer")},as.touchmove=t=>{t.inputState.setSelectionOrigin("select.pointer")},as.touchend=(t,e)=>{t.inputState.touchActive=!1},ls.mousedown=(t,e)=>{if(t.observer.flush(),t.inputState.lastTouchTime>Date.now()-2e3)return!1;let i=null;for(let n of t.state.facet(Ai))if(i=n(t,e),i)break;if(i||0!=e.button||(i=function(t,e){let i=t.posAndSideAtCoords({x:e.clientX,y:e.clientY},!1),n=vs(e),s=t.state.selection;return{update(t){t.docChanged&&(i.pos=t.changes.mapPos(i.pos),s=s.map(t.changes))},get(e,r,o){let l,a=t.posAndSideAtCoords({x:e.clientX,y:e.clientY},!1),h=fs(t,a.pos,a.assoc,n);if(i.pos!=a.pos&&!r){let e=fs(t,i.pos,i.assoc,n),s=Math.min(e.from,h.from),r=Math.max(e.to,h.to);h=s1&&(l=function(t,e){for(let i=0;i=e)return W.create(t.ranges.slice(0,i).concat(t.ranges.slice(i+1)),t.mainIndex==i?0:t.mainIndex-(t.mainIndex>i?1:0))}return null}(s,a.pos))?l:o?s.addRange(h):W.create([h])}}}(t,e)),i){let n=!t.hasFocus;t.inputState.startMouseSelection(new os(t,e,i,n)),n&&t.observer.ignore(()=>{Ye(t.contentDOM);let e=t.root.activeElement;e&&!e.contains(t.contentDOM)&&e.blur()});let s=t.inputState.mouseSelection;if(s)return s.start(e),!1===s.dragging}else t.inputState.setSelectionOrigin("select.pointer");return!1};const ds=ye.ie&&ye.ie_version<=11;let ps=null,ms=0,gs=0;function vs(t){if(!ds)return t.detail;let e=ps,i=gs;return ps=t,gs=Date.now(),ms=!e||i>Date.now()-400&&Math.abs(e.clientX-t.clientX)<2&&Math.abs(e.clientY-t.clientY)<2?(ms+1)%3:1}function ws(t,e,i,n){if(!(i=cs(t.state,Ri,i)))return;let s=t.posAtCoords({x:e.clientX,y:e.clientY},!1),{draggedContent:r}=t.inputState,o=n&&r&&function(t,e){let i=t.state.facet(Ci);return i.length?i[0](e):ye.mac?!e.altKey:!e.ctrlKey}(t,e)?{from:r.from,to:r.to}:null,l={from:s,insert:i},a=t.state.changes(o?[o,l]:l);t.focus(),t.dispatch({changes:a,selection:{anchor:a.mapPos(s,-1),head:a.mapPos(s,1)},userEvent:o?"move.drop":"input.drop"}),t.inputState.draggedContent=null}ls.dragstart=(t,e)=>{let{selection:{main:i}}=t.state;if(e.target.draggable){let n=t.docView.tile.nearest(e.target);if(n&&n.isWidget()){let t=n.posAtStart,e=t+n.length;(t>=i.to||e<=i.from)&&(i=W.undirectionalRange(t,e))}}let{inputState:n}=t;return n.mouseSelection&&(n.mouseSelection.dragging=!0),n.draggedContent=i,e.dataTransfer&&(e.dataTransfer.setData("Text",cs(t.state,Pi,t.state.sliceDoc(i.from,i.to))),e.dataTransfer.effectAllowed="copyMove"),!1},ls.dragend=t=>(t.inputState.draggedContent=null,!1),ls.drop=(t,e)=>{if(!e.dataTransfer)return!1;if(t.state.readOnly)return!0;let i=e.dataTransfer.files;if(i&&i.length){let n=Array(i.length),s=0,r=()=>{++s==i.length&&ws(t,e,n.filter(t=>null!=t).join(t.state.lineBreak),!1)};for(let t=0;t{/[\x00-\x08\x0e-\x1f]{2}/.test(e.result)||(n[t]=e.result),r()},e.readAsText(i[t])}return!0}{let i=e.dataTransfer.getData("Text");if(i)return ws(t,e,i,!0),!0}return!1},ls.paste=(t,e)=>{if(t.state.readOnly)return!0;t.observer.flush();let i=hs?null:e.clipboardData;return i?(us(t,i.getData("text/plain")||i.getData("text/uri-list")),!0):(function(t){let e=t.dom.parentNode;if(!e)return;let i=e.appendChild(document.createElement("textarea"));i.style.cssText="position: fixed; left: -10000px; top: 10px",i.focus(),setTimeout(()=>{t.focus(),i.remove(),us(t,i.value)},50)}(t),!1)};let bs=null;ls.copy=ls.cut=(t,e)=>{if(!We(t.contentDOM,t.observer.selectionRange))return!1;let{text:i,ranges:n,linewise:s}=function(t){let e=[],i=[],n=!1;for(let n of t.selection.ranges)n.empty||(e.push(t.sliceDoc(n.from,n.to)),i.push(n));if(!e.length){let s=-1;for(let{from:n}of t.selection.ranges){let r=t.doc.lineAt(n);r.number>s&&(e.push(r.text),i.push({from:r.from,to:Math.min(t.doc.length,r.to+1)})),s=r.number}n=!0}return{text:cs(t,Pi,e.join(t.lineBreak)),ranges:i,linewise:n}}(t.state);if(!i&&!s)return!1;bs=s?i:null,"cut"!=e.type||t.state.readOnly||t.dispatch({changes:n,scrollIntoView:!0,userEvent:"delete.cut"});let r=hs?null:e.clipboardData;return r?(r.clearData(),r.setData("text/plain",i),!0):(function(t,e){let i=t.dom.parentNode;if(!i)return;let n=i.appendChild(document.createElement("textarea"));n.style.cssText="position: fixed; left: -10000px; top: 10px",n.value=e,n.focus(),n.selectionEnd=e.length,n.selectionStart=0,setTimeout(()=>{n.remove(),t.focus()},50)}(t,i),!1)};const ys=dt.define();function xs(t,e){let i=[];for(let n of t.facet(Di)){let s=n(t,e);s&&i.push(s)}return i.length?t.update({effects:i,annotations:ys.of(!0)}):null}function ks(t){setTimeout(()=>{let e=t.hasFocus;if(e!=t.inputState.notifiedFocused){let i=xs(t.state,e);i?t.dispatch(i):t.update([])}},10)}as.focus=t=>{t.inputState.lastFocusTime=Date.now(),t.scrollDOM.scrollTop||!t.inputState.lastScrollTop&&!t.inputState.lastScrollLeft||(t.scrollDOM.scrollTop=t.inputState.lastScrollTop,t.scrollDOM.scrollLeft=t.inputState.lastScrollLeft),ks(t)},as.blur=t=>{t.observer.clearSelectionRange(),ks(t)},as.compositionstart=as.compositionupdate=t=>{t.observer.editContext||(null==t.inputState.compositionFirstChange&&(t.inputState.compositionFirstChange=!0),t.inputState.composing<0&&(t.inputState.composing=0))},as.compositionend=t=>{t.observer.editContext||(t.inputState.composing=-1,t.inputState.compositionEndedAt=Date.now(),t.inputState.compositionPendingKey=!0,t.inputState.compositionPendingChange=t.observer.pendingRecords().length>0,t.inputState.compositionFirstChange=null,ye.chrome&&ye.android?t.observer.flushSoon():t.inputState.compositionPendingChange?Promise.resolve().then(()=>t.observer.flush()):setTimeout(()=>{t.inputState.composing<0&&t.docView.hasComposition&&t.update([])},50))},as.contextmenu=t=>{t.inputState.lastContextMenu=Date.now()},ls.beforeinput=(t,e)=>{var i,n;if("insertText"!=e.inputType&&"insertCompositionText"!=e.inputType||(t.inputState.insertingText=e.data,t.inputState.insertingTextAt=Date.now()),"insertReplacementText"==e.inputType&&t.observer.editContext){let n=null===(i=e.dataTransfer)||void 0===i?void 0:i.getData("text/plain"),s=e.getTargetRanges();if(n&&s.length){let e=s[0],i=t.posAtDOM(e.startContainer,e.startOffset),r=t.posAtDOM(e.endContainer,e.endOffset);return Gn(t,{from:i,to:r,insert:t.state.toText(n)},null),!0}}let s;if(ye.chrome&&ye.android&&(s=is.find(t=>t.inputType==e.inputType))&&(t.observer.delayAndroidKey(s.key,s.keyCode),"Backspace"==s.key||"Delete"==s.key)){let e=(null===(n=window.visualViewport)||void 0===n?void 0:n.height)||0;setTimeout(()=>{var i;((null===(i=window.visualViewport)||void 0===i?void 0:i.height)||0)>e+10&&t.hasFocus&&(t.contentDOM.blur(),t.focus())},100)}return ye.ios&&"deleteContentForward"==e.inputType&&t.observer.flushSoon(),ye.safari&&"insertText"==e.inputType&&t.inputState.composing>=0&&setTimeout(()=>as.compositionend(t,e),20),!1};const Ss=new Set;const Cs=["pre-wrap","normal","pre-line","break-spaces"];let As=!1;function Ms(){As=!1}class Os{constructor(t){this.lineWrapping=t,this.doc=f.empty,this.heightSamples={},this.lineHeight=14,this.charWidth=7,this.textHeight=14,this.lineLength=30}heightForGap(t,e){let i=this.doc.lineAt(e).number-this.doc.lineAt(t).number+1;return this.lineWrapping&&(i+=Math.max(0,Math.ceil((e-t-i*this.lineLength*.5)/this.lineLength))),this.lineHeight*i}heightForLine(t){if(!this.lineWrapping)return this.lineHeight;return(1+Math.max(0,Math.ceil((t-this.lineLength)/Math.max(1,this.lineLength-5))))*this.lineHeight}setDoc(t){return this.doc=t,this}mustRefreshForWrapping(t){return Cs.indexOf(t)>-1!=this.lineWrapping}mustRefreshForHeights(t){let e=!1;for(let i=0;i-1,l=Math.abs(e-this.lineHeight)>.3||this.lineWrapping!=o;if(this.lineWrapping=o,this.lineHeight=e,this.charWidth=i,this.textHeight=n,this.lineLength=s,l){this.heightSamples={};for(let t=0;t0}set outdated(t){this.flags=(t?2:0)|-3&this.flags}setHeight(t){this.height!=t&&(Math.abs(this.height-t)>Ps&&(As=!0),this.height=t)}replace(t,e,i){return Bs.of(i)}decomposeLeft(t,e){e.push(this)}decomposeRight(t,e){e.push(this)}applyChanges(t,e,i,n){let s=this,r=i.doc;for(let o=n.length-1;o>=0;o--){let{fromA:l,toA:a,fromB:h,toB:c}=n[o],u=s.lineAt(l,Rs.ByPosNoHeight,i.setDoc(e),0,0),f=u.to>=a?u:s.lineAt(a,Rs.ByPosNoHeight,i,0,0);for(c+=f.to-a,a=f.to;o>0&&u.from<=n[o-1].toA;)l=n[o-1].fromA,h=n[o-1].fromB,o--,l2*s){let s=t[e-1];s.break?t.splice(--e,1,s.left,null,s.right):t.splice(--e,1,s.left,s.right),i+=1+s.break,n-=s.size}else{if(!(s>2*n))break;{let e=t[i];e.break?t.splice(i,1,e.left,null,e.right):t.splice(i,1,e.left,e.right),i+=2+e.break,s-=e.size}}else if(n=s&&r(this.lineAt(0,Rs.ByPos,i,n,s))}setMeasuredHeight(t){let e=t.heights[t.index++];e<0?(this.spaceAbove=-e,e=t.heights[t.index++]):this.spaceAbove=0,this.setHeight(e)}updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more&&this.setMeasuredHeight(n),this.outdated=!1,this}toString(){return`block(${this.length})`}}class Ns extends Is{constructor(t,e,i){super(t,e,null),this.collapsed=0,this.widgetHeight=0,this.breaks=0,this.spaceAbove=i}mainBlock(t,e){return new Ds(e,this.length,t+this.spaceAbove,this.height-this.spaceAbove,this.breaks)}replace(t,e,i){let n=i[0];return 1==i.length&&(n instanceof Ns||n instanceof Ws&&4&n.flags)&&Math.abs(this.length-n.length)<10?(n instanceof Ws?n=new Ns(n.length,this.height,this.spaceAbove):n.height=this.height,this.outdated||(n.outdated=!1),n):Bs.of(i)}updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more?this.setMeasuredHeight(n):(i||this.outdated)&&(this.spaceAbove=0,this.setHeight(Math.max(this.widgetHeight,t.heightForLine(this.length-this.collapsed))+this.breaks*t.lineHeight)),this.outdated=!1,this}toString(){return`line(${this.length}${this.collapsed?-this.collapsed:""}${this.widgetHeight?":"+this.widgetHeight:""})`}}class Ws extends Bs{constructor(t){super(t,0)}heightMetrics(t,e){let i,n=t.doc.lineAt(e).number,s=t.doc.lineAt(e+this.length).number,r=s-n+1,o=0;if(t.lineWrapping){let e=Math.min(this.height,t.lineHeight*r);i=e/r,this.length>r+1&&(o=(this.height-e)/(this.length-r-1))}else i=this.height/r;return{firstLine:n,lastLine:s,perLine:i,perChar:o}}blockAt(t,e,i,n){let{firstLine:s,lastLine:r,perLine:o,perChar:l}=this.heightMetrics(e,n);if(e.lineWrapping){let s=n+(t0){let t=i[i.length-1];t instanceof Ws?i[i.length-1]=new Ws(t.length+n):i.push(null,new Ws(n-1))}if(t>0){let e=i[0];e instanceof Ws?i[0]=new Ws(t+e.length):i.unshift(new Ws(t-1),null)}return Bs.of(i)}decomposeLeft(t,e){e.push(new Ws(t-1),null)}decomposeRight(t,e){e.push(null,new Ws(this.length-t-1))}updateHeight(t,e=0,i=!1,n){let s=e+this.length;if(n&&n.from<=e+this.length&&n.more){let i=[],r=Math.max(e,n.from),o=-1;for(n.from>e&&i.push(new Ws(n.from-e-1).updateHeight(t,e));r<=s&&n.more;){let e=t.doc.lineAt(r).length;i.length&&i.push(null);let s=n.heights[n.index++],l=0;s<0&&(l=-s,s=n.heights[n.index++]),-1==o?o=s:Math.abs(s-o)>=Ps&&(o=-2);let a=new Ns(e,s,l);a.outdated=!1,i.push(a),r+=e+1}r<=s&&i.push(null,new Ws(s-r).updateHeight(t,r));let l=Bs.of(i);return(o<0||Math.abs(l.height-this.height)>=Ps||Math.abs(o-this.heightMetrics(t,e).perLine)>=Ps)&&(As=!0),Es(this,l)}return(i||this.outdated)&&(this.setHeight(t.heightForGap(e,e+this.length)),this.outdated=!1),this}toString(){return`gap(${this.length})`}}class Hs extends Bs{constructor(t,e,i){super(t.length+e+i.length,t.height+i.height,e|(t.outdated||i.outdated?2:0)),this.left=t,this.right=i,this.size=t.size+i.size}get break(){return 1&this.flags}blockAt(t,e,i,n){let s=i+this.left.height;return to))return a;let h=e==Rs.ByPosNoHeight?Rs.ByPosNoHeight:Rs.ByPos;return l?a.join(this.right.lineAt(o,h,i,r,o)):this.left.lineAt(o,h,i,n,s).join(a)}forEachLine(t,e,i,n,s,r){let o=n+this.left.height,l=s+this.left.length+this.break;if(this.break)t=l&&this.right.forEachLine(t,e,i,o,l,r);else{let a=this.lineAt(l,Rs.ByPos,i,n,s);t=t&&a.from<=e&&r(a),e>a.to&&this.right.forEachLine(a.to+1,e,i,o,l,r)}}replace(t,e,i){let n=this.left.length+this.break;if(ethis.left.length)return this.balanced(this.left,this.right.replace(t-n,e-n,i));let s=[];t>0&&this.decomposeLeft(t,s);let r=s.length;for(let t of i)s.push(t);if(t>0&&Vs(s,r-1),e=i&&e.push(null)),t>i&&this.right.decomposeLeft(t-i,e)}decomposeRight(t,e){let i=this.left.length,n=i+this.break;if(t>=n)return this.right.decomposeRight(t-n,e);t2*e.size||e.size>2*t.size?Bs.of(this.break?[t,null,e]:[t,e]):(this.left=Es(this.left,t),this.right=Es(this.right,e),this.setHeight(t.height+e.height),this.outdated=t.outdated||e.outdated,this.size=t.size+e.size,this.length=t.length+this.break+e.length,this)}updateHeight(t,e=0,i=!1,n){let{left:s,right:r}=this,o=e+s.length+this.break,l=null;return n&&n.from<=e+s.length&&n.more?l=s=s.updateHeight(t,e,i,n):s.updateHeight(t,e,i),n&&n.from<=o+r.length&&n.more?l=r=r.updateHeight(t,o,i,n):r.updateHeight(t,o,i),l?this.balanced(s,r):(this.height=this.left.height+this.right.height,this.outdated=!1,this)}toString(){return this.left+(this.break?" ":"-")+this.right}}function Vs(t,e){let i,n;null==t[e]&&(i=t[e-1])instanceof Ws&&(n=t[e+1])instanceof Ws&&t.splice(e-1,3,new Ws(i.length+1+n.length))}class zs{constructor(t,e){this.pos=t,this.oracle=e,this.nodes=[],this.lineStart=-1,this.lineEnd=-1,this.covering=null,this.writtenTo=t}get isCovered(){return this.covering&&this.nodes[this.nodes.length-1]==this.covering}span(t,e){if(this.lineStart>-1){let t=Math.min(e,this.lineEnd),i=this.nodes[this.nodes.length-1];i instanceof Ns?i.length+=t-this.pos:(t>this.pos||!this.isCovered)&&this.nodes.push(new Ns(t-this.pos,-1,0)),this.writtenTo=t,e>t&&(this.nodes.push(null),this.writtenTo++,this.lineStart=-1)}this.pos=e}point(t,e,i){if(t=5)&&this.addLineDeco(n,s,r)}else e>t&&this.span(t,e);this.lineEnd>-1&&this.lineEnd-1)return;let{from:t,to:e}=this.oracle.doc.lineAt(this.pos);this.lineStart=t,this.lineEnd=e,this.writtenTot&&this.nodes.push(new Ns(this.pos-t,-1,0)),this.writtenTo=this.pos}blankContent(t,e){let i=new Ws(e-t);return this.oracle.doc.lineAt(t).to==e&&(i.flags|=4),i}ensureLine(){this.enterLine();let t=this.nodes.length?this.nodes[this.nodes.length-1]:null;if(t instanceof Ns)return t;let e=new Ns(0,-1,0);return this.nodes.push(e),e}addBlock(t){this.enterLine();let e=t.deco;e&&e.startSide>0&&!this.isCovered&&this.ensureLine(),this.nodes.push(t),this.writtenTo=this.pos=this.pos+t.length,e&&e.endSide>0&&(this.covering=t)}addLineDeco(t,e,i){let n=this.ensureLine();n.length+=i,n.collapsed+=i,n.widgetHeight=Math.max(n.widgetHeight,t),n.breaks+=e,this.writtenTo=this.pos=this.pos+i}finish(t){let e=0==this.nodes.length?null:this.nodes[this.nodes.length-1];!(this.lineStart>-1)||e instanceof Ns||this.isCovered?(this.writtenToi.clientHeight||i.scrollWidth>i.clientWidth)&&"visible"!=n.overflow){let n=i.getBoundingClientRect();r=Math.max(r,n.left),o=Math.min(o,n.right),l=Math.max(l,n.top),a=Math.min(e==t.parentNode?s.innerHeight:a,n.bottom)}e="absolute"==n.position||"fixed"==n.position?i.offsetParent:i.parentNode}else{if(11!=e.nodeType)break;e=e.host}return{left:r-i.left,right:Math.max(r,o)-i.left,top:l-(i.top+e),bottom:Math.max(l,a)-(i.top+e)}}function _s(t,e){let i=t.getBoundingClientRect();return{left:0,right:i.right-i.left,top:e,bottom:i.bottom-(i.top+e)}}class Us{constructor(t,e,i,n){this.from=t,this.to=e,this.size=i,this.displaySize=n}static same(t,e){if(t.length!=e.length)return!1;for(let i=0;i"function"!=typeof t&&"cm-lineWrapping"==t.class);this.heightOracle=new Os(i),this.stateDeco=Ys(e),this.heightMap=Bs.empty().applyChanges(this.stateDeco,f.empty,this.heightOracle.setDoc(e.doc),[new en(0,0,0,e.doc.length)]);for(let t=0;t<2&&(this.viewport=this.getViewport(0,null),this.updateForViewport());t++);this.updateViewportLines(),this.lineGaps=this.ensureLineGaps([]),this.lineGapDeco=Te.set(this.lineGaps.map(t=>t.draw(this,!1))),this.scrollParent=t.scrollDOM,this.computeVisibleRanges()}updateForViewport(){let t=[this.viewport],{main:e}=this.state.selection;for(let i=0;i<=1;i++){let n=i?e.head:e.anchor;if(!t.some(({from:t,to:e})=>n>=t&&n<=e)){let{from:e,to:i}=this.lineBlockAt(n);t.push(new Ks(e,i))}}return this.viewports=t.sort((t,e)=>t.from-e.from),this.updateScaler()}updateScaler(){let t=this.scaler;return this.scaler=this.heightMap.height<=7e6?Gs:new Js(this.heightOracle,this.heightMap,this.viewports),t.eq(this.scaler)?0:2}updateViewportLines(){this.viewportLines=[],this.heightMap.forEachLine(this.viewport.from,this.viewport.to,this.heightOracle.setDoc(this.state.doc),0,0,t=>{this.viewportLines.push(Zs(t,this.scaler))})}update(t,e=null){this.state=t.state;let i=this.stateDeco;this.stateDeco=Ys(this.state);let n=t.changedRanges,s=en.extendWithRanges(n,function(t,e,i){let n=new Fs;return It.compare(t,e,i,n,0),n.changes}(i,this.stateDeco,t?t.changes:D.empty(this.state.doc.length))),r=this.heightMap.height,o=this.scrolledToBottom?null:this.scrollAnchorAt(this.scrollOffset);Ms(),this.heightMap=this.heightMap.applyChanges(this.stateDeco,t.startState.doc,this.heightOracle.setDoc(this.state.doc),s),(this.heightMap.height!=r||As)&&(t.flags|=2),o?(this.scrollAnchorPos=t.changes.mapPos(o.from,-1),this.scrollAnchorHeight=o.top):(this.scrollAnchorPos=-1,this.scrollAnchorHeight=r);let l=s.length?this.mapViewport(this.viewport,t.changes):this.viewport;(e&&(e.range.headl.to)||!this.viewportIsAppropriate(l))&&(l=this.getViewport(0,e));let a=l.from!=this.viewport.from||l.to!=this.viewport.to;this.viewport=l,t.flags|=this.updateForViewport(),(a||!t.changes.empty||2&t.flags)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps,t.changes))),t.flags|=this.computeVisibleRanges(t.changes),e&&(this.scrollTarget=e),!this.mustEnforceCursorAssoc&&(t.selectionSet||t.focusChanged)&&t.view.lineWrapping&&t.state.selection.main.empty&&t.state.selection.main.assoc&&!t.state.facet(Ei)&&(this.mustEnforceCursorAssoc=!0)}measure(){let{view:t}=this,e=t.contentDOM,i=window.getComputedStyle(e),n=this.heightOracle,s=i.whiteSpace;this.defaultTextDirection="rtl"==i.direction?si.RTL:si.LTR;let r=this.heightOracle.mustRefreshForWrapping(s)||"refresh"===this.mustMeasureContent,o=e.getBoundingClientRect(),l=r||this.mustMeasureContent||this.contentDOMHeight!=o.height;this.contentDOMHeight=o.height,this.mustMeasureContent=!1;let a=0,h=0;if(o.width&&o.height){let{scaleX:t,scaleY:i}=$e(e,o);(t>.005&&Math.abs(this.scaleX-t)>.005||i>.005&&Math.abs(this.scaleY-i)>.005)&&(this.scaleX=t,this.scaleY=i,a|=16,r=l=!0)}let c=(parseInt(i.paddingTop)||0)*this.scaleY,u=(parseInt(i.paddingBottom)||0)*this.scaleY;this.paddingTop==c&&this.paddingBottom==u||(this.paddingTop=c,this.paddingBottom=u,a|=18),this.editorWidth!=t.scrollDOM.clientWidth&&(n.lineWrapping&&(l=!0),this.editorWidth=t.scrollDOM.clientWidth,a|=16);let d=Ke(this.view.contentDOM,!1).y;d!=this.scrollParent&&(this.scrollParent=d,this.scrollAnchorHeight=-1,this.scrollOffset=0);let p=this.getScrollOffset();this.scrollOffset!=p&&(this.scrollAnchorHeight=-1,this.scrollOffset=p),this.scrolledToBottom=ti(this.scrollParent||t.win);let m=(this.printing?_s:qs)(e,this.paddingTop),g=m.top-this.pixelViewport.top,v=m.bottom-this.pixelViewport.bottom;this.pixelViewport=m;let w=this.pixelViewport.bottom>this.pixelViewport.top&&this.pixelViewport.right>this.pixelViewport.left;if(w!=this.inView&&(this.inView=w,w&&(l=!0)),!this.inView&&!this.scrollTarget&&!function(t){let e=t.getBoundingClientRect(),i=t.ownerDocument.defaultView||window;return e.left0&&e.top0}(t.dom))return 0;let b=o.width;if(this.contentDOMWidth==b&&this.editorHeight==t.scrollDOM.clientHeight||(this.contentDOMWidth=o.width,this.editorHeight=t.scrollDOM.clientHeight,a|=16),l){let e=t.docView.measureVisibleLineHeights(this.viewport);if(n.mustRefreshForHeights(e)&&(r=!0),r||n.lineWrapping&&Math.abs(b-this.contentDOMWidth)>n.charWidth){let{lineHeight:i,charWidth:o,textHeight:l}=t.docView.measureTextSize();r=i>0&&n.refresh(s,i,o,l,Math.max(5,b/o),e),r&&(t.docView.minWidth=0,a|=16)}g>0&&v>0?h=Math.max(g,v):g<0&&v<0&&(h=Math.min(g,v)),Ms();for(let i of this.viewports){let s=i.from==this.viewport.from?e:t.docView.measureVisibleLineHeights(i);this.heightMap=(r?Bs.empty().applyChanges(this.stateDeco,f.empty,this.heightOracle,[new en(0,0,0,t.state.doc.length)]):this.heightMap).updateHeight(n,0,r,new Ts(i.from,s))}As&&(a|=2)}let y=!this.viewportIsAppropriate(this.viewport,h)||this.scrollTarget&&(this.scrollTarget.range.headthis.viewport.to);return y&&(2&a&&(a|=this.updateScaler()),this.viewport=this.getViewport(h,this.scrollTarget),a|=this.updateForViewport()),(2&a||y)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(r?[]:this.lineGaps,t)),a|=this.computeVisibleRanges(),this.mustEnforceCursorAssoc&&(this.mustEnforceCursorAssoc=!1,t.docView.enforceCursorAssoc()),a}get visibleTop(){return this.scaler.fromDOM(this.pixelViewport.top)}get visibleBottom(){return this.scaler.fromDOM(this.pixelViewport.bottom)}getViewport(t,e){let i=.5-Math.max(-.5,Math.min(.5,t/1e3/2)),n=this.heightMap,s=this.heightOracle,{visibleTop:r,visibleBottom:o}=this,l=new Ks(n.lineAt(r-1e3*i,Rs.ByHeight,s,0,0).from,n.lineAt(o+1e3*(1-i),Rs.ByHeight,s,0,0).to);if(e){let{head:t}=e.range;if(tl.to){let i,r=Math.min(this.editorHeight,this.pixelViewport.bottom-this.pixelViewport.top),o=n.lineAt(t,Rs.ByPos,s,0,0);i="center"==e.y?(o.top+o.bottom)/2-r/2:"start"==e.y||"nearest"==e.y&&t=o+Math.max(10,Math.min(i,250)))&&n>r-2e3&&s>1,r=n<<1;if(this.defaultTextDirection!=si.LTR&&!i)return[];let o=[],l=(n,r,a,h)=>{if(r-nn&&tt.from>=a.from&&t.to<=a.to&&Math.abs(t.from-n)t.frome));if(!f){if(rt.from<=r&&t.to>=r)){let t=e.moveToLineBoundary(W.cursor(r),!1,!0).head;t>n&&(r=t)}let t=this.gapSize(a,n,r,h);f=new Us(n,r,t,i||t<2e6?t:2e6)}o.push(f)},a=e=>{if(e.lengths&&(n.push({from:s,to:t}),r+=t-s),s=e}},20),s2e6)for(let i of t)i.from>=e.from&&i.frome.from&&l(e.from,o,e,s),at.draw(this,this.heightOracle.lineWrapping))))}computeVisibleRanges(t){let e=this.stateDeco;this.lineGaps.length&&(e=e.concat(this.lineGapDeco));let i=[];It.spans(e,this.viewport.from,this.viewport.to,{span(t,e){i.push({from:t,to:e})},point(){}},20);let n=0;if(i.length!=this.visibleRanges.length)n=12;else for(let e=0;e=this.viewport.from&&t<=this.viewport.to&&this.viewportLines.find(e=>e.from<=t&&e.to>=t)||Zs(this.heightMap.lineAt(t,Rs.ByPos,this.heightOracle,0,0),this.scaler)}lineBlockAtHeight(t){return t>=this.viewportLines[0].top&&t<=this.viewportLines[this.viewportLines.length-1].bottom&&this.viewportLines.find(e=>e.top<=t&&e.bottom>=t)||Zs(this.heightMap.lineAt(this.scaler.fromDOM(t),Rs.ByHeight,this.heightOracle,0,0),this.scaler)}getScrollOffset(){return(this.scrollParent==this.view.scrollDOM?this.scrollParent.scrollTop:(this.scrollParent?this.scrollParent.getBoundingClientRect().top:0)-this.view.contentDOM.getBoundingClientRect().top)*this.scaleY}scrollAnchorAt(t){let e=this.lineBlockAtHeight(t+8);return e.from>=this.viewport.from||this.viewportLines[0].top-t>200?e:this.viewportLines[0]}elementAtHeight(t){return Zs(this.heightMap.blockAt(this.scaler.fromDOM(t),this.heightOracle,0,0),this.scaler)}get docHeight(){return this.scaler.toDOM(this.heightMap.height)}get contentHeight(){return this.docHeight+this.paddingTop+this.paddingBottom}}class Ks{constructor(t,e){this.from=t,this.to=e}}function js({total:t,ranges:e},i){if(i<=0)return e[0].from;if(i>=1)return e[e.length-1].to;let n=Math.floor(t*i);for(let t=0;;t++){let{from:i,to:s}=e[t],r=s-i;if(n<=r)return i+n;n-=r}}function Xs(t,e){let i=0;for(let{from:n,to:s}of t.ranges){if(e<=s){i+=e-n;break}i+=s-n}return i/t.total}const Gs={toDOM:t=>t,fromDOM:t=>t,scale:1,eq(t){return t==this}};function Ys(t){let e=t.facet($i).filter(t=>"function"!=typeof t),i=t.facet(ji).filter(t=>"function"!=typeof t);return i.length&&e.push(It.join(i)),e}class Js{constructor(t,e,i){let n=0,s=0,r=0;this.viewports=i.map(({from:i,to:s})=>{let r=e.lineAt(i,Rs.ByPos,t,0,0).top,o=e.lineAt(s,Rs.ByPos,t,0,0).bottom;return n+=o-r,{from:i,to:s,top:r,bottom:o,domTop:0,domBottom:0}}),this.scale=(7e6-n)/(e.height-n);for(let t of this.viewports)t.domTop=r+(t.top-s)*this.scale,r=t.domBottom=t.domTop+(t.bottom-t.top),s=t.bottom}toDOM(t){for(let e=0,i=0,n=0;;e++){let s=ee.from==t.viewports[i].from&&e.to==t.viewports[i].to))}}function Zs(t,e){if(1==e.scale)return t;let i=e.toDOM(t.top),n=e.toDOM(t.bottom);return new Ds(t.from,t.length,i,n-i,Array.isArray(t._content)?t._content.map(t=>Zs(t,e)):t._content)}const tr=z.define({combine:t=>t.join(" ")}),er=z.define({combine:t=>t.indexOf(!0)>-1}),ir=Jt.newName(),nr=Jt.newName(),sr=Jt.newName(),rr={"&light":"."+nr,"&dark":"."+sr};function or(t,e,i){return new Jt(e,{finish:e=>/&/.test(e)?e.replace(/&\w*/,e=>{if("&"==e)return t;if(!i||!i[e])throw new RangeError(`Unsupported selector: ${e}`);return i[e]}):t+" "+e})}const lr=or("."+ir,{"&":{position:"relative !important",boxSizing:"border-box","&.cm-focused":{outline:"1px dotted #212121"},display:"flex !important",flexDirection:"column"},".cm-scroller":{display:"flex !important",alignItems:"flex-start !important",fontFamily:"monospace",lineHeight:1.4,height:"100%",overflowX:"auto",position:"relative",zIndex:0,overflowAnchor:"none"},".cm-content":{margin:0,flexGrow:2,flexShrink:0,display:"block",whiteSpace:"pre",wordWrap:"normal",boxSizing:"border-box",minHeight:"100%",padding:"4px 0",outline:"none","&[contenteditable=true]":{WebkitUserModify:"read-write-plaintext-only"}},".cm-lineWrapping":{whiteSpace_fallback:"pre-wrap",whiteSpace:"break-spaces",wordBreak:"break-word",overflowWrap:"anywhere",flexShrink:1},"&light .cm-content":{caretColor:"black"},"&dark .cm-content":{caretColor:"white"},".cm-line":{display:"block",padding:"0 2px 0 6px"},".cm-layer":{userSelect:"none",position:"absolute",left:0,top:0,contain:"size style","& > *":{position:"absolute"}},"&light .cm-selectionBackground":{background:"#d9d9d9"},"&dark .cm-selectionBackground":{background:"#222"},"&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground":{background:"#d7d4f0"},"&dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground":{background:"#233"},".cm-cursorLayer":{pointerEvents:"none"},"&.cm-focused > .cm-scroller > .cm-cursorLayer":{animation:"steps(1) cm-blink 1.2s infinite"},"@keyframes cm-blink":{"0%":{},"50%":{opacity:0},"100%":{}},"@keyframes cm-blink2":{"0%":{},"50%":{opacity:0},"100%":{}},".cm-cursor, .cm-dropCursor":{borderLeft:"1.2px solid black",marginLeft:"-0.6px",pointerEvents:"none"},".cm-cursor":{display:"none"},"&dark .cm-cursor":{borderLeftColor:"#ddd"},".cm-selectionHandle":{backgroundColor:"currentColor",width:"1.5px"},".cm-selectionHandle-start::before, .cm-selectionHandle-end::before":{content:'""',backgroundColor:"inherit",borderRadius:"50%",width:"8px",height:"8px",position:"absolute",left:"-3.25px"},".cm-selectionHandle-start::before":{top:"-8px"},".cm-selectionHandle-end::before":{bottom:"-8px"},".cm-dropCursor":{position:"absolute"},"&.cm-focused > .cm-scroller > .cm-cursorLayer .cm-cursor":{display:"block"},".cm-iso":{unicodeBidi:"isolate"},".cm-announced":{position:"fixed",top:"-10000px"},"@media print":{".cm-announced":{display:"none"}},"&light .cm-activeLine":{backgroundColor:"#cceeff44"},"&dark .cm-activeLine":{backgroundColor:"#99eeff33"},"&light .cm-specialChar":{color:"red"},"&dark .cm-specialChar":{color:"#f78"},".cm-gutters":{flexShrink:0,display:"flex",height:"100%",boxSizing:"border-box",zIndex:200},".cm-gutters-before":{insetInlineStart:0},".cm-gutters-after":{insetInlineEnd:0},"&light .cm-gutters":{backgroundColor:"#f5f5f5",color:"#6c6c6c",border:"0px solid #ddd","&.cm-gutters-before":{borderRightWidth:"1px"},"&.cm-gutters-after":{borderLeftWidth:"1px"}},"&dark .cm-gutters":{backgroundColor:"#333338",color:"#ccc"},".cm-gutter":{display:"flex !important",flexDirection:"column",flexShrink:0,boxSizing:"border-box",minHeight:"100%",overflow:"hidden"},".cm-gutterElement":{boxSizing:"border-box"},".cm-lineNumbers .cm-gutterElement":{padding:"0 3px 0 5px",minWidth:"20px",textAlign:"right",whiteSpace:"nowrap"},"&light .cm-activeLineGutter":{backgroundColor:"#e2f2ff"},"&dark .cm-activeLineGutter":{backgroundColor:"#222227"},".cm-panels":{boxSizing:"border-box",position:"sticky",left:0,right:0,zIndex:300},"&light .cm-panels":{backgroundColor:"#f5f5f5",color:"black"},"&light .cm-panels-top":{borderBottom:"1px solid #ddd"},"&light .cm-panels-bottom":{borderTop:"1px solid #ddd"},"&dark .cm-panels":{backgroundColor:"#333338",color:"white"},".cm-dialog":{padding:"2px 19px 4px 6px",position:"relative","& label":{fontSize:"80%"}},".cm-dialog-close":{position:"absolute",top:"3px",right:"4px",backgroundColor:"inherit",border:"none",font:"inherit",fontSize:"14px",padding:"0"},".cm-tab":{display:"inline-block",overflow:"hidden",verticalAlign:"bottom"},".cm-widgetBuffer":{verticalAlign:"text-top",height:"1em",width:0,display:"inline"},".cm-placeholder":{color:"#888",display:"inline-block",verticalAlign:"top",userSelect:"none"},".cm-highlightSpace":{backgroundImage:"radial-gradient(circle at 50% 55%, #aaa 20%, transparent 5%)",backgroundPosition:"center"},".cm-highlightTab":{backgroundImage:'url(\'data:image/svg+xml,\')',backgroundSize:"auto 100%",backgroundPosition:"right 90%",backgroundRepeat:"no-repeat"},".cm-trailingSpace":{backgroundColor:"#ff332255"},".cm-button":{verticalAlign:"middle",color:"inherit",fontSize:"70%",padding:".2em 1em",borderRadius:"1px"},"&light .cm-button":{backgroundImage:"linear-gradient(#eff1f5, #d9d9df)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#b4b4b4, #d0d3d6)"}},"&dark .cm-button":{backgroundImage:"linear-gradient(#393939, #111)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#111, #333)"}},".cm-textfield":{verticalAlign:"middle",color:"inherit",fontSize:"70%",border:"1px solid silver",padding:".2em .5em"},"&light .cm-textfield":{backgroundColor:"white"},"&dark .cm-textfield":{border:"1px solid #555",backgroundColor:"inherit"}},rr),ar={childList:!0,characterData:!0,subtree:!0,attributes:!0,characterDataOldValue:!0},hr=ye.ie&&ye.ie_version<=11;class cr{constructor(t){this.view=t,this.active=!1,this.editContext=null,this.selectionRange=new je,this.selectionChanged=!1,this.delayedFlush=-1,this.resizeTimeout=-1,this.queue=[],this.delayedAndroidKey=null,this.flushingAndroidKey=-1,this.lastChange=0,this.scrollTargets=[],this.intersection=null,this.resizeScroll=null,this.intersecting=!1,this.gapIntersection=null,this.gaps=[],this.printQuery=null,this.parentCheck=-1,this.dom=t.contentDOM,this.observer=new MutationObserver(e=>{for(let t of e)this.queue.push(t);(ye.ie&&ye.ie_version<=11||ye.ios&&t.composing)&&e.some(t=>"childList"==t.type&&t.removedNodes.length||"characterData"==t.type&&t.oldValue.length>t.target.nodeValue.length)?this.flushSoon():this.flush()}),!window.EditContext||!ye.android||!1===t.constructor.EDIT_CONTEXT||ye.chrome&&ye.chrome_version<126||(this.editContext=new dr(t),t.state.facet(Vi)&&(t.contentDOM.editContext=this.editContext.editContext)),hr&&(this.onCharData=t=>{this.queue.push({target:t.target,type:"characterData",oldValue:t.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this),this.onResize=this.onResize.bind(this),this.onPrint=this.onPrint.bind(this),this.onScroll=this.onScroll.bind(this),window.matchMedia&&(this.printQuery=window.matchMedia("print")),"function"==typeof ResizeObserver&&(this.resizeScroll=new ResizeObserver(()=>{var t;(null===(t=this.view.docView)||void 0===t?void 0:t.lastUpdate){this.parentCheck<0&&(this.parentCheck=setTimeout(this.listenForScroll.bind(this),1e3)),t.length>0&&t[t.length-1].intersectionRatio>0!=this.intersecting&&(this.intersecting=!this.intersecting,this.intersecting!=this.view.inView&&this.onScrollChanged(document.createEvent("Event")))},{threshold:[0,.001]}),this.intersection.observe(this.dom),this.gapIntersection=new IntersectionObserver(t=>{t.length>0&&t[t.length-1].intersectionRatio>0&&this.onScrollChanged(document.createEvent("Event"))},{})),this.listenForScroll(),this.readSelectionRange()}onScrollChanged(t){this.view.inputState.runHandlers("scroll",t),this.intersecting&&this.view.measure()}onScroll(t){this.intersecting&&this.flush(!1),this.editContext&&this.view.requestMeasure(this.editContext.measureReq),this.onScrollChanged(t)}onResize(){this.resizeTimeout<0&&(this.resizeTimeout=setTimeout(()=>{this.resizeTimeout=-1,this.view.requestMeasure()},50))}onPrint(t){("change"!=t.type&&t.type||t.matches)&&(this.view.viewState.printing=!0,this.view.measure(),setTimeout(()=>{this.view.viewState.printing=!1,this.view.requestMeasure()},500))}updateGaps(t){if(this.gapIntersection&&(t.length!=this.gaps.length||this.gaps.some((e,i)=>e!=t[i]))){this.gapIntersection.disconnect();for(let e of t)this.gapIntersection.observe(e);this.gaps=t}}onSelectionChange(t){let e=this.selectionChanged;if(!this.readSelectionRange()||this.delayedAndroidKey)return;let{view:i}=this,n=this.selectionRange;if(i.state.facet(Vi)?i.root.activeElement!=this.dom:!We(this.dom,n))return;let s=n.anchorNode&&i.docView.tile.nearest(n.anchorNode);s&&s.isWidget()&&s.widget.ignoreEvent(t)?e||(this.selectionChanged=!1):(ye.ie&&ye.ie_version<=11||ye.android&&ye.chrome)&&!i.state.selection.main.empty&&n.focusNode&&Ve(n.focusNode,n.focusOffset,n.anchorNode,n.anchorOffset)?this.flushSoon():this.flush(!1)}readSelectionRange(){let{view:t}=this,e=Ie(t.root);if(!e)return!1;let i=ye.safari&&11==t.root.nodeType&&t.root.activeElement==this.dom&&function(t,e){if(e.getComposedRanges){let i=e.getComposedRanges(t.root)[0];if(i)return fr(t,i)}let i=null;function n(t){t.preventDefault(),t.stopImmediatePropagation(),i=t.getTargetRanges()[0]}return t.contentDOM.addEventListener("beforeinput",n,!0),t.dom.ownerDocument.execCommand("indent"),t.contentDOM.removeEventListener("beforeinput",n,!0),i?fr(t,i):null}(this.view,e)||e;if(!i||this.selectionRange.eq(i))return!1;let n=We(this.dom,i);return n&&!this.selectionChanged&&t.inputState.lastFocusTime>Date.now()-200&&t.inputState.lastTouchTime{let t=this.delayedAndroidKey;if(t){this.clearDelayedAndroidKey(),this.view.inputState.lastKeyCode=t.keyCode,this.view.inputState.lastKeyTime=Date.now(),!this.flush()&&t.force&&Ze(this.dom,t.key,t.keyCode)}};this.flushingAndroidKey=this.view.win.requestAnimationFrame(t)}this.delayedAndroidKey&&"Enter"!=t||(this.delayedAndroidKey={key:t,keyCode:e,force:this.lastChange{this.delayedFlush=-1,this.flush()}))}forceFlush(){this.delayedFlush>=0&&(this.view.win.cancelAnimationFrame(this.delayedFlush),this.delayedFlush=-1),this.flush()}pendingRecords(){for(let t of this.observer.takeRecords())this.queue.push(t);return this.queue}processRecords(){let t=this.pendingRecords();t.length&&(this.queue=[]);let e=-1,i=-1,n=!1;for(let s of t){let t=this.readMutation(s);t&&(t.typeOver&&(n=!0),-1==e?({from:e,to:i}=t):(e=Math.min(t.from,e),i=Math.max(t.to,i)))}return{from:e,to:i,typeOver:n}}readChange(){let{from:t,to:e,typeOver:i}=this.processRecords(),n=this.selectionChanged&&We(this.dom,this.selectionRange);if(t<0&&!n)return null;t>-1&&(this.lastChange=Date.now()),this.view.inputState.lastFocusTime=0,this.selectionChanged=!1;let s=new Kn(this.view,t,e,i);return this.view.docView.domChanged={newSel:s.newSel?s.newSel.main:null},s}flush(t=!0){if(this.delayedFlush>=0||this.delayedAndroidKey)return!1;t&&this.readSelectionRange();let e=this.readChange();if(!e)return this.view.requestMeasure(),!1;let i=this.view.state,n=Xn(this.view,e);return this.view.state==i&&(e.domChanged||e.newSel&&!Jn(this.view.state.selection,e.newSel.main))&&this.view.update([]),n}readMutation(t){let e=this.view.docView.tile.nearest(t.target);if(!e||e.isWidget())return null;if(e.markDirty("attributes"==t.type),"childList"==t.type){let i=ur(e,t.previousSibling||t.target.previousSibling,-1),n=ur(e,t.nextSibling||t.target.nextSibling,1);return{from:i?e.posAfter(i):e.posAtStart,to:n?e.posBefore(n):e.posAtEnd,typeOver:!1}}return"characterData"==t.type?{from:e.posAtStart,to:e.posAtEnd,typeOver:t.target.nodeValue==t.oldValue}:null}setWindow(t){t!=this.win&&(this.removeWindowListeners(this.win),this.win=t,this.addWindowListeners(this.win))}addWindowListeners(t){t.addEventListener("resize",this.onResize),this.printQuery?this.printQuery.addEventListener?this.printQuery.addEventListener("change",this.onPrint):this.printQuery.addListener(this.onPrint):t.addEventListener("beforeprint",this.onPrint),t.addEventListener("scroll",this.onScroll),t.document.addEventListener("selectionchange",this.onSelectionChange)}removeWindowListeners(t){t.removeEventListener("scroll",this.onScroll),t.removeEventListener("resize",this.onResize),this.printQuery?this.printQuery.removeEventListener?this.printQuery.removeEventListener("change",this.onPrint):this.printQuery.removeListener(this.onPrint):t.removeEventListener("beforeprint",this.onPrint),t.document.removeEventListener("selectionchange",this.onSelectionChange)}update(t){this.editContext&&(this.editContext.update(t),t.startState.facet(Vi)!=t.state.facet(Vi)&&(t.view.contentDOM.editContext=t.state.facet(Vi)?this.editContext.editContext:null))}destroy(){var t,e,i;this.stop(),null===(t=this.intersection)||void 0===t||t.disconnect(),null===(e=this.gapIntersection)||void 0===e||e.disconnect(),null===(i=this.resizeScroll)||void 0===i||i.disconnect();for(let t of this.scrollTargets)t.removeEventListener("scroll",this.onScroll);this.removeWindowListeners(this.win),clearTimeout(this.parentCheck),clearTimeout(this.resizeTimeout),this.win.cancelAnimationFrame(this.delayedFlush),this.win.cancelAnimationFrame(this.flushingAndroidKey),this.editContext&&(this.view.contentDOM.editContext=null,this.editContext.destroy())}}function ur(t,e,i){for(;e;){let n=rn.get(e);if(n&&n.parent==t)return n;let s=e.parentNode;e=s!=t.dom?s:i>0?e.nextSibling:e.previousSibling}return null}function fr(t,e){let i=e.startContainer,n=e.startOffset,s=e.endContainer,r=e.endOffset,o=t.docView.domAtPos(t.state.selection.main.anchor,1);return Ve(o.node,o.offset,s,r)&&([i,n,s,r]=[s,r,i,n]),{anchorNode:i,anchorOffset:n,focusNode:s,focusOffset:r}}class dr{constructor(t){this.from=0,this.to=0,this.pendingContextChange=null,this.handlers=Object.create(null),this.composing=null,this.resetRange(t.state);let e=this.editContext=new window.EditContext({text:t.state.doc.sliceString(this.from,this.to),selectionStart:this.toContextPos(Math.max(this.from,Math.min(this.to,t.state.selection.main.anchor))),selectionEnd:this.toContextPos(t.state.selection.main.head)});this.handlers.textupdate=i=>{let n=t.state.selection.main,{anchor:s,head:r}=n,o=this.toEditorPos(i.updateRangeStart),l=this.toEditorPos(i.updateRangeEnd);t.inputState.composing>=0&&!this.composing&&(this.composing={contextBase:i.updateRangeStart,editorBase:o,drifted:!1});let a=l-o>i.text.length;o==this.from&&sthis.to&&(l=s);let h=Yn(t.state.sliceDoc(o,l),i.text,(a?n.from:n.to)-o,a?"end":null);if(!h){let e=W.single(this.toEditorPos(i.selectionStart),this.toEditorPos(i.selectionEnd));return void(Jn(e,n)||t.dispatch({selection:e,userEvent:"select"}))}let c={from:h.from+o,to:h.toA+o,insert:f.of(i.text.slice(h.from,h.toB).split("\n"))};if((ye.mac||ye.android)&&c.from==r-1&&/^\. ?$/.test(i.text)&&"off"==t.contentDOM.getAttribute("autocorrect")&&(c={from:o,to:l,insert:f.of([i.text.replace("."," ")])}),this.pendingContextChange=c,!t.state.readOnly){let e=this.to-this.from+(c.to-c.from+c.insert.length);Gn(t,c,W.single(this.toEditorPos(i.selectionStart,e),this.toEditorPos(i.selectionEnd,e)))}this.pendingContextChange&&(this.revertPending(t.state),this.setSelection(t.state)),c.from=0&&!/[\\p{Alphabetic}\\p{Number}_]/.test(e.text.slice(Math.max(0,i.updateRangeStart-1),Math.min(e.text.length,i.updateRangeStart+1)))&&this.handlers.compositionend(i)},this.handlers.characterboundsupdate=i=>{let n=[],s=null;for(let e=this.toEditorPos(i.rangeStart),r=this.toEditorPos(i.rangeEnd);e{let i=[];for(let t of e.getTextFormats()){let e=t.underlineStyle,n=t.underlineThickness;if(!/none/i.test(e)&&!/none/i.test(n)){let s=this.toEditorPos(t.rangeStart),r=this.toEditorPos(t.rangeEnd);if(s{t.inputState.composing<0&&(t.inputState.composing=0,t.inputState.compositionFirstChange=!0)},this.handlers.compositionend=()=>{if(t.inputState.composing=-1,t.inputState.compositionFirstChange=null,this.composing){let{drifted:e}=this.composing;this.composing=null,e&&this.reset(t.state)}};for(let t in this.handlers)e.addEventListener(t,this.handlers[t]);this.measureReq={read:t=>{let e=Ie(t.root);e&&e.rangeCount&&this.editContext.updateSelectionBounds(e.getRangeAt(0).getBoundingClientRect())}}}applyEdits(t){let e=0,i=!1,n=this.pendingContextChange;return t.changes.iterChanges((s,r,o,l,a)=>{if(i)return;let h=a.length-(r-s);if(n&&r>=n.to){if(n.from==s&&n.to==r&&n.insert.eq(a))return n=this.pendingContextChange=null,e+=h,void(this.to+=h);n=null,this.revertPending(t.state)}if(s+=e,(r+=e)<=this.from)this.from+=h,this.to+=h;else if(sthis.to||this.to-this.from+a.length>3e4)return void(i=!0);this.editContext.updateText(this.toContextPos(s),this.toContextPos(r),a.toString()),this.to+=h}e+=h}),n&&!i&&this.revertPending(t.state),!i}update(t){let e=this.pendingContextChange,i=t.startState.selection.main;this.composing&&(this.composing.drifted||!t.changes.touchesRange(i.from,i.to)&&t.transactions.some(t=>!t.isUserEvent("input.type")&&t.changes.touchesRange(this.from,this.to)))?(this.composing.drifted=!0,this.composing.editorBase=t.changes.mapPos(this.composing.editorBase)):this.applyEdits(t)&&this.rangeIsValid(t.state)?(t.docChanged||t.selectionSet||e)&&this.setSelection(t.state):(this.pendingContextChange=null,this.reset(t.state)),(t.geometryChanged||t.docChanged||t.selectionSet)&&t.view.requestMeasure(this.measureReq)}resetRange(t){let{head:e}=t.selection.main;this.from=Math.max(0,e-1e4),this.to=Math.min(t.doc.length,e+1e4)}reset(t){this.resetRange(t),this.editContext.updateText(0,this.editContext.text.length,t.doc.sliceString(this.from,this.to)),this.setSelection(t)}revertPending(t){let e=this.pendingContextChange;this.pendingContextChange=null,this.editContext.updateText(this.toContextPos(e.from),this.toContextPos(e.from+e.insert.length),t.doc.sliceString(e.from,e.to))}setSelection(t){let{main:e}=t.selection,i=this.toContextPos(Math.max(this.from,Math.min(this.to,e.anchor))),n=this.toContextPos(e.head);this.editContext.selectionStart==i&&this.editContext.selectionEnd==n||this.editContext.updateSelection(i,n)}rangeIsValid(t){let{head:e}=t.selection.main;return!(this.from>0&&e-this.from<500||this.to3e4)}toEditorPos(t,e=this.to-this.from){t=Math.min(t,e);let i=this.composing;return i&&i.drifted?i.editorBase+(t-i.contextBase):t+this.from}toContextPos(t){let e=this.composing;return e&&e.drifted?e.contextBase+(t-e.editorBase):t-this.from}destroy(){for(let t in this.handlers)this.editContext.removeEventListener(t,this.handlers[t])}}class pr{get state(){return this.viewState.state}get viewport(){return this.viewState.viewport}get visibleRanges(){return this.viewState.visibleRanges}get inView(){return this.viewState.inView}get composing(){return!!this.inputState&&this.inputState.composing>0}get compositionStarted(){return!!this.inputState&&this.inputState.composing>=0}get root(){return this._root}get win(){return this.dom.ownerDocument.defaultView||window}constructor(t={}){var e;this.plugins=[],this.pluginMap=new Map,this.editorAttrs={},this.contentAttrs={},this.bidiCache=[],this.destroyed=!1,this.updateState=2,this.measureScheduled=-1,this.measureRequests=[],this.contentDOM=document.createElement("div"),this.scrollDOM=document.createElement("div"),this.scrollDOM.tabIndex=-1,this.scrollDOM.className="cm-scroller",this.scrollDOM.appendChild(this.contentDOM),this.announceDOM=document.createElement("div"),this.announceDOM.className="cm-announced",this.announceDOM.setAttribute("aria-live","polite"),this.dom=document.createElement("div"),this.dom.appendChild(this.announceDOM),this.dom.appendChild(this.scrollDOM),t.parent&&t.parent.appendChild(this.dom);let{dispatch:i}=t;this.dispatchTransactions=t.dispatchTransactions||i&&(t=>t.forEach(t=>i(t,this)))||(t=>this.update(t)),this.dispatch=this.dispatch.bind(this),this._root=t.root||function(t){for(;t;){if(t&&(9==t.nodeType||11==t.nodeType&&t.host))return t;t=t.assignedSlot||t.parentNode}return null}(t.parent)||document,this.viewState=new $s(this,t.state||Tt.create(t)),t.scrollTo&&t.scrollTo.is(Ni)&&(this.viewState.scrollTarget=t.scrollTo.value.clip(this.viewState.state)),this.plugins=this.state.facet(Fi).map(t=>new _i(t));for(let t of this.plugins)t.update(this);this.observer=new cr(this),this.inputState=new Zn(this),this.inputState.ensureHandlers(this.plugins),this.docView=new Tn(this),this.mountStyles(),this.updateAttrs(),this.updateState=0,this.requestMeasure(),(null===(e=document.fonts)||void 0===e?void 0:e.ready)&&document.fonts.ready.then(()=>{this.viewState.mustMeasureContent="refresh",this.requestMeasure()})}dispatch(...t){let e=1==t.length&&t[0]instanceof vt?t:1==t.length&&Array.isArray(t[0])?t[0]:[this.state.update(...t)];this.dispatchTransactions(e,this)}update(t){if(0!=this.updateState)throw new Error("Calls to EditorView.update are not allowed while an update is in progress");let e,i=!1,n=!1,s=this.state;for(let e of t){if(e.startState!=s)throw new RangeError("Trying to update state with a transaction that doesn't start from the previous state.");s=e.state}if(this.destroyed)return void(this.viewState.state=s);let r=this.hasFocus,o=0,l=null;t.some(t=>t.annotation(ys))?(this.inputState.notifiedFocused=r,o=1):r!=this.inputState.notifiedFocused&&(this.inputState.notifiedFocused=r,l=xs(s,r),l||(o=1));let a=this.observer.delayedAndroidKey,h=null;if(a?(this.observer.clearDelayedAndroidKey(),h=this.observer.readChange(),(h&&!this.state.doc.eq(s.doc)||!this.state.selection.eq(s.selection))&&(h=null)):this.observer.clear(),s.facet(Tt.phrases)!=this.state.facet(Tt.phrases))return this.setState(s);e=nn.create(this,s,t),e.flags|=o;let c=this.viewState.scrollTarget;try{this.updateState=2;for(let e of t){if(c&&(c=c.map(e.changes)),e.scrollIntoView){let{main:t}=e.state.selection,{x:i,y:n}=this.state.facet(pr.cursorScrollMargin);c=new Ii(t.empty?t:W.cursor(t.head,t.head>t.anchor?-1:1),"nearest","nearest",n,i)}for(let t of e.effects)t.is(Ni)&&(c=t.value.clip(this.state))}this.viewState.update(e,c),this.bidiCache=vr.update(this.bidiCache,e.changes),e.empty||(this.updatePlugins(e),this.inputState.update(e)),i=this.docView.update(e),this.state.facet(tn)!=this.styleModules&&this.mountStyles(),n=this.updateAttrs(),this.showAnnouncements(t),this.docView.updateSelection(i,t.some(t=>t.isUserEvent("select.pointer")))}finally{this.updateState=0}if(e.startState.facet(tr)!=e.state.facet(tr)&&(this.viewState.mustMeasureContent=!0),(i||n||c||this.viewState.mustEnforceCursorAssoc||this.viewState.mustMeasureContent)&&this.requestMeasure(),i&&this.docViewUpdate(),!e.empty)for(let t of this.state.facet(Oi))try{t(e)}catch(t){Hi(this.state,t,"update listener")}(l||h)&&Promise.resolve().then(()=>{l&&this.state==l.startState&&this.dispatch(l),h&&!Xn(this,h)&&a.force&&Ze(this.contentDOM,a.key,a.keyCode)})}setState(t){if(0!=this.updateState)throw new Error("Calls to EditorView.setState are not allowed while an update is in progress");if(this.destroyed)return void(this.viewState.state=t);this.updateState=2;let e=this.hasFocus;try{for(let t of this.plugins)t.destroy(this);this.viewState=new $s(this,t),this.plugins=t.facet(Fi).map(t=>new _i(t)),this.pluginMap.clear();for(let t of this.plugins)t.update(this);this.docView.destroy(),this.docView=new Tn(this),this.inputState.ensureHandlers(this.plugins),this.mountStyles(),this.updateAttrs(),this.bidiCache=[]}finally{this.updateState=0}e&&this.focus(),this.requestMeasure()}updatePlugins(t){let e=t.startState.facet(Fi),i=t.state.facet(Fi);if(e!=i){let n=[];for(let s of i){let i=e.indexOf(s);if(i<0)n.push(new _i(s));else{let e=this.plugins[i];e.mustUpdate=t,n.push(e)}}for(let e of this.plugins)e.mustUpdate!=t&&e.destroy(this);this.plugins=n,this.pluginMap.clear()}else for(let e of this.plugins)e.mustUpdate=t;for(let t=0;t-1&&this.win.cancelAnimationFrame(this.measureScheduled),this.observer.delayedAndroidKey)return this.measureScheduled=-1,void this.requestMeasure();this.measureScheduled=0,t&&this.observer.forceFlush();let e=null,i=this.viewState.scrollParent,n=this.viewState.getScrollOffset(),{scrollAnchorPos:s,scrollAnchorHeight:r}=this.viewState;Math.abs(n-this.viewState.scrollOffset)>1&&(r=-1),this.viewState.scrollAnchorHeight=-1;try{for(let t=0;;t++){if(r<0)if(ti(i||this.win))s=-1,r=this.viewState.heightMap.height;else{let t=this.viewState.scrollAnchorAt(n);s=t.from,r=t.top}this.updateState=1;let o=this.viewState.measure();if(!o&&!this.measureRequests.length&&null==this.viewState.scrollTarget)break;if(t>5){console.warn(this.measureRequests.length?"Measure loop restarted more than 5 times":"Viewport failed to stabilize");break}let l=[];4&o||([this.measureRequests,l]=[l,this.measureRequests]);let a=l.map(t=>{try{return t.read(this)}catch(t){return Hi(this.state,t),gr}}),h=nn.create(this,this.state,[]),c=!1;h.flags|=o,e?e.flags|=o:e=h,this.updateState=2,h.empty||(this.updatePlugins(h),this.inputState.update(h),this.updateAttrs(),c=this.docView.update(h),c&&this.docViewUpdate());for(let t=0;t1||t<-1)&&!(ye.ios&&this.inputState.lastIOSMomentumScroll>Date.now()-100)&&(i==this.scrollDOM||this.hasFocus||Math.max(this.inputState.lastWheelEvent,this.inputState.lastTouchTime)>Date.now()-100)){n+=t,i?i.scrollTop+=t:this.win.scrollBy(0,t),r=-1;continue}}}break}}}finally{this.updateState=0,this.measureScheduled=-1}if(e&&!e.empty)for(let t of this.state.facet(Oi))t(e)}get themeClasses(){return ir+" "+(this.state.facet(er)?sr:nr)+" "+this.state.facet(tr)}updateAttrs(){let t=wr(this,Ui,{class:"cm-editor"+(this.hasFocus?" cm-focused ":" ")+this.themeClasses}),e={spellcheck:"false",autocorrect:"off",autocapitalize:"off",writingsuggestions:"false",translate:"no",contenteditable:this.state.facet(Vi)?"true":"false",class:"cm-content",style:`${ye.tabSize}: ${this.state.tabSize}`,role:"textbox","aria-multiline":"true"};this.state.readOnly&&(e["aria-readonly"]="true"),wr(this,Qi,e);let i=this.observer.ignore(()=>{let i=Ce(this.contentDOM,this.contentAttrs,e),n=Ce(this.dom,this.editorAttrs,t);return i||n});return this.editorAttrs=t,this.contentAttrs=e,i}showAnnouncements(t){let e=!0;for(let i of t)for(let t of i.effects)if(t.is(pr.announce)){e&&(this.announceDOM.textContent=""),e=!1,this.announceDOM.appendChild(document.createElement("div")).textContent=t.value}}mountStyles(){this.styleModules=this.state.facet(tn);let t=this.state.facet(pr.cspNonce);Jt.mount(this.root,this.styleModules.concat(lr).reverse(),t?{nonce:t}:void 0)}readMeasured(){if(2==this.updateState)throw new Error("Reading the editor layout isn't allowed during an update");0==this.updateState&&this.measureScheduled>-1&&this.measure(!1)}requestMeasure(t){if(this.measureScheduled<0&&(this.measureScheduled=this.win.requestAnimationFrame(()=>this.measure())),t){if(this.measureRequests.indexOf(t)>-1)return;if(null!=t.key)for(let e=0;ee.plugin==t)||null),e&&e.update(this).value}get documentTop(){return this.contentDOM.getBoundingClientRect().top+this.viewState.paddingTop}get documentPadding(){return{top:this.viewState.paddingTop,bottom:this.viewState.paddingBottom}}get scaleX(){return this.viewState.scaleX}get scaleY(){return this.viewState.scaleY}elementAtHeight(t){return this.readMeasured(),this.viewState.elementAtHeight(t)}lineBlockAtHeight(t){return this.readMeasured(),this.viewState.lineBlockAtHeight(t)}get viewportLineBlocks(){return this.viewState.viewportLines}lineBlockAt(t){return this.viewState.lineBlockAt(t)}get contentHeight(){return this.viewState.contentHeight}moveByChar(t,e,i){return Hn(this,t,In(this,t,e,i))}moveByGroup(t,e){return Hn(this,t,In(this,t,e,e=>function(t,e,i){let n=t.state.charCategorizer(e),s=n(i);return t=>{let e=n(t);return s==Ct.Space&&(s=e),s==e}}(this,t.head,e)))}visualLineSide(t,e){let i=this.bidiSpans(t),n=this.textDirectionAt(t.from),s=i[e?i.length-1:0];return W.cursor(s.side(e,n)+t.from,s.forward(!e,n)?1:-1)}moveToLineBoundary(t,e,i=!0){return function(t,e,i,n){let s=Ln(t,e.head,e.assoc||-1),r=n&&s.type==Oe.Text&&(t.lineWrapping||s.widgetLineBreaks)?t.coordsAtPos(e.assoc<0&&e.head>s.from?e.head-1:e.head):null;if(r){let e=t.dom.getBoundingClientRect(),n=t.textDirectionAt(s.from),o=t.posAtCoords({x:i==(n==si.LTR)?e.right-1:e.left+1,y:(r.top+r.bottom)/2});if(null!=o)return W.cursor(o,i?-1:1)}return W.cursor(i?s.to:s.from,i?-1:1)}(this,t,e,i)}moveVertically(t,e,i){return Hn(this,t,function(t,e,i,n){let s=e.head,r=i?1:-1;if(s==(i?t.state.doc.length:0))return W.cursor(s,e.assoc);let o,l=e.goalColumn,a=t.contentDOM.getBoundingClientRect(),h=t.coordsAtPos(s,e.assoc||((e.empty?i:e.head==e.from)?1:-1)),c=t.documentTop;if(h)null==l&&(l=h.left-a.left),o=r<0?h.top:h.bottom;else{let e=t.viewState.lineBlockAt(s);null==l&&(l=Math.min(a.right-a.left,t.defaultCharacterWidth*(s-e.from))),o=(r<0?e.top:e.bottom)+c}let u=a.left+l,f=t.viewState.heightOracle.textHeight>>1,d=null!=n?n:f;for(let e=0;;e+=f){let n=o+(d+e)*r,s=zn(t,{x:u,y:n},!1,r);if(i?n>a.bottom:no:cthis.viewport.to?this.textDirection:(this.readMeasured(),this.docView.textDirectionAt(t))}get lineWrapping(){return this.viewState.heightOracle.lineWrapping}bidiSpans(t){if(t.length>mr)return bi(t.length);let e,i=this.textDirectionAt(t.from);for(let n of this.bidiCache)if(n.from==t.from&&n.dir==i&&(n.fresh||mi(n.isolates,e=Yi(this,t))))return n.order;e||(e=Yi(this,t));let n=function(t,e,i){if(!t)return[new pi(0,0,e==oi?1:0)];if(e==ri&&!i.length&&!di.test(t))return bi(t.length);if(i.length)for(;t.length>gi.length;)gi[gi.length]=256;let n=[],s=e==ri?0:1;return wi(t,s,s,i,0,t.length,n),n}(t.text,i,e);return this.bidiCache.push(new vr(t.from,t.to,i,e,!0,n)),n}get hasFocus(){var t;return(this.dom.ownerDocument.hasFocus()||ye.safari&&(null===(t=this.inputState)||void 0===t?void 0:t.lastContextMenu)>Date.now()-3e4)&&this.root.activeElement==this.contentDOM}focus(){this.observer.ignore(()=>{Ye(this.contentDOM),this.docView.updateSelection()})}setRoot(t){this._root!=t&&(this._root=t,this.observer.setWindow((9==t.nodeType?t:t.ownerDocument).defaultView||window),this.mountStyles())}destroy(){this.root.activeElement==this.contentDOM&&this.contentDOM.blur();for(let t of this.plugins)t.destroy(this);this.plugins=[],this.inputState.destroy(),this.docView.destroy(),this.dom.remove(),this.observer.destroy(),this.measureScheduled>-1&&this.win.cancelAnimationFrame(this.measureScheduled),this.destroyed=!0}static scrollIntoView(t,e={}){var i,n,s,r;return Ni.of(new Ii("number"==typeof t?W.cursor(t):t,null!==(i=e.y)&&void 0!==i?i:"nearest",null!==(n=e.x)&&void 0!==n?n:"nearest",null!==(s=e.yMargin)&&void 0!==s?s:5,null!==(r=e.xMargin)&&void 0!==r?r:5))}scrollSnapshot(){let{scrollTop:t,scrollLeft:e}=this.scrollDOM,i=this.viewState.scrollAnchorAt(t);return Ni.of(new Ii(W.cursor(i.from),"start","start",i.top-t,e,!0))}setTabFocusMode(t){null==t?this.inputState.tabFocusMode=this.inputState.tabFocusMode<0?0:-1:"boolean"==typeof t?this.inputState.tabFocusMode=t?0:-1:0!=this.inputState.tabFocusMode&&(this.inputState.tabFocusMode=Date.now()+t)}static domEventHandlers(t){return qi.define(()=>({}),{eventHandlers:t})}static domEventObservers(t){return qi.define(()=>({}),{eventObservers:t})}static theme(t,e){let i=Jt.newName(),n=[tr.of(i),tn.of(or(`.${i}`,t))];return e&&e.dark&&n.push(er.of(!0)),n}static baseTheme(t){return Z.lowest(tn.of(or("."+ir,t,rr)))}static findFromDOM(t){var e;let i=t.querySelector(".cm-content"),n=i&&rn.get(i)||rn.get(t);return(null===(e=null==n?void 0:n.root)||void 0===e?void 0:e.view)||null}}pr.styleModule=tn,pr.inputHandler=Ti,pr.clipboardInputFilter=Ri,pr.clipboardOutputFilter=Pi,pr.scrollHandler=Li,pr.focusChangeEffect=Di,pr.perLineTextDirection=Bi,pr.exceptionSink=Mi,pr.updateListener=Oi,pr.editable=Vi,pr.mouseSelectionStyle=Ai,pr.dragMovesSelection=Ci,pr.clickAddsSelectionRange=Si,pr.decorations=$i,pr.blockWrappers=Ki,pr.outerDecorations=ji,pr.atomicRanges=Xi,pr.bidiIsolatedRanges=Gi,pr.cursorScrollMargin=z.define({combine:t=>{let e=5,i=5;for(let n of t)"number"==typeof n?e=i=n:({x:e,y:i}=n);return{x:e,y:i}}}),pr.scrollMargins=Ji,pr.darkTheme=er,pr.cspNonce=z.define({combine:t=>t.length?t[0]:""}),pr.contentAttributes=Qi,pr.editorAttributes=Ui,pr.lineWrapping=pr.contentAttributes.of({class:"cm-lineWrapping"}),pr.announce=gt.define();const mr=4096,gr={};class vr{constructor(t,e,i,n,s,r){this.from=t,this.to=e,this.dir=i,this.isolates=n,this.fresh=s,this.order=r}static update(t,e){if(e.empty&&!t.some(t=>t.fresh))return t;let i=[],n=t.length?t[t.length-1].dir:si.LTR;for(let s=Math.max(0,t.length-10);s=0;s--){let e=n[s],r="function"==typeof e?e(t):e;r&&xe(r,i)}return i}const br=ye.mac?"mac":ye.windows?"win":ye.linux?"linux":"key";function yr(t,e,i){return e.altKey&&(t="Alt-"+t),e.ctrlKey&&(t="Ctrl-"+t),e.metaKey&&(t="Meta-"+t),!1!==i&&e.shiftKey&&(t="Shift-"+t),t}const xr=Z.default(pr.domEventHandlers({keydown:(t,e)=>Tr(Cr(e.state),t,e,"editor")})),kr=z.define({enables:xr}),Sr=new WeakMap;function Cr(t){let e=t.facet(kr),i=Sr.get(e);return i||Sr.set(e,i=function(t,e=br){let i=Object.create(null),n=Object.create(null),s=(t,e)=>{let i=n[t];if(null==i)n[t]=e;else if(i!=e)throw new Error("Key binding "+t+" is used both as a regular binding and as a multi-stroke prefix")},r=(t,n,r,o,l)=>{var a,h;let c=i[t]||(i[t]=Object.create(null)),u=n.split(/ (?!$)/).map(t=>function(t,e){const i=t.split(/-(?!$)/);let n,s,r,o,l=i[i.length-1];"Space"==l&&(l=" ");for(let t=0;t{let n=Ar={view:e,prefix:i,scope:t};return setTimeout(()=>{Ar==n&&(Ar=null)},Mr),!0}]})}let f=u.join(" ");s(f,!1);let d=c[f]||(c[f]={preventDefault:!1,stopPropagation:!1,run:(null===(h=null===(a=c._any)||void 0===a?void 0:a.run)||void 0===h?void 0:h.slice())||[]});r&&d.run.push(r),o&&(d.preventDefault=!0),l&&(d.stopPropagation=!0)};for(let n of t){let t=n.scope?n.scope.split(" "):["editor"];if(n.any)for(let e of t){let t=i[e]||(i[e]=Object.create(null));t._any||(t._any={preventDefault:!1,stopPropagation:!1,run:[]});let{any:s}=n;for(let e in t)t[e].run.push(t=>s(t,Or))}let s=n[e]||n.key;if(s)for(let e of t)r(e,s,n.run,n.preventDefault,n.stopPropagation),n.shift&&r(e,"Shift-"+s,n.shift,n.preventDefault,n.stopPropagation)}return i}(e.reduce((t,e)=>t.concat(e),[]))),i}let Ar=null;const Mr=4e3;let Or=null;function Tr(t,e,i,n){Or=e;let s=function(t){var e=!(ne&&t.metaKey&&t.shiftKey&&!t.ctrlKey&&!t.altKey||se&&t.shiftKey&&t.key&&1==t.key.length||"Unidentified"==t.key)&&t.key||(t.shiftKey?ie:ee)[t.keyCode]||t.key||"Unidentified";return"Esc"==e&&(e="Escape"),"Del"==e&&(e="Delete"),"Left"==e&&(e="ArrowLeft"),"Up"==e&&(e="ArrowUp"),"Right"==e&&(e="ArrowRight"),"Down"==e&&(e="ArrowDown"),e}(e),r=A(S(s,0))==s.length&&" "!=s,o="",l=!1,a=!1,h=!1;Ar&&Ar.view==i&&Ar.scope==n&&(o=Ar.prefix+" ",ss.indexOf(e.keyCode)<0&&(a=!0,Ar=null));let c,u,f=new Set,d=t=>{if(t){for(let e of t.run)if(!f.has(e)&&(f.add(e),e(i)))return t.stopPropagation&&(h=!0),!0;t.preventDefault&&(t.stopPropagation&&(h=!0),a=!0)}return!1},p=t[n];return p&&(d(p[o+yr(s,e,!r)])?l=!0:!r||!(e.altKey||e.metaKey||e.ctrlKey)||ye.windows&&e.ctrlKey&&e.altKey||ye.mac&&e.altKey&&!e.ctrlKey&&!e.metaKey||!(c=ee[e.keyCode])||c==s?r&&e.shiftKey&&d(p[o+yr(s,e,!0)])&&(l=!0):(d(p[o+yr(c,e,!0)])||e.shiftKey&&(u=ie[e.keyCode])!=s&&u!=c&&d(p[o+yr(u,e,!1)]))&&(l=!0),!l&&d(p._any)&&(l=!0)),a&&(l=!0),l&&h&&e.stopPropagation(),Or=null,l}class Dr{constructor(t,e,i,n,s){this.className=t,this.left=e,this.top=i,this.width=n,this.height=s}draw(){let t=document.createElement("div");return t.className=this.className,this.adjust(t),t}update(t,e){return e.className==this.className&&(this.adjust(t),!0)}adjust(t){t.style.left=this.left+"px",t.style.top=this.top+"px",null!=this.width&&(t.style.width=this.width+"px"),t.style.height=this.height+"px"}eq(t){return this.left==t.left&&this.top==t.top&&this.width==t.width&&this.height==t.height&&this.className==t.className}static forRange(t,e,i){if(i.empty){let n=t.coordsAtPos(i.head,i.assoc||1);if(!n)return[];let s=Rr(t);return[new Dr(e,n.left-s.left,n.top-s.top,null,n.bottom-n.top)]}return function(t,e,i){if(i.to<=t.viewport.from||i.from>=t.viewport.to)return[];let n=Math.max(i.from,t.viewport.from),s=Math.min(i.to,t.viewport.to),r=t.textDirection==si.LTR,o=t.contentDOM,l=o.getBoundingClientRect(),a=Rr(t),h=o.querySelector(".cm-line"),c=h&&window.getComputedStyle(h),u=l.left+(c?parseInt(c.paddingLeft)+Math.min(0,parseInt(c.textIndent)):0),f=l.right-(c?parseInt(c.paddingRight):0),d=Ln(t,n,1),p=Ln(t,s,-1),m=d.type==Oe.Text?d:null,g=p.type==Oe.Text?p:null;m&&(t.lineWrapping||d.widgetLineBreaks)&&(m=Pr(t,n,1,m));g&&(t.lineWrapping||p.widgetLineBreaks)&&(g=Pr(t,s,-1,g));if(m&&g&&m.from==g.from&&m.to==g.to)return w(b(i.from,i.to,m));{let e=m?b(i.from,null,m):y(d,!1),n=g?b(null,i.to,g):y(p,!0),s=[];return(m||d).to<(g||p).from-(m&&g?1:0)||d.widgetLineBreaks>1&&e.bottom+t.defaultLineHeight/2h&&n.from=r)break;l>s&&a(Math.max(t,s),null==e&&t<=h,Math.min(l,r),null==i&&l>=c,o.dir)}if(s=n.to+1,s>=r)break}return 0==l.length&&a(h,null==e,c,null==i,t.textDirection),{top:s,bottom:o,horizontal:l}}function y(t,e){let i=l.top+(e?t.top:t.bottom);return{top:i,bottom:i,horizontal:[]}}}(t,e,i)}}function Rr(t){let e=t.scrollDOM.getBoundingClientRect();return{left:(t.textDirection==si.LTR?e.left:e.right-t.scrollDOM.clientWidth*t.scaleX)-t.scrollDOM.scrollLeft*t.scaleX,top:e.top-t.scrollDOM.scrollTop*t.scaleY}}function Pr(t,e,i,n){let s=t.coordsAtPos(e,2*i);if(!s)return n;let r=t.dom.getBoundingClientRect(),o=(s.top+s.bottom)/2,l=t.posAtCoords({x:r.left+1,y:o}),a=t.posAtCoords({x:r.right-1,y:o});return null==l||null==a?n:{from:Math.max(n.from,Math.min(l,a)),to:Math.min(n.to,Math.max(l,a))}}class Br{constructor(t,e){this.view=t,this.layer=e,this.drawn=[],this.scaleX=1,this.scaleY=1,this.measureReq={read:this.measure.bind(this),write:this.draw.bind(this)},this.dom=t.scrollDOM.appendChild(document.createElement("div")),this.dom.classList.add("cm-layer"),e.above&&this.dom.classList.add("cm-layer-above"),e.class&&this.dom.classList.add(e.class),this.scale(),this.dom.setAttribute("aria-hidden","true"),this.setOrder(t.state),t.requestMeasure(this.measureReq),e.mount&&e.mount(this.dom,t)}update(t){t.startState.facet(Er)!=t.state.facet(Er)&&this.setOrder(t.state),(this.layer.update(t,this.dom)||t.geometryChanged)&&(this.scale(),t.view.requestMeasure(this.measureReq))}docViewUpdate(t){!1!==this.layer.updateOnDocViewUpdate&&t.requestMeasure(this.measureReq)}setOrder(t){let e=0,i=t.facet(Er);for(;e{return i=t,n=this.drawn[e],!(i.constructor==n.constructor&&i.eq(n));var i,n})){let e=this.dom.firstChild,i=0;for(let n of t)n.update&&e&&n.constructor&&this.drawn[i].constructor&&n.update(e,this.drawn[i])?(e=e.nextSibling,i++):this.dom.insertBefore(n.draw(),e);for(;e;){let t=e.nextSibling;e.remove(),e=t}this.drawn=t,ye.webkit&&(this.dom.style.display=this.dom.firstChild?"":"none")}}destroy(){this.layer.destroy&&this.layer.destroy(this.dom,this.view),this.dom.remove()}}const Er=z.define();function Lr(t){return[qi.define(e=>new Br(e,t)),Er.of(t)]}const Ir=z.define({combine:t=>Dt(t,{cursorBlinkRate:1200,drawRangeCursor:!0,iosSelectionHandles:!0},{cursorBlinkRate:(t,e)=>Math.min(t,e),drawRangeCursor:(t,e)=>t||e})});function Nr(t={}){return[Ir.of(t),Hr,zr,Fr,Ei.of(!0)]}function Wr(t){return t.startState.facet(Ir)!=t.state.facet(Ir)}const Hr=Lr({above:!0,markers(t){let{state:e}=t,i=e.facet(Ir),n=[];for(let s of e.selection.ranges){let r=s==e.selection.main;if(s.empty||i.drawRangeCursor&&!(r&&ye.ios&&i.iosSelectionHandles)){let e=r?"cm-cursor cm-cursor-primary":"cm-cursor cm-cursor-secondary",i=s.empty?s:W.cursor(s.head,s.assoc);for(let s of Dr.forRange(t,e,i))n.push(s)}}return n},update(t,e){t.transactions.some(t=>t.selection)&&(e.style.animationName="cm-blink"==e.style.animationName?"cm-blink2":"cm-blink");let i=Wr(t);return i&&Vr(t.state,e),t.docChanged||t.selectionSet||i},mount(t,e){Vr(e.state,t)},class:"cm-cursorLayer"});function Vr(t,e){e.style.animationDuration=t.facet(Ir).cursorBlinkRate+"ms"}const zr=Lr({above:!1,markers(t){let e=[],{main:i,ranges:n}=t.state.selection;for(let i of n)if(!i.empty)for(let n of Dr.forRange(t,"cm-selectionBackground",i))e.push(n);if(ye.ios&&!i.empty&&t.state.facet(Ir).iosSelectionHandles){for(let n of Dr.forRange(t,"cm-selectionHandle cm-selectionHandle-start",W.cursor(i.from,1)))e.push(n);for(let n of Dr.forRange(t,"cm-selectionHandle cm-selectionHandle-end",W.cursor(i.to,1)))e.push(n)}return e},update:(t,e)=>t.docChanged||t.selectionSet||t.viewportChanged||Wr(t),class:"cm-selectionLayer"}),Fr=Z.highest(pr.theme({".cm-line":{"& ::selection, &::selection":{backgroundColor:"transparent !important"},caretColor:"transparent !important"},".cm-content":{caretColor:"transparent !important","& :focus":{caretColor:"initial !important","&::selection, & ::selection":{backgroundColor:"Highlight !important"}}}})),qr=gt.define({map:(t,e)=>null==t?null:e.mapPos(t)}),_r=K.define({create:()=>null,update:(t,e)=>(null!=t&&(t=e.changes.mapPos(t)),e.effects.reduce((t,e)=>e.is(qr)?e.value:t,t))}),Ur=qi.fromClass(class{constructor(t){this.view=t,this.cursor=null,this.measureReq={read:this.readPos.bind(this),write:this.drawCursor.bind(this)}}update(t){var e;let i=t.state.field(_r);null==i?null!=this.cursor&&(null===(e=this.cursor)||void 0===e||e.remove(),this.cursor=null):(this.cursor||(this.cursor=this.view.scrollDOM.appendChild(document.createElement("div")),this.cursor.className="cm-dropCursor"),(t.startState.field(_r)!=i||t.docChanged||t.geometryChanged)&&this.view.requestMeasure(this.measureReq))}readPos(){let{view:t}=this,e=t.state.field(_r),i=null!=e&&t.coordsAtPos(e);if(!i)return null;let n=t.scrollDOM.getBoundingClientRect();return{left:i.left-n.left+t.scrollDOM.scrollLeft*t.scaleX,top:i.top-n.top+t.scrollDOM.scrollTop*t.scaleY,height:i.bottom-i.top}}drawCursor(t){if(this.cursor){let{scaleX:e,scaleY:i}=this.view;t?(this.cursor.style.left=t.left/e+"px",this.cursor.style.top=t.top/i+"px",this.cursor.style.height=t.height/i+"px"):this.cursor.style.left="-100000px"}}destroy(){this.cursor&&this.cursor.remove()}setDropPos(t){this.view.state.field(_r)!=t&&this.view.dispatch({effects:qr.of(t)})}},{eventObservers:{dragover(t){this.setDropPos(this.view.posAtCoords({x:t.clientX,y:t.clientY}))},dragleave(t){t.target!=this.view.contentDOM&&this.view.contentDOM.contains(t.relatedTarget)||this.setDropPos(null)},dragend(){this.setDropPos(null)},drop(){this.setDropPos(null)}}});function Qr(t,e,i,n,s){e.lastIndex=0;for(let r,o=t.iterRange(i,n),l=i;!o.next().done;l+=o.value.length)if(!o.lineBreak)for(;r=e.exec(o.value);)s(l+r.index,r)}class $r{constructor(t){const{regexp:e,decoration:i,decorate:n,boundary:s,maxLength:r=1e3}=t;if(!e.global)throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");if(this.regexp=e,n)this.addMatch=(t,e,i,s)=>n(s,i,i+t[0].length,t,e);else if("function"==typeof i)this.addMatch=(t,e,n,s)=>{let r=i(t,e,n);r&&s(n,n+t[0].length,r)};else{if(!i)throw new RangeError("Either 'decorate' or 'decoration' should be provided to MatchDecorator");this.addMatch=(t,e,n,s)=>s(n,n+t[0].length,i)}this.boundary=s,this.maxLength=r}createDeco(t){let e=new Nt,i=e.add.bind(e);for(let{from:e,to:n}of function(t,e){let i=t.visibleRanges;if(1==i.length&&i[0].from==t.viewport.from&&i[0].to==t.viewport.to)return i;let n=[];for(let{from:s,to:r}of i)s=Math.max(t.state.doc.lineAt(s).from,s-e),r=Math.min(t.state.doc.lineAt(r).to,r+e),n.length&&n[n.length-1].to>=s?n[n.length-1].to=r:n.push({from:s,to:r});return n}(t,this.maxLength))Qr(t.state.doc,this.regexp,e,n,(e,n)=>this.addMatch(n,t,e,i));return e.finish()}updateDeco(t,e){let i=1e9,n=-1;return t.docChanged&&t.changes.iterChanges((e,s,r,o)=>{o>=t.view.viewport.from&&r<=t.view.viewport.to&&(i=Math.min(r,i),n=Math.max(o,n))}),t.viewportMoved||n-i>1e3?this.createDeco(t.view):n>-1?this.updateRange(t.view,e.map(t.changes),i,n):e}updateRange(t,e,i,n){for(let s of t.visibleRanges){let r=Math.max(s.from,i),o=Math.min(s.to,n);if(o>=r){let i=t.state.doc.lineAt(r),n=i.toi.from;r--)if(this.boundary.test(i.text[r-1-i.from])){l=r;break}for(;oc.push(i.range(t,e));if(i==n)for(this.regexp.lastIndex=l-i.from;(h=this.regexp.exec(i.text))&&h.indexthis.addMatch(i,t,e,u));e=e.update({filterFrom:l,filterTo:a,filter:(t,e)=>ta,add:c})}}return e}}const Kr=null!=/x/.unicode?"gu":"g",jr=new RegExp("[\0-\b\n--Ÿ­؜​‎‏\u2028\u2029‭‮⁦⁧⁩\ufeff-]",Kr),Xr={0:"null",7:"bell",8:"backspace",10:"newline",11:"vertical tab",13:"carriage return",27:"escape",8203:"zero width space",8204:"zero width non-joiner",8205:"zero width joiner",8206:"left-to-right mark",8207:"right-to-left mark",8232:"line separator",8237:"left-to-right override",8238:"right-to-left override",8294:"left-to-right isolate",8295:"right-to-left isolate",8297:"pop directional isolate",8233:"paragraph separator",65279:"zero width no-break space",65532:"object replacement"};let Gr=null;const Yr=z.define({combine(t){let e=Dt(t,{render:null,specialChars:jr,addSpecialChars:null});return(e.replaceTabs=!function(){var t;if(null==Gr&&"undefined"!=typeof document&&document.body){let e=document.body.style;Gr=null!=(null!==(t=e.tabSize)&&void 0!==t?t:e.MozTabSize)}return Gr||!1}())&&(e.specialChars=new RegExp("\t|"+e.specialChars.source,Kr)),e.addSpecialChars&&(e.specialChars=new RegExp(e.specialChars.source+"|"+e.addSpecialChars.source,Kr)),e}});function Jr(t={}){return[Yr.of(t),Zr||(Zr=qi.fromClass(class{constructor(t){this.view=t,this.decorations=Te.none,this.decorationCache=Object.create(null),this.decorator=this.makeDecorator(t.state.facet(Yr)),this.decorations=this.decorator.createDeco(t)}makeDecorator(t){return new $r({regexp:t.specialChars,decoration:(e,i,n)=>{let{doc:s}=i.state,r=S(e[0],0);if(9==r){let t=s.lineAt(n),e=i.state.tabSize,r=Kt(t.text,e,n-t.from);return Te.replace({widget:new eo((e-r%e)*this.view.defaultCharacterWidth/this.view.scaleX)})}return this.decorationCache[r]||(this.decorationCache[r]=Te.replace({widget:new to(t,r)}))},boundary:t.replaceTabs?void 0:/[^]/})}update(t){let e=t.state.facet(Yr);t.startState.facet(Yr)!=e?(this.decorator=this.makeDecorator(e),this.decorations=this.decorator.createDeco(t.view)):this.decorations=this.decorator.updateDeco(t,this.decorations)}},{decorations:t=>t.decorations}))]}let Zr=null;class to extends Me{constructor(t,e){super(),this.options=t,this.code=e}eq(t){return t.code==this.code}toDOM(t){let e=function(t){return t>=32?"•":10==t?"␤":String.fromCharCode(9216+t)}(this.code),i=t.state.phrase("Control character")+" "+(Xr[this.code]||"0x"+this.code.toString(16)),n=this.options.render&&this.options.render(this.code,i,e);if(n)return n;let s=document.createElement("span");return s.textContent=e,s.title=i,s.setAttribute("aria-label",i),s.className="cm-specialChar",s}ignoreEvent(){return!1}}class eo extends Me{constructor(t){super(),this.width=t}eq(t){return t.width==this.width}toDOM(){let t=document.createElement("span");return t.textContent="\t",t.className="cm-tab",t.style.width=this.width+"px",t}ignoreEvent(){return!1}}const io=Te.line({class:"cm-activeLine"}),no=qi.fromClass(class{constructor(t){this.decorations=this.getDeco(t)}update(t){(t.docChanged||t.selectionSet)&&(this.decorations=this.getDeco(t.view))}getDeco(t){let e=-1,i=[];for(let n of t.state.selection.ranges){let s=t.lineBlockAt(n.head);s.from>e&&(i.push(io.range(s.from)),e=s.from)}return Te.set(i)}},{decorations:t=>t.decorations}),so=2e3;function ro(t,e){let i=t.posAtCoords({x:e.clientX,y:e.clientY},!1),n=t.state.doc.lineAt(i),s=i-n.from,r=s>so?-1:s==n.length?function(t,e){let i=t.coordsAtPos(t.viewport.from);return i?Math.round(Math.abs((i.left-e)/t.defaultCharacterWidth)):-1}(t,e.clientX):Kt(n.text,t.state.tabSize,i-n.from);return{line:n.number,col:r,off:s}}function oo(t,e){let i=ro(t,e),n=t.state.selection;return i?{update(t){if(t.docChanged){let e=t.changes.mapPos(t.startState.doc.line(i.line).from),s=t.state.doc.lineAt(e);i={line:s.number,col:i.col,off:Math.min(i.off,s.length)},n=n.map(t.changes)}},get(e,s,r){let o=ro(t,e);if(!o)return n;let l=function(t,e,i){let n=Math.min(e.line,i.line),s=Math.max(e.line,i.line),r=[];if(e.off>so||i.off>so||e.col<0||i.col<0){let o=Math.min(e.off,i.off),l=Math.max(e.off,i.off);for(let e=n;e<=s;e++){let i=t.doc.line(e);i.length<=l&&r.push(W.range(i.from+o,i.to+l))}}else{let o=Math.min(e.col,i.col),l=Math.max(e.col,i.col);for(let e=n;e<=s;e++){let i=t.doc.line(e),n=jt(i.text,o,t.tabSize,!0);if(n<0)r.push(W.cursor(i.to));else{let e=jt(i.text,l,t.tabSize);r.push(W.range(i.from+n,i.from+e))}}}return r}(t.state,i,o);return l.length?r?W.create(l.concat(n.ranges)):W.create(l):n}}:null}function lo(t){let e=(null==t?void 0:t.eventFilter)||(t=>t.altKey&&0==t.button);return pr.mouseSelectionStyle.of((t,i)=>e(i)?oo(t,i):null)}const ao={Alt:[18,t=>!!t.altKey],Control:[17,t=>!!t.ctrlKey],Shift:[16,t=>!!t.shiftKey],Meta:[91,t=>!!t.metaKey]},ho={style:"cursor: crosshair"};function co(t={}){let[e,i]=ao[t.key||"Alt"],n=qi.fromClass(class{constructor(t){this.view=t,this.isDown=!1}set(t){this.isDown!=t&&(this.isDown=t,this.view.update([]))}},{eventObservers:{keydown(t){this.set(t.keyCode==e||i(t))},keyup(t){t.keyCode!=e&&i(t)||this.set(!1)},mousemove(t){this.set(i(t))}}});return[n,pr.contentAttributes.of(t=>{var e;return(null===(e=t.plugin(n))||void 0===e?void 0:e.isDown)?ho:null})]}const uo="-10000px";class fo{constructor(t,e,i,n){this.facet=e,this.createTooltipView=i,this.removeTooltipView=n,this.input=t.state.facet(e),this.tooltips=this.input.filter(t=>t);let s=null;this.tooltipViews=this.tooltips.map(t=>s=i(t,s))}update(t,e){var i;let n=t.state.facet(this.facet),s=n.filter(t=>t);if(n===this.input){for(let e of this.tooltipViews)e.update&&e.update(t);return!1}let r=[],o=e?[]:null;for(let i=0;ie[i]=t),e.length=o.length),this.input=n,this.tooltips=s,this.tooltipViews=r,!0}}function po(t={}){return go.of(t)}function mo(t){let e=t.dom.ownerDocument.documentElement;return{top:0,left:0,bottom:e.clientHeight,right:e.clientWidth}}const go=z.define({combine:t=>{var e,i,n;return{position:ye.ios?"absolute":(null===(e=t.find(t=>t.position))||void 0===e?void 0:e.position)||"fixed",parent:(null===(i=t.find(t=>t.parent))||void 0===i?void 0:i.parent)||null,tooltipSpace:(null===(n=t.find(t=>t.tooltipSpace))||void 0===n?void 0:n.tooltipSpace)||mo}}}),vo=new WeakMap,wo=qi.fromClass(class{constructor(t){this.view=t,this.above=[],this.inView=!0,this.madeAbsolute=!1,this.lastTransaction=0,this.measureTimeout=-1;let e=t.state.facet(go);this.position=e.position,this.parent=e.parent,this.classes=t.themeClasses,this.createContainer(),this.measureReq={read:this.readMeasure.bind(this),write:this.writeMeasure.bind(this),key:this},this.resizeObserver="function"==typeof ResizeObserver?new ResizeObserver(()=>this.measureSoon()):null,this.manager=new fo(t,ko,(t,e)=>this.createTooltip(t,e),t=>{this.resizeObserver&&this.resizeObserver.unobserve(t.dom),t.dom.remove()}),this.above=this.manager.tooltips.map(t=>!!t.above),this.intersectionObserver="function"==typeof IntersectionObserver?new IntersectionObserver(t=>{Date.now()>this.lastTransaction-50&&t.length>0&&t[t.length-1].intersectionRatio<1&&this.measureSoon()},{threshold:[1]}):null,this.observeIntersection(),t.win.addEventListener("resize",this.measureSoon=this.measureSoon.bind(this)),this.maybeMeasure()}createContainer(){this.parent?(this.container=document.createElement("div"),this.container.style.position="relative",this.container.className=this.view.themeClasses,this.parent.appendChild(this.container)):this.container=this.view.dom}observeIntersection(){if(this.intersectionObserver){this.intersectionObserver.disconnect();for(let t of this.manager.tooltipViews)this.intersectionObserver.observe(t.dom)}}measureSoon(){this.measureTimeout<0&&(this.measureTimeout=setTimeout(()=>{this.measureTimeout=-1,this.maybeMeasure()},50))}update(t){t.transactions.length&&(this.lastTransaction=Date.now());let e=this.manager.update(t,this.above);e&&this.observeIntersection();let i=e||t.geometryChanged,n=t.state.facet(go);if(n.position!=this.position&&!this.madeAbsolute){this.position=n.position;for(let t of this.manager.tooltipViews)t.dom.style.position=this.position;i=!0}if(n.parent!=this.parent){this.parent&&this.container.remove(),this.parent=n.parent,this.createContainer();for(let t of this.manager.tooltipViews)this.container.appendChild(t.dom);i=!0}else this.parent&&this.view.themeClasses!=this.classes&&(this.classes=this.container.className=this.view.themeClasses);i&&this.maybeMeasure()}createTooltip(t,e){let i=t.create(this.view),n=e?e.dom:null;if(i.dom.classList.add("cm-tooltip"),t.arrow&&!i.dom.querySelector(".cm-tooltip > .cm-tooltip-arrow")){let t=document.createElement("div");t.className="cm-tooltip-arrow",i.dom.appendChild(t)}return i.dom.style.position=this.position,i.dom.style.top=uo,i.dom.style.left="0px",this.container.insertBefore(i.dom,n),i.mount&&i.mount(this.view),this.resizeObserver&&this.resizeObserver.observe(i.dom),i}destroy(){var t,e,i;this.view.win.removeEventListener("resize",this.measureSoon);for(let e of this.manager.tooltipViews)e.dom.remove(),null===(t=e.destroy)||void 0===t||t.call(e);this.parent&&this.container.remove(),null===(e=this.resizeObserver)||void 0===e||e.disconnect(),null===(i=this.intersectionObserver)||void 0===i||i.disconnect(),clearTimeout(this.measureTimeout)}readMeasure(){let t=1,e=1,i=!1;if("fixed"==this.position&&this.manager.tooltipViews.length){let{dom:t}=this.manager.tooltipViews[0];if(ye.safari){let e=t.getBoundingClientRect();i=Math.abs(e.top+1e4)>1||Math.abs(e.left)>1}else i=!!t.offsetParent&&t.offsetParent!=this.container.ownerDocument.body}if(i||"absolute"==this.position)if(this.parent){let i=this.parent.getBoundingClientRect();i.width&&i.height&&(t=i.width/this.parent.offsetWidth,e=i.height/this.parent.offsetHeight)}else({scaleX:t,scaleY:e}=this.view.viewState);let n=this.view.scrollDOM.getBoundingClientRect(),s=Zi(this.view);return{visible:{left:n.left+s.left,top:n.top+s.top,right:n.right-s.right,bottom:n.bottom-s.bottom},parent:this.parent?this.container.getBoundingClientRect():this.view.dom.getBoundingClientRect(),pos:this.manager.tooltips.map((t,e)=>{let i=this.manager.tooltipViews[e];return i.getCoords?i.getCoords(t.pos):this.view.coordsAtPos(t.pos)}),size:this.manager.tooltipViews.map(({dom:t})=>t.getBoundingClientRect()),space:this.view.state.facet(go).tooltipSpace(this.view),scaleX:t,scaleY:e,makeAbsolute:i}}writeMeasure(t){var e;if(t.makeAbsolute){this.madeAbsolute=!0,this.position="absolute";for(let t of this.manager.tooltipViews)t.dom.style.position="absolute"}let{visible:i,space:n,scaleX:s,scaleY:r}=t,o=[];for(let l=0;l=Math.min(i.bottom,n.bottom)||u.rightMath.min(i.right,n.right)+.1)){c.style.top=uo;continue}let d=a.arrow?h.dom.querySelector(".cm-tooltip-arrow"):null,p=d?7:0,m=f.right-f.left,g=null!==(e=vo.get(h))&&void 0!==e?e:f.bottom-f.top,v=h.offset||xo,w=this.view.textDirection==si.LTR,b=f.width>n.right-n.left?w?n.left:n.right-f.width:w?Math.max(n.left,Math.min(u.left-(d?14:0)+v.x,n.right-m)):Math.min(Math.max(n.left,u.left-m+(d?14:0)-v.x),n.right-m),y=this.above[l];!a.strictSide&&(y?u.top-g-p-v.yn.bottom)&&y==n.bottom-u.bottom>u.top-n.top&&(y=this.above[l]=!y);let x=(y?u.top-n.top:n.bottom-u.bottom)-p;if(xb&&t.topk&&(k=y?t.top-g-2-p:t.bottom+p+2);if("absolute"==this.position?(c.style.top=(k-t.parent.top)/r+"px",bo(c,(b-t.parent.left)/s)):(c.style.top=k/r+"px",bo(c,b/s)),d){let t=u.left+(w?v.x:-v.x)-(b+14-7);d.style.left=t/s+"px"}!0!==h.overlap&&o.push({left:b,top:k,right:S,bottom:k+g}),c.classList.toggle("cm-tooltip-above",y),c.classList.toggle("cm-tooltip-below",!y),h.positioned&&h.positioned(t.space)}}maybeMeasure(){if(this.manager.tooltips.length&&(this.view.inView&&this.view.requestMeasure(this.measureReq),this.inView!=this.view.inView&&(this.inView=this.view.inView,!this.inView)))for(let t of this.manager.tooltipViews)t.dom.style.top=uo}},{eventObservers:{scroll(){this.maybeMeasure()}}});function bo(t,e){let i=parseInt(t.style.left,10);(isNaN(i)||Math.abs(e-i)>1)&&(t.style.left=e+"px")}const yo=pr.baseTheme({".cm-tooltip":{zIndex:500,boxSizing:"border-box"},"&light .cm-tooltip":{border:"1px solid #bbb",backgroundColor:"#f5f5f5"},"&light .cm-tooltip-section:not(:first-child)":{borderTop:"1px solid #bbb"},"&dark .cm-tooltip":{backgroundColor:"#333338",color:"white"},".cm-tooltip-arrow":{height:"7px",width:"14px",position:"absolute",zIndex:-1,overflow:"hidden","&:before, &:after":{content:"''",position:"absolute",width:0,height:0,borderLeft:"7px solid transparent",borderRight:"7px solid transparent"},".cm-tooltip-above &":{bottom:"-7px","&:before":{borderTop:"7px solid #bbb"},"&:after":{borderTop:"7px solid #f5f5f5",bottom:"1px"}},".cm-tooltip-below &":{top:"-7px","&:before":{borderBottom:"7px solid #bbb"},"&:after":{borderBottom:"7px solid #f5f5f5",top:"1px"}}},"&dark .cm-tooltip .cm-tooltip-arrow":{"&:before":{borderTopColor:"#333338",borderBottomColor:"#333338"},"&:after":{borderTopColor:"transparent",borderBottomColor:"transparent"}}}),xo={x:0,y:0},ko=z.define({enables:[wo,yo]}),So=z.define({combine:t=>t.reduce((t,e)=>t.concat(e),[])});class Co{static create(t){return new Co(t)}constructor(t){this.view=t,this.mounted=!1,this.dom=document.createElement("div"),this.dom.classList.add("cm-tooltip-hover"),this.manager=new fo(t,So,(t,e)=>this.createHostedView(t,e),t=>t.dom.remove())}createHostedView(t,e){let i=t.create(this.view);return i.dom.classList.add("cm-tooltip-section"),this.dom.insertBefore(i.dom,e?e.dom.nextSibling:this.dom.firstChild),this.mounted&&i.mount&&i.mount(this.view),i}mount(t){for(let e of this.manager.tooltipViews)e.mount&&e.mount(t);this.mounted=!0}positioned(t){for(let e of this.manager.tooltipViews)e.positioned&&e.positioned(t)}update(t){this.manager.update(t)}destroy(){var t;for(let e of this.manager.tooltipViews)null===(t=e.destroy)||void 0===t||t.call(e)}passProp(t){let e;for(let i of this.manager.tooltipViews){let n=i[t];if(void 0!==n)if(void 0===e)e=n;else if(e!==n)return}return e}get offset(){return this.passProp("offset")}get getCoords(){return this.passProp("getCoords")}get overlap(){return this.passProp("overlap")}get resize(){return this.passProp("resize")}}const Ao=ko.compute([So],t=>{let e=t.facet(So);return 0===e.length?null:{pos:Math.min(...e.map(t=>t.pos)),end:Math.max(...e.map(t=>{var e;return null!==(e=t.end)&&void 0!==e?e:t.pos})),create:Co.create,above:e[0].above,arrow:e.some(t=>t.arrow)}}),Mo=z.define();class Oo{constructor(t,e,i,n,s,r){this.view=t,this.source=e,this.field=i,this.locked=n,this.setHover=s,this.hoverTime=r,this.hoverTimeout=-1,this.restartTimeout=-1,this.pending=null,this.lastMove={x:0,y:0,target:t.dom,time:0},this.checkHover=this.checkHover.bind(this),t.dom.addEventListener("mouseleave",this.mouseleave=this.mouseleave.bind(this)),t.dom.addEventListener("mousemove",this.mousemove=this.mousemove.bind(this))}update(t){this.pending&&(this.pending=null,clearTimeout(this.restartTimeout),this.restartTimeout=setTimeout(()=>this.startHover(),20))}get active(){return this.view.state.field(this.field)}checkHover(){if(this.hoverTimeout=-1,this.active.length)return;let t=Date.now()-this.lastMove.time;ti.bottom||e.xi.right+t.defaultCharacterWidth)return;let r=t.bidiSpans(t.state.doc.lineAt(n)).find(t=>t.from<=n&&t.to>=n),o=r&&r.dir==si.RTL?-1:1;s=e.x{if(e&&(!Array.isArray(e)||e.length)){let i=Array.isArray(e)?e:[e];n&&this.locked.set(i,n),t.dispatch({effects:this.setHover.of(i)})}};if(s&&"then"in s){let i=this.pending={pos:e};s.then(t=>{this.pending==i&&(this.pending=null,r(t))},e=>Hi(t.state,e,"hover tooltip"))}else r(s)}get tooltip(){let t=this.view.plugin(wo),e=t?t.manager.tooltips.findIndex(t=>t.create==Co.create):-1;return e>-1?t.manager.tooltipViews[e]:null}mousemove(t){var e,i;this.lastMove={x:t.clientX,y:t.clientY,target:t.target,time:Date.now()},this.hoverTimeout<0&&(this.hoverTimeout=setTimeout(this.checkHover,this.hoverTime));let{active:n,tooltip:s}=this;if(n.length&&!this.locked.has(n)&&s&&!function(t,e){let i,{left:n,right:s,top:r,bottom:o}=t.getBoundingClientRect();if(i=t.querySelector(".cm-tooltip-arrow")){let t=i.getBoundingClientRect();r=Math.min(t.top,r),o=Math.max(t.bottom,o)}return e.clientX>=n-To&&e.clientX<=s+To&&e.clientY>=r-To&&e.clientY<=o+To}(s.dom,t)||this.pending){let{pos:s}=n[0]||this.pending,r=null!==(i=null===(e=n[0])||void 0===e?void 0:e.end)&&void 0!==i?i:s;(s==r?this.view.posAtCoords(this.lastMove)==s:function(t,e,i,n,s){let r=t.scrollDOM.getBoundingClientRect(),o=t.documentTop+t.documentPadding.top+t.contentHeight;if(r.left>n||r.rights||Math.min(r.bottom,o)=e&&l<=i}(this.view,s,r,t.clientX,t.clientY))||(this.view.dispatch({effects:this.setHover.of([])}),this.pending=null)}}mouseleave(t){clearTimeout(this.hoverTimeout),this.hoverTimeout=-1;let{active:e}=this;if(e.length&&!this.locked.has(e)){let{tooltip:e}=this;e&&e.dom.contains(t.relatedTarget)?this.watchTooltipLeave(e.dom):this.view.dispatch({effects:this.setHover.of([])})}}watchTooltipLeave(t){let e=i=>{t.removeEventListener("mouseleave",e);let{active:n}=this;!n.length||this.locked.has(n)||this.view.dom.contains(i.relatedTarget)||this.view.dispatch({effects:this.setHover.of([])})};t.addEventListener("mouseleave",e)}destroy(){clearTimeout(this.hoverTimeout),clearTimeout(this.restartTimeout),this.view.dom.removeEventListener("mouseleave",this.mouseleave),this.view.dom.removeEventListener("mousemove",this.mousemove)}}const To=4;function Do(t,e={}){let i=gt.define(),n=new WeakMap,s=K.define({create:()=>[],update(t,r){let o=n.get(t);if(t.length&&(e.hideOnChange&&(r.docChanged||r.selection)||o&&o(r)?t=[]:e.hideOn&&(t=t.filter(t=>!e.hideOn(r,t)))),r.docChanged&&t.length){let e=[];for(let i of t){let t=r.changes.mapPos(i.pos,-1,O.TrackDel);if(null!=t){let n=Object.assign(Object.create(null),i);n.pos=t,null!=n.end&&(n.end=r.changes.mapPos(n.end)),e.push(n)}}t=e}for(let e of r.effects)e.is(i)&&(t=e.value,o=void 0),(e.is(Po)&&!e.value||e.value==s)&&(t=[]);return t.length&&o&&n.set(t,o),t},provide:t=>So.from(t)});const r=qi.define(r=>new Oo(r,t,s,n,i,e.hoverTime||300));return{active:s,extension:[s,r,Mo.of(r),Ao]}}function Ro(t,e){let i=t.plugin(wo);if(!i)return null;let n=i.manager.tooltips.indexOf(e);return n<0?null:i.manager.tooltipViews[n]}const Po=gt.define(),Bo=z.define({combine(t){let e,i;for(let n of t)e=e||n.topContainer,i=i||n.bottomContainer;return{topContainer:e,bottomContainer:i}}});function Eo(t,e){let i=t.plugin(Lo),n=i?i.specs.indexOf(e):-1;return n>-1?i.panels[n]:null}const Lo=qi.fromClass(class{constructor(t){this.input=t.state.facet(Wo),this.specs=this.input.filter(t=>t),this.panels=this.specs.map(e=>e(t));let e=t.state.facet(Bo);this.top=new Io(t,!0,e.topContainer),this.bottom=new Io(t,!1,e.bottomContainer),this.top.sync(this.panels.filter(t=>t.top)),this.bottom.sync(this.panels.filter(t=>!t.top));for(let t of this.panels)t.dom.classList.add("cm-panel"),t.mount&&t.mount()}update(t){let e=t.state.facet(Bo);this.top.container!=e.topContainer&&(this.top.sync([]),this.top=new Io(t.view,!0,e.topContainer)),this.bottom.container!=e.bottomContainer&&(this.bottom.sync([]),this.bottom=new Io(t.view,!1,e.bottomContainer)),this.top.syncClasses(),this.bottom.syncClasses();let i=t.state.facet(Wo);if(i!=this.input){let e=i.filter(t=>t),n=[],s=[],r=[],o=[];for(let i of e){let e,l=this.specs.indexOf(i);l<0?(e=i(t.view),o.push(e)):(e=this.panels[l],e.update&&e.update(t)),n.push(e),(e.top?s:r).push(e)}this.specs=e,this.panels=n,this.top.sync(s),this.bottom.sync(r);for(let t of o)t.dom.classList.add("cm-panel"),t.mount&&t.mount()}else for(let e of this.panels)e.update&&e.update(t)}destroy(){this.top.sync([]),this.bottom.sync([])}},{provide:t=>pr.scrollMargins.of(e=>{let i=e.plugin(t);return i&&{top:i.top.scrollMargin(),bottom:i.bottom.scrollMargin()}})});class Io{constructor(t,e,i){this.view=t,this.top=e,this.container=i,this.dom=void 0,this.classes="",this.panels=[],this.syncClasses()}sync(t){for(let e of this.panels)e.destroy&&t.indexOf(e)<0&&e.destroy();this.panels=t,this.syncDOM()}syncDOM(){if(0==this.panels.length)return void(this.dom&&(this.dom.remove(),this.dom=void 0));if(!this.dom){this.dom=document.createElement("div"),this.dom.className=this.top?"cm-panels cm-panels-top":"cm-panels cm-panels-bottom",this.dom.style[this.top?"top":"bottom"]="0";let t=this.container||this.view.dom;t.insertBefore(this.dom,this.top?t.firstChild:null)}let t=this.dom.firstChild;for(let e of this.panels)if(e.dom.parentNode==this.dom){for(;t!=e.dom;)t=No(t);t=t.nextSibling}else this.dom.insertBefore(e.dom,t);for(;t;)t=No(t)}scrollMargin(){return!this.dom||this.container?0:Math.max(0,this.top?this.dom.getBoundingClientRect().bottom-Math.max(0,this.view.scrollDOM.getBoundingClientRect().top):Math.min(innerHeight,this.view.scrollDOM.getBoundingClientRect().bottom)-this.dom.getBoundingClientRect().top)}syncClasses(){if(this.container&&this.classes!=this.view.themeClasses){for(let t of this.classes.split(" "))t&&this.container.classList.remove(t);for(let t of(this.classes=this.view.themeClasses).split(" "))t&&this.container.classList.add(t)}}}function No(t){let e=t.nextSibling;return t.remove(),e}const Wo=z.define({enables:Lo});function Ho(t,e){let i,n=new Promise(t=>i=t),s=t=>function(t,e,i){let n=e.content?e.content(t,()=>o(null)):null;if(!n){if(n=le("form"),e.input){let t=le("input",e.input);/^(text|password|number|email|tel|url)$/.test(t.type)&&t.classList.add("cm-textfield"),t.name||(t.name="input"),n.appendChild(le("label",(e.label||"")+": ",t))}else n.appendChild(document.createTextNode(e.label||""));n.appendChild(document.createTextNode(" ")),n.appendChild(le("button",{class:"cm-button",type:"submit"},e.submitLabel||"OK"))}let s="FORM"==n.nodeName?[n]:n.querySelectorAll("form");for(let t=0;t{27==t.keyCode?(t.preventDefault(),o(null)):13==t.keyCode&&(t.preventDefault(),o(e))}),e.addEventListener("submit",t=>{t.preventDefault(),o(e)})}let r=le("div",n,le("button",{onclick:()=>o(null),"aria-label":t.state.phrase("close"),class:"cm-dialog-close",type:"button"},["×"]));e.class&&(r.className=e.class);function o(e){r.contains(r.ownerDocument.activeElement)&&t.focus(),i(e)}return r.classList.add("cm-dialog"),{dom:r,top:e.top,mount:()=>{if(e.focus){let t;t="string"==typeof e.focus?n.querySelector(e.focus):n.querySelector("input")||n.querySelector("button"),t&&"select"in t?t.select():t&&"focus"in t&&t.focus()}}}}(t,e,i);t.state.field(Vo,!1)?t.dispatch({effects:zo.of(s)}):t.dispatch({effects:gt.appendConfig.of(Vo.init(()=>[s]))});let r=Fo.of(s);return{close:r,result:n.then(e=>((t.win.queueMicrotask||(e=>t.win.setTimeout(e,10)))(()=>{t.state.field(Vo).indexOf(s)>-1&&t.dispatch({effects:r})}),e))}}const Vo=K.define({create:()=>[],update(t,e){for(let i of e.effects)i.is(zo)?t=[i.value].concat(t):i.is(Fo)&&(t=t.filter(t=>t!=i.value));return t},provide:t=>Wo.computeN([t],e=>e.field(t))}),zo=gt.define(),Fo=gt.define();class qo extends Rt{compare(t){return this==t||this.constructor==t.constructor&&this.eq(t)}eq(t){return!1}destroy(t){}}qo.prototype.elementClass="",qo.prototype.toDOM=void 0,qo.prototype.mapMode=O.TrackBefore,qo.prototype.startSide=qo.prototype.endSide=-1,qo.prototype.point=!0;const _o=z.define(),Uo=z.define(),Qo={class:"",renderEmptyElements:!1,elementStyle:"",markers:()=>It.empty,lineMarker:()=>null,widgetMarker:()=>null,lineMarkerChange:null,initialSpacer:null,updateSpacer:null,domEventHandlers:{},side:"before"},$o=z.define();function Ko(t){return[Xo(),$o.of({...Qo,...t})]}const jo=z.define({combine:t=>t.some(t=>t)});function Xo(t){let e=[Go];return t&&!1===t.fixed&&e.push(jo.of(!0)),e}const Go=qi.fromClass(class{constructor(t){this.view=t,this.domAfter=null,this.prevViewport=t.viewport,this.dom=document.createElement("div"),this.dom.className="cm-gutters cm-gutters-before",this.dom.setAttribute("aria-hidden","true"),this.dom.style.minHeight=this.view.contentHeight/this.view.scaleY+"px",this.gutters=t.state.facet($o).map(e=>new tl(t,e)),this.fixed=!t.state.facet(jo);for(let t of this.gutters)"after"==t.config.side?this.getDOMAfter().appendChild(t.dom):this.dom.appendChild(t.dom);this.fixed&&(this.dom.style.position="sticky"),this.syncGutters(!1),t.scrollDOM.insertBefore(this.dom,t.contentDOM)}getDOMAfter(){return this.domAfter||(this.domAfter=document.createElement("div"),this.domAfter.className="cm-gutters cm-gutters-after",this.domAfter.setAttribute("aria-hidden","true"),this.domAfter.style.minHeight=this.view.contentHeight/this.view.scaleY+"px",this.domAfter.style.position=this.fixed?"sticky":"",this.view.scrollDOM.appendChild(this.domAfter)),this.domAfter}update(t){if(this.updateGutters(t)){let e=this.prevViewport,i=t.view.viewport,n=Math.min(e.to,i.to)-Math.max(e.from,i.from);this.syncGutters(n<.8*(i.to-i.from))}if(t.geometryChanged){let t=this.view.contentHeight/this.view.scaleY+"px";this.dom.style.minHeight=t,this.domAfter&&(this.domAfter.style.minHeight=t)}this.view.state.facet(jo)!=!this.fixed&&(this.fixed=!this.fixed,this.dom.style.position=this.fixed?"sticky":"",this.domAfter&&(this.domAfter.style.position=this.fixed?"sticky":"")),this.prevViewport=t.view.viewport}syncGutters(t){let e=this.dom.nextSibling;t&&(this.dom.remove(),this.domAfter&&this.domAfter.remove());let i=It.iter(this.view.state.facet(_o),this.view.viewport.from),n=[],s=this.gutters.map(t=>new Zo(t,this.view.viewport,-this.view.documentPadding.top));for(let t of this.view.viewportLineBlocks)if(n.length&&(n=[]),Array.isArray(t.type)){let e=!0;for(let r of t.type)if(r.type==Oe.Text&&e){Jo(i,n,r.from);for(let t of s)t.line(this.view,r,n);e=!1}else if(r.widget)for(let t of s)t.widget(this.view,r)}else if(t.type==Oe.Text){Jo(i,n,t.from);for(let e of s)e.line(this.view,t,n)}else if(t.widget)for(let e of s)e.widget(this.view,t);for(let t of s)t.finish();t&&(this.view.scrollDOM.insertBefore(this.dom,e),this.domAfter&&this.view.scrollDOM.appendChild(this.domAfter))}updateGutters(t){let e=t.startState.facet($o),i=t.state.facet($o),n=t.docChanged||t.heightChanged||t.viewportChanged||!It.eq(t.startState.facet(_o),t.state.facet(_o),t.view.viewport.from,t.view.viewport.to);if(e==i)for(let e of this.gutters)e.update(t)&&(n=!0);else{n=!0;let s=[];for(let n of i){let i=e.indexOf(n);i<0?s.push(new tl(this.view,n)):(this.gutters[i].update(t),s.push(this.gutters[i]))}for(let t of this.gutters)t.dom.remove(),s.indexOf(t)<0&&t.destroy();for(let t of s)"after"==t.config.side?this.getDOMAfter().appendChild(t.dom):this.dom.appendChild(t.dom);this.gutters=s}return n}destroy(){for(let t of this.gutters)t.destroy();this.dom.remove(),this.domAfter&&this.domAfter.remove()}},{provide:t=>pr.scrollMargins.of(e=>{let i=e.plugin(t);if(!i||0==i.gutters.length||!i.fixed)return null;let n=i.dom.offsetWidth*e.scaleX,s=i.domAfter?i.domAfter.offsetWidth*e.scaleX:0;return e.textDirection==si.LTR?{left:n,right:s}:{right:n,left:s}})});function Yo(t){return Array.isArray(t)?t:[t]}function Jo(t,e,i){for(;t.value&&t.from<=i;)t.from==i&&e.push(t.value),t.next()}class Zo{constructor(t,e,i){this.gutter=t,this.height=i,this.i=0,this.cursor=It.iter(t.markers,e.from)}addElement(t,e,i){let{gutter:n}=this,s=(e.top-this.height)/t.scaleY,r=e.height/t.scaleY;if(this.i==n.elements.length){let e=new el(t,r,s,i);n.elements.push(e),n.dom.appendChild(e.dom)}else n.elements[this.i].update(t,r,s,i);this.height=e.bottom,this.i++}line(t,e,i){let n=[];Jo(this.cursor,n,e.from),i.length&&(n=n.concat(i));let s=this.gutter.config.lineMarker(t,e,n);s&&n.unshift(s);let r=this.gutter;(0!=n.length||r.config.renderEmptyElements)&&this.addElement(t,e,n)}widget(t,e){let i=this.gutter.config.widgetMarker(t,e.widget,e),n=i?[i]:null;for(let i of t.state.facet(Uo)){let s=i(t,e.widget,e);s&&(n||(n=[])).push(s)}n&&this.addElement(t,e,n)}finish(){let t=this.gutter;for(;t.elements.length>this.i;){let e=t.elements.pop();t.dom.removeChild(e.dom),e.destroy()}}}class tl{constructor(t,e){this.view=t,this.config=e,this.elements=[],this.spacer=null,this.dom=document.createElement("div"),this.dom.className="cm-gutter"+(this.config.class?" "+this.config.class:"");for(let i in e.domEventHandlers)this.dom.addEventListener(i,n=>{let s,r=n.target;if(r!=this.dom&&this.dom.contains(r)){for(;r.parentNode!=this.dom;)r=r.parentNode;let t=r.getBoundingClientRect();s=(t.top+t.bottom)/2}else s=n.clientY;let o=t.lineBlockAtHeight(s-t.documentTop);e.domEventHandlers[i](t,o,n)&&n.preventDefault()});this.markers=Yo(e.markers(t)),e.initialSpacer&&(this.spacer=new el(t,0,0,[e.initialSpacer(t)]),this.dom.appendChild(this.spacer.dom),this.spacer.dom.style.cssText+="visibility: hidden; pointer-events: none")}update(t){let e=this.markers;if(this.markers=Yo(this.config.markers(t.view)),this.spacer&&this.config.updateSpacer){let e=this.config.updateSpacer(this.spacer.markers[0],t);e!=this.spacer.markers[0]&&this.spacer.update(t.view,0,0,[e])}let i=t.view.viewport;return!It.eq(this.markers,e,i.from,i.to)||!!this.config.lineMarkerChange&&this.config.lineMarkerChange(t)}destroy(){for(let t of this.elements)t.destroy()}}class el{constructor(t,e,i,n){this.height=-1,this.above=0,this.markers=[],this.dom=document.createElement("div"),this.dom.className="cm-gutterElement",this.update(t,e,i,n)}update(t,e,i,n){this.height!=e&&(this.height=e,this.dom.style.height=e+"px"),this.above!=i&&(this.dom.style.marginTop=(this.above=i)?i+"px":""),function(t,e){if(t.length!=e.length)return!1;for(let i=0;iDt(t,{formatNumber:String,domEventHandlers:{}},{domEventHandlers(t,e){let i=Object.assign({},t);for(let t in e){let n=i[t],s=e[t];i[t]=n?(t,e,i)=>n(t,e,i)||s(t,e,i):s}return i}})});class rl extends qo{constructor(t){super(),this.number=t}eq(t){return this.number==t.number}toDOM(){return document.createTextNode(this.number)}}function ol(t,e){return t.state.facet(sl).formatNumber(e,t.state)}const ll=$o.compute([sl],t=>({class:"cm-lineNumbers",renderEmptyElements:!1,markers:t=>t.state.facet(il),lineMarker:(t,e,i)=>i.some(t=>t.toDOM)?null:new rl(ol(t,t.state.doc.lineAt(e.from).number)),widgetMarker:(t,e,i)=>{for(let n of t.state.facet(nl)){let s=n(t,e,i);if(s)return s}return null},lineMarkerChange:t=>t.startState.facet(sl)!=t.state.facet(sl),initialSpacer:t=>new rl(ol(t,hl(t.state.doc.lines))),updateSpacer(t,e){let i=ol(e.view,hl(e.view.state.doc.lines));return i==t.number?t:new rl(i)},domEventHandlers:t.facet(sl).domEventHandlers,side:"before"}));function al(t={}){return[sl.of(t),Xo(),ll]}function hl(t){let e=9;for(;e{let e=[],i=-1;for(let n of t.selection.ranges){let s=t.doc.lineAt(n.head).from;s>i&&(i=s,e.push(cl.range(s)))}return It.of(e)});const fl=1024;let dl=0;class pl{constructor(t,e){this.from=t,this.to=e}}class ml{constructor(t={}){this.id=dl++,this.perNode=!!t.perNode,this.deserialize=t.deserialize||(()=>{throw new Error("This node type doesn't define a deserialize function")}),this.combine=t.combine||null}add(t){if(this.perNode)throw new RangeError("Can't add per-node props to node types");return"function"!=typeof t&&(t=wl.match(t)),e=>{let i=t(e);return void 0===i?null:[this,i]}}}ml.closedBy=new ml({deserialize:t=>t.split(" ")}),ml.openedBy=new ml({deserialize:t=>t.split(" ")}),ml.group=new ml({deserialize:t=>t.split(" ")}),ml.isolate=new ml({deserialize:t=>{if(t&&"rtl"!=t&&"ltr"!=t&&"auto"!=t)throw new RangeError("Invalid value for isolate: "+t);return t||"auto"}}),ml.contextHash=new ml({perNode:!0}),ml.lookAhead=new ml({perNode:!0}),ml.mounted=new ml({perNode:!0});class gl{constructor(t,e,i,n=!1){this.tree=t,this.overlay=e,this.parser=i,this.bracketed=n}static get(t){return t&&t.props&&t.props[ml.mounted.id]}}const vl=Object.create(null);class wl{constructor(t,e,i,n=0){this.name=t,this.props=e,this.id=i,this.flags=n}static define(t){let e=t.props&&t.props.length?Object.create(null):vl,i=(t.top?1:0)|(t.skipped?2:0)|(t.error?4:0)|(null==t.name?8:0),n=new wl(t.name||"",e,t.id,i);if(t.props)for(let i of t.props)if(Array.isArray(i)||(i=i(n)),i){if(i[0].perNode)throw new RangeError("Can't store a per-node prop on a node type");e[i[0].id]=i[1]}return n}prop(t){return this.props[t.id]}get isTop(){return(1&this.flags)>0}get isSkipped(){return(2&this.flags)>0}get isError(){return(4&this.flags)>0}get isAnonymous(){return(8&this.flags)>0}is(t){if("string"==typeof t){if(this.name==t)return!0;let e=this.prop(ml.group);return!!e&&e.indexOf(t)>-1}return this.id==t}static match(t){let e=Object.create(null);for(let i in t)for(let n of i.split(" "))e[n]=t[i];return t=>{for(let i=t.prop(ml.group),n=-1;n<(i?i.length:0);n++){let s=e[n<0?t.name:i[n]];if(s)return s}}}}wl.none=new wl("",Object.create(null),0,8);class bl{constructor(t){this.types=t;for(let e=0;e=e){let o=new Dl(r.tree,r.overlay[0].from+t.from,-1,t);(s||(s=[n])).push(Ol(o,e,i,!1))}}return s?Ll(s):n}(this,t,e)}iterate(t){let{enter:e,leave:i,from:n=0,to:s=this.length}=t,r=t.mode||0,o=(r&kl.IncludeAnonymous)>0;for(let t=this.cursor(r|kl.IncludeAnonymous);;){let r=!1;if(t.from<=s&&t.to>=n&&(!o&&t.type.isAnonymous||!1!==e(t))){if(t.firstChild())continue;r=!0}for(;r&&i&&(o||!t.type.isAnonymous)&&i(t),!t.nextSibling();){if(!t.parent())return;r=!0}}}prop(t){return t.perNode?this.props?this.props[t.id]:void 0:this.type.prop(t)}get propValues(){let t=[];if(this.props)for(let e in this.props)t.push([+e,this.props[e]]);return t}balance(t={}){return this.children.length<=8?this:zl(wl.none,this.children,this.positions,0,this.children.length,0,this.length,(t,e,i)=>new Sl(this.type,t,e,i,this.propValues),t.makeTree||((t,e,i)=>new Sl(wl.none,t,e,i)))}static build(t){return function(t){var e;let{buffer:i,nodeSet:n,maxBufferLength:s=fl,reused:r=[],minRepeatType:o=n.types.length}=t,l=Array.isArray(i)?new Cl(i,i.length):i,a=n.types,h=0,c=0;function u(t,e,i,w,b,y){let{id:x,start:k,end:S,size:C}=l,A=c,M=h;if(C<0){if(l.next(),-1==C){let e=r[x];return i.push(e),void w.push(k-t)}if(-3==C)return void(h=x);if(-4==C)return void(c=x);throw new RangeError(`Unrecognized record size: ${C}`)}let O,T,D=a[x],R=k-t;if(S-k<=s&&(T=g(l.pos-e,b))){let e=new Uint16Array(T.size-T.skip),i=l.pos-T.size,s=e.length;for(;l.pos>i;)s=v(T.start,e,s);O=new Al(e,S-T.start,n),R=T.start-t}else{let t=l.pos-C;l.next();let e=[],i=[],n=x>=o?x:-1,r=0,a=S;for(;l.pos>t;)n>=0&&l.id==n&&l.size>=0?(l.end<=a-s&&(p(e,i,k,r,l.end,a,n,A,M),r=e.length,a=l.end),l.next()):y>2500?f(k,t,e,i):u(k,t,e,i,n,y+1);if(n>=0&&r>0&&r-1&&r>0){let t=d(D,M);O=zl(D,e,i,0,e.length,0,S-k,t,t)}else O=m(D,e,i,S-k,A-S,M)}i.push(O),w.push(R)}function f(t,e,i,r){let o=[],a=0,h=-1;for(;l.pos>e;){let{id:t,start:e,end:i,size:n}=l;if(n>4)l.next();else{if(h>-1&&e=0;t-=3)e[i++]=o[t],e[i++]=o[t+1]-s,e[i++]=o[t+2]-s,e[i++]=i;i.push(new Al(e,o[2]-s,n)),r.push(s-t)}}function d(t,e){return(i,n,s)=>{let r,o,l=0,a=i.length-1;if(a>=0&&(r=i[a])instanceof Sl){if(!a&&r.type==t&&r.length==s)return r;(o=r.prop(ml.lookAhead))&&(l=n[a]+r.length+o)}return m(t,i,n,s,l,e)}}function p(t,e,i,s,r,o,l,a,h){let c=[],u=[];for(;t.length>s;)c.push(t.pop()),u.push(e.pop()+i-r);t.push(m(n.types[l],c,u,o-r,a-o,h)),e.push(r-i)}function m(t,e,i,n,s,r,o){if(r){let t=[ml.contextHash,r];o=o?[t].concat(o):[t]}if(s>25){let t=[ml.lookAhead,s];o=o?[t].concat(o):[t]}return new Sl(t,e,i,n,o)}function g(t,e){let i=l.fork(),n=0,r=0,a=0,h=i.end-s,c={size:0,start:0,skip:0};t:for(let s=i.pos-t;i.pos>s;){let t=i.size;if(i.id==e&&t>=0){c.size=n,c.start=r,c.skip=a,a+=4,n+=4,i.next();continue}let l=i.pos-t;if(t<0||l=o?4:0,f=i.start;for(i.next();i.pos>l;){if(i.size<0){if(-3!=i.size&&-4!=i.size)break t;u+=4}else i.id>=o&&(u+=4);i.next()}r=f,n+=t,a+=u}return(e<0||n==t)&&(c.size=n,c.start=r,c.skip=a),c.size>4?c:void 0}function v(t,e,i){let{id:n,start:s,end:r,size:a}=l;if(l.next(),a>=0&&n4){let n=l.pos-(a-4);for(;l.pos>n;)i=v(t,e,i)}e[--i]=o,e[--i]=r-t,e[--i]=s-t,e[--i]=n}else-3==a?h=n:-4==a&&(c=n);return i}let w=[],b=[];for(;l.pos>0;)u(t.start||0,t.bufferStart||0,w,b,-1,0);let y=null!==(e=t.length)&&void 0!==e?e:w.length?b[0]+w[0].length:0;return new Sl(a[t.topID],w.reverse(),b.reverse(),y)}(t)}}Sl.empty=new Sl(wl.none,[],[],0);class Cl{constructor(t,e){this.buffer=t,this.index=e}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}get pos(){return this.index}next(){this.index-=4}fork(){return new Cl(this.buffer,this.index)}}class Al{constructor(t,e,i){this.buffer=t,this.length=e,this.set=i}get type(){return wl.none}toString(){let t=[];for(let e=0;e0));l=r[l+3]);return o}slice(t,e,i){let n=this.buffer,s=new Uint16Array(e-t),r=0;for(let o=t,l=0;o=e&&ie;case 1:return i<=e&&n>e;case 2:return n>e;case 4:return!0}}function Ol(t,e,i,n){for(var s;t.from==t.to||(i<1?t.from>=e:t.from>e)||(i>-1?t.to<=e:t.to0?o.length:-1;t!=a;t+=e){let a,h=o[t],c=l[t]+r.from;if(s&kl.EnterBracketed&&h instanceof Sl&&(a=gl.get(h))&&!a.overlay&&a.bracketed&&i>=c&&i<=c+h.length||Ml(n,i,c,c+h.length))if(h instanceof Al){if(s&kl.ExcludeBuffers)continue;let o=h.findChild(0,h.buffer.length,e,i-c,n);if(o>-1)return new El(new Bl(r,h,t,c),null,o)}else if(s&kl.IncludeAnonymous||!h.type.isAnonymous||Wl(h)){let o;if(!(s&kl.IgnoreMounts)&&(o=gl.get(h))&&!o.overlay)return new Dl(o.tree,c,t,r);let l=new Dl(h,c,t,r);return s&kl.IncludeAnonymous||!l.type.isAnonymous?l:l.nextChild(e<0?h.children.length-1:0,e,i,n,s)}}if(s&kl.IncludeAnonymous||!r.type.isAnonymous)return null;if(t=r.index>=0?r.index+e:e<0?-1:r._parent._tree.children.length,r=r._parent,!r)return null}}get firstChild(){return this.nextChild(0,1,0,4)}get lastChild(){return this.nextChild(this._tree.children.length-1,-1,0,4)}childAfter(t){return this.nextChild(0,1,t,2)}childBefore(t){return this.nextChild(this._tree.children.length-1,-1,t,-2)}prop(t){return this._tree.prop(t)}enter(t,e,i=0){let n;if(!(i&kl.IgnoreOverlays)&&(n=gl.get(this._tree))&&n.overlay){let s=t-this.from,r=i&kl.EnterBracketed&&n.bracketed;for(let{from:t,to:i}of n.overlay)if((e>0||r?t<=s:t=s:i>s))return new Dl(n.tree,n.overlay[0].from+this.from,-1,this)}return this.nextChild(0,1,t,e,i)}nextSignificantParent(){let t=this;for(;t.type.isAnonymous&&t._parent;)t=t._parent;return t}get parent(){return this._parent?this._parent.nextSignificantParent():null}get nextSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index+1,1,0,4):null}get prevSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index-1,-1,0,4):null}get tree(){return this._tree}toTree(){return this._tree}toString(){return this._tree.toString()}}function Rl(t,e,i,n){let s=t.cursor(),r=[];if(!s.firstChild())return r;if(null!=i)for(let t=!1;!t;)if(t=s.type.is(i),!s.nextSibling())return r;for(;;){if(null!=n&&s.type.is(n))return r;if(s.type.is(e)&&r.push(s.node),!s.nextSibling())return null==n?r:[]}}function Pl(t,e,i=e.length-1){for(let n=t;i>=0;n=n.parent){if(!n)return!1;if(!n.type.isAnonymous){if(e[i]&&e[i]!=n.name)return!1;i--}}return!0}class Bl{constructor(t,e,i,n){this.parent=t,this.buffer=e,this.index=i,this.start=n}}class El extends Tl{get name(){return this.type.name}get from(){return this.context.start+this.context.buffer.buffer[this.index+1]}get to(){return this.context.start+this.context.buffer.buffer[this.index+2]}constructor(t,e,i){super(),this.context=t,this._parent=e,this.index=i,this.type=t.buffer.set.types[t.buffer.buffer[i]]}child(t,e,i){let{buffer:n}=this.context,s=n.findChild(this.index+4,n.buffer[this.index+3],t,e-this.context.start,i);return s<0?null:new El(this.context,this,s)}get firstChild(){return this.child(1,0,4)}get lastChild(){return this.child(-1,0,4)}childAfter(t){return this.child(1,t,2)}childBefore(t){return this.child(-1,t,-2)}prop(t){return this.type.prop(t)}enter(t,e,i=0){if(i&kl.ExcludeBuffers)return null;let{buffer:n}=this.context,s=n.findChild(this.index+4,n.buffer[this.index+3],e>0?1:-1,t-this.context.start,e);return s<0?null:new El(this.context,this,s)}get parent(){return this._parent||this.context.parent.nextSignificantParent()}externalSibling(t){return this._parent?null:this.context.parent.nextChild(this.context.index+t,t,0,4)}get nextSibling(){let{buffer:t}=this.context,e=t.buffer[this.index+3];return e<(this._parent?t.buffer[this._parent.index+3]:t.buffer.length)?new El(this.context,this._parent,e):this.externalSibling(1)}get prevSibling(){let{buffer:t}=this.context,e=this._parent?this._parent.index+4:0;return this.index==e?this.externalSibling(-1):new El(this.context,this._parent,t.findChild(e,this.index,-1,0,4))}get tree(){return null}toTree(){let t=[],e=[],{buffer:i}=this.context,n=this.index+4,s=i.buffer[this.index+3];if(s>n){let r=i.buffer[this.index+1];t.push(i.slice(n,s,r)),e.push(0)}return new Sl(this.type,t,e,this.to-this.from)}toString(){return this.context.buffer.childString(this.index)}}function Ll(t){if(!t.length)return null;let e=0,i=t[0];for(let n=1;ni.from||s.to0){if(this.index-1)for(let n=e+t,s=t<0?-1:i._tree.children.length;n!=s;n+=t){let t=i._tree.children[n];if(this.mode&kl.IncludeAnonymous||t instanceof Al||!t.type.isAnonymous||Wl(t))return!1}return!0}move(t,e){if(e&&this.enterChild(t,0,4))return!0;for(;;){if(this.sibling(t))return!0;if(this.atLastNode(t)||!this.parent())return!1}}next(t=!0){return this.move(1,t)}prev(t=!0){return this.move(-1,t)}moveTo(t,e=0){for(;(this.from==this.to||(e<1?this.from>=t:this.from>t)||(e>-1?this.to<=t:this.to=0;){for(let r=t;r;r=r._parent)if(r.index==n){if(n==this.index)return r;e=r,i=s+1;break t}n=this.stack[--s]}for(let t=i;t=0;s--){if(s<0)return Pl(this._tree,t,n);let r=i[e.buffer[this.stack[s]]];if(!r.isAnonymous){if(t[n]&&t[n]!=r.name)return!1;n--}}return!0}}function Wl(t){return t.children.some(t=>t instanceof Al||!t.type.isAnonymous||Wl(t))}const Hl=new WeakMap;function Vl(t,e){if(!t.isAnonymous||e instanceof Al||e.type!=t)return 1;let i=Hl.get(e);if(null==i){i=1;for(let n of e.children){if(n.type!=t||!(n instanceof Sl)){i=1;break}i+=Vl(t,n)}Hl.set(e,i)}return i}function zl(t,e,i,n,s,r,o,l,a){let h=0;for(let i=n;i=c)break;p+=e}if(h==s+1){if(p>c){let t=i[s];e(t.children,t.positions,0,t.children.length,n[s]+l);continue}u.push(i[s])}else{let e=n[h-1]+i[h-1].length-d;u.push(zl(t,i,n,s,h,d,e,null,a))}f.push(d+l-r)}}(e,i,n,s,0),(l||a)(u,f,o)}class Fl{constructor(t,e,i,n,s=!1,r=!1){this.from=t,this.to=e,this.tree=i,this.offset=n,this.open=(s?1:0)|(r?2:0)}get openStart(){return(1&this.open)>0}get openEnd(){return(2&this.open)>0}static addTree(t,e=[],i=!1){let n=[new Fl(0,t.length,t,0,!1,i)];for(let i of e)i.to>t.length&&n.push(i);return n}static applyChanges(t,e,i=128){if(!e.length)return t;let n=[],s=1,r=t.length?t[0]:null;for(let o=0,l=0,a=0;;o++){let h=o=i)for(;r&&r.from=e.from||c<=e.to||a){let t=Math.max(e.from,l)-a,i=Math.min(e.to,c)-a;e=t>=i?null:new Fl(t,i,e.tree,e.offset+a,o>0,!!h)}if(e&&n.push(e),r.to>c)break;r=snew pl(t.from,t.to)):[new pl(0,0)]:[new pl(0,t.length)],this.createParse(t,e||[],i)}parse(t,e,i){let n=this.startParse(t,e,i);for(;;){let t=n.advance();if(t)return t}}}class _l{constructor(t){this.string=t}get length(){return this.string.length}chunk(t){return this.string.slice(t)}get lineChunks(){return!1}read(t,e){return this.string.slice(t,e)}}new ml({perNode:!0});let Ul=0;class Ql{constructor(t,e,i,n){this.name=t,this.set=e,this.base=i,this.modified=n,this.id=Ul++}toString(){let{name:t}=this;for(let e of this.modified)e.name&&(t=`${e.name}(${t})`);return t}static define(t,e){let i="string"==typeof t?t:"?";if(t instanceof Ql&&(e=t),null==e?void 0:e.base)throw new Error("Can not derive from a modified tag");let n=new Ql(i,[],null,[]);if(n.set.push(n),e)for(let t of e.set)n.set.push(t);return n}static defineModifier(t){let e=new Kl(t);return t=>t.modified.indexOf(e)>-1?t:Kl.get(t.base||t,t.modified.concat(e).sort((t,e)=>t.id-e.id))}}let $l=0;class Kl{constructor(t){this.name=t,this.instances=[],this.id=$l++}static get(t,e){if(!e.length)return t;let i=e[0].instances.find(i=>{return i.base==t&&(n=e,s=i.modified,n.length==s.length&&n.every((t,e)=>t==s[e]));var n,s});if(i)return i;let n=[],s=new Ql(t.name,n,t,e);for(let t of e)t.instances.push(s);let r=function(t){let e=[[]];for(let i=0;ie.length-t.length)}(e);for(let e of t.set)if(!e.modified.length)for(let t of r)n.push(Kl.get(e,t));return s}}function jl(t){let e=Object.create(null);for(let i in t){let n=t[i];Array.isArray(n)||(n=[n]);for(let t of i.split(" "))if(t){let i=[],s=2,r=t;for(let e=0;;){if("..."==r&&e>0&&e+3==t.length){s=1;break}let n=/^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(r);if(!n)throw new RangeError("Invalid path: "+t);if(i.push("*"==n[0]?"":'"'==n[0][0]?JSON.parse(n[0]):n[0]),e+=n[0].length,e==t.length)break;let o=t[e++];if(e==t.length&&"!"==o){s=0;break}if("/"!=o)throw new RangeError("Invalid path: "+t);r=t.slice(e)}let o=i.length-1,l=i[o];if(!l)throw new RangeError("Invalid path: "+t);let a=new Gl(n,s,o>0?i.slice(0,o):null);e[l]=a.sort(e[l])}}return Xl.add(e)}const Xl=new ml({combine(t,e){let i,n,s;for(;t||e;){if(!t||e&&t.depth>=e.depth?(s=e,e=e.next):(s=t,t=t.next),i&&i.mode==s.mode&&!s.context&&!i.context)continue;let r=new Gl(s.tags,s.mode,s.context);i?i.next=r:n=r,i=r}return n}});class Gl{constructor(t,e,i,n){this.tags=t,this.mode=e,this.context=i,this.next=n}get opaque(){return 0==this.mode}get inherit(){return 1==this.mode}sort(t){return!t||t.depth{let e=s;for(let n of t)for(let t of n.set){let n=i[t.id];if(n){e=e?e+" "+n:n;break}}return e},scope:n}}function Jl(t,e,i,n=0,s=t.length){let r=new Zl(n,Array.isArray(e)?e:[e],i);r.highlightRange(t.cursor(),n,s,"",r.highlighters),r.flush(s)}Gl.empty=new Gl([],2,null);class Zl{constructor(t,e,i){this.at=t,this.highlighters=e,this.span=i,this.class=""}startSpan(t,e){e!=this.class&&(this.flush(t),t>this.at&&(this.at=t),this.class=e)}flush(t){t>this.at&&this.class&&this.span(this.at,t,this.class)}highlightRange(t,e,i,n,s){let{type:r,from:o,to:l}=t;if(o>=i||l<=e)return;r.isTop&&(s=this.highlighters.filter(t=>!t.scope||t.scope(r)));let a=n,h=function(t){let e=t.type.prop(Xl);for(;e&&e.context&&!t.matchContext(e.context);)e=e.next;return e||null}(t)||Gl.empty,c=function(t,e){let i=null;for(let n of t){let t=n.style(e);t&&(i=i?i+" "+t:t)}return i}(s,h.tags);if(c&&(a&&(a+=" "),a+=c,1==h.mode&&(n+=(n?" ":"")+c)),this.startSpan(Math.max(e,o),a),h.opaque)return;let u=t.tree&&t.tree.prop(ml.mounted);if(u&&u.overlay){let r=t.node.enter(u.overlay[0].from+o,1),h=this.highlighters.filter(t=>!t.scope||t.scope(u.tree.type)),c=t.firstChild();for(let f=0,d=o;;f++){let p=f=m)&&t.nextSibling()););if(!p||m>i)break;d=p.to+o,d>e&&(this.highlightRange(r.cursor(),Math.max(e,p.from+o),Math.min(i,d),"",h),this.startSpan(Math.min(i,d),a))}c&&t.parent()}else if(t.firstChild()){u&&(n="");do{if(!(t.to<=e)){if(t.from>=i)break;this.highlightRange(t,e,i,n,s),this.startSpan(Math.min(i,t.to),a)}}while(t.nextSibling());t.parent()}}}const ta=Ql.define,ea=ta(),ia=ta(),na=ta(ia),sa=ta(ia),ra=ta(),oa=ta(ra),la=ta(ra),aa=ta(),ha=ta(aa),ca=ta(),ua=ta(),fa=ta(),da=ta(fa),pa=ta(),ma={comment:ea,lineComment:ta(ea),blockComment:ta(ea),docComment:ta(ea),name:ia,variableName:ta(ia),typeName:na,tagName:ta(na),propertyName:sa,attributeName:ta(sa),className:ta(ia),labelName:ta(ia),namespace:ta(ia),macroName:ta(ia),literal:ra,string:oa,docString:ta(oa),character:ta(oa),attributeValue:ta(oa),number:la,integer:ta(la),float:ta(la),bool:ta(ra),regexp:ta(ra),escape:ta(ra),color:ta(ra),url:ta(ra),keyword:ca,self:ta(ca),null:ta(ca),atom:ta(ca),unit:ta(ca),modifier:ta(ca),operatorKeyword:ta(ca),controlKeyword:ta(ca),definitionKeyword:ta(ca),moduleKeyword:ta(ca),operator:ua,derefOperator:ta(ua),arithmeticOperator:ta(ua),logicOperator:ta(ua),bitwiseOperator:ta(ua),compareOperator:ta(ua),updateOperator:ta(ua),definitionOperator:ta(ua),typeOperator:ta(ua),controlOperator:ta(ua),punctuation:fa,separator:ta(fa),bracket:da,angleBracket:ta(da),squareBracket:ta(da),paren:ta(da),brace:ta(da),content:aa,heading:ha,heading1:ta(ha),heading2:ta(ha),heading3:ta(ha),heading4:ta(ha),heading5:ta(ha),heading6:ta(ha),contentSeparator:ta(aa),list:ta(aa),quote:ta(aa),emphasis:ta(aa),strong:ta(aa),link:ta(aa),monospace:ta(aa),strikethrough:ta(aa),inserted:ta(),deleted:ta(),changed:ta(),invalid:ta(),meta:pa,documentMeta:ta(pa),annotation:ta(pa),processingInstruction:ta(pa),definition:Ql.defineModifier("definition"),constant:Ql.defineModifier("constant"),function:Ql.defineModifier("function"),standard:Ql.defineModifier("standard"),local:Ql.defineModifier("local"),special:Ql.defineModifier("special")};for(let t in ma){let e=ma[t];e instanceof Ql&&(e.name=t)}var ga;Yl([{tag:ma.link,class:"tok-link"},{tag:ma.heading,class:"tok-heading"},{tag:ma.emphasis,class:"tok-emphasis"},{tag:ma.strong,class:"tok-strong"},{tag:ma.keyword,class:"tok-keyword"},{tag:ma.atom,class:"tok-atom"},{tag:ma.bool,class:"tok-bool"},{tag:ma.url,class:"tok-url"},{tag:ma.labelName,class:"tok-labelName"},{tag:ma.inserted,class:"tok-inserted"},{tag:ma.deleted,class:"tok-deleted"},{tag:ma.literal,class:"tok-literal"},{tag:ma.string,class:"tok-string"},{tag:ma.number,class:"tok-number"},{tag:[ma.regexp,ma.escape,ma.special(ma.string)],class:"tok-string2"},{tag:ma.variableName,class:"tok-variableName"},{tag:ma.local(ma.variableName),class:"tok-variableName tok-local"},{tag:ma.definition(ma.variableName),class:"tok-variableName tok-definition"},{tag:ma.special(ma.variableName),class:"tok-variableName2"},{tag:ma.definition(ma.propertyName),class:"tok-propertyName tok-definition"},{tag:ma.typeName,class:"tok-typeName"},{tag:ma.namespace,class:"tok-namespace"},{tag:ma.className,class:"tok-className"},{tag:ma.macroName,class:"tok-macroName"},{tag:ma.propertyName,class:"tok-propertyName"},{tag:ma.operator,class:"tok-operator"},{tag:ma.comment,class:"tok-comment"},{tag:ma.meta,class:"tok-meta"},{tag:ma.invalid,class:"tok-invalid"},{tag:ma.punctuation,class:"tok-punctuation"}]);const va=new ml;const wa=new ml;class ba{constructor(t,e,i=[],n=""){this.data=t,this.name=n,Tt.prototype.hasOwnProperty("tree")||Object.defineProperty(Tt.prototype,"tree",{get(){return ka(this)}}),this.parser=e,this.extension=[Pa.of(this),Tt.languageData.of((t,e,i)=>{let n=ya(t,e,i),s=n.type.prop(va);if(!s)return[];let r=t.facet(s),o=n.type.prop(wa);if(o){let s=n.resolve(e-n.from,i);for(let e of o)if(e.test(s,t)){let i=t.facet(e.facet);return"replace"==e.type?i:i.concat(r)}}return r})].concat(i)}isActiveAt(t,e,i=-1){return ya(t,e,i).type.prop(va)==this.data}findRegions(t){let e=t.facet(Pa);if((null==e?void 0:e.data)==this.data)return[{from:0,to:t.doc.length}];if(!e||!e.allowsNesting)return[];let i=[],n=(t,e)=>{if(t.prop(va)==this.data)return void i.push({from:e,to:e+t.length});let s=t.prop(ml.mounted);if(s){if(s.tree.prop(va)==this.data){if(s.overlay)for(let t of s.overlay)i.push({from:t.from+e,to:t.to+e});else i.push({from:e,to:e+t.length});return}if(s.overlay){let t=i.length;if(n(s.tree,s.overlay[0].from+e),i.length>t)return}}for(let i=0;it.concat(i):void 0}));var i;return new xa(e,t.parser.configure({props:[va.add(t=>t.isTop?e:void 0)]}),t.name)}configure(t,e){return new xa(this.data,this.parser.configure(t),e||this.name)}get allowsNesting(){return this.parser.hasWrappers()}}function ka(t){let e=t.field(ba.state,!1);return e?e.tree:Sl.empty}class Sa{constructor(t){this.doc=t,this.cursorPos=0,this.string="",this.cursor=t.iter()}get length(){return this.doc.length}syncTo(t){return this.string=this.cursor.next(t-this.cursorPos).value,this.cursorPos=t+this.string.length,this.cursorPos-this.string.length}chunk(t){return this.syncTo(t),this.string}get lineChunks(){return!0}read(t,e){let i=this.cursorPos-this.string.length;return t=this.cursorPos?this.doc.sliceString(t,e):this.string.slice(t-i,e-i)}}let Ca=null;class Aa{constructor(t,e,i=[],n,s,r,o,l){this.parser=t,this.state=e,this.fragments=i,this.tree=n,this.treeLen=s,this.viewport=r,this.skipped=o,this.scheduleOn=l,this.parse=null,this.tempSkipped=[]}static create(t,e,i){return new Aa(t,e,[],Sl.empty,0,i,[],null)}startParse(){return this.parser.startParse(new Sa(this.state.doc),this.fragments)}work(t,e){return null!=e&&e>=this.state.doc.length&&(e=void 0),this.tree!=Sl.empty&&this.isDone(null!=e?e:this.state.doc.length)?(this.takeTree(),!0):this.withContext(()=>{var i;if("number"==typeof t){let e=Date.now()+t;t=()=>Date.now()>e}for(this.parse||(this.parse=this.startParse()),null!=e&&(null==this.parse.stoppedAt||this.parse.stoppedAt>e)&&e=this.treeLen&&((null==this.parse.stoppedAt||this.parse.stoppedAt>t)&&this.parse.stopAt(t),this.withContext(()=>{for(;!(e=this.parse.advance()););}),this.treeLen=t,this.tree=e,this.fragments=this.withoutTempSkipped(Fl.addTree(this.tree,this.fragments,!0)),this.parse=null)}withContext(t){let e=Ca;Ca=this;try{return t()}finally{Ca=e}}withoutTempSkipped(t){for(let e;e=this.tempSkipped.pop();)t=Ma(t,e.from,e.to);return t}changes(t,e){let{fragments:i,tree:n,treeLen:s,viewport:r,skipped:o}=this;if(this.takeTree(),!t.empty){let e=[];if(t.iterChangedRanges((t,i,n,s)=>e.push({fromA:t,toA:i,fromB:n,toB:s})),i=Fl.applyChanges(i,e),n=Sl.empty,s=0,r={from:t.mapPos(r.from,-1),to:t.mapPos(r.to,1)},this.skipped.length){o=[];for(let e of this.skipped){let i=t.mapPos(e.from,1),n=t.mapPos(e.to,-1);it.from&&(this.fragments=Ma(this.fragments,i,n),this.skipped.splice(e--,1))}return!(this.skipped.length>=e)&&(this.reset(),!0)}reset(){this.parse&&(this.takeTree(),this.parse=null)}skipUntilInView(t,e){this.skipped.push({from:t,to:e})}static getSkippingParser(t){return new class extends ql{createParse(e,i,n){let s=n[0].from,r=n[n.length-1].to;return{parsedPos:s,advance(){let e=Ca;if(e){for(let t of n)e.tempSkipped.push(t);t&&(e.scheduleOn=e.scheduleOn?Promise.all([e.scheduleOn,t]):t)}return this.parsedPos=r,new Sl(wl.none,[],[],r-s)},stoppedAt:null,stopAt(){}}}}}isDone(t){t=Math.min(t,this.state.doc.length);let e=this.fragments;return this.treeLen>=t&&e.length&&0==e[0].from&&e[0].to>=t}static get(){return Ca}}function Ma(t,e,i){return Fl.applyChanges(t,[{fromA:e,toA:i,fromB:e,toB:i}])}class Oa{constructor(t){this.context=t,this.tree=t.tree}apply(t){if(!t.docChanged&&this.tree==this.context.tree)return this;let e=this.context.changes(t.changes,t.state),i=this.context.treeLen==t.startState.doc.length?void 0:Math.max(t.changes.mapPos(this.context.treeLen),e.viewport.to);return e.work(20,i)||e.takeTree(),new Oa(e)}static init(t){let e=Math.min(3e3,t.doc.length),i=Aa.create(t.facet(Pa).parser,t,{from:0,to:e});return i.work(20,e)||i.takeTree(),new Oa(i)}}ba.state=K.define({create:Oa.init,update(t,e){for(let t of e.effects)if(t.is(ba.setState))return t.value;return e.startState.facet(Pa)!=e.state.facet(Pa)?Oa.init(e.state):t.apply(e)}});let Ta=t=>{let e=setTimeout(()=>t(),500);return()=>clearTimeout(e)};"undefined"!=typeof requestIdleCallback&&(Ta=t=>{let e=-1,i=setTimeout(()=>{e=requestIdleCallback(t,{timeout:400})},100);return()=>e<0?clearTimeout(i):cancelIdleCallback(e)});const Da="undefined"!=typeof navigator&&(null===(ga=navigator.scheduling)||void 0===ga?void 0:ga.isInputPending)?()=>navigator.scheduling.isInputPending():null,Ra=qi.fromClass(class{constructor(t){this.view=t,this.working=null,this.workScheduled=0,this.chunkEnd=-1,this.chunkBudget=-1,this.work=this.work.bind(this),this.scheduleWork()}update(t){let e=this.view.state.field(ba.state).context;(e.updateViewport(t.view.viewport)||this.view.viewport.to>e.treeLen)&&this.scheduleWork(),(t.docChanged||t.selectionSet)&&(this.view.hasFocus&&(this.chunkBudget+=50),this.scheduleWork()),this.checkAsyncSchedule(e)}scheduleWork(){if(this.working)return;let{state:t}=this.view,e=t.field(ba.state);e.tree==e.context.tree&&e.context.isDone(t.doc.length)||(this.working=Ta(this.work))}work(t){this.working=null;let e=Date.now();if(this.chunkEndn+1e3,l=s.context.work(()=>Da&&Da()||Date.now()>r,n+(o?0:1e5));this.chunkBudget-=Date.now()-e,(l||this.chunkBudget<=0)&&(s.context.takeTree(),this.view.dispatch({effects:ba.setState.of(new Oa(s.context))})),this.chunkBudget>0&&(!l||o)&&this.scheduleWork(),this.checkAsyncSchedule(s.context)}checkAsyncSchedule(t){t.scheduleOn&&(this.workScheduled++,t.scheduleOn.then(()=>this.scheduleWork()).catch(t=>Hi(this.view.state,t)).then(()=>this.workScheduled--),t.scheduleOn=null)}destroy(){this.working&&this.working()}isWorking(){return!!(this.working||this.workScheduled>0)}},{eventHandlers:{focus(){this.scheduleWork()}}}),Pa=z.define({combine:t=>t.length?t[0]:null,enables:t=>[ba.state,Ra,pr.contentAttributes.compute([t],e=>{let i=e.facet(t);return i&&i.name?{"data-language":i.name}:{}})]});class Ba{constructor(t,e=[]){this.language=t,this.support=e,this.extension=[t,e]}}const Ea=z.define(),La=z.define({combine:t=>{if(!t.length)return" ";let e=t[0];if(!e||/\S/.test(e)||Array.from(e).some(t=>t!=e[0]))throw new Error("Invalid indent unit: "+JSON.stringify(t[0]));return e}});function Ia(t){let e=t.facet(La);return 9==e.charCodeAt(0)?t.tabSize*e.length:e.length}function Na(t,e){let i="",n=t.tabSize,s=t.facet(La)[0];if("\t"==s){for(;e>=n;)i+="\t",e-=n;s=" "}for(let t=0;t=e?function(t,e,i){let n=e.resolveStack(i),s=e.resolveInner(i,-1).resolve(i,0).enterUnfinishedNodesBefore(i);if(s!=n.node){let t=[];for(let e=s;e&&!(e.fromn.node.to||e.from==n.node.from&&e.type==n.node.type);e=e.parent)t.push(e);for(let e=t.length-1;e>=0;e--)n={node:t[e],next:n}}return za(n,t,i)}(t,i,e):null}class Ha{constructor(t,e={}){this.state=t,this.options=e,this.unit=Ia(t)}lineAt(t,e=1){let i=this.state.doc.lineAt(t),{simulateBreak:n,simulateDoubleBreak:s}=this.options;return null!=n&&n>=i.from&&n<=i.to?s&&n==t?{text:"",from:t}:(e<0?n-1&&(s+=r-this.countColumn(i,i.search(/\S|$/))),s}countColumn(t,e=t.length){return Kt(t,this.state.tabSize,e)}lineIndent(t,e=1){let{text:i,from:n}=this.lineAt(t,e),s=this.options.overrideIndentation;if(s){let t=s(n);if(t>-1)return t}return this.countColumn(i,i.search(/\S|$/))}get simulatedBreak(){return this.options.simulateBreak||null}}const Va=new ml;function za(t,e,i){for(let n=t;n;n=n.next){let t=Fa(n.node);if(t)return t(_a.create(e,i,n))}return 0}function Fa(t){let e=t.type.prop(Va);if(e)return e;let i,n=t.firstChild;if(n&&(i=n.type.prop(ml.closedBy))){let e=t.lastChild,n=e&&i.indexOf(e.name)>-1;return t=>function(t,e,i,n,s){let r=t.textAfter,o=r.match(/^\s*/)[0].length,l=n&&r.slice(o,o+n.length)==n||s==t.pos+o,a=e?function(t){let e=t.node,i=e.childAfter(e.from),n=e.lastChild;if(!i)return null;let s=t.options.simulateBreak,r=t.state.doc.lineAt(i.from),o=null==s||s<=r.from?r.to:Math.min(r.to,s);for(let t=i.to;;){let s=e.childAfter(t);if(!s||s==n)return null;if(!s.type.isSkipped){if(s.from>=o)return null;let t=/^ */.exec(r.text.slice(i.to-r.from))[0].length;return{from:i.from,to:i.to+t}}t=s.to}}(t):null;return a?l?t.column(a.from):t.column(a.to):t.baseIndent+(l?0:t.unit*i)}(t,!0,1,void 0,n&&!function(t){return t.pos==t.options.simulateBreak&&t.options.simulateDoubleBreak}(t)?e.from:void 0)}return null==t.parent?qa:null}function qa(){return 0}class _a extends Ha{constructor(t,e,i){super(t.state,t.options),this.base=t,this.pos=e,this.context=i}get node(){return this.context.node}static create(t,e,i){return new _a(t,e,i)}get textAfter(){return this.textAfterPos(this.pos)}get baseIndent(){return this.baseIndentFor(this.node)}baseIndentFor(t){let e=this.state.doc.lineAt(t.from);for(;;){let i=t.resolve(e.from);for(;i.parent&&i.parent.from==i.from;)i=i.parent;if(Ua(i,t))break;e=this.state.doc.lineAt(i.from)}return this.lineIndent(e.from)}continue(){return za(this.context.next,this.base,this.pos)}}function Ua(t,e){for(let i=e;i;i=i.parent)if(t==i)return!0;return!1}function Qa({except:t,units:e=1}={}){return i=>{let n=t&&t.test(i.textAfter);return i.baseIndent+(n?0:e*i.unit)}}const $a=z.define(),Ka=new ml;function ja(t){let e=t.lastChild;return e&&e.to==t.to&&e.type.isError}function Xa(t,e,i){for(let n of t.facet($a)){let s=n(t,e,i);if(s)return s}return function(t,e,i){let n=ka(t);if(n.lengthi)continue;if(s&&o.from=e&&n.to>i&&(s=n)}}return s}(t,e,i)}function Ga(t,e){let i=e.mapPos(t.from,1),n=e.mapPos(t.to,-1);return i>=n?void 0:{from:i,to:n}}const Ya=gt.define({map:Ga}),Ja=gt.define({map:Ga});function Za(t){let e=[];for(let{head:i}of t.state.selection.ranges)e.some(t=>t.from<=i&&t.to>=i)||e.push(t.lineBlockAt(i));return e}const th=K.define({create:()=>Te.none,update(t,e){e.isUserEvent("delete")&&e.changes.iterChangedRanges((e,i)=>t=eh(t,e,i)),t=t.map(e.changes);let i=[];for(let n of e.effects)n.is(Ya)&&!nh(t,n.value.from,n.value.to)?i.push(n.value):n.is(Ja)&&(t=t.update({filter:(t,e)=>n.value.from!=t||n.value.to!=e,filterFrom:n.value.from,filterTo:n.value.to}));if(i.length){let{preparePlaceholder:n}=e.state.facet(ah),s=i.map(t=>(n?Te.replace({widget:new fh(n(e.state,t))}):uh).range(t.from,t.to));t=t.update({add:s})}return e.selection&&(t=eh(t,e.selection.main.head)),t},provide:t=>pr.decorations.from(t),toJSON(t,e){let i=[];return t.between(0,e.doc.length,(t,e)=>{i.push(t,e)}),i},fromJSON(t){if(!Array.isArray(t)||t.length%2)throw new RangeError("Invalid JSON for fold state");let e=[];for(let i=0;i{te&&(n=!0)}),n?t.update({filterFrom:e,filterTo:i,filter:(t,n)=>t>=i||n<=e}):t}function ih(t,e,i){var n;let s=null;return null===(n=t.field(th,!1))||void 0===n||n.between(e,i,(t,e)=>{(!s||s.from>t)&&(s={from:t,to:e})}),s}function nh(t,e,i){let n=!1;return t.between(e,e,(t,s)=>{t==e&&s==i&&(n=!0)}),n}function sh(t,e){return t.field(th,!1)?e:e.concat(gt.appendConfig.of(hh()))}function rh(t,e,i=!0){let n=t.state.doc.lineAt(e.from).number,s=t.state.doc.lineAt(e.to).number;return pr.announce.of(`${t.state.phrase(i?"Folded lines":"Unfolded lines")} ${n} ${t.state.phrase("to")} ${s}.`)}const oh=[{key:"Ctrl-Shift-[",mac:"Cmd-Alt-[",run:t=>{for(let e of Za(t)){let i=Xa(t.state,e.from,e.to);if(i)return t.dispatch({effects:sh(t.state,[Ya.of(i),rh(t,i)])}),!0}return!1}},{key:"Ctrl-Shift-]",mac:"Cmd-Alt-]",run:t=>{if(!t.state.field(th,!1))return!1;let e=[];for(let i of Za(t)){let n=ih(t.state,i.from,i.to);n&&e.push(Ja.of(n),rh(t,n,!1))}return e.length&&t.dispatch({effects:e}),e.length>0}},{key:"Ctrl-Alt-[",run:t=>{let{state:e}=t,i=[];for(let n=0;n{let e=t.state.field(th,!1);if(!e||!e.size)return!1;let i=[];return e.between(0,t.state.doc.length,(t,e)=>{i.push(Ja.of({from:t,to:e}))}),t.dispatch({effects:i}),!0}}],lh={placeholderDOM:null,preparePlaceholder:null,placeholderText:"…"},ah=z.define({combine:t=>Dt(t,lh)});function hh(t){let e=[th,gh];return t&&e.push(ah.of(t)),e}function ch(t,e){let{state:i}=t,n=i.facet(ah),s=e=>{let i=t.lineBlockAt(t.posAtDOM(e.target)),n=ih(t.state,i.from,i.to);n&&t.dispatch({effects:Ja.of(n)}),e.preventDefault()};if(n.placeholderDOM)return n.placeholderDOM(t,s,e);let r=document.createElement("span");return r.textContent=n.placeholderText,r.setAttribute("aria-label",i.phrase("folded code")),r.title=i.phrase("unfold"),r.className="cm-foldPlaceholder",r.onclick=s,r}const uh=Te.replace({widget:new class extends Me{toDOM(t){return ch(t,null)}}});class fh extends Me{constructor(t){super(),this.value=t}eq(t){return this.value==t.value}toDOM(t){return ch(t,this.value)}}const dh={openText:"⌄",closedText:"›",markerDOM:null,domEventHandlers:{},foldingChanged:()=>!1};class ph extends qo{constructor(t,e){super(),this.config=t,this.open=e}eq(t){return this.config==t.config&&this.open==t.open}toDOM(t){if(this.config.markerDOM)return this.config.markerDOM(this.open);let e=document.createElement("span");return e.textContent=this.open?this.config.openText:this.config.closedText,e.title=t.state.phrase(this.open?"Fold line":"Unfold line"),e}}function mh(t={}){let e={...dh,...t},i=new ph(e,!0),n=new ph(e,!1),s=qi.fromClass(class{constructor(t){this.from=t.viewport.from,this.markers=this.buildMarkers(t)}update(t){(t.docChanged||t.viewportChanged||t.startState.facet(Pa)!=t.state.facet(Pa)||t.startState.field(th,!1)!=t.state.field(th,!1)||ka(t.startState)!=ka(t.state)||e.foldingChanged(t))&&(this.markers=this.buildMarkers(t.view))}buildMarkers(t){let e=new Nt;for(let s of t.viewportLineBlocks){let r=ih(t.state,s.from,s.to)?n:Xa(t.state,s.from,s.to)?i:null;r&&e.add(s.from,s.from,r)}return e.finish()}}),{domEventHandlers:r}=e;return[s,Ko({class:"cm-foldGutter",markers(t){var e;return(null===(e=t.plugin(s))||void 0===e?void 0:e.markers)||It.empty},initialSpacer:()=>new ph(e,!1),domEventHandlers:{...r,click:(t,e,i)=>{if(r.click&&r.click(t,e,i))return!0;let n=ih(t.state,e.from,e.to);if(n)return t.dispatch({effects:Ja.of(n)}),!0;let s=Xa(t.state,e.from,e.to);return!!s&&(t.dispatch({effects:Ya.of(s)}),!0)}}}),hh()]}const gh=pr.baseTheme({".cm-foldPlaceholder":{backgroundColor:"#eee",border:"1px solid #ddd",color:"#888",borderRadius:".2em",margin:"0 1px",padding:"0 1px",cursor:"pointer"},".cm-foldGutter span":{padding:"0 1px",cursor:"pointer"}});class vh{constructor(t,e){let i;function n(t){let e=Jt.newName();return(i||(i=Object.create(null)))["."+e]=t,e}this.specs=t;const s="string"==typeof e.all?e.all:e.all?n(e.all):void 0,r=e.scope;this.scope=r instanceof ba?t=>t.prop(va)==r.data:r?t=>t==r:void 0,this.style=Yl(t.map(t=>({tag:t.tag,class:t.class||n(Object.assign({},t,{tag:null}))})),{all:s}).style,this.module=i?new Jt(i):null,this.themeType=e.themeType}static define(t,e){return new vh(t,e||{})}}const wh=z.define(),bh=z.define({combine:t=>t.length?[t[0]]:null});function yh(t){let e=t.facet(wh);return e.length?e:t.facet(bh)}function xh(t,e){let i,n=[Sh];return t instanceof vh&&(t.module&&n.push(pr.styleModule.of(t.module)),i=t.themeType),(null==e?void 0:e.fallback)?n.push(bh.of(t)):i?n.push(wh.computeN([pr.darkTheme],e=>e.facet(pr.darkTheme)==("dark"==i)?[t]:[])):n.push(wh.of(t)),n}class kh{constructor(t){this.markCache=Object.create(null),this.tree=ka(t.state),this.decorations=this.buildDeco(t,yh(t.state)),this.decoratedTo=t.viewport.to}update(t){let e=ka(t.state),i=yh(t.state),n=i!=yh(t.startState),{viewport:s}=t.view,r=t.changes.mapPos(this.decoratedTo,1);e.length=s.to?(this.decorations=this.decorations.map(t.changes),this.decoratedTo=r):(e!=this.tree||t.viewportChanged||n)&&(this.tree=e,this.decorations=this.buildDeco(t.view,i),this.decoratedTo=s.to)}buildDeco(t,e){if(!e||!this.tree.length)return Te.none;let i=new Nt;for(let{from:n,to:s}of t.visibleRanges)Jl(this.tree,e,(t,e,n)=>{i.add(t,e,this.markCache[n]||(this.markCache[n]=Te.mark({class:n})))},n,s);return i.finish()}}const Sh=Z.high(qi.fromClass(kh,{decorations:t=>t.decorations})),Ch=vh.define([{tag:ma.meta,color:"#404740"},{tag:ma.link,textDecoration:"underline"},{tag:ma.heading,textDecoration:"underline",fontWeight:"bold"},{tag:ma.emphasis,fontStyle:"italic"},{tag:ma.strong,fontWeight:"bold"},{tag:ma.strikethrough,textDecoration:"line-through"},{tag:ma.keyword,color:"#708"},{tag:[ma.atom,ma.bool,ma.url,ma.contentSeparator,ma.labelName],color:"#219"},{tag:[ma.literal,ma.inserted],color:"#164"},{tag:[ma.string,ma.deleted],color:"#a11"},{tag:[ma.regexp,ma.escape,ma.special(ma.string)],color:"#e40"},{tag:ma.definition(ma.variableName),color:"#00f"},{tag:ma.local(ma.variableName),color:"#30a"},{tag:[ma.typeName,ma.namespace],color:"#085"},{tag:ma.className,color:"#167"},{tag:[ma.special(ma.variableName),ma.macroName],color:"#256"},{tag:ma.definition(ma.propertyName),color:"#00c"},{tag:ma.comment,color:"#940"},{tag:ma.invalid,color:"#f00"}]),Ah=pr.baseTheme({"&.cm-focused .cm-matchingBracket":{backgroundColor:"#328c8252"},"&.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bb555544"}}),Mh="()[]{}",Oh=z.define({combine:t=>Dt(t,{afterCursor:!0,brackets:Mh,maxScanDistance:1e4,renderMatch:Rh})}),Th=Te.mark({class:"cm-matchingBracket"}),Dh=Te.mark({class:"cm-nonmatchingBracket"});function Rh(t){let e=[],i=t.matched?Th:Dh;return e.push(i.range(t.start.from,t.start.to)),t.end&&e.push(i.range(t.end.from,t.end.to)),e}function Ph(t){let e=[],i=t.facet(Oh);for(let n of t.selection.ranges){if(!n.empty)continue;let s=Wh(t,n.head,-1,i)||n.head>0&&Wh(t,n.head-1,1,i)||i.afterCursor&&(Wh(t,n.head,1,i)||n.headt.decorations}),Ah];function Eh(t={}){return[Oh.of(t),Bh]}const Lh=new ml;function Ih(t,e,i){let n=t.prop(e<0?ml.openedBy:ml.closedBy);if(n)return n;if(1==t.name.length){let n=i.indexOf(t.name);if(n>-1&&n%2==(e<0?1:0))return[i[n+e]]}return null}function Nh(t){let e=t.type.prop(Lh);return e?e(t.node):t}function Wh(t,e,i,n={}){let s=n.maxScanDistance||1e4,r=n.brackets||Mh,o=ka(t),l=o.resolveInner(e,i);for(let n=l;n;n=n.parent){let s=Ih(n.type,i,r);if(s&&n.from0?e>=o.from&&eo.from&&e<=o.to))return Hh(t,e,i,n,o,s,r)}}return function(t,e,i,n,s,r,o){if(i<0?!e:e==t.doc.length)return null;let l=i<0?t.sliceDoc(e-1,e):t.sliceDoc(e,e+1),a=o.indexOf(l);if(a<0||a%2==0!=i>0)return null;let h={from:i<0?e-1:e,to:i>0?e+1:e},c=t.doc.iterRange(e,i>0?t.doc.length:0),u=0;for(let t=0;!c.next().done&&t<=r;){let r=c.value;i<0&&(t+=r.length);let l=e+t*i;for(let t=i>0?0:r.length-1,e=i>0?r.length:-1;t!=e;t+=i){let e=o.indexOf(r[t]);if(!(e<0||n.resolveInner(l+t,1).type!=s))if(e%2==0==i>0)u++;else{if(1==u)return{start:h,end:{from:l+t,to:l+t+1},matched:e>>1==a>>1};u--}}i>0&&(t+=r.length)}return c.done?{start:h,matched:!1}:null}(t,e,i,o,l.type,s,r)}function Hh(t,e,i,n,s,r,o){let l=n.parent,a={from:s.from,to:s.to},h=0,c=null==l?void 0:l.cursor();if(c&&(i<0?c.childBefore(n.from):c.childAfter(n.to)))do{if(i<0?c.to<=n.from:c.from>=n.to){if(0==h&&r.indexOf(c.type.name)>-1&&c.from-1||(Fh.push(t),console.warn(e))}function Qh(t,e){let i=[];for(let n of e.split(" ")){let e=[];for(let i of n.split(".")){let n=t[i]||ma[i];n?"function"==typeof n?e.length?e=e.map(n):Uh(i,`Modifier ${i} used at start of tag`):e.length?Uh(i,`Tag ${i} used as modifier`):e=Array.isArray(n)?n:[n]:Uh(i,`Unknown highlighting tag ${i}`)}for(let t of e)i.push(t)}if(!i.length)return 0;let n=e.replace(/ /g,"_"),s=n+" "+i.map(t=>t.id),r=qh[s];if(r)return r.id;let o=qh[s]=wl.define({id:zh.length,name:n,props:[jl({[n]:i})]});return zh.push(o),o.id}si.RTL,si.LTR;function $h(t,e){return({state:i,dispatch:n})=>{if(i.readOnly)return!1;let s=t(e,i);return!!s&&(n(i.update(s)),!0)}}const Kh=$h(Zh,0),jh=$h(Jh,0),Xh=$h((t,e)=>Jh(t,e,function(t){let e=[];for(let i of t.selection.ranges){let n=t.doc.lineAt(i.from),s=i.to<=n.to?n:t.doc.lineAt(i.to);s.from>n.from&&s.from==i.to&&(s=i.to==n.to+1?n:t.doc.lineAt(i.to-1));let r=e.length-1;r>=0&&e[r].to>n.from?e[r].to=s.to:e.push({from:n.from+/^\s*/.exec(n.text)[0].length,to:s.to})}return e}(e)),0);function Gh(t,e){let i=t.languageDataAt("commentTokens",e,1);return i.length?i[0]:{}}const Yh=50;function Jh(t,e,i=e.selection.ranges){let n=i.map(t=>Gh(e,t.from).block);if(!n.every(t=>t))return null;let s=i.map((t,i)=>function(t,{open:e,close:i},n,s){let r,o,l=t.sliceDoc(n-Yh,n),a=t.sliceDoc(s,s+Yh),h=/\s*$/.exec(l)[0].length,c=/^\s*/.exec(a)[0].length,u=l.length-h;if(l.slice(u-e.length,u)==e&&a.slice(c,c+i.length)==i)return{open:{pos:n-h,margin:h&&1},close:{pos:s+c,margin:c&&1}};s-n<=2*Yh?r=o=t.sliceDoc(n,s):(r=t.sliceDoc(n,n+Yh),o=t.sliceDoc(s-Yh,s));let f=/^\s*/.exec(r)[0].length,d=/\s*$/.exec(o)[0].length,p=o.length-d-i.length;return r.slice(f,f+e.length)==e&&o.slice(p,p+i.length)==i?{open:{pos:n+f+e.length,margin:/\s/.test(r.charAt(f+e.length))?1:0},close:{pos:s-d-i.length,margin:/\s/.test(o.charAt(p-1))?1:0}}:null}(e,n[i],t.from,t.to));if(2!=t&&!s.every(t=>t))return{changes:e.changes(i.map((t,e)=>s[e]?[]:[{from:t.from,insert:n[e].open+" "},{from:t.to,insert:" "+n[e].close}]))};if(1!=t&&s.some(t=>t)){let t=[];for(let e,i=0;is&&(t==r||r>a.from)){s=a.from;let t=/^\s*/.exec(a.text)[0].length,e=t==a.length,r=a.text.slice(t,t+i.length)==i?t:-1;tt.comment<0&&(!t.empty||t.single))){let t=[];for(let{line:e,token:i,indent:s,empty:r,single:o}of n)!o&&r||t.push({from:e.from+s,insert:i+" "});let i=e.changes(t);return{changes:i,selection:e.selection.map(i,1)}}if(1!=t&&n.some(t=>t.comment>=0)){let t=[];for(let{line:e,comment:i,token:s}of n)if(i>=0){let n=e.from+i,r=n+s.length;" "==e.text[r-e.from]&&r++,t.push({from:n,to:r})}return{changes:t}}return null}const tc=dt.define(),ec=dt.define(),ic=z.define(),nc=z.define({combine:t=>Dt(t,{minDepth:100,newGroupDelay:500,joinToEvent:(t,e)=>e},{minDepth:Math.max,newGroupDelay:Math.min,joinToEvent:(t,e)=>(i,n)=>t(i,n)||e(i,n)})}),sc=K.define({create:()=>yc.empty,update(t,e){let i=e.state.facet(nc),n=e.annotation(tc);if(n){let s=cc.fromTransaction(e,n.selection),r=n.side,o=0==r?t.undone:t.done;return o=s?uc(o,o.length,i.minDepth,s):mc(o,e.startState.selection),new yc(0==r?n.rest:o,0==r?o:n.rest)}let s=e.annotation(ec);if("full"!=s&&"before"!=s||(t=t.isolate()),!1===e.annotation(vt.addToHistory))return e.changes.empty?t:t.addMapping(e.changes.desc);let r=cc.fromTransaction(e),o=e.annotation(vt.time),l=e.annotation(vt.userEvent);return r?t=t.addChanges(r,o,l,i,e):e.selection&&(t=t.addSelection(e.startState.selection,o,l,i.newGroupDelay)),"full"!=s&&"after"!=s||(t=t.isolate()),t},toJSON:t=>({done:t.done.map(t=>t.toJSON()),undone:t.undone.map(t=>t.toJSON())}),fromJSON:t=>new yc(t.done.map(cc.fromJSON),t.undone.map(cc.fromJSON))});function rc(t,e){return function({state:i,dispatch:n}){if(!e&&i.readOnly)return!1;let s=i.field(sc,!1);if(!s)return!1;let r=s.pop(t,i,e);return!!r&&(n(r),!0)}}const oc=rc(0,!1),lc=rc(1,!1),ac=rc(0,!0),hc=rc(1,!0);class cc{constructor(t,e,i,n,s){this.changes=t,this.effects=e,this.mapped=i,this.startSelection=n,this.selectionsAfter=s}setSelAfter(t){return new cc(this.changes,this.effects,this.mapped,this.startSelection,t)}toJSON(){var t,e,i;return{changes:null===(t=this.changes)||void 0===t?void 0:t.toJSON(),mapped:null===(e=this.mapped)||void 0===e?void 0:e.toJSON(),startSelection:null===(i=this.startSelection)||void 0===i?void 0:i.toJSON(),selectionsAfter:this.selectionsAfter.map(t=>t.toJSON())}}static fromJSON(t){return new cc(t.changes&&D.fromJSON(t.changes),[],t.mapped&&T.fromJSON(t.mapped),t.startSelection&&W.fromJSON(t.startSelection),t.selectionsAfter.map(W.fromJSON))}static fromTransaction(t,e){let i=dc;for(let e of t.startState.facet(ic)){let n=e(t);n.length&&(i=i.concat(n))}return!i.length&&t.changes.empty?null:new cc(t.changes.invert(t.startState.doc),i,void 0,e||t.startState.selection,dc)}static selection(t){return new cc(void 0,dc,void 0,void 0,t)}}function uc(t,e,i,n){let s=e+1>i+20?e-i-1:0,r=t.slice(s,e);return r.push(n),r}function fc(t,e){return t.length?e.length?t.concat(e):t:e}const dc=[],pc=200;function mc(t,e){if(t.length){let i=t[t.length-1],n=i.selectionsAfter.slice(Math.max(0,i.selectionsAfter.length-pc));return n.length&&n[n.length-1].eq(e)?t:(n.push(e),uc(t,t.length-1,1e9,i.setSelAfter(n)))}return[cc.selection([e])]}function gc(t){let e=t[t.length-1],i=t.slice();return i[t.length-1]=e.setSelAfter(e.selectionsAfter.slice(0,e.selectionsAfter.length-1)),i}function vc(t,e){if(!t.length)return t;let i=t.length,n=dc;for(;i;){let s=wc(t[i-1],e,n);if(s.changes&&!s.changes.empty||s.effects.length){let e=t.slice(0,i);return e[i-1]=s,e}e=s.mapped,i--,n=s.selectionsAfter}return n.length?[cc.selection(n)]:dc}function wc(t,e,i){let n=fc(t.selectionsAfter.length?t.selectionsAfter.map(t=>t.map(e)):dc,i);if(!t.changes)return cc.selection(n);let s=t.changes.map(e),r=e.mapDesc(t.changes,!0),o=t.mapped?t.mapped.composeDesc(r):r;return new cc(s,gt.mapEffects(t.effects,e),o,t.startSelection.map(r),n)}const bc=/^(input\.type|delete)($|\.)/;class yc{constructor(t,e,i=0,n=void 0){this.done=t,this.undone=e,this.prevTime=i,this.prevUserEvent=n}isolate(){return this.prevTime?new yc(this.done,this.undone):this}addChanges(t,e,i,n,s){let r=this.done,o=r[r.length-1];return r=o&&o.changes&&!o.changes.empty&&t.changes&&(!i||bc.test(i))&&(!o.selectionsAfter.length&&e-this.prevTimei.push(t,e)),e.iterChangedRanges((t,e,s,r)=>{for(let t=0;t=e&&s<=o&&(n=!0)}}),n}(o.changes,t.changes))||"input.type.compose"==i)?uc(r,r.length-1,n.minDepth,new cc(t.changes.compose(o.changes),fc(gt.mapEffects(t.effects,o.changes),o.effects),o.mapped,o.startSelection,dc)):uc(r,r.length,n.minDepth,t),new yc(r,dc,e,i)}addSelection(t,e,i,n){let s=this.done.length?this.done[this.done.length-1].selectionsAfter:dc;return s.length>0&&e-this.prevTimet.empty!=o.ranges[e].empty).length)?this:new yc(mc(this.done,t),this.undone,e,i);var r,o}addMapping(t){return new yc(vc(this.done,t),vc(this.undone,t),this.prevTime,this.prevUserEvent)}pop(t,e,i){let n=0==t?this.done:this.undone;if(0==n.length)return null;let s=n[n.length-1],r=s.selectionsAfter[0]||(s.startSelection?s.startSelection.map(s.changes.invertedDesc,1):e.selection);if(i&&s.selectionsAfter.length)return e.update({selection:s.selectionsAfter[s.selectionsAfter.length-1],annotations:tc.of({side:t,rest:gc(n),selection:r}),userEvent:0==t?"select.undo":"select.redo",scrollIntoView:!0});if(s.changes){let i=1==n.length?dc:n.slice(0,n.length-1);return s.mapped&&(i=vc(i,s.mapped)),e.update({changes:s.changes,selection:s.startSelection,effects:s.effects,annotations:tc.of({side:t,rest:i,selection:r}),filter:!1,userEvent:0==t?"undo":"redo",scrollIntoView:!0})}return null}}yc.empty=new yc(dc,dc);const xc=[{key:"Mod-z",run:oc,preventDefault:!0},{key:"Mod-y",mac:"Mod-Shift-z",run:lc,preventDefault:!0},{linux:"Ctrl-Shift-z",run:lc,preventDefault:!0},{key:"Mod-u",run:ac,preventDefault:!0},{key:"Alt-u",mac:"Mod-Shift-u",run:hc,preventDefault:!0}];function kc(t,e){return W.create(t.ranges.map(e),t.mainIndex)}function Sc(t,e){return t.update({selection:e,scrollIntoView:!0,userEvent:"select"})}function Cc({state:t,dispatch:e},i){let n=kc(t.selection,i);return!n.eq(t.selection,!0)&&(e(Sc(t,n)),!0)}function Ac(t,e){return W.cursor(e?t.to:t.from)}function Mc(t,e){return Cc(t,i=>i.empty?t.moveByChar(i,e):Ac(i,e))}function Oc(t){return t.textDirectionAt(t.state.selection.main.head)==si.LTR}const Tc=t=>Mc(t,!Oc(t)),Dc=t=>Mc(t,Oc(t));function Rc(t,e){return Cc(t,i=>i.empty?t.moveByGroup(i,e):Ac(i,e))}function Pc(t,e,i){if(e.type.prop(i))return!0;let n=e.to-e.from;return n&&(n>2||/[^\s,.;:]/.test(t.sliceDoc(e.from,e.to)))||e.firstChild}function Bc(t,e,i){let n,s,r=ka(t).resolveInner(e.head),o=i?ml.closedBy:ml.openedBy;for(let n=e.head;;){let e=i?r.childAfter(n):r.childBefore(n);if(!e)break;Pc(t,e,o)?r=e:n=i?e.to:e.from}return s=r.type.prop(o)&&(n=i?Wh(t,r.from,1):Wh(t,r.to,-1))&&n.matched?i?n.end.to:n.end.from:i?r.to:r.from,W.cursor(s,i?-1:1)}function Ec(t,e){return Cc(t,i=>{if(!i.empty)return Ac(i,e);let n=t.moveVertically(i,e);return n.head!=i.head?n:t.moveToLineBoundary(i,e)})}const Lc=t=>Ec(t,!1),Ic=t=>Ec(t,!0);function Nc(t){let e,i=t.scrollDOM.clientHeighti.empty?t.moveVertically(i,e,n.height):Ac(i,e));if(r.eq(s.selection))return!1;if(n.selfScroll){let e=t.coordsAtPos(s.selection.main.head),o=t.scrollDOM.getBoundingClientRect(),l=o.top+n.marginTop,a=o.bottom-n.marginBottom;e&&e.top>l&&e.bottomWc(t,!1),Vc=t=>Wc(t,!0);function zc(t,e,i){let n=t.lineBlockAt(e.head),s=t.moveToLineBoundary(e,i);if(s.head==e.head&&s.head!=(i?n.to:n.from)&&(s=t.moveToLineBoundary(e,i,!1)),!i&&s.head==n.from&&n.length){let i=/^\s*/.exec(t.state.sliceDoc(n.from,Math.min(n.from+100,n.to)))[0].length;i&&e.head!=n.from+i&&(s=W.cursor(n.from+i))}return s}function Fc(t,e,i){let n=kc(t.state.selection,t=>{t.undirectional&&t.head>=t.anchor!=e&&(t=W.range(t.head,t.anchor));let n=i(t);return W.range(t.anchor,n.head,n.goalColumn,n.bidiLevel||void 0,n.assoc)});return!n.eq(t.state.selection)&&(t.dispatch(Sc(t.state,n)),!0)}function qc(t,e){return Fc(t,e,i=>t.moveByChar(i,e))}const _c=t=>qc(t,!Oc(t)),Uc=t=>qc(t,Oc(t));function Qc(t,e){return Fc(t,e,i=>t.moveByGroup(i,e))}function $c(t,e){return Fc(t,e,i=>t.moveVertically(i,e))}const Kc=t=>$c(t,!1),jc=t=>$c(t,!0);function Xc(t,e){return Fc(t,e,i=>t.moveVertically(i,e,Nc(t).height))}const Gc=t=>Xc(t,!1),Yc=t=>Xc(t,!0),Jc=({state:t,dispatch:e})=>(e(Sc(t,{anchor:0})),!0),Zc=({state:t,dispatch:e})=>(e(Sc(t,{anchor:t.doc.length})),!0),tu=({state:t,dispatch:e})=>(e(Sc(t,{anchor:t.selection.main.anchor,head:0})),!0),eu=({state:t,dispatch:e})=>(e(Sc(t,{anchor:t.selection.main.anchor,head:t.doc.length})),!0);function iu(t,e){let{state:i}=t,n=i.selection,s=i.selection.ranges.slice();for(let n of i.selection.ranges){let r=i.doc.lineAt(n.head);if(e?r.to0)for(let i=n;;){let n=t.moveVertically(i,e);if(n.headr.to){s.some(t=>t.head==n.head)||s.push(n);break}if(n.head==i.head)break;i=n}}return s.length!=n.ranges.length&&(t.dispatch(Sc(i,W.create(s,s.length-1))),!0)}function nu(t,e){if(t.state.readOnly)return!1;let i="delete.selection",{state:n}=t,s=n.changeByRange(n=>{let{from:s,to:r}=n;if(s==r){let o=e(n);os&&(i="delete.forward",o=su(t,o,!0)),s=Math.min(s,o),r=Math.max(r,o)}else s=su(t,s,!1),r=su(t,r,!0);return s==r?{range:n}:{changes:{from:s,to:r},range:W.cursor(s,se(t)))n.between(e,e,(t,n)=>{te&&(e=i?n:t)});return e}const ru=(t,e,i)=>nu(t,n=>{let s,r,o=n.from,{state:l}=t,a=l.doc.lineAt(o);if(i&&!e&&o>a.from&&oru(t,!1,!0),lu=t=>ru(t,!0,!1),au=(t,e)=>nu(t,i=>{let n=i.head,{state:s}=t,r=s.doc.lineAt(n),o=s.charCategorizer(n);for(let t=null;;){if(n==(e?r.to:r.from)){n==i.head&&r.number!=(e?s.doc.lines:1)&&(n+=e?1:-1);break}let l=k(r.text,n-r.from,e)+r.from,a=r.text.slice(Math.min(n,l)-r.from,Math.max(n,l)-r.from),h=o(a);if(null!=t&&h!=t)break;" "==a&&n==i.head||(t=h),n=l}return n}),hu=t=>au(t,!1);function cu(t){let e=[],i=-1;for(let n of t.selection.ranges){let s=t.doc.lineAt(n.from),r=t.doc.lineAt(n.to);if(n.empty||n.to!=r.from||(r=t.doc.lineAt(n.to-1)),i>=s.number){let t=e[e.length-1];t.to=r.to,t.ranges.push(n)}else e.push({from:s.from,to:r.to,ranges:[n]});i=r.number+1}return e}function uu(t,e,i){if(t.readOnly)return!1;let n=[],s=[];for(let e of cu(t)){if(i?e.to==t.doc.length:0==e.from)continue;let r=t.doc.lineAt(i?e.to+1:e.from-1),o=r.length+1;if(i){n.push({from:e.to,to:r.to},{from:e.from,insert:r.text+t.lineBreak});for(let i of e.ranges)s.push(W.range(Math.min(t.doc.length,i.anchor+o),Math.min(t.doc.length,i.head+o)))}else{n.push({from:r.from,to:e.from},{from:e.to,insert:t.lineBreak+r.text});for(let t of e.ranges)s.push(W.range(t.anchor-o,t.head-o))}}return!!n.length&&(e(t.update({changes:n,scrollIntoView:!0,selection:W.create(s,t.selection.mainIndex),userEvent:"move.line"})),!0)}function fu(t,e,i){if(t.readOnly)return!1;let n=[];for(let e of cu(t))i?n.push({from:e.from,insert:t.doc.slice(e.from,e.to)+t.lineBreak}):n.push({from:e.to,insert:t.lineBreak+t.doc.slice(e.from,e.to)});let s=t.changes(n);return e(t.update({changes:s,selection:t.selection.map(s,i?1:-1),scrollIntoView:!0,userEvent:"input.copyline"})),!0}const du=pu(!1);function pu(t){return({state:e,dispatch:i})=>{if(e.readOnly)return!1;let n=e.changeByRange(i=>{let{from:n,to:s}=i,r=e.doc.lineAt(n),o=!t&&n==s&&function(t,e){if(/\(\)|\[\]|\{\}/.test(t.sliceDoc(e-1,e+1)))return{from:e,to:e};let i,n=ka(t).resolveInner(e),s=n.childBefore(e),r=n.childAfter(e);return s&&r&&s.to<=e&&r.from>=e&&(i=s.type.prop(ml.closedBy))&&i.indexOf(r.name)>-1&&t.doc.lineAt(s.to).from==t.doc.lineAt(r.from).from&&!/\S/.test(t.sliceDoc(s.to,r.from))?{from:s.to,to:r.from}:null}(e,n);t&&(n=s=(s<=r.to?r:e.doc.lineAt(s)).to);let l=new Ha(e,{simulateBreak:n,simulateDoubleBreak:!!o}),a=Wa(l,n);for(null==a&&(a=Kt(/^\s*/.exec(e.doc.lineAt(n).text)[0],e.tabSize));sr.from&&n{let s=[];for(let r=n.from;r<=n.to;){let o=t.doc.lineAt(r);o.number>i&&(n.empty||n.to>o.from)&&(e(o,s,n),i=o.number),r=o.to+1}let r=t.changes(s);return{changes:s,range:W.range(r.mapPos(n.anchor,1),r.mapPos(n.head,1))}})}const gu=[{key:"Alt-ArrowLeft",mac:"Ctrl-ArrowLeft",run:t=>Cc(t,e=>Bc(t.state,e,!Oc(t))),shift:t=>{let e=!Oc(t);return Fc(t,e,i=>Bc(t.state,i,e))}},{key:"Alt-ArrowRight",mac:"Ctrl-ArrowRight",run:t=>Cc(t,e=>Bc(t.state,e,Oc(t))),shift:t=>{let e=Oc(t);return Fc(t,e,i=>Bc(t.state,i,e))}},{key:"Alt-ArrowUp",run:({state:t,dispatch:e})=>uu(t,e,!1)},{key:"Shift-Alt-ArrowUp",run:({state:t,dispatch:e})=>fu(t,e,!1)},{key:"Alt-ArrowDown",run:({state:t,dispatch:e})=>uu(t,e,!0)},{key:"Shift-Alt-ArrowDown",run:({state:t,dispatch:e})=>fu(t,e,!0)},{key:"Mod-Alt-ArrowUp",run:t=>iu(t,!1)},{key:"Mod-Alt-ArrowDown",run:t=>iu(t,!0)},{key:"Escape",run:({state:t,dispatch:e})=>{let i=t.selection,n=null;return i.ranges.length>1?n=W.create([i.main]):i.main.empty||(n=W.create([W.cursor(i.main.head)])),!!n&&(e(Sc(t,n)),!0)}},{key:"Mod-Enter",run:pu(!0)},{key:"Alt-l",mac:"Ctrl-l",run:({state:t,dispatch:e})=>{let i=cu(t).map(({from:e,to:i})=>W.range(e,Math.min(i+1,t.doc.length)));return e(t.update({selection:W.create(i),userEvent:"select"})),!0}},{key:"Mod-i",run:({state:t,dispatch:e})=>{let i=kc(t.selection,e=>{let i=ka(t),n=i.resolveStack(e.from,1);if(e.empty){let t=i.resolveStack(e.from,-1);t.node.from>=n.node.from&&t.node.to<=n.node.to&&(n=t)}for(let t=n;t;t=t.next){let{node:i}=t;if((i.from=e.to||i.to>e.to&&i.from<=e.from)&&t.next)return W.range(i.to,i.from)}return e});return!i.eq(t.selection)&&(e(Sc(t,i)),!0)},preventDefault:!0},{key:"Mod-[",run:({state:t,dispatch:e})=>!t.readOnly&&(e(t.update(mu(t,(e,i)=>{let n=/^\s*/.exec(e.text)[0];if(!n)return;let s=Kt(n,t.tabSize),r=0,o=Na(t,Math.max(0,s-Ia(t)));for(;r!t.readOnly&&(e(t.update(mu(t,(e,i)=>{i.push({from:e.from,insert:t.facet(La)})}),{userEvent:"input.indent"})),!0)},{key:"Mod-Alt-\\",run:({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=Object.create(null),n=new Ha(t,{overrideIndentation:t=>{let e=i[t];return null==e?-1:e}}),s=mu(t,(e,s,r)=>{let o=Wa(n,e.from);if(null==o)return;/\S/.test(e.text)||(o=0);let l=/^\s*/.exec(e.text)[0],a=Na(t,o);(l!=a||r.from{if(t.state.readOnly)return!1;let{state:e}=t,i=e.changes(cu(e).map(({from:t,to:i})=>(t>0?t--:i{let i;if(t.lineWrapping){let n=t.lineBlockAt(e.head),s=t.coordsAtPos(e.head,e.assoc||1);s&&(i=n.bottom+t.documentTop-s.bottom+t.defaultLineHeight/2)}return t.moveVertically(e,!0,i)}).map(i);return t.dispatch({changes:i,selection:n,scrollIntoView:!0,userEvent:"delete.line"}),!0}},{key:"Shift-Mod-\\",run:({state:t,dispatch:e})=>function(t,e,i){let n=!1,s=kc(t.selection,e=>{let s=Wh(t,e.head,-1)||Wh(t,e.head,1)||e.head>0&&Wh(t,e.head-1,1)||e.head{let{state:e}=t,i=e.doc.lineAt(e.selection.main.from),n=Gh(t.state,i.from);return n.line?Kh(t):!!n.block&&Xh(t)}},{key:"Alt-A",run:jh},{key:"Ctrl-m",mac:"Shift-Alt-m",run:t=>(t.setTabFocusMode(),!0)}].concat([{key:"ArrowLeft",run:Tc,shift:_c,preventDefault:!0},{key:"Mod-ArrowLeft",mac:"Alt-ArrowLeft",run:t=>Rc(t,!Oc(t)),shift:t=>Qc(t,!Oc(t)),preventDefault:!0},{mac:"Cmd-ArrowLeft",run:t=>Cc(t,e=>zc(t,e,!Oc(t))),shift:t=>{let e=!Oc(t);return Fc(t,e,i=>zc(t,i,e))},preventDefault:!0},{key:"ArrowRight",run:Dc,shift:Uc,preventDefault:!0},{key:"Mod-ArrowRight",mac:"Alt-ArrowRight",run:t=>Rc(t,Oc(t)),shift:t=>Qc(t,Oc(t)),preventDefault:!0},{mac:"Cmd-ArrowRight",run:t=>Cc(t,e=>zc(t,e,Oc(t))),shift:t=>{let e=Oc(t);return Fc(t,e,i=>zc(t,i,e))},preventDefault:!0},{key:"ArrowUp",run:Lc,shift:Kc,preventDefault:!0},{mac:"Cmd-ArrowUp",run:Jc,shift:tu},{mac:"Ctrl-ArrowUp",run:Hc,shift:Gc},{key:"ArrowDown",run:Ic,shift:jc,preventDefault:!0},{mac:"Cmd-ArrowDown",run:Zc,shift:eu},{mac:"Ctrl-ArrowDown",run:Vc,shift:Yc},{key:"PageUp",run:Hc,shift:Gc},{key:"PageDown",run:Vc,shift:Yc},{key:"Home",run:t=>Cc(t,e=>zc(t,e,!1)),shift:t=>Fc(t,!1,e=>zc(t,e,!1)),preventDefault:!0},{key:"Mod-Home",run:Jc,shift:tu},{key:"End",run:t=>Cc(t,e=>zc(t,e,!0)),shift:t=>Fc(t,!0,e=>zc(t,e,!0)),preventDefault:!0},{key:"Mod-End",run:Zc,shift:eu},{key:"Enter",run:du,shift:du},{key:"Mod-a",run:({state:t,dispatch:e})=>(e(t.update({selection:{anchor:0,head:t.doc.length},userEvent:"select"})),!0)},{key:"Backspace",run:ou,shift:ou,preventDefault:!0},{key:"Delete",run:lu,preventDefault:!0},{key:"Mod-Backspace",mac:"Alt-Backspace",run:hu,preventDefault:!0},{key:"Mod-Delete",mac:"Alt-Delete",run:t=>au(t,!0),preventDefault:!0},{mac:"Mod-Backspace",run:t=>nu(t,e=>{let i=t.moveToLineBoundary(e,!1).head;return e.head>i?i:Math.max(0,e.head-1)}),preventDefault:!0},{mac:"Mod-Delete",run:t=>nu(t,e=>{let i=t.moveToLineBoundary(e,!0).head;return e.headCc(t,e=>W.cursor(t.lineBlockAt(e.head).from,1)),shift:t=>Fc(t,!1,e=>W.cursor(t.lineBlockAt(e.head).from))},{key:"Ctrl-e",run:t=>Cc(t,e=>W.cursor(t.lineBlockAt(e.head).to,-1)),shift:t=>Fc(t,!0,e=>W.cursor(t.lineBlockAt(e.head).to))},{key:"Ctrl-d",run:lu},{key:"Ctrl-h",run:ou},{key:"Ctrl-k",run:t=>nu(t,e=>{let i=t.lineBlockAt(e.head).to;return e.head{if(t.readOnly)return!1;let i=t.changeByRange(t=>({changes:{from:t.from,to:t.to,insert:f.of(["",""])},range:W.cursor(t.from)}));return e(t.update(i,{scrollIntoView:!0,userEvent:"input"})),!0}},{key:"Ctrl-t",run:({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=t.changeByRange(e=>{if(!e.empty||0==e.from||e.from==t.doc.length)return{range:e};let i=e.from,n=t.doc.lineAt(i),s=i==n.from?i-1:k(n.text,i-n.from,!1)+n.from,r=i==n.to?i+1:k(n.text,i-n.from,!0)+n.from;return{changes:{from:s,to:r,insert:t.doc.slice(i,r).append(t.doc.slice(s,i))},range:W.cursor(r)}});return!i.changes.empty&&(e(t.update(i,{scrollIntoView:!0,userEvent:"move.character"})),!0)}},{key:"Ctrl-v",run:Vc}].map(t=>({mac:t.key,run:t.run,shift:t.shift})))),vu="function"==typeof String.prototype.normalize?t=>t.normalize("NFKD"):t=>t;class wu{constructor(t,e,i=0,n=t.length,s,r){this.test=r,this.value={from:0,to:0,precise:!1},this.done=!1,this.matches=[],this.buffer="",this.bufferPos=0,this.iter=t.iterRange(i,n),this.bufferStart=i,this.normalize=s?t=>s(vu(t)):vu,this.query=this.normalize(e)}peek(){if(this.bufferPos==this.buffer.length){if(this.bufferStart+=this.buffer.length,this.iter.next(),this.iter.done)return-1;this.bufferPos=0,this.buffer=this.iter.value}return S(this.buffer,this.bufferPos)}next(){for(;this.matches.length;)this.matches.pop();return this.nextOverlapping()}nextOverlapping(){for(;;){let t=this.peek();if(t<0)return this.done=!0,this;let e=C(t),i=this.bufferStart+this.bufferPos;this.bufferPos+=A(t);let n=this.normalize(e);if(n.length)for(let t=0,s=i,r=!0;;t++){let i=n.charCodeAt(t),o=this.match(i,s,r,this.bufferPos+this.bufferStart,t==n.length-1);if(o)return this.value=o,this;if(t==n.length-1)break;r&&tthis.to&&(this.curLine=this.curLine.slice(0,this.to-this.curLineStart)),this.iter.next())}nextLine(){this.curLineStart=this.curLineStart+this.curLine.length+1,this.curLineStart>this.to?this.curLine="":this.getLine(0)}next(){for(let t=this.matchPos-this.curLineStart;;){this.re.lastIndex=t;let e=this.matchPos<=this.to&&this.re.exec(this.curLine);if(e){let i=this.curLineStart+e.index,n=i+e[0].length;if(this.matchPos=Au(this.text,n+(i==n?1:0)),i==this.curLineStart+this.curLine.length&&this.nextLine(),(ithis.value.to)&&(!this.test||this.test(i,n,e)))return this.value={from:i,to:n,precise:!0,match:e},this;t=this.matchPos-this.curLineStart}else{if(!(this.curLineStart+this.curLine.length=i||n.to<=e){let n=new Su(e,t.sliceString(e,i));return ku.set(t,n),n}if(n.from==e&&n.to==i)return n;let{text:s,from:r}=n;return r>e&&(s=t.sliceString(e,r)+s,r=e),n.to=this.to?this.to:this.text.lineAt(t).to}next(){for(;;){let t=this.re.lastIndex=this.matchPos-this.flat.from,e=this.re.exec(this.flat.text);if(e&&!e[0]&&e.index==t&&(this.re.lastIndex=t+1,e=this.re.exec(this.flat.text)),e){let t=this.flat.from+e.index,i=t+e[0].length;if((this.flat.to>=this.to||e.index+e[0].length<=this.flat.text.length-10)&&(!this.test||this.test(t,i,e)))return this.value={from:t,to:i,precise:!0,match:e},this.matchPos=Au(this.text,i+(t==i?1:0)),this}if(this.flat.to==this.to)return this.done=!0,this;this.flat=Su.get(this.text,this.flat.from,this.chunkEnd(this.flat.from+2*this.flat.text.length))}}}function Au(t,e){if(e>=t.length)return e;let i,n=t.lineAt(e);for(;e=56320&&i<57344;)e++;return e}"undefined"!=typeof Symbol&&(xu.prototype[Symbol.iterator]=Cu.prototype[Symbol.iterator]=function(){return this});const Mu={highlightWordAroundCursor:!1,minSelectionLength:1,maxMatches:100,wholeWords:!1},Ou=z.define({combine:t=>Dt(t,Mu,{highlightWordAroundCursor:(t,e)=>t||e,minSelectionLength:Math.min,maxMatches:Math.min})});function Tu(t){let e=[Eu,Bu];return t&&e.push(Ou.of(t)),e}const Du=Te.mark({class:"cm-selectionMatch"}),Ru=Te.mark({class:"cm-selectionMatch cm-selectionMatch-main"});function Pu(t,e,i,n){return!(0!=i&&t(e.sliceDoc(i-1,i))==Ct.Word||n!=e.doc.length&&t(e.sliceDoc(n,n+1))==Ct.Word)}const Bu=qi.fromClass(class{constructor(t){this.decorations=this.getDeco(t)}update(t){(t.selectionSet||t.docChanged||t.viewportChanged)&&(this.decorations=this.getDeco(t.view))}getDeco(t){let e=t.state.facet(Ou),{state:i}=t,n=i.selection;if(n.ranges.length>1)return Te.none;let s,r=n.main,o=null;if(r.empty){if(!e.highlightWordAroundCursor)return Te.none;let t=i.wordAt(r.head);if(!t)return Te.none;o=i.charCategorizer(r.head),s=i.sliceDoc(t.from,t.to)}else{let t=r.to-r.from;if(t200)return Te.none;if(e.wholeWords){if(s=i.sliceDoc(r.from,r.to),o=i.charCategorizer(r.head),!Pu(o,i,r.from,r.to)||!function(t,e,i,n){return t(e.sliceDoc(i,i+1))==Ct.Word&&t(e.sliceDoc(n-1,n))==Ct.Word}(o,i,r.from,r.to))return Te.none}else if(s=i.sliceDoc(r.from,r.to),!s)return Te.none}let l=[];for(let n of t.visibleRanges){let t=new wu(i.doc,s,n.from,n.to);for(;!t.next().done;){let{from:n,to:s}=t.value;if((!o||Pu(o,i,n,s))&&(r.empty&&n<=r.from&&s>=r.to?l.push(Ru.range(n,s)):(n>=r.to||s<=r.from)&&l.push(Du.range(n,s)),l.length>e.maxMatches))return Te.none}}return Te.set(l)}},{decorations:t=>t.decorations}),Eu=pr.baseTheme({".cm-selectionMatch":{backgroundColor:"#99ff7780"},".cm-searchMatch .cm-selectionMatch":{backgroundColor:"transparent"}});const Lu=z.define({combine:t=>Dt(t,{top:!1,caseSensitive:!1,literal:!1,regexp:!1,wholeWord:!1,createPanel:t=>new cf(t),scrollToMatch:t=>pr.scrollIntoView(t)})});class Iu{constructor(t){this.search=t.search,this.caseSensitive=!!t.caseSensitive,this.literal=!!t.literal,this.regexp=!!t.regexp,this.replace=t.replace||"",this.valid=!!this.search&&(!this.regexp||function(t){try{return new RegExp(t,yu),!0}catch(t){return!1}}(this.search)),this.unquoted=this.unquote(this.search),this.wholeWord=!!t.wholeWord,this.test=t.test}unquote(t){return this.literal?t:t.replace(/\\([nrt\\])/g,(t,e)=>"n"==e?"\n":"r"==e?"\r":"t"==e?"\t":"\\")}eq(t){return this.search==t.search&&this.replace==t.replace&&this.caseSensitive==t.caseSensitive&&this.regexp==t.regexp&&this.wholeWord==t.wholeWord&&this.test==t.test}create(){return this.regexp?new qu(this):new Hu(this)}getCursor(t,e=0,i){let n=t.doc?t:Tt.create({doc:t});return null==i&&(i=n.doc.length),this.regexp?Vu(this,n,e,i):Wu(this,n,e,i)}}class Nu{constructor(t){this.spec=t}}function Wu(t,e,i,n){let s;return t.wholeWord&&(s=function(t,e){return(i,n,s,r)=>((r>i||r+s.length{if(i&&!i(n,s,r,o))return!1;let l=n>=o&&s<=o+r.length?r.slice(n-o,s-o):e.doc.sliceString(n,s);return t(l,e,n,s)}}(t.test,e,s)),new wu(e.doc,t.unquoted,i,n,t.caseSensitive?void 0:t=>t.toLowerCase(),s)}class Hu extends Nu{constructor(t){super(t)}nextMatch(t,e,i){let n=Wu(this.spec,t,i,t.doc.length).nextOverlapping();if(n.done){let i=Math.min(t.doc.length,e+this.spec.unquoted.length);n=Wu(this.spec,t,0,i).nextOverlapping()}return n.done||n.value.from==e&&n.value.to==i?null:n.value}prevMatchInRange(t,e,i){for(let n=i;;){let i=Math.max(e,n-1e4-this.spec.unquoted.length),s=Wu(this.spec,t,i,n),r=null;for(;!s.nextOverlapping().done;)r=s.value;if(r)return r;if(i==e)return null;n-=1e4}}prevMatch(t,e,i){let n=this.prevMatchInRange(t,0,e);return n||(n=this.prevMatchInRange(t,Math.max(0,i-this.spec.unquoted.length),t.doc.length)),!n||n.from==e&&n.to==i?null:n}getReplacement(t){return this.spec.unquote(this.spec.replace)}matchAll(t,e){let i=Wu(this.spec,t,0,t.doc.length),n=[];for(;!i.next().done;){if(n.length>=e)return null;n.push(i.value)}return n}highlight(t,e,i,n){let s=Wu(this.spec,t,Math.max(0,e-this.spec.unquoted.length),Math.min(i+this.spec.unquoted.length,t.doc.length));for(;!s.next().done;)n(s.value.from,s.value.to)}}function Vu(t,e,i,n){let s;var r;return t.wholeWord&&(r=e.charCategorizer(e.selection.main.head),s=(t,e,i)=>!i[0].length||(r(zu(i.input,i.index))!=Ct.Word||r(Fu(i.input,i.index))!=Ct.Word)&&(r(Fu(i.input,i.index+i[0].length))!=Ct.Word||r(zu(i.input,i.index+i[0].length))!=Ct.Word)),t.test&&(s=function(t,e,i){return(n,s,r)=>(!i||i(n,s,r))&&t(r[0],e,n,s)}(t.test,e,s)),new xu(e.doc,t.search,{ignoreCase:!t.caseSensitive,test:s},i,n)}function zu(t,e){return t.slice(k(t,e,!1),e)}function Fu(t,e){return t.slice(e,k(t,e))}class qu extends Nu{nextMatch(t,e,i){let n=Vu(this.spec,t,i,t.doc.length).next();return n.done&&(n=Vu(this.spec,t,0,e).next()),n.done?null:n.value}prevMatchInRange(t,e,i){for(let n=1;;n++){let s=Math.max(e,i-1e4*n),r=Vu(this.spec,t,s,i),o=null;for(;!r.next().done;)o=r.value;if(o&&(s==e||o.from>s+10))return o;if(s==e)return null}}prevMatch(t,e,i){return this.prevMatchInRange(t,0,e)||this.prevMatchInRange(t,i,t.doc.length)}getReplacement(t){return this.spec.unquote(this.spec.replace).replace(/\$([$&]|\d+)/g,(e,i)=>{if("&"==i)return t.match[0];if("$"==i)return"$";for(let e=i.length;e>0;e--){let n=+i.slice(0,e);if(n>0&&n=e)return null;n.push(i.value)}return n}highlight(t,e,i,n){let s=Vu(this.spec,t,Math.max(0,e-250),Math.min(i+250,t.doc.length));for(;!s.next().done;)n(s.value.from,s.value.to)}}const _u=gt.define(),Uu=gt.define(),Qu=K.define({create:t=>new $u(sf(t).create(),null),update(t,e){for(let i of e.effects)i.is(_u)?t=new $u(i.value.create(),t.panel):i.is(Uu)&&(t=new $u(t.query,i.value?nf:null));return t},provide:t=>Wo.from(t,t=>t.panel)});class $u{constructor(t,e){this.query=t,this.panel=e}}const Ku=Te.mark({class:"cm-searchMatch"}),ju=Te.mark({class:"cm-searchMatch cm-searchMatch-selected"}),Xu=qi.fromClass(class{constructor(t){this.view=t,this.decorations=this.highlight(t.state.field(Qu))}update(t){let e=t.state.field(Qu);(e!=t.startState.field(Qu)||t.docChanged||t.selectionSet||t.viewportChanged)&&(this.decorations=this.highlight(e))}highlight({query:t,panel:e}){if(!e||!t.spec.valid)return Te.none;let{view:i}=this,n=new Nt;for(let e=0,s=i.visibleRanges,r=s.length;es[e+1].from-500;)l=s[++e].to;t.highlight(i.state,o,l,(t,e)=>{let s=i.state.selection.ranges.some(i=>i.from==t&&i.to==e);n.add(t,e,s?ju:Ku)})}return n.finish()}},{decorations:t=>t.decorations});function Gu(t){return e=>{let i=e.state.field(Qu,!1);return i&&i.query.spec.valid?t(e,i):lf(e)}}const Yu=Gu((t,{query:e})=>{let{to:i}=t.state.selection.main,n=e.nextMatch(t.state,i,i);if(!n)return!1;let s=W.single(n.from,n.to),r=t.state.facet(Lu);return t.dispatch({selection:s,effects:[pf(t,n),r.scrollToMatch(s.main,t)],userEvent:"select.search"}),of(t),!0}),Ju=Gu((t,{query:e})=>{let{state:i}=t,{from:n}=i.selection.main,s=e.prevMatch(i,n,n);if(!s)return!1;let r=W.single(s.from,s.to),o=t.state.facet(Lu);return t.dispatch({selection:r,effects:[pf(t,s),o.scrollToMatch(r.main,t)],userEvent:"select.search"}),of(t),!0}),Zu=Gu((t,{query:e})=>{let i=e.matchAll(t.state,1e3);return!(!i||!i.length)&&(t.dispatch({selection:W.create(i.map(t=>W.range(t.from,t.to))),userEvent:"select.search.matches"}),!0)}),tf=Gu((t,{query:e})=>{let{state:i}=t,{from:n,to:s}=i.selection.main;if(i.readOnly)return!1;let r=e.nextMatch(i,n,n);if(!r)return!1;let o,l,a=r,h=[],c=[];a.precise?a.from==n&&a.to==s&&(l=i.toText(e.getReplacement(a)),h.push({from:a.from,to:a.to,insert:l}),a=e.nextMatch(i,a.from,a.to),c.push(pr.announce.of(i.phrase("replaced match on line $",i.doc.lineAt(n).number)+"."))):a=e.nextMatch(i,a.from,a.to);let u=t.state.changes(h);return a&&(o=W.single(a.from,a.to).map(u),c.push(pf(t,a)),c.push(i.facet(Lu).scrollToMatch(o.main,t))),t.dispatch({changes:u,selection:o,effects:c,userEvent:"input.replace"}),!0}),ef=Gu((t,{query:e})=>{if(t.state.readOnly)return!1;let i=[];for(let n of e.matchAll(t.state,1e9)){let{from:t,to:s,precise:r}=n;r&&i.push({from:t,to:s,insert:e.getReplacement(n)})}if(!i.length)return!1;let n=t.state.phrase("replaced $ matches",i.length)+".";return t.dispatch({changes:i,effects:pr.announce.of(n),userEvent:"input.replace.all"}),!0});function nf(t){return t.state.facet(Lu).createPanel(t)}function sf(t,e){var i,n,s,r,o;let l=t.selection.main,a=l.empty||l.to>l.from+100?"":t.sliceDoc(l.from,l.to);if(e&&!a)return e;let h=t.facet(Lu);return new Iu({search:(null!==(i=null==e?void 0:e.literal)&&void 0!==i?i:h.literal)?a:a.replace(/\n/g,"\\n"),caseSensitive:null!==(n=null==e?void 0:e.caseSensitive)&&void 0!==n?n:h.caseSensitive,literal:null!==(s=null==e?void 0:e.literal)&&void 0!==s?s:h.literal,regexp:null!==(r=null==e?void 0:e.regexp)&&void 0!==r?r:h.regexp,wholeWord:null!==(o=null==e?void 0:e.wholeWord)&&void 0!==o?o:h.wholeWord})}function rf(t){let e=Eo(t,nf);return e&&e.dom.querySelector("[main-field]")}function of(t){let e=rf(t);e&&e==t.root.activeElement&&e.select()}const lf=t=>{let e=t.state.field(Qu,!1);if(e&&e.panel){let i=rf(t);if(i&&i!=t.root.activeElement){let n=sf(t.state,e.query.spec);n.valid&&t.dispatch({effects:_u.of(n)}),i.focus(),i.select()}}else t.dispatch({effects:[Uu.of(!0),e?_u.of(sf(t.state,e.query.spec)):gt.appendConfig.of(gf)]});return!0},af=t=>{let e=t.state.field(Qu,!1);if(!e||!e.panel)return!1;let i=Eo(t,nf);return i&&i.dom.contains(t.root.activeElement)&&t.focus(),t.dispatch({effects:Uu.of(!1)}),!0},hf=[{key:"Mod-f",run:lf,scope:"editor search-panel"},{key:"F3",run:Yu,shift:Ju,scope:"editor search-panel",preventDefault:!0},{key:"Mod-g",run:Yu,shift:Ju,scope:"editor search-panel",preventDefault:!0},{key:"Escape",run:af,scope:"editor search-panel"},{key:"Mod-Shift-l",run:({state:t,dispatch:e})=>{let i=t.selection;if(i.ranges.length>1||i.main.empty)return!1;let{from:n,to:s}=i.main,r=[],o=0;for(let e=new wu(t.doc,t.sliceDoc(n,s));!e.next().done;){if(r.length>1e3)return!1;e.value.from==n&&(o=r.length),r.push(W.range(e.value.from,e.value.to))}return e(t.update({selection:W.create(r,o),userEvent:"select.search.matches"})),!0}},{key:"Mod-Alt-g",run:t=>{let{state:e}=t,i=String(e.doc.lineAt(t.state.selection.main.head).number),{close:n,result:s}=Ho(t,{label:e.phrase("Go to line"),input:{type:"text",name:"line",value:i},focus:!0,submitLabel:e.phrase("go")});return s.then(i=>{let s=i&&/^([+-])?(\d+)?(:\d+)?(%)?$/.exec(i.elements.line.value);if(!s)return void t.dispatch({effects:n});let r=e.doc.lineAt(e.selection.main.head),[,o,l,a,h]=s,c=a?+a.slice(1):0,u=l?+l:r.number;if(l&&h){let t=u/100;o&&(t=t*("-"==o?-1:1)+r.number/e.doc.lines),u=Math.round(e.doc.lines*t)}else l&&o&&(u=u*("-"==o?-1:1)+r.number);let f=e.doc.line(Math.max(1,Math.min(e.doc.lines,u))),d=W.cursor(f.from+Math.max(0,Math.min(c,f.length)));t.dispatch({effects:[n,pr.scrollIntoView(d.from,{y:"center"})],selection:d})}),!0}},{key:"Mod-d",run:({state:t,dispatch:e})=>{let{ranges:i}=t.selection;if(i.some(t=>t.from===t.to))return(({state:t,dispatch:e})=>{let{selection:i}=t,n=W.create(i.ranges.map(e=>t.wordAt(e.head)||W.cursor(e.head)),i.mainIndex);return!n.eq(i)&&(e(t.update({selection:n})),!0)})({state:t,dispatch:e});let n=t.sliceDoc(i[0].from,i[0].to);if(t.selection.ranges.some(e=>t.sliceDoc(e.from,e.to)!=n))return!1;let s=function(t,e){let{main:i,ranges:n}=t.selection,s=t.wordAt(i.head),r=s&&s.from==i.from&&s.to==i.to;for(let i=!1,s=new wu(t.doc,e,n[n.length-1].to);;){if(s.next(),!s.done){if(i&&n.some(t=>t.from==s.value.from))continue;if(r){let e=t.wordAt(s.value.from);if(!e||e.from!=s.value.from||e.to!=s.value.to)continue}return s.value}if(i)return null;s=new wu(t.doc,e,0,Math.max(0,n[n.length-1].from-1)),i=!0}}(t,n);return!!s&&(e(t.update({selection:t.selection.addRange(W.range(s.from,s.to),!1),effects:pr.scrollIntoView(s.to)})),!0)},preventDefault:!0}];class cf{constructor(t){this.view=t;let e=this.query=t.state.field(Qu).query.spec;function i(t,e,i){return le("button",{class:"cm-button",name:t,onclick:e,type:"button"},i)}this.commit=this.commit.bind(this),this.searchField=le("input",{value:e.search,placeholder:uf(t,"Find"),"aria-label":uf(t,"Find"),class:"cm-textfield",name:"search",form:"","main-field":"true",onchange:this.commit,onkeyup:this.commit}),this.replaceField=le("input",{value:e.replace,placeholder:uf(t,"Replace"),"aria-label":uf(t,"Replace"),class:"cm-textfield",name:"replace",form:"",onchange:this.commit,onkeyup:this.commit}),this.caseField=le("input",{type:"checkbox",name:"case",form:"",checked:e.caseSensitive,onchange:this.commit}),this.reField=le("input",{type:"checkbox",name:"re",form:"",checked:e.regexp,onchange:this.commit}),this.wordField=le("input",{type:"checkbox",name:"word",form:"",checked:e.wholeWord,onchange:this.commit}),this.dom=le("div",{onkeydown:t=>this.keydown(t),class:"cm-search"},[this.searchField,i("next",()=>Yu(t),[uf(t,"next")]),i("prev",()=>Ju(t),[uf(t,"previous")]),i("select",()=>Zu(t),[uf(t,"all")]),le("label",null,[this.caseField,uf(t,"match case")]),le("label",null,[this.reField,uf(t,"regexp")]),le("label",null,[this.wordField,uf(t,"by word")]),...t.state.readOnly?[]:[le("br"),this.replaceField,i("replace",()=>tf(t),[uf(t,"replace")]),i("replaceAll",()=>ef(t),[uf(t,"replace all")])],le("button",{name:"close",onclick:()=>af(t),"aria-label":uf(t,"close"),type:"button"},["×"])])}commit(){let t=new Iu({search:this.searchField.value,caseSensitive:this.caseField.checked,regexp:this.reField.checked,wholeWord:this.wordField.checked,replace:this.replaceField.value});t.eq(this.query)||(this.query=t,this.view.dispatch({effects:_u.of(t)}))}keydown(t){var e,i,n;e=this.view,i=t,n="search-panel",Tr(Cr(e.state),i,e,n)?t.preventDefault():13==t.keyCode&&t.target==this.searchField?(t.preventDefault(),(t.shiftKey?Ju:Yu)(this.view)):13==t.keyCode&&t.target==this.replaceField&&(t.preventDefault(),tf(this.view))}update(t){for(let e of t.transactions)for(let t of e.effects)t.is(_u)&&!t.value.eq(this.query)&&this.setQuery(t.value)}setQuery(t){this.query=t,this.searchField.value=t.search,this.replaceField.value=t.replace,this.caseField.checked=t.caseSensitive,this.reField.checked=t.regexp,this.wordField.checked=t.wholeWord}mount(){this.searchField.select()}get pos(){return 80}get top(){return this.view.state.facet(Lu).top}}function uf(t,e){return t.state.phrase(e)}const ff=30,df=/[\s\.,:;?!]/;function pf(t,{from:e,to:i}){let n=t.state.doc.lineAt(e),s=t.state.doc.lineAt(i).to,r=Math.max(n.from,e-ff),o=Math.min(s,i+ff),l=t.state.sliceDoc(r,o);if(r!=n.from)for(let t=0;tl.length-ff;t--)if(!df.test(l[t-1])&&df.test(l[t])){l=l.slice(0,t);break}return pr.announce.of(`${t.state.phrase("current match")}. ${l} ${t.state.phrase("on line")} ${n.number}.`)}const mf=pr.baseTheme({".cm-panel.cm-search":{padding:"2px 6px 4px",position:"relative","& [name=close]":{position:"absolute",top:"0",right:"4px",backgroundColor:"inherit",border:"none",font:"inherit",padding:0,margin:0},"& input, & button, & label":{margin:".2em .6em .2em 0"},"& input[type=checkbox]":{marginRight:".2em"},"& label":{fontSize:"80%",whiteSpace:"pre"}},"&light .cm-searchMatch":{backgroundColor:"#ffff0054"},"&dark .cm-searchMatch":{backgroundColor:"#00ffff8a"},"&light .cm-searchMatch-selected":{backgroundColor:"#ff6a0054"},"&dark .cm-searchMatch-selected":{backgroundColor:"#ff00ff8a"}}),gf=[Qu,Z.low(Xu),mf];class vf{constructor(t,e,i,n){this.state=t,this.pos=e,this.explicit=i,this.view=n,this.abortListeners=[],this.abortOnDocChange=!1}tokenBefore(t){let e=ka(this.state).resolveInner(this.pos,-1);for(;e&&t.indexOf(e.name)<0;)e=e.parent;return e?{from:e.from,to:this.pos,text:this.state.sliceDoc(e.from,this.pos),type:e.type}:null}matchBefore(t){let e=this.state.doc.lineAt(this.pos),i=Math.max(e.from,this.pos-250),n=e.text.slice(i-e.from,this.pos-e.from),s=n.search(kf(t,!1));return s<0?null:{from:i+s,to:this.pos,text:n.slice(s)}}get aborted(){return null==this.abortListeners}addEventListener(t,e,i){"abort"==t&&this.abortListeners&&(this.abortListeners.push(e),i&&i.onDocChange&&(this.abortOnDocChange=!0))}}function wf(t){let e=Object.keys(t).join(""),i=/\w/.test(e);return i&&(e=e.replace(/\w/g,"")),`[${i?"\\w":""}${e.replace(/[^\w\s]/g,"\\$&")}]`}function bf(t){let e=t.map(t=>"string"==typeof t?{label:t}:t),[i,n]=e.every(t=>/^\w+$/.test(t.label))?[/\w*$/,/\w+$/]:function(t){let e=Object.create(null),i=Object.create(null);for(let{label:n}of t){e[n[0]]=!0;for(let t=1;t{let s=t.matchBefore(n);return s||t.explicit?{from:s?s.from:t.pos,options:e,validFor:i}:null}}class yf{constructor(t,e,i,n){this.completion=t,this.source=e,this.match=i,this.score=n}}function xf(t){return t.selection.main.from}function kf(t,e){var i;let{source:n}=t,s=e&&"^"!=n[0],r="$"!=n[n.length-1];return s||r?new RegExp(`${s?"^":""}(?:${n})${r?"$":""}`,null!==(i=t.flags)&&void 0!==i?i:t.ignoreCase?"i":""):t}const Sf=dt.define();function Cf(t,e,i,n){let{main:s}=t.selection,r=i-s.from,o=n-s.from;return{...t.changeByRange(l=>{if(l!=s&&i!=n&&t.sliceDoc(l.from+r,l.from+o)!=t.sliceDoc(i,n))return{range:l};let a=t.toText(e);return{changes:{from:l.from+r,to:n==s.from?l.to:l.from+o,insert:a},range:W.cursor(l.from+r+a.length)}}),scrollIntoView:!0,userEvent:"input.complete"}}const Af=new WeakMap;function Mf(t){if(!Array.isArray(t))return t;let e=Af.get(t);return e||Af.set(t,e=bf(t)),e}const Of=gt.define(),Tf=gt.define();class Df{constructor(t){this.pattern=t,this.chars=[],this.folded=[],this.any=[],this.precise=[],this.byWord=[],this.score=0,this.matched=[];for(let e=0;e=48&&a<=57||a>=97&&a<=122?2:a>=65&&a<=90?1:0:(w=C(a))!=w.toLowerCase()?1:w!=w.toUpperCase()?2:0;(!n||1==b&&m||0==v&&0!=b)&&(e[c]==a||i[c]==a&&(u=!0)?r[c++]=n:r.length&&(g=!1)),v=b,n+=A(a)}return c==l&&0==r[0]&&g?this.result((u?-200:0)-100,r,t):f==l&&0==d?this.ret(-200-t.length+(p==t.length?0:-100),[0,p]):o>-1?this.ret(-700-t.length,[o,o+this.pattern.length]):f==l?this.ret(-900-t.length,[d,p]):c==l?this.result((u?-200:0)-100-700+(g?0:-1100),r,t):2==e.length?null:this.result((n[0]?-700:0)-200-1100,n,t)}result(t,e,i){let n=[],s=0;for(let t of e){let e=t+(this.astral?A(S(i,t)):1);s&&n[s-1]==t?n[s-1]=e:(n[s++]=t,n[s++]=e)}return this.ret(t-i.length,n)}}class Rf{constructor(t){this.pattern=t,this.matched=[],this.score=0,this.folded=t.toLowerCase()}match(t){if(t.lengthDt(t,{activateOnTyping:!0,activateOnCompletion:()=>!1,activateOnTypingDelay:100,selectOnOpen:!0,override:null,closeOnBlur:!0,maxRenderedOptions:100,defaultKeymap:!0,tooltipClass:()=>"",optionClass:()=>"",aboveCursor:!1,icons:!0,addToOptions:[],positionInfo:Ef,filterStrict:!1,compareCompletions:(t,e)=>(t.sortText||t.label).localeCompare(e.sortText||e.label),interactionDelay:75,updateSyncTime:100},{defaultKeymap:(t,e)=>t&&e,closeOnBlur:(t,e)=>t&&e,icons:(t,e)=>t&&e,tooltipClass:(t,e)=>i=>Bf(t(i),e(i)),optionClass:(t,e)=>i=>Bf(t(i),e(i)),addToOptions:(t,e)=>t.concat(e),filterStrict:(t,e)=>t||e})});function Bf(t,e){return t?e?t+" "+e:t:e}function Ef(t,e,i,n,s,r){let o,l,a=t.textDirection==si.RTL,h=a,c=!1,u="top",f=e.left-s.left,d=s.right-e.right,p=n.right-n.left,m=n.bottom-n.top;if(h&&f=m||t>e.top?o=i.bottom-e.top:(u="bottom",o=e.bottom-i.top)}return{style:`${u}: ${o/((e.bottom-e.top)/r.offsetHeight)}px; max-width: ${l/((e.right-e.left)/r.offsetWidth)}px`,class:"cm-completionInfo-"+(c?a?"left-narrow":"right-narrow":h?"left":"right")}}const Lf=gt.define();function If(t,e,i){if(t<=i)return{from:0,to:t};if(e<0&&(e=0),e<=t>>1){let t=Math.floor(e/i);return{from:t*i,to:(t+1)*i}}let n=Math.ceil((t-e)/i);return{from:t-n*i,to:t-(n-1)*i}}class Nf{constructor(t,e,i){this.view=t,this.stateField=e,this.applyCompletion=i,this.info=null,this.infoDestroy=null,this.placeInfoReq={read:()=>this.measureInfo(),write:t=>this.placeInfo(t),key:this},this.space=null,this.currentClass="";let n=t.state.field(e),{options:s,selected:r}=n.open,o=t.state.facet(Pf);this.optionContent=function(t){let e=t.addToOptions.slice();return t.icons&&e.push({render(t){let e=document.createElement("div");return e.classList.add("cm-completionIcon"),t.type&&e.classList.add(...t.type.split(/\s+/g).map(t=>"cm-completionIcon-"+t)),e.setAttribute("aria-hidden","true"),e},position:20}),e.push({render(t,e,i,n){let s=document.createElement("span");s.className="cm-completionLabel";let r=t.displayLabel||t.label,o=0;for(let t=0;to&&s.appendChild(document.createTextNode(r.slice(o,e)));let l=s.appendChild(document.createElement("span"));l.appendChild(document.createTextNode(r.slice(e,i))),l.className="cm-completionMatchedText",o=i}return ot.position-e.position).map(t=>t.render)}(o),this.optionClass=o.optionClass,this.tooltipClass=o.tooltipClass,this.range=If(s.length,r,o.maxRenderedOptions),this.dom=document.createElement("div"),this.dom.className="cm-tooltip-autocomplete",this.updateTooltipClass(t.state),this.dom.addEventListener("mousedown",i=>{let{options:n}=t.state.field(e).open;for(let e,s=i.target;s&&s!=this.dom;s=s.parentNode)if("LI"==s.nodeName&&(e=/-(\d+)$/.exec(s.id))&&+e[1]this.list.lastChild.getBoundingClientRect().bottom?this.range.to:null;null!=e&&(t.dispatch({effects:Lf.of(e)}),i.preventDefault())}}),this.dom.addEventListener("focusout",e=>{let i=t.state.field(this.stateField,!1);i&&i.tooltip&&t.state.facet(Pf).closeOnBlur&&e.relatedTarget!=t.contentDOM&&t.dispatch({effects:Tf.of(null)})}),this.showOptions(s,n.id)}mount(){this.updateSel()}showOptions(t,e){this.list&&this.list.remove(),this.list=this.dom.appendChild(this.createListBox(t,e,this.range)),this.list.addEventListener("scroll",()=>{this.info&&this.view.requestMeasure(this.placeInfoReq)})}update(t){var e;let i=t.state.field(this.stateField),n=t.startState.field(this.stateField);if(this.updateTooltipClass(t.state),i!=n){let{options:s,selected:r,disabled:o}=i.open;n.open&&n.open.options==s||(this.range=If(s.length,r,t.state.facet(Pf).maxRenderedOptions),this.showOptions(s,i.id)),this.updateSel(),o!=(null===(e=n.open)||void 0===e?void 0:e.disabled)&&this.dom.classList.toggle("cm-tooltip-autocomplete-disabled",!!o)}}updateTooltipClass(t){let e=this.tooltipClass(t);if(e!=this.currentClass){for(let t of this.currentClass.split(" "))t&&this.dom.classList.remove(t);for(let t of e.split(" "))t&&this.dom.classList.add(t);this.currentClass=e}}positioned(t){this.space=t,this.info&&this.view.requestMeasure(this.placeInfoReq)}updateSel(){let t=this.view.state.field(this.stateField),e=t.open;(e.selected>-1&&e.selected=this.range.to)&&(this.range=If(e.options.length,e.selected,this.view.state.facet(Pf).maxRenderedOptions),this.showOptions(e.options,t.id));let i=this.updateSelectedOption(e.selected);if(i){this.destroyInfo();let{completion:n}=e.options[e.selected],{info:s}=n;if(!s)return;let r="string"==typeof s?document.createTextNode(s):s(n);if(!r)return;"then"in r?r.then(e=>{e&&this.view.state.field(this.stateField,!1)==t&&this.addInfoPane(e,n)}).catch(t=>Hi(this.view.state,t,"completion info")):(this.addInfoPane(r,n),i.setAttribute("aria-describedby",this.info.id))}}addInfoPane(t,e){this.destroyInfo();let i=this.info=document.createElement("div");if(i.className="cm-tooltip cm-completionInfo",i.id="cm-completionInfo-"+Math.floor(65535*Math.random()).toString(16),null!=t.nodeType)i.appendChild(t),this.infoDestroy=null;else{let{dom:e,destroy:n}=t;i.appendChild(e),this.infoDestroy=n||null}this.dom.appendChild(i),this.view.requestMeasure(this.placeInfoReq)}updateSelectedOption(t){let e=null;for(let i=this.list.firstChild,n=this.range.from;i;i=i.nextSibling,n++)"LI"==i.nodeName&&i.id?n==t?i.hasAttribute("aria-selected")||(i.setAttribute("aria-selected","true"),e=i):i.hasAttribute("aria-selected")&&(i.removeAttribute("aria-selected"),i.removeAttribute("aria-describedby")):n--;return e&&function(t,e){let i=t.getBoundingClientRect(),n=e.getBoundingClientRect(),s=i.height/t.offsetHeight;n.topi.bottom&&(t.scrollTop+=(n.bottom-i.bottom)/s)}(this.list,e),e}measureInfo(){let t=this.dom.querySelector("[aria-selected]");if(!t||!this.info)return null;let e=this.dom.getBoundingClientRect(),i=this.info.getBoundingClientRect(),n=t.getBoundingClientRect(),s=this.space;if(!s){let t=this.dom.ownerDocument.documentElement;s={left:0,top:0,right:t.clientWidth,bottom:t.clientHeight}}return n.top>Math.min(s.bottom,e.bottom)-10||n.bottom{t.target==n&&t.preventDefault()});let s=null;for(let r=i.from;ri.from||0==i.from))if(s=t,"string"!=typeof a&&a.header)n.appendChild(a.header(a));else{n.appendChild(document.createElement("completion-section")).textContent=t}}const h=n.appendChild(document.createElement("li"));h.id=e+"-"+r,h.setAttribute("role","option");let c=this.optionClass(o);c&&(h.className=c);for(let t of this.optionContent){let e=t(o,this.view.state,this.view,l);e&&h.appendChild(e)}}return i.from&&n.classList.add("cm-completionListIncompleteTop"),i.tonew Nf(i,t,e)}function Hf(t){return 100*(t.boost||0)+(t.apply?10:0)+(t.info?5:0)+(t.type?1:0)}class Vf{constructor(t,e,i,n,s,r){this.options=t,this.attrs=e,this.tooltip=i,this.timestamp=n,this.selected=s,this.disabled=r}setSelected(t,e){return t==this.selected||t>=this.options.length?this:new Vf(this.options,_f(e,t),this.tooltip,this.timestamp,t,this.disabled)}static build(t,e,i,n,s,r){if(n&&!r&&t.some(t=>t.isPending))return n.setDisabled();let o=function(t,e){let i=[],n=null,s=null,r=t=>{i.push(t);let{section:e}=t.completion;if(e){n||(n=[]);let t="string"==typeof e?e:e.name;n.some(e=>e.name==t)||n.push("string"==typeof e?{name:t}:e)}},o=e.facet(Pf);for(let n of t)if(n.hasResult()){let t=n.result.getMatch;if(!1===n.result.filter)for(let e of n.result.options)r(new yf(e,n.source,t?t(e):[],1e9-i.length));else{let i,l=e.sliceDoc(n.from,n.to),a=o.filterStrict?new Rf(l):new Df(l);for(let e of n.result.options)if(i=a.match(e.label)){let o=e.displayLabel?t?t(e,i.matched):[]:i.matched,l=i.score+(e.boost||0);if(r(new yf(e,n.source,o,l)),"object"==typeof e.section&&"dynamic"===e.section.rank){let{name:t}=e.section;s||(s=Object.create(null)),s[t]=Math.max(l,s[t]||-1e9)}}}}if(n){let t=Object.create(null),e=0,r=(t,e)=>("dynamic"===t.rank&&"dynamic"===e.rank?s[e.name]-s[t.name]:0)||("number"==typeof t.rank?t.rank:1e9)-("number"==typeof e.rank?e.rank:1e9)||(t.namee.score-t.score||h(t.completion,e.completion))){let e=t.completion;!a||a.label!=e.label||a.detail!=e.detail||null!=a.type&&null!=e.type&&a.type!=e.type||a.apply!=e.apply||a.boost!=e.boost?l.push(t):Hf(t.completion)>Hf(a)&&(l[l.length-1]=t),a=t.completion}return l}(t,e);if(!o.length)return n&&t.some(t=>t.isPending)?n.setDisabled():null;let l=e.facet(Pf).selectOnOpen?0:-1;if(n&&n.selected!=l&&-1!=n.selected){let t=n.options[n.selected].completion;for(let e=0;ee.hasResult()?Math.min(t,e.from):t,1e8),create:Yf,above:s.aboveCursor},n?n.timestamp:Date.now(),l,!1)}map(t){return new Vf(this.options,this.attrs,{...this.tooltip,pos:t.mapPos(this.tooltip.pos)},this.timestamp,this.selected,this.disabled)}setDisabled(){return new Vf(this.options,this.attrs,this.tooltip,this.timestamp,this.selected,!0)}}class zf{constructor(t,e,i){this.active=t,this.id=e,this.open=i}static start(){return new zf(Uf,"cm-ac-"+Math.floor(2e6*Math.random()).toString(36),null)}update(t){let{state:e}=t,i=e.facet(Pf),n=(i.override||e.languageDataAt("autocomplete",xf(e)).map(Mf)).map(e=>(this.active.find(t=>t.source==e)||new $f(e,this.active.some(t=>0!=t.state)?1:0)).update(t,i));n.length==this.active.length&&n.every((t,e)=>t==this.active[e])&&(n=this.active);let s=this.open,r=t.effects.some(t=>t.is(jf));s&&t.docChanged&&(s=s.map(t.changes)),t.selection||n.some(e=>e.hasResult()&&t.changes.touchesRange(e.from,e.to))||!function(t,e){if(t==e)return!0;for(let i=0,n=0;;){for(;it.isPending)&&(s=null),!s&&n.every(t=>!t.isPending)&&n.some(t=>t.hasResult())&&(n=n.map(t=>t.hasResult()?new $f(t.source,0):t));for(let e of t.effects)e.is(Lf)&&(s=s&&s.setSelected(e.value,this.id));return n==this.active&&s==this.open?this:new zf(n,this.id,s)}get tooltip(){return this.open?this.open.tooltip:null}get attrs(){return this.open?this.open.attrs:this.active.length?Ff:qf}}const Ff={"aria-autocomplete":"list"},qf={};function _f(t,e){let i={"aria-autocomplete":"list","aria-haspopup":"listbox","aria-controls":t};return e>-1&&(i["aria-activedescendant"]=t+"-"+e),i}const Uf=[];function Qf(t,e){if(t.isUserEvent("input.complete")){let i=t.annotation(Sf);if(i&&e.activateOnCompletion(i))return 12}let i=t.isUserEvent("input.type");return i&&e.activateOnTyping?5:i?1:t.isUserEvent("delete.backward")?2:t.selection?8:t.docChanged?16:0}class $f{constructor(t,e,i=!1){this.source=t,this.state=e,this.explicit=i}hasResult(){return!1}get isPending(){return 1==this.state}update(t,e){let i=Qf(t,e),n=this;(8&i||16&i&&this.touches(t))&&(n=new $f(n.source,0)),4&i&&0==n.state&&(n=new $f(this.source,1)),n=n.updateFor(t,i);for(let e of t.effects)if(e.is(Of))n=new $f(n.source,1,e.value);else if(e.is(Tf))n=new $f(n.source,0);else if(e.is(jf))for(let t of e.value)t.source==n.source&&(n=t);return n}updateFor(t,e){return this.map(t.changes)}map(t){return this}touches(t){return t.changes.touchesRange(xf(t.state))}}class Kf extends $f{constructor(t,e,i,n,s,r){super(t,3,e),this.limit=i,this.result=n,this.from=s,this.to=r}hasResult(){return!0}updateFor(t,e){var i;if(!(3&e))return this.map(t.changes);let n=this.result;n.map&&!t.changes.empty&&(n=n.map(n,t.changes));let s=t.changes.mapPos(this.from),r=t.changes.mapPos(this.to,1),o=xf(t.state);if(o>r||!n||2&e&&(xf(t.startState)==this.from||ot.map(t=>t.map(e))}),Xf=K.define({create:()=>zf.start(),update:(t,e)=>t.update(e),provide:t=>[ko.from(t,t=>t.tooltip),pr.contentAttributes.from(t,t=>t.attrs)]});function Gf(t,e){const i=e.completion.apply||e.completion.label;let n=t.state.field(Xf).active.find(t=>t.source==e.source);return n instanceof Kf&&("string"==typeof i?t.dispatch({...Cf(t.state,i,n.from,n.to),annotations:Sf.of(e.completion)}):i(t,e.completion,n.from,n.to),!0)}const Yf=Wf(Xf,Gf);function Jf(t,e="option"){return i=>{let n=i.state.field(Xf,!1);if(!n||!n.open||n.open.disabled||Date.now()-n.open.timestamp-1?n.open.selected+r*(t?1:-1):t?0:o-1;return l<0?l="page"==e?0:o-1:l>=o&&(l="page"==e?o-1:0),i.dispatch({effects:Lf.of(l)}),!0}}const Zf=t=>!!t.state.field(Xf,!1)&&(t.dispatch({effects:Of.of(!0)}),!0);class td{constructor(t,e){this.active=t,this.context=e,this.time=Date.now(),this.updates=[],this.done=void 0}}const ed=qi.fromClass(class{constructor(t){this.view=t,this.debounceUpdate=-1,this.running=[],this.debounceAccept=-1,this.pendingStart=!1,this.composing=0;for(let e of t.state.field(Xf).active)e.isPending&&this.startQuery(e)}update(t){let e=t.state.field(Xf),i=t.state.facet(Pf);if(!t.selectionSet&&!t.docChanged&&t.startState.field(Xf)==e)return;let n=t.transactions.some(t=>{let e=Qf(t,i);return 8&e||(t.selection||t.docChanged)&&!(3&e)});for(let e=0;e50&&Date.now()-i.time>1e3){for(let t of i.context.abortListeners)try{t()}catch(t){Hi(this.view.state,t)}i.context.abortListeners=null,this.running.splice(e--,1)}else i.updates.push(...t.transactions)}this.debounceUpdate>-1&&clearTimeout(this.debounceUpdate),t.transactions.some(t=>t.effects.some(t=>t.is(Of)))&&(this.pendingStart=!0);let s=this.pendingStart?50:i.activateOnTypingDelay;if(this.debounceUpdate=e.active.some(t=>t.isPending&&!this.running.some(e=>e.active.source==t.source))?setTimeout(()=>this.startUpdate(),s):-1,0!=this.composing)for(let e of t.transactions)e.isUserEvent("input.type")?this.composing=2:2==this.composing&&e.selection&&(this.composing=3)}startUpdate(){this.debounceUpdate=-1,this.pendingStart=!1;let{state:t}=this.view,e=t.field(Xf);for(let t of e.active)t.isPending&&!this.running.some(e=>e.active.source==t.source)&&this.startQuery(t);this.running.length&&e.open&&e.open.disabled&&(this.debounceAccept=setTimeout(()=>this.accept(),this.view.state.facet(Pf).updateSyncTime))}startQuery(t){let{state:e}=this.view,i=xf(e),n=new vf(e,i,t.explicit,this.view),s=new td(t,n);this.running.push(s),Promise.resolve(t.source(n)).then(t=>{s.context.aborted||(s.done=t||null,this.scheduleAccept())},t=>{this.view.dispatch({effects:Tf.of(null)}),Hi(this.view.state,t)})}scheduleAccept(){this.running.every(t=>void 0!==t.done)?this.accept():this.debounceAccept<0&&(this.debounceAccept=setTimeout(()=>this.accept(),this.view.state.facet(Pf).updateSyncTime))}accept(){var t;this.debounceAccept>-1&&clearTimeout(this.debounceAccept),this.debounceAccept=-1;let e=[],i=this.view.state.facet(Pf),n=this.view.state.field(Xf);for(let s=0;st.source==r.active.source);if(o&&o.isPending)if(null==r.done){let t=new $f(r.active.source,0);for(let e of r.updates)t=t.update(e,i);t.isPending||e.push(t)}else this.startQuery(o)}(e.length||n.open&&n.open.disabled)&&this.view.dispatch({effects:jf.of(e)})}},{eventHandlers:{blur(t){let e=this.view.state.field(Xf,!1);if(e&&e.tooltip&&this.view.state.facet(Pf).closeOnBlur){let i=e.open&&Ro(this.view,e.open.tooltip);i&&i.dom.contains(t.relatedTarget)||setTimeout(()=>this.view.dispatch({effects:Tf.of(null)}),10)}},compositionstart(){this.composing=1},compositionend(){3==this.composing&&setTimeout(()=>this.view.dispatch({effects:Of.of(!1)}),20),this.composing=0}}}),id="object"==typeof navigator&&/Win/.test(navigator.platform),nd=Z.highest(pr.domEventHandlers({keydown(t,e){let i=e.state.field(Xf,!1);if(!i||!i.open||i.open.disabled||i.open.selected<0||t.key.length>1||t.ctrlKey&&(!id||!t.altKey)||t.metaKey)return!1;let n=i.open.options[i.open.selected],s=i.active.find(t=>t.source==n.source),r=n.completion.commitCharacters||s.result.commitCharacters;return r&&r.indexOf(t.key)>-1&&Gf(e,n),!1}})),sd=pr.baseTheme({".cm-tooltip.cm-tooltip-autocomplete":{"& > ul":{fontFamily:"monospace",whiteSpace:"nowrap",overflow:"hidden auto",maxWidth_fallback:"700px",maxWidth:"min(700px, 95vw)",minWidth:"250px",maxHeight:"10em",height:"100%",listStyle:"none",margin:0,padding:0,"& > li, & > completion-section":{padding:"1px 3px",lineHeight:1.2},"& > li":{overflowX:"hidden",textOverflow:"ellipsis",cursor:"pointer"},"& > completion-section":{display:"list-item",borderBottom:"1px solid silver",paddingLeft:"0.5em",opacity:.7}}},"&light .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#17c",color:"white"},"&light .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#777"},"&dark .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#347",color:"white"},"&dark .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#444"},".cm-completionListIncompleteTop:before, .cm-completionListIncompleteBottom:after":{content:'"···"',opacity:.5,display:"block",textAlign:"center",cursor:"pointer"},".cm-tooltip.cm-completionInfo":{position:"absolute",padding:"3px 9px",width:"max-content",maxWidth:"400px",boxSizing:"border-box",whiteSpace:"pre-line"},".cm-completionInfo.cm-completionInfo-left":{right:"100%"},".cm-completionInfo.cm-completionInfo-right":{left:"100%"},".cm-completionInfo.cm-completionInfo-left-narrow":{right:"30px"},".cm-completionInfo.cm-completionInfo-right-narrow":{left:"30px"},"&light .cm-snippetField":{backgroundColor:"#00000022"},"&dark .cm-snippetField":{backgroundColor:"#ffffff22"},".cm-snippetFieldPosition":{verticalAlign:"text-top",width:0,height:"1.15em",display:"inline-block",margin:"0 -0.7px -.7em",borderLeft:"1.4px dotted #888"},".cm-completionMatchedText":{textDecoration:"underline"},".cm-completionDetail":{marginLeft:"0.5em",fontStyle:"italic"},".cm-completionIcon":{fontSize:"90%",width:".8em",display:"inline-block",textAlign:"center",paddingRight:".6em",opacity:"0.6",boxSizing:"content-box"},".cm-completionIcon-function, .cm-completionIcon-method":{"&:after":{content:"'ƒ'"}},".cm-completionIcon-class":{"&:after":{content:"'○'"}},".cm-completionIcon-interface":{"&:after":{content:"'◌'"}},".cm-completionIcon-variable":{"&:after":{content:"'𝑥'"}},".cm-completionIcon-constant":{"&:after":{content:"'𝐶'"}},".cm-completionIcon-type":{"&:after":{content:"'𝑡'"}},".cm-completionIcon-enum":{"&:after":{content:"'∪'"}},".cm-completionIcon-property":{"&:after":{content:"'□'"}},".cm-completionIcon-keyword":{"&:after":{content:"'🔑︎'"}},".cm-completionIcon-namespace":{"&:after":{content:"'▢'"}},".cm-completionIcon-text":{"&:after":{content:"'abc'",fontSize:"50%",verticalAlign:"middle"}}}),rd={brackets:["(","[","{","'",'"'],before:")]}:;>",stringPrefixes:[]},od=gt.define({map(t,e){let i=e.mapPos(t,-1,O.TrackAfter);return null==i?void 0:i}}),ld=new class extends Rt{};ld.startSide=1,ld.endSide=-1;const ad=K.define({create:()=>It.empty,update(t,e){if(t=t.map(e.changes),e.selection){let i=e.state.doc.lineAt(e.selection.main.head);t=t.update({filter:t=>t>=i.from&&t<=i.to})}for(let i of e.effects)i.is(od)&&(t=t.update({add:[ld.range(i.value,i.value+1)]}));return t}});const hd="()[]{}<>«»»«[]{}";function cd(t){for(let e=0;e<16;e+=2)if(hd.charCodeAt(e)==t)return hd.charAt(e+1);return C(t<128?t:t+1)}function ud(t,e){return t.languageDataAt("closeBrackets",e)[0]||rd}const fd="object"==typeof navigator&&/Android\b/.test(navigator.userAgent),dd=pr.inputHandler.of((t,e,i,n)=>{if((fd?t.composing:t.compositionStarted)||t.state.readOnly)return!1;let s=t.state.selection.main;if(n.length>2||2==n.length&&1==A(S(n,0))||e!=s.from||i!=s.to)return!1;let r=function(t,e){let i=ud(t,t.selection.main.head),n=i.brackets||rd.brackets;for(let s of n){let r=cd(S(s,0));if(e==s)return r==s?bd(t,s,n.indexOf(s+s+s)>-1,i):vd(t,s,r,i.before||rd.before);if(e==r&&md(t,t.selection.main.from))return wd(t,s,r)}return null}(t.state,n);return!!r&&(t.dispatch(r),!0)}),pd=[{key:"Backspace",run:({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=ud(t,t.selection.main.head).brackets||rd.brackets,n=null,s=t.changeByRange(e=>{if(e.empty){let n=function(t,e){let i=t.sliceString(e-2,e);return A(S(i,0))==i.length?i:i.slice(1)}(t.doc,e.head);for(let s of i)if(s==n&&gd(t.doc,e.head)==cd(S(s,0)))return{changes:{from:e.head-s.length,to:e.head+s.length},range:W.cursor(e.head-s.length)}}return{range:n=e}});return n||e(t.update(s,{scrollIntoView:!0,userEvent:"delete.backward"})),!n}}];function md(t,e){let i=!1;return t.field(ad).between(0,t.doc.length,t=>{t==e&&(i=!0)}),i}function gd(t,e){let i=t.sliceString(e,e+2);return i.slice(0,A(S(i,0)))}function vd(t,e,i,n){let s=null,r=t.changeByRange(r=>{if(!r.empty)return{changes:[{insert:e,from:r.from},{insert:i,from:r.to}],effects:od.of(r.to+e.length),range:W.range(r.anchor+e.length,r.head+e.length)};let o=gd(t.doc,r.head);return!o||/\s/.test(o)||n.indexOf(o)>-1?{changes:{insert:e+i,from:r.head},effects:od.of(r.head+e.length),range:W.cursor(r.head+e.length)}:{range:s=r}});return s?null:t.update(r,{scrollIntoView:!0,userEvent:"input.type"})}function wd(t,e,i){let n=null,s=t.changeByRange(e=>e.empty&&gd(t.doc,e.head)==i?{changes:{from:e.head,to:e.head+i.length,insert:i},range:W.cursor(e.head+i.length)}:n={range:e});return n?null:t.update(s,{scrollIntoView:!0,userEvent:"input.type"})}function bd(t,e,i,n){let s=n.stringPrefixes||rd.stringPrefixes,r=null,o=t.changeByRange(n=>{if(!n.empty)return{changes:[{insert:e,from:n.from},{insert:e,from:n.to}],effects:od.of(n.to+e.length),range:W.range(n.anchor+e.length,n.head+e.length)};let o,l=n.head,a=gd(t.doc,l);if(a==e){if(yd(t,l))return{changes:{insert:e+e,from:l},effects:od.of(l+e.length),range:W.cursor(l+e.length)};if(md(t,l)){let n=i&&t.sliceDoc(l,l+3*e.length)==e+e+e?e+e+e:e;return{changes:{from:l,to:l+n.length,insert:n},range:W.cursor(l+n.length)}}}else{if(i&&t.sliceDoc(l-2*e.length,l)==e+e&&(o=xd(t,l-2*e.length,s))>-1&&yd(t,o))return{changes:{insert:e+e+e+e,from:l},effects:od.of(l+e.length),range:W.cursor(l+e.length)};if(t.charCategorizer(l)(a)!=Ct.Word&&xd(t,l,s)>-1&&!function(t,e,i,n){let s=ka(t).resolveInner(e,-1),r=n.reduce((t,e)=>Math.max(t,e.length),0);for(let o=0;o<5;o++){let o=t.sliceDoc(s.from,Math.min(s.to,s.from+i.length+r)),l=o.indexOf(i);if(!l||l>-1&&n.indexOf(o.slice(0,l))>-1){let e=s.firstChild;for(;e&&e.from==s.from&&e.to-e.from>i.length+l;){if(t.sliceDoc(e.to-i.length,e.to)==i)return!1;e=e.firstChild}return!0}let a=s.to==e&&s.parent;if(!a)break;s=a}return!1}(t,l,e,s))return{changes:{insert:e+e,from:l},effects:od.of(l+e.length),range:W.cursor(l+e.length)}}return{range:r=n}});return r?null:t.update(o,{scrollIntoView:!0,userEvent:"input.type"})}function yd(t,e){let i=ka(t).resolveInner(e+1);return i.parent&&i.from==e}function xd(t,e,i){let n=t.charCategorizer(e);if(n(t.sliceDoc(e-1,e))!=Ct.Word)return e;for(let s of i){let i=e-s.length;if(t.sliceDoc(i,e)==s&&n(t.sliceDoc(i-1,i))!=Ct.Word)return i}return-1}function kd(t={}){return[nd,Xf,Pf.of(t),ed,Cd,sd]}const Sd=[{key:"Ctrl-Space",run:Zf},{mac:"Alt-`",run:Zf},{mac:"Alt-i",run:Zf},{key:"Escape",run:t=>{let e=t.state.field(Xf,!1);return!(!e||!e.active.some(t=>0!=t.state))&&(t.dispatch({effects:Tf.of(null)}),!0)}},{key:"ArrowDown",run:Jf(!0)},{key:"ArrowUp",run:Jf(!1)},{key:"PageDown",run:Jf(!0,"page")},{key:"PageUp",run:Jf(!1,"page")},{key:"Enter",run:t=>{let e=t.state.field(Xf,!1);return!(t.state.readOnly||!e||!e.open||e.open.selected<0||e.open.disabled||Date.now()-e.open.timestampt.facet(Pf).defaultKeymap?[Sd]:[]));class Ad{constructor(t,e,i){this.from=t,this.to=e,this.diagnostic=i}}class Md{constructor(t,e,i){this.diagnostics=t,this.panel=e,this.selected=i}static init(t,e,i){let n=i.facet(Wd).markerFilter;n&&(t=n(t,i));let s=t.slice().sort((t,e)=>t.from-e.from||t.to-e.to),r=new Nt,o=[],l=0,a=i.doc.iter(),h=0,c=i.doc.length;for(let t=0;;){let e,i,n=t==s.length?null:s[t];if(!n&&!o.length)break;if(o.length)e=l,i=o.reduce((t,e)=>Math.min(t,e.to),n&&n.from>e?n.from:1e8);else{if(e=n.from,e>c)break;i=n.to,o.push(n),t++}for(;tn.from||n.to==e)){i=Math.min(n.from,i);break}o.push(n),t++,i=Math.min(n.to,i)}i=Math.min(i,c);let u=!1;if(o.some(t=>t.from==e&&(t.to==i||i==c))&&(u=e==i,!u&&i-e<10)){let t=e-(h+a.value.length);t>0&&(a.next(t),h=e);for(let t=e;;){if(t>=i){u=!0;break}if(!a.lineBreak&&h+a.value.length>t)break;t=h+a.value.length,h+=a.value.length,a.next()}}let f=Kd(o);if(u)r.add(e,e,Te.widget({widget:new Fd(f),diagnostics:o.slice()}));else{let t=o.reduce((t,e)=>e.markClass?t+" "+e.markClass:t,"");r.add(e,i,Te.mark({class:"cm-lintRange cm-lintRange-"+f+t,diagnostics:o.slice(),inclusiveEnd:o.some(t=>t.to>i)}))}if(l=i,l==c)break;for(let t=0;t{if(!(e&&s.diagnostics.indexOf(e)<0))if(n){if(s.diagnostics.indexOf(n.diagnostic)<0)return!1;n=new Ad(n.from,i,n.diagnostic)}else n=new Ad(t,i,e||s.diagnostics[0])}),n}const Td=gt.define(),Dd=gt.define(),Rd=gt.define(),Pd=K.define({create:()=>new Md(Te.none,null,null),update(t,e){if(e.docChanged&&t.diagnostics.size){let i=t.diagnostics.map(e.changes),n=null,s=t.panel;if(t.selected){let s=e.changes.mapPos(t.selected.from,1);n=Od(i,t.selected.diagnostic,s)||Od(i,null,s)}!i.size&&s&&e.state.facet(Wd).autoPanel&&(s=null),t=new Md(i,s,n)}for(let i of e.effects)if(i.is(Td)){let n=e.state.facet(Wd).autoPanel?i.value.length?_d.open:null:t.panel;t=Md.init(i.value,n,e.state)}else i.is(Dd)?t=new Md(t.diagnostics,i.value?_d.open:null,t.selected):i.is(Rd)&&(t=new Md(t.diagnostics,t.panel,i.value));return t},provide:t=>[Wo.from(t,t=>t.panel),pr.decorations.from(t,t=>t.diagnostics)]}),Bd=Te.mark({class:"cm-lintRange cm-lintRange-active"});function Ed(t,e,i){let n,{diagnostics:s}=t.state.field(Pd),r=-1,o=-1;s.between(e-(i<0?1:0),e+(i>0?1:0),(t,s,{spec:l})=>{if(e>=t&&e<=s&&(t==s||(e>t||i>0)&&(e({dom:Ld(t,n)})}:null}function Ld(t,e){return le("ul",{class:"cm-tooltip-lint"},e.map(e=>zd(t,e,!1)))}const Id=t=>{let e=t.state.field(Pd,!1);return!(!e||!e.panel)&&(t.dispatch({effects:Dd.of(!1)}),!0)},Nd=[{key:"Mod-Shift-m",run:t=>{let e=t.state.field(Pd,!1);var i,n;e&&e.panel||t.dispatch({effects:(i=t.state,n=[Dd.of(!0)],i.field(Pd,!1)?n:n.concat(gt.appendConfig.of(Xd)))});let s=Eo(t,_d.open);return s&&s.dom.querySelector(".cm-panel-lint ul").focus(),!0},preventDefault:!0},{key:"F8",run:t=>{let e=t.state.field(Pd,!1);if(!e)return!1;let i=t.state.selection.main,n=Od(e.diagnostics,null,i.to+1);return!(!n&&(n=Od(e.diagnostics,null,0),!n||n.from==i.from&&n.to==i.to))&&(t.dispatch({selection:{anchor:n.from,head:n.to},scrollIntoView:!0}),function(t,e,i,n={}){var s;let r=t.state.facet(Mo).map(e=>t.plugin(e)).filter(t=>!!t);if(n.tooltip&&n.tooltip.active){let t=r.find(t=>t.field==n.tooltip.active);t&&(r=[t])}for(let o of r)o.activateHover(t,e,i,null!==(s=n.until)&&void 0!==s?s:()=>!1)}(t,n.from,1,{tooltip:jd,until:t=>t.docChanged||t.newSelection.main.headn.to}),!0)}}],Wd=z.define({combine:t=>({sources:t.map(t=>t.source).filter(t=>null!=t),...Dt(t.map(t=>t.config),{delay:750,markerFilter:null,tooltipFilter:null,needsRefresh:null,hideOn:()=>null},{delay:Math.max,markerFilter:Hd,tooltipFilter:Hd,needsRefresh:(t,e)=>t?e?i=>t(i)||e(i):t:e,hideOn:(t,e)=>t?e?(i,n,s)=>t(i,n,s)||e(i,n,s):t:e,autoPanel:(t,e)=>t||e})})});function Hd(t,e){return t?e?(i,n)=>e(t(i,n),n):t:e}function Vd(t){let e=[];if(t)t:for(let{name:i}of t){for(let t=0;tt.toLowerCase()==n.toLowerCase())){e.push(n);continue t}}e.push("")}return e}function zd(t,e,i){var n;let s=i?Vd(e.actions):[];return le("li",{class:"cm-diagnostic cm-diagnostic-"+e.severity},le("span",{class:"cm-diagnosticText"},e.renderMessage?e.renderMessage(t):e.message),null===(n=e.actions)||void 0===n?void 0:n.map((i,n)=>{let r=!1,o=n=>{if(n.preventDefault(),r)return;r=!0;let s=Od(t.state.field(Pd).diagnostics,e);s&&i.apply(t,s.from,s.to)},{name:l}=i,a=s[n]?l.indexOf(s[n]):-1,h=a<0?l:[l.slice(0,a),le("u",l.slice(a,a+1)),l.slice(a+1)];return le("button",{type:"button",class:"cm-diagnosticAction"+(i.markClass?" "+i.markClass:""),onclick:o,onmousedown:o,"aria-label":` Action: ${l}${a<0?"":` (access key "${s[n]})"`}.`},h)}),e.source&&le("div",{class:"cm-diagnosticSource"},e.source))}class Fd extends Me{constructor(t){super(),this.sev=t}eq(t){return t.sev==this.sev}toDOM(){return le("span",{class:"cm-lintPoint cm-lintPoint-"+this.sev})}}class qd{constructor(t,e){this.diagnostic=e,this.id="item_"+Math.floor(4294967295*Math.random()).toString(16),this.dom=zd(t,e,!0),this.dom.id=this.id,this.dom.setAttribute("role","option")}}class _d{constructor(t){this.view=t,this.items=[];this.list=le("ul",{tabIndex:0,role:"listbox","aria-label":this.view.state.phrase("Diagnostics"),onkeydown:e=>{if(!(e.ctrlKey||e.altKey||e.metaKey)){if(27==e.keyCode)Id(this.view),this.view.focus();else if(38==e.keyCode||33==e.keyCode)this.moveSelection((this.selectedIndex-1+this.items.length)%this.items.length);else if(40==e.keyCode||34==e.keyCode)this.moveSelection((this.selectedIndex+1)%this.items.length);else if(36==e.keyCode)this.moveSelection(0);else if(35==e.keyCode)this.moveSelection(this.items.length-1);else if(13==e.keyCode)this.view.focus();else{if(!(e.keyCode>=65&&e.keyCode<=90&&this.selectedIndex>=0))return;{let{diagnostic:i}=this.items[this.selectedIndex],n=Vd(i.actions);for(let s=0;s{for(let e=0;eId(this.view)},"×")),this.update()}get selectedIndex(){let t=this.view.state.field(Pd).selected;if(!t)return-1;for(let e=0;e{for(let t of l.diagnostics){if(r.has(t))continue;r.add(t);let o,l=-1;for(let e=i;ei&&(this.items.splice(i,l-i),n=!0)),e&&o.diagnostic==e.diagnostic?o.dom.hasAttribute("aria-selected")||(o.dom.setAttribute("aria-selected","true"),s=o):o.dom.hasAttribute("aria-selected")&&o.dom.removeAttribute("aria-selected"),i++}});i({sel:s.dom.getBoundingClientRect(),panel:this.list.getBoundingClientRect()}),write:({sel:t,panel:e})=>{let i=e.height/this.list.offsetHeight;t.tope.bottom&&(this.list.scrollTop+=(t.bottom-e.bottom)/i)}})):this.selectedIndex<0&&this.list.removeAttribute("aria-activedescendant"),n&&this.sync()}sync(){let t=this.list.firstChild;function e(){let e=t;t=e.nextSibling,e.remove()}for(let i of this.items)if(i.dom.parentNode==this.list){for(;t!=i.dom;)e();t=i.dom.nextSibling}else this.list.insertBefore(i.dom,t);for(;t;)e()}moveSelection(t){if(this.selectedIndex<0)return;let e=Od(this.view.state.field(Pd).diagnostics,this.items[t].diagnostic);e&&this.view.dispatch({selection:{anchor:e.from,head:e.to},scrollIntoView:!0,effects:Rd.of(e)})}static open(t){return new _d(t)}}function Ud(t){return function(t,e='viewBox="0 0 40 40"'){return`url('data:image/svg+xml,${encodeURIComponent(t)}')`}(``,'width="6" height="3"')}const Qd=pr.baseTheme({".cm-diagnostic":{padding:"3px 6px 3px 8px",marginLeft:"-1px",display:"block",whiteSpace:"pre-wrap"},".cm-diagnostic-error":{borderLeft:"5px solid #d11"},".cm-diagnostic-warning":{borderLeft:"5px solid orange"},".cm-diagnostic-info":{borderLeft:"5px solid #999"},".cm-diagnostic-hint":{borderLeft:"5px solid #66d"},".cm-diagnosticAction":{font:"inherit",border:"none",padding:"2px 4px",backgroundColor:"#444",color:"white",borderRadius:"3px",marginLeft:"8px",cursor:"pointer"},".cm-diagnosticSource":{fontSize:"70%",opacity:.7},".cm-lintRange":{backgroundPosition:"left bottom",backgroundRepeat:"repeat-x",paddingBottom:"0.7px"},".cm-lintRange-error":{backgroundImage:Ud("#f11")},".cm-lintRange-warning":{backgroundImage:Ud("orange")},".cm-lintRange-info":{backgroundImage:Ud("#999")},".cm-lintRange-hint":{backgroundImage:Ud("#66d")},".cm-lintRange-active":{backgroundColor:"#ffdd9980"},".cm-tooltip-lint":{padding:0,margin:0},".cm-lintPoint":{position:"relative","&:after":{content:'""',position:"absolute",bottom:0,left:"-2px",borderLeft:"3px solid transparent",borderRight:"3px solid transparent",borderBottom:"4px solid #d11"}},".cm-lintPoint-warning":{"&:after":{borderBottomColor:"orange"}},".cm-lintPoint-info":{"&:after":{borderBottomColor:"#999"}},".cm-lintPoint-hint":{"&:after":{borderBottomColor:"#66d"}},".cm-panel.cm-panel-lint":{position:"relative","& ul":{maxHeight:"100px",overflowY:"auto","& [aria-selected]":{backgroundColor:"#ddd","& u":{textDecoration:"underline"}},"&:focus [aria-selected]":{background_fallback:"#bdf",backgroundColor:"Highlight",color_fallback:"white",color:"HighlightText"},"& u":{textDecoration:"none"},padding:0,margin:0},"& [name=close]":{position:"absolute",top:"0",right:"2px",background:"inherit",border:"none",font:"inherit",padding:0,margin:0}},"&dark .cm-lintRange-active":{backgroundColor:"#86714a80"},"&dark .cm-panel.cm-panel-lint ul":{"& [aria-selected]":{backgroundColor:"#2e343e"}}});function $d(t){return"error"==t?4:"warning"==t?3:"info"==t?2:1}function Kd(t){let e="hint",i=1;for(let n of t){let t=$d(n.severity);t>i&&(i=t,e=n.severity)}return e}const jd=Do(Ed,{hideOn:function(t,e){let i=e.pos,n=e.end||i,s=t.state.facet(Wd).hideOn(t,i,n);if(null!=s)return s;let r=t.startState.doc.lineAt(e.pos);return!(!t.effects.some(t=>t.is(Td))&&!t.changes.touchesRange(r.from,Math.max(r.to,n)))}}),Xd=[Pd,pr.decorations.compute([Pd],t=>{let{selected:e,panel:i}=t.field(Pd);return e&&i&&e.from!=e.to?Te.set([Bd.range(e.from,e.to)]):Te.none}),jd,Qd];class Gd{constructor(t,e,i,n,s,r,o,l,a,h=0,c){this.p=t,this.stack=e,this.state=i,this.reducePos=n,this.pos=s,this.score=r,this.buffer=o,this.bufferBase=l,this.curContext=a,this.lookAhead=h,this.parent=c}toString(){return`[${this.stack.filter((t,e)=>e%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(t,e,i=0){let n=t.parser.context;return new Gd(t,[],e,i,i,0,[],0,n?new Yd(n,n.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(t,e){this.stack.push(this.state,e,this.bufferBase+this.buffer.length),this.state=t}reduce(t){var e;let i=t>>19,n=65535&t,{parser:s}=this.p,r=this.reducePos=2e3&&!(null===(e=this.p.parser.nodeSet.types[n])||void 0===e?void 0:e.isAnonymous)&&(a==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=h):this.p.lastBigReductionSizel;)this.stack.pop();this.reduceContext(n,a)}storeNode(t,e,i,n=4,s=!1){if(0==t&&(!this.stack.length||this.stack[this.stack.length-1]0&&0==this.buffer[t-4]&&this.buffer[t-1]>-1){if(e==i)return;if(this.buffer[t-2]>=e)return void(this.buffer[t-2]=i)}}if(s&&this.pos!=i){let s=this.buffer.length;if(s>0&&(0!=this.buffer[s-4]||this.buffer[s-1]<0)){let t=!1;for(let e=s;e>0&&this.buffer[e-2]>i;e-=4)if(this.buffer[e-1]>=0){t=!0;break}if(t)for(;s>0&&this.buffer[s-2]>i;)this.buffer[s]=this.buffer[s-4],this.buffer[s+1]=this.buffer[s-3],this.buffer[s+2]=this.buffer[s-2],this.buffer[s+3]=this.buffer[s-1],s-=4,n>4&&(n-=4)}this.buffer[s]=t,this.buffer[s+1]=e,this.buffer[s+2]=i,this.buffer[s+3]=n}else this.buffer.push(t,e,i,n)}shift(t,e,i,n){if(131072&t)this.pushState(65535&t,this.pos);else if(262144&t)this.pos=n,this.shiftContext(e,i),e<=this.p.parser.maxNode&&this.buffer.push(e,i,n,4);else{let s=t,{parser:r}=this.p;this.pos=n;let o=r.stateFlag(s,1);!o&&(n>i||e<=r.maxNode)&&(this.reducePos=n),this.pushState(s,o?i:Math.min(i,this.reducePos)),this.shiftContext(e,i),e<=r.maxNode&&this.buffer.push(e,i,n,4)}}apply(t,e,i,n){65536&t?this.reduce(t):this.shift(t,e,i,n)}useNode(t,e){let i=this.p.reused.length-1;(i<0||this.p.reused[i]!=t)&&(this.p.reused.push(t),i++);let n=this.pos;this.reducePos=this.pos=n+t.length,this.pushState(e,n),this.buffer.push(i,n,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,t,this,this.p.stream.reset(this.pos-t.length)))}split(){let t=this,e=t.buffer.length;for(e&&0==t.buffer[e-4]&&(e-=4);e>0&&t.buffer[e-2]>t.reducePos;)e-=4;let i=t.buffer.slice(e),n=t.bufferBase+e;for(;t&&n==t.bufferBase;)t=t.parent;return new Gd(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,i,n,this.curContext,this.lookAhead,t)}recoverByDelete(t,e){let i=t<=this.p.parser.maxNode;i&&this.storeNode(t,this.pos,e,4),this.storeNode(0,this.pos,e,i?8:4),this.pos=this.reducePos=e,this.score-=190}canShift(t){for(let e=new Jd(this);;){let i=this.p.parser.stateSlot(e.state,4)||this.p.parser.hasAction(e.state,t);if(0==i)return!1;if(!(65536&i))return!0;e.reduce(i)}}recoverByInsert(t){if(this.stack.length>=300)return[];let e=this.p.parser.nextStates(this.state);if(e.length>8||this.stack.length>=120){let i=[];for(let n,s=0;s1&e&&t==n)||i.push(e[t],n)}e=i}let i=[];for(let t=0;t>19,n=65535&e,s=this.stack.length-3*i;if(s<0||t.getGoto(this.stack[s],n,!1)<0){let t=this.findForcedReduction();if(null==t)return!1;e=t}this.storeNode(0,this.pos,this.pos,4,!0),this.score-=100}return this.reducePos=this.pos,this.reduce(e),!0}findForcedReduction(){let{parser:t}=this.p,e=[],i=(n,s)=>{if(!e.includes(n))return e.push(n),t.allActions(n,e=>{if(393216&e);else if(65536&e){let i=(e>>19)-s;if(i>1){let n=65535&e,s=this.stack.length-3*i;if(s>=0&&t.getGoto(this.stack[s],n,!1)>=0)return i<<19|65536|n}}else{let t=i(e,s+1);if(null!=t)return t}})};return i(this.state,0)}forceAll(){for(;!this.p.parser.stateFlag(this.state,2);)if(!this.forceReduce()){this.storeNode(0,this.pos,this.pos,4,!0);break}return this}get deadEnd(){if(3!=this.stack.length)return!1;let{parser:t}=this.p;return 65535==t.data[t.stateSlot(this.state,1)]&&!t.stateSlot(this.state,4)}restart(){this.storeNode(0,this.pos,this.pos,4,!0),this.state=this.stack[0],this.stack.length=0}sameState(t){if(this.state!=t.state||this.stack.length!=t.stack.length)return!1;for(let e=0;e0&&this.emitLookAhead()}}class Yd{constructor(t,e){this.tracker=t,this.context=e,this.hash=t.strict?t.hash(e):0}}class Jd{constructor(t){this.start=t,this.state=t.state,this.stack=t.stack,this.base=this.stack.length}reduce(t){let e=65535&t,i=t>>19;0==i?(this.stack==this.start.stack&&(this.stack=this.stack.slice()),this.stack.push(this.state,0,0),this.base+=3):this.base-=3*(i-1);let n=this.start.p.parser.getGoto(this.stack[this.base-3],e,!0);this.state=n}}class Zd{constructor(t,e,i){this.stack=t,this.pos=e,this.index=i,this.buffer=t.buffer,0==this.index&&this.maybeNext()}static create(t,e=t.bufferBase+t.buffer.length){return new Zd(t,e,e-t.bufferBase)}maybeNext(){let t=this.stack.parent;null!=t&&(this.index=this.stack.bufferBase-t.bufferBase,this.stack=t,this.buffer=t.buffer)}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}next(){this.index-=4,this.pos-=4,0==this.index&&this.maybeNext()}fork(){return new Zd(this.stack,this.pos,this.index)}}function tp(t,e=Uint16Array){if("string"!=typeof t)return t;let i=null;for(let n=0,s=0;n=92&&e--,e>=34&&e--;let s=e-32;if(s>=46&&(s-=46,i=!0),r+=s,i)break;r*=46}i?i[s++]=r:i=new e(r)}return i}class ep{constructor(){this.start=-1,this.value=-1,this.end=-1,this.extended=-1,this.lookAhead=0,this.mask=0,this.context=0}}const ip=new ep;class np{constructor(t,e){this.input=t,this.ranges=e,this.chunk="",this.chunkOff=0,this.chunk2="",this.chunk2Pos=0,this.next=-1,this.token=ip,this.rangeIndex=0,this.pos=this.chunkPos=e[0].from,this.range=e[0],this.end=e[e.length-1].to,this.readNext()}resolveOffset(t,e){let i=this.range,n=this.rangeIndex,s=this.pos+t;for(;si.to:s>=i.to;){if(n==this.ranges.length-1)return null;let t=this.ranges[++n];s+=t.from-i.to,i=t}return s}clipPos(t){if(t>=this.range.from&&tt)return Math.max(t,e.from);return this.end}peek(t){let e,i,n=this.chunkOff+t;if(n>=0&&n=this.chunk2Pos&&en.to&&(this.chunk2=this.chunk2.slice(0,n.to-e)),i=this.chunk2.charCodeAt(0)}}return e>=this.token.lookAhead&&(this.token.lookAhead=e+1),i}acceptToken(t,e=0){let i=e?this.resolveOffset(e,-1):this.pos;if(null==i||i=this.chunk2Pos&&this.posthis.range.to?t.slice(0,this.range.to-this.pos):t,this.chunkPos=this.pos,this.chunkOff=0}}readNext(){return this.chunkOff>=this.chunk.length&&(this.getChunk(),this.chunkOff==this.chunk.length)?this.next=-1:this.next=this.chunk.charCodeAt(this.chunkOff)}advance(t=1){for(this.chunkOff+=t;this.pos+t>=this.range.to;){if(this.rangeIndex==this.ranges.length-1)return this.setDone();t-=this.range.to-this.pos,this.range=this.ranges[++this.rangeIndex],this.pos=this.range.from}return this.pos+=t,this.pos>=this.token.lookAhead&&(this.token.lookAhead=this.pos+1),this.readNext()}setDone(){return this.pos=this.chunkPos=this.end,this.range=this.ranges[this.rangeIndex=this.ranges.length-1],this.chunk="",this.next=-1}reset(t,e){if(e?(this.token=e,e.start=t,e.lookAhead=t+1,e.value=e.extended=-1):this.token=ip,this.pos!=t){if(this.pos=t,t==this.end)return this.setDone(),this;for(;t=this.range.to;)this.range=this.ranges[++this.rangeIndex];t>=this.chunkPos&&t=this.chunkPos&&e<=this.chunkPos+this.chunk.length)return this.chunk.slice(t-this.chunkPos,e-this.chunkPos);if(t>=this.chunk2Pos&&e<=this.chunk2Pos+this.chunk2.length)return this.chunk2.slice(t-this.chunk2Pos,e-this.chunk2Pos);if(t>=this.range.from&&e<=this.range.to)return this.input.read(t,e);let i="";for(let n of this.ranges){if(n.from>=e)break;n.to>t&&(i+=this.input.read(Math.max(n.from,t),Math.min(n.to,e)))}return i}}class sp{constructor(t,e){this.data=t,this.id=e}token(t,e){let{parser:i}=e.p;!function(t,e,i,n,s,r){let o=0,l=1<0){let i=t[n];if(a.allows(i)&&(-1==e.token.value||e.token.value==i||lp(i,e.token.value,s,r))){e.acceptToken(i);break}}let n=e.next,h=0,c=t[o+2];if(!(e.next<0&&c>h&&65535==t[i+3*c-3])){for(;h>1,r=i+s+(s<<1),l=t[r],a=t[r+1]||65536;if(n=a)){o=t[r+2],e.advance();continue t}h=s+1}}break}o=t[i+3*c-1]}}(this.data,t,e,this.id,i.data,i.tokenPrecTable)}}sp.prototype.contextual=sp.prototype.fallback=sp.prototype.extend=!1,sp.prototype.fallback=sp.prototype.extend=!1;class rp{constructor(t,e={}){this.token=t,this.contextual=!!e.contextual,this.fallback=!!e.fallback,this.extend=!!e.extend}}function op(t,e,i){for(let n,s=e;65535!=(n=t[s]);s++)if(n==i)return s-e;return-1}function lp(t,e,i,n){let s=op(i,n,e);return s<0||op(i,n,t)e)&&!n.type.isError)return i<0?Math.max(0,Math.min(n.to-1,e-25)):Math.min(t.length,Math.max(n.from+1,e+25));if(i<0?n.prevSibling():n.nextSibling())break;if(!n.parent())return i<0?0:t.length}}class up{constructor(t,e){this.fragments=t,this.nodeSet=e,this.i=0,this.fragment=null,this.safeFrom=-1,this.safeTo=-1,this.trees=[],this.start=[],this.index=[],this.nextFragment()}nextFragment(){let t=this.fragment=this.i==this.fragments.length?null:this.fragments[this.i++];if(t){for(this.safeFrom=t.openStart?cp(t.tree,t.from+t.offset,1)-t.offset:t.from,this.safeTo=t.openEnd?cp(t.tree,t.to+t.offset,-1)-t.offset:t.to;this.trees.length;)this.trees.pop(),this.start.pop(),this.index.pop();this.trees.push(t.tree),this.start.push(-t.offset),this.index.push(0),this.nextStart=this.safeFrom}else this.nextStart=1e9}nodeAt(t){if(tt)return this.nextStart=r,null;if(s instanceof Sl){if(r==t){if(r=Math.max(this.safeFrom,t)&&(this.trees.push(s),this.start.push(r),this.index.push(0))}else this.index[e]++,this.nextStart=r+s.length}}}class fp{constructor(t,e){this.stream=e,this.tokens=[],this.mainToken=null,this.actions=[],this.tokens=t.tokenizers.map(t=>new ep)}getActions(t){let e=0,i=null,{parser:n}=t.p,{tokenizers:s}=n,r=n.stateSlot(t.state,3),o=t.curContext?t.curContext.hash:0,l=0;for(let n=0;nh.end+25&&(l=Math.max(h.lookAhead,l)),0!=h.value)){let n=e;if(h.extended>-1&&(e=this.addActions(t,h.extended,h.end,e)),e=this.addActions(t,h.value,h.end,e),!a.extend&&(i=h,e>n))break}}for(;this.actions.length>e;)this.actions.pop();return l&&t.setLookAhead(l),i||t.pos!=this.stream.end||(i=new ep,i.value=t.p.parser.eofTerm,i.start=i.end=t.pos,e=this.addActions(t,i.value,i.end,e)),this.mainToken=i,this.actions}getMainToken(t){if(this.mainToken)return this.mainToken;let e=new ep,{pos:i,p:n}=t;return e.start=i,e.end=Math.min(i+1,n.stream.end),e.value=i==n.stream.end?n.parser.eofTerm:0,e}updateCachedToken(t,e,i){let n=this.stream.clipPos(i.pos);if(e.token(this.stream.reset(n,t),i),t.value>-1){let{parser:e}=i.p;for(let n=0;n=0&&i.p.parser.dialect.allows(s>>1)){1&s?t.extended=s>>1:t.value=s>>1;break}}}else t.value=0,t.end=this.stream.clipPos(n+1)}putAction(t,e,i,n){for(let e=0;e4*t.bufferLength?new up(i,t.nodeSet):null}get parsedPos(){return this.minStackPos}advance(){let t,e,i=this.stacks,n=this.minStackPos,s=this.stacks=[];if(this.bigReductionCount>300&&1==i.length){let[t]=i;for(;t.forceReduce()&&t.stack.length&&t.stack[t.stack.length-2]>=this.lastBigReductionStart;);this.bigReductionCount=this.lastBigReductionSize=0}for(let r=0;rn)s.push(o);else{if(this.advanceStack(o,s,i))continue;{t||(t=[],e=[]),t.push(o);let i=this.tokens.getMainToken(o);e.push(i.value,i.end)}}break}}if(!s.length){let e=t&&function(t){let e=null;for(let i of t){let t=i.p.stoppedAt;(i.pos==i.p.stream.end||null!=t&&i.pos>t)&&i.p.parser.stateFlag(i.state,2)&&(!e||e.scorethis.stoppedAt?t[0]:this.runRecovery(t,e,s);if(i)return ap&&console.log("Force-finish "+this.stackID(i)),this.stackToTree(i.forceAll())}if(this.recovering){let t=1==this.recovering?1:3*this.recovering;if(s.length>t)for(s.sort((t,e)=>e.score-t.score);s.length>t;)s.pop();s.some(t=>t.reducePos>n)&&this.recovering--}else if(s.length>1){t:for(let t=0;t500&&n.buffer.length>500){if(!((e.score-n.score||e.buffer.length-n.buffer.length)>0)){s.splice(t--,1);continue t}s.splice(i--,1)}}}s.length>12&&(s.sort((t,e)=>e.score-t.score),s.splice(12,s.length-12))}this.minStackPos=s[0].pos;for(let t=1;t ":"";if(null!=this.stoppedAt&&n>this.stoppedAt)return t.forceReduce()?t:null;if(this.fragments){let e=t.curContext&&t.curContext.tracker.strict,i=e?t.curContext.hash:0;for(let o=this.fragments.nodeAt(n);o;){let n=this.parser.nodeSet.types[o.type.id]==o.type?s.getGoto(t.state,o.type.id):-1;if(n>-1&&o.length&&(!e||(o.prop(ml.contextHash)||0)==i))return t.useNode(o,n),ap&&console.log(r+this.stackID(t)+` (via reuse of ${s.getName(o.type.id)})`),!0;if(!(o instanceof Sl)||0==o.children.length||o.positions[0]>0)break;let l=o.children[0];if(!(l instanceof Sl&&0==o.positions[0]))break;o=l}}let o=s.stateSlot(t.state,4);if(o>0)return t.reduce(o),ap&&console.log(r+this.stackID(t)+` (via always-reduce ${s.getName(65535&o)})`),!0;if(t.stack.length>=8400)for(;t.stack.length>6e3&&t.forceReduce(););let l=this.tokens.getActions(t);for(let o=0;on?e.push(f):i.push(f)}return!1}advanceFully(t,e){let i=t.pos;for(;;){if(!this.advanceStack(t,null,null))return!1;if(t.pos>i)return pp(t,e),!0}}runRecovery(t,e,i){let n=null,s=!1;for(let r=0;r ":"";if(o.deadEnd){if(s)continue;if(s=!0,o.restart(),ap&&console.log(h+this.stackID(o)+" (restarted)"),this.advanceFully(o,i))continue}let c=o.split(),u=h;for(let t=0;t<10&&c.forceReduce();t++){if(ap&&console.log(u+this.stackID(c)+" (via force-reduce)"),this.advanceFully(c,i))break;ap&&(u=this.stackID(c)+" -> ")}for(let t of o.recoverByInsert(l))ap&&console.log(h+this.stackID(t)+" (via recover-insert)"),this.advanceFully(t,i);this.stream.end>o.pos?(a==o.pos&&(a++,l=0),o.recoverByDelete(l,a),ap&&console.log(h+this.stackID(o)+` (via recover-delete ${this.parser.getName(l)})`),pp(o,i)):(!n||n.scoret.topRules[e][1]),n=[];for(let t=0;t=0)s(n,t,e[i++]);else{let r=e[i+-n];for(let o=-n;o>0;o--)s(e[i++],t,r);i++}}}this.nodeSet=new bl(e.map((e,s)=>wl.define({name:s>=this.minRepeatTerm?void 0:e,id:s,props:n[s],top:i.indexOf(s)>-1,error:0==s,skipped:t.skippedNodes&&t.skippedNodes.indexOf(s)>-1}))),t.propSources&&(this.nodeSet=this.nodeSet.extend(...t.propSources)),this.strict=!1,this.bufferLength=fl;let r=tp(t.tokenData);this.context=t.context,this.specializerSpecs=t.specialized||[],this.specialized=new Uint16Array(this.specializerSpecs.length);for(let t=0;t"number"==typeof t?new sp(r,t):t),this.topRules=t.topRules,this.dialects=t.dialects||{},this.dynamicPrecedences=t.dynamicPrecedences||null,this.tokenPrecTable=t.tokenPrec,this.termNames=t.termNames||null,this.maxNode=this.nodeSet.types.length-1,this.dialect=this.parseDialect(),this.top=this.topRules[Object.keys(this.topRules)[0]]}createParse(t,e,i){let n=new dp(this,t,e,i);for(let s of this.wrappers)n=s(n,t,e,i);return n}getGoto(t,e,i=!1){let n=this.goto;if(e>=n[0])return-1;for(let s=n[e+1];;){let e=n[s++],r=1&e,o=n[s++];if(r&&i)return o;for(let i=s+(e>>1);s0}validAction(t,e){return!!this.allActions(t,t=>t==e||null)}allActions(t,e){let i=this.stateSlot(t,4),n=i?e(i):void 0;for(let i=this.stateSlot(t,1);null==n;i+=3){if(65535==this.data[i]){if(1!=this.data[i+1])break;i=vp(this.data,i+2)}n=e(vp(this.data,i+1))}return n}nextStates(t){let e=[];for(let i=this.stateSlot(t,1);;i+=3){if(65535==this.data[i]){if(1!=this.data[i+1])break;i=vp(this.data,i+2)}if(!(1&this.data[i+2])){let t=this.data[i+1];e.some((e,i)=>1&i&&e==t)||e.push(this.data[i],t)}}return e}configure(t){let e=Object.assign(Object.create(gp.prototype),this);if(t.props&&(e.nodeSet=this.nodeSet.extend(...t.props)),t.top){let i=this.topRules[t.top];if(!i)throw new RangeError(`Invalid top rule name ${t.top}`);e.top=i}return t.tokenizers&&(e.tokenizers=this.tokenizers.map(e=>{let i=t.tokenizers.find(t=>t.from==e);return i?i.to:e})),t.specializers&&(e.specializers=this.specializers.slice(),e.specializerSpecs=this.specializerSpecs.map((i,n)=>{let s=t.specializers.find(t=>t.from==i.external);if(!s)return i;let r=Object.assign(Object.assign({},i),{external:s.to});return e.specializers[n]=wp(r),r})),t.contextTracker&&(e.context=t.contextTracker),t.dialect&&(e.dialect=this.parseDialect(t.dialect)),null!=t.strict&&(e.strict=t.strict),t.wrap&&(e.wrappers=e.wrappers.concat(t.wrap)),null!=t.bufferLength&&(e.bufferLength=t.bufferLength),e}hasWrappers(){return this.wrappers.length>0}getName(t){return this.termNames?this.termNames[t]:String(t<=this.maxNode&&this.nodeSet.types[t].name||t)}get eofTerm(){return this.maxNode+1}get topNode(){return this.nodeSet.types[this.top[1]]}dynamicPrecedence(t){let e=this.dynamicPrecedences;return null==e?0:e[t]||0}parseDialect(t){let e=Object.keys(this.dialects),i=e.map(()=>!1);if(t)for(let n of t.split(" ")){let t=e.indexOf(n);t>=0&&(i[t]=!0)}let n=null;for(let t=0;tt.external(i,n)<<1|e}return t.get}function bp(t){return t>=65&&t<=90||t>=97&&t<=122||t>=48&&t<=57}function yp(t){return t>=48&&t<=57||t>=97&&t<=102||t>=65&&t<=70}function xp(t,e,i){for(let n=!1;;){if(t.next<0)return;if(t.next==e&&!n)return void t.advance();n=i&&!n&&92==t.next,t.advance()}}function kp(t,e){for(;95==t.next||bp(t.next);)null!=e&&(e+=String.fromCharCode(t.next)),t.advance();return e}function Sp(t,e){for(;48==t.next||49==t.next;)t.advance();e&&t.next==e&&t.advance()}function Cp(t,e){for(;;){if(46==t.next){if(e)break;e=!0}else if(t.next<48||t.next>57)break;t.advance()}if(69==t.next||101==t.next)for(t.advance(),43!=t.next&&45!=t.next||t.advance();t.next>=48&&t.next<=57;)t.advance()}function Ap(t){for(;!(t.next<0||10==t.next);)t.advance()}function Mp(t,e){for(let i=0;i!=&|~^/",specialVar:"?",identifierQuotes:'"',caseInsensitiveIdentifiers:!1,words:Tp("absolute action add after all allocate alter and any are as asc assertion at authorization before begin between both breadth by call cascade cascaded case cast catalog check close collate collation column commit condition connect connection constraint constraints constructor continue corresponding count create cross cube current current_date current_default_transform_group current_transform_group_for_type current_path current_role current_time current_timestamp current_user cursor cycle data day deallocate declare default deferrable deferred delete depth deref desc describe descriptor deterministic diagnostics disconnect distinct do domain drop dynamic each else elseif end end-exec equals escape except exception exec execute exists exit external fetch first for foreign found from free full function general get global go goto grant group grouping handle having hold hour identity if immediate in indicator initially inner inout input insert intersect into is isolation join key language last lateral leading leave left level like limit local localtime localtimestamp locator loop map match method minute modifies module month names natural nesting new next no none not of old on only open option or order ordinality out outer output overlaps pad parameter partial path prepare preserve primary prior privileges procedure public read reads recursive redo ref references referencing relative release repeat resignal restrict result return returns revoke right role rollback rollup routine row rows savepoint schema scroll search second section select session session_user set sets signal similar size some space specific specifictype sql sqlexception sqlstate sqlwarning start state static system_user table temporary then timezone_hour timezone_minute to trailing transaction translation treat trigger under undo union unique unnest until update usage user using value values view when whenever where while with without work write year zone ","array binary bit boolean char character clob date decimal double float int integer interval large national nchar nclob numeric object precision real smallint time timestamp varchar varying ")};function Rp(t){return new rp(e=>{var i;let{next:n}=e;if(e.advance(),Mp(n,Op)){for(;Mp(e.next,Op);)e.advance();e.acceptToken(36)}else if(36==n&&t.doubleDollarQuotedStrings){let t=kp(e,"");36==e.next&&(e.advance(),function(t,e){t:for(;;){if(t.next<0)return;if(36==t.next){t.advance();for(let i=0;i1){e.advance(),xp(e,39,t.backslashEscapes),e.acceptToken(3);break}if(!bp(e.next))break;e.advance()}else if(t.plsqlQuotingMechanism&&(113==n||81==n)&&39==e.next&&e.peek(1)>0&&!Mp(e.peek(1),Op)){let t=e.peek(1);e.advance(2),function(t,e){let i="[{<(".indexOf(String.fromCharCode(e)),n=i<0?e:"]}>)".charCodeAt(i);for(;;){if(t.next<0)return;if(t.next==n&&39==t.peek(1))return void t.advance(2);t.advance()}}(e,t),e.acceptToken(3)}else if(Mp(n,t.identifierQuotes)){xp(e,91==n?93:n,!1),e.acceptToken(19)}else if(40==n)e.acceptToken(7);else if(41==n)e.acceptToken(8);else if(123==n)e.acceptToken(9);else if(125==n)e.acceptToken(10);else if(91==n)e.acceptToken(11);else if(93==n)e.acceptToken(12);else if(59==n)e.acceptToken(13);else if(t.unquotedBitLiterals&&48==n&&98==e.next)e.advance(),Sp(e),e.acceptToken(22);else if(98!=n&&66!=n||39!=e.next&&34!=e.next){if(48==n&&(120==e.next||88==e.next)||(120==n||88==n)&&39==e.next){let t=39==e.next;for(e.advance();yp(e.next);)e.advance();t&&39==e.next&&e.advance(),e.acceptToken(4)}else if(46==n&&e.next>=48&&e.next<=57)Cp(e,!0),e.acceptToken(4);else if(46==n)e.acceptToken(14);else if(n>=48&&n<=57)Cp(e,!1),e.acceptToken(4);else if(Mp(n,t.operatorChars)){for(;Mp(e.next,t.operatorChars);)e.advance();e.acceptToken(15)}else if(Mp(n,t.specialVar))e.next==n&&e.advance(),function(t){if(39==t.next||34==t.next||96==t.next){let e=t.next;t.advance(),xp(t,e,!1)}else kp(t)}(e),e.acceptToken(17);else if(58==n||44==n)e.acceptToken(16);else if(bp(n)){let s=kp(e,String.fromCharCode(n));e.acceptToken(46==e.next||46==e.peek(-s.length-1)?18:null!==(i=t.words[s.toLowerCase()])&&void 0!==i?i:18)}}else{const i=e.next;e.advance(),t.treatBitsAsBytes?(xp(e,i,t.backslashEscapes),e.acceptToken(23)):(Sp(e,i),e.acceptToken(22))}else e.advance(),xp(e,39,t.backslashEscapes),e.acceptToken(3);else e.advance(),xp(e,39,!0),e.acceptToken(3);else Ap(e),e.acceptToken(1)})}const Pp=Rp(Dp),Bp=gp.deserialize({version:14,states:"%vQ]QQOOO#wQRO'#DSO$OQQO'#CwO%eQQO'#CxO%lQQO'#CyO%sQQO'#CzOOQQ'#DS'#DSOOQQ'#C}'#C}O'UQRO'#C{OOQQ'#Cv'#CvOOQQ'#C|'#C|Q]QQOOQOQQOOO'`QQO'#DOO(xQRO,59cO)PQQO,59cO)UQQO'#DSOOQQ,59d,59dO)cQQO,59dOOQQ,59e,59eO)jQQO,59eOOQQ,59f,59fO)qQQO,59fOOQQ-E6{-E6{OOQQ,59b,59bOOQQ-E6z-E6zOOQQ,59j,59jOOQQ-E6|-E6|O+VQRO1G.}O+^QQO,59cOOQQ1G/O1G/OOOQQ1G/P1G/POOQQ1G/Q1G/QP+kQQO'#C}O+rQQO1G.}O)PQQO,59cO,PQQO'#Cw",stateData:",[~OtOSPOSQOS~ORUOSUOTUOUUOVROXSOZTO]XO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O^]ORvXSvXTvXUvXVvXXvXZvX]vX_vX`vXavXbvXcvXdvXevXfvXgvXhvX~OsvX~P!jOa_Ob_Oc_O~ORUOSUOTUOUUOVROXSOZTO^tO_UO`UOa`Ob`Oc`OdUOeUOfUOgUOhUO~OWaO~P$ZOYcO~P$ZO[eO~P$ZORUOSUOTUOUUOVROXSOZTO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O]hOsoX~P%zOajObjOcjO~O^]ORkaSkaTkaUkaVkaXkaZka]ka_ka`kaakabkackadkaekafkagkahka~Oska~P'kO^]O~OWvXYvX[vX~P!jOWnO~P$ZOYoO~P$ZO[pO~P$ZO^]ORkiSkiTkiUkiVkiXkiZki]ki_ki`kiakibkickidkiekifkigkihki~Oski~P)xOWkaYka[ka~P'kO]hO~P$ZOWkiYki[ki~P)xOasObsOcsO~O",goto:"#hwPPPPPPPPPPPPPPPPPPPPPPPPPPx||||!Y!^!d!xPPP#[TYOZeUORSTWZbdfqT[OZQZORiZSWOZQbRQdSQfTZgWbdfqQ^PWk^lmrQl_Qm`RrseVORSTWZbdfq",nodeNames:"⚠ LineComment BlockComment String Number Bool Null ( ) { } [ ] ; . Operator Punctuation SpecialVar Identifier QuotedIdentifier Keyword Type Bits Bytes Builtin Script Statement CompositeIdentifier Parens Braces Brackets Statement",maxTerm:38,nodeProps:[["isolate",-4,1,2,3,19,""]],skippedNodes:[0,1,2],repeatNodeCount:3,tokenData:"RORO",tokenizers:[0,Pp],topRules:{Script:[0,25]},tokenPrec:0});function Ep(t){let e=t.cursor().moveTo(t.from,-1);for(;/Comment/.test(e.name);)e.moveTo(e.from,-1);return e.node}function Lp(t,e){let i=t.sliceString(e.from,e.to),n=/^([`'"\[])(.*)([`'"\]])$/.exec(i);return n?n[2]:i}function Ip(t){return t&&("Identifier"==t.name||"QuotedIdentifier"==t.name)}function Np(t,e){if("CompositeIdentifier"==e.name){let i=[];for(let n=e.firstChild;n;n=n.nextSibling)Ip(n)&&i.push(Lp(t,n));return i}return[Lp(t,e)]}function Wp(t,e){for(let i=[];;){if(!e||"."!=e.name)return i;let n=Ep(e);if(!Ip(n))return i;i.unshift(Lp(t,n)),e=Ep(n)}}function Hp(t,e){let i=ka(t).resolveInner(e,-1),n=function(t,e){let i;for(let t=e;!i;t=t.parent){if(!t)return null;"Statement"==t.name&&(i=t)}let n=null;for(let e=i.firstChild,s=!1,r=null;e;e=e.nextSibling){let i="Keyword"==e.name?t.sliceString(e.from,e.to).toLowerCase():null,o=null;if(s)if("as"==i&&r&&Ip(e.nextSibling))o=Lp(t,e.nextSibling);else{if(i&&Vp.has(i))break;r&&Ip(e)&&(o=Lp(t,e))}else s="from"==i;o&&(n||(n=Object.create(null)),n[o]=Np(t,r)),r=/Identifier$/.test(e.name)?e:null}return n}(t.doc,i);return"Identifier"==i.name||"QuotedIdentifier"==i.name||"Keyword"==i.name?{from:i.from,quoted:"QuotedIdentifier"==i.name?t.doc.sliceString(i.from,i.from+1):null,parents:Wp(t.doc,Ep(i)),aliases:n}:"."==i.name?{from:e,quoted:null,parents:Wp(t.doc,i),aliases:n}:{from:e,quoted:null,parents:[],empty:!0,aliases:n}}const Vp=new Set("where group having order union intersect except all distinct limit offset fetch for".split(" "));function zp(t,e,i){return i.map(i=>({...i,label:i.label[0]==t?i.label:t+i.label+e,apply:void 0}))}const Fp=/^\w*$/,qp=/^[`'"\[]?\w*[`'"\]]?$/;function _p(t){return t.self&&"string"==typeof t.self.label}class Up{constructor(t,e){this.idQuote=t,this.idCaseInsensitive=e,this.list=[],this.children=void 0}child(t){let e=this.children||(this.children=Object.create(null)),i=e[t];return i||(t&&!this.list.some(e=>e.label==t)&&this.list.push(Qp(t,"type",this.idQuote,this.idCaseInsensitive)),e[t]=new Up(this.idQuote,this.idCaseInsensitive))}maybeChild(t){return this.children?this.children[t]:null}addCompletion(t){let e=this.list.findIndex(e=>e.label==t.label);e>-1?this.list[e]=t:this.list.push(t)}addCompletions(t){for(let e of t)this.addCompletion("string"==typeof e?Qp(e,"property",this.idQuote,this.idCaseInsensitive):e)}addNamespace(t){Array.isArray(t)?this.addCompletions(t):_p(t)?this.addNamespace(t.children):this.addNamespaceObject(t)}addNamespaceObject(t){for(let e of Object.keys(t)){let i=t[e],n=null,s=e.replace(/\\?\./g,t=>"."==t?"\0":t).split("\0"),r=this;_p(i)&&(n=i.self,i=i.children);for(let t=0;t{return i(e?n.toUpperCase():n,21==(s=t[n])?"type":20==s?"keyword":"variable");var s});return s=["QuotedIdentifier","String","LineComment","BlockComment","."],r=bf(n),t=>{for(let e=ka(t.state).resolveInner(t.pos,-1);e;e=e.parent){if(s.indexOf(e.name)>-1)return null;if(e.type.isTop)break}return r(t)};var s,r}let jp=Bp.configure({props:[Va.add({Statement:Qa()}),Ka.add({Statement:(t,e)=>({from:Math.min(t.from+100,e.doc.lineAt(t.from).to),to:t.to}),BlockComment:t=>({from:t.from+2,to:t.to-2})}),jl({Keyword:ma.keyword,Type:ma.typeName,Builtin:ma.standard(ma.name),Bits:ma.number,Bytes:ma.string,Bool:ma.bool,Null:ma.null,Number:ma.number,String:ma.string,Identifier:ma.name,QuotedIdentifier:ma.special(ma.string),SpecialVar:ma.special(ma.name),LineComment:ma.lineComment,BlockComment:ma.blockComment,Operator:ma.operator,"Semi Punctuation":ma.punctuation,"( )":ma.paren,"{ }":ma.brace,"[ ]":ma.squareBracket})]});class Xp{constructor(t,e,i){this.dialect=t,this.language=e,this.spec=i}get extension(){return this.language.extension}configureLanguage(t,e){return new Xp(this.dialect,this.language.configure(t,e),this.spec)}static define(t){let e=function(t,e,i,n){let s={};for(let e in Dp)s[e]=(t.hasOwnProperty(e)?t:Dp)[e];return e&&(s.words=Tp(e,i||"",n)),s}(t,t.keywords,t.types,t.builtin),i=xa.define({name:"sql",parser:jp.configure({tokenizers:[{from:Pp,to:Rp(e)}]}),languageData:{commentTokens:{line:"--",block:{open:"/*",close:"*/"}},closeBrackets:{brackets:["(","[","{","'",'"',"`"]}}});return new Xp(e,i,t)}}function Gp(t,e){return{label:t,type:e,boost:-1}}function Yp(t,e=!1,i){return Kp(t.dialect.words,e,i||Gp)}function Jp(t){return t.schema?function(t,e,i,n,s,r){var o;let l=(null===(o=null==r?void 0:r.spec.identifierQuotes)||void 0===o?void 0:o[0])||'"',a=new Up(l,!!(null==r?void 0:r.spec.caseInsensitiveIdentifiers)),h=s?a.child(s):null;return a.addNamespace(t),e&&(h||a).addCompletions(e),i&&a.addCompletions(i),h&&a.addCompletions(h.list),n&&a.addCompletions((h||a).child(n).list),t=>{let{parents:e,from:i,quoted:s,empty:r,aliases:o}=Hp(t.state,t.pos);if(r&&!t.explicit)return null;o&&1==e.length&&(e=o[e[0]]||e);let l=a;for(let t of e){for(;!l.children||!l.children[t];)if(l==a&&h)l=h;else{if(l!=h||!n)return null;l=l.child(n)}let e=l.maybeChild(t);if(!e)return null;l=e}let c=l.list;if(l==a&&o&&(c=c.concat(Object.keys(o).map(t=>({label:t,type:"constant"})))),s){let e=s[0],n=$p(e);return{from:i,to:t.state.sliceDoc(t.pos,t.pos+1)==n?t.pos+1:void 0,options:zp(e,n,c),validFor:qp}}return{from:i,options:c,validFor:Fp}}}(t.schema,t.tables,t.schemas,t.defaultTable,t.defaultSchema,t.dialect||tm):()=>null}function Zp(t){return t.schema?(t.dialect||tm).language.data.of({autocomplete:Jp(t)}):[]}const tm=Xp.define({}),em=Xp.define({keywords:"and as asc between by case cast count current_date current_time current_timestamp desc distinct each else escape except exists explain filter first for from full generated group having if in index inner intersect into isnull join last left like limit not null or order outer over pragma primary query raise range regexp right rollback row select set table then to union unique using values view virtual when where",types:"null integer real text blob",builtin:"",operatorChars:"*+-%<>!=&|/~",identifierQuotes:'`"',specialVar:"@:?$",caseInsensitiveIdentifiers:!0}),im=dt.define();function nm(t={}){return function(t={}){let e=t.dialect||tm;return new Ba(e.language,[Zp(t),e.language.data.of({autocomplete:Yp(e,t.upperCaseKeywords,t.keywordCompletion)})])}({dialect:em,schema:t.schema,defaultTable:t.defaultTable,defaultSchema:t.defaultSchema})}function sm(t,e,i){const n=[al(),ul,Jr(),mh(),Nr(),[_r,Ur],Tt.allowMultipleSelections.of(!0),Tt.transactionFilter.of(t=>{if(!t.docChanged||!t.isUserEvent("input.type")&&!t.isUserEvent("input.complete"))return t;let e=t.startState.languageDataAt("indentOnInput",t.startState.selection.main.head);if(!e.length)return t;let i=t.newDoc,{head:n}=t.newSelection.main,s=i.lineAt(n);if(n>s.from+200)return t;let r=i.sliceString(s.from,n);if(!e.some(t=>t.test(r)))return t;let{state:o}=t,l=-1,a=[];for(let{head:t}of o.selection.ranges){let e=o.doc.lineAt(t);if(e.from==l)continue;l=e.from;let i=Wa(o,e.from);if(null==i)continue;let n=/^\s*/.exec(e.text)[0],s=Na(o,i);n!=s&&a.push({from:e.from,to:e.from+n.length,insert:s})}return a.length?[t,{changes:a,sequential:!0}]:t}),xh(Ch,{fallback:!0}),Eh(),[dd,ad],kd(),lo(),co(),no,Tu()],s=[...pd,...gu,...hf,...oh,...Sd,...Nd];return t?(n.push(function(t={}){return[sc,nc.of(t),pr.domEventHandlers({beforeinput(t,e){let i="historyUndo"==t.inputType?oc:"historyRedo"==t.inputType?lc:null;return!!i&&(t.preventDefault(),i(e))}})]}()),s.push(...xc)):(e&&s.push({key:"Mod-z",preventDefault:!0,run:()=>(e(),!0)}),i&&s.push({key:"Mod-y",mac:"Mod-Shift-z",preventDefault:!0,run:()=>(i(),!0)},{key:"Mod-Shift-z",preventDefault:!0,run:()=>(i(),!0)})),n.push(kr.of(s)),n}return t.SQLiteDialect=em,t.editorFromTextArea=function(t,e={}){const i=function(t,e={}){const{doc:i="",schema:n,defaultTable:s,defaultSchema:r,history:o=!0,onHostUndo:l,onHostRedo:a,extensions:h=[],fixedTooltips:c=!1,onChange:u,onSubmit:f,onEscape:d,lineWrapping:p=!0}=e,m=new et,g=[];if(f){const t=()=>(f(w),!0);g.push({key:"Mod-Enter",run:t},{key:"Shift-Enter",run:t})}d&&g.push({key:"Escape",run:()=>(d(w),!0)});const v=[Z.highest(kr.of(g)),...sm(o,l,a),p?pr.lineWrapping:[],c?po({position:"fixed"}):[],m.of(nm({schema:n,defaultTable:s,defaultSchema:r})),u?pr.updateListener.of(t=>{t.docChanged&&(t.transactions.some(t=>t.annotation(im))||u(t))}):[],...h];let w=new pr({doc:i,extensions:v,...t?{parent:t}:{}});return{view:w,updateSchema(t){w.dispatch({effects:m.reconfigure(nm(t))})},destroy(){w.destroy()},get value(){return w.state.doc.toString()},set value(t){w.dispatch({changes:{from:0,to:w.state.doc.length,insert:t},annotations:im.of(!0)})}}}(null,{doc:t.value,schema:e.schema,defaultTable:e.defaultTable,defaultSchema:e.defaultSchema,onSubmit:e=>{t.value=e.state.doc.toString(),t.form.submit()}}),n=i.view;n.updateSchema=i.updateSchema;let s=n.contentDOM.closest(".cm-editor");return new ResizeObserver(function(){n.requestMeasure()}).observe(s,{attributes:!0}),t.parentNode.insertBefore(n.dom,t),t.style.display="none",t.form&&t.form.addEventListener("submit",()=>{t.value=n.state.doc.toString()}),n},t}({}); diff --git a/datasette/static/cm-editor.js b/datasette/static/cm-editor.js new file mode 100644 index 00000000..55337890 --- /dev/null +++ b/datasette/static/cm-editor.js @@ -0,0 +1,51 @@ +// IIFE entry point for Datasette's own SQL editor pages. Built as +// cm-editor.bundle.js (global name `cm`) and included by _codemirror.html. +// +// This is a thin consumer of the datasette-sql-editor.js primitives so there is +// a single CodeMirror implementation. rollup inlines the shared module into this +// bundle. +import { createSqlEditor, SQLiteDialect } from "./datasette-sql-editor.js"; + +// Re-exported so plugins/pages using the IIFE global can reach the curated +// dialect (cm.SQLiteDialect) without a second CodeMirror instance. +export { SQLiteDialect }; + +// Utility function from https://codemirror.net/docs/migration/. Wraps a textarea +// with a CodeMirror SQL editor, mirroring the textarea's value back on submit. +// Returns the EditorView (with an added updateSchema method) for backwards +// compatibility with existing callers (window.editor). +export function editorFromTextArea(textarea, conf = {}) { + const submit = (view) => { + textarea.value = view.state.doc.toString(); + textarea.form.submit(); + }; + + const handle = createSqlEditor(null, { + doc: textarea.value, + schema: conf.schema, + defaultTable: conf.defaultTable, + defaultSchema: conf.defaultSchema, + onSubmit: submit, + }); + const view = handle.view; + + // Preserve the historical public surface: callers use view.updateSchema(conf). + view.updateSchema = handle.updateSchema; + + // Idea taken from https://discuss.codemirror.net/t/resizing-codemirror-6/3265. + // Using CSS resize: both and scheduling a measurement when the element changes. + let editorDOM = view.contentDOM.closest(".cm-editor"); + let observer = new ResizeObserver(function () { + view.requestMeasure(); + }); + observer.observe(editorDOM, { attributes: true }); + + textarea.parentNode.insertBefore(view.dom, textarea); + textarea.style.display = "none"; + if (textarea.form) { + textarea.form.addEventListener("submit", () => { + textarea.value = view.state.doc.toString(); + }); + } + return view; +} diff --git a/datasette/static/column-chooser.js b/datasette/static/column-chooser.js index 29729f27..198641f3 100644 --- a/datasette/static/column-chooser.js +++ b/datasette/static/column-chooser.js @@ -1,9 +1,7 @@ -let columnChooserInstanceCounter = 0; - class ColumnChooser extends HTMLElement { constructor() { super(); - this.titleId = `column-chooser-title-${++columnChooserInstanceCounter}`; + this.attachShadow({ mode: "open" }); // State this._items = []; @@ -28,60 +26,375 @@ class ColumnChooser extends HTMLElement { // Bound handlers this._onMove = this._onMove.bind(this); this._onUp = this._onUp.bind(this); - } - connectedCallback() { - if (this._modal) return; - this.innerHTML = ` - + this.shadowRoot.innerHTML = ` + + +
- - + +
-
+ `; // DOM refs - this._modal = this.querySelector("datasette-modal"); - this._listWrap = this.querySelector(".list-wrap"); - this._dragList = this.querySelector(".drag-list"); - this._pulseTop = this.querySelector(".scroll-pulse.top"); - this._pulseBot = this.querySelector(".scroll-pulse.bot"); - this._selectAllBtn = this.querySelector(".select-all"); - this._deselectAllBtn = this.querySelector(".deselect-all"); - this._cancelBtn = this.querySelector(".modal-btn-ghost"); - this._applyBtn = this.querySelector(".modal-btn-primary"); - this._countEl = this.querySelector(".modal-meta"); - this._footerEl = this.querySelector(".footer-info"); + this._dialog = this.shadowRoot.querySelector("dialog"); + this._listWrap = this.shadowRoot.getElementById("listWrap"); + this._dragList = this.shadowRoot.getElementById("dragList"); + this._pulseTop = this.shadowRoot.getElementById("pulseTop"); + this._pulseBot = this.shadowRoot.getElementById("pulseBot"); + this._selectAllBtn = this.shadowRoot.getElementById("selectAllBtn"); + this._deselectAllBtn = this.shadowRoot.getElementById("deselectAllBtn"); + this._cancelBtn = this.shadowRoot.getElementById("cancelBtn"); + this._applyBtn = this.shadowRoot.getElementById("applyBtn"); + this._countEl = this.shadowRoot.getElementById("selectedCount"); + this._footerEl = this.shadowRoot.getElementById("footerInfo"); // Event listeners this._selectAllBtn.addEventListener("click", () => this._selectAll()); this._deselectAllBtn.addEventListener("click", () => this._deselectAll()); - this._cancelBtn.addEventListener("click", () => - this._modal.requestClose("cancel"), - ); + this._cancelBtn.addEventListener("click", () => this._close()); this._applyBtn.addEventListener("click", () => this._apply()); - this._modal.beforeClose = () => { - this._items = this._savedItems ? [...this._savedItems] : this._items; - this._checked = this._savedChecked - ? new Set(this._savedChecked) - : this._checked; - return true; - }; + this._dialog.addEventListener("click", (e) => { + if (e.target === this._dialog) this._close(); + }); + this._dialog.addEventListener("cancel", (e) => { + e.preventDefault(); + this._close(); + }); } /** @@ -101,11 +414,19 @@ class ColumnChooser extends HTMLElement { this._savedChecked = new Set(this._checked); this._render(); - this._modal.show(); + this._dialog.showModal(); } // ── Internal methods ── + _close() { + this._items = this._savedItems ? [...this._savedItems] : this._items; + this._checked = this._savedChecked + ? new Set(this._savedChecked) + : this._checked; + this._dialog.close(); + } + _selectAll() { this._items.forEach((col) => this._checked.add(col)); this._dragList.querySelectorAll('input[type="checkbox"]').forEach((cb) => { @@ -124,7 +445,7 @@ class ColumnChooser extends HTMLElement { _apply() { const selected = this._items.filter((col) => this._checked.has(col)); - this._modal.close(); + this._dialog.close(); if (this._onApply) { this._onApply(selected); } @@ -151,13 +472,11 @@ class ColumnChooser extends HTMLElement { - + ${col}
`; - li.querySelector(".drag-item-label").textContent = col; - li.querySelector("input").addEventListener("change", (e) => { e.target.checked ? this._checked.add(col) : this._checked.delete(col); this._updateCounts(); @@ -190,7 +509,7 @@ class ColumnChooser extends HTMLElement { this._ghostOffX = e.clientX - rect.left; this._ghostOffY = e.clientY - rect.top; - // Keep the drag preview inside the dialog so it stays above the backdrop. + // Build ghost inside shadow DOM this._ghost = document.createElement("div"); this._ghost.className = "drag-ghost"; this._ghost.style.width = rect.width + "px"; @@ -199,7 +518,7 @@ class ColumnChooser extends HTMLElement { this._ghost.querySelector(".drop-indicator")?.remove(); const h = this._ghost.querySelector(".drag-handle"); if (h) h.style.color = "var(--accent)"; - this._modal.dialog.appendChild(this._ghost); + this.shadowRoot.appendChild(this._ghost); srcEl.classList.add("is-dragging"); this._positionGhost(e.clientX, e.clientY); diff --git a/datasette/static/datasette-sql-editor.bundle.js b/datasette/static/datasette-sql-editor.bundle.js new file mode 100644 index 00000000..ac9765a4 --- /dev/null +++ b/datasette/static/datasette-sql-editor.bundle.js @@ -0,0 +1 @@ +let t=[],e=[];function i(i){if(i<768)return!1;for(let n=0,s=t.length;;){let r=n+s>>1;if(i=e[r]))return!0;n=r+1}if(n==s)return!1}}function n(t){return t>=127462&&t<=127487}(()=>{let i="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map(t=>t?parseInt(t,36):1);for(let n=0,s=0;n=0&&n(l(t,s));)i++,s-=2;if(i%2==0)break;e+=2}}}return e}function o(t,e,i){for(;e>1;){let n=r(t,e-2,i);if(n=56320&&t<57344}function h(t){return t>=55296&&t<56320}function c(t){return t<65536?1:2}class u{lineAt(t){if(t<0||t>this.length)throw new RangeError(`Invalid position ${t} in document of length ${this.length}`);return this.lineInner(t,!1,1,0)}line(t){if(t<1||t>this.lines)throw new RangeError(`Invalid line number ${t} in ${this.lines}-line document`);return this.lineInner(t,!0,1,0)}replace(t,e,i){[t,e]=y(this,t,e);let n=[];return this.decompose(0,t,n,2),i.length&&i.decompose(0,i.length,n,3),this.decompose(e,this.length,n,1),d.from(n,this.length-(e-t)+i.length)}append(t){return this.replace(this.length,this.length,t)}slice(t,e=this.length){[t,e]=y(this,t,e);let i=[];return this.decompose(t,e,i,0),d.from(i,e-t)}eq(t){if(t==this)return!0;if(t.length!=this.length||t.lines!=this.lines)return!1;let e=this.scanIdentical(t,1),i=this.length-this.scanIdentical(t,-1),n=new g(this),s=new g(t);for(let t=e,r=e;;){if(n.next(t),s.next(t),t=0,n.lineBreak!=s.lineBreak||n.done!=s.done||n.value!=s.value)return!1;if(r+=n.value.length,n.done||r>=i)return!0}}iter(t=1){return new g(this,t)}iterRange(t,e=this.length){return new v(this,t,e)}iterLines(t,e){let i;if(null==t)i=this.iter();else{null==e&&(e=this.lines+1);let n=this.line(t).from;i=this.iterRange(n,Math.max(n,e==this.lines+1?this.length:e<=1?0:this.line(e-1).to))}return new w(i)}toString(){return this.sliceString(0)}toJSON(){let t=[];return this.flatten(t),t}constructor(){}static of(t){if(0==t.length)throw new RangeError("A document must have at least one line");return 1!=t.length||t[0]?t.length<=32?new f(t):d.from(f.split(t,[])):u.empty}}class f extends u{constructor(t,e=function(t){let e=-1;for(let i of t)e+=i.length+1;return e}(t)){super(),this.text=t,this.length=e}get lines(){return this.text.length}get children(){return null}lineInner(t,e,i,n){for(let s=0;;s++){let r=this.text[s],o=n+r.length;if((e?i:o)>=t)return new b(n,o,i,r);n=o+1,i++}}decompose(t,e,i,n){let s=t<=0&&e>=this.length?this:new f(m(this.text,t,e),Math.min(e,this.length)-Math.max(0,t));if(1&n){let t=i.pop(),e=p(s.text,t.text.slice(),0,s.length);if(e.length<=32)i.push(new f(e,t.length+s.length));else{let t=e.length>>1;i.push(new f(e.slice(0,t)),new f(e.slice(t)))}}else i.push(s)}replace(t,e,i){if(!(i instanceof f))return super.replace(t,e,i);[t,e]=y(this,t,e);let n=p(this.text,p(i.text,m(this.text,0,t)),e),s=this.length+i.length-(e-t);return n.length<=32?new f(n,s):d.from(f.split(n,[]),s)}sliceString(t,e=this.length,i="\n"){[t,e]=y(this,t,e);let n="";for(let s=0,r=0;s<=e&&rt&&r&&(n+=i),ts&&(n+=o.slice(Math.max(0,t-s),e-s)),s=l+1}return n}flatten(t){for(let e of this.text)t.push(e)}scanIdentical(){return 0}static split(t,e){let i=[],n=-1;for(let s of t)i.push(s),n+=s.length+1,32==i.length&&(e.push(new f(i,n)),i=[],n=-1);return n>-1&&e.push(new f(i,n)),e}}class d extends u{constructor(t,e){super(),this.children=t,this.length=e,this.lines=0;for(let e of t)this.lines+=e.lines}lineInner(t,e,i,n){for(let s=0;;s++){let r=this.children[s],o=n+r.length,l=i+r.lines-1;if((e?l:o)>=t)return r.lineInner(t,e,i,n);n=o+1,i=l+1}}decompose(t,e,i,n){for(let s=0,r=0;r<=e&&s=r){let s=n&((r<=t?1:0)|(l>=e?2:0));r>=t&&l<=e&&!s?i.push(o):o.decompose(t-r,e-r,i,s)}r=l+1}}replace(t,e,i){if([t,e]=y(this,t,e),i.lines=s&&e<=o){let l=r.replace(t-s,e-s,i),a=this.lines-r.lines+l.lines;if(l.lines
>4&&l.lines>a>>6){let s=this.children.slice();return s[n]=l,new d(s,this.length-(e-t)+i.length)}return super.replace(s,o,l)}s=o+1}return super.replace(t,e,i)}sliceString(t,e=this.length,i="\n"){[t,e]=y(this,t,e);let n="";for(let s=0,r=0;st&&s&&(n+=i),tr&&(n+=o.sliceString(t-r,e-r,i)),r=l+1}return n}flatten(t){for(let e of this.children)e.flatten(t)}scanIdentical(t,e){if(!(t instanceof d))return 0;let i=0,[n,s,r,o]=e>0?[0,0,this.children.length,t.children.length]:[this.children.length-1,t.children.length-1,-1,-1];for(;;n+=e,s+=e){if(n==r||s==o)return i;let l=this.children[n],a=t.children[s];if(l!=a)return i+l.scanIdentical(a,e);i+=l.length+1}}static from(t,e=t.reduce((t,e)=>t+e.length+1,-1)){let i=0;for(let e of t)i+=e.lines;if(i<32){let i=[];for(let e of t)e.flatten(i);return new f(i,e)}let n=Math.max(32,i>>5),s=n<<1,r=n>>1,o=[],l=0,a=-1,h=[];function c(t){let e;if(t.lines>s&&t instanceof d)for(let e of t.children)c(e);else t.lines>r&&(l>r||!l)?(u(),o.push(t)):t instanceof f&&l&&(e=h[h.length-1])instanceof f&&t.lines+e.lines<=32?(l+=t.lines,a+=t.length+1,h[h.length-1]=new f(e.text.concat(t.text),e.length+1+t.length)):(l+t.lines>n&&u(),l+=t.lines,a+=t.length+1,h.push(t))}function u(){0!=l&&(o.push(1==h.length?h[0]:d.from(h,a)),a=-1,l=h.length=0)}for(let e of t)c(e);return u(),1==o.length?o[0]:new d(o,e)}}function p(t,e,i=0,n=1e9){for(let s=0,r=0,o=!0;r=i&&(a>n&&(l=l.slice(0,n-s)),s0?1:(t instanceof f?t.text.length:t.children.length)<<1]}nextInner(t,e){for(this.done=this.lineBreak=!1;;){let i=this.nodes.length-1,n=this.nodes[i],s=this.offsets[i],r=s>>1,o=n instanceof f?n.text.length:n.children.length;if(r==(e>0?o:0)){if(0==i)return this.done=!0,this.value="",this;e>0&&this.offsets[i-1]++,this.nodes.pop(),this.offsets.pop()}else if((1&s)==(e>0?0:1)){if(this.offsets[i]+=e,0==t)return this.lineBreak=!0,this.value="\n",this;t--}else if(n instanceof f){let s=n.text[r+(e<0?-1:0)];if(this.offsets[i]+=e,s.length>Math.max(0,t))return this.value=0==t?s:e>0?s.slice(t):s.slice(0,s.length-t),this;t-=s.length}else{let s=n.children[r+(e<0?-1:0)];t>s.length?(t-=s.length,this.offsets[i]+=e):(e<0&&this.offsets[i]--,this.nodes.push(s),this.offsets.push(e>0?1:(s instanceof f?s.text.length:s.children.length)<<1))}}}next(t=0){return t<0&&(this.nextInner(-t,-this.dir),t=this.value.length),this.nextInner(t,this.dir)}}class v{constructor(t,e,i){this.value="",this.done=!1,this.cursor=new g(t,e>i?-1:1),this.pos=e>i?t.length:0,this.from=Math.min(e,i),this.to=Math.max(e,i)}nextInner(t,e){if(e<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;t+=Math.max(0,e<0?this.pos-this.to:this.from-this.pos);let i=e<0?this.pos-this.from:this.to-this.pos;t>i&&(t=i),i-=t;let{value:n}=this.cursor.next(t);return this.pos+=(n.length+t)*e,this.value=n.length<=i?n:e<0?n.slice(n.length-i):n.slice(0,i),this.done=!this.value,this}next(t=0){return t<0?t=Math.max(t,this.from-this.pos):t>0&&(t=Math.min(t,this.to-this.pos)),this.nextInner(t,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&""!=this.value}}class w{constructor(t){this.inner=t,this.afterBreak=!0,this.value="",this.done=!1}next(t=0){let{done:e,lineBreak:i,value:n}=this.inner.next(t);return e&&this.afterBreak?(this.value="",this.afterBreak=!1):e?(this.done=!0,this.value=""):i?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=n,this.afterBreak=!1),this}get lineBreak(){return!1}}"undefined"!=typeof Symbol&&(u.prototype[Symbol.iterator]=function(){return this.iter()},g.prototype[Symbol.iterator]=v.prototype[Symbol.iterator]=w.prototype[Symbol.iterator]=function(){return this});class b{constructor(t,e,i,n){this.from=t,this.to=e,this.number=i,this.text=n}get length(){return this.to-this.from}}function y(t,e,i){return[e=Math.max(0,Math.min(t.length,e)),Math.max(e,Math.min(t.length,i))]}function x(t,e,i=!0,n=!0){return s(t,e,i,n)}function k(t,e){let i=t.charCodeAt(e);if(!(n=i,n>=55296&&n<56320&&e+1!=t.length))return i;var n;let s=t.charCodeAt(e+1);return function(t){return t>=56320&&t<57344}(s)?s-56320+(i-55296<<10)+65536:i}function S(t){return t<=65535?String.fromCharCode(t):(t-=65536,String.fromCharCode(55296+(t>>10),56320+(1023&t)))}function C(t){return t<65536?1:2}const A=/\r\n?|\n/;var M=function(t){return t[t.Simple=0]="Simple",t[t.TrackDel=1]="TrackDel",t[t.TrackBefore=2]="TrackBefore",t[t.TrackAfter=3]="TrackAfter",t}(M||(M={}));class O{constructor(t){this.sections=t}get length(){let t=0;for(let e=0;et)return s+(t-n);s+=o}else{if(i!=M.Simple&&a>=t&&(i==M.TrackDel&&nt||i==M.TrackBefore&&nt))return null;if(a>t||a==t&&e<0&&!o)return t==n||e<0?s:s+l;s+=l}n=a}if(t>n)throw new RangeError(`Position ${t} is out of range for changeset of length ${n}`);return s}touchesRange(t,e=t){for(let i=0,n=0;i=0&&n<=e&&s>=t)return!(ne)||"cover";n=s}return!1}toString(){let t="";for(let e=0;e=0?":"+n:"")}return t}toJSON(){return this.sections}static fromJSON(t){if(!Array.isArray(t)||t.length%2||t.some(t=>"number"!=typeof t))throw new RangeError("Invalid JSON representation of ChangeDesc");return new O(t)}static create(t){return new O(t)}}class T extends O{constructor(t,e){super(t),this.inserted=e}apply(t){if(this.length!=t.length)throw new RangeError("Applying change set to a document with the wrong length");return P(this,(e,i,n,s,r)=>t=t.replace(n,n+(i-e),r),!1),t}mapDesc(t,e=!1){return B(this,t,e,!0)}invert(t){let e=this.sections.slice(),i=[];for(let n=0,s=0;n=0){e[n]=o,e[n+1]=r;let l=n>>1;for(;i.length0&&R(i,e,s.text),s.forward(t),o+=t}let a=t[r++];for(;o>1].toJSON()))}return t}static of(t,e,i){let n=[],s=[],r=0,o=null;function l(t=!1){if(!t&&!n.length)return;ro||t<0||o>e)throw new RangeError(`Invalid change range ${t} to ${o} (in doc of length ${e})`);let c=h?"string"==typeof h?u.of(h.split(i||A)):h:u.empty,f=c.length;if(t==o&&0==f)return;tr&&D(n,t-r,-1),D(n,o-t,f),R(s,n,c),r=o}}(t),l(!o),o}static empty(t){return new T(t?[t,-1]:[],[])}static fromJSON(t){if(!Array.isArray(t))throw new RangeError("Invalid JSON representation of ChangeSet");let e=[],i=[];for(let n=0;ne&&"string"!=typeof t))throw new RangeError("Invalid JSON representation of ChangeSet");if(1==s.length)e.push(s[0],0);else{for(;i.length=0&&i<=0&&i==t[s+1]?t[s]+=e:s>=0&&0==e&&0==t[s]?t[s+1]+=i:n?(t[s]+=e,t[s+1]+=i):t.push(e,i)}function R(t,e,i){if(0==i.length)return;let n=e.length-2>>1;if(n>1])),!(i||o==t.sections.length||t.sections[o+1]<0);)l=t.sections[o++],a=t.sections[o++];e(s,h,r,c,f),s=h,r=c}}}function B(t,e,i,n=!1){let s=[],r=n?[]:null,o=new L(t),l=new L(e);for(let t=-1;;){if(o.done&&l.len||l.done&&o.len)throw new Error("Mismatched change set lengths");if(-1==o.ins&&-1==l.ins){let t=Math.min(o.len,l.len);D(s,t,-1),o.forward(t),l.forward(t)}else if(l.ins>=0&&(o.ins<0||t==o.i||0==o.off&&(l.len=0&&t=0)){if(o.done&&l.done)return r?T.createSet(s,r):O.create(s);throw new Error("Mismatched change set lengths")}{let e=0,i=o.len;for(;i;)if(-1==l.ins){let t=Math.min(i,l.len);e+=t,i-=t,l.forward(t)}else{if(!(0==l.ins&&l.lene||o.ins>=0&&o.len>e)&&(t||n.length>i),r.forward2(e),o.forward(e)}}else D(n,0,o.ins,t),s&&R(s,n,o.text),o.next()}}class L{constructor(t){this.set=t,this.i=0,this.next()}next(){let{sections:t}=this.set;this.i>1;return e>=t.length?u.empty:t[e]}textBit(t){let{inserted:e}=this.set,i=this.i-2>>1;return i>=e.length&&!t?u.empty:e[i].slice(this.off,null==t?void 0:this.off+t)}forward(t){t==this.len?this.next():(this.len-=t,this.off+=t)}forward2(t){-1==this.ins?this.forward(t):t==this.ins?this.next():(this.ins-=t,this.off+=t)}}class I{constructor(t,e,i,n){this.from=t,this.to=e,this.flags=i,this.goalColumn=n}get anchor(){return 32&this.flags?this.to:this.from}get head(){return 32&this.flags?this.from:this.to}get empty(){return this.from==this.to}get assoc(){return 8&this.flags?-1:16&this.flags?1:0}get undirectional(){return(64&this.flags)>0}get bidiLevel(){let t=7&this.flags;return 7==t?null:t}map(t,e=-1){let i,n;return this.empty?i=n=t.mapPos(this.from,e):(i=t.mapPos(this.from,1),n=t.mapPos(this.to,-1)),i==this.from&&n==this.to?this:new I(i,n,this.flags,this.goalColumn)}extend(t,e=t,i=0){if(t<=this.anchor&&e>=this.anchor)return N.range(t,e,void 0,void 0,i);let n=Math.abs(t-this.anchor)>Math.abs(e-this.anchor)?t:e;return N.range(this.anchor,n,void 0,void 0,i)}eq(t,e=!1){return!(this.anchor!=t.anchor||this.head!=t.head||this.goalColumn!=t.goalColumn||e&&this.empty&&this.assoc!=t.assoc)}toJSON(){return{anchor:this.anchor,head:this.head}}static fromJSON(t){if(!t||"number"!=typeof t.anchor||"number"!=typeof t.head)throw new RangeError("Invalid JSON representation for SelectionRange");return N.range(t.anchor,t.head)}static create(t,e,i,n){return new I(t,e,i,n)}}class N{constructor(t,e){this.ranges=t,this.mainIndex=e}map(t,e=-1){return t.empty?this:N.create(this.ranges.map(i=>i.map(t,e)),this.mainIndex)}eq(t,e=!1){if(this.ranges.length!=t.ranges.length||this.mainIndex!=t.mainIndex)return!1;for(let i=0;it.toJSON()),main:this.mainIndex}}static fromJSON(t){if(!t||!Array.isArray(t.ranges)||"number"!=typeof t.main||t.main>=t.ranges.length)throw new RangeError("Invalid JSON representation for EditorSelection");return new N(t.ranges.map(t=>I.fromJSON(t)),t.main)}static single(t,e=t){return new N([N.range(t,e)],0)}static create(t,e=0){if(0==t.length)throw new RangeError("A selection needs at least one range");for(let i=0,n=0;nt.from-e.from),e=t.indexOf(i);for(let i=1;in.head?N.range(o,r):N.range(r,o))}}return new N(t,e)}}function W(t,e){for(let i of t.ranges)if(i.to>e)throw new RangeError("Selection points outside of document")}let H=0;class V{constructor(t,e,i,n,s){this.combine=t,this.compareInput=e,this.compare=i,this.isStatic=n,this.id=H++,this.default=t([]),this.extensions="function"==typeof s?s(this):s}get reader(){return this}static define(t={}){return new V(t.combine||(t=>t),t.compareInput||((t,e)=>t===e),t.compare||(t.combine?(t,e)=>t===e:z),!!t.static,t.enables)}of(t){return new F([],this,0,t)}compute(t,e){if(this.isStatic)throw new Error("Can't compute a static facet");return new F(t,this,1,e)}computeN(t,e){if(this.isStatic)throw new Error("Can't compute a static facet");return new F(t,this,2,e)}from(t,e){return e||(e=t=>t),this.compute([t],i=>e(i.field(t)))}}function z(t,e){return t==e||t.length==e.length&&t.every((t,i)=>t===e[i])}class F{constructor(t,e,i,n){this.dependencies=t,this.facet=e,this.type=i,this.value=n,this.id=H++}dynamicSlot(t){var e;let i=this.value,n=this.facet.compareInput,s=this.id,r=t[s]>>1,o=2==this.type,l=!1,a=!1,h=[];for(let i of this.dependencies)"doc"==i?l=!0:"selection"==i?a=!0:1&(null!==(e=t[i.id])&&void 0!==e?e:1)||h.push(t[i.id]);return{create:t=>(t.values[r]=i(t),1),update(t,e){if(l&&e.docChanged||a&&(e.docChanged||e.selection)||_(t,h)){let e=i(t);if(o?!q(e,t.values[r],n):!n(e,t.values[r]))return t.values[r]=e,1}return 0},reconfigure:(t,e)=>{let l,a=e.config.address[s];if(null!=a){let s=st(e,a);if(this.dependencies.every(i=>i instanceof V?e.facet(i)===t.facet(i):!(i instanceof Q)||e.field(i,!1)==t.field(i,!1))||(o?q(l=i(t),s,n):n(l=i(t),s)))return t.values[r]=s,0}else l=i(t);return t.values[r]=l,1}}}get extension(){return this}}function q(t,e,i){if(t.length!=e.length)return!1;for(let n=0;nt[e.id]),s=i.map(t=>t.type),r=n.filter(t=>!(1&t)),o=t[e.id]>>1;function l(t){let i=[];for(let e=0;et===e),t);return t.provide&&(e.provides=t.provide(e)),e}create(t){let e=t.facet(U).find(t=>t.field==this);return((null==e?void 0:e.create)||this.createF)(t)}slot(t){let e=t[this.id]>>1;return{create:t=>(t.values[e]=this.create(t),1),update:(t,i)=>{let n=t.values[e],s=this.updateF(n,i);return this.compareF(n,s)?0:(t.values[e]=s,1)},reconfigure:(t,i)=>{let n,s=t.facet(U),r=i.facet(U);return(n=s.find(t=>t.field==this))&&n!=r.find(t=>t.field==this)?(t.values[e]=n.create(t),1):null!=i.config.address[this.id]?(t.values[e]=i.field(this),0):(t.values[e]=this.create(t),1)}}}init(t){return[this,U.of({field:this,create:t})]}get extension(){return this}}const j=4,K=3,X=2,G=1;function Y(t){return e=>new Z(e,t)}const J={highest:Y(0),high:Y(G),default:Y(X),low:Y(K),lowest:Y(j)};class Z{constructor(t,e){this.inner=t,this.prec=e}get extension(){return this}}class tt{of(t){return new et(this,t)}reconfigure(t){return tt.reconfigure.of({compartment:this,extension:t})}get(t){return t.config.compartments.get(this)}}class et{constructor(t,e){this.compartment=t,this.inner=e}get extension(){return this}}class it{constructor(t,e,i,n,s,r){for(this.base=t,this.compartments=e,this.dynamicSlots=i,this.address=n,this.staticValues=s,this.facets=r,this.statusTemplate=[];this.statusTemplate.length>1]}static resolve(t,e,i){let n=[],s=Object.create(null),r=new Map;for(let i of function(t,e,i){let n=[[],[],[],[],[]],s=new Map;function r(t,o){let l=s.get(t);if(null!=l){if(l<=o)return;let e=n[l].indexOf(t);e>-1&&n[l].splice(e,1),t instanceof et&&i.delete(t.compartment)}if(s.set(t,o),Array.isArray(t))for(let e of t)r(e,o);else if(t instanceof et){if(i.has(t.compartment))throw new RangeError("Duplicate use of compartment in extensions");let n=e.get(t.compartment)||t.inner;i.set(t.compartment,n),r(n,o)}else if(t instanceof Z)r(t.inner,t.prec);else if(t instanceof Q)n[o].push(t),t.provides&&r(t.provides,o);else if(t instanceof F)n[o].push(t),t.facet.extensions&&r(t.facet.extensions,X);else{let e=t.extension;if(!e)throw new Error(`Unrecognized extension value in extension set (${t}).`);if(e==t)throw new Error(`Unrecognized extension value in extension set (${t}). This sometimes happens because multiple instances of @codemirror/state are loaded, breaking instanceof checks.`);r(e,o)}}return r(t,X),n.reduce((t,e)=>t.concat(e))}(t,e,r))i instanceof Q?n.push(i):(s[i.facet.id]||(s[i.facet.id]=[])).push(i);let o=Object.create(null),l=[],a=[];for(let t of n)o[t.id]=a.length<<1,a.push(e=>t.slot(e));let h=null==i?void 0:i.config.facets;for(let t in s){let e=s[t],n=e[0].facet,r=h&&h[t]||[];if(e.every(t=>0==t.type))if(o[n.id]=l.length<<1|1,z(r,e))l.push(i.facet(n));else{let t=n.combine(e.map(t=>t.value));l.push(i&&n.compare(t,i.facet(n))?i.facet(n):t)}else{for(let t of e)0==t.type?(o[t.id]=l.length<<1|1,l.push(t.value)):(o[t.id]=a.length<<1,a.push(e=>t.dynamicSlot(e)));o[n.id]=a.length<<1,a.push(t=>$(t,n,e))}}let c=a.map(t=>t(o));return new it(t,r,c,o,l,s)}}function nt(t,e){if(1&e)return 2;let i=e>>1,n=t.status[i];if(4==n)throw new Error("Cyclic dependency between fields and/or facets");if(2&n)return n;t.status[i]=4;let s=t.computeSlot(t,t.config.dynamicSlots[i]);return t.status[i]=2|s}function st(t,e){return 1&e?t.config.staticValues[e>>1]:t.values[e>>1]}const rt=V.define(),ot=V.define({combine:t=>t.some(t=>t),static:!0}),lt=V.define({combine:t=>t.length?t[0]:void 0,static:!0}),at=V.define(),ht=V.define(),ct=V.define(),ut=V.define({combine:t=>!!t.length&&t[0]});class ft{constructor(t,e){this.type=t,this.value=e}static define(){return new dt}}class dt{of(t){return new ft(this,t)}}class pt{constructor(t){this.map=t}of(t){return new mt(this,t)}}class mt{constructor(t,e){this.type=t,this.value=e}map(t){let e=this.type.map(this.value,t);return void 0===e?void 0:e==this.value?this:new mt(this.type,e)}is(t){return this.type==t}static define(t={}){return new pt(t.map||(t=>t))}static mapEffects(t,e){if(!t.length)return t;let i=[];for(let n of t){let t=n.map(e);t&&i.push(t)}return i}}mt.reconfigure=mt.define(),mt.appendConfig=mt.define();class gt{constructor(t,e,i,n,s,r){this.startState=t,this.changes=e,this.selection=i,this.effects=n,this.annotations=s,this.scrollIntoView=r,this._doc=null,this._state=null,i&&W(i,e.newLength),s.some(t=>t.type==gt.time)||(this.annotations=s.concat(gt.time.of(Date.now())))}static create(t,e,i,n,s,r){return new gt(t,e,i,n,s,r)}get newDoc(){return this._doc||(this._doc=this.changes.apply(this.startState.doc))}get newSelection(){return this.selection||this.startState.selection.map(this.changes)}get state(){return this._state||this.startState.applyTransaction(this),this._state}annotation(t){for(let e of this.annotations)if(e.type==t)return e.value}get docChanged(){return!this.changes.empty}get reconfigured(){return this.startState.config!=this.state.config}isUserEvent(t){let e=this.annotation(gt.userEvent);return!(!e||!(e==t||e.length>t.length&&e.slice(0,t.length)==t&&"."==e[t.length]))}}function vt(t,e){let i=[];for(let n=0,s=0;;){let r,o;if(n=t[n]))r=t[n++],o=t[n++];else{if(!(s=0;s--){let r=i[s](t);r&&Object.keys(r).length&&(n=wt(n,bt(e,r,t.changes.newLength),!0))}return n==t?t:gt.create(e,t.changes,t.selection,n.effects,n.annotations,n.scrollIntoView)}(i?function(t){let e=t.startState,i=!0;for(let n of e.facet(at)){let e=n(t);if(!1===e){i=!1;break}Array.isArray(e)&&(i=!0===i?e:vt(i,e))}if(!0!==i){let n,s;if(!1===i)s=t.changes.invertedDesc,n=T.empty(e.doc.length);else{let e=t.changes.filter(i);n=e.changes,s=e.filtered.mapDesc(e.changes).invertedDesc}t=gt.create(e,n,t.selection&&t.selection.map(s),mt.mapEffects(t.effects,s),t.annotations,t.scrollIntoView)}let n=e.facet(ht);for(let i=n.length-1;i>=0;i--){let s=n[i](t);t=s instanceof gt?s:Array.isArray(s)&&1==s.length&&s[0]instanceof gt?s[0]:yt(e,kt(s),!1)}return t}(s):s)}gt.time=ft.define(),gt.userEvent=ft.define(),gt.addToHistory=ft.define(),gt.remote=ft.define();const xt=[];function kt(t){return null==t?xt:Array.isArray(t)?t:[t]}var St=function(t){return t[t.Word=0]="Word",t[t.Space=1]="Space",t[t.Other=2]="Other",t}(St||(St={}));const Ct=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;let At;try{At=new RegExp("[\\p{Alphabetic}\\p{Number}_]","u")}catch(t){}function Mt(t){return e=>{if(!/\S/.test(e))return St.Space;if(function(t){if(At)return At.test(t);for(let e=0;e"€"&&(i.toUpperCase()!=i.toLowerCase()||Ct.test(i)))return!0}return!1}(e))return St.Word;for(let i=0;i-1)return St.Word;return St.Other}}class Ot{constructor(t,e,i,n,s,r){this.config=t,this.doc=e,this.selection=i,this.values=n,this.status=t.statusTemplate.slice(),this.computeSlot=s,r&&(r._state=this);for(let t=0;ts.set(e,t)),i=null),s.set(e.value.compartment,e.value.extension)):e.is(mt.reconfigure)?(i=null,n=e.value):e.is(mt.appendConfig)&&(i=null,n=kt(n).concat(e.value));if(i)e=t.startState.values.slice();else{i=it.resolve(n,s,this),e=new Ot(i,this.doc,this.selection,i.dynamicSlots.map(()=>null),(t,e)=>e.reconfigure(t,this),null).values}let r=t.startState.facet(ot)?t.newSelection:t.newSelection.asSingle();new Ot(i,t.newDoc,r,e,(e,i)=>i.update(e,t),t)}replaceSelection(t){return"string"==typeof t&&(t=this.toText(t)),this.changeByRange(e=>({changes:{from:e.from,to:e.to,insert:t},range:N.cursor(e.from+t.length)}))}changeByRange(t){let e=this.selection,i=t(e.ranges[0]),n=this.changes(i.changes),s=[i.range],r=kt(i.effects);for(let i=1;is.spec.fromJSON(r,t)))}return Ot.create({doc:t.doc,selection:N.fromJSON(t.selection),extensions:e.extensions?n.concat([e.extensions]):n})}static create(t={}){let e=it.resolve(t.extensions||[],new Map),i=t.doc instanceof u?t.doc:u.of((t.doc||"").split(e.staticFacet(Ot.lineSeparator)||A)),n=t.selection?t.selection instanceof N?t.selection:N.single(t.selection.anchor,t.selection.head):N.single(0);return W(n,i.length),e.staticFacet(ot)||(n=n.asSingle()),new Ot(e,i,n,e.dynamicSlots.map(()=>null),(t,e)=>e.create(t),null)}get tabSize(){return this.facet(Ot.tabSize)}get lineBreak(){return this.facet(Ot.lineSeparator)||"\n"}get readOnly(){return this.facet(ut)}phrase(t,...e){for(let e of this.facet(Ot.phrases))if(Object.prototype.hasOwnProperty.call(e,t)){t=e[t];break}return e.length&&(t=t.replace(/\$(\$|\d*)/g,(t,i)=>{if("$"==i)return"$";let n=+(i||1);return!n||n>e.length?t:e[n-1]})),t}languageDataAt(t,e,i=-1){let n=[];for(let s of this.facet(rt))for(let r of s(this,e,i))Object.prototype.hasOwnProperty.call(r,t)&&n.push(r[t]);return n}charCategorizer(t){let e=this.languageDataAt("wordChars",t);return Mt(e.length?e[0]:"")}wordAt(t){let{text:e,from:i,length:n}=this.doc.lineAt(t),s=this.charCategorizer(t),r=t-i,o=t-i;for(;r>0;){let t=x(e,r,!1);if(s(e.slice(t,r))!=St.Word)break;r=t}for(;ot.length?t[0]:4}),Ot.lineSeparator=lt,Ot.readOnly=ut,Ot.phrases=V.define({compare(t,e){let i=Object.keys(t),n=Object.keys(e);return i.length==n.length&&i.every(i=>t[i]==e[i])}}),Ot.languageData=rt,Ot.changeFilter=at,Ot.transactionFilter=ht,Ot.transactionExtender=ct,tt.reconfigure=mt.define();class Dt{eq(t){return this==t}range(t,e=t){return Pt.create(t,e,this)}}function Rt(t,e){return t==e||t.constructor==e.constructor&&t.eq(e)}Dt.prototype.startSide=Dt.prototype.endSide=0,Dt.prototype.point=!1,Dt.prototype.mapMode=M.TrackDel;let Pt=class t{constructor(t,e,i){this.from=t,this.to=e,this.value=i}static create(e,i,n){return new t(e,i,n)}};function Bt(t,e){return t.from-e.from||t.value.startSide-e.value.startSide}class Et{constructor(t,e,i,n){this.from=t,this.to=e,this.value=i,this.maxPoint=n}get length(){return this.to[this.to.length-1]}findIndex(t,e,i,n=0){let s=i?this.to:this.from;for(let r=n,o=s.length;;){if(r==o)return r;let n=r+o>>1,l=s[n]-t||(i?this.value[n].endSide:this.value[n].startSide)-e;if(n==r)return l>=0?r:o;l>=0?o=n:r=n+1}}between(t,e,i,n){for(let s=this.findIndex(e,-1e9,!0),r=this.findIndex(i,1e9,!1,s);sh||a==h&&c.startSide>0&&c.endSide<=0)continue;(h-a||c.endSide-c.startSide)<0||(r<0&&(r=a),c.point&&(o=Math.max(o,h-a)),i.push(c),n.push(a-r),s.push(h-r))}return{mapped:i.length?new Et(n,s,i,o):null,pos:r}}}class Lt{constructor(t,e,i,n){this.chunkPos=t,this.chunk=e,this.nextLayer=i,this.maxPoint=n}static create(t,e,i,n){return new Lt(t,e,i,n)}get length(){let t=this.chunk.length-1;return t<0?0:Math.max(this.chunkEnd(t),this.nextLayer.length)}get size(){if(this.isEmpty)return 0;let t=this.nextLayer.size;for(let e of this.chunk)t+=e.value.length;return t}chunkEnd(t){return this.chunkPos[t]+this.chunk[t].length}update(t){let{add:e=[],sort:i=!1,filterFrom:n=0,filterTo:s=this.length}=t,r=t.filter;if(0==e.length&&!r)return this;if(i&&(e=e.slice().sort(Bt)),this.isEmpty)return e.length?Lt.of(e):this;let o=new Wt(this,null,-1).goto(0),l=0,a=[],h=new It;for(;o.value||l=0){let t=e[l++];h.addInner(t.from,t.to,t.value)||a.push(t)}else 1==o.rangeIndex&&o.chunkIndexthis.chunkEnd(o.chunkIndex)||so.to||s=s&&t<=s+r.length&&!1===r.between(s,t-s,e-s,i))return}this.nextLayer.between(t,e,i)}}iter(t=0){return Ht.from([this]).goto(t)}get isEmpty(){return this.nextLayer==this}static iter(t,e=0){return Ht.from(t).goto(e)}static compare(t,e,i,n,s=-1){let r=t.filter(t=>t.maxPoint>0||!t.isEmpty&&t.maxPoint>=s),o=e.filter(t=>t.maxPoint>0||!t.isEmpty&&t.maxPoint>=s),l=Nt(r,o,i),a=new zt(r,l,s),h=new zt(o,l,s);i.iterGaps((t,e,i)=>Ft(a,t,h,e,i,n)),i.empty&&0==i.length&&Ft(a,0,h,0,0,n)}static eq(t,e,i=0,n){null==n&&(n=999999999);let s=t.filter(t=>!t.isEmpty&&e.indexOf(t)<0),r=e.filter(e=>!e.isEmpty&&t.indexOf(e)<0);if(s.length!=r.length)return!1;if(!s.length)return!0;let o=Nt(s,r),l=new zt(s,o,0).goto(i),a=new zt(r,o,0).goto(i);for(;;){if(l.to!=a.to||!qt(l.active,a.active)||l.point&&(!a.point||!Rt(l.point,a.point)))return!1;if(l.to>n)return!0;l.next(),a.next()}}static spans(t,e,i,n,s=-1){let r=new zt(t,null,s).goto(e),o=e,l=r.openStart;for(;;){let t=Math.min(r.to,i);if(r.point){let i=r.activeForPoint(r.to),s=r.pointFromo&&(n.span(o,t,r.active,l),l=r.openEnd(t));if(r.to>i)return l+(r.point&&r.to>i?1:0);o=r.to,r.next()}}static of(t,e=!1){let i=new It;for(let n of t instanceof Pt?[t]:e?function(t){if(t.length>1)for(let e=t[0],i=1;i0)return t.slice().sort(Bt);e=n}return t}(t):t)i.add(n.from,n.to,n.value);return i.finish()}static join(t){if(!t.length)return Lt.empty;let e=t[t.length-1];for(let i=t.length-2;i>=0;i--)for(let n=t[i];n!=Lt.empty;n=n.nextLayer)e=new Lt(n.chunkPos,n.chunk,e,Math.max(n.maxPoint,e.maxPoint));return e}}Lt.empty=new Lt([],[],null,-1),Lt.empty.nextLayer=Lt.empty;class It{finishChunk(t){this.chunks.push(new Et(this.from,this.to,this.value,this.maxPoint)),this.chunkPos.push(this.chunkStart),this.chunkStart=-1,this.setMaxPoint=Math.max(this.setMaxPoint,this.maxPoint),this.maxPoint=-1,t&&(this.from=[],this.to=[],this.value=[])}constructor(){this.chunks=[],this.chunkPos=[],this.chunkStart=-1,this.last=null,this.lastFrom=-1e9,this.lastTo=-1e9,this.from=[],this.to=[],this.value=[],this.maxPoint=-1,this.setMaxPoint=-1,this.nextLayer=null}add(t,e,i){this.addInner(t,e,i)||(this.nextLayer||(this.nextLayer=new It)).add(t,e,i)}addInner(t,e,i){let n=t-this.lastTo||i.startSide-this.last.endSide;if(n<=0&&(t-this.lastFrom||i.startSide-this.last.startSide)<0)throw new Error("Ranges must be added sorted by `from` position and `startSide`");return!(n<0)&&(250==this.from.length&&this.finishChunk(!0),this.chunkStart<0&&(this.chunkStart=t),this.from.push(t-this.chunkStart),this.to.push(e-this.chunkStart),this.last=i,this.lastFrom=t,this.lastTo=e,this.value.push(i),i.point&&(this.maxPoint=Math.max(this.maxPoint,e-t)),!0)}addChunk(t,e){if((t-this.lastTo||e.value[0].startSide-this.last.endSide)<0)return!1;this.from.length&&this.finishChunk(!0),this.setMaxPoint=Math.max(this.setMaxPoint,e.maxPoint),this.chunks.push(e),this.chunkPos.push(t);let i=e.value.length-1;return this.last=e.value[i],this.lastFrom=e.from[i]+t,this.lastTo=e.to[i]+t,!0}finish(){return this.finishInner(Lt.empty)}finishInner(t){if(this.from.length&&this.finishChunk(!1),0==this.chunks.length)return t;let e=Lt.create(this.chunkPos,this.chunks,this.nextLayer?this.nextLayer.finishInner(t):t,this.setMaxPoint);return this.from=null,e}}function Nt(t,e,i){let n=new Map;for(let e of t)for(let t=0;t=this.minPoint)break}}}setRangeIndex(t){if(t==this.layer.chunk[this.chunkIndex].value.length){if(this.chunkIndex++,this.skip)for(;this.chunkIndex=i&&n.push(new Wt(r,e,i,s));return 1==n.length?n[0]:new Ht(n)}get startSide(){return this.value?this.value.startSide:0}goto(t,e=-1e9){for(let i of this.heap)i.goto(t,e);for(let t=this.heap.length>>1;t>=0;t--)Vt(this.heap,t);return this.next(),this}forward(t,e){for(let i of this.heap)i.forward(t,e);for(let t=this.heap.length>>1;t>=0;t--)Vt(this.heap,t);(this.to-t||this.value.endSide-e)<0&&this.next()}next(){if(0==this.heap.length)this.from=this.to=1e9,this.value=null,this.rank=-1;else{let t=this.heap[0];this.from=t.from,this.to=t.to,this.value=t.value,this.rank=t.rank,t.value&&t.next(),Vt(this.heap,0)}}}function Vt(t,e){for(let i=t[e];;){let n=1+(e<<1);if(n>=t.length)break;let s=t[n];if(n+1=0&&(s=t[n+1],n++),i.compare(s)<0)break;t[n]=i,t[e]=s,e=n}}class zt{constructor(t,e,i){this.minPoint=i,this.active=[],this.activeTo=[],this.activeRank=[],this.minActive=-1,this.point=null,this.pointFrom=0,this.pointRank=0,this.to=-1e9,this.endSide=0,this.openStart=-1,this.cursor=Ht.from(t,e,i)}goto(t,e=-1e9){return this.cursor.goto(t,e),this.active.length=this.activeTo.length=this.activeRank.length=0,this.minActive=-1,this.to=t,this.endSide=e,this.openStart=-1,this.next(),this}forward(t,e){for(;this.minActive>-1&&(this.activeTo[this.minActive]-t||this.active[this.minActive].endSide-e)<0;)this.removeActive(this.minActive);this.cursor.forward(t,e)}removeActive(t){_t(this.active,t),_t(this.activeTo,t),_t(this.activeRank,t),this.minActive=Ut(this.active,this.activeTo)}addActive(t){let e=0,{value:i,to:n,rank:s}=this.cursor;for(;e0;)e++;$t(this.active,e,i),$t(this.activeTo,e,n),$t(this.activeRank,e,s),t&&$t(t,e,this.cursor.from),this.minActive=Ut(this.active,this.activeTo)}next(){let t=this.to,e=this.point;this.point=null;let i=this.openStart<0?[]:null;for(;;){let n=this.minActive;if(n>-1&&(this.activeTo[n]-this.cursor.from||this.active[n].endSide-this.cursor.startSide)<0){if(this.activeTo[n]>t){this.to=this.activeTo[n],this.endSide=this.active[n].endSide;break}this.removeActive(n),i&&_t(i,n)}else{if(!this.cursor.value){this.to=this.endSide=1e9;break}if(this.cursor.from>t){this.to=this.cursor.from,this.endSide=this.cursor.startSide;break}{let t=this.cursor.value;if(t.point){if(!(e&&this.cursor.to==this.to&&this.cursor.from=0&&i[e]=0&&!(this.activeRank[i]t||this.activeTo[i]==t&&this.active[i].endSide>=this.point.endSide)&&e.push(this.active[i]);return e.reverse()}openEnd(t){let e=0;for(let i=this.activeTo.length-1;i>=0&&this.activeTo[i]>t;i--)e++;return e}}function Ft(t,e,i,n,s,r){t.goto(e),i.goto(n);let o=n+s,l=n,a=n-e,h=!!r.boundChange;for(let e=!1;;){let n=t.to+a-i.to,s=n||t.endSide-i.endSide,c=s<0?t.to+a:i.to,u=Math.min(c,o);if(t.point||i.point?(t.point&&i.point&&Rt(t.point,i.point)&&qt(t.activeForPoint(t.to),i.activeForPoint(i.to))||r.comparePoint(l,u,t.point,i.point),e=!1):(e&&r.boundChange(l),u>l&&!qt(t.active,i.active)&&r.compareRange(l,u,t.active,i.active),h&&uo)break;l=c,s<=0&&t.next(),s>=0&&i.next()}}function qt(t,e){if(t.length!=e.length)return!1;for(let i=0;i=e;i--)t[i+1]=t[i];t[e]=i}function Ut(t,e){let i=-1,n=1e9;for(let s=0;s=e)return n;if(n==t.length)break;s+=9==t.charCodeAt(n)?i-s%i:1,n=x(t,n)}return!0===n?-1:t.length}const Kt="undefined"==typeof Symbol?"__ͼ":Symbol.for("ͼ"),Xt="undefined"==typeof Symbol?"__styleSet"+Math.floor(1e8*Math.random()):Symbol("styleSet"),Gt="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:{};class Yt{constructor(t,e){this.rules=[];let{finish:i}=e||{};function n(t){return/^@/.test(t)?[t]:t.split(/,\s*/)}function s(t,e,r,o){let l=[],a=/^@(\w+)\b/.exec(t[0]),h=a&&"keyframes"==a[1];if(a&&null==e)return r.push(t[0]+";");for(let i in e){let o=e[i];if(/&/.test(i))s(i.split(/,\s*/).map(e=>t.map(t=>e.replace(/&/,t))).reduce((t,e)=>t.concat(e)),o,r);else if(o&&"object"==typeof o){if(!a)throw new RangeError("The value of a property ("+i+") should be a primitive value.");s(n(i),o,l,h)}else null!=o&&l.push(i.replace(/_.*/,"").replace(/[A-Z]/g,t=>"-"+t.toLowerCase())+": "+o+";")}(l.length||h)&&r.push((!i||a||o?t:t.map(i)).join(", ")+" {"+l.join(" ")+"}")}for(let e in t)s(n(e),t[e],this.rules)}getRules(){return this.rules.join("\n")}static newName(){let t=Gt[Kt]||1;return Gt[Kt]=t+1,"ͼ"+t.toString(36)}static mount(t,e,i){let n=t[Xt],s=i&&i.nonce;n?s&&n.setNonce(s):n=new Zt(t,s),n.mount(Array.isArray(e)?e:[e],t)}}let Jt=new Map;class Zt{constructor(t,e){let i=t.ownerDocument||t,n=i.defaultView;if(!t.head&&t.adoptedStyleSheets&&n.CSSStyleSheet){let e=Jt.get(i);if(e)return t[Xt]=e;this.sheet=new n.CSSStyleSheet,Jt.set(i,this)}else this.styleTag=i.createElement("style"),e&&this.styleTag.setAttribute("nonce",e);this.modules=[],t[Xt]=this}mount(t,e){let i=this.sheet,n=0,s=0;for(let e=0;e-1&&(this.modules.splice(o,1),s--,o=-1),-1==o){if(this.modules.splice(s++,0,r),i)for(let t=0;t",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'},ie="undefined"!=typeof navigator&&/Mac/.test(navigator.platform),ne="undefined"!=typeof navigator&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),se=0;se<10;se++)te[48+se]=te[96+se]=String(se);for(se=1;se<=24;se++)te[se+111]="F"+se;for(se=65;se<=90;se++)te[se]=String.fromCharCode(se+32),ee[se]=String.fromCharCode(se);for(var re in te)ee.hasOwnProperty(re)||(ee[re]=te[re]);function oe(){var t=arguments[0];"string"==typeof t&&(t=document.createElement(t));var e=1,i=arguments[1];if(i&&"object"==typeof i&&null==i.nodeType&&!Array.isArray(i)){for(var n in i)if(Object.prototype.hasOwnProperty.call(i,n)){var s=i[n];"string"==typeof s?t.setAttribute(n,s):null!=s&&(t[n]=s)}e++}for(;e2);var be={mac:we||/Mac/.test(ae.platform),windows:/Win/.test(ae.platform),linux:/Linux|X11/.test(ae.platform),ie:de,ie_version:ue?he.documentMode||6:fe?+fe[1]:ce?+ce[1]:0,gecko:pe,gecko_version:pe?+(/Firefox\/(\d+)/.exec(ae.userAgent)||[0,0])[1]:0,chrome:!!me,chrome_version:me?+me[1]:0,ios:we,android:/Android\b/.test(ae.userAgent),webkit:ge,webkit_version:ge?+(/\bAppleWebKit\/(\d+)/.exec(ae.userAgent)||[0,0])[1]:0,safari:ve,safari_version:ve?+(/\bVersion\/(\d+(\.\d+)?)/.exec(ae.userAgent)||[0,0])[1]:0,tabSize:null!=he.documentElement.style.tabSize?"tab-size":"-moz-tab-size"};function ye(t,e){for(let i in t)"class"==i&&e.class?e.class+=" "+t.class:"style"==i&&e.style?e.style+=";"+t.style:e[i]=t[i];return e}const xe=Object.create(null);function ke(t,e,i){if(t==e)return!0;t||(t=xe),e||(e=xe);let n=Object.keys(t),s=Object.keys(e);if(n.length-(i&&n.indexOf(i)>-1?1:0)!=s.length-(i&&s.indexOf(i)>-1?1:0))return!1;for(let r of n)if(r!=i&&(-1==s.indexOf(r)||t[r]!==e[r]))return!1;return!0}function Se(t,e,i){let n=!1;if(e)for(let s in e)i&&s in i||(n=!0,"style"==s?t.style.cssText="":t.removeAttribute(s));if(i)for(let s in i)e&&e[s]==i[s]||(n=!0,"style"==s?t.style.cssText=i[s]:t.setAttribute(s,i[s]));return n}function Ce(t){let e=Object.create(null);for(let i=0;i0?3e8:-4e8:e>0?1e8:-1e8,new Re(t,e,e,i,t.widget||null,!1)}static replace(t){let e,i,n=!!t.block;if(t.isBlockGap)e=-5e8,i=4e8;else{let{start:s,end:r}=Pe(t,n);e=(s?n?-3e8:-1:5e8)-1,i=1+(r?n?2e8:1:-6e8)}return new Re(t,e,i,n,t.widget||null,!0)}static line(t){return new De(t)}static set(t,e=!1){return Lt.of(t,e)}hasHeight(){return!!this.widget&&this.widget.estimatedHeight>-1}}Oe.none=Lt.empty;class Te extends Oe{constructor(t){let{start:e,end:i}=Pe(t);super(e?-1:5e8,i?1:-6e8,null,t),this.tagName=t.tagName||"span",this.attrs=t.class&&t.attributes?ye(t.attributes,{class:t.class}):t.class?{class:t.class}:t.attributes||xe}eq(t){return this==t||t instanceof Te&&this.tagName==t.tagName&&ke(this.attrs,t.attrs)}range(t,e=t){if(t>=e)throw new RangeError("Mark decorations may not be empty");return super.range(t,e)}}Te.prototype.point=!1;class De extends Oe{constructor(t){super(-2e8,-2e8,null,t)}eq(t){return t instanceof De&&this.spec.class==t.spec.class&&ke(this.spec.attributes,t.spec.attributes)}range(t,e=t){if(e!=t)throw new RangeError("Line decoration ranges must be zero-length");return super.range(t,e)}}De.prototype.mapMode=M.TrackBefore,De.prototype.point=!0;class Re extends Oe{constructor(t,e,i,n,s,r){super(e,i,s,t),this.block=n,this.isReplace=r,this.mapMode=n?e<=0?M.TrackBefore:M.TrackAfter:M.TrackDel}get type(){return this.startSide!=this.endSide?Me.WidgetRange:this.startSide<=0?Me.WidgetBefore:Me.WidgetAfter}get heightRelevant(){return this.block||!!this.widget&&(this.widget.estimatedHeight>=5||this.widget.lineBreaks>0)}eq(t){return t instanceof Re&&(e=this.widget,i=t.widget,e==i||!!(e&&i&&e.compare(i)))&&this.block==t.block&&this.startSide==t.startSide&&this.endSide==t.endSide;var e,i}range(t,e=t){if(this.isReplace&&(t>e||t==e&&this.startSide>0&&this.endSide<=0))throw new RangeError("Invalid range for replacement decoration");if(!this.isReplace&&e!=t)throw new RangeError("Widget decorations can only have zero-length ranges");return super.range(t,e)}}function Pe(t,e=!1){let{inclusiveStart:i,inclusiveEnd:n}=t;return null==i&&(i=t.inclusive),null==n&&(n=t.inclusive),{start:null!=i?i:e,end:null!=n?n:e}}function Be(t,e,i,n=0){let s=i.length-1;s>=0&&i[s]+n>=t?i[s]=Math.max(i[s],e):i.push(t,e)}Re.prototype.point=!0;class Ee extends Dt{constructor(t,e,i){super(),this.tagName=t,this.attributes=e,this.rank=i}eq(t){return t==this||t instanceof Ee&&this.tagName==t.tagName&&ke(this.attributes,t.attributes)}static create(t){return new Ee(t.tagName,t.attributes||xe,null==t.rank?50:Math.max(0,Math.min(t.rank,100)))}static set(t,e=!1){return Lt.of(t,e)}}function Le(t){let e;return e=11==t.nodeType?t.getSelection?t:t.ownerDocument:t,e.getSelection()}function Ie(t,e){return!!e&&(t==e||t.contains(1!=e.nodeType?e.parentNode:e))}function Ne(t,e){if(!e.anchorNode)return!1;try{return Ie(t,e.anchorNode)}catch(t){return!1}}function We(t){return 3==t.nodeType?Ye(t,0,t.nodeValue.length).getClientRects():1==t.nodeType?t.getClientRects():[]}function He(t,e,i,n){return!!i&&(Fe(t,e,i,n,-1)||Fe(t,e,i,n,1))}function Ve(t){for(var e=0;;e++)if(!(t=t.previousSibling))return e}function ze(t){return 1==t.nodeType&&/^(DIV|P|LI|UL|OL|BLOCKQUOTE|DD|DT|H\d|SECTION|PRE)$/.test(t.nodeName)}function Fe(t,e,i,n,s){for(;;){if(t==i&&e==n)return!0;if(e==(s<0?0:qe(t))){if("DIV"==t.nodeName)return!1;let i=t.parentNode;if(!i||1!=i.nodeType)return!1;e=Ve(t)+(s<0?0:1),t=i}else{if(1!=t.nodeType)return!1;if(1==(t=t.childNodes[e+(s<0?-1:0)]).nodeType&&"false"==t.contentEditable)return!1;e=s<0?qe(t):0}}}function qe(t){return 3==t.nodeType?t.nodeValue.length:t.childNodes.length}function _e(t,e){let{left:i,right:n}=t;if(i==n)return t;let s=e?i:n;return{left:s,right:s,top:t.top,bottom:t.bottom}}function $e(t){let e=t.visualViewport;return e?{left:0,right:e.width,top:0,bottom:e.height}:{left:0,right:t.innerWidth,top:0,bottom:t.innerHeight}}function Ue(t,e){let i=e.width/t.offsetWidth,n=e.height/t.offsetHeight;return(i>.995&&i<1.005||!isFinite(i)||Math.abs(e.width-t.offsetWidth)<1)&&(i=1),(n>.995&&n<1.005||!isFinite(n)||Math.abs(e.height-t.offsetHeight)<1)&&(n=1),{scaleX:i,scaleY:n}}function Qe(t,e=!0){let i=t.ownerDocument,n=null,s=null;for(let r=t.parentNode;r&&(r!=i.body&&(e&&!n||!s));)if(1==r.nodeType)!s&&r.scrollHeight>r.clientHeight&&(s=r),e&&!n&&r.scrollWidth>r.clientWidth&&(n=r),r=r.assignedSlot||r.parentNode;else{if(11!=r.nodeType)break;r=r.host}return{x:n,y:s}}Ee.prototype.startSide=Ee.prototype.endSide=-1;class je{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}eq(t){return this.anchorNode==t.anchorNode&&this.anchorOffset==t.anchorOffset&&this.focusNode==t.focusNode&&this.focusOffset==t.focusOffset}setRange(t){let{anchorNode:e,focusNode:i}=t;this.set(e,Math.min(t.anchorOffset,e?qe(e):0),i,Math.min(t.focusOffset,i?qe(i):0))}set(t,e,i,n){this.anchorNode=t,this.anchorOffset=e,this.focusNode=i,this.focusOffset=n}}let Ke,Xe=null;function Ge(t){if(t.setActive)return t.setActive();if(Xe)return t.focus(Xe);let e=[];for(let i=t;i&&(e.push(i,i.scrollTop,i.scrollLeft),i!=i.ownerDocument);i=i.parentNode);if(t.focus(null==Xe?{get preventScroll(){return Xe={preventScroll:!0},!0}}:void 0),!Xe){Xe=!1;for(let t=0;tMath.max(0,t.document.documentElement.scrollHeight-t.innerHeight-4):t.scrollTop>Math.max(1,t.scrollHeight-t.clientHeight-4)}function ti(t,e){for(let i=t,n=e;;){if(3==i.nodeType&&n>0)return{node:i,offset:n};if(1==i.nodeType&&n>0){if("false"==i.contentEditable)return null;i=i.childNodes[n-1],n=qe(i)}else{if(!i.parentNode||ze(i))return null;n=Ve(i),i=i.parentNode}}}function ei(t,e){for(let i=t,n=e;;){if(3==i.nodeType&&n=26&&(Xe=!1);class ii{constructor(t,e,i=!0){this.node=t,this.offset=e,this.precise=i}static before(t,e){return new ii(t.parentNode,Ve(t),e)}static after(t,e){return new ii(t.parentNode,Ve(t)+1,e)}}var ni=function(t){return t[t.LTR=0]="LTR",t[t.RTL=1]="RTL",t}(ni||(ni={}));const si=ni.LTR,ri=ni.RTL;function oi(t){let e=[];for(let i=0;i=e){if(o.level==i)return r;(s<0||(0!=n?n<0?o.frome:t[s].level>o.level))&&(s=r)}}if(s<0)throw new RangeError("Index out of range");return s}}function pi(t,e){if(t.length!=e.length)return!1;for(let i=0;ia&&o.push(new di(a,p.from,f)),vi(t,p.direction==si!=!(f%2)?n+1:n,s,p.inner,p.from,p.to,o),a=p.to}d=p.to}else{if(d==i||(e?mi[d]!=l:mi[d]==l))break;d++}u?gi(t,a,d,n+1,s,u,o):ae;){let i=!0,c=!1;if(!h||a>r[h-1].to){let t=mi[a-1];t!=l&&(i=!1,c=16==t)}let u=i||1!=l?null:[],f=i?n:n+1,d=a;t:for(;;)if(h&&d==r[h-1].to){if(c)break t;let p=r[--h];if(!i)for(let t=p.from,i=h;;){if(t==e)break t;if(!i||r[i-1].to!=t){if(mi[t-1]==l)break t;break}t=r[--i].from}if(u)u.push(p);else{p.to=0;t-=3)if(ci[t+1]==-i){let e=ci[t+2],i=2&e?s:4&e?1&e?r:s:0;i&&(mi[o]=mi[ci[t]]=i),l=t;break}}else{if(189==ci.length)break;ci[l++]=o,ci[l++]=e,ci[l++]=a}else if(2==(n=mi[o])||1==n){let t=n==s;a=t?0:1;for(let e=l-3;e>=0;e-=3){let i=ci[e+2];if(2&i)break;if(t)ci[e+2]|=2;else{if(4&i)break;ci[e+2]|=4}}}}}(t,s,r,n,l),function(t,e,i,n){for(let s=0,r=n;s<=i.length;s++){let o=s?i[s-1].to:t,l=sa;)e==r&&(e=i[--n].from,r=n?i[n-1].to:t),mi[--e]=c;a=o}else r=o,a++}}}(s,r,n,l),gi(t,s,r,e,i,n,o)}function wi(t){return[new di(0,t,0)]}let bi="";function yi(t,e,i,n,s){var r;let o=n.head-t.from,l=di.find(e,o,null!==(r=n.bidiLevel)&&void 0!==r?r:-1,n.assoc),a=e[l],h=a.side(s,i);if(o==h){let t=l+=s?1:-1;if(t<0||t>=e.length)return null;a=e[l=t],o=a.side(!s,i),h=a.side(s,i)}let c=x(t.text,o,a.forward(s,i));(ca.to)&&(c=h),bi=t.text.slice(Math.min(o,c),Math.max(o,c));let u=l==(s?e.length-1:0)?null:e[l+(s?1:-1)];return u&&c==h&&u.level+(s?0:1)t.some(t=>t)}),Bi=V.define({combine:t=>t.some(t=>t)}),Ei=V.define();class Li{constructor(t,e,i,n,s,r=!1){this.range=t,this.y=e,this.x=i,this.yMargin=n,this.xMargin=s,this.isSnapshot=r}map(t){return t.empty?this:new Li(this.range.map(t),this.y,this.x,this.yMargin,this.xMargin,this.isSnapshot)}clip(t){return this.range.to<=t.doc.length?this:new Li(N.cursor(t.doc.length),this.y,this.x,this.yMargin,this.xMargin,this.isSnapshot)}}const Ii=mt.define({map:(t,e)=>t.map(e)}),Ni=mt.define();function Wi(t,e,i){let n=t.facet(Ai);n.length?n[0](e):window.onerror&&window.onerror(String(e),i,void 0,void 0,e)||(i?console.error(i+":",e):console.error(e))}const Hi=V.define({combine:t=>!t.length||t[0]});let Vi=0;const zi=V.define({combine:t=>t.filter((e,i)=>{for(let n=0;n{let e=[];return r&&e.push(Ui.of(e=>{let i=e.plugin(t);return i?r(i):Oe.none})),s&&e.push(s(t)),e})}static fromClass(t,e){return Fi.define((e,i)=>new t(e,i),e)}}class qi{constructor(t){this.spec=t,this.mustUpdate=null,this.value=null}get plugin(){return this.spec&&this.spec.plugin}update(t){if(this.value){if(this.mustUpdate){let t=this.mustUpdate;if(this.mustUpdate=null,this.value.update)try{this.value.update(t)}catch(e){if(Wi(t.state,e,"CodeMirror plugin crashed"),this.value.destroy)try{this.value.destroy()}catch(t){}this.deactivate()}}}else if(this.spec)try{this.value=this.spec.plugin.create(t,this.spec.arg)}catch(e){Wi(t.state,e,"CodeMirror plugin crashed"),this.deactivate()}return this}destroy(t){var e;if(null===(e=this.value)||void 0===e?void 0:e.destroy)try{this.value.destroy()}catch(e){Wi(t.state,e,"CodeMirror plugin crashed")}}deactivate(){this.spec=this.value=null}}const _i=V.define(),$i=V.define(),Ui=V.define(),Qi=V.define(),ji=V.define(),Ki=V.define(),Xi=V.define();function Gi(t,e){let i=t.state.facet(Xi);if(!i.length)return i;let n=i.map(e=>e instanceof Function?e(t):e),s=[];return Lt.spans(n,e.from,e.to,{point(){},span(t,i,n,r){let o=t-e.from,l=i-e.from,a=s;for(let t=n.length-1;t>=0;t--,r--){let i,s=n[t].spec.bidiIsolate;if(null==s&&(s=xi(e.text,o,l)),r>0&&a.length&&(i=a[a.length-1]).to==o&&i.direction==s)i.to=l,a=i.inner;else{let t={from:o,to:l,direction:s,inner:[]};a.push(t),a=t.inner}}}}),s}const Yi=V.define();function Ji(t){let e=0,i=0,n=0,s=0;for(let r of t.state.facet(Yi)){let o=r(t);o&&(null!=o.left&&(e=Math.max(e,o.left)),null!=o.right&&(i=Math.max(i,o.right)),null!=o.top&&(n=Math.max(n,o.top)),null!=o.bottom&&(s=Math.max(s,o.bottom)))}return{left:e,right:i,top:n,bottom:s}}const Zi=V.define();class tn{constructor(t,e,i,n){this.fromA=t,this.toA=e,this.fromB=i,this.toB=n}join(t){return new tn(Math.min(this.fromA,t.fromA),Math.max(this.toA,t.toA),Math.min(this.fromB,t.fromB),Math.max(this.toB,t.toB))}addToSet(t){let e=t.length,i=this;for(;e>0;e--){let n=t[e-1];if(!(n.fromA>i.toA)){if(n.toAn.push(new tn(t,e,i,s))),this.changedRanges=n}static create(t,e,i){return new en(t,e,i)}get viewportChanged(){return(4&this.flags)>0}get viewportMoved(){return(8&this.flags)>0}get heightChanged(){return(2&this.flags)>0}get geometryChanged(){return this.docChanged||(18&this.flags)>0}get focusChanged(){return(1&this.flags)>0}get docChanged(){return!this.changes.empty}get selectionSet(){return this.transactions.some(t=>t.selection)}get empty(){return 0==this.flags&&0==this.transactions.length}}const nn=[];class sn{constructor(t,e,i=0){this.dom=t,this.length=e,this.flags=i,this.parent=null,t.cmTile=this}get breakAfter(){return 1&this.flags}get children(){return nn}isWidget(){return!1}get isHidden(){return!1}isComposite(){return!1}isLine(){return!1}isText(){return!1}isBlock(){return!1}get domAttrs(){return null}sync(t){if(this.flags|=2,4&this.flags){this.flags&=-5;let t=this.domAttrs;t&&function(t,e){for(let i=t.attributes.length-1;i>=0;i--){let n=t.attributes[i].name;null==e[n]&&t.removeAttribute(n)}for(let i in e){let n=e[i];"style"==i?t.style.cssText=n:t.getAttribute(i)!=n&&t.setAttribute(i,n)}}(this.dom,t)}}toString(){return this.constructor.name+(this.children.length?`(${this.children})`:"")+(this.breakAfter?"#":"")}destroy(){this.parent=null}setDOM(t){this.dom=t,t.cmTile=this}get posAtStart(){return this.parent?this.parent.posBefore(this):0}get posAtEnd(){return this.posAtStart+this.length}posBefore(t,e=this.posAtStart){let i=e;for(let e of this.children){if(e==t)return i;i+=e.length+e.breakAfter}throw new RangeError("Invalid child in posBefore")}posAfter(t){return this.posBefore(t)+t.length}covers(t){return!0}coordsIn(t,e,i){return null}domPosFor(t,e){let i=Ve(this.dom),n=this.length?t>0:e>0;return new ii(this.parent.dom,i+(n?1:0),0==t||t==this.length)}markDirty(t){this.flags&=-3,t&&(this.flags|=4),this.parent&&2&this.parent.flags&&this.parent.markDirty(!1)}get overrideDOMText(){return null}get root(){for(let t=this;t;t=t.parent)if(t instanceof ln)return t;return null}static get(t){return t.cmTile}}class rn extends sn{constructor(t){super(t,0),this._children=[]}isComposite(){return!0}get children(){return this._children}get lastChild(){return this.children.length?this.children[this.children.length-1]:null}append(t){this.children.push(t),t.parent=this}sync(t){if(2&this.flags)return;super.sync(t);let e,i=this.dom,n=null,s=(null==t?void 0:t.node)==i?t:null,r=0;for(let o of this.children){if(o.sync(t),r+=o.length+o.breakAfter,e=n?n.nextSibling:i.firstChild,s&&e!=o.dom&&(s.written=!0),o.dom.parentNode==i)for(;e&&e!=o.dom;)e=on(e);else i.insertBefore(o.dom,e);n=o.dom}for(e=n?n.nextSibling:i.firstChild,s&&e&&(s.written=!0);e;)e=on(e);this.length=r}}function on(t){let e=t.nextSibling;return t.parentNode.removeChild(t),e}class ln extends rn{constructor(t,e){super(e),this.view=t}owns(t){for(;t;t=t.parent)if(t==this)return!0;return!1}isBlock(){return!0}nearest(t){for(;;){if(!t)return null;let e=sn.get(t);if(e&&this.owns(e))return e;t=t.parentNode}}blockTiles(t){for(let e=[],i=this,n=0,s=0;;)if(n==i.children.length){if(!e.length)return;i=i.parent,i.breakAfter&&s++,n=e.pop()}else{let r=i.children[n++];if(r instanceof an)e.push(n),i=r,n=0;else{let e=s+r.length,i=t(r,s);if(void 0!==i)return i;s=e+r.breakAfter}}}resolveBlock(t,e){let i,n,s=-1,r=-1;if(this.blockTiles((o,l)=>{let a=l+o.length;if(t>=l&&t<=a){if(o.isWidget()&&e>=-1&&e<=1){if(32&o.flags)return!0;16&o.flags&&(i=void 0)}(lt||t==l&&(e>1?o.length:o.covers(-1)))&&(!n||!o.isWidget()&&n.isWidget())&&(n=o,r=t-l)}}),!i&&!n)throw new Error("No tile at position "+t);return i&&e<0||!n?{tile:i,offset:s}:{tile:n,offset:r}}}class an extends rn{constructor(t,e){super(t),this.wrapper=e}isBlock(){return!0}covers(t){return!!this.children.length&&(t<0?this.children[0].covers(-1):this.lastChild.covers(1))}get domAttrs(){return this.wrapper.attributes}static of(t,e){let i=new an(e||document.createElement(t.tagName),t);return e||(i.flags|=4),i}}class hn extends rn{constructor(t,e){super(t),this.attrs=e}isLine(){return!0}static start(t,e,i){let n=new hn(e||document.createElement("div"),t);return e&&i||(n.flags|=4),n}get domAttrs(){return this.attrs}resolveInline(t,e,i){let n=null,s=-1,r=null,o=-1;!function t(l,a){for(let h=0,c=0;h=a&&(u.isComposite()?t(u,a-c):(!r||r.isHidden&&(e>0&&!(32&r.flags)||i&&cn(r,u)))&&(f>a||32&u.flags)?(r=u,o=a-c):(cn&&(t=n);let s=t,r=t,o=0;0==t&&e<0||t==n&&e>=0?be.chrome||be.gecko||(t?(s--,o=1):r=0)?0:l.length-1];return be.safari&&!o&&0==a.width&&(a=Array.prototype.find.call(l,t=>t.width)||a),null==i?a:_e(a,(o?o>0:e<0)==i)}static of(t,e){let i=new fn(e||document.createTextNode(t),t);return e||(i.flags|=2),i}}class dn extends sn{constructor(t,e,i,n){super(t,e,n),this.widget=i}isWidget(){return!0}get isHidden(){return this.widget.isHidden}covers(t){return!(48&this.flags)&&(this.flags&(t<0?64:128))>0}coordsIn(t,e){return this.coordsInWidget(t,e,!1)}coordsInWidget(t,e,i){let n=this.widget.coordsAt(this.dom,t,e);if(n)return n;if(i)return _e(this.dom.getBoundingClientRect(),this.length?0==t:e<=0);{let e=this.dom.getClientRects(),i=null;if(!e.length)return null;let n=!!(16&this.flags)||!(32&this.flags)&&t>0;for(let s=n?e.length-1:0;i=e[s],!(t>0?0==s:s==e.length-1||i.top0==i)}}class mn{constructor(t){this.index=0,this.beforeBreak=!1,this.parents=[],this.tile=t}advance(t,e,i){let{tile:n,index:s,beforeBreak:r,parents:o}=this;for(;t||e>0;)if(n.isComposite())if(r){if(!t)break;i&&i.break(),t--,r=!1}else if(s==n.children.length){if(!t&&!o.length)break;i&&i.leave(n),r=!!n.breakAfter,({tile:n,index:s}=o.pop()),s++}else{let l=n.children[s],a=l.breakAfter;!(e>0?l.length<=t:l.length=0;t--){let i=e.marks[t],s=n.lastChild;if(s instanceof un&&s.mark.eq(i.mark))s.dom!=i.dom&&s.setDOM(Cn(i.dom)),n=s;else{if(this.cache.reused.get(i)){let t=sn.get(i.dom);t&&t.setDOM(Cn(i.dom))}let t=un.of(i.mark,i.dom);n.append(t),n=t}this.cache.reused.set(i,2)}let s=sn.get(t.text);s&&this.cache.reused.set(s,2);let r=new fn(t.text,t.text.nodeValue);r.flags|=8,this.pos=t.range.toB,n.append(r)}addInlineWidget(t,e,i){let n=this.afterWidget&&48&t.flags&&(48&this.afterWidget.flags)==(48&t.flags);n||this.flushBuffer();let s=this.ensureMarks(e,i);n||16&t.flags||s.append(this.getBuffer(1)),s.append(t),this.pos+=t.length,this.afterWidget=t}addMark(t,e,i){this.flushBuffer(),this.ensureMarks(e,i).append(t),this.pos+=t.length,this.afterWidget=null}addBlockWidget(t){this.getBlockPos().append(t),this.pos+=t.length,this.lastBlock=t,this.endLine()}continueWidget(t){(this.afterWidget||this.lastBlock).length+=t,this.pos+=t}addLineStart(t,e){var i;t||(t=Sn);let n=hn.start(t,e||(null===(i=this.cache.find(hn))||void 0===i?void 0:i.dom),!!e);this.getBlockPos().append(this.lastBlock=this.curLine=n)}addLine(t){this.getBlockPos().append(t),this.pos+=t.length,this.lastBlock=t,this.endLine()}addBreak(){this.lastBlock.flags|=1,this.endLine(),this.pos++}addLineStartIfNotCovered(t){this.blockPosCovered()||this.addLineStart(t)}ensureLine(t){this.curLine||this.addLineStart(t)}ensureMarks(t,e){var i;let n=this.curLine;for(let s=t.length-1;s>=0;s--){let r,o=t[s];if(e>0&&(r=n.lastChild)&&r instanceof un&&r.mark.eq(o))n=r,e--;else{let t=un.of(o,null===(i=this.cache.find(un,t=>t.mark.eq(o)))||void 0===i?void 0:i.dom);n.append(t),n=t,e=0}}return n}endLine(){if(this.curLine){this.flushBuffer();let t=this.curLine.lastChild;t&&kn(this.curLine,!1)&&("BR"==t.dom.nodeName||!t.isWidget()||be.ios&&kn(this.curLine,!0))||this.curLine.append(this.cache.findWidget(Mn,0,32)||new dn(Mn.toDOM(),0,Mn,32)),this.curLine=this.afterWidget=null}}updateBlockWrappers(){this.wrapperPos>this.pos+1e4&&(this.blockWrappers.goto(this.pos),this.wrappers.length=0);for(let t=this.wrappers.length-1;t>=0;t--)this.wrappers[t].to=this.pos){let e=102*t.rank+t.value.rank,i=new gn(t.from,t.to,t.value,e),n=this.wrappers.length;for(;n>0&&(this.wrappers[n-1].rank-i.rank||this.wrappers[n-1].to-i.to)<0;)n--;this.wrappers.splice(n,0,i)}this.wrapperPos=this.pos}getBlockPos(){var t;this.updateBlockWrappers();let e=this.root;for(let i of this.wrappers){let n=e.lastChild;if(i.fromt.wrapper.eq(i.wrapper)))||void 0===t?void 0:t.dom);e.append(n),e=n}}return e}blockPosCovered(){let t=this.lastBlock;return null!=t&&!t.breakAfter&&(!t.isWidget()||(160&t.flags)>0)}getBuffer(t){let e=2|(t<0?16:32),i=this.cache.find(pn,void 0,1);return i&&(i.flags=e),i||new pn(e)}flushBuffer(){!this.afterWidget||32&this.afterWidget.flags||(this.afterWidget.parent.append(this.getBuffer(-1)),this.afterWidget=null)}}class wn{constructor(t){this.skipCount=0,this.text="",this.textOff=0,this.cursor=t.iter()}skip(t){this.textOff+t<=this.text.length?this.textOff+=t:(this.skipCount+=t-(this.text.length-this.textOff),this.text="",this.textOff=0)}next(t){if(this.textOff==this.text.length){let{value:e,lineBreak:i,done:n}=this.cursor.next(this.skipCount);if(this.skipCount=0,n)throw new Error("Ran out of text content when drawing inline views");this.text=e;let s=this.textOff=Math.min(t,e.length);return i?null:e.slice(0,s)}let e=Math.min(this.text.length,this.textOff+t),i=this.text.slice(this.textOff,e);return this.textOff=e,i}}const bn=[dn,hn,fn,un,pn,an,ln];for(let t=0;t[]),this.index=bn.map(()=>0),this.reused=new Map}add(t){let e=t.constructor.bucket,i=this.buckets[e];i.length<6?i.push(t):i[this.index[e]=(this.index[e]+1)%6]=t}find(t,e,i=2){let n=t.bucket,s=this.buckets[n],r=this.index[n];for(let t=0;t{if(this.cache.add(t),t.isComposite())return!1},enter:t=>this.cache.add(t),leave:()=>{},break:()=>{}}}run(t,e){let i=e&&this.getCompositionContext(e.text);for(let n=0,s=0,r=0;;){let o=rn){let t=l-n;this.preserve(t,!r,!o),n=l,s+=t}if(!o)break;e&&o.fromA<=e.range.fromA&&o.toA>=e.range.toA?(this.forward(o.fromA,e.range.fromA,e.range.fromA1;i--){let n=i==t.parents.length?t.tile:t.parents[i].tile;n instanceof un&&e.push(n.mark)}return e}(this.old),s=this.openMarks;this.old.advance(t,i?1:-1,{skip:(t,e,i)=>{if(t.isWidget())if(this.openWidget)this.builder.continueWidget(i-e);else{let r=i>0||e{t.isLine()?this.builder.addLineStart(t.attrs,this.cache.maybeReuse(t)):(this.cache.add(t),t instanceof un&&n.unshift(t.mark)),this.openWidget=!1},leave:t=>{t.isLine()?n.length&&(n.length=s=0):t instanceof un&&(n.shift(),s=Math.min(s,n.length))},break:()=>{this.builder.addBreak(),this.openWidget=!1}}),this.text.skip(t)}emit(t,e){let i=null,n=this.builder,s=-1,r=Lt.spans(this.decorations,t,e,{point:(t,e,r,o,l,a)=>{if(r instanceof Re){if(this.disallowBlockEffectsFor[a]){if(r.block)throw new RangeError("Block decorations may not be specified via plugins");if(e>this.view.state.doc.lineAt(t).to)throw new RangeError("Decorations that replace line breaks may not be specified via plugins")}if(s=o.length,l>o.length)n.continueWidget(e-t);else{let s=r.widget||(r.block?An.block:An.inline),a=function(t){let e=t.isReplace?(t.startSide<0?64:0)|(t.endSide>0?128:0):t.startSide>0?32:16;t.block&&(e|=256);return e}(r),h=this.cache.findWidget(s,e-t,a)||dn.of(s,this.view,e-t,a);r.block?(r.startSide>0&&n.addLineStartIfNotCovered(i),n.addBlockWidget(h)):(n.ensureLine(i),n.addInlineWidget(h,o,l))}i=null}else i=function(t,e){let i=e.spec.attributes,n=e.spec.class;if(!i&&!n)return t;t||(t={class:"cm-line"});i&&ye(i,t);n&&(t.class+=" "+n);return t}(i,r);e>t&&this.text.skip(e-t)},span:(t,e,r,o)=>{for(let s=t;s-1&&(this.openWidget=r>s),this.openWidget||n.addLineStartIfNotCovered(i),this.openMarks=r}forward(t,e,i=1){e-t<=10?this.old.advance(e-t,i,this.reuseWalker):(this.old.advance(5,-1,this.reuseWalker),this.old.advance(e-t-10,-1),this.old.advance(5,i,this.reuseWalker))}getCompositionContext(t){let e=[],i=null;for(let n=t.parentNode;;n=n.parentNode){let t=sn.get(n);if(n==this.view.contentDOM)break;t instanceof un?e.push(t):(null==t?void 0:t.isLine())?i=t:t instanceof an||("DIV"!=n.nodeName||i||n==this.view.contentDOM?i||e.push(un.of(new Te({tagName:n.nodeName.toLowerCase(),attributes:Ce(n)}),n)):i=new hn(n,Sn))}return{line:i,marks:e}}}function kn(t,e){let i=t=>{for(let n of t.children)if((e?n.isText():n.length)||i(n))return!0;return!1};return i(t)}const Sn={class:"cm-line"};function Cn(t){let e=sn.get(t);return e&&e.setDOM(t.cloneNode()),t}class An extends Ae{constructor(t){super(),this.tag=t}eq(t){return t.tag==this.tag}toDOM(){return document.createElement(this.tag)}updateDOM(t){return t.nodeName.toLowerCase()==this.tag}get isHidden(){return!0}}An.inline=new An("span"),An.block=new An("div");const Mn=new class extends Ae{toDOM(){return document.createElement("br")}get isHidden(){return!0}get editable(){return!0}};class On{constructor(t){this.view=t,this.decorations=[],this.blockWrappers=[],this.dynamicDecorationMap=[!1],this.domChanged=null,this.hasComposition=null,this.editContextFormatting=Oe.none,this.lastCompositionAfterCursor=!1,this.minWidth=0,this.minWidthFrom=0,this.minWidthTo=0,this.impreciseAnchor=null,this.impreciseHead=null,this.forceSelection=!1,this.lastUpdate=Date.now(),this.updateDeco(),this.tile=new ln(t,t.contentDOM),this.updateInner([new tn(0,0,0,t.state.doc.length)],null)}update(t){var e;let i=t.changedRanges;this.minWidth>0&&i.length&&(i.every(({fromA:t,toA:e})=>ethis.minWidthTo)?(this.minWidthFrom=t.changes.mapPos(this.minWidthFrom,1),this.minWidthTo=t.changes.mapPos(this.minWidthTo,1)):this.minWidth=this.minWidthFrom=this.minWidthTo=0),this.updateEditContextFormatting(t);let n=-1;this.view.inputState.composing>=0&&!this.view.observer.editContext&&((null===(e=this.domChanged)||void 0===e?void 0:e.newSel)?n=this.domChanged.newSel.head:function(t,e){let i=!1;e&&t.iterChangedRanges((t,n)=>{te.from&&(i=!0)});return i}(t.changes,this.hasComposition)||t.selectionSet||(n=t.state.selection.main.head));let s=n>-1?function(t,e,i){let n=Dn(t,i);if(!n)return null;let{node:s,from:r,to:o}=n,l=s.nodeValue;if(/[\n\r]/.test(l))return null;if(t.state.doc.sliceString(n.from,n.to)!=l)return null;let a=e.invertedDesc;return{range:new tn(a.mapPos(r),a.mapPos(o),r,o),text:s}}(this.view,t.changes,n):null;if(this.domChanged=null,this.hasComposition){let{from:e,to:n}=this.hasComposition;i=new tn(e,n,t.changes.mapPos(e,-1),t.changes.mapPos(n,1)).addToSet(i.slice())}this.hasComposition=s?{from:s.range.fromB,to:s.range.toB}:null,(be.ie||be.chrome)&&!s&&t&&t.state.doc.lines!=t.startState.doc.lines&&(this.forceSelection=!0);let r=this.decorations,o=this.blockWrappers;this.updateDeco();let l=function(t,e,i){let n=new Rn;return Lt.compare(t,e,i,n),n.changes}(r,this.decorations,t.changes);l.length&&(i=tn.extendWithRanges(i,l));let a=function(t,e,i){let n=new Pn;return Lt.compare(t,e,i,n),n.changes}(o,this.blockWrappers,t.changes);return a.length&&(i=tn.extendWithRanges(i,a)),s&&!i.some(t=>t.fromA<=s.range.fromA&&t.toA>=s.range.toA)&&(i=s.range.addToSet(i.slice())),!(2&this.tile.flags&&0==i.length)&&(this.updateInner(i,s),t.transactions.length&&(this.lastUpdate=Date.now()),!0)}updateInner(t,e){this.view.viewState.mustMeasureContent=!0;let{observer:i}=this.view;i.ignore(()=>{if(e||t.length){let i=this.tile,n=new xn(this.view,i,this.blockWrappers,this.decorations,this.dynamicDecorationMap);e&&sn.get(e.text)&&n.cache.reused.set(sn.get(e.text),2),this.tile=n.run(t,e),Tn(i,n.cache.reused)}this.tile.dom.style.height=this.view.viewState.contentHeight/this.view.scaleY+"px",this.tile.dom.style.flexBasis=this.minWidth?this.minWidth+"px":"";let n=be.chrome||be.ios?{node:i.selectionRange.focusNode,written:!1}:void 0;this.tile.sync(n),!n||!n.written&&i.selectionRange.focusNode==n.node&&this.tile.dom.contains(n.node)||(this.forceSelection=!0),this.tile.dom.style.height=""});let n=[];if(this.view.viewport.from||this.view.viewport.to-1)&&Ne(i,this.view.observer.selectionRange)&&!(n&&i.contains(n));if(!(s||e||r))return;let o=this.forceSelection;this.forceSelection=!1;let l,a,h=this.view.state.selection.main;if(h.empty?a=l=this.inlineDOMNearPos(h.anchor,h.assoc||1):(a=this.inlineDOMNearPos(h.head,h.head==h.from?1:-1),l=this.inlineDOMNearPos(h.anchor,h.anchor==h.from?1:-1)),be.gecko&&h.empty&&!this.hasComposition&&(1==(c=l).node.nodeType&&c.node.firstChild&&(0==c.offset||"false"==c.node.childNodes[c.offset-1].contentEditable)&&(c.offset==c.node.childNodes.length||"false"==c.node.childNodes[c.offset].contentEditable))){let t=document.createTextNode("");this.view.observer.ignore(()=>l.node.insertBefore(t,l.node.childNodes[l.offset]||null)),l=a=new ii(t,0),o=!0}var c;let u=this.view.observer.selectionRange;!o&&u.focusNode&&(He(l.node,l.offset,u.anchorNode,u.anchorOffset)&&He(a.node,a.offset,u.focusNode,u.focusOffset)||this.suppressWidgetCursorChange(u,h))||(this.view.observer.ignore(()=>{be.android&&be.chrome&&i.contains(u.focusNode)&&function(t,e){for(let i=t;i&&i!=e;i=i.assignedSlot||i.parentNode)if(1==i.nodeType&&"false"==i.contentEditable)return!0;return!1}(u.focusNode,i)&&(i.blur(),i.focus({preventScroll:!0}));let t=Le(this.view.root);if(t)if(h.empty){if(be.gecko){let t=(e=l.node,s=l.offset,1!=e.nodeType?0:(s&&"false"==e.childNodes[s-1].contentEditable?1:0)|(sh.head&&([l,a]=[a,l]),e.setEnd(a.node,a.offset),e.setStart(l.node,l.offset),t.removeAllRanges(),t.addRange(e)}else;var e,s;r&&this.view.root.activeElement==i&&(i.blur(),n&&n.focus())}),this.view.observer.setSelectionRange(l,a)),this.impreciseAnchor=l.precise?null:new ii(u.anchorNode,u.anchorOffset),this.impreciseHead=a.precise?null:new ii(u.focusNode,u.focusOffset)}suppressWidgetCursorChange(t,e){return this.hasComposition&&e.empty&&He(t.focusNode,t.focusOffset,t.anchorNode,t.anchorOffset)&&this.posFromDOM(t.focusNode,t.focusOffset)==e.head}enforceCursorAssoc(){if(this.hasComposition)return;let{view:t}=this,e=t.state.selection.main,i=Le(t.root),{anchorNode:n,anchorOffset:s}=t.observer.selectionRange;if(!(i&&e.empty&&e.assoc&&i.modify))return;let r=this.lineAt(e.head,e.assoc);if(!r)return;let o=r.posAtStart;if(e.head==o||e.head==o+r.length)return;let l=this.coordsAt(e.head,-1),a=this.coordsAt(e.head,1);if(!l||!a||l.bottom>a.top)return;let h=this.domAtPos(e.head+e.assoc,e.assoc);i.collapse(h.node,h.offset),i.modify("move",e.assoc<0?"forward":"backward","lineboundary"),t.observer.readSelectionRange();let c=t.observer.selectionRange;t.docView.posFromDOM(c.anchorNode,c.anchorOffset)!=e.from&&i.collapse(n,s)}posFromDOM(t,e){let i=this.tile.nearest(t);if(!i)return 2&this.tile.dom.compareDocumentPosition(t)?0:this.view.state.doc.length;let n=i.posAtStart;if(!i.isComposite())return i.isText()?t==i.dom?n+e:n+(e?i.length:0):n;{let s;if(t==i.dom)s=i.dom.childNodes[e];else{let n=0==qe(t)?0:0==e?-1:1;for(;;){let e=t.parentNode;if(e==i.dom)break;0==n&&e.firstChild!=e.lastChild&&(n=t==e.firstChild?-1:1),t=e}s=n<0?t:t.nextSibling}if(s==i.dom.firstChild)return n;for(;s&&!sn.get(s);)s=s.nextSibling;if(!s)return n+i.length;for(let t=0,e=n;;t++){let n=i.children[t];if(n.dom==s)return e;e+=n.length+n.breakAfter}}}domAtPos(t,e){let{tile:i,offset:n}=this.tile.resolveBlock(t,e);return i.isWidget()?i.domPosFor(n,e):i.domIn(n,e)}inlineDOMNearPos(t,e){let i,n,s=-1,r=!1,o=-1,l=!1;return this.tile.blockTiles((e,a)=>{if(e.isWidget()){if(32&e.flags&&a>=t)return!0;16&e.flags&&(r=!0)}else{let h=a+e.length;if(a<=t&&(i=e,s=t-a,r=h=t&&!n&&(n=e,o=t-a,l=a>t),a>t&&n)return!0}}),i||n?(r&&n?i=null:l&&i&&(n=null),i&&e<0||!n?i.domIn(s,e):n.domIn(o,e)):this.domAtPos(t,e)}coordsAt(t,e,i){let{tile:n,offset:s}=this.tile.resolveBlock(t,e);return n.isWidget()?n.widget instanceof Bn?null:n.coordsInWidget(s,e,!0):n.coordsIn(s,e,i)}lineAt(t,e){let{tile:i}=this.tile.resolveBlock(t,e);return i.isLine()?i:null}coordsForChar(t){let{tile:e,offset:i}=this.tile.resolveBlock(t,1);if(!e.isLine())return null;return function t(e,i){if(e.isComposite())for(let n of e.children){if(n.length>=i){let e=t(n,i);if(e)return e}if((i-=n.length)<0)break}else if(e.isText()&&iMath.max(this.view.scrollDOM.clientWidth,this.minWidth)+1,o=-1,l=this.view.textDirection==ni.LTR,a=0,h=(t,c,u)=>{for(let f=0;fn);f++){let n=t.children[f],d=c+n.length,p=n.dom.getBoundingClientRect(),{height:m}=p;if(u&&!f&&(a+=p.top-u.top),n instanceof an)d>i&&h(n,c,p);else if(c>=i&&(a>0&&e.push(-a),e.push(m+a),a=0,r)){let t=n.dom.lastChild,e=t?We(t):[];if(e.length){let t=e[e.length-1],i=l?t.right-p.left:p.right-t.left;i>o&&(o=i,this.minWidth=s,this.minWidthFrom=c,this.minWidthTo=d)}}u&&f==t.children.length-1&&(a+=u.bottom-p.bottom),c=d+n.breakAfter}};return h(this.tile,0,null),e}textDirectionAt(t){let{tile:e}=this.tile.resolveBlock(t,1);return"rtl"==getComputedStyle(e.dom).direction?ni.RTL:ni.LTR}measureTextSize(){let t=this.tile.blockTiles(t=>{if(t.isLine()&&t.children.length&&t.length<=20){let e,i=0;for(let n of t.children){if(!n.isText()||/[^ -~]/.test(n.text))return;let t=We(n.dom);if(1!=t.length)return;i+=t[0].width,e=t[0].height}if(i)return{lineHeight:t.dom.getBoundingClientRect().height,charWidth:i/t.length,textHeight:e}}});if(t)return t;let e,i,n,s=document.createElement("div");return s.className="cm-line",s.style.width="99999px",s.style.position="absolute",s.textContent="abc def ghi jkl mno pqr stu",this.view.observer.ignore(()=>{this.tile.dom.appendChild(s);let t=We(s.firstChild)[0];e=s.getBoundingClientRect().height,i=t&&t.width?t.width/27:7,n=t&&t.height?t.height:e,s.remove()}),{lineHeight:e,charWidth:i,textHeight:n}}computeBlockGapDeco(){let t=[],e=this.view.viewState;for(let i=0,n=0;;n++){let s=n==e.viewports.length?null:e.viewports[n],r=s?s.from-1:this.view.state.doc.length;if(r>i){let n=(e.lineBlockAt(r).bottom-e.lineBlockAt(i).top)/this.view.scaleY;t.push(Oe.replace({widget:new Bn(n),block:!0,inclusive:!0,isBlockGap:!0}).range(i,r))}if(!s)break;i=s.to+1}return Oe.set(t)}updateDeco(){let t=1,e=this.view.state.facet(Ui).map(e=>(this.dynamicDecorationMap[t++]="function"==typeof e)?e(this.view):e),i=!1,n=this.view.state.facet(ji).map((t,e)=>{let n="function"==typeof t;return n&&(i=!0),n?t(this.view):t});for(n.length&&(this.dynamicDecorationMap[t++]=i,e.push(Lt.join(n))),this.decorations=[this.editContextFormatting,...e,this.computeBlockGapDeco(),this.view.viewState.lineGapDeco];t"function"==typeof t?t(this.view):t)}scrollIntoView(t){if(t.isSnapshot){let e=this.view.viewState.lineBlockAt(t.range.head);return this.view.scrollDOM.scrollTop=e.top-t.yMargin,void(this.view.scrollDOM.scrollLeft=t.xMargin)}for(let e of this.view.state.facet(Ei))try{if(e(this.view,t.range,t))return!0}catch(t){Wi(this.view.state,t,"scroll handler")}let e,{range:i}=t,n=this.coordsAt(i.head,i.assoc||(i.head>i.anchor?-1:1));if(!n)return;!i.empty&&(e=this.coordsAt(i.anchor,i.anchor>i.head?-1:1))&&(n={left:Math.min(n.left,e.left),top:Math.min(n.top,e.top),right:Math.max(n.right,e.right),bottom:Math.max(n.bottom,e.bottom)});let s=Ji(this.view),r={left:n.left-s.left,top:n.top-s.top,right:n.right+s.right,bottom:n.bottom+s.bottom},{offsetWidth:o,offsetHeight:l}=this.view.scrollDOM;if(function(t,e,i,n,s,r,o,l){let a=t.ownerDocument,h=a.defaultView||window;for(let c=t,u=!1;c&&!u;)if(1==c.nodeType){let t,f=c==a.body,d=1,p=1;if(f)t=$e(h);else{if(/^(fixed|sticky)$/.test(getComputedStyle(c).position)&&(u=!0),c.scrollHeight<=c.clientHeight&&c.scrollWidth<=c.clientWidth){c=c.assignedSlot||c.parentNode;continue}let e=c.getBoundingClientRect();({scaleX:d,scaleY:p}=Ue(c,e)),t={left:e.left,right:e.left+c.clientWidth*d,top:e.top,bottom:e.top+c.clientHeight*p}}let m=0,g=0;if("nearest"==s)e.top0&&e.bottom>t.bottom+g&&(g=e.bottom-t.bottom+o)):e.bottom>t.bottom-o&&(g=e.bottom-t.bottom+o,i<0&&e.top-g0&&e.right>t.right+m&&(m=e.right-t.right+r)):e.right>t.right-r&&(m=e.right-t.right+r,i<0&&e.leftt.bottom||e.leftt.right)&&(e={left:Math.max(e.left,t.left),right:Math.min(e.right,t.right),top:Math.max(e.top,t.top),bottom:Math.min(e.bottom,t.bottom)}),c=c.assignedSlot||c.parentNode}else{if(11!=c.nodeType)break;c=c.host}}(this.view.scrollDOM,r,i.head1&&(n.top>window.pageYOffset+window.visualViewport.offsetTop+window.visualViewport.height||n.bottomt.isWidget()||t.children.some(e);return e(this.tile.resolveBlock(t,1).tile)}destroy(){Tn(this.tile)}}function Tn(t,e){let i=null==e?void 0:e.get(t);if(1!=i){null==i&&t.destroy();for(let i of t.children)Tn(i,e)}}function Dn(t,e){let i=t.observer.selectionRange;if(!i.focusNode)return null;let n=ti(i.focusNode,i.focusOffset),s=ei(i.focusNode,i.focusOffset),r=n||s;if(s&&n&&s.node!=n.node){let e=sn.get(s.node);if(!e||e.isText()&&e.text!=s.node.nodeValue)r=s;else if(t.docView.lastCompositionAfterCursor){let t=sn.get(n.node);!t||t.isText()&&t.text!=n.node.nodeValue||(r=s)}}if(t.docView.lastCompositionAfterCursor=r!=n,!r)return null;let o=e-r.offset;return{from:o,to:o+r.node.nodeValue.length,node:r.node}}let Rn=class{constructor(){this.changes=[]}compareRange(t,e){Be(t,e,this.changes)}comparePoint(t,e){Be(t,e,this.changes)}boundChange(t){Be(t,t,this.changes)}};class Pn{constructor(){this.changes=[]}compareRange(t,e){Be(t,e,this.changes)}comparePoint(){}boundChange(t){Be(t,t,this.changes)}}class Bn extends Ae{constructor(t){super(),this.height=t}toDOM(){let t=document.createElement("div");return t.className="cm-gap",this.updateDOM(t),t}eq(t){return t.height==this.height}updateDOM(t){return t.style.height=this.height+"px",!0}get editable(){return!0}get estimatedHeight(){return this.height}ignoreEvent(){return!1}}function En(t,e,i){let n=t.lineBlockAt(e);if(Array.isArray(n.type)){let t;for(let s of n.type){if(s.from>e)break;if(!(s.toe)return s;t&&(s.type!=Me.Text||t.type==s.type&&!(i<0?s.frome))||(t=s)}}return t||n}return n}function Ln(t,e,i,n){let s=t.state.doc.lineAt(e.head),r=t.bidiSpans(s),o=t.textDirectionAt(s.from);for(let l=e,a=null;;){let e=yi(s,r,o,l,i),h=bi;if(!e){if(s.number==(i?t.state.doc.lines:1))return l;h="\n",s=t.state.doc.line(s.number+(i?1:-1)),r=t.bidiSpans(s),e=t.visualLineSide(s,!i)}if(a){if(!a(h))return l}else{if(!n)return e;a=n(h)}l=e}}function In(t,e,i){for(;;){let n=0;for(let s of t)s.between(e-1,e+1,(t,s,r)=>{if(e>t&&ee(t)),i.from,e.head>i.from?-1:1);return n==i.from?i:N.cursor(n,nt.viewState.docHeight)return new Hn(t.state.doc.length,-1);if(s=t.elementAtHeight(h),null==n)break;if(s.type==Me.Text){if(n<0?s.tot.viewport.to)break;let e=t.docView.coordsAt(n<0?s.from:s.to,n>0?-1:1);if(e&&(n<0?e.top<=h+o:e.bottom>=h+o))break}let e=t.viewState.heightOracle.textHeight/2;h=n>0?s.bottom+e:s.top-e}if(t.viewport.from>=s.to||t.viewport.to<=s.from){if(i)return null;if(s.type==Me.Text){let e=function(t,e,i,n,s){let r=Math.round((n-e.left)*t.defaultCharacterWidth);if(t.lineWrapping&&i.height>1.5*t.defaultLineHeight){let e=t.viewState.heightOracle.textHeight;r+=Math.floor((s-i.top-.5*(t.defaultLineHeight-e))/e)*t.viewState.heightOracle.lineLength}let o=t.state.sliceDoc(i.from,i.to);return i.from+jt(o,r,t.state.tabSize)}(t,r,s,l,a);return new Hn(e,e==s.from?1:-1)}}if(s.type!=Me.Text)return h<(s.top+s.bottom)/2?new Hn(s.from,1):new Hn(s.to,-1);let c=t.docView.lineAt(s.from,2);return c&&c.length==s.length||(c=t.docView.lineAt(s.from,-2)),new zn(t,l,a,t.textDirectionAt(s.from)).scanTile(c,s.from)}class zn{constructor(t,e,i,n){this.view=t,this.x=e,this.y=i,this.baseDir=n,this.line=null,this.spans=null}bidiSpansAt(t){return(!this.line||this.line.from>t||this.line.to1||i.length&&(i[0].level!=this.baseDir||i[0].to+n.from>1;e:if(a.has(f)){let t=o+Math.floor(Math.random()*i);for(let e=0;e1)){if(i.bottomthis.y)(!s||s.top>i.top)&&(s=i),a=-1;else{let t=i.left>this.x?this.x-i.left:i.right(i+i+o)/3)return this.y=n.bottom-1,this.scan(t,e,!0);if(s&&s.top<(i+o+o)/3)return this.y=s.top+1,this.scan(t,e,!0)}let f=(h?this.dirAt(t[c],1):this.baseDir)==ni.LTR;return{i:c,after:this.x>(r.left+r.right)/2==f}}scanText(t,e){let i=[];for(let n=0;n{let s=i[n]-e,r=i[n+1]-e;return Ye(t.dom,s,r).getClientRects()});return n.after?new Hn(i[n.i+1],-1):new Hn(i[n.i],1)}scanTile(t,e){if(!t.length)return new Hn(e,1);if(1==t.children.length){let i=t.children[0];if(i.isText())return this.scanText(i,e);if(i.isComposite())return this.scanTile(i,e)}let i=[e];for(let n=0,s=e;n{let i=t.children[e];return 48&i.flags?null:(1==i.dom.nodeType?i.dom:Ye(i.dom,0,i.length)).getClientRects()}),s=t.children[n.i],r=i[n.i];return s.isText()?this.scanText(s,r):s.isComposite()?this.scanTile(s,r):n.after?new Hn(i[n.i+1],-1):new Hn(r,1)}}const Fn="￿";class qn{constructor(t,e){this.points=t,this.view=e,this.text="",this.lineSeparator=e.state.facet(Ot.lineSeparator)}append(t){this.text+=t}lineBreak(){this.text+=Fn}readRange(t,e){if(!t)return this;let i=t.parentNode;for(let n=t;;){this.findPointBefore(i,n);let t=this.text.length;this.readNode(n);let s=sn.get(n),r=n.nextSibling;if(r==e){(null==s?void 0:s.breakAfter)&&!r&&i!=this.view.contentDOM&&this.lineBreak();break}let o=sn.get(r);(s&&o?s.breakAfter:(s?s.breakAfter:ze(n))||ze(r)&&("BR"!=n.nodeName||(null==s?void 0:s.isWidget()))&&this.text.length>t)&&!$n(r,e)&&this.lineBreak(),n=r}return this.findPointBefore(i,e),this}readTextNode(t){let e=t.nodeValue;for(let i of this.points)i.node==t&&(i.pos=this.text.length+Math.min(i.offset,e.length));for(let i=0,n=this.lineSeparator?null:/\r\n?|\n/g;;){let s,r=-1,o=1;if(this.lineSeparator?(r=e.indexOf(this.lineSeparator,i),o=this.lineSeparator.length):(s=n.exec(e))&&(r=s.index,o=s[0].length),this.append(e.slice(i,r<0?e.length:r)),r<0)break;if(this.lineBreak(),o>1)for(let e of this.points)e.node==t&&e.pos>this.text.length&&(e.pos-=o-1);i=r+o}}readNode(t){let e=sn.get(t),i=e&&e.overrideDOMText;if(null!=i){this.findPointInside(t,i.length);for(let t=i.iter();!t.next().done;)t.lineBreak?this.lineBreak():this.append(t.value)}else 3==t.nodeType?this.readTextNode(t):"BR"==t.nodeName?t.nextSibling&&this.lineBreak():1==t.nodeType&&this.readRange(t.firstChild,null)}findPointBefore(t,e){for(let i of this.points)i.node==t&&t.childNodes[i.offset]==e&&(i.pos=this.text.length)}findPointInside(t,e){for(let i of this.points)(3==t.nodeType?i.node==t:t.contains(i.node))&&(i.pos=this.text.length+(_n(t,i.node,i.offset)?e:0))}}function _n(t,e,i){for(;;){if(!e||i-1;let{impreciseHead:s,impreciseAnchor:r}=t.docView,o=t.state.selection;if(t.state.readOnly&&e>-1)this.newSel=null;else if(e>-1&&(this.bounds=jn(t.docView.tile,e,i,0))){let e=s||r?[]:function(t){let e=[];if(t.root.activeElement!=t.contentDOM)return e;let{anchorNode:i,anchorOffset:n,focusNode:s,focusOffset:r}=t.observer.selectionRange;i&&(e.push(new Un(i,n)),s==i&&r==n||e.push(new Un(s,r)));return e}(t),i=new qn(e,t);i.readRange(this.bounds.startDOM,this.bounds.endDOM),this.text=i.text,this.newSel=function(t,e){if(0==t.length)return null;let i=t[0].pos,n=2==t.length?t[1].pos:i;return i>-1&&n>-1?N.single(i+e,n+e):null}(e,this.bounds.from)}else{let e=t.observer.selectionRange,i=s&&s.node==e.focusNode&&s.offset==e.focusOffset||!Ie(t.contentDOM,e.focusNode)?o.main.head:t.docView.posFromDOM(e.focusNode,e.focusOffset),n=r&&r.node==e.anchorNode&&r.offset==e.anchorOffset||!Ie(t.contentDOM,e.anchorNode)?o.main.anchor:t.docView.posFromDOM(e.anchorNode,e.anchorOffset),l=t.viewport;if((be.ios||be.chrome)&&i!=n&&Math.min(i,n)<=o.main.from&&Math.max(i,n)>=o.main.to&&(l.from>0||l.to-1&&o.ranges.length>1)this.newSel=o.replaceRange(N.range(n,i));else if(t.lineWrapping&&n==i&&(!o.main.empty||o.main.head!=i)&&t.inputState.lastTouchTime>Date.now()-100){let e=t.coordsAtPos(i,-1),n=0;e&&(n=t.inputState.lastTouchY<=e.bottom?-1:1),this.newSel=N.create([N.cursor(i,n)])}else this.newSel=N.single(n,i)}}}function jn(t,e,i,n){if(t.isComposite()){let s=-1,r=-1,o=-1,l=-1;for(let a=0,h=n,c=n;ai)return jn(n,e,i,h);if(u>=e&&-1==s&&(s=a,r=h),h>i&&n.dom.parentNode==t.dom){o=a,l=c;break}c=u,h=u+n.breakAfter}return{from:r,to:l<0?n+t.length:l,startDOM:(s?t.children[s-1].dom.nextSibling:null)||t.dom.firstChild,endDOM:o=0?t.children[o].dom:null}}return t.isText()?{from:n,to:n+t.length,startDOM:t.dom,endDOM:t.dom.nextSibling}:null}function Kn(t,e){let i,{newSel:n}=e,{state:s}=t,r=s.selection.main,o=t.inputState.lastKeyTime>Date.now()-100?t.inputState.lastKeyCode:-1;if(e.bounds){let{from:t,to:n}=e.bounds,l=r.from,a=null;(8===o||be.android&&e.text.length=t&&r.to<=n&&(e.typeOver||f!=e.text)&&f.slice(0,r.from-t)==e.text.slice(0,r.from-t)&&f.slice(r.to-t)==e.text.slice(h=e.text.length-(f.length-(r.to-t)))?i={from:r.from,to:r.to,insert:u.of(e.text.slice(r.from-t,h).split(Fn))}:(c=Gn(f,e.text,l-t,a))&&(be.chrome&&13==o&&c.toB==c.from+2&&e.text.slice(c.from,c.toB)==Fn+Fn&&c.toB--,i={from:t+c.from,to:t+c.toA,insert:u.of(e.text.slice(c.from,c.toB).split(Fn))})}else n&&(!t.hasFocus&&s.facet(Hi)||Yn(n,r))&&(n=null);if(!i&&!n)return!1;if((be.mac||be.android)&&i&&i.from==i.to&&i.from==r.head-1&&/^\. ?$/.test(i.insert.toString())&&"off"==t.contentDOM.getAttribute("autocorrect")?(n&&2==i.insert.length&&(n=N.single(n.main.anchor-1,n.main.head-1)),i={from:i.from,to:i.to,insert:u.of([i.insert.toString().replace("."," ")])}):s.doc.lineAt(r.from).toDate.now()-50?i={from:r.from,to:r.to,insert:s.toText(t.inputState.insertingText)}:be.chrome&&i&&i.from==i.to&&i.from==r.head&&"\n "==i.insert.toString()&&t.lineWrapping&&(n&&(n=N.single(n.main.anchor-1,n.main.head-1)),i={from:r.from,to:r.to,insert:u.of([" "])}),i)return Xn(t,i,n,o);if(n&&!Yn(n,r)){let e=!1,i="select";return t.inputState.lastSelectionTime>Date.now()-50&&("select"==t.inputState.lastSelectionOrigin&&(e=!0),i=t.inputState.lastSelectionOrigin,"select.pointer"==i&&(n=Nn(s.facet(Ki).map(e=>e(t)),n))),t.dispatch({selection:n,scrollIntoView:e,userEvent:i}),!0}return!1}function Xn(t,e,i,n=-1){if(be.ios&&t.inputState.flushIOSKey(e))return!0;let s=t.state.selection.main;if(be.android&&(e.to==s.to&&(e.from==s.from||e.from==s.from-1&&" "==t.state.sliceDoc(e.from,s.from))&&1==e.insert.length&&2==e.insert.lines&&Je(t.contentDOM,"Enter",13)||(e.from==s.from-1&&e.to==s.to&&0==e.insert.length||8==n&&e.insert.lengths.head)&&Je(t.contentDOM,"Backspace",8)||e.from==s.from&&e.to==s.to+1&&0==e.insert.length&&Je(t.contentDOM,"Delete",46)))return!0;let r,o=e.insert.toString();t.inputState.composing>=0&&t.inputState.composing++;let l=()=>r||(r=function(t,e,i){let n,s=t.state,r=s.selection.main,o=-1;if(e.from==e.to&&e.fromr.to){let i=e.frome(t)),n,i);e.from==l&&(o=l)}if(o>-1)n={changes:e,selection:N.cursor(e.from+e.insert.length,-1)};else if(e.from>=r.from&&e.to<=r.to&&e.to-e.from>=(r.to-r.from)/3&&(!i||i.main.empty&&i.main.from==e.from+e.insert.length)&&t.inputState.composing<0){let i=r.frome.to?s.sliceDoc(e.to,r.to):"";n=s.replaceSelection(t.state.toText(i+e.insert.sliceString(0,void 0,t.state.lineBreak)+o))}else{let o=s.changes(e),l=i&&i.main.to<=o.newLength?i.main:void 0;if(s.selection.ranges.length>1&&(t.inputState.composing>=0||t.inputState.compositionPendingChange)&&e.to<=r.to+10&&e.to>=r.to-10){let a,h=t.state.sliceDoc(e.from,e.to),c=i&&Dn(t,i.main.head);if(c){let t=e.insert.length-(e.to-e.from);a={from:c.from,to:c.to-t}}else a=t.state.doc.lineAt(r.head);let u=r.to-e.to;n=s.changeByRange(i=>{if(i.from==r.from&&i.to==r.to)return{changes:o,range:l||i.map(o)};let n=i.to-u,c=n-h.length;if(t.state.sliceDoc(c,n)!=h||n>=a.from&&c<=a.to)return{range:i};let f=s.changes({from:c,to:n,insert:e.insert}),d=i.to-r.to;return{changes:f,range:l?N.range(Math.max(0,l.anchor+d),Math.max(0,l.head+d)):i.map(f)}})}else n={changes:o,selection:l&&s.selection.replaceRange(l)}}let l="input.type";(t.composing||t.inputState.compositionPendingChange&&t.inputState.compositionEndedAt>Date.now()-50)&&(t.inputState.compositionPendingChange=!1,l+=".compose",t.inputState.compositionFirstChange&&(l+=".start",t.inputState.compositionFirstChange=!1));return s.update(n,{userEvent:l,scrollIntoView:!0})}(t,e,i));return t.state.facet(Oi).some(i=>i(t,e.from,e.to,o,l))||t.dispatch(l()),!0}function Gn(t,e,i,n){let s=Math.min(t.length,e.length),r=0;for(;r0&&l>0&&t.charCodeAt(o-1)==e.charCodeAt(l-1);)o--,l--;if("end"==n){i-=o+Math.max(0,r-Math.min(o,l))-r}if(o=o?r-i:0,l=r+(l-o),o=r}else if(l=l?r-i:0,o=r+(o-l),l=r}return{from:r,toA:o,toB:l}}function Yn(t,e){return e.head==t.main.head&&e.anchor==t.main.anchor}class Jn{setSelectionOrigin(t){this.lastSelectionOrigin=t,this.lastSelectionTime=Date.now()}constructor(t){this.view=t,this.lastKeyCode=0,this.lastKeyTime=0,this.touchActive=!1,this.lastTouchTime=0,this.lastTouchX=0,this.lastTouchY=0,this.lastFocusTime=0,this.lastScrollTop=0,this.lastScrollLeft=0,this.lastWheelEvent=0,this.pendingIOSKey=void 0,this.lastIOSMomentumScroll=0,this.tabFocusMode=-1,this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastContextMenu=0,this.scrollHandlers=[],this.handlers=Object.create(null),this.composing=-1,this.compositionFirstChange=null,this.compositionEndedAt=0,this.compositionPendingKey=!1,this.compositionPendingChange=!1,this.insertingText="",this.insertingTextAt=0,this.mouseSelection=null,this.draggedContent=null,this.handleEvent=this.handleEvent.bind(this),this.notifiedFocused=t.hasFocus,be.safari&&t.contentDOM.addEventListener("input",()=>null),be.gecko&&function(t){ks.has(t)||(ks.add(t),t.addEventListener("copy",()=>{}),t.addEventListener("cut",()=>{}))}(t.contentDOM.ownerDocument)}handleEvent(t){(function(t,e){if(!e.bubbles)return!0;if(e.defaultPrevented)return!1;for(let i,n=e.target;n!=t.contentDOM;n=n.parentNode)if(!n||11==n.nodeType||(i=sn.get(n))&&i.isWidget()&&!i.isHidden&&i.widget.ignoreEvent(e))return!1;return!0})(this.view,t)&&!this.ignoreDuringComposition(t)&&("keydown"==t.type&&this.keydown(t)||(0!=this.view.updateState?Promise.resolve().then(()=>this.runHandlers(t.type,t)):this.runHandlers(t.type,t)))}runHandlers(t,e){let i=this.handlers[t];if(i){for(let t of i.observers)t(this.view,e);for(let t of i.handlers){if(e.defaultPrevented)break;if(t(this.view,e)){e.preventDefault();break}}}}ensureHandlers(t){let e=ts(t),i=this.handlers,n=this.view.contentDOM;for(let t in e)if("scroll"!=t){let s=!e[t].handlers.length,r=i[t];r&&s!=!r.handlers.length&&(n.removeEventListener(t,this.handleEvent),r=null),r||n.addEventListener(t,this.handleEvent,{passive:s})}for(let t in i)"scroll"==t||e[t]||n.removeEventListener(t,this.handleEvent);this.handlers=e}keydown(t){if(this.lastKeyCode=t.keyCode,this.lastKeyTime=Date.now(),9==t.keyCode&&this.tabFocusMode>-1&&(!this.tabFocusMode||Date.now()<=this.tabFocusMode))return!0;if(this.tabFocusMode>0&&27!=t.keyCode&&ns.indexOf(t.keyCode)<0&&(this.tabFocusMode=-1),be.android&&be.chrome&&!t.synthetic&&(13==t.keyCode||8==t.keyCode))return this.view.observer.delayAndroidKey(t.key,t.keyCode),!0;if(be.ios&&!t.synthetic&&!t.altKey&&!t.metaKey&&(es.some(e=>e.keyCode==t.keyCode)&&!t.ctrlKey||is.indexOf(t.key)>-1&&t.ctrlKey)){let i={ctrlKey:t.ctrlKey,altKey:t.altKey,metaKey:t.metaKey,shiftKey:t.shiftKey};return i.shiftKey&&be.ios&&!/^(off|none)$/.test(this.view.contentDOM.autocapitalize)&&((e=this.view.win).visualViewport&&e.visualViewport.height*e.visualViewport.scale/e.document.documentElement.clientHeight<.85)&&(i.shiftKey=!1),this.pendingIOSKey={key:t.key,keyCode:t.keyCode,mods:i},setTimeout(()=>this.flushIOSKey(),250),!0}var e;return 229!=t.keyCode&&this.view.observer.forceFlush(),!1}flushIOSKey(t){let e=this.pendingIOSKey;return!!e&&(!("Enter"==e.key&&t&&t.from0||!!(be.safari&&!be.ios&&this.compositionPendingKey&&Date.now()-this.compositionEndedAt<100)&&(this.compositionPendingKey=!1,!0))}startMouseSelection(t){this.mouseSelection&&this.mouseSelection.destroy(),this.mouseSelection=t}update(t){this.view.observer.update(t),this.mouseSelection&&this.mouseSelection.update(t),this.draggedContent&&t.docChanged&&(this.draggedContent=this.draggedContent.map(t.changes)),t.transactions.length&&(this.lastKeyCode=this.lastSelectionTime=0)}destroy(){this.mouseSelection&&this.mouseSelection.destroy()}}function Zn(t,e){return(i,n)=>{try{return e.call(t,n,i)}catch(t){Wi(i.state,t)}}}function ts(t){let e=Object.create(null);function i(t){return e[t]||(e[t]={observers:[],handlers:[]})}for(let e of t){let t=e.spec,n=t&&t.plugin.domEventHandlers,s=t&&t.plugin.domEventObservers;if(n)for(let t in n){let s=n[t];s&&i(t).handlers.push(Zn(e.value,s))}if(s)for(let t in s){let n=s[t];n&&i(t).observers.push(Zn(e.value,n))}}for(let t in os)i(t).handlers.push(os[t]);for(let t in ls)i(t).observers.push(ls[t]);return e}const es=[{key:"Backspace",keyCode:8,inputType:"deleteContentBackward"},{key:"Enter",keyCode:13,inputType:"insertParagraph"},{key:"Enter",keyCode:13,inputType:"insertLineBreak"},{key:"Delete",keyCode:46,inputType:"deleteContentForward"}],is="dthko",ns=[16,17,18,20,91,92,224,225];function ss(t){return.7*Math.max(0,t)+8}class rs{constructor(t,e,i,n){this.view=t,this.startEvent=e,this.style=i,this.mustSelect=n,this.scrollSpeed={x:0,y:0},this.scrolling=-1,this.lastEvent=e,this.scrollParents=Qe(t.contentDOM),this.atoms=t.state.facet(Ki).map(e=>e(t));let s=t.contentDOM.ownerDocument;s.addEventListener("mousemove",this.move=this.move.bind(this)),s.addEventListener("mouseup",this.up=this.up.bind(this)),this.extend=e.shiftKey,this.multiple=t.state.facet(Ot.allowMultipleSelections)&&function(t,e){let i=t.state.facet(ki);return i.length?i[0](e):be.mac?e.metaKey:e.ctrlKey}(t,e),this.dragging=!(!function(t,e){let{main:i}=t.state.selection;if(i.empty)return!1;let n=Le(t.root);if(!n||0==n.rangeCount)return!0;let s=n.getRangeAt(0).getClientRects();for(let t=0;t=e.clientX&&i.top<=e.clientY&&i.bottom>=e.clientY)return!0}return!1}(t,e)||1!=gs(e))&&null}start(t){!1===this.dragging&&this.select(t)}move(t){if(0==t.buttons)return this.destroy();if(this.dragging||null==this.dragging&&(e=this.startEvent,i=t,Math.max(Math.abs(e.clientX-i.clientX),Math.abs(e.clientY-i.clientY))<10))return;var e,i;this.select(this.lastEvent=t);let n=0,s=0,r=0,o=0,l=this.view.win.innerWidth,a=this.view.win.innerHeight;this.scrollParents.x&&({left:r,right:l}=this.scrollParents.x.getBoundingClientRect()),this.scrollParents.y&&({top:o,bottom:a}=this.scrollParents.y.getBoundingClientRect());let h=Ji(this.view);t.clientX-h.left<=r+6?n=-ss(r-t.clientX):t.clientX+h.right>=l-6&&(n=ss(t.clientX-l)),t.clientY-h.top<=o+6?s=-ss(o-t.clientY):t.clientY+h.bottom>=a-6&&(s=ss(t.clientY-a)),this.setScrollSpeed(n,s)}up(t){null==this.dragging&&this.select(this.lastEvent),this.dragging||t.preventDefault(),this.destroy()}destroy(){this.setScrollSpeed(0,0);let t=this.view.contentDOM.ownerDocument;t.removeEventListener("mousemove",this.move),t.removeEventListener("mouseup",this.up),this.view.inputState.mouseSelection=this.view.inputState.draggedContent=null}setScrollSpeed(t,e){this.scrollSpeed={x:t,y:e},t||e?this.scrolling<0&&(this.scrolling=setInterval(()=>this.scroll(),50)):this.scrolling>-1&&(clearInterval(this.scrolling),this.scrolling=-1)}scroll(){let{x:t,y:e}=this.scrollSpeed;t&&this.scrollParents.x&&(this.scrollParents.x.scrollLeft+=t,t=0),e&&this.scrollParents.y&&(this.scrollParents.y.scrollTop+=e,e=0),(t||e)&&this.view.win.scrollBy(t,e),!1===this.dragging&&this.select(this.lastEvent)}select(t){let{view:e}=this,i=Nn(this.atoms,this.style.get(t,this.extend,this.multiple));!this.mustSelect&&i.eq(e.state.selection,!1===this.dragging)||this.view.dispatch({selection:i,userEvent:"select.pointer"}),this.mustSelect=!1}update(t){t.transactions.some(t=>t.isUserEvent("input.type"))?this.destroy():this.style.update(t)&&setTimeout(()=>this.select(this.lastEvent),20)}}const os=Object.create(null),ls=Object.create(null),as=be.ie&&be.ie_version<15||be.ios&&be.webkit_version<604;function hs(t,e,i){for(let n of t.facet(e))i=n(i,t);return i}function cs(t,e){e=hs(t.state,Di,e);let i,{state:n}=t,s=1,r=n.toText(e),o=r.lines==n.selection.ranges.length;if(null!=ws&&n.selection.ranges.every(t=>t.empty)&&ws==r.toString()){let t=-1;i=n.changeByRange(i=>{let l=n.doc.lineAt(i.from);if(l.from==t)return{range:i};t=l.from;let a=n.toText((o?r.line(s++).text:e)+n.lineBreak);return{changes:{from:l.from,insert:a},range:N.cursor(i.from+a.length)}})}else i=o?n.changeByRange(t=>{let e=r.line(s++);return{changes:{from:t.from,to:t.to,insert:e.text},range:N.cursor(t.from+e.length)}}):n.replaceSelection(r);t.dispatch(i,{userEvent:"input.paste",scrollIntoView:!0})}function us(t,e,i,n){if(1==n)return N.cursor(e,i);if(2==n)return function(t,e,i=1){let n=t.charCategorizer(e),s=t.doc.lineAt(e),r=e-s.from;if(0==s.length)return N.cursor(e);0==r?i=1:r==s.length&&(i=-1);let o=r,l=r;i<0?o=x(s.text,r,!1):l=x(s.text,r);let a=n(s.text.slice(o,l));for(;o>0;){let t=x(s.text,o,!1);if(n(s.text.slice(t,o))!=a)break;o=t}for(;l{let e=t.inputState;e.lastScrollTop=t.scrollDOM.scrollTop,e.lastScrollLeft=t.scrollDOM.scrollLeft,be.ios&&!e.touchActive&&(e.lastIOSMomentumScroll=Date.now())},ls.wheel=ls.mousewheel=t=>{t.inputState.lastWheelEvent=Date.now()},os.keydown=(t,e)=>(t.inputState.setSelectionOrigin("select"),27==e.keyCode&&0!=t.inputState.tabFocusMode&&(t.inputState.tabFocusMode=Date.now()+2e3),!1),ls.touchstart=(t,e)=>{let i=t.inputState,n=e.targetTouches[0];i.touchActive=!0,i.lastTouchTime=Date.now(),n&&(i.lastTouchX=n.clientX,i.lastTouchY=n.clientY),i.setSelectionOrigin("select.pointer")},ls.touchmove=t=>{t.inputState.setSelectionOrigin("select.pointer")},ls.touchend=(t,e)=>{t.inputState.touchActive=!1},os.mousedown=(t,e)=>{if(t.observer.flush(),t.inputState.lastTouchTime>Date.now()-2e3)return!1;let i=null;for(let n of t.state.facet(Ci))if(i=n(t,e),i)break;if(i||0!=e.button||(i=function(t,e){let i=t.posAndSideAtCoords({x:e.clientX,y:e.clientY},!1),n=gs(e),s=t.state.selection;return{update(t){t.docChanged&&(i.pos=t.changes.mapPos(i.pos),s=s.map(t.changes))},get(e,r,o){let l,a=t.posAndSideAtCoords({x:e.clientX,y:e.clientY},!1),h=us(t,a.pos,a.assoc,n);if(i.pos!=a.pos&&!r){let e=us(t,i.pos,i.assoc,n),s=Math.min(e.from,h.from),r=Math.max(e.to,h.to);h=s1&&(l=function(t,e){for(let i=0;i=e)return N.create(t.ranges.slice(0,i).concat(t.ranges.slice(i+1)),t.mainIndex==i?0:t.mainIndex-(t.mainIndex>i?1:0))}return null}(s,a.pos))?l:o?s.addRange(h):N.create([h])}}}(t,e)),i){let n=!t.hasFocus;t.inputState.startMouseSelection(new rs(t,e,i,n)),n&&t.observer.ignore(()=>{Ge(t.contentDOM);let e=t.root.activeElement;e&&!e.contains(t.contentDOM)&&e.blur()});let s=t.inputState.mouseSelection;if(s)return s.start(e),!1===s.dragging}else t.inputState.setSelectionOrigin("select.pointer");return!1};const fs=be.ie&&be.ie_version<=11;let ds=null,ps=0,ms=0;function gs(t){if(!fs)return t.detail;let e=ds,i=ms;return ds=t,ms=Date.now(),ps=!e||i>Date.now()-400&&Math.abs(e.clientX-t.clientX)<2&&Math.abs(e.clientY-t.clientY)<2?(ps+1)%3:1}function vs(t,e,i,n){if(!(i=hs(t.state,Di,i)))return;let s=t.posAtCoords({x:e.clientX,y:e.clientY},!1),{draggedContent:r}=t.inputState,o=n&&r&&function(t,e){let i=t.state.facet(Si);return i.length?i[0](e):be.mac?!e.altKey:!e.ctrlKey}(t,e)?{from:r.from,to:r.to}:null,l={from:s,insert:i},a=t.state.changes(o?[o,l]:l);t.focus(),t.dispatch({changes:a,selection:{anchor:a.mapPos(s,-1),head:a.mapPos(s,1)},userEvent:o?"move.drop":"input.drop"}),t.inputState.draggedContent=null}os.dragstart=(t,e)=>{let{selection:{main:i}}=t.state;if(e.target.draggable){let n=t.docView.tile.nearest(e.target);if(n&&n.isWidget()){let t=n.posAtStart,e=t+n.length;(t>=i.to||e<=i.from)&&(i=N.undirectionalRange(t,e))}}let{inputState:n}=t;return n.mouseSelection&&(n.mouseSelection.dragging=!0),n.draggedContent=i,e.dataTransfer&&(e.dataTransfer.setData("Text",hs(t.state,Ri,t.state.sliceDoc(i.from,i.to))),e.dataTransfer.effectAllowed="copyMove"),!1},os.dragend=t=>(t.inputState.draggedContent=null,!1),os.drop=(t,e)=>{if(!e.dataTransfer)return!1;if(t.state.readOnly)return!0;let i=e.dataTransfer.files;if(i&&i.length){let n=Array(i.length),s=0,r=()=>{++s==i.length&&vs(t,e,n.filter(t=>null!=t).join(t.state.lineBreak),!1)};for(let t=0;t{/[\x00-\x08\x0e-\x1f]{2}/.test(e.result)||(n[t]=e.result),r()},e.readAsText(i[t])}return!0}{let i=e.dataTransfer.getData("Text");if(i)return vs(t,e,i,!0),!0}return!1},os.paste=(t,e)=>{if(t.state.readOnly)return!0;t.observer.flush();let i=as?null:e.clipboardData;return i?(cs(t,i.getData("text/plain")||i.getData("text/uri-list")),!0):(function(t){let e=t.dom.parentNode;if(!e)return;let i=e.appendChild(document.createElement("textarea"));i.style.cssText="position: fixed; left: -10000px; top: 10px",i.focus(),setTimeout(()=>{t.focus(),i.remove(),cs(t,i.value)},50)}(t),!1)};let ws=null;os.copy=os.cut=(t,e)=>{if(!Ne(t.contentDOM,t.observer.selectionRange))return!1;let{text:i,ranges:n,linewise:s}=function(t){let e=[],i=[],n=!1;for(let n of t.selection.ranges)n.empty||(e.push(t.sliceDoc(n.from,n.to)),i.push(n));if(!e.length){let s=-1;for(let{from:n}of t.selection.ranges){let r=t.doc.lineAt(n);r.number>s&&(e.push(r.text),i.push({from:r.from,to:Math.min(t.doc.length,r.to+1)})),s=r.number}n=!0}return{text:hs(t,Ri,e.join(t.lineBreak)),ranges:i,linewise:n}}(t.state);if(!i&&!s)return!1;ws=s?i:null,"cut"!=e.type||t.state.readOnly||t.dispatch({changes:n,scrollIntoView:!0,userEvent:"delete.cut"});let r=as?null:e.clipboardData;return r?(r.clearData(),r.setData("text/plain",i),!0):(function(t,e){let i=t.dom.parentNode;if(!i)return;let n=i.appendChild(document.createElement("textarea"));n.style.cssText="position: fixed; left: -10000px; top: 10px",n.value=e,n.focus(),n.selectionEnd=e.length,n.selectionStart=0,setTimeout(()=>{n.remove(),t.focus()},50)}(t,i),!1)};const bs=ft.define();function ys(t,e){let i=[];for(let n of t.facet(Ti)){let s=n(t,e);s&&i.push(s)}return i.length?t.update({effects:i,annotations:bs.of(!0)}):null}function xs(t){setTimeout(()=>{let e=t.hasFocus;if(e!=t.inputState.notifiedFocused){let i=ys(t.state,e);i?t.dispatch(i):t.update([])}},10)}ls.focus=t=>{t.inputState.lastFocusTime=Date.now(),t.scrollDOM.scrollTop||!t.inputState.lastScrollTop&&!t.inputState.lastScrollLeft||(t.scrollDOM.scrollTop=t.inputState.lastScrollTop,t.scrollDOM.scrollLeft=t.inputState.lastScrollLeft),xs(t)},ls.blur=t=>{t.observer.clearSelectionRange(),xs(t)},ls.compositionstart=ls.compositionupdate=t=>{t.observer.editContext||(null==t.inputState.compositionFirstChange&&(t.inputState.compositionFirstChange=!0),t.inputState.composing<0&&(t.inputState.composing=0))},ls.compositionend=t=>{t.observer.editContext||(t.inputState.composing=-1,t.inputState.compositionEndedAt=Date.now(),t.inputState.compositionPendingKey=!0,t.inputState.compositionPendingChange=t.observer.pendingRecords().length>0,t.inputState.compositionFirstChange=null,be.chrome&&be.android?t.observer.flushSoon():t.inputState.compositionPendingChange?Promise.resolve().then(()=>t.observer.flush()):setTimeout(()=>{t.inputState.composing<0&&t.docView.hasComposition&&t.update([])},50))},ls.contextmenu=t=>{t.inputState.lastContextMenu=Date.now()},os.beforeinput=(t,e)=>{var i,n;if("insertText"!=e.inputType&&"insertCompositionText"!=e.inputType||(t.inputState.insertingText=e.data,t.inputState.insertingTextAt=Date.now()),"insertReplacementText"==e.inputType&&t.observer.editContext){let n=null===(i=e.dataTransfer)||void 0===i?void 0:i.getData("text/plain"),s=e.getTargetRanges();if(n&&s.length){let e=s[0],i=t.posAtDOM(e.startContainer,e.startOffset),r=t.posAtDOM(e.endContainer,e.endOffset);return Xn(t,{from:i,to:r,insert:t.state.toText(n)},null),!0}}let s;if(be.chrome&&be.android&&(s=es.find(t=>t.inputType==e.inputType))&&(t.observer.delayAndroidKey(s.key,s.keyCode),"Backspace"==s.key||"Delete"==s.key)){let e=(null===(n=window.visualViewport)||void 0===n?void 0:n.height)||0;setTimeout(()=>{var i;((null===(i=window.visualViewport)||void 0===i?void 0:i.height)||0)>e+10&&t.hasFocus&&(t.contentDOM.blur(),t.focus())},100)}return be.ios&&"deleteContentForward"==e.inputType&&t.observer.flushSoon(),be.safari&&"insertText"==e.inputType&&t.inputState.composing>=0&&setTimeout(()=>ls.compositionend(t,e),20),!1};const ks=new Set;const Ss=["pre-wrap","normal","pre-line","break-spaces"];let Cs=!1;function As(){Cs=!1}class Ms{constructor(t){this.lineWrapping=t,this.doc=u.empty,this.heightSamples={},this.lineHeight=14,this.charWidth=7,this.textHeight=14,this.lineLength=30}heightForGap(t,e){let i=this.doc.lineAt(e).number-this.doc.lineAt(t).number+1;return this.lineWrapping&&(i+=Math.max(0,Math.ceil((e-t-i*this.lineLength*.5)/this.lineLength))),this.lineHeight*i}heightForLine(t){if(!this.lineWrapping)return this.lineHeight;return(1+Math.max(0,Math.ceil((t-this.lineLength)/Math.max(1,this.lineLength-5))))*this.lineHeight}setDoc(t){return this.doc=t,this}mustRefreshForWrapping(t){return Ss.indexOf(t)>-1!=this.lineWrapping}mustRefreshForHeights(t){let e=!1;for(let i=0;i-1,l=Math.abs(e-this.lineHeight)>.3||this.lineWrapping!=o;if(this.lineWrapping=o,this.lineHeight=e,this.charWidth=i,this.textHeight=n,this.lineLength=s,l){this.heightSamples={};for(let t=0;t0}set outdated(t){this.flags=(t?2:0)|-3&this.flags}setHeight(t){this.height!=t&&(Math.abs(this.height-t)>Rs&&(Cs=!0),this.height=t)}replace(t,e,i){return Ps.of(i)}decomposeLeft(t,e){e.push(this)}decomposeRight(t,e){e.push(this)}applyChanges(t,e,i,n){let s=this,r=i.doc;for(let o=n.length-1;o>=0;o--){let{fromA:l,toA:a,fromB:h,toB:c}=n[o],u=s.lineAt(l,Ds.ByPosNoHeight,i.setDoc(e),0,0),f=u.to>=a?u:s.lineAt(a,Ds.ByPosNoHeight,i,0,0);for(c+=f.to-a,a=f.to;o>0&&u.from<=n[o-1].toA;)l=n[o-1].fromA,h=n[o-1].fromB,o--,l2*s){let s=t[e-1];s.break?t.splice(--e,1,s.left,null,s.right):t.splice(--e,1,s.left,s.right),i+=1+s.break,n-=s.size}else{if(!(s>2*n))break;{let e=t[i];e.break?t.splice(i,1,e.left,null,e.right):t.splice(i,1,e.left,e.right),i+=2+e.break,s-=e.size}}else if(n=s&&r(this.lineAt(0,Ds.ByPos,i,n,s))}setMeasuredHeight(t){let e=t.heights[t.index++];e<0?(this.spaceAbove=-e,e=t.heights[t.index++]):this.spaceAbove=0,this.setHeight(e)}updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more&&this.setMeasuredHeight(n),this.outdated=!1,this}toString(){return`block(${this.length})`}}class Is extends Ls{constructor(t,e,i){super(t,e,null),this.collapsed=0,this.widgetHeight=0,this.breaks=0,this.spaceAbove=i}mainBlock(t,e){return new Ts(e,this.length,t+this.spaceAbove,this.height-this.spaceAbove,this.breaks)}replace(t,e,i){let n=i[0];return 1==i.length&&(n instanceof Is||n instanceof Ns&&4&n.flags)&&Math.abs(this.length-n.length)<10?(n instanceof Ns?n=new Is(n.length,this.height,this.spaceAbove):n.height=this.height,this.outdated||(n.outdated=!1),n):Ps.of(i)}updateHeight(t,e=0,i=!1,n){return n&&n.from<=e&&n.more?this.setMeasuredHeight(n):(i||this.outdated)&&(this.spaceAbove=0,this.setHeight(Math.max(this.widgetHeight,t.heightForLine(this.length-this.collapsed))+this.breaks*t.lineHeight)),this.outdated=!1,this}toString(){return`line(${this.length}${this.collapsed?-this.collapsed:""}${this.widgetHeight?":"+this.widgetHeight:""})`}}class Ns extends Ps{constructor(t){super(t,0)}heightMetrics(t,e){let i,n=t.doc.lineAt(e).number,s=t.doc.lineAt(e+this.length).number,r=s-n+1,o=0;if(t.lineWrapping){let e=Math.min(this.height,t.lineHeight*r);i=e/r,this.length>r+1&&(o=(this.height-e)/(this.length-r-1))}else i=this.height/r;return{firstLine:n,lastLine:s,perLine:i,perChar:o}}blockAt(t,e,i,n){let{firstLine:s,lastLine:r,perLine:o,perChar:l}=this.heightMetrics(e,n);if(e.lineWrapping){let s=n+(t0){let t=i[i.length-1];t instanceof Ns?i[i.length-1]=new Ns(t.length+n):i.push(null,new Ns(n-1))}if(t>0){let e=i[0];e instanceof Ns?i[0]=new Ns(t+e.length):i.unshift(new Ns(t-1),null)}return Ps.of(i)}decomposeLeft(t,e){e.push(new Ns(t-1),null)}decomposeRight(t,e){e.push(null,new Ns(this.length-t-1))}updateHeight(t,e=0,i=!1,n){let s=e+this.length;if(n&&n.from<=e+this.length&&n.more){let i=[],r=Math.max(e,n.from),o=-1;for(n.from>e&&i.push(new Ns(n.from-e-1).updateHeight(t,e));r<=s&&n.more;){let e=t.doc.lineAt(r).length;i.length&&i.push(null);let s=n.heights[n.index++],l=0;s<0&&(l=-s,s=n.heights[n.index++]),-1==o?o=s:Math.abs(s-o)>=Rs&&(o=-2);let a=new Is(e,s,l);a.outdated=!1,i.push(a),r+=e+1}r<=s&&i.push(null,new Ns(s-r).updateHeight(t,r));let l=Ps.of(i);return(o<0||Math.abs(l.height-this.height)>=Rs||Math.abs(o-this.heightMetrics(t,e).perLine)>=Rs)&&(Cs=!0),Bs(this,l)}return(i||this.outdated)&&(this.setHeight(t.heightForGap(e,e+this.length)),this.outdated=!1),this}toString(){return`gap(${this.length})`}}class Ws extends Ps{constructor(t,e,i){super(t.length+e+i.length,t.height+i.height,e|(t.outdated||i.outdated?2:0)),this.left=t,this.right=i,this.size=t.size+i.size}get break(){return 1&this.flags}blockAt(t,e,i,n){let s=i+this.left.height;return to))return a;let h=e==Ds.ByPosNoHeight?Ds.ByPosNoHeight:Ds.ByPos;return l?a.join(this.right.lineAt(o,h,i,r,o)):this.left.lineAt(o,h,i,n,s).join(a)}forEachLine(t,e,i,n,s,r){let o=n+this.left.height,l=s+this.left.length+this.break;if(this.break)t=l&&this.right.forEachLine(t,e,i,o,l,r);else{let a=this.lineAt(l,Ds.ByPos,i,n,s);t=t&&a.from<=e&&r(a),e>a.to&&this.right.forEachLine(a.to+1,e,i,o,l,r)}}replace(t,e,i){let n=this.left.length+this.break;if(ethis.left.length)return this.balanced(this.left,this.right.replace(t-n,e-n,i));let s=[];t>0&&this.decomposeLeft(t,s);let r=s.length;for(let t of i)s.push(t);if(t>0&&Hs(s,r-1),e=i&&e.push(null)),t>i&&this.right.decomposeLeft(t-i,e)}decomposeRight(t,e){let i=this.left.length,n=i+this.break;if(t>=n)return this.right.decomposeRight(t-n,e);t2*e.size||e.size>2*t.size?Ps.of(this.break?[t,null,e]:[t,e]):(this.left=Bs(this.left,t),this.right=Bs(this.right,e),this.setHeight(t.height+e.height),this.outdated=t.outdated||e.outdated,this.size=t.size+e.size,this.length=t.length+this.break+e.length,this)}updateHeight(t,e=0,i=!1,n){let{left:s,right:r}=this,o=e+s.length+this.break,l=null;return n&&n.from<=e+s.length&&n.more?l=s=s.updateHeight(t,e,i,n):s.updateHeight(t,e,i),n&&n.from<=o+r.length&&n.more?l=r=r.updateHeight(t,o,i,n):r.updateHeight(t,o,i),l?this.balanced(s,r):(this.height=this.left.height+this.right.height,this.outdated=!1,this)}toString(){return this.left+(this.break?" ":"-")+this.right}}function Hs(t,e){let i,n;null==t[e]&&(i=t[e-1])instanceof Ns&&(n=t[e+1])instanceof Ns&&t.splice(e-1,3,new Ns(i.length+1+n.length))}class Vs{constructor(t,e){this.pos=t,this.oracle=e,this.nodes=[],this.lineStart=-1,this.lineEnd=-1,this.covering=null,this.writtenTo=t}get isCovered(){return this.covering&&this.nodes[this.nodes.length-1]==this.covering}span(t,e){if(this.lineStart>-1){let t=Math.min(e,this.lineEnd),i=this.nodes[this.nodes.length-1];i instanceof Is?i.length+=t-this.pos:(t>this.pos||!this.isCovered)&&this.nodes.push(new Is(t-this.pos,-1,0)),this.writtenTo=t,e>t&&(this.nodes.push(null),this.writtenTo++,this.lineStart=-1)}this.pos=e}point(t,e,i){if(t=5)&&this.addLineDeco(n,s,r)}else e>t&&this.span(t,e);this.lineEnd>-1&&this.lineEnd-1)return;let{from:t,to:e}=this.oracle.doc.lineAt(this.pos);this.lineStart=t,this.lineEnd=e,this.writtenTot&&this.nodes.push(new Is(this.pos-t,-1,0)),this.writtenTo=this.pos}blankContent(t,e){let i=new Ns(e-t);return this.oracle.doc.lineAt(t).to==e&&(i.flags|=4),i}ensureLine(){this.enterLine();let t=this.nodes.length?this.nodes[this.nodes.length-1]:null;if(t instanceof Is)return t;let e=new Is(0,-1,0);return this.nodes.push(e),e}addBlock(t){this.enterLine();let e=t.deco;e&&e.startSide>0&&!this.isCovered&&this.ensureLine(),this.nodes.push(t),this.writtenTo=this.pos=this.pos+t.length,e&&e.endSide>0&&(this.covering=t)}addLineDeco(t,e,i){let n=this.ensureLine();n.length+=i,n.collapsed+=i,n.widgetHeight=Math.max(n.widgetHeight,t),n.breaks+=e,this.writtenTo=this.pos=this.pos+i}finish(t){let e=0==this.nodes.length?null:this.nodes[this.nodes.length-1];!(this.lineStart>-1)||e instanceof Is||this.isCovered?(this.writtenToi.clientHeight||i.scrollWidth>i.clientWidth)&&"visible"!=n.overflow){let n=i.getBoundingClientRect();r=Math.max(r,n.left),o=Math.min(o,n.right),l=Math.max(l,n.top),a=Math.min(e==t.parentNode?s.innerHeight:a,n.bottom)}e="absolute"==n.position||"fixed"==n.position?i.offsetParent:i.parentNode}else{if(11!=e.nodeType)break;e=e.host}return{left:r-i.left,right:Math.max(r,o)-i.left,top:l-(i.top+e),bottom:Math.max(l,a)-(i.top+e)}}function qs(t,e){let i=t.getBoundingClientRect();return{left:0,right:i.right-i.left,top:e,bottom:i.bottom-(i.top+e)}}class _s{constructor(t,e,i,n){this.from=t,this.to=e,this.size=i,this.displaySize=n}static same(t,e){if(t.length!=e.length)return!1;for(let i=0;i"function"!=typeof t&&"cm-lineWrapping"==t.class);this.heightOracle=new Ms(i),this.stateDeco=Gs(e),this.heightMap=Ps.empty().applyChanges(this.stateDeco,u.empty,this.heightOracle.setDoc(e.doc),[new tn(0,0,0,e.doc.length)]);for(let t=0;t<2&&(this.viewport=this.getViewport(0,null),this.updateForViewport());t++);this.updateViewportLines(),this.lineGaps=this.ensureLineGaps([]),this.lineGapDeco=Oe.set(this.lineGaps.map(t=>t.draw(this,!1))),this.scrollParent=t.scrollDOM,this.computeVisibleRanges()}updateForViewport(){let t=[this.viewport],{main:e}=this.state.selection;for(let i=0;i<=1;i++){let n=i?e.head:e.anchor;if(!t.some(({from:t,to:e})=>n>=t&&n<=e)){let{from:e,to:i}=this.lineBlockAt(n);t.push(new Qs(e,i))}}return this.viewports=t.sort((t,e)=>t.from-e.from),this.updateScaler()}updateScaler(){let t=this.scaler;return this.scaler=this.heightMap.height<=7e6?Xs:new Ys(this.heightOracle,this.heightMap,this.viewports),t.eq(this.scaler)?0:2}updateViewportLines(){this.viewportLines=[],this.heightMap.forEachLine(this.viewport.from,this.viewport.to,this.heightOracle.setDoc(this.state.doc),0,0,t=>{this.viewportLines.push(Js(t,this.scaler))})}update(t,e=null){this.state=t.state;let i=this.stateDeco;this.stateDeco=Gs(this.state);let n=t.changedRanges,s=tn.extendWithRanges(n,function(t,e,i){let n=new zs;return Lt.compare(t,e,i,n,0),n.changes}(i,this.stateDeco,t?t.changes:T.empty(this.state.doc.length))),r=this.heightMap.height,o=this.scrolledToBottom?null:this.scrollAnchorAt(this.scrollOffset);As(),this.heightMap=this.heightMap.applyChanges(this.stateDeco,t.startState.doc,this.heightOracle.setDoc(this.state.doc),s),(this.heightMap.height!=r||Cs)&&(t.flags|=2),o?(this.scrollAnchorPos=t.changes.mapPos(o.from,-1),this.scrollAnchorHeight=o.top):(this.scrollAnchorPos=-1,this.scrollAnchorHeight=r);let l=s.length?this.mapViewport(this.viewport,t.changes):this.viewport;(e&&(e.range.headl.to)||!this.viewportIsAppropriate(l))&&(l=this.getViewport(0,e));let a=l.from!=this.viewport.from||l.to!=this.viewport.to;this.viewport=l,t.flags|=this.updateForViewport(),(a||!t.changes.empty||2&t.flags)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(this.mapLineGaps(this.lineGaps,t.changes))),t.flags|=this.computeVisibleRanges(t.changes),e&&(this.scrollTarget=e),!this.mustEnforceCursorAssoc&&(t.selectionSet||t.focusChanged)&&t.view.lineWrapping&&t.state.selection.main.empty&&t.state.selection.main.assoc&&!t.state.facet(Bi)&&(this.mustEnforceCursorAssoc=!0)}measure(){let{view:t}=this,e=t.contentDOM,i=window.getComputedStyle(e),n=this.heightOracle,s=i.whiteSpace;this.defaultTextDirection="rtl"==i.direction?ni.RTL:ni.LTR;let r=this.heightOracle.mustRefreshForWrapping(s)||"refresh"===this.mustMeasureContent,o=e.getBoundingClientRect(),l=r||this.mustMeasureContent||this.contentDOMHeight!=o.height;this.contentDOMHeight=o.height,this.mustMeasureContent=!1;let a=0,h=0;if(o.width&&o.height){let{scaleX:t,scaleY:i}=Ue(e,o);(t>.005&&Math.abs(this.scaleX-t)>.005||i>.005&&Math.abs(this.scaleY-i)>.005)&&(this.scaleX=t,this.scaleY=i,a|=16,r=l=!0)}let c=(parseInt(i.paddingTop)||0)*this.scaleY,f=(parseInt(i.paddingBottom)||0)*this.scaleY;this.paddingTop==c&&this.paddingBottom==f||(this.paddingTop=c,this.paddingBottom=f,a|=18),this.editorWidth!=t.scrollDOM.clientWidth&&(n.lineWrapping&&(l=!0),this.editorWidth=t.scrollDOM.clientWidth,a|=16);let d=Qe(this.view.contentDOM,!1).y;d!=this.scrollParent&&(this.scrollParent=d,this.scrollAnchorHeight=-1,this.scrollOffset=0);let p=this.getScrollOffset();this.scrollOffset!=p&&(this.scrollAnchorHeight=-1,this.scrollOffset=p),this.scrolledToBottom=Ze(this.scrollParent||t.win);let m=(this.printing?qs:Fs)(e,this.paddingTop),g=m.top-this.pixelViewport.top,v=m.bottom-this.pixelViewport.bottom;this.pixelViewport=m;let w=this.pixelViewport.bottom>this.pixelViewport.top&&this.pixelViewport.right>this.pixelViewport.left;if(w!=this.inView&&(this.inView=w,w&&(l=!0)),!this.inView&&!this.scrollTarget&&!function(t){let e=t.getBoundingClientRect(),i=t.ownerDocument.defaultView||window;return e.left0&&e.top0}(t.dom))return 0;let b=o.width;if(this.contentDOMWidth==b&&this.editorHeight==t.scrollDOM.clientHeight||(this.contentDOMWidth=o.width,this.editorHeight=t.scrollDOM.clientHeight,a|=16),l){let e=t.docView.measureVisibleLineHeights(this.viewport);if(n.mustRefreshForHeights(e)&&(r=!0),r||n.lineWrapping&&Math.abs(b-this.contentDOMWidth)>n.charWidth){let{lineHeight:i,charWidth:o,textHeight:l}=t.docView.measureTextSize();r=i>0&&n.refresh(s,i,o,l,Math.max(5,b/o),e),r&&(t.docView.minWidth=0,a|=16)}g>0&&v>0?h=Math.max(g,v):g<0&&v<0&&(h=Math.min(g,v)),As();for(let i of this.viewports){let s=i.from==this.viewport.from?e:t.docView.measureVisibleLineHeights(i);this.heightMap=(r?Ps.empty().applyChanges(this.stateDeco,u.empty,this.heightOracle,[new tn(0,0,0,t.state.doc.length)]):this.heightMap).updateHeight(n,0,r,new Os(i.from,s))}Cs&&(a|=2)}let y=!this.viewportIsAppropriate(this.viewport,h)||this.scrollTarget&&(this.scrollTarget.range.headthis.viewport.to);return y&&(2&a&&(a|=this.updateScaler()),this.viewport=this.getViewport(h,this.scrollTarget),a|=this.updateForViewport()),(2&a||y)&&this.updateViewportLines(),(this.lineGaps.length||this.viewport.to-this.viewport.from>4e3)&&this.updateLineGaps(this.ensureLineGaps(r?[]:this.lineGaps,t)),a|=this.computeVisibleRanges(),this.mustEnforceCursorAssoc&&(this.mustEnforceCursorAssoc=!1,t.docView.enforceCursorAssoc()),a}get visibleTop(){return this.scaler.fromDOM(this.pixelViewport.top)}get visibleBottom(){return this.scaler.fromDOM(this.pixelViewport.bottom)}getViewport(t,e){let i=.5-Math.max(-.5,Math.min(.5,t/1e3/2)),n=this.heightMap,s=this.heightOracle,{visibleTop:r,visibleBottom:o}=this,l=new Qs(n.lineAt(r-1e3*i,Ds.ByHeight,s,0,0).from,n.lineAt(o+1e3*(1-i),Ds.ByHeight,s,0,0).to);if(e){let{head:t}=e.range;if(tl.to){let i,r=Math.min(this.editorHeight,this.pixelViewport.bottom-this.pixelViewport.top),o=n.lineAt(t,Ds.ByPos,s,0,0);i="center"==e.y?(o.top+o.bottom)/2-r/2:"start"==e.y||"nearest"==e.y&&t=o+Math.max(10,Math.min(i,250)))&&n>r-2e3&&s>1,r=n<<1;if(this.defaultTextDirection!=ni.LTR&&!i)return[];let o=[],l=(n,r,a,h)=>{if(r-nn&&tt.from>=a.from&&t.to<=a.to&&Math.abs(t.from-n)t.frome));if(!f){if(rt.from<=r&&t.to>=r)){let t=e.moveToLineBoundary(N.cursor(r),!1,!0).head;t>n&&(r=t)}let t=this.gapSize(a,n,r,h);f=new _s(n,r,t,i||t<2e6?t:2e6)}o.push(f)},a=e=>{if(e.lengths&&(n.push({from:s,to:t}),r+=t-s),s=e}},20),s2e6)for(let i of t)i.from>=e.from&&i.frome.from&&l(e.from,o,e,s),at.draw(this,this.heightOracle.lineWrapping))))}computeVisibleRanges(t){let e=this.stateDeco;this.lineGaps.length&&(e=e.concat(this.lineGapDeco));let i=[];Lt.spans(e,this.viewport.from,this.viewport.to,{span(t,e){i.push({from:t,to:e})},point(){}},20);let n=0;if(i.length!=this.visibleRanges.length)n=12;else for(let e=0;e=this.viewport.from&&t<=this.viewport.to&&this.viewportLines.find(e=>e.from<=t&&e.to>=t)||Js(this.heightMap.lineAt(t,Ds.ByPos,this.heightOracle,0,0),this.scaler)}lineBlockAtHeight(t){return t>=this.viewportLines[0].top&&t<=this.viewportLines[this.viewportLines.length-1].bottom&&this.viewportLines.find(e=>e.top<=t&&e.bottom>=t)||Js(this.heightMap.lineAt(this.scaler.fromDOM(t),Ds.ByHeight,this.heightOracle,0,0),this.scaler)}getScrollOffset(){return(this.scrollParent==this.view.scrollDOM?this.scrollParent.scrollTop:(this.scrollParent?this.scrollParent.getBoundingClientRect().top:0)-this.view.contentDOM.getBoundingClientRect().top)*this.scaleY}scrollAnchorAt(t){let e=this.lineBlockAtHeight(t+8);return e.from>=this.viewport.from||this.viewportLines[0].top-t>200?e:this.viewportLines[0]}elementAtHeight(t){return Js(this.heightMap.blockAt(this.scaler.fromDOM(t),this.heightOracle,0,0),this.scaler)}get docHeight(){return this.scaler.toDOM(this.heightMap.height)}get contentHeight(){return this.docHeight+this.paddingTop+this.paddingBottom}}class Qs{constructor(t,e){this.from=t,this.to=e}}function js({total:t,ranges:e},i){if(i<=0)return e[0].from;if(i>=1)return e[e.length-1].to;let n=Math.floor(t*i);for(let t=0;;t++){let{from:i,to:s}=e[t],r=s-i;if(n<=r)return i+n;n-=r}}function Ks(t,e){let i=0;for(let{from:n,to:s}of t.ranges){if(e<=s){i+=e-n;break}i+=s-n}return i/t.total}const Xs={toDOM:t=>t,fromDOM:t=>t,scale:1,eq(t){return t==this}};function Gs(t){let e=t.facet(Ui).filter(t=>"function"!=typeof t),i=t.facet(ji).filter(t=>"function"!=typeof t);return i.length&&e.push(Lt.join(i)),e}class Ys{constructor(t,e,i){let n=0,s=0,r=0;this.viewports=i.map(({from:i,to:s})=>{let r=e.lineAt(i,Ds.ByPos,t,0,0).top,o=e.lineAt(s,Ds.ByPos,t,0,0).bottom;return n+=o-r,{from:i,to:s,top:r,bottom:o,domTop:0,domBottom:0}}),this.scale=(7e6-n)/(e.height-n);for(let t of this.viewports)t.domTop=r+(t.top-s)*this.scale,r=t.domBottom=t.domTop+(t.bottom-t.top),s=t.bottom}toDOM(t){for(let e=0,i=0,n=0;;e++){let s=ee.from==t.viewports[i].from&&e.to==t.viewports[i].to))}}function Js(t,e){if(1==e.scale)return t;let i=e.toDOM(t.top),n=e.toDOM(t.bottom);return new Ts(t.from,t.length,i,n-i,Array.isArray(t._content)?t._content.map(t=>Js(t,e)):t._content)}const Zs=V.define({combine:t=>t.join(" ")}),tr=V.define({combine:t=>t.indexOf(!0)>-1}),er=Yt.newName(),ir=Yt.newName(),nr=Yt.newName(),sr={"&light":"."+ir,"&dark":"."+nr};function rr(t,e,i){return new Yt(e,{finish:e=>/&/.test(e)?e.replace(/&\w*/,e=>{if("&"==e)return t;if(!i||!i[e])throw new RangeError(`Unsupported selector: ${e}`);return i[e]}):t+" "+e})}const or=rr("."+er,{"&":{position:"relative !important",boxSizing:"border-box","&.cm-focused":{outline:"1px dotted #212121"},display:"flex !important",flexDirection:"column"},".cm-scroller":{display:"flex !important",alignItems:"flex-start !important",fontFamily:"monospace",lineHeight:1.4,height:"100%",overflowX:"auto",position:"relative",zIndex:0,overflowAnchor:"none"},".cm-content":{margin:0,flexGrow:2,flexShrink:0,display:"block",whiteSpace:"pre",wordWrap:"normal",boxSizing:"border-box",minHeight:"100%",padding:"4px 0",outline:"none","&[contenteditable=true]":{WebkitUserModify:"read-write-plaintext-only"}},".cm-lineWrapping":{whiteSpace_fallback:"pre-wrap",whiteSpace:"break-spaces",wordBreak:"break-word",overflowWrap:"anywhere",flexShrink:1},"&light .cm-content":{caretColor:"black"},"&dark .cm-content":{caretColor:"white"},".cm-line":{display:"block",padding:"0 2px 0 6px"},".cm-layer":{userSelect:"none",position:"absolute",left:0,top:0,contain:"size style","& > *":{position:"absolute"}},"&light .cm-selectionBackground":{background:"#d9d9d9"},"&dark .cm-selectionBackground":{background:"#222"},"&light.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground":{background:"#d7d4f0"},"&dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground":{background:"#233"},".cm-cursorLayer":{pointerEvents:"none"},"&.cm-focused > .cm-scroller > .cm-cursorLayer":{animation:"steps(1) cm-blink 1.2s infinite"},"@keyframes cm-blink":{"0%":{},"50%":{opacity:0},"100%":{}},"@keyframes cm-blink2":{"0%":{},"50%":{opacity:0},"100%":{}},".cm-cursor, .cm-dropCursor":{borderLeft:"1.2px solid black",marginLeft:"-0.6px",pointerEvents:"none"},".cm-cursor":{display:"none"},"&dark .cm-cursor":{borderLeftColor:"#ddd"},".cm-selectionHandle":{backgroundColor:"currentColor",width:"1.5px"},".cm-selectionHandle-start::before, .cm-selectionHandle-end::before":{content:'""',backgroundColor:"inherit",borderRadius:"50%",width:"8px",height:"8px",position:"absolute",left:"-3.25px"},".cm-selectionHandle-start::before":{top:"-8px"},".cm-selectionHandle-end::before":{bottom:"-8px"},".cm-dropCursor":{position:"absolute"},"&.cm-focused > .cm-scroller > .cm-cursorLayer .cm-cursor":{display:"block"},".cm-iso":{unicodeBidi:"isolate"},".cm-announced":{position:"fixed",top:"-10000px"},"@media print":{".cm-announced":{display:"none"}},"&light .cm-activeLine":{backgroundColor:"#cceeff44"},"&dark .cm-activeLine":{backgroundColor:"#99eeff33"},"&light .cm-specialChar":{color:"red"},"&dark .cm-specialChar":{color:"#f78"},".cm-gutters":{flexShrink:0,display:"flex",height:"100%",boxSizing:"border-box",zIndex:200},".cm-gutters-before":{insetInlineStart:0},".cm-gutters-after":{insetInlineEnd:0},"&light .cm-gutters":{backgroundColor:"#f5f5f5",color:"#6c6c6c",border:"0px solid #ddd","&.cm-gutters-before":{borderRightWidth:"1px"},"&.cm-gutters-after":{borderLeftWidth:"1px"}},"&dark .cm-gutters":{backgroundColor:"#333338",color:"#ccc"},".cm-gutter":{display:"flex !important",flexDirection:"column",flexShrink:0,boxSizing:"border-box",minHeight:"100%",overflow:"hidden"},".cm-gutterElement":{boxSizing:"border-box"},".cm-lineNumbers .cm-gutterElement":{padding:"0 3px 0 5px",minWidth:"20px",textAlign:"right",whiteSpace:"nowrap"},"&light .cm-activeLineGutter":{backgroundColor:"#e2f2ff"},"&dark .cm-activeLineGutter":{backgroundColor:"#222227"},".cm-panels":{boxSizing:"border-box",position:"sticky",left:0,right:0,zIndex:300},"&light .cm-panels":{backgroundColor:"#f5f5f5",color:"black"},"&light .cm-panels-top":{borderBottom:"1px solid #ddd"},"&light .cm-panels-bottom":{borderTop:"1px solid #ddd"},"&dark .cm-panels":{backgroundColor:"#333338",color:"white"},".cm-dialog":{padding:"2px 19px 4px 6px",position:"relative","& label":{fontSize:"80%"}},".cm-dialog-close":{position:"absolute",top:"3px",right:"4px",backgroundColor:"inherit",border:"none",font:"inherit",fontSize:"14px",padding:"0"},".cm-tab":{display:"inline-block",overflow:"hidden",verticalAlign:"bottom"},".cm-widgetBuffer":{verticalAlign:"text-top",height:"1em",width:0,display:"inline"},".cm-placeholder":{color:"#888",display:"inline-block",verticalAlign:"top",userSelect:"none"},".cm-highlightSpace":{backgroundImage:"radial-gradient(circle at 50% 55%, #aaa 20%, transparent 5%)",backgroundPosition:"center"},".cm-highlightTab":{backgroundImage:'url(\'data:image/svg+xml,\')',backgroundSize:"auto 100%",backgroundPosition:"right 90%",backgroundRepeat:"no-repeat"},".cm-trailingSpace":{backgroundColor:"#ff332255"},".cm-button":{verticalAlign:"middle",color:"inherit",fontSize:"70%",padding:".2em 1em",borderRadius:"1px"},"&light .cm-button":{backgroundImage:"linear-gradient(#eff1f5, #d9d9df)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#b4b4b4, #d0d3d6)"}},"&dark .cm-button":{backgroundImage:"linear-gradient(#393939, #111)",border:"1px solid #888","&:active":{backgroundImage:"linear-gradient(#111, #333)"}},".cm-textfield":{verticalAlign:"middle",color:"inherit",fontSize:"70%",border:"1px solid silver",padding:".2em .5em"},"&light .cm-textfield":{backgroundColor:"white"},"&dark .cm-textfield":{border:"1px solid #555",backgroundColor:"inherit"}},sr),lr={childList:!0,characterData:!0,subtree:!0,attributes:!0,characterDataOldValue:!0},ar=be.ie&&be.ie_version<=11;class hr{constructor(t){this.view=t,this.active=!1,this.editContext=null,this.selectionRange=new je,this.selectionChanged=!1,this.delayedFlush=-1,this.resizeTimeout=-1,this.queue=[],this.delayedAndroidKey=null,this.flushingAndroidKey=-1,this.lastChange=0,this.scrollTargets=[],this.intersection=null,this.resizeScroll=null,this.intersecting=!1,this.gapIntersection=null,this.gaps=[],this.printQuery=null,this.parentCheck=-1,this.dom=t.contentDOM,this.observer=new MutationObserver(e=>{for(let t of e)this.queue.push(t);(be.ie&&be.ie_version<=11||be.ios&&t.composing)&&e.some(t=>"childList"==t.type&&t.removedNodes.length||"characterData"==t.type&&t.oldValue.length>t.target.nodeValue.length)?this.flushSoon():this.flush()}),!window.EditContext||!be.android||!1===t.constructor.EDIT_CONTEXT||be.chrome&&be.chrome_version<126||(this.editContext=new fr(t),t.state.facet(Hi)&&(t.contentDOM.editContext=this.editContext.editContext)),ar&&(this.onCharData=t=>{this.queue.push({target:t.target,type:"characterData",oldValue:t.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this),this.onResize=this.onResize.bind(this),this.onPrint=this.onPrint.bind(this),this.onScroll=this.onScroll.bind(this),window.matchMedia&&(this.printQuery=window.matchMedia("print")),"function"==typeof ResizeObserver&&(this.resizeScroll=new ResizeObserver(()=>{var t;(null===(t=this.view.docView)||void 0===t?void 0:t.lastUpdate){this.parentCheck<0&&(this.parentCheck=setTimeout(this.listenForScroll.bind(this),1e3)),t.length>0&&t[t.length-1].intersectionRatio>0!=this.intersecting&&(this.intersecting=!this.intersecting,this.intersecting!=this.view.inView&&this.onScrollChanged(document.createEvent("Event")))},{threshold:[0,.001]}),this.intersection.observe(this.dom),this.gapIntersection=new IntersectionObserver(t=>{t.length>0&&t[t.length-1].intersectionRatio>0&&this.onScrollChanged(document.createEvent("Event"))},{})),this.listenForScroll(),this.readSelectionRange()}onScrollChanged(t){this.view.inputState.runHandlers("scroll",t),this.intersecting&&this.view.measure()}onScroll(t){this.intersecting&&this.flush(!1),this.editContext&&this.view.requestMeasure(this.editContext.measureReq),this.onScrollChanged(t)}onResize(){this.resizeTimeout<0&&(this.resizeTimeout=setTimeout(()=>{this.resizeTimeout=-1,this.view.requestMeasure()},50))}onPrint(t){("change"!=t.type&&t.type||t.matches)&&(this.view.viewState.printing=!0,this.view.measure(),setTimeout(()=>{this.view.viewState.printing=!1,this.view.requestMeasure()},500))}updateGaps(t){if(this.gapIntersection&&(t.length!=this.gaps.length||this.gaps.some((e,i)=>e!=t[i]))){this.gapIntersection.disconnect();for(let e of t)this.gapIntersection.observe(e);this.gaps=t}}onSelectionChange(t){let e=this.selectionChanged;if(!this.readSelectionRange()||this.delayedAndroidKey)return;let{view:i}=this,n=this.selectionRange;if(i.state.facet(Hi)?i.root.activeElement!=this.dom:!Ne(this.dom,n))return;let s=n.anchorNode&&i.docView.tile.nearest(n.anchorNode);s&&s.isWidget()&&s.widget.ignoreEvent(t)?e||(this.selectionChanged=!1):(be.ie&&be.ie_version<=11||be.android&&be.chrome)&&!i.state.selection.main.empty&&n.focusNode&&He(n.focusNode,n.focusOffset,n.anchorNode,n.anchorOffset)?this.flushSoon():this.flush(!1)}readSelectionRange(){let{view:t}=this,e=Le(t.root);if(!e)return!1;let i=be.safari&&11==t.root.nodeType&&t.root.activeElement==this.dom&&function(t,e){if(e.getComposedRanges){let i=e.getComposedRanges(t.root)[0];if(i)return ur(t,i)}let i=null;function n(t){t.preventDefault(),t.stopImmediatePropagation(),i=t.getTargetRanges()[0]}return t.contentDOM.addEventListener("beforeinput",n,!0),t.dom.ownerDocument.execCommand("indent"),t.contentDOM.removeEventListener("beforeinput",n,!0),i?ur(t,i):null}(this.view,e)||e;if(!i||this.selectionRange.eq(i))return!1;let n=Ne(this.dom,i);return n&&!this.selectionChanged&&t.inputState.lastFocusTime>Date.now()-200&&t.inputState.lastTouchTime{let t=this.delayedAndroidKey;if(t){this.clearDelayedAndroidKey(),this.view.inputState.lastKeyCode=t.keyCode,this.view.inputState.lastKeyTime=Date.now(),!this.flush()&&t.force&&Je(this.dom,t.key,t.keyCode)}};this.flushingAndroidKey=this.view.win.requestAnimationFrame(t)}this.delayedAndroidKey&&"Enter"!=t||(this.delayedAndroidKey={key:t,keyCode:e,force:this.lastChange{this.delayedFlush=-1,this.flush()}))}forceFlush(){this.delayedFlush>=0&&(this.view.win.cancelAnimationFrame(this.delayedFlush),this.delayedFlush=-1),this.flush()}pendingRecords(){for(let t of this.observer.takeRecords())this.queue.push(t);return this.queue}processRecords(){let t=this.pendingRecords();t.length&&(this.queue=[]);let e=-1,i=-1,n=!1;for(let s of t){let t=this.readMutation(s);t&&(t.typeOver&&(n=!0),-1==e?({from:e,to:i}=t):(e=Math.min(t.from,e),i=Math.max(t.to,i)))}return{from:e,to:i,typeOver:n}}readChange(){let{from:t,to:e,typeOver:i}=this.processRecords(),n=this.selectionChanged&&Ne(this.dom,this.selectionRange);if(t<0&&!n)return null;t>-1&&(this.lastChange=Date.now()),this.view.inputState.lastFocusTime=0,this.selectionChanged=!1;let s=new Qn(this.view,t,e,i);return this.view.docView.domChanged={newSel:s.newSel?s.newSel.main:null},s}flush(t=!0){if(this.delayedFlush>=0||this.delayedAndroidKey)return!1;t&&this.readSelectionRange();let e=this.readChange();if(!e)return this.view.requestMeasure(),!1;let i=this.view.state,n=Kn(this.view,e);return this.view.state==i&&(e.domChanged||e.newSel&&!Yn(this.view.state.selection,e.newSel.main))&&this.view.update([]),n}readMutation(t){let e=this.view.docView.tile.nearest(t.target);if(!e||e.isWidget())return null;if(e.markDirty("attributes"==t.type),"childList"==t.type){let i=cr(e,t.previousSibling||t.target.previousSibling,-1),n=cr(e,t.nextSibling||t.target.nextSibling,1);return{from:i?e.posAfter(i):e.posAtStart,to:n?e.posBefore(n):e.posAtEnd,typeOver:!1}}return"characterData"==t.type?{from:e.posAtStart,to:e.posAtEnd,typeOver:t.target.nodeValue==t.oldValue}:null}setWindow(t){t!=this.win&&(this.removeWindowListeners(this.win),this.win=t,this.addWindowListeners(this.win))}addWindowListeners(t){t.addEventListener("resize",this.onResize),this.printQuery?this.printQuery.addEventListener?this.printQuery.addEventListener("change",this.onPrint):this.printQuery.addListener(this.onPrint):t.addEventListener("beforeprint",this.onPrint),t.addEventListener("scroll",this.onScroll),t.document.addEventListener("selectionchange",this.onSelectionChange)}removeWindowListeners(t){t.removeEventListener("scroll",this.onScroll),t.removeEventListener("resize",this.onResize),this.printQuery?this.printQuery.removeEventListener?this.printQuery.removeEventListener("change",this.onPrint):this.printQuery.removeListener(this.onPrint):t.removeEventListener("beforeprint",this.onPrint),t.document.removeEventListener("selectionchange",this.onSelectionChange)}update(t){this.editContext&&(this.editContext.update(t),t.startState.facet(Hi)!=t.state.facet(Hi)&&(t.view.contentDOM.editContext=t.state.facet(Hi)?this.editContext.editContext:null))}destroy(){var t,e,i;this.stop(),null===(t=this.intersection)||void 0===t||t.disconnect(),null===(e=this.gapIntersection)||void 0===e||e.disconnect(),null===(i=this.resizeScroll)||void 0===i||i.disconnect();for(let t of this.scrollTargets)t.removeEventListener("scroll",this.onScroll);this.removeWindowListeners(this.win),clearTimeout(this.parentCheck),clearTimeout(this.resizeTimeout),this.win.cancelAnimationFrame(this.delayedFlush),this.win.cancelAnimationFrame(this.flushingAndroidKey),this.editContext&&(this.view.contentDOM.editContext=null,this.editContext.destroy())}}function cr(t,e,i){for(;e;){let n=sn.get(e);if(n&&n.parent==t)return n;let s=e.parentNode;e=s!=t.dom?s:i>0?e.nextSibling:e.previousSibling}return null}function ur(t,e){let i=e.startContainer,n=e.startOffset,s=e.endContainer,r=e.endOffset,o=t.docView.domAtPos(t.state.selection.main.anchor,1);return He(o.node,o.offset,s,r)&&([i,n,s,r]=[s,r,i,n]),{anchorNode:i,anchorOffset:n,focusNode:s,focusOffset:r}}class fr{constructor(t){this.from=0,this.to=0,this.pendingContextChange=null,this.handlers=Object.create(null),this.composing=null,this.resetRange(t.state);let e=this.editContext=new window.EditContext({text:t.state.doc.sliceString(this.from,this.to),selectionStart:this.toContextPos(Math.max(this.from,Math.min(this.to,t.state.selection.main.anchor))),selectionEnd:this.toContextPos(t.state.selection.main.head)});this.handlers.textupdate=i=>{let n=t.state.selection.main,{anchor:s,head:r}=n,o=this.toEditorPos(i.updateRangeStart),l=this.toEditorPos(i.updateRangeEnd);t.inputState.composing>=0&&!this.composing&&(this.composing={contextBase:i.updateRangeStart,editorBase:o,drifted:!1});let a=l-o>i.text.length;o==this.from&&sthis.to&&(l=s);let h=Gn(t.state.sliceDoc(o,l),i.text,(a?n.from:n.to)-o,a?"end":null);if(!h){let e=N.single(this.toEditorPos(i.selectionStart),this.toEditorPos(i.selectionEnd));return void(Yn(e,n)||t.dispatch({selection:e,userEvent:"select"}))}let c={from:h.from+o,to:h.toA+o,insert:u.of(i.text.slice(h.from,h.toB).split("\n"))};if((be.mac||be.android)&&c.from==r-1&&/^\. ?$/.test(i.text)&&"off"==t.contentDOM.getAttribute("autocorrect")&&(c={from:o,to:l,insert:u.of([i.text.replace("."," ")])}),this.pendingContextChange=c,!t.state.readOnly){let e=this.to-this.from+(c.to-c.from+c.insert.length);Xn(t,c,N.single(this.toEditorPos(i.selectionStart,e),this.toEditorPos(i.selectionEnd,e)))}this.pendingContextChange&&(this.revertPending(t.state),this.setSelection(t.state)),c.from=0&&!/[\\p{Alphabetic}\\p{Number}_]/.test(e.text.slice(Math.max(0,i.updateRangeStart-1),Math.min(e.text.length,i.updateRangeStart+1)))&&this.handlers.compositionend(i)},this.handlers.characterboundsupdate=i=>{let n=[],s=null;for(let e=this.toEditorPos(i.rangeStart),r=this.toEditorPos(i.rangeEnd);e{let i=[];for(let t of e.getTextFormats()){let e=t.underlineStyle,n=t.underlineThickness;if(!/none/i.test(e)&&!/none/i.test(n)){let s=this.toEditorPos(t.rangeStart),r=this.toEditorPos(t.rangeEnd);if(s{t.inputState.composing<0&&(t.inputState.composing=0,t.inputState.compositionFirstChange=!0)},this.handlers.compositionend=()=>{if(t.inputState.composing=-1,t.inputState.compositionFirstChange=null,this.composing){let{drifted:e}=this.composing;this.composing=null,e&&this.reset(t.state)}};for(let t in this.handlers)e.addEventListener(t,this.handlers[t]);this.measureReq={read:t=>{let e=Le(t.root);e&&e.rangeCount&&this.editContext.updateSelectionBounds(e.getRangeAt(0).getBoundingClientRect())}}}applyEdits(t){let e=0,i=!1,n=this.pendingContextChange;return t.changes.iterChanges((s,r,o,l,a)=>{if(i)return;let h=a.length-(r-s);if(n&&r>=n.to){if(n.from==s&&n.to==r&&n.insert.eq(a))return n=this.pendingContextChange=null,e+=h,void(this.to+=h);n=null,this.revertPending(t.state)}if(s+=e,(r+=e)<=this.from)this.from+=h,this.to+=h;else if(sthis.to||this.to-this.from+a.length>3e4)return void(i=!0);this.editContext.updateText(this.toContextPos(s),this.toContextPos(r),a.toString()),this.to+=h}e+=h}),n&&!i&&this.revertPending(t.state),!i}update(t){let e=this.pendingContextChange,i=t.startState.selection.main;this.composing&&(this.composing.drifted||!t.changes.touchesRange(i.from,i.to)&&t.transactions.some(t=>!t.isUserEvent("input.type")&&t.changes.touchesRange(this.from,this.to)))?(this.composing.drifted=!0,this.composing.editorBase=t.changes.mapPos(this.composing.editorBase)):this.applyEdits(t)&&this.rangeIsValid(t.state)?(t.docChanged||t.selectionSet||e)&&this.setSelection(t.state):(this.pendingContextChange=null,this.reset(t.state)),(t.geometryChanged||t.docChanged||t.selectionSet)&&t.view.requestMeasure(this.measureReq)}resetRange(t){let{head:e}=t.selection.main;this.from=Math.max(0,e-1e4),this.to=Math.min(t.doc.length,e+1e4)}reset(t){this.resetRange(t),this.editContext.updateText(0,this.editContext.text.length,t.doc.sliceString(this.from,this.to)),this.setSelection(t)}revertPending(t){let e=this.pendingContextChange;this.pendingContextChange=null,this.editContext.updateText(this.toContextPos(e.from),this.toContextPos(e.from+e.insert.length),t.doc.sliceString(e.from,e.to))}setSelection(t){let{main:e}=t.selection,i=this.toContextPos(Math.max(this.from,Math.min(this.to,e.anchor))),n=this.toContextPos(e.head);this.editContext.selectionStart==i&&this.editContext.selectionEnd==n||this.editContext.updateSelection(i,n)}rangeIsValid(t){let{head:e}=t.selection.main;return!(this.from>0&&e-this.from<500||this.to3e4)}toEditorPos(t,e=this.to-this.from){t=Math.min(t,e);let i=this.composing;return i&&i.drifted?i.editorBase+(t-i.contextBase):t+this.from}toContextPos(t){let e=this.composing;return e&&e.drifted?e.contextBase+(t-e.editorBase):t-this.from}destroy(){for(let t in this.handlers)this.editContext.removeEventListener(t,this.handlers[t])}}class dr{get state(){return this.viewState.state}get viewport(){return this.viewState.viewport}get visibleRanges(){return this.viewState.visibleRanges}get inView(){return this.viewState.inView}get composing(){return!!this.inputState&&this.inputState.composing>0}get compositionStarted(){return!!this.inputState&&this.inputState.composing>=0}get root(){return this._root}get win(){return this.dom.ownerDocument.defaultView||window}constructor(t={}){var e;this.plugins=[],this.pluginMap=new Map,this.editorAttrs={},this.contentAttrs={},this.bidiCache=[],this.destroyed=!1,this.updateState=2,this.measureScheduled=-1,this.measureRequests=[],this.contentDOM=document.createElement("div"),this.scrollDOM=document.createElement("div"),this.scrollDOM.tabIndex=-1,this.scrollDOM.className="cm-scroller",this.scrollDOM.appendChild(this.contentDOM),this.announceDOM=document.createElement("div"),this.announceDOM.className="cm-announced",this.announceDOM.setAttribute("aria-live","polite"),this.dom=document.createElement("div"),this.dom.appendChild(this.announceDOM),this.dom.appendChild(this.scrollDOM),t.parent&&t.parent.appendChild(this.dom);let{dispatch:i}=t;this.dispatchTransactions=t.dispatchTransactions||i&&(t=>t.forEach(t=>i(t,this)))||(t=>this.update(t)),this.dispatch=this.dispatch.bind(this),this._root=t.root||function(t){for(;t;){if(t&&(9==t.nodeType||11==t.nodeType&&t.host))return t;t=t.assignedSlot||t.parentNode}return null}(t.parent)||document,this.viewState=new Us(this,t.state||Ot.create(t)),t.scrollTo&&t.scrollTo.is(Ii)&&(this.viewState.scrollTarget=t.scrollTo.value.clip(this.viewState.state)),this.plugins=this.state.facet(zi).map(t=>new qi(t));for(let t of this.plugins)t.update(this);this.observer=new hr(this),this.inputState=new Jn(this),this.inputState.ensureHandlers(this.plugins),this.docView=new On(this),this.mountStyles(),this.updateAttrs(),this.updateState=0,this.requestMeasure(),(null===(e=document.fonts)||void 0===e?void 0:e.ready)&&document.fonts.ready.then(()=>{this.viewState.mustMeasureContent="refresh",this.requestMeasure()})}dispatch(...t){let e=1==t.length&&t[0]instanceof gt?t:1==t.length&&Array.isArray(t[0])?t[0]:[this.state.update(...t)];this.dispatchTransactions(e,this)}update(t){if(0!=this.updateState)throw new Error("Calls to EditorView.update are not allowed while an update is in progress");let e,i=!1,n=!1,s=this.state;for(let e of t){if(e.startState!=s)throw new RangeError("Trying to update state with a transaction that doesn't start from the previous state.");s=e.state}if(this.destroyed)return void(this.viewState.state=s);let r=this.hasFocus,o=0,l=null;t.some(t=>t.annotation(bs))?(this.inputState.notifiedFocused=r,o=1):r!=this.inputState.notifiedFocused&&(this.inputState.notifiedFocused=r,l=ys(s,r),l||(o=1));let a=this.observer.delayedAndroidKey,h=null;if(a?(this.observer.clearDelayedAndroidKey(),h=this.observer.readChange(),(h&&!this.state.doc.eq(s.doc)||!this.state.selection.eq(s.selection))&&(h=null)):this.observer.clear(),s.facet(Ot.phrases)!=this.state.facet(Ot.phrases))return this.setState(s);e=en.create(this,s,t),e.flags|=o;let c=this.viewState.scrollTarget;try{this.updateState=2;for(let e of t){if(c&&(c=c.map(e.changes)),e.scrollIntoView){let{main:t}=e.state.selection,{x:i,y:n}=this.state.facet(dr.cursorScrollMargin);c=new Li(t.empty?t:N.cursor(t.head,t.head>t.anchor?-1:1),"nearest","nearest",n,i)}for(let t of e.effects)t.is(Ii)&&(c=t.value.clip(this.state))}this.viewState.update(e,c),this.bidiCache=gr.update(this.bidiCache,e.changes),e.empty||(this.updatePlugins(e),this.inputState.update(e)),i=this.docView.update(e),this.state.facet(Zi)!=this.styleModules&&this.mountStyles(),n=this.updateAttrs(),this.showAnnouncements(t),this.docView.updateSelection(i,t.some(t=>t.isUserEvent("select.pointer")))}finally{this.updateState=0}if(e.startState.facet(Zs)!=e.state.facet(Zs)&&(this.viewState.mustMeasureContent=!0),(i||n||c||this.viewState.mustEnforceCursorAssoc||this.viewState.mustMeasureContent)&&this.requestMeasure(),i&&this.docViewUpdate(),!e.empty)for(let t of this.state.facet(Mi))try{t(e)}catch(t){Wi(this.state,t,"update listener")}(l||h)&&Promise.resolve().then(()=>{l&&this.state==l.startState&&this.dispatch(l),h&&!Kn(this,h)&&a.force&&Je(this.contentDOM,a.key,a.keyCode)})}setState(t){if(0!=this.updateState)throw new Error("Calls to EditorView.setState are not allowed while an update is in progress");if(this.destroyed)return void(this.viewState.state=t);this.updateState=2;let e=this.hasFocus;try{for(let t of this.plugins)t.destroy(this);this.viewState=new Us(this,t),this.plugins=t.facet(zi).map(t=>new qi(t)),this.pluginMap.clear();for(let t of this.plugins)t.update(this);this.docView.destroy(),this.docView=new On(this),this.inputState.ensureHandlers(this.plugins),this.mountStyles(),this.updateAttrs(),this.bidiCache=[]}finally{this.updateState=0}e&&this.focus(),this.requestMeasure()}updatePlugins(t){let e=t.startState.facet(zi),i=t.state.facet(zi);if(e!=i){let n=[];for(let s of i){let i=e.indexOf(s);if(i<0)n.push(new qi(s));else{let e=this.plugins[i];e.mustUpdate=t,n.push(e)}}for(let e of this.plugins)e.mustUpdate!=t&&e.destroy(this);this.plugins=n,this.pluginMap.clear()}else for(let e of this.plugins)e.mustUpdate=t;for(let t=0;t-1&&this.win.cancelAnimationFrame(this.measureScheduled),this.observer.delayedAndroidKey)return this.measureScheduled=-1,void this.requestMeasure();this.measureScheduled=0,t&&this.observer.forceFlush();let e=null,i=this.viewState.scrollParent,n=this.viewState.getScrollOffset(),{scrollAnchorPos:s,scrollAnchorHeight:r}=this.viewState;Math.abs(n-this.viewState.scrollOffset)>1&&(r=-1),this.viewState.scrollAnchorHeight=-1;try{for(let t=0;;t++){if(r<0)if(Ze(i||this.win))s=-1,r=this.viewState.heightMap.height;else{let t=this.viewState.scrollAnchorAt(n);s=t.from,r=t.top}this.updateState=1;let o=this.viewState.measure();if(!o&&!this.measureRequests.length&&null==this.viewState.scrollTarget)break;if(t>5){console.warn(this.measureRequests.length?"Measure loop restarted more than 5 times":"Viewport failed to stabilize");break}let l=[];4&o||([this.measureRequests,l]=[l,this.measureRequests]);let a=l.map(t=>{try{return t.read(this)}catch(t){return Wi(this.state,t),mr}}),h=en.create(this,this.state,[]),c=!1;h.flags|=o,e?e.flags|=o:e=h,this.updateState=2,h.empty||(this.updatePlugins(h),this.inputState.update(h),this.updateAttrs(),c=this.docView.update(h),c&&this.docViewUpdate());for(let t=0;t1||t<-1)&&!(be.ios&&this.inputState.lastIOSMomentumScroll>Date.now()-100)&&(i==this.scrollDOM||this.hasFocus||Math.max(this.inputState.lastWheelEvent,this.inputState.lastTouchTime)>Date.now()-100)){n+=t,i?i.scrollTop+=t:this.win.scrollBy(0,t),r=-1;continue}}}break}}}finally{this.updateState=0,this.measureScheduled=-1}if(e&&!e.empty)for(let t of this.state.facet(Mi))t(e)}get themeClasses(){return er+" "+(this.state.facet(tr)?nr:ir)+" "+this.state.facet(Zs)}updateAttrs(){let t=vr(this,_i,{class:"cm-editor"+(this.hasFocus?" cm-focused ":" ")+this.themeClasses}),e={spellcheck:"false",autocorrect:"off",autocapitalize:"off",writingsuggestions:"false",translate:"no",contenteditable:this.state.facet(Hi)?"true":"false",class:"cm-content",style:`${be.tabSize}: ${this.state.tabSize}`,role:"textbox","aria-multiline":"true"};this.state.readOnly&&(e["aria-readonly"]="true"),vr(this,$i,e);let i=this.observer.ignore(()=>{let i=Se(this.contentDOM,this.contentAttrs,e),n=Se(this.dom,this.editorAttrs,t);return i||n});return this.editorAttrs=t,this.contentAttrs=e,i}showAnnouncements(t){let e=!0;for(let i of t)for(let t of i.effects)if(t.is(dr.announce)){e&&(this.announceDOM.textContent=""),e=!1,this.announceDOM.appendChild(document.createElement("div")).textContent=t.value}}mountStyles(){this.styleModules=this.state.facet(Zi);let t=this.state.facet(dr.cspNonce);Yt.mount(this.root,this.styleModules.concat(or).reverse(),t?{nonce:t}:void 0)}readMeasured(){if(2==this.updateState)throw new Error("Reading the editor layout isn't allowed during an update");0==this.updateState&&this.measureScheduled>-1&&this.measure(!1)}requestMeasure(t){if(this.measureScheduled<0&&(this.measureScheduled=this.win.requestAnimationFrame(()=>this.measure())),t){if(this.measureRequests.indexOf(t)>-1)return;if(null!=t.key)for(let e=0;ee.plugin==t)||null),e&&e.update(this).value}get documentTop(){return this.contentDOM.getBoundingClientRect().top+this.viewState.paddingTop}get documentPadding(){return{top:this.viewState.paddingTop,bottom:this.viewState.paddingBottom}}get scaleX(){return this.viewState.scaleX}get scaleY(){return this.viewState.scaleY}elementAtHeight(t){return this.readMeasured(),this.viewState.elementAtHeight(t)}lineBlockAtHeight(t){return this.readMeasured(),this.viewState.lineBlockAtHeight(t)}get viewportLineBlocks(){return this.viewState.viewportLines}lineBlockAt(t){return this.viewState.lineBlockAt(t)}get contentHeight(){return this.viewState.contentHeight}moveByChar(t,e,i){return Wn(this,t,Ln(this,t,e,i))}moveByGroup(t,e){return Wn(this,t,Ln(this,t,e,e=>function(t,e,i){let n=t.state.charCategorizer(e),s=n(i);return t=>{let e=n(t);return s==St.Space&&(s=e),s==e}}(this,t.head,e)))}visualLineSide(t,e){let i=this.bidiSpans(t),n=this.textDirectionAt(t.from),s=i[e?i.length-1:0];return N.cursor(s.side(e,n)+t.from,s.forward(!e,n)?1:-1)}moveToLineBoundary(t,e,i=!0){return function(t,e,i,n){let s=En(t,e.head,e.assoc||-1),r=n&&s.type==Me.Text&&(t.lineWrapping||s.widgetLineBreaks)?t.coordsAtPos(e.assoc<0&&e.head>s.from?e.head-1:e.head):null;if(r){let e=t.dom.getBoundingClientRect(),n=t.textDirectionAt(s.from),o=t.posAtCoords({x:i==(n==ni.LTR)?e.right-1:e.left+1,y:(r.top+r.bottom)/2});if(null!=o)return N.cursor(o,i?-1:1)}return N.cursor(i?s.to:s.from,i?-1:1)}(this,t,e,i)}moveVertically(t,e,i){return Wn(this,t,function(t,e,i,n){let s=e.head,r=i?1:-1;if(s==(i?t.state.doc.length:0))return N.cursor(s,e.assoc);let o,l=e.goalColumn,a=t.contentDOM.getBoundingClientRect(),h=t.coordsAtPos(s,e.assoc||((e.empty?i:e.head==e.from)?1:-1)),c=t.documentTop;if(h)null==l&&(l=h.left-a.left),o=r<0?h.top:h.bottom;else{let e=t.viewState.lineBlockAt(s);null==l&&(l=Math.min(a.right-a.left,t.defaultCharacterWidth*(s-e.from))),o=(r<0?e.top:e.bottom)+c}let u=a.left+l,f=t.viewState.heightOracle.textHeight>>1,d=null!=n?n:f;for(let e=0;;e+=f){let n=o+(d+e)*r,s=Vn(t,{x:u,y:n},!1,r);if(i?n>a.bottom:no:cthis.viewport.to?this.textDirection:(this.readMeasured(),this.docView.textDirectionAt(t))}get lineWrapping(){return this.viewState.heightOracle.lineWrapping}bidiSpans(t){if(t.length>pr)return wi(t.length);let e,i=this.textDirectionAt(t.from);for(let n of this.bidiCache)if(n.from==t.from&&n.dir==i&&(n.fresh||pi(n.isolates,e=Gi(this,t))))return n.order;e||(e=Gi(this,t));let n=function(t,e,i){if(!t)return[new di(0,0,e==ri?1:0)];if(e==si&&!i.length&&!fi.test(t))return wi(t.length);if(i.length)for(;t.length>mi.length;)mi[mi.length]=256;let n=[],s=e==si?0:1;return vi(t,s,s,i,0,t.length,n),n}(t.text,i,e);return this.bidiCache.push(new gr(t.from,t.to,i,e,!0,n)),n}get hasFocus(){var t;return(this.dom.ownerDocument.hasFocus()||be.safari&&(null===(t=this.inputState)||void 0===t?void 0:t.lastContextMenu)>Date.now()-3e4)&&this.root.activeElement==this.contentDOM}focus(){this.observer.ignore(()=>{Ge(this.contentDOM),this.docView.updateSelection()})}setRoot(t){this._root!=t&&(this._root=t,this.observer.setWindow((9==t.nodeType?t:t.ownerDocument).defaultView||window),this.mountStyles())}destroy(){this.root.activeElement==this.contentDOM&&this.contentDOM.blur();for(let t of this.plugins)t.destroy(this);this.plugins=[],this.inputState.destroy(),this.docView.destroy(),this.dom.remove(),this.observer.destroy(),this.measureScheduled>-1&&this.win.cancelAnimationFrame(this.measureScheduled),this.destroyed=!0}static scrollIntoView(t,e={}){var i,n,s,r;return Ii.of(new Li("number"==typeof t?N.cursor(t):t,null!==(i=e.y)&&void 0!==i?i:"nearest",null!==(n=e.x)&&void 0!==n?n:"nearest",null!==(s=e.yMargin)&&void 0!==s?s:5,null!==(r=e.xMargin)&&void 0!==r?r:5))}scrollSnapshot(){let{scrollTop:t,scrollLeft:e}=this.scrollDOM,i=this.viewState.scrollAnchorAt(t);return Ii.of(new Li(N.cursor(i.from),"start","start",i.top-t,e,!0))}setTabFocusMode(t){null==t?this.inputState.tabFocusMode=this.inputState.tabFocusMode<0?0:-1:"boolean"==typeof t?this.inputState.tabFocusMode=t?0:-1:0!=this.inputState.tabFocusMode&&(this.inputState.tabFocusMode=Date.now()+t)}static domEventHandlers(t){return Fi.define(()=>({}),{eventHandlers:t})}static domEventObservers(t){return Fi.define(()=>({}),{eventObservers:t})}static theme(t,e){let i=Yt.newName(),n=[Zs.of(i),Zi.of(rr(`.${i}`,t))];return e&&e.dark&&n.push(tr.of(!0)),n}static baseTheme(t){return J.lowest(Zi.of(rr("."+er,t,sr)))}static findFromDOM(t){var e;let i=t.querySelector(".cm-content"),n=i&&sn.get(i)||sn.get(t);return(null===(e=null==n?void 0:n.root)||void 0===e?void 0:e.view)||null}}dr.styleModule=Zi,dr.inputHandler=Oi,dr.clipboardInputFilter=Di,dr.clipboardOutputFilter=Ri,dr.scrollHandler=Ei,dr.focusChangeEffect=Ti,dr.perLineTextDirection=Pi,dr.exceptionSink=Ai,dr.updateListener=Mi,dr.editable=Hi,dr.mouseSelectionStyle=Ci,dr.dragMovesSelection=Si,dr.clickAddsSelectionRange=ki,dr.decorations=Ui,dr.blockWrappers=Qi,dr.outerDecorations=ji,dr.atomicRanges=Ki,dr.bidiIsolatedRanges=Xi,dr.cursorScrollMargin=V.define({combine:t=>{let e=5,i=5;for(let n of t)"number"==typeof n?e=i=n:({x:e,y:i}=n);return{x:e,y:i}}}),dr.scrollMargins=Yi,dr.darkTheme=tr,dr.cspNonce=V.define({combine:t=>t.length?t[0]:""}),dr.contentAttributes=$i,dr.editorAttributes=_i,dr.lineWrapping=dr.contentAttributes.of({class:"cm-lineWrapping"}),dr.announce=mt.define();const pr=4096,mr={};class gr{constructor(t,e,i,n,s,r){this.from=t,this.to=e,this.dir=i,this.isolates=n,this.fresh=s,this.order=r}static update(t,e){if(e.empty&&!t.some(t=>t.fresh))return t;let i=[],n=t.length?t[t.length-1].dir:ni.LTR;for(let s=Math.max(0,t.length-10);s=0;s--){let e=n[s],r="function"==typeof e?e(t):e;r&&ye(r,i)}return i}const wr=be.mac?"mac":be.windows?"win":be.linux?"linux":"key";function br(t,e,i){return e.altKey&&(t="Alt-"+t),e.ctrlKey&&(t="Ctrl-"+t),e.metaKey&&(t="Meta-"+t),!1!==i&&e.shiftKey&&(t="Shift-"+t),t}const yr=J.default(dr.domEventHandlers({keydown:(t,e)=>Or(Sr(e.state),t,e,"editor")})),xr=V.define({enables:yr}),kr=new WeakMap;function Sr(t){let e=t.facet(xr),i=kr.get(e);return i||kr.set(e,i=function(t,e=wr){let i=Object.create(null),n=Object.create(null),s=(t,e)=>{let i=n[t];if(null==i)n[t]=e;else if(i!=e)throw new Error("Key binding "+t+" is used both as a regular binding and as a multi-stroke prefix")},r=(t,n,r,o,l)=>{var a,h;let c=i[t]||(i[t]=Object.create(null)),u=n.split(/ (?!$)/).map(t=>function(t,e){const i=t.split(/-(?!$)/);let n,s,r,o,l=i[i.length-1];"Space"==l&&(l=" ");for(let t=0;t{let n=Cr={view:e,prefix:i,scope:t};return setTimeout(()=>{Cr==n&&(Cr=null)},Ar),!0}]})}let f=u.join(" ");s(f,!1);let d=c[f]||(c[f]={preventDefault:!1,stopPropagation:!1,run:(null===(h=null===(a=c._any)||void 0===a?void 0:a.run)||void 0===h?void 0:h.slice())||[]});r&&d.run.push(r),o&&(d.preventDefault=!0),l&&(d.stopPropagation=!0)};for(let n of t){let t=n.scope?n.scope.split(" "):["editor"];if(n.any)for(let e of t){let t=i[e]||(i[e]=Object.create(null));t._any||(t._any={preventDefault:!1,stopPropagation:!1,run:[]});let{any:s}=n;for(let e in t)t[e].run.push(t=>s(t,Mr))}let s=n[e]||n.key;if(s)for(let e of t)r(e,s,n.run,n.preventDefault,n.stopPropagation),n.shift&&r(e,"Shift-"+s,n.shift,n.preventDefault,n.stopPropagation)}return i}(e.reduce((t,e)=>t.concat(e),[]))),i}let Cr=null;const Ar=4e3;let Mr=null;function Or(t,e,i,n){Mr=e;let s=function(t){var e=!(ie&&t.metaKey&&t.shiftKey&&!t.ctrlKey&&!t.altKey||ne&&t.shiftKey&&t.key&&1==t.key.length||"Unidentified"==t.key)&&t.key||(t.shiftKey?ee:te)[t.keyCode]||t.key||"Unidentified";return"Esc"==e&&(e="Escape"),"Del"==e&&(e="Delete"),"Left"==e&&(e="ArrowLeft"),"Up"==e&&(e="ArrowUp"),"Right"==e&&(e="ArrowRight"),"Down"==e&&(e="ArrowDown"),e}(e),r=C(k(s,0))==s.length&&" "!=s,o="",l=!1,a=!1,h=!1;Cr&&Cr.view==i&&Cr.scope==n&&(o=Cr.prefix+" ",ns.indexOf(e.keyCode)<0&&(a=!0,Cr=null));let c,u,f=new Set,d=t=>{if(t){for(let e of t.run)if(!f.has(e)&&(f.add(e),e(i)))return t.stopPropagation&&(h=!0),!0;t.preventDefault&&(t.stopPropagation&&(h=!0),a=!0)}return!1},p=t[n];return p&&(d(p[o+br(s,e,!r)])?l=!0:!r||!(e.altKey||e.metaKey||e.ctrlKey)||be.windows&&e.ctrlKey&&e.altKey||be.mac&&e.altKey&&!e.ctrlKey&&!e.metaKey||!(c=te[e.keyCode])||c==s?r&&e.shiftKey&&d(p[o+br(s,e,!0)])&&(l=!0):(d(p[o+br(c,e,!0)])||e.shiftKey&&(u=ee[e.keyCode])!=s&&u!=c&&d(p[o+br(u,e,!1)]))&&(l=!0),!l&&d(p._any)&&(l=!0)),a&&(l=!0),l&&h&&e.stopPropagation(),Mr=null,l}class Tr{constructor(t,e,i,n,s){this.className=t,this.left=e,this.top=i,this.width=n,this.height=s}draw(){let t=document.createElement("div");return t.className=this.className,this.adjust(t),t}update(t,e){return e.className==this.className&&(this.adjust(t),!0)}adjust(t){t.style.left=this.left+"px",t.style.top=this.top+"px",null!=this.width&&(t.style.width=this.width+"px"),t.style.height=this.height+"px"}eq(t){return this.left==t.left&&this.top==t.top&&this.width==t.width&&this.height==t.height&&this.className==t.className}static forRange(t,e,i){if(i.empty){let n=t.coordsAtPos(i.head,i.assoc||1);if(!n)return[];let s=Dr(t);return[new Tr(e,n.left-s.left,n.top-s.top,null,n.bottom-n.top)]}return function(t,e,i){if(i.to<=t.viewport.from||i.from>=t.viewport.to)return[];let n=Math.max(i.from,t.viewport.from),s=Math.min(i.to,t.viewport.to),r=t.textDirection==ni.LTR,o=t.contentDOM,l=o.getBoundingClientRect(),a=Dr(t),h=o.querySelector(".cm-line"),c=h&&window.getComputedStyle(h),u=l.left+(c?parseInt(c.paddingLeft)+Math.min(0,parseInt(c.textIndent)):0),f=l.right-(c?parseInt(c.paddingRight):0),d=En(t,n,1),p=En(t,s,-1),m=d.type==Me.Text?d:null,g=p.type==Me.Text?p:null;m&&(t.lineWrapping||d.widgetLineBreaks)&&(m=Rr(t,n,1,m));g&&(t.lineWrapping||p.widgetLineBreaks)&&(g=Rr(t,s,-1,g));if(m&&g&&m.from==g.from&&m.to==g.to)return w(b(i.from,i.to,m));{let e=m?b(i.from,null,m):y(d,!1),n=g?b(null,i.to,g):y(p,!0),s=[];return(m||d).to<(g||p).from-(m&&g?1:0)||d.widgetLineBreaks>1&&e.bottom+t.defaultLineHeight/2h&&n.from=r)break;l>s&&a(Math.max(t,s),null==e&&t<=h,Math.min(l,r),null==i&&l>=c,o.dir)}if(s=n.to+1,s>=r)break}return 0==l.length&&a(h,null==e,c,null==i,t.textDirection),{top:s,bottom:o,horizontal:l}}function y(t,e){let i=l.top+(e?t.top:t.bottom);return{top:i,bottom:i,horizontal:[]}}}(t,e,i)}}function Dr(t){let e=t.scrollDOM.getBoundingClientRect();return{left:(t.textDirection==ni.LTR?e.left:e.right-t.scrollDOM.clientWidth*t.scaleX)-t.scrollDOM.scrollLeft*t.scaleX,top:e.top-t.scrollDOM.scrollTop*t.scaleY}}function Rr(t,e,i,n){let s=t.coordsAtPos(e,2*i);if(!s)return n;let r=t.dom.getBoundingClientRect(),o=(s.top+s.bottom)/2,l=t.posAtCoords({x:r.left+1,y:o}),a=t.posAtCoords({x:r.right-1,y:o});return null==l||null==a?n:{from:Math.max(n.from,Math.min(l,a)),to:Math.min(n.to,Math.max(l,a))}}class Pr{constructor(t,e){this.view=t,this.layer=e,this.drawn=[],this.scaleX=1,this.scaleY=1,this.measureReq={read:this.measure.bind(this),write:this.draw.bind(this)},this.dom=t.scrollDOM.appendChild(document.createElement("div")),this.dom.classList.add("cm-layer"),e.above&&this.dom.classList.add("cm-layer-above"),e.class&&this.dom.classList.add(e.class),this.scale(),this.dom.setAttribute("aria-hidden","true"),this.setOrder(t.state),t.requestMeasure(this.measureReq),e.mount&&e.mount(this.dom,t)}update(t){t.startState.facet(Br)!=t.state.facet(Br)&&this.setOrder(t.state),(this.layer.update(t,this.dom)||t.geometryChanged)&&(this.scale(),t.view.requestMeasure(this.measureReq))}docViewUpdate(t){!1!==this.layer.updateOnDocViewUpdate&&t.requestMeasure(this.measureReq)}setOrder(t){let e=0,i=t.facet(Br);for(;e{return i=t,n=this.drawn[e],!(i.constructor==n.constructor&&i.eq(n));var i,n})){let e=this.dom.firstChild,i=0;for(let n of t)n.update&&e&&n.constructor&&this.drawn[i].constructor&&n.update(e,this.drawn[i])?(e=e.nextSibling,i++):this.dom.insertBefore(n.draw(),e);for(;e;){let t=e.nextSibling;e.remove(),e=t}this.drawn=t,be.webkit&&(this.dom.style.display=this.dom.firstChild?"":"none")}}destroy(){this.layer.destroy&&this.layer.destroy(this.dom,this.view),this.dom.remove()}}const Br=V.define();function Er(t){return[Fi.define(e=>new Pr(e,t)),Br.of(t)]}const Lr=V.define({combine:t=>Tt(t,{cursorBlinkRate:1200,drawRangeCursor:!0,iosSelectionHandles:!0},{cursorBlinkRate:(t,e)=>Math.min(t,e),drawRangeCursor:(t,e)=>t||e})});function Ir(t={}){return[Lr.of(t),Wr,Vr,zr,Bi.of(!0)]}function Nr(t){return t.startState.facet(Lr)!=t.state.facet(Lr)}const Wr=Er({above:!0,markers(t){let{state:e}=t,i=e.facet(Lr),n=[];for(let s of e.selection.ranges){let r=s==e.selection.main;if(s.empty||i.drawRangeCursor&&!(r&&be.ios&&i.iosSelectionHandles)){let e=r?"cm-cursor cm-cursor-primary":"cm-cursor cm-cursor-secondary",i=s.empty?s:N.cursor(s.head,s.assoc);for(let s of Tr.forRange(t,e,i))n.push(s)}}return n},update(t,e){t.transactions.some(t=>t.selection)&&(e.style.animationName="cm-blink"==e.style.animationName?"cm-blink2":"cm-blink");let i=Nr(t);return i&&Hr(t.state,e),t.docChanged||t.selectionSet||i},mount(t,e){Hr(e.state,t)},class:"cm-cursorLayer"});function Hr(t,e){e.style.animationDuration=t.facet(Lr).cursorBlinkRate+"ms"}const Vr=Er({above:!1,markers(t){let e=[],{main:i,ranges:n}=t.state.selection;for(let i of n)if(!i.empty)for(let n of Tr.forRange(t,"cm-selectionBackground",i))e.push(n);if(be.ios&&!i.empty&&t.state.facet(Lr).iosSelectionHandles){for(let n of Tr.forRange(t,"cm-selectionHandle cm-selectionHandle-start",N.cursor(i.from,1)))e.push(n);for(let n of Tr.forRange(t,"cm-selectionHandle cm-selectionHandle-end",N.cursor(i.to,1)))e.push(n)}return e},update:(t,e)=>t.docChanged||t.selectionSet||t.viewportChanged||Nr(t),class:"cm-selectionLayer"}),zr=J.highest(dr.theme({".cm-line":{"& ::selection, &::selection":{backgroundColor:"transparent !important"},caretColor:"transparent !important"},".cm-content":{caretColor:"transparent !important","& :focus":{caretColor:"initial !important","&::selection, & ::selection":{backgroundColor:"Highlight !important"}}}})),Fr=mt.define({map:(t,e)=>null==t?null:e.mapPos(t)}),qr=Q.define({create:()=>null,update:(t,e)=>(null!=t&&(t=e.changes.mapPos(t)),e.effects.reduce((t,e)=>e.is(Fr)?e.value:t,t))}),_r=Fi.fromClass(class{constructor(t){this.view=t,this.cursor=null,this.measureReq={read:this.readPos.bind(this),write:this.drawCursor.bind(this)}}update(t){var e;let i=t.state.field(qr);null==i?null!=this.cursor&&(null===(e=this.cursor)||void 0===e||e.remove(),this.cursor=null):(this.cursor||(this.cursor=this.view.scrollDOM.appendChild(document.createElement("div")),this.cursor.className="cm-dropCursor"),(t.startState.field(qr)!=i||t.docChanged||t.geometryChanged)&&this.view.requestMeasure(this.measureReq))}readPos(){let{view:t}=this,e=t.state.field(qr),i=null!=e&&t.coordsAtPos(e);if(!i)return null;let n=t.scrollDOM.getBoundingClientRect();return{left:i.left-n.left+t.scrollDOM.scrollLeft*t.scaleX,top:i.top-n.top+t.scrollDOM.scrollTop*t.scaleY,height:i.bottom-i.top}}drawCursor(t){if(this.cursor){let{scaleX:e,scaleY:i}=this.view;t?(this.cursor.style.left=t.left/e+"px",this.cursor.style.top=t.top/i+"px",this.cursor.style.height=t.height/i+"px"):this.cursor.style.left="-100000px"}}destroy(){this.cursor&&this.cursor.remove()}setDropPos(t){this.view.state.field(qr)!=t&&this.view.dispatch({effects:Fr.of(t)})}},{eventObservers:{dragover(t){this.setDropPos(this.view.posAtCoords({x:t.clientX,y:t.clientY}))},dragleave(t){t.target!=this.view.contentDOM&&this.view.contentDOM.contains(t.relatedTarget)||this.setDropPos(null)},dragend(){this.setDropPos(null)},drop(){this.setDropPos(null)}}});function $r(t,e,i,n,s){e.lastIndex=0;for(let r,o=t.iterRange(i,n),l=i;!o.next().done;l+=o.value.length)if(!o.lineBreak)for(;r=e.exec(o.value);)s(l+r.index,r)}class Ur{constructor(t){const{regexp:e,decoration:i,decorate:n,boundary:s,maxLength:r=1e3}=t;if(!e.global)throw new RangeError("The regular expression given to MatchDecorator should have its 'g' flag set");if(this.regexp=e,n)this.addMatch=(t,e,i,s)=>n(s,i,i+t[0].length,t,e);else if("function"==typeof i)this.addMatch=(t,e,n,s)=>{let r=i(t,e,n);r&&s(n,n+t[0].length,r)};else{if(!i)throw new RangeError("Either 'decorate' or 'decoration' should be provided to MatchDecorator");this.addMatch=(t,e,n,s)=>s(n,n+t[0].length,i)}this.boundary=s,this.maxLength=r}createDeco(t){let e=new It,i=e.add.bind(e);for(let{from:e,to:n}of function(t,e){let i=t.visibleRanges;if(1==i.length&&i[0].from==t.viewport.from&&i[0].to==t.viewport.to)return i;let n=[];for(let{from:s,to:r}of i)s=Math.max(t.state.doc.lineAt(s).from,s-e),r=Math.min(t.state.doc.lineAt(r).to,r+e),n.length&&n[n.length-1].to>=s?n[n.length-1].to=r:n.push({from:s,to:r});return n}(t,this.maxLength))$r(t.state.doc,this.regexp,e,n,(e,n)=>this.addMatch(n,t,e,i));return e.finish()}updateDeco(t,e){let i=1e9,n=-1;return t.docChanged&&t.changes.iterChanges((e,s,r,o)=>{o>=t.view.viewport.from&&r<=t.view.viewport.to&&(i=Math.min(r,i),n=Math.max(o,n))}),t.viewportMoved||n-i>1e3?this.createDeco(t.view):n>-1?this.updateRange(t.view,e.map(t.changes),i,n):e}updateRange(t,e,i,n){for(let s of t.visibleRanges){let r=Math.max(s.from,i),o=Math.min(s.to,n);if(o>=r){let i=t.state.doc.lineAt(r),n=i.toi.from;r--)if(this.boundary.test(i.text[r-1-i.from])){l=r;break}for(;oc.push(i.range(t,e));if(i==n)for(this.regexp.lastIndex=l-i.from;(h=this.regexp.exec(i.text))&&h.indexthis.addMatch(i,t,e,u));e=e.update({filterFrom:l,filterTo:a,filter:(t,e)=>ta,add:c})}}return e}}const Qr=null!=/x/.unicode?"gu":"g",jr=new RegExp("[\0-\b\n--Ÿ­؜​‎‏\u2028\u2029‭‮⁦⁧⁩\ufeff-]",Qr),Kr={0:"null",7:"bell",8:"backspace",10:"newline",11:"vertical tab",13:"carriage return",27:"escape",8203:"zero width space",8204:"zero width non-joiner",8205:"zero width joiner",8206:"left-to-right mark",8207:"right-to-left mark",8232:"line separator",8237:"left-to-right override",8238:"right-to-left override",8294:"left-to-right isolate",8295:"right-to-left isolate",8297:"pop directional isolate",8233:"paragraph separator",65279:"zero width no-break space",65532:"object replacement"};let Xr=null;const Gr=V.define({combine(t){let e=Tt(t,{render:null,specialChars:jr,addSpecialChars:null});return(e.replaceTabs=!function(){var t;if(null==Xr&&"undefined"!=typeof document&&document.body){let e=document.body.style;Xr=null!=(null!==(t=e.tabSize)&&void 0!==t?t:e.MozTabSize)}return Xr||!1}())&&(e.specialChars=new RegExp("\t|"+e.specialChars.source,Qr)),e.addSpecialChars&&(e.specialChars=new RegExp(e.specialChars.source+"|"+e.addSpecialChars.source,Qr)),e}});function Yr(t={}){return[Gr.of(t),Jr||(Jr=Fi.fromClass(class{constructor(t){this.view=t,this.decorations=Oe.none,this.decorationCache=Object.create(null),this.decorator=this.makeDecorator(t.state.facet(Gr)),this.decorations=this.decorator.createDeco(t)}makeDecorator(t){return new Ur({regexp:t.specialChars,decoration:(e,i,n)=>{let{doc:s}=i.state,r=k(e[0],0);if(9==r){let t=s.lineAt(n),e=i.state.tabSize,r=Qt(t.text,e,n-t.from);return Oe.replace({widget:new to((e-r%e)*this.view.defaultCharacterWidth/this.view.scaleX)})}return this.decorationCache[r]||(this.decorationCache[r]=Oe.replace({widget:new Zr(t,r)}))},boundary:t.replaceTabs?void 0:/[^]/})}update(t){let e=t.state.facet(Gr);t.startState.facet(Gr)!=e?(this.decorator=this.makeDecorator(e),this.decorations=this.decorator.createDeco(t.view)):this.decorations=this.decorator.updateDeco(t,this.decorations)}},{decorations:t=>t.decorations}))]}let Jr=null;class Zr extends Ae{constructor(t,e){super(),this.options=t,this.code=e}eq(t){return t.code==this.code}toDOM(t){let e=function(t){return t>=32?"•":10==t?"␤":String.fromCharCode(9216+t)}(this.code),i=t.state.phrase("Control character")+" "+(Kr[this.code]||"0x"+this.code.toString(16)),n=this.options.render&&this.options.render(this.code,i,e);if(n)return n;let s=document.createElement("span");return s.textContent=e,s.title=i,s.setAttribute("aria-label",i),s.className="cm-specialChar",s}ignoreEvent(){return!1}}class to extends Ae{constructor(t){super(),this.width=t}eq(t){return t.width==this.width}toDOM(){let t=document.createElement("span");return t.textContent="\t",t.className="cm-tab",t.style.width=this.width+"px",t}ignoreEvent(){return!1}}const eo=Oe.line({class:"cm-activeLine"}),io=Fi.fromClass(class{constructor(t){this.decorations=this.getDeco(t)}update(t){(t.docChanged||t.selectionSet)&&(this.decorations=this.getDeco(t.view))}getDeco(t){let e=-1,i=[];for(let n of t.state.selection.ranges){let s=t.lineBlockAt(n.head);s.from>e&&(i.push(eo.range(s.from)),e=s.from)}return Oe.set(i)}},{decorations:t=>t.decorations}),no=2e3;function so(t,e){let i=t.posAtCoords({x:e.clientX,y:e.clientY},!1),n=t.state.doc.lineAt(i),s=i-n.from,r=s>no?-1:s==n.length?function(t,e){let i=t.coordsAtPos(t.viewport.from);return i?Math.round(Math.abs((i.left-e)/t.defaultCharacterWidth)):-1}(t,e.clientX):Qt(n.text,t.state.tabSize,i-n.from);return{line:n.number,col:r,off:s}}function ro(t,e){let i=so(t,e),n=t.state.selection;return i?{update(t){if(t.docChanged){let e=t.changes.mapPos(t.startState.doc.line(i.line).from),s=t.state.doc.lineAt(e);i={line:s.number,col:i.col,off:Math.min(i.off,s.length)},n=n.map(t.changes)}},get(e,s,r){let o=so(t,e);if(!o)return n;let l=function(t,e,i){let n=Math.min(e.line,i.line),s=Math.max(e.line,i.line),r=[];if(e.off>no||i.off>no||e.col<0||i.col<0){let o=Math.min(e.off,i.off),l=Math.max(e.off,i.off);for(let e=n;e<=s;e++){let i=t.doc.line(e);i.length<=l&&r.push(N.range(i.from+o,i.to+l))}}else{let o=Math.min(e.col,i.col),l=Math.max(e.col,i.col);for(let e=n;e<=s;e++){let i=t.doc.line(e),n=jt(i.text,o,t.tabSize,!0);if(n<0)r.push(N.cursor(i.to));else{let e=jt(i.text,l,t.tabSize);r.push(N.range(i.from+n,i.from+e))}}}return r}(t.state,i,o);return l.length?r?N.create(l.concat(n.ranges)):N.create(l):n}}:null}function oo(t){let e=(null==t?void 0:t.eventFilter)||(t=>t.altKey&&0==t.button);return dr.mouseSelectionStyle.of((t,i)=>e(i)?ro(t,i):null)}const lo={Alt:[18,t=>!!t.altKey],Control:[17,t=>!!t.ctrlKey],Shift:[16,t=>!!t.shiftKey],Meta:[91,t=>!!t.metaKey]},ao={style:"cursor: crosshair"};function ho(t={}){let[e,i]=lo[t.key||"Alt"],n=Fi.fromClass(class{constructor(t){this.view=t,this.isDown=!1}set(t){this.isDown!=t&&(this.isDown=t,this.view.update([]))}},{eventObservers:{keydown(t){this.set(t.keyCode==e||i(t))},keyup(t){t.keyCode!=e&&i(t)||this.set(!1)},mousemove(t){this.set(i(t))}}});return[n,dr.contentAttributes.of(t=>{var e;return(null===(e=t.plugin(n))||void 0===e?void 0:e.isDown)?ao:null})]}const co="-10000px";class uo{constructor(t,e,i,n){this.facet=e,this.createTooltipView=i,this.removeTooltipView=n,this.input=t.state.facet(e),this.tooltips=this.input.filter(t=>t);let s=null;this.tooltipViews=this.tooltips.map(t=>s=i(t,s))}update(t,e){var i;let n=t.state.facet(this.facet),s=n.filter(t=>t);if(n===this.input){for(let e of this.tooltipViews)e.update&&e.update(t);return!1}let r=[],o=e?[]:null;for(let i=0;ie[i]=t),e.length=o.length),this.input=n,this.tooltips=s,this.tooltipViews=r,!0}}function fo(t={}){return mo.of(t)}function po(t){let e=t.dom.ownerDocument.documentElement;return{top:0,left:0,bottom:e.clientHeight,right:e.clientWidth}}const mo=V.define({combine:t=>{var e,i,n;return{position:be.ios?"absolute":(null===(e=t.find(t=>t.position))||void 0===e?void 0:e.position)||"fixed",parent:(null===(i=t.find(t=>t.parent))||void 0===i?void 0:i.parent)||null,tooltipSpace:(null===(n=t.find(t=>t.tooltipSpace))||void 0===n?void 0:n.tooltipSpace)||po}}}),go=new WeakMap,vo=Fi.fromClass(class{constructor(t){this.view=t,this.above=[],this.inView=!0,this.madeAbsolute=!1,this.lastTransaction=0,this.measureTimeout=-1;let e=t.state.facet(mo);this.position=e.position,this.parent=e.parent,this.classes=t.themeClasses,this.createContainer(),this.measureReq={read:this.readMeasure.bind(this),write:this.writeMeasure.bind(this),key:this},this.resizeObserver="function"==typeof ResizeObserver?new ResizeObserver(()=>this.measureSoon()):null,this.manager=new uo(t,xo,(t,e)=>this.createTooltip(t,e),t=>{this.resizeObserver&&this.resizeObserver.unobserve(t.dom),t.dom.remove()}),this.above=this.manager.tooltips.map(t=>!!t.above),this.intersectionObserver="function"==typeof IntersectionObserver?new IntersectionObserver(t=>{Date.now()>this.lastTransaction-50&&t.length>0&&t[t.length-1].intersectionRatio<1&&this.measureSoon()},{threshold:[1]}):null,this.observeIntersection(),t.win.addEventListener("resize",this.measureSoon=this.measureSoon.bind(this)),this.maybeMeasure()}createContainer(){this.parent?(this.container=document.createElement("div"),this.container.style.position="relative",this.container.className=this.view.themeClasses,this.parent.appendChild(this.container)):this.container=this.view.dom}observeIntersection(){if(this.intersectionObserver){this.intersectionObserver.disconnect();for(let t of this.manager.tooltipViews)this.intersectionObserver.observe(t.dom)}}measureSoon(){this.measureTimeout<0&&(this.measureTimeout=setTimeout(()=>{this.measureTimeout=-1,this.maybeMeasure()},50))}update(t){t.transactions.length&&(this.lastTransaction=Date.now());let e=this.manager.update(t,this.above);e&&this.observeIntersection();let i=e||t.geometryChanged,n=t.state.facet(mo);if(n.position!=this.position&&!this.madeAbsolute){this.position=n.position;for(let t of this.manager.tooltipViews)t.dom.style.position=this.position;i=!0}if(n.parent!=this.parent){this.parent&&this.container.remove(),this.parent=n.parent,this.createContainer();for(let t of this.manager.tooltipViews)this.container.appendChild(t.dom);i=!0}else this.parent&&this.view.themeClasses!=this.classes&&(this.classes=this.container.className=this.view.themeClasses);i&&this.maybeMeasure()}createTooltip(t,e){let i=t.create(this.view),n=e?e.dom:null;if(i.dom.classList.add("cm-tooltip"),t.arrow&&!i.dom.querySelector(".cm-tooltip > .cm-tooltip-arrow")){let t=document.createElement("div");t.className="cm-tooltip-arrow",i.dom.appendChild(t)}return i.dom.style.position=this.position,i.dom.style.top=co,i.dom.style.left="0px",this.container.insertBefore(i.dom,n),i.mount&&i.mount(this.view),this.resizeObserver&&this.resizeObserver.observe(i.dom),i}destroy(){var t,e,i;this.view.win.removeEventListener("resize",this.measureSoon);for(let e of this.manager.tooltipViews)e.dom.remove(),null===(t=e.destroy)||void 0===t||t.call(e);this.parent&&this.container.remove(),null===(e=this.resizeObserver)||void 0===e||e.disconnect(),null===(i=this.intersectionObserver)||void 0===i||i.disconnect(),clearTimeout(this.measureTimeout)}readMeasure(){let t=1,e=1,i=!1;if("fixed"==this.position&&this.manager.tooltipViews.length){let{dom:t}=this.manager.tooltipViews[0];if(be.safari){let e=t.getBoundingClientRect();i=Math.abs(e.top+1e4)>1||Math.abs(e.left)>1}else i=!!t.offsetParent&&t.offsetParent!=this.container.ownerDocument.body}if(i||"absolute"==this.position)if(this.parent){let i=this.parent.getBoundingClientRect();i.width&&i.height&&(t=i.width/this.parent.offsetWidth,e=i.height/this.parent.offsetHeight)}else({scaleX:t,scaleY:e}=this.view.viewState);let n=this.view.scrollDOM.getBoundingClientRect(),s=Ji(this.view);return{visible:{left:n.left+s.left,top:n.top+s.top,right:n.right-s.right,bottom:n.bottom-s.bottom},parent:this.parent?this.container.getBoundingClientRect():this.view.dom.getBoundingClientRect(),pos:this.manager.tooltips.map((t,e)=>{let i=this.manager.tooltipViews[e];return i.getCoords?i.getCoords(t.pos):this.view.coordsAtPos(t.pos)}),size:this.manager.tooltipViews.map(({dom:t})=>t.getBoundingClientRect()),space:this.view.state.facet(mo).tooltipSpace(this.view),scaleX:t,scaleY:e,makeAbsolute:i}}writeMeasure(t){var e;if(t.makeAbsolute){this.madeAbsolute=!0,this.position="absolute";for(let t of this.manager.tooltipViews)t.dom.style.position="absolute"}let{visible:i,space:n,scaleX:s,scaleY:r}=t,o=[];for(let l=0;l=Math.min(i.bottom,n.bottom)||u.rightMath.min(i.right,n.right)+.1)){c.style.top=co;continue}let d=a.arrow?h.dom.querySelector(".cm-tooltip-arrow"):null,p=d?7:0,m=f.right-f.left,g=null!==(e=go.get(h))&&void 0!==e?e:f.bottom-f.top,v=h.offset||yo,w=this.view.textDirection==ni.LTR,b=f.width>n.right-n.left?w?n.left:n.right-f.width:w?Math.max(n.left,Math.min(u.left-(d?14:0)+v.x,n.right-m)):Math.min(Math.max(n.left,u.left-m+(d?14:0)-v.x),n.right-m),y=this.above[l];!a.strictSide&&(y?u.top-g-p-v.yn.bottom)&&y==n.bottom-u.bottom>u.top-n.top&&(y=this.above[l]=!y);let x=(y?u.top-n.top:n.bottom-u.bottom)-p;if(xb&&t.topk&&(k=y?t.top-g-2-p:t.bottom+p+2);if("absolute"==this.position?(c.style.top=(k-t.parent.top)/r+"px",wo(c,(b-t.parent.left)/s)):(c.style.top=k/r+"px",wo(c,b/s)),d){let t=u.left+(w?v.x:-v.x)-(b+14-7);d.style.left=t/s+"px"}!0!==h.overlap&&o.push({left:b,top:k,right:S,bottom:k+g}),c.classList.toggle("cm-tooltip-above",y),c.classList.toggle("cm-tooltip-below",!y),h.positioned&&h.positioned(t.space)}}maybeMeasure(){if(this.manager.tooltips.length&&(this.view.inView&&this.view.requestMeasure(this.measureReq),this.inView!=this.view.inView&&(this.inView=this.view.inView,!this.inView)))for(let t of this.manager.tooltipViews)t.dom.style.top=co}},{eventObservers:{scroll(){this.maybeMeasure()}}});function wo(t,e){let i=parseInt(t.style.left,10);(isNaN(i)||Math.abs(e-i)>1)&&(t.style.left=e+"px")}const bo=dr.baseTheme({".cm-tooltip":{zIndex:500,boxSizing:"border-box"},"&light .cm-tooltip":{border:"1px solid #bbb",backgroundColor:"#f5f5f5"},"&light .cm-tooltip-section:not(:first-child)":{borderTop:"1px solid #bbb"},"&dark .cm-tooltip":{backgroundColor:"#333338",color:"white"},".cm-tooltip-arrow":{height:"7px",width:"14px",position:"absolute",zIndex:-1,overflow:"hidden","&:before, &:after":{content:"''",position:"absolute",width:0,height:0,borderLeft:"7px solid transparent",borderRight:"7px solid transparent"},".cm-tooltip-above &":{bottom:"-7px","&:before":{borderTop:"7px solid #bbb"},"&:after":{borderTop:"7px solid #f5f5f5",bottom:"1px"}},".cm-tooltip-below &":{top:"-7px","&:before":{borderBottom:"7px solid #bbb"},"&:after":{borderBottom:"7px solid #f5f5f5",top:"1px"}}},"&dark .cm-tooltip .cm-tooltip-arrow":{"&:before":{borderTopColor:"#333338",borderBottomColor:"#333338"},"&:after":{borderTopColor:"transparent",borderBottomColor:"transparent"}}}),yo={x:0,y:0},xo=V.define({enables:[vo,bo]}),ko=V.define({combine:t=>t.reduce((t,e)=>t.concat(e),[])});class So{static create(t){return new So(t)}constructor(t){this.view=t,this.mounted=!1,this.dom=document.createElement("div"),this.dom.classList.add("cm-tooltip-hover"),this.manager=new uo(t,ko,(t,e)=>this.createHostedView(t,e),t=>t.dom.remove())}createHostedView(t,e){let i=t.create(this.view);return i.dom.classList.add("cm-tooltip-section"),this.dom.insertBefore(i.dom,e?e.dom.nextSibling:this.dom.firstChild),this.mounted&&i.mount&&i.mount(this.view),i}mount(t){for(let e of this.manager.tooltipViews)e.mount&&e.mount(t);this.mounted=!0}positioned(t){for(let e of this.manager.tooltipViews)e.positioned&&e.positioned(t)}update(t){this.manager.update(t)}destroy(){var t;for(let e of this.manager.tooltipViews)null===(t=e.destroy)||void 0===t||t.call(e)}passProp(t){let e;for(let i of this.manager.tooltipViews){let n=i[t];if(void 0!==n)if(void 0===e)e=n;else if(e!==n)return}return e}get offset(){return this.passProp("offset")}get getCoords(){return this.passProp("getCoords")}get overlap(){return this.passProp("overlap")}get resize(){return this.passProp("resize")}}const Co=xo.compute([ko],t=>{let e=t.facet(ko);return 0===e.length?null:{pos:Math.min(...e.map(t=>t.pos)),end:Math.max(...e.map(t=>{var e;return null!==(e=t.end)&&void 0!==e?e:t.pos})),create:So.create,above:e[0].above,arrow:e.some(t=>t.arrow)}}),Ao=V.define();class Mo{constructor(t,e,i,n,s,r){this.view=t,this.source=e,this.field=i,this.locked=n,this.setHover=s,this.hoverTime=r,this.hoverTimeout=-1,this.restartTimeout=-1,this.pending=null,this.lastMove={x:0,y:0,target:t.dom,time:0},this.checkHover=this.checkHover.bind(this),t.dom.addEventListener("mouseleave",this.mouseleave=this.mouseleave.bind(this)),t.dom.addEventListener("mousemove",this.mousemove=this.mousemove.bind(this))}update(t){this.pending&&(this.pending=null,clearTimeout(this.restartTimeout),this.restartTimeout=setTimeout(()=>this.startHover(),20))}get active(){return this.view.state.field(this.field)}checkHover(){if(this.hoverTimeout=-1,this.active.length)return;let t=Date.now()-this.lastMove.time;ti.bottom||e.xi.right+t.defaultCharacterWidth)return;let r=t.bidiSpans(t.state.doc.lineAt(n)).find(t=>t.from<=n&&t.to>=n),o=r&&r.dir==ni.RTL?-1:1;s=e.x{if(e&&(!Array.isArray(e)||e.length)){let i=Array.isArray(e)?e:[e];n&&this.locked.set(i,n),t.dispatch({effects:this.setHover.of(i)})}};if(s&&"then"in s){let i=this.pending={pos:e};s.then(t=>{this.pending==i&&(this.pending=null,r(t))},e=>Wi(t.state,e,"hover tooltip"))}else r(s)}get tooltip(){let t=this.view.plugin(vo),e=t?t.manager.tooltips.findIndex(t=>t.create==So.create):-1;return e>-1?t.manager.tooltipViews[e]:null}mousemove(t){var e,i;this.lastMove={x:t.clientX,y:t.clientY,target:t.target,time:Date.now()},this.hoverTimeout<0&&(this.hoverTimeout=setTimeout(this.checkHover,this.hoverTime));let{active:n,tooltip:s}=this;if(n.length&&!this.locked.has(n)&&s&&!function(t,e){let i,{left:n,right:s,top:r,bottom:o}=t.getBoundingClientRect();if(i=t.querySelector(".cm-tooltip-arrow")){let t=i.getBoundingClientRect();r=Math.min(t.top,r),o=Math.max(t.bottom,o)}return e.clientX>=n-Oo&&e.clientX<=s+Oo&&e.clientY>=r-Oo&&e.clientY<=o+Oo}(s.dom,t)||this.pending){let{pos:s}=n[0]||this.pending,r=null!==(i=null===(e=n[0])||void 0===e?void 0:e.end)&&void 0!==i?i:s;(s==r?this.view.posAtCoords(this.lastMove)==s:function(t,e,i,n,s){let r=t.scrollDOM.getBoundingClientRect(),o=t.documentTop+t.documentPadding.top+t.contentHeight;if(r.left>n||r.rights||Math.min(r.bottom,o)=e&&l<=i}(this.view,s,r,t.clientX,t.clientY))||(this.view.dispatch({effects:this.setHover.of([])}),this.pending=null)}}mouseleave(t){clearTimeout(this.hoverTimeout),this.hoverTimeout=-1;let{active:e}=this;if(e.length&&!this.locked.has(e)){let{tooltip:e}=this;e&&e.dom.contains(t.relatedTarget)?this.watchTooltipLeave(e.dom):this.view.dispatch({effects:this.setHover.of([])})}}watchTooltipLeave(t){let e=i=>{t.removeEventListener("mouseleave",e);let{active:n}=this;!n.length||this.locked.has(n)||this.view.dom.contains(i.relatedTarget)||this.view.dispatch({effects:this.setHover.of([])})};t.addEventListener("mouseleave",e)}destroy(){clearTimeout(this.hoverTimeout),clearTimeout(this.restartTimeout),this.view.dom.removeEventListener("mouseleave",this.mouseleave),this.view.dom.removeEventListener("mousemove",this.mousemove)}}const Oo=4;function To(t,e={}){let i=mt.define(),n=new WeakMap,s=Q.define({create:()=>[],update(t,r){let o=n.get(t);if(t.length&&(e.hideOnChange&&(r.docChanged||r.selection)||o&&o(r)?t=[]:e.hideOn&&(t=t.filter(t=>!e.hideOn(r,t)))),r.docChanged&&t.length){let e=[];for(let i of t){let t=r.changes.mapPos(i.pos,-1,M.TrackDel);if(null!=t){let n=Object.assign(Object.create(null),i);n.pos=t,null!=n.end&&(n.end=r.changes.mapPos(n.end)),e.push(n)}}t=e}for(let e of r.effects)e.is(i)&&(t=e.value,o=void 0),(e.is(Ro)&&!e.value||e.value==s)&&(t=[]);return t.length&&o&&n.set(t,o),t},provide:t=>ko.from(t)});const r=Fi.define(r=>new Mo(r,t,s,n,i,e.hoverTime||300));return{active:s,extension:[s,r,Ao.of(r),Co]}}function Do(t,e){let i=t.plugin(vo);if(!i)return null;let n=i.manager.tooltips.indexOf(e);return n<0?null:i.manager.tooltipViews[n]}const Ro=mt.define(),Po=V.define({combine(t){let e,i;for(let n of t)e=e||n.topContainer,i=i||n.bottomContainer;return{topContainer:e,bottomContainer:i}}});function Bo(t,e){let i=t.plugin(Eo),n=i?i.specs.indexOf(e):-1;return n>-1?i.panels[n]:null}const Eo=Fi.fromClass(class{constructor(t){this.input=t.state.facet(No),this.specs=this.input.filter(t=>t),this.panels=this.specs.map(e=>e(t));let e=t.state.facet(Po);this.top=new Lo(t,!0,e.topContainer),this.bottom=new Lo(t,!1,e.bottomContainer),this.top.sync(this.panels.filter(t=>t.top)),this.bottom.sync(this.panels.filter(t=>!t.top));for(let t of this.panels)t.dom.classList.add("cm-panel"),t.mount&&t.mount()}update(t){let e=t.state.facet(Po);this.top.container!=e.topContainer&&(this.top.sync([]),this.top=new Lo(t.view,!0,e.topContainer)),this.bottom.container!=e.bottomContainer&&(this.bottom.sync([]),this.bottom=new Lo(t.view,!1,e.bottomContainer)),this.top.syncClasses(),this.bottom.syncClasses();let i=t.state.facet(No);if(i!=this.input){let e=i.filter(t=>t),n=[],s=[],r=[],o=[];for(let i of e){let e,l=this.specs.indexOf(i);l<0?(e=i(t.view),o.push(e)):(e=this.panels[l],e.update&&e.update(t)),n.push(e),(e.top?s:r).push(e)}this.specs=e,this.panels=n,this.top.sync(s),this.bottom.sync(r);for(let t of o)t.dom.classList.add("cm-panel"),t.mount&&t.mount()}else for(let e of this.panels)e.update&&e.update(t)}destroy(){this.top.sync([]),this.bottom.sync([])}},{provide:t=>dr.scrollMargins.of(e=>{let i=e.plugin(t);return i&&{top:i.top.scrollMargin(),bottom:i.bottom.scrollMargin()}})});class Lo{constructor(t,e,i){this.view=t,this.top=e,this.container=i,this.dom=void 0,this.classes="",this.panels=[],this.syncClasses()}sync(t){for(let e of this.panels)e.destroy&&t.indexOf(e)<0&&e.destroy();this.panels=t,this.syncDOM()}syncDOM(){if(0==this.panels.length)return void(this.dom&&(this.dom.remove(),this.dom=void 0));if(!this.dom){this.dom=document.createElement("div"),this.dom.className=this.top?"cm-panels cm-panels-top":"cm-panels cm-panels-bottom",this.dom.style[this.top?"top":"bottom"]="0";let t=this.container||this.view.dom;t.insertBefore(this.dom,this.top?t.firstChild:null)}let t=this.dom.firstChild;for(let e of this.panels)if(e.dom.parentNode==this.dom){for(;t!=e.dom;)t=Io(t);t=t.nextSibling}else this.dom.insertBefore(e.dom,t);for(;t;)t=Io(t)}scrollMargin(){return!this.dom||this.container?0:Math.max(0,this.top?this.dom.getBoundingClientRect().bottom-Math.max(0,this.view.scrollDOM.getBoundingClientRect().top):Math.min(innerHeight,this.view.scrollDOM.getBoundingClientRect().bottom)-this.dom.getBoundingClientRect().top)}syncClasses(){if(this.container&&this.classes!=this.view.themeClasses){for(let t of this.classes.split(" "))t&&this.container.classList.remove(t);for(let t of(this.classes=this.view.themeClasses).split(" "))t&&this.container.classList.add(t)}}}function Io(t){let e=t.nextSibling;return t.remove(),e}const No=V.define({enables:Eo});function Wo(t,e){let i,n=new Promise(t=>i=t),s=t=>function(t,e,i){let n=e.content?e.content(t,()=>o(null)):null;if(!n){if(n=oe("form"),e.input){let t=oe("input",e.input);/^(text|password|number|email|tel|url)$/.test(t.type)&&t.classList.add("cm-textfield"),t.name||(t.name="input"),n.appendChild(oe("label",(e.label||"")+": ",t))}else n.appendChild(document.createTextNode(e.label||""));n.appendChild(document.createTextNode(" ")),n.appendChild(oe("button",{class:"cm-button",type:"submit"},e.submitLabel||"OK"))}let s="FORM"==n.nodeName?[n]:n.querySelectorAll("form");for(let t=0;t{27==t.keyCode?(t.preventDefault(),o(null)):13==t.keyCode&&(t.preventDefault(),o(e))}),e.addEventListener("submit",t=>{t.preventDefault(),o(e)})}let r=oe("div",n,oe("button",{onclick:()=>o(null),"aria-label":t.state.phrase("close"),class:"cm-dialog-close",type:"button"},["×"]));e.class&&(r.className=e.class);function o(e){r.contains(r.ownerDocument.activeElement)&&t.focus(),i(e)}return r.classList.add("cm-dialog"),{dom:r,top:e.top,mount:()=>{if(e.focus){let t;t="string"==typeof e.focus?n.querySelector(e.focus):n.querySelector("input")||n.querySelector("button"),t&&"select"in t?t.select():t&&"focus"in t&&t.focus()}}}}(t,e,i);t.state.field(Ho,!1)?t.dispatch({effects:Vo.of(s)}):t.dispatch({effects:mt.appendConfig.of(Ho.init(()=>[s]))});let r=zo.of(s);return{close:r,result:n.then(e=>((t.win.queueMicrotask||(e=>t.win.setTimeout(e,10)))(()=>{t.state.field(Ho).indexOf(s)>-1&&t.dispatch({effects:r})}),e))}}const Ho=Q.define({create:()=>[],update(t,e){for(let i of e.effects)i.is(Vo)?t=[i.value].concat(t):i.is(zo)&&(t=t.filter(t=>t!=i.value));return t},provide:t=>No.computeN([t],e=>e.field(t))}),Vo=mt.define(),zo=mt.define();class Fo extends Dt{compare(t){return this==t||this.constructor==t.constructor&&this.eq(t)}eq(t){return!1}destroy(t){}}Fo.prototype.elementClass="",Fo.prototype.toDOM=void 0,Fo.prototype.mapMode=M.TrackBefore,Fo.prototype.startSide=Fo.prototype.endSide=-1,Fo.prototype.point=!0;const qo=V.define(),_o=V.define(),$o={class:"",renderEmptyElements:!1,elementStyle:"",markers:()=>Lt.empty,lineMarker:()=>null,widgetMarker:()=>null,lineMarkerChange:null,initialSpacer:null,updateSpacer:null,domEventHandlers:{},side:"before"},Uo=V.define();function Qo(t){return[Ko(),Uo.of({...$o,...t})]}const jo=V.define({combine:t=>t.some(t=>t)});function Ko(t){let e=[Xo];return t&&!1===t.fixed&&e.push(jo.of(!0)),e}const Xo=Fi.fromClass(class{constructor(t){this.view=t,this.domAfter=null,this.prevViewport=t.viewport,this.dom=document.createElement("div"),this.dom.className="cm-gutters cm-gutters-before",this.dom.setAttribute("aria-hidden","true"),this.dom.style.minHeight=this.view.contentHeight/this.view.scaleY+"px",this.gutters=t.state.facet(Uo).map(e=>new Zo(t,e)),this.fixed=!t.state.facet(jo);for(let t of this.gutters)"after"==t.config.side?this.getDOMAfter().appendChild(t.dom):this.dom.appendChild(t.dom);this.fixed&&(this.dom.style.position="sticky"),this.syncGutters(!1),t.scrollDOM.insertBefore(this.dom,t.contentDOM)}getDOMAfter(){return this.domAfter||(this.domAfter=document.createElement("div"),this.domAfter.className="cm-gutters cm-gutters-after",this.domAfter.setAttribute("aria-hidden","true"),this.domAfter.style.minHeight=this.view.contentHeight/this.view.scaleY+"px",this.domAfter.style.position=this.fixed?"sticky":"",this.view.scrollDOM.appendChild(this.domAfter)),this.domAfter}update(t){if(this.updateGutters(t)){let e=this.prevViewport,i=t.view.viewport,n=Math.min(e.to,i.to)-Math.max(e.from,i.from);this.syncGutters(n<.8*(i.to-i.from))}if(t.geometryChanged){let t=this.view.contentHeight/this.view.scaleY+"px";this.dom.style.minHeight=t,this.domAfter&&(this.domAfter.style.minHeight=t)}this.view.state.facet(jo)!=!this.fixed&&(this.fixed=!this.fixed,this.dom.style.position=this.fixed?"sticky":"",this.domAfter&&(this.domAfter.style.position=this.fixed?"sticky":"")),this.prevViewport=t.view.viewport}syncGutters(t){let e=this.dom.nextSibling;t&&(this.dom.remove(),this.domAfter&&this.domAfter.remove());let i=Lt.iter(this.view.state.facet(qo),this.view.viewport.from),n=[],s=this.gutters.map(t=>new Jo(t,this.view.viewport,-this.view.documentPadding.top));for(let t of this.view.viewportLineBlocks)if(n.length&&(n=[]),Array.isArray(t.type)){let e=!0;for(let r of t.type)if(r.type==Me.Text&&e){Yo(i,n,r.from);for(let t of s)t.line(this.view,r,n);e=!1}else if(r.widget)for(let t of s)t.widget(this.view,r)}else if(t.type==Me.Text){Yo(i,n,t.from);for(let e of s)e.line(this.view,t,n)}else if(t.widget)for(let e of s)e.widget(this.view,t);for(let t of s)t.finish();t&&(this.view.scrollDOM.insertBefore(this.dom,e),this.domAfter&&this.view.scrollDOM.appendChild(this.domAfter))}updateGutters(t){let e=t.startState.facet(Uo),i=t.state.facet(Uo),n=t.docChanged||t.heightChanged||t.viewportChanged||!Lt.eq(t.startState.facet(qo),t.state.facet(qo),t.view.viewport.from,t.view.viewport.to);if(e==i)for(let e of this.gutters)e.update(t)&&(n=!0);else{n=!0;let s=[];for(let n of i){let i=e.indexOf(n);i<0?s.push(new Zo(this.view,n)):(this.gutters[i].update(t),s.push(this.gutters[i]))}for(let t of this.gutters)t.dom.remove(),s.indexOf(t)<0&&t.destroy();for(let t of s)"after"==t.config.side?this.getDOMAfter().appendChild(t.dom):this.dom.appendChild(t.dom);this.gutters=s}return n}destroy(){for(let t of this.gutters)t.destroy();this.dom.remove(),this.domAfter&&this.domAfter.remove()}},{provide:t=>dr.scrollMargins.of(e=>{let i=e.plugin(t);if(!i||0==i.gutters.length||!i.fixed)return null;let n=i.dom.offsetWidth*e.scaleX,s=i.domAfter?i.domAfter.offsetWidth*e.scaleX:0;return e.textDirection==ni.LTR?{left:n,right:s}:{right:n,left:s}})});function Go(t){return Array.isArray(t)?t:[t]}function Yo(t,e,i){for(;t.value&&t.from<=i;)t.from==i&&e.push(t.value),t.next()}class Jo{constructor(t,e,i){this.gutter=t,this.height=i,this.i=0,this.cursor=Lt.iter(t.markers,e.from)}addElement(t,e,i){let{gutter:n}=this,s=(e.top-this.height)/t.scaleY,r=e.height/t.scaleY;if(this.i==n.elements.length){let e=new tl(t,r,s,i);n.elements.push(e),n.dom.appendChild(e.dom)}else n.elements[this.i].update(t,r,s,i);this.height=e.bottom,this.i++}line(t,e,i){let n=[];Yo(this.cursor,n,e.from),i.length&&(n=n.concat(i));let s=this.gutter.config.lineMarker(t,e,n);s&&n.unshift(s);let r=this.gutter;(0!=n.length||r.config.renderEmptyElements)&&this.addElement(t,e,n)}widget(t,e){let i=this.gutter.config.widgetMarker(t,e.widget,e),n=i?[i]:null;for(let i of t.state.facet(_o)){let s=i(t,e.widget,e);s&&(n||(n=[])).push(s)}n&&this.addElement(t,e,n)}finish(){let t=this.gutter;for(;t.elements.length>this.i;){let e=t.elements.pop();t.dom.removeChild(e.dom),e.destroy()}}}class Zo{constructor(t,e){this.view=t,this.config=e,this.elements=[],this.spacer=null,this.dom=document.createElement("div"),this.dom.className="cm-gutter"+(this.config.class?" "+this.config.class:"");for(let i in e.domEventHandlers)this.dom.addEventListener(i,n=>{let s,r=n.target;if(r!=this.dom&&this.dom.contains(r)){for(;r.parentNode!=this.dom;)r=r.parentNode;let t=r.getBoundingClientRect();s=(t.top+t.bottom)/2}else s=n.clientY;let o=t.lineBlockAtHeight(s-t.documentTop);e.domEventHandlers[i](t,o,n)&&n.preventDefault()});this.markers=Go(e.markers(t)),e.initialSpacer&&(this.spacer=new tl(t,0,0,[e.initialSpacer(t)]),this.dom.appendChild(this.spacer.dom),this.spacer.dom.style.cssText+="visibility: hidden; pointer-events: none")}update(t){let e=this.markers;if(this.markers=Go(this.config.markers(t.view)),this.spacer&&this.config.updateSpacer){let e=this.config.updateSpacer(this.spacer.markers[0],t);e!=this.spacer.markers[0]&&this.spacer.update(t.view,0,0,[e])}let i=t.view.viewport;return!Lt.eq(this.markers,e,i.from,i.to)||!!this.config.lineMarkerChange&&this.config.lineMarkerChange(t)}destroy(){for(let t of this.elements)t.destroy()}}class tl{constructor(t,e,i,n){this.height=-1,this.above=0,this.markers=[],this.dom=document.createElement("div"),this.dom.className="cm-gutterElement",this.update(t,e,i,n)}update(t,e,i,n){this.height!=e&&(this.height=e,this.dom.style.height=e+"px"),this.above!=i&&(this.dom.style.marginTop=(this.above=i)?i+"px":""),function(t,e){if(t.length!=e.length)return!1;for(let i=0;iTt(t,{formatNumber:String,domEventHandlers:{}},{domEventHandlers(t,e){let i=Object.assign({},t);for(let t in e){let n=i[t],s=e[t];i[t]=n?(t,e,i)=>n(t,e,i)||s(t,e,i):s}return i}})});class sl extends Fo{constructor(t){super(),this.number=t}eq(t){return this.number==t.number}toDOM(){return document.createTextNode(this.number)}}function rl(t,e){return t.state.facet(nl).formatNumber(e,t.state)}const ol=Uo.compute([nl],t=>({class:"cm-lineNumbers",renderEmptyElements:!1,markers:t=>t.state.facet(el),lineMarker:(t,e,i)=>i.some(t=>t.toDOM)?null:new sl(rl(t,t.state.doc.lineAt(e.from).number)),widgetMarker:(t,e,i)=>{for(let n of t.state.facet(il)){let s=n(t,e,i);if(s)return s}return null},lineMarkerChange:t=>t.startState.facet(nl)!=t.state.facet(nl),initialSpacer:t=>new sl(rl(t,al(t.state.doc.lines))),updateSpacer(t,e){let i=rl(e.view,al(e.view.state.doc.lines));return i==t.number?t:new sl(i)},domEventHandlers:t.facet(nl).domEventHandlers,side:"before"}));function ll(t={}){return[nl.of(t),Ko(),ol]}function al(t){let e=9;for(;e{let e=[],i=-1;for(let n of t.selection.ranges){let s=t.doc.lineAt(n.head).from;s>i&&(i=s,e.push(hl.range(s)))}return Lt.of(e)});const ul=1024;let fl=0;class dl{constructor(t,e){this.from=t,this.to=e}}class pl{constructor(t={}){this.id=fl++,this.perNode=!!t.perNode,this.deserialize=t.deserialize||(()=>{throw new Error("This node type doesn't define a deserialize function")}),this.combine=t.combine||null}add(t){if(this.perNode)throw new RangeError("Can't add per-node props to node types");return"function"!=typeof t&&(t=vl.match(t)),e=>{let i=t(e);return void 0===i?null:[this,i]}}}pl.closedBy=new pl({deserialize:t=>t.split(" ")}),pl.openedBy=new pl({deserialize:t=>t.split(" ")}),pl.group=new pl({deserialize:t=>t.split(" ")}),pl.isolate=new pl({deserialize:t=>{if(t&&"rtl"!=t&&"ltr"!=t&&"auto"!=t)throw new RangeError("Invalid value for isolate: "+t);return t||"auto"}}),pl.contextHash=new pl({perNode:!0}),pl.lookAhead=new pl({perNode:!0}),pl.mounted=new pl({perNode:!0});class ml{constructor(t,e,i,n=!1){this.tree=t,this.overlay=e,this.parser=i,this.bracketed=n}static get(t){return t&&t.props&&t.props[pl.mounted.id]}}const gl=Object.create(null);class vl{constructor(t,e,i,n=0){this.name=t,this.props=e,this.id=i,this.flags=n}static define(t){let e=t.props&&t.props.length?Object.create(null):gl,i=(t.top?1:0)|(t.skipped?2:0)|(t.error?4:0)|(null==t.name?8:0),n=new vl(t.name||"",e,t.id,i);if(t.props)for(let i of t.props)if(Array.isArray(i)||(i=i(n)),i){if(i[0].perNode)throw new RangeError("Can't store a per-node prop on a node type");e[i[0].id]=i[1]}return n}prop(t){return this.props[t.id]}get isTop(){return(1&this.flags)>0}get isSkipped(){return(2&this.flags)>0}get isError(){return(4&this.flags)>0}get isAnonymous(){return(8&this.flags)>0}is(t){if("string"==typeof t){if(this.name==t)return!0;let e=this.prop(pl.group);return!!e&&e.indexOf(t)>-1}return this.id==t}static match(t){let e=Object.create(null);for(let i in t)for(let n of i.split(" "))e[n]=t[i];return t=>{for(let i=t.prop(pl.group),n=-1;n<(i?i.length:0);n++){let s=e[n<0?t.name:i[n]];if(s)return s}}}}vl.none=new vl("",Object.create(null),0,8);class wl{constructor(t){this.types=t;for(let e=0;e=e){let o=new Tl(r.tree,r.overlay[0].from+t.from,-1,t);(s||(s=[n])).push(Ml(o,e,i,!1))}}return s?El(s):n}(this,t,e)}iterate(t){let{enter:e,leave:i,from:n=0,to:s=this.length}=t,r=t.mode||0,o=(r&xl.IncludeAnonymous)>0;for(let t=this.cursor(r|xl.IncludeAnonymous);;){let r=!1;if(t.from<=s&&t.to>=n&&(!o&&t.type.isAnonymous||!1!==e(t))){if(t.firstChild())continue;r=!0}for(;r&&i&&(o||!t.type.isAnonymous)&&i(t),!t.nextSibling();){if(!t.parent())return;r=!0}}}prop(t){return t.perNode?this.props?this.props[t.id]:void 0:this.type.prop(t)}get propValues(){let t=[];if(this.props)for(let e in this.props)t.push([+e,this.props[e]]);return t}balance(t={}){return this.children.length<=8?this:Vl(vl.none,this.children,this.positions,0,this.children.length,0,this.length,(t,e,i)=>new kl(this.type,t,e,i,this.propValues),t.makeTree||((t,e,i)=>new kl(vl.none,t,e,i)))}static build(t){return function(t){var e;let{buffer:i,nodeSet:n,maxBufferLength:s=ul,reused:r=[],minRepeatType:o=n.types.length}=t,l=Array.isArray(i)?new Sl(i,i.length):i,a=n.types,h=0,c=0;function u(t,e,i,w,b,y){let{id:x,start:k,end:S,size:C}=l,A=c,M=h;if(C<0){if(l.next(),-1==C){let e=r[x];return i.push(e),void w.push(k-t)}if(-3==C)return void(h=x);if(-4==C)return void(c=x);throw new RangeError(`Unrecognized record size: ${C}`)}let O,T,D=a[x],R=k-t;if(S-k<=s&&(T=g(l.pos-e,b))){let e=new Uint16Array(T.size-T.skip),i=l.pos-T.size,s=e.length;for(;l.pos>i;)s=v(T.start,e,s);O=new Cl(e,S-T.start,n),R=T.start-t}else{let t=l.pos-C;l.next();let e=[],i=[],n=x>=o?x:-1,r=0,a=S;for(;l.pos>t;)n>=0&&l.id==n&&l.size>=0?(l.end<=a-s&&(p(e,i,k,r,l.end,a,n,A,M),r=e.length,a=l.end),l.next()):y>2500?f(k,t,e,i):u(k,t,e,i,n,y+1);if(n>=0&&r>0&&r-1&&r>0){let t=d(D,M);O=Vl(D,e,i,0,e.length,0,S-k,t,t)}else O=m(D,e,i,S-k,A-S,M)}i.push(O),w.push(R)}function f(t,e,i,r){let o=[],a=0,h=-1;for(;l.pos>e;){let{id:t,start:e,end:i,size:n}=l;if(n>4)l.next();else{if(h>-1&&e=0;t-=3)e[i++]=o[t],e[i++]=o[t+1]-s,e[i++]=o[t+2]-s,e[i++]=i;i.push(new Cl(e,o[2]-s,n)),r.push(s-t)}}function d(t,e){return(i,n,s)=>{let r,o,l=0,a=i.length-1;if(a>=0&&(r=i[a])instanceof kl){if(!a&&r.type==t&&r.length==s)return r;(o=r.prop(pl.lookAhead))&&(l=n[a]+r.length+o)}return m(t,i,n,s,l,e)}}function p(t,e,i,s,r,o,l,a,h){let c=[],u=[];for(;t.length>s;)c.push(t.pop()),u.push(e.pop()+i-r);t.push(m(n.types[l],c,u,o-r,a-o,h)),e.push(r-i)}function m(t,e,i,n,s,r,o){if(r){let t=[pl.contextHash,r];o=o?[t].concat(o):[t]}if(s>25){let t=[pl.lookAhead,s];o=o?[t].concat(o):[t]}return new kl(t,e,i,n,o)}function g(t,e){let i=l.fork(),n=0,r=0,a=0,h=i.end-s,c={size:0,start:0,skip:0};t:for(let s=i.pos-t;i.pos>s;){let t=i.size;if(i.id==e&&t>=0){c.size=n,c.start=r,c.skip=a,a+=4,n+=4,i.next();continue}let l=i.pos-t;if(t<0||l=o?4:0,f=i.start;for(i.next();i.pos>l;){if(i.size<0){if(-3!=i.size&&-4!=i.size)break t;u+=4}else i.id>=o&&(u+=4);i.next()}r=f,n+=t,a+=u}return(e<0||n==t)&&(c.size=n,c.start=r,c.skip=a),c.size>4?c:void 0}function v(t,e,i){let{id:n,start:s,end:r,size:a}=l;if(l.next(),a>=0&&n4){let n=l.pos-(a-4);for(;l.pos>n;)i=v(t,e,i)}e[--i]=o,e[--i]=r-t,e[--i]=s-t,e[--i]=n}else-3==a?h=n:-4==a&&(c=n);return i}let w=[],b=[];for(;l.pos>0;)u(t.start||0,t.bufferStart||0,w,b,-1,0);let y=null!==(e=t.length)&&void 0!==e?e:w.length?b[0]+w[0].length:0;return new kl(a[t.topID],w.reverse(),b.reverse(),y)}(t)}}kl.empty=new kl(vl.none,[],[],0);class Sl{constructor(t,e){this.buffer=t,this.index=e}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}get pos(){return this.index}next(){this.index-=4}fork(){return new Sl(this.buffer,this.index)}}class Cl{constructor(t,e,i){this.buffer=t,this.length=e,this.set=i}get type(){return vl.none}toString(){let t=[];for(let e=0;e0));l=r[l+3]);return o}slice(t,e,i){let n=this.buffer,s=new Uint16Array(e-t),r=0;for(let o=t,l=0;o=e&&ie;case 1:return i<=e&&n>e;case 2:return n>e;case 4:return!0}}function Ml(t,e,i,n){for(var s;t.from==t.to||(i<1?t.from>=e:t.from>e)||(i>-1?t.to<=e:t.to0?o.length:-1;t!=a;t+=e){let a,h=o[t],c=l[t]+r.from;if(s&xl.EnterBracketed&&h instanceof kl&&(a=ml.get(h))&&!a.overlay&&a.bracketed&&i>=c&&i<=c+h.length||Al(n,i,c,c+h.length))if(h instanceof Cl){if(s&xl.ExcludeBuffers)continue;let o=h.findChild(0,h.buffer.length,e,i-c,n);if(o>-1)return new Bl(new Pl(r,h,t,c),null,o)}else if(s&xl.IncludeAnonymous||!h.type.isAnonymous||Nl(h)){let o;if(!(s&xl.IgnoreMounts)&&(o=ml.get(h))&&!o.overlay)return new Tl(o.tree,c,t,r);let l=new Tl(h,c,t,r);return s&xl.IncludeAnonymous||!l.type.isAnonymous?l:l.nextChild(e<0?h.children.length-1:0,e,i,n,s)}}if(s&xl.IncludeAnonymous||!r.type.isAnonymous)return null;if(t=r.index>=0?r.index+e:e<0?-1:r._parent._tree.children.length,r=r._parent,!r)return null}}get firstChild(){return this.nextChild(0,1,0,4)}get lastChild(){return this.nextChild(this._tree.children.length-1,-1,0,4)}childAfter(t){return this.nextChild(0,1,t,2)}childBefore(t){return this.nextChild(this._tree.children.length-1,-1,t,-2)}prop(t){return this._tree.prop(t)}enter(t,e,i=0){let n;if(!(i&xl.IgnoreOverlays)&&(n=ml.get(this._tree))&&n.overlay){let s=t-this.from,r=i&xl.EnterBracketed&&n.bracketed;for(let{from:t,to:i}of n.overlay)if((e>0||r?t<=s:t=s:i>s))return new Tl(n.tree,n.overlay[0].from+this.from,-1,this)}return this.nextChild(0,1,t,e,i)}nextSignificantParent(){let t=this;for(;t.type.isAnonymous&&t._parent;)t=t._parent;return t}get parent(){return this._parent?this._parent.nextSignificantParent():null}get nextSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index+1,1,0,4):null}get prevSibling(){return this._parent&&this.index>=0?this._parent.nextChild(this.index-1,-1,0,4):null}get tree(){return this._tree}toTree(){return this._tree}toString(){return this._tree.toString()}}function Dl(t,e,i,n){let s=t.cursor(),r=[];if(!s.firstChild())return r;if(null!=i)for(let t=!1;!t;)if(t=s.type.is(i),!s.nextSibling())return r;for(;;){if(null!=n&&s.type.is(n))return r;if(s.type.is(e)&&r.push(s.node),!s.nextSibling())return null==n?r:[]}}function Rl(t,e,i=e.length-1){for(let n=t;i>=0;n=n.parent){if(!n)return!1;if(!n.type.isAnonymous){if(e[i]&&e[i]!=n.name)return!1;i--}}return!0}class Pl{constructor(t,e,i,n){this.parent=t,this.buffer=e,this.index=i,this.start=n}}class Bl extends Ol{get name(){return this.type.name}get from(){return this.context.start+this.context.buffer.buffer[this.index+1]}get to(){return this.context.start+this.context.buffer.buffer[this.index+2]}constructor(t,e,i){super(),this.context=t,this._parent=e,this.index=i,this.type=t.buffer.set.types[t.buffer.buffer[i]]}child(t,e,i){let{buffer:n}=this.context,s=n.findChild(this.index+4,n.buffer[this.index+3],t,e-this.context.start,i);return s<0?null:new Bl(this.context,this,s)}get firstChild(){return this.child(1,0,4)}get lastChild(){return this.child(-1,0,4)}childAfter(t){return this.child(1,t,2)}childBefore(t){return this.child(-1,t,-2)}prop(t){return this.type.prop(t)}enter(t,e,i=0){if(i&xl.ExcludeBuffers)return null;let{buffer:n}=this.context,s=n.findChild(this.index+4,n.buffer[this.index+3],e>0?1:-1,t-this.context.start,e);return s<0?null:new Bl(this.context,this,s)}get parent(){return this._parent||this.context.parent.nextSignificantParent()}externalSibling(t){return this._parent?null:this.context.parent.nextChild(this.context.index+t,t,0,4)}get nextSibling(){let{buffer:t}=this.context,e=t.buffer[this.index+3];return e<(this._parent?t.buffer[this._parent.index+3]:t.buffer.length)?new Bl(this.context,this._parent,e):this.externalSibling(1)}get prevSibling(){let{buffer:t}=this.context,e=this._parent?this._parent.index+4:0;return this.index==e?this.externalSibling(-1):new Bl(this.context,this._parent,t.findChild(e,this.index,-1,0,4))}get tree(){return null}toTree(){let t=[],e=[],{buffer:i}=this.context,n=this.index+4,s=i.buffer[this.index+3];if(s>n){let r=i.buffer[this.index+1];t.push(i.slice(n,s,r)),e.push(0)}return new kl(this.type,t,e,this.to-this.from)}toString(){return this.context.buffer.childString(this.index)}}function El(t){if(!t.length)return null;let e=0,i=t[0];for(let n=1;ni.from||s.to0){if(this.index-1)for(let n=e+t,s=t<0?-1:i._tree.children.length;n!=s;n+=t){let t=i._tree.children[n];if(this.mode&xl.IncludeAnonymous||t instanceof Cl||!t.type.isAnonymous||Nl(t))return!1}return!0}move(t,e){if(e&&this.enterChild(t,0,4))return!0;for(;;){if(this.sibling(t))return!0;if(this.atLastNode(t)||!this.parent())return!1}}next(t=!0){return this.move(1,t)}prev(t=!0){return this.move(-1,t)}moveTo(t,e=0){for(;(this.from==this.to||(e<1?this.from>=t:this.from>t)||(e>-1?this.to<=t:this.to=0;){for(let r=t;r;r=r._parent)if(r.index==n){if(n==this.index)return r;e=r,i=s+1;break t}n=this.stack[--s]}for(let t=i;t=0;s--){if(s<0)return Rl(this._tree,t,n);let r=i[e.buffer[this.stack[s]]];if(!r.isAnonymous){if(t[n]&&t[n]!=r.name)return!1;n--}}return!0}}function Nl(t){return t.children.some(t=>t instanceof Cl||!t.type.isAnonymous||Nl(t))}const Wl=new WeakMap;function Hl(t,e){if(!t.isAnonymous||e instanceof Cl||e.type!=t)return 1;let i=Wl.get(e);if(null==i){i=1;for(let n of e.children){if(n.type!=t||!(n instanceof kl)){i=1;break}i+=Hl(t,n)}Wl.set(e,i)}return i}function Vl(t,e,i,n,s,r,o,l,a){let h=0;for(let i=n;i=c)break;p+=e}if(h==s+1){if(p>c){let t=i[s];e(t.children,t.positions,0,t.children.length,n[s]+l);continue}u.push(i[s])}else{let e=n[h-1]+i[h-1].length-d;u.push(Vl(t,i,n,s,h,d,e,null,a))}f.push(d+l-r)}}(e,i,n,s,0),(l||a)(u,f,o)}class zl{constructor(t,e,i,n,s=!1,r=!1){this.from=t,this.to=e,this.tree=i,this.offset=n,this.open=(s?1:0)|(r?2:0)}get openStart(){return(1&this.open)>0}get openEnd(){return(2&this.open)>0}static addTree(t,e=[],i=!1){let n=[new zl(0,t.length,t,0,!1,i)];for(let i of e)i.to>t.length&&n.push(i);return n}static applyChanges(t,e,i=128){if(!e.length)return t;let n=[],s=1,r=t.length?t[0]:null;for(let o=0,l=0,a=0;;o++){let h=o=i)for(;r&&r.from=e.from||c<=e.to||a){let t=Math.max(e.from,l)-a,i=Math.min(e.to,c)-a;e=t>=i?null:new zl(t,i,e.tree,e.offset+a,o>0,!!h)}if(e&&n.push(e),r.to>c)break;r=snew dl(t.from,t.to)):[new dl(0,0)]:[new dl(0,t.length)],this.createParse(t,e||[],i)}parse(t,e,i){let n=this.startParse(t,e,i);for(;;){let t=n.advance();if(t)return t}}}class ql{constructor(t){this.string=t}get length(){return this.string.length}chunk(t){return this.string.slice(t)}get lineChunks(){return!1}read(t,e){return this.string.slice(t,e)}}new pl({perNode:!0});let _l=0;class $l{constructor(t,e,i,n){this.name=t,this.set=e,this.base=i,this.modified=n,this.id=_l++}toString(){let{name:t}=this;for(let e of this.modified)e.name&&(t=`${e.name}(${t})`);return t}static define(t,e){let i="string"==typeof t?t:"?";if(t instanceof $l&&(e=t),null==e?void 0:e.base)throw new Error("Can not derive from a modified tag");let n=new $l(i,[],null,[]);if(n.set.push(n),e)for(let t of e.set)n.set.push(t);return n}static defineModifier(t){let e=new Ql(t);return t=>t.modified.indexOf(e)>-1?t:Ql.get(t.base||t,t.modified.concat(e).sort((t,e)=>t.id-e.id))}}let Ul=0;class Ql{constructor(t){this.name=t,this.instances=[],this.id=Ul++}static get(t,e){if(!e.length)return t;let i=e[0].instances.find(i=>{return i.base==t&&(n=e,s=i.modified,n.length==s.length&&n.every((t,e)=>t==s[e]));var n,s});if(i)return i;let n=[],s=new $l(t.name,n,t,e);for(let t of e)t.instances.push(s);let r=function(t){let e=[[]];for(let i=0;ie.length-t.length)}(e);for(let e of t.set)if(!e.modified.length)for(let t of r)n.push(Ql.get(e,t));return s}}function jl(t){let e=Object.create(null);for(let i in t){let n=t[i];Array.isArray(n)||(n=[n]);for(let t of i.split(" "))if(t){let i=[],s=2,r=t;for(let e=0;;){if("..."==r&&e>0&&e+3==t.length){s=1;break}let n=/^"(?:[^"\\]|\\.)*?"|[^\/!]+/.exec(r);if(!n)throw new RangeError("Invalid path: "+t);if(i.push("*"==n[0]?"":'"'==n[0][0]?JSON.parse(n[0]):n[0]),e+=n[0].length,e==t.length)break;let o=t[e++];if(e==t.length&&"!"==o){s=0;break}if("/"!=o)throw new RangeError("Invalid path: "+t);r=t.slice(e)}let o=i.length-1,l=i[o];if(!l)throw new RangeError("Invalid path: "+t);let a=new Xl(n,s,o>0?i.slice(0,o):null);e[l]=a.sort(e[l])}}return Kl.add(e)}const Kl=new pl({combine(t,e){let i,n,s;for(;t||e;){if(!t||e&&t.depth>=e.depth?(s=e,e=e.next):(s=t,t=t.next),i&&i.mode==s.mode&&!s.context&&!i.context)continue;let r=new Xl(s.tags,s.mode,s.context);i?i.next=r:n=r,i=r}return n}});class Xl{constructor(t,e,i,n){this.tags=t,this.mode=e,this.context=i,this.next=n}get opaque(){return 0==this.mode}get inherit(){return 1==this.mode}sort(t){return!t||t.depth{let e=s;for(let n of t)for(let t of n.set){let n=i[t.id];if(n){e=e?e+" "+n:n;break}}return e},scope:n}}function Yl(t,e,i,n=0,s=t.length){let r=new Jl(n,Array.isArray(e)?e:[e],i);r.highlightRange(t.cursor(),n,s,"",r.highlighters),r.flush(s)}Xl.empty=new Xl([],2,null);class Jl{constructor(t,e,i){this.at=t,this.highlighters=e,this.span=i,this.class=""}startSpan(t,e){e!=this.class&&(this.flush(t),t>this.at&&(this.at=t),this.class=e)}flush(t){t>this.at&&this.class&&this.span(this.at,t,this.class)}highlightRange(t,e,i,n,s){let{type:r,from:o,to:l}=t;if(o>=i||l<=e)return;r.isTop&&(s=this.highlighters.filter(t=>!t.scope||t.scope(r)));let a=n,h=function(t){let e=t.type.prop(Kl);for(;e&&e.context&&!t.matchContext(e.context);)e=e.next;return e||null}(t)||Xl.empty,c=function(t,e){let i=null;for(let n of t){let t=n.style(e);t&&(i=i?i+" "+t:t)}return i}(s,h.tags);if(c&&(a&&(a+=" "),a+=c,1==h.mode&&(n+=(n?" ":"")+c)),this.startSpan(Math.max(e,o),a),h.opaque)return;let u=t.tree&&t.tree.prop(pl.mounted);if(u&&u.overlay){let r=t.node.enter(u.overlay[0].from+o,1),h=this.highlighters.filter(t=>!t.scope||t.scope(u.tree.type)),c=t.firstChild();for(let f=0,d=o;;f++){let p=f=m)&&t.nextSibling()););if(!p||m>i)break;d=p.to+o,d>e&&(this.highlightRange(r.cursor(),Math.max(e,p.from+o),Math.min(i,d),"",h),this.startSpan(Math.min(i,d),a))}c&&t.parent()}else if(t.firstChild()){u&&(n="");do{if(!(t.to<=e)){if(t.from>=i)break;this.highlightRange(t,e,i,n,s),this.startSpan(Math.min(i,t.to),a)}}while(t.nextSibling());t.parent()}}}const Zl=$l.define,ta=Zl(),ea=Zl(),ia=Zl(ea),na=Zl(ea),sa=Zl(),ra=Zl(sa),oa=Zl(sa),la=Zl(),aa=Zl(la),ha=Zl(),ca=Zl(),ua=Zl(),fa=Zl(ua),da=Zl(),pa={comment:ta,lineComment:Zl(ta),blockComment:Zl(ta),docComment:Zl(ta),name:ea,variableName:Zl(ea),typeName:ia,tagName:Zl(ia),propertyName:na,attributeName:Zl(na),className:Zl(ea),labelName:Zl(ea),namespace:Zl(ea),macroName:Zl(ea),literal:sa,string:ra,docString:Zl(ra),character:Zl(ra),attributeValue:Zl(ra),number:oa,integer:Zl(oa),float:Zl(oa),bool:Zl(sa),regexp:Zl(sa),escape:Zl(sa),color:Zl(sa),url:Zl(sa),keyword:ha,self:Zl(ha),null:Zl(ha),atom:Zl(ha),unit:Zl(ha),modifier:Zl(ha),operatorKeyword:Zl(ha),controlKeyword:Zl(ha),definitionKeyword:Zl(ha),moduleKeyword:Zl(ha),operator:ca,derefOperator:Zl(ca),arithmeticOperator:Zl(ca),logicOperator:Zl(ca),bitwiseOperator:Zl(ca),compareOperator:Zl(ca),updateOperator:Zl(ca),definitionOperator:Zl(ca),typeOperator:Zl(ca),controlOperator:Zl(ca),punctuation:ua,separator:Zl(ua),bracket:fa,angleBracket:Zl(fa),squareBracket:Zl(fa),paren:Zl(fa),brace:Zl(fa),content:la,heading:aa,heading1:Zl(aa),heading2:Zl(aa),heading3:Zl(aa),heading4:Zl(aa),heading5:Zl(aa),heading6:Zl(aa),contentSeparator:Zl(la),list:Zl(la),quote:Zl(la),emphasis:Zl(la),strong:Zl(la),link:Zl(la),monospace:Zl(la),strikethrough:Zl(la),inserted:Zl(),deleted:Zl(),changed:Zl(),invalid:Zl(),meta:da,documentMeta:Zl(da),annotation:Zl(da),processingInstruction:Zl(da),definition:$l.defineModifier("definition"),constant:$l.defineModifier("constant"),function:$l.defineModifier("function"),standard:$l.defineModifier("standard"),local:$l.defineModifier("local"),special:$l.defineModifier("special")};for(let t in pa){let e=pa[t];e instanceof $l&&(e.name=t)}var ma;Gl([{tag:pa.link,class:"tok-link"},{tag:pa.heading,class:"tok-heading"},{tag:pa.emphasis,class:"tok-emphasis"},{tag:pa.strong,class:"tok-strong"},{tag:pa.keyword,class:"tok-keyword"},{tag:pa.atom,class:"tok-atom"},{tag:pa.bool,class:"tok-bool"},{tag:pa.url,class:"tok-url"},{tag:pa.labelName,class:"tok-labelName"},{tag:pa.inserted,class:"tok-inserted"},{tag:pa.deleted,class:"tok-deleted"},{tag:pa.literal,class:"tok-literal"},{tag:pa.string,class:"tok-string"},{tag:pa.number,class:"tok-number"},{tag:[pa.regexp,pa.escape,pa.special(pa.string)],class:"tok-string2"},{tag:pa.variableName,class:"tok-variableName"},{tag:pa.local(pa.variableName),class:"tok-variableName tok-local"},{tag:pa.definition(pa.variableName),class:"tok-variableName tok-definition"},{tag:pa.special(pa.variableName),class:"tok-variableName2"},{tag:pa.definition(pa.propertyName),class:"tok-propertyName tok-definition"},{tag:pa.typeName,class:"tok-typeName"},{tag:pa.namespace,class:"tok-namespace"},{tag:pa.className,class:"tok-className"},{tag:pa.macroName,class:"tok-macroName"},{tag:pa.propertyName,class:"tok-propertyName"},{tag:pa.operator,class:"tok-operator"},{tag:pa.comment,class:"tok-comment"},{tag:pa.meta,class:"tok-meta"},{tag:pa.invalid,class:"tok-invalid"},{tag:pa.punctuation,class:"tok-punctuation"}]);const ga=new pl;const va=new pl;class wa{constructor(t,e,i=[],n=""){this.data=t,this.name=n,Ot.prototype.hasOwnProperty("tree")||Object.defineProperty(Ot.prototype,"tree",{get(){return xa(this)}}),this.parser=e,this.extension=[Ra.of(this),Ot.languageData.of((t,e,i)=>{let n=ba(t,e,i),s=n.type.prop(ga);if(!s)return[];let r=t.facet(s),o=n.type.prop(va);if(o){let s=n.resolve(e-n.from,i);for(let e of o)if(e.test(s,t)){let i=t.facet(e.facet);return"replace"==e.type?i:i.concat(r)}}return r})].concat(i)}isActiveAt(t,e,i=-1){return ba(t,e,i).type.prop(ga)==this.data}findRegions(t){let e=t.facet(Ra);if((null==e?void 0:e.data)==this.data)return[{from:0,to:t.doc.length}];if(!e||!e.allowsNesting)return[];let i=[],n=(t,e)=>{if(t.prop(ga)==this.data)return void i.push({from:e,to:e+t.length});let s=t.prop(pl.mounted);if(s){if(s.tree.prop(ga)==this.data){if(s.overlay)for(let t of s.overlay)i.push({from:t.from+e,to:t.to+e});else i.push({from:e,to:e+t.length});return}if(s.overlay){let t=i.length;if(n(s.tree,s.overlay[0].from+e),i.length>t)return}}for(let i=0;it.concat(i):void 0}));var i;return new ya(e,t.parser.configure({props:[ga.add(t=>t.isTop?e:void 0)]}),t.name)}configure(t,e){return new ya(this.data,this.parser.configure(t),e||this.name)}get allowsNesting(){return this.parser.hasWrappers()}}function xa(t){let e=t.field(wa.state,!1);return e?e.tree:kl.empty}class ka{constructor(t){this.doc=t,this.cursorPos=0,this.string="",this.cursor=t.iter()}get length(){return this.doc.length}syncTo(t){return this.string=this.cursor.next(t-this.cursorPos).value,this.cursorPos=t+this.string.length,this.cursorPos-this.string.length}chunk(t){return this.syncTo(t),this.string}get lineChunks(){return!0}read(t,e){let i=this.cursorPos-this.string.length;return t=this.cursorPos?this.doc.sliceString(t,e):this.string.slice(t-i,e-i)}}let Sa=null;class Ca{constructor(t,e,i=[],n,s,r,o,l){this.parser=t,this.state=e,this.fragments=i,this.tree=n,this.treeLen=s,this.viewport=r,this.skipped=o,this.scheduleOn=l,this.parse=null,this.tempSkipped=[]}static create(t,e,i){return new Ca(t,e,[],kl.empty,0,i,[],null)}startParse(){return this.parser.startParse(new ka(this.state.doc),this.fragments)}work(t,e){return null!=e&&e>=this.state.doc.length&&(e=void 0),this.tree!=kl.empty&&this.isDone(null!=e?e:this.state.doc.length)?(this.takeTree(),!0):this.withContext(()=>{var i;if("number"==typeof t){let e=Date.now()+t;t=()=>Date.now()>e}for(this.parse||(this.parse=this.startParse()),null!=e&&(null==this.parse.stoppedAt||this.parse.stoppedAt>e)&&e=this.treeLen&&((null==this.parse.stoppedAt||this.parse.stoppedAt>t)&&this.parse.stopAt(t),this.withContext(()=>{for(;!(e=this.parse.advance()););}),this.treeLen=t,this.tree=e,this.fragments=this.withoutTempSkipped(zl.addTree(this.tree,this.fragments,!0)),this.parse=null)}withContext(t){let e=Sa;Sa=this;try{return t()}finally{Sa=e}}withoutTempSkipped(t){for(let e;e=this.tempSkipped.pop();)t=Aa(t,e.from,e.to);return t}changes(t,e){let{fragments:i,tree:n,treeLen:s,viewport:r,skipped:o}=this;if(this.takeTree(),!t.empty){let e=[];if(t.iterChangedRanges((t,i,n,s)=>e.push({fromA:t,toA:i,fromB:n,toB:s})),i=zl.applyChanges(i,e),n=kl.empty,s=0,r={from:t.mapPos(r.from,-1),to:t.mapPos(r.to,1)},this.skipped.length){o=[];for(let e of this.skipped){let i=t.mapPos(e.from,1),n=t.mapPos(e.to,-1);it.from&&(this.fragments=Aa(this.fragments,i,n),this.skipped.splice(e--,1))}return!(this.skipped.length>=e)&&(this.reset(),!0)}reset(){this.parse&&(this.takeTree(),this.parse=null)}skipUntilInView(t,e){this.skipped.push({from:t,to:e})}static getSkippingParser(t){return new class extends Fl{createParse(e,i,n){let s=n[0].from,r=n[n.length-1].to;return{parsedPos:s,advance(){let e=Sa;if(e){for(let t of n)e.tempSkipped.push(t);t&&(e.scheduleOn=e.scheduleOn?Promise.all([e.scheduleOn,t]):t)}return this.parsedPos=r,new kl(vl.none,[],[],r-s)},stoppedAt:null,stopAt(){}}}}}isDone(t){t=Math.min(t,this.state.doc.length);let e=this.fragments;return this.treeLen>=t&&e.length&&0==e[0].from&&e[0].to>=t}static get(){return Sa}}function Aa(t,e,i){return zl.applyChanges(t,[{fromA:e,toA:i,fromB:e,toB:i}])}class Ma{constructor(t){this.context=t,this.tree=t.tree}apply(t){if(!t.docChanged&&this.tree==this.context.tree)return this;let e=this.context.changes(t.changes,t.state),i=this.context.treeLen==t.startState.doc.length?void 0:Math.max(t.changes.mapPos(this.context.treeLen),e.viewport.to);return e.work(20,i)||e.takeTree(),new Ma(e)}static init(t){let e=Math.min(3e3,t.doc.length),i=Ca.create(t.facet(Ra).parser,t,{from:0,to:e});return i.work(20,e)||i.takeTree(),new Ma(i)}}wa.state=Q.define({create:Ma.init,update(t,e){for(let t of e.effects)if(t.is(wa.setState))return t.value;return e.startState.facet(Ra)!=e.state.facet(Ra)?Ma.init(e.state):t.apply(e)}});let Oa=t=>{let e=setTimeout(()=>t(),500);return()=>clearTimeout(e)};"undefined"!=typeof requestIdleCallback&&(Oa=t=>{let e=-1,i=setTimeout(()=>{e=requestIdleCallback(t,{timeout:400})},100);return()=>e<0?clearTimeout(i):cancelIdleCallback(e)});const Ta="undefined"!=typeof navigator&&(null===(ma=navigator.scheduling)||void 0===ma?void 0:ma.isInputPending)?()=>navigator.scheduling.isInputPending():null,Da=Fi.fromClass(class{constructor(t){this.view=t,this.working=null,this.workScheduled=0,this.chunkEnd=-1,this.chunkBudget=-1,this.work=this.work.bind(this),this.scheduleWork()}update(t){let e=this.view.state.field(wa.state).context;(e.updateViewport(t.view.viewport)||this.view.viewport.to>e.treeLen)&&this.scheduleWork(),(t.docChanged||t.selectionSet)&&(this.view.hasFocus&&(this.chunkBudget+=50),this.scheduleWork()),this.checkAsyncSchedule(e)}scheduleWork(){if(this.working)return;let{state:t}=this.view,e=t.field(wa.state);e.tree==e.context.tree&&e.context.isDone(t.doc.length)||(this.working=Oa(this.work))}work(t){this.working=null;let e=Date.now();if(this.chunkEndn+1e3,l=s.context.work(()=>Ta&&Ta()||Date.now()>r,n+(o?0:1e5));this.chunkBudget-=Date.now()-e,(l||this.chunkBudget<=0)&&(s.context.takeTree(),this.view.dispatch({effects:wa.setState.of(new Ma(s.context))})),this.chunkBudget>0&&(!l||o)&&this.scheduleWork(),this.checkAsyncSchedule(s.context)}checkAsyncSchedule(t){t.scheduleOn&&(this.workScheduled++,t.scheduleOn.then(()=>this.scheduleWork()).catch(t=>Wi(this.view.state,t)).then(()=>this.workScheduled--),t.scheduleOn=null)}destroy(){this.working&&this.working()}isWorking(){return!!(this.working||this.workScheduled>0)}},{eventHandlers:{focus(){this.scheduleWork()}}}),Ra=V.define({combine:t=>t.length?t[0]:null,enables:t=>[wa.state,Da,dr.contentAttributes.compute([t],e=>{let i=e.facet(t);return i&&i.name?{"data-language":i.name}:{}})]});class Pa{constructor(t,e=[]){this.language=t,this.support=e,this.extension=[t,e]}}const Ba=V.define(),Ea=V.define({combine:t=>{if(!t.length)return" ";let e=t[0];if(!e||/\S/.test(e)||Array.from(e).some(t=>t!=e[0]))throw new Error("Invalid indent unit: "+JSON.stringify(t[0]));return e}});function La(t){let e=t.facet(Ea);return 9==e.charCodeAt(0)?t.tabSize*e.length:e.length}function Ia(t,e){let i="",n=t.tabSize,s=t.facet(Ea)[0];if("\t"==s){for(;e>=n;)i+="\t",e-=n;s=" "}for(let t=0;t=e?function(t,e,i){let n=e.resolveStack(i),s=e.resolveInner(i,-1).resolve(i,0).enterUnfinishedNodesBefore(i);if(s!=n.node){let t=[];for(let e=s;e&&!(e.fromn.node.to||e.from==n.node.from&&e.type==n.node.type);e=e.parent)t.push(e);for(let e=t.length-1;e>=0;e--)n={node:t[e],next:n}}return Va(n,t,i)}(t,i,e):null}class Wa{constructor(t,e={}){this.state=t,this.options=e,this.unit=La(t)}lineAt(t,e=1){let i=this.state.doc.lineAt(t),{simulateBreak:n,simulateDoubleBreak:s}=this.options;return null!=n&&n>=i.from&&n<=i.to?s&&n==t?{text:"",from:t}:(e<0?n-1&&(s+=r-this.countColumn(i,i.search(/\S|$/))),s}countColumn(t,e=t.length){return Qt(t,this.state.tabSize,e)}lineIndent(t,e=1){let{text:i,from:n}=this.lineAt(t,e),s=this.options.overrideIndentation;if(s){let t=s(n);if(t>-1)return t}return this.countColumn(i,i.search(/\S|$/))}get simulatedBreak(){return this.options.simulateBreak||null}}const Ha=new pl;function Va(t,e,i){for(let n=t;n;n=n.next){let t=za(n.node);if(t)return t(qa.create(e,i,n))}return 0}function za(t){let e=t.type.prop(Ha);if(e)return e;let i,n=t.firstChild;if(n&&(i=n.type.prop(pl.closedBy))){let e=t.lastChild,n=e&&i.indexOf(e.name)>-1;return t=>function(t,e,i,n,s){let r=t.textAfter,o=r.match(/^\s*/)[0].length,l=n&&r.slice(o,o+n.length)==n||s==t.pos+o,a=e?function(t){let e=t.node,i=e.childAfter(e.from),n=e.lastChild;if(!i)return null;let s=t.options.simulateBreak,r=t.state.doc.lineAt(i.from),o=null==s||s<=r.from?r.to:Math.min(r.to,s);for(let t=i.to;;){let s=e.childAfter(t);if(!s||s==n)return null;if(!s.type.isSkipped){if(s.from>=o)return null;let t=/^ */.exec(r.text.slice(i.to-r.from))[0].length;return{from:i.from,to:i.to+t}}t=s.to}}(t):null;return a?l?t.column(a.from):t.column(a.to):t.baseIndent+(l?0:t.unit*i)}(t,!0,1,void 0,n&&!function(t){return t.pos==t.options.simulateBreak&&t.options.simulateDoubleBreak}(t)?e.from:void 0)}return null==t.parent?Fa:null}function Fa(){return 0}class qa extends Wa{constructor(t,e,i){super(t.state,t.options),this.base=t,this.pos=e,this.context=i}get node(){return this.context.node}static create(t,e,i){return new qa(t,e,i)}get textAfter(){return this.textAfterPos(this.pos)}get baseIndent(){return this.baseIndentFor(this.node)}baseIndentFor(t){let e=this.state.doc.lineAt(t.from);for(;;){let i=t.resolve(e.from);for(;i.parent&&i.parent.from==i.from;)i=i.parent;if(_a(i,t))break;e=this.state.doc.lineAt(i.from)}return this.lineIndent(e.from)}continue(){return Va(this.context.next,this.base,this.pos)}}function _a(t,e){for(let i=e;i;i=i.parent)if(t==i)return!0;return!1}function $a({except:t,units:e=1}={}){return i=>{let n=t&&t.test(i.textAfter);return i.baseIndent+(n?0:e*i.unit)}}const Ua=V.define(),Qa=new pl;function ja(t){let e=t.lastChild;return e&&e.to==t.to&&e.type.isError}function Ka(t,e,i){for(let n of t.facet(Ua)){let s=n(t,e,i);if(s)return s}return function(t,e,i){let n=xa(t);if(n.lengthi)continue;if(s&&o.from=e&&n.to>i&&(s=n)}}return s}(t,e,i)}function Xa(t,e){let i=e.mapPos(t.from,1),n=e.mapPos(t.to,-1);return i>=n?void 0:{from:i,to:n}}const Ga=mt.define({map:Xa}),Ya=mt.define({map:Xa});function Ja(t){let e=[];for(let{head:i}of t.state.selection.ranges)e.some(t=>t.from<=i&&t.to>=i)||e.push(t.lineBlockAt(i));return e}const Za=Q.define({create:()=>Oe.none,update(t,e){e.isUserEvent("delete")&&e.changes.iterChangedRanges((e,i)=>t=th(t,e,i)),t=t.map(e.changes);let i=[];for(let n of e.effects)n.is(Ga)&&!ih(t,n.value.from,n.value.to)?i.push(n.value):n.is(Ya)&&(t=t.update({filter:(t,e)=>n.value.from!=t||n.value.to!=e,filterFrom:n.value.from,filterTo:n.value.to}));if(i.length){let{preparePlaceholder:n}=e.state.facet(lh),s=i.map(t=>(n?Oe.replace({widget:new uh(n(e.state,t))}):ch).range(t.from,t.to));t=t.update({add:s})}return e.selection&&(t=th(t,e.selection.main.head)),t},provide:t=>dr.decorations.from(t),toJSON(t,e){let i=[];return t.between(0,e.doc.length,(t,e)=>{i.push(t,e)}),i},fromJSON(t){if(!Array.isArray(t)||t.length%2)throw new RangeError("Invalid JSON for fold state");let e=[];for(let i=0;i{te&&(n=!0)}),n?t.update({filterFrom:e,filterTo:i,filter:(t,n)=>t>=i||n<=e}):t}function eh(t,e,i){var n;let s=null;return null===(n=t.field(Za,!1))||void 0===n||n.between(e,i,(t,e)=>{(!s||s.from>t)&&(s={from:t,to:e})}),s}function ih(t,e,i){let n=!1;return t.between(e,e,(t,s)=>{t==e&&s==i&&(n=!0)}),n}function nh(t,e){return t.field(Za,!1)?e:e.concat(mt.appendConfig.of(ah()))}function sh(t,e,i=!0){let n=t.state.doc.lineAt(e.from).number,s=t.state.doc.lineAt(e.to).number;return dr.announce.of(`${t.state.phrase(i?"Folded lines":"Unfolded lines")} ${n} ${t.state.phrase("to")} ${s}.`)}const rh=[{key:"Ctrl-Shift-[",mac:"Cmd-Alt-[",run:t=>{for(let e of Ja(t)){let i=Ka(t.state,e.from,e.to);if(i)return t.dispatch({effects:nh(t.state,[Ga.of(i),sh(t,i)])}),!0}return!1}},{key:"Ctrl-Shift-]",mac:"Cmd-Alt-]",run:t=>{if(!t.state.field(Za,!1))return!1;let e=[];for(let i of Ja(t)){let n=eh(t.state,i.from,i.to);n&&e.push(Ya.of(n),sh(t,n,!1))}return e.length&&t.dispatch({effects:e}),e.length>0}},{key:"Ctrl-Alt-[",run:t=>{let{state:e}=t,i=[];for(let n=0;n{let e=t.state.field(Za,!1);if(!e||!e.size)return!1;let i=[];return e.between(0,t.state.doc.length,(t,e)=>{i.push(Ya.of({from:t,to:e}))}),t.dispatch({effects:i}),!0}}],oh={placeholderDOM:null,preparePlaceholder:null,placeholderText:"…"},lh=V.define({combine:t=>Tt(t,oh)});function ah(t){let e=[Za,mh];return t&&e.push(lh.of(t)),e}function hh(t,e){let{state:i}=t,n=i.facet(lh),s=e=>{let i=t.lineBlockAt(t.posAtDOM(e.target)),n=eh(t.state,i.from,i.to);n&&t.dispatch({effects:Ya.of(n)}),e.preventDefault()};if(n.placeholderDOM)return n.placeholderDOM(t,s,e);let r=document.createElement("span");return r.textContent=n.placeholderText,r.setAttribute("aria-label",i.phrase("folded code")),r.title=i.phrase("unfold"),r.className="cm-foldPlaceholder",r.onclick=s,r}const ch=Oe.replace({widget:new class extends Ae{toDOM(t){return hh(t,null)}}});class uh extends Ae{constructor(t){super(),this.value=t}eq(t){return this.value==t.value}toDOM(t){return hh(t,this.value)}}const fh={openText:"⌄",closedText:"›",markerDOM:null,domEventHandlers:{},foldingChanged:()=>!1};class dh extends Fo{constructor(t,e){super(),this.config=t,this.open=e}eq(t){return this.config==t.config&&this.open==t.open}toDOM(t){if(this.config.markerDOM)return this.config.markerDOM(this.open);let e=document.createElement("span");return e.textContent=this.open?this.config.openText:this.config.closedText,e.title=t.state.phrase(this.open?"Fold line":"Unfold line"),e}}function ph(t={}){let e={...fh,...t},i=new dh(e,!0),n=new dh(e,!1),s=Fi.fromClass(class{constructor(t){this.from=t.viewport.from,this.markers=this.buildMarkers(t)}update(t){(t.docChanged||t.viewportChanged||t.startState.facet(Ra)!=t.state.facet(Ra)||t.startState.field(Za,!1)!=t.state.field(Za,!1)||xa(t.startState)!=xa(t.state)||e.foldingChanged(t))&&(this.markers=this.buildMarkers(t.view))}buildMarkers(t){let e=new It;for(let s of t.viewportLineBlocks){let r=eh(t.state,s.from,s.to)?n:Ka(t.state,s.from,s.to)?i:null;r&&e.add(s.from,s.from,r)}return e.finish()}}),{domEventHandlers:r}=e;return[s,Qo({class:"cm-foldGutter",markers(t){var e;return(null===(e=t.plugin(s))||void 0===e?void 0:e.markers)||Lt.empty},initialSpacer:()=>new dh(e,!1),domEventHandlers:{...r,click:(t,e,i)=>{if(r.click&&r.click(t,e,i))return!0;let n=eh(t.state,e.from,e.to);if(n)return t.dispatch({effects:Ya.of(n)}),!0;let s=Ka(t.state,e.from,e.to);return!!s&&(t.dispatch({effects:Ga.of(s)}),!0)}}}),ah()]}const mh=dr.baseTheme({".cm-foldPlaceholder":{backgroundColor:"#eee",border:"1px solid #ddd",color:"#888",borderRadius:".2em",margin:"0 1px",padding:"0 1px",cursor:"pointer"},".cm-foldGutter span":{padding:"0 1px",cursor:"pointer"}});class gh{constructor(t,e){let i;function n(t){let e=Yt.newName();return(i||(i=Object.create(null)))["."+e]=t,e}this.specs=t;const s="string"==typeof e.all?e.all:e.all?n(e.all):void 0,r=e.scope;this.scope=r instanceof wa?t=>t.prop(ga)==r.data:r?t=>t==r:void 0,this.style=Gl(t.map(t=>({tag:t.tag,class:t.class||n(Object.assign({},t,{tag:null}))})),{all:s}).style,this.module=i?new Yt(i):null,this.themeType=e.themeType}static define(t,e){return new gh(t,e||{})}}const vh=V.define(),wh=V.define({combine:t=>t.length?[t[0]]:null});function bh(t){let e=t.facet(vh);return e.length?e:t.facet(wh)}function yh(t,e){let i,n=[kh];return t instanceof gh&&(t.module&&n.push(dr.styleModule.of(t.module)),i=t.themeType),(null==e?void 0:e.fallback)?n.push(wh.of(t)):i?n.push(vh.computeN([dr.darkTheme],e=>e.facet(dr.darkTheme)==("dark"==i)?[t]:[])):n.push(vh.of(t)),n}class xh{constructor(t){this.markCache=Object.create(null),this.tree=xa(t.state),this.decorations=this.buildDeco(t,bh(t.state)),this.decoratedTo=t.viewport.to}update(t){let e=xa(t.state),i=bh(t.state),n=i!=bh(t.startState),{viewport:s}=t.view,r=t.changes.mapPos(this.decoratedTo,1);e.length=s.to?(this.decorations=this.decorations.map(t.changes),this.decoratedTo=r):(e!=this.tree||t.viewportChanged||n)&&(this.tree=e,this.decorations=this.buildDeco(t.view,i),this.decoratedTo=s.to)}buildDeco(t,e){if(!e||!this.tree.length)return Oe.none;let i=new It;for(let{from:n,to:s}of t.visibleRanges)Yl(this.tree,e,(t,e,n)=>{i.add(t,e,this.markCache[n]||(this.markCache[n]=Oe.mark({class:n})))},n,s);return i.finish()}}const kh=J.high(Fi.fromClass(xh,{decorations:t=>t.decorations})),Sh=gh.define([{tag:pa.meta,color:"#404740"},{tag:pa.link,textDecoration:"underline"},{tag:pa.heading,textDecoration:"underline",fontWeight:"bold"},{tag:pa.emphasis,fontStyle:"italic"},{tag:pa.strong,fontWeight:"bold"},{tag:pa.strikethrough,textDecoration:"line-through"},{tag:pa.keyword,color:"#708"},{tag:[pa.atom,pa.bool,pa.url,pa.contentSeparator,pa.labelName],color:"#219"},{tag:[pa.literal,pa.inserted],color:"#164"},{tag:[pa.string,pa.deleted],color:"#a11"},{tag:[pa.regexp,pa.escape,pa.special(pa.string)],color:"#e40"},{tag:pa.definition(pa.variableName),color:"#00f"},{tag:pa.local(pa.variableName),color:"#30a"},{tag:[pa.typeName,pa.namespace],color:"#085"},{tag:pa.className,color:"#167"},{tag:[pa.special(pa.variableName),pa.macroName],color:"#256"},{tag:pa.definition(pa.propertyName),color:"#00c"},{tag:pa.comment,color:"#940"},{tag:pa.invalid,color:"#f00"}]),Ch=dr.baseTheme({"&.cm-focused .cm-matchingBracket":{backgroundColor:"#328c8252"},"&.cm-focused .cm-nonmatchingBracket":{backgroundColor:"#bb555544"}}),Ah="()[]{}",Mh=V.define({combine:t=>Tt(t,{afterCursor:!0,brackets:Ah,maxScanDistance:1e4,renderMatch:Dh})}),Oh=Oe.mark({class:"cm-matchingBracket"}),Th=Oe.mark({class:"cm-nonmatchingBracket"});function Dh(t){let e=[],i=t.matched?Oh:Th;return e.push(i.range(t.start.from,t.start.to)),t.end&&e.push(i.range(t.end.from,t.end.to)),e}function Rh(t){let e=[],i=t.facet(Mh);for(let n of t.selection.ranges){if(!n.empty)continue;let s=Nh(t,n.head,-1,i)||n.head>0&&Nh(t,n.head-1,1,i)||i.afterCursor&&(Nh(t,n.head,1,i)||n.headt.decorations}),Ch];function Bh(t={}){return[Mh.of(t),Ph]}const Eh=new pl;function Lh(t,e,i){let n=t.prop(e<0?pl.openedBy:pl.closedBy);if(n)return n;if(1==t.name.length){let n=i.indexOf(t.name);if(n>-1&&n%2==(e<0?1:0))return[i[n+e]]}return null}function Ih(t){let e=t.type.prop(Eh);return e?e(t.node):t}function Nh(t,e,i,n={}){let s=n.maxScanDistance||1e4,r=n.brackets||Ah,o=xa(t),l=o.resolveInner(e,i);for(let n=l;n;n=n.parent){let s=Lh(n.type,i,r);if(s&&n.from0?e>=o.from&&eo.from&&e<=o.to))return Wh(t,e,i,n,o,s,r)}}return function(t,e,i,n,s,r,o){if(i<0?!e:e==t.doc.length)return null;let l=i<0?t.sliceDoc(e-1,e):t.sliceDoc(e,e+1),a=o.indexOf(l);if(a<0||a%2==0!=i>0)return null;let h={from:i<0?e-1:e,to:i>0?e+1:e},c=t.doc.iterRange(e,i>0?t.doc.length:0),u=0;for(let t=0;!c.next().done&&t<=r;){let r=c.value;i<0&&(t+=r.length);let l=e+t*i;for(let t=i>0?0:r.length-1,e=i>0?r.length:-1;t!=e;t+=i){let e=o.indexOf(r[t]);if(!(e<0||n.resolveInner(l+t,1).type!=s))if(e%2==0==i>0)u++;else{if(1==u)return{start:h,end:{from:l+t,to:l+t+1},matched:e>>1==a>>1};u--}}i>0&&(t+=r.length)}return c.done?{start:h,matched:!1}:null}(t,e,i,o,l.type,s,r)}function Wh(t,e,i,n,s,r,o){let l=n.parent,a={from:s.from,to:s.to},h=0,c=null==l?void 0:l.cursor();if(c&&(i<0?c.childBefore(n.from):c.childAfter(n.to)))do{if(i<0?c.to<=n.from:c.from>=n.to){if(0==h&&r.indexOf(c.type.name)>-1&&c.from-1||(zh.push(t),console.warn(e))}function $h(t,e){let i=[];for(let n of e.split(" ")){let e=[];for(let i of n.split(".")){let n=t[i]||pa[i];n?"function"==typeof n?e.length?e=e.map(n):_h(i,`Modifier ${i} used at start of tag`):e.length?_h(i,`Tag ${i} used as modifier`):e=Array.isArray(n)?n:[n]:_h(i,`Unknown highlighting tag ${i}`)}for(let t of e)i.push(t)}if(!i.length)return 0;let n=e.replace(/ /g,"_"),s=n+" "+i.map(t=>t.id),r=Fh[s];if(r)return r.id;let o=Fh[s]=vl.define({id:Vh.length,name:n,props:[jl({[n]:i})]});return Vh.push(o),o.id}ni.RTL,ni.LTR;function Uh(t,e){return({state:i,dispatch:n})=>{if(i.readOnly)return!1;let s=t(e,i);return!!s&&(n(i.update(s)),!0)}}const Qh=Uh(Jh,0),jh=Uh(Yh,0),Kh=Uh((t,e)=>Yh(t,e,function(t){let e=[];for(let i of t.selection.ranges){let n=t.doc.lineAt(i.from),s=i.to<=n.to?n:t.doc.lineAt(i.to);s.from>n.from&&s.from==i.to&&(s=i.to==n.to+1?n:t.doc.lineAt(i.to-1));let r=e.length-1;r>=0&&e[r].to>n.from?e[r].to=s.to:e.push({from:n.from+/^\s*/.exec(n.text)[0].length,to:s.to})}return e}(e)),0);function Xh(t,e){let i=t.languageDataAt("commentTokens",e,1);return i.length?i[0]:{}}const Gh=50;function Yh(t,e,i=e.selection.ranges){let n=i.map(t=>Xh(e,t.from).block);if(!n.every(t=>t))return null;let s=i.map((t,i)=>function(t,{open:e,close:i},n,s){let r,o,l=t.sliceDoc(n-Gh,n),a=t.sliceDoc(s,s+Gh),h=/\s*$/.exec(l)[0].length,c=/^\s*/.exec(a)[0].length,u=l.length-h;if(l.slice(u-e.length,u)==e&&a.slice(c,c+i.length)==i)return{open:{pos:n-h,margin:h&&1},close:{pos:s+c,margin:c&&1}};s-n<=2*Gh?r=o=t.sliceDoc(n,s):(r=t.sliceDoc(n,n+Gh),o=t.sliceDoc(s-Gh,s));let f=/^\s*/.exec(r)[0].length,d=/\s*$/.exec(o)[0].length,p=o.length-d-i.length;return r.slice(f,f+e.length)==e&&o.slice(p,p+i.length)==i?{open:{pos:n+f+e.length,margin:/\s/.test(r.charAt(f+e.length))?1:0},close:{pos:s-d-i.length,margin:/\s/.test(o.charAt(p-1))?1:0}}:null}(e,n[i],t.from,t.to));if(2!=t&&!s.every(t=>t))return{changes:e.changes(i.map((t,e)=>s[e]?[]:[{from:t.from,insert:n[e].open+" "},{from:t.to,insert:" "+n[e].close}]))};if(1!=t&&s.some(t=>t)){let t=[];for(let e,i=0;is&&(t==r||r>a.from)){s=a.from;let t=/^\s*/.exec(a.text)[0].length,e=t==a.length,r=a.text.slice(t,t+i.length)==i?t:-1;tt.comment<0&&(!t.empty||t.single))){let t=[];for(let{line:e,token:i,indent:s,empty:r,single:o}of n)!o&&r||t.push({from:e.from+s,insert:i+" "});let i=e.changes(t);return{changes:i,selection:e.selection.map(i,1)}}if(1!=t&&n.some(t=>t.comment>=0)){let t=[];for(let{line:e,comment:i,token:s}of n)if(i>=0){let n=e.from+i,r=n+s.length;" "==e.text[r-e.from]&&r++,t.push({from:n,to:r})}return{changes:t}}return null}const Zh=ft.define(),tc=ft.define(),ec=V.define(),ic=V.define({combine:t=>Tt(t,{minDepth:100,newGroupDelay:500,joinToEvent:(t,e)=>e},{minDepth:Math.max,newGroupDelay:Math.min,joinToEvent:(t,e)=>(i,n)=>t(i,n)||e(i,n)})}),nc=Q.define({create:()=>bc.empty,update(t,e){let i=e.state.facet(ic),n=e.annotation(Zh);if(n){let s=hc.fromTransaction(e,n.selection),r=n.side,o=0==r?t.undone:t.done;return o=s?cc(o,o.length,i.minDepth,s):pc(o,e.startState.selection),new bc(0==r?n.rest:o,0==r?o:n.rest)}let s=e.annotation(tc);if("full"!=s&&"before"!=s||(t=t.isolate()),!1===e.annotation(gt.addToHistory))return e.changes.empty?t:t.addMapping(e.changes.desc);let r=hc.fromTransaction(e),o=e.annotation(gt.time),l=e.annotation(gt.userEvent);return r?t=t.addChanges(r,o,l,i,e):e.selection&&(t=t.addSelection(e.startState.selection,o,l,i.newGroupDelay)),"full"!=s&&"after"!=s||(t=t.isolate()),t},toJSON:t=>({done:t.done.map(t=>t.toJSON()),undone:t.undone.map(t=>t.toJSON())}),fromJSON:t=>new bc(t.done.map(hc.fromJSON),t.undone.map(hc.fromJSON))});function sc(t,e){return function({state:i,dispatch:n}){if(!e&&i.readOnly)return!1;let s=i.field(nc,!1);if(!s)return!1;let r=s.pop(t,i,e);return!!r&&(n(r),!0)}}const rc=sc(0,!1),oc=sc(1,!1),lc=sc(0,!0),ac=sc(1,!0);class hc{constructor(t,e,i,n,s){this.changes=t,this.effects=e,this.mapped=i,this.startSelection=n,this.selectionsAfter=s}setSelAfter(t){return new hc(this.changes,this.effects,this.mapped,this.startSelection,t)}toJSON(){var t,e,i;return{changes:null===(t=this.changes)||void 0===t?void 0:t.toJSON(),mapped:null===(e=this.mapped)||void 0===e?void 0:e.toJSON(),startSelection:null===(i=this.startSelection)||void 0===i?void 0:i.toJSON(),selectionsAfter:this.selectionsAfter.map(t=>t.toJSON())}}static fromJSON(t){return new hc(t.changes&&T.fromJSON(t.changes),[],t.mapped&&O.fromJSON(t.mapped),t.startSelection&&N.fromJSON(t.startSelection),t.selectionsAfter.map(N.fromJSON))}static fromTransaction(t,e){let i=fc;for(let e of t.startState.facet(ec)){let n=e(t);n.length&&(i=i.concat(n))}return!i.length&&t.changes.empty?null:new hc(t.changes.invert(t.startState.doc),i,void 0,e||t.startState.selection,fc)}static selection(t){return new hc(void 0,fc,void 0,void 0,t)}}function cc(t,e,i,n){let s=e+1>i+20?e-i-1:0,r=t.slice(s,e);return r.push(n),r}function uc(t,e){return t.length?e.length?t.concat(e):t:e}const fc=[],dc=200;function pc(t,e){if(t.length){let i=t[t.length-1],n=i.selectionsAfter.slice(Math.max(0,i.selectionsAfter.length-dc));return n.length&&n[n.length-1].eq(e)?t:(n.push(e),cc(t,t.length-1,1e9,i.setSelAfter(n)))}return[hc.selection([e])]}function mc(t){let e=t[t.length-1],i=t.slice();return i[t.length-1]=e.setSelAfter(e.selectionsAfter.slice(0,e.selectionsAfter.length-1)),i}function gc(t,e){if(!t.length)return t;let i=t.length,n=fc;for(;i;){let s=vc(t[i-1],e,n);if(s.changes&&!s.changes.empty||s.effects.length){let e=t.slice(0,i);return e[i-1]=s,e}e=s.mapped,i--,n=s.selectionsAfter}return n.length?[hc.selection(n)]:fc}function vc(t,e,i){let n=uc(t.selectionsAfter.length?t.selectionsAfter.map(t=>t.map(e)):fc,i);if(!t.changes)return hc.selection(n);let s=t.changes.map(e),r=e.mapDesc(t.changes,!0),o=t.mapped?t.mapped.composeDesc(r):r;return new hc(s,mt.mapEffects(t.effects,e),o,t.startSelection.map(r),n)}const wc=/^(input\.type|delete)($|\.)/;class bc{constructor(t,e,i=0,n=void 0){this.done=t,this.undone=e,this.prevTime=i,this.prevUserEvent=n}isolate(){return this.prevTime?new bc(this.done,this.undone):this}addChanges(t,e,i,n,s){let r=this.done,o=r[r.length-1];return r=o&&o.changes&&!o.changes.empty&&t.changes&&(!i||wc.test(i))&&(!o.selectionsAfter.length&&e-this.prevTimei.push(t,e)),e.iterChangedRanges((t,e,s,r)=>{for(let t=0;t=e&&s<=o&&(n=!0)}}),n}(o.changes,t.changes))||"input.type.compose"==i)?cc(r,r.length-1,n.minDepth,new hc(t.changes.compose(o.changes),uc(mt.mapEffects(t.effects,o.changes),o.effects),o.mapped,o.startSelection,fc)):cc(r,r.length,n.minDepth,t),new bc(r,fc,e,i)}addSelection(t,e,i,n){let s=this.done.length?this.done[this.done.length-1].selectionsAfter:fc;return s.length>0&&e-this.prevTimet.empty!=o.ranges[e].empty).length)?this:new bc(pc(this.done,t),this.undone,e,i);var r,o}addMapping(t){return new bc(gc(this.done,t),gc(this.undone,t),this.prevTime,this.prevUserEvent)}pop(t,e,i){let n=0==t?this.done:this.undone;if(0==n.length)return null;let s=n[n.length-1],r=s.selectionsAfter[0]||(s.startSelection?s.startSelection.map(s.changes.invertedDesc,1):e.selection);if(i&&s.selectionsAfter.length)return e.update({selection:s.selectionsAfter[s.selectionsAfter.length-1],annotations:Zh.of({side:t,rest:mc(n),selection:r}),userEvent:0==t?"select.undo":"select.redo",scrollIntoView:!0});if(s.changes){let i=1==n.length?fc:n.slice(0,n.length-1);return s.mapped&&(i=gc(i,s.mapped)),e.update({changes:s.changes,selection:s.startSelection,effects:s.effects,annotations:Zh.of({side:t,rest:i,selection:r}),filter:!1,userEvent:0==t?"undo":"redo",scrollIntoView:!0})}return null}}bc.empty=new bc(fc,fc);const yc=[{key:"Mod-z",run:rc,preventDefault:!0},{key:"Mod-y",mac:"Mod-Shift-z",run:oc,preventDefault:!0},{linux:"Ctrl-Shift-z",run:oc,preventDefault:!0},{key:"Mod-u",run:lc,preventDefault:!0},{key:"Alt-u",mac:"Mod-Shift-u",run:ac,preventDefault:!0}];function xc(t,e){return N.create(t.ranges.map(e),t.mainIndex)}function kc(t,e){return t.update({selection:e,scrollIntoView:!0,userEvent:"select"})}function Sc({state:t,dispatch:e},i){let n=xc(t.selection,i);return!n.eq(t.selection,!0)&&(e(kc(t,n)),!0)}function Cc(t,e){return N.cursor(e?t.to:t.from)}function Ac(t,e){return Sc(t,i=>i.empty?t.moveByChar(i,e):Cc(i,e))}function Mc(t){return t.textDirectionAt(t.state.selection.main.head)==ni.LTR}const Oc=t=>Ac(t,!Mc(t)),Tc=t=>Ac(t,Mc(t));function Dc(t,e){return Sc(t,i=>i.empty?t.moveByGroup(i,e):Cc(i,e))}function Rc(t,e,i){if(e.type.prop(i))return!0;let n=e.to-e.from;return n&&(n>2||/[^\s,.;:]/.test(t.sliceDoc(e.from,e.to)))||e.firstChild}function Pc(t,e,i){let n,s,r=xa(t).resolveInner(e.head),o=i?pl.closedBy:pl.openedBy;for(let n=e.head;;){let e=i?r.childAfter(n):r.childBefore(n);if(!e)break;Rc(t,e,o)?r=e:n=i?e.to:e.from}return s=r.type.prop(o)&&(n=i?Nh(t,r.from,1):Nh(t,r.to,-1))&&n.matched?i?n.end.to:n.end.from:i?r.to:r.from,N.cursor(s,i?-1:1)}function Bc(t,e){return Sc(t,i=>{if(!i.empty)return Cc(i,e);let n=t.moveVertically(i,e);return n.head!=i.head?n:t.moveToLineBoundary(i,e)})}const Ec=t=>Bc(t,!1),Lc=t=>Bc(t,!0);function Ic(t){let e,i=t.scrollDOM.clientHeighti.empty?t.moveVertically(i,e,n.height):Cc(i,e));if(r.eq(s.selection))return!1;if(n.selfScroll){let e=t.coordsAtPos(s.selection.main.head),o=t.scrollDOM.getBoundingClientRect(),l=o.top+n.marginTop,a=o.bottom-n.marginBottom;e&&e.top>l&&e.bottomNc(t,!1),Hc=t=>Nc(t,!0);function Vc(t,e,i){let n=t.lineBlockAt(e.head),s=t.moveToLineBoundary(e,i);if(s.head==e.head&&s.head!=(i?n.to:n.from)&&(s=t.moveToLineBoundary(e,i,!1)),!i&&s.head==n.from&&n.length){let i=/^\s*/.exec(t.state.sliceDoc(n.from,Math.min(n.from+100,n.to)))[0].length;i&&e.head!=n.from+i&&(s=N.cursor(n.from+i))}return s}function zc(t,e,i){let n=xc(t.state.selection,t=>{t.undirectional&&t.head>=t.anchor!=e&&(t=N.range(t.head,t.anchor));let n=i(t);return N.range(t.anchor,n.head,n.goalColumn,n.bidiLevel||void 0,n.assoc)});return!n.eq(t.state.selection)&&(t.dispatch(kc(t.state,n)),!0)}function Fc(t,e){return zc(t,e,i=>t.moveByChar(i,e))}const qc=t=>Fc(t,!Mc(t)),_c=t=>Fc(t,Mc(t));function $c(t,e){return zc(t,e,i=>t.moveByGroup(i,e))}function Uc(t,e){return zc(t,e,i=>t.moveVertically(i,e))}const Qc=t=>Uc(t,!1),jc=t=>Uc(t,!0);function Kc(t,e){return zc(t,e,i=>t.moveVertically(i,e,Ic(t).height))}const Xc=t=>Kc(t,!1),Gc=t=>Kc(t,!0),Yc=({state:t,dispatch:e})=>(e(kc(t,{anchor:0})),!0),Jc=({state:t,dispatch:e})=>(e(kc(t,{anchor:t.doc.length})),!0),Zc=({state:t,dispatch:e})=>(e(kc(t,{anchor:t.selection.main.anchor,head:0})),!0),tu=({state:t,dispatch:e})=>(e(kc(t,{anchor:t.selection.main.anchor,head:t.doc.length})),!0);function eu(t,e){let{state:i}=t,n=i.selection,s=i.selection.ranges.slice();for(let n of i.selection.ranges){let r=i.doc.lineAt(n.head);if(e?r.to0)for(let i=n;;){let n=t.moveVertically(i,e);if(n.headr.to){s.some(t=>t.head==n.head)||s.push(n);break}if(n.head==i.head)break;i=n}}return s.length!=n.ranges.length&&(t.dispatch(kc(i,N.create(s,s.length-1))),!0)}function iu(t,e){if(t.state.readOnly)return!1;let i="delete.selection",{state:n}=t,s=n.changeByRange(n=>{let{from:s,to:r}=n;if(s==r){let o=e(n);os&&(i="delete.forward",o=nu(t,o,!0)),s=Math.min(s,o),r=Math.max(r,o)}else s=nu(t,s,!1),r=nu(t,r,!0);return s==r?{range:n}:{changes:{from:s,to:r},range:N.cursor(s,se(t)))n.between(e,e,(t,n)=>{te&&(e=i?n:t)});return e}const su=(t,e,i)=>iu(t,n=>{let s,r,o=n.from,{state:l}=t,a=l.doc.lineAt(o);if(i&&!e&&o>a.from&&osu(t,!1,!0),ou=t=>su(t,!0,!1),lu=(t,e)=>iu(t,i=>{let n=i.head,{state:s}=t,r=s.doc.lineAt(n),o=s.charCategorizer(n);for(let t=null;;){if(n==(e?r.to:r.from)){n==i.head&&r.number!=(e?s.doc.lines:1)&&(n+=e?1:-1);break}let l=x(r.text,n-r.from,e)+r.from,a=r.text.slice(Math.min(n,l)-r.from,Math.max(n,l)-r.from),h=o(a);if(null!=t&&h!=t)break;" "==a&&n==i.head||(t=h),n=l}return n}),au=t=>lu(t,!1);function hu(t){let e=[],i=-1;for(let n of t.selection.ranges){let s=t.doc.lineAt(n.from),r=t.doc.lineAt(n.to);if(n.empty||n.to!=r.from||(r=t.doc.lineAt(n.to-1)),i>=s.number){let t=e[e.length-1];t.to=r.to,t.ranges.push(n)}else e.push({from:s.from,to:r.to,ranges:[n]});i=r.number+1}return e}function cu(t,e,i){if(t.readOnly)return!1;let n=[],s=[];for(let e of hu(t)){if(i?e.to==t.doc.length:0==e.from)continue;let r=t.doc.lineAt(i?e.to+1:e.from-1),o=r.length+1;if(i){n.push({from:e.to,to:r.to},{from:e.from,insert:r.text+t.lineBreak});for(let i of e.ranges)s.push(N.range(Math.min(t.doc.length,i.anchor+o),Math.min(t.doc.length,i.head+o)))}else{n.push({from:r.from,to:e.from},{from:e.to,insert:t.lineBreak+r.text});for(let t of e.ranges)s.push(N.range(t.anchor-o,t.head-o))}}return!!n.length&&(e(t.update({changes:n,scrollIntoView:!0,selection:N.create(s,t.selection.mainIndex),userEvent:"move.line"})),!0)}function uu(t,e,i){if(t.readOnly)return!1;let n=[];for(let e of hu(t))i?n.push({from:e.from,insert:t.doc.slice(e.from,e.to)+t.lineBreak}):n.push({from:e.to,insert:t.lineBreak+t.doc.slice(e.from,e.to)});let s=t.changes(n);return e(t.update({changes:s,selection:t.selection.map(s,i?1:-1),scrollIntoView:!0,userEvent:"input.copyline"})),!0}const fu=du(!1);function du(t){return({state:e,dispatch:i})=>{if(e.readOnly)return!1;let n=e.changeByRange(i=>{let{from:n,to:s}=i,r=e.doc.lineAt(n),o=!t&&n==s&&function(t,e){if(/\(\)|\[\]|\{\}/.test(t.sliceDoc(e-1,e+1)))return{from:e,to:e};let i,n=xa(t).resolveInner(e),s=n.childBefore(e),r=n.childAfter(e);return s&&r&&s.to<=e&&r.from>=e&&(i=s.type.prop(pl.closedBy))&&i.indexOf(r.name)>-1&&t.doc.lineAt(s.to).from==t.doc.lineAt(r.from).from&&!/\S/.test(t.sliceDoc(s.to,r.from))?{from:s.to,to:r.from}:null}(e,n);t&&(n=s=(s<=r.to?r:e.doc.lineAt(s)).to);let l=new Wa(e,{simulateBreak:n,simulateDoubleBreak:!!o}),a=Na(l,n);for(null==a&&(a=Qt(/^\s*/.exec(e.doc.lineAt(n).text)[0],e.tabSize));sr.from&&n{let s=[];for(let r=n.from;r<=n.to;){let o=t.doc.lineAt(r);o.number>i&&(n.empty||n.to>o.from)&&(e(o,s,n),i=o.number),r=o.to+1}let r=t.changes(s);return{changes:s,range:N.range(r.mapPos(n.anchor,1),r.mapPos(n.head,1))}})}const mu=[{key:"Alt-ArrowLeft",mac:"Ctrl-ArrowLeft",run:t=>Sc(t,e=>Pc(t.state,e,!Mc(t))),shift:t=>{let e=!Mc(t);return zc(t,e,i=>Pc(t.state,i,e))}},{key:"Alt-ArrowRight",mac:"Ctrl-ArrowRight",run:t=>Sc(t,e=>Pc(t.state,e,Mc(t))),shift:t=>{let e=Mc(t);return zc(t,e,i=>Pc(t.state,i,e))}},{key:"Alt-ArrowUp",run:({state:t,dispatch:e})=>cu(t,e,!1)},{key:"Shift-Alt-ArrowUp",run:({state:t,dispatch:e})=>uu(t,e,!1)},{key:"Alt-ArrowDown",run:({state:t,dispatch:e})=>cu(t,e,!0)},{key:"Shift-Alt-ArrowDown",run:({state:t,dispatch:e})=>uu(t,e,!0)},{key:"Mod-Alt-ArrowUp",run:t=>eu(t,!1)},{key:"Mod-Alt-ArrowDown",run:t=>eu(t,!0)},{key:"Escape",run:({state:t,dispatch:e})=>{let i=t.selection,n=null;return i.ranges.length>1?n=N.create([i.main]):i.main.empty||(n=N.create([N.cursor(i.main.head)])),!!n&&(e(kc(t,n)),!0)}},{key:"Mod-Enter",run:du(!0)},{key:"Alt-l",mac:"Ctrl-l",run:({state:t,dispatch:e})=>{let i=hu(t).map(({from:e,to:i})=>N.range(e,Math.min(i+1,t.doc.length)));return e(t.update({selection:N.create(i),userEvent:"select"})),!0}},{key:"Mod-i",run:({state:t,dispatch:e})=>{let i=xc(t.selection,e=>{let i=xa(t),n=i.resolveStack(e.from,1);if(e.empty){let t=i.resolveStack(e.from,-1);t.node.from>=n.node.from&&t.node.to<=n.node.to&&(n=t)}for(let t=n;t;t=t.next){let{node:i}=t;if((i.from=e.to||i.to>e.to&&i.from<=e.from)&&t.next)return N.range(i.to,i.from)}return e});return!i.eq(t.selection)&&(e(kc(t,i)),!0)},preventDefault:!0},{key:"Mod-[",run:({state:t,dispatch:e})=>!t.readOnly&&(e(t.update(pu(t,(e,i)=>{let n=/^\s*/.exec(e.text)[0];if(!n)return;let s=Qt(n,t.tabSize),r=0,o=Ia(t,Math.max(0,s-La(t)));for(;r!t.readOnly&&(e(t.update(pu(t,(e,i)=>{i.push({from:e.from,insert:t.facet(Ea)})}),{userEvent:"input.indent"})),!0)},{key:"Mod-Alt-\\",run:({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=Object.create(null),n=new Wa(t,{overrideIndentation:t=>{let e=i[t];return null==e?-1:e}}),s=pu(t,(e,s,r)=>{let o=Na(n,e.from);if(null==o)return;/\S/.test(e.text)||(o=0);let l=/^\s*/.exec(e.text)[0],a=Ia(t,o);(l!=a||r.from{if(t.state.readOnly)return!1;let{state:e}=t,i=e.changes(hu(e).map(({from:t,to:i})=>(t>0?t--:i{let i;if(t.lineWrapping){let n=t.lineBlockAt(e.head),s=t.coordsAtPos(e.head,e.assoc||1);s&&(i=n.bottom+t.documentTop-s.bottom+t.defaultLineHeight/2)}return t.moveVertically(e,!0,i)}).map(i);return t.dispatch({changes:i,selection:n,scrollIntoView:!0,userEvent:"delete.line"}),!0}},{key:"Shift-Mod-\\",run:({state:t,dispatch:e})=>function(t,e,i){let n=!1,s=xc(t.selection,e=>{let s=Nh(t,e.head,-1)||Nh(t,e.head,1)||e.head>0&&Nh(t,e.head-1,1)||e.head{let{state:e}=t,i=e.doc.lineAt(e.selection.main.from),n=Xh(t.state,i.from);return n.line?Qh(t):!!n.block&&Kh(t)}},{key:"Alt-A",run:jh},{key:"Ctrl-m",mac:"Shift-Alt-m",run:t=>(t.setTabFocusMode(),!0)}].concat([{key:"ArrowLeft",run:Oc,shift:qc,preventDefault:!0},{key:"Mod-ArrowLeft",mac:"Alt-ArrowLeft",run:t=>Dc(t,!Mc(t)),shift:t=>$c(t,!Mc(t)),preventDefault:!0},{mac:"Cmd-ArrowLeft",run:t=>Sc(t,e=>Vc(t,e,!Mc(t))),shift:t=>{let e=!Mc(t);return zc(t,e,i=>Vc(t,i,e))},preventDefault:!0},{key:"ArrowRight",run:Tc,shift:_c,preventDefault:!0},{key:"Mod-ArrowRight",mac:"Alt-ArrowRight",run:t=>Dc(t,Mc(t)),shift:t=>$c(t,Mc(t)),preventDefault:!0},{mac:"Cmd-ArrowRight",run:t=>Sc(t,e=>Vc(t,e,Mc(t))),shift:t=>{let e=Mc(t);return zc(t,e,i=>Vc(t,i,e))},preventDefault:!0},{key:"ArrowUp",run:Ec,shift:Qc,preventDefault:!0},{mac:"Cmd-ArrowUp",run:Yc,shift:Zc},{mac:"Ctrl-ArrowUp",run:Wc,shift:Xc},{key:"ArrowDown",run:Lc,shift:jc,preventDefault:!0},{mac:"Cmd-ArrowDown",run:Jc,shift:tu},{mac:"Ctrl-ArrowDown",run:Hc,shift:Gc},{key:"PageUp",run:Wc,shift:Xc},{key:"PageDown",run:Hc,shift:Gc},{key:"Home",run:t=>Sc(t,e=>Vc(t,e,!1)),shift:t=>zc(t,!1,e=>Vc(t,e,!1)),preventDefault:!0},{key:"Mod-Home",run:Yc,shift:Zc},{key:"End",run:t=>Sc(t,e=>Vc(t,e,!0)),shift:t=>zc(t,!0,e=>Vc(t,e,!0)),preventDefault:!0},{key:"Mod-End",run:Jc,shift:tu},{key:"Enter",run:fu,shift:fu},{key:"Mod-a",run:({state:t,dispatch:e})=>(e(t.update({selection:{anchor:0,head:t.doc.length},userEvent:"select"})),!0)},{key:"Backspace",run:ru,shift:ru,preventDefault:!0},{key:"Delete",run:ou,preventDefault:!0},{key:"Mod-Backspace",mac:"Alt-Backspace",run:au,preventDefault:!0},{key:"Mod-Delete",mac:"Alt-Delete",run:t=>lu(t,!0),preventDefault:!0},{mac:"Mod-Backspace",run:t=>iu(t,e=>{let i=t.moveToLineBoundary(e,!1).head;return e.head>i?i:Math.max(0,e.head-1)}),preventDefault:!0},{mac:"Mod-Delete",run:t=>iu(t,e=>{let i=t.moveToLineBoundary(e,!0).head;return e.headSc(t,e=>N.cursor(t.lineBlockAt(e.head).from,1)),shift:t=>zc(t,!1,e=>N.cursor(t.lineBlockAt(e.head).from))},{key:"Ctrl-e",run:t=>Sc(t,e=>N.cursor(t.lineBlockAt(e.head).to,-1)),shift:t=>zc(t,!0,e=>N.cursor(t.lineBlockAt(e.head).to))},{key:"Ctrl-d",run:ou},{key:"Ctrl-h",run:ru},{key:"Ctrl-k",run:t=>iu(t,e=>{let i=t.lineBlockAt(e.head).to;return e.head{if(t.readOnly)return!1;let i=t.changeByRange(t=>({changes:{from:t.from,to:t.to,insert:u.of(["",""])},range:N.cursor(t.from)}));return e(t.update(i,{scrollIntoView:!0,userEvent:"input"})),!0}},{key:"Ctrl-t",run:({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=t.changeByRange(e=>{if(!e.empty||0==e.from||e.from==t.doc.length)return{range:e};let i=e.from,n=t.doc.lineAt(i),s=i==n.from?i-1:x(n.text,i-n.from,!1)+n.from,r=i==n.to?i+1:x(n.text,i-n.from,!0)+n.from;return{changes:{from:s,to:r,insert:t.doc.slice(i,r).append(t.doc.slice(s,i))},range:N.cursor(r)}});return!i.changes.empty&&(e(t.update(i,{scrollIntoView:!0,userEvent:"move.character"})),!0)}},{key:"Ctrl-v",run:Hc}].map(t=>({mac:t.key,run:t.run,shift:t.shift})))),gu="function"==typeof String.prototype.normalize?t=>t.normalize("NFKD"):t=>t;class vu{constructor(t,e,i=0,n=t.length,s,r){this.test=r,this.value={from:0,to:0,precise:!1},this.done=!1,this.matches=[],this.buffer="",this.bufferPos=0,this.iter=t.iterRange(i,n),this.bufferStart=i,this.normalize=s?t=>s(gu(t)):gu,this.query=this.normalize(e)}peek(){if(this.bufferPos==this.buffer.length){if(this.bufferStart+=this.buffer.length,this.iter.next(),this.iter.done)return-1;this.bufferPos=0,this.buffer=this.iter.value}return k(this.buffer,this.bufferPos)}next(){for(;this.matches.length;)this.matches.pop();return this.nextOverlapping()}nextOverlapping(){for(;;){let t=this.peek();if(t<0)return this.done=!0,this;let e=S(t),i=this.bufferStart+this.bufferPos;this.bufferPos+=C(t);let n=this.normalize(e);if(n.length)for(let t=0,s=i,r=!0;;t++){let i=n.charCodeAt(t),o=this.match(i,s,r,this.bufferPos+this.bufferStart,t==n.length-1);if(o)return this.value=o,this;if(t==n.length-1)break;r&&tthis.to&&(this.curLine=this.curLine.slice(0,this.to-this.curLineStart)),this.iter.next())}nextLine(){this.curLineStart=this.curLineStart+this.curLine.length+1,this.curLineStart>this.to?this.curLine="":this.getLine(0)}next(){for(let t=this.matchPos-this.curLineStart;;){this.re.lastIndex=t;let e=this.matchPos<=this.to&&this.re.exec(this.curLine);if(e){let i=this.curLineStart+e.index,n=i+e[0].length;if(this.matchPos=Cu(this.text,n+(i==n?1:0)),i==this.curLineStart+this.curLine.length&&this.nextLine(),(ithis.value.to)&&(!this.test||this.test(i,n,e)))return this.value={from:i,to:n,precise:!0,match:e},this;t=this.matchPos-this.curLineStart}else{if(!(this.curLineStart+this.curLine.length=i||n.to<=e){let n=new ku(e,t.sliceString(e,i));return xu.set(t,n),n}if(n.from==e&&n.to==i)return n;let{text:s,from:r}=n;return r>e&&(s=t.sliceString(e,r)+s,r=e),n.to=this.to?this.to:this.text.lineAt(t).to}next(){for(;;){let t=this.re.lastIndex=this.matchPos-this.flat.from,e=this.re.exec(this.flat.text);if(e&&!e[0]&&e.index==t&&(this.re.lastIndex=t+1,e=this.re.exec(this.flat.text)),e){let t=this.flat.from+e.index,i=t+e[0].length;if((this.flat.to>=this.to||e.index+e[0].length<=this.flat.text.length-10)&&(!this.test||this.test(t,i,e)))return this.value={from:t,to:i,precise:!0,match:e},this.matchPos=Cu(this.text,i+(t==i?1:0)),this}if(this.flat.to==this.to)return this.done=!0,this;this.flat=ku.get(this.text,this.flat.from,this.chunkEnd(this.flat.from+2*this.flat.text.length))}}}function Cu(t,e){if(e>=t.length)return e;let i,n=t.lineAt(e);for(;e=56320&&i<57344;)e++;return e}"undefined"!=typeof Symbol&&(yu.prototype[Symbol.iterator]=Su.prototype[Symbol.iterator]=function(){return this});const Au={highlightWordAroundCursor:!1,minSelectionLength:1,maxMatches:100,wholeWords:!1},Mu=V.define({combine:t=>Tt(t,Au,{highlightWordAroundCursor:(t,e)=>t||e,minSelectionLength:Math.min,maxMatches:Math.min})});function Ou(t){let e=[Bu,Pu];return t&&e.push(Mu.of(t)),e}const Tu=Oe.mark({class:"cm-selectionMatch"}),Du=Oe.mark({class:"cm-selectionMatch cm-selectionMatch-main"});function Ru(t,e,i,n){return!(0!=i&&t(e.sliceDoc(i-1,i))==St.Word||n!=e.doc.length&&t(e.sliceDoc(n,n+1))==St.Word)}const Pu=Fi.fromClass(class{constructor(t){this.decorations=this.getDeco(t)}update(t){(t.selectionSet||t.docChanged||t.viewportChanged)&&(this.decorations=this.getDeco(t.view))}getDeco(t){let e=t.state.facet(Mu),{state:i}=t,n=i.selection;if(n.ranges.length>1)return Oe.none;let s,r=n.main,o=null;if(r.empty){if(!e.highlightWordAroundCursor)return Oe.none;let t=i.wordAt(r.head);if(!t)return Oe.none;o=i.charCategorizer(r.head),s=i.sliceDoc(t.from,t.to)}else{let t=r.to-r.from;if(t200)return Oe.none;if(e.wholeWords){if(s=i.sliceDoc(r.from,r.to),o=i.charCategorizer(r.head),!Ru(o,i,r.from,r.to)||!function(t,e,i,n){return t(e.sliceDoc(i,i+1))==St.Word&&t(e.sliceDoc(n-1,n))==St.Word}(o,i,r.from,r.to))return Oe.none}else if(s=i.sliceDoc(r.from,r.to),!s)return Oe.none}let l=[];for(let n of t.visibleRanges){let t=new vu(i.doc,s,n.from,n.to);for(;!t.next().done;){let{from:n,to:s}=t.value;if((!o||Ru(o,i,n,s))&&(r.empty&&n<=r.from&&s>=r.to?l.push(Du.range(n,s)):(n>=r.to||s<=r.from)&&l.push(Tu.range(n,s)),l.length>e.maxMatches))return Oe.none}}return Oe.set(l)}},{decorations:t=>t.decorations}),Bu=dr.baseTheme({".cm-selectionMatch":{backgroundColor:"#99ff7780"},".cm-searchMatch .cm-selectionMatch":{backgroundColor:"transparent"}});const Eu=V.define({combine:t=>Tt(t,{top:!1,caseSensitive:!1,literal:!1,regexp:!1,wholeWord:!1,createPanel:t=>new hf(t),scrollToMatch:t=>dr.scrollIntoView(t)})});class Lu{constructor(t){this.search=t.search,this.caseSensitive=!!t.caseSensitive,this.literal=!!t.literal,this.regexp=!!t.regexp,this.replace=t.replace||"",this.valid=!!this.search&&(!this.regexp||function(t){try{return new RegExp(t,bu),!0}catch(t){return!1}}(this.search)),this.unquoted=this.unquote(this.search),this.wholeWord=!!t.wholeWord,this.test=t.test}unquote(t){return this.literal?t:t.replace(/\\([nrt\\])/g,(t,e)=>"n"==e?"\n":"r"==e?"\r":"t"==e?"\t":"\\")}eq(t){return this.search==t.search&&this.replace==t.replace&&this.caseSensitive==t.caseSensitive&&this.regexp==t.regexp&&this.wholeWord==t.wholeWord&&this.test==t.test}create(){return this.regexp?new Fu(this):new Wu(this)}getCursor(t,e=0,i){let n=t.doc?t:Ot.create({doc:t});return null==i&&(i=n.doc.length),this.regexp?Hu(this,n,e,i):Nu(this,n,e,i)}}class Iu{constructor(t){this.spec=t}}function Nu(t,e,i,n){let s;return t.wholeWord&&(s=function(t,e){return(i,n,s,r)=>((r>i||r+s.length{if(i&&!i(n,s,r,o))return!1;let l=n>=o&&s<=o+r.length?r.slice(n-o,s-o):e.doc.sliceString(n,s);return t(l,e,n,s)}}(t.test,e,s)),new vu(e.doc,t.unquoted,i,n,t.caseSensitive?void 0:t=>t.toLowerCase(),s)}class Wu extends Iu{constructor(t){super(t)}nextMatch(t,e,i){let n=Nu(this.spec,t,i,t.doc.length).nextOverlapping();if(n.done){let i=Math.min(t.doc.length,e+this.spec.unquoted.length);n=Nu(this.spec,t,0,i).nextOverlapping()}return n.done||n.value.from==e&&n.value.to==i?null:n.value}prevMatchInRange(t,e,i){for(let n=i;;){let i=Math.max(e,n-1e4-this.spec.unquoted.length),s=Nu(this.spec,t,i,n),r=null;for(;!s.nextOverlapping().done;)r=s.value;if(r)return r;if(i==e)return null;n-=1e4}}prevMatch(t,e,i){let n=this.prevMatchInRange(t,0,e);return n||(n=this.prevMatchInRange(t,Math.max(0,i-this.spec.unquoted.length),t.doc.length)),!n||n.from==e&&n.to==i?null:n}getReplacement(t){return this.spec.unquote(this.spec.replace)}matchAll(t,e){let i=Nu(this.spec,t,0,t.doc.length),n=[];for(;!i.next().done;){if(n.length>=e)return null;n.push(i.value)}return n}highlight(t,e,i,n){let s=Nu(this.spec,t,Math.max(0,e-this.spec.unquoted.length),Math.min(i+this.spec.unquoted.length,t.doc.length));for(;!s.next().done;)n(s.value.from,s.value.to)}}function Hu(t,e,i,n){let s;var r;return t.wholeWord&&(r=e.charCategorizer(e.selection.main.head),s=(t,e,i)=>!i[0].length||(r(Vu(i.input,i.index))!=St.Word||r(zu(i.input,i.index))!=St.Word)&&(r(zu(i.input,i.index+i[0].length))!=St.Word||r(Vu(i.input,i.index+i[0].length))!=St.Word)),t.test&&(s=function(t,e,i){return(n,s,r)=>(!i||i(n,s,r))&&t(r[0],e,n,s)}(t.test,e,s)),new yu(e.doc,t.search,{ignoreCase:!t.caseSensitive,test:s},i,n)}function Vu(t,e){return t.slice(x(t,e,!1),e)}function zu(t,e){return t.slice(e,x(t,e))}class Fu extends Iu{nextMatch(t,e,i){let n=Hu(this.spec,t,i,t.doc.length).next();return n.done&&(n=Hu(this.spec,t,0,e).next()),n.done?null:n.value}prevMatchInRange(t,e,i){for(let n=1;;n++){let s=Math.max(e,i-1e4*n),r=Hu(this.spec,t,s,i),o=null;for(;!r.next().done;)o=r.value;if(o&&(s==e||o.from>s+10))return o;if(s==e)return null}}prevMatch(t,e,i){return this.prevMatchInRange(t,0,e)||this.prevMatchInRange(t,i,t.doc.length)}getReplacement(t){return this.spec.unquote(this.spec.replace).replace(/\$([$&]|\d+)/g,(e,i)=>{if("&"==i)return t.match[0];if("$"==i)return"$";for(let e=i.length;e>0;e--){let n=+i.slice(0,e);if(n>0&&n=e)return null;n.push(i.value)}return n}highlight(t,e,i,n){let s=Hu(this.spec,t,Math.max(0,e-250),Math.min(i+250,t.doc.length));for(;!s.next().done;)n(s.value.from,s.value.to)}}const qu=mt.define(),_u=mt.define(),$u=Q.define({create:t=>new Uu(nf(t).create(),null),update(t,e){for(let i of e.effects)i.is(qu)?t=new Uu(i.value.create(),t.panel):i.is(_u)&&(t=new Uu(t.query,i.value?ef:null));return t},provide:t=>No.from(t,t=>t.panel)});class Uu{constructor(t,e){this.query=t,this.panel=e}}const Qu=Oe.mark({class:"cm-searchMatch"}),ju=Oe.mark({class:"cm-searchMatch cm-searchMatch-selected"}),Ku=Fi.fromClass(class{constructor(t){this.view=t,this.decorations=this.highlight(t.state.field($u))}update(t){let e=t.state.field($u);(e!=t.startState.field($u)||t.docChanged||t.selectionSet||t.viewportChanged)&&(this.decorations=this.highlight(e))}highlight({query:t,panel:e}){if(!e||!t.spec.valid)return Oe.none;let{view:i}=this,n=new It;for(let e=0,s=i.visibleRanges,r=s.length;es[e+1].from-500;)l=s[++e].to;t.highlight(i.state,o,l,(t,e)=>{let s=i.state.selection.ranges.some(i=>i.from==t&&i.to==e);n.add(t,e,s?ju:Qu)})}return n.finish()}},{decorations:t=>t.decorations});function Xu(t){return e=>{let i=e.state.field($u,!1);return i&&i.query.spec.valid?t(e,i):of(e)}}const Gu=Xu((t,{query:e})=>{let{to:i}=t.state.selection.main,n=e.nextMatch(t.state,i,i);if(!n)return!1;let s=N.single(n.from,n.to),r=t.state.facet(Eu);return t.dispatch({selection:s,effects:[df(t,n),r.scrollToMatch(s.main,t)],userEvent:"select.search"}),rf(t),!0}),Yu=Xu((t,{query:e})=>{let{state:i}=t,{from:n}=i.selection.main,s=e.prevMatch(i,n,n);if(!s)return!1;let r=N.single(s.from,s.to),o=t.state.facet(Eu);return t.dispatch({selection:r,effects:[df(t,s),o.scrollToMatch(r.main,t)],userEvent:"select.search"}),rf(t),!0}),Ju=Xu((t,{query:e})=>{let i=e.matchAll(t.state,1e3);return!(!i||!i.length)&&(t.dispatch({selection:N.create(i.map(t=>N.range(t.from,t.to))),userEvent:"select.search.matches"}),!0)}),Zu=Xu((t,{query:e})=>{let{state:i}=t,{from:n,to:s}=i.selection.main;if(i.readOnly)return!1;let r=e.nextMatch(i,n,n);if(!r)return!1;let o,l,a=r,h=[],c=[];a.precise?a.from==n&&a.to==s&&(l=i.toText(e.getReplacement(a)),h.push({from:a.from,to:a.to,insert:l}),a=e.nextMatch(i,a.from,a.to),c.push(dr.announce.of(i.phrase("replaced match on line $",i.doc.lineAt(n).number)+"."))):a=e.nextMatch(i,a.from,a.to);let u=t.state.changes(h);return a&&(o=N.single(a.from,a.to).map(u),c.push(df(t,a)),c.push(i.facet(Eu).scrollToMatch(o.main,t))),t.dispatch({changes:u,selection:o,effects:c,userEvent:"input.replace"}),!0}),tf=Xu((t,{query:e})=>{if(t.state.readOnly)return!1;let i=[];for(let n of e.matchAll(t.state,1e9)){let{from:t,to:s,precise:r}=n;r&&i.push({from:t,to:s,insert:e.getReplacement(n)})}if(!i.length)return!1;let n=t.state.phrase("replaced $ matches",i.length)+".";return t.dispatch({changes:i,effects:dr.announce.of(n),userEvent:"input.replace.all"}),!0});function ef(t){return t.state.facet(Eu).createPanel(t)}function nf(t,e){var i,n,s,r,o;let l=t.selection.main,a=l.empty||l.to>l.from+100?"":t.sliceDoc(l.from,l.to);if(e&&!a)return e;let h=t.facet(Eu);return new Lu({search:(null!==(i=null==e?void 0:e.literal)&&void 0!==i?i:h.literal)?a:a.replace(/\n/g,"\\n"),caseSensitive:null!==(n=null==e?void 0:e.caseSensitive)&&void 0!==n?n:h.caseSensitive,literal:null!==(s=null==e?void 0:e.literal)&&void 0!==s?s:h.literal,regexp:null!==(r=null==e?void 0:e.regexp)&&void 0!==r?r:h.regexp,wholeWord:null!==(o=null==e?void 0:e.wholeWord)&&void 0!==o?o:h.wholeWord})}function sf(t){let e=Bo(t,ef);return e&&e.dom.querySelector("[main-field]")}function rf(t){let e=sf(t);e&&e==t.root.activeElement&&e.select()}const of=t=>{let e=t.state.field($u,!1);if(e&&e.panel){let i=sf(t);if(i&&i!=t.root.activeElement){let n=nf(t.state,e.query.spec);n.valid&&t.dispatch({effects:qu.of(n)}),i.focus(),i.select()}}else t.dispatch({effects:[_u.of(!0),e?qu.of(nf(t.state,e.query.spec)):mt.appendConfig.of(mf)]});return!0},lf=t=>{let e=t.state.field($u,!1);if(!e||!e.panel)return!1;let i=Bo(t,ef);return i&&i.dom.contains(t.root.activeElement)&&t.focus(),t.dispatch({effects:_u.of(!1)}),!0},af=[{key:"Mod-f",run:of,scope:"editor search-panel"},{key:"F3",run:Gu,shift:Yu,scope:"editor search-panel",preventDefault:!0},{key:"Mod-g",run:Gu,shift:Yu,scope:"editor search-panel",preventDefault:!0},{key:"Escape",run:lf,scope:"editor search-panel"},{key:"Mod-Shift-l",run:({state:t,dispatch:e})=>{let i=t.selection;if(i.ranges.length>1||i.main.empty)return!1;let{from:n,to:s}=i.main,r=[],o=0;for(let e=new vu(t.doc,t.sliceDoc(n,s));!e.next().done;){if(r.length>1e3)return!1;e.value.from==n&&(o=r.length),r.push(N.range(e.value.from,e.value.to))}return e(t.update({selection:N.create(r,o),userEvent:"select.search.matches"})),!0}},{key:"Mod-Alt-g",run:t=>{let{state:e}=t,i=String(e.doc.lineAt(t.state.selection.main.head).number),{close:n,result:s}=Wo(t,{label:e.phrase("Go to line"),input:{type:"text",name:"line",value:i},focus:!0,submitLabel:e.phrase("go")});return s.then(i=>{let s=i&&/^([+-])?(\d+)?(:\d+)?(%)?$/.exec(i.elements.line.value);if(!s)return void t.dispatch({effects:n});let r=e.doc.lineAt(e.selection.main.head),[,o,l,a,h]=s,c=a?+a.slice(1):0,u=l?+l:r.number;if(l&&h){let t=u/100;o&&(t=t*("-"==o?-1:1)+r.number/e.doc.lines),u=Math.round(e.doc.lines*t)}else l&&o&&(u=u*("-"==o?-1:1)+r.number);let f=e.doc.line(Math.max(1,Math.min(e.doc.lines,u))),d=N.cursor(f.from+Math.max(0,Math.min(c,f.length)));t.dispatch({effects:[n,dr.scrollIntoView(d.from,{y:"center"})],selection:d})}),!0}},{key:"Mod-d",run:({state:t,dispatch:e})=>{let{ranges:i}=t.selection;if(i.some(t=>t.from===t.to))return(({state:t,dispatch:e})=>{let{selection:i}=t,n=N.create(i.ranges.map(e=>t.wordAt(e.head)||N.cursor(e.head)),i.mainIndex);return!n.eq(i)&&(e(t.update({selection:n})),!0)})({state:t,dispatch:e});let n=t.sliceDoc(i[0].from,i[0].to);if(t.selection.ranges.some(e=>t.sliceDoc(e.from,e.to)!=n))return!1;let s=function(t,e){let{main:i,ranges:n}=t.selection,s=t.wordAt(i.head),r=s&&s.from==i.from&&s.to==i.to;for(let i=!1,s=new vu(t.doc,e,n[n.length-1].to);;){if(s.next(),!s.done){if(i&&n.some(t=>t.from==s.value.from))continue;if(r){let e=t.wordAt(s.value.from);if(!e||e.from!=s.value.from||e.to!=s.value.to)continue}return s.value}if(i)return null;s=new vu(t.doc,e,0,Math.max(0,n[n.length-1].from-1)),i=!0}}(t,n);return!!s&&(e(t.update({selection:t.selection.addRange(N.range(s.from,s.to),!1),effects:dr.scrollIntoView(s.to)})),!0)},preventDefault:!0}];class hf{constructor(t){this.view=t;let e=this.query=t.state.field($u).query.spec;function i(t,e,i){return oe("button",{class:"cm-button",name:t,onclick:e,type:"button"},i)}this.commit=this.commit.bind(this),this.searchField=oe("input",{value:e.search,placeholder:cf(t,"Find"),"aria-label":cf(t,"Find"),class:"cm-textfield",name:"search",form:"","main-field":"true",onchange:this.commit,onkeyup:this.commit}),this.replaceField=oe("input",{value:e.replace,placeholder:cf(t,"Replace"),"aria-label":cf(t,"Replace"),class:"cm-textfield",name:"replace",form:"",onchange:this.commit,onkeyup:this.commit}),this.caseField=oe("input",{type:"checkbox",name:"case",form:"",checked:e.caseSensitive,onchange:this.commit}),this.reField=oe("input",{type:"checkbox",name:"re",form:"",checked:e.regexp,onchange:this.commit}),this.wordField=oe("input",{type:"checkbox",name:"word",form:"",checked:e.wholeWord,onchange:this.commit}),this.dom=oe("div",{onkeydown:t=>this.keydown(t),class:"cm-search"},[this.searchField,i("next",()=>Gu(t),[cf(t,"next")]),i("prev",()=>Yu(t),[cf(t,"previous")]),i("select",()=>Ju(t),[cf(t,"all")]),oe("label",null,[this.caseField,cf(t,"match case")]),oe("label",null,[this.reField,cf(t,"regexp")]),oe("label",null,[this.wordField,cf(t,"by word")]),...t.state.readOnly?[]:[oe("br"),this.replaceField,i("replace",()=>Zu(t),[cf(t,"replace")]),i("replaceAll",()=>tf(t),[cf(t,"replace all")])],oe("button",{name:"close",onclick:()=>lf(t),"aria-label":cf(t,"close"),type:"button"},["×"])])}commit(){let t=new Lu({search:this.searchField.value,caseSensitive:this.caseField.checked,regexp:this.reField.checked,wholeWord:this.wordField.checked,replace:this.replaceField.value});t.eq(this.query)||(this.query=t,this.view.dispatch({effects:qu.of(t)}))}keydown(t){var e,i,n;e=this.view,i=t,n="search-panel",Or(Sr(e.state),i,e,n)?t.preventDefault():13==t.keyCode&&t.target==this.searchField?(t.preventDefault(),(t.shiftKey?Yu:Gu)(this.view)):13==t.keyCode&&t.target==this.replaceField&&(t.preventDefault(),Zu(this.view))}update(t){for(let e of t.transactions)for(let t of e.effects)t.is(qu)&&!t.value.eq(this.query)&&this.setQuery(t.value)}setQuery(t){this.query=t,this.searchField.value=t.search,this.replaceField.value=t.replace,this.caseField.checked=t.caseSensitive,this.reField.checked=t.regexp,this.wordField.checked=t.wholeWord}mount(){this.searchField.select()}get pos(){return 80}get top(){return this.view.state.facet(Eu).top}}function cf(t,e){return t.state.phrase(e)}const uf=30,ff=/[\s\.,:;?!]/;function df(t,{from:e,to:i}){let n=t.state.doc.lineAt(e),s=t.state.doc.lineAt(i).to,r=Math.max(n.from,e-uf),o=Math.min(s,i+uf),l=t.state.sliceDoc(r,o);if(r!=n.from)for(let t=0;tl.length-uf;t--)if(!ff.test(l[t-1])&&ff.test(l[t])){l=l.slice(0,t);break}return dr.announce.of(`${t.state.phrase("current match")}. ${l} ${t.state.phrase("on line")} ${n.number}.`)}const pf=dr.baseTheme({".cm-panel.cm-search":{padding:"2px 6px 4px",position:"relative","& [name=close]":{position:"absolute",top:"0",right:"4px",backgroundColor:"inherit",border:"none",font:"inherit",padding:0,margin:0},"& input, & button, & label":{margin:".2em .6em .2em 0"},"& input[type=checkbox]":{marginRight:".2em"},"& label":{fontSize:"80%",whiteSpace:"pre"}},"&light .cm-searchMatch":{backgroundColor:"#ffff0054"},"&dark .cm-searchMatch":{backgroundColor:"#00ffff8a"},"&light .cm-searchMatch-selected":{backgroundColor:"#ff6a0054"},"&dark .cm-searchMatch-selected":{backgroundColor:"#ff00ff8a"}}),mf=[$u,J.low(Ku),pf];class gf{constructor(t,e,i,n){this.state=t,this.pos=e,this.explicit=i,this.view=n,this.abortListeners=[],this.abortOnDocChange=!1}tokenBefore(t){let e=xa(this.state).resolveInner(this.pos,-1);for(;e&&t.indexOf(e.name)<0;)e=e.parent;return e?{from:e.from,to:this.pos,text:this.state.sliceDoc(e.from,this.pos),type:e.type}:null}matchBefore(t){let e=this.state.doc.lineAt(this.pos),i=Math.max(e.from,this.pos-250),n=e.text.slice(i-e.from,this.pos-e.from),s=n.search(xf(t,!1));return s<0?null:{from:i+s,to:this.pos,text:n.slice(s)}}get aborted(){return null==this.abortListeners}addEventListener(t,e,i){"abort"==t&&this.abortListeners&&(this.abortListeners.push(e),i&&i.onDocChange&&(this.abortOnDocChange=!0))}}function vf(t){let e=Object.keys(t).join(""),i=/\w/.test(e);return i&&(e=e.replace(/\w/g,"")),`[${i?"\\w":""}${e.replace(/[^\w\s]/g,"\\$&")}]`}function wf(t){let e=t.map(t=>"string"==typeof t?{label:t}:t),[i,n]=e.every(t=>/^\w+$/.test(t.label))?[/\w*$/,/\w+$/]:function(t){let e=Object.create(null),i=Object.create(null);for(let{label:n}of t){e[n[0]]=!0;for(let t=1;t{let s=t.matchBefore(n);return s||t.explicit?{from:s?s.from:t.pos,options:e,validFor:i}:null}}class bf{constructor(t,e,i,n){this.completion=t,this.source=e,this.match=i,this.score=n}}function yf(t){return t.selection.main.from}function xf(t,e){var i;let{source:n}=t,s=e&&"^"!=n[0],r="$"!=n[n.length-1];return s||r?new RegExp(`${s?"^":""}(?:${n})${r?"$":""}`,null!==(i=t.flags)&&void 0!==i?i:t.ignoreCase?"i":""):t}const kf=ft.define();function Sf(t,e,i,n){let{main:s}=t.selection,r=i-s.from,o=n-s.from;return{...t.changeByRange(l=>{if(l!=s&&i!=n&&t.sliceDoc(l.from+r,l.from+o)!=t.sliceDoc(i,n))return{range:l};let a=t.toText(e);return{changes:{from:l.from+r,to:n==s.from?l.to:l.from+o,insert:a},range:N.cursor(l.from+r+a.length)}}),scrollIntoView:!0,userEvent:"input.complete"}}const Cf=new WeakMap;function Af(t){if(!Array.isArray(t))return t;let e=Cf.get(t);return e||Cf.set(t,e=wf(t)),e}const Mf=mt.define(),Of=mt.define();class Tf{constructor(t){this.pattern=t,this.chars=[],this.folded=[],this.any=[],this.precise=[],this.byWord=[],this.score=0,this.matched=[];for(let e=0;e=48&&a<=57||a>=97&&a<=122?2:a>=65&&a<=90?1:0:(w=S(a))!=w.toLowerCase()?1:w!=w.toUpperCase()?2:0;(!n||1==b&&m||0==v&&0!=b)&&(e[c]==a||i[c]==a&&(u=!0)?r[c++]=n:r.length&&(g=!1)),v=b,n+=C(a)}return c==l&&0==r[0]&&g?this.result((u?-200:0)-100,r,t):f==l&&0==d?this.ret(-200-t.length+(p==t.length?0:-100),[0,p]):o>-1?this.ret(-700-t.length,[o,o+this.pattern.length]):f==l?this.ret(-900-t.length,[d,p]):c==l?this.result((u?-200:0)-100-700+(g?0:-1100),r,t):2==e.length?null:this.result((n[0]?-700:0)-200-1100,n,t)}result(t,e,i){let n=[],s=0;for(let t of e){let e=t+(this.astral?C(k(i,t)):1);s&&n[s-1]==t?n[s-1]=e:(n[s++]=t,n[s++]=e)}return this.ret(t-i.length,n)}}class Df{constructor(t){this.pattern=t,this.matched=[],this.score=0,this.folded=t.toLowerCase()}match(t){if(t.lengthTt(t,{activateOnTyping:!0,activateOnCompletion:()=>!1,activateOnTypingDelay:100,selectOnOpen:!0,override:null,closeOnBlur:!0,maxRenderedOptions:100,defaultKeymap:!0,tooltipClass:()=>"",optionClass:()=>"",aboveCursor:!1,icons:!0,addToOptions:[],positionInfo:Bf,filterStrict:!1,compareCompletions:(t,e)=>(t.sortText||t.label).localeCompare(e.sortText||e.label),interactionDelay:75,updateSyncTime:100},{defaultKeymap:(t,e)=>t&&e,closeOnBlur:(t,e)=>t&&e,icons:(t,e)=>t&&e,tooltipClass:(t,e)=>i=>Pf(t(i),e(i)),optionClass:(t,e)=>i=>Pf(t(i),e(i)),addToOptions:(t,e)=>t.concat(e),filterStrict:(t,e)=>t||e})});function Pf(t,e){return t?e?t+" "+e:t:e}function Bf(t,e,i,n,s,r){let o,l,a=t.textDirection==ni.RTL,h=a,c=!1,u="top",f=e.left-s.left,d=s.right-e.right,p=n.right-n.left,m=n.bottom-n.top;if(h&&f=m||t>e.top?o=i.bottom-e.top:(u="bottom",o=e.bottom-i.top)}return{style:`${u}: ${o/((e.bottom-e.top)/r.offsetHeight)}px; max-width: ${l/((e.right-e.left)/r.offsetWidth)}px`,class:"cm-completionInfo-"+(c?a?"left-narrow":"right-narrow":h?"left":"right")}}const Ef=mt.define();function Lf(t,e,i){if(t<=i)return{from:0,to:t};if(e<0&&(e=0),e<=t>>1){let t=Math.floor(e/i);return{from:t*i,to:(t+1)*i}}let n=Math.ceil((t-e)/i);return{from:t-n*i,to:t-(n-1)*i}}class If{constructor(t,e,i){this.view=t,this.stateField=e,this.applyCompletion=i,this.info=null,this.infoDestroy=null,this.placeInfoReq={read:()=>this.measureInfo(),write:t=>this.placeInfo(t),key:this},this.space=null,this.currentClass="";let n=t.state.field(e),{options:s,selected:r}=n.open,o=t.state.facet(Rf);this.optionContent=function(t){let e=t.addToOptions.slice();return t.icons&&e.push({render(t){let e=document.createElement("div");return e.classList.add("cm-completionIcon"),t.type&&e.classList.add(...t.type.split(/\s+/g).map(t=>"cm-completionIcon-"+t)),e.setAttribute("aria-hidden","true"),e},position:20}),e.push({render(t,e,i,n){let s=document.createElement("span");s.className="cm-completionLabel";let r=t.displayLabel||t.label,o=0;for(let t=0;to&&s.appendChild(document.createTextNode(r.slice(o,e)));let l=s.appendChild(document.createElement("span"));l.appendChild(document.createTextNode(r.slice(e,i))),l.className="cm-completionMatchedText",o=i}return ot.position-e.position).map(t=>t.render)}(o),this.optionClass=o.optionClass,this.tooltipClass=o.tooltipClass,this.range=Lf(s.length,r,o.maxRenderedOptions),this.dom=document.createElement("div"),this.dom.className="cm-tooltip-autocomplete",this.updateTooltipClass(t.state),this.dom.addEventListener("mousedown",i=>{let{options:n}=t.state.field(e).open;for(let e,s=i.target;s&&s!=this.dom;s=s.parentNode)if("LI"==s.nodeName&&(e=/-(\d+)$/.exec(s.id))&&+e[1]this.list.lastChild.getBoundingClientRect().bottom?this.range.to:null;null!=e&&(t.dispatch({effects:Ef.of(e)}),i.preventDefault())}}),this.dom.addEventListener("focusout",e=>{let i=t.state.field(this.stateField,!1);i&&i.tooltip&&t.state.facet(Rf).closeOnBlur&&e.relatedTarget!=t.contentDOM&&t.dispatch({effects:Of.of(null)})}),this.showOptions(s,n.id)}mount(){this.updateSel()}showOptions(t,e){this.list&&this.list.remove(),this.list=this.dom.appendChild(this.createListBox(t,e,this.range)),this.list.addEventListener("scroll",()=>{this.info&&this.view.requestMeasure(this.placeInfoReq)})}update(t){var e;let i=t.state.field(this.stateField),n=t.startState.field(this.stateField);if(this.updateTooltipClass(t.state),i!=n){let{options:s,selected:r,disabled:o}=i.open;n.open&&n.open.options==s||(this.range=Lf(s.length,r,t.state.facet(Rf).maxRenderedOptions),this.showOptions(s,i.id)),this.updateSel(),o!=(null===(e=n.open)||void 0===e?void 0:e.disabled)&&this.dom.classList.toggle("cm-tooltip-autocomplete-disabled",!!o)}}updateTooltipClass(t){let e=this.tooltipClass(t);if(e!=this.currentClass){for(let t of this.currentClass.split(" "))t&&this.dom.classList.remove(t);for(let t of e.split(" "))t&&this.dom.classList.add(t);this.currentClass=e}}positioned(t){this.space=t,this.info&&this.view.requestMeasure(this.placeInfoReq)}updateSel(){let t=this.view.state.field(this.stateField),e=t.open;(e.selected>-1&&e.selected=this.range.to)&&(this.range=Lf(e.options.length,e.selected,this.view.state.facet(Rf).maxRenderedOptions),this.showOptions(e.options,t.id));let i=this.updateSelectedOption(e.selected);if(i){this.destroyInfo();let{completion:n}=e.options[e.selected],{info:s}=n;if(!s)return;let r="string"==typeof s?document.createTextNode(s):s(n);if(!r)return;"then"in r?r.then(e=>{e&&this.view.state.field(this.stateField,!1)==t&&this.addInfoPane(e,n)}).catch(t=>Wi(this.view.state,t,"completion info")):(this.addInfoPane(r,n),i.setAttribute("aria-describedby",this.info.id))}}addInfoPane(t,e){this.destroyInfo();let i=this.info=document.createElement("div");if(i.className="cm-tooltip cm-completionInfo",i.id="cm-completionInfo-"+Math.floor(65535*Math.random()).toString(16),null!=t.nodeType)i.appendChild(t),this.infoDestroy=null;else{let{dom:e,destroy:n}=t;i.appendChild(e),this.infoDestroy=n||null}this.dom.appendChild(i),this.view.requestMeasure(this.placeInfoReq)}updateSelectedOption(t){let e=null;for(let i=this.list.firstChild,n=this.range.from;i;i=i.nextSibling,n++)"LI"==i.nodeName&&i.id?n==t?i.hasAttribute("aria-selected")||(i.setAttribute("aria-selected","true"),e=i):i.hasAttribute("aria-selected")&&(i.removeAttribute("aria-selected"),i.removeAttribute("aria-describedby")):n--;return e&&function(t,e){let i=t.getBoundingClientRect(),n=e.getBoundingClientRect(),s=i.height/t.offsetHeight;n.topi.bottom&&(t.scrollTop+=(n.bottom-i.bottom)/s)}(this.list,e),e}measureInfo(){let t=this.dom.querySelector("[aria-selected]");if(!t||!this.info)return null;let e=this.dom.getBoundingClientRect(),i=this.info.getBoundingClientRect(),n=t.getBoundingClientRect(),s=this.space;if(!s){let t=this.dom.ownerDocument.documentElement;s={left:0,top:0,right:t.clientWidth,bottom:t.clientHeight}}return n.top>Math.min(s.bottom,e.bottom)-10||n.bottom{t.target==n&&t.preventDefault()});let s=null;for(let r=i.from;ri.from||0==i.from))if(s=t,"string"!=typeof a&&a.header)n.appendChild(a.header(a));else{n.appendChild(document.createElement("completion-section")).textContent=t}}const h=n.appendChild(document.createElement("li"));h.id=e+"-"+r,h.setAttribute("role","option");let c=this.optionClass(o);c&&(h.className=c);for(let t of this.optionContent){let e=t(o,this.view.state,this.view,l);e&&h.appendChild(e)}}return i.from&&n.classList.add("cm-completionListIncompleteTop"),i.tonew If(i,t,e)}function Wf(t){return 100*(t.boost||0)+(t.apply?10:0)+(t.info?5:0)+(t.type?1:0)}class Hf{constructor(t,e,i,n,s,r){this.options=t,this.attrs=e,this.tooltip=i,this.timestamp=n,this.selected=s,this.disabled=r}setSelected(t,e){return t==this.selected||t>=this.options.length?this:new Hf(this.options,qf(e,t),this.tooltip,this.timestamp,t,this.disabled)}static build(t,e,i,n,s,r){if(n&&!r&&t.some(t=>t.isPending))return n.setDisabled();let o=function(t,e){let i=[],n=null,s=null,r=t=>{i.push(t);let{section:e}=t.completion;if(e){n||(n=[]);let t="string"==typeof e?e:e.name;n.some(e=>e.name==t)||n.push("string"==typeof e?{name:t}:e)}},o=e.facet(Rf);for(let n of t)if(n.hasResult()){let t=n.result.getMatch;if(!1===n.result.filter)for(let e of n.result.options)r(new bf(e,n.source,t?t(e):[],1e9-i.length));else{let i,l=e.sliceDoc(n.from,n.to),a=o.filterStrict?new Df(l):new Tf(l);for(let e of n.result.options)if(i=a.match(e.label)){let o=e.displayLabel?t?t(e,i.matched):[]:i.matched,l=i.score+(e.boost||0);if(r(new bf(e,n.source,o,l)),"object"==typeof e.section&&"dynamic"===e.section.rank){let{name:t}=e.section;s||(s=Object.create(null)),s[t]=Math.max(l,s[t]||-1e9)}}}}if(n){let t=Object.create(null),e=0,r=(t,e)=>("dynamic"===t.rank&&"dynamic"===e.rank?s[e.name]-s[t.name]:0)||("number"==typeof t.rank?t.rank:1e9)-("number"==typeof e.rank?e.rank:1e9)||(t.namee.score-t.score||h(t.completion,e.completion))){let e=t.completion;!a||a.label!=e.label||a.detail!=e.detail||null!=a.type&&null!=e.type&&a.type!=e.type||a.apply!=e.apply||a.boost!=e.boost?l.push(t):Wf(t.completion)>Wf(a)&&(l[l.length-1]=t),a=t.completion}return l}(t,e);if(!o.length)return n&&t.some(t=>t.isPending)?n.setDisabled():null;let l=e.facet(Rf).selectOnOpen?0:-1;if(n&&n.selected!=l&&-1!=n.selected){let t=n.options[n.selected].completion;for(let e=0;ee.hasResult()?Math.min(t,e.from):t,1e8),create:Gf,above:s.aboveCursor},n?n.timestamp:Date.now(),l,!1)}map(t){return new Hf(this.options,this.attrs,{...this.tooltip,pos:t.mapPos(this.tooltip.pos)},this.timestamp,this.selected,this.disabled)}setDisabled(){return new Hf(this.options,this.attrs,this.tooltip,this.timestamp,this.selected,!0)}}class Vf{constructor(t,e,i){this.active=t,this.id=e,this.open=i}static start(){return new Vf(_f,"cm-ac-"+Math.floor(2e6*Math.random()).toString(36),null)}update(t){let{state:e}=t,i=e.facet(Rf),n=(i.override||e.languageDataAt("autocomplete",yf(e)).map(Af)).map(e=>(this.active.find(t=>t.source==e)||new Uf(e,this.active.some(t=>0!=t.state)?1:0)).update(t,i));n.length==this.active.length&&n.every((t,e)=>t==this.active[e])&&(n=this.active);let s=this.open,r=t.effects.some(t=>t.is(jf));s&&t.docChanged&&(s=s.map(t.changes)),t.selection||n.some(e=>e.hasResult()&&t.changes.touchesRange(e.from,e.to))||!function(t,e){if(t==e)return!0;for(let i=0,n=0;;){for(;it.isPending)&&(s=null),!s&&n.every(t=>!t.isPending)&&n.some(t=>t.hasResult())&&(n=n.map(t=>t.hasResult()?new Uf(t.source,0):t));for(let e of t.effects)e.is(Ef)&&(s=s&&s.setSelected(e.value,this.id));return n==this.active&&s==this.open?this:new Vf(n,this.id,s)}get tooltip(){return this.open?this.open.tooltip:null}get attrs(){return this.open?this.open.attrs:this.active.length?zf:Ff}}const zf={"aria-autocomplete":"list"},Ff={};function qf(t,e){let i={"aria-autocomplete":"list","aria-haspopup":"listbox","aria-controls":t};return e>-1&&(i["aria-activedescendant"]=t+"-"+e),i}const _f=[];function $f(t,e){if(t.isUserEvent("input.complete")){let i=t.annotation(kf);if(i&&e.activateOnCompletion(i))return 12}let i=t.isUserEvent("input.type");return i&&e.activateOnTyping?5:i?1:t.isUserEvent("delete.backward")?2:t.selection?8:t.docChanged?16:0}class Uf{constructor(t,e,i=!1){this.source=t,this.state=e,this.explicit=i}hasResult(){return!1}get isPending(){return 1==this.state}update(t,e){let i=$f(t,e),n=this;(8&i||16&i&&this.touches(t))&&(n=new Uf(n.source,0)),4&i&&0==n.state&&(n=new Uf(this.source,1)),n=n.updateFor(t,i);for(let e of t.effects)if(e.is(Mf))n=new Uf(n.source,1,e.value);else if(e.is(Of))n=new Uf(n.source,0);else if(e.is(jf))for(let t of e.value)t.source==n.source&&(n=t);return n}updateFor(t,e){return this.map(t.changes)}map(t){return this}touches(t){return t.changes.touchesRange(yf(t.state))}}class Qf extends Uf{constructor(t,e,i,n,s,r){super(t,3,e),this.limit=i,this.result=n,this.from=s,this.to=r}hasResult(){return!0}updateFor(t,e){var i;if(!(3&e))return this.map(t.changes);let n=this.result;n.map&&!t.changes.empty&&(n=n.map(n,t.changes));let s=t.changes.mapPos(this.from),r=t.changes.mapPos(this.to,1),o=yf(t.state);if(o>r||!n||2&e&&(yf(t.startState)==this.from||ot.map(t=>t.map(e))}),Kf=Q.define({create:()=>Vf.start(),update:(t,e)=>t.update(e),provide:t=>[xo.from(t,t=>t.tooltip),dr.contentAttributes.from(t,t=>t.attrs)]});function Xf(t,e){const i=e.completion.apply||e.completion.label;let n=t.state.field(Kf).active.find(t=>t.source==e.source);return n instanceof Qf&&("string"==typeof i?t.dispatch({...Sf(t.state,i,n.from,n.to),annotations:kf.of(e.completion)}):i(t,e.completion,n.from,n.to),!0)}const Gf=Nf(Kf,Xf);function Yf(t,e="option"){return i=>{let n=i.state.field(Kf,!1);if(!n||!n.open||n.open.disabled||Date.now()-n.open.timestamp-1?n.open.selected+r*(t?1:-1):t?0:o-1;return l<0?l="page"==e?0:o-1:l>=o&&(l="page"==e?o-1:0),i.dispatch({effects:Ef.of(l)}),!0}}const Jf=t=>!!t.state.field(Kf,!1)&&(t.dispatch({effects:Mf.of(!0)}),!0);class Zf{constructor(t,e){this.active=t,this.context=e,this.time=Date.now(),this.updates=[],this.done=void 0}}const td=Fi.fromClass(class{constructor(t){this.view=t,this.debounceUpdate=-1,this.running=[],this.debounceAccept=-1,this.pendingStart=!1,this.composing=0;for(let e of t.state.field(Kf).active)e.isPending&&this.startQuery(e)}update(t){let e=t.state.field(Kf),i=t.state.facet(Rf);if(!t.selectionSet&&!t.docChanged&&t.startState.field(Kf)==e)return;let n=t.transactions.some(t=>{let e=$f(t,i);return 8&e||(t.selection||t.docChanged)&&!(3&e)});for(let e=0;e50&&Date.now()-i.time>1e3){for(let t of i.context.abortListeners)try{t()}catch(t){Wi(this.view.state,t)}i.context.abortListeners=null,this.running.splice(e--,1)}else i.updates.push(...t.transactions)}this.debounceUpdate>-1&&clearTimeout(this.debounceUpdate),t.transactions.some(t=>t.effects.some(t=>t.is(Mf)))&&(this.pendingStart=!0);let s=this.pendingStart?50:i.activateOnTypingDelay;if(this.debounceUpdate=e.active.some(t=>t.isPending&&!this.running.some(e=>e.active.source==t.source))?setTimeout(()=>this.startUpdate(),s):-1,0!=this.composing)for(let e of t.transactions)e.isUserEvent("input.type")?this.composing=2:2==this.composing&&e.selection&&(this.composing=3)}startUpdate(){this.debounceUpdate=-1,this.pendingStart=!1;let{state:t}=this.view,e=t.field(Kf);for(let t of e.active)t.isPending&&!this.running.some(e=>e.active.source==t.source)&&this.startQuery(t);this.running.length&&e.open&&e.open.disabled&&(this.debounceAccept=setTimeout(()=>this.accept(),this.view.state.facet(Rf).updateSyncTime))}startQuery(t){let{state:e}=this.view,i=yf(e),n=new gf(e,i,t.explicit,this.view),s=new Zf(t,n);this.running.push(s),Promise.resolve(t.source(n)).then(t=>{s.context.aborted||(s.done=t||null,this.scheduleAccept())},t=>{this.view.dispatch({effects:Of.of(null)}),Wi(this.view.state,t)})}scheduleAccept(){this.running.every(t=>void 0!==t.done)?this.accept():this.debounceAccept<0&&(this.debounceAccept=setTimeout(()=>this.accept(),this.view.state.facet(Rf).updateSyncTime))}accept(){var t;this.debounceAccept>-1&&clearTimeout(this.debounceAccept),this.debounceAccept=-1;let e=[],i=this.view.state.facet(Rf),n=this.view.state.field(Kf);for(let s=0;st.source==r.active.source);if(o&&o.isPending)if(null==r.done){let t=new Uf(r.active.source,0);for(let e of r.updates)t=t.update(e,i);t.isPending||e.push(t)}else this.startQuery(o)}(e.length||n.open&&n.open.disabled)&&this.view.dispatch({effects:jf.of(e)})}},{eventHandlers:{blur(t){let e=this.view.state.field(Kf,!1);if(e&&e.tooltip&&this.view.state.facet(Rf).closeOnBlur){let i=e.open&&Do(this.view,e.open.tooltip);i&&i.dom.contains(t.relatedTarget)||setTimeout(()=>this.view.dispatch({effects:Of.of(null)}),10)}},compositionstart(){this.composing=1},compositionend(){3==this.composing&&setTimeout(()=>this.view.dispatch({effects:Mf.of(!1)}),20),this.composing=0}}}),ed="object"==typeof navigator&&/Win/.test(navigator.platform),id=J.highest(dr.domEventHandlers({keydown(t,e){let i=e.state.field(Kf,!1);if(!i||!i.open||i.open.disabled||i.open.selected<0||t.key.length>1||t.ctrlKey&&(!ed||!t.altKey)||t.metaKey)return!1;let n=i.open.options[i.open.selected],s=i.active.find(t=>t.source==n.source),r=n.completion.commitCharacters||s.result.commitCharacters;return r&&r.indexOf(t.key)>-1&&Xf(e,n),!1}})),nd=dr.baseTheme({".cm-tooltip.cm-tooltip-autocomplete":{"& > ul":{fontFamily:"monospace",whiteSpace:"nowrap",overflow:"hidden auto",maxWidth_fallback:"700px",maxWidth:"min(700px, 95vw)",minWidth:"250px",maxHeight:"10em",height:"100%",listStyle:"none",margin:0,padding:0,"& > li, & > completion-section":{padding:"1px 3px",lineHeight:1.2},"& > li":{overflowX:"hidden",textOverflow:"ellipsis",cursor:"pointer"},"& > completion-section":{display:"list-item",borderBottom:"1px solid silver",paddingLeft:"0.5em",opacity:.7}}},"&light .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#17c",color:"white"},"&light .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#777"},"&dark .cm-tooltip-autocomplete ul li[aria-selected]":{background:"#347",color:"white"},"&dark .cm-tooltip-autocomplete-disabled ul li[aria-selected]":{background:"#444"},".cm-completionListIncompleteTop:before, .cm-completionListIncompleteBottom:after":{content:'"···"',opacity:.5,display:"block",textAlign:"center",cursor:"pointer"},".cm-tooltip.cm-completionInfo":{position:"absolute",padding:"3px 9px",width:"max-content",maxWidth:"400px",boxSizing:"border-box",whiteSpace:"pre-line"},".cm-completionInfo.cm-completionInfo-left":{right:"100%"},".cm-completionInfo.cm-completionInfo-right":{left:"100%"},".cm-completionInfo.cm-completionInfo-left-narrow":{right:"30px"},".cm-completionInfo.cm-completionInfo-right-narrow":{left:"30px"},"&light .cm-snippetField":{backgroundColor:"#00000022"},"&dark .cm-snippetField":{backgroundColor:"#ffffff22"},".cm-snippetFieldPosition":{verticalAlign:"text-top",width:0,height:"1.15em",display:"inline-block",margin:"0 -0.7px -.7em",borderLeft:"1.4px dotted #888"},".cm-completionMatchedText":{textDecoration:"underline"},".cm-completionDetail":{marginLeft:"0.5em",fontStyle:"italic"},".cm-completionIcon":{fontSize:"90%",width:".8em",display:"inline-block",textAlign:"center",paddingRight:".6em",opacity:"0.6",boxSizing:"content-box"},".cm-completionIcon-function, .cm-completionIcon-method":{"&:after":{content:"'ƒ'"}},".cm-completionIcon-class":{"&:after":{content:"'○'"}},".cm-completionIcon-interface":{"&:after":{content:"'◌'"}},".cm-completionIcon-variable":{"&:after":{content:"'𝑥'"}},".cm-completionIcon-constant":{"&:after":{content:"'𝐶'"}},".cm-completionIcon-type":{"&:after":{content:"'𝑡'"}},".cm-completionIcon-enum":{"&:after":{content:"'∪'"}},".cm-completionIcon-property":{"&:after":{content:"'□'"}},".cm-completionIcon-keyword":{"&:after":{content:"'🔑︎'"}},".cm-completionIcon-namespace":{"&:after":{content:"'▢'"}},".cm-completionIcon-text":{"&:after":{content:"'abc'",fontSize:"50%",verticalAlign:"middle"}}}),sd={brackets:["(","[","{","'",'"'],before:")]}:;>",stringPrefixes:[]},rd=mt.define({map(t,e){let i=e.mapPos(t,-1,M.TrackAfter);return null==i?void 0:i}}),od=new class extends Dt{};od.startSide=1,od.endSide=-1;const ld=Q.define({create:()=>Lt.empty,update(t,e){if(t=t.map(e.changes),e.selection){let i=e.state.doc.lineAt(e.selection.main.head);t=t.update({filter:t=>t>=i.from&&t<=i.to})}for(let i of e.effects)i.is(rd)&&(t=t.update({add:[od.range(i.value,i.value+1)]}));return t}});const ad="()[]{}<>«»»«[]{}";function hd(t){for(let e=0;e<16;e+=2)if(ad.charCodeAt(e)==t)return ad.charAt(e+1);return S(t<128?t:t+1)}function cd(t,e){return t.languageDataAt("closeBrackets",e)[0]||sd}const ud="object"==typeof navigator&&/Android\b/.test(navigator.userAgent),fd=dr.inputHandler.of((t,e,i,n)=>{if((ud?t.composing:t.compositionStarted)||t.state.readOnly)return!1;let s=t.state.selection.main;if(n.length>2||2==n.length&&1==C(k(n,0))||e!=s.from||i!=s.to)return!1;let r=function(t,e){let i=cd(t,t.selection.main.head),n=i.brackets||sd.brackets;for(let s of n){let r=hd(k(s,0));if(e==s)return r==s?wd(t,s,n.indexOf(s+s+s)>-1,i):gd(t,s,r,i.before||sd.before);if(e==r&&pd(t,t.selection.main.from))return vd(t,s,r)}return null}(t.state,n);return!!r&&(t.dispatch(r),!0)}),dd=[{key:"Backspace",run:({state:t,dispatch:e})=>{if(t.readOnly)return!1;let i=cd(t,t.selection.main.head).brackets||sd.brackets,n=null,s=t.changeByRange(e=>{if(e.empty){let n=function(t,e){let i=t.sliceString(e-2,e);return C(k(i,0))==i.length?i:i.slice(1)}(t.doc,e.head);for(let s of i)if(s==n&&md(t.doc,e.head)==hd(k(s,0)))return{changes:{from:e.head-s.length,to:e.head+s.length},range:N.cursor(e.head-s.length)}}return{range:n=e}});return n||e(t.update(s,{scrollIntoView:!0,userEvent:"delete.backward"})),!n}}];function pd(t,e){let i=!1;return t.field(ld).between(0,t.doc.length,t=>{t==e&&(i=!0)}),i}function md(t,e){let i=t.sliceString(e,e+2);return i.slice(0,C(k(i,0)))}function gd(t,e,i,n){let s=null,r=t.changeByRange(r=>{if(!r.empty)return{changes:[{insert:e,from:r.from},{insert:i,from:r.to}],effects:rd.of(r.to+e.length),range:N.range(r.anchor+e.length,r.head+e.length)};let o=md(t.doc,r.head);return!o||/\s/.test(o)||n.indexOf(o)>-1?{changes:{insert:e+i,from:r.head},effects:rd.of(r.head+e.length),range:N.cursor(r.head+e.length)}:{range:s=r}});return s?null:t.update(r,{scrollIntoView:!0,userEvent:"input.type"})}function vd(t,e,i){let n=null,s=t.changeByRange(e=>e.empty&&md(t.doc,e.head)==i?{changes:{from:e.head,to:e.head+i.length,insert:i},range:N.cursor(e.head+i.length)}:n={range:e});return n?null:t.update(s,{scrollIntoView:!0,userEvent:"input.type"})}function wd(t,e,i,n){let s=n.stringPrefixes||sd.stringPrefixes,r=null,o=t.changeByRange(n=>{if(!n.empty)return{changes:[{insert:e,from:n.from},{insert:e,from:n.to}],effects:rd.of(n.to+e.length),range:N.range(n.anchor+e.length,n.head+e.length)};let o,l=n.head,a=md(t.doc,l);if(a==e){if(bd(t,l))return{changes:{insert:e+e,from:l},effects:rd.of(l+e.length),range:N.cursor(l+e.length)};if(pd(t,l)){let n=i&&t.sliceDoc(l,l+3*e.length)==e+e+e?e+e+e:e;return{changes:{from:l,to:l+n.length,insert:n},range:N.cursor(l+n.length)}}}else{if(i&&t.sliceDoc(l-2*e.length,l)==e+e&&(o=yd(t,l-2*e.length,s))>-1&&bd(t,o))return{changes:{insert:e+e+e+e,from:l},effects:rd.of(l+e.length),range:N.cursor(l+e.length)};if(t.charCategorizer(l)(a)!=St.Word&&yd(t,l,s)>-1&&!function(t,e,i,n){let s=xa(t).resolveInner(e,-1),r=n.reduce((t,e)=>Math.max(t,e.length),0);for(let o=0;o<5;o++){let o=t.sliceDoc(s.from,Math.min(s.to,s.from+i.length+r)),l=o.indexOf(i);if(!l||l>-1&&n.indexOf(o.slice(0,l))>-1){let e=s.firstChild;for(;e&&e.from==s.from&&e.to-e.from>i.length+l;){if(t.sliceDoc(e.to-i.length,e.to)==i)return!1;e=e.firstChild}return!0}let a=s.to==e&&s.parent;if(!a)break;s=a}return!1}(t,l,e,s))return{changes:{insert:e+e,from:l},effects:rd.of(l+e.length),range:N.cursor(l+e.length)}}return{range:r=n}});return r?null:t.update(o,{scrollIntoView:!0,userEvent:"input.type"})}function bd(t,e){let i=xa(t).resolveInner(e+1);return i.parent&&i.from==e}function yd(t,e,i){let n=t.charCategorizer(e);if(n(t.sliceDoc(e-1,e))!=St.Word)return e;for(let s of i){let i=e-s.length;if(t.sliceDoc(i,e)==s&&n(t.sliceDoc(i-1,i))!=St.Word)return i}return-1}function xd(t={}){return[id,Kf,Rf.of(t),td,Sd,nd]}const kd=[{key:"Ctrl-Space",run:Jf},{mac:"Alt-`",run:Jf},{mac:"Alt-i",run:Jf},{key:"Escape",run:t=>{let e=t.state.field(Kf,!1);return!(!e||!e.active.some(t=>0!=t.state))&&(t.dispatch({effects:Of.of(null)}),!0)}},{key:"ArrowDown",run:Yf(!0)},{key:"ArrowUp",run:Yf(!1)},{key:"PageDown",run:Yf(!0,"page")},{key:"PageUp",run:Yf(!1,"page")},{key:"Enter",run:t=>{let e=t.state.field(Kf,!1);return!(t.state.readOnly||!e||!e.open||e.open.selected<0||e.open.disabled||Date.now()-e.open.timestampt.facet(Rf).defaultKeymap?[kd]:[]));class Cd{constructor(t,e,i){this.from=t,this.to=e,this.diagnostic=i}}class Ad{constructor(t,e,i){this.diagnostics=t,this.panel=e,this.selected=i}static init(t,e,i){let n=i.facet(Nd).markerFilter;n&&(t=n(t,i));let s=t.slice().sort((t,e)=>t.from-e.from||t.to-e.to),r=new It,o=[],l=0,a=i.doc.iter(),h=0,c=i.doc.length;for(let t=0;;){let e,i,n=t==s.length?null:s[t];if(!n&&!o.length)break;if(o.length)e=l,i=o.reduce((t,e)=>Math.min(t,e.to),n&&n.from>e?n.from:1e8);else{if(e=n.from,e>c)break;i=n.to,o.push(n),t++}for(;tn.from||n.to==e)){i=Math.min(n.from,i);break}o.push(n),t++,i=Math.min(n.to,i)}i=Math.min(i,c);let u=!1;if(o.some(t=>t.from==e&&(t.to==i||i==c))&&(u=e==i,!u&&i-e<10)){let t=e-(h+a.value.length);t>0&&(a.next(t),h=e);for(let t=e;;){if(t>=i){u=!0;break}if(!a.lineBreak&&h+a.value.length>t)break;t=h+a.value.length,h+=a.value.length,a.next()}}let f=Qd(o);if(u)r.add(e,e,Oe.widget({widget:new zd(f),diagnostics:o.slice()}));else{let t=o.reduce((t,e)=>e.markClass?t+" "+e.markClass:t,"");r.add(e,i,Oe.mark({class:"cm-lintRange cm-lintRange-"+f+t,diagnostics:o.slice(),inclusiveEnd:o.some(t=>t.to>i)}))}if(l=i,l==c)break;for(let t=0;t{if(!(e&&s.diagnostics.indexOf(e)<0))if(n){if(s.diagnostics.indexOf(n.diagnostic)<0)return!1;n=new Cd(n.from,i,n.diagnostic)}else n=new Cd(t,i,e||s.diagnostics[0])}),n}const Od=mt.define(),Td=mt.define(),Dd=mt.define(),Rd=Q.define({create:()=>new Ad(Oe.none,null,null),update(t,e){if(e.docChanged&&t.diagnostics.size){let i=t.diagnostics.map(e.changes),n=null,s=t.panel;if(t.selected){let s=e.changes.mapPos(t.selected.from,1);n=Md(i,t.selected.diagnostic,s)||Md(i,null,s)}!i.size&&s&&e.state.facet(Nd).autoPanel&&(s=null),t=new Ad(i,s,n)}for(let i of e.effects)if(i.is(Od)){let n=e.state.facet(Nd).autoPanel?i.value.length?qd.open:null:t.panel;t=Ad.init(i.value,n,e.state)}else i.is(Td)?t=new Ad(t.diagnostics,i.value?qd.open:null,t.selected):i.is(Dd)&&(t=new Ad(t.diagnostics,t.panel,i.value));return t},provide:t=>[No.from(t,t=>t.panel),dr.decorations.from(t,t=>t.diagnostics)]}),Pd=Oe.mark({class:"cm-lintRange cm-lintRange-active"});function Bd(t,e,i){let n,{diagnostics:s}=t.state.field(Rd),r=-1,o=-1;s.between(e-(i<0?1:0),e+(i>0?1:0),(t,s,{spec:l})=>{if(e>=t&&e<=s&&(t==s||(e>t||i>0)&&(e({dom:Ed(t,n)})}:null}function Ed(t,e){return oe("ul",{class:"cm-tooltip-lint"},e.map(e=>Vd(t,e,!1)))}const Ld=t=>{let e=t.state.field(Rd,!1);return!(!e||!e.panel)&&(t.dispatch({effects:Td.of(!1)}),!0)},Id=[{key:"Mod-Shift-m",run:t=>{let e=t.state.field(Rd,!1);var i,n;e&&e.panel||t.dispatch({effects:(i=t.state,n=[Td.of(!0)],i.field(Rd,!1)?n:n.concat(mt.appendConfig.of(Kd)))});let s=Bo(t,qd.open);return s&&s.dom.querySelector(".cm-panel-lint ul").focus(),!0},preventDefault:!0},{key:"F8",run:t=>{let e=t.state.field(Rd,!1);if(!e)return!1;let i=t.state.selection.main,n=Md(e.diagnostics,null,i.to+1);return!(!n&&(n=Md(e.diagnostics,null,0),!n||n.from==i.from&&n.to==i.to))&&(t.dispatch({selection:{anchor:n.from,head:n.to},scrollIntoView:!0}),function(t,e,i,n={}){var s;let r=t.state.facet(Ao).map(e=>t.plugin(e)).filter(t=>!!t);if(n.tooltip&&n.tooltip.active){let t=r.find(t=>t.field==n.tooltip.active);t&&(r=[t])}for(let o of r)o.activateHover(t,e,i,null!==(s=n.until)&&void 0!==s?s:()=>!1)}(t,n.from,1,{tooltip:jd,until:t=>t.docChanged||t.newSelection.main.headn.to}),!0)}}],Nd=V.define({combine:t=>({sources:t.map(t=>t.source).filter(t=>null!=t),...Tt(t.map(t=>t.config),{delay:750,markerFilter:null,tooltipFilter:null,needsRefresh:null,hideOn:()=>null},{delay:Math.max,markerFilter:Wd,tooltipFilter:Wd,needsRefresh:(t,e)=>t?e?i=>t(i)||e(i):t:e,hideOn:(t,e)=>t?e?(i,n,s)=>t(i,n,s)||e(i,n,s):t:e,autoPanel:(t,e)=>t||e})})});function Wd(t,e){return t?e?(i,n)=>e(t(i,n),n):t:e}function Hd(t){let e=[];if(t)t:for(let{name:i}of t){for(let t=0;tt.toLowerCase()==n.toLowerCase())){e.push(n);continue t}}e.push("")}return e}function Vd(t,e,i){var n;let s=i?Hd(e.actions):[];return oe("li",{class:"cm-diagnostic cm-diagnostic-"+e.severity},oe("span",{class:"cm-diagnosticText"},e.renderMessage?e.renderMessage(t):e.message),null===(n=e.actions)||void 0===n?void 0:n.map((i,n)=>{let r=!1,o=n=>{if(n.preventDefault(),r)return;r=!0;let s=Md(t.state.field(Rd).diagnostics,e);s&&i.apply(t,s.from,s.to)},{name:l}=i,a=s[n]?l.indexOf(s[n]):-1,h=a<0?l:[l.slice(0,a),oe("u",l.slice(a,a+1)),l.slice(a+1)];return oe("button",{type:"button",class:"cm-diagnosticAction"+(i.markClass?" "+i.markClass:""),onclick:o,onmousedown:o,"aria-label":` Action: ${l}${a<0?"":` (access key "${s[n]})"`}.`},h)}),e.source&&oe("div",{class:"cm-diagnosticSource"},e.source))}class zd extends Ae{constructor(t){super(),this.sev=t}eq(t){return t.sev==this.sev}toDOM(){return oe("span",{class:"cm-lintPoint cm-lintPoint-"+this.sev})}}class Fd{constructor(t,e){this.diagnostic=e,this.id="item_"+Math.floor(4294967295*Math.random()).toString(16),this.dom=Vd(t,e,!0),this.dom.id=this.id,this.dom.setAttribute("role","option")}}class qd{constructor(t){this.view=t,this.items=[];this.list=oe("ul",{tabIndex:0,role:"listbox","aria-label":this.view.state.phrase("Diagnostics"),onkeydown:e=>{if(!(e.ctrlKey||e.altKey||e.metaKey)){if(27==e.keyCode)Ld(this.view),this.view.focus();else if(38==e.keyCode||33==e.keyCode)this.moveSelection((this.selectedIndex-1+this.items.length)%this.items.length);else if(40==e.keyCode||34==e.keyCode)this.moveSelection((this.selectedIndex+1)%this.items.length);else if(36==e.keyCode)this.moveSelection(0);else if(35==e.keyCode)this.moveSelection(this.items.length-1);else if(13==e.keyCode)this.view.focus();else{if(!(e.keyCode>=65&&e.keyCode<=90&&this.selectedIndex>=0))return;{let{diagnostic:i}=this.items[this.selectedIndex],n=Hd(i.actions);for(let s=0;s{for(let e=0;eLd(this.view)},"×")),this.update()}get selectedIndex(){let t=this.view.state.field(Rd).selected;if(!t)return-1;for(let e=0;e{for(let t of l.diagnostics){if(r.has(t))continue;r.add(t);let o,l=-1;for(let e=i;ei&&(this.items.splice(i,l-i),n=!0)),e&&o.diagnostic==e.diagnostic?o.dom.hasAttribute("aria-selected")||(o.dom.setAttribute("aria-selected","true"),s=o):o.dom.hasAttribute("aria-selected")&&o.dom.removeAttribute("aria-selected"),i++}});i({sel:s.dom.getBoundingClientRect(),panel:this.list.getBoundingClientRect()}),write:({sel:t,panel:e})=>{let i=e.height/this.list.offsetHeight;t.tope.bottom&&(this.list.scrollTop+=(t.bottom-e.bottom)/i)}})):this.selectedIndex<0&&this.list.removeAttribute("aria-activedescendant"),n&&this.sync()}sync(){let t=this.list.firstChild;function e(){let e=t;t=e.nextSibling,e.remove()}for(let i of this.items)if(i.dom.parentNode==this.list){for(;t!=i.dom;)e();t=i.dom.nextSibling}else this.list.insertBefore(i.dom,t);for(;t;)e()}moveSelection(t){if(this.selectedIndex<0)return;let e=Md(this.view.state.field(Rd).diagnostics,this.items[t].diagnostic);e&&this.view.dispatch({selection:{anchor:e.from,head:e.to},scrollIntoView:!0,effects:Dd.of(e)})}static open(t){return new qd(t)}}function _d(t){return function(t,e='viewBox="0 0 40 40"'){return`url('data:image/svg+xml,${encodeURIComponent(t)}')`}(``,'width="6" height="3"')}const $d=dr.baseTheme({".cm-diagnostic":{padding:"3px 6px 3px 8px",marginLeft:"-1px",display:"block",whiteSpace:"pre-wrap"},".cm-diagnostic-error":{borderLeft:"5px solid #d11"},".cm-diagnostic-warning":{borderLeft:"5px solid orange"},".cm-diagnostic-info":{borderLeft:"5px solid #999"},".cm-diagnostic-hint":{borderLeft:"5px solid #66d"},".cm-diagnosticAction":{font:"inherit",border:"none",padding:"2px 4px",backgroundColor:"#444",color:"white",borderRadius:"3px",marginLeft:"8px",cursor:"pointer"},".cm-diagnosticSource":{fontSize:"70%",opacity:.7},".cm-lintRange":{backgroundPosition:"left bottom",backgroundRepeat:"repeat-x",paddingBottom:"0.7px"},".cm-lintRange-error":{backgroundImage:_d("#f11")},".cm-lintRange-warning":{backgroundImage:_d("orange")},".cm-lintRange-info":{backgroundImage:_d("#999")},".cm-lintRange-hint":{backgroundImage:_d("#66d")},".cm-lintRange-active":{backgroundColor:"#ffdd9980"},".cm-tooltip-lint":{padding:0,margin:0},".cm-lintPoint":{position:"relative","&:after":{content:'""',position:"absolute",bottom:0,left:"-2px",borderLeft:"3px solid transparent",borderRight:"3px solid transparent",borderBottom:"4px solid #d11"}},".cm-lintPoint-warning":{"&:after":{borderBottomColor:"orange"}},".cm-lintPoint-info":{"&:after":{borderBottomColor:"#999"}},".cm-lintPoint-hint":{"&:after":{borderBottomColor:"#66d"}},".cm-panel.cm-panel-lint":{position:"relative","& ul":{maxHeight:"100px",overflowY:"auto","& [aria-selected]":{backgroundColor:"#ddd","& u":{textDecoration:"underline"}},"&:focus [aria-selected]":{background_fallback:"#bdf",backgroundColor:"Highlight",color_fallback:"white",color:"HighlightText"},"& u":{textDecoration:"none"},padding:0,margin:0},"& [name=close]":{position:"absolute",top:"0",right:"2px",background:"inherit",border:"none",font:"inherit",padding:0,margin:0}},"&dark .cm-lintRange-active":{backgroundColor:"#86714a80"},"&dark .cm-panel.cm-panel-lint ul":{"& [aria-selected]":{backgroundColor:"#2e343e"}}});function Ud(t){return"error"==t?4:"warning"==t?3:"info"==t?2:1}function Qd(t){let e="hint",i=1;for(let n of t){let t=Ud(n.severity);t>i&&(i=t,e=n.severity)}return e}const jd=To(Bd,{hideOn:function(t,e){let i=e.pos,n=e.end||i,s=t.state.facet(Nd).hideOn(t,i,n);if(null!=s)return s;let r=t.startState.doc.lineAt(e.pos);return!(!t.effects.some(t=>t.is(Od))&&!t.changes.touchesRange(r.from,Math.max(r.to,n)))}}),Kd=[Rd,dr.decorations.compute([Rd],t=>{let{selected:e,panel:i}=t.field(Rd);return e&&i&&e.from!=e.to?Oe.set([Pd.range(e.from,e.to)]):Oe.none}),jd,$d];class Xd{constructor(t,e,i,n,s,r,o,l,a,h=0,c){this.p=t,this.stack=e,this.state=i,this.reducePos=n,this.pos=s,this.score=r,this.buffer=o,this.bufferBase=l,this.curContext=a,this.lookAhead=h,this.parent=c}toString(){return`[${this.stack.filter((t,e)=>e%3==0).concat(this.state)}]@${this.pos}${this.score?"!"+this.score:""}`}static start(t,e,i=0){let n=t.parser.context;return new Xd(t,[],e,i,i,0,[],0,n?new Gd(n,n.start):null,0,null)}get context(){return this.curContext?this.curContext.context:null}pushState(t,e){this.stack.push(this.state,e,this.bufferBase+this.buffer.length),this.state=t}reduce(t){var e;let i=t>>19,n=65535&t,{parser:s}=this.p,r=this.reducePos=2e3&&!(null===(e=this.p.parser.nodeSet.types[n])||void 0===e?void 0:e.isAnonymous)&&(a==this.p.lastBigReductionStart?(this.p.bigReductionCount++,this.p.lastBigReductionSize=h):this.p.lastBigReductionSizel;)this.stack.pop();this.reduceContext(n,a)}storeNode(t,e,i,n=4,s=!1){if(0==t&&(!this.stack.length||this.stack[this.stack.length-1]0&&0==this.buffer[t-4]&&this.buffer[t-1]>-1){if(e==i)return;if(this.buffer[t-2]>=e)return void(this.buffer[t-2]=i)}}if(s&&this.pos!=i){let s=this.buffer.length;if(s>0&&(0!=this.buffer[s-4]||this.buffer[s-1]<0)){let t=!1;for(let e=s;e>0&&this.buffer[e-2]>i;e-=4)if(this.buffer[e-1]>=0){t=!0;break}if(t)for(;s>0&&this.buffer[s-2]>i;)this.buffer[s]=this.buffer[s-4],this.buffer[s+1]=this.buffer[s-3],this.buffer[s+2]=this.buffer[s-2],this.buffer[s+3]=this.buffer[s-1],s-=4,n>4&&(n-=4)}this.buffer[s]=t,this.buffer[s+1]=e,this.buffer[s+2]=i,this.buffer[s+3]=n}else this.buffer.push(t,e,i,n)}shift(t,e,i,n){if(131072&t)this.pushState(65535&t,this.pos);else if(262144&t)this.pos=n,this.shiftContext(e,i),e<=this.p.parser.maxNode&&this.buffer.push(e,i,n,4);else{let s=t,{parser:r}=this.p;this.pos=n;let o=r.stateFlag(s,1);!o&&(n>i||e<=r.maxNode)&&(this.reducePos=n),this.pushState(s,o?i:Math.min(i,this.reducePos)),this.shiftContext(e,i),e<=r.maxNode&&this.buffer.push(e,i,n,4)}}apply(t,e,i,n){65536&t?this.reduce(t):this.shift(t,e,i,n)}useNode(t,e){let i=this.p.reused.length-1;(i<0||this.p.reused[i]!=t)&&(this.p.reused.push(t),i++);let n=this.pos;this.reducePos=this.pos=n+t.length,this.pushState(e,n),this.buffer.push(i,n,this.reducePos,-1),this.curContext&&this.updateContext(this.curContext.tracker.reuse(this.curContext.context,t,this,this.p.stream.reset(this.pos-t.length)))}split(){let t=this,e=t.buffer.length;for(e&&0==t.buffer[e-4]&&(e-=4);e>0&&t.buffer[e-2]>t.reducePos;)e-=4;let i=t.buffer.slice(e),n=t.bufferBase+e;for(;t&&n==t.bufferBase;)t=t.parent;return new Xd(this.p,this.stack.slice(),this.state,this.reducePos,this.pos,this.score,i,n,this.curContext,this.lookAhead,t)}recoverByDelete(t,e){let i=t<=this.p.parser.maxNode;i&&this.storeNode(t,this.pos,e,4),this.storeNode(0,this.pos,e,i?8:4),this.pos=this.reducePos=e,this.score-=190}canShift(t){for(let e=new Yd(this);;){let i=this.p.parser.stateSlot(e.state,4)||this.p.parser.hasAction(e.state,t);if(0==i)return!1;if(!(65536&i))return!0;e.reduce(i)}}recoverByInsert(t){if(this.stack.length>=300)return[];let e=this.p.parser.nextStates(this.state);if(e.length>8||this.stack.length>=120){let i=[];for(let n,s=0;s1&e&&t==n)||i.push(e[t],n)}e=i}let i=[];for(let t=0;t>19,n=65535&e,s=this.stack.length-3*i;if(s<0||t.getGoto(this.stack[s],n,!1)<0){let t=this.findForcedReduction();if(null==t)return!1;e=t}this.storeNode(0,this.pos,this.pos,4,!0),this.score-=100}return this.reducePos=this.pos,this.reduce(e),!0}findForcedReduction(){let{parser:t}=this.p,e=[],i=(n,s)=>{if(!e.includes(n))return e.push(n),t.allActions(n,e=>{if(393216&e);else if(65536&e){let i=(e>>19)-s;if(i>1){let n=65535&e,s=this.stack.length-3*i;if(s>=0&&t.getGoto(this.stack[s],n,!1)>=0)return i<<19|65536|n}}else{let t=i(e,s+1);if(null!=t)return t}})};return i(this.state,0)}forceAll(){for(;!this.p.parser.stateFlag(this.state,2);)if(!this.forceReduce()){this.storeNode(0,this.pos,this.pos,4,!0);break}return this}get deadEnd(){if(3!=this.stack.length)return!1;let{parser:t}=this.p;return 65535==t.data[t.stateSlot(this.state,1)]&&!t.stateSlot(this.state,4)}restart(){this.storeNode(0,this.pos,this.pos,4,!0),this.state=this.stack[0],this.stack.length=0}sameState(t){if(this.state!=t.state||this.stack.length!=t.stack.length)return!1;for(let e=0;e0&&this.emitLookAhead()}}class Gd{constructor(t,e){this.tracker=t,this.context=e,this.hash=t.strict?t.hash(e):0}}class Yd{constructor(t){this.start=t,this.state=t.state,this.stack=t.stack,this.base=this.stack.length}reduce(t){let e=65535&t,i=t>>19;0==i?(this.stack==this.start.stack&&(this.stack=this.stack.slice()),this.stack.push(this.state,0,0),this.base+=3):this.base-=3*(i-1);let n=this.start.p.parser.getGoto(this.stack[this.base-3],e,!0);this.state=n}}class Jd{constructor(t,e,i){this.stack=t,this.pos=e,this.index=i,this.buffer=t.buffer,0==this.index&&this.maybeNext()}static create(t,e=t.bufferBase+t.buffer.length){return new Jd(t,e,e-t.bufferBase)}maybeNext(){let t=this.stack.parent;null!=t&&(this.index=this.stack.bufferBase-t.bufferBase,this.stack=t,this.buffer=t.buffer)}get id(){return this.buffer[this.index-4]}get start(){return this.buffer[this.index-3]}get end(){return this.buffer[this.index-2]}get size(){return this.buffer[this.index-1]}next(){this.index-=4,this.pos-=4,0==this.index&&this.maybeNext()}fork(){return new Jd(this.stack,this.pos,this.index)}}function Zd(t,e=Uint16Array){if("string"!=typeof t)return t;let i=null;for(let n=0,s=0;n=92&&e--,e>=34&&e--;let s=e-32;if(s>=46&&(s-=46,i=!0),r+=s,i)break;r*=46}i?i[s++]=r:i=new e(r)}return i}class tp{constructor(){this.start=-1,this.value=-1,this.end=-1,this.extended=-1,this.lookAhead=0,this.mask=0,this.context=0}}const ep=new tp;class ip{constructor(t,e){this.input=t,this.ranges=e,this.chunk="",this.chunkOff=0,this.chunk2="",this.chunk2Pos=0,this.next=-1,this.token=ep,this.rangeIndex=0,this.pos=this.chunkPos=e[0].from,this.range=e[0],this.end=e[e.length-1].to,this.readNext()}resolveOffset(t,e){let i=this.range,n=this.rangeIndex,s=this.pos+t;for(;si.to:s>=i.to;){if(n==this.ranges.length-1)return null;let t=this.ranges[++n];s+=t.from-i.to,i=t}return s}clipPos(t){if(t>=this.range.from&&tt)return Math.max(t,e.from);return this.end}peek(t){let e,i,n=this.chunkOff+t;if(n>=0&&n=this.chunk2Pos&&en.to&&(this.chunk2=this.chunk2.slice(0,n.to-e)),i=this.chunk2.charCodeAt(0)}}return e>=this.token.lookAhead&&(this.token.lookAhead=e+1),i}acceptToken(t,e=0){let i=e?this.resolveOffset(e,-1):this.pos;if(null==i||i=this.chunk2Pos&&this.posthis.range.to?t.slice(0,this.range.to-this.pos):t,this.chunkPos=this.pos,this.chunkOff=0}}readNext(){return this.chunkOff>=this.chunk.length&&(this.getChunk(),this.chunkOff==this.chunk.length)?this.next=-1:this.next=this.chunk.charCodeAt(this.chunkOff)}advance(t=1){for(this.chunkOff+=t;this.pos+t>=this.range.to;){if(this.rangeIndex==this.ranges.length-1)return this.setDone();t-=this.range.to-this.pos,this.range=this.ranges[++this.rangeIndex],this.pos=this.range.from}return this.pos+=t,this.pos>=this.token.lookAhead&&(this.token.lookAhead=this.pos+1),this.readNext()}setDone(){return this.pos=this.chunkPos=this.end,this.range=this.ranges[this.rangeIndex=this.ranges.length-1],this.chunk="",this.next=-1}reset(t,e){if(e?(this.token=e,e.start=t,e.lookAhead=t+1,e.value=e.extended=-1):this.token=ep,this.pos!=t){if(this.pos=t,t==this.end)return this.setDone(),this;for(;t=this.range.to;)this.range=this.ranges[++this.rangeIndex];t>=this.chunkPos&&t=this.chunkPos&&e<=this.chunkPos+this.chunk.length)return this.chunk.slice(t-this.chunkPos,e-this.chunkPos);if(t>=this.chunk2Pos&&e<=this.chunk2Pos+this.chunk2.length)return this.chunk2.slice(t-this.chunk2Pos,e-this.chunk2Pos);if(t>=this.range.from&&e<=this.range.to)return this.input.read(t,e);let i="";for(let n of this.ranges){if(n.from>=e)break;n.to>t&&(i+=this.input.read(Math.max(n.from,t),Math.min(n.to,e)))}return i}}class np{constructor(t,e){this.data=t,this.id=e}token(t,e){let{parser:i}=e.p;!function(t,e,i,n,s,r){let o=0,l=1<0){let i=t[n];if(a.allows(i)&&(-1==e.token.value||e.token.value==i||op(i,e.token.value,s,r))){e.acceptToken(i);break}}let n=e.next,h=0,c=t[o+2];if(!(e.next<0&&c>h&&65535==t[i+3*c-3])){for(;h>1,r=i+s+(s<<1),l=t[r],a=t[r+1]||65536;if(n=a)){o=t[r+2],e.advance();continue t}h=s+1}}break}o=t[i+3*c-1]}}(this.data,t,e,this.id,i.data,i.tokenPrecTable)}}np.prototype.contextual=np.prototype.fallback=np.prototype.extend=!1,np.prototype.fallback=np.prototype.extend=!1;class sp{constructor(t,e={}){this.token=t,this.contextual=!!e.contextual,this.fallback=!!e.fallback,this.extend=!!e.extend}}function rp(t,e,i){for(let n,s=e;65535!=(n=t[s]);s++)if(n==i)return s-e;return-1}function op(t,e,i,n){let s=rp(i,n,e);return s<0||rp(i,n,t)e)&&!n.type.isError)return i<0?Math.max(0,Math.min(n.to-1,e-25)):Math.min(t.length,Math.max(n.from+1,e+25));if(i<0?n.prevSibling():n.nextSibling())break;if(!n.parent())return i<0?0:t.length}}class cp{constructor(t,e){this.fragments=t,this.nodeSet=e,this.i=0,this.fragment=null,this.safeFrom=-1,this.safeTo=-1,this.trees=[],this.start=[],this.index=[],this.nextFragment()}nextFragment(){let t=this.fragment=this.i==this.fragments.length?null:this.fragments[this.i++];if(t){for(this.safeFrom=t.openStart?hp(t.tree,t.from+t.offset,1)-t.offset:t.from,this.safeTo=t.openEnd?hp(t.tree,t.to+t.offset,-1)-t.offset:t.to;this.trees.length;)this.trees.pop(),this.start.pop(),this.index.pop();this.trees.push(t.tree),this.start.push(-t.offset),this.index.push(0),this.nextStart=this.safeFrom}else this.nextStart=1e9}nodeAt(t){if(tt)return this.nextStart=r,null;if(s instanceof kl){if(r==t){if(r=Math.max(this.safeFrom,t)&&(this.trees.push(s),this.start.push(r),this.index.push(0))}else this.index[e]++,this.nextStart=r+s.length}}}class up{constructor(t,e){this.stream=e,this.tokens=[],this.mainToken=null,this.actions=[],this.tokens=t.tokenizers.map(t=>new tp)}getActions(t){let e=0,i=null,{parser:n}=t.p,{tokenizers:s}=n,r=n.stateSlot(t.state,3),o=t.curContext?t.curContext.hash:0,l=0;for(let n=0;nh.end+25&&(l=Math.max(h.lookAhead,l)),0!=h.value)){let n=e;if(h.extended>-1&&(e=this.addActions(t,h.extended,h.end,e)),e=this.addActions(t,h.value,h.end,e),!a.extend&&(i=h,e>n))break}}for(;this.actions.length>e;)this.actions.pop();return l&&t.setLookAhead(l),i||t.pos!=this.stream.end||(i=new tp,i.value=t.p.parser.eofTerm,i.start=i.end=t.pos,e=this.addActions(t,i.value,i.end,e)),this.mainToken=i,this.actions}getMainToken(t){if(this.mainToken)return this.mainToken;let e=new tp,{pos:i,p:n}=t;return e.start=i,e.end=Math.min(i+1,n.stream.end),e.value=i==n.stream.end?n.parser.eofTerm:0,e}updateCachedToken(t,e,i){let n=this.stream.clipPos(i.pos);if(e.token(this.stream.reset(n,t),i),t.value>-1){let{parser:e}=i.p;for(let n=0;n=0&&i.p.parser.dialect.allows(s>>1)){1&s?t.extended=s>>1:t.value=s>>1;break}}}else t.value=0,t.end=this.stream.clipPos(n+1)}putAction(t,e,i,n){for(let e=0;e4*t.bufferLength?new cp(i,t.nodeSet):null}get parsedPos(){return this.minStackPos}advance(){let t,e,i=this.stacks,n=this.minStackPos,s=this.stacks=[];if(this.bigReductionCount>300&&1==i.length){let[t]=i;for(;t.forceReduce()&&t.stack.length&&t.stack[t.stack.length-2]>=this.lastBigReductionStart;);this.bigReductionCount=this.lastBigReductionSize=0}for(let r=0;rn)s.push(o);else{if(this.advanceStack(o,s,i))continue;{t||(t=[],e=[]),t.push(o);let i=this.tokens.getMainToken(o);e.push(i.value,i.end)}}break}}if(!s.length){let e=t&&function(t){let e=null;for(let i of t){let t=i.p.stoppedAt;(i.pos==i.p.stream.end||null!=t&&i.pos>t)&&i.p.parser.stateFlag(i.state,2)&&(!e||e.scorethis.stoppedAt?t[0]:this.runRecovery(t,e,s);if(i)return lp&&console.log("Force-finish "+this.stackID(i)),this.stackToTree(i.forceAll())}if(this.recovering){let t=1==this.recovering?1:3*this.recovering;if(s.length>t)for(s.sort((t,e)=>e.score-t.score);s.length>t;)s.pop();s.some(t=>t.reducePos>n)&&this.recovering--}else if(s.length>1){t:for(let t=0;t500&&n.buffer.length>500){if(!((e.score-n.score||e.buffer.length-n.buffer.length)>0)){s.splice(t--,1);continue t}s.splice(i--,1)}}}s.length>12&&(s.sort((t,e)=>e.score-t.score),s.splice(12,s.length-12))}this.minStackPos=s[0].pos;for(let t=1;t ":"";if(null!=this.stoppedAt&&n>this.stoppedAt)return t.forceReduce()?t:null;if(this.fragments){let e=t.curContext&&t.curContext.tracker.strict,i=e?t.curContext.hash:0;for(let o=this.fragments.nodeAt(n);o;){let n=this.parser.nodeSet.types[o.type.id]==o.type?s.getGoto(t.state,o.type.id):-1;if(n>-1&&o.length&&(!e||(o.prop(pl.contextHash)||0)==i))return t.useNode(o,n),lp&&console.log(r+this.stackID(t)+` (via reuse of ${s.getName(o.type.id)})`),!0;if(!(o instanceof kl)||0==o.children.length||o.positions[0]>0)break;let l=o.children[0];if(!(l instanceof kl&&0==o.positions[0]))break;o=l}}let o=s.stateSlot(t.state,4);if(o>0)return t.reduce(o),lp&&console.log(r+this.stackID(t)+` (via always-reduce ${s.getName(65535&o)})`),!0;if(t.stack.length>=8400)for(;t.stack.length>6e3&&t.forceReduce(););let l=this.tokens.getActions(t);for(let o=0;on?e.push(f):i.push(f)}return!1}advanceFully(t,e){let i=t.pos;for(;;){if(!this.advanceStack(t,null,null))return!1;if(t.pos>i)return dp(t,e),!0}}runRecovery(t,e,i){let n=null,s=!1;for(let r=0;r ":"";if(o.deadEnd){if(s)continue;if(s=!0,o.restart(),lp&&console.log(h+this.stackID(o)+" (restarted)"),this.advanceFully(o,i))continue}let c=o.split(),u=h;for(let t=0;t<10&&c.forceReduce();t++){if(lp&&console.log(u+this.stackID(c)+" (via force-reduce)"),this.advanceFully(c,i))break;lp&&(u=this.stackID(c)+" -> ")}for(let t of o.recoverByInsert(l))lp&&console.log(h+this.stackID(t)+" (via recover-insert)"),this.advanceFully(t,i);this.stream.end>o.pos?(a==o.pos&&(a++,l=0),o.recoverByDelete(l,a),lp&&console.log(h+this.stackID(o)+` (via recover-delete ${this.parser.getName(l)})`),dp(o,i)):(!n||n.scoret.topRules[e][1]),n=[];for(let t=0;t=0)s(n,t,e[i++]);else{let r=e[i+-n];for(let o=-n;o>0;o--)s(e[i++],t,r);i++}}}this.nodeSet=new wl(e.map((e,s)=>vl.define({name:s>=this.minRepeatTerm?void 0:e,id:s,props:n[s],top:i.indexOf(s)>-1,error:0==s,skipped:t.skippedNodes&&t.skippedNodes.indexOf(s)>-1}))),t.propSources&&(this.nodeSet=this.nodeSet.extend(...t.propSources)),this.strict=!1,this.bufferLength=ul;let r=Zd(t.tokenData);this.context=t.context,this.specializerSpecs=t.specialized||[],this.specialized=new Uint16Array(this.specializerSpecs.length);for(let t=0;t"number"==typeof t?new np(r,t):t),this.topRules=t.topRules,this.dialects=t.dialects||{},this.dynamicPrecedences=t.dynamicPrecedences||null,this.tokenPrecTable=t.tokenPrec,this.termNames=t.termNames||null,this.maxNode=this.nodeSet.types.length-1,this.dialect=this.parseDialect(),this.top=this.topRules[Object.keys(this.topRules)[0]]}createParse(t,e,i){let n=new fp(this,t,e,i);for(let s of this.wrappers)n=s(n,t,e,i);return n}getGoto(t,e,i=!1){let n=this.goto;if(e>=n[0])return-1;for(let s=n[e+1];;){let e=n[s++],r=1&e,o=n[s++];if(r&&i)return o;for(let i=s+(e>>1);s0}validAction(t,e){return!!this.allActions(t,t=>t==e||null)}allActions(t,e){let i=this.stateSlot(t,4),n=i?e(i):void 0;for(let i=this.stateSlot(t,1);null==n;i+=3){if(65535==this.data[i]){if(1!=this.data[i+1])break;i=gp(this.data,i+2)}n=e(gp(this.data,i+1))}return n}nextStates(t){let e=[];for(let i=this.stateSlot(t,1);;i+=3){if(65535==this.data[i]){if(1!=this.data[i+1])break;i=gp(this.data,i+2)}if(!(1&this.data[i+2])){let t=this.data[i+1];e.some((e,i)=>1&i&&e==t)||e.push(this.data[i],t)}}return e}configure(t){let e=Object.assign(Object.create(mp.prototype),this);if(t.props&&(e.nodeSet=this.nodeSet.extend(...t.props)),t.top){let i=this.topRules[t.top];if(!i)throw new RangeError(`Invalid top rule name ${t.top}`);e.top=i}return t.tokenizers&&(e.tokenizers=this.tokenizers.map(e=>{let i=t.tokenizers.find(t=>t.from==e);return i?i.to:e})),t.specializers&&(e.specializers=this.specializers.slice(),e.specializerSpecs=this.specializerSpecs.map((i,n)=>{let s=t.specializers.find(t=>t.from==i.external);if(!s)return i;let r=Object.assign(Object.assign({},i),{external:s.to});return e.specializers[n]=vp(r),r})),t.contextTracker&&(e.context=t.contextTracker),t.dialect&&(e.dialect=this.parseDialect(t.dialect)),null!=t.strict&&(e.strict=t.strict),t.wrap&&(e.wrappers=e.wrappers.concat(t.wrap)),null!=t.bufferLength&&(e.bufferLength=t.bufferLength),e}hasWrappers(){return this.wrappers.length>0}getName(t){return this.termNames?this.termNames[t]:String(t<=this.maxNode&&this.nodeSet.types[t].name||t)}get eofTerm(){return this.maxNode+1}get topNode(){return this.nodeSet.types[this.top[1]]}dynamicPrecedence(t){let e=this.dynamicPrecedences;return null==e?0:e[t]||0}parseDialect(t){let e=Object.keys(this.dialects),i=e.map(()=>!1);if(t)for(let n of t.split(" ")){let t=e.indexOf(n);t>=0&&(i[t]=!0)}let n=null;for(let t=0;tt.external(i,n)<<1|e}return t.get}function wp(t){return t>=65&&t<=90||t>=97&&t<=122||t>=48&&t<=57}function bp(t){return t>=48&&t<=57||t>=97&&t<=102||t>=65&&t<=70}function yp(t,e,i){for(let n=!1;;){if(t.next<0)return;if(t.next==e&&!n)return void t.advance();n=i&&!n&&92==t.next,t.advance()}}function xp(t,e){for(;95==t.next||wp(t.next);)null!=e&&(e+=String.fromCharCode(t.next)),t.advance();return e}function kp(t,e){for(;48==t.next||49==t.next;)t.advance();e&&t.next==e&&t.advance()}function Sp(t,e){for(;;){if(46==t.next){if(e)break;e=!0}else if(t.next<48||t.next>57)break;t.advance()}if(69==t.next||101==t.next)for(t.advance(),43!=t.next&&45!=t.next||t.advance();t.next>=48&&t.next<=57;)t.advance()}function Cp(t){for(;!(t.next<0||10==t.next);)t.advance()}function Ap(t,e){for(let i=0;i!=&|~^/",specialVar:"?",identifierQuotes:'"',caseInsensitiveIdentifiers:!1,words:Op("absolute action add after all allocate alter and any are as asc assertion at authorization before begin between both breadth by call cascade cascaded case cast catalog check close collate collation column commit condition connect connection constraint constraints constructor continue corresponding count create cross cube current current_date current_default_transform_group current_transform_group_for_type current_path current_role current_time current_timestamp current_user cursor cycle data day deallocate declare default deferrable deferred delete depth deref desc describe descriptor deterministic diagnostics disconnect distinct do domain drop dynamic each else elseif end end-exec equals escape except exception exec execute exists exit external fetch first for foreign found from free full function general get global go goto grant group grouping handle having hold hour identity if immediate in indicator initially inner inout input insert intersect into is isolation join key language last lateral leading leave left level like limit local localtime localtimestamp locator loop map match method minute modifies module month names natural nesting new next no none not of old on only open option or order ordinality out outer output overlaps pad parameter partial path prepare preserve primary prior privileges procedure public read reads recursive redo ref references referencing relative release repeat resignal restrict result return returns revoke right role rollback rollup routine row rows savepoint schema scroll search second section select session session_user set sets signal similar size some space specific specifictype sql sqlexception sqlstate sqlwarning start state static system_user table temporary then timezone_hour timezone_minute to trailing transaction translation treat trigger under undo union unique unnest until update usage user using value values view when whenever where while with without work write year zone ","array binary bit boolean char character clob date decimal double float int integer interval large national nchar nclob numeric object precision real smallint time timestamp varchar varying ")};function Dp(t){return new sp(e=>{var i;let{next:n}=e;if(e.advance(),Ap(n,Mp)){for(;Ap(e.next,Mp);)e.advance();e.acceptToken(36)}else if(36==n&&t.doubleDollarQuotedStrings){let t=xp(e,"");36==e.next&&(e.advance(),function(t,e){t:for(;;){if(t.next<0)return;if(36==t.next){t.advance();for(let i=0;i1){e.advance(),yp(e,39,t.backslashEscapes),e.acceptToken(3);break}if(!wp(e.next))break;e.advance()}else if(t.plsqlQuotingMechanism&&(113==n||81==n)&&39==e.next&&e.peek(1)>0&&!Ap(e.peek(1),Mp)){let t=e.peek(1);e.advance(2),function(t,e){let i="[{<(".indexOf(String.fromCharCode(e)),n=i<0?e:"]}>)".charCodeAt(i);for(;;){if(t.next<0)return;if(t.next==n&&39==t.peek(1))return void t.advance(2);t.advance()}}(e,t),e.acceptToken(3)}else if(Ap(n,t.identifierQuotes)){yp(e,91==n?93:n,!1),e.acceptToken(19)}else if(40==n)e.acceptToken(7);else if(41==n)e.acceptToken(8);else if(123==n)e.acceptToken(9);else if(125==n)e.acceptToken(10);else if(91==n)e.acceptToken(11);else if(93==n)e.acceptToken(12);else if(59==n)e.acceptToken(13);else if(t.unquotedBitLiterals&&48==n&&98==e.next)e.advance(),kp(e),e.acceptToken(22);else if(98!=n&&66!=n||39!=e.next&&34!=e.next){if(48==n&&(120==e.next||88==e.next)||(120==n||88==n)&&39==e.next){let t=39==e.next;for(e.advance();bp(e.next);)e.advance();t&&39==e.next&&e.advance(),e.acceptToken(4)}else if(46==n&&e.next>=48&&e.next<=57)Sp(e,!0),e.acceptToken(4);else if(46==n)e.acceptToken(14);else if(n>=48&&n<=57)Sp(e,!1),e.acceptToken(4);else if(Ap(n,t.operatorChars)){for(;Ap(e.next,t.operatorChars);)e.advance();e.acceptToken(15)}else if(Ap(n,t.specialVar))e.next==n&&e.advance(),function(t){if(39==t.next||34==t.next||96==t.next){let e=t.next;t.advance(),yp(t,e,!1)}else xp(t)}(e),e.acceptToken(17);else if(58==n||44==n)e.acceptToken(16);else if(wp(n)){let s=xp(e,String.fromCharCode(n));e.acceptToken(46==e.next||46==e.peek(-s.length-1)?18:null!==(i=t.words[s.toLowerCase()])&&void 0!==i?i:18)}}else{const i=e.next;e.advance(),t.treatBitsAsBytes?(yp(e,i,t.backslashEscapes),e.acceptToken(23)):(kp(e,i),e.acceptToken(22))}else e.advance(),yp(e,39,t.backslashEscapes),e.acceptToken(3);else e.advance(),yp(e,39,!0),e.acceptToken(3);else Cp(e),e.acceptToken(1)})}const Rp=Dp(Tp),Pp=mp.deserialize({version:14,states:"%vQ]QQOOO#wQRO'#DSO$OQQO'#CwO%eQQO'#CxO%lQQO'#CyO%sQQO'#CzOOQQ'#DS'#DSOOQQ'#C}'#C}O'UQRO'#C{OOQQ'#Cv'#CvOOQQ'#C|'#C|Q]QQOOQOQQOOO'`QQO'#DOO(xQRO,59cO)PQQO,59cO)UQQO'#DSOOQQ,59d,59dO)cQQO,59dOOQQ,59e,59eO)jQQO,59eOOQQ,59f,59fO)qQQO,59fOOQQ-E6{-E6{OOQQ,59b,59bOOQQ-E6z-E6zOOQQ,59j,59jOOQQ-E6|-E6|O+VQRO1G.}O+^QQO,59cOOQQ1G/O1G/OOOQQ1G/P1G/POOQQ1G/Q1G/QP+kQQO'#C}O+rQQO1G.}O)PQQO,59cO,PQQO'#Cw",stateData:",[~OtOSPOSQOS~ORUOSUOTUOUUOVROXSOZTO]XO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O^]ORvXSvXTvXUvXVvXXvXZvX]vX_vX`vXavXbvXcvXdvXevXfvXgvXhvX~OsvX~P!jOa_Ob_Oc_O~ORUOSUOTUOUUOVROXSOZTO^tO_UO`UOa`Ob`Oc`OdUOeUOfUOgUOhUO~OWaO~P$ZOYcO~P$ZO[eO~P$ZORUOSUOTUOUUOVROXSOZTO^QO_UO`UOaPObPOcPOdUOeUOfUOgUOhUO~O]hOsoX~P%zOajObjOcjO~O^]ORkaSkaTkaUkaVkaXkaZka]ka_ka`kaakabkackadkaekafkagkahka~Oska~P'kO^]O~OWvXYvX[vX~P!jOWnO~P$ZOYoO~P$ZO[pO~P$ZO^]ORkiSkiTkiUkiVkiXkiZki]ki_ki`kiakibkickidkiekifkigkihki~Oski~P)xOWkaYka[ka~P'kO]hO~P$ZOWkiYki[ki~P)xOasObsOcsO~O",goto:"#hwPPPPPPPPPPPPPPPPPPPPPPPPPPx||||!Y!^!d!xPPP#[TYOZeUORSTWZbdfqT[OZQZORiZSWOZQbRQdSQfTZgWbdfqQ^PWk^lmrQl_Qm`RrseVORSTWZbdfq",nodeNames:"⚠ LineComment BlockComment String Number Bool Null ( ) { } [ ] ; . Operator Punctuation SpecialVar Identifier QuotedIdentifier Keyword Type Bits Bytes Builtin Script Statement CompositeIdentifier Parens Braces Brackets Statement",maxTerm:38,nodeProps:[["isolate",-4,1,2,3,19,""]],skippedNodes:[0,1,2],repeatNodeCount:3,tokenData:"RORO",tokenizers:[0,Rp],topRules:{Script:[0,25]},tokenPrec:0});function Bp(t){let e=t.cursor().moveTo(t.from,-1);for(;/Comment/.test(e.name);)e.moveTo(e.from,-1);return e.node}function Ep(t,e){let i=t.sliceString(e.from,e.to),n=/^([`'"\[])(.*)([`'"\]])$/.exec(i);return n?n[2]:i}function Lp(t){return t&&("Identifier"==t.name||"QuotedIdentifier"==t.name)}function Ip(t,e){if("CompositeIdentifier"==e.name){let i=[];for(let n=e.firstChild;n;n=n.nextSibling)Lp(n)&&i.push(Ep(t,n));return i}return[Ep(t,e)]}function Np(t,e){for(let i=[];;){if(!e||"."!=e.name)return i;let n=Bp(e);if(!Lp(n))return i;i.unshift(Ep(t,n)),e=Bp(n)}}function Wp(t,e){let i=xa(t).resolveInner(e,-1),n=function(t,e){let i;for(let t=e;!i;t=t.parent){if(!t)return null;"Statement"==t.name&&(i=t)}let n=null;for(let e=i.firstChild,s=!1,r=null;e;e=e.nextSibling){let i="Keyword"==e.name?t.sliceString(e.from,e.to).toLowerCase():null,o=null;if(s)if("as"==i&&r&&Lp(e.nextSibling))o=Ep(t,e.nextSibling);else{if(i&&Hp.has(i))break;r&&Lp(e)&&(o=Ep(t,e))}else s="from"==i;o&&(n||(n=Object.create(null)),n[o]=Ip(t,r)),r=/Identifier$/.test(e.name)?e:null}return n}(t.doc,i);return"Identifier"==i.name||"QuotedIdentifier"==i.name||"Keyword"==i.name?{from:i.from,quoted:"QuotedIdentifier"==i.name?t.doc.sliceString(i.from,i.from+1):null,parents:Np(t.doc,Bp(i)),aliases:n}:"."==i.name?{from:e,quoted:null,parents:Np(t.doc,i),aliases:n}:{from:e,quoted:null,parents:[],empty:!0,aliases:n}}const Hp=new Set("where group having order union intersect except all distinct limit offset fetch for".split(" "));function Vp(t,e,i){return i.map(i=>({...i,label:i.label[0]==t?i.label:t+i.label+e,apply:void 0}))}const zp=/^\w*$/,Fp=/^[`'"\[]?\w*[`'"\]]?$/;function qp(t){return t.self&&"string"==typeof t.self.label}class _p{constructor(t,e){this.idQuote=t,this.idCaseInsensitive=e,this.list=[],this.children=void 0}child(t){let e=this.children||(this.children=Object.create(null)),i=e[t];return i||(t&&!this.list.some(e=>e.label==t)&&this.list.push($p(t,"type",this.idQuote,this.idCaseInsensitive)),e[t]=new _p(this.idQuote,this.idCaseInsensitive))}maybeChild(t){return this.children?this.children[t]:null}addCompletion(t){let e=this.list.findIndex(e=>e.label==t.label);e>-1?this.list[e]=t:this.list.push(t)}addCompletions(t){for(let e of t)this.addCompletion("string"==typeof e?$p(e,"property",this.idQuote,this.idCaseInsensitive):e)}addNamespace(t){Array.isArray(t)?this.addCompletions(t):qp(t)?this.addNamespace(t.children):this.addNamespaceObject(t)}addNamespaceObject(t){for(let e of Object.keys(t)){let i=t[e],n=null,s=e.replace(/\\?\./g,t=>"."==t?"\0":t).split("\0"),r=this;qp(i)&&(n=i.self,i=i.children);for(let t=0;t{return i(e?n.toUpperCase():n,21==(s=t[n])?"type":20==s?"keyword":"variable");var s});return s=["QuotedIdentifier","String","LineComment","BlockComment","."],r=wf(n),t=>{for(let e=xa(t.state).resolveInner(t.pos,-1);e;e=e.parent){if(s.indexOf(e.name)>-1)return null;if(e.type.isTop)break}return r(t)};var s,r}let jp=Pp.configure({props:[Ha.add({Statement:$a()}),Qa.add({Statement:(t,e)=>({from:Math.min(t.from+100,e.doc.lineAt(t.from).to),to:t.to}),BlockComment:t=>({from:t.from+2,to:t.to-2})}),jl({Keyword:pa.keyword,Type:pa.typeName,Builtin:pa.standard(pa.name),Bits:pa.number,Bytes:pa.string,Bool:pa.bool,Null:pa.null,Number:pa.number,String:pa.string,Identifier:pa.name,QuotedIdentifier:pa.special(pa.string),SpecialVar:pa.special(pa.name),LineComment:pa.lineComment,BlockComment:pa.blockComment,Operator:pa.operator,"Semi Punctuation":pa.punctuation,"( )":pa.paren,"{ }":pa.brace,"[ ]":pa.squareBracket})]});class Kp{constructor(t,e,i){this.dialect=t,this.language=e,this.spec=i}get extension(){return this.language.extension}configureLanguage(t,e){return new Kp(this.dialect,this.language.configure(t,e),this.spec)}static define(t){let e=function(t,e,i,n){let s={};for(let e in Tp)s[e]=(t.hasOwnProperty(e)?t:Tp)[e];return e&&(s.words=Op(e,i||"",n)),s}(t,t.keywords,t.types,t.builtin),i=ya.define({name:"sql",parser:jp.configure({tokenizers:[{from:Rp,to:Dp(e)}]}),languageData:{commentTokens:{line:"--",block:{open:"/*",close:"*/"}},closeBrackets:{brackets:["(","[","{","'",'"',"`"]}}});return new Kp(e,i,t)}}function Xp(t,e){return{label:t,type:e,boost:-1}}function Gp(t,e=!1,i){return Qp(t.dialect.words,e,i||Xp)}function Yp(t){return t.schema?function(t,e,i,n,s,r){var o;let l=(null===(o=null==r?void 0:r.spec.identifierQuotes)||void 0===o?void 0:o[0])||'"',a=new _p(l,!!(null==r?void 0:r.spec.caseInsensitiveIdentifiers)),h=s?a.child(s):null;return a.addNamespace(t),e&&(h||a).addCompletions(e),i&&a.addCompletions(i),h&&a.addCompletions(h.list),n&&a.addCompletions((h||a).child(n).list),t=>{let{parents:e,from:i,quoted:s,empty:r,aliases:o}=Wp(t.state,t.pos);if(r&&!t.explicit)return null;o&&1==e.length&&(e=o[e[0]]||e);let l=a;for(let t of e){for(;!l.children||!l.children[t];)if(l==a&&h)l=h;else{if(l!=h||!n)return null;l=l.child(n)}let e=l.maybeChild(t);if(!e)return null;l=e}let c=l.list;if(l==a&&o&&(c=c.concat(Object.keys(o).map(t=>({label:t,type:"constant"})))),s){let e=s[0],n=Up(e);return{from:i,to:t.state.sliceDoc(t.pos,t.pos+1)==n?t.pos+1:void 0,options:Vp(e,n,c),validFor:Fp}}return{from:i,options:c,validFor:zp}}}(t.schema,t.tables,t.schemas,t.defaultTable,t.defaultSchema,t.dialect||tm):()=>null}function Jp(t){return t.schema?(t.dialect||tm).language.data.of({autocomplete:Yp(t)}):[]}function Zp(t={}){let e=t.dialect||tm;return new Pa(e.language,[Jp(t),e.language.data.of({autocomplete:Gp(e,t.upperCaseKeywords,t.keywordCompletion)})])}const tm=Kp.define({}),em=Kp.define({keywords:"and as asc between by case cast count current_date current_time current_timestamp desc distinct each else escape except exists explain filter first for from full generated group having if in index inner intersect into isnull join last left like limit not null or order outer over pragma primary query raise range regexp right rollback row select set table then to union unique using values view virtual when where",types:"null integer real text blob",builtin:"",operatorChars:"*+-%<>!=&|/~",identifierQuotes:'`"',specialVar:"@:?$",caseInsensitiveIdentifiers:!0}),im=ft.define();function nm(t={}){return Zp({dialect:em,schema:t.schema,defaultTable:t.defaultTable,defaultSchema:t.defaultSchema})}function sm(t,e,i){const n=[ll(),cl,Yr(),ph(),Ir(),[qr,_r],Ot.allowMultipleSelections.of(!0),Ot.transactionFilter.of(t=>{if(!t.docChanged||!t.isUserEvent("input.type")&&!t.isUserEvent("input.complete"))return t;let e=t.startState.languageDataAt("indentOnInput",t.startState.selection.main.head);if(!e.length)return t;let i=t.newDoc,{head:n}=t.newSelection.main,s=i.lineAt(n);if(n>s.from+200)return t;let r=i.sliceString(s.from,n);if(!e.some(t=>t.test(r)))return t;let{state:o}=t,l=-1,a=[];for(let{head:t}of o.selection.ranges){let e=o.doc.lineAt(t);if(e.from==l)continue;l=e.from;let i=Na(o,e.from);if(null==i)continue;let n=/^\s*/.exec(e.text)[0],s=Ia(o,i);n!=s&&a.push({from:e.from,to:e.from+n.length,insert:s})}return a.length?[t,{changes:a,sequential:!0}]:t}),yh(Sh,{fallback:!0}),Bh(),[fd,ld],xd(),oo(),ho(),io,Ou()],s=[...dd,...mu,...af,...rh,...kd,...Id];return t?(n.push(function(t={}){return[nc,ic.of(t),dr.domEventHandlers({beforeinput(t,e){let i="historyUndo"==t.inputType?rc:"historyRedo"==t.inputType?oc:null;return!!i&&(t.preventDefault(),i(e))}})]}()),s.push(...yc)):(e&&s.push({key:"Mod-z",preventDefault:!0,run:()=>(e(),!0)}),i&&s.push({key:"Mod-y",mac:"Mod-Shift-z",preventDefault:!0,run:()=>(i(),!0)},{key:"Mod-Shift-z",preventDefault:!0,run:()=>(i(),!0)})),n.push(xr.of(s)),n}function rm(t,e={}){const{doc:i="",schema:n,defaultTable:s,defaultSchema:r,history:o=!0,onHostUndo:l,onHostRedo:a,extensions:h=[],fixedTooltips:c=!1,onChange:u,onSubmit:f,onEscape:d,lineWrapping:p=!0}=e,m=new tt,g=[];if(f){const t=()=>(f(w),!0);g.push({key:"Mod-Enter",run:t},{key:"Shift-Enter",run:t})}d&&g.push({key:"Escape",run:()=>(d(w),!0)});const v=[J.highest(xr.of(g)),...sm(o,l,a),p?dr.lineWrapping:[],c?fo({position:"fixed"}):[],m.of(nm({schema:n,defaultTable:s,defaultSchema:r})),u?dr.updateListener.of(t=>{t.docChanged&&(t.transactions.some(t=>t.annotation(im))||u(t))}):[],...h];let w=new dr({doc:i,extensions:v,...t?{parent:t}:{}});return{view:w,updateSchema(t){w.dispatch({effects:m.reconfigure(nm(t))})},destroy(){w.destroy()},get value(){return w.state.doc.toString()},set value(t){w.dispatch({changes:{from:0,to:w.state.doc.length,insert:t},annotations:im.of(!0)})}}}function om(t,e){const i={label:t,type:"property",boost:10};return e&&(i.detail=e),i}async function lm(t,e){const i=`${(t||"").replace(/\/+$/,"")}/${encodeURIComponent(e)}/-/editor-schema.json`,n=await fetch(i,{credentials:"same-origin"});if(!n.ok)throw new Error(`datasetteSchema: failed to fetch ${i} (${n.status} ${n.statusText})`);return function(t){const e={};for(const i of t||[]){const t=(i.columns||[]).map(t=>om(t.name,t.type));i.view?e[i.name]={self:{label:i.name,type:"class",detail:"view"},children:t}:e[i.name]=t}return e}((await n.json()).tables)}export{ft as Annotation,tt as Compartment,Ot as EditorState,dr as EditorView,J as Prec,Kp as SQLDialect,em as SQLiteDialect,xd as autocompletion,kd as completionKeymap,rm as createSqlEditor,lm as datasetteSchema,im as hostChange,xr as keymap,Zp as sql,fo as tooltips}; diff --git a/datasette/static/datasette-sql-editor.js b/datasette/static/datasette-sql-editor.js new file mode 100644 index 00000000..70a731ba --- /dev/null +++ b/datasette/static/datasette-sql-editor.js @@ -0,0 +1,326 @@ +// datasette-sql-editor: ESM primitives for embedding Datasette's SQL editor. +// +// This is the single source of truth for Datasette's CodeMirror setup. The IIFE +// entry point (cm-editor.js, served as cm-editor.bundle.js for Datasette's own +// pages) is a thin consumer of these primitives, and plugin authors can import +// this module directly from /-/static/datasette-sql-editor.js to get a SQL +// editor that shares ONE CodeMirror instance per page (no duplicate +// @codemirror/state bug). +// +// Built by rollup.config.mjs into datasette-sql-editor.bundle.js. + +import { + EditorView, + keymap, + lineNumbers, + highlightActiveLineGutter, + highlightSpecialChars, + drawSelection, + dropCursor, + rectangularSelection, + crosshairCursor, + highlightActiveLine, + tooltips, +} from "@codemirror/view"; +import { EditorState, Compartment, Annotation, Prec } from "@codemirror/state"; +import { + foldGutter, + indentOnInput, + syntaxHighlighting, + defaultHighlightStyle, + bracketMatching, + foldKeymap, +} from "@codemirror/language"; +import { history, defaultKeymap, historyKeymap } from "@codemirror/commands"; +import { highlightSelectionMatches, searchKeymap } from "@codemirror/search"; +import { + closeBrackets, + autocompletion, + closeBracketsKeymap, + completionKeymap, +} from "@codemirror/autocomplete"; +import { lintKeymap } from "@codemirror/lint"; +import { sql, SQLDialect } from "@codemirror/lang-sql"; + +// A curated variation of SQLite from lang-sql: +// https://github.com/codemirror/lang-sql/blob/ebf115fffdbe07f91465ccbd82868c587f8182bc/src/sql.ts#L231 +export const SQLiteDialect = SQLDialect.define({ + // Based on https://www.sqlite.org/lang_keywords.html, restricted to likely + // keywords used in select queries. + // https://github.com/simonw/datasette/pull/1893#issuecomment-1316401895: + keywords: + "and as asc between by case cast count current_date current_time current_timestamp desc distinct each else escape except exists explain filter first for from full generated group having if in index inner intersect into isnull join last left like limit not null or order outer over pragma primary query raise range regexp right rollback row select set table then to union unique using values view virtual when where", + // https://www.sqlite.org/datatype3.html + types: "null integer real text blob", + builtin: "", + operatorChars: "*+-%<>!=&|/~", + identifierQuotes: '`"', + specialVar: "@:?$", + caseInsensitiveIdentifiers: true, +}); + +// Annotation used to tag host-originated changes (e.g. a ProseMirror/collab host +// pushing edits into the editor, or the exported `value` setter). Changes tagged +// with this annotation do NOT re-fire onChange, so hosts can suppress the echo of +// their own edits. Mirrors datasette-paper's `fromPM` pattern. +export const hostChange = Annotation.define(); + +// Builds the sql() language extension from a {schema, defaultTable, defaultSchema} +// conf object. Undefined fields are fine - lang-sql ignores them. +function sqlExtension(conf = {}) { + return sql({ + dialect: SQLiteDialect, + schema: conf.schema, + defaultTable: conf.defaultTable, + defaultSchema: conf.defaultSchema, + }); +} + +// Replicates codemirror's basicSetup (node_modules/codemirror/dist/index.js) as a +// plain array so we can drop the undo history when `withHistory` is false. When +// history is off we optionally forward Mod-z / Mod-y / Mod-Shift-z to the host so +// an external undo stack (ProseMirror, collab) can own undo/redo. +function baseSetup(withHistory, onHostUndo, onHostRedo) { + const setup = [ + lineNumbers(), + highlightActiveLineGutter(), + highlightSpecialChars(), + foldGutter(), + drawSelection(), + dropCursor(), + EditorState.allowMultipleSelections.of(true), + indentOnInput(), + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + bracketMatching(), + closeBrackets(), + autocompletion(), + rectangularSelection(), + crosshairCursor(), + highlightActiveLine(), + highlightSelectionMatches(), + ]; + const bindings = [ + ...closeBracketsKeymap, + ...defaultKeymap, + ...searchKeymap, + ...foldKeymap, + ...completionKeymap, + ...lintKeymap, + ]; + if (withHistory) { + setup.push(history()); + bindings.push(...historyKeymap); + } else { + if (onHostUndo) { + bindings.push({ + key: "Mod-z", + preventDefault: true, + run: () => { + onHostUndo(); + return true; + }, + }); + } + if (onHostRedo) { + bindings.push( + { + key: "Mod-y", + mac: "Mod-Shift-z", + preventDefault: true, + run: () => { + onHostRedo(); + return true; + }, + }, + { + key: "Mod-Shift-z", + preventDefault: true, + run: () => { + onHostRedo(); + return true; + }, + }, + ); + } + } + setup.push(keymap.of(bindings)); + return setup; +} + +// createSqlEditor(parent, opts) -> handle +// +// opts: +// doc initial document string (default "") +// schema lang-sql SQLNamespace for autocomplete +// defaultTable unqualified-column default table +// defaultSchema default schema/attached-database name +// history include CM undo history (default true); false forwards +// undo/redo to onHostUndo/onHostRedo +// onHostUndo called on Mod-z when history is false +// onHostRedo called on Mod-y / Mod-Shift-z when history is false +// extensions extra CodeMirror extensions to append (default []) +// fixedTooltips use position:"fixed" tooltips for overflow-clipped containers +// onChange called (update) on user edits; host-annotated changes are +// suppressed +// onSubmit called (view) on Mod-Enter / Shift-Enter (highest precedence) +// onEscape called (view) on Escape +// lineWrapping soft-wrap long lines (default true) +// +// handle: {view, updateSchema(conf), destroy(), get value(), set value(v)} +export function createSqlEditor(parent, opts = {}) { + const { + doc = "", + schema, + defaultTable, + defaultSchema, + history: withHistory = true, + onHostUndo, + onHostRedo, + extensions = [], + fixedTooltips = false, + onChange, + onSubmit, + onEscape, + lineWrapping = true, + } = opts; + + const sqlCompartment = new Compartment(); + + // Highest-precedence keymap so submit/escape win over the basic keymap. + const priorityBindings = []; + if (onSubmit) { + const runSubmit = () => { + onSubmit(view); + return true; + }; + priorityBindings.push( + { key: "Mod-Enter", run: runSubmit }, + { key: "Shift-Enter", run: runSubmit }, + ); + } + if (onEscape) { + priorityBindings.push({ + key: "Escape", + run: () => { + onEscape(view); + return true; + }, + }); + } + + const editorExtensions = [ + Prec.highest(keymap.of(priorityBindings)), + ...baseSetup(withHistory, onHostUndo, onHostRedo), + lineWrapping ? EditorView.lineWrapping : [], + fixedTooltips ? tooltips({ position: "fixed" }) : [], + sqlCompartment.of(sqlExtension({ schema, defaultTable, defaultSchema })), + onChange + ? EditorView.updateListener.of((update) => { + if (!update.docChanged) return; + // Suppress echoes of host-originated changes. + if (update.transactions.some((tr) => tr.annotation(hostChange))) { + return; + } + onChange(update); + }) + : [], + ...extensions, + ]; + + let view = new EditorView({ + doc, + extensions: editorExtensions, + ...(parent ? { parent } : {}), + }); + + return { + view, + // Swap out the schema/defaultTable/defaultSchema used for autocomplete after + // the editor has been created. + // https://codemirror.net/examples/config/#dynamic-configuration + updateSchema(conf) { + view.dispatch({ + effects: sqlCompartment.reconfigure(sqlExtension(conf)), + }); + }, + destroy() { + view.destroy(); + }, + get value() { + return view.state.doc.toString(); + }, + // Host-originated: tagged with hostChange so it does not re-fire onChange. + set value(newValue) { + view.dispatch({ + changes: { from: 0, to: view.state.doc.length, insert: newValue }, + annotations: hostChange.of(true), + }); + }, + }; +} + +// Maps ticket 05's neutral editor-schema shape +// {tables: [{name, view: bool, columns: [{name, type}]}]} +// to a lang-sql SQLNamespace of Completion objects. Kept identical to +// _editor_schema() / _column_completion() in datasette/views/query_helpers.py so +// server-inlined and client-fetched schemas behave the same. +function columnCompletion(name, type) { + const completion = { label: name, type: "property", boost: 10 }; + if (type) { + completion.detail = type; + } + return completion; +} + +function schemaFromTables(tables) { + const schema = {}; + for (const table of tables || []) { + const completions = (table.columns || []).map((column) => + columnCompletion(column.name, column.type), + ); + if (table.view) { + schema[table.name] = { + self: { label: table.name, type: "class", detail: "view" }, + children: completions, + }; + } else { + schema[table.name] = completions; + } + } + return schema; +} + +// datasetteSchema(baseUrl, database) -> Promise +// +// Fetches GET {baseUrl}/{database}/-/editor-schema.json (ticket 05) and maps the +// neutral payload to a lang-sql SQLNamespace ready to pass as opts.schema / +// updateSchema({schema}). baseUrl is Datasette's base_url (may be "" or "/" or a +// mount prefix). Throws a descriptive Error on a non-200 response. +export async function datasetteSchema(baseUrl, database) { + const base = (baseUrl || "").replace(/\/+$/, ""); + const url = `${base}/${encodeURIComponent(database)}/-/editor-schema.json`; + const response = await fetch(url, { credentials: "same-origin" }); + if (!response.ok) { + throw new Error( + `datasetteSchema: failed to fetch ${url} (${response.status} ${response.statusText})`, + ); + } + const data = await response.json(); + return schemaFromTables(data.tables); +} + +// Re-export the CodeMirror pieces callers need so plugin code shares this +// module's single CM instance instead of bundling its own. +export { + EditorView, + EditorState, + Compartment, + Annotation, + Prec, + keymap, + tooltips, + sql, + SQLDialect, + autocompletion, + completionKeymap, +}; diff --git a/datasette/static/edit-tools.js b/datasette/static/edit-tools.js index 0f61ebd6..9e8b93f6 100644 --- a/datasette/static/edit-tools.js +++ b/datasette/static/edit-tools.js @@ -915,7 +915,6 @@ function showTableCreateDialogError(state, message) { function setTableCreateDialogSaving(state, isSaving) { state.isSaving = isSaving; - state.modal.busy = isSaving; state.columnList .querySelectorAll("input, select, button") .forEach(function (control) { @@ -2044,7 +2043,8 @@ async function createTableFromDataPreview(state) { var tableUrl = responseData.table_url || fallbackTableUrl(responseData.table || payload.table); - state.modal.close({ restoreFocus: false }); + state.shouldRestoreFocus = false; + state.dialog.close(); if (tableUrl) { location.href = tableUrl; } else { @@ -2118,7 +2118,8 @@ async function saveTableCreateDialog(state) { var tableUrl = responseData.table_url || fallbackTableUrl(responseData.table || payload.table); - state.modal.close({ restoreFocus: false }); + state.shouldRestoreFocus = false; + state.dialog.close(); if (tableUrl) { location.href = tableUrl; } else { @@ -2140,6 +2141,18 @@ function confirmDiscardTableCreateChanges(state) { return window.confirm("Discard this new table?"); } +function closeTableCreateDialogIfConfirmed(state) { + if (!state || state.isSaving) { + return false; + } + if (!confirmDiscardTableCreateChanges(state)) { + return false; + } + state.shouldRestoreFocus = true; + state.dialog.close(); + return true; +} + function ensureTableCreateDialog(manager) { if (tableCreateDialogState) { return tableCreateDialogState; @@ -2148,8 +2161,7 @@ function ensureTableCreateDialog(manager) { return null; } - var modal = DatasetteModal.create(); - var dialog = modal.dialog; + var dialog = document.createElement("dialog"); dialog.id = TABLE_CREATE_DIALOG_ID; dialog.className = "table-create-dialog"; dialog.setAttribute("aria-labelledby", "table-create-title"); @@ -2159,7 +2171,7 @@ function ensureTableCreateDialog(manager) {
-
[^\\/\\.]+)(\\.(?P\\w+))?$`` " - "for a table page. Use this attribute to group requests by route. " - "Omitted when no route matches.", - optional=True, -) -URL_PATH = Attribute( - "url.path", - "The URL path, excluding the query string.", -) -URL_SCHEME = Attribute("url.scheme", "``http`` or ``https``.") -SERVER_ADDRESS = Attribute( - "server.address", - "The ``Host`` header, including any ``:port`` suffix. This value is " - "supplied by the client.", - optional=True, -) -USER_AGENT_ORIGINAL = Attribute( - "user_agent.original", - "The ``User-Agent`` header, verbatim. Omitted if the client sent none.", - optional=True, -) -INTERNAL_CLIENT = Attribute( - "datasette.internal_client", - "``True`` for requests made through ``datasette.client``. Calls made " - "inside another request produce a nested ``SERVER`` span. Filter on " - "this attribute to exclude internal requests from request counts. " - "Omitted for requests received over the network.", - optional=True, -) -ERROR_TYPE = Attribute( - "error.type", - "The exception class name for a failed operation. On HTTP spans, also " - "set to the status code as a string for 5xx responses. A 4xx response " - "alone does not set this attribute or an error status.", - optional=True, -) - -DB_SYSTEM = Attribute("db.system", "Always ``sqlite``.") -DB_NAMESPACE = Attribute("db.namespace", "Name of the database being queried.") -OPERATION = Attribute( - "datasette.operation", - "Whether the operation was a read or a write.", - values={"read", "write"}, -) -DB_QUERY_TEXT = Attribute( - "db.query.text", - "The SQL, truncated to 2048 characters. Bound parameter values are not " - "recorded. For callback methods, ``datasette.callback`` is recorded instead.", - optional=True, -) -CALLBACK = Attribute( - "datasette.callback", - "The qualified name of the Python callable passed to ``execute_fn()``, " - "``execute_write_fn()`` or ``execute_isolated_fn()``, for example " - "``TableInsertView.post..insert_or_upsert_rows``. Set instead of " - "``db.query.text``. Lambdas appear as ````; use a named function " - "for a more descriptive span.", - optional=True, -) -DB_OPERATION_NAME = Attribute( - "db.operation.name", - "The statement's leading keyword, such as ``SELECT``, ``INSERT`` or " - "``CREATE``, if it matches the supported allowlist. Statements beginning " - "with a common table expression report ``WITH``. Omitted for unrecognized " - "keywords and ``execute_write_script()``.", - optional=True, -) -PARAM_COUNT = Attribute( - "datasette.param_count", - "Number of bound parameters. Recorded instead of the values themselves.", - optional=True, -) -PARAM_SETS = Attribute( - "datasette.param_sets", - "Number of parameter sets consumed by ``execute_write_many()``. " - "The parameter values are not recorded.", - optional=True, -) -TIME_LIMIT_MS = Attribute( - "datasette.time_limit_ms", - "Time limit applied to the read query, in milliseconds: " - ":ref:`setting_sql_time_limit_ms` or a shorter ``custom_time_limit``.", - optional=True, -) -ROWS_RETURNED = Attribute( - "datasette.rows_returned", - "Number of rows returned by a successful read query.", - optional=True, -) -TRUNCATED = Attribute( - "datasette.truncated", - "True if the result was cut short by :ref:`setting_max_returned_rows`.", - optional=True, -) -INTERRUPTED = Attribute( - "datasette.interrupted", - "True if the query exceeded its time limit. The span status is set to " - "``ERROR`` unless the caller used a ``custom_time_limit`` shorter than " - ":ref:`setting_sql_time_limit_ms`, in which case the status is left unset.", - optional=True, -) -SQL_ERROR_SUPPRESSED = Attribute( - "datasette.sql_error_suppressed", - "True for a non-timeout SQL error with ``log_sql_errors=False``. The " - "exception is still raised, but the span status is left unset.", - optional=True, -) -EXECUTESCRIPT = Attribute( - "datasette.executescript", - "True for ``execute_write_script()``, which runs multiple statements.", - optional=True, -) -EXECUTEMANY = Attribute( - "datasette.executemany", - "True for ``execute_write_many()``, which runs one statement against many " - "parameter sets.", - optional=True, -) -ISOLATED_CONNECTION = Attribute( - "datasette.isolated_connection", - "True if the write ran on its own connection rather than the shared write " - "connection.", -) -TRANSACTION = Attribute( - "datasette.transaction", - "False for statements such as ``VACUUM`` that cannot run inside a transaction.", -) - - -# --- Spans ---------------------------------------------------------------- - -HTTP_REQUEST = SpanName( - "{http.request.method} {http.route}", - "One span per HTTP request, containing spans from plugin middleware and " - "database operations. Named for the HTTP method and matched route, or " - "just the method if no route matches. Incoming ``traceparent`` headers " - "are extracted using the global propagator to continue the caller's " - "trace. Incoming ``baggage`` is not propagated into plugin or downstream " - "context in this release. Set ``OTEL_PROPAGATORS=none`` to disable " - "extraction. For public instances, strip trace context headers at your " - "proxy if callers should not supply trace context.", - ( - HTTP_REQUEST_METHOD, - HTTP_ROUTE, - URL_PATH, - URL_SCHEME, - SERVER_ADDRESS, - USER_AGENT_ORIGINAL, - HTTP_RESPONSE_STATUS_CODE, - ERROR_TYPE, - INTERNAL_CLIENT, - ), - dynamic=True, - kind=SpanKind.SERVER, -) - -DB_QUERY = SpanName( - "db.query", - "A SQL operation, including time spent queued for a worker thread. For " - "``block=False`` writes, the span ends after the write is queued. " - "Callback methods record ``datasette.callback`` in place of ``db.query.text``.", - ( - DB_SYSTEM, - DB_NAMESPACE, - DB_QUERY_TEXT, - CALLBACK, - DB_OPERATION_NAME, - PARAM_COUNT, - PARAM_SETS, - TIME_LIMIT_MS, - ROWS_RETURNED, - TRUNCATED, - INTERRUPTED, - SQL_ERROR_SUPPRESSED, - EXECUTESCRIPT, - EXECUTEMANY, - ), - kind=SpanKind.CLIENT, -) - -DB_QUERY_EXECUTE = SpanName( - "db.query.execute", - "The read executing inside a SQL worker thread. Child of ``db.query``; the " - "gap between the two is time spent waiting for a thread.", -) - -DB_WRITE_QUEUE_WAIT = SpanName( - "db.write.queue_wait", - "Time a write spent waiting in its database's write queue. For " - "``block=True``, this is a child of ``db.query``. For ``block=False``, " - "it is a root span linked to the span that queued the write, since the " - "write can outlive that request.", -) - -DB_WRITE_EXECUTE = SpanName( - "db.write.execute", - "The write executing on the write thread. For ``block=True``, this is " - "a child of ``db.query``. For ``block=False``, it is a root span linked " - "to the span that queued the write.", - (ISOLATED_CONNECTION, TRANSACTION), -) - -STARTUP = SpanName( - "datasette.startup", - "Startup work performed by ``invoke_startup()``, including registration " - "hooks, schema catalog updates, saved queries, column type configuration " - "and the ``startup`` hook. Runs during instance startup, either before " - "serving requests or as part of the first request.", -) - -SPANS = ( - HTTP_REQUEST, - DB_QUERY, - DB_QUERY_EXECUTE, - DB_WRITE_QUEUE_WAIT, - DB_WRITE_EXECUTE, - STARTUP, -) - - -def span_for(emitted_name, kind=None, spans=None): - """ - Resolve an emitted span name to its registry entry, or None. - - Exact matches take precedence over `prefix=True` entries, which take - precedence over `dynamic=True` entries matched by `kind`. - - `spans` defaults to Datasette's own registry. - """ - if spans is None: - spans = SPANS - for span in spans: - if span.dynamic: - continue - if emitted_name == span: - return span - for span in spans: - if span.prefix and emitted_name.startswith(span): - return span - if kind is not None: - for span in spans: - if span.dynamic and span.kind == kind: - return span - return None - - -def metric_for(emitted_name, metrics=None): - """ - Resolve an emitted metric name to its registry entry, or None. - - `metrics` defaults to Datasette's own registry. - """ - if metrics is None: - metrics = METRICS - for metric in metrics: - if emitted_name == metric: - return metric - return None - - -def attribute_allowed(entry, emitted_key): - """ - Whether `emitted_key` is a registered attribute of `entry`. - - `entry` is a `SpanName` or a `MetricName` - both carry `.attributes`. - """ - if entry is None: - return False - return emitted_key in entry.attributes - - -def attribute_value_allowed(entry, emitted_key, value): - """ - Whether `value` is permitted for `emitted_key` on `entry` (a `SpanName` - or a `MetricName`). - - Any value is allowed if the attribute does not declare `values=`. - """ - if entry is None: - return False - for attribute in entry.attributes: - if attribute == emitted_key: - return attribute.values is None or value in attribute.values - return False - - -# --- Metrics -------------------------------------------------------------- - -# Bucket boundaries in seconds for every duration histogram. OpenTelemetry's -# defaults are designed for milliseconds and would put almost every SQLite -# query in the first bucket. These are the semantic conventions' recommended -# boundaries for db.client.operation.duration, plus 0.0001 and 0.0005 for -# fast in-process SQLite queries. -DURATION_BUCKETS = (0.0001, 0.0005, 0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10) - -M_OPERATION_DURATION = MetricName( - "db.client.operation.duration", - HISTOGRAM, - "s", - "Duration of a SQL operation, including callback-based calls such as " - "``execute_fn()``. For ``block=False`` writes, measures enqueue time.", - (DB_SYSTEM, DB_NAMESPACE, OPERATION, ERROR_TYPE), - buckets=DURATION_BUCKETS, -) - -M_WRITE_QUEUE_WAIT = MetricName( - "datasette.write.queue_wait", - HISTOGRAM, - "s", - "Time each write waited in its database's write queue.", - (DB_NAMESPACE,), - buckets=DURATION_BUCKETS, -) - -M_QUERIES_INTERRUPTED = MetricName( - "datasette.sql.queries.interrupted", - COUNTER, - "{query}", - "Queries cancelled for exceeding :ref:`setting_sql_time_limit_ms`. A " - "rising rate can indicate that queries need optimization or a higher " - "time limit. Caller-selected timeouts shorter than this limit, such as " - "those used for facet suggestion, are excluded.", - (DB_NAMESPACE,), -) - -M_THREADS_LIMIT = MetricName( - "datasette.sql.threads.limit", - GAUGE, - "{thread}", - "Maximum concurrent read queries, configured by " - ":ref:`setting_num_sql_threads`. Not reported when ``num_sql_threads`` " - "is ``0``.", -) - -M_THREADS_QUEUE_DEPTH = MetricName( - "datasette.sql.threads.queue_depth", - GAUGE, - "{query}", - "Read queries waiting for a free SQL thread. Sustained values above " - "zero indicate a saturated read pool.", -) - -M_QUERIES_PENDING = MetricName( - "datasette.sql.queries.pending", - GAUGE, - "{query}", - "Read queries submitted to the pool and not yet complete. Sum across " - "databases and compare with ``datasette.sql.threads.limit`` to assess " - "pool usage.", - (DB_NAMESPACE,), -) - -M_WRITE_QUEUE_DEPTH = MetricName( - "datasette.write.queue_depth", - GAUGE, - "{write}", - "Writes waiting for a database's single write thread. Increasing " - "``num_sql_threads`` does not increase write concurrency. Not reported for " - "databases that have never been written to.", - (DB_NAMESPACE,), -) - -M_CONNECTIONS_OPEN = MetricName( - "datasette.connections.open", - GAUGE, - "{connection}", - "Open SQLite connections managed by Datasette.", - (DB_NAMESPACE,), -) - -METRICS = ( - M_OPERATION_DURATION, - M_WRITE_QUEUE_WAIT, - M_QUERIES_INTERRUPTED, - M_THREADS_LIMIT, - M_THREADS_QUEUE_DEPTH, - M_QUERIES_PENDING, - M_WRITE_QUEUE_DEPTH, - M_CONNECTIONS_OPEN, -) diff --git a/datasette/telemetry_testing.py b/datasette/telemetry_testing.py deleted file mode 100644 index 77a4431b..00000000 --- a/datasette/telemetry_testing.py +++ /dev/null @@ -1,427 +0,0 @@ -""" -Pytest helpers for testing OpenTelemetry instrumentation - Datasette's own -and any plugin's. Part of Datasette's public plugin API; see the "Telemetry -for plugin authors" documentation. - -Usage from a plugin's ``conftest.py``:: - - from datasette.telemetry_testing import ( # noqa: F401 - MetricsCollector, - otel_metrics, - otel_meter_provider, - otel_provider, - otel_spans, - ) - -Tests can then use the ``otel_spans`` and ``otel_metrics`` fixtures. The -OpenTelemetry SDK is imported lazily, and the fixtures skip if it is not -installed. -""" - -import subprocess -import sys - -import pytest - -from .telemetry_registry import ( - attribute_allowed, - attribute_value_allowed, - metric_for, - span_for, -) - -_span_exporter = None -_metric_reader = None - - -def install_span_exporter(): - """ - Install a TracerProvider + InMemorySpanExporter once per process and - return the exporter, or None when the SDK is not installed. - - Uses `SimpleSpanProcessor` so spans are exported as soon as they end. - """ - global _span_exporter - if _span_exporter is not None: - return _span_exporter - try: - from opentelemetry import trace as otel_trace - from opentelemetry.sdk.trace import TracerProvider - from opentelemetry.sdk.trace.export import SimpleSpanProcessor - from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( - InMemorySpanExporter, - ) - except ImportError: - return None - exporter = InMemorySpanExporter() - provider = TracerProvider() - provider.add_span_processor(SimpleSpanProcessor(exporter)) - otel_trace.set_tracer_provider(provider) - # set_tracer_provider() is ignored if a provider was already installed, - # in which case the fixtures skip - if otel_trace.get_tracer_provider() is not provider: - return None - _span_exporter = exporter - return exporter - - -def install_metric_reader(): - """ - Install a MeterProvider + InMemoryMetricReader once per process and - return the reader, or None when the SDK is not installed. - - Uses delta temporality for counters and histograms, so each collection - only reports measurements since the previous one. - """ - global _metric_reader - if _metric_reader is not None: - return _metric_reader - try: - from opentelemetry import metrics as otel_metrics_api - from opentelemetry.sdk.metrics import Counter, Histogram, MeterProvider - from opentelemetry.sdk.metrics.export import ( - AggregationTemporality, - InMemoryMetricReader, - ) - except ImportError: - return None - reader = InMemoryMetricReader( - preferred_temporality={ - Counter: AggregationTemporality.DELTA, - Histogram: AggregationTemporality.DELTA, - } - ) - provider = MeterProvider(metric_readers=[reader]) - otel_metrics_api.set_meter_provider(provider) - if otel_metrics_api.get_meter_provider() is not provider: - return None - _metric_reader = reader - return reader - - -@pytest.fixture(scope="session", autouse=True) -def otel_provider(): - "Install the span exporter once per test session, before any spans are created." - install_span_exporter() - - -@pytest.fixture(scope="session", autouse=True) -def otel_meter_provider(): - "Install the metric reader once per test session." - install_metric_reader() - - -@pytest.fixture(autouse=True) -def otel_reset(): - "Clear recorded spans and drain collected metrics after every test." - yield - if _span_exporter is not None: - _span_exporter.clear() - if _metric_reader is not None: - _metric_reader.get_metrics_data() - - -@pytest.fixture -def otel_spans(): - """ - The in-memory span exporter, cleared before the test. Call - `.get_finished_spans()` to retrieve spans. - """ - pytest.importorskip("opentelemetry.sdk") - exporter = install_span_exporter() - if exporter is None: - pytest.skip("OpenTelemetry SDK provider was not installed") - exporter.clear() - yield exporter - - -class MetricsCollector: - """ - Wraps an `InMemoryMetricReader`. - - `collect()` runs a collection cycle and stores a snapshot, which - `points()` and `point()` then query. - """ - - def __init__(self, reader): - self.reader = reader - self.snapshot = {} - # (instrumentation scope name, sdk Metric) pairs from the last collect() - self.collected = [] - - def collect(self): - self.snapshot = {} - self.collected = [] - data = self.reader.get_metrics_data() - if data is None: - return self.snapshot - for resource_metrics in data.resource_metrics: - for scope_metrics in resource_metrics.scope_metrics: - scope_name = scope_metrics.scope.name if scope_metrics.scope else None - for metric in scope_metrics.metrics: - self.snapshot.setdefault(metric.name, []).extend( - metric.data.data_points - ) - self.collected.append((scope_name, metric)) - return self.snapshot - - def points(self, name, attributes=None): - "Data points for `name` whose attributes are a superset of `attributes`." - found = [] - for point in self.snapshot.get(name, []): - point_attributes = dict(point.attributes or {}) - if all(point_attributes.get(k) == v for k, v in (attributes or {}).items()): - found.append(point) - return found - - def point(self, name, attributes=None): - "The single matching data point, asserting there is exactly one." - found = self.points(name, attributes) - assert len(found) == 1, ( - f"expected exactly one {name} point matching {attributes}, " - f"got {len(found)}: {found}" - ) - return found[0] - - -@pytest.fixture -def otel_metrics(): - "A `MetricsCollector`, drained before the test so counts start from zero." - pytest.importorskip("opentelemetry.sdk") - reader = install_metric_reader() - if reader is None: - pytest.skip("OpenTelemetry SDK meter provider was not installed") - reader.get_metrics_data() - yield MetricsCollector(reader) - - -def _scoped(finished_spans, scope_name): - if scope_name is None: - return list(finished_spans) - return [ - span - for span in finished_spans - if span.instrumentation_scope and span.instrumentation_scope.name == scope_name - ] - - -def assert_spans_conform(registry_spans, finished_spans, scope_name=None): - """ - Assert every finished span is registered in `registry_spans`, sets only - registered attributes and uses allowed attribute values. - - Pass `scope_name` to only check spans from that instrumentation scope. - """ - problems = [] - for span in _scoped(finished_spans, scope_name): - entry = span_for(str(span.name), kind=span.kind, spans=registry_spans) - if entry is None: - problems.append(f"unregistered span: {span.name!r}") - continue - for key, value in (span.attributes or {}).items(): - if not attribute_allowed(entry, str(key)): - problems.append(f"{span.name}: unregistered attribute {key!r}") - elif not attribute_value_allowed(entry, str(key), value): - problems.append( - f"{span.name}: {key}={value!r} not in the declared enum" - ) - assert not problems, "\n".join(problems) - - -def assert_spans_covered(registry_spans, finished_spans, scope_name=None): - """ - Assert every entry in `registry_spans` was emitted at least once, with - each of its attributes that is not `optional=True`. - """ - spans = _scoped(finished_spans, scope_name) - seen_attributes = {} - for span in spans: - entry = span_for(str(span.name), kind=span.kind, spans=registry_spans) - if entry is not None: - seen = seen_attributes.setdefault(str(entry), set()) - seen.update(str(key) for key in (span.attributes or {})) - problems = [] - for entry in registry_spans: - if str(entry) not in seen_attributes: - problems.append(f"registered span never emitted: {entry!r}") - continue - required = { - str(attribute) for attribute in entry.attributes if not attribute.optional - } - missing = required - seen_attributes[str(entry)] - if missing: - problems.append( - f"{entry}: registered attributes never emitted: {sorted(missing)}" - ) - assert not problems, "\n".join(problems) - - -# Registry instrument kinds mapped to the SDK data type collected for them. -# Both counter kinds collect as Sum, distinguished by is_monotonic. -_KIND_TO_DATA_TYPE = { - "Counter": "Sum", - "UpDownCounter": "Sum", - "Histogram": "Histogram", - "Observable gauge": "Gauge", -} -_KIND_IS_MONOTONIC = {"Counter": True, "UpDownCounter": False} - - -def _scoped_metrics(collector, scope_name): - for scope, metric in collector.collected: - if scope_name is None or scope == scope_name: - yield metric - - -def assert_metrics_conform(registry_metrics, collector, scope_name=None): - """ - Assert every metric in the collector's last `collect()` is registered in - `registry_metrics` with a matching instrument kind and unit, sets only - registered attributes and uses allowed attribute values. - - Pass `scope_name` to only check metrics from that instrumentation scope. - """ - problems = set() - for metric in _scoped_metrics(collector, scope_name): - entry = metric_for(metric.name, metrics=registry_metrics) - if entry is None: - problems.add(f"unregistered metric: {metric.name!r}") - continue - expected_data_type = _KIND_TO_DATA_TYPE.get(entry.kind) - actual_data_type = type(metric.data).__name__ - if expected_data_type is not None and actual_data_type != expected_data_type: - problems.add( - f"{metric.name}: registry declares {entry.kind}, " - f"SDK collected {actual_data_type}" - ) - expected_monotonic = _KIND_IS_MONOTONIC.get(entry.kind) - actual_monotonic = getattr(metric.data, "is_monotonic", None) - if ( - expected_monotonic is not None - and actual_monotonic is not None - and actual_monotonic != expected_monotonic - ): - problems.add( - f"{metric.name}: registry declares {entry.kind}, but the " - f"collected Sum is_monotonic={actual_monotonic}" - ) - if (metric.unit or "") != (entry.unit or ""): - problems.add( - f"{metric.name}: instrument unit {metric.unit!r} != " - f"registry unit {entry.unit!r}" - ) - for point in metric.data.data_points: - for key, value in dict(point.attributes or {}).items(): - if not attribute_allowed(entry, str(key)): - problems.add(f"{metric.name}: unregistered attribute {key!r}") - elif not attribute_value_allowed(entry, str(key), value): - problems.add( - f"{metric.name}: {key}={value!r} not in the declared enum" - ) - assert not problems, "\n".join(sorted(problems)) - - -def assert_metrics_covered(registry_metrics, collector, scope_name=None): - """ - Assert every entry in `registry_metrics` was collected at least once, - with each of its attributes that is not `optional=True`. - - Call `collect()` once after the workload and before this check. - """ - seen_attributes = {} - for metric in _scoped_metrics(collector, scope_name): - entry = metric_for(metric.name, metrics=registry_metrics) - if entry is None: - continue - seen = seen_attributes.setdefault(str(entry), set()) - for point in metric.data.data_points: - seen.update(str(key) for key in dict(point.attributes or {})) - problems = [] - for entry in registry_metrics: - if str(entry) not in seen_attributes: - problems.append(f"registered metric never collected: {entry!r}") - continue - required = { - str(attribute) for attribute in entry.attributes if not attribute.optional - } - missing = required - seen_attributes[str(entry)] - if missing: - problems.append( - f"{entry}: registered attributes never collected: {sorted(missing)}" - ) - assert not problems, "\n".join(problems) - - -def assert_no_forbidden_values( - forbidden, finished_spans=None, collector=None, scope_name=None -): - """ - Assert that none of the `forbidden` strings appear anywhere in the - emitted telemetry: span names, span attribute values, span event names - and attributes, span status descriptions, or metric point attributes. - - Use fake private values such as tokens or email addresses in your test - workload, then check that they were not recorded: - - FORBIDDEN = {"secret-token-123", "alice@example.com"} - run_workload_using_those_values() - assert_no_forbidden_values( - FORBIDDEN, - finished_spans=otel_spans.get_finished_spans(), - collector=otel_metrics, - ) - - Matches substrings of each value's string form. Empty strings in - `forbidden` are ignored. Leave `scope_name` unset to also check - Datasette's own telemetry. - """ - needles = [needle for needle in forbidden if needle] - leaks = set() - - def check(value, where): - text = str(value) - for needle in needles: - if needle in text: - leaks.add(f"{where} contains {needle!r}") - - if finished_spans is not None: - for span in _scoped(finished_spans, scope_name): - check(span.name, f"span name {str(span.name)!r}") - for key, value in (span.attributes or {}).items(): - check(value, f"{span.name} attribute {key}") - for event in span.events or (): - check(event.name, f"{span.name} event name") - for key, value in (event.attributes or {}).items(): - check(value, f"{span.name} event {event.name} attribute {key}") - if span.status is not None and span.status.description: - check(span.status.description, f"{span.name} status description") - if collector is not None: - for metric in _scoped_metrics(collector, scope_name): - for point in metric.data.data_points: - for key, value in dict(point.attributes or {}).items(): - check(value, f"metric {metric.name} attribute {key}") - assert not leaks, "forbidden values leaked into telemetry:\n" + "\n".join( - sorted(leaks) - ) - - -def assert_package_never_imports_sdk(*module_names): - """ - Import the named modules in a fresh interpreter and assert none of them - imported `opentelemetry.sdk`. - - Run the test that calls this early in your suite: on macOS with CPython - 3.13, starting a subprocess from a process with many threads can crash. - """ - imports = "; ".join(f"import {name}" for name in module_names) - code = ( - f"import sys; {imports}; " - "print([m for m in sys.modules if m.startswith('opentelemetry.sdk')])" - ) - result = subprocess.run( - [sys.executable, "-c", code], capture_output=True, text=True, check=True - ) - assert result.stdout.strip() == "[]", ( - f"importing {module_names} pulled in the OpenTelemetry SDK: " - f"{result.stdout.strip()}" - ) diff --git a/datasette/templates/_codemirror.html b/datasette/templates/_codemirror.html index 75c16168..657f99ac 100644 --- a/datasette/templates/_codemirror.html +++ b/datasette/templates/_codemirror.html @@ -1,5 +1,5 @@ - + diff --git a/datasette/templates/_permissions_debug_tabs.html b/datasette/templates/_permissions_debug_tabs.html index 8e0f486e..d7203c1e 100644 --- a/datasette/templates/_permissions_debug_tabs.html +++ b/datasette/templates/_permissions_debug_tabs.html @@ -44,10 +44,10 @@ diff --git a/datasette/templates/allow_debug.html b/datasette/templates/allow_debug.html index fda4032c..1ecc92df 100644 --- a/datasette/templates/allow_debug.html +++ b/datasette/templates/allow_debug.html @@ -3,11 +3,29 @@ {% block title %}Debug allow rules{% endblock %} {% block extra_head %} -{% include "_permission_ui_styles.html" %} {% endblock %} @@ -20,28 +38,24 @@ p.message-warning {

Use this tool to try out different actor and allow combinations. See Defining permissions with "allow" blocks for documentation.

-
-
-
-
- - -
-
- - -
-
-
- -
- +
+
+

+ +
+
+

+ +
+
+ +
+ - {% if error %}

{{ error }}

{% endif %} +{% if error %}

{{ error }}

{% endif %} - {% if result == "True" %}

Result: allow

{% endif %} +{% if result == "True" %}

Result: allow

{% endif %} - {% if result == "False" %}

Result: deny

{% endif %} -
+{% if result == "False" %}

Result: deny

{% endif %} {% endblock %} diff --git a/datasette/templates/api_explorer.html b/datasette/templates/api_explorer.html index 32686af1..4927cb8d 100644 --- a/datasette/templates/api_explorer.html +++ b/datasette/templates/api_explorer.html @@ -3,6 +3,7 @@ {% block title %}API Explorer{% endblock %} {% block extra_head %} + {% endblock %} {% block content %} @@ -125,7 +126,7 @@ getForm.addEventListener("submit", (ev) => { document.getElementById('response-status').textContent = response.status; return response.json(); }).then((data) => { - output.querySelector('pre').textContent = JSON.stringify(data, null, 2); + output.querySelector('pre').innerHTML = jsonFormatHighlight(data); errorList.style.display = 'none'; }).catch((error) => { alert(error); @@ -173,7 +174,7 @@ postForm.addEventListener("submit", (ev) => { } else { errorList.style.display = 'none'; } - output.querySelector('pre').textContent = JSON.stringify(data, null, 2); + output.querySelector('pre').innerHTML = jsonFormatHighlight(data); output.style.display = 'block'; }).catch(err => { alert("Error: " + err); diff --git a/datasette/templates/base.html b/datasette/templates/base.html index e5aa46f3..18288439 100644 --- a/datasette/templates/base.html +++ b/datasette/templates/base.html @@ -8,7 +8,6 @@ {% endfor %} - {% for url in extra_js_urls %} diff --git a/datasette/templates/debug_allowed.html b/datasette/templates/debug_allowed.html index c73cdfb7..83cc1ae6 100644 --- a/datasette/templates/debug_allowed.html +++ b/datasette/templates/debug_allowed.html @@ -3,6 +3,7 @@ {% block title %}Allowed Resources{% endblock %} {% block extra_head %} + {% include "_permission_ui_styles.html" %} {% include "_debug_common_functions.html" %} {% endblock %} @@ -48,7 +49,7 @@
- + Number of results per page (max 200)
@@ -197,7 +198,7 @@ function displayResults(data) { } // Update raw JSON - document.getElementById('raw-json').textContent = JSON.stringify(data, null, 2); + document.getElementById('raw-json').innerHTML = jsonFormatHighlight(data); } function displayError(data) { @@ -207,7 +208,7 @@ function displayError(data) { resultsContent.innerHTML = `
Error: ${escapeHtml(data.error || 'Unknown error')}
`; - document.getElementById('raw-json').textContent = JSON.stringify(data, null, 2); + document.getElementById('raw-json').innerHTML = jsonFormatHighlight(data); } // Disable child input if parent is empty diff --git a/datasette/templates/debug_check.html b/datasette/templates/debug_check.html index c0081c66..3b229a25 100644 --- a/datasette/templates/debug_check.html +++ b/datasette/templates/debug_check.html @@ -1,8 +1,9 @@ {% extends "base.html" %} -{% block title %}Explain a permission decision{% endblock %} +{% block title %}Permission Check{% endblock %} {% block extra_head %} + {% include "_permission_ui_styles.html" %} {% include "_debug_common_functions.html" %} {% endblock %} {% block content %} -

Explain a permission decision

+

Permission check

{% set current_tab = "check" %} {% include "_permissions_debug_tabs.html" %} -

Test an actor, action and resource. The result explains which rules matched, which specificity level won, and whether actor restrictions or required actions changed the verdict.

+

Use this tool to test permission checks for the current actor. It queries the /-/check.json API endpoint.

+ +{% if request.actor %} +

Current actor: {{ request.actor.get("id", "anonymous") }}

+{% else %} +

Current actor: anonymous (not logged in)

+{% endif %}
-
+
- - - Use null for an anonymous actor. This actor is simulated; it does not change who you are signed in as. -
- -
- + - The operation to evaluate + The permission action to check
-
- +
+ - The database or other parent resource + For database-level permissions, specify the database name
-
- - - The table, query or other child resource +
+ + + For table-level permissions, specify the table name (requires parent)
- +
+ {% endblock %} diff --git a/datasette/templates/debug_permissions_playground.html b/datasette/templates/debug_permissions_playground.html index 8b0cbbcf..4410a677 100644 --- a/datasette/templates/debug_permissions_playground.html +++ b/datasette/templates/debug_permissions_playground.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}Permission activity{% endblock %} +{% block title %}Debug permissions{% endblock %} {% block extra_head %} {% include "_permission_ui_styles.html" %} @@ -20,45 +20,60 @@ .check-action, .check-when, .check-result { font-size: 1.3em; } +textarea { + height: 10em; + width: 95%; + box-sizing: border-box; + padding: 0.5em; + border: 2px dotted black; +} +.two-col { + display: inline-block; + width: 48%; +} +.two-col label { + width: 48%; +} +@media only screen and (max-width: 576px) { + .two-col { + width: 100%; + } +} {% endblock %} {% block content %} -

Permission activity

+

Permission playground

{% set current_tab = "permissions" %} {% include "_permissions_debug_tabs.html" %} -

Raw simulator

- -

This form runs a hypothetical permission check and returns its raw explanation JSON. Use the Explain tool for a visual explanation of the same decision.

+

This tool lets you simulate an actor and a permission check for that actor.

-
-
-
- - -
+
+
+ +
-
-
- - -
-
- - -
-
- - -
+
+
+
+ + +
+
+ + +
+
+ +
@@ -110,7 +125,7 @@ debugPost.addEventListener('submit', function(ev) { }); -

Recent permission checks

+

Recent permissions checks

{% if filter != "all" %}All{% else %}All{% endif %}, diff --git a/datasette/templates/debug_rules.html b/datasette/templates/debug_rules.html index a74c18f7..d00ba9cc 100644 --- a/datasette/templates/debug_rules.html +++ b/datasette/templates/debug_rules.html @@ -3,6 +3,7 @@ {% block title %}Permission Rules{% endblock %} {% block extra_head %} + {% include "_permission_ui_styles.html" %} {% include "_debug_common_functions.html" %} {% endblock %} @@ -36,7 +37,7 @@

- + Number of results per page (max 200)
@@ -184,7 +185,7 @@ function displayResults(data) { } // Update raw JSON - document.getElementById('raw-json').textContent = JSON.stringify(data, null, 2); + document.getElementById('raw-json').innerHTML = jsonFormatHighlight(data); } function displayError(data) { @@ -194,7 +195,7 @@ function displayError(data) { resultsContent.innerHTML = `
Error: ${escapeHtml(data.error || 'Unknown error')}
`; - document.getElementById('raw-json').textContent = JSON.stringify(data, null, 2); + document.getElementById('raw-json').innerHTML = jsonFormatHighlight(data); } diff --git a/datasette/templates/index.html b/datasette/templates/index.html index dabf4804..03349279 100644 --- a/datasette/templates/index.html +++ b/datasette/templates/index.html @@ -26,7 +26,8 @@ {% if database.show_table_row_counts %}{{ "{:,}".format(database.hidden_table_rows_sum) }} rows in {% endif %}{{ database.hidden_tables_count }} hidden table{% if database.hidden_tables_count != 1 %}s{% endif -%} {% endif -%} {% if database.views_count -%} - , {{ "{:,}".format(database.views_count) }} view{% if database.views_count != 1 %}s{% endif %} + {% if database.tables_count or database.hidden_tables_count %}, {% endif -%} + {{ "{:,}".format(database.views_count) }} view{% if database.views_count != 1 %}s{% endif %} {% endif %}

{% for table in database.tables_and_views_truncated %}{{ table.name }}{% if table.private %} 🔒{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}{% if database.tables_and_views_more %}, ...{% endif %}

diff --git a/datasette/templates/table.html b/datasette/templates/table.html index 3ce88e35..cd7c9329 100644 --- a/datasette/templates/table.html +++ b/datasette/templates/table.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}{{ database }}: {{ table }}: {% if count_truncated %}{{ "{:,}".format(count - 1) }}+ rows{% elif count or count == 0 %}{{ "{:,}".format(count) }} row{% if count == 1 %}{% else %}s{% endif %}{% endif %}{% if human_description_en %} {{ human_description_en }}{% endif %}{% endblock %} +{% block title %}{{ database }}: {{ table }}: {% if count_truncated %}>{{ "{:,}".format(count - 1) }} rows{% elif count or count == 0 %}{{ "{:,}".format(count) }} row{% if count == 1 %}{% else %}s{% endif %}{% endif %}{% if human_description_en %} {{ human_description_en }}{% endif %}{% endblock %} {% block extra_head %} {{- super() -}} @@ -47,12 +47,11 @@ {% endif %} {% if count or human_description_en %} -

- {% if count_truncated %}{{ "{:,}".format(count - 1) }}+ rows - - +

+ {% if count_truncated %}>{{ "{:,}".format(count - 1) }} rows + {% if allow_execute_sql and query.sql %} count all{% endif %} {% elif count or count == 0 %}{{ "{:,}".format(count) }} row{% if count == 1 %}{% else %}s{% endif %}{% endif %} - {% if human_description_en %}{{ human_description_en }}{% endif %} + {% if human_description_en %}{{ human_description_en }}{% endif %}

{% endif %} @@ -127,7 +126,7 @@ {% endif %} {% if query.sql and allow_execute_sql %} -

✎ View and edit SQL

+

✎ View and edit SQL

{% endif %} diff --git a/datasette/tokens.py b/datasette/tokens.py index 79f840d2..4f905339 100644 --- a/datasette/tokens.py +++ b/datasette/tokens.py @@ -10,7 +10,7 @@ from __future__ import annotations import dataclasses import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import itsdangerous @@ -50,24 +50,24 @@ class TokenRestrictions: database: dict[str, list[str]] = dataclasses.field(default_factory=dict) resource: dict[str, dict[str, list[str]]] = dataclasses.field(default_factory=dict) - def allow_all(self, action: str) -> TokenRestrictions: + def allow_all(self, action: str) -> "TokenRestrictions": """Allow an action across all databases and resources.""" self.all.append(action) return self - def allow_database(self, database: str, action: str) -> TokenRestrictions: + def allow_database(self, database: str, action: str) -> "TokenRestrictions": """Allow an action on a specific database.""" self.database.setdefault(database, []).append(action) return self def allow_resource( self, database: str, resource: str, action: str - ) -> TokenRestrictions: + ) -> "TokenRestrictions": """Allow an action on a specific resource within a database.""" self.resource.setdefault(database, {}).setdefault(resource, []).append(action) return self - def abbreviated(self, datasette: Datasette) -> dict | None: + def abbreviated(self, datasette: "Datasette") -> Optional[dict]: """ Return the abbreviated ``_r`` dictionary shape for this set of restrictions, using action abbreviations registered with ``datasette``. @@ -112,16 +112,16 @@ class TokenHandler: async def create_token( self, - datasette: Datasette, + datasette: "Datasette", actor_id: str, *, - expires_after: int | None = None, - restrictions: TokenRestrictions | None = None, + expires_after: Optional[int] = None, + restrictions: Optional[TokenRestrictions] = None, ) -> str: """Create and return a token string for the given actor.""" raise NotImplementedError - async def verify_token(self, datasette: Datasette, token: str) -> dict | None: + async def verify_token(self, datasette: "Datasette", token: str) -> Optional[dict]: """ Verify a token and return an actor dict. @@ -142,11 +142,11 @@ class SignedTokenHandler(TokenHandler): async def create_token( self, - datasette: Datasette, + datasette: "Datasette", actor_id: str, *, - expires_after: int | None = None, - restrictions: TokenRestrictions | None = None, + expires_after: Optional[int] = None, + restrictions: Optional[TokenRestrictions] = None, ) -> str: if not datasette.setting("allow_signed_tokens"): raise ValueError( @@ -163,7 +163,7 @@ class SignedTokenHandler(TokenHandler): token["_r"] = abbreviated return "dstok_{}".format(datasette.sign(token, namespace="token")) - async def verify_token(self, datasette: Datasette, token: str) -> dict | None: + async def verify_token(self, datasette: "Datasette", token: str) -> Optional[dict]: prefix = "dstok_" if not token.startswith(prefix): @@ -200,8 +200,9 @@ class SignedTokenHandler(TokenHandler): ): duration = max_signed_tokens_ttl - if duration and time.time() - created > duration: - raise TokenInvalid("Token has expired") + if duration: + if time.time() - created > duration: + raise TokenInvalid("Token has expired") actor = {"id": decoded["a"], "token": "dstok"} diff --git a/datasette/tracer.py b/datasette/tracer.py index 1fbda6f9..28f3cc09 100644 --- a/datasette/tracer.py +++ b/datasette/tracer.py @@ -1,11 +1,10 @@ import asyncio -import json -import time -import traceback from contextlib import contextmanager from contextvars import ContextVar - from markupsafe import escape +import time +import json +import traceback tracers = {} @@ -133,17 +132,17 @@ class AsgiTracer: "num_traces": len(traces), "traces": traces, } - content_type = next( - ( + try: + content_type = [ v.decode("utf8") for k, v in response_headers if k.lower() == b"content-type" - ), - "", - ) + ][0] + except IndexError: + content_type = "" if "text/html" in content_type and b"" in accumulated_body: extra = escape(json.dumps(trace_info, indent=2)) - extra_html = f"
{extra}
".encode() + extra_html = f"
{extra}
".encode("utf8") accumulated_body = accumulated_body.replace(b"", extra_html) elif "json" in content_type and accumulated_body.startswith(b"{"): data = json.loads(accumulated_body.decode("utf8")) diff --git a/datasette/url_builder.py b/datasette/url_builder.py index f8da20f3..16b3d42b 100644 --- a/datasette/url_builder.py +++ b/datasette/url_builder.py @@ -1,7 +1,6 @@ +from .utils import tilde_encode, path_with_format, PrefixedUrlString import urllib -from .utils import PrefixedUrlString, path_with_format, tilde_encode - class Urls: def __init__(self, ds): @@ -9,7 +8,8 @@ class Urls: def path(self, path, format=None): if not isinstance(path, PrefixedUrlString): - path = path.removeprefix("/") + if path.startswith("/"): + path = path[1:] path = self.ds.setting("base_url") + path if format is not None: path = path_with_format(path=path, format=format) @@ -56,7 +56,6 @@ class Urls: return PrefixedUrlString(path) def row_blob(self, database, table, row_path, column): - return ( - self.table(database, table) - + f"/{row_path}.blob?_blob_column={urllib.parse.quote_plus(column)}" + return self.table(database, table) + "/{}.blob?_blob_column={}".format( + row_path, urllib.parse.quote_plus(column) ) diff --git a/datasette/utils/__init__.py b/datasette/utils/__init__.py index 669d04c7..42574d3b 100644 --- a/datasette/utils/__init__.py +++ b/datasette/utils/__init__.py @@ -1,31 +1,29 @@ import asyncio -import base64 import binascii +from contextlib import contextmanager +import aiofiles +import click +from collections import OrderedDict, namedtuple, Counter import copy import dataclasses +import base64 import hashlib import inspect import json -import os -import re -import secrets -import shlex -import shutil -import tempfile -import time -import types -import typing -import urllib -from collections import Counter, OrderedDict, namedtuple -from collections.abc import Iterable -from contextlib import contextmanager - -import aiofiles -import click import markupsafe import mergedeep +import os +import re +import shlex +import tempfile +import typing +import time +import types +import secrets +import shutil +from typing import Iterable, List, Tuple +import urllib import yaml - from .shutil_backport import copytree from .sqlite import sqlite3, supports_table_xinfo @@ -38,7 +36,7 @@ if typing.TYPE_CHECKING: class PaginatedResources: """Paginated results from allowed_resources query.""" - resources: list["Resource"] + resources: List["Resource"] next: str | None # Keyset token for next page (None if no more results) _datasette: typing.Any = dataclasses.field(default=None, repr=False) _action: str = dataclasses.field(default=None, repr=False) @@ -85,132 +83,22 @@ class PaginatedResources: # From https://www.sqlite.org/lang_keywords.html -reserved_words = { - "abort", - "action", - "add", - "after", - "all", - "alter", - "analyze", - "and", - "as", - "asc", - "attach", - "autoincrement", - "before", - "begin", - "between", - "by", - "cascade", - "case", - "cast", - "check", - "collate", - "column", - "commit", - "conflict", - "constraint", - "create", - "cross", - "current_date", - "current_time", - "current_timestamp", - "database", - "default", - "deferrable", - "deferred", - "delete", - "desc", - "detach", - "distinct", - "drop", - "each", - "else", - "end", - "escape", - "except", - "exclusive", - "exists", - "explain", - "fail", - "for", - "foreign", - "from", - "full", - "glob", - "group", - "having", - "if", - "ignore", - "immediate", - "in", - "index", - "indexed", - "initially", - "inner", - "insert", - "instead", - "intersect", - "into", - "is", - "isnull", - "join", - "key", - "left", - "like", - "limit", - "match", - "natural", - "no", - "not", - "notnull", - "null", - "of", - "offset", - "on", - "or", - "order", - "outer", - "plan", - "pragma", - "primary", - "query", - "raise", - "recursive", - "references", - "regexp", - "reindex", - "release", - "rename", - "replace", - "restrict", - "right", - "rollback", - "row", - "savepoint", - "select", - "set", - "table", - "temp", - "temporary", - "then", - "to", - "transaction", - "trigger", - "union", - "unique", - "update", - "using", - "vacuum", - "values", - "view", - "virtual", - "when", - "where", - "with", - "without", -} +reserved_words = set( + ( + "abort action add after all alter analyze and as asc attach autoincrement " + "before begin between by cascade case cast check collate column commit " + "conflict constraint create cross current_date current_time " + "current_timestamp database default deferrable deferred delete desc detach " + "distinct drop each else end escape except exclusive exists explain fail " + "for foreign from full glob group having if ignore immediate in index " + "indexed initially inner insert instead intersect into is isnull join key " + "left like limit match natural no not notnull null of offset on or order " + "outer plan pragma primary query raise recursive references regexp reindex " + "release rename replace restrict right rollback row savepoint select set " + "table temp temporary then to transaction trigger union unique update using " + "vacuum values view virtual when where with without" + ).split() +) APT_GET_DOCKERFILE_EXTRAS = r""" RUN apt-get update && \ @@ -270,7 +158,7 @@ functions_marked_as_documented = [] def documented(fn=None, *, label=None): def decorate(fn): - fn._datasette_docs_label = label or f"internals_utils_{fn.__name__}" + fn._datasette_docs_label = label or "internals_utils_{}".format(fn.__name__) functions_marked_as_documented.append(fn) return fn @@ -472,7 +360,7 @@ disallawed_sql_res = [ ( re.compile(f"pragma(?!_({'|'.join(allowed_pragmas)}))"), "Statement contained a disallowed PRAGMA. Allowed pragma functions are {}".format( - ", ".join(f"pragma_{pragma}()" for pragma in allowed_pragmas) + ", ".join("pragma_{}()".format(pragma) for pragma in allowed_pragmas) ), ) ] @@ -568,7 +456,7 @@ def escape_css_string(s): def escape_sqlite(s): - if _boring_keyword_re.fullmatch(s) and (s.lower() not in reserved_words): + if _boring_keyword_re.match(s) and (s.lower() not in reserved_words): return s return '"{}"'.format(s.replace('"', '""')) @@ -646,7 +534,10 @@ CMD {cmd}""".format( else "" ), environment_variables="\n".join( - [f"ENV {key} '{value}'" for key, value in environment_variables.items()] + [ + "ENV {} '{}'".format(key, value) + for key, value in environment_variables.items() + ] ), install_from=" ".join(install), files=" ".join(files), @@ -745,11 +636,11 @@ def detect_primary_keys(conn, table): def get_outbound_foreign_keys(conn, table): - infos = conn.execute(f"PRAGMA foreign_key_list({escape_sqlite(table)})").fetchall() + infos = conn.execute(f"PRAGMA foreign_key_list([{table}])").fetchall() fks = [] for info in infos: if info is not None: - id, seq, table_name, from_, to_, _on_update, _on_delete, _match = info + id, seq, table_name, from_, to_, on_update, on_delete, match = info fks.append( { "column": from_, @@ -820,8 +711,7 @@ def detect_spatialite(conn): def detect_fts(conn, table): """Detect if table has a corresponding FTS virtual table and return it""" - sql, params = detect_fts_sql(table) - rows = conn.execute(sql, params).fetchall() + rows = conn.execute(detect_fts_sql(table)).fetchall() if len(rows) == 0: return None else: @@ -829,26 +719,18 @@ def detect_fts(conn, table): def detect_fts_sql(table): - escaped_table = table.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") - return ( - r""" - select name from sqlite_master - where rootpage = 0 - and ( - sql like :fts_double_quoted escape char(92) - or sql like :fts_bracket_quoted escape char(92) - or ( - tbl_name = :table - and sql like '%VIRTUAL TABLE%USING FTS%' - ) + return r""" + select name from sqlite_master + where rootpage = 0 + and ( + sql like '%VIRTUAL TABLE%USING FTS%content="{table}"%' + or sql like '%VIRTUAL TABLE%USING FTS%content=[{table}]%' + or ( + tbl_name = "{table}" + and sql like '%VIRTUAL TABLE%USING FTS%' ) - """, - { - "fts_double_quoted": f'%VIRTUAL TABLE%USING FTS%content="{escaped_table}"%', - "fts_bracket_quoted": f"%VIRTUAL TABLE%USING FTS%content=[{escaped_table}]%", - "table": table, - }, - ) + ) + """.format(table=table.replace("'", "''")) def detect_json1(conn=None): @@ -859,7 +741,7 @@ def detect_json1(conn=None): try: conn.execute("SELECT json('{}')") return True - except sqlite3.Error: + except Exception: return False finally: if close_conn: @@ -939,7 +821,9 @@ def is_url(value): if not value.startswith("http://") and not value.startswith("https://"): return False # Any whitespace at all is invalid - return not whitespace_re.search(value) + if whitespace_re.search(value): + return False + return True css_class_re = re.compile(r"^[a-zA-Z]+[_a-zA-Z0-9-]*$") @@ -992,9 +876,7 @@ def module_from_path(path, name): mod.__file__ = path with open(path, "r") as file: code = compile(file.read(), path, "exec", dont_inherit=True) - # Executing the file is the whole point - this is how --plugins-dir loads - # plugins and how metadata/config .py files are evaluated - exec(code, mod.__dict__) # noqa: S102 + exec(code, mod.__dict__) return mod @@ -1151,7 +1033,9 @@ def escape_fts(query): query += '"' bits = _escape_fts_re.split(query) bits = [b for b in bits if b and b != '""'] - return " ".join(f'"{bit}"' if not bit.startswith('"') else bit for bit in bits) + return " ".join( + '"{}"'.format(bit) if not bit.startswith('"') else bit for bit in bits + ) class MultiParams: @@ -1163,7 +1047,7 @@ class MultiParams: data[key], (list, tuple) ), "dictionary data should be a dictionary of key => [list]" self._data = data - elif isinstance(data, (list, tuple)): + elif isinstance(data, list) or isinstance(data, tuple): new_data = {} for item in data: assert ( @@ -1253,7 +1137,9 @@ def _gather_arguments(fn, kwargs): for parameter in parameters: if parameter not in kwargs: raise TypeError( - f"{fn} requires parameters {tuple(parameters)}, missing: {set(parameters) - set(kwargs.keys())}" + "{} requires parameters {}, missing: {}".format( + fn, tuple(parameters), set(parameters) - set(kwargs.keys()) + ) ) call_with.append(kwargs[parameter]) return call_with @@ -1322,9 +1208,9 @@ def resolve_env_secrets(config, environ): """Create copy that recursively replaces {"$env": "NAME"} with values from environ""" if isinstance(config, dict): if list(config.keys()) == ["$env"]: - return environ.get(next(iter(config.values()))) + return environ.get(list(config.values())[0]) elif list(config.keys()) == ["$file"]: - with open(next(iter(config.values()))) as fp: + with open(list(config.values())[0]) as fp: return fp.read() else: return { @@ -1420,7 +1306,7 @@ _named_param_re = re.compile(r":(\w+)") @documented -def named_parameters(sql: str) -> list[str]: +def named_parameters(sql: str) -> List[str]: """ Given a SQL statement, return a list of named parameters that are used in the statement @@ -1433,7 +1319,7 @@ def named_parameters(sql: str) -> list[str]: return _named_param_re.findall(sql) -async def derive_named_parameters(db: "Database", sql: str) -> list[str]: +async def derive_named_parameters(db: "Database", sql: str) -> List[str]: """ This undocumented but stable method exists for backwards compatibility with plugins that were using it before it switched to named_parameters() @@ -1457,9 +1343,9 @@ def parse_size_limit(value, default, maximum, name="_size"): if size < 0: raise ValueError except ValueError: - raise ValueError(f"{name} must be a positive integer") + raise ValueError("{} must be a positive integer".format(name)) if size > maximum: - raise ValueError(f"{name} must be <= {maximum}") + raise ValueError("{} must be <= {}".format(name, maximum)) return size @@ -1517,7 +1403,7 @@ class TildeEncoder(dict): elif b == _space: res = "+" else: - res = f"~{b:02X}" + res = "~{:02X}".format(b) self[b] = res return res @@ -1566,13 +1452,7 @@ async def row_sql_params_pks(db, table, pk_values): if use_rowid: select = "rowid, *" pks = ["rowid"] - wheres = [] - for i, pk in enumerate(pks): - escaped_pk = escape_sqlite(pk) - # Preserve the historic always-quoted SQL exposed by _extra=query - if escaped_pk == pk: - escaped_pk = f'"{pk}"' - wheres.append(f"{escaped_pk}=:p{i}") + wheres = [f'"{pk}"=:p{i}' for i, pk in enumerate(pks)] sql = f"select {select} from {escape_sqlite(table)} where {' AND '.join(wheres)}" params = {} for i, pk_value in enumerate(pk_values): @@ -1618,7 +1498,7 @@ def _combine(base: dict, update: dict) -> dict: return base -def pairs_to_nested_config(pairs: list[tuple[str, typing.Any]]) -> dict: +def pairs_to_nested_config(pairs: typing.List[typing.Tuple[str, typing.Any]]) -> dict: """ Parse a list of key-value pairs into a nested dictionary. """ @@ -1633,7 +1513,7 @@ def make_slot_function(name, datasette, request, **kwargs): from datasette.plugins import pm method = getattr(pm.hook, name, None) - assert method is not None, f"No hook found for {name}" + assert method is not None, "No hook found for {}".format(name) async def inner(): html_bits = [] @@ -1657,7 +1537,7 @@ def prune_empty_dicts(d: dict): d.pop(key, None) -def move_plugins_and_allow(source: dict, destination: dict) -> tuple[dict, dict]: +def move_plugins_and_allow(source: dict, destination: dict) -> Tuple[dict, dict]: """ Move 'plugins' and 'allow' keys from source to destination dictionary. Creates hierarchy in destination if needed. After moving, recursively remove any keys @@ -1744,7 +1624,7 @@ def redact_keys(original: dict, key_patterns: Iterable) -> dict: return { k: ( redact(v) - if not any(pattern in k.casefold() for pattern in key_patterns) + if not any(pattern in k for pattern in key_patterns) else "***" ) for k, v in data.items() diff --git a/datasette/utils/actions_sql.py b/datasette/utils/actions_sql.py index 297f5ae5..c7137e6b 100644 --- a/datasette/utils/actions_sql.py +++ b/datasette/utils/actions_sql.py @@ -29,15 +29,6 @@ from datasette.utils.permissions import gather_permission_sql_from_hooks if TYPE_CHECKING: from datasette.app import Datasette - from datasette.permissions import Action - - -def _child_collation(action: "Action") -> str: - """Match resource identity without changing the spelling returned by SQL.""" - resource_class = action.resource_class - if resource_class is not None and resource_class.case_insensitive_child: - return "NOCASE" - return "BINARY" async def build_allowed_resources_sql( @@ -158,7 +149,6 @@ async def _build_single_action_sql( raise ValueError(f"Unknown action: {action}") # Get base resources SQL from the resource class - child_collation = _child_collation(action_obj) base_resources_sql = await action_obj.resource_class.resources_sql( datasette, actor=actor ) @@ -195,7 +185,7 @@ async def _build_single_action_sql( if permission_sql.sql is None: continue rule_sqls.append(f""" - SELECT parent, child COLLATE {child_collation} AS child, allow, reason, '{permission_sql.source}' AS source_plugin FROM ( + SELECT parent, child, allow, reason, '{permission_sql.source}' AS source_plugin FROM ( {permission_sql.sql} ) """.strip()) @@ -262,62 +252,88 @@ async def _build_single_action_sql( ] ) - # Continue with the cascading logic. - # Aggregate the RULES by cascade level (small), rather than grouping - # base x rules (which scales with the number of resources). - def _agg(select_key, where, group_by): - parts = [ - f" SELECT {select_key}", - " MAX(CASE WHEN allow = 0 THEN 1 ELSE 0 END) AS any_deny,", - " MAX(CASE WHEN allow = 1 THEN 1 ELSE 0 END) AS any_allow,", - " json_group_array(CASE WHEN allow = 0 THEN source_plugin || ': ' || reason END) AS deny_reasons,", - " json_group_array(CASE WHEN allow = 1 THEN source_plugin || ': ' || reason END) AS allow_reasons", - f" FROM all_rules WHERE {where}", - ] - if group_by: - parts.append(f" GROUP BY {group_by}") - return parts - + # Continue with the cascading logic query_parts.extend( - ["child_agg AS ("] - + _agg( - "parent, child,", - "parent IS NOT NULL AND child IS NOT NULL", - "parent, child", - ) - + ["),", "parent_agg AS ("] - + _agg("parent,", "parent IS NOT NULL AND child IS NULL", "parent") - + ["),", "global_agg AS ("] - + _agg("", "parent IS NULL AND child IS NULL", None) - + ["),"] + [ + "child_lvl AS (", + " SELECT b.parent, b.child,", + " MAX(CASE WHEN ar.allow = 0 THEN 1 ELSE 0 END) AS any_deny,", + " MAX(CASE WHEN ar.allow = 1 THEN 1 ELSE 0 END) AS any_allow,", + " json_group_array(CASE WHEN ar.allow = 0 THEN ar.source_plugin || ': ' || ar.reason END) AS deny_reasons,", + " json_group_array(CASE WHEN ar.allow = 1 THEN ar.source_plugin || ': ' || ar.reason END) AS allow_reasons", + " FROM base b", + " LEFT JOIN all_rules ar ON ar.parent = b.parent AND ar.child = b.child", + " GROUP BY b.parent, b.child", + "),", + "parent_lvl AS (", + " SELECT b.parent, b.child,", + " MAX(CASE WHEN ar.allow = 0 THEN 1 ELSE 0 END) AS any_deny,", + " MAX(CASE WHEN ar.allow = 1 THEN 1 ELSE 0 END) AS any_allow,", + " json_group_array(CASE WHEN ar.allow = 0 THEN ar.source_plugin || ': ' || ar.reason END) AS deny_reasons,", + " json_group_array(CASE WHEN ar.allow = 1 THEN ar.source_plugin || ': ' || ar.reason END) AS allow_reasons", + " FROM base b", + " LEFT JOIN all_rules ar ON ar.parent = b.parent AND ar.child IS NULL", + " GROUP BY b.parent, b.child", + "),", + "global_lvl AS (", + " SELECT b.parent, b.child,", + " MAX(CASE WHEN ar.allow = 0 THEN 1 ELSE 0 END) AS any_deny,", + " MAX(CASE WHEN ar.allow = 1 THEN 1 ELSE 0 END) AS any_allow,", + " json_group_array(CASE WHEN ar.allow = 0 THEN ar.source_plugin || ': ' || ar.reason END) AS deny_reasons,", + " json_group_array(CASE WHEN ar.allow = 1 THEN ar.source_plugin || ': ' || ar.reason END) AS allow_reasons", + " FROM base b", + " LEFT JOIN all_rules ar ON ar.parent IS NULL AND ar.child IS NULL", + " GROUP BY b.parent, b.child", + "),", + ] ) # Add anonymous decision logic if needed if include_is_private: - - def _anon_agg(select_key, where, group_by): - parts = [ - f" SELECT {select_key}", - " MAX(CASE WHEN allow = 0 THEN 1 ELSE 0 END) AS any_deny,", - " MAX(CASE WHEN allow = 1 THEN 1 ELSE 0 END) AS any_allow", - f" FROM anon_rules WHERE {where}", - ] - if group_by: - parts.append(f" GROUP BY {group_by}") - return parts - query_parts.extend( - ["anon_child_agg AS ("] - + _anon_agg( - f"parent, child COLLATE {child_collation} AS child,", - "parent IS NOT NULL AND child IS NOT NULL", - f"parent, child COLLATE {child_collation}", - ) - + ["),", "anon_parent_agg AS ("] - + _anon_agg("parent,", "parent IS NOT NULL AND child IS NULL", "parent") - + ["),", "anon_global_agg AS ("] - + _anon_agg("", "parent IS NULL AND child IS NULL", None) - + ["),"] + [ + "anon_child_lvl AS (", + " SELECT b.parent, b.child,", + " MAX(CASE WHEN ar.allow = 0 THEN 1 ELSE 0 END) AS any_deny,", + " MAX(CASE WHEN ar.allow = 1 THEN 1 ELSE 0 END) AS any_allow", + " FROM base b", + " LEFT JOIN anon_rules ar ON ar.parent = b.parent AND ar.child = b.child", + " GROUP BY b.parent, b.child", + "),", + "anon_parent_lvl AS (", + " SELECT b.parent, b.child,", + " MAX(CASE WHEN ar.allow = 0 THEN 1 ELSE 0 END) AS any_deny,", + " MAX(CASE WHEN ar.allow = 1 THEN 1 ELSE 0 END) AS any_allow", + " FROM base b", + " LEFT JOIN anon_rules ar ON ar.parent = b.parent AND ar.child IS NULL", + " GROUP BY b.parent, b.child", + "),", + "anon_global_lvl AS (", + " SELECT b.parent, b.child,", + " MAX(CASE WHEN ar.allow = 0 THEN 1 ELSE 0 END) AS any_deny,", + " MAX(CASE WHEN ar.allow = 1 THEN 1 ELSE 0 END) AS any_allow", + " FROM base b", + " LEFT JOIN anon_rules ar ON ar.parent IS NULL AND ar.child IS NULL", + " GROUP BY b.parent, b.child", + "),", + "anon_decisions AS (", + " SELECT", + " b.parent, b.child,", + " CASE", + " WHEN acl.any_deny = 1 THEN 0", + " WHEN acl.any_allow = 1 THEN 1", + " WHEN apl.any_deny = 1 THEN 0", + " WHEN apl.any_allow = 1 THEN 1", + " WHEN agl.any_deny = 1 THEN 0", + " WHEN agl.any_allow = 1 THEN 1", + " ELSE 0", + " END AS anon_is_allowed", + " FROM base b", + " JOIN anon_child_lvl acl ON b.parent = acl.parent AND (b.child = acl.child OR (b.child IS NULL AND acl.child IS NULL))", + " JOIN anon_parent_lvl apl ON b.parent = apl.parent AND (b.child = apl.child OR (b.child IS NULL AND apl.child IS NULL))", + " JOIN anon_global_lvl agl ON b.parent = agl.parent AND (b.child = agl.child OR (b.child IS NULL AND agl.child IS NULL))", + "),", + ] ) # Final decisions @@ -326,28 +342,31 @@ async def _build_single_action_sql( "decisions AS (", " SELECT", " b.parent, b.child,", - " -- Cascading permission logic: child -> parent -> global, DENY beats ALLOW at each level", + " -- Cascading permission logic: child → parent → global, DENY beats ALLOW at each level", " -- Priority order:", - " -- 1. Child-level deny 2. Child-level allow", - " -- 3. Parent-level deny 4. Parent-level allow", - " -- 5. Global-level deny 6. Global-level allow", + " -- 1. Child-level deny (most specific, blocks access)", + " -- 2. Child-level allow (most specific, grants access)", + " -- 3. Parent-level deny (intermediate, blocks access)", + " -- 4. Parent-level allow (intermediate, grants access)", + " -- 5. Global-level deny (least specific, blocks access)", + " -- 6. Global-level allow (least specific, grants access)", " -- 7. Default deny (no rules match)", " CASE", - " WHEN ca.any_deny = 1 THEN 0", - " WHEN ca.any_allow = 1 THEN 1", - " WHEN pa.any_deny = 1 THEN 0", - " WHEN pa.any_allow = 1 THEN 1", - " WHEN ga.any_deny = 1 THEN 0", - " WHEN ga.any_allow = 1 THEN 1", + " WHEN cl.any_deny = 1 THEN 0", + " WHEN cl.any_allow = 1 THEN 1", + " WHEN pl.any_deny = 1 THEN 0", + " WHEN pl.any_allow = 1 THEN 1", + " WHEN gl.any_deny = 1 THEN 0", + " WHEN gl.any_allow = 1 THEN 1", " ELSE 0", " END AS is_allowed,", " CASE", - " WHEN ca.any_deny = 1 THEN ca.deny_reasons", - " WHEN ca.any_allow = 1 THEN ca.allow_reasons", - " WHEN pa.any_deny = 1 THEN pa.deny_reasons", - " WHEN pa.any_allow = 1 THEN pa.allow_reasons", - " WHEN ga.any_deny = 1 THEN ga.deny_reasons", - " WHEN ga.any_allow = 1 THEN ga.allow_reasons", + " WHEN cl.any_deny = 1 THEN cl.deny_reasons", + " WHEN cl.any_allow = 1 THEN cl.allow_reasons", + " WHEN pl.any_deny = 1 THEN pl.deny_reasons", + " WHEN pl.any_allow = 1 THEN pl.allow_reasons", + " WHEN gl.any_deny = 1 THEN gl.deny_reasons", + " WHEN gl.any_allow = 1 THEN gl.allow_reasons", " ELSE '[]'", " END AS reason", ] @@ -355,34 +374,21 @@ async def _build_single_action_sql( if include_is_private: query_parts.append( - " , CASE WHEN (" - "CASE" - " WHEN aca.any_deny = 1 THEN 0" - " WHEN aca.any_allow = 1 THEN 1" - " WHEN apa.any_deny = 1 THEN 0" - " WHEN apa.any_allow = 1 THEN 1" - " WHEN aga.any_deny = 1 THEN 0" - " WHEN aga.any_allow = 1 THEN 1" - " ELSE 0 END" - ") = 0 THEN 1 ELSE 0 END AS is_private" + " , CASE WHEN ad.anon_is_allowed = 0 THEN 1 ELSE 0 END AS is_private" ) query_parts.extend( [ " FROM base b", - " LEFT JOIN child_agg ca ON ca.parent = b.parent AND ca.child = b.child", - " LEFT JOIN parent_agg pa ON pa.parent = b.parent", - " CROSS JOIN global_agg ga", + " JOIN child_lvl cl ON b.parent = cl.parent AND (b.child = cl.child OR (b.child IS NULL AND cl.child IS NULL))", + " JOIN parent_lvl pl ON b.parent = pl.parent AND (b.child = pl.child OR (b.child IS NULL AND pl.child IS NULL))", + " JOIN global_lvl gl ON b.parent = gl.parent AND (b.child = gl.child OR (b.child IS NULL AND gl.child IS NULL))", ] ) if include_is_private: - query_parts.extend( - [ - " LEFT JOIN anon_child_agg aca ON aca.parent = b.parent AND aca.child = b.child", - " LEFT JOIN anon_parent_agg apa ON apa.parent = b.parent", - " CROSS JOIN anon_global_agg aga", - ] + query_parts.append( + " JOIN anon_decisions ad ON b.parent = ad.parent AND (b.child = ad.child OR (b.child IS NULL AND ad.child IS NULL))" ) query_parts.append(")") @@ -392,31 +398,10 @@ async def _build_single_action_sql( # Wrap each restriction_sql in a subquery to avoid operator precedence issues # with UNION ALL inside the restriction SQL statements restriction_intersect = "\nINTERSECT\n".join( - f"SELECT parent, child COLLATE {child_collation} AS child FROM ({sql})" - for sql in restriction_sqls + f"SELECT * FROM ({sql})" for sql in restriction_sqls ) - # Decompose by NULL-pattern so the final filter can use pure-equality - # EXISTS lookups (satisfiable via automatic indexes) instead of a - # correlated OR-scan over the whole list. query_parts.extend( - [ - ",", - "restriction_list AS (", - f" {restriction_intersect}", - "),", - "restriction_exact AS (", - " SELECT parent, child FROM restriction_list WHERE parent IS NOT NULL AND child IS NOT NULL", - "),", - "restriction_parent_any AS (", - " SELECT DISTINCT parent FROM restriction_list WHERE parent IS NOT NULL AND child IS NULL", - "),", - "restriction_child_any AS (", - " SELECT DISTINCT child FROM restriction_list WHERE parent IS NULL AND child IS NOT NULL", - "),", - "restriction_all AS (", - " SELECT 1 AS matched FROM restriction_list WHERE parent IS NULL AND child IS NULL LIMIT 1", - ")", - ] + [",", "restriction_list AS (", f" {restriction_intersect}", ")"] ) # Final SELECT @@ -431,11 +416,10 @@ async def _build_single_action_sql( # Add restriction filter if there are restrictions if restriction_sqls: query_parts.append(""" - AND ( - EXISTS (SELECT 1 FROM restriction_all) - OR EXISTS (SELECT 1 FROM restriction_parent_any r WHERE r.parent = decisions.parent) - OR EXISTS (SELECT 1 FROM restriction_child_any r WHERE r.child = decisions.child) - OR EXISTS (SELECT 1 FROM restriction_exact r WHERE r.parent = decisions.parent AND r.child = decisions.child) + AND EXISTS ( + SELECT 1 FROM restriction_list r + WHERE (r.parent = decisions.parent OR r.parent IS NULL) + AND (r.child = decisions.child OR r.child IS NULL) )""") # Add parent filter if specified @@ -491,7 +475,6 @@ async def build_permission_rules_sql( union_parts = [] all_params = {} restriction_sqls = [] - child_collation = _child_collation(action_obj) for permission_sql in permission_sqls: all_params.update(permission_sql.params or {}) @@ -505,7 +488,7 @@ async def build_permission_rules_sql( continue union_parts.append(f""" - SELECT parent, child COLLATE {child_collation} AS child, allow, reason, '{permission_sql.source}' AS source_plugin FROM ( + SELECT parent, child, allow, reason, '{permission_sql.source}' AS source_plugin FROM ( {permission_sql.sql} ) """.strip()) @@ -576,7 +559,6 @@ async def check_permissions_for_actions( verdicts = {} for i, (action, permission_sqls) in enumerate(zip(unique_actions, gathered)): - child_collation = _child_collation(datasette.actions[action]) prefix = f"a{i}_" rule_parts = [] restriction_parts = [] @@ -602,7 +584,7 @@ async def check_permissions_for_actions( if sql is None: continue rule_parts.append( - f"SELECT parent, child COLLATE {child_collation} AS child, allow, reason, '{permission_sql.source}' AS source_plugin FROM (\n{sql}\n)" + f"SELECT parent, child, allow, reason, '{permission_sql.source}' AS source_plugin FROM (\n{sql}\n)" ) if not rule_parts: @@ -636,8 +618,7 @@ async def check_permissions_for_actions( if restriction_parts: # Database-level restrictions (parent, NULL) match all children restriction_intersect = "\nINTERSECT\n".join( - f"SELECT parent, child COLLATE {child_collation} AS child FROM ({sql})" - for sql in restriction_parts + f"SELECT * FROM ({sql})" for sql in restriction_parts ) ctes.append(f"a{i}_restriction AS (\n{restriction_intersect}\n)") verdict_sql = f"""({verdict_sql}) AND EXISTS ( @@ -692,240 +673,3 @@ async def check_permission_for_resource( child=child, ) return results[action] - - -async def explain_permission_for_resource( - *, - datasette: "Datasette", - actor: dict | None, - action: str, - parent: str | None, - child: str | None, -) -> dict: - """Explain a permission decision for one action and resource. - - This is intended for Datasette's permission debugging tools. It uses the - same ``permission_resources_sql`` hook results and the same resolution - rules as :func:`check_permissions_for_actions`, but also returns the - matching rules, actor restriction results and ``also_requires`` chain. - - The returned dictionary is part of Datasette's unstable debugging API. - """ - - action_obj = datasette.actions.get(action) - if action_obj is None: - raise ValueError(f"Unknown action: {action}") - - explanation = await _explain_single_action( - datasette=datasette, - actor=actor, - action=action, - parent=parent, - child=child, - ) - - required_actions = [] - if action_obj.also_requires: - required = await explain_permission_for_resource( - datasette=datasette, - actor=actor, - action=action_obj.also_requires, - parent=parent, - child=child, - ) - required_actions.append(required) - - explanation["required_actions"] = required_actions - explanation["allowed"] = bool( - explanation["rule_allowed"] - and explanation["restriction_allowed"] - and all(required["allowed"] for required in required_actions) - ) - explanation["summary"] = _permission_explanation_summary(explanation) - return explanation - - -async def _explain_single_action( - *, - datasette: "Datasette", - actor: dict | None, - action: str, - parent: str | None, - child: str | None, -) -> dict: - """Return matching rules and restrictions for a single action.""" - from datasette.utils.permissions import SKIP_PERMISSION_CHECKS - - permission_sqls = await gather_permission_sql_from_hooks( - datasette=datasette, - actor=actor, - action=action, - ) - - if permission_sqls is SKIP_PERMISSION_CHECKS: - return { - "action": action, - "rule_allowed": True, - "restriction_allowed": True, - "winning_scope": "global", - "matched_rules": [ - { - "scope": "global", - "effect": "allow", - "source": "skip_permission_checks", - "reason": "Permission checks were explicitly skipped", - "decisive": True, - "ignored_because": None, - } - ], - "restrictions": [], - } - - db = datasette.get_internal_database() - matched_rules = [] - restrictions = [] - child_collation = _child_collation(datasette.actions[action]) - - for permission_sql in permission_sqls: - params = dict(permission_sql.params or {}) - parent_param = _unused_parameter_name(params, "_explain_parent") - params[parent_param] = parent - child_param = _unused_parameter_name(params, "_explain_child") - params[child_param] = child - - if permission_sql.sql: - rows = await db.execute( - f""" - SELECT parent, child, allow, reason - FROM ({permission_sql.sql}) AS permission_rules - WHERE (parent IS NULL OR parent = :{parent_param}) - AND (child IS NULL OR child COLLATE {child_collation} = :{child_param}) - """, - params, - ) - for row in rows: - specificity = ( - 2 - if row["child"] is not None - else 1 if row["parent"] is not None else 0 - ) - matched_rules.append( - { - "scope": ("resource", "parent", "global")[2 - specificity], - "effect": "allow" if row["allow"] else "deny", - "source": permission_sql.source, - "reason": row["reason"], - "_specificity": specificity, - } - ) - - if permission_sql.restriction_sql: - restriction_row = ( - await db.execute( - f""" - SELECT EXISTS( - SELECT 1 FROM ({permission_sql.restriction_sql}) AS restriction_rules - WHERE (parent IS NULL OR parent = :{parent_param}) - AND (child IS NULL OR child COLLATE {child_collation} = :{child_param}) - ) AS resource_is_in_allowlist - """, - params, - ) - ).first() - restriction_allowed = bool(restriction_row[0]) - restrictions.append( - { - "source": permission_sql.source, - "allowed": restriction_allowed, - "reason": params.get("deny") - or ( - "Resource is included in this restriction allowlist" - if restriction_allowed - else "Resource is not included in this restriction allowlist" - ), - } - ) - - matched_rules.sort( - key=lambda rule: ( - -rule["_specificity"], - 0 if rule["effect"] == "deny" else 1, - rule["source"] or "", - rule["reason"] or "", - ) - ) - - if matched_rules: - winning_specificity = matched_rules[0]["_specificity"] - winning_rules = [ - rule - for rule in matched_rules - if rule["_specificity"] == winning_specificity - ] - rule_allowed = not any(rule["effect"] == "deny" for rule in winning_rules) - winning_scope = winning_rules[0]["scope"] - else: - winning_specificity = None - rule_allowed = False - winning_scope = None - - for rule in matched_rules: - specificity = rule.pop("_specificity") - if specificity != winning_specificity: - rule["decisive"] = False - rule["ignored_because"] = "A more specific rule matched" - elif not rule_allowed and rule["effect"] == "allow": - rule["decisive"] = False - rule["ignored_because"] = "A deny rule matched at the same scope" - else: - rule["decisive"] = True - rule["ignored_because"] = None - - return { - "action": action, - "rule_allowed": rule_allowed, - "restriction_allowed": all( - restriction["allowed"] for restriction in restrictions - ), - "winning_scope": winning_scope, - "matched_rules": matched_rules, - "restrictions": restrictions, - } - - -def _unused_parameter_name(params: dict, preferred: str) -> str: - """Return a SQL parameter name that is not already in ``params``.""" - candidate = preferred - suffix = 2 - while candidate in params: - candidate = f"{preferred}_{suffix}" - suffix += 1 - return candidate - - -def _permission_explanation_summary(explanation: dict) -> str: - denied_requirement = next( - ( - required - for required in explanation["required_actions"] - if not required["allowed"] - ), - None, - ) - if denied_requirement: - return ( - f"Denied because {explanation['action']} also requires " - f"{denied_requirement['action']}, which was denied." - ) - if not explanation["matched_rules"]: - return "Denied because no permission rule matched this actor and resource." - if not explanation["rule_allowed"]: - return ( - f"Denied by a {explanation['winning_scope']}-level rule. " - "Deny rules take precedence over allow rules at the same scope." - ) - if not explanation["restriction_allowed"]: - return ( - "Denied because the resource is not included in the actor's restrictions." - ) - return f"Allowed by the matching {explanation['winning_scope']}-level rule." diff --git a/datasette/utils/asgi.py b/datasette/utils/asgi.py index 2d4a6cff..610b86f2 100644 --- a/datasette/utils/asgi.py +++ b/datasette/utils/asgi.py @@ -1,30 +1,28 @@ -import asyncio import json -import re -from http.cookies import Morsel, SimpleCookie -from mimetypes import guess_type -from pathlib import Path -from urllib.parse import parse_qs, parse_qsl, urlunparse - -import aiofiles -import aiofiles.os - +from typing import Optional from datasette.utils import MultiParams, calculate_etag, error_body, sha256_file from datasette.utils.multipart import ( - DEFAULT_MAX_FIELD_SIZE, - DEFAULT_MAX_FIELDS, + parse_form_data, + MultipartParseError, + FormData, DEFAULT_MAX_FILE_SIZE, + DEFAULT_MAX_REQUEST_SIZE, + DEFAULT_MAX_FIELDS, DEFAULT_MAX_FILES, + DEFAULT_MAX_PARTS, + DEFAULT_MAX_FIELD_SIZE, DEFAULT_MAX_MEMORY_FILE_SIZE, DEFAULT_MAX_PART_HEADER_BYTES, DEFAULT_MAX_PART_HEADER_LINES, - DEFAULT_MAX_PARTS, - DEFAULT_MAX_REQUEST_SIZE, DEFAULT_MIN_FREE_DISK_BYTES, - FormData, - MultipartParseError, - parse_form_data, ) +from mimetypes import guess_type +from urllib.parse import parse_qs, urlunparse, parse_qsl +from pathlib import Path +from http.cookies import SimpleCookie, Morsel +import aiofiles +import aiofiles.os +import re # Workaround for adding samesite support to pre 3.8 python Morsel._reserved["samesite"] = "SameSite" @@ -83,19 +81,6 @@ SAMESITE_VALUES = ("strict", "lax", "none") DEFAULT_MAX_POST_BODY_BYTES = 2 * 1024 * 1024 # 2MB -class _RequestHeaders(dict): - """Incoming headers with lowercase keys and case-insensitive lookups.""" - - def __getitem__(self, key): - return super().__getitem__(key.lower()) - - def get(self, key, default=None): - return super().get(key.lower(), default) - - def __contains__(self, key): - return super().__contains__(key.lower()) - - class Request: def __init__(self, scope, receive, max_post_body_bytes=DEFAULT_MAX_POST_BODY_BYTES): self.scope = scope @@ -103,7 +88,7 @@ class Request: self.max_post_body_bytes = max_post_body_bytes def __repr__(self): - return f'' + return ''.format(self.method, self.url) @property def method(self): @@ -125,10 +110,10 @@ class Request: @property def headers(self): - return _RequestHeaders( - (k.decode("latin-1").lower(), v.decode("latin-1")) + return { + k.decode("latin-1").lower(): v.decode("latin-1") for k, v in self.scope.get("headers") or [] - ) + } @property def host(self): @@ -182,7 +167,7 @@ class Request: if max_bytes is None: max_bytes = self.max_post_body_bytes too_large = PayloadTooLarge( - f"Request body exceeded maximum size of {max_bytes} bytes" + "Request body exceeded maximum size of {} bytes".format(max_bytes) ) if max_bytes: # Reject early if the client declares an oversized body @@ -221,7 +206,7 @@ class Request: max_request_size: int = DEFAULT_MAX_REQUEST_SIZE, max_fields: int = DEFAULT_MAX_FIELDS, max_files: int = DEFAULT_MAX_FILES, - max_parts: int | None = DEFAULT_MAX_PARTS, + max_parts: Optional[int] = DEFAULT_MAX_PARTS, max_field_size: int = DEFAULT_MAX_FIELD_SIZE, max_memory_file_size: int = DEFAULT_MAX_MEMORY_FILE_SIZE, max_part_header_bytes: int = DEFAULT_MAX_PART_HEADER_BYTES, @@ -314,24 +299,12 @@ class AsgiLifespan: while True: message = await receive() if message["type"] == "lifespan.startup": - try: - for fn in self.on_startup: - await fn() - except Exception as e: # noqa: BLE001 - await send( - {"type": "lifespan.startup.failed", "message": str(e)} - ) - return + for fn in self.on_startup: + await fn() await send({"type": "lifespan.startup.complete"}) elif message["type"] == "lifespan.shutdown": - try: - for fn in self.on_shutdown: - await fn() - except Exception as e: # noqa: BLE001 - await send( - {"type": "lifespan.shutdown.failed", "message": str(e)} - ) - return + for fn in self.on_shutdown: + await fn() await send({"type": "lifespan.shutdown.complete"}) return else: @@ -511,8 +484,6 @@ def asgi_static(root_path, chunk_size=4096, headers=None, content_type=None): await asgi_send_html(send, "404: File not found", 404) return - # Only the actual static-file handler can bypass dynamic response privacy. - inner_static._datasette_static = True return inner_static @@ -558,9 +529,9 @@ class Response: httponly=False, samesite="lax", ): - assert ( - samesite in SAMESITE_VALUES - ), f"samesite should be one of {SAMESITE_VALUES}" + assert samesite in SAMESITE_VALUES, "samesite should be one of {}".format( + SAMESITE_VALUES + ) cookie = SimpleCookie() cookie[key] = value for prop_name, prop_value in ( @@ -652,23 +623,10 @@ class AsgiRunOnFirstRequest: self.asgi = asgi self.on_startup = on_startup self._started = False - # Guards against concurrent early requests interleaving with startup: - # without this, several requests could all observe `_started is - # False` and proceed before any of them finish running the hooks. - self._lock = asyncio.Lock() async def __call__(self, scope, receive, send): - # Leave "lifespan" scope events alone - this shim only exists as a - # fallback for hosts that never send them. It wraps AsgiLifespan, so - # if it ran on_startup here too, a startup exception would escape - # before AsgiLifespan's own try/except got a chance to turn it into - # a lifespan.startup.failed message. - if scope["type"] != "lifespan" and not self._started: - async with self._lock: - # Re-check: another request may have finished startup while - # we were waiting for the lock. - if not self._started: - for hook in self.on_startup: - await hook() - self._started = True + if not self._started: + self._started = True + for hook in self.on_startup: + await hook() return await self.asgi(scope, receive, send) diff --git a/datasette/utils/baseconv.py b/datasette/utils/baseconv.py index 0469d7a8..c4b64908 100644 --- a/datasette/utils/baseconv.py +++ b/datasette/utils/baseconv.py @@ -13,7 +13,7 @@ Originally shared here: https://www.djangosnippets.org/snippets/1431/ """ -class BaseConverter: +class BaseConverter(object): decimal_digits = "0123456789" def __init__(self, digits): diff --git a/datasette/utils/check_callable.py b/datasette/utils/check_callable.py index e21a769b..a0997d20 100644 --- a/datasette/utils/check_callable.py +++ b/datasette/utils/check_callable.py @@ -1,6 +1,6 @@ import inspect import types -from typing import Any, NamedTuple +from typing import NamedTuple, Any class CallableStatus(NamedTuple): @@ -19,7 +19,7 @@ def check_callable(obj: Any) -> CallableStatus: if isinstance(obj, types.FunctionType): return CallableStatus(True, inspect.iscoroutinefunction(obj)) - if callable(obj): + if hasattr(obj, "__call__"): return CallableStatus(True, inspect.iscoroutinefunction(obj.__call__)) - assert False, f"obj {obj!r} is somehow callable with no __call__ method" + assert False, "obj {} is somehow callable with no __call__ method".format(repr(obj)) diff --git a/datasette/utils/internal_db.py b/datasette/utils/internal_db.py index 0ddeb847..10ca32a5 100644 --- a/datasette/utils/internal_db.py +++ b/datasette/utils/internal_db.py @@ -3,7 +3,7 @@ import textwrap from sqlite_utils import Database as SQLiteUtilsDatabase from sqlite_utils import Migrations -from datasette.utils import escape_sqlite, table_column_details +from datasette.utils import table_column_details INTERNAL_DB_SCHEMA_TABLES = { "catalog_databases", @@ -180,9 +180,29 @@ async def init_internal_db(db): await db.execute_write_fn(apply_migrations, transaction=False) -async def populate_schema_tables(internal_db, db, schema_version): +async def populate_schema_tables(internal_db, db): database_name = db.name + def delete_everything(conn): + conn.execute( + "DELETE FROM catalog_tables WHERE database_name = ?", [database_name] + ) + conn.execute( + "DELETE FROM catalog_views WHERE database_name = ?", [database_name] + ) + conn.execute( + "DELETE FROM catalog_columns WHERE database_name = ?", [database_name] + ) + conn.execute( + "DELETE FROM catalog_foreign_keys WHERE database_name = ?", + [database_name], + ) + conn.execute( + "DELETE FROM catalog_indexes WHERE database_name = ?", [database_name] + ) + + await internal_db.execute_write_fn(delete_everything) + tables = (await db.execute("select * from sqlite_master WHERE type = 'table'")).rows views = (await db.execute("select * from sqlite_master WHERE type = 'view'")).rows @@ -207,30 +227,25 @@ async def populate_schema_tables(internal_db, db, schema_version): columns = table_column_details(conn, table_name) columns_to_insert.extend( { - "database_name": database_name, - "table_name": table_name, + **{"database_name": database_name, "table_name": table_name}, **column._asdict(), } for column in columns ) foreign_keys = conn.execute( - f"PRAGMA foreign_key_list({escape_sqlite(table_name)})" + f"PRAGMA foreign_key_list([{table_name}])" ).fetchall() foreign_keys_to_insert.extend( { - "database_name": database_name, - "table_name": table_name, + **{"database_name": database_name, "table_name": table_name}, **dict(foreign_key), } for foreign_key in foreign_keys ) - indexes = conn.execute( - f"PRAGMA index_list({escape_sqlite(table_name)})" - ).fetchall() + indexes = conn.execute(f"PRAGMA index_list([{table_name}])").fetchall() indexes_to_insert.extend( { - "database_name": database_name, - "table_name": table_name, + **{"database_name": database_name, "table_name": table_name}, **dict(index), } for index in indexes @@ -251,76 +266,47 @@ async def populate_schema_tables(internal_db, db, schema_version): indexes_to_insert, ) = await db.execute_fn(collect_info) - def replace_catalog(conn): - # Delete child rows before their catalog_tables parents so this also - # works if a prepare_connection plugin enables foreign key enforcement. - for table in ( - "catalog_columns", - "catalog_foreign_keys", - "catalog_indexes", - "catalog_views", - "catalog_tables", - ): - conn.execute( - f"DELETE FROM {table} WHERE database_name = ?", - [database_name], - ) - conn.execute( - """ - INSERT OR REPLACE INTO catalog_databases ( - database_name, path, is_memory, schema_version - ) VALUES (?, ?, ?, ?) - """, - [ - database_name, - str(db.path) if db.path is not None else None, - db.is_memory, - schema_version, - ], + await internal_db.execute_write_many( + """ + INSERT INTO catalog_tables (database_name, table_name, rootpage, sql) + values (?, ?, ?, ?) + """, + tables_to_insert, + ) + await internal_db.execute_write_many( + """ + INSERT INTO catalog_views (database_name, view_name, rootpage, sql) + values (?, ?, ?, ?) + """, + views_to_insert, + ) + await internal_db.execute_write_many( + """ + INSERT INTO catalog_columns ( + database_name, table_name, cid, name, type, "notnull", default_value, is_pk, hidden + ) VALUES ( + :database_name, :table_name, :cid, :name, :type, :notnull, :default_value, :is_pk, :hidden ) - conn.executemany( - """ - INSERT INTO catalog_tables (database_name, table_name, rootpage, sql) - values (?, ?, ?, ?) - """, - tables_to_insert, + """, + columns_to_insert, + ) + await internal_db.execute_write_many( + """ + INSERT INTO catalog_foreign_keys ( + database_name, table_name, "id", seq, "table", "from", "to", on_update, on_delete, match + ) VALUES ( + :database_name, :table_name, :id, :seq, :table, :from, :to, :on_update, :on_delete, :match ) - conn.executemany( - """ - INSERT INTO catalog_views (database_name, view_name, rootpage, sql) - values (?, ?, ?, ?) - """, - views_to_insert, + """, + foreign_keys_to_insert, + ) + await internal_db.execute_write_many( + """ + INSERT INTO catalog_indexes ( + database_name, table_name, seq, name, "unique", origin, partial + ) VALUES ( + :database_name, :table_name, :seq, :name, :unique, :origin, :partial ) - conn.executemany( - """ - INSERT INTO catalog_columns ( - database_name, table_name, cid, name, type, "notnull", default_value, is_pk, hidden - ) VALUES ( - :database_name, :table_name, :cid, :name, :type, :notnull, :default_value, :is_pk, :hidden - ) - """, - columns_to_insert, - ) - conn.executemany( - """ - INSERT INTO catalog_foreign_keys ( - database_name, table_name, "id", seq, "table", "from", "to", on_update, on_delete, match - ) VALUES ( - :database_name, :table_name, :id, :seq, :table, :from, :to, :on_update, :on_delete, :match - ) - """, - foreign_keys_to_insert, - ) - conn.executemany( - """ - INSERT INTO catalog_indexes ( - database_name, table_name, seq, name, "unique", origin, partial - ) VALUES ( - :database_name, :table_name, :seq, :name, :unique, :origin, :partial - ) - """, - indexes_to_insert, - ) - - await internal_db.execute_write_fn(replace_catalog) + """, + indexes_to_insert, + ) diff --git a/datasette/utils/multipart.py b/datasette/utils/multipart.py index 182c7ab1..cfa77486 100644 --- a/datasette/utils/multipart.py +++ b/datasette/utils/multipart.py @@ -11,10 +11,15 @@ Supports: import asyncio import shutil import tempfile -from collections.abc import Callable from dataclasses import dataclass, field from typing import ( Any, + Callable, + Dict, + List, + Optional, + Tuple, + Union, ) from urllib.parse import parse_qsl @@ -24,7 +29,7 @@ DEFAULT_MAX_REQUEST_SIZE = 100 * 1024 * 1024 # 100MB DEFAULT_MAX_FIELDS = 1000 DEFAULT_MAX_FILES = 100 # If max_parts is not specified, it defaults to max_fields + max_files -DEFAULT_MAX_PARTS: int | None = None +DEFAULT_MAX_PARTS: Optional[int] = None DEFAULT_MAX_FIELD_SIZE = 100 * 1024 # 100KB DEFAULT_MAX_MEMORY_FILE_SIZE = 1024 * 1024 # 1MB DEFAULT_MAX_PART_HEADER_BYTES = 16 * 1024 # 16KB @@ -35,6 +40,8 @@ DEFAULT_MIN_FREE_DISK_BYTES = 50 * 1024 * 1024 # 50MB class MultipartParseError(Exception): """Raised when multipart parsing fails.""" + pass + @dataclass class UploadedFile: @@ -50,7 +57,7 @@ class UploadedFile: name: str filename: str - content_type: str | None + content_type: Optional[str] size: int _file: tempfile.SpooledTemporaryFile = field(repr=False) @@ -79,8 +86,7 @@ class UploadedFile: def __del__(self): try: self._file.close() - except Exception: # noqa: BLE001, S110 - # __del__ must never raise + except Exception: pass @@ -92,27 +98,27 @@ class FormData: """ def __init__(self): - self._data: list[tuple[str, str | UploadedFile]] = [] + self._data: List[Tuple[str, Union[str, UploadedFile]]] = [] - def append(self, key: str, value: str | UploadedFile) -> None: + def append(self, key: str, value: Union[str, UploadedFile]) -> None: """Add a key-value pair.""" self._data.append((key, value)) - def __getitem__(self, key: str) -> str | UploadedFile: + def __getitem__(self, key: str) -> Union[str, UploadedFile]: """Get the first value for a key.""" for k, v in self._data: if k == key: return v raise KeyError(key) - def get(self, key: str, default: Any = None) -> str | UploadedFile | None: + def get(self, key: str, default: Any = None) -> Optional[Union[str, UploadedFile]]: """Get the first value for a key, or default if not found.""" try: return self[key] except KeyError: return default - def getlist(self, key: str) -> list[str | UploadedFile]: + def getlist(self, key: str) -> List[Union[str, UploadedFile]]: """Get all values for a key.""" return [v for k, v in self._data if k == key] @@ -136,15 +142,15 @@ class FormData: """Return unique keys.""" return list(self) - def items(self) -> list[tuple[str, str | UploadedFile]]: + def items(self) -> List[Tuple[str, Union[str, UploadedFile]]]: """Return all key-value pairs.""" return list(self._data) - def values(self) -> list[str | UploadedFile]: + def values(self) -> List[Union[str, UploadedFile]]: """Return all values.""" return [v for _, v in self._data] - def _uploaded_files(self) -> list[UploadedFile]: + def _uploaded_files(self) -> List[UploadedFile]: """Return UploadedFile instances contained in this form.""" return [v for _, v in self._data if isinstance(v, UploadedFile)] @@ -157,7 +163,7 @@ class FormData: for uploaded in self._uploaded_files(): try: uploaded.close_sync() - except Exception: # noqa: BLE001, S110 + except Exception: # Best-effort cleanup; ignore close errors pass @@ -166,7 +172,7 @@ class FormData: for uploaded in self._uploaded_files(): try: await uploaded.close() - except Exception: # noqa: BLE001, S110 + except Exception: # Best-effort cleanup; ignore close errors pass @@ -183,13 +189,13 @@ class FormData: await self.aclose() -def parse_content_disposition(header: str) -> dict[str, str | None]: +def parse_content_disposition(header: str) -> Dict[str, Optional[str]]: """ Parse Content-Disposition header value. Returns dict with 'name', 'filename' keys (filename may be None). """ - result: dict[str, str | None] = {"name": None, "filename": None} + result: Dict[str, Optional[str]] = {"name": None, "filename": None} # Split on semicolons, handling quoted strings parts = [] @@ -232,8 +238,7 @@ def parse_content_disposition(header: str) -> dict[str, str | None]: from urllib.parse import unquote result["filename"] = unquote(encoded, encoding="utf-8") - except Exception: # noqa: BLE001, S110 - # Malformed RFC 5987 filename* - fall back to the plain filename + except Exception: pass continue @@ -245,19 +250,20 @@ def parse_content_disposition(header: str) -> dict[str, str | None]: if key == "name": result["name"] = value - # Only set filename if filename* hasn't already set it - elif key == "filename" and result["filename"] is None: - # Strip path components (security) - # Handle both Unix and Windows paths - value = value.replace("\\", "/") - if "/" in value: - value = value.rsplit("/", 1)[-1] - result["filename"] = value + elif key == "filename": + # Only set if filename* hasn't already set it + if result["filename"] is None: + # Strip path components (security) + # Handle both Unix and Windows paths + value = value.replace("\\", "/") + if "/" in value: + value = value.rsplit("/", 1)[-1] + result["filename"] = value return result -def parse_content_type(header: str) -> tuple[str, dict[str, str]]: +def parse_content_type(header: str) -> Tuple[str, Dict[str, str]]: """ Parse Content-Type header value. @@ -301,7 +307,7 @@ class MultipartParser: max_request_size: int = DEFAULT_MAX_REQUEST_SIZE, max_fields: int = DEFAULT_MAX_FIELDS, max_files: int = DEFAULT_MAX_FILES, - max_parts: int | None = DEFAULT_MAX_PARTS, + max_parts: Optional[int] = DEFAULT_MAX_PARTS, max_field_size: int = DEFAULT_MAX_FIELD_SIZE, max_memory_file_size: int = DEFAULT_MAX_MEMORY_FILE_SIZE, max_part_header_bytes: int = DEFAULT_MAX_PART_HEADER_BYTES, @@ -342,12 +348,12 @@ class MultipartParser: self._tempdir = tempfile.gettempdir() # Current part state - self.current_headers: dict[str, str] = {} - self.current_file: tempfile.SpooledTemporaryFile | None = None + self.current_headers: Dict[str, str] = {} + self.current_file: Optional[tempfile.SpooledTemporaryFile] = None self.current_body = bytearray() - self.current_name: str | None = None - self.current_filename: str | None = None - self.current_content_type: str | None = None + self.current_name: Optional[str] = None + self.current_filename: Optional[str] = None + self.current_content_type: Optional[str] = None def feed(self, chunk: bytes) -> None: """Feed a chunk of data to the parser.""" @@ -358,13 +364,6 @@ class MultipartParser: self.buffer.extend(chunk) self._process() - def close(self) -> None: - """Discard completed uploads and any file still being received.""" - if self.current_file is not None: - self.current_file.close() - self.current_file = None - self.form_data.close() - def _process(self) -> None: """Process buffered data.""" while True: @@ -455,7 +454,7 @@ class MultipartParser: # Parse header try: line_str = line.decode("utf-8", errors="replace") - except UnicodeDecodeError: + except Exception: line_str = line.decode("latin-1") if ":" in line_str: @@ -482,9 +481,7 @@ class MultipartParser: if self.file_count > self.max_files: raise MultipartParseError("Too many files") if self.handle_files: - # Outlives this method - it is filled in across parser callbacks - # and then handed to the UploadedFile the caller consumes - self.current_file = tempfile.SpooledTemporaryFile( # noqa: SIM115 + self.current_file = tempfile.SpooledTemporaryFile( max_size=self.max_memory_file_size ) else: @@ -584,9 +581,6 @@ class MultipartParser: def _finish_part(self) -> None: """Finalize current part and add to form data.""" if self.current_name is None: - if self.current_file is not None: - self.current_file.close() - self.current_file = None return if self.current_filename is not None: @@ -650,7 +644,7 @@ async def parse_form_data( max_request_size: int = DEFAULT_MAX_REQUEST_SIZE, max_fields: int = DEFAULT_MAX_FIELDS, max_files: int = DEFAULT_MAX_FILES, - max_parts: int | None = DEFAULT_MAX_PARTS, + max_parts: Optional[int] = DEFAULT_MAX_PARTS, max_field_size: int = DEFAULT_MAX_FIELD_SIZE, max_memory_file_size: int = DEFAULT_MAX_MEMORY_FILE_SIZE, max_part_header_bytes: int = DEFAULT_MAX_PART_HEADER_BYTES, @@ -732,50 +726,29 @@ async def parse_form_data( batch_target = 64 * 1024 batch = bytearray() - async def run_parser(fn, *args): - # Cancellation must not close files while a worker is using them. - task = asyncio.create_task(asyncio.to_thread(fn, *args)) - try: - return await asyncio.shield(task) - except asyncio.CancelledError as cancelled: - try: - while not task.done(): - try: - await asyncio.shield(task) - except asyncio.CancelledError: - continue - task.result() - finally: - raise cancelled - async def flush_batch() -> None: if batch: data = bytes(batch) batch.clear() - await run_parser(parser.feed, data) + await asyncio.to_thread(parser.feed, data) - try: - while True: - message = await receive() - message_type = message.get("type") - if message_type == "http.disconnect": - raise MultipartParseError("Client disconnected during request body") - if message_type is not None and message_type != "http.request": - continue - chunk = message.get("body", b"") - if chunk: - batch.extend(chunk) - if len(batch) >= batch_target: - await flush_batch() - if not message.get("more_body", False): - break + while True: + message = await receive() + message_type = message.get("type") + if message_type == "http.disconnect": + raise MultipartParseError("Client disconnected during request body") + if message_type is not None and message_type != "http.request": + continue + chunk = message.get("body", b"") + if chunk: + batch.extend(chunk) + if len(batch) >= batch_target: + await flush_batch() + if not message.get("more_body", False): + break - await flush_batch() - return await run_parser(parser.finalize) - except BaseException: - # No FormData is returned to the caller to take ownership on failure. - await asyncio.to_thread(parser.close) - raise + await flush_batch() + return await asyncio.to_thread(parser.finalize) else: raise MultipartParseError( diff --git a/datasette/utils/permissions.py b/datasette/utils/permissions.py index 5a8ee8e2..fd1e41a1 100644 --- a/datasette/utils/permissions.py +++ b/datasette/utils/permissions.py @@ -2,9 +2,8 @@ from __future__ import annotations import json +from typing import Any, Dict, Iterable, List, Sequence, Tuple import sqlite3 -from collections.abc import Iterable, Sequence -from typing import Any from datasette.permissions import PermissionSQL from datasette.plugins import pm @@ -16,7 +15,7 @@ SKIP_PERMISSION_CHECKS = object() async def gather_permission_sql_from_hooks( *, datasette, actor: dict | None, action: str -) -> list[PermissionSQL] | object: +) -> List[PermissionSQL] | object: """Collect PermissionSQL objects from the permission_resources_sql hook. Ensures that each returned PermissionSQL has a populated ``source``. @@ -35,7 +34,7 @@ async def gather_permission_sql_from_hooks( hookimpls = hook_caller.get_hookimpls() hook_results = list(hook_caller(datasette=datasette, actor=actor, action=action)) - collected: list[PermissionSQL] = [] + collected: List[PermissionSQL] = [] actor_json = json.dumps(actor) if actor is not None else None actor_id = actor.get("id") if isinstance(actor, dict) else None @@ -72,7 +71,7 @@ def _iter_permission_sql_from_result( if isinstance(result, PermissionSQL): return [result] if isinstance(result, (list, tuple)): - collected: list[PermissionSQL] = [] + collected: List[PermissionSQL] = [] for item in result: collected.extend(_iter_permission_sql_from_result(item, action=action)) return collected @@ -91,7 +90,7 @@ def _iter_permission_sql_from_result( def build_rules_union( actor: dict | None, plugins: Sequence[PermissionSQL] -) -> tuple[str, dict[str, Any]]: +) -> Tuple[str, Dict[str, Any]]: """ Compose plugin SQL into a UNION ALL. @@ -103,10 +102,10 @@ def build_rules_union( The system reserves these parameter names: :actor, :actor_id, :action, :filter_parent Plugin parameters should be prefixed with a unique identifier (e.g., source name). """ - parts: list[str] = [] + parts: List[str] = [] actor_json = json.dumps(actor) if actor else None actor_id = actor.get("id") if actor else None - params: dict[str, Any] = {"actor": actor_json, "actor_id": actor_id} + params: Dict[str, Any] = {"actor": actor_json, "actor_id": actor_id} for p in plugins: # No namespacing - just use plugin params as-is @@ -142,10 +141,10 @@ async def resolve_permissions_from_catalog( plugins: Sequence[Any], action: str, candidate_sql: str, - candidate_params: dict[str, Any] | None = None, + candidate_params: Dict[str, Any] | None = None, *, implicit_deny: bool = True, -) -> list[dict[str, Any]]: +) -> List[Dict[str, Any]]: """ Resolve permissions by embedding the provided *candidate_sql* in a CTE. @@ -169,8 +168,8 @@ async def resolve_permissions_from_catalog( - parent, child, allow, reason, source_plugin, depth - resource (rendered "/parent/child" or "/parent" or "/") """ - resolved_plugins: list[PermissionSQL] = [] - restriction_sqls: list[str] = [] + resolved_plugins: List[PermissionSQL] = [] + restriction_sqls: List[str] = [] for plugin in plugins: if callable(plugin) and not isinstance(plugin, PermissionSQL): @@ -399,11 +398,11 @@ async def resolve_permissions_with_candidates( db, actor: dict | None, plugins: Sequence[Any], - candidates: list[tuple[str, str | None]], + candidates: List[Tuple[str, str | None]], action: str, *, implicit_deny: bool = True, -) -> list[dict[str, Any]]: +) -> List[Dict[str, Any]]: """ Resolve permissions without any external candidate table by embedding the candidates as a UNION of parameterized SELECTs in a CTE. @@ -412,8 +411,8 @@ async def resolve_permissions_with_candidates( actor: actor dict (or None), made available as :actor (JSON), :actor_id, and :action """ # Build a small CTE for candidates. - cand_rows_sql: list[str] = [] - cand_params: dict[str, Any] = {} + cand_rows_sql: List[str] = [] + cand_params: Dict[str, Any] = {} for i, (parent, child) in enumerate(candidates): pkey = f"cand_p_{i}" ckey = f"cand_c_{i}" diff --git a/datasette/utils/shutil_backport.py b/datasette/utils/shutil_backport.py index d323f5d6..d1fd1bd7 100644 --- a/datasette/utils/shutil_backport.py +++ b/datasette/utils/shutil_backport.py @@ -6,7 +6,7 @@ https://github.com/python/cpython/blob/v3.8.3/LICENSE """ import os -from shutil import Error, copy, copy2, copystat +from shutil import copy, copy2, copystat, Error def _copytree( diff --git a/datasette/utils/sql_analysis.py b/datasette/utils/sql_analysis.py index 6325e890..1be28982 100644 --- a/datasette/utils/sql_analysis.py +++ b/datasette/utils/sql_analysis.py @@ -1,8 +1,6 @@ -import sys from dataclasses import dataclass from typing import Literal -from datasette.utils import escape_sqlite from datasette.utils.sqlite import SQLiteTableType, sqlite3, sqlite_table_type SQLOperation = Literal[ @@ -197,16 +195,6 @@ def _allow_authorizer_action(*args): return sqlite3.SQLITE_OK -def _disable_authorizer(conn): - # Python 3.11 added support for unregistering an authorizer using None. - # On Python 3.10, None is installed as the callback instead, and the next - # statement fails with "not authorized" when sqlite3 tries to call it. - if sys.version_info >= (3, 11): - conn.set_authorizer(None) - else: - conn.set_authorizer(_allow_authorizer_action) - - def analyze_sql_tables( conn, sql: str, @@ -220,9 +208,7 @@ def analyze_sql_tables( This function is synchronous and connection-based. It temporarily installs a SQLite authorizer, prepares ``EXPLAIN ``, and returns the operation - callbacks observed while SQLite compiles the statement. ``CREATE VIEW`` is - additionally executed inside a rolled-back savepoint so its source-table reads - can be discovered by analyzing a query against the temporary view. + callbacks observed while SQLite compiles the statement. """ operations: dict[OperationKey, set[str]] = {} @@ -427,12 +413,12 @@ def analyze_sql_tables( database=None, table=None, sqlite_schema=sqlite_schema, - target=f"{arg1} {arg2}" if arg2 is not None else arg1, + target="{} {}".format(arg1, arg2) if arg2 is not None else arg1, source=source, ) return sqlite3.SQLITE_OK - action_name = _AUTHORIZER_ACTION_NAMES.get(action, f"SQLITE_{action}") + action_name = _AUTHORIZER_ACTION_NAMES.get(action, "SQLITE_{}".format(action)) record( "unknown", "unknown", @@ -495,7 +481,7 @@ def analyze_sql_tables( conn, key.table, schema=key.sqlite_schema ) finally: - _disable_authorizer(conn) + conn.set_authorizer(None) has_schema_operation = any( key.target_type in {"table", "index", "view", "trigger", "virtual-table"} @@ -535,7 +521,9 @@ def analyze_sql_tables( and key.target in _SQLITE_INTERNAL_SCHEMA_FUNCTIONS ): return True - return bool(key_is_drop_table_delete(key)) + if key_is_drop_table_delete(key): + return True + return False def table_kind_for(key: OperationKey) -> SQLiteTableType | None: if ( @@ -546,7 +534,7 @@ def analyze_sql_tables( return None return table_kind_cache[(key.sqlite_schema, key.table)] - analysis = SQLAnalysis( + return SQLAnalysis( operations=tuple( Operation( operation=key.operation, @@ -563,58 +551,3 @@ def analyze_sql_tables( for key, columns in operations.items() ) ) - - # SQLite does not resolve the SELECT body of a view when preparing CREATE - # VIEW, so its authorizer does not report reads from the view's source - # tables. Temporarily create the view, analyze a query against it (which - # does resolve the body), then roll the schema change back. Database-level - # callers use an isolated writable connection for this analysis. - create_view_operations = tuple( - operation - for operation in analysis.operations - if operation.operation == "create" and operation.target_type == "view" - ) - if not create_view_operations: - return analysis - - savepoint = "datasette_analyze_create_view" - conn.execute(f"SAVEPOINT {savepoint}") - try: - conn.execute(sql, params if params is not None else {}) - dependency_reads = [] - for view_operation in create_view_operations: - if view_operation.sqlite_schema is None or view_operation.table is None: - raise sqlite3.OperationalError( - "Could not determine the created view name" - ) - quoted_schema = escape_sqlite(view_operation.sqlite_schema) - quoted_view = escape_sqlite(view_operation.table) - qualified_view = f"{quoted_schema}.{quoted_view}" - view_analysis = analyze_sql_tables( - conn, - f"SELECT * FROM {qualified_view}", - database_name=database_name, - schema_to_database=schema_to_database, - ) - dependency_reads.extend( - operation - for operation in view_analysis.operations - if operation.operation == "read" - and not ( - operation.sqlite_schema == view_operation.sqlite_schema - and operation.table == view_operation.table - ) - ) - finally: - conn.execute(f"ROLLBACK TO {savepoint}") - conn.execute(f"RELEASE {savepoint}") - - existing_operations = set(analysis.operations) - return SQLAnalysis( - operations=analysis.operations - + tuple( - operation - for operation in dependency_reads - if operation not in existing_operations - ) - ) diff --git a/datasette/utils/sqlite.py b/datasette/utils/sqlite.py index 2ae1be9b..4743ae4c 100644 --- a/datasette/utils/sqlite.py +++ b/datasette/utils/sqlite.py @@ -15,17 +15,8 @@ if hasattr(sqlite3, "enable_callback_tracebacks"): _cached_sqlite_version = None _cached_supports_returning = None SQLiteTableType = Literal["table", "view", "virtual", "shadow"] -_SQLITE_IDENTIFIER_RE = ( - r"""(?:"(?:[^"]|"")*"|'(?:[^']|'')*'|`(?:[^`]|``)*`|\[[^\]]*\]|[^\s.()'"`\[\]]+)""" -) _VIRTUAL_TABLE_MODULE_RE = re.compile( - r"^\s*CREATE\s+VIRTUAL\s+TABLE\b\s*(?:IF\s+NOT\s+EXISTS\s+)?" - + _SQLITE_IDENTIFIER_RE - + r"(?:\s*\.\s*" - + _SQLITE_IDENTIFIER_RE - + r")?\s*\bUSING\b\s*(" - + _SQLITE_IDENTIFIER_RE - + r")", + r"\bCREATE\s+VIRTUAL\s+TABLE\b.*?\bUSING\s+([^\s(]+)", re.IGNORECASE | re.DOTALL, ) _VIRTUAL_TABLE_SHADOW_SUFFIXES = { @@ -92,58 +83,24 @@ def sqlite_table_type( ) -> SQLiteTableType | None: if supports_table_list(): try: - # Use the "PRAGMA table_list" statement form rather than the - # pragma_table_list(...) table-valued function. The - # table-valued function is resolved like an ordinary relation - # name, so an attacker-created table or view literally named - # "pragma_table_list" can shadow it and spoof the reported - # type (e.g. claiming a virtual table is an ordinary table). - # The PRAGMA statement form is a distinct piece of SQL syntax - # that always invokes SQLite's built-in pragma, so it cannot - # be shadowed by a user-created relation. + query = "select type from pragma_table_list where name = ?" + params: tuple[str, ...] = (table,) if schema is not None: - query = f"PRAGMA {_quote_identifier(schema)}.table_list" - else: - query = "PRAGMA table_list" - cursor = conn.execute(query) - columns = [description[0] for description in cursor.description] - for row in cursor.fetchall(): - record = dict(zip(columns, row)) - if record.get("name") != table: - continue - if schema is not None and record.get("schema") != schema: - continue - row_type = record.get("type") - if row_type in {"table", "view", "virtual", "shadow"}: - return row_type + query += " and schema = ?" + params = (table, schema) + row = conn.execute(query, params).fetchone() + if row is not None and row[0] in {"table", "view", "virtual", "shadow"}: + return row[0] except sqlite3.DatabaseError: pass return _sqlite_table_type_from_schema(conn, table, schema=schema) -def check_structured_write_table(conn, table: str, *, allow_missing=False): - """Validate a row-write target on the connection that will perform the write.""" - # SQLite resolves identifiers case-insensitively. The create API must not - # treat a differently cased existing name as a missing table. - row = conn.execute( - "select name from main.sqlite_master where name = ? collate nocase " - "and type in ('table', 'view')", - (table,), - ).fetchone() - if row is None and allow_missing: - return - if row is not None and sqlite_table_type(conn, row[0]) == "table": - return - # Virtual table modules can interpret row writes as administrative operations. - # Their shadow tables are internal storage, not independently writable data. - raise ValueError("Structured writes require an ordinary table") - - def sqlite_hidden_table_names(conn, *, schema: str | None = "main") -> list[str]: schema_table = _sqlite_schema_table(schema) try: rows = conn.execute( - f"select name, sql from {schema_table} where type = 'table'" + "select name, sql from {} where type = 'table'".format(schema_table) ).fetchall() except sqlite3.DatabaseError: return [] @@ -161,63 +118,6 @@ def sqlite_hidden_table_names(conn, *, schema: str | None = "main") -> list[str] return sorted(hidden_tables) + content_fts_tables -def sqlite_derived_table_dependencies( - conn, *, schema: str | None = "main" -) -> dict[str, str]: - """Return implementation table -> logical/content table dependencies. - - ``PRAGMA table_list`` safely identifies virtual and shadow tables, but - does not report which virtual table owns a shadow table or which table is - named by an FTS ``content=`` option. Derive those relationships from - ``sqlite_master`` DDL and the documented shadow-table suffixes. - - Database errors propagate: failed discovery must not be mistaken for an - empty dependency map and cached as permission to skip inheritance. - """ - schema_table = _sqlite_schema_table(schema) - rows = conn.execute( - f"select name, sql from {schema_table} where type = 'table'" - ).fetchall() - - table_names = {row[0] for row in rows} - # SQLite identifiers fold ASCII letters only. - identifier_case = str.maketrans( - "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz" - ) - canonical_names = {name.translate(identifier_case): name for name in table_names} - dependencies = {} - for virtual_table, sql in rows: - module = _virtual_table_module(sql) - if module is None: - continue - - # SQLite's documented shadow tables are implementation details of - # their logical virtual table. - for suffix in _VIRTUAL_TABLE_SHADOW_SUFFIXES.get(module, ()): - shadow_table = virtual_table + suffix - if shadow_table in table_names: - dependencies[shadow_table] = virtual_table - - # An external-content FTS table can expose values fetched from its - # content table, so it must also depend on that table's permission. - if module in {"fts3", "fts4", "fts5"}: - content_table = _fts_external_content_table(sql) - if content_table: - dependencies[virtual_table] = content_table - - if module in {"fts5vocab", "fts4aux"}: - source = _fts_vocabulary_source(sql, module, schema or "main") - source = ( - canonical_names.get(source.translate(identifier_case)) - if source - else None - ) - # An unresolved source is itself derived, so the one-hop policy denies it. - dependencies[virtual_table] = source or virtual_table - - return dependencies - - def _sqlite_table_type_from_schema( conn, table: str, @@ -227,7 +127,7 @@ def _sqlite_table_type_from_schema( schema_table = _sqlite_schema_table(schema) try: row = conn.execute( - f"select type, sql from {schema_table} where name = ?", + "select type, sql from {} where name = ?".format(schema_table), (table,), ).fetchone() except sqlite3.DatabaseError: @@ -255,7 +155,7 @@ def _is_known_shadow_table( schema_table = _sqlite_schema_table(schema) try: rows = conn.execute( - f"select name, sql from {schema_table} where type = 'table'" + "select name, sql from {} where type = 'table'".format(schema_table) ).fetchall() except sqlite3.DatabaseError: return False @@ -274,7 +174,7 @@ def _sqlite_schema_table(schema: str | None) -> str: return "sqlite_master" if schema == "temp": return "sqlite_temp_master" - return f"{_quote_identifier(schema)}.sqlite_master" + return "{}.sqlite_master".format(_quote_identifier(schema)) def _quote_identifier(value: str) -> str: @@ -284,151 +184,10 @@ def _quote_identifier(value: str) -> str: def _virtual_table_module(sql: str | None) -> str | None: if not sql: return None - match = _VIRTUAL_TABLE_MODULE_RE.search(_strip_sql_comments(sql)) - if match is None: - return None - return _unquote_sql_value(match.group(1)).lower() - - -def _fts_external_content_table(sql: str | None) -> str | None: - """Extract the external ``content=`` table from an FTS declaration.""" - if not sql: - return None - sql = _strip_sql_comments(sql) match = _VIRTUAL_TABLE_MODULE_RE.search(sql) if match is None: return None - open_paren = sql.find("(", match.end()) - if open_paren == -1: - return None - close_paren = sql.rfind(")") - if close_paren <= open_paren: - return None - - for argument in _split_sql_arguments(sql[open_paren + 1 : close_paren]): - key, separator, value = argument.partition("=") - if not separator or key.strip().lower() != "content": - continue - return _unquote_sql_value(value.strip()) - return None - - -def _fts_vocabulary_source(sql: str, module: str, schema: str) -> str | None: - """Resolve a vocabulary source within the current SQLite schema. - - Cross-schema sources cannot be represented by the dependency map and - are conservatively left unresolved. - """ - sql = _strip_sql_comments(sql) - match = _VIRTUAL_TABLE_MODULE_RE.search(sql) - if match is None: - return None - start = sql.find("(", match.end()) - end = sql.rfind(")") - if start < 0 or end <= start: - return None - arguments = [ - _unquote_sql_value(arg.strip()) - for arg in _split_sql_arguments(sql[start + 1 : end]) - ] - expected = 2 if module == "fts5vocab" else 1 - if len(arguments) == expected: - return arguments[0] - if len(arguments) == expected + 1 and arguments[0].lower() == schema.lower(): - return arguments[1] - return None - - -def _split_sql_arguments(arguments: str) -> list[str]: - """Split comma-separated SQLite arguments without splitting quoted text.""" - parts = [] - start = 0 - quote = None - closing_quote = None - index = 0 - while index < len(arguments): - char = arguments[index] - if quote is None: - if char in {"'", '"', "`", "["}: - quote = char - closing_quote = "]" if char == "[" else char - elif char == ",": - parts.append(arguments[start:index]) - start = index + 1 - elif char == closing_quote: - # Single/double/backtick quoting escapes the delimiter by - # doubling it. Square-bracket identifiers do not. - if ( - quote != "[" - and index + 1 < len(arguments) - and arguments[index + 1] == closing_quote - ): - index += 1 - else: - quote = None - closing_quote = None - index += 1 - parts.append(arguments[start:]) - return parts - - -def _strip_sql_comments(sql: str) -> str: - """Remove SQLite comments while preserving quoted strings/identifiers.""" - output = [] - quote = None - closing_quote = None - index = 0 - while index < len(sql): - char = sql[index] - next_char = sql[index + 1] if index + 1 < len(sql) else "" - if quote is None: - if char in {"'", '"', "`", "["}: - quote = char - closing_quote = "]" if char == "[" else char - output.append(char) - elif char == "-" and next_char == "-": - index += 2 - while index < len(sql) and sql[index] not in "\r\n": - index += 1 - output.append(" ") - continue - elif char == "/" and next_char == "*": - index += 2 - while index + 1 < len(sql) and sql[index : index + 2] != "*/": - index += 1 - index = min(index + 2, len(sql)) - output.append(" ") - continue - else: - output.append(char) - else: - output.append(char) - if char == closing_quote: - if ( - quote != "[" - and index + 1 < len(sql) - and sql[index + 1] == closing_quote - ): - output.append(sql[index + 1]) - index += 1 - else: - quote = None - closing_quote = None - index += 1 - return "".join(output) - - -def _unquote_sql_value(value: str) -> str: - if len(value) < 2: - return value - pairs = {"'": "'", '"': '"', "`": "`", "[": "]"} - closing = pairs.get(value[0]) - if closing is None or value[-1] != closing: - return value - unquoted = value[1:-1] - if value[0] != "[": - unquoted = unquoted.replace(closing * 2, closing) - return unquoted + return match.group(1).strip("\"'[]`").lower() def _is_fts_content_virtual_table(sql: str | None) -> bool: diff --git a/datasette/utils/testing.py b/datasette/utils/testing.py index e0cb74a7..de7e94af 100644 --- a/datasette/utils/testing.py +++ b/datasette/utils/testing.py @@ -1,10 +1,9 @@ -import json -from urllib.parse import urlencode - from asgiref.sync import async_to_sync +from urllib.parse import urlencode +import json # These wrapper classes pre-date the introduction of -# datasette.client and httpx2 to Datasette. They could +# datasette.client and httpx to Datasette. They could # be removed if the Datasette tests are modified to # call datasette.client directly. diff --git a/datasette/version.py b/datasette/version.py index 64f28946..387144e9 100644 --- a/datasette/version.py +++ b/datasette/version.py @@ -1,2 +1,2 @@ -__version__ = "1.0a41" +__version__ = "1.0a36" __version_info__ = tuple(__version__.split(".")) diff --git a/datasette/views/__init__.py b/datasette/views/__init__.py index bac3b39e..ed7e175f 100644 --- a/datasette/views/__init__.py +++ b/datasette/views/__init__.py @@ -1,7 +1,7 @@ +from dataclasses import dataclass import dataclasses import types import typing -from dataclasses import dataclass @dataclass(frozen=True) @@ -74,14 +74,16 @@ class Context: extra_class = table_extra_registry.classes_by_name[name] except KeyError: raise KeyError( - f"{cls.__name__}.{name} is declared with from_extra() but there is no " - "registered extra of that name" + "{}.{} is declared with from_extra() but there is no " + "registered extra of that name".format(cls.__name__, name) ) if cls.extras_scope is not None and not extra_class.available_for( cls.extras_scope ): raise ValueError( - f"{cls.__name__}.{name} is declared with from_extra() but the {name} extra is " - f"not available for scope {cls.extras_scope}" + "{}.{} is declared with from_extra() but the {} extra is " + "not available for scope {}".format( + cls.__name__, name, name, cls.extras_scope + ) ) return extra_class.description or "" diff --git a/datasette/views/base.py b/datasette/views/base.py index 7262edb4..66e14a6d 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -2,20 +2,20 @@ import csv import hashlib import sys +from datasette.utils.asgi import Request from datasette.utils import ( + add_cors_headers, EscapeHtmlWriter, InvalidSql, LimitedWriter, - add_cors_headers, path_from_row_pks, path_with_format, sqlite3, ) from datasette.utils.asgi import ( AsgiStream, - BadRequest, - Request, Response, + BadRequest, ) @@ -35,7 +35,7 @@ class DatasetteError(Exception): self.error_dict = error_dict or {} self.status = status self.message_is_html = message_is_html - # Plain text used for JSON and CSV error responses when message is HTML + # Plain text used for JSON error responses when message is HTML self.plain_message = plain_message @@ -129,10 +129,12 @@ class BaseView: template = environment.select_template(templates) template_context = { **context, - "select_templates": [ - f"{'*' if template_name == template.name else ''}{template_name}" - for template_name in templates - ], + **{ + "select_templates": [ + f"{'*' if template_name == template.name else ''}{template_name}" + for template_name in templates + ], + }, } headers = {} if self.has_json_alternate: @@ -149,7 +151,9 @@ class BaseView: template_context["alternate_url_json"] = alternate_url_json headers.update( { - "Link": f'<{alternate_url_json}>; rel="alternate"; type="application/json+datasette"' + "Link": '<{}>; rel="alternate"; type="application/json+datasette"'.format( + alternate_url_json + ) } ) return Response.html( @@ -180,7 +184,9 @@ async def stream_csv(datasette, fetch_data, request, database): stream = request.args.get("_stream") # Do not calculate facets or counts: extra_parameters = [ - f"{key}=1" for key in ("_nofacet", "_nocount") if not request.args.get(key) + "{}=1".format(key) + for key in ("_nofacet", "_nocount") + if not request.args.get(key) ] if extra_parameters: # Replace request object with a new one with modified scope @@ -210,6 +216,9 @@ async def stream_csv(datasette, fetch_data, request, database): except (sqlite3.OperationalError, InvalidSql) as e: raise DatasetteError(str(e), title="Invalid SQL", status=400) + except sqlite3.OperationalError as e: + raise DatasetteError(str(e)) + except DatasetteError: raise @@ -316,9 +325,8 @@ async def stream_csv(datasette, fetch_data, request, database): else: new_row.append(cell) await writer.writerow(new_row) - except Exception as ex: # noqa: BLE001 - # Streaming CSV: report the error into the response body and stop - sys.stderr.write(f"Caught this error: {ex}\n") + except Exception as ex: + sys.stderr.write("Caught this error: {}\n".format(ex)) sys.stderr.flush() await r.write(str(ex)) return diff --git a/datasette/views/database.py b/datasette/views/database.py index da207601..0133289a 100644 --- a/datasette/views/database.py +++ b/datasette/views/database.py @@ -1,56 +1,52 @@ +from dataclasses import asdict, dataclass, field +from urllib.parse import parse_qsl, urlencode import asyncio import hashlib import itertools import json +import markupsafe import os import textwrap -from dataclasses import asdict, dataclass, field -from urllib.parse import parse_qsl, urlencode - -import markupsafe +from datasette.extras import extra_names_from_request, ExtraScope from datasette.database import QueryInterrupted -from datasette.extras import ExtraScope, extra_names_from_request -from datasette.plugins import pm from datasette.resources import DatabaseResource, QueryResource from datasette.stored_queries import StoredQuery, stored_query_to_dict +from datasette.write_sql import QueryWriteRejected from datasette.utils import ( - InvalidSql, add_cors_headers, await_me_maybe, - call_with_supported_arguments, error_body, + call_with_supported_arguments, + named_parameters as derive_named_parameters, format_bytes, - is_url, make_slot_function, + tilde_decode, + to_css_class, + validate_sql_select, + is_url, path_with_added_args, path_with_format, path_with_removed_args, sqlite3, - tilde_decode, - to_css_class, truncate_url, - validate_sql_select, + InvalidSql, ) -from datasette.utils import ( - named_parameters as derive_named_parameters, -) -from datasette.utils.asgi import AsgiFileDownload, Forbidden, NotFound, Response -from datasette.write_sql import QueryWriteRejected +from datasette.utils.asgi import AsgiFileDownload, NotFound, Response, Forbidden +from datasette.plugins import pm -from . import Context from .base import DatasetteError, View, stream_csv from .query_helpers import ( - _block_framing, _ensure_stored_query_execution_permissions, - _table_columns, + _editor_schema, ) -from .table_create_alter import _create_table_ui_context from .table_extras import ( QueryExtraContext, resolve_query_extras, table_extra_registry, ) +from .table_create_alter import _create_table_ui_context +from . import Context @dataclass @@ -107,7 +103,7 @@ class DatabaseView(View): return response if format_ not in ("html", "json"): - raise NotFound(f"Invalid format: {format_}") + raise NotFound("Invalid format: {}".format(format_)) metadata = await datasette.get_database_metadata(database) @@ -171,7 +167,7 @@ class DatabaseView(View): "label": "Create table", "description": "Create a new table in this database.", "attrs": { - "aria-label": f"Create table in {database}", + "aria-label": "Create table in {}".format(database), "data-database-action": "create-table", }, } @@ -208,7 +204,7 @@ class DatabaseView(View): "queries_count": queries_count, "allow_execute_sql": allow_execute_sql, "table_columns": ( - await _table_columns(datasette, database) if allow_execute_sql else {} + await _editor_schema(datasette, database) if allow_execute_sql else {} ), "metadata": await datasette.get_database_metadata(database), } @@ -249,7 +245,7 @@ class DatabaseView(View): queries_count=queries_count, allow_execute_sql=allow_execute_sql, table_columns=( - await _table_columns(datasette, database) + await _editor_schema(datasette, database) if allow_execute_sql else {} ), @@ -278,7 +274,9 @@ class DatabaseView(View): view_name="database", ), headers={ - "Link": f'<{alternate_url_json}>; rel="alternate"; type="application/json+datasette"' + "Link": '<{}>; rel="alternate"; type="application/json+datasette"'.format( + alternate_url_json + ) }, ) @@ -459,6 +457,11 @@ class QueryContext(Context): "help": "Dictionary mapping table names to lists of column names, used to power SQL autocomplete." } ) + default_table: str = field( + metadata={ + "help": "Name of the focal table for this query, if any - set when the query page was reached from a table-scoped context (such as the table page's 'View and edit SQL' link) so the SQL editor can complete that table's columns unprefixed. ``None`` otherwise, including for stored/canned queries." + } + ) alternate_url_json: str = field( metadata={"help": "URL for alternate JSON version of this page"} ) @@ -561,7 +564,7 @@ async def database_download(request, datasette): if datasette.cors: add_cors_headers(headers) if db.hash: - etag = f'"{db.hash}"' + etag = '"{}"'.format(db.hash) headers["Etag"] = etag # Has user seen this already? if_none_match = request.headers.get("if-none-match") @@ -648,15 +651,8 @@ class QueryView(View): ok = None redirect_url = None try: - execute_write_kwargs = {"request": request} - if stored_query.is_trusted: - analysis = await db.analyze_sql(stored_query.sql, params_for_query) - if any( - operation.operation == "vacuum" for operation in analysis.operations - ): - execute_write_kwargs["transaction"] = False cursor = await db.execute_write( - stored_query.sql, params_for_query, **execute_write_kwargs + stored_query.sql, params_for_query, request=request ) # success message can come from on_success_message or on_success_message_sql message = None @@ -669,9 +665,8 @@ class QueryView(View): ).first() if message_result: message = message_result[0] - except Exception as ex: # noqa: BLE001 - # Stored-query on_success_message_sql is user-authored - message = f"Error running on_success_message_sql: {ex}" + except Exception as ex: + message = "Error running on_success_message_sql: {}".format(ex) message_type = datasette.ERROR if not message: if stored_query.on_success_message: @@ -685,8 +680,7 @@ class QueryView(View): redirect_url = stored_query.on_success_redirect ok = True - except Exception as ex: # noqa: BLE001 - # Stored-query execution is user-authored SQL + except Exception as ex: message = stored_query.on_error_message or str(ex) message_type = datasette.ERROR redirect_url = stored_query.on_error_redirect @@ -727,6 +721,15 @@ class QueryView(View): # Create lookup dict for quick access allowed_dict = {r.child: r for r in allowed_tables_page.resources} + # If the request carries a ?_table= pointing at a real (visible) table + # or view in this database, treat this as a table-scoped query - e.g. + # arriving here via the "View and edit SQL" link on a table page - so + # the SQL editor can offer that table's columns unprefixed. Anything + # else (including stored/canned queries, which may reference more + # than one table) leaves this as None. + requested_table = request.args.get("_table") + default_table = requested_table if requested_table in allowed_dict else None + # Are we a stored query? stored_query = None stored_query_write = False @@ -820,16 +823,16 @@ class QueryView(View): rows = results.rows except QueryInterrupted as ex: raise DatasetteError( - textwrap.dedent(f""" + textwrap.dedent("""

SQL query took too long. The time limit is controlled by the sql_time_limit_ms configuration option.

- + - """).strip(), + """.format(markupsafe.escape(ex.sql))).strip(), title="SQL Interrupted", status=400, message_is_html=True, @@ -845,6 +848,8 @@ class QueryView(View): columns = [] except (sqlite3.OperationalError, InvalidSql) as ex: raise DatasetteError(str(ex), title="Invalid SQL", status=400) + except sqlite3.OperationalError as ex: + raise DatasetteError(str(ex)) except DatasetteError: raise @@ -861,13 +866,12 @@ class QueryView(View): raise DatasetteError("?sql= is required", status=400) async def fetch_data_for_csv(request, _next=None): - # Reuse the trusted magic parameter values prepared above. - results = await db.execute(sql, params_for_query, truncate=True) + results = await db.execute(sql, params, truncate=True) data = {"rows": results.rows, "columns": results.columns} return data, None, None return await stream_csv(datasette, fetch_data_for_csv, request, db.name) - elif format_ in datasette.renderers: + elif format_ in datasette.renderers.keys(): if not sql: raise DatasetteError("?sql= is required", status=400) data = {"ok": True, "rows": rows, "columns": columns} @@ -959,7 +963,9 @@ class QueryView(View): } headers.update( { - "Link": f'<{alternate_url_json}>; rel="alternate"; type="application/json+datasette"' + "Link": '<{}>; rel="alternate"; type="application/json+datasette"'.format( + alternate_url_json + ) } ) metadata = await query_metadata() @@ -1040,7 +1046,9 @@ class QueryView(View): + "?" + urlencode( { - "sql": sql, + **{ + "sql": sql, + }, **named_parameter_values, } ) @@ -1103,10 +1111,11 @@ class QueryView(View): datasette, database, request, rows, columns ), table_columns=( - await _table_columns(datasette, database) + await _editor_schema(datasette, database) if allow_execute_sql else {} ), + default_table=default_table, columns=columns, renderers=renderers, url_csv=datasette.urls.path( @@ -1142,11 +1151,9 @@ class QueryView(View): headers=headers, ) else: - assert False, f"Invalid format: {format_}" + assert False, "Invalid format: {}".format(format_) if datasette.cors: add_cors_headers(r.headers) - if stored_query_write and format_ == "html": - _block_framing(r) return r @@ -1245,7 +1252,7 @@ async def display_rows(datasette, database, request, rows, columns): '<Binary: {:,} byte{}>'.format( blob_url, ( - f' title="{formatted}"' + ' title="{}"'.format(formatted) if "bytes" not in formatted else "" ), diff --git a/datasette/views/execute_write.py b/datasette/views/execute_write.py index c4f5e3fe..f51b1f45 100644 --- a/datasette/views/execute_write.py +++ b/datasette/views/execute_write.py @@ -1,7 +1,6 @@ import re from urllib.parse import urlencode -from datasette.database import QueryInterrupted from datasette.resources import DatabaseResource from datasette.utils import UNSTABLE_API_MESSAGE, sqlite3 from datasette.utils.asgi import Response @@ -9,8 +8,8 @@ from datasette.utils.asgi import Response from .base import BaseView from .database import display_rows as display_query_rows from .query_helpers import ( - SQL_PARAMETER_FORM_PREFIX, QueryValidationError, + SQL_PARAMETER_FORM_PREFIX, _analysis_is_write, _analysis_rows, _analysis_rows_with_permissions, @@ -22,6 +21,7 @@ from .query_helpers import ( _inserted_row_url, _json_or_form_payload, _prepare_execute_write, + _editor_schema, _table_columns, _wants_json, ) @@ -32,7 +32,15 @@ WRITE_TEMPLATE_LABELS = { "delete": "Delete rows", } WRITE_TEMPLATE_OPERATIONS = tuple(WRITE_TEMPLATE_LABELS) -CREATE_TABLE_TEMPLATE_SQL = "create table new_table (\n id integer primary key,\n name text\n -- created text default (datetime('now'))\n)" +CREATE_TABLE_TEMPLATE_SQL = "\n".join( + ( + "create table new_table (", + " id integer primary key,", + " name text", + " -- created text default (datetime('now'))", + ")", + ) +) def _parameter_names(columns): @@ -42,11 +50,11 @@ def _parameter_names(columns): base = re.sub(r"[^a-z0-9_]+", "_", column.lower()) base = base.strip("_") or "value" if base[0].isdigit(): - base = f"p_{base}" + base = "p_{}".format(base) name = base index = 2 while name in seen: - name = f"{base}_{index}" + name = "{}_{}".format(base, index) index += 1 seen.add(name) names[column] = name @@ -58,7 +66,7 @@ def _quote_identifier(identifier): def _preferred_where_column(table, columns): - lower_table_id = f"{table.lower()}_id" + lower_table_id = "{}_id".format(table.lower()) return ( next((column for column in columns if column.lower() == "id"), None) or next( @@ -83,15 +91,17 @@ def _insert_template_sql(table, columns): auto_pk = _auto_incrementing_primary_key(columns) insert_columns = [column for column in column_names if column != auto_pk] if not insert_columns: - return f"insert into {_quote_identifier(table)}\ndefault values" + return "insert into {}\ndefault values".format(_quote_identifier(table)) names = _parameter_names(insert_columns) return "\n".join( ( - f"insert into {_quote_identifier(table)} (", - ",\n".join(f" {_quote_identifier(column)}" for column in insert_columns), + "insert into {} (".format(_quote_identifier(table)), + ",\n".join( + " {}".format(_quote_identifier(column)) for column in insert_columns + ), ")", "values (", - ",\n".join(f" :{names[column]}" for column in insert_columns), + ",\n".join(" :{}".format(names[column]) for column in insert_columns), ")", ) ) @@ -105,14 +115,18 @@ def _update_template_sql(table, columns): if not set_columns: return "\n".join( ( - f"update {_quote_identifier(table)}", - f"set {_quote_identifier(where_column)} = :new_{names[where_column]}", - f"where {_quote_identifier(where_column)} = :{names[where_column]}", + "update {}".format(_quote_identifier(table)), + "set {} = :new_{}".format( + _quote_identifier(where_column), names[where_column] + ), + "where {} = :{}".format( + _quote_identifier(where_column), names[where_column] + ), ) ) return "\n".join( ( - f"update {_quote_identifier(table)}", + "update {}".format(_quote_identifier(table)), "set " + ",\n".join( "{}{} = :{}".format( @@ -122,7 +136,9 @@ def _update_template_sql(table, columns): ) for index, column in enumerate(set_columns) ), - f"where {_quote_identifier(where_column)} = :{names[where_column]}", + "where {} = :{}".format( + _quote_identifier(where_column), names[where_column] + ), ) ) @@ -133,8 +149,10 @@ def _delete_template_sql(table, columns): where_column = _preferred_where_column(table, column_names) return "\n".join( ( - f"delete from {_quote_identifier(table)}", - f"where {_quote_identifier(where_column)} = :{names[where_column]}", + "delete from {}".format(_quote_identifier(table)), + "where {} = :{}".format( + _quote_identifier(where_column), names[where_column] + ), ) ) @@ -249,6 +267,7 @@ class ExecuteWriteView(BaseView): write_template_tables = await _write_template_tables( self.ds, db, table_columns, hidden_table_names, request.actor ) + editor_schema = await _editor_schema(self.ds, db.name) write_template_operations = _write_template_operations(write_template_tables) write_create_table_template_sql = await _create_table_template_sql( self.ds, db, request.actor @@ -311,7 +330,7 @@ class ExecuteWriteView(BaseView): "sql_parameter_name_prefix": SQL_PARAMETER_FORM_PREFIX, "execute_disabled": bool(execute_disabled_reason), "execute_disabled_reason": execute_disabled_reason, - "table_columns": table_columns, + "table_columns": editor_schema, "write_template_tables": write_template_tables, "write_template_operations": write_template_operations, "write_create_table_template_sql": write_create_table_template_sql, @@ -385,7 +404,7 @@ class ExecuteWriteView(BaseView): try: execute_write_kwargs = {"request": request} cursor = await db.execute_write(sql, params, **execute_write_kwargs) - except (QueryInterrupted, sqlite3.DatabaseError) as ex: + except sqlite3.DatabaseError as ex: message = str(ex) if wants_json: return _block_framing(Response.error([message], 400)) diff --git a/datasette/views/index.py b/datasette/views/index.py index f73ee38a..67296cd1 100644 --- a/datasette/views/index.py +++ b/datasette/views/index.py @@ -2,11 +2,11 @@ import json from datasette.plugins import pm from datasette.utils import ( - UNSTABLE_API_MESSAGE, - CustomJSONEncoder, add_cors_headers, await_me_maybe, make_slot_function, + CustomJSONEncoder, + UNSTABLE_API_MESSAGE, ) from datasette.utils.asgi import Response from datasette.version import __version__ @@ -46,15 +46,15 @@ class IndexView(BaseView): databases = [] # Iterate over allowed databases instead of all databases - for name, allowed_db in allowed_db_dict.items(): + for name in allowed_db_dict.keys(): db = self.ds.databases[name] - database_private = allowed_db.private + database_private = allowed_db_dict[name].private # Get allowed tables/views for this database allowed_for_db = tables_by_db.get(name, {}) # Get table names from allowed set instead of db.table_names() - table_names = [child_name for child_name in allowed_for_db] + table_names = [child_name for child_name in allowed_for_db.keys()] hidden_table_names = set(await db.hidden_table_names()) @@ -99,7 +99,7 @@ class IndexView(BaseView): # We will be sorting by number of relationships, so populate that field all_foreign_keys = await db.get_all_foreign_keys() for table, foreign_keys in all_foreign_keys.items(): - if table in tables: + if table in tables.keys(): count = len(foreign_keys["incoming"] + foreign_keys["outgoing"]) tables[table]["num_relationships_for_sorting"] = count @@ -121,7 +121,8 @@ class IndexView(BaseView): # Only add views if this is less than TRUNCATE_AT if len(tables_and_views_truncated) < TRUNCATE_AT: num_views_to_add = TRUNCATE_AT - len(tables_and_views_truncated) - tables_and_views_truncated.extend(views[:num_views_to_add]) + for view in views[:num_views_to_add]: + tables_and_views_truncated.append(view) databases.append( { diff --git a/datasette/views/query_helpers.py b/datasette/views/query_helpers.py index 725d9cdb..014026f5 100644 --- a/datasette/views/query_helpers.py +++ b/datasette/views/query_helpers.py @@ -5,19 +5,6 @@ from datasette.resources import DatabaseResource from datasette.stored_queries import ( StoredQuery, ) -from datasette.utils import ( - InvalidSql, - escape_sqlite, - parse_size_limit, - path_from_row_pks, - sqlite3, - validate_sql_select, -) -from datasette.utils import ( - named_parameters as derive_named_parameters, -) -from datasette.utils.asgi import Forbidden -from datasette.utils.sql_analysis import Operation, SQLAnalysis from datasette.write_sql import ( IgnoreWriteSqlOperation, QueryWriteRejected, @@ -25,6 +12,17 @@ from datasette.write_sql import ( decision_for_write_sql_operation, operation_is_write, ) +from datasette.utils import ( + parse_size_limit, + named_parameters as derive_named_parameters, + escape_sqlite, + path_from_row_pks, + sqlite3, + validate_sql_select, + InvalidSql, +) +from datasette.utils.asgi import Forbidden +from datasette.utils.sql_analysis import Operation, SQLAnalysis _query_name_re = re.compile(r"^[^/\.\n]+$") @@ -93,7 +91,7 @@ def _as_optional_bool(value, name): return True if lowered in {"0", "false", "f", "no", "off"}: return False - raise QueryValidationError(f"{name} must be 0 or 1") + raise QueryValidationError("{} must be 0 or 1".format(name)) def _query_list_limit(value, default, maximum): @@ -173,7 +171,7 @@ async def _json_or_form_payload(request): try: return json.loads(body or b"{}"), True except json.JSONDecodeError as e: - raise QueryValidationError(f"Invalid JSON: {e}") + raise QueryValidationError("Invalid JSON: {}".format(e)) return await request.post_vars(), False @@ -194,7 +192,7 @@ async def _analyze_user_query(datasette, db, sql, *, actor): try: analysis = await db.analyze_sql(sql, params) except sqlite3.DatabaseError as ex: - raise QueryValidationError(f"Could not analyze query: {ex}") from ex + raise QueryValidationError("Could not analyze query: {}".format(ex)) from ex is_write = _analysis_is_write(analysis) if is_write: @@ -295,7 +293,8 @@ def _coerce_execute_write_payload(data, is_json): for key, value in data.items(): if key in {"sql", "csrftoken", "_json"}: continue - key = key.removeprefix(SQL_PARAMETER_FORM_PREFIX) + if key.startswith(SQL_PARAMETER_FORM_PREFIX): + key = key[len(SQL_PARAMETER_FORM_PREFIX) :] params[key] = value if not isinstance(params, dict): raise QueryValidationError("params must be a dictionary") @@ -315,7 +314,7 @@ async def _prepare_execute_write(datasette, db, sql, params, actor): try: analysis = await db.analyze_sql(sql, params) except sqlite3.DatabaseError as ex: - raise QueryValidationError(f"Could not analyze query: {ex}") from ex + raise QueryValidationError("Could not analyze query: {}".format(ex)) from ex if not _analysis_is_write(analysis): raise QueryValidationError( "Use /-/query for read-only SQL; this endpoint only executes writes" @@ -497,7 +496,7 @@ async def _inserted_row_url(datasette, db, analysis, cursor): ) try: result = await db.execute( - f"select {select} from {escape_sqlite(table)} where rowid = ?", + "select {} from {} where rowid = ?".format(select, escape_sqlite(table)), [lastrowid], ) except sqlite3.DatabaseError: @@ -635,3 +634,92 @@ async def _table_columns(datasette, database_name): for view_name in await db.view_names(): table_columns[view_name] = [] return table_columns + + +def _column_completion(name, type_): + # A @codemirror/lang-sql Completion object for a single column. boost keeps + # columns ranked above bare SQL keywords in the autocomplete popup. + completion = { + "label": name, + "type": "property", + "boost": 10, + } + if type_: + completion["detail"] = type_ + return completion + + +async def _schema_tables(datasette, database_name, *, include_hidden=True): + """ + Neutral introspection of a database's tables and views for SQL editors. + + Returns an ordered list of dicts, one per table or view:: + + {"name": str, "view": bool, + "columns": [{"name": str, "type": str}, ...]} + + ``type`` is the SQLite declared column type (empty string when the column + has no declared type). Regular-table columns come from the internal + ``catalog_columns`` catalog; views are absent from that catalog so their + columns are read directly via PRAGMA table_xinfo. Hidden tables (FTS shadow + tables and the like) are excluded unless ``include_hidden`` is True. This is + the shared, serialization-agnostic source for both ``_editor_schema`` (which + maps it to lang-sql Completion objects) and the ``/-/editor-schema.json`` + endpoint (which emits it directly). + """ + internal_db = datasette.get_internal_database() + result = await internal_db.execute( + "select table_name, name, type from catalog_columns where database_name = ?", + [database_name], + ) + table_columns = {} + for row in result.rows: + table_columns.setdefault(row["table_name"], []).append( + {"name": row["name"], "type": row["type"]} + ) + db = datasette.get_database(database_name) + hidden = set() if include_hidden else set(await db.hidden_table_names()) + tables = [] + for table_name, columns in table_columns.items(): + if table_name in hidden: + continue + tables.append({"name": table_name, "view": False, "columns": columns}) + # Views are not represented in catalog_columns, so pull their real columns + # directly (PRAGMA table_xinfo works against views too). + for view_name in await db.view_names(): + columns = [ + {"name": column.name, "type": column.type} + for column in await db.table_column_details(view_name) + ] + tables.append({"name": view_name, "view": True, "columns": columns}) + return tables + + +async def _editor_schema(datasette, database_name): + """ + Build a lang-sql SQLNamespace for the CodeMirror SQL editor autocomplete. + + Returns a dict keyed by table or view name. Table values are lists of + Completion objects (one per column, carrying the column's SQLite type as + ``detail``). Views are wrapped in a ``{"self": Completion, "children": [...]}`` + container so the popup can label them as views while still completing their + real columns. See @codemirror/lang-sql >= 6.6 SQLNamespace / Completion. + """ + schema = {} + for table in await _schema_tables(datasette, database_name, include_hidden=True): + completions = [ + _column_completion(column["name"], column["type"]) + for column in table["columns"] + ] + if table["view"]: + schema[table["name"]] = { + "self": { + "label": table["name"], + "type": "class", + "detail": "view", + }, + "children": completions, + } + else: + schema[table["name"]] = completions + return schema diff --git a/datasette/views/row.py b/datasette/views/row.py index 92e75199..c90a3bbe 100644 --- a/datasette/views/row.py +++ b/datasette/views/row.py @@ -8,37 +8,34 @@ from dataclasses import dataclass, field import markupsafe import sqlite_utils +from datasette.utils.asgi import NotFound, Forbidden, PayloadTooLarge, Response from datasette.database import QueryInterrupted -from datasette.events import DeleteRowEvent, UpdateRowEvent -from datasette.extras import ExtraScope, extra_names_from_request -from datasette.plugins import pm +from datasette.events import UpdateRowEvent, DeleteRowEvent from datasette.resources import TableResource +from .base import BaseView, DatasetteError, stream_csv from datasette.utils import ( - CustomJSONEncoder, - CustomRow, - InvalidSql, - WriteJsonValueError, add_cors_headers, await_me_maybe, call_with_supported_arguments, + CustomJSONEncoder, + CustomRow, decode_write_json_row, - escape_sqlite, + InvalidSql, make_slot_function, path_from_row_pks, path_with_format, path_with_removed_args, - sqlite3, - tilde_decode, to_css_class, + escape_sqlite, + sqlite3, + WriteJsonValueError, ) -from datasette.utils.asgi import Forbidden, NotFound, PayloadTooLarge, Response -from datasette.utils.sqlite import check_structured_write_table - +from datasette.plugins import pm +from datasette.extras import extra_names_from_request, ExtraScope from . import Context, from_extra -from .base import BaseView, DatasetteError, stream_csv from .table import ( - _table_page_data, display_columns_and_rows, + _table_page_data, row_label_from_label_column, ) from .table_extras import RowExtraContext, resolve_row_extras, table_extra_registry @@ -139,12 +136,6 @@ class RowContext(Context): ) -async def _database_and_table_resource_from_request(datasette, request): - db = await datasette.resolve_database(request) - table = tilde_decode(request.url_vars["table"]) - return db, table, TableResource(database=db.name, table=table) - - class RowView(BaseView): name = "row" @@ -196,16 +187,16 @@ class RowView(BaseView): data, extra_template_data, templates = response_or_template_contexts except QueryInterrupted as ex: raise DatasetteError( - textwrap.dedent(f""" + textwrap.dedent("""

SQL query took too long. The time limit is controlled by the sql_time_limit_ms configuration option.

- + - """).strip(), + """.format(markupsafe.escape(ex.sql))).strip(), title="SQL Interrupted", status=400, message_is_html=True, @@ -216,13 +207,15 @@ class RowView(BaseView): ) except (sqlite3.OperationalError, InvalidSql) as e: raise DatasetteError(str(e), title="Invalid SQL", status=400) + except sqlite3.OperationalError as e: + raise DatasetteError(str(e)) except DatasetteError: raise end = time.perf_counter() data["query_ms"] = (end - start) * 1000 - if format_ in self.ds.renderers: + if format_ in self.ds.renderers.keys(): # Dispatch request to the correct output format renderer # (CSV is not handled here due to streaming) result = call_with_supported_arguments( @@ -265,13 +258,13 @@ class RowView(BaseView): if status_code is not None: response.status = status_code else: - raise NotFound(f"Invalid format: {format_}") + raise NotFound("Invalid format: {}".format(format_)) ttl = request.args.get("_ttl", None) if ttl is None or not ttl.isdigit(): ttl = self.ds.setting("default_cache_ttl") - return self.set_response_headers(response, ttl, request) + return self.set_response_headers(response, ttl) async def html(self, request, data, extra_template_data, templates): extras = {} @@ -380,54 +373,42 @@ class RowView(BaseView): view_name=self.name, ), headers={ - "Link": f'<{alternate_url_json}>; rel="alternate"; type="application/json+datasette"' + "Link": '<{}>; rel="alternate"; type="application/json+datasette"'.format( + alternate_url_json + ) }, ) - def set_response_headers(self, response, ttl, request=None): - private = getattr(request, "_datasette_private_response", False) + def set_response_headers(self, response, ttl): # Set far-future cache expiry if self.ds.cache_headers and response.status == 200: - if private: - # This response is only visible to the current actor (denied - # to anonymous requests), so it must never be stored by a - # shared cache/CDN - and ?_ttl= must not override that. - response.headers["Cache-Control"] = "private, no-store" - response.headers["Vary"] = "Cookie" + ttl = int(ttl) + if ttl == 0: + ttl_header = "no-cache" else: - ttl = int(ttl) - if ttl == 0: - ttl_header = "no-cache" - else: - ttl_header = f"max-age={ttl}" - response.headers["Cache-Control"] = ttl_header + ttl_header = f"max-age={ttl}" + response.headers["Cache-Control"] = ttl_header response.headers["Referrer-Policy"] = "no-referrer" if self.ds.cors: add_cors_headers(response.headers) return response async def data(self, request, default_labels=False): - db, table, resource = await _database_and_table_resource_from_request( - self.ds, request - ) + resolved = await self.ds.resolve_row(request) + db = resolved.db database = db.name + table = resolved.table + pk_values = resolved.pk_values - # Check the URL resource before resolving the row, so a denied request - # cannot distinguish an existing primary key from a missing one. + # Ensure user has permission to view this row visible, private = await self.ds.check_visibility( request.actor, action="view-table", - resource=resource, + resource=TableResource(database=database, table=table), ) if not visible: raise Forbidden("You do not have permission to view this table") - # Record whether this response is private (visible to this actor - # only) so set_response_headers() can set appropriate Cache-Control - # headers, regardless of which output format ends up being rendered. - request._datasette_private_response = private - resolved = await self.ds.resolve_row(request) - pk_values = resolved.pk_values results = await resolved.db.execute( resolved.sql, resolved.params, truncate=True ) @@ -504,8 +485,8 @@ class RowView(BaseView): for row in display_rows: for cell in row: if cell["column"] in pk_set: - cell["value"] = markupsafe.Markup("{}").format( - cell["value"] + cell["value"] = markupsafe.Markup( + "{}".format(cell["value"]) ) label_column = await db.label_column_for_table(table) if is_table else None @@ -519,7 +500,7 @@ class RowView(BaseView): row_action_label = pk_path if row_label and row_label != pk_path: - row_action_label = f"{pk_path} {row_label}" + row_action_label = "{} {}".format(pk_path, row_label) row_action_permissions = {} if is_table and db.is_mutable: @@ -532,7 +513,7 @@ class RowView(BaseView): row_actions = [] if row_action_permissions.get("update-row"): attrs = { - "aria-label": f"Edit row {row_action_label}", + "aria-label": "Edit row {}".format(row_action_label), "data-row": row_path, "data-row-action": "edit", } @@ -548,7 +529,7 @@ class RowView(BaseView): ) if row_action_permissions.get("delete-row"): attrs = { - "aria-label": f"Delete row {row_action_label}", + "aria-label": "Delete row {}".format(row_action_label), "data-row": row_path, "data-row-action": "delete", } @@ -578,7 +559,7 @@ class RowView(BaseView): "private": private, "columns": reordered_columns, "foreign_key_tables": await self.foreign_key_tables( - database, table, pk_values, actor=request.actor + database, table, pk_values ), "database_color": db.color, "display_columns": display_columns, @@ -655,23 +636,12 @@ class RowView(BaseView): ), ) - async def foreign_key_tables(self, database, table, pk_values, *, actor): + async def foreign_key_tables(self, database, table, pk_values): if len(pk_values) != 1: return [] db = self.ds.databases[database] all_foreign_keys = await db.get_all_foreign_keys() - foreign_keys = [] - table_permissions = {} - for fk in all_foreign_keys[table]["incoming"]: - other_table = fk["other_table"] - if other_table not in table_permissions: - table_permissions[other_table] = await self.ds.allowed( - action="view-table", - resource=TableResource(database=database, table=other_table), - actor=actor, - ) - if table_permissions[other_table]: - foreign_keys.append(fk) + foreign_keys = all_foreign_keys[table]["incoming"] if len(foreign_keys) == 0: return [] @@ -709,7 +679,7 @@ class RowView(BaseView): key, ",".join(pk_values), ) - foreign_key_tables.append({**fk, "count": count, "link": link}) + foreign_key_tables.append({**fk, **{"count": count, "link": link}}) return foreign_key_tables @@ -728,57 +698,38 @@ def _truncated_row_flash_label(label): return label[: ROW_FLASH_LABEL_MAX_LENGTH - 1] + "\u2026" -async def _row_flash_message( - datasette, request, action, resolved, row=None, *, refresh_row=False -): +async def _row_flash_message(db, action, resolved, row=None): pk_label = ", ".join(resolved.pk_values) - # Mutation permission does not grant access to stored row labels. - if not await datasette.allowed( - action="view-table", - resource=TableResource(database=resolved.db.name, table=resolved.table), - actor=request.actor, - ): - return f"{action} row {pk_label}" - - if refresh_row and row is None: - results = await resolved.db.execute( - resolved.sql, resolved.params, truncate=True - ) - row = results.first() - label_column = await resolved.db.label_column_for_table(resolved.table) + label_column = await db.label_column_for_table(resolved.table) label = row_label_from_label_column(row or resolved.row, label_column) if label: label = _truncated_row_flash_label(label) if label and label != pk_label: - return f"{action} row {pk_label} ({label})" - return f"{action} row {pk_label}" + return "{} row {} ({})".format(action, pk_label, label) + return "{} row {}".format(action, pk_label) async def _resolve_row_and_check_permission(datasette, request, permission): - from datasette.app import DatabaseNotFound, RowNotFound, TableNotFound - - try: - _, _, resource = await _database_and_table_resource_from_request( - datasette, request - ) - except DatabaseNotFound as e: - return False, Response.error([f"Database not found: {e.database_name}"], 404) - - # Check the URL resource before resolving the row, so a denied request - # cannot distinguish an existing primary key from a missing one. - if not await datasette.allowed( - action=permission, - resource=resource, - actor=request.actor, - ): - return False, Response.error(["Permission denied"], 403) + from datasette.app import DatabaseNotFound, TableNotFound, RowNotFound try: resolved = await datasette.resolve_row(request) + except DatabaseNotFound as e: + return False, Response.error( + ["Database not found: {}".format(e.database_name)], 404 + ) except TableNotFound as e: - return False, Response.error([f"Table not found: {e.table}"], 404) + return False, Response.error(["Table not found: {}".format(e.table)], 404) except RowNotFound as e: - return False, Response.error([f"Record not found: {e.pk_values}"], 404) + return False, Response.error(["Record not found: {}".format(e.pk_values)], 404) + + # Ensure user has permission to delete this row + if not await datasette.allowed( + action=permission, + resource=TableResource(database=resolved.db.name, table=resolved.table), + actor=request.actor, + ): + return False, Response.error(["Permission denied"], 403) return True, resolved @@ -798,13 +749,11 @@ class RowDeleteView(BaseView): # Delete table def delete_row(conn): - check_structured_write_table(conn, resolved.table) sqlite_utils.Database(conn)[resolved.table].delete(resolved.pk_values) try: await resolved.db.execute_write_fn(delete_row, request=request) - except Exception as e: # noqa: BLE001 - # TODO: narrow to expected write errors so Datasette bugs surface as 500s + except Exception as e: return Response.error([str(e)], 400) await self.ds.track_event( @@ -820,7 +769,7 @@ class RowDeleteView(BaseView): table_url = self.ds.urls.table(resolved.db.name, resolved.table) self.ds.add_message( request, - await _row_flash_message(self.ds, request, "Deleted", resolved), + await _row_flash_message(resolved.db, "Deleted", resolved), self.ds.INFO, ) return Response.json({"ok": True, "redirect": str(table_url)}, status=200) @@ -844,7 +793,7 @@ class RowUpdateView(BaseView): try: data = await request.json() except json.JSONDecodeError as e: - return Response.error([f"Invalid JSON: {e}"]) + return Response.error(["Invalid JSON: {}".format(e)]) except PayloadTooLarge as e: return Response.error([str(e)], 413) @@ -881,27 +830,18 @@ class RowUpdateView(BaseView): return Response.error(["Permission denied for alter-table"], 403) def update_row(conn): - check_structured_write_table(conn, resolved.table) sqlite_utils.Database(conn)[resolved.table].update( resolved.pk_values, update, alter=alter ) try: await resolved.db.execute_write_fn(update_row, request=request) - except Exception as e: # noqa: BLE001 - # TODO: narrow to expected write errors so Datasette bugs surface as 500s + except Exception as e: return Response.error([str(e)], 400) result = {"ok": True} returned_row = None - # Only read back and disclose the stored row if the actor is also - # allowed to view this table - update-row alone must not be usable - # to read data the actor cannot otherwise see. - if data.get("return") and await self.ds.allowed( - action="view-table", - resource=TableResource(database=resolved.db.name, table=resolved.table), - actor=request.actor, - ): + if data.get("return"): results = await resolved.db.execute( resolved.sql, resolved.params, truncate=True ) @@ -918,15 +858,16 @@ class RowUpdateView(BaseView): ) if request.args.get("_message"): + message_row = returned_row + if message_row is None: + results = await resolved.db.execute( + resolved.sql, resolved.params, truncate=True + ) + message_row = results.first() self.ds.add_message( request, await _row_flash_message( - self.ds, - request, - "Updated", - resolved, - row=returned_row, - refresh_row=True, + resolved.db, "Updated", resolved, row=message_row ), self.ds.INFO, ) diff --git a/datasette/views/special.py b/datasette/views/special.py index 72e80316..c92ebc8f 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -1,25 +1,23 @@ import json import logging -import secrets -import urllib - -from datasette.events import CreateTokenEvent, LoginEvent, LogoutEvent from datasette.jump import JumpSQL, namespace_sql_params from datasette.plugins import pm +from datasette.events import LogoutEvent, LoginEvent, CreateTokenEvent from datasette.resources import DatabaseResource, TableResource +from datasette.utils.asgi import Response, Forbidden from datasette.utils import ( UNSTABLE_API_MESSAGE, actor_matches_allow, + parse_size_limit, add_cors_headers, await_me_maybe, error_body, - parse_size_limit, - tilde_decode, tilde_encode, + tilde_decode, ) -from datasette.utils.asgi import Forbidden, Response - from .base import BaseView, View +import secrets +import urllib logger = logging.getLogger(__name__) @@ -181,7 +179,9 @@ class AutocompleteDebugView(BaseView): ) context.update( { - "autocomplete_url": f"{self.ds.urls.table(database_name, table_name)}/-/autocomplete", + "autocomplete_url": "{}/-/autocomplete".format( + self.ds.urls.table(database_name, table_name) + ), "label_column": await db.label_column_for_table(table_name), } ) @@ -311,7 +311,6 @@ class AllowedResourcesView(BaseView): has_json_alternate = False async def get(self, request): - await self.ds.ensure_permission(action="view-instance", actor=request.actor) await self.ds.refresh_schemas() # Check if user has permissions-debug (to show sensitive fields) @@ -421,11 +420,8 @@ class AllowedResourcesView(BaseView): row["reason"] = resource.reasons allowed_rows.append(row) - except Exception: # noqa: BLE001 - # Returns empty results if the catalog tables don't exist yet, but - # also swallows the AttributeError raised for instance-level actions - # such as view-instance, which have no resource_class. - # TODO: handle that case explicitly and narrow this to sqlite3.Error + except Exception: + # If catalog tables don't exist yet, return empty results return ( { "ok": True, @@ -527,7 +523,7 @@ class PermissionRulesView(BaseView): from datasette.utils.actions_sql import build_permission_rules_sql - union_sql, union_params, _restriction_sqls = await build_permission_rules_sql( + union_sql, union_params, restriction_sqls = await build_permission_rules_sql( self.ds, actor, action ) await self.ds.refresh_schemas() @@ -604,7 +600,7 @@ class PermissionRulesView(BaseView): async def _check_permission_for_actor(ds, action, parent, child, actor): - """Shared logic for checking and explaining a permission decision.""" + """Shared logic for checking permissions. Returns a dict with check results.""" if action not in ds.actions: return error_body(f"Unknown action: {action}", 404), 404 @@ -633,28 +629,15 @@ async def _check_permission_for_actor(ds, action, parent, child, actor): allowed = await ds.allowed(action=action, resource=resource_obj, actor=actor) - from datasette.utils.actions_sql import explain_permission_for_resource - - explanation = await explain_permission_for_resource( - datasette=ds, - actor=actor, - action=action, - parent=parent, - child=child, - ) - response = { "ok": True, - "unstable": UNSTABLE_API_MESSAGE, "action": action, "allowed": bool(allowed), - "actor": actor, "resource": { "parent": parent, "child": child, "path": _resource_path(parent, child), }, - "explanation": explanation, } if actor and "id" in actor: @@ -672,25 +655,11 @@ class PermissionCheckView(BaseView): as_format = request.url_vars.get("format") if not as_format: - actions = [ - { - "name": action.name, - "description": action.description, - "takes_parent": action.takes_parent, - "takes_child": action.takes_child, - "also_requires": action.also_requires, - } - for action in sorted( - self.ds.actions.values(), key=lambda action: action.name - ) - ] return await self.render( ["debug_check.html"], request, { - "actions": actions, - "actor_json": request.args.get("actor") - or json.dumps(request.actor, indent=2), + "sorted_actions": sorted(self.ds.actions.keys()), "has_debug_permission": True, }, ) @@ -702,18 +671,9 @@ class PermissionCheckView(BaseView): parent = request.args.get("parent") child = request.args.get("child") - actor = request.actor - actor_json = request.args.get("actor") - if actor_json is not None: - try: - actor = json.loads(actor_json) - except json.JSONDecodeError as ex: - return Response.error(f"Invalid actor JSON: {ex}", 400) - if actor is not None and not isinstance(actor, dict): - return Response.error("actor must be a JSON object or null", 400) response, status = await _check_permission_for_actor( - self.ds, action, parent, child, actor + self.ds, action, parent, child, request.actor ) return Response.json(response, status=status) @@ -797,8 +757,6 @@ class CreateTokenView(BaseView): raise Forbidden( "Token authentication cannot be used to create additional tokens" ) - if "_r" in request.actor: - raise Forbidden("Restricted actors cannot create API tokens") async def shared(self, request): self.check_permission(request) @@ -876,11 +834,6 @@ class CreateTokenView(BaseView): else: errors.append("Invalid expire duration unit") - if errors: - context = await self.shared(request) - context["errors"] = errors - return await self.render(["create_token.html"], request, context) - # Are there any restrictions? from datasette.tokens import TokenRestrictions @@ -947,7 +900,7 @@ class ApiExplorerView(BaseView): tables.append({"name": table, "links": table_links}) table_links.append( { - "label": f"Get rows for {table}", + "label": "Get rows for {}".format(table), "method": "GET", "path": self.ds.urls.table(name, table, format="json"), } @@ -967,7 +920,7 @@ class ApiExplorerView(BaseView): { "path": self.ds.urls.table(name, table) + "/-/insert", "method": "POST", - "label": f"Insert rows into {table}", + "label": "Insert rows into {}".format(table), "json": { "rows": [ { @@ -981,7 +934,7 @@ class ApiExplorerView(BaseView): { "path": self.ds.urls.table(name, table) + "/-/upsert", "method": "POST", - "label": f"Upsert rows into {table}", + "label": "Upsert rows into {}".format(table), "json": { "rows": [ { @@ -1011,7 +964,7 @@ class ApiExplorerView(BaseView): table_links.append( { "path": self.ds.urls.table(name, table) + "/-/drop", - "label": f"Drop table {table}", + "label": "Drop table {}".format(table), "json": {"confirm": False}, "method": "POST", } @@ -1028,7 +981,7 @@ class ApiExplorerView(BaseView): database_links.append( { "path": self.ds.urls.database(name) + "/-/create", - "label": f"Create table in {name}", + "label": "Create table in {}".format(name), "json": { "table": "new_table", "columns": [ @@ -1269,21 +1222,14 @@ class SchemaBaseView(BaseView): has_json_alternate = False - async def get_database_schema(self, database_name, actor): + async def get_database_schema(self, database_name): """Get schema SQL for a database.""" db = self.ds.databases[database_name] - allowed_tables_page = await self.ds.allowed_resources( - "view-table", actor, parent=database_name - ) - allowed_table_names = { - resource.child async for resource in allowed_tables_page.all() - } result = await db.execute( - "select tbl_name, sql from sqlite_master where sql is not null" - ) - return ";\n".join( - row["sql"] for row in result.rows if row["tbl_name"] in allowed_table_names + "select group_concat(sql, ';' || CHAR(10)) as schema from sqlite_master where sql is not null" ) + row = result.first() + return row["schema"] if row and row["schema"] else "" def format_json_response(self, data): """Format data as JSON response with CORS headers if needed.""" @@ -1345,7 +1291,7 @@ class InstanceSchemaView(SchemaBaseView): # Get schema for each database schemas = [] for database_name in allowed_databases: - schema = await self.get_database_schema(database_name, request.actor) + schema = await self.get_database_schema(database_name) schemas.append({"database": database_name, "schema": schema}) if format_ == "json": @@ -1386,7 +1332,7 @@ class DatabaseSchemaView(SchemaBaseView): if database_name not in self.ds.databases: return self.format_error_response("Database not found", format_) - schema = await self.get_database_schema(database_name, request.actor) + schema = await self.get_database_schema(database_name) if format_ == "json": return self.format_json_response( @@ -1399,6 +1345,59 @@ class DatabaseSchemaView(SchemaBaseView): return await self.format_html_response(request, schemas) +class DatabaseEditorSchemaView(BaseView): + """ + JSON introspection of a database's tables, views and columns shaped for SQL + editor autocomplete consumers (the CodeMirror ```` + component and external clients such as datasette-paper). + + Distinct from :class:`DatabaseSchemaView` (``//-/schema.json``), which + returns the raw DDL as a SQL string gated on ``view-database`` alone. This + endpoint returns a neutral structured payload and is gated on both + ``view-database`` and ``execute-sql`` — the same permissions as the inline + editor schema handed to the SQL query page. + """ + + name = "database_editor_schema" + has_json_alternate = False + + async def get(self, request): + from .query_helpers import _schema_tables + + database_name = request.url_vars["database"] + + # view-database is checked first so actors without it cannot + # distinguish an existing database from a missing one, and a denied + # request only ever leaks the permission action name, never table names. + await self.ds.ensure_permission( + action="view-database", + resource=DatabaseResource(database=database_name), + actor=request.actor, + ) + if database_name not in self.ds.databases: + headers = {} + if self.ds.cors: + add_cors_headers(headers) + return Response.json( + error_body("Database not found", 404), status=404, headers=headers + ) + await self.ds.ensure_permission( + action="execute-sql", + resource=DatabaseResource(database=database_name), + actor=request.actor, + ) + + await self.ds.refresh_schemas() + tables = await _schema_tables(self.ds, database_name, include_hidden=False) + + headers = {} + if self.ds.cors: + add_cors_headers(headers) + return Response.json( + {"database": database_name, "tables": tables}, headers=headers + ) + + class TableSchemaView(SchemaBaseView): """ Displays schema for a specific table. @@ -1425,8 +1424,7 @@ class TableSchemaView(SchemaBaseView): # Get schema for the table db = self.ds.databases[database_name] result = await db.execute( - "select sql from sqlite_master where name = ? " - "and type in ('table', 'view') and sql is not null", + "select sql from sqlite_master where name = ? and sql is not null", [table_name], ) row = result.first() diff --git a/datasette/views/stored_queries.py b/datasette/views/stored_queries.py index 03bd9b29..d64f37d2 100644 --- a/datasette/views/stored_queries.py +++ b/datasette/views/stored_queries.py @@ -124,7 +124,7 @@ class QueryListView(BaseView): pairs.append(("_next", page.next)) next_url = self.ds.absolute_url( request, - f"{request.path}?{urlencode(pairs)}", + "{}?{}".format(request.path, urlencode(pairs)), ) current_filters = { @@ -279,7 +279,7 @@ class QueryCreateView(BaseView): ), ) response.status = status - return _block_framing(response) + return response async def get(self, request): db = await self.ds.resolve_database(request) @@ -415,7 +415,7 @@ class QueryDefinitionView(BaseView): query_name = tilde_decode(request.url_vars["query"]) query = await self.ds.get_query(db.name, query_name) if query is None: - return Response.error([f"Query not found: {query_name}"], 404) + return Response.error(["Query not found: {}".format(query_name)], 404) if not await self.ds.allowed( action="view-query", resource=QueryResource(db.name, query_name), @@ -439,7 +439,7 @@ class QueryUpdateView(BaseView): query_name = tilde_decode(request.url_vars["query"]) existing = await self.ds.get_query(db.name, query_name) if existing is None: - return Response.error([f"Query not found: {query_name}"], 404) + return Response.error(["Query not found: {}".format(query_name)], 404) if not await self.ds.allowed( action="update-query", resource=QueryResource(db.name, query_name), @@ -527,12 +527,12 @@ class QueryEditView(BaseView): ), ) response.status = status - return _block_framing(response) + return response async def get(self, request): db, query_name, existing = await self._load(request) if existing is None: - return Response.error([f"Query not found: {query_name}"], 404) + return Response.error(["Query not found: {}".format(query_name)], 404) await self.ds.ensure_permission( action="update-query", resource=QueryResource(db.name, query_name), @@ -545,7 +545,7 @@ class QueryEditView(BaseView): async def post(self, request): db, query_name, existing = await self._load(request) if existing is None: - return Response.error([f"Query not found: {query_name}"], 404) + return Response.error(["Query not found: {}".format(query_name)], 404) if not await self.ds.allowed( action="update-query", resource=QueryResource(db.name, query_name), @@ -629,7 +629,7 @@ class QueryDeleteView(BaseView): async def get(self, request): db, query_name, existing = await self._load(request) if existing is None: - return Response.error([f"Query not found: {query_name}"], 404) + return Response.error(["Query not found: {}".format(query_name)], 404) await self.ds.ensure_permission( action="delete-query", resource=QueryResource(db.name, query_name), @@ -639,23 +639,21 @@ class QueryDeleteView(BaseView): return Response.error( ["Trusted queries cannot be deleted using the API"], 403 ) - return _block_framing( - await self.render( - ["query_delete.html"], - request, - { - "database": db.name, - "database_color": db.color, - "query": stored_query_to_dict(existing), - "query_url": self.ds.urls.table(db.name, query_name), - }, - ) + return await self.render( + ["query_delete.html"], + request, + { + "database": db.name, + "database_color": db.color, + "query": stored_query_to_dict(existing), + "query_url": self.ds.urls.table(db.name, query_name), + }, ) async def post(self, request): db, query_name, existing = await self._load(request) if existing is None: - return Response.error([f"Query not found: {query_name}"], 404) + return Response.error(["Query not found: {}".format(query_name)], 404) if not await self.ds.allowed( action="delete-query", resource=QueryResource(db.name, query_name), @@ -667,13 +665,13 @@ class QueryDeleteView(BaseView): ["Trusted queries cannot be deleted using the API"], 403 ) - _data, is_json = await _json_or_form_payload(request) + data, is_json = await _json_or_form_payload(request) await self.ds.remove_query(db.name, query_name) if is_json: return Response.json({"ok": True}) self.ds.add_message( request, - f"Query “{existing.title or query_name}” deleted", + "Query “{}” deleted".format(existing.title or query_name), self.ds.INFO, ) return Response.redirect(self.ds.urls.path(self.ds.urls.database(db.name))) diff --git a/datasette/views/table.py b/datasette/views/table.py index 0a908c69..f7edd744 100644 --- a/datasette/views/table.py +++ b/datasette/views/table.py @@ -1,54 +1,50 @@ import asyncio import itertools import json -import time import urllib import urllib.parse -from dataclasses import dataclass, field import markupsafe -import sqlite_utils -from datasette import tracer from datasette.column_types import SQLiteType -from datasette.database import QueryInterrupted +from datasette.extras import extra_names_from_request +from datasette.plugins import pm from datasette.events import ( AlterTableEvent, DropTableEvent, InsertRowsEvent, UpsertRowsEvent, ) -from datasette.extras import ExtraScope, extra_names_from_request -from datasette.filters import Filters -from datasette.plugins import pm +from datasette.database import QueryInterrupted +from datasette import tracer from datasette.resources import DatabaseResource, TableResource from datasette.utils import ( - CustomJSONEncoder, - CustomRow, - InvalidSql, - WriteJsonValueError, add_cors_headers, - append_querystring, await_me_maybe, call_with_supported_arguments, + CustomJSONEncoder, + CustomRow, + append_querystring, compound_keys_after_sql, decode_write_json_rows, + format_bytes, + make_slot_function, + tilde_encode, escape_sqlite, filters_should_redirect, - format_bytes, is_url, - make_slot_function, path_from_row_pks, path_with_added_args, path_with_format, path_with_removed_args, path_with_replaced_args, - sqlite3, - tilde_encode, to_css_class, truncate_url, urlsafe_components, value_as_boolean, + InvalidSql, + WriteJsonValueError, + sqlite3, ) from datasette.utils.asgi import ( BadRequest, @@ -58,8 +54,11 @@ from datasette.utils.asgi import ( Request, Response, ) -from datasette.utils.sqlite import check_structured_write_table +from datasette.filters import Filters +import sqlite_utils +from dataclasses import dataclass, field +from datasette.extras import ExtraScope from . import Context, from_extra from .base import BaseView, DatasetteError, stream_csv from .database import QueryView @@ -537,7 +536,7 @@ async def _table_insert_ui( columns.append(column_data) data = { - "path": f"{datasette.urls.table(database_name, table_name)}/-/insert", + "path": "{}/-/insert".format(datasette.urls.table(database_name, table_name)), "tableName": table_name, "columns": columns, "bulkColumns": bulk_columns, @@ -545,8 +544,8 @@ async def _table_insert_ui( "maxInsertRows": datasette.setting("max_insert_rows"), } if can_update: - data["upsertPath"] = ( - f"{datasette.urls.table(database_name, table_name)}/-/upsert" + data["upsertPath"] = "{}/-/upsert".format( + datasette.urls.table(database_name, table_name) ) return data @@ -605,7 +604,7 @@ async def _table_alter_ui( columns.append(column_data) data = { - "path": f"{datasette.urls.table(database_name, table_name)}/-/alter", + "path": "{}/-/alter".format(datasette.urls.table(database_name, table_name)), "tableName": table_name, "columns": columns, "primaryKeys": pks, @@ -631,7 +630,9 @@ async def _table_alter_ui( actor=request.actor, ) if can_drop_table: - data["dropPath"] = f"{datasette.urls.table(database_name, table_name)}/-/drop" + data["dropPath"] = "{}/-/drop".format( + datasette.urls.table(database_name, table_name) + ) return data @@ -672,7 +673,7 @@ async def display_columns_and_rows( } pks = await db.primary_keys(table_name) pks_for_display = pks - if not pks_for_display and not await db.view_exists(table_name): + if not pks_for_display: pks_for_display = ["rowid"] label_column = None if link_column: @@ -727,10 +728,12 @@ async def display_columns_and_rows( row_label = row_label_from_label_column(row, label_column) row_action_label = pk_path if row_label and row_label != pk_path: - row_action_label = f"{pk_path} {row_label}" + row_action_label = "{} {}".format(pk_path, row_label) table_path = datasette.urls.table(database_name, table_name) - row_link = ( - f'{markupsafe.escape(pk_path)!s}' + row_link = '{flat_pks}'.format( + table_path=table_path, + flat_pks=str(markupsafe.escape(pk_path)), + flat_pks_quoted=row_path, ) edit_icon = ( '
[^\/\.]+)(\.(?P\w+))?$ - -.. [[[cog - from telemetry_doc import spans - spans(cog) -.. ]]] - -``{http.request.method} {http.route}`` - One span per HTTP request, containing spans from plugin middleware and database operations. Named for the HTTP method and matched route, or just the method if no route matches. Incoming ``traceparent`` headers are extracted using the global propagator to continue the caller's trace. Incoming ``baggage`` is not propagated into plugin or downstream context in this release. Set ``OTEL_PROPAGATORS=none`` to disable extraction. For public instances, strip trace context headers at your proxy if callers should not supply trace context. - - Kind: ``SERVER``. - - Attributes: - - - ``http.request.method`` - The HTTP request method. Methods outside the nine defined by RFC 9110 and RFC 5789 are recorded as ``_OTHER``. - - ``http.route`` *(optional)* - The regular expression for the matched route, for example ``/(?P[^\/\.]+)/(?P
[^\/\.]+)(\.(?P\w+))?$`` for a table page. Use this attribute to group requests by route. Omitted when no route matches. - - ``url.path`` - The URL path, excluding the query string. - - ``url.scheme`` - ``http`` or ``https``. - - ``server.address`` *(optional)* - The ``Host`` header, including any ``:port`` suffix. This value is supplied by the client. - - ``user_agent.original`` *(optional)* - The ``User-Agent`` header, verbatim. Omitted if the client sent none. - - ``http.response.status_code`` *(optional)* - The HTTP response status code. Omitted if no response was started. - - ``error.type`` *(optional)* - The exception class name for a failed operation. On HTTP spans, also set to the status code as a string for 5xx responses. A 4xx response alone does not set this attribute or an error status. - - ``datasette.internal_client`` *(optional)* - ``True`` for requests made through ``datasette.client``. Calls made inside another request produce a nested ``SERVER`` span. Filter on this attribute to exclude internal requests from request counts. Omitted for requests received over the network. - -``db.query`` - A SQL operation, including time spent queued for a worker thread. For ``block=False`` writes, the span ends after the write is queued. Callback methods record ``datasette.callback`` in place of ``db.query.text``. - - Kind: ``CLIENT``. - - Attributes: - - - ``db.system`` - Always ``sqlite``. - - ``db.namespace`` - Name of the database being queried. - - ``db.query.text`` *(optional)* - The SQL, truncated to 2048 characters. Bound parameter values are not recorded. For callback methods, ``datasette.callback`` is recorded instead. - - ``datasette.callback`` *(optional)* - The qualified name of the Python callable passed to ``execute_fn()``, ``execute_write_fn()`` or ``execute_isolated_fn()``, for example ``TableInsertView.post..insert_or_upsert_rows``. Set instead of ``db.query.text``. Lambdas appear as ````; use a named function for a more descriptive span. - - ``db.operation.name`` *(optional)* - The statement's leading keyword, such as ``SELECT``, ``INSERT`` or ``CREATE``, if it matches the supported allowlist. Statements beginning with a common table expression report ``WITH``. Omitted for unrecognized keywords and ``execute_write_script()``. - - ``datasette.param_count`` *(optional)* - Number of bound parameters. Recorded instead of the values themselves. - - ``datasette.param_sets`` *(optional)* - Number of parameter sets consumed by ``execute_write_many()``. The parameter values are not recorded. - - ``datasette.time_limit_ms`` *(optional)* - Time limit applied to the read query, in milliseconds: :ref:`setting_sql_time_limit_ms` or a shorter ``custom_time_limit``. - - ``datasette.rows_returned`` *(optional)* - Number of rows returned by a successful read query. - - ``datasette.truncated`` *(optional)* - True if the result was cut short by :ref:`setting_max_returned_rows`. - - ``datasette.interrupted`` *(optional)* - True if the query exceeded its time limit. The span status is set to ``ERROR`` unless the caller used a ``custom_time_limit`` shorter than :ref:`setting_sql_time_limit_ms`, in which case the status is left unset. - - ``datasette.sql_error_suppressed`` *(optional)* - True for a non-timeout SQL error with ``log_sql_errors=False``. The exception is still raised, but the span status is left unset. - - ``datasette.executescript`` *(optional)* - True for ``execute_write_script()``, which runs multiple statements. - - ``datasette.executemany`` *(optional)* - True for ``execute_write_many()``, which runs one statement against many parameter sets. - -``db.query.execute`` - The read executing inside a SQL worker thread. Child of ``db.query``; the gap between the two is time spent waiting for a thread. - - No attributes. - -``db.write.queue_wait`` - Time a write spent waiting in its database's write queue. For ``block=True``, this is a child of ``db.query``. For ``block=False``, it is a root span linked to the span that queued the write, since the write can outlive that request. - - No attributes. - -``db.write.execute`` - The write executing on the write thread. For ``block=True``, this is a child of ``db.query``. For ``block=False``, it is a root span linked to the span that queued the write. - - Attributes: - - - ``datasette.isolated_connection`` - True if the write ran on its own connection rather than the shared write connection. - - ``datasette.transaction`` - False for statements such as ``VACUUM`` that cannot run inside a transaction. - -``datasette.startup`` - Startup work performed by ``invoke_startup()``, including registration hooks, schema catalog updates, saved queries, column type configuration and the ``startup`` hook. Runs during instance startup, either before serving requests or as part of the first request. - - No attributes. - -.. [[[end]]] - -.. _internals_telemetry_metrics: - -Metric reference ----------------- - -Spans describe events; metrics describe levels and rates. Metrics can be used to answer questions like "Am I saturating my :ref:`setting_num_sql_threads` threads right now?". Trace sampling drops a portion of traces but does not drop any metrics. - -Datasette configures duration histograms in **seconds**. OpenTelemetry's default boundaries are tuned for milliseconds but these would file every SQLite query into a single bucket, making quantile queries meaningless. - -This reference is also generated from ``datasette/telemetry_registry.py``: - -.. [[[cog - from telemetry_doc import metrics - metrics(cog) -.. ]]] - -``db.client.operation.duration`` - Histogram, unit ``s``. Duration of a SQL operation, including callback-based calls such as ``execute_fn()``. For ``block=False`` writes, measures enqueue time. - - Bucket boundaries: ``0.0001``, ``0.0005``, ``0.001``, ``0.005``, ``0.01``, ``0.05``, ``0.1``, ``0.5``, ``1``, ``5``, ``10``. - - Attributes: - - - ``db.system`` - Always ``sqlite``. - - ``db.namespace`` - Name of the database being queried. - - ``datasette.operation`` - Whether the operation was a read or a write. One of: ``read``, ``write``. - - ``error.type`` *(optional)* - The exception class name for a failed operation. On HTTP spans, also set to the status code as a string for 5xx responses. A 4xx response alone does not set this attribute or an error status. - -``datasette.write.queue_wait`` - Histogram, unit ``s``. Time each write waited in its database's write queue. - - Bucket boundaries: ``0.0001``, ``0.0005``, ``0.001``, ``0.005``, ``0.01``, ``0.05``, ``0.1``, ``0.5``, ``1``, ``5``, ``10``. - - Attributes: - - - ``db.namespace`` - Name of the database being queried. - -``datasette.sql.queries.interrupted`` - Counter, unit ``{query}``. Queries cancelled for exceeding :ref:`setting_sql_time_limit_ms`. A rising rate can indicate that queries need optimization or a higher time limit. Caller-selected timeouts shorter than this limit, such as those used for facet suggestion, are excluded. - - Attributes: - - - ``db.namespace`` - Name of the database being queried. - -``datasette.sql.threads.limit`` - Observable gauge, unit ``{thread}``. Maximum concurrent read queries, configured by :ref:`setting_num_sql_threads`. Not reported when ``num_sql_threads`` is ``0``. - - No attributes. - -``datasette.sql.threads.queue_depth`` - Observable gauge, unit ``{query}``. Read queries waiting for a free SQL thread. Sustained values above zero indicate a saturated read pool. - - No attributes. - -``datasette.sql.queries.pending`` - Observable gauge, unit ``{query}``. Read queries submitted to the pool and not yet complete. Sum across databases and compare with ``datasette.sql.threads.limit`` to assess pool usage. - - Attributes: - - - ``db.namespace`` - Name of the database being queried. - -``datasette.write.queue_depth`` - Observable gauge, unit ``{write}``. Writes waiting for a database's single write thread. Increasing ``num_sql_threads`` does not increase write concurrency. Not reported for databases that have never been written to. - - Attributes: - - - ``db.namespace`` - Name of the database being queried. - -``datasette.connections.open`` - Observable gauge, unit ``{connection}``. Open SQLite connections managed by Datasette. - - Attributes: - - - ``db.namespace`` - Name of the database being queried. - -.. [[[end]]] - -Exemplars -~~~~~~~~~ - -An OpenTelemetry `exemplar `__ attaches a trace ID and span ID to one sample backing a histogram measurement. Where a spike in ``db.client.operation.duration`` alone tells you "queries were slow sometime in this minute", the exemplar attached to one of the samples in that spike gives you the trace ID of an actual slow query to open: - -.. code-block:: text - - db.client.operation.duration count=4 - exemplars: 4 - value=0.001564s trace_id=ddfaf45fd4e14913497d7efeac95f381 span_id=fd5792bdbb01e533 - value=0.006320s trace_id=34aea775ade11a3c5f716695731000fe span_id=25ed9e29dd84dbee - value=0.045253s trace_id=a65cb58d1460a179f0d04046ff51ed0d span_id=7f34d6378c85d062 - value=0.305240s trace_id=6089f4c515c221c0ca7bb53667b37ac8 span_id=0516f4a6641eaa0b - -.. _internals_telemetry_privacy: - -Privacy and safety ------------------- - -Datasette does not configure a telemetry exporter itself. If you enable one, traces may contain sensitive information: - -- **SQL text is truncated to 2048 characters.** Literal values in that text are retained. Bound SQL parameter values are not added as attributes; ``datasette.param_count`` records only their count. -- **Request spans include URL paths, host names and User-Agent headers.** Paths can include identifying values such as row primary keys. Core does not add actor identifiers, cookies, authorization headers, client IP addresses or a ``url.query`` attribute. -- **Exception messages and tracebacks may be recorded.** These can contain data from requests or database operations. - -Review what your application and plugins record before exporting telemetry to an external service. Restrict access to exported data and configure redaction or filtering where needed. - .. _internals_csrf: CSRF protection @@ -2769,20 +2332,7 @@ No token, cookie, or hidden form field is needed. Any ```` i Datasette's internal database ============================= -Datasette maintains an "internal" SQLite database used for configuration, caching, and storage. Plugins can store configuration, settings, and other data inside this database. By default, Datasette will use a temporary in-memory SQLite database as the internal database, which is created at startup and destroyed at shutdown. - -To persist internal data across Datasette instances, use the ``--internal`` option to specify the path to a SQLite database: - -.. code-block:: bash - - datasette mydatabase.db --internal internal.db - -You can also set the ``DATASETTE_INTERNAL`` environment variable to specify this path without passing ``--internal`` each time: - -.. code-block:: bash - - export DATASETTE_INTERNAL=/path/to/internal.db - datasette mydatabase.db +Datasette maintains an "internal" SQLite database used for configuration, caching, and storage. Plugins can store configuration, settings, and other data inside this database. By default, Datasette will use a temporary in-memory SQLite database as the internal database, which is created at startup and destroyed at shutdown. Users of Datasette can optionally pass in a ``--internal`` flag to specify the path to a SQLite database to use as the internal database, which will persist internal data across Datasette instances. Datasette maintains tables called ``catalog_databases``, ``catalog_tables``, ``catalog_views``, ``catalog_columns``, ``catalog_indexes``, ``catalog_foreign_keys`` with details of the attached databases and their schemas. These tables should not be considered a stable API - they may change between Datasette releases. @@ -3071,12 +2621,12 @@ This example uses trace to record the start, end and duration of any HTTP GET re .. code-block:: python from datasette.tracer import trace - import httpx2 + import httpx async def fetch_url(url): with trace("fetch-url", url=url): - async with httpx2.AsyncClient() as client: + async with httpx.AsyncClient() as client: return await client.get(url) .. _internals_tracer_trace_child_tasks: diff --git a/docs/introspection.rst b/docs/introspection.rst index 132f9edf..b78e4860 100644 --- a/docs/introspection.rst +++ b/docs/introspection.rst @@ -9,7 +9,7 @@ Each of these pages can be viewed in your browser. Add ``.json`` to the URL to g JSON responses that return an object include an ``"ok": true`` key, consistent with the rest of the :ref:`JSON API `. -The introspection endpoints documented on this page are covered by the :ref:`JSON API stability promise `, with the exception of the debug endpoints ``/-/threads``, ``/-/tasks`` and ``/-/actions``, whose shapes may change in future releases. +The introspection endpoints documented on this page are covered by the :ref:`JSON API stability promise `, with the exception of the debug endpoints ``/-/threads`` and ``/-/actions``, whose shapes may change in future releases. .. _JsonDataView_metadata: @@ -76,19 +76,22 @@ Shows the version of Datasette, Python and SQLite. `Versions example `_: +Shows a list of currently installed plugins and their versions. `Plugins example `_: .. code-block:: json - [ - { - "name": "datasette_cluster_map", - "static": true, - "templates": false, - "version": "0.10", - "hooks": ["extra_css_urls", "extra_js_urls", "extra_body_script"] - } - ] + { + "ok": true, + "plugins": [ + { + "name": "datasette_cluster_map", + "static": true, + "templates": false, + "version": "0.10", + "hooks": ["extra_css_urls", "extra_js_urls", "extra_body_script"] + } + ] + } Add ``?all=1`` to include details of the default plugins baked into Datasette. @@ -278,42 +281,6 @@ Shows details of threads and ``asyncio`` tasks. This endpoint requires the ``per ] } -.. _JsonDataView_tasks: - -/-/tasks --------- - -Shows the state of every supervised background task registered with :ref:`datasette.add_background_task() `; see also :ref:`BackgroundTask ` for what each field below means, and :ref:`datasette_lifecycle` for when tasks are launched. This endpoint requires the ``permissions-debug`` permission, since a crashed task's ``exception`` field can reveal internals such as file paths or query text: - -.. code-block:: json - - { - "ok": true, - "tasks": [ - { - "name": "my_plugin.poll_for_updates", - "state": "running", - "function": "my_plugin.poll_for_updates", - "started_at": "2026-07-30T12:00:00+00:00", - "exception": null - }, - { - "name": "my_plugin.broken_task", - "state": "crashed", - "function": "my_plugin.broken_task", - "started_at": "2026-07-30T12:00:00+00:00", - "exception": "ValueError('something went wrong')" - } - ], - "launched": true - } - -Each entry's ``function`` identifies the callable by its dotted module and qualified name. - -Each entry's ``state`` is one of ``registered`` (added but not yet launched), ``running``, ``completed``, ``crashed`` or ``cancelled``. ``exception`` is a one-line ``repr()`` of the exception for a ``crashed`` task, or ``null`` otherwise - the full traceback is written to the ``datasette.background_tasks`` logger instead, to keep this payload skimmable. - -The top-level ``launched`` flag reports whether the instance has run its one-time background task launch (after ``startup`` hooks finish, or via lifespan/first-request/:ref:`start_background_tasks() `). It distinguishes "no tasks have been registered" (``tasks`` is empty either way) from "tasks are registered but nothing has armed the launch yet" (``launched`` is ``false`` and every task's ``state`` is still ``registered``) - useful when debugging a host that never triggers Datasette's lifespan events. - .. _JsonDataView_actor: /-/actor diff --git a/docs/javascript_plugins.rst b/docs/javascript_plugins.rst index 159f952d..c4283cac 100644 --- a/docs/javascript_plugins.rst +++ b/docs/javascript_plugins.rst @@ -1,7 +1,7 @@ .. _javascript_plugins: -JavaScript in plugins -===================== +JavaScript plugins +================== Datasette can run custom JavaScript in several different ways: @@ -35,7 +35,7 @@ Your JavaScript code can listen out for this event using ``document.addEventList datasetteManager ---------------- -The ``datasetteManager`` object +The ``datasetteManager`` object ``VERSION`` - string The version of Datasette @@ -474,136 +474,6 @@ Custom fields are responsible for preserving the accessibility of the form: Plugins should not submit the row themselves from inside ``makeColumnField()`` controls. Datasette owns the insert/edit dialog lifecycle, form submission, API call, error handling and row refresh. -.. _javascript_plugins_modals: - -Reusable modal dialogs ----------------------- - -Plugins can use ``DatasetteModal`` to create dialogs with the same appearance and keyboard behavior as Datasette's built-in dialogs. The component provides a native modal dialog, shared styles, Escape and backdrop dismissal, busy-state dismissal guards and focus restoration. - -Creating a dialog -~~~~~~~~~~~~~~~~~ - -``DatasetteModal.create()`` returns a detached ```` element containing a native ````. Access that native element through ``modal.dialog``. Populate its content before appending the wrapper to the page, then call ``modal.show()`` to open it. - -This example uses the :ref:`datasette_init event ` to add a button that opens a dialog: - -.. literalinclude:: shots/modal-example.js - :language: javascript - -Clicking that button opens this dialog: - -.. only:: not latex - - .. image:: images/modal-example.webp - :width: 584px - :alt: A dialog titled Example dialog, with the text "This dialog uses Datasette's shared styles and keyboard behavior." and a Close button in the footer, shown in front of a dimmed Datasette page - -Opening and closing -~~~~~~~~~~~~~~~~~~~ - -``modal.show(options)`` - Opens the native dialog using ``showModal()``. ``options`` is an optional object with these optional properties: - - - ``returnFocusTo`` (DOM element): Focus returns to this element when the dialog closes. Defaults to the element with keyboard focus immediately before the dialog opens. - - ``initialFocus`` (DOM element or function): An element inside the dialog whose ``focus()`` method will be called, or a function called with no arguments that moves focus itself. - - Use this to focus on an input field when the dialog opens. - -``modal.close(options)`` - Closes the dialog directly. ``options`` is an optional object with one optional property: - - - ``restoreFocus`` (boolean): Whether closing returns focus to the element recorded by ``show()``. Defaults to ``true``. - -``modal.requestClose(source)`` - Alternative to ``.close()`` that requests dismissal through the busy-state and ``beforeClose`` guards described below. Returns ``true`` if it closes the dialog, or ``false`` if the dialog is already closed or a guard prevents dismissal. Close and Cancel buttons should use this method. - - ``source`` is an optional string that is passed to ``beforeClose`` and identifies what requested dismissal. Datasette supplies ``"escape"`` for the Escape key or a native cancel event and ``"backdrop"`` for a click outside the dialog. ``source`` defaults to ``"cancel"``. - -Listen for the native dialog's ``close`` event to clean up resources such as pending requests or custom fields: - -.. code-block:: javascript - - modal.dialog.addEventListener("close", () => { - // Clean up content-specific resources here. - }); - -If the dialog is no longer needed, remove the wrapper with ``modal.remove()``. - -Dismissal guards and busy state -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can set ``modal.beforeClose`` to a synchronous function that receives the ``source`` string described above and returns ``false`` in order to keep the dialog open. - -Use ``source`` to decide what to do. This example prompts the user to ask if they want to discard unsaved changes - for example if they click outside the modal or hit Escape - but doesn't prompt them if they clicked a button like the Close one above that sets the ``source`` string to ``cancel``. - -.. code-block:: javascript - - modal.beforeClose = (source) => { - if (source === "cancel") return true; - return confirm("Discard unsaved changes?"); - }; - - -Set ``modal.busy = true`` while saving to prevent user dismissal. While busy, ``requestClose()`` returns ``false`` without calling ``beforeClose``. - -If an operation fails, set ``modal.busy = false`` so the user can retry or close the dialog. A successful operation can call ``modal.close()`` even while busy. - -.. _javascript_plugins_modal_classes: - -Shared CSS classes -~~~~~~~~~~~~~~~~~~ - -The classes in the example above provide built-in styling. This dialog uses every class listed below, including a ``modal-meta`` count in the header and ``footer-info`` text next to ``modal-btn-ghost`` and ``modal-btn-primary`` buttons in the footer: - -.. only:: not latex - - .. image:: images/modal-classes.webp - :width: 584px - :alt: A dialog titled Export rows with a "3 selected" badge in its header, a list of three plant names in the body, and a footer containing the text "CSV, UTF-8", a Cancel button and a blue Export button - -The following classes can be used by your modal: - -``datasette-modal`` - Added automatically to the native ```` when the wrapper is connected to the page. Provides the dialog's sizing, background, rounded corners, shadow, backdrop and animations. - -``modal-header`` - Adds padding, a bottom border and a horizontal layout for the title and optional metadata. - -``modal-title`` - Sets the title's font size, weight and color. Use ``aria-labelledby`` to associate the title with the dialog. - -``modal-meta`` - Styles optional metadata, such as a selected-item count, as small monospace text with a rounded background. - -``modal-body`` - Adds padding and makes overflowing content scroll while the header and footer remain visible. Sets ``min-height: 0``, ``overflow: auto`` and ``padding: 16px 24px 24px``. - -``modal-footer`` - Adds padding, a top border and a background to the action area. Arranges its contents horizontally, with buttons aligned to the right. - -``footer-info`` - Styles supporting text in the footer and lets it fill the space before the action buttons. - -``modal-btn`` - Provides base button styling, including padding, rounded corners, font and disabled appearance. Use it together with ``modal-btn-primary`` or ``modal-btn-ghost``. - -``modal-btn-primary`` - Gives a button an accent-colored background and white text, suitable for a primary action such as Save. - -``modal-btn-ghost`` - Gives a button a transparent background, muted text and a border, suitable for a secondary action such as Close or Cancel. - -These button classes are also used by Datasette's built-in dialogs. - -You can customize layout and sizing without adding extra classes. For example, this CSS uses the dialog's existing ID to widen it while keeping it inside the viewport: - -.. code-block:: css - - dialog#my-plugin-dialog { - width: min(720px, calc(100vw - 32px)); - } - .. _javascript_datasette_manager_selectors: Selectors diff --git a/docs/json_api.rst b/docs/json_api.rst index 73212e70..8eeba631 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -48,7 +48,6 @@ Some JSON endpoints are **exempt** from this promise: debug playground. - Debug and support endpoints are documented so you can use them, but their JSON shapes are not frozen: :ref:`/-/threads `, - :ref:`/-/tasks `, :ref:`/-/actions `, the :ref:`permission debug endpoints ` (``/-/allowed``, ``/-/rules``, ``/-/check``) and the @@ -153,6 +152,62 @@ Values for named SQL parameters can be provided as additional query string param The response uses the same default representation described above. +.. _json_api_editor_schema: + +.. _DatabaseEditorSchemaView: + +Schema for SQL editors +---------------------- + +The ``/-/editor-schema.json`` endpoint returns a machine-readable description of +a database's tables, views and columns, shaped for SQL editor autocomplete. It +powers Datasette's own CodeMirror SQL editor and is available for external +consumers such as embeddable editor components. + +:: + + GET //-/editor-schema.json + +Access requires both the :ref:`actions_view_database` and +:ref:`actions_execute_sql` permissions for the database - the same gate as the +inline editor schema on the SQL query page. A request that fails either check +receives a ``403`` JSON error that does not reveal any table or column names. + +The response is a neutral structure - a ``database`` name and a list of +``tables``, each with a ``view`` flag (``true`` for SQL views) and a list of +``columns`` carrying the SQLite declared ``type`` (an empty string when the +column has no declared type): + +.. code-block:: json + + { + "database": "fixtures", + "tables": [ + { + "name": "facetable", + "view": false, + "columns": [ + {"name": "pk", "type": "INTEGER"}, + {"name": "state", "type": "TEXT"} + ] + }, + { + "name": "paginated_view", + "view": true, + "columns": [ + {"name": "content", "type": "TEXT"} + ] + } + ] + } + +Hidden tables - such as the shadow tables that back SQLite full-text search - +are excluded from the response. + +This endpoint is distinct from the :ref:`database schema endpoint ` +at ``//-/schema.json``, which returns the raw ``CREATE`` statements as +a SQL string. + .. _json_api_shapes: Different shapes @@ -1327,23 +1382,6 @@ The following extras are available for arbitrary SQL query responses and stored, .. [[[end]]] -.. _TableCountView: - -Counting all matching rows --------------------------- - -``POST //
/-/count`` returns an exact count of the rows matching the table's query string filters:: - - POST /fixtures/facetable/-/count?state=CA - - {"ok": true, "count": 10} - -The endpoint supports the same column, search and plugin filters as the table page. Pagination and display options such as ``_next``, ``_size`` and ``_sort`` do not affect the count. - -This requires ``view-table`` permission. ``execute-sql`` permission is only needed if using ``_where`` filters. - -Unlike the ``count`` extra, this count is not capped by the row count limit. The usual SQL time limit still applies; a timed-out count returns a 400 JSON error. - .. _TableAutocompleteView: Table autocomplete @@ -1679,8 +1717,6 @@ The request body is always parsed as JSON, regardless of the request's ``Content The row-based write APIs can write :ref:`binary values in JSON ` using Datasette's Base64 representation for BLOB data. -Structured inserts, upserts, updates and deletes only support ordinary SQLite tables. Virtual tables and their internal shadow tables are rejected, including when adding rows to an existing table through the create-table API. Writes to ordinary content tables can still update full-text search indexes through configured triggers. - .. _ExecuteWriteView: Executing write SQL diff --git a/docs/json_api_doc.py b/docs/json_api_doc.py index 9a4dba23..422e67f4 100644 --- a/docs/json_api_doc.py +++ b/docs/json_api_doc.py @@ -46,7 +46,7 @@ def table_extras(cog): cog.out("\n") for scope, heading, intro, classes in classes_by_scope: cog.out("{}\n{}\n\n".format(heading, "~" * len(heading))) - cog.out(f"{intro}\n\n") + cog.out("{}\n\n".format(intro)) for cls in classes: examples = _examples_for_scope(cls, scope) description = cls.description or "" @@ -58,16 +58,16 @@ def table_extras(cog): if notes: description = "{} ({})".format(description, " ".join(notes)).strip() - cog.out(f"``{cls.key()}``\n") - cog.out(f" {description}\n\n") + cog.out("``{}``\n".format(cls.key())) + cog.out(" {}\n\n".format(description)) for example in examples: if example.path: value = live_examples[(example.path, example.key or cls.key())] - cog.out(f" ``GET {example.path}``\n\n") + cog.out(" ``GET {}``\n\n".format(example.path)) else: value = example.value if example.note: - cog.out(f" {example.note}\n\n") + cog.out(" {}\n\n".format(example.note)) cog.out(" .. code-block:: json\n\n") cog.out(textwrap.indent(json.dumps(value, indent=2), " ")) cog.out("\n\n") @@ -139,7 +139,7 @@ async def _fetch_live_examples(scoped_classes): response = await datasette.client.get(example.path) assert response.status_code == 200, example.path data = response.json() - assert key in data, f"{key} missing from {example.path}" + assert key in data, "{} missing from {}".format(key, example.path) examples[(example.path, key)] = data[key] finally: for db in datasette.databases.values(): diff --git a/docs/metadata_doc.py b/docs/metadata_doc.py index 1bf17f8e..031b3ddd 100644 --- a/docs/metadata_doc.py +++ b/docs/metadata_doc.py @@ -1,8 +1,7 @@ import json import textwrap - -from ruamel.yaml import YAML from yaml import safe_dump +from ruamel.yaml import YAML def metadata_example(cog, data=None, yaml=None): @@ -34,10 +33,10 @@ def config_example( else: data = input output_yaml = safe_dump(input, sort_keys=False) - cog.out(f"\n.. tab:: {yaml_title}\n\n") + cog.out("\n.. tab:: {}\n\n".format(yaml_title)) cog.out(" .. code-block:: yaml\n\n") cog.out(textwrap.indent(output_yaml, " ")) - cog.out(f"\n\n.. tab:: {json_title}\n\n") + cog.out("\n\n.. tab:: {}\n\n".format(json_title)) cog.out(" .. code-block:: json\n\n") cog.out(textwrap.indent(json.dumps(data, indent=2), " ")) cog.out("\n") @@ -45,10 +44,8 @@ def config_example( def internal_schema(cog): import asyncio - - from sqlite_utils import Database - from datasette.app import Datasette + from sqlite_utils import Database ds = Datasette() db = ds.get_internal_database() diff --git a/docs/plugin_hooks.rst b/docs/plugin_hooks.rst index 3efff7a4..049cb292 100644 --- a/docs/plugin_hooks.rst +++ b/docs/plugin_hooks.rst @@ -217,7 +217,7 @@ Extra template variables that should be made available in the rendered template ``datasette`` - :ref:`internals_datasette` You can use this to access plugin configuration options via ``datasette.plugin_config(your_plugin_name)`` -This hook supports the following return values: +This hook can return one of three different types: Dictionary If you return a dictionary its keys and values will be merged into the template context. @@ -228,9 +228,6 @@ Function that returns a dictionary Function that returns an awaitable function that returns a dictionary You can also return a function which returns an awaitable function which returns a dictionary. -``None`` - The hook itself, or a function or awaitable it returns, can return ``None`` when no extra variables are needed. Variables returned by other plugins are still included. - Datasette runs Jinja2 in `async mode `__, which means you can add awaitable functions to the template scope and they will be automatically awaited when they are rendered by the template. .. warning:: @@ -257,6 +254,8 @@ This example returns an awaitable function which adds a list of ``hidden_table_n return { "hidden_table_names": await db.hidden_table_names() } + else: + return {} return hidden_table_names @@ -496,10 +495,10 @@ Lets you customize the display of values within table cells in the HTML table vi The name of the column being rendered ``table`` - string or None - The name of the table or view - or ``None`` if this is a custom SQL query + The name of the table - or ``None`` if this is a custom SQL query ``pks`` - list of strings - The primary key column names for the table being rendered. For tables without an explicitly defined primary key, this will be ``["rowid"]``. For custom SQL queries and views, this will be an empty list ``[]``. + The primary key column names for the table being rendered. For tables without an explicitly defined primary key, this will be ``["rowid"]``. For custom SQL queries and views (where ``table`` is ``None``), this will be an empty list ``[]``. ``database`` - string The name of the database @@ -1108,7 +1107,7 @@ Return an `ASGI `__ middleware wrapper function th This is a very powerful hook. You can use it to manipulate the entire Datasette response, or even to configure new URL routes that will be handled by your own custom code. -You can write your ASGI code directly against the low-level specification, or you can use the middleware utilities provided by an ASGI framework such as `Starlette `__. +You can write your ASGI code directly against the low-level specification, or you can use the middleware utilities provided by an ASGI framework such as `Starlette `__. This example plugin adds a ``x-databases`` HTTP header listing the currently attached databases: @@ -1158,7 +1157,7 @@ Examples: `datasette-cors `__, `dat startup(datasette) ------------------ -This hook fires when the Datasette application server first starts up. It runs on the same event loop that goes on to serve requests, so it is safe to create loop-bound primitives and register background work here — see :ref:`datasette_lifecycle` for the full guarantee and the three ways startup can be triggered. +This hook fires when the Datasette application server first starts up. Here is an example that validates required plugin configuration. The server will fail to start and show an error if the validation check fails: @@ -1196,7 +1195,6 @@ Potential use-cases: * Create database tables that a plugin needs on startup * Validate the configuration for a plugin on startup, and raise an error if it is invalid * Raise a ``datasette.utils.StartupError("message")`` exception to prevent Datasette from starting and display that message to the user. -* Register supervised long-lived background work using :ref:`datasette_add_background_task`, which core launches once every plugin's ``startup()`` hook has finished. .. note:: @@ -1213,31 +1211,6 @@ Potential use-cases: Examples: `datasette-saved-queries `__, `datasette-init `__ -.. _plugin_hook_shutdown: - -shutdown(datasette) -------------------- - -This hook fires once, when the Datasette application server is shutting down gracefully - triggered by the ASGI ``lifespan.shutdown`` event, which includes pressing Ctrl-C or sending ``SIGTERM`` to a ``datasette serve`` process. It is not called on a hard kill (``SIGKILL``), since there is no opportunity to run any code in that case. - -Like ``startup()``, this can be a regular function or it can return an async function to be awaited. - -It runs before Datasette cancels any background tasks it is supervising (see :ref:`datasette_add_background_task`) and before it closes its database connections, so you can use it to tell your plugin's own background work to stop gracefully while a database connection is still available to write out any final state. See :ref:`datasette_lifecycle` for exactly where this fits into the full startup-to-shutdown sequence: - -.. code-block:: python - - @hookimpl - def shutdown(datasette): - async def inner(): - db = datasette.get_database() - await db.execute_write( - "insert into shutdown_log (at) values (datetime('now'))" - ) - - return inner - -If your ``shutdown()`` hook raises an exception it will be logged but not re-raised, so one plugin's broken shutdown code cannot prevent other plugins - or Datasette itself - from finishing their own teardown. - .. _plugin_hook_actor_from_request: actor_from_request(datasette, request) diff --git a/docs/plugin_telemetry.rst b/docs/plugin_telemetry.rst deleted file mode 100644 index e4c7381d..00000000 --- a/docs/plugin_telemetry.rst +++ /dev/null @@ -1,270 +0,0 @@ -.. _plugin_telemetry: - -Telemetry for plugin authors -============================ - -Datasette core emits OpenTelemetry spans and metrics for the work it does itself - see :ref:`internals_telemetry` for what those are and how an operator turns them on. This page is about the other half: instrumenting the work **your plugin** does, so that a plugin's queries, background jobs and custom operations show up in the same traces and the same metrics pipeline, using the same conventions. - -.. _plugin_telemetry_scope: - -Use your own instrumentation scope ----------------------------------- - -Create a tracer and meter using your plugin's own instrumentation scope: - -.. code-block:: python - - from opentelemetry import metrics, trace - - from my_plugin import __version__ - - tracer = trace.get_tracer("my_plugin", __version__) - meter = metrics.get_meter("my_plugin", __version__) - -Use these naming rules: - -- **Scope**: use your plugin's import package name, such as ``my_plugin``. This lets users filter telemetry by plugin. -- **Signal prefix**: prefix spans, metrics and custom attributes with your package name (``my_plugin.*``) or a product name (``paper.*``). The ``datasette.*`` prefix is reserved for core. - -Reuse shared attribute names where they describe the same thing: ``db.namespace`` for a database name, or ``error.type`` for an exception class. - -If you pass ``schema_url=`` when creating a tracer or meter, choose the semantic-convention version that matches your attributes. Datasette's version is available as ``datasette.telemetry.SCHEMA_URL``. Omit ``schema_url`` if you are unsure which version applies. - -.. _plugin_telemetry_registry: - -Declare a registry ------------------- - -Use ``Attribute``, ``SpanName`` and ``MetricName`` from ``datasette.telemetry_registry`` to describe your plugin's telemetry. Registry entries are strings and can be passed directly to OpenTelemetry: - -.. code-block:: python - - from datasette.telemetry_registry import ( - Attribute, - MetricName, - SpanName, - ) - - OUTCOME = Attribute( - "my_plugin.outcome", - "How the job ended.", - values={"ok", "error", "skipped"}, - ) - JOB_NAME = Attribute( - "my_plugin.job", "The registered job name." - ) - - JOB_RUN = SpanName( - "my_plugin.job.run", - "One execution of a scheduled job.", - (OUTCOME, JOB_NAME), - ) - - # A span family with a variable suffix - emitted as "my_plugin.chat gpt-5" - CHAT = SpanName( - "my_plugin.chat ", - "One model call, named ``my_plugin.chat {model}``.", - prefix=True, - ) - - SPANS = (JOB_RUN, CHAT) - - JOB_DURATION = MetricName( - "my_plugin.job.duration", - "Histogram", - "s", - "How long each job took.", - (JOB_NAME, OUTCOME), - buckets=(0.01, 0.1, 1, 10, 60, 600, 3600), - ) - - METRICS = (JOB_DURATION,) - -The example uses these optional arguments: - -``values`` - iterable - Allowed values for an ``Attribute``. The :ref:`conformance helpers ` check that emitted values belong to this set. Omit it to allow any value. - -``prefix`` - boolean - For ``SpanName``, match emitted names by prefix. Defaults to ``False``. Exact names take precedence over prefix matches. Avoid overlapping prefixes: the first matching entry in the registry wins. - -``buckets`` - iterable - Histogram boundaries for a ``MetricName``, expressed in the metric's unit. Pass these to ``meter.create_histogram()`` using ``explicit_bucket_boundaries_advisory=JOB_DURATION.buckets``. Choose boundaries suitable for the operations you measure. For SQLite timings, ``datasette.telemetry_registry.DURATION_BUCKETS`` provides boundaries from 0.0001 to 10 seconds. - -.. _plugin_telemetry_privacy: - -Privacy and cardinality rules ------------------------------ - -Core does not explicitly attach bound SQL parameter values, actor identifiers, cookies, authorization headers, client IP addresses or URL query strings as attributes. It does record SQL text, URL paths, host names, User-Agent headers and exception details, which may contain sensitive information. See :ref:`internals_telemetry_privacy`. - -- Prefer closed enums, booleans, counts and durations for attribute values. Avoid recording personal information, tokens or other secrets. -- If you record SQL, use ``datasette.telemetry.sql_attribute()`` on spans only. It truncates SQL text but does not redact literal values. Do not add bound parameter values. -- Keep metric dimensions bounded. For user input or other unbounded values, record a count, a byte size, a truncation flag or an enum outcome instead. - -Use ``assert_no_forbidden_values()`` in :ref:`plugin_telemetry_testing` to check for specific sensitive values in captured telemetry. This helper does not automatically identify all sensitive information. - -.. _plugin_telemetry_callbacks: - -Your database work is already traced ------------------------------------- - -Every call your plugin makes through :ref:`db.execute() `, :ref:`db.execute_fn() `, :ref:`db.execute_write() ` and :ref:`db.execute_write_fn() ` already emits core's ``db.query`` spans and is counted in the ``db.client.operation.duration`` histogram. Two consequences: - -- **Pass named callables**, not lambdas: the span for a callback-style call is identified by ``datasette.callback``, the callable's qualified name, and a lambda reports ````. -- If you also wrap those calls in your own span or histogram, you are creating a *second* series in *your* scope - that is fine and sometimes right (yours can carry plugin-level attributes core cannot know), but it is a deliberate two-series design, not a substitute for core's. - -.. _plugin_telemetry_request_span: - -Enriching the request span --------------------------- - -Inside a view or ASGI middleware, ``datasette.telemetry.request_span(scope)`` returns the recording ``SERVER`` span for the current request, or ``None`` when nothing is recording - which is also your signal to skip any work done only to compute attributes: - -.. code-block:: python - - from datasette.telemetry import request_span - - - async def my_view(request): - span = request_span(request.scope) - if span is not None: - span.set_attribute("my_plugin.cache", "hit") - ... - -.. _plugin_telemetry_background: - -Background work: roots with links ---------------------------------- - -For background work that can outlive a request, create a root span linked to the span that scheduled it. Call ``linked_root_span_kwargs()`` when scheduling the work, then pass the result when starting its span. If there is no valid span context to capture, the new span has no link: - -.. code-block:: python - - from datasette.telemetry import linked_root_span_kwargs - - # Capture the current span when scheduling the work: - kwargs = linked_root_span_kwargs() - - # Later, wherever the work actually runs: - with tracer.start_as_current_span( - "my_plugin.job.run", **kwargs - ) as span: - span.set_attribute(OUTCOME, "ok") - -For periodic tasks, create a root span and increment a counter on each iteration, including iterations with no work. Record the result in an outcome attribute. A gauge reporting the time since the last iteration can help monitor tasks with long intervals. - -``asyncio.create_task()`` inherits the current trace context. Use ``linked_root_span_kwargs()`` to start background work with its own root span and a link to that context. - -Tracers and meters can be created at module scope. In embedded deployments, configure the application's providers before the work you want to record begins. - -.. _plugin_telemetry_gauges: - -Observable gauges ------------------ - -Use an observable gauge for current values such as the number of open streams or the length of a queue. The SDK calls its callback when collecting metrics: - -- Track live objects using weak references, such as a ``weakref.WeakSet``, and unregister them when they close. -- Callbacks may run on a different thread from request handlers. Protect shared state and avoid waiting on locks held by request handlers. -- Read cached state and yield ``Observation`` values. Keep callbacks synchronous and free of I/O. Refresh cached values outside the callback; use a separate gauge to report their age if needed. - -Without a provider, gauge callbacks are not invoked. - -.. _plugin_telemetry_testing: - -Testing your instrumentation ----------------------------- - -Use ``datasette.telemetry_testing`` to capture telemetry in your tests and check it against your registry. Add `opentelemetry-sdk `__ to your test dependencies, then import these fixtures in ``conftest.py``: - -.. code-block:: python - - from datasette.telemetry_testing import ( # noqa: F401 - otel_metrics, - otel_meter_provider, - otel_provider, - otel_reset, - otel_spans, - ) - -``otel_provider`` and ``otel_meter_provider`` - Automatically configure in-memory recording for spans and metrics once per test session. - -``otel_reset`` - Automatically clears recorded spans and drains collected metrics after every test. - -``otel_spans`` - Provides an ``InMemorySpanExporter``. Call ``get_finished_spans()`` to retrieve spans recorded during the test. - -``otel_metrics`` - Provides a metrics collector. Call ``collect()`` to capture a snapshot, then use ``point()`` or ``points()`` to inspect it. - -Tests requesting ``otel_spans`` or ``otel_metrics`` skip if the SDK is unavailable or another provider has already been installed. - -The assertion helpers check the recorded telemetry against your registry: - -``assert_spans_conform()`` - Checks that emitted spans and attributes are registered, and attribute values match any declared ``values=`` enums. - -``assert_metrics_conform()`` - Checks that emitted metrics and attributes are registered, attribute values match any declared enums, and instrument kinds and units match the registry. - -``assert_spans_covered()`` and ``assert_metrics_covered()`` - Check that every registered span or metric and its required attributes appeared during the test. Attributes marked ``optional=True`` are excluded from this check; test those separately. - -Pass your plugin's instrumentation scope as ``scope_name`` to these helpers, since the fixtures also record Datasette's own telemetry. - -Run a workload that exercises your instrumentation, then call ``otel_metrics.collect()`` once before checking the metrics. Counters and histograms report measurements since the previous collection. Keep the Datasette instance open until collection so observable gauges can report its state: - -.. code-block:: python - - from datasette.telemetry_testing import ( - assert_metrics_conform, - assert_metrics_covered, - assert_package_never_imports_sdk, - assert_spans_covered, - assert_spans_conform, - ) - - from my_plugin.telemetry import METRICS, SPANS - - - def test_api_only_dependency(): - assert_package_never_imports_sdk("my_plugin") - - - def test_conformance(otel_spans, otel_metrics): - run_a_workload_that_exercises_everything() - finished = otel_spans.get_finished_spans() - # Everything emitted is registered (and enum values are legal): - assert_spans_conform( - SPANS, finished, scope_name="my_plugin" - ) - # Everything registered was emitted: - assert_spans_covered( - SPANS, finished, scope_name="my_plugin" - ) - # Collect once, then check the metrics: - otel_metrics.collect() - assert_metrics_conform( - METRICS, otel_metrics, scope_name="my_plugin" - ) - assert_metrics_covered( - METRICS, otel_metrics, scope_name="my_plugin" - ) - -``assert_package_never_imports_sdk()`` checks that importing your plugin does not import the OpenTelemetry SDK. Run this test early in your suite; see the helper's docstring for a macOS threading limitation. - -Use ``assert_no_forbidden_values()`` to check for private data in telemetry. Include fake email addresses, tokens or usernames in your test workload, then pass those values, the finished spans and the collected metrics to the helper. It checks span names, attributes, events, status descriptions and metric attributes. - -Leave ``scope_name`` unset for privacy checks so they include both your plugin's telemetry and Datasette's own. - -.. _plugin_telemetry_caveats: - -Known caveats -------------- - -- **Streaming responses hold the request span open.** Core's request span ends when the response body finishes, so for an SSE or long-streaming route its duration is the connection lifetime. If you need per-message timing on a stream, emit your own child spans or span events per message, and use gauges for concurrent-stream counts. -- **A plugin timing core's work double-measures by design.** See :ref:`plugin_telemetry_callbacks` above. -- ``datasette.client`` requests made from inside a request produce a nested ``SERVER`` span. Those spans carry ``datasette.internal_client: true`` - filter on it to keep kind-based dashboards from double-counting requests. diff --git a/docs/plugins.rst b/docs/plugins.rst index 296ef55d..d32a9fe6 100644 --- a/docs/plugins.rst +++ b/docs/plugins.rst @@ -261,15 +261,6 @@ If you run ``datasette plugins --all`` it will include default plugins that ship "permission_resources_sql" ] }, - { - "name": "datasette.default_permissions.sqlite_statistics", - "static": false, - "templates": false, - "version": null, - "hooks": [ - "permission_resources_sql" - ] - }, { "name": "datasette.default_permissions.tokens", "static": false, diff --git a/docs/settings.rst b/docs/settings.rst index f3e6636d..9c114e4a 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -67,21 +67,10 @@ The following options can be set using ``--setting name value``, or by storing t default_allow_sql ~~~~~~~~~~~~~~~~~ -.. [[[cog - from settings_doc import setting_default - setting_default(cog, "default_allow_sql") -.. ]]] - -Default: ``on`` - -.. [[[end]]] - Should users be able to execute arbitrary SQL queries by default? Setting this to ``off`` causes permission checks for :ref:`actions_execute_sql` to fail by default. -This setting controls the ability to submit arbitrary SQL. It does not disable structured table-browsing features that use SQL generated by Datasette, such as sorting, column filters and :ref:`facets`. Use :ref:`setting_allow_facet` to control whether users can request facets. - :: datasette mydatabase.db --setting default_allow_sql off @@ -93,14 +82,6 @@ Another way to achieve this is to add ``"allow_sql": false`` to your ``datasette default_page_size ~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "default_page_size") -.. ]]] - -Default: ``100`` - -.. [[[end]]] - The default number of rows returned by the table page. You can over-ride this on a per-page basis using the ``?_size=80`` query string parameter, provided you do not specify a value higher than the ``max_returned_rows`` setting. You can set this default using ``--setting`` like so:: datasette mydatabase.db --setting default_page_size 50 @@ -110,15 +91,7 @@ The default number of rows returned by the table page. You can over-ride this on sql_time_limit_ms ~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "sql_time_limit_ms") -.. ]]] - -Default: ``1000`` - -.. [[[end]]] - -Time limit for SQL queries, in milliseconds. If a query takes longer than this to run Datasette will terminate the query and return an error. +By default, queries have a time limit of one second. If a query takes longer than this to run Datasette will terminate the query and return an error. If this time limit is too short for you, you can customize it using the ``sql_time_limit_ms`` limit - for example, to increase it to 3.5 seconds:: @@ -135,15 +108,7 @@ This would set the time limit to 100ms for that specific query. This feature is max_returned_rows ~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "max_returned_rows") -.. ]]] - -Default: ``1000`` - -.. [[[end]]] - -The maximum number of rows Datasette returns at a time. If you execute a query that exceeds this limit, Datasette will truncate the result set and include a warning. You can use OFFSET/LIMIT or other methods in your SQL to implement pagination if you need to return more rows. +Datasette returns a maximum of 1,000 rows of data at a time. If you execute a query that returns more than 1,000 rows, Datasette will return the first 1,000 and include a warning that the result set has been truncated. You can use OFFSET/LIMIT or other methods in your SQL to implement pagination if you need to return more than 1,000 rows. You can increase or decrease this limit like so:: @@ -154,15 +119,7 @@ You can increase or decrease this limit like so:: max_insert_rows ~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "max_insert_rows") -.. ]]] - -Default: ``100`` - -.. [[[end]]] - -Maximum rows that can be inserted at a time using the bulk insert API, see :ref:`TableInsertView`. +Maximum rows that can be inserted at a time using the bulk insert API, see :ref:`TableInsertView`. Defaults to 100. You can increase or decrease this limit like so:: @@ -173,15 +130,7 @@ You can increase or decrease this limit like so:: max_post_body_bytes ~~~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "max_post_body_bytes") -.. ]]] - -Default: ``2097152`` - -.. [[[end]]] - -Maximum size in bytes for a POST body that Datasette reads fully into memory, such as JSON submitted to the :ref:`write API `. Requests with larger bodies are rejected with an HTTP 413 error. +Maximum size in bytes for a POST body that Datasette reads fully into memory, such as JSON submitted to the :ref:`write API `. Requests with larger bodies are rejected with an HTTP 413 error. Defaults to 2,097,152 (2MB). This limit exists to protect against memory exhaustion: unlike file uploads handled by ``request.form()``, which stream to disk, these bodies are held entirely in memory and parsing them as JSON can multiply their memory footprint several times over. @@ -198,15 +147,7 @@ Set it to 0 to disable the limit entirely:: num_sql_threads ~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "num_sql_threads") -.. ]]] - -Default: ``3`` - -.. [[[end]]] - -Maximum number of threads in the thread pool Datasette uses to execute SQLite queries. +Maximum number of threads in the thread pool Datasette uses to execute SQLite queries. Defaults to 3. :: @@ -219,17 +160,9 @@ Setting this to 0 turns off threaded SQL queries entirely - useful for environme allow_facet ~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "allow_facet") -.. ]]] - -Default: ``on`` - -.. [[[end]]] - Allow users to specify columns they would like to facet on using the ``?_facet=COLNAME`` URL parameter to the table view. -If disabled, facets will still be displayed if they have been specifically enabled in ``metadata.json`` configuration for the table. +This is enabled by default. If disabled, facets will still be displayed if they have been specifically enabled in ``metadata.json`` configuration for the table. Here's how to disable this feature:: @@ -240,15 +173,7 @@ Here's how to disable this feature:: default_facet_size ~~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "default_facet_size") -.. ]]] - -Default: ``30`` - -.. [[[end]]] - -The default number of unique rows returned by :ref:`facets`. You can customize it like this:: +The default number of unique rows returned by :ref:`facets` is 30. You can customize it like this:: datasette mydatabase.db --setting default_facet_size 50 @@ -257,15 +182,7 @@ The default number of unique rows returned by :ref:`facets`. You can customize i facet_time_limit_ms ~~~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "facet_time_limit_ms") -.. ]]] - -Default: ``200`` - -.. [[[end]]] - -The time limit in milliseconds Datasette allows for calculating a facet. You can customize it like this:: +This is the time limit Datasette allows for calculating a facet, which defaults to 200ms:: datasette mydatabase.db --setting facet_time_limit_ms 1000 @@ -274,15 +191,7 @@ The time limit in milliseconds Datasette allows for calculating a facet. You can facet_suggest_time_limit_ms ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "facet_suggest_time_limit_ms") -.. ]]] - -Default: ``50`` - -.. [[[end]]] - -When Datasette calculates suggested facets it needs to run a SQL query for every column in your table. This time limit, in milliseconds, applies separately to each query. If the time limit is exceeded the column will not be suggested as a facet. +When Datasette calculates suggested facets it needs to run a SQL query for every column in your table. The default for this time limit is 50ms to account for the fact that it needs to run once for every column. If the time limit is exceeded the column will not be suggested as a facet. You can increase this time limit like so:: @@ -293,15 +202,7 @@ You can increase this time limit like so:: suggest_facets ~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "suggest_facets") -.. ]]] - -Default: ``on`` - -.. [[[end]]] - -Should Datasette calculate suggested facets? Turn this off like so:: +Should Datasette calculate suggested facets? On by default, turn this off like so:: datasette mydatabase.db --setting suggest_facets off @@ -310,15 +211,7 @@ Should Datasette calculate suggested facets? Turn this off like so:: allow_download ~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "allow_download") -.. ]]] - -Default: ``on`` - -.. [[[end]]] - -Should users be able to download the original SQLite database using a link on the database index page? Databases can only be downloaded if they are served in immutable mode and not in-memory. If downloading is unavailable for either of these reasons, the download link is hidden even if ``allow_download`` is on. To disable database downloads, use the following:: +Should users be able to download the original SQLite database using a link on the database index page? This is turned on by default. However, databases can only be downloaded if they are served in immutable mode and not in-memory. If downloading is unavailable for either of these reasons, the download link is hidden even if ``allow_download`` is on. To disable database downloads, use the following:: datasette mydatabase.db --setting allow_download off @@ -327,17 +220,9 @@ Should users be able to download the original SQLite database using a link on th allow_signed_tokens ~~~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "allow_signed_tokens") -.. ]]] - -Default: ``on`` - -.. [[[end]]] - Should users be able to create signed API tokens to access Datasette? -Use the following to turn it off:: +This is turned on by default. Use the following to turn it off:: datasette mydatabase.db --setting allow_signed_tokens off @@ -348,17 +233,9 @@ Turning this setting off will disable the ``/-/create-token`` page, :ref:`descri max_signed_tokens_ttl ~~~~~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "max_signed_tokens_ttl") -.. ]]] - -Default: ``0`` - -.. [[[end]]] - Maximum allowed expiry time for signed API tokens created by users. -A value of ``0`` means no limit - tokens can be created that will never expire. +Defaults to ``0`` which means no limit - tokens can be created that will never expire. Set this to a value in seconds to limit the maximum expiry time. For example, to set that limit to 24 hours you would use:: @@ -371,36 +248,18 @@ This setting is enforced when incoming tokens are processed. default_cache_ttl ~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "default_cache_ttl") -.. ]]] - -Default: ``5`` - -.. [[[end]]] - -Default HTTP caching max-age header in seconds, used for ``Cache-Control: max-age=X``. Can be over-ridden on a per-request basis using the ``?_ttl=`` query string parameter. Set this to ``0`` to disable HTTP caching entirely. +Default HTTP caching max-age header in seconds, used for ``Cache-Control: max-age=X``. Can be over-ridden on a per-request basis using the ``?_ttl=`` query string parameter. Set this to ``0`` to disable HTTP caching entirely. Defaults to 5 seconds. :: datasette mydatabase.db --setting default_cache_ttl 60 -Dynamic responses for authenticated actors, requests with cookies or an ``Authorization`` header, and responses that set cookies use ``Cache-Control: private, no-store``. This takes precedence over ``default_cache_ttl`` and ``?_ttl=``, even when cache headers are otherwise disabled. Anonymous dynamic responses vary by ``Cookie`` and ``Authorization``. Static assets retain their own cache policy. - .. _setting_cache_size_kb: cache_size_kb ~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "cache_size_kb") -.. ]]] - -Default: ``0`` - -.. [[[end]]] - -Sets the amount of memory SQLite uses for its `per-connection cache `_, in KB. Set this to ``0`` to use SQLite's default cache size. +Sets the amount of memory SQLite uses for its `per-connection cache `_, in KB. :: @@ -411,17 +270,9 @@ Sets the amount of memory SQLite uses for its `per-connection cache ` where an entire table (potentially hundreds of thousands of rows) can be exported as a single CSV -file. You can turn it off like this: +file. This is turned on by default - you can turn it off like this: :: @@ -432,16 +283,8 @@ file. You can turn it off like this: max_csv_mb ~~~~~~~~~~ -.. [[[cog - setting_default(cog, "max_csv_mb") -.. ]]] - -Default: ``100`` - -.. [[[end]]] - -The maximum size of CSV that can be exported, in megabytes. -You can disable the limit entirely by setting this to 0: +The maximum size of CSV that can be exported, in megabytes. Defaults to 100MB. +You can disable the limit entirely by settings this to 0: :: @@ -452,14 +295,6 @@ You can disable the limit entirely by setting this to 0: truncate_cells_html ~~~~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "truncate_cells_html") -.. ]]] - -Default: ``2048`` - -.. [[[end]]] - In the HTML table view, truncate any strings that are longer than this value. The full value will still be available in CSV, JSON and on the individual row HTML page. Set this to 0 to disable truncation. @@ -473,14 +308,6 @@ HTML page. Set this to 0 to disable truncation. force_https_urls ~~~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "force_https_urls") -.. ]]] - -Default: ``off`` - -.. [[[end]]] - Forces self-referential URLs in the JSON output to always use the ``https://`` protocol. This is useful for cases where the application itself is hosted using HTTP but is served to the outside world via a proxy that enables HTTPS. @@ -494,14 +321,6 @@ HTTP but is served to the outside world via a proxy that enables HTTPS. template_debug ~~~~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "template_debug") -.. ]]] - -Default: ``off`` - -.. [[[end]]] - This setting enables template context debug mode, which is useful to help understand what variables are available to custom templates when you are writing them. Enable it like this:: @@ -521,14 +340,6 @@ Some examples: trace_debug ~~~~~~~~~~~ -.. [[[cog - setting_default(cog, "trace_debug") -.. ]]] - -Default: ``off`` - -.. [[[end]]] - This setting enables appending ``?_trace=1`` to any page in order to see the SQL queries and other trace information that was used to generate that page. Enable it like this:: @@ -547,14 +358,6 @@ See :ref:`internals_tracer` for details on how to hook into this mechanism as a base_url ~~~~~~~~ -.. [[[cog - setting_default(cog, "base_url") -.. ]]] - -Default: ``/`` - -.. [[[end]]] - If you are running Datasette behind a proxy, it may be useful to change the root path used for the Datasette instance. For example, if you are sending traffic from ``https://www.example.com/tools/datasette/`` through to a proxied Datasette instance you may wish Datasette to use ``/tools/datasette/`` as its root URL. diff --git a/docs/settings_doc.py b/docs/settings_doc.py deleted file mode 100644 index e041f333..00000000 --- a/docs/settings_doc.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Cog helper for documenting setting defaults from Datasette's registry.""" - - -def setting_default(cog, name): - from datasette.app import DEFAULT_SETTINGS - - default = DEFAULT_SETTINGS[name] - if isinstance(default, bool): - default = "on" if default else "off" - cog.out(f"\nDefault: ``{default}``\n\n") diff --git a/docs/shots.yml b/docs/shots.yml deleted file mode 100644 index 0a5f9fa6..00000000 --- a/docs/shots.yml +++ /dev/null @@ -1,38 +0,0 @@ -# Screenshots used by the documentation, taken using shot-scraper: -# https://shot-scraper.datasette.io/en/stable/multi.html -# -# Run "just shots" from the repository root to create any that are -# missing. Existing images are skipped, so delete an image to recreate it. -# -# Paths are relative to this docs/ directory. - -# Serves the JavaScript in docs/shots/ and loads it on every page. -# List form means the datasette process is stopped directly when done. -- server: - - datasette - - --memory - - --port - - 8755 - - --static - - shots:shots - - -s - - extra_js_urls - - '["/shots/modal-example.js", "/shots/modal-classes.js"]' - -# javascript_plugins.rst - Reusable modal dialogs -- output: images/modal-example.webp - url: http://localhost:8755/ - javascript: | - document.querySelector('[aria-controls="my-plugin-dialog"]').click(); - selector: "#my-plugin-dialog" - padding: 32 - quality: 70 - -- output: images/modal-classes.webp - url: http://localhost:8755/ - javascript: | - document.querySelector('[aria-controls="export-dialog"]').click(); - document.activeElement.blur(); - selector: "#export-dialog" - padding: 32 - quality: 70 diff --git a/docs/shots/modal-classes.js b/docs/shots/modal-classes.js deleted file mode 100644 index 57372613..00000000 --- a/docs/shots/modal-classes.js +++ /dev/null @@ -1,41 +0,0 @@ -// Demonstrates every shared modal CSS class, for images/modal-classes.webp -document.addEventListener("datasette_init", () => { - const openButton = document.createElement("button"); - openButton.type = "button"; - openButton.textContent = "Open export dialog"; - openButton.setAttribute("aria-haspopup", "dialog"); - openButton.setAttribute("aria-controls", "export-dialog"); - - const modal = DatasetteModal.create(); - const dialog = modal.dialog; - dialog.id = "export-dialog"; - dialog.setAttribute("aria-labelledby", "export-dialog-title"); - dialog.innerHTML = ` - - - `; - - const [cancelButton, exportButton] = dialog.querySelectorAll(".modal-footer button"); - cancelButton.addEventListener("click", () => modal.requestClose("cancel")); - exportButton.addEventListener("click", () => modal.close()); - openButton.addEventListener("click", () => { - modal.show({ returnFocusTo: openButton, initialFocus: exportButton }); - }); - - document.body.append(modal); - document.querySelector("section.content").append(openButton); -}); diff --git a/docs/shots/modal-example.js b/docs/shots/modal-example.js deleted file mode 100644 index fd8210ab..00000000 --- a/docs/shots/modal-example.js +++ /dev/null @@ -1,38 +0,0 @@ -document.addEventListener("datasette_init", () => { - const openButton = document.createElement("button"); - openButton.type = "button"; - openButton.textContent = "Open example dialog"; - // Indicate that this button opens a dialog: - openButton.setAttribute("aria-haspopup", "dialog"); - // Identify which dialog it controls: - openButton.setAttribute("aria-controls", "my-plugin-dialog"); - - const modal = DatasetteModal.create(); - const dialog = modal.dialog; - dialog.id = "my-plugin-dialog"; - // Tell screenreaders the dialog is labelled by #my-plugin-dialog-title - dialog.setAttribute("aria-labelledby", "my-plugin-dialog-title"); - dialog.innerHTML = ` - - - `; - - const closeButton = dialog.querySelector("button"); - closeButton.addEventListener("click", () => { - modal.requestClose("cancel"); - }); - openButton.addEventListener("click", () => { - modal.show({ returnFocusTo: openButton, initialFocus: closeButton }); - }); - - document.body.append(modal); - document.querySelector("section.content").append(openButton); -}); diff --git a/docs/telemetry_doc.py b/docs/telemetry_doc.py deleted file mode 100644 index 9c4086ae..00000000 --- a/docs/telemetry_doc.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Cog helpers that render the span and metric reference in ``internals.rst`` -from ``datasette/telemetry_registry.py``. -""" - - -def _attribute_lines(cog, attributes): - if not attributes: - cog.out(" No attributes.\n\n") - return - cog.out(" Attributes:\n\n") - for attribute in attributes: - suffix = " *(optional)*" if attribute.optional else "" - line = f" - ``{attribute}``{suffix} - {attribute.description}" - if attribute.values is not None: - rendered = ", ".join(f"``{value}``" for value in sorted(attribute.values)) - line += f" One of: {rendered}." - cog.out(line + "\n") - cog.out("\n") - - -def spans(cog): - from opentelemetry.trace import SpanKind - - from datasette.telemetry_registry import SPANS - - cog.out("\n") - for span in SPANS: - cog.out(f"``{span}``\n") - cog.out(f" {span.description}\n\n") - # Only show the kind for spans that are not INTERNAL - if span.kind != SpanKind.INTERNAL: - cog.out(f" Kind: ``{span.kind.name}``.\n\n") - _attribute_lines(cog, span.attributes) - - -def metrics(cog): - from datasette.telemetry_registry import METRICS - - cog.out("\n") - for metric in METRICS: - cog.out(f"``{metric}``\n") - cog.out(f" {metric.kind}, unit ``{metric.unit}``. {metric.description}\n\n") - if metric.buckets: - boundaries = ", ".join(f"``{boundary}``" for boundary in metric.buckets) - cog.out(f" Bucket boundaries: {boundaries}.\n\n") - _attribute_lines(cog, metric.attributes) diff --git a/docs/template_context.rst b/docs/template_context.rst index e445b335..890846bb 100644 --- a/docs/template_context.rst +++ b/docs/template_context.rst @@ -168,6 +168,9 @@ The page for arbitrary SQL queries (/database/-/query?sql=...) and stored querie ``db_is_immutable`` - ``bool`` Boolean indicating if this database is immutable +``default_table`` - ``str`` + Name of the focal table for this query, if any - set when the query page was reached from a table-scoped context (such as the table page's 'View and edit SQL' link) so the SQL editor can complete that table's columns unprefixed. ``None`` otherwise, including for stored/canned queries. + ``display_rows`` - ``list`` List of result rows formatted for HTML display. Each row is a list of rendered cell values in the same order as ``columns``. diff --git a/docs/template_context_doc.py b/docs/template_context_doc.py index 95b8a366..a5f4fb6f 100644 --- a/docs/template_context_doc.py +++ b/docs/template_context_doc.py @@ -21,12 +21,14 @@ def template_context(cog): ), ) for name, doc in TEMPLATE_BASE_CONTEXT.items(): - cog.out(f"``{name}``\n") - cog.out(f" {doc}\n\n") + cog.out("``{}``\n".format(name)) + cog.out(" {}\n\n".format(doc)) for klass in PAGES.values(): title = "{} page".format(klass.__name__.removesuffix("Context")) - intro = f"{klass.__doc__} Rendered using the ``{klass.documented_template}`` template." + intro = "{} Rendered using the ``{}`` template.".format( + klass.__doc__, klass.documented_template + ) _section(cog, title, intro) if klass.extras_scope is not None: cog.out( @@ -34,10 +36,10 @@ def template_context(cog): "` for this page.\n\n" ) for f in sorted(klass.documented_fields(), key=lambda f: f.name): - cog.out(f"``{f.name}`` - ``{f.type_name}``\n") - cog.out(f" {f.help}\n\n") + cog.out("``{}`` - ``{}``\n".format(f.name, f.type_name)) + cog.out(" {}\n\n".format(f.help)) def _section(cog, title, intro): cog.out("{}\n{}\n\n".format(title, "-" * len(title))) - cog.out(f"{intro}\n\n") + cog.out("{}\n\n".format(intro)) diff --git a/docs/testing_plugins.rst b/docs/testing_plugins.rst index 81dc90dc..15891963 100644 --- a/docs/testing_plugins.rst +++ b/docs/testing_plugins.rst @@ -25,7 +25,7 @@ If you use the template described in :ref:`writing_plugins_cookiecutter` your pl ) -This test uses the :ref:`internals_datasette_client` object to exercise a test instance of Datasette. ``datasette.client`` is a wrapper around the `HTTPX2 `__ Python library which can imitate HTTP requests using ASGI. This is the recommended way to write tests against a Datasette instance. +This test uses the :ref:`internals_datasette_client` object to exercise a test instance of Datasette. ``datasette.client`` is a wrapper around the `HTTPX `__ Python library which can imitate HTTP requests using ASGI. This is the recommended way to write tests against a Datasette instance. This test also uses the `pytest-asyncio `__ package to add support for ``async def`` test functions running under pytest. @@ -57,7 +57,7 @@ Then run the tests using pytest like so:: Setting up a Datasette test instance ------------------------------------ -Use :ref:`datasette.client ` to make requests against a test instance. The first request runs startup hooks and launches registered background tasks automatically: +The above example shows the easiest way to start writing tests against a Datasette instance: .. code-block:: python @@ -71,24 +71,16 @@ Use :ref:`datasette.client ` to make requests agains response = await datasette.client.get("/-/plugins.json") assert response.status_code == 200 -If your test uses Datasette directly without making a request, call ``await datasette.invoke_startup()`` to initialize the instance and run its startup hooks: +Creating a ``Datasette()`` instance like this as useful shortcut in tests, but there is one detail you need to be aware of. It's important to ensure that the async method ``.invoke_startup()`` is called on that instance. You can do that like this: .. code-block:: python datasette = Datasette(memory=True) await datasette.invoke_startup() -This runs the :ref:`plugin_hook_startup` and :ref:`plugin_hook_prepare_jinja2_environment` hooks on the same event loop as your test. It does not launch registered background tasks. +This method registers any :ref:`plugin_hook_startup` or :ref:`plugin_hook_prepare_jinja2_environment` plugins that might themselves need to make async calls. -To run tasks registered with :ref:`datasette_add_background_task` without making a request, use ``await datasette.start_background_tasks()``. This runs startup if needed and launches every registered task: - -.. code-block:: python - - datasette = Datasette(memory=True) - await datasette.start_background_tasks() - # Tasks registered by startup() hooks have been launched - -See :ref:`datasette_lifecycle` for the full startup and shutdown sequence. +If you are using ``await datasette.client.get()`` and similar methods then you don't need to worry about this - Datasette automatically calls ``invoke_startup()`` the first time it handles a request. .. _testing_plugins_datasette_fixtures_database: @@ -162,7 +154,7 @@ If you need to opt out of this behavior, add the following to your ``pytest.ini` Using datasette.client in tests ------------------------------- -The :ref:`internals_datasette_client` mechanism is designed for use in tests. It provides access to a pre-configured `HTTPX2 async client `__ instance that can make GET, POST and other HTTP requests against a Datasette instance from inside a test. +The :ref:`internals_datasette_client` mechanism is designed for use in tests. It provides access to a pre-configured `HTTPX async client `__ instance that can make GET, POST and other HTTP requests against a Datasette instance from inside a test. A simple test looks like this: @@ -281,22 +273,22 @@ If you want to create that test database repeatedly for every individual test fu .. _testing_plugins_pytest_httpx: -Testing outbound HTTP calls with pytest-httpx2 ----------------------------------------------- +Testing outbound HTTP calls with pytest-httpx +--------------------------------------------- If your plugin makes outbound HTTP calls - for example datasette-auth-github or datasette-import-table - you may need to mock those HTTP requests in your tests. -The `pytest-httpx2 `__ package provides a ``httpx2_mock`` fixture, built on `respx `__, for mocking outbound calls made using HTTPX2. +The `pytest-httpx `__ package is a useful library for mocking calls. It can be tricky to use with Datasette though since it mocks all HTTPX requests, and Datasette's own testing mechanism uses HTTPX internally. -Datasette's own ``datasette.client`` mechanism uses HTTPX2 internally too, but those requests are passed directly to the ASGI application rather than being sent over the network, so they are not affected by the mock. +To avoid breaking your tests, you can return ``["localhost"]`` from the ``non_mocked_hosts()`` fixture. -As an example, here's a very simple plugin which executes an HTTP request and returns the resulting content: +As an example, here's a very simple plugin which executes an HTTP response and returns the resulting content: .. code-block:: python from datasette import hookimpl from datasette.utils.asgi import Response - import httpx2 + import httpx @hookimpl @@ -314,18 +306,27 @@ As an example, here's a very simple plugin which executes an HTTP request and re """) vars = await request.post_vars() url = vars["url"] - return Response.text(httpx2.get(url).text) + return Response.text(httpx.get(url).text) -Here's a test for that plugin that mocks the HTTPX2 outbound request: +Here's a test for that plugin that mocks the HTTPX outbound request: .. code-block:: python from datasette.app import Datasette + import pytest - async def test_outbound_http_call(httpx2_mock): - httpx2_mock.get("https://www.example.com/").respond( - text="Hello world" + @pytest.fixture + def non_mocked_hosts(): + # This ensures httpx-mock will not affect Datasette's own + # httpx calls made in the tests by datasette.client: + return ["localhost"] + + + async def test_outbound_http_call(httpx_mock): + httpx_mock.add_response( + url="https://www.example.com/", + text="Hello world", ) datasette = Datasette([], memory=True) response = await datasette.client.post( @@ -334,13 +335,11 @@ Here's a test for that plugin that mocks the HTTPX2 outbound request: ) assert response.text == "Hello world" - outbound_request = httpx2_mock.calls.last.request + outbound_request = httpx_mock.get_request() assert ( outbound_request.url == "https://www.example.com/" ) -If your plugin still makes its outbound calls using the original ``httpx`` library you can continue to mock those using `pytest-httpx `__. - .. _testing_plugins_register_in_test: Registering a plugin for the duration of a test diff --git a/docs/writing_plugins.rst b/docs/writing_plugins.rst index bbefe344..d1e5e75a 100644 --- a/docs/writing_plugins.rst +++ b/docs/writing_plugins.rst @@ -203,40 +203,6 @@ Templates should be bundled for distribution using the same ``package_data`` mec You can also use wildcards here such as ``templates/*.html``. See `datasette-edit-schema `__ for an example of this pattern. -.. _writing_plugins_custom_templates_breadcrumbs: - -Adding breadcrumbs -~~~~~~~~~~~~~~~~~~ - -Plugin templates that extend ``base.html`` can use the ``crumbs.nav()`` macro to display breadcrumb links back to the Datasette homepage, and optionally to a database and a table. Override the ``crumbs`` block to specify which links to include: - -.. code-block:: html+jinja - - {% extends "base.html" %} - - {% block title %}Manage {{ table }}{% endblock %} - - {% block crumbs %} - - {{ crumbs.nav(request=request, database=database, table=table) }} - - {{ crumbs.nav(request=request, database=database) }} - {% endblock %} - - {% block content %} -

Manage {{ table }}

- {% endblock %} - -The macro accepts these arguments: - -* ``request``: the current request, used to check the actor's permissions. -* ``database``: an optional database name, as a string -* ``table``: an optional table name, as a string. If you pass ``table``, you must also pass ``database``. - -For a database-level plugin page, use ``{{ crumbs.nav(request=request, database=database) }}``. For a page with just a homepage link, use ``{{ crumbs.nav(request=request) }}``, which is also the default provided by ``base.html`` if you do not override the block. - -The table-level example renders links in the form ``home / database / table``. Each link is only included if the current actor has permission to view that resource. - .. _writing_plugins_configuration: Writing plugins that accept configuration diff --git a/package-lock.json b/package-lock.json index 213999c1..ee93cdb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,22 @@ { "name": "datasette", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "datasette", "dependencies": { - "@codemirror/lang-sql": "^6.3.3", + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/commands": "^6.10.4", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/language": "^6.12.4", + "@codemirror/lint": "^6.9.7", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.6", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-terser": "^0.1.0", - "codemirror": "^6.0.1", + "codemirror": "^6.0.2", "rollup": "^3.30.0" }, "devDependencies": { @@ -17,175 +24,184 @@ } }, "node_modules/@codemirror/autocomplete": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.3.2.tgz", - "integrity": "sha512-+VzxrHWkuvSSt0fw4I57SULo/NMrLnNgm6JHrkbIYfDw9jZJNTruCwkv32TCqSeC8xIXhYWMuxawwr/xOoHr8w==", + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.5.0", - "@lezer/common": "^1.0.0" - }, - "peerDependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.17.0", "@lezer/common": "^1.0.0" } }, "node_modules/@codemirror/commands": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.1.2.tgz", - "integrity": "sha512-sO3jdX1s0pam6lIdeSJLMN3DQ6mPEbM4yLvyKkdqtmd/UDwhXA5+AwFJ89rRXm6vTeOXBsE5cAmlos/t7MJdgg==", + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz", + "integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==", + "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0" + "@codemirror/state": "^6.7.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" } }, "node_modules/@codemirror/lang-sql": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.3.3.tgz", - "integrity": "sha512-VNsHju8500fkiDyDU8jZyGQ8M0iXU0SmfeCoCeAYkACcEFlX63BOT8311pICXyw43VYRbS23w54RgSEQmixGjQ==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", + "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "node_modules/@codemirror/language": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.3.1.tgz", - "integrity": "sha512-MK+G1QKaGfSEUg9YEFaBkMBI6j1ge4VMBPZv9fDYotw7w695c42x5Ba1mmwBkesYnzYFBfte6Hh9TDcKa6xORQ==", + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", + "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", + "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "node_modules/@codemirror/lint": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.1.0.tgz", - "integrity": "sha512-mdvDQrjRmYPvQ3WrzF6Ewaao+NWERYtpthJvoQ3tK3t/44Ynhk8ZGjTSL9jMEv8CgSMogmt75X8ceOZRDSXHtQ==", + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", + "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", + "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.42.0", "crelt": "^1.0.5" } }, "node_modules/@codemirror/search": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.2.3.tgz", - "integrity": "sha512-V9n9233lopQhB1dyjsBK2Wc1i+8hcCqxl1wQ46c5HWWLePoe4FluV3TGHoZ04rBRlGjNyz9DTmpJErig8UE4jw==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz", + "integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==", + "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "node_modules/@codemirror/state": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.4.tgz", - "integrity": "sha512-g+3OJuRylV5qsXuuhrc6Cvs1NQluNioepYMM2fhnpYkNk7NgX+j0AFuevKSVKzTDmDyt9+Puju+zPdHNECzCNQ==" + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz", + "integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } }, "node_modules/@codemirror/view": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.5.1.tgz", - "integrity": "sha512-xBKP8N3AXOs06VcKvIuvIQoUlGs7Hb78ftJWahLaRX909jKPMgGxR5XjvrawzTTZMSTU3DzdjDNPwG6fPM/ypQ==", + "version": "6.43.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz", + "integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==", + "license": "MIT", "dependencies": { - "@codemirror/state": "^6.1.4", - "style-mod": "^4.0.0", + "@codemirror/state": "^6.7.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@lezer/common": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.1.tgz", - "integrity": "sha512-8TR5++Q/F//tpDsLd5zkrvEX5xxeemafEaek7mUp7Y+bI8cKQXdSqhzTOBaOogETcMOVr0pT3BBPXp13477ciw==" + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" }, "node_modules/@lezer/highlight": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.2.tgz", - "integrity": "sha512-CAun1WR1glxG9ZdOokTZwXbcwB7PXkIEyZRUMFBVwSrhTcogWq634/ByNImrkUnQhjju6xsIaOBIxvcRJtplXQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", "dependencies": { - "@lezer/common": "^1.0.0" + "@lezer/common": "^1.3.0" } }, "node_modules/@lezer/lr": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.4.tgz", - "integrity": "sha512-L/52/oMJBFXXx8qBYF4UgktLP2geQ/qn5Fd8+5L/mqlLLCB9+qdKktFAtejd9FdFMaFx6lrP5rmLz4sN3Kplcg==", + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" } }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz", + "integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==", + "license": "MIT" + }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz", - "integrity": "sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==", + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "license": "MIT", "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.0", "is-module": "^1.0.0", "resolve": "^1.22.1" }, @@ -193,7 +209,7 @@ "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^2.78.0||^3.0.0" + "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -205,6 +221,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.1.0.tgz", "integrity": "sha512-N2KK+qUfHX2hBzVzM41UWGLrEmcjVC37spC8R3c9mt3oEDFKh3N2e12/lLp9aVSt86veR0TQiCNQXrm8C6aiUQ==", + "license": "MIT", "dependencies": { "terser": "^5.15.1" }, @@ -221,19 +238,20 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" + "picomatch": "^4.0.2" }, "engines": { "node": ">=14.0.0" }, "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0" + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "peerDependenciesMeta": { "rollup": { @@ -242,19 +260,22 @@ } }, "node_modules/@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "license": "MIT" }, "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -265,23 +286,14 @@ "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" }, "node_modules/codemirror": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", - "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", @@ -295,31 +307,45 @@ "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" }, "node_modules/crelt": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", - "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", + "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", + "license": "MIT" }, "node_modules/deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -329,41 +355,36 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-builtin-module": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.0.tgz", - "integrity": "sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==", + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", "dependencies": { - "builtin-modules": "^3.3.0" + "function-bind": "^1.1.2" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -372,28 +393,31 @@ "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", "dev": true, "license": "MIT", "bin": { @@ -407,17 +431,22 @@ } }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.9.0", + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -426,6 +455,8 @@ "version": "3.30.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", + "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -441,6 +472,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -449,20 +481,23 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "node_modules/style-mod": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", - "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -471,12 +506,13 @@ } }, "node_modules/terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", + "license": "BSD-2-Clause", "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -488,361 +524,10 @@ } }, "node_modules/w3c-keyname": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", - "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" - } - }, - "dependencies": { - "@codemirror/autocomplete": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.3.2.tgz", - "integrity": "sha512-+VzxrHWkuvSSt0fw4I57SULo/NMrLnNgm6JHrkbIYfDw9jZJNTruCwkv32TCqSeC8xIXhYWMuxawwr/xOoHr8w==", - "requires": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.5.0", - "@lezer/common": "^1.0.0" - } - }, - "@codemirror/commands": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.1.2.tgz", - "integrity": "sha512-sO3jdX1s0pam6lIdeSJLMN3DQ6mPEbM4yLvyKkdqtmd/UDwhXA5+AwFJ89rRXm6vTeOXBsE5cAmlos/t7MJdgg==", - "requires": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0" - } - }, - "@codemirror/lang-sql": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.3.3.tgz", - "integrity": "sha512-VNsHju8500fkiDyDU8jZyGQ8M0iXU0SmfeCoCeAYkACcEFlX63BOT8311pICXyw43VYRbS23w54RgSEQmixGjQ==", - "requires": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "@codemirror/language": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.3.1.tgz", - "integrity": "sha512-MK+G1QKaGfSEUg9YEFaBkMBI6j1ge4VMBPZv9fDYotw7w695c42x5Ba1mmwBkesYnzYFBfte6Hh9TDcKa6xORQ==", - "requires": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "@codemirror/lint": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.1.0.tgz", - "integrity": "sha512-mdvDQrjRmYPvQ3WrzF6Ewaao+NWERYtpthJvoQ3tK3t/44Ynhk8ZGjTSL9jMEv8CgSMogmt75X8ceOZRDSXHtQ==", - "requires": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "crelt": "^1.0.5" - } - }, - "@codemirror/search": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.2.3.tgz", - "integrity": "sha512-V9n9233lopQhB1dyjsBK2Wc1i+8hcCqxl1wQ46c5HWWLePoe4FluV3TGHoZ04rBRlGjNyz9DTmpJErig8UE4jw==", - "requires": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "crelt": "^1.0.5" - } - }, - "@codemirror/state": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.1.4.tgz", - "integrity": "sha512-g+3OJuRylV5qsXuuhrc6Cvs1NQluNioepYMM2fhnpYkNk7NgX+j0AFuevKSVKzTDmDyt9+Puju+zPdHNECzCNQ==" - }, - "@codemirror/view": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.5.1.tgz", - "integrity": "sha512-xBKP8N3AXOs06VcKvIuvIQoUlGs7Hb78ftJWahLaRX909jKPMgGxR5XjvrawzTTZMSTU3DzdjDNPwG6fPM/ypQ==", - "requires": { - "@codemirror/state": "^6.1.4", - "style-mod": "^4.0.0", - "w3c-keyname": "^2.2.4" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" - }, - "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "@lezer/common": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.1.tgz", - "integrity": "sha512-8TR5++Q/F//tpDsLd5zkrvEX5xxeemafEaek7mUp7Y+bI8cKQXdSqhzTOBaOogETcMOVr0pT3BBPXp13477ciw==" - }, - "@lezer/highlight": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.2.tgz", - "integrity": "sha512-CAun1WR1glxG9ZdOokTZwXbcwB7PXkIEyZRUMFBVwSrhTcogWq634/ByNImrkUnQhjju6xsIaOBIxvcRJtplXQ==", - "requires": { - "@lezer/common": "^1.0.0" - } - }, - "@lezer/lr": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.2.4.tgz", - "integrity": "sha512-L/52/oMJBFXXx8qBYF4UgktLP2geQ/qn5Fd8+5L/mqlLLCB9+qdKktFAtejd9FdFMaFx6lrP5rmLz4sN3Kplcg==", - "requires": { - "@lezer/common": "^1.0.0" - } - }, - "@rollup/plugin-node-resolve": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz", - "integrity": "sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg==", - "requires": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-builtin-module": "^3.2.0", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - } - }, - "@rollup/plugin-terser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.1.0.tgz", - "integrity": "sha512-N2KK+qUfHX2hBzVzM41UWGLrEmcjVC37spC8R3c9mt3oEDFKh3N2e12/lLp9aVSt86veR0TQiCNQXrm8C6aiUQ==", - "requires": { - "terser": "^5.15.1" - } - }, - "@rollup/pluginutils": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz", - "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==", - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - } - }, - "@types/estree": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", - "integrity": "sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==" - }, - "@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" - }, - "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==" - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==" - }, - "codemirror": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", - "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", - "requires": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" - }, - "crelt": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz", - "integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==" - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" - }, - "estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "is-builtin-module": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.0.tgz", - "integrity": "sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==", - "requires": { - "builtin-modules": "^3.3.0" - } - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "requires": { - "has": "^1.0.3" - } - }, - "is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==" - }, - "prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "rollup": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", - "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", - "requires": { - "fsevents": "~2.3.2" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "style-mod": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz", - "integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==" - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" - }, - "terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", - "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - } - }, - "w3c-keyname": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz", - "integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==" + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" } } } diff --git a/package.json b/package.json index 27abd0cd..ccc94974 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,22 @@ "prettier": "^3.0.0" }, "scripts": { + "build:codemirror": "rollup -c", "fix": "npm run prettier -- --write", "prettier": "prettier 'datasette/static/*[!.min|bundle].js'" }, "dependencies": { - "@codemirror/lang-sql": "^6.3.3", + "@codemirror/autocomplete": "^6.20.3", + "@codemirror/commands": "^6.10.4", + "@codemirror/lang-sql": "^6.10.0", + "@codemirror/language": "^6.12.4", + "@codemirror/lint": "^6.9.7", + "@codemirror/search": "^6.7.1", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.6", "@rollup/plugin-node-resolve": "^15.0.1", "@rollup/plugin-terser": "^0.1.0", - "codemirror": "^6.0.1", + "codemirror": "^6.0.2", "rollup": "^3.30.0" } } diff --git a/pyproject.toml b/pyproject.toml index cd8b5513..70496cdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,9 +28,9 @@ dependencies = [ "click-default-group>=1.2.3", "Jinja2>=2.10.3", "hupper>=1.9", - "httpx2>=2.0", + "httpx>=0.20,<1.0", "pluggy>=1.0", - "uvicorn>=0.29", + "uvicorn>=0.11", "aiofiles>=0.4", "PyYAML>=5.3", "mergedeep>=1.1.1", @@ -40,7 +40,6 @@ dependencies = [ "setuptools", "pip", "pydantic>=2", - "opentelemetry-api>=1.37", ] [project.urls] @@ -64,17 +63,16 @@ dev = [ "pytest-xdist>=2.2.1", "pytest-asyncio>=1.2.0", "beautifulsoup4>=4.8.1", - "black==26.5.1", + "black==26.3.1", "blacken-docs==1.20.0", "pytest-timeout>=1.4.2", "trustme>=0.7", "cogapp>=3.3.0", "multipart-form-data-conformance==0.1a0", - "ruff>=0.16.0", - "opentelemetry-sdk>=1.37", + "ruff>=0.9", # docs "Sphinx==7.4.7", - "furo==2025.12.19", + "furo==2025.9.25", "sphinx-autobuild", "codespell>=2.2.5", "sphinx-copybutton", @@ -87,9 +85,6 @@ dev = [ playwright = [ "pytest-playwright>=0.8.0", ] -shots = [ - "shot-scraper>=1.12", -] [project.optional-dependencies] rich = ["rich"] @@ -107,5 +102,9 @@ datasette = ["templates/*.html"] [tool.setuptools.dynamic] version = {attr = "datasette.version.__version__"} +[tool.ruff] +line-length = 160 +select = ["E", "F", "W"] + [tool.uv] package = true diff --git a/pytest.ini b/pytest.ini index 590054de..75de6925 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,4 @@ [pytest] -addopts = --ignore=ignored filterwarnings= # https://github.com/pallets/jinja/issues/927 ignore:Using or importing the ABCs::jinja2 diff --git a/rollup.config.mjs b/rollup.config.mjs new file mode 100644 index 00000000..026dcf6e --- /dev/null +++ b/rollup.config.mjs @@ -0,0 +1,30 @@ +import { nodeResolve } from "@rollup/plugin-node-resolve"; +import terser from "@rollup/plugin-terser"; + +const plugins = [nodeResolve(), terser()]; + +export default [ + // IIFE bundle for Datasette's own pages (global name `cm`, included by + // _codemirror.html). The shared datasette-sql-editor.js module is inlined. + { + input: "datasette/static/cm-editor.js", + output: { + file: "datasette/static/cm-editor.bundle.js", + format: "iife", + name: "cm", + }, + plugins, + }, + // Self-contained ESM bundle for plugin authors to import directly, e.g. + // import {createSqlEditor, datasetteSchema} from + // "/-/static/datasette-sql-editor.bundle.js" + // No bare specifiers remain; all @codemirror/* deps are inlined. + { + input: "datasette/static/datasette-sql-editor.js", + output: { + file: "datasette/static/datasette-sql-editor.bundle.js", + format: "es", + }, + plugins, + }, +]; diff --git a/ruff.toml b/ruff.toml index 3c4345bf..74447a8c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,7 +1,2 @@ line-length = 160 -target-version = "py310" - -[lint.flake8-bugbear] -# from_extra() returns a dataclasses.field(), so it is safe as a dataclass -# default - ruff cannot see through the wrapper (RUF009) -extend-immutable-calls = ["datasette.views.from_extra"] \ No newline at end of file +target-version = "py310" \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 5fdbd51d..7ec03146 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,15 @@ +import httpx import importlib.metadata import os import pathlib +import pytest +import pytest_asyncio import re -import socket import subprocess import sys import tempfile import time from dataclasses import dataclass - -import httpx2 -import pytest -import pytest_asyncio - from datasette import Event, hookimpl try: @@ -33,39 +30,15 @@ UNDOCUMENTED_PERMISSIONS = { } -def wait_until_responds(url, timeout=5.0, client=httpx2, process=None, **kwargs): +def wait_until_responds(url, timeout=5.0, client=httpx, **kwargs): start = time.time() while time.time() - start < timeout: - # If the server died there is no point waiting out the timeout - fail - # now, with its output, instead of after `timeout` seconds of silence - if process is not None and process.poll() is not None: - raise AssertionError( - "Server exited early with returncode {}\n{}".format( - process.returncode, process.stdout.read().decode("utf-8") - ) - ) try: client.get(url, **kwargs) return - except httpx2.TransportError: + except httpx.ConnectError: time.sleep(0.1) - raise AssertionError(f"Timed out waiting for {url} to respond") - - -def find_free_port(): - with socket.socket() as sock: - sock.bind(("127.0.0.1", 0)) - return sock.getsockname()[1] - - -from datasette.telemetry_testing import ( # noqa: F401 - MetricsCollector, - otel_meter_provider, - otel_metrics, - otel_provider, - otel_reset, - otel_spans, -) + raise AssertionError("Timed out waiting for {} to respond".format(url)) @pytest.fixture @@ -81,12 +54,10 @@ def bare_ds(): @pytest_asyncio.fixture(scope="session") async def ds_client(): - import secrets - from datasette.app import Datasette from datasette.database import Database - from .fixtures import CONFIG, METADATA, PLUGINS_DIR + import secrets ds = Datasette( metadata=METADATA, @@ -116,10 +87,7 @@ async def ds_client(): await db.execute_write_fn(prepare) await ds.invoke_startup() - try: - yield ds.client - finally: - ds.close() + return ds.client def pytest_report_header(config): @@ -128,8 +96,8 @@ def pytest_report_header(config): conn.close() sqlite_utils_version = importlib.metadata.version("sqlite-utils") headers = [ - f"SQLite: {version}", - f"sqlite-utils: {sqlite_utils_version}", + "SQLite: {}".format(version), + "sqlite-utils: {}".format(sqlite_utils_version), ] if config.getoption("--playwright"): try: @@ -181,11 +149,6 @@ def pytest_collection_modifyitems(config, items): move_to_front(items, "test_spatialite_error_if_attempt_to_open_spatialite") move_to_front(items, "test_package") move_to_front(items, "test_package_with_port") - # These start subprocesses, which can crash on macOS/CPython 3.13 late in - # a test run once the pytest process has started many threads - move_to_front(items, "test_datasette_package_never_imports_the_sdk") - move_to_front(items, "test_kit_module_itself_never_imports_the_sdk") - move_to_front(items, "test_no_provider_takes_the_fast_path") def move_to_front(items, test_name): @@ -212,8 +175,8 @@ def restore_working_directory(tmpdir, request): @pytest.fixture(scope="session", autouse=True) def check_actions_are_documented(): - from datasette.default_actions import register_actions as default_register_actions from datasette.plugins import pm + from datasette.default_actions import register_actions as default_register_actions content = ( pathlib.Path(__file__).parent.parent / "docs" / "authentication.rst" @@ -239,7 +202,7 @@ def check_actions_are_documented(): if kwargs["action"] in core_actions: assert ( action in documented_actions - ), f"Undocumented permission action: {action}" + ), "Undocumented permission action: {}".format(action) pm.add_hookcall_monitoring( before=before, after=lambda outcome, hook_name, hook_impls, kwargs: None @@ -284,24 +247,12 @@ def ds_localhost_http_server(): # Avoid FileNotFoundError: [Errno 2] No such file or directory: cwd=tempfile.gettempdir(), ) - try: - wait_until_responds("http://localhost:8041/", process=ds_proc) - yield ds_proc - finally: - stop_process(ds_proc) - - -def stop_process(proc): - try: - if proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=5) - except subprocess.TimeoutExpired: - proc.kill() - proc.wait() - finally: - proc.stdout.close() + wait_until_responds("http://localhost:8041/") + # Check it started successfully + assert not ds_proc.poll(), ds_proc.stdout.read().decode("utf-8") + yield ds_proc + # Shut it down at the end of the pytest session + ds_proc.terminate() @pytest.fixture(scope="session") @@ -322,27 +273,11 @@ def ds_unix_domain_socket_server(tmp_path_factory): cwd=tempfile.gettempdir(), ) # Poll until available - transport = httpx2.HTTPTransport(uds=uds) - client = httpx2.Client(transport=transport) + transport = httpx.HTTPTransport(uds=uds) + client = httpx.Client(transport=transport) try: - # Probe with a socket we own: the HTTP transport can leak a socket - # when connect() fails before the UDS server has started listening. - start = time.monotonic() - while True: - try: - with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as probe: - probe.settimeout(0.1) - probe.connect(uds) - break - except OSError: - if ds_proc.poll() is not None or time.monotonic() - start > 30: - raise - time.sleep(0.1) wait_until_responds( - "http://localhost/_memory.json", - timeout=30.0, - client=client, - process=ds_proc, + "http://localhost/_memory.json", timeout=30.0, client=client ) # Check it started successfully assert not ds_proc.poll(), ds_proc.stdout.read().decode("utf-8") @@ -350,75 +285,20 @@ def ds_unix_domain_socket_server(tmp_path_factory): finally: client.close() # Shut it down at the end of the pytest session - stop_process(ds_proc) + ds_proc.terminate() + try: + ds_proc.wait(timeout=5) + except subprocess.TimeoutExpired: + ds_proc.kill() + ds_proc.wait() try: os.unlink(uds) except FileNotFoundError: pass -@pytest.fixture -def serve_with_plugins(tmp_path): - """Factory fixture for starting ``datasette serve`` in a subprocess with - plugins written to a temporary ``--plugins-dir``. - - For tests that need the real serve path: event-loop wiring, exit codes, - signals. The usual in-process ``pm.register`` plugin pattern can't reach - a subprocess, so plugin source is written out as importable files instead. - - Unlike ``ds_localhost_http_server`` this is function-scoped and takes a - fresh port each time, because each test needs its own plugins. Call it as:: - - proc, port = serve_with_plugins({"my_plugin": PLUGIN_SOURCE}) - - ``plugins`` maps module name to Python source. Pass - ``wait_for_startup=False`` when the server is expected to fail during - startup rather than begin serving. Extra CLI arguments are passed through. - Every process started is terminated when the test ends. - """ - processes = [] - - def start(plugins, *extra_args, wait_for_startup=True): - plugins_dir = tmp_path / "plugins" - plugins_dir.mkdir(exist_ok=True) - for module_name, source in plugins.items(): - (plugins_dir / f"{module_name}.py").write_text(source, "utf-8") - port = find_free_port() - proc = subprocess.Popen( - [ - sys.executable, - "-m", - "datasette", - "--memory", - "--plugins-dir", - str(plugins_dir), - "-h", - "127.0.0.1", - "-p", - str(port), - *extra_args, - ], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - # Avoid FileNotFoundError: [Errno 2] No such file or directory: - cwd=tempfile.gettempdir(), - ) - processes.append(proc) - if wait_for_startup: - wait_until_responds( - f"http://127.0.0.1:{port}/-/versions.json", process=proc - ) - return proc, port - - yield start - - for proc in processes: - stop_process(proc) - - # Import fixtures from fixtures.py to make them available -from .fixtures import ( # noqa: F401 - TEMP_PLUGIN_SECRET_FILE, +from .fixtures import ( # noqa: E402, F401 app_client, app_client_base_url_prefix, app_client_conflicting_database_names, @@ -435,4 +315,5 @@ from .fixtures import ( # noqa: F401 app_client_with_dot, app_client_with_trace, make_app_client, + TEMP_PLUGIN_SECRET_FILE, ) diff --git a/tests/fixtures.py b/tests/fixtures.py index f8710ad4..8ab3633f 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,13 +1,3 @@ -import contextlib -import json -import os -import pathlib -import tempfile -import textwrap - -import click -import pytest - from datasette.app import Datasette from datasette.fixtures import ( EXTRA_DATABASE_SQL, @@ -15,6 +5,14 @@ from datasette.fixtures import ( write_fixture_database, ) from datasette.utils.testing import TestClient +import click +import contextlib +import json +import os +import pathlib +import pytest +import tempfile +import textwrap # This temp file is used by one of the plugin config tests TEMP_PLUGIN_SECRET_FILE = os.path.join(tempfile.gettempdir(), "plugin-secret") @@ -169,10 +167,12 @@ def make_app_client( template_dir=template_dir, crossdb=crossdb, ) - try: - yield TestClient(ds) - finally: - ds.close() + yield TestClient(ds) + # Close as many database connections as possible + # to try and avoid too many open files error + for db in ds.databases.values(): + if not db.is_memory: + db.close() @pytest.fixture(scope="session") @@ -184,10 +184,9 @@ def app_client(): @pytest.fixture(scope="session") def app_client_no_files(): ds = Datasette([]) - try: - yield TestClient(ds) - finally: - ds.close() + yield TestClient(ds) + for db in ds.databases.values(): + db.close() @pytest.fixture(scope="session") diff --git a/tests/plugins/my_plugin.py b/tests/plugins/my_plugin.py index baf20a77..c89ad0a3 100644 --- a/tests/plugins/my_plugin.py +++ b/tests/plugins/my_plugin.py @@ -1,16 +1,16 @@ import asyncio +from datasette import hookimpl +from datasette.facets import Facet +from datasette.tokens import TokenHandler +from datasette import tracer +from datasette.permissions import Action +from datasette.resources import DatabaseResource +from datasette.utils import path_with_added_args +from datasette.utils.asgi import asgi_send_json, Response import base64 import json import urllib.parse -from datasette import hookimpl, tracer -from datasette.facets import Facet -from datasette.permissions import Action -from datasette.resources import DatabaseResource -from datasette.tokens import TokenHandler -from datasette.utils import path_with_added_args -from datasette.utils.asgi import Response, asgi_send_json - @hookimpl def prepare_connection(conn, database, datasette): @@ -305,7 +305,11 @@ def startup(datasette): datasette._startup_hook_fired = True # And test some import shortcuts too - from datasette import Forbidden, NotFound, Response, actor_matches_allow, hookimpl + from datasette import Response + from datasette import Forbidden + from datasette import NotFound + from datasette import hookimpl + from datasette import actor_matches_allow _ = (Response, Forbidden, NotFound, hookimpl, actor_matches_allow) @@ -369,7 +373,7 @@ def table_actions(datasette, database, table, actor, request): "label": "Plugin button", "description": "Runs JavaScript from a plugin", "attrs": { - "aria-label": f"Plugin button for {table}", + "aria-label": "Plugin button for {}".format(table), "data-plugin-action": "plugin-button", "data-database": database, "data-table": table, diff --git a/tests/plugins/my_plugin_2.py b/tests/plugins/my_plugin_2.py index 26e45a5b..864637a6 100644 --- a/tests/plugins/my_plugin_2.py +++ b/tests/plugins/my_plugin_2.py @@ -1,10 +1,8 @@ -import json -from functools import wraps - -import markupsafe - from datasette import hookimpl from datasette.utils.asgi import Response +from functools import wraps +import markupsafe +import json @hookimpl @@ -35,7 +33,11 @@ def render_cell(value, database): if set(data.keys()) != {"href", "label"}: return None href = data["href"] - if not (href.startswith(("/", "http://", "https://"))): + if not ( + href.startswith("/") + or href.startswith("http://") + or href.startswith("https://") + ): return None return markupsafe.Markup( '{label}'.format( @@ -52,7 +54,7 @@ def extra_template_vars(template, database, table, view_name, request, datasette datasette._last_request = request async def query_database(sql): - first_db = next(iter(datasette.databases.keys())) + first_db = list(datasette.databases.keys())[0] return (await datasette.execute(first_db, sql)).rows[0][0] async def inner(): @@ -170,10 +172,10 @@ def register_routes(datasette): path = config["path"] def new_table(request): - return Response.text(f"/db/table: {sorted(request.url_vars.items())}") + return Response.text("/db/table: {}".format(sorted(request.url_vars.items()))) return [ - (rf"/{path}/$", lambda: Response.text(path.upper())), + (r"/{}/$".format(path), lambda: Response.text(path.upper())), # Also serves to demonstrate over-ride of default paths: (r"/(?P[^/]+)/(?P[^/]+?$)", new_table), ] diff --git a/tests/plugins/register_output_renderer.py b/tests/plugins/register_output_renderer.py index 671f2d1e..cfe15215 100644 --- a/tests/plugins/register_output_renderer.py +++ b/tests/plugins/register_output_renderer.py @@ -1,7 +1,6 @@ -import json - from datasette import hookimpl from datasette.utils.asgi import Response +import json async def can_render( @@ -19,7 +18,9 @@ async def can_render( "request": request, "view_name": view_name, } - return not request.args.get("_no_can_render") + if request.args.get("_no_can_render"): + return False + return True async def render_test_all_parameters( diff --git a/tests/plugins/sleep_sql_function.py b/tests/plugins/sleep_sql_function.py index 2fca1d66..d4b32a09 100644 --- a/tests/plugins/sleep_sql_function.py +++ b/tests/plugins/sleep_sql_function.py @@ -1,6 +1,5 @@ -import time - from datasette import hookimpl +import time @hookimpl diff --git a/tests/test_actions_sql.py b/tests/test_actions_sql.py index 76320bd1..a1fca971 100644 --- a/tests/test_actions_sql.py +++ b/tests/test_actions_sql.py @@ -10,11 +10,10 @@ These tests verify: import pytest import pytest_asyncio - -from datasette import hookimpl from datasette.app import Datasette from datasette.permissions import PermissionSQL from datasette.resources import DatabaseResource, QueryResource, TableResource +from datasette import hookimpl def test_resource_string_representations(): @@ -91,7 +90,7 @@ async def test_allowed_resources_global_allow(test_ds): assert all(isinstance(t, TableResource) for t in tables) # Check specific tables are present - table_set = {(t.parent, t.child) for t in tables} + table_set = set((t.parent, t.child) for t in tables) assert ("analytics", "events") in table_set assert ("analytics", "users") in table_set assert ("analytics", "sensitive") in table_set diff --git a/tests/test_actor_restriction_bug.py b/tests/test_actor_restriction_bug.py index 6e633ff6..0bfc9e1e 100644 --- a/tests/test_actor_restriction_bug.py +++ b/tests/test_actor_restriction_bug.py @@ -6,7 +6,6 @@ config allow blocks can bypass table-level restrictions. """ import pytest - from datasette.app import Datasette from datasette.resources import TableResource diff --git a/tests/test_allowed_many.py b/tests/test_allowed_many.py index 2f20e8e8..08b952fb 100644 --- a/tests/test_allowed_many.py +++ b/tests/test_allowed_many.py @@ -10,8 +10,6 @@ Layer 3: table/database views precompute all registered actions before import pytest import pytest_asyncio - -from datasette import hookimpl from datasette.app import Datasette from datasette.permissions import ( Action, @@ -20,6 +18,7 @@ from datasette.permissions import ( _permission_check_cache, ) from datasette.resources import DatabaseResource, TableResource +from datasette import hookimpl class CountingRulesPlugin: @@ -115,7 +114,7 @@ async def test_allowed_not_memoized_without_cache(counting_ds): async def test_cache_keyed_on_full_actor_identity(counting_ds): """Interleaved checks for different actors never share cache entries.""" # Uses drop-table because default permissions deny it to non-root actors - ds, _plugin = counting_ds + ds, plugin = counting_ds resource = TableResource("analytics", "users") token = _permission_check_cache.set({}) try: @@ -181,7 +180,7 @@ async def test_cache_keyed_on_resource(counting_ds): @pytest.mark.asyncio async def test_skip_permission_checks_bypasses_cache(counting_ds): - ds, _plugin = counting_ds + ds, plugin = counting_ds resource = TableResource("analytics", "users") token = _permission_check_cache.set({}) try: diff --git a/tests/test_allowed_resources.py b/tests/test_allowed_resources.py index e251deab..e247aa78 100644 --- a/tests/test_allowed_resources.py +++ b/tests/test_allowed_resources.py @@ -7,10 +7,9 @@ based on permission rules from plugins and configuration. import pytest import pytest_asyncio - -from datasette import hookimpl from datasette.app import Datasette from datasette.permissions import PermissionSQL +from datasette import hookimpl # Test plugin that provides permission rules diff --git a/tests/test_api.py b/tests/test_api.py index 690cb080..5ed14283 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,16 +1,13 @@ -import pathlib -import urllib - -import pytest - from datasette.app import Datasette from datasette.plugins import DEFAULT_PLUGINS -from datasette.resources import DatabaseResource, TableResource -from datasette.utils import UNSTABLE_API_MESSAGE, escape_sqlite, tilde_encode +from datasette.utils import UNSTABLE_API_MESSAGE from datasette.utils.sqlite import sqlite_version from datasette.version import __version__ - -from .fixtures import EXPECTED_PLUGINS, make_app_client +from .fixtures import make_app_client, EXPECTED_PLUGINS +import pathlib +import pytest +import sys +import urllib @pytest.mark.asyncio @@ -19,7 +16,7 @@ async def test_homepage(ds_client): assert response.status_code == 200 assert "application/json; charset=utf-8" == response.headers["content-type"] data = response.json() - assert sorted(data.get("metadata").keys()) == [ + assert sorted(list(data.get("metadata").keys())) == [ "about", "about_url", "description_html", @@ -102,11 +99,14 @@ async def test_database_page(ds_client): "tags", } - # The external-content index is visible, but its shadow tables need a - # second dependency hop and are excluded by the one-hop permission policy. + # Expected hidden tables expected_hidden_tables = { "no_primary_key", "searchable_fts", + "searchable_fts_config", + "searchable_fts_data", + "searchable_fts_docsize", + "searchable_fts_idx", } # Verify all expected tables exist @@ -384,7 +384,9 @@ async def test_row_pk_arity_mismatch_returns_400(ds_client, row_path, suffix): # because the SQL had one bind placeholder per PK column but params were # only bound for the supplied components. It should be a 400 instead, # mirroring the existing guard in datasette/views/table.py. - response = await ds_client.get(f"/fixtures/compound_primary_key/{row_path}{suffix}") + response = await ds_client.get( + "/fixtures/compound_primary_key/{}{}".format(row_path, suffix) + ) assert response.status_code == 400 if suffix == ".json": assert response.json()["ok"] is False @@ -456,67 +458,6 @@ async def test_row_foreign_key_tables(ds_client): ] -@pytest.mark.asyncio -async def test_row_foreign_key_tables_omit_denied_tables(request): - actor = {"id": "reader"} - ds = Datasette( - memory=True, - default_deny=True, - config={ - "databases": { - "data": { - "tables": { - "parents": {"permissions": {"view-table": True}}, - "private_children": {"permissions": {"view-table": False}}, - } - } - } - }, - ) - request.addfinalizer(ds.close) - db = ds.add_memory_database("fk_count_leak", name="data") - await db.execute_write("create table parents (id integer primary key, name text)") - await db.execute_write(""" - create table private_children ( - id integer primary key, - parent_id integer references parents(id) - ) - """) - await db.execute_write("insert into parents values (1, 'Public parent')") - await db.execute_write(""" - insert into private_children (id, parent_id) values - (1, 1), - (2, 1), - (3, 1) - """) - await ds.invoke_startup() - - parent = TableResource(database="data", table="parents") - private_children = TableResource(database="data", table="private_children") - assert await ds.allowed(action="view-table", resource=parent, actor=actor) - assert not await ds.allowed( - action="view-table", resource=private_children, actor=actor - ) - assert not await ds.allowed( - action="execute-sql", - resource=DatabaseResource(database="data"), - actor=actor, - ) - - direct_child = await ds.client.get("/data/private_children.json", actor=actor) - assert direct_child.status_code == 403 - parent_response = await ds.client.get( - "/data/parents/1.json?_extra=foreign_key_tables", actor=actor - ) - assert parent_response.status_code == 200 - - foreign_key_tables = parent_response.json().get("foreign_key_tables", []) - assert foreign_key_tables == [], ( - "denied child table name, foreign-key column, and row count disclosed: " - f"{foreign_key_tables}" - ) - - @pytest.mark.asyncio async def test_row_extras(ds_client): response = await ds_client.get( @@ -659,7 +600,8 @@ async def test_threads_json(ds_client): finally: ds_client.ds.root_enabled = False expected_keys = {"ok", "threads", "num_threads"} - expected_keys.update({"tasks", "num_tasks"}) + if sys.version_info >= (3, 7, 0): + expected_keys.update({"tasks", "num_tasks"}) data = response.json() assert set(data.keys()) == expected_keys # Should be at least one _execute_writes thread for __INTERNAL__ @@ -672,13 +614,13 @@ async def test_plugins_json(ds_client): response = await ds_client.get("/-/plugins.json") # Filter out TrackEventPlugin actual_plugins = sorted( - [p for p in response.json() if p["name"] != "TrackEventPlugin"], + [p for p in response.json()["plugins"] if p["name"] != "TrackEventPlugin"], key=lambda p: p["name"], ) assert EXPECTED_PLUGINS == actual_plugins # Try with ?all=1 response = await ds_client.get("/-/plugins.json?all=1") - names = {p["name"] for p in response.json()} + names = {p["name"] for p in response.json()["plugins"]} assert names.issuperset(p["name"] for p in EXPECTED_PLUGINS) assert names.issuperset(DEFAULT_PLUGINS) @@ -953,7 +895,10 @@ async def test_hidden_sqlite_stat1_table(): await db.execute_write("analyze") data = (await ds.client.get("/db.json?_show_hidden=1")).json() tables = [(t["name"], t["hidden"]) for t in data["tables"]] - assert tables == [("normal", False)] + assert tables in ( + [("normal", False), ("sqlite_stat1", True)], + [("normal", False), ("sqlite_stat1", True), ("sqlite_stat4", True)], + ) @pytest.mark.asyncio @@ -985,33 +930,6 @@ async def test_tilde_encoded_database_names(db_name): assert response2.status_code == 200 -@pytest.mark.asyncio -@pytest.mark.parametrize("table_name", ("[foo]", "foo]", "[foo]/bar")) -async def test_table_with_reserved_characters_in_name(table_name): - # Table names containing characters such as "]" that cannot be escaped - # using SQLite [bracket] quoting used to break schema introspection and - # the table page - https://github.com/simonw/datasette/issues/2431 - ds = Datasette() - db = ds.add_memory_database("test_reserved_table_names") - await db.execute_write( - f"create table {escape_sqlite(table_name)} (id integer primary key, name text)" - ) - await db.execute_write( - f"insert into {escape_sqlite(table_name)} (id, name) values (1, 'one')" - ) - # Schema introspection (populate_schema_tables) must not crash: - db_response = await ds.client.get("/test_reserved_table_names.json") - assert db_response.status_code == 200 - tables = {t["name"]: t for t in db_response.json()["tables"]} - assert tables[table_name]["count"] == 1 - # And the table page itself must load and return the row: - table_response = await ds.client.get( - f"/test_reserved_table_names/{tilde_encode(table_name)}.json?_shape=array" - ) - assert table_response.status_code == 200 - assert table_response.json() == [{"id": 1, "name": "one"}] - - @pytest.mark.asyncio @pytest.mark.parametrize( "config,expected", diff --git a/tests/test_api_write.py b/tests/test_api_write.py index 50bf1d67..a803fbbc 100644 --- a/tests/test_api_write.py +++ b/tests/test_api_write.py @@ -1,25 +1,21 @@ -import time - -import pytest -import sqlite_utils - from datasette.app import Datasette from datasette.events import RenameTableEvent from datasette.utils import error_body, escape_sqlite, sqlite3 - from .utils import last_event +import pytest +import time def assert_schema_contains(fragment, schema): - assert ( - fragment in schema - ), f"Expected schema to contain {fragment!r}, got {schema!r}" + assert fragment in schema, "Expected schema to contain {!r}, got {!r}".format( + fragment, schema + ) def assert_schema_not_contains(fragment, schema): assert ( fragment not in schema - ), f"Expected schema not to contain {fragment!r}, got {schema!r}" + ), "Expected schema not to contain {!r}, got {!r}".format(fragment, schema) @pytest.fixture @@ -51,114 +47,17 @@ def write_token(ds, actor_id="root", permissions=None): def _headers(token): return { - "Authorization": f"Bearer {token}", + "Authorization": "Bearer {}".format(token), "Content-Type": "application/json", } -@pytest.mark.asyncio -@pytest.mark.parametrize("operation", ["read", "read_row", "rename"]) -async def test_trailing_lf_table_permissions(tmp_path, operation): - # SQLite treats "secret" and "secret\n" as different table names. Permission - # checks and SQL execution must agree on which table a request targets. - db_path = tmp_path / "data.db" - conn = sqlite3.connect(str(db_path)) - conn.executescript( - "create table secret (id integer primary key, value text);" - "insert into secret values (1, 'private');" - ) - conn.close() - # Allow builder to create and use tables generally, but explicitly deny - # access to the existing secret table below. Disable arbitrary SQL access. - grants = { - action: {"id": "builder"} - for action in ( - "view-database", - "create-table", - "view-table", - "insert-row", - "alter-table", - ) - } - ds = Datasette( - [str(db_path)], - default_deny=True, - settings={"default_allow_sql": False}, - config={ - "permissions": {"view-instance": {"id": "builder"}}, - "databases": { - "data": { - "permissions": grants, - "tables": { - "secret": { - "permissions": { - "view-table": False, - "insert-row": False, - "alter-table": False, - } - } - }, - } - }, - }, - ) - headers = _headers(write_token(ds, actor_id="builder")) - try: - # Establish that the protected table is inaccessible before creating - # a second table whose name differs only by a trailing line feed. - response = await ds.client.get("/data/secret.json", headers=headers) - assert response.status_code == 403 - response = await ds.client.get( - "/data/-/query.json?sql=select+*+from+secret", headers=headers - ) - assert response.status_code == 403 - # Distinct values let us detect if an operation targets secret - # instead of the newly created secret\n table. - response = await ds.client.post( - "/data/-/create", - json={"table": "secret\n", "row": {"id": 1, "value": "decoy"}, "pk": "id"}, - headers=headers, - ) - assert response.status_code == 201, response.text - # ~0A is Datasette's URL encoding for the line feed in the table name. - if operation in ("read", "read_row"): - # Both table and row endpoints must return only the permitted row. - path = "/1.json" if operation == "read_row" else ".json" - response = await ds.client.get( - "/data/secret~0A" + path + "?_shape=array", headers=headers - ) - assert response.status_code == 200, response.text - assert response.json() == [{"id": 1, "value": "decoy"}] - else: - # Renaming must move the permitted table, preserving its contents - # and removing its old name from the database. - response = await ds.client.post( - "/data/secret~0A/-/alter", - json={"operations": [{"op": "rename_table", "args": {"to": "moved"}}]}, - headers=headers, - ) - assert response.status_code == 200, response.text - db = ds.get_database("data") - assert ( - await db.execute('select value from "moved"') - ).single_value() == "decoy" - assert "secret\n" not in await db.table_names() - # Verify that the protected table and its data are unchanged, and that - # the API still denies access to it. - db = ds.get_database("data") - assert ( - await db.execute('select value from "secret"') - ).single_value() == "private" - response = await ds.client.get("/data/secret.json", headers=headers) - assert response.status_code == 403 - finally: - ds.close() - - def _insert_and_fetch_created(conn, table, insert_sql): cursor = conn.execute(insert_sql) return conn.execute( - f"select created, typeof(created) from {escape_sqlite(table)} where rowid = ?", + "select created, typeof(created) from {} where rowid = ?".format( + escape_sqlite(table) + ), (cursor.lastrowid,), ).fetchone() @@ -167,82 +66,6 @@ BASE64_WRITE_API_VALUE = {"$base64": True, "encoded": "AAEC/f7/"} BASE64_WRITE_API_LITERAL = '{"$base64": true, "encoded": "AAEC/f7/"}' -@pytest.mark.asyncio -@pytest.mark.parametrize("use_fallback", (False, True)) -@pytest.mark.parametrize( - "operation", ("insert", "upsert", "update", "delete", "create", "create_uppercase") -) -@pytest.mark.parametrize( - "module,definition,values,shadow_suffix", - ( - ("fts5", "body", "'original'", "_content"), - ("fts4", "body", "'original'", "_content"), - ("rtree", "id, minx, maxx", "1, 0, 1", "_rowid"), - ), -) -@pytest.mark.parametrize("shadow", (False, True)) -async def test_structured_writes_require_ordinary_tables( - ds_write, - monkeypatch, - use_fallback, - operation, - module, - definition, - values, - shadow_suffix, - shadow, -): - if use_fallback: - monkeypatch.setattr("datasette.utils.sqlite.supports_table_list", lambda: False) - db = ds_write.get_database("data") - await db.execute_write(f"create virtual table indexed using {module}({definition})") - await db.execute_write(f"insert into indexed values ({values})") - table = "indexed" + (shadow_suffix if shadow else "") - row = (await db.execute(f"select rowid, * from {escape_sqlite(table)}")).dicts()[0] - pks = await db.primary_keys(table) - pk_value = row[pks[0] if pks else "rowid"] - before = await db.execute_fn(lambda conn: list(conn.iterdump())) - - if operation in ("create", "create_uppercase"): - path = "/data/-/create" - body = { - "table": table.upper() if operation == "create_uppercase" else table, - "rows": [row], - } - elif operation in ("update", "delete"): - path = f"/data/{table}/{pk_value}/-/{operation}" - body = {"update": row} if operation == "update" else {} - else: - path = f"/data/{table}/-/{operation}" - body = {"rows": [row]} - response = await ds_write.client.post( - path, json=body, headers=_headers(write_token(ds_write)) - ) - assert response.status_code == 400, response.text - assert response.json()["errors"] == ["Structured writes require an ordinary table"] - assert await db.execute_fn(lambda conn: list(conn.iterdump())) == before - - -@pytest.mark.asyncio -async def test_structured_writes_to_content_table_maintain_fts(ds_write): - db = ds_write.get_database("data") - await db.execute_write_fn( - lambda conn: sqlite_utils.Database(conn)["docs"].enable_fts( - ["title"], create_triggers=True - ) - ) - response = await ds_write.client.post( - "/data/docs/-/insert", - json={"row": {"id": 1, "title": "ordinary content"}}, - headers=_headers(write_token(ds_write)), - ) - assert response.status_code == 201, response.text - matches = await db.execute( - "select rowid from docs_fts where docs_fts match ?", ["ordinary"] - ) - assert [row[0] for row in matches.rows] == [1] - - @pytest.mark.asyncio async def test_base64_write_api_create_table_infers_blob_and_raw_escapes(ds_write): token = write_token(ds_write) @@ -418,7 +241,7 @@ async def test_insert_row(ds_write, content_type): "/data/docs/-/insert", json={"row": {"title": "Test", "score": 1.2, "age": 5}}, headers={ - "Authorization": f"Bearer {token}", + "Authorization": "Bearer {}".format(token), "Content-Type": content_type, }, ) @@ -463,7 +286,11 @@ async def test_insert_row_alter(ds_write): @pytest.mark.parametrize("return_rows", (True, False)) async def test_insert_rows(ds_write, return_rows): token = write_token(ds_write) - data = {"rows": [{"title": f"Test {i}", "score": 1.0, "age": 5} for i in range(20)]} + data = { + "rows": [ + {"title": "Test {}".format(i), "score": 1.0, "age": 5} for i in range(20) + ] + } if return_rows: data["return"] = True response = await ds_write.client.post( @@ -487,7 +314,8 @@ async def test_insert_rows(ds_write, return_rows): ).dicts() assert len(actual_rows) == 20 assert actual_rows == [ - {"id": i + 1, "title": f"Test {i}", "score": 1.0, "age": 5} for i in range(20) + {"id": i + 1, "title": "Test {}".format(i), "score": 1.0, "age": 5} + for i in range(20) ] assert response.json()["ok"] is True if return_rows: @@ -733,13 +561,13 @@ async def test_insert_or_upsert_row_errors( ) if special_case == "bad_token": token += "bad" - kwargs = { - "json": input, - "headers": { - "Authorization": f"Bearer {token}", + kwargs = dict( + json=input, + headers={ + "Authorization": "Bearer {}".format(token), "Content-Type": "application/json", }, - } + ) if special_case != "bad_token": actor_response = ( @@ -794,7 +622,7 @@ async def test_upsert_permissions_per_table(ds_write, allowed): "/data/docs/-/upsert", json={"rows": [{"id": 1, "title": "One"}]}, headers={ - "Authorization": f"Bearer {token}", + "Authorization": "Bearer {}".format(token), }, ) if allowed: @@ -1031,7 +859,9 @@ async def test_delete_row(ds_write, table, row_for_create, pks, delete_path): # Should be a single row assert ( await ds_write.client.get( - f"/data/-/query.json?_shape=arrayfirst&sql=select+count(*)+from+{table}" + "/data/-/query.json?_shape=arrayfirst&sql=select+count(*)+from+{}".format( + table + ) ) ).json() == [1] # Now delete the row @@ -1039,12 +869,14 @@ async def test_delete_row(ds_write, table, row_for_create, pks, delete_path): # Special case for that rowid table delete_path = ( await ds_write.client.get( - f"/data/-/query.json?_shape=arrayfirst&sql=select+rowid+from+{table}" + "/data/-/query.json?_shape=arrayfirst&sql=select+rowid+from+{}".format( + table + ) ) ).json()[0] delete_response = await ds_write.client.post( - f"/data/{table}/{delete_path}/-/delete", + "/data/{}/{}/-/delete".format(table, delete_path), headers=_headers(write_token(ds_write)), ) assert delete_response.status_code == 200 @@ -1057,7 +889,9 @@ async def test_delete_row(ds_write, table, row_for_create, pks, delete_path): assert event.pks == str(delete_path).split(",") assert ( await ds_write.client.get( - f"/data/-/query.json?_shape=arrayfirst&sql=select+count(*)+from+{table}" + "/data/-/query.json?_shape=arrayfirst&sql=select+count(*)+from+{}".format( + table + ) ) ).json() == [0] @@ -1107,7 +941,7 @@ async def test_update_row_invalid_key(ds_write): pk = await _insert_row(ds_write) - path = f"/data/docs/{pk}/-/update" + path = "/data/docs/{}/-/update".format(pk) response = await ds_write.client.post( path, json={"update": {"title": "New title"}, "bad_key": 1}, @@ -1126,7 +960,7 @@ async def test_update_row_invalid_key(ds_write): async def test_update_row_alter(ds_write): token = write_token(ds_write, permissions=["ur", "at"]) pk = await _insert_row(ds_write) - path = f"/data/docs/{pk}/-/update" + path = "/data/docs/{}/-/update".format(pk) response = await ds_write.client.post( path, json={"update": {"title": "New title", "extra": "extra"}, "alter": True}, @@ -1282,9 +1116,9 @@ async def test_alter_table_integer_default_expr( assert expected_schema in data["schema"] columns = await db.execute("select * from pragma_table_info('docs')") - created_column = next( + created_column = [ column for column in columns.dicts() if column["name"] == "created" - ) + ][0] assert created_column["type"] == "INTEGER" assert expected_schema in created_column["dflt_value"] @@ -1471,7 +1305,7 @@ async def test_alter_table_foreign_key_without_fk_column_requires_single_pk(ds_w @pytest.mark.asyncio async def test_foreign_key_suggestions(ds_write): - token = write_token(ds_write, permissions=["alter-table", "view-table"]) + token = write_token(ds_write, permissions=["at"]) db = ds_write.get_database("data") await db.execute_write("create table owners (id integer primary key)") await db.execute_write("insert into owners (id) values (1), (2), (3)") @@ -1537,7 +1371,7 @@ async def test_foreign_key_suggestions_permission_denied(ds_write): @pytest.mark.asyncio async def test_foreign_key_suggestions_fail_open(ds_write, monkeypatch): - token = write_token(ds_write, permissions=["alter-table", "view-table"]) + token = write_token(ds_write, permissions=["at"]) db = ds_write.get_database("data") await db.execute_write("create table owners (id integer primary key)") @@ -1568,7 +1402,7 @@ async def test_foreign_key_suggestions_fail_open(ds_write, monkeypatch): @pytest.mark.asyncio async def test_foreign_key_targets(ds_write): - token = write_token(ds_write, permissions=["create-table", "view-table"]) + token = write_token(ds_write, permissions=["ct"]) db = ds_write.get_database("data") await db.execute_write("create table owners (id integer primary key)") await db.execute_write("create table categories (slug varchar(30) primary key)") @@ -1585,8 +1419,7 @@ async def test_foreign_key_targets(ds_write): await db.execute_write("create table no_pk (name text)") try: await db.execute_write("create virtual table search_docs using fts5(body)") - except Exception: # noqa: BLE001, S110 - # FTS5 is not available in every SQLite build + except Exception: pass response = await ds_write.client.get( @@ -1792,7 +1625,7 @@ async def test_update_row(ds_write, input, expected_errors, use_return): token = write_token(ds_write) pk = await _insert_row(ds_write) - path = f"/data/docs/{pk}/-/update" + path = "/data/docs/{}/-/update".format(pk) data = {"update": input} if use_return: @@ -1827,7 +1660,7 @@ async def test_update_row(ds_write, input, expected_errors, use_return): # And fetch the row to check it's updated response = await ds_write.client.get( - f"/data/docs/{pk}.json?_shape=array", + "/data/docs/{}.json?_shape=array".format(pk), ) assert response.status_code == 200 row = response.json()[0] @@ -1901,42 +1734,6 @@ async def test_drop_table(ds_write, scenario): assert (await ds_write.client.get("/data/docs")).status_code == 404 -@pytest.mark.asyncio -async def test_drop_table_cleans_up_fts(ds_write): - db = ds_write.get_database("data") - - def enable_fts(conn): - sqlite_utils.Database(conn)["docs"].enable_fts(["title"], create_triggers=True) - - await db.execute_write_fn(enable_fts) - assert { - row[0] - for row in await db.execute( - "select name from sqlite_master where type = 'table' and name like 'docs_fts%'" - ) - } == { - "docs_fts", - "docs_fts_config", - "docs_fts_data", - "docs_fts_docsize", - "docs_fts_idx", - } - - response = await ds_write.client.post( - "/data/docs/-/drop", - json={"confirm": True}, - headers=_headers(write_token(ds_write)), - ) - - assert response.json() == {"ok": True} - assert [ - row[0] - for row in await db.execute( - "select name from sqlite_master where type = 'table' and name like 'docs_fts%'" - ) - ] == [] - - @pytest.mark.asyncio @pytest.mark.parametrize( "input,expected_status,expected_response,expected_events", @@ -2509,7 +2306,7 @@ async def test_create_table_integer_default_expr( ds_write, default_expr, minimum_value, expected_schema ): token = write_token(ds_write) - table = f"default_{default_expr}" + table = "default_{}".format(default_expr) response = await ds_write.client.post( "/data/-/create", json={ @@ -2537,7 +2334,7 @@ async def test_create_table_integer_default_expr( row = await db.execute_write_fn( lambda conn: _insert_and_fetch_created( - conn, table, f"insert into {escape_sqlite(table)} default values" + conn, table, "insert into {} default values".format(escape_sqlite(table)) ) ) assert row[0] > minimum_value @@ -2920,119 +2717,3 @@ async def test_create_using_alter_against_existing_table( insert_rows_event = ds_write._tracked_events[1] assert insert_rows_event.name == "insert-rows" assert insert_rows_event.num_rows == 1 - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - ("denied_action", "request_body"), - ( - ( - "insert-row", - { - "table": "salaries", - "rows": [{"id": 9, "note": "INJ-VIA-CREATE"}], - }, - ), - ( - "update-row", - { - "table": "salaries", - "rows": [{"id": 1, "note": "REPLACED"}], - "pk": "id", - "replace": True, - }, - ), - ( - "alter-table", - { - "table": "salaries", - "rows": [{"id": 9, "note": "INSERTED", "extra": "NEW"}], - "alter": True, - }, - ), - ), -) -async def test_create_table_existing_table_respects_table_level_denial( - denied_action, request_body -): - # GHSA-53fc-rhfg-h7qp issue 2: POST /db/-/create against an existing table - # inserts rows into it, so insert-row (and update-row / alter-table) must be - # checked against the TableResource, not just the DatabaseResource. - ds = Datasette( - memory=True, - config={ - "databases": { - # id=editor user has each permission at the database level, but - # the selected action is explicitly denied on the salaries table - "data": { - "permissions": { - "create-table": {"id": "editor"}, - "insert-row": {"id": "editor"}, - "update-row": {"id": "editor"}, - "alter-table": {"id": "editor"}, - }, - "tables": { - "salaries": {"permissions": {denied_action: False}}, - }, - } - } - }, - ) - db = ds.add_memory_database( - f"create_table_existing_table_denied_{denied_action}", name="data" - ) - await db.execute_write("create table salaries (id integer primary key, note text)") - await db.execute_write("insert into salaries values (1, 'TOPSECRET-A')") - await ds.invoke_startup() - - if denied_action == "insert-row": - # Sanity: direct insert into salaries is denied for this actor - direct = await ds.client.post( - "/data/salaries/-/insert", - actor={"id": "editor"}, - json={"row": {"id": 9, "note": "INJ-DIRECT"}}, - ) - assert direct.status_code == 403 - - response = await ds.client.post( - "/data/-/create", - actor={"id": "editor"}, - json=request_body, - ) - assert response.status_code == 403, response.json() - assert response.json()["errors"] == [f"Permission denied: need {denied_action}"] - rows = (await db.execute("select id, note from salaries order by id")).rows - assert [tuple(r) for r in rows] == [(1, "TOPSECRET-A")] - assert await db.table_columns("salaries") == ["id", "note"] - - -@pytest.mark.asyncio -async def test_create_table_respects_predeclared_table_level_denial(): - ds = Datasette( - memory=True, - config={ - "databases": { - "data": { - "permissions": { - "create-table": {"id": "editor"}, - "insert-row": {"id": "editor"}, - }, - "tables": { - "planned_table": {"permissions": {"insert-row": False}}, - }, - } - } - }, - ) - db = ds.add_memory_database("create_table_predeclared_denial", name="data") - await ds.invoke_startup() - - response = await ds.client.post( - "/data/-/create", - actor={"id": "editor"}, - json={"table": "planned_table", "rows": [{"id": 1}]}, - ) - - assert response.status_code == 403, response.json() - assert response.json()["errors"] == ["Permission denied: need insert-row"] - assert not await db.table_exists("planned_table") diff --git a/tests/test_auth.py b/tests/test_auth.py index 68a2e6fd..d2913ecc 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,18 +1,14 @@ -import time -from unittest.mock import AsyncMock - -import pytest from bs4 import BeautifulSoup as Soup +from .utils import cookie_was_deleted, last_event from click.testing import CliRunner - +from datasette.utils import baseconv from datasette.cli import cli from datasette.resources import ( DatabaseResource, TableResource, ) -from datasette.utils import baseconv - -from .utils import cookie_was_deleted, last_event +import pytest +import time @pytest.mark.asyncio @@ -208,7 +204,7 @@ def test_auth_create_token( assert response2.status == 200 if errors: for error in errors: - assert f'

{error}

' in response2.text + assert '

{}

'.format(error) in response2.text else: # Check create-token event event = last_event(app_client.ds) @@ -232,41 +228,12 @@ def test_auth_create_token( # And test that token response3 = app_client.get( "/-/actor.json", - headers={"Authorization": "Bearer {}".format(f"dstok_{token}")}, + headers={"Authorization": "Bearer {}".format("dstok_{}".format(token))}, ) assert response3.status == 200 assert response3.json["actor"]["id"] == "test" -@pytest.mark.asyncio -@pytest.mark.parametrize("method", ["GET", "POST"]) -@pytest.mark.parametrize( - "restrictions", - [ - {}, - {"a": ["vi"]}, - {"d": {"db": ["vd"]}}, - {"r": {"db": {"t1": ["vt"]}}}, - ], - ids=["empty", "instance", "database", "table"], -) -async def test_auth_create_token_not_allowed_for_restricted_actors( - bare_ds, monkeypatch, method, restrictions -): - create_token = AsyncMock() - monkeypatch.setattr(bare_ds, "create_token", create_token) - - response = await bare_ds.client.request( - method, - "/-/create-token", - actor={"id": "test", "_r": restrictions}, - ) - - assert response.status_code == 403 - assert "Restricted actors cannot create API tokens" in response.text - create_token.assert_not_called() - - @pytest.mark.asyncio async def test_auth_create_token_not_allowed_for_tokens(ds_client): ds_tok = ds_client.ds.sign( @@ -274,7 +241,7 @@ async def test_auth_create_token_not_allowed_for_tokens(ds_client): ) response = await ds_client.get( "/-/create-token", - headers={"Authorization": f"Bearer dstok_{ds_tok}"}, + headers={"Authorization": "Bearer dstok_{}".format(ds_tok)}, ) assert response.status_code == 403 @@ -319,12 +286,12 @@ async def test_auth_with_dstok_token(ds_client, scenario, should_work): elif scenario == "invalid_token": token = "invalid" if token: - token = f"dstok_{token}" + token = "dstok_{}".format(token) if scenario == "allow_signed_tokens_off": ds_client.ds._settings["allow_signed_tokens"] = False headers = {} if token: - headers["Authorization"] = f"Bearer {token}" + headers["Authorization"] = "Bearer {}".format(token) response = await ds_client.get("/-/actor.json", headers=headers) try: if should_work: @@ -371,7 +338,7 @@ def test_cli_create_token(app_client, expires): assert details.keys() == expected_keys assert details["a"] == "test" response = app_client.get( - "/-/actor.json", headers={"Authorization": f"Bearer {token}"} + "/-/actor.json", headers={"Authorization": "Bearer {}".format(token)} ) if expires is None or expires > 0: expected_actor = { @@ -554,25 +521,3 @@ async def test_root_without_root_enabled_no_special_permissions(ds_client): ) is not True ), "Root without root_enabled should not automatically get set-column-type" - - -@pytest.mark.parametrize("expire_after", (1, 300, 3600, 30 * 24 * 60 * 60)) -def test_set_actor_cookie_honours_expire_after(expire_after): - # GHSA-53fc-rhfg-h7qp issue 4: expire_after is documented as a number of - # seconds, but every value was being replaced with 24 hours. - from datasette.app import Datasette - from datasette.utils.asgi import Response - - ds = Datasette(memory=True) - response = Response.text("") - before = int(time.time()) - ds.set_actor_cookie(response, {"id": "test"}, expire_after=expire_after) - after = int(time.time()) - - (header,) = response._set_cookie_headers - assert header.startswith("ds_actor=") - value = header[len("ds_actor=") :].split(";", 1)[0] - data = ds.unsign(value, "actor") - assert data["a"] == {"id": "test"} - expires_at = baseconv.base62.decode(data["e"]) - assert before + expire_after <= expires_at <= after + expire_after diff --git a/tests/test_background_tasks.py b/tests/test_background_tasks.py deleted file mode 100644 index c0d7ce49..00000000 --- a/tests/test_background_tasks.py +++ /dev/null @@ -1,381 +0,0 @@ -""" -Tests for datasette.add_background_task() / start_background_tasks() and the -BackgroundTask / BackgroundTaskSupervisor machinery in -datasette/background_tasks.py. -""" - -import asyncio -import contextlib -import logging - -import httpx2 -import pytest - -from datasette import hookimpl -from datasette.app import Datasette -from datasette.plugins import pm - - -async def _drive_lifespan_startup(app): - """Send a single lifespan.startup message into app's ASGI lifespan loop - and return the list of messages sent back, without ever sending - lifespan.shutdown. Copied from tests/test_lifespan.py's helper of the - same name - mirrors what a real server does: after startup completes - it parks waiting for the next event, and we cancel that wait once - we've observed the startup response. - """ - messages_sent = [] - startup_responded = asyncio.Event() - delivered = False - - async def receive(): - nonlocal delivered - if not delivered: - delivered = True - return {"type": "lifespan.startup"} - await asyncio.Event().wait() - - async def send(message): - messages_sent.append(message) - startup_responded.set() - - task = asyncio.create_task(app({"type": "lifespan"}, receive, send)) - try: - await asyncio.wait_for(startup_responded.wait(), timeout=5) - finally: - task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await task - return messages_sent - - -@pytest.mark.asyncio -async def test_tasks_registered_in_startup_hook_run_after_lifespan_startup(): - # Two tasks registered by one plugin's startup hook - order preserved, - # both running after lifespan startup completes, and no HTTP request - # of any kind is issued anywhere in this test. - events = [] - - async def task_one(datasette): - events.append("task_one") - # Wait indefinitely to simulate long-lived background work, keeping - # the task "running" for the assertions below until cleanup cancels it. - await asyncio.Event().wait() - - async def task_two(datasette): - events.append("task_two") - await asyncio.Event().wait() - - class TwoTaskPlugin: - __name__ = "TwoTaskPlugin" - - @hookimpl - def startup(self, datasette): - async def inner(): - datasette.add_background_task(task_one, name="task-one") - datasette.add_background_task(task_two, name="task-two") - - return inner - - ds = Datasette(memory=True) - pm.register(TwoTaskPlugin(), name="two_task_plugin") - try: - app = ds.app() - messages = await _drive_lifespan_startup(app) - assert {"type": "lifespan.startup.complete"} in messages - - handles = ds._background_tasks.tasks() - assert [h.name for h in handles] == ["task-one", "task-two"] - - # Let both tasks run their first line of code. - await asyncio.sleep(0) - assert handles[0].state == "running" - assert handles[1].state == "running" - assert events == ["task_one", "task_two"] - finally: - pm.unregister(name="two_task_plugin") - await ds._background_tasks.cancel_all(grace=1.0) - - -@pytest.mark.asyncio -async def test_launch_waits_for_every_startup_hook_before_running_any_task(): - # PluginA registers a task from its startup hook; PluginB does the - # same from ITS startup hook, which runs after PluginA's (forced with - # tryfirst=True on A). Even though A's registration happens first, - # A's task body must not actually execute until every startup hook - - # including B's - has finished, since launch only happens after - # invoke_startup() completes. This is the ordering guarantee that - # dissolves datasette-cron's tryfirst=True launch hack. - hook_call_order = [] - seen_names_when_a_ran = {} - - async def task_a(datasette): - seen_names_when_a_ran["names"] = [ - h.name for h in datasette._background_tasks.tasks() - ] - - async def task_b(datasette): - pass - - class PluginA: - __name__ = "PluginA" - - @hookimpl(tryfirst=True) - def startup(self, datasette): - async def inner(): - hook_call_order.append("A") - datasette.add_background_task(task_a, name="task-a") - - return inner - - class PluginB: - __name__ = "PluginB" - - @hookimpl - def startup(self, datasette): - async def inner(): - hook_call_order.append("B") - datasette.add_background_task(task_b, name="task-b") - - return inner - - ds = Datasette(memory=True) - pm.register(PluginA(), name="plugin_a") - pm.register(PluginB(), name="plugin_b") - try: - await ds.start_background_tasks() - # Confirm A's startup hook really did run (and register task-a) - # strictly before B's startup hook ran. - assert hook_call_order == ["A", "B"] - - handles = ds._background_tasks.tasks() - await asyncio.wait_for(asyncio.gather(*[h.task for h in handles]), timeout=5) - # Yet by the time task-a's own body executed (after launch, which - # only happens once every startup hook - including B's - has - # finished), task-b was already registered. - assert "task-b" in seen_names_when_a_ran["names"] - finally: - pm.unregister(name="plugin_a") - pm.unregister(name="plugin_b") - - -@pytest.mark.asyncio -async def test_concurrent_first_requests_launch_background_tasks_exactly_once(): - launch_count = {"n": 0} - - async def counting_task(datasette): - launch_count["n"] += 1 - - class CountingTaskPlugin: - __name__ = "CountingTaskPlugin" - - @hookimpl - def startup(self, datasette): - async def inner(): - datasette.add_background_task(counting_task, name="counting-task") - - return inner - - ds = Datasette(memory=True) - pm.register(CountingTaskPlugin(), name="counting_task_plugin") - try: - app = ds.app() - transport = httpx2.ASGITransport(app=app) - async with httpx2.AsyncClient( - transport=transport, base_url="http://localhost" - ) as client: - responses = await asyncio.gather( - *[client.get("/-/versions.json") for _ in range(10)] - ) - assert all(response.status_code == 200 for response in responses) - - handles = ds._background_tasks.tasks() - assert len(handles) == 1 - await asyncio.wait_for(handles[0].task, timeout=5) - assert launch_count["n"] == 1 - finally: - pm.unregister(name="counting_task_plugin") - - -@pytest.mark.asyncio -async def test_post_launch_registration_starts_immediately_and_cancel_works(): - ds = Datasette(memory=True) - await ds.start_background_tasks() # nothing registered yet, but launched - - started = asyncio.Event() - - async def long_running(datasette): - started.set() - await asyncio.Event().wait() - - handle = ds.add_background_task(long_running, name="dynamic-task") - # Registered after launch: starts immediately rather than sitting in - # "registered" limbo. - assert handle.state == "running" - assert handle.task is not None - - await asyncio.wait_for(started.wait(), timeout=5) - assert handle.state == "running" - - handle.cancel() - with pytest.raises(asyncio.CancelledError): - await handle.task - await asyncio.sleep(0) - assert handle.state == "cancelled" - - -@pytest.mark.asyncio -async def test_pre_launch_registration_starts_as_registered(): - ds = Datasette(memory=True) - - async def task(datasette): - pass - - handle = ds.add_background_task(task, name="buffered-task") - assert handle.state == "registered" - assert handle.task is None - - handle.cancel() # not yet launched: deregisters instead of cancelling - assert handle not in ds._background_tasks.tasks() - - -@pytest.mark.asyncio -async def test_crashing_task_logs_traceback_and_state_is_crashed(caplog): - ds = Datasette(memory=True) - await ds.start_background_tasks() - - survivor_ran = asyncio.Event() - - async def crashing_task(datasette): - raise RuntimeError("kaboom") - - async def survivor(datasette): - survivor_ran.set() - - with caplog.at_level(logging.ERROR, logger="datasette.background_tasks"): - crash_handle = ds.add_background_task(crashing_task, name="crashing_task") - survivor_handle = ds.add_background_task(survivor, name="survivor") - await asyncio.wait_for( - asyncio.gather( - crash_handle.task, survivor_handle.task, return_exceptions=True - ), - timeout=5, - ) - - assert crash_handle.state == "crashed" - assert isinstance(crash_handle.exception, RuntimeError) - assert str(crash_handle.exception) == "kaboom" - - # The crash must not affect any other task. - assert survivor_ran.is_set() - assert survivor_handle.state == "completed" - - assert "crashing_task" in caplog.text - assert "kaboom" in caplog.text - assert "Traceback" in caplog.text - assert "RuntimeError" in caplog.text - - -def test_name_collisions_get_suffixed_and_explicit_names_are_respected(): - ds = Datasette(memory=True) - - async def noop(datasette): - pass - - async def another_noop(datasette): - pass - - h1 = ds.add_background_task(noop, name="dup") - h2 = ds.add_background_task(another_noop, name="dup") - h3 = ds.add_background_task(noop, name="dup") - assert [h1.name, h2.name, h3.name] == ["dup", "dup-2", "dup-3"] - - h_explicit = ds.add_background_task(noop, name="explicit-name") - assert h_explicit.name == "explicit-name" - - h_default = ds.add_background_task(noop) - assert h_default.name == noop.__qualname__ - - -@pytest.mark.asyncio -async def test_start_background_tasks_on_bare_datasette(): - # The headless-CLI path (datasette-rss's `fetch --due` shape): no - # server, no lifespan, no first HTTP request - just an explicit call. - ran = asyncio.Event() - - async def task(datasette): - ran.set() - - ds = Datasette([]) - assert ds._startup_invoked is False - - handle = ds.add_background_task(task, name="headless-task") - assert handle.state == "registered" - - await ds.start_background_tasks() - - assert ds._startup_invoked is True - await asyncio.wait_for(ran.wait(), timeout=5) - await asyncio.wait_for(handle.task, timeout=5) - # handle.task being done only guarantees the coroutine has returned, - # not that our done-callback (which updates handle.state) has run yet - - # asyncio schedules done-callbacks via call_soon, and awaiting an - # already-done future/task returns immediately without giving the loop - # a chance to drain its ready queue. Yield once to let it run. - await asyncio.sleep(0) - assert handle.state == "completed" - - -@pytest.mark.asyncio -async def test_cancel_all_cancels_running_tasks_and_leaves_completed_alone(): - ds = Datasette(memory=True) - await ds.start_background_tasks() - - async def forever(datasette): - await asyncio.Event().wait() - - async def quick(datasette): - return "done" - - forever_handle = ds.add_background_task(forever, name="forever") - quick_handle = ds.add_background_task(quick, name="quick") - await asyncio.wait_for(quick_handle.task, timeout=5) - assert quick_handle.state == "completed" - - await ds._background_tasks.cancel_all(grace=1.0) - - assert forever_handle.state == "cancelled" - assert quick_handle.state == "completed" - - -@pytest.mark.asyncio -async def test_cancel_all_logs_stragglers_that_outlive_the_grace_period(caplog): - ds = Datasette(memory=True) - await ds.start_background_tasks() - - async def stubborn(datasette): - with contextlib.suppress(asyncio.CancelledError): - await asyncio.sleep(10) - # Swallowing CancelledError above and returning normally simulates - # a task that ignores cancellation for longer than the grace period. - await asyncio.sleep(10) - - handle = ds.add_background_task(stubborn, name="stubborn-task") - # Let the task actually start running and reach its first sleep (inside - # the CancelledError-suppressing block) before cancelling it - a task - # cancelled before it has ever run its first step never enters that - # block at all (the throw happens before the coroutine body starts), - # so it would finish cancelling immediately instead of behaving like a - # straggler. - await asyncio.sleep(0) - - with caplog.at_level(logging.WARNING, logger="datasette.background_tasks"): - await ds._background_tasks.cancel_all(grace=0.1) - - assert "stubborn-task" in caplog.text - - # Clean up: actually cancel it now that the test has made its - # assertion, so it doesn't leak past the end of the test. - handle.task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await handle.task diff --git a/tests/test_base_view.py b/tests/test_base_view.py index b46f7ce1..c1b0cf20 100644 --- a/tests/test_base_view.py +++ b/tests/test_base_view.py @@ -1,10 +1,8 @@ -import json - -import pytest - +from datasette.views.base import View from datasette import Request, Response from datasette.app import Datasette -from datasette.views.base import View +import json +import pytest class GetView(View): diff --git a/tests/test_cli.py b/tests/test_cli.py index fbd4a8a9..cbd8edad 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,28 +1,23 @@ +from .fixtures import ( + make_app_client, + TestClient as _TestClient, + EXPECTED_PLUGINS, +) +from datasette.app import SETTINGS +from datasette.plugins import DEFAULT_PLUGINS, pm +from datasette.cli import cli, serve +from datasette.version import __version__ +from datasette.utils import tilde_encode +from datasette.utils.sqlite import sqlite3 +from click.testing import CliRunner import io import json import pathlib +import pytest import sys import textwrap from unittest import mock -import pytest -from click.testing import CliRunner - -from datasette.app import SETTINGS -from datasette.cli import cli, serve -from datasette.plugins import DEFAULT_PLUGINS, pm -from datasette.utils import tilde_encode -from datasette.utils.sqlite import sqlite3 -from datasette.version import __version__ - -from .fixtures import ( - EXPECTED_PLUGINS, - make_app_client, -) -from .fixtures import ( - TestClient as _TestClient, -) - def test_inspect_cli(app_client): runner = CliRunner() @@ -465,7 +460,7 @@ def test_serve_create(tmpdir): @pytest.mark.parametrize("argument", ("-c", "--config")) @pytest.mark.parametrize("format_", ("json", "yaml")) def test_serve_config(tmpdir, argument, format_): - config_path = tmpdir / f"datasette.{format_}" + config_path = tmpdir / "datasette.{}".format(format_) config_path.write_text( ( "settings:\n default_page_size: 5\n" @@ -518,13 +513,13 @@ def test_weird_database_names(tmpdir, filename): result1 = runner.invoke(cli, [db_path, "--get", "/"]) assert result1.exit_code == 0, result1.output filename_no_stem = filename.rsplit(".", 1)[0] - expected_link = ( - f'{filename_no_stem}' + expected_link = '{}'.format( + tilde_encode(filename_no_stem), filename_no_stem ) assert expected_link in result1.output # Now try hitting that database page result2 = runner.invoke( - cli, [db_path, "--get", f"/{tilde_encode(filename_no_stem)}"] + cli, [db_path, "--get", "/{}".format(tilde_encode(filename_no_stem))] ) assert result2.exit_code == 0, result2.output diff --git a/tests/test_cli_serve_get.py b/tests/test_cli_serve_get.py index 562bd7aa..fe9416d6 100644 --- a/tests/test_cli_serve_get.py +++ b/tests/test_cli_serve_get.py @@ -1,10 +1,8 @@ -import json -import textwrap - -from click.testing import CliRunner - from datasette.cli import cli from datasette.plugins import pm +from click.testing import CliRunner +import textwrap +import json def test_serve_with_get(tmp_path_factory): @@ -46,59 +44,9 @@ def test_serve_with_get(tmp_path_factory): # Annoyingly that new test plugin stays resident - we need # to manually unregister it to avoid conflict with other tests - to_unregister = next( + to_unregister = [ p for p in pm.get_plugins() if p.__name__ == "init_for_serve_with_get.py" - ) - pm.unregister(to_unregister) - - -def test_serve_with_get_does_not_launch_background_tasks(tmp_path_factory): - # --get must never launch background tasks, even though its TestClient - # request - # flows through the full ASGI stack (including the AsgiRunOnFirstRequest - # fallback that would otherwise launch them). The plugin's startup hook - # itself still runs (registration happens) - only the launch is - # suppressed, so the sentinel file the background task would write must - # never appear. - plugins_dir = tmp_path_factory.mktemp("plugins_for_get_background_tasks") - sentinel = plugins_dir / "sentinel.txt" - (plugins_dir / "bg_task_for_get.py").write_text( - textwrap.dedent( - f""" - from datasette import hookimpl - - @hookimpl - def startup(datasette): - async def inner(): - async def task(datasette): - with open("{sentinel!s}", "w") as fp: - fp.write("ran") - - datasette.add_background_task(task, name="get-sentinel-task") - - return inner - """, - ), - "utf-8", - ) - runner = CliRunner() - result = runner.invoke( - cli, - [ - "serve", - "--memory", - "--plugins-dir", - str(plugins_dir), - "--get", - "/_memory/-/query.json?sql=select+1", - ], - ) - assert result.exit_code == 0, result.output - assert not sentinel.exists() - - to_unregister = next( - p for p in pm.get_plugins() if p.__name__ == "bg_task_for_get.py" - ) + ][0] pm.unregister(to_unregister) diff --git a/tests/test_cli_serve_server.py b/tests/test_cli_serve_server.py index 163d3fcc..47f23c08 100644 --- a/tests/test_cli_serve_server.py +++ b/tests/test_cli_serve_server.py @@ -1,15 +1,11 @@ -import signal -import socket -import subprocess -import time - -import httpx2 +import httpx import pytest +import socket @pytest.mark.serial def test_serve_localhost_http(ds_localhost_http_server): - response = httpx2.get("http://localhost:8041/_memory.json") + response = httpx.get("http://localhost:8041/_memory.json") assert { "database": "_memory", "path": "/_memory", @@ -23,205 +19,11 @@ def test_serve_localhost_http(ds_localhost_http_server): ) def test_serve_unix_domain_socket(ds_unix_domain_socket_server): _, uds = ds_unix_domain_socket_server - transport = httpx2.HTTPTransport(uds=uds) - with httpx2.Client(transport=transport) as client: - response = client.get("http://localhost/_memory.json") + transport = httpx.HTTPTransport(uds=uds) + client = httpx.Client(transport=transport) + response = client.get("http://localhost/_memory.json") assert { "database": "_memory", "path": "/_memory", "tables": [], }.items() <= response.json().items() - - -# Shaped after datasette-litestream's startup hook, which schedules a -# background task with asyncio.get_running_loop().create_task(...): -# https://github.com/datasette/datasette-litestream -MARKER_TASK_PLUGIN = """ -import asyncio -from datasette import hookimpl -from datasette.utils.asgi import Response - - -@hookimpl -def startup(datasette): - datasette._startup_calls = getattr(datasette, "_startup_calls", 0) + 1 - - async def _mark(): - # Must await before setting the flag: a task with no internal - # await point could finish on the throwaway loop before it - # closed, masking the regression this test guards against. - await asyncio.sleep(0.2) - datasette._marker_task_ran = True - - asyncio.get_running_loop().create_task(_mark()) - - -@hookimpl -def register_routes(): - async def marker_status(datasette): - return Response.json( - { - "marker_task_ran": getattr(datasette, "_marker_task_ran", False), - "startup_calls": getattr(datasette, "_startup_calls", 0), - } - ) - - return [(r"^/-/marker-task-ran$", marker_status)] -""" - - -STARTUP_ERROR_PLUGIN = """ -from datasette import hookimpl -from datasette.utils import StartupError - - -@hookimpl -def startup(datasette): - raise StartupError("boom from plugin") -""" - - -@pytest.mark.serial -def test_startup_hook_background_task_runs_on_serving_loop(serve_with_plugins): - """ - Litestream-shaped regression test: a startup hook that does - asyncio.get_running_loop().create_task(...) must have that task - actually execute before/while the server is handling requests. This - only holds if invoke_startup() and uvicorn.Server.serve() share one - event loop. This test fails against unmodified main, where - invoke_startup() runs on a throwaway loop that is closed before - uvicorn opens its own loop to serve. - """ - _, port = serve_with_plugins({"marker_task_plugin": MARKER_TASK_PLUGIN}) - # The fixture has already waited for the server to answer requests. The - # marker task deliberately awaits before setting its flag, so poll for a - # moment rather than assuming it landed before the first request arrived. - deadline = time.time() + 3.0 - payload = {} - while time.time() < deadline: - payload = httpx2.get( - f"http://127.0.0.1:{port}/-/marker-task-ran", timeout=1.0 - ).json() - if payload["marker_task_ran"]: - break - time.sleep(0.05) - assert payload.get("marker_task_ran"), ( - "The startup hook's asyncio.create_task(...) never ran - " - "invoke_startup() and the server are not sharing an event loop" - ) - # Polling above means this test would also pass if the startup hook were - # re-run on the serving loop by the first-request fallback - which would - # hide exactly the bug being tested. invoke_startup() is idempotent today - # so that cannot happen; assert it explicitly so that if the idempotency - # guard is ever removed this test fails loudly instead of silently - # becoming a no-op. - assert payload["startup_calls"] == 1, ( - "startup hook ran {} times - the marker may have been set by a " - "re-run on the serving loop rather than by the original task".format( - payload["startup_calls"] - ) - ) - - -@pytest.mark.serial -def test_startup_error_fails_fast_before_port_binds(serve_with_plugins): - """ - A "startup" plugin hook that raises StartupError must fail fast: print - the message, exit non-zero, and never accept a connection on the port - - the failure must happen before uvicorn.Server binds the socket. - """ - proc, port = serve_with_plugins( - {"startup_error_plugin": STARTUP_ERROR_PLUGIN}, wait_for_startup=False - ) - stdout, _ = proc.communicate(timeout=15) - output = stdout.decode("utf-8") - assert proc.returncode not in (0, None), output - assert "boom from plugin" in output, output - - # Nothing is listening on the port now the process has exited. This - # confirms the socket was not left bound; on its own it cannot prove the - # failure preceded the bind, since a port nothing ever touched also - # refuses connections. - with ( - pytest.raises(OSError), - socket.create_connection(("127.0.0.1", port), timeout=0.2), - ): - pass - - -# Verify that SIGTERM and SIGINT sent to `datasette serve` trigger uvicorn's -# lifespan.shutdown event and run the plugin shutdown hooks. The plugin below -# writes a sentinel file from its shutdown hook so the tests can check that -# cleanup ran after the server subprocess exits. -SHUTDOWN_SENTINEL_PLUGIN_TEMPLATE = """ -import pathlib -from datasette import hookimpl - -SENTINEL_PATH = {sentinel_path!r} - - -@hookimpl -def shutdown(datasette): - pathlib.Path(SENTINEL_PATH).write_text("shutdown ran", "utf-8") -""" - - -def _start_serve_with_shutdown_sentinel(serve_with_plugins, tmp_path): - sentinel_path = tmp_path / "shutdown-sentinel.txt" - proc, _ = serve_with_plugins( - { - "shutdown_sentinel_plugin": SHUTDOWN_SENTINEL_PLUGIN_TEMPLATE.format( - sentinel_path=str(sentinel_path) - ) - } - ) - return proc, sentinel_path - - -@pytest.mark.serial -def test_sigterm_runs_shutdown_hooks(serve_with_plugins, tmp_path): - ds_proc, sentinel_path = _start_serve_with_shutdown_sentinel( - serve_with_plugins, tmp_path - ) - assert not sentinel_path.exists() - ds_proc.send_signal(signal.SIGTERM) - try: - ds_proc.wait(timeout=10) - except subprocess.TimeoutExpired: - ds_proc.kill() - ds_proc.wait() - raise AssertionError( - "datasette serve did not exit within 10s of SIGTERM\n" - + ds_proc.stdout.read().decode("utf-8") - ) - output = ds_proc.stdout.read().decode("utf-8") - assert sentinel_path.exists(), ( - "shutdown hook never wrote its sentinel file after SIGTERM\n" + output - ) - assert sentinel_path.read_text("utf-8") == "shutdown ran" - - -@pytest.mark.serial -@pytest.mark.skipif( - not hasattr(signal, "SIGINT"), reason="Requires signal.SIGINT support" -) -def test_sigint_runs_shutdown_hooks(serve_with_plugins, tmp_path): - ds_proc, sentinel_path = _start_serve_with_shutdown_sentinel( - serve_with_plugins, tmp_path - ) - assert not sentinel_path.exists() - ds_proc.send_signal(signal.SIGINT) - try: - ds_proc.wait(timeout=10) - except subprocess.TimeoutExpired: - ds_proc.kill() - ds_proc.wait() - raise AssertionError( - "datasette serve did not exit within 10s of SIGINT\n" - + ds_proc.stdout.read().decode("utf-8") - ) - output = ds_proc.stdout.read().decode("utf-8") - assert sentinel_path.exists(), ( - "shutdown hook never wrote its sentinel file after SIGINT\n" + output - ) - assert sentinel_path.read_text("utf-8") == "shutdown ran" diff --git a/tests/test_column_types.py b/tests/test_column_types.py index d8dfc627..cd308ec9 100644 --- a/tests/test_column_types.py +++ b/tests/test_column_types.py @@ -1,11 +1,7 @@ import json import logging -import time -import markupsafe -import pytest from bs4 import BeautifulSoup as Soup - from datasette.app import Datasette from datasette.column_types import ( ColumnType, @@ -13,7 +9,11 @@ from datasette.column_types import ( ) from datasette.hookspecs import hookimpl from datasette.plugins import pm -from datasette.utils import StartupError, error_body, sqlite3 +from datasette.utils import error_body, sqlite3 +from datasette.utils import StartupError +import markupsafe +import pytest +import time @pytest.fixture @@ -31,7 +31,6 @@ def ds_ct(tmp_path_factory): "'https://example.com', '{\"key\": \"value\"}')" ) db.commit() - db.close() ds = Datasette( [db_path], config={ @@ -71,7 +70,6 @@ def ds_ct_editor_permission(tmp_path_factory): "'https://example.com', '{\"key\": \"value\"}')" ) db.commit() - db.close() ds = Datasette( [db_path], config={ @@ -106,7 +104,7 @@ def write_token(ds, actor_id="root", permissions=None): def _headers(token): return { - "Authorization": f"Bearer {token}", + "Authorization": "Bearer {}".format(token), "Content-Type": "application/json", } diff --git a/tests/test_config_dir.py b/tests/test_config_dir.py index 00540464..42c6ae60 100644 --- a/tests/test_config_dir.py +++ b/tests/test_config_dir.py @@ -1,12 +1,10 @@ import json import pathlib - import pytest from datasette.app import Datasette -from datasette.utils import StartupError from datasette.utils.sqlite import sqlite3 - +from datasette.utils import StartupError from .fixtures import TestClient as _TestClient PLUGIN = """ @@ -111,7 +109,7 @@ def test_settings(config_dir_client): def test_plugins(config_dir_client): response = config_dir_client.get("/-/plugins.json") assert 200 == response.status - plugins = response.json + plugins = response.json["plugins"] assert "hooray.py" in {p["name"] for p in plugins} assert "non_py_file.txt" not in {p["name"] for p in plugins} assert "mypy_cache" not in {p["name"] for p in plugins} diff --git a/tests/test_crossdb.py b/tests/test_crossdb.py index ffd0870c..11e53224 100644 --- a/tests/test_crossdb.py +++ b/tests/test_crossdb.py @@ -1,9 +1,7 @@ -import sqlite3 -import urllib - -from click.testing import CliRunner - from datasette.cli import cli +from click.testing import CliRunner +import urllib +import sqlite3 def test_crossdb_join(app_client_two_attached_databases_crossdb_enabled): @@ -42,7 +40,7 @@ def test_crossdb_warning_if_too_many_databases(tmp_path_factory): db_dir = tmp_path_factory.mktemp("dbs") dbs = [] for i in range(11): - path = str(db_dir / f"db_{i}.db") + path = str(db_dir / "db_{}.db".format(i)) conn = sqlite3.connect(path) conn.execute("vacuum") conn.close() diff --git a/tests/test_csrf_middleware.py b/tests/test_csrf_middleware.py index 6c78f69d..2fcfb216 100644 --- a/tests/test_csrf_middleware.py +++ b/tests/test_csrf_middleware.py @@ -44,7 +44,7 @@ async def _run_middleware(scope): await mw(scope, None, send) if inner_called: return ("allowed",) - start = next(m for m in sent if m["type"] == "http.response.start") + start = [m for m in sent if m["type"] == "http.response.start"][0] return ("blocked", start["status"]) diff --git a/tests/test_csv.py b/tests/test_csv.py index adae7e24..a2f03776 100644 --- a/tests/test_csv.py +++ b/tests/test_csv.py @@ -1,9 +1,7 @@ -import urllib.parse - -import pytest -from bs4 import BeautifulSoup as Soup - from datasette.app import Datasette +from bs4 import BeautifulSoup as Soup +import pytest +import urllib.parse EXPECTED_TABLE_CSV = """id,content 1,hello @@ -166,66 +164,6 @@ async def test_custom_sql_csv(ds_client): assert response.text == EXPECTED_CUSTOM_CSV -@pytest.mark.asyncio -@pytest.mark.parametrize("download", (False, True)) -@pytest.mark.parametrize( - "query_string,expected_error", - ( - ("sql=select+blah", "no such column: blah"), - ("sql=select+*+from+missing", "no such table: missing"), - ("sql=select+from", 'near "from": syntax error'), - ( - "sql=delete+from+simple_primary_key", - "Statement must be a SELECT", - ), - ("", "?sql= is required"), - ( - "sql=select+sleep(0.01)&_timelimit=5", - ( - "SQL query took too long. The time limit is" - " controlled by the sql_time_limit_ms setting." - ), - ), - ), -) -async def test_custom_sql_csv_errors(ds_client, query_string, expected_error, download): - if download: - query_string += "&_dl=1" - response = await ds_client.get(f"/fixtures/-/query.csv?{query_string}") - assert response.status_code == 400 - assert response.headers["content-type"] == "text/plain; charset=utf-8" - assert "content-disposition" not in response.headers - assert response.text == expected_error - - -@pytest.mark.asyncio -async def test_custom_sql_csv_error_head(ds_client): - response = await ds_client.head("/fixtures/-/query.csv?sql=select+blah") - assert response.status_code == 400 - assert response.headers["content-type"] == "text/plain; charset=utf-8" - assert response.content == b"" - - -@pytest.mark.asyncio -async def test_custom_sql_csv_error_cors(): - ds = Datasette(cors=True) - response = await ds.client.get("/_memory/-/query.csv?sql=select+blah") - assert response.status_code == 400 - assert response.headers["content-type"] == "text/plain; charset=utf-8" - assert response.headers["access-control-allow-origin"] == "*" - assert response.text == "no such column: blah" - - -@pytest.mark.asyncio -async def test_table_csv_error(ds_client): - response = await ds_client.get( - "/fixtures/simple_primary_key.csv?_where=blah&_stream=1" - ) - assert response.status_code == 400 - assert response.headers["content-type"] == "text/plain; charset=utf-8" - assert response.text == "no such column: blah" - - @pytest.mark.asyncio async def test_table_csv_download(ds_client): response = await ds_client.get("/fixtures/simple_primary_key.csv?_dl=1") @@ -289,20 +227,6 @@ async def test_table_csv_stream(ds_client): assert len([b for b in response.content.split(b"\r\n") if b]) == 1002 -@pytest.mark.asyncio -async def test_view_csv_stream(ds_client): - # Without _stream should return header + 100 rows: - response = await ds_client.get("/fixtures/paginated_view.csv?_size=max") - assert len([b for b in response.content.split(b"\r\n") if b]) == 101 - # With _stream=1 should paginate through all pages and return header + 202 rows - response = await ds_client.get("/fixtures/paginated_view.csv?_stream=1") - lines = [b for b in response.content.split(b"\r\n") if b] - assert len(lines) == 203 - # Ensure there are no duplicate rows from looping - assert len(set(lines[1:])) == 202 - assert lines[0] == b"content,content_extra" - - def test_csv_trace(app_client_with_trace): response = app_client_with_trace.get("/fixtures/simple_primary_key.csv?_trace=1") assert response.headers["content-type"] == "text/html; charset=utf-8" diff --git a/tests/test_custom_pages.py b/tests/test_custom_pages.py index 32cfc43d..86cdcc6b 100644 --- a/tests/test_custom_pages.py +++ b/tests/test_custom_pages.py @@ -1,7 +1,5 @@ import pathlib - import pytest - from .fixtures import make_app_client TEST_TEMPLATE_DIRS = str(pathlib.Path(__file__).parent / "test_templates") diff --git a/tests/test_default_deny.py b/tests/test_default_deny.py index f456a17f..f1e43064 100644 --- a/tests/test_default_deny.py +++ b/tests/test_default_deny.py @@ -1,5 +1,4 @@ import pytest - from datasette.app import Datasette from datasette.resources import DatabaseResource, TableResource diff --git a/tests/test_docs.py b/tests/test_docs.py index b36b773e..0bcb5e62 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -2,22 +2,20 @@ Tests to ensure certain things are documented. """ -import re -from pathlib import Path - -import pytest - -import datasette.fixtures # noqa: F401 from datasette import app, utils +import datasette.fixtures # noqa: F401 from datasette.app import Datasette from datasette.filters import Filters +from pathlib import Path +import pytest +import re docs_path = Path(__file__).parent.parent / "docs" label_re = re.compile(r"\.\. _([^\s:]+):") def get_headings(content, underline="-"): - heading_re = re.compile(rf"(\w+)(\([^)]*\))?\n\{underline}+\n") + heading_re = re.compile(r"(\w+)(\([^)]*\))?\n\{}+\n".format(underline)) return {h[0] for h in heading_re.findall(content)} @@ -27,20 +25,14 @@ def get_labels(filename): @pytest.fixture(scope="session") -def settings_sections(): - content = (docs_path / "settings.rst").read_text() - sections = re.split(r"^(\w+)\n~+\n", content, flags=re.MULTILINE) - return dict(zip(sections[1::2], sections[2::2])) +def settings_headings(): + return get_headings((docs_path / "settings.rst").read_text(), "~") -def test_settings_are_documented(settings_sections, subtests): +def test_settings_are_documented(settings_headings, subtests): for setting in app.SETTINGS: with subtests.test(setting=setting.name): - assert setting.name in settings_sections - assert ( - f'setting_default(cog, "{setting.name}")' - in settings_sections[setting.name] - ) + assert setting.name in settings_headings @pytest.fixture(scope="session") diff --git a/tests/test_docs_plugins.py b/tests/test_docs_plugins.py index 4a0014b4..613160ac 100644 --- a/tests/test_docs_plugins.py +++ b/tests/test_docs_plugins.py @@ -1,10 +1,9 @@ # fmt: off # -- start datasette_with_plugin_fixture -- -import pytest -import pytest_asyncio - from datasette import hookimpl from datasette.app import Datasette +import pytest +import pytest_asyncio @pytest_asyncio.fixture diff --git a/tests/test_error_shape.py b/tests/test_error_shape.py index 94c9a7c9..768814fd 100644 --- a/tests/test_error_shape.py +++ b/tests/test_error_shape.py @@ -17,10 +17,8 @@ present and the legacy "title" key must not be. https://github.com/simonw/datasette/issues - 1.0 API consistency """ -import time - import pytest - +import time from datasette.app import Datasette from datasette.utils import sqlite3 @@ -88,7 +86,7 @@ async def test_write_api_validation_error_shape(ds_error_shape): "/data/docs/-/insert", json={"rows": [{"nope": 1}, {"also_nope": 2}]}, headers={ - "Authorization": f"Bearer {token}", + "Authorization": "Bearer {}".format(token), "Content-Type": "application/json", }, ) @@ -412,7 +410,7 @@ async def test_expired_token_returns_401(ds_error_shape): ) ) response = await ds_error_shape.client.get( - "/-/actor.json", headers={"Authorization": f"Bearer {token}"} + "/-/actor.json", headers={"Authorization": "Bearer {}".format(token)} ) data = assert_canonical_error(response, 401) assert "expired" in data["error"].lower() @@ -448,7 +446,7 @@ async def test_valid_token_still_authenticates(ds_error_shape): ) ) response = await ds_error_shape.client.get( - "/-/actor.json", headers={"Authorization": f"Bearer {token}"} + "/-/actor.json", headers={"Authorization": "Bearer {}".format(token)} ) assert response.status_code == 200 assert response.json()["actor"]["id"] == "root" @@ -479,7 +477,7 @@ async def test_token_when_signed_tokens_disabled_returns_401(tmp_path_factory): ds.sign({"a": "root", "t": int(time.time())}, namespace="token") ) response = await ds.client.get( - "/-/actor.json", headers={"Authorization": f"Bearer {token}"} + "/-/actor.json", headers={"Authorization": "Bearer {}".format(token)} ) data = assert_canonical_error(response, 401) assert "not enabled" in data["error"] @@ -644,7 +642,7 @@ async def test_query_list_size_rejects_non_integer(ds_client): @pytest.mark.asyncio @pytest.mark.parametrize("endpoint", ("allowed", "rules")) async def test_debug_endpoints_use_size_and_page_parameters(ds_error_shape, endpoint): - base = f"/-/{endpoint}.json?action=view-instance" + base = "/-/{}.json?action=view-instance".format(endpoint) ok = await ds_error_shape.client.get( base + "&_size=1&_page=1", actor={"id": "root"} ) diff --git a/tests/test_extras.py b/tests/test_extras.py index 4e008926..73b4965e 100644 --- a/tests/test_extras.py +++ b/tests/test_extras.py @@ -1,5 +1,4 @@ import asyncio -from typing import ClassVar import pytest @@ -8,7 +7,7 @@ from datasette.extras import Extra, ExtraRegistry, ExtraScope class SlowValueExtra(Extra): description = "Returns context['value'], optionally slowly" - scopes: ClassVar[set[ExtraScope]] = {ExtraScope.TABLE} + scopes = {ExtraScope.TABLE} async def resolve(self, context): if context["slow"]: @@ -18,7 +17,7 @@ class SlowValueExtra(Extra): class DependentExtra(Extra): description = "Depends on slow_value" - scopes: ClassVar[set[ExtraScope]] = {ExtraScope.TABLE} + scopes = {ExtraScope.TABLE} async def resolve(self, context, slow_value): return slow_value + 1 @@ -26,7 +25,7 @@ class DependentExtra(Extra): class InternalOnlyExtra(Extra): description = "Internal extra for HTML templates only" - scopes: ClassVar[set[ExtraScope]] = {ExtraScope.TABLE} + scopes = {ExtraScope.TABLE} public = False async def resolve(self, context): @@ -53,7 +52,7 @@ def _registered_extra_classes(): @pytest.mark.parametrize("cls", _registered_extra_classes(), ids=lambda cls: cls.key()) def test_registered_extras_have_descriptions(cls): # Every registered extra is part of the documented template/JSON contract - assert cls.description, f"{cls.__name__} is missing a description" + assert cls.description, "{} is missing a description".format(cls.__name__) def test_registry_is_built_once_per_scope(): diff --git a/tests/test_facets.py b/tests/test_facets.py index 10325f9a..8c22ffce 100644 --- a/tests/test_facets.py +++ b/tests/test_facets.py @@ -1,49 +1,11 @@ -import json -from urllib.parse import parse_qsl, urlsplit - -import pytest - from datasette.app import Datasette from datasette.database import Database -from datasette.facets import ( - ArrayFacet, - ColumnFacet, - DateFacet, - Facet, - load_facet_configs, -) -from datasette.utils import detect_json1 +from datasette.facets import Facet, ColumnFacet, ArrayFacet, DateFacet from datasette.utils.asgi import Request - +from datasette.utils import detect_json1 from .fixtures import make_app_client - - -@pytest.mark.parametrize( - "query_string", - ("_facets=ignored", "_facet=state&_facets=ignored", "_facets=ignored&_facet=state"), -) -@pytest.mark.parametrize("table_config", ({}, {"facets": ["state"]})) -def test_facet_configs_ignore_unrelated_prefixes(query_string, table_config): - expected = load_facet_configs( - Request.fake("/?_facet=state" if "_facet=" in query_string else "/"), - table_config, - ) - assert ( - load_facet_configs(Request.fake("/?" + query_string), table_config) == expected - ) - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "query_string", - ("_facet=state&_facets=ignored", "_facets=ignored&_facet=state"), -) -async def test_facet_ignores_unrelated_prefixes(ds_client, query_string): - response = await ds_client.get("/fixtures/facetable.json?" + query_string) - assert response.status_code == 200 - facets = response.json()["facet_results"]["results"] - assert set(facets) == {"state"} - assert facets["state"]["results"] +import json +import pytest @pytest.mark.asyncio @@ -186,63 +148,6 @@ async def test_column_facet_results(ds_client): ] == buckets -@pytest.mark.asyncio -@pytest.mark.parametrize( - "column,filters,remaining", - [ - ("COUNTY", "COUNTY=Lee", []), - ("COUNTY", "COUNTY__exact=Lee", []), - ("COUNTY", "COUNTY=Lee&COUNTY__exact=Lee", []), - ("COUNTY", "COUNTY__exact=Lee&COUNTY__exact=Lee", []), - ( - "COUNTY", - "COUNTY__exact=Lee&COUNTY__exact=Polk", - [("COUNTY__exact", "Polk")], - ), - ("_county", "_county__exact=Lee", []), - ("_county", "_county__exact=Lee&_county=Lee", [("_county", "Lee")]), - ], -) -async def test_column_facet_selected_exact_filters( - ds_client, column, filters, remaining -): - facet = ColumnFacet( - ds_client.ds, - Request.fake(f"/?_facet={column}&{filters}&other=keep&_sort={column}"), - database="fixtures", - sql=f"select 'Lee' as {column}", - ) - buckets, timed_out = await facet.facet_results() - assert not timed_out - result = buckets[0]["results"][0] - assert result["selected"] is True - assert parse_qsl(urlsplit(result["toggle_url"]).query) == [ - ("_facet", column), - *remaining, - ("other", "keep"), - ("_sort", column), - ] - - -@pytest.mark.asyncio -async def test_column_facet_underscore_argument_is_not_a_filter(ds_client): - facet = ColumnFacet( - ds_client.ds, - Request.fake("/?_facet=_county&_county=Lee"), - database="fixtures", - sql="select 'Lee' as _county", - ) - buckets, timed_out = await facet.facet_results() - assert not timed_out - result = buckets[0]["results"][0] - assert result["selected"] is False - assert parse_qsl(urlsplit(result["toggle_url"]).query) == [ - ("_facet", "_county"), - ("_county", "Lee"), - ("_county__exact", "Lee"), - ] - - @pytest.mark.asyncio async def test_column_facet_results_column_starts_with_underscore(ds_client): facet = ColumnFacet( @@ -632,7 +537,7 @@ async def test_facet_size(): for j in range(1, 4): await db.execute_write( "insert into neighbourhoods (city, neighbourhood) values (?, ?)", - [f"City {i}", f"Neighbourhood {j}"], + ["City {}".format(i), "Neighbourhood {}".format(j)], ) response = await ds.client.get( "/test_facet_size/neighbourhoods.json?_extra=suggested_facets" diff --git a/tests/test_filters.py b/tests/test_filters.py index 9f201fdf..eda9e9a1 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -1,7 +1,6 @@ -import pytest - -from datasette.filters import Filters, search_filters, through_filters, where_filters +from datasette.filters import Filters, through_filters, where_filters, search_filters from datasette.utils.asgi import Request +import pytest @pytest.mark.parametrize( @@ -66,12 +65,12 @@ from datasette.utils.asgi import Request # JSON arraycontains, arraynotcontains ( (("Availability+Info__arraycontains", "yes"),), - [':p0 in (select value from json_each("table"."Availability+Info"))'], + [":p0 in (select value from json_each([table].[Availability+Info]))"], ["yes"], ), ( (("Availability+Info__arraynotcontains", "yes"),), - [':p0 not in (select value from json_each("table"."Availability+Info"))'], + [":p0 not in (select value from json_each([table].[Availability+Info]))"], ["yes"], ), ], @@ -83,35 +82,6 @@ def test_build_where(args, expected_where, expected_params): assert {f"p{i}": param for i, param in enumerate(expected_params)} == actual_params -@pytest.mark.parametrize( - "key,expected_where", - ( - ( - 'has"quote__exact', - '"has""quote" = :p0', - ), - ( - 'has"quote__isnull', - '"has""quote" is null', - ), - ( - "has]bracket__arraycontains", - ':p0 in (select value from json_each("table"."has]bracket"))', - ), - ), -) -def test_build_where_escapes_column_names(key, expected_where): - filters = Filters(((key, "value"),)) - sql_bits, _ = filters.build_where_clauses("table") - assert sql_bits == [expected_where] - - -def test_build_where_escapes_table_name(): - filters = Filters((("tags__arraycontains", "value"),)) - sql_bits, _ = filters.build_where_clauses("items]bracket") - assert sql_bits == [':p0 in (select value from json_each("items]bracket"."tags"))'] - - @pytest.mark.asyncio async def test_through_filters_from_request(ds_client): request = Request.fake( diff --git a/tests/test_fts_permissions.py b/tests/test_fts_permissions.py deleted file mode 100644 index 24a40cef..00000000 --- a/tests/test_fts_permissions.py +++ /dev/null @@ -1,412 +0,0 @@ -import pytest - -from datasette import hookimpl -from datasette.app import Datasette -from datasette.permissions import Action, PermissionSQL, _permission_check_cache -from datasette.resources import DatabaseResource, TableResource -from datasette.utils.sqlite import sqlite3, sqlite_derived_table_dependencies - - -@pytest.mark.asyncio -@pytest.mark.parametrize("fts_module", ["fts4", "fts5"]) -@pytest.mark.parametrize("actor", [None, {"id": "root"}], ids=["anonymous", "root"]) -async def test_derived_permissions_allow_one_hop_but_deny_nested_sources( - fts_module, actor -): - class InspectPlugin: - @hookimpl - def register_actions(self): - return [ - Action( - name="inspect-derived", - description="Inspect a table", - resource_class=TableResource, - also_requires="view-table", - ) - ] - - @hookimpl - def permission_resources_sql(self, action): - if action == "inspect-derived": - return PermissionSQL( - sql="SELECT NULL AS parent, NULL AS child, 1 AS allow, 'inspect allowed' AS reason" - ) - - ds = Datasette(memory=True) - ds.pm.register(InspectPlugin(), name="inspect-derived-test") - db = ds.add_memory_database( - f"derived_one_hop_{fts_module}_{actor is not None}", name="data" - ) - await db.execute_write("create table Documents (body text)") - await db.execute_write( - f"create virtual table Search using {fts_module}(body, content='Documents')" - ) - await db.execute_write( - f"create virtual table Nested using {fts_module}(body, content='sEaRcH')" - ) - await ds.invoke_startup() - token = _permission_check_cache.set({}) - try: - # Both direct permissions are allowed, but a derived source makes its - # dependent unavailable even to an actor who can view the whole chain. - # Check and cache Search first so its cached grant cannot grant Nested. - for table, expected in ( - ("Documents", True), - ("Search", True), - ("Nested", False), - ("Search_docsize", False), - ): - for spelling in (table, table.upper(), table.lower()): - assert await ds.allowed_many( - actions=["view-table", "inspect-derived"], - resource=TableResource("data", spelling), - actor=actor, - ) == {"view-table": expected, "inspect-derived": expected} - - page = await ds.allowed_resources( - "view-table", actor, parent="data", include_is_private=True, limit=1000 - ) - allowed = {resource.child for resource in page.resources} - assert {"Documents", "Search"}.issubset(allowed) - assert "Nested" not in allowed - assert "Search_docsize" not in allowed - finally: - _permission_check_cache.reset(token) - ds.pm.unregister(name="inspect-derived-test") - ds.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("listing", [False, True], ids=["individual", "listing"]) -async def test_derived_permission_discovery_error_is_retried(monkeypatch, listing): - ds = Datasette(memory=True) - db = ds.add_memory_database(f"derived_discovery_error_{listing}", name="data") - await db.execute_write("create table documents (id integer primary key)") - await ds.invoke_startup() - - class UnavailableSchema: - def execute(self, sql): - raise sqlite3.DatabaseError("schema temporarily unavailable") - - async def check(): - if listing: - return await ds.allowed_resources("view-table", parent="data") - return await ds.allowed( - action="view-table", resource=TableResource("data", "documents") - ) - - token = _permission_check_cache.set({}) - try: - with monkeypatch.context() as patch: - patch.setattr( - "datasette.database.sqlite_derived_table_dependencies", - lambda conn: sqlite_derived_table_dependencies(UnavailableSchema()), - ) - with pytest.raises(sqlite3.DatabaseError, match="schema temporarily"): - await check() - - # Failed discovery must not cache an empty map or a permission grant. - assert db._cached_derived_table_dependencies is None - assert not _permission_check_cache.get() - result = await check() - if listing: - assert [resource.child for resource in result.resources] == ["documents"] - else: - assert result is True - assert db._cached_derived_table_dependencies is not None - finally: - _permission_check_cache.reset(token) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("fts_module", ("fts4", "fts5")) -async def test_external_content_fts_inherits_content_table_view_permission(fts_module): - actor = {"id": "reader"} - secret_marker = "ISSUE_17_EXTERNAL_CONTENT_FTS_SECRET" - ds = Datasette( - memory=True, - config={ - "permissions": { - "view-instance": {"id": "reader"}, - "view-database": {"id": "reader"}, - "view-table": {"id": "reader"}, - "execute-sql": {"id": "nobody"}, - }, - "databases": { - "data": { - "tables": { - "secret": {"permissions": {"view-table": False}}, - } - } - }, - }, - ) - db = ds.add_memory_database(f"issue_17_{fts_module}_permissions", name="data") - await db.execute_write("create table secret (id integer primary key, body text)") - await db.execute_write( - "insert into secret (body) values (?)", - [secret_marker], - ) - fts_options = "body, content='secret'" - if fts_module == "fts5": - fts_options += ", content_rowid='id'" - await db.execute_write( - f"create virtual table secret_fts using {fts_module}({fts_options})" - ) - await db.execute_write("insert into secret_fts(secret_fts) values ('rebuild')") - await ds.invoke_startup() - - try: - assert "secret_fts" in await db.hidden_table_names() - assert ( - await ds.allowed( - action="execute-sql", - resource=DatabaseResource("data"), - actor=actor, - ) - is False - ) - - direct = await ds.client.get("/data/secret.json", actor=actor) - assert direct.status_code == 403 - - companion = await ds.client.get( - "/data/secret_fts.json?_shape=array", - actor=actor, - ) - assert companion.status_code in (403, 404), ( - "An automatically hidden external-content FTS table must inherit " - "the content table's view denial or be unavailable: " - f"{companion.text}" - ) - assert secret_marker not in companion.text - finally: - ds.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("fts_module", ("fts4", "fts5")) -@pytest.mark.parametrize("contentless", (False, True), ids=("internal", "contentless")) -async def test_fts_shadow_tables_inherit_logical_table_view_permission( - fts_module, contentless -): - table_config = { - "secret_fts": {"permissions": {"view-table": False}}, - # An explicit allow on one implementation table must not override - # the logical FTS table's denial. - "secret_fts_docsize": {"permissions": {"view-table": True}}, - } - ds = Datasette( - memory=True, - config={ - "permissions": { - "view-instance": True, - "view-database": True, - "view-table": True, - "execute-sql": False, - }, - "databases": {"data": {"tables": table_config}}, - }, - ) - db = ds.add_memory_database( - f"issue_17_{fts_module}_{'contentless' if contentless else 'internal'}", - name="data", - ) - options = "body, content=''" if contentless else "body" - await db.execute_write( - f"create virtual table secret_fts using {fts_module}({options})" - ) - await db.execute_write( - "insert into secret_fts(rowid, body) values (1, 'ISSUE_17_SHADOW_SECRET')" - ) - await ds.invoke_startup() - - try: - dependencies = await db.derived_table_dependencies() - shadow_tables = sorted( - table for table, source in dependencies.items() if source == "secret_fts" - ) - assert shadow_tables - assert "secret_fts_docsize" in shadow_tables - - for shadow_table in shadow_tables: - assert ( - await ds.allowed( - action="view-table", - resource=TableResource("data", shadow_table), - ) - is False - ) - response = await ds.client.get(f"/data/{shadow_table}.json?_shape=array") - assert response.status_code == 403 - assert "ISSUE_17_SHADOW_SECRET" not in response.text - - allowed = await ds.allowed_resources("view-table", parent="data", limit=1000) - allowed_names = {resource.child for resource in allowed.resources} - assert not set(shadow_tables).intersection(allowed_names) - - database_json = await ds.client.get("/data.json") - assert database_json.status_code == 200 - for shadow_table in shadow_tables: - assert shadow_table not in database_json.text - - schema_json = await ds.client.get("/data/-/schema.json") - assert schema_json.status_code == 200 - for shadow_table in shadow_tables: - assert shadow_table not in schema_json.text - finally: - ds.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "content_allowed,companion_allowed,expected", - ( - (False, True, False), - (True, False, False), - (True, True, True), - ), -) -async def test_external_content_and_companion_permissions_are_both_required( - content_allowed, companion_allowed, expected -): - ds = Datasette( - memory=True, - default_deny=True, - config={ - "permissions": { - "view-instance": True, - "view-database": True, - }, - "databases": { - "data": { - "tables": { - "secret": {"permissions": {"view-table": content_allowed}}, - "secret_fts": { - "permissions": {"view-table": companion_allowed} - }, - } - } - }, - }, - ) - db = ds.add_memory_database( - f"issue_17_explicit_{int(content_allowed)}_{int(companion_allowed)}", - name="data", - ) - await db.execute_write("create table secret(id integer primary key, body text)") - await db.execute_write("insert into secret(body) values ('ISSUE_17_MATRIX_SECRET')") - await db.execute_write( - "create virtual table secret_fts using fts5(" - "body, content='secret', content_rowid='id')" - ) - await db.execute_write("insert into secret_fts(secret_fts) values ('rebuild')") - await ds.invoke_startup() - - try: - assert ( - await ds.allowed( - action="view-table", - resource=TableResource("data", "secret_fts"), - ) - is expected - ) - response = await ds.client.get("/data/secret_fts.json?_shape=array") - assert response.status_code == (200 if expected else 403) - if not expected: - assert "ISSUE_17_MATRIX_SECRET" not in response.text - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_derived_tables_propagate_private_flag_and_route_permissions(): - actor = {"id": "reader"} - ds = Datasette( - memory=True, - config={ - "permissions": { - "view-instance": True, - "view-database": True, - "view-table": True, - }, - "databases": { - "data": { - "tables": { - "secret": {"permissions": {"view-table": {"id": "reader"}}} - } - } - }, - }, - ) - db = ds.add_memory_database("issue_17_private_flag", name="data") - await db.execute_write("create table secret(id integer primary key, body text)") - await db.execute_write("insert into secret(body) values ('PRIVATE')") - await db.execute_write( - "create virtual table secret_fts using fts5(" - "body, content='secret', content_rowid='id')" - ) - await db.execute_write("insert into secret_fts(secret_fts) values ('rebuild')") - await ds.invoke_startup() - - try: - actor_page = await ds.allowed_resources( - "view-table", actor, parent="data", include_is_private=True, limit=1000 - ) - actor_resources = { - resource.child: resource for resource in actor_page.resources - } - derived_names = set(await db.derived_table_dependencies()) - assert "secret_fts" in actor_resources - assert actor_resources["secret_fts"].private - # Shadow tables depend on the already-derived external-content FTS - # table, so they remain unavailable even to the permitted reader. - assert not (derived_names - {"secret_fts"}).intersection(actor_resources) - - anonymous_page = await ds.allowed_resources( - "view-table", parent="data", limit=1000 - ) - anonymous_names = {resource.child for resource in anonymous_page.resources} - assert not derived_names.intersection(anonymous_names) - - for path in ( - "/data/secret_fts.json?_facet=body", - "/data/secret_fts.csv", - "/data/secret_fts/-/autocomplete?q=PRIVATE", - "/data/secret_fts/-/schema.json", - ): - denied = await ds.client.get(path) - assert denied.status_code == 403 - allowed = await ds.client.get(path, actor=actor) - assert allowed.status_code == 200 - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_cyclic_derived_table_dependencies_fail_closed(): - ds = Datasette(memory=True) - db = ds.add_memory_database("issue_17_cycle", name="data") - await db.execute_write( - "create virtual table first_fts using fts5(body, content='second_fts')" - ) - await db.execute_write( - "create virtual table second_fts using fts5(body, content='first_fts')" - ) - await ds.invoke_startup() - - try: - for table in ("first_fts", "second_fts"): - assert ( - await ds.allowed( - action="view-table", resource=TableResource("data", table) - ) - is False - ) - - page = await ds.allowed_resources("view-table", parent="data", limit=1000) - allowed_names = {resource.child for resource in page.resources} - assert "first_fts" not in allowed_names - assert "second_fts" not in allowed_names - finally: - ds.close() diff --git a/tests/test_html.py b/tests/test_html.py index 1434d166..f53ed09c 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1,19 +1,16 @@ +from bs4 import BeautifulSoup as Soup +from datasette.app import Datasette +from datasette.utils import allowed_pragmas +from .fixtures import make_app_client +from .utils import assert_footer_links, inner_html import copy import hashlib import json import pathlib +import pytest import re import urllib.parse -import pytest -from bs4 import BeautifulSoup as Soup - -from datasette.app import Datasette -from datasette.utils import allowed_pragmas - -from .fixtures import make_app_client -from .utils import assert_footer_links, inner_html - def test_homepage(app_client_two_attached_databases): response = app_client_two_attached_databases.get("/") @@ -36,10 +33,8 @@ def test_homepage(app_client_two_attached_databases): h2 = soup.select("h2")[0] assert "extra database" == h2.text.strip() counts_p, links_p = h2.find_all_next("p")[:2] - # Shadow tables of the external-content index are denied, so they do not - # contribute to the table or row totals. assert ( - "2 rows in 1 table, 2 rows in 1 hidden table, 1 view" == counts_p.text.strip() + "2 rows in 1 table, 5 rows in 4 hidden tables, 1 view" == counts_p.text.strip() ) # We should only show visible, not hidden tables here: table_links = [ @@ -51,36 +46,6 @@ def test_homepage(app_client_two_attached_databases): ] == table_links -@pytest.mark.asyncio -@pytest.mark.parametrize( - "sql,expected", - ( - (["create view one as select 1 as n"], "0 tables, 1 view"), - ( - ["create view one as select 1 as n", "create view two as select 2 as n"], - "0 tables, 2 views", - ), - ( - ["create table t (id integer primary key)", "create view v as select 1"], - "0 rows in 1 table, 1 view", - ), - ), -) -async def test_homepage_database_summary_separators(sql, expected): - # https://github.com/simonw/datasette/issues/2012 - ds = Datasette() - await ds.invoke_startup() - db = ds.add_memory_database("summary_separators") - for statement in sql: - await db.execute_write(statement) - response = await ds.client.get("/") - assert response.status_code == 200 - soup = Soup(response.text, "html.parser") - h2 = next(h2 for h2 in soup.select("h2") if h2.text.strip() == "summary_separators") - counts_p = h2.find_next("p") - assert " ".join(counts_p.text.split()) == expected - - @pytest.mark.asyncio @pytest.mark.parametrize("path", ("/", "/-/")) async def test_homepage_alternative_location(path, tmp_path_factory): @@ -177,7 +142,9 @@ def test_static_mounts_hash_cache_control(): ) incorrect_hash = hashlib.sha256(b"incorrect").hexdigest()[:12] - response = client.get(f"/custom-static/test_html.py?_hash={incorrect_hash}") + response = client.get( + "/custom-static/test_html.py?_hash={}".format(incorrect_hash) + ) assert response.status_code == 200 assert "cache-control" not in response.headers @@ -252,9 +219,11 @@ async def test_disallowed_custom_sql_pragma(ds_client): "/fixtures/-/query?sql=SELECT+*+FROM+pragma_not_on_allow_list('idx52')" ) assert response.status_code == 400 - pragmas = ", ".join(f"pragma_{pragma}()" for pragma in allowed_pragmas) + pragmas = ", ".join("pragma_{}()".format(pragma) for pragma in allowed_pragmas) assert ( - f"Statement contained a disallowed PRAGMA. Allowed pragma functions are {pragmas}" + "Statement contained a disallowed PRAGMA. Allowed pragma functions are {}".format( + pragmas + ) in response.text ) @@ -315,6 +284,53 @@ async def test_query_page_with_no_sql(ds_client): assert 'class="rows-and-columns"' not in response.text +@pytest.mark.asyncio +async def test_table_page_view_and_edit_sql_link_carries_table(ds_client): + # The table page's "View and edit SQL" link should point at the query + # page with a &_table= param identifying the focal table, so the SQL + # editor can offer that table's columns unprefixed. + response = await ds_client.get("/fixtures/facetable") + assert response.status_code == 200 + soup = Soup(response.content, "html.parser") + link = soup.find("span", string="View and edit SQL").find_parent("a") + assert link is not None + assert "_table=facetable" in link["href"] + + +@pytest.mark.asyncio +async def test_query_page_default_table_from_table_scoped_link(ds_client): + # Following the table page's edit-SQL link should result in a query page + # whose SQL editor is initialized with defaultTable set to that table. + table_response = await ds_client.get("/fixtures/facetable") + soup = Soup(table_response.content, "html.parser") + href = soup.find("span", string="View and edit SQL").find_parent("a")["href"] + response = await ds_client.get(href, follow_redirects=True) + assert response.status_code == 200 + assert 'defaultTable: "facetable"' in response.text + + +@pytest.mark.asyncio +async def test_query_page_no_default_table_without_table_scope(ds_client): + # The plain database query page (no focal table) should not set + # defaultTable at all. + response = await ds_client.get("/fixtures/-/query?sql=select+1") + assert response.status_code == 200 + assert "defaultTable" not in response.text + + +@pytest.mark.asyncio +async def test_query_page_ignores_invalid_table_param(ds_client): + # A ?_table= value that isn't a real table/view in this database should + # not be reflected back into the page - and should not break execution + # of the query itself (leading-underscore params are not treated as SQL + # bind parameters unless they appear as :name in the SQL). + response = await ds_client.get( + "/fixtures/-/query?sql=select+1&_table=not_a_real_table" + ) + assert response.status_code == 200 + assert "defaultTable" not in response.text + + @pytest.mark.asyncio async def test_query_csv_with_no_sql_is_400(ds_client): # https://github.com/simonw/datasette/issues/2743 @@ -809,8 +825,8 @@ def test_stored_query_show_hide_metadata_option( }, memory=True, ) as client: - expected_show_hide_fragment = ( - f'({expected_show_hide_text})' + expected_show_hide_fragment = '({})'.format( + expected_show_hide_link, expected_show_hide_text ) response = client.get("/_memory/one" + querystring) html = response.text @@ -819,7 +835,10 @@ def test_stored_query_show_hide_metadata_option( )[0] assert show_hide_fragment == expected_show_hide_fragment if expected_hidden: - assert f'' in html + assert ( + ''.format(expected_hidden) + in html + ) else: assert '; rel="alternate"; type="application/json+datasette"' + assert link == '<{}>; rel="alternate"; type="application/json+datasette"'.format( + expected + ) assert ( - f'' + ''.format( + expected + ) in response.text ) @@ -1316,8 +1339,8 @@ async def test_database_color(ds_client): expected_color = ds_client.ds.get_database("fixtures").color # Should be something like #9403e5 expected_fragments = ( - f"10px solid #{expected_color}", - f"border-color: #{expected_color}", + "10px solid #{}".format(expected_color), + "border-color: #{}".format(expected_color), ) assert len(expected_color) == 6 for path in ( diff --git a/tests/test_http_span.py b/tests/test_http_span.py deleted file mode 100644 index 1fcc93b0..00000000 --- a/tests/test_http_span.py +++ /dev/null @@ -1,739 +0,0 @@ -""" -Tests for the HTTP request span created by TelemetryMiddleware and the -`http.route` enrichment added by the router. -""" - -import asyncio -import itertools -import json -import subprocess -import sys -import textwrap -import time - -import pytest -import pytest_asyncio - -pytest.importorskip("opentelemetry.sdk") - -from opentelemetry.trace import ( - NonRecordingSpan, - SpanContext, - SpanKind, - StatusCode, - TraceFlags, -) - -from datasette import hookimpl -from datasette.app import Datasette -from datasette.telemetry import ( - REQUEST_SPAN_SCOPE_KEY, - TelemetryMiddleware, - request_span, - tracer, -) -from datasette.utils import resolve_routes - -# Named in-memory databases are shared between instances, so each fixture -# needs a unique name. -_names = itertools.count() - - -PLUGIN_MIDDLEWARE_SPAN = "test.plugin.middleware" - - -class _MiddlewarePlugin: - "A plugin asgi_wrapper() that creates a span." - - __name__ = "HttpSpanMiddlewarePlugin" - - @hookimpl - def asgi_wrapper(self, datasette): - def wrap(app): - async def wrapped(scope, receive, send): - with tracer.start_as_current_span(PLUGIN_MIDDLEWARE_SPAN): - await app(scope, receive, send) - - return wrapped - - return wrap - - -class _RaisingMiddlewarePlugin: - """ - A plugin asgi_wrapper() that raises. `route_path` turns most exceptions - into a 500, so this is how an exception reaches the request span. - """ - - __name__ = "HttpSpanRaisingMiddlewarePlugin" - - def __init__(self, call_app_first): - self.call_app_first = call_app_first - - @hookimpl - def asgi_wrapper(self, datasette): - call_app_first = self.call_app_first - - def wrap(app): - async def wrapped(scope, receive, send): - if call_app_first: - await app(scope, receive, send) - raise RuntimeError("wrapper exploded") - - return wrapped - - return wrap - - -class _BoomPlugin: - "A route that raises, which route_path turns into a 500." - - __name__ = "HttpSpanBoomPlugin" - - @hookimpl - def register_routes(self): - return [(r"^/-/http-span-boom$", lambda: 1 / 0)] - - -@pytest_asyncio.fixture -async def ds(): - name = f"httpspan{next(_names)}" - instance = Datasette(memory=True) - instance.add_memory_database(name) - await instance.invoke_startup() - await instance.get_database(name).execute_write( - "create table t (id integer primary key, v text)" - ) - instance.db_name = name - try: - yield instance - finally: - instance.close() - - -@pytest_asyncio.fixture -async def ds_paging(): - """ - An instance whose table is bigger than `max_returned_rows`, so a - `?_stream=1` export runs queries for later pages during the body send. - """ - name = f"httpspanpaging{next(_names)}" - # Both settings are needed: lowering only max_returned_rows gives a - # single page with no `next` token. - instance = Datasette( - memory=True, settings={"max_returned_rows": 5, "default_page_size": 3} - ) - instance.add_memory_database(name) - await instance.invoke_startup() - db = instance.get_database(name) - await db.execute_write("create table t (id integer primary key, v text)") - await db.execute_write_many( - "insert into t (id, v) values (?, ?)", [[i, f"v{i}"] for i in range(40)] - ) - instance.db_name = name - try: - yield instance - finally: - instance.close() - - -def _server_spans(otel_spans): - return [ - span for span in otel_spans.get_finished_spans() if span.kind is SpanKind.SERVER - ] - - -def _route_for(ds, path): - "The compiled pattern Datasette's own router resolves `path` to." - match, _view = resolve_routes(ds._routes(), path) - assert match is not None, f"{path} matches no route" - return match.re.pattern - - -@pytest.mark.asyncio -async def test_plugin_asgi_wrapper_middleware_runs_inside_the_request_span( - ds, otel_spans -): - """ - Spans created by plugin asgi_wrapper() middleware are children of the - request span. - """ - ds.pm.register(_MiddlewarePlugin(), name="httpspan-middleware") - try: - otel_spans.clear() - response = await ds.client.get(f"/{ds.db_name}/t") - assert response.status_code == 200 - finally: - ds.pm.unregister(name="httpspan-middleware") - - spans = otel_spans.get_finished_spans() - server = [span for span in spans if span.kind is SpanKind.SERVER] - assert len(server) == 1, "expected exactly one SERVER span per request" - server_span = server[0] - assert server_span.parent is None, "the request span should be the trace root" - - plugin_spans = [span for span in spans if span.name == PLUGIN_MIDDLEWARE_SPAN] - assert len(plugin_spans) == 1 - assert plugin_spans[0].parent is not None - assert plugin_spans[0].parent.span_id == server_span.context.span_id - assert plugin_spans[0].context.trace_id == server_span.context.trace_id - - # Database spans are in the same trace. - queries = [span for span in spans if span.name == "db.query"] - assert queries, "a table page should have issued at least one query" - for query in queries: - assert query.context.trace_id == server_span.context.trace_id - - -@pytest.mark.asyncio -async def test_unrecognised_method_is_clamped(ds, otel_spans): - """ - Unknown methods are recorded as `_OTHER` in both the attribute and the - span name, which the router rebuilds from the raw `request.method`. - """ - otel_spans.clear() - await ds.client.request("FROB", f"/{ds.db_name}/t") - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].attributes["http.request.method"] == "_OTHER" - assert server[0].name == f"_OTHER {server[0].attributes['http.route']}" - - -@pytest.mark.asyncio -async def test_known_method_is_not_clamped(ds, otel_spans): - "Known methods are recorded unchanged." - otel_spans.clear() - await ds.client.get(f"/{ds.db_name}/t") - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].attributes["http.request.method"] == "GET" - assert server[0].name == f"GET {server[0].attributes['http.route']}" - - -@pytest.mark.asyncio -async def test_the_query_string_is_never_recorded(ds, otel_spans): - "No attribute on any span contains the query string." - marker = "canary-9f2b1c" - otel_spans.clear() - await ds.client.get(f"/{ds.db_name}/t?_facet=v&_nosuch={marker}") - spans = otel_spans.get_finished_spans() - assert _server_spans(otel_spans), "no request span was emitted" - leaked = [ - f"{span.name} -> {key}={value!r}" - for span in spans - for key, value in (span.attributes or {}).items() - if marker in str(value) or key == "url.query" - ] - assert not leaked, "the query string reached a span attribute: " + ", ".join(leaked) - - -@pytest.mark.asyncio -async def test_url_path_is_recorded_without_the_query_string(ds, otel_spans): - otel_spans.clear() - await ds.client.get(f"/{ds.db_name}/t?_facet=v") - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].attributes["url.path"] == f"/{ds.db_name}/t" - - -@pytest.mark.asyncio -async def test_escaping_exception_sets_error_type_and_reraises(ds, otel_spans): - """ - An exception that escapes `route_path` is recorded and re-raised. No - response started, so no status code is recorded. - """ - ds.pm.register( - _RaisingMiddlewarePlugin(call_app_first=False), name="httpspan-raiser" - ) - try: - otel_spans.clear() - with pytest.raises(RuntimeError): - await ds.client.get(f"/{ds.db_name}/t") - finally: - ds.pm.unregister(name="httpspan-raiser") - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].attributes["error.type"] == "RuntimeError" - assert "http.response.status_code" not in server[0].attributes - assert server[0].status.status_code is StatusCode.ERROR - - -@pytest.mark.asyncio -async def test_an_escaping_exception_beats_the_status_code_for_error_type( - ds, otel_spans -): - """ - A 500 response followed by an exception records the exception class as - `error.type`, not "500". - """ - ds.pm.register(_BoomPlugin(), name="httpspan-boom") - ds.pm.register( - _RaisingMiddlewarePlugin(call_app_first=True), name="httpspan-raiser" - ) - try: - otel_spans.clear() - with pytest.raises(RuntimeError): - await ds.client.get("/-/http-span-boom") - finally: - ds.pm.unregister(name="httpspan-raiser") - ds.pm.unregister(name="httpspan-boom") - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].attributes["http.response.status_code"] == 500 - assert server[0].attributes["error.type"] == "RuntimeError" - - -@pytest.mark.asyncio -async def test_a_404_is_not_an_error(ds, otel_spans): - """ - A 4xx records the status code but no `error.type` or error status. - `/no-such-database-at-all` matches the database route, so `http.route` - is still set. - """ - otel_spans.clear() - response = await ds.client.get("/no-such-database-at-all") - assert response.status_code == 404 - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].attributes["http.response.status_code"] == 404 - assert "error.type" not in server[0].attributes - assert server[0].status.status_code is StatusCode.UNSET - assert "http.route" in server[0].attributes - assert server[0].name != "GET" - - -@pytest.mark.asyncio -async def test_an_unrouted_404_has_no_route_and_a_bare_method_name(ds, otel_spans): - """ - With no matching route the span keeps the bare method name. Most missing - paths still match a route, so this uses a path deeper than any route. - """ - otel_spans.clear() - response = await ds.client.get("/a/b/c/d/e") - assert response.status_code == 404 - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].name == "GET" - assert "http.route" not in server[0].attributes - assert server[0].attributes["http.response.status_code"] == 404 - assert server[0].status.status_code is StatusCode.UNSET - - -@pytest.mark.asyncio -async def test_only_the_first_http_response_start_is_recorded(otel_spans): - "The `send` wrapper records the status from the first `http.response.start`." - - async def two_starts(scope, receive, send): - await send({"type": "http.response.start", "status": 200, "headers": []}) - await send({"type": "http.response.start", "status": 503, "headers": []}) - await send({"type": "http.response.body", "body": b""}) - - middleware = TelemetryMiddleware(two_starts) - scope = { - "type": "http", - "method": "GET", - "path": "/twice", - "raw_path": b"/twice", - "scheme": "http", - "headers": [], - } - otel_spans.clear() - await middleware(scope, None, lambda message: asyncio.sleep(0)) - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].attributes["http.response.status_code"] == 200 - assert "error.type" not in server[0].attributes - - -@pytest.mark.asyncio -async def test_lifespan_scope_passes_through_unspanned(otel_spans): - """ - Lifespan scopes reach `AsgiLifespan`, which sits inside this middleware, - without creating a SERVER span. - """ - instance = Datasette(memory=True) - app = instance.app() - events = iter([{"type": "lifespan.startup"}, {"type": "lifespan.shutdown"}]) - sent = [] - - async def receive(): - return next(events) - - async def send(message): - sent.append(message["type"]) - - otel_spans.clear() - await app({"type": "lifespan"}, receive, send) - assert sent == ["lifespan.startup.complete", "lifespan.shutdown.complete"] - assert not _server_spans(otel_spans) - - -@pytest.mark.asyncio -async def test_http_route_is_the_compiled_pattern(ds, otel_spans): - "`http.route` is the compiled regex of the route Datasette's router resolves." - path = f"/{ds.db_name}/t" - expected = _route_for(ds, path) - otel_spans.clear() - assert (await ds.client.get(path)).status_code == 200 - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].attributes["http.route"] == expected - assert server[0].name == f"GET {expected}" - # The raw pattern, not a prettified template: - assert "(?P" in expected - - -@pytest.mark.asyncio -async def test_the_route_lands_on_the_request_span_not_a_plugins_current_span( - ds, otel_spans -): - """ - The route is set on the span the middleware started, found through the - ASGI scope, not on a plugin `asgi_wrapper()` span that is current during - routing. - """ - ds.pm.register(_MiddlewarePlugin(), name="httpspan-middleware") - try: - otel_spans.clear() - path = f"/{ds.db_name}/t" - expected = _route_for(ds, path) - assert (await ds.client.get(path)).status_code == 200 - finally: - ds.pm.unregister(name="httpspan-middleware") - - spans = otel_spans.get_finished_spans() - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].attributes["http.route"] == expected - assert server[0].name == f"GET {expected}" - # The plugin's span keeps its name and has no route attribute. - plugin_spans = [span for span in spans if span.name == PLUGIN_MIDDLEWARE_SPAN] - assert len(plugin_spans) == 1 - assert "http.route" not in (plugin_spans[0].attributes or {}) - - -@pytest.mark.asyncio -async def test_request_span_attributes(ds, otel_spans): - "The attributes recorded for an ordinary request." - path = f"/{ds.db_name}/t" - otel_spans.clear() - assert (await ds.client.get(path)).status_code == 200 - server = _server_spans(otel_spans) - assert len(server) == 1 - attributes = server[0].attributes - assert attributes["http.request.method"] == "GET" - assert attributes["url.path"] == path - assert attributes["url.scheme"] == "http" - assert attributes["http.response.status_code"] == 200 - assert attributes["http.route"] == _route_for(ds, path) - assert server[0].status.status_code is StatusCode.UNSET - # The client IP address and query string are not recorded. - assert "client.address" not in attributes - assert "url.query" not in attributes - - -@pytest.mark.asyncio -async def test_db_query_spans_are_children_of_the_request_span(ds, otel_spans): - """ - Every `db.query` span descends from the request span, which is the only - root span. - """ - otel_spans.clear() - assert (await ds.client.get(f"/{ds.db_name}/t?_facet=v")).status_code == 200 - spans = otel_spans.get_finished_spans() - server = _server_spans(otel_spans) - assert len(server) == 1 - server_span = server[0] - assert server_span.parent is None - - by_span_id = {span.context.span_id: span for span in spans} - roots = [span for span in spans if span.parent is None] - assert [span.name for span in roots] == [server_span.name], ( - "every span from a request should hang off the request span, but these " - f"are roots: {sorted(span.name for span in roots)}" - ) - - queries = [span for span in spans if span.name == "db.query"] - assert queries, "a faceted table page should have issued queries" - for query in queries: - assert query.context.trace_id == server_span.context.trace_id - # Walk up to the root, which should be the request span. - current = query - seen = 0 - while current.parent is not None: - current = by_span_id[current.parent.span_id] - seen += 1 - assert seen < 20, "parent chain did not terminate" - assert current is server_span - - -@pytest.mark.asyncio -async def test_500_sets_error_status_and_error_type(ds, otel_spans): - """ - `route_path` turns the exception into a 500 response, so `error.type` is - the status code as a string. - """ - ds.pm.register(_BoomPlugin(), name="httpspan-boom") - try: - otel_spans.clear() - response = await ds.client.get("/-/http-span-boom") - assert response.status_code == 500 - finally: - ds.pm.unregister(name="httpspan-boom") - server = _server_spans(otel_spans) - assert len(server) == 1 - assert server[0].attributes["http.response.status_code"] == 500 - assert server[0].attributes["error.type"] == "500" - assert server[0].status.status_code is StatusCode.ERROR - - -@pytest.mark.asyncio -async def test_csv_stream_span_covers_the_body_send(ds_paging, otel_spans): - """ - The request span covers a streamed CSV body, including queries for later - pages that run after the response has started. - - Driven as raw ASGI to timestamp `http.response.start` with `time.time_ns()`, - the clock the SDK uses for spans. - """ - app = ds_paging.app() - body = [] - response_started_at = None - - async def receive(): - return {"type": "http.request", "body": b"", "more_body": False} - - async def send(message): - nonlocal response_started_at - if message["type"] == "http.response.start": - assert message["status"] == 200 - response_started_at = time.time_ns() - else: - body.append(message.get("body") or b"") - - otel_spans.clear() - await app( - { - "type": "http", - "http_version": "1.1", - "method": "GET", - "path": f"/{ds_paging.db_name}/t.csv", - "raw_path": f"/{ds_paging.db_name}/t.csv".encode("latin-1"), - "query_string": b"_stream=1", - "scheme": "http", - "headers": [(b"host", b"localhost")], - }, - receive, - send, - ) - # 40 rows plus a header, so the export read past the first page - assert len(b"".join(body).decode("utf-8").strip().splitlines()) == 41 - assert response_started_at is not None - - spans = otel_spans.get_finished_spans() - server = _server_spans(otel_spans) - assert len(server) == 1 - server_span = server[0] - queries = [span for span in spans if span.name == "db.query"] - assert len(queries) > 1 - during_body = [span for span in queries if span.start_time > response_started_at] - assert during_body, ( - "no query ran after the response started, so this workload cannot " - "distinguish a span that covers the body send from one that ends when " - "the handler returns - the export is not paging" - ) - last_query_end = max(span.end_time for span in queries) - assert server_span.end_time > last_query_end, ( - "the request span ended before the last query of a streaming export - " - "it is not covering the response body" - ) - for query in queries: - assert query.context.trace_id == server_span.context.trace_id - - -@pytest.mark.asyncio -async def test_inbound_traceparent_becomes_the_parent(ds, otel_spans): - """ - An inbound `traceparent` header continues the caller's trace. It uses the - sampled flag (`-01`) because the SDK's default sampler is parent-based. - """ - trace_id = "4bf92f3577b34da6a3ce929d0e0e4736" - parent_span_id = "00f067aa0ba902b7" - otel_spans.clear() - response = await ds.client.get( - f"/{ds.db_name}/t", - headers={"traceparent": f"00-{trace_id}-{parent_span_id}-01"}, - ) - assert response.status_code == 200 - server = _server_spans(otel_spans) - assert len(server) == 1 - server_span = server[0] - assert f"{server_span.context.trace_id:032x}" == trace_id - assert server_span.parent is not None - assert f"{server_span.parent.span_id:016x}" == parent_span_id - assert server_span.parent.is_remote - # Database spans are in the caller's trace too. - queries = [ - span for span in otel_spans.get_finished_spans() if span.name == "db.query" - ] - assert queries - for query in queries: - assert f"{query.context.trace_id:032x}" == trace_id - - -@pytest.mark.asyncio -async def test_user_supplied_sql_in_the_query_string_is_never_recorded(ds, otel_spans): - """ - SQL from `?sql=` is not recorded on the request span or in any `url.*` - or `http.*` attribute. `db.query.text` is expected to contain it. - """ - marker = "secret_marker_5b1f" - otel_spans.clear() - # `/{db}?sql=` redirects to the query view, so request that directly. - response = await ds.client.get(f"/{ds.db_name}/-/query?sql=select+'{marker}'") - assert response.status_code == 200 - spans = otel_spans.get_finished_spans() - server = _server_spans(otel_spans) - assert len(server) == 1 - leaked = [ - f"{span.name} -> {key}={value!r}" - for span in spans - for key, value in (span.attributes or {}).items() - if (span is server[0] or str(key).startswith(("url.", "http."))) - and (marker in str(value) or str(key) == "url.query") - ] - assert not leaked, "the query string reached a span attribute: " + ", ".join(leaked) - # Confirm the query ran with the marker. - assert marker in response.text - - -def test_request_span_skips_a_valid_but_non_recording_span(): - """ - `request_span()` returns None for a `NonRecordingSpan` with a valid remote - span context, which is what an inbound `traceparent` produces with no - provider installed. - """ - remote = SpanContext( - trace_id=0x4BF92F3577B34DA6A3CE929D0E0E4736, - span_id=0x00F067AA0BA902B7, - is_remote=True, - trace_flags=TraceFlags(TraceFlags.SAMPLED), - ) - assert remote.is_valid - non_recording = NonRecordingSpan(remote) - assert non_recording.is_recording() is False - assert request_span({REQUEST_SPAN_SCOPE_KEY: non_recording}) is None - # No span in the scope and no current span: - assert request_span({}) is None - # A recording span is returned: - with tracer.start_as_current_span("test.request_span.recording") as span: - assert request_span({REQUEST_SPAN_SCOPE_KEY: span}) is span - # Falls back to the current span, such as one created by another - # SERVER instrumentation: - assert request_span({}) is span - - -NO_PROVIDER_PROGRAM = textwrap.dedent(""" - import asyncio, json, sys - - from datasette.telemetry import TelemetryMiddleware - - seen = {} - - - async def inner(scope, receive, send): - seen.setdefault("sends", []).append(send) - seen.setdefault("scopes", []).append(scope) - await send({"type": "http.response.start", "status": 200, "headers": []}) - await send({"type": "http.response.body", "body": b""}) - - - async def real_send(message): - pass - - - async def main(): - middleware = TelemetryMiddleware(inner) - for headers in ([], [(b"traceparent", b"00-" + b"a" * 32 + b"-" + b"b" * 16 + b"-01")]): - await middleware( - { - "type": "http", - "method": "GET", - "path": "/", - "raw_path": b"/", - "scheme": "http", - "headers": headers, - }, - None, - real_send, - ) - print( - json.dumps( - { - "unwrapped": [send is real_send for send in seen["sends"]], - "scope_keys": [ - "datasette.telemetry.request_span" in scope - for scope in seen["scopes"] - ], - "sdk_imported": any( - name.startswith("opentelemetry.sdk") for name in sys.modules - ), - } - ) - ) - - - asyncio.run(main()) - """) - - -def test_no_provider_takes_the_fast_path(): - """ - With no `TracerProvider` installed the middleware passes the original - `send` to the application, including for requests with a `traceparent`. - - Runs in a subprocess because the suite installs a provider for the whole - process. conftest.py moves this test to the front of the run by name. - """ - result = subprocess.run( - [sys.executable, "-c", NO_PROVIDER_PROGRAM], - capture_output=True, - text=True, - check=True, - ) - report = json.loads(result.stdout) - assert report["sdk_imported"] is False, "the SDK loaded in a fresh interpreter" - assert report["unwrapped"] == [True, True], ( - "the middleware wrapped `send` with no provider installed; the second " - "entry is the inbound-traceparent case, which fails if the fast path " - "is guarded on is_valid instead of is_recording()" - ) - # Nothing is stored in the scope either. - assert report["scope_keys"] == [False, False] - - -@pytest.mark.asyncio -async def test_internal_client_requests_are_marked(ds, otel_spans): - """ - `datasette.internal_client` is set on SERVER spans for `datasette.client` - requests, but not for requests made directly to the ASGI app. - """ - otel_spans.clear() - assert (await ds.client.get("/")).status_code == 200 - server = _server_spans(otel_spans) - assert server - assert all( - span.attributes.get("datasette.internal_client") is True for span in server - ) - - import httpx2 - - transport = httpx2.ASGITransport(app=ds.app()) - async with httpx2.AsyncClient( - transport=transport, base_url="http://localhost" - ) as client: - otel_spans.clear() - assert (await client.get("/")).status_code == 200 - server = _server_spans(otel_spans) - assert server - assert all("datasette.internal_client" not in span.attributes for span in server) diff --git a/tests/test_internal_db.py b/tests/test_internal_db.py index b4bb964d..e1ab51bb 100644 --- a/tests/test_internal_db.py +++ b/tests/test_internal_db.py @@ -1,6 +1,5 @@ -import sqlite3 - import pytest +import sqlite3 from datasette.utils import escape_sqlite from datasette.utils.internal_db import INTERNAL_DB_SCHEMA_SQL @@ -138,7 +137,7 @@ async def test_internal_foreign_key_references(ds_client): return { row[1] for row in conn.execute( - f"PRAGMA table_info({escape_sqlite(table_name)})" + "PRAGMA table_info({})".format(escape_sqlite(table_name)) ).fetchall() } @@ -148,7 +147,7 @@ async def test_internal_foreign_key_references(ds_client): for _, name in sorted( (row[5], row[1]) for row in conn.execute( - f"PRAGMA table_info({escape_sqlite(table_name)})" + "PRAGMA table_info({})".format(escape_sqlite(table_name)) ).fetchall() if row[5] ) @@ -160,7 +159,7 @@ async def test_internal_foreign_key_references(ds_client): for table_name in table_names: foreign_key_rows = conn.execute( - f"PRAGMA foreign_key_list({escape_sqlite(table_name)})" + "PRAGMA foreign_key_list({})".format(escape_sqlite(table_name)) ).fetchall() foreign_keys_by_id = {} for foreign_key in foreign_key_rows: @@ -170,16 +169,25 @@ async def test_internal_foreign_key_references(ds_client): foreign_key_rows.sort(key=lambda row: row[1]) other_table = foreign_key_rows[0][2] other_columns = [row[4] for row in foreign_key_rows] - message = f'Column "{table_name}.{foreign_key_rows[0][3]}" references other table "{other_table}" which does not exist' + message = 'Column "{}.{}" references other table "{}" which does not exist'.format( + table_name, foreign_key_rows[0][3], other_table + ) assert other_table in table_names, message + " (bad table)" if all(other_column is None for other_column in other_columns): other_columns = primary_keys_for_table(other_table) - length_message = f'Foreign key from "{table_name}" to "{other_table}" has {len(foreign_key_rows)} columns but references {len(other_columns)} columns' + length_message = 'Foreign key from "{}" to "{}" has {} columns but references {} columns'.format( + table_name, + other_table, + len(foreign_key_rows), + len(other_columns), + ) assert len(other_columns) == len(foreign_key_rows), length_message for foreign_key, other_column in zip(foreign_key_rows, other_columns): column = foreign_key[3] - message = f'Column "{table_name}.{column}" references other column "{other_table}.{other_column}" which does not exist' + message = 'Column "{}.{}" references other column "{}.{}" which does not exist'.format( + table_name, column, other_table, other_column + ) assert other_column in columns_by_table[other_table], ( message + " (bad column)" ) @@ -237,10 +245,10 @@ async def test_stale_catalog_entry_database_fix(tmp_path): @pytest.mark.asyncio async def test_stale_catalog_child_entries_removed_for_missing_database(tmp_path): - import sqlite3 - from datasette.app import Datasette + import sqlite3 + internal_db_path = str(tmp_path / "internal.db") alpha_db_path = str(tmp_path / "alpha.db") bravo_db_path = str(tmp_path / "bravo.db") @@ -285,10 +293,10 @@ async def test_stale_catalog_child_entries_removed_for_missing_database(tmp_path @pytest.mark.asyncio async def test_orphan_stale_catalog_child_entries_removed(tmp_path): - import sqlite3 - from datasette.app import Datasette + import sqlite3 + internal_db_path = str(tmp_path / "internal.db") alpha_db_path = str(tmp_path / "alpha.db") diff --git a/tests/test_internals_database.py b/tests/test_internals_database.py index 9398afb9..bad4e8ca 100644 --- a/tests/test_internals_database.py +++ b/tests/test_internals_database.py @@ -3,30 +3,16 @@ Tests for the datasette.database.Database class """ import asyncio -import threading -import uuid from types import SimpleNamespace - -import pytest -import sqlite_utils -from opentelemetry import context as otel_context_api - from datasette.app import Datasette -from datasette.database import ( - Database, - DatasetteClosedError, - ExecuteWriteResult, - MultipleValues, - QueryInterrupted, - Results, - _deliver_write_result, -) +from datasette.database import Database, ExecuteWriteResult, Results, MultipleValues +from datasette.database import DatasetteClosedError +from datasette.database import _deliver_write_result +from datasette.utils.sqlite import sqlite3, supports_returning from datasette.utils import Column -from datasette.utils.sqlite import ( - sqlite3, - sqlite_derived_table_dependencies, - supports_returning, -) +import pytest +import time +import uuid requires_sqlite_returning = pytest.mark.skipif( not supports_returning(), reason="SQLite does not support RETURNING" @@ -45,31 +31,6 @@ async def test_execute(db): assert 15 == len(results) -@pytest.mark.asyncio -async def test_derived_dependency_cache_survives_failed_refresh(monkeypatch): - ds = Datasette(memory=True) - db = ds.add_memory_database(uuid.uuid4().hex, name="data") - await db.derived_table_dependencies() - previous_cache = db._cached_derived_table_dependencies - await db.execute_write("create table dependency_cache_refresh (id integer)") - - class UnavailableSchema: - def execute(self, sql): - raise sqlite3.DatabaseError("schema temporarily unavailable") - - with monkeypatch.context() as patch: - patch.setattr( - "datasette.database.sqlite_derived_table_dependencies", - lambda conn: sqlite_derived_table_dependencies(UnavailableSchema()), - ) - with pytest.raises(sqlite3.DatabaseError, match="schema temporarily"): - await db.derived_table_dependencies() - assert db._cached_derived_table_dependencies == previous_cache - - await db.derived_table_dependencies() - assert db._cached_derived_table_dependencies[0] != previous_cache[0] - - @pytest.mark.asyncio async def test_results_first(db): assert None is (await db.execute("select * from facetable where pk > 100")).first() @@ -82,7 +43,7 @@ async def test_results_first(db): @pytest.mark.parametrize("expected", (True, False)) async def test_results_bool(db, expected): where = "" if expected else "where pk = 0" - results = await db.execute(f"select * from facetable {where}") + results = await db.execute("select * from facetable {}".format(where)) assert bool(results) is expected @@ -510,28 +471,6 @@ async def test_view_names(db): ] -@pytest.mark.asyncio -@pytest.mark.parametrize("num_sql_threads", [0, 3]) -async def test_execute_write_zero_time_limit(num_sql_threads): - ds = Datasette(settings={"num_sql_threads": num_sql_threads}) - db = ds.add_memory_database(uuid.uuid4().hex, name="write_limits") - try: - await ds.invoke_startup() - await db.execute_write("create table items(value integer)") - # Zero expires at the first SQLite progress callback, regardless of speed. - with pytest.raises(QueryInterrupted): - await db.execute_write( - "insert into items(value) values (1)", time_limit_ms=0 - ) - # No new timeout handler: the interrupted operation must have cleared it. - await db.execute_write( - "insert into items(value) values (2)", time_limit_ms=None - ) - assert (await db.execute("select value from items")).single_value() == 2 - finally: - ds.close() - - @pytest.mark.asyncio async def test_execute_write_block_true(db): result = await db.execute_write( @@ -676,7 +615,7 @@ async def test_execute_write_block_false(db): "update roadside_attractions set name = ? where pk = ?", ["Mystery!", 1], ) - await asyncio.sleep(0.1) + time.sleep(0.1) rows = await db.execute("select name from roadside_attractions where pk = 1") assert "Mystery!" == rows.rows[0][0] @@ -694,7 +633,7 @@ async def test_execute_write_with_returning_block_false(db): ) assert isinstance(task_id, uuid.UUID) - await asyncio.sleep(0.1) + time.sleep(0.1) assert ( await db.execute("select name from write_returning_block_false") ).single_value() == "Cleo" @@ -759,33 +698,6 @@ async def test_execute_write_fn_block_false(db): assert isinstance(task_id, uuid.UUID) -@pytest.mark.asyncio -@pytest.mark.parametrize("disable_threads", (False, True)) -async def test_execute_write_fn_block_false_returns_uuid(tmp_path, disable_threads): - # block=False is documented to return "a UUID representing the queued task". - # With num_sql_threads=0 there is no write thread, so the non-threaded branch - # has to satisfy the same contract as the threaded one. - settings = {"num_sql_threads": 0} if disable_threads else {} - ds = Datasette([], memory=True, settings=settings) - await ds.invoke_startup() - db = ds.add_memory_database("test_block_false") - await db.execute_write( - "create table if not exists t (id integer primary key, v text)" - ) - - def write_fn(conn): - conn.execute("insert into t (v) values ('a')") - # Returns None, like most write functions. - - task_id = await db.execute_write_fn(write_fn, block=False) - - assert isinstance(task_id, uuid.UUID) - # Distinct per call, so a caller can tell two queued tasks apart. - second = await db.execute_write_fn(write_fn, block=False) - assert isinstance(second, uuid.UUID) - assert second != task_id - - @pytest.mark.asyncio async def test_execute_write_fn_block_true(db): def write_fn(conn): @@ -806,51 +718,15 @@ async def test_execute_write_fn_exception(db): await db.execute_write_fn(write_fn) -@pytest.mark.asyncio -@pytest.mark.parametrize("num_sql_threads", (0, 1)) -async def test_execute_write_fn_sqlite_utils_transaction(tmp_path, num_sql_threads): - # A write inside a failing Datasette task must never become visible or - # survive the rollback. Exercise both the synchronous and writer-thread - # paths against a file-backed database so a second connection can observe - # committed state independently. - db_path = tmp_path / "test.db" - sqlite3.connect(db_path).close() - ds = Datasette([str(db_path)], settings={"num_sql_threads": num_sql_threads}) - db = ds.get_database("test") - await db.execute_write("create table items (id integer primary key)") - # This reader is used inside the write callback, which may run on another - # thread, but it is never accessed concurrently. - reader = sqlite3.connect(db_path, check_same_thread=False) - - def insert_then_fail(conn): - # Datasette must open the outer transaction before sqlite-utils writes. - assert conn.in_transaction - sqlite_utils.Database(conn)["items"].insert({"id": 1}) - # If sqlite-utils committed its own transaction, this would return 1. - assert reader.execute("select count(*) from items").fetchone()[0] == 0 - # Simulate a later step failing after the sqlite-utils write succeeded. - raise ValueError("deliberate") - - try: - with pytest.raises(ValueError, match="deliberate"): - await db.execute_write_fn(insert_then_fail) - # The outer transaction must roll back the sqlite-utils write as well. - assert reader.execute("select count(*) from items").fetchone()[0] == 0 - finally: - reader.close() - db.close() - - @pytest.mark.asyncio @pytest.mark.parametrize("param_name", ["conn", "connection", "db", "c"]) async def test_execute_write_fn_accepts_any_single_param_name(db, param_name): # Plugins historically relied on the fact that the callback was invoked # positionally, so any parameter name worked. Preserve that contract. scope = {} - # exec() is how we build a function with a parameterized argument name - exec( # noqa: S102 - f"def write_fn({param_name}):\n" - f" return {param_name}.execute('select 1 + 1').fetchone()[0]", + exec( + "def write_fn({0}):\n" + " return {0}.execute('select 1 + 1').fetchone()[0]".format(param_name), scope, ) write_fn = scope["write_fn"] @@ -874,9 +750,7 @@ async def test_execute_write_fn_with_track_event(db): @pytest.mark.asyncio -# func_only so the budget covers the write-thread call under test, not the -# one-off app_client fixture setup this test may be first to trigger -@pytest.mark.timeout(1, func_only=True) +@pytest.mark.timeout(1) async def test_execute_write_fn_connection_exception(tmpdir, app_client): path = str(tmpdir / "immutable.db") conn = sqlite3.connect(path) @@ -1304,103 +1178,3 @@ async def test_database_close_is_idempotent(tmpdir): # Second call should be a no-op, not raise db.close() ds._internal_database.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("num_sql_threads", [0, 2]) -@pytest.mark.parametrize("named", [False, True]) -async def test_close_releases_memory_connections(num_sql_threads, named): - ds = Datasette(memory=True, settings={"num_sql_threads": num_sql_threads}) - db = ds.add_memory_database(uuid.uuid4().hex) if named else ds.get_database() - read_connection = await db.execute_fn(lambda conn: conn) - write_connection = await db.execute_write_fn(lambda conn: conn) - ds.close() - for conn in (read_connection, write_connection): - with pytest.raises(sqlite3.ProgrammingError, match="closed"): - conn.execute("select 1") - - -_CONTEXT_LEAK_MARKER_KEY = "otel-context-leak-marker" - - -@pytest.mark.asyncio -@pytest.mark.parametrize("num_sql_threads", (0, 1)) -async def test_write_thread_context_is_detached_between_tasks( - tmp_path, monkeypatch, num_sql_threads -): - """ - The write thread attaches each task's OpenTelemetry context and detaches - it before the next task, including when the task raises an exception. - - Checks that each task sees the context from when it was queued, and that - the write thread's attach depth does not grow between tasks. - """ - name = f"context_leak_test_{num_sql_threads}" - db_path = tmp_path / f"{name}.db" - sqlite3.connect(db_path).close() - ds = Datasette([str(db_path)], settings={"num_sql_threads": num_sql_threads}) - db = ds.get_database(name) - await db.execute_write("create table t (id integer primary key)") - - write_thread_name = f"_execute_writes for database {name}" - depth = {"value": 0} - real_attach = otel_context_api.attach - real_detach = otel_context_api.detach - - def counting_attach(context): - token = real_attach(context) - if threading.current_thread().name == write_thread_name: - depth["value"] += 1 - return token - - def counting_detach(token): - real_detach(token) - if threading.current_thread().name == write_thread_name: - depth["value"] -= 1 - - # database.py and opentelemetry.trace both call these via the module - monkeypatch.setattr(otel_context_api, "attach", counting_attach) - monkeypatch.setattr(otel_context_api, "detach", counting_detach) - - seen_markers = [] - seen_depths = [] - - def probe(conn): - seen_markers.append(otel_context_api.get_value(_CONTEXT_LEAK_MARKER_KEY)) - seen_depths.append(depth["value"]) - - def failing_probe(conn): - probe(conn) - raise ValueError("deliberate failure inside a write task") - - try: - for i in range(5): - ctx = otel_context_api.set_value(_CONTEXT_LEAK_MARKER_KEY, f"marker-{i}") - token = real_attach(ctx) - try: - if i == 2: - with pytest.raises(ValueError): - await db.execute_write_fn(failing_probe) - else: - await db.execute_write_fn(probe) - finally: - real_detach(token) - - # No marker is set here, so the final probe should see None - assert otel_context_api.get_value(_CONTEXT_LEAK_MARKER_KEY) is None - await db.execute_write_fn(probe) - finally: - db.close() - - assert seen_markers == [ - "marker-0", - "marker-1", - "marker-2", - "marker-3", - "marker-4", - None, - ] - assert len(set(seen_depths)) == 1, ( - f"write thread context stack grew across tasks: {seen_depths} - " - "a token was attached without being detached" - ) diff --git a/tests/test_internals_datasette.py b/tests/test_internals_datasette.py index ed2aeaf0..85598c05 100644 --- a/tests/test_internals_datasette.py +++ b/tests/test_internals_datasette.py @@ -9,15 +9,13 @@ import importlib import os import sqlite3 import time - -import pytest -from itsdangerous import BadSignature - from datasette import Context -from datasette.app import Database, Datasette, ResourcesSQL +from datasette.app import Datasette, Database, ResourcesSQL from datasette.database import DatasetteClosedError from datasette.resources import DatabaseResource from datasette.utils import PrefixedUrlString +from itsdangerous import BadSignature +import pytest @pytest.fixture @@ -79,7 +77,9 @@ async def test_static_template_function_hashes_core_asset(tmp_path, monkeypatch) template = ds.get_jinja_environment().from_string("{{ static('demo.js') }}") expected_hash = hashlib.sha256(b"const demo = true;").hexdigest()[:12] - assert await template.render_async() == f"/-/static/demo.js?_hash={expected_hash}" + assert await template.render_async() == "/-/static/demo.js?_hash={}".format( + expected_hash + ) assert isinstance(ds.static("demo.js"), PrefixedUrlString) @@ -101,7 +101,7 @@ def test_static_hash_recalculated_when_cache_headers_disabled(tmp_path, monkeypa asset_path.write_bytes(b"let a = 2;") expected_hash = hashlib.sha256(b"let a = 2;").hexdigest()[:12] - assert ds.static("demo.js") == f"/-/static/demo.js?_hash={expected_hash}" + assert ds.static("demo.js") == "/-/static/demo.js?_hash={}".format(expected_hash) assert ds.static("demo.js") != first_url @@ -114,12 +114,12 @@ def test_static_hashes_mounted_static_file(tmp_path): expected_hash = hashlib.sha256(b"body { color: black; }").hexdigest()[:12] assert ds.static("styles.css", mount="assets") == ( - f"/assets/styles.css?_hash={expected_hash}" + "/assets/styles.css?_hash={}".format(expected_hash) ) ds._settings["base_url"] = "/prefix/" assert ds.static("styles.css", mount="assets") == ( - f"/prefix/assets/styles.css?_hash={expected_hash}" + "/prefix/assets/styles.css?_hash={}".format(expected_hash) ) @@ -144,7 +144,9 @@ def test_static_hashes_plugin_static_file(tmp_path, monkeypatch): expected_hash = hashlib.sha256(b"console.log('plugin');").hexdigest()[:12] assert ds.static("plugin.js", plugin="datasette_cluster_map") == ( - f"/-/static-plugins/datasette_cluster_map/plugin.js?_hash={expected_hash}" + "/-/static-plugins/datasette_cluster_map/plugin.js?_hash={}".format( + expected_hash + ) ) diff --git a/tests/test_internals_datasette_client.py b/tests/test_internals_datasette_client.py index 29046dc3..e9aaaae8 100644 --- a/tests/test_internals_datasette_client.py +++ b/tests/test_internals_datasette_client.py @@ -1,7 +1,6 @@ -import httpx2 +import httpx import pytest import pytest_asyncio - from datasette.app import Datasette @@ -43,7 +42,7 @@ async def datasette_with_permissions(): async def test_client_methods(datasette, method, path, expected_status): client_method = getattr(datasette.client, method) response = await client_method(path) - assert isinstance(response, httpx2.Response) + assert isinstance(response, httpx.Response) assert response.status_code == expected_status # Try that again using datasette.client.request response2 = await datasette.client.request(method, path) @@ -63,7 +62,7 @@ async def test_client_post(datasette, prefix): "message": "A message", }, ) - assert isinstance(response, httpx2.Response) + assert isinstance(response, httpx.Response) assert response.status_code == 302 assert "ds_messages" in response.cookies finally: @@ -135,7 +134,7 @@ async def test_skip_permission_checks_all_methods(datasette_with_permissions, me response = await client_method("/test_db.json", skip_permission_checks=True) # We don't check status code since some methods might not be allowed, # but we verify the request doesn't fail due to permissions - assert isinstance(response, httpx2.Response) + assert isinstance(response, httpx.Response) @pytest.mark.asyncio @@ -239,7 +238,7 @@ async def test_in_client_returns_false_outside_request(datasette): @pytest.mark.asyncio async def test_in_client_returns_true_inside_request(): """Test that datasette.in_client() returns True inside a client request""" - from datasette import Response, hookimpl + from datasette import hookimpl, Response class TestPlugin: __name__ = "test_in_client_plugin" @@ -340,7 +339,7 @@ async def test_actor_parameter_all_http_methods(datasette, method): client_method = getattr(datasette.client, method) # Just verify no TypeError about unexpected 'actor' kwarg response = await client_method("/", actor={"id": "root"}) - assert isinstance(response, httpx2.Response) + assert isinstance(response, httpx.Response) @pytest.mark.asyncio diff --git a/tests/test_internals_request.py b/tests/test_internals_request.py index 91ef368f..6d2dc70a 100644 --- a/tests/test_internals_request.py +++ b/tests/test_internals_request.py @@ -1,8 +1,6 @@ -import json - -import pytest - from datasette.utils.asgi import PayloadTooLarge, Request +import json +import pytest def _post_scope(headers=None): @@ -35,51 +33,6 @@ def _receive_chunks(chunks): return receive -@pytest.mark.parametrize( - "header_name", [b"content-type", b"Content-Type", b"CONTENT-TYPE"] -) -@pytest.mark.parametrize("lookup", ["content-type", "Content-Type", "CONTENT-TYPE"]) -def test_request_headers_case_insensitive(header_name, lookup): - request = Request({"headers": [(header_name, b"application/json")]}, None) - assert request.headers.get(lookup) == "application/json" - assert request.headers[lookup] == "application/json" - assert lookup in request.headers - - -def test_request_headers_mapping(): - request = Request( - { - "headers": [ - (b"Content-Type", b"application/json"), - (b"X-Title", "café".encode("latin-1")), - (b"CONTENT-TYPE", b"text/plain"), - ] - }, - None, - ) - headers = request.headers - expected = {"content-type": "text/plain", "x-title": "café"} - assert headers == expected - assert dict(headers) == expected - assert list(headers) == list(expected) - assert list(headers.keys()) == list(expected.keys()) - assert list(headers.items()) == list(expected.items()) - assert json.loads(json.dumps(headers)) == expected - assert headers["Content-Type"] == "text/plain" - assert headers["X-Title"] == "café" - - -@pytest.mark.parametrize("scope", [{}, {"headers": None}, {"headers": []}]) -def test_request_headers_missing(scope): - headers = Request(scope, None).headers - assert headers == {} - assert headers.get("Content-Type") is None - assert headers.get("Content-Type", "default") == "default" - assert "Content-Type" not in headers - with pytest.raises(KeyError): - headers["Content-Type"] - - @pytest.mark.asyncio async def test_request_post_vars(): scope = { diff --git a/tests/test_internals_response.py b/tests/test_internals_response.py index aa3e1ae2..2366dcde 100644 --- a/tests/test_internals_response.py +++ b/tests/test_internals_response.py @@ -1,8 +1,6 @@ -import json - -import pytest - from datasette.utils.asgi import Response +import json +import pytest def test_response_html(): diff --git a/tests/test_internals_urls.py b/tests/test_internals_urls.py index 50c61995..24fa745d 100644 --- a/tests/test_internals_urls.py +++ b/tests/test_internals_urls.py @@ -1,7 +1,6 @@ -import pytest - from datasette.app import Datasette from datasette.utils import PrefixedUrlString +import pytest @pytest.fixture(scope="module") diff --git a/tests/test_label_column_for_table.py b/tests/test_label_column_for_table.py index b67b8882..7667b595 100644 --- a/tests/test_label_column_for_table.py +++ b/tests/test_label_column_for_table.py @@ -1,7 +1,6 @@ import pytest - -from datasette.app import Datasette from datasette.database import Database +from datasette.app import Datasette @pytest.mark.asyncio diff --git a/tests/test_lifespan.py b/tests/test_lifespan.py deleted file mode 100644 index 3a25ccbc..00000000 --- a/tests/test_lifespan.py +++ /dev/null @@ -1,344 +0,0 @@ -""" -Tests for wiring Datasette startup (setup_db table counts + invoke_startup) -into the ASGI lifespan protocol. - -These exercise Datasette._startup_sequence() via three different callers: -- AsgiLifespan, by hand-driving lifespan.startup messages (no HTTP request) -- AsgiRunOnFirstRequest, the fallback for hosts that never send lifespan - events (this is what DatasetteClient / plain httpx2.ASGITransport uses) -- Both at once, to prove startup hooks run at most once -""" - -import asyncio -import contextlib -import sqlite3 - -import httpx2 -import pytest - -from datasette import hookimpl -from datasette.app import Datasette -from datasette.database import Database -from datasette.plugins import pm - - -async def _drive_lifespan_startup(app): - """Send a single lifespan.startup message into app's ASGI lifespan loop - and return the list of messages sent back - without ever sending - lifespan.shutdown. Mirrors what a real server does: after startup - completes it parks waiting for the next event. We cancel that wait - once we've observed the startup response, rather than closing the - Datasette instance down with a shutdown message. - """ - messages_sent = [] - startup_responded = asyncio.Event() - delivered = False - - async def receive(): - nonlocal delivered - if not delivered: - delivered = True - return {"type": "lifespan.startup"} - # No further messages: block until the task is cancelled below, - # same as a real server parked waiting for lifespan.shutdown. - await asyncio.Event().wait() - - async def send(message): - messages_sent.append(message) - startup_responded.set() - - task = asyncio.create_task(app({"type": "lifespan"}, receive, send)) - try: - await asyncio.wait_for(startup_responded.wait(), timeout=5) - finally: - task.cancel() - with contextlib.suppress(asyncio.CancelledError): - await task - return messages_sent - - -@pytest.mark.asyncio -async def test_lifespan_startup_runs_before_any_request(): - ds = Datasette(memory=True) - assert ds._startup_invoked is False - app = ds.app() - - messages = await _drive_lifespan_startup(app) - - assert {"type": "lifespan.startup.complete"} in messages - assert ds._startup_invoked is True - # Internal catalog tables should be populated too, entirely without an - # HTTP request having been made. - internal_db = ds.get_internal_database() - databases = await internal_db.execute("select * from catalog_databases") - assert len(databases.rows) >= 1 - - -@pytest.mark.asyncio -async def test_lifespan_startup_failure_reports_lifespan_startup_failed(): - class RaisingStartupPlugin: - __name__ = "RaisingStartupPlugin" - - @hookimpl - def startup(self, datasette): - async def inner(): - raise RuntimeError("boom from startup hook") - - return inner - - ds = Datasette(memory=True) - pm.register(RaisingStartupPlugin(), name="raising_startup_plugin") - try: - app = ds.app() - messages = await _drive_lifespan_startup(app) - finally: - pm.unregister(name="raising_startup_plugin") - - assert messages == [ - {"type": "lifespan.startup.failed", "message": "boom from startup hook"} - ] - # The exception happened before invoke_startup() got to the end of its - # body, so startup is not considered to have completed. - assert ds._startup_invoked is False - - -@pytest.mark.asyncio -async def test_startup_runs_exactly_once_across_lifespan_and_first_request(): - call_count = {"n": 0} - - class CountingStartupPlugin: - __name__ = "CountingStartupPlugin" - - @hookimpl - def startup(self, datasette): - async def inner(): - call_count["n"] += 1 - - return inner - - ds = Datasette(memory=True) - pm.register(CountingStartupPlugin(), name="counting_startup_plugin") - try: - # Build the ASGI app once, the way a real deployment does - and - # reuse the SAME app instance for both the lifespan drive and the - # HTTP requests below, since a fresh ds.app() call would reset the - # AsgiRunOnFirstRequest fallback's state. - app = ds.app() - - messages = await _drive_lifespan_startup(app) - assert {"type": "lifespan.startup.complete"} in messages - assert call_count["n"] == 1 - - # A first HTTP request (as if the host never sent lifespan events, - # or lifespan already ran) should not run the hook again. - transport = httpx2.ASGITransport(app=app) - async with httpx2.AsyncClient( - transport=transport, base_url="http://localhost" - ) as client: - response1 = await client.get("/-/versions.json") - assert response1.status_code == 200 - # ... nor should a second, repeat request. - response2 = await client.get("/-/versions.json") - assert response2.status_code == 200 - finally: - pm.unregister(name="counting_startup_plugin") - - assert call_count["n"] == 1 - - -@pytest.mark.asyncio -async def test_no_lifespan_first_request_still_triggers_startup(): - # Pin today's behavior: a client that never drives ASGI lifespan events - # at all (like httpx2.ASGITransport, which DatasetteClient uses) still - # gets startup armed by the AsgiRunOnFirstRequest fallback. - ds = Datasette(memory=True) - assert ds._startup_invoked is False - app = ds.app() - transport = httpx2.ASGITransport(app=app) - async with httpx2.AsyncClient( - transport=transport, base_url="http://localhost" - ) as client: - response = await client.get("/-/versions.json") - assert response.status_code == 200 - - assert ds._startup_invoked is True - internal_db = ds.get_internal_database() - databases = await internal_db.execute("select * from catalog_databases") - assert len(databases.rows) >= 1 - - -@pytest.mark.asyncio -async def test_datasette_client_first_request_triggers_startup(): - # Same as above, but through the real DatasetteClient (ds.client) that - # plugins and tests actually use, to confirm nothing regressed there. - ds = Datasette(memory=True) - assert ds._startup_invoked is False - response = await ds.client.get("/-/versions.json") - assert response.status_code == 200 - assert ds._startup_invoked is True - - -@pytest.mark.asyncio -async def test_concurrent_first_requests_all_wait_for_slow_startup(): - call_count = {"n": 0} - - class SlowStartupPlugin: - __name__ = "SlowStartupPlugin" - - @hookimpl - def startup(self, datasette): - async def inner(): - call_count["n"] += 1 - await asyncio.sleep(0.2) - - return inner - - ds = Datasette(memory=True) - pm.register(SlowStartupPlugin(), name="slow_startup_plugin") - try: - app = ds.app() - transport = httpx2.ASGITransport(app=app) - async with httpx2.AsyncClient( - transport=transport, base_url="http://localhost" - ) as client: - responses = await asyncio.gather( - *[client.get("/-/versions.json") for _ in range(10)] - ) - finally: - pm.unregister(name="slow_startup_plugin") - - # Every one of the 10 simultaneous first requests must have blocked - # until startup actually finished, not raced ahead of it. - assert all(response.status_code == 200 for response in responses) - assert call_count["n"] == 1 - assert ds._startup_invoked is True - - -@pytest.mark.asyncio -async def test_setup_db_still_runs_when_invoke_startup_ran_first(tmp_path, monkeypatch): - # Regression test: `datasette serve` (cli.py _serve_async) calls - # ds.invoke_startup() directly, before uvicorn ever sends a - # lifespan.startup event that drives _startup_sequence(). If - # _startup_sequence()'s fast path only checked `_startup_invoked`, it - # would see startup already done and skip the immutable-database - # table-count precompute (setup_db) entirely - a silent regression - # versus main, where AsgiRunOnFirstRequest ran setup_db unconditionally - # on request #1. - db_path = tmp_path / "immutable.db" - conn = sqlite3.connect(str(db_path)) - conn.execute("create table t (id integer primary key)") - conn.commit() - conn.close() - - ds = Datasette([], immutables=[str(db_path)]) - - call_count = {"n": 0} - original_table_counts = Database.table_counts - - async def counting_table_counts(self, *args, **kwargs): - call_count["n"] += 1 - return await original_table_counts(self, *args, **kwargs) - - monkeypatch.setattr(Database, "table_counts", counting_table_counts) - - # Simulate the CLI path: invoke_startup() runs directly and completes - # BEFORE _startup_sequence() ever gets a chance to run setup_db. - await ds.invoke_startup() - assert ds._startup_invoked is True - assert call_count["n"] == 0 - - # The lifespan/first-request path (or the CLI itself, per the fix) - # calling the shared entry point afterwards must still precompute - # table counts for immutable databases. - await ds._startup_sequence() - assert call_count["n"] == 1 - assert ds._setup_db_done is True - - # Idempotency: a second call must not recompute. - await ds._startup_sequence() - assert call_count["n"] == 1 - - -@pytest.mark.asyncio -async def test_asgi_wrapper_runs_after_startup_fallback_path(): - class AssertStartupPlugin: - __name__ = "AssertStartupPlugin" - - @hookimpl - def asgi_wrapper(self, datasette): - def wrap(app): - async def check_startup(scope, receive, send): - if scope["type"] == "http": - assert ( - datasette._startup_invoked is True - ), "asgi_wrapper saw an http scope before startup completed" - await app(scope, receive, send) - - return check_startup - - return wrap - - ds = Datasette(memory=True) - pm.register(AssertStartupPlugin(), name="assert_startup_plugin") - try: - assert ds._startup_invoked is False - app = ds.app() - transport = httpx2.ASGITransport(app=app) - async with httpx2.AsyncClient( - transport=transport, base_url="http://localhost" - ) as client: - response = await client.get("/-/versions.json") - assert response.status_code == 200 - finally: - pm.unregister(name="assert_startup_plugin") - - assert ds._startup_invoked is True - - -@pytest.mark.asyncio -async def test_short_circuit_wrapper_no_longer_defers_startup(): - # Middleware that returns a response before getting to the rest of - # Datasette should still cause _startup_invoked=True - class ShortCircuitPlugin: - __name__ = "ShortCircuitPlugin" - - @hookimpl - def asgi_wrapper(self, datasette): - def wrap(app): - async def forbidden(scope, receive, send): - if scope["type"] != "http": - await app(scope, receive, send) - return - await send( - { - "type": "http.response.start", - "status": 403, - "headers": [[b"content-type", b"text/plain"]], - } - ) - await send( - { - "type": "http.response.body", - "body": b"Forbidden", - } - ) - - return forbidden - - return wrap - - ds = Datasette(memory=True) - pm.register(ShortCircuitPlugin(), name="short_circuit_plugin") - try: - assert ds._startup_invoked is False - app = ds.app() - transport = httpx2.ASGITransport(app=app) - async with httpx2.AsyncClient( - transport=transport, base_url="http://localhost" - ) as client: - response = await client.get("/-/versions.json") - assert response.status_code == 403 - finally: - pm.unregister(name="short_circuit_plugin") - - assert ds._startup_invoked is True diff --git a/tests/test_load_extensions.py b/tests/test_load_extensions.py index a7c2bc24..cdadb091 100644 --- a/tests/test_load_extensions.py +++ b/tests/test_load_extensions.py @@ -1,9 +1,6 @@ -from pathlib import Path -from unittest import mock - -import pytest - from datasette.app import Datasette +import pytest +from pathlib import Path # not necessarily a full path - the full compiled path looks like "ext.dylib" # or another suffix, but sqlite will, under the hood, decide which file @@ -21,29 +18,6 @@ def has_compiled_ext(): return False -@pytest.mark.parametrize("load_fails", (False, True)) -def test_load_extension_is_disabled(load_fails): - ds = Datasette(sqlite_extensions=[COMPILED_EXTENSION_PATH]) - connection = mock.Mock() - if load_fails: - connection.load_extension.side_effect = RuntimeError - - if load_fails: - with pytest.raises(RuntimeError): - ds._prepare_connection(connection, "data") - else: - ds._prepare_connection(connection, "data") - - # Extensions are loaded using the Python API, never via SQL - assert connection.load_extension.mock_calls == [ - mock.call(COMPILED_EXTENSION_PATH), - ] - assert connection.enable_load_extension.mock_calls == [ - mock.call(True), - mock.call(False), - ] - - @pytest.mark.asyncio @pytest.mark.skipif(not has_compiled_ext(), reason="Requires compiled ext.c") async def test_load_extension_default_entrypoint(): @@ -88,20 +62,3 @@ async def test_load_extension_multiple_entrypoints(): response = await ds.client.get("/_memory/-/query.json?_shape=arrays&sql=select+c()") assert response.status_code == 200 assert response.json()["rows"][0][0] == "c" - - -@pytest.mark.asyncio -@pytest.mark.skipif(not has_compiled_ext(), reason="Requires compiled ext.c") -async def test_sql_cannot_load_additional_extension(): - ds = Datasette(sqlite_extensions=[COMPILED_EXTENSION_PATH]) - - response = await ds.client.get( - "/_memory/-/query.json", - params={ - "sql": "select load_extension(:path, :entrypoint)", - "path": COMPILED_EXTENSION_PATH, - "entrypoint": "sqlite3_ext_b_init", - }, - ) - assert response.status_code == 400 - assert response.json()["error"] == "not authorized" diff --git a/tests/test_messages.py b/tests/test_messages.py index 60eb7938..62d9f647 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -1,6 +1,5 @@ -import pytest - from .utils import cookie_was_deleted +import pytest @pytest.mark.asyncio diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 8cc91db3..0dc3ecd7 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -4,145 +4,14 @@ Tests for request.form() multipart form data parsing. Uses TDD approach - these tests are written first, then implementation follows. """ -import asyncio import base64 import json -import threading +import pytest from collections import namedtuple -import pytest from multipart_form_data_conformance import get_tests_dir -from datasette.utils.asgi import BadRequest, Request -from datasette.utils.multipart import MultipartParseError, parse_form_data - - -@pytest.fixture -def upload_files(monkeypatch): - from datasette.utils import multipart - - files = [] - original = multipart.tempfile.SpooledTemporaryFile - - def create_file(*args, **kwargs): - file = original(*args, **kwargs) - files.append(file) - return file - - monkeypatch.setattr(multipart.tempfile, "SpooledTemporaryFile", create_file) - yield files - for file in files: - file.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "failure", - [ - "file_limit", - "request_limit", - "truncated", - "disconnect", - "receive_error", - "cancel", - ], -) -async def test_failed_upload_closes_completed_and_partial_files(upload_files, failure): - # Complete one file and begin another, flushing the parser's 64 KiB batch. - body = ( - b'--boundary\r\nContent-Disposition: form-data; name="one"; filename="one"\r\n\r\n' - b'first\r\n--boundary\r\nContent-Disposition: form-data; name="two"; filename="two"\r\n\r\n' - + b"x" * (64 * 1024) - ) - calls = 0 - - async def receive(): - nonlocal calls - calls += 1 - if calls == 1: - return {"type": "http.request", "body": body, "more_body": True} - if failure == "disconnect": - return {"type": "http.disconnect"} - if failure == "receive_error": - raise OSError("receive failed") - if failure == "cancel": - raise asyncio.CancelledError - return {"type": "http.request", "body": b"x" * 100, "more_body": False} - - kwargs = {} - if failure == "file_limit": - kwargs["max_file_size"] = 1024 - if failure == "request_limit": - kwargs["max_request_size"] = len(body) - error = { - "receive_error": OSError, - "cancel": asyncio.CancelledError, - }.get(failure, MultipartParseError) - with pytest.raises(error): - await parse_form_data( - receive, "multipart/form-data; boundary=boundary", files=True, **kwargs - ) - assert len(upload_files) == 2 - assert all(file.closed for file in upload_files) - - -@pytest.mark.asyncio -async def test_unnamed_upload_is_closed(upload_files): - body = ( - b'--boundary\r\nContent-Disposition: form-data; filename="ignored"\r\n\r\n' - b"content\r\n--boundary--\r\n" - ) - form = await parse_form_data( - make_receive(body), "multipart/form-data; boundary=boundary", files=True - ) - assert len(form) == 0 - assert len(upload_files) == 1 - assert upload_files[0].closed - - -@pytest.mark.asyncio -@pytest.mark.parametrize("worker_error", [False, True]) -async def test_cancelled_upload_waits_for_worker( - upload_files, monkeypatch, worker_error -): - from datasette.utils.multipart import MultipartParser - - loop = asyncio.get_running_loop() - started = asyncio.Event() - release = threading.Event() - original_feed = MultipartParser.feed - - def blocking_feed(self, chunk): - original_feed(self, chunk) - loop.call_soon_threadsafe(started.set) - assert release.wait(5) - if worker_error: - raise MultipartParseError("worker failed") - - monkeypatch.setattr(MultipartParser, "feed", blocking_feed) - body = ( - b'--boundary\r\nContent-Disposition: form-data; name="file"; filename="file"\r\n\r\n' - + b"x" * (64 * 1024) - ) - task = asyncio.create_task( - parse_form_data( - make_receive(body), "multipart/form-data; boundary=boundary", files=True - ) - ) - try: - await asyncio.wait_for(started.wait(), timeout=5) - # A second cancellation must also leave cleanup waiting for the worker. - for _ in range(2): - task.cancel() - await asyncio.sleep(0) - assert not task.done() - assert len(upload_files) == 1 - assert not upload_files[0].closed - finally: - release.set() - with pytest.raises(asyncio.CancelledError): - await task - assert upload_files[0].closed +from datasette.utils.asgi import Request, BadRequest def make_receive(body: bytes): @@ -1209,73 +1078,75 @@ async def test_conformance(test_spec, headers, body): await request.form(files=True) return - async with await request.form(files=True) as form: - # Verify each expected part - for i, expected_part in enumerate(expected["parts"]): - name = expected_part["name"] + # Parse form data + form = await request.form(files=True) - # Get value(s) for this name - values = form.getlist(name) + # Verify each expected part + for i, expected_part in enumerate(expected["parts"]): + name = expected_part["name"] - # Find the value at the correct index for this name - # (handles multiple values with same name) - same_name_count = sum(1 for p in expected["parts"][:i] if p["name"] == name) + # Get value(s) for this name + values = form.getlist(name) - if same_name_count >= len(values): - pytest.fail( - f"Expected part {name} at index {same_name_count} but only {len(values)} found" - ) + # Find the value at the correct index for this name + # (handles multiple values with same name) + same_name_count = sum(1 for p in expected["parts"][:i] if p["name"] == name) - value = values[same_name_count] - - # Determine expected content - if "body_base64" in expected_part: - expected_content = base64.b64decode(expected_part["body_base64"]) - elif "body_text" in expected_part: - expected_content = expected_part["body_text"].encode("utf-8") - else: - expected_content = None - - # Check for file vs field - # A part is a file if it has a filename OR filename_star - is_file = ( - expected_part.get("filename") is not None - or expected_part.get("filename_star") is not None + if same_name_count >= len(values): + pytest.fail( + f"Expected part {name} at index {same_name_count} but only {len(values)} found" ) - if is_file: - # It's a file - assert hasattr(value, "filename"), f"Expected file for {name}" + value = values[same_name_count] - # Check filename - use filename_star if present, else filename - expected_filename = expected_part.get( - "filename_star" - ) or expected_part.get("filename") - if expected_filename: - assert ( - value.filename == expected_filename - ), f"Filename mismatch: expected {expected_filename!r}, got {value.filename!r}" + # Determine expected content + if "body_base64" in expected_part: + expected_content = base64.b64decode(expected_part["body_base64"]) + elif "body_text" in expected_part: + expected_content = expected_part["body_text"].encode("utf-8") + else: + expected_content = None - if expected_part.get("content_type"): - assert value.content_type == expected_part["content_type"] + # Check for file vs field + # A part is a file if it has a filename OR filename_star + is_file = ( + expected_part.get("filename") is not None + or expected_part.get("filename_star") is not None + ) - content = await value.read() + if is_file: + # It's a file + assert hasattr(value, "filename"), f"Expected file for {name}" + + # Check filename - use filename_star if present, else filename + expected_filename = expected_part.get("filename_star") or expected_part.get( + "filename" + ) + if expected_filename: assert ( - len(content) == expected_part["body_size"] - ), f"Size mismatch: expected {expected_part['body_size']}, got {len(content)}" - if expected_content is not None: - assert content == expected_content - else: - # It's a text field - if hasattr(value, "filename"): - pytest.fail(f"Expected text field for {name}, got file") + value.filename == expected_filename + ), f"Filename mismatch: expected {expected_filename!r}, got {value.filename!r}" - if expected_content is not None: - # For text fields, value is a string - try: - expected_text = expected_content.decode("utf-8") - except UnicodeDecodeError: - expected_text = expected_content.decode("latin-1") - assert ( - value == expected_text - ), f"Value mismatch: expected {expected_text!r}, got {value!r}" + if expected_part.get("content_type"): + assert value.content_type == expected_part["content_type"] + + content = await value.read() + assert ( + len(content) == expected_part["body_size"] + ), f"Size mismatch: expected {expected_part['body_size']}, got {len(content)}" + if expected_content is not None: + assert content == expected_content + else: + # It's a text field + if hasattr(value, "filename"): + pytest.fail(f"Expected text field for {name}, got file") + + if expected_content is not None: + # For text fields, value is a string + try: + expected_text = expected_content.decode("utf-8") + except UnicodeDecodeError: + expected_text = expected_content.decode("latin-1") + assert ( + value == expected_text + ), f"Value mismatch: expected {expected_text!r}, got {value!r}" diff --git a/tests/test_numeric_filter_values.py b/tests/test_numeric_filter_values.py deleted file mode 100644 index 1fceeabe..00000000 --- a/tests/test_numeric_filter_values.py +++ /dev/null @@ -1,47 +0,0 @@ -import sqlite3 -from contextlib import closing - -import pytest - -from datasette.filters import Filters - - -@pytest.mark.parametrize( - "value,expected", - ( - ("3.5", 3.5), - ("-2", -2), - ("-2.5", -2.5), - ("1e3", 1000.0), - ("not-a-number", "not-a-number"), - ("nan", "nan"), - ("inf", "inf"), - ("-inf", "-inf"), - ), -) -def test_numeric_filter_parameters(value, expected): - filters = Filters((("score__gt", value),)) - sql_bits, params = filters.build_where_clauses("items") - - assert sql_bits == ['"score" > :p0'] - assert params == {"p0": expected} - - -def test_numeric_filter_parameters_against_calculated_view(): - with closing(sqlite3.connect(":memory:")) as conn: - conn.execute("create table searchable(pk integer)") - conn.executemany("insert into searchable(pk) values (?)", [(0,), (1,), (2,)]) - conn.execute( - "create view calculated as " - "select pk + 1 as pk_plus_one, pk / 2.0 as score from searchable" - ) - - sql_bits, params = Filters((("score__gt", "0.1"),)).build_where_clauses( - "calculated" - ) - rows = conn.execute( - "select score from calculated where {}".format(" and ".join(sql_bits)), - params, - ).fetchall() - - assert rows == [(0.5,), (1.0,)] diff --git a/tests/test_package.py b/tests/test_package.py index 43b20589..f05f3ece 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -1,11 +1,9 @@ +from click.testing import CliRunner +from datasette import cli +from unittest import mock import os import pathlib -from unittest import mock - import pytest -from click.testing import CliRunner - -from datasette import cli class CaptureDockerfile: diff --git a/tests/test_permission_endpoints.py b/tests/test_permission_endpoints.py index 774064fd..8726ab62 100644 --- a/tests/test_permission_endpoints.py +++ b/tests/test_permission_endpoints.py @@ -6,7 +6,6 @@ Tests for permission endpoints: import pytest import pytest_asyncio - from datasette.app import Datasette @@ -433,8 +432,8 @@ async def test_execute_sql_requires_view_database(): A user who has execute-sql permission but not view-database permission should not be able to execute SQL on that database. """ - from datasette import hookimpl from datasette.permissions import PermissionSQL + from datasette import hookimpl class TestPermissionPlugin: __name__ = "TestPermissionPlugin" @@ -494,31 +493,3 @@ async def test_execute_sql_requires_view_database(): ) finally: ds.pm.unregister(plugin) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("path", ["/-/allowed", "/-/allowed.json?action=view-table"]) -async def test_allowed_requires_view_instance(path): - """ - GHSA-hp2x-vx2r-6vxg: /-/allowed should be gated like its /-/rules sibling. - - An actor who is denied view-instance gets 403 from / and /-/rules, but - /-/allowed (HTML and JSON) currently returns 200 to the same actor. - """ - ds = Datasette(config={"allow": {"id": "alice"}}) - await ds.invoke_startup() - db = ds.add_memory_database("live") - await db.execute_write("CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY)") - await ds.refresh_schemas() - - assert (await ds.client.get("/")).status_code == 403 - assert (await ds.client.get("/-/rules.json?action=view-table")).status_code == 403 - - response = await ds.client.get(path) - assert response.status_code == 403 - - # Alice is still allowed - response = await ds.client.get( - path, cookies={"ds_actor": ds.client.actor_cookie({"id": "alice"})} - ) - assert response.status_code == 200 diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 0a77bd37..892777e1 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -1,23 +1,20 @@ import collections -import copy -import json -import re -import time -import urllib -from pprint import pprint - -import pytest -import pytest_asyncio from asgiref.sync import async_to_sync -from bs4 import BeautifulSoup as Soup -from click.testing import CliRunner - from datasette.app import Datasette from datasette.cli import cli from datasette.default_permissions import restrictions_allow_action from datasette.utils import UNSTABLE_API_MESSAGE - from .fixtures import assert_permissions_checked, make_app_client +from click.testing import CliRunner +from bs4 import BeautifulSoup as Soup +import copy +import json +from pprint import pprint +import pytest_asyncio +import pytest +import re +import time +import urllib @pytest.fixture(scope="module") @@ -298,13 +295,24 @@ def test_execute_sql(config): # Extract the schema= portion of the JavaScript schema_json = schema_re.search(response_text).group(1) schema = json.loads(schema_json) - assert set(schema["attraction_characteristic"]) == {"name", "pk"} - assert schema["paginated_view"] == [] + assert {c["label"] for c in schema["attraction_characteristic"]} == { + "name", + "pk", + } + # Views are self/children containers carrying their real columns + assert schema["paginated_view"]["self"]["detail"] == "view" + assert {c["label"] for c in schema["paginated_view"]["children"]} == { + "content", + "content_extra", + } assert form_fragment in response_text query_response = client.get("/fixtures/-/query?sql=select+1", cookies=cookies) assert query_response.status == 200 schema2 = json.loads(schema_re.search(query_response.text).group(1)) - assert set(schema2["attraction_characteristic"]) == {"name", "pk"} + assert {c["label"] for c in schema2["attraction_characteristic"]} == { + "name", + "pk", + } assert ( client.get("/fixtures/facet_cities?_where=id=3", cookies=cookies).status == 200 @@ -460,20 +468,6 @@ async def test_permissions_debug(ds_client, filter_): assert checks == expected_checks -@pytest.mark.asyncio -@pytest.mark.parametrize( - "permissions_debug,expected_status", - ( - (1, 200), - (0, 403), - ), -) -async def test_permissions_debug_numeric_boolean(permissions_debug, expected_status): - ds = Datasette(config={"permissions": {"permissions-debug": permissions_debug}}) - response = await ds.client.get("/-/permissions") - assert response.status_code == expected_status - - @pytest.mark.asyncio @pytest.mark.parametrize( "actor,allow,expected_fragment", @@ -520,7 +514,6 @@ def view_instance_client(): "/-/plugins", "/-/settings", "/-/threads", - "/-/tasks", "/-/databases", "/-/permissions", "/-/messages", @@ -606,7 +599,9 @@ def test_permissions_cascade(cascade_app_client, path, permissions, expected_sta ) assert ( response.status == expected_status - ), f"path: {path}, permissions: {permissions}, expected_status: {expected_status}, status: {response.status}" + ), "path: {}, permissions: {}, expected_status: {}, status: {}".format( + path, permissions, expected_status, response.status + ) finally: cascade_app_client.ds.config = previous_config @@ -764,12 +759,7 @@ async def test_actor_restricted_permissions( } if actor.get("id"): expected["actor_id"] = actor["id"] - data = response.json() - for key, value in expected.items(): - assert data[key] == value - assert data["actor"] == actor - assert data["explanation"]["allowed"] is expected_result - assert data["explanation"]["summary"] + assert response.json() == expected PermConfigTestCase = collections.namedtuple( @@ -1755,8 +1745,6 @@ async def test_permission_check_view_requires_debug_permission(): data = response.json() assert data["action"] == "view-instance" assert data["allowed"] is True - assert data["explanation"]["allowed"] is True - assert data["explanation"]["summary"] @pytest.mark.asyncio @@ -1782,211 +1770,6 @@ async def test_permission_check_view_query_actions(action): } -@pytest.mark.asyncio -async def test_permission_check_explains_specificity_for_hypothetical_actor(): - ds = Datasette( - config={ - "permissions": {"view-table": {"id": "alice"}}, - "databases": { - "analytics": { - "permissions": {"view-table": False}, - "tables": { - "public": {"permissions": {"view-table": {"id": "alice"}}} - }, - } - }, - } - ) - ds.root_enabled = True - await ds.invoke_startup() - - def path_for(child): - return "/-/check.json?" + urllib.parse.urlencode( - { - "action": "view-table", - "parent": "analytics", - "child": child, - "actor": json.dumps({"id": "alice"}), - } - ) - - public_response = await ds.client.get(path_for("public"), actor={"id": "root"}) - assert public_response.status_code == 200 - public = public_response.json() - assert public["actor"] == {"id": "alice"} - assert public["allowed"] is True - assert public["explanation"]["allowed"] is True - assert public["explanation"]["winning_scope"] == "resource" - public_rules = public["explanation"]["matched_rules"] - assert any( - rule["scope"] == "resource" and rule["effect"] == "allow" and rule["decisive"] - for rule in public_rules - ) - assert any( - rule["scope"] == "parent" - and rule["effect"] == "deny" - and rule["ignored_because"] == "A more specific rule matched" - for rule in public_rules - ) - - private_response = await ds.client.get(path_for("private"), actor={"id": "root"}) - assert private_response.status_code == 200 - private = private_response.json() - assert private["allowed"] is False - assert private["explanation"]["allowed"] is False - assert private["explanation"]["winning_scope"] == "parent" - assert private["explanation"]["summary"].startswith("Denied by a parent-level rule") - - -@pytest.mark.asyncio -async def test_permission_check_explains_deny_wins_at_same_scope(): - ds = Datasette(config={"permissions": {"view-table": {"id": "someone-else"}}}) - ds.root_enabled = True - await ds.invoke_startup() - path = "/-/check.json?" + urllib.parse.urlencode( - { - "action": "view-table", - "parent": "analytics", - "child": "users", - "actor": json.dumps({"id": "alice"}), - } - ) - response = await ds.client.get(path, actor={"id": "root"}) - assert response.status_code == 200 - data = response.json() - assert data["allowed"] is False - assert data["explanation"]["winning_scope"] == "global" - rules = data["explanation"]["matched_rules"] - assert any(rule["effect"] == "deny" and rule["decisive"] for rule in rules) - assert any( - rule["effect"] == "allow" - and rule["ignored_because"] == "A deny rule matched at the same scope" - for rule in rules - ) - - -@pytest.mark.asyncio -async def test_permission_check_explains_default_deny(): - ds = Datasette() - ds.root_enabled = True - await ds.invoke_startup() - path = "/-/check.json?" + urllib.parse.urlencode( - { - "action": "insert-row", - "parent": "analytics", - "child": "users", - "actor": json.dumps({"id": "alice"}), - } - ) - response = await ds.client.get(path, actor={"id": "root"}) - assert response.status_code == 200 - data = response.json() - assert data["allowed"] is False - explanation = data["explanation"] - assert explanation["allowed"] is False - assert explanation["matched_rules"] == [] - assert explanation["winning_scope"] is None - assert explanation["summary"] == ( - "Denied because no permission rule matched this actor and resource." - ) - - -@pytest.mark.asyncio -async def test_permission_check_explains_actor_restrictions(): - ds = Datasette() - ds.root_enabled = True - await ds.invoke_startup() - restricted_actor = { - "id": "alice", - "_r": {"r": {"analytics": {"public": ["vt"]}}}, - } - path = "/-/check.json?" + urllib.parse.urlencode( - { - "action": "view-table", - "parent": "analytics", - "child": "private", - "actor": json.dumps(restricted_actor), - } - ) - response = await ds.client.get(path, actor={"id": "root"}) - assert response.status_code == 200 - data = response.json() - assert data["allowed"] is False - explanation = data["explanation"] - assert explanation["rule_allowed"] is True - assert explanation["restriction_allowed"] is False - assert explanation["allowed"] is False - assert explanation["restrictions"] - assert any( - restriction["allowed"] is False for restriction in explanation["restrictions"] - ) - assert "actor's restrictions" in explanation["summary"] - - -@pytest.mark.asyncio -async def test_permission_check_explains_required_actions(): - from datasette import hookimpl - from datasette.permissions import PermissionSQL - - class StoreQueryPermissions: - @hookimpl - def permission_resources_sql(self, actor, action): - if not actor or actor.get("id") != "alice": - return None - if action == "store-query": - return PermissionSQL( - sql="SELECT 'analytics' AS parent, NULL AS child, 1 AS allow, 'alice can store queries' AS reason" - ) - if action == "execute-sql": - return PermissionSQL( - sql="SELECT 'analytics' AS parent, NULL AS child, 0 AS allow, 'alice cannot execute SQL' AS reason" - ) - - ds = Datasette() - ds.root_enabled = True - await ds.invoke_startup() - ds.pm.register(StoreQueryPermissions(), name="store-query-test") - path = "/-/check.json?" + urllib.parse.urlencode( - { - "action": "store-query", - "parent": "analytics", - "actor": json.dumps({"id": "alice"}), - } - ) - response = await ds.client.get(path, actor={"id": "root"}) - assert response.status_code == 200 - data = response.json() - assert data["allowed"] is False - explanation = data["explanation"] - assert explanation["rule_allowed"] is True - assert explanation["required_actions"][0]["action"] == "execute-sql" - assert explanation["required_actions"][0]["allowed"] is False - assert explanation["summary"] == ( - "Denied because store-query also requires execute-sql, which was denied." - ) - - -@pytest.mark.asyncio -async def test_permission_check_hypothetical_actor_validation(): - ds = Datasette() - ds.root_enabled = True - await ds.invoke_startup() - - response = await ds.client.get( - "/-/check.json?action=view-instance&actor=not-json", - actor={"id": "root"}, - ) - assert response.status_code == 400 - assert response.json()["error"].startswith("Invalid actor JSON:") - - response = await ds.client.get( - "/-/check.json?action=view-instance&actor=%5B%5D", - actor={"id": "root"}, - ) - assert response.status_code == 400 - assert response.json()["error"] == "actor must be a JSON object or null" - - @pytest.mark.asyncio async def test_root_allow_block_with_table_restricted_actor(): """ @@ -2041,7 +1824,7 @@ async def test_databases_json_respects_view_database(tmp_path_factory): paths = [] for name in ("public", "private"): - path = str(db_directory / f"{name}.db") + path = str(db_directory / "{}.db".format(name)) conn = _sqlite3.connect(path) conn.execute("vacuum") conn.close() diff --git a/tests/test_playwright.py b/tests/test_playwright.py index 07a11947..eb1edb57 100644 --- a/tests/test_playwright.py +++ b/tests/test_playwright.py @@ -5,7 +5,7 @@ import subprocess import sys import time -import httpx2 +import httpx import pytest from datasette.fixtures import write_fixture_database @@ -34,11 +34,11 @@ def wait_for_server(process, url, timeout=30): f"stderr:\n{stderr}" ) try: - response = httpx2.get(url, timeout=1.0) + response = httpx.get(url, timeout=1.0) if response.status_code < 500: return last_error = f"HTTP {response.status_code}: {response.text[:200]}" - except httpx2.HTTPError as ex: + except httpx.HTTPError as ex: last_error = repr(ex) time.sleep(0.1) if process.poll() is None: @@ -108,11 +108,6 @@ def write_playwright_database(db_path): conn = sqlite3.connect(db_path) try: conn.executescript(""" - create table count_numbers (id integer primary key); - with recursive sequence(id) as ( - select 1 union all select id + 1 from sequence where id < 10002 - ) - insert into count_numbers select id from sequence; create table projects ( id integer primary key, title text not null, @@ -341,7 +336,7 @@ def project_rows(datasette_server, **filters): "_shape": "objects", **{key: str(value) for key, value in filters.items()}, } - response = httpx2.get(f"{datasette_server}data/projects.json", params=params) + response = httpx.get(f"{datasette_server}data/projects.json", params=params) response.raise_for_status() return response.json()["rows"] @@ -353,7 +348,7 @@ def project_row(datasette_server, pk): def binary_file_blob(datasette_server, pk): - response = httpx2.get( + response = httpx.get( f"{datasette_server}data/binary_files/{pk}.blob", params={"_blob_column": "data"}, ) @@ -374,7 +369,7 @@ def bulk_default_rows(datasette_server, **filters): "_shape": "objects", **{key: str(value) for key, value in filters.items()}, } - response = httpx2.get(f"{datasette_server}data/bulk_defaults.json", params=params) + response = httpx.get(f"{datasette_server}data/bulk_defaults.json", params=params) response.raise_for_status() return response.json()["rows"] @@ -384,7 +379,7 @@ def upsert_item_rows(datasette_server, **filters): "_shape": "objects", **{key: str(value) for key, value in filters.items()}, } - response = httpx2.get(f"{datasette_server}data/upsert_items.json", params=params) + response = httpx.get(f"{datasette_server}data/upsert_items.json", params=params) response.raise_for_status() return response.json()["rows"] @@ -478,7 +473,7 @@ def test_create_table_flow(page, datasette_server): page.wait_for_url("**/data/playwright_created") assert "playwright_created" in page.locator("h1").inner_text() - response = httpx2.get( + response = httpx.get( f"{datasette_server}data/playwright_created.json?_extra=columns,column_types" ) response.raise_for_status() @@ -492,7 +487,7 @@ def test_create_table_flow(page, datasette_server): assert data["column_types"] == { "metadata": {"type": "json", "config": None}, } - schema_response = httpx2.get( + schema_response = httpx.get( f"{datasette_server}data/-/query.json", params={ "sql": ( @@ -608,7 +603,7 @@ def test_create_table_from_data_flow(page, datasette_server): dialog.locator(".table-create-save").click() page.wait_for_url("**/data/playwright_from_data") - response = httpx2.get( + response = httpx.get( f"{datasette_server}data/playwright_from_data.json?_shape=objects" ) response.raise_for_status() @@ -644,7 +639,7 @@ def test_create_table_from_csv_keeps_numeric_type_when_values_are_blank( dialog.locator(".table-create-save").click() page.wait_for_url("**/data/playwright_numeric_blanks") - response = httpx2.get( + response = httpx.get( f"{datasette_server}data/playwright_numeric_blanks.json?_shape=objects" ) response.raise_for_status() @@ -653,7 +648,7 @@ def test_create_table_from_csv_keeps_numeric_type_when_values_are_blank( {"name": "B", "score": None}, ] - schema_response = httpx2.get( + schema_response = httpx.get( f"{datasette_server}data/-/query.json", params={ "sql": ( @@ -861,7 +856,7 @@ def test_alter_table_flow(page, datasette_server): columns = [] for _ in range(20): - response = httpx2.get(f"{datasette_server}data/projects.json?_extra=columns") + response = httpx.get(f"{datasette_server}data/projects.json?_extra=columns") response.raise_for_status() columns = response.json()["columns"] if "status" in columns: @@ -1033,14 +1028,15 @@ def test_alter_table_cancel_skips_discard_prompt(page, datasette_server): dialog.locator(".table-alter-add-column").click() dialog.locator(".table-alter-column-name").last.fill("escape_me") page.keyboard.press("Escape") - page.wait_for_function("window.__discardConfirmMessages.length === 1") assert page.evaluate("() => window.__discardConfirmMessages") == [ "Discard table changes?" ] assert dialog.evaluate("node => node.open") is True page.evaluate("() => window.__discardConfirmMessages = []") - page.mouse.click(2, 2) + dialog.evaluate( + """node => node.dispatchEvent(new MouseEvent("click", {bubbles: true}))""" + ) assert page.evaluate("() => window.__discardConfirmMessages") == [ "Discard table changes?" ] @@ -1083,92 +1079,6 @@ def test_navigation_search_renders_jump_sections_from_javascript_plugins( page.wait_for_url("**/-/playwright-agent") -@pytest.mark.playwright -def test_navigation_search_created_from_javascript(page, datasette_server): - from playwright.sync_api import expect - - page.goto(datasette_server) - page.evaluate("""() => { - const search = document.createElement('navigation-search'); - search.id = 'additional-search'; - search.setAttribute('items', JSON.stringify([ - {name: 'Projects', url: '/data/projects'} - ])); - document.body.append(search); - const unrelated = document.createElement('div'); - unrelated.className = 'search-container'; - unrelated.id = 'outside-search'; - document.body.append(unrelated); - search.openMenu(); - }""") - search = page.locator("#additional-search") - dialog = search.get_by_role("dialog", name="Jump to", exact=True) - expect(dialog).to_be_visible() - # Page styles and ordinary DOM queries can reach the component's controls. - page.add_style_tag( - content="#additional-search .search-input { border-top-color: rgb(1, 2, 3); }" - ) - field = dialog.get_by_role("combobox", name="Jump to", exact=True) - expect(field).to_have_css("border-top-color", "rgb(1, 2, 3)") - assert field.evaluate("node => document.getElementById(node.id) === node") - expect(page.locator("#outside-search")).to_have_css("display", "block") - field.fill("projects") - expect(dialog.get_by_role("option")).to_contain_text("Projects") - field.press("Enter") - page.wait_for_url("**/data/projects") - - -@pytest.mark.playwright -def test_column_chooser_selection_and_drag_in_document(page, datasette_server): - from playwright.sync_api import expect - - page.goto(datasette_server + "data/projects") - page.emulate_media(reduced_motion="reduce") - page.evaluate("""() => { - const chooser = document.createElement('column-chooser'); - chooser.id = 'additional-chooser'; - document.body.append(chooser); - window.appliedColumns = null; - chooser.open({ - columns: ['title', 'notes', 'score'], - selected: ['title', 'notes'], - onApply: columns => { window.appliedColumns = columns; } - }); - }""") - chooser = page.locator("#additional-chooser") - dialog = chooser.get_by_role("dialog", name="Choose columns") - expect(dialog).to_be_visible() - assert dialog.evaluate("""node => { - const id = node.getAttribute('aria-labelledby'); - return document.querySelectorAll(`#${id}`).length === 1 && - node.contains(document.getElementById(id)); - }""") - expect(dialog.locator(".modal-meta")).to_have_text("2 of 3 selected") - dialog.get_by_role("button", name="Deselect all", exact=True).click() - expect(dialog.locator(".modal-meta")).to_have_text("0 of 3 selected") - dialog.get_by_role("button", name="Select all", exact=True).click() - expect(dialog.locator(".modal-meta")).to_have_text("3 of 3 selected") - # Move title after score using the same pointer events as mouse/touch dragging. - handle = dialog.locator(".drag-handle").first.bounding_box() - target = dialog.locator(".drag-item").last.bounding_box() - page.mouse.move(handle["x"] + handle["width"] / 2, handle["y"] + 24) - page.mouse.down() - page.mouse.move(target["x"] + 24, target["y"] + target["height"] - 4, steps=5) - expect(dialog.locator(".drag-ghost")).to_be_visible() - page.mouse.up() - expect(dialog.locator(".drag-item-label")).to_have_text(["notes", "score", "title"]) - dialog.get_by_role("button", name="Apply", exact=True).click() - expect(dialog).not_to_be_visible() - assert page.evaluate("appliedColumns") == ["notes", "score", "title"] - chooser.evaluate( - "node => node.open({columns: ['title', 'notes'], selected: ['title']})" - ) - dialog.get_by_role("button", name="Deselect all", exact=True).click() - dialog.get_by_role("button", name="Cancel", exact=True).click() - expect(dialog).not_to_be_visible() - assert page.evaluate("appliedColumns") == ["notes", "score", "title"] - - @pytest.mark.playwright def test_insert_row_flow_uses_custom_column_field(page, datasette_server): page.add_init_script(""" @@ -1689,323 +1599,8 @@ def test_delete_row_flow_removes_row(page, datasette_server): dialog = page.locator("#row-delete-dialog") dialog.wait_for() assert "Delete row 1" in dialog.inner_text() - dialog.locator(".row-delete-confirm").press("Enter") + dialog.locator(".row-delete-confirm").click() page.locator(".row-mutation-status", has_text="Deleted row 1").wait_for() page.locator('tr[data-row="1"]').wait_for(state="detached") assert project_rows(datasette_server, id=1) == [] - - -@pytest.mark.playwright -def test_count_all(page, datasette_server): - page.goto(datasette_server + "data/count_numbers?id__gt=1&_sort=id") - assert page.locator(".table-count").inner_text() == "10,000+ rows" - with page.expect_response("**/count_numbers/-/count?*") as response: - page.get_by_role("button", name="count all", exact=True).click() - assert response.value.request.method == "POST" - assert response.value.request.post_data is None - assert "content-type" not in response.value.request.headers - assert response.value.json() == {"ok": True, "count": 10001} - page.wait_for_function( - 'document.querySelector(".table-count").textContent === "10,001 rows"' - ) - assert page.locator(".count-all").count() == 0 - assert "id" in page.locator("h3").first.inner_text() - - -@pytest.mark.playwright -def test_count_all_error_retry(page, datasette_server): - page.goto(datasette_server + "data/count_numbers?id__gt=1") - page.route( - "**/count_numbers/-/count?*", - lambda route: route.fulfill( - status=400, - content_type="application/json", - body=json.dumps({"ok": False, "errors": ["Count query timed out"]}), - ), - ) - button = page.get_by_role("button", name="count all", exact=True) - button.click() - page.wait_for_function( - 'document.querySelector(".count-error").textContent === "Count query timed out"' - ) - assert button.is_enabled() - page.unroute("**/count_numbers/-/count?*") - button.click() - page.wait_for_function( - 'document.querySelector(".table-count").textContent === "10,001 rows"' - ) - assert page.locator(".count-error").inner_text() == "" - - -@pytest.mark.playwright -def test_modal_lifecycle(page, datasette_server): - from playwright.sync_api import expect - - page.goto(datasette_server) - page.evaluate( - """() => { - const trigger = document.createElement('button'); - trigger.id = 'modal-trigger'; - trigger.textContent = 'Open test modal'; - document.body.append(trigger); - window.testModal = DatasetteModal.create(); - const dialog = testModal.dialog; - dialog.id = 'test-modal'; - dialog.setAttribute('aria-labelledby', 'test-modal-title'); - dialog.innerHTML = ` -

Test modal

- - - `; - // Padding is part of the dialog, never a backdrop dismissal. - dialog.style.padding = '30px'; - document.body.append(testModal); - window.closeSources = []; - testModal.beforeClose = source => { - closeSources.push(source); - return window.allowClose; - }; - window.allowClose = false; - trigger.onclick = () => testModal.show({ - returnFocusTo: trigger, initialFocus: dialog.querySelector('input') - }); - dialog.querySelector('button').onclick = () => testModal.requestClose('cancel'); - }""", - ) - trigger = page.locator("#modal-trigger") - trigger.click() - dialog = page.get_by_role("dialog", name="Test modal", exact=True) - expect(dialog.get_by_role("textbox", name="First field")).to_be_focused() - assert dialog.evaluate("node => node instanceof HTMLDialogElement") - expect(dialog).to_have_css("display", "flex") - # Native modality keeps background content inert and keyboard focus inside. - page.keyboard.press("Tab") - expect(dialog.get_by_role("textbox", name="Second field")).to_be_focused() - page.keyboard.press("Shift+Tab") - expect(dialog.get_by_role("textbox", name="First field")).to_be_focused() - trigger.evaluate("node => node.focus()") - expect(dialog.get_by_role("textbox", name="First field")).to_be_focused() - - page.keyboard.down("Escape") - assert page.evaluate("closeSources") == [] - page.keyboard.up("Escape") - page.wait_for_function("closeSources.length === 1") - assert page.evaluate("closeSources") == ["escape"] - expect(dialog).to_be_visible() - - dialog.click(position={"x": 3, "y": 3}) - assert page.evaluate("closeSources") == ["escape"] - # A drag which starts inside and ends on the backdrop must not dismiss. - box = dialog.bounding_box() - page.mouse.move(box["x"] + 3, box["y"] + 3) - page.mouse.down() - page.mouse.move(2, 2) - page.mouse.up() - assert page.evaluate("closeSources") == ["escape"] - page.mouse.click(2, 2) - assert page.evaluate("closeSources") == ["escape", "backdrop"] - - page.evaluate("testModal.busy = true; allowClose = true") - expect(dialog).to_have_attribute("aria-busy", "true") - page.keyboard.press("Escape") - page.mouse.click(2, 2) - dialog.get_by_role("button", name="Cancel").click() - expect(dialog).to_be_visible() - assert page.evaluate("closeSources") == ["escape", "backdrop"] - page.evaluate("testModal.busy = false") - dialog.get_by_role("button", name="Cancel").click() - expect(dialog).not_to_be_visible() - expect(trigger).to_be_focused() - assert page.evaluate("closeSources") == ["escape", "backdrop", "cancel"] - - # Reopening, including an extra show() call, preserves the original return-focus target. - trigger.click() - page.evaluate("testModal.show()") - page.keyboard.press("Escape") - expect(dialog).not_to_be_visible() - expect(trigger).to_be_focused() - - # Completion bypasses busy/confirmation and must not steal a caller's focus. - trigger.click() - page.evaluate("""() => new Promise(resolve => { - const next = document.createElement('button'); - next.id = 'after-save'; - next.textContent = 'Next action'; - document.body.append(next); - testModal.dialog.addEventListener('close', resolve, {once: true}); - testModal.busy = true; - testModal.close({restoreFocus: false}); - next.focus(); - })""") - expect(dialog).not_to_be_visible() - expect(page.locator("#after-save")).to_be_focused() - - -@pytest.mark.playwright -def test_modal_nested_escape_and_cleanup(page, datasette_server): - from playwright.sync_api import expect - - page.goto(datasette_server + "data/projects") - trigger = page.locator('tr[data-row="1"] button[data-row-action="edit"]') - trigger.click() - dialog = page.locator("#row-edit-dialog") - field = dialog.locator('input[name="title"]') - expect(field).to_be_visible() - field.fill("Unsaved title") - page.evaluate("""() => { - window.confirmations = []; - window.confirm = message => { confirmations.push(message); return false; }; - }""") - # Plugin controls can consume Escape without closing their containing form. - field.evaluate("""node => node.addEventListener('keydown', event => { - if (event.key === 'Escape') event.preventDefault(); - }, {once: true})""") - field.press("Escape") - assert page.evaluate("confirmations") == [] - expect(dialog).to_be_visible() - field.press("Escape") - page.wait_for_function("confirmations.length === 1") - assert page.evaluate("confirmations") == ["Discard unsaved changes to this row?"] - - # A nested native modal closes independently, then returns focus to its field. - field.evaluate("""node => { - node.focus(); - window.nestedModal = DatasetteModal.create(); - nestedModal.dialog.setAttribute('aria-label', 'Nested picker'); - nestedModal.dialog.innerHTML = ''; - node.closest('dialog').append(nestedModal); - nestedModal.show(); - }""") - nested = page.get_by_role("dialog", name="Nested picker") - page.keyboard.press("Escape") - expect(nested).not_to_be_visible() - expect(dialog).to_be_visible() - expect(field).to_be_focused() - assert page.evaluate("confirmations.length") == 1 - - # Closing before keyup cancels the pending confirmation, including on reopen. - page.keyboard.down("Escape") - dialog.locator(".row-edit-cancel").click() - expect(dialog).not_to_be_visible() - trigger.click() - page.keyboard.up("Escape") - expect(field).to_be_visible() - assert page.evaluate("confirmations.length") == 1 - expect(dialog).to_be_visible() - # Native cancel (e.g. an accessibility action) does not wait for keyboard input. - field.fill("Another edit") - dialog.evaluate( - "node => node.dispatchEvent(new Event('cancel', {cancelable: true}))" - ) - assert page.evaluate("confirmations.length") == 2 - dialog.locator(".row-edit-cancel").click() - expect(trigger).to_be_focused() - - -@pytest.mark.playwright -@pytest.mark.parametrize("name", ["jump", "columns", "type", "mobile"]) -def test_modal_consumers_dismiss_and_restore_focus(page, datasette_server, name): - from playwright.sync_api import expect - - page_errors = [] - page.on("pageerror", lambda error: page_errors.append(str(error))) - if name == "mobile": - page.set_viewport_size({"width": 390, "height": 844}) - page.emulate_media(reduced_motion="reduce") - page.goto(datasette_server + "data/projects") - if name == "jump": - trigger = page.locator("details.nav-menu summary") - trigger.click() - page.locator("[data-navigation-search-open]").click() - dialog = page.locator("navigation-search dialog") - elif name == "columns": - # Open through its public API with a real, focused page control. - trigger = page.locator("details.actions-menu-links summary") - trigger.focus() - page.evaluate( - "document.querySelector('column-chooser').open({columns: ['id', 'title'], selected: ['id']})" - ) - dialog = page.locator("column-chooser dialog") - elif name == "type": - trigger = page.locator("details.actions-menu-links summary") - trigger.focus() - page.evaluate( - "openSetColumnTypeDialog(document.querySelector('th[data-column=title]'))" - ) - dialog = page.locator("#set-column-type-dialog") - else: - trigger = page.locator(".column-actions-mobile") - trigger.click() - dialog = page.locator("#mobile-column-actions-dialog") - expect(dialog).to_be_visible() - expect(dialog).to_have_css("border-radius", "8px" if name == "mobile" else "12px") - expect(dialog).to_have_css("animation-name", "none") - assert dialog.evaluate("node => node.parentElement.localName") == "datasette-modal" - assert dialog.evaluate("node => node.getRootNode() === document") - page.keyboard.press("Escape") - expect(dialog).not_to_be_visible() - expect(trigger).to_be_focused() - assert page_errors == [] - - -@pytest.mark.playwright -def test_modal_disconnect_cleans_up_pending_escape(page, datasette_server): - from playwright.sync_api import expect - - page.goto(datasette_server) - page.evaluate("""() => { - window.detachable = DatasetteModal.create(); - detachable.dialog.setAttribute('aria-label', 'Detachable'); - detachable.dialog.innerHTML = ''; - window.closeAttempts = 0; - detachable.beforeClose = () => { closeAttempts++; return false; }; - document.body.append(detachable); - detachable.show(); - }""") - dialog = page.get_by_role("dialog", name="Detachable") - page.keyboard.down("Escape") - page.evaluate("detachable.remove()") - page.keyboard.up("Escape") - assert page.evaluate("closeAttempts") == 0 - assert page.evaluate("detachable.dialog.open") is False - page.evaluate("document.body.append(detachable); detachable.show()") - expect(dialog).to_be_visible() - page.keyboard.press("Escape") - page.wait_for_function("closeAttempts === 1") - expect(dialog).to_be_visible() - - -@pytest.mark.playwright -@pytest.mark.parametrize("kind", ["create", "alter"]) -def test_schema_modal_escape_confirmation_and_focus(page, datasette_server, kind): - from playwright.sync_api import expect - - path = "data" if kind == "create" else "data/projects" - page.goto(datasette_server + path) - menu = page.locator("details.actions-menu-links") - menu.locator("summary").click() - selector = "data-database-action" if kind == "create" else "data-table-action" - menu.locator(f'button[{selector}="{kind}-table"]').click() - dialog = page.locator(f"#table-{kind}-dialog") - if kind == "create": - dialog.locator('input[name="table"]').fill("unsaved_table") - else: - dialog.locator(".table-alter-add-column").click() - # Real browser confirms, including WebKit, should appear once and stay usable. - confirmations = [] - - def reject(prompt): - confirmations.append(prompt.message) - prompt.dismiss() - - page.on("dialog", reject) - with page.expect_event("dialog"): - page.keyboard.press("Escape") - expect(dialog).to_be_visible() - assert len(confirmations) == 1 - page.remove_listener("dialog", reject) - page.on("dialog", lambda prompt: prompt.accept()) - page.keyboard.press("Escape") - expect(dialog).not_to_be_visible() - expect(menu.locator("summary")).to_be_focused() diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 1084d270..5c4034db 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -1,3 +1,21 @@ +from bs4 import BeautifulSoup as Soup +from .fixtures import ( + make_app_client, + TEMP_PLUGIN_SECRET_FILE, + PLUGINS_DIR, + TestClient as _TestClient, +) # noqa +from click.testing import CliRunner +from datasette.app import Datasette +from datasette import cli, hookimpl +from datasette.fixtures import TABLES +from datasette.filters import FilterArguments +from datasette.plugins import get_plugins, DEFAULT_PLUGINS, pm +from datasette.permissions import PermissionSQL, Action +from datasette.resources import DatabaseResource +from datasette.utils.sqlite import sqlite3 +from datasette.utils import StartupError, await_me_maybe +from jinja2 import ChoiceLoader, FileSystemLoader import base64 import datetime import importlib @@ -6,31 +24,8 @@ import os import pathlib import re import textwrap -import urllib - import pytest -from bs4 import BeautifulSoup as Soup -from click.testing import CliRunner -from jinja2 import ChoiceLoader, FileSystemLoader - -from datasette import cli, hookimpl -from datasette.app import Datasette -from datasette.filters import FilterArguments -from datasette.fixtures import TABLES -from datasette.permissions import Action, PermissionSQL -from datasette.plugins import DEFAULT_PLUGINS, get_plugins, pm -from datasette.resources import DatabaseResource -from datasette.utils import StartupError, await_me_maybe -from datasette.utils.sqlite import sqlite3 - -from .fixtures import ( - PLUGINS_DIR, - TEMP_PLUGIN_SECRET_FILE, - make_app_client, -) -from .fixtures import ( - TestClient as _TestClient, -) +import urllib at_memory_re = re.compile(r" at 0x\w+") @@ -40,7 +35,7 @@ at_memory_re = re.compile(r" at 0x\w+") ) def test_plugin_hooks_have_tests(plugin_hook): """Every plugin hook should be referenced in this test module""" - tests_in_this_module = [t for t in globals() if t.startswith("test_hook_")] + tests_in_this_module = [t for t in globals().keys() if t.startswith("test_hook_")] ok = False for test in tests_in_this_module: if plugin_hook in test: @@ -53,13 +48,6 @@ def test_hook_jump_items_sql(): assert "jump_items_sql" in dir(pm.hook) -def test_hook_shutdown(): - # Detailed behavior (ordering against background-task cancellation and - # close(), idempotency, exception handling, sync vs async support) is - # covered in tests/test_shutdown.py. - assert "shutdown" in dir(pm.hook) - - @pytest.mark.asyncio async def test_hook_plugins_dir_plugin_prepare_connection(ds_client): response = await ds_client.get( @@ -137,11 +125,11 @@ async def test_hook_extra_css_urls(ds_client, path, expected_decoded_object): response = await ds_client.get(path) assert response.status_code == 200 links = Soup(response.text, "html.parser").find_all("link") - special_href = next( + special_href = [ link for link in links if link.attrs["href"].endswith("/extra-css-urls-demo.css") - )["href"] + ][0]["href"] # This link has a base64-encoded JSON blob in it encoded = special_href.split("/")[3] actual_decoded_object = json.loads(base64.b64decode(encoded).decode("utf8")) @@ -164,7 +152,7 @@ async def test_hook_extra_js_urls(ds_client): "type": "module", }, ]: - assert any(s == attrs for s in script_attrs), f"Expected: {attrs}" + assert any(s == attrs for s in script_attrs), "Expected: {}".format(attrs) @pytest.mark.asyncio @@ -327,8 +315,7 @@ async def test_plugin_config_env_from_list(ds_client): @pytest.mark.asyncio async def test_plugin_config_file(ds_client): - # Blocking write is fine here - it is tiny test setup, not request handling - with open(TEMP_PLUGIN_SECRET_FILE, "w") as fp: # noqa: ASYNC230 + with open(TEMP_PLUGIN_SECRET_FILE, "w") as fp: fp.write("FROM_FILE") assert {"foo": "FROM_FILE"} == ds_client.ds.plugin_config("file-plugin") os.remove(TEMP_PLUGIN_SECRET_FILE) @@ -429,72 +416,6 @@ def test_hook_extra_template_vars(restore_working_directory): } == extra_template_vars_from_awaitable -@pytest.mark.asyncio -@pytest.mark.parametrize( - "return_style", ["direct", "callable", "async_callable", "awaitable"] -) -async def test_hook_extra_template_vars_none(ds_client, return_style): - class OtherPlugin: - @hookimpl - def extra_template_vars(self): - return {"other": "present"} - - class ConditionalPlugin: - @hookimpl - def extra_template_vars(self, view_name): - def inner(): - if view_name == "database": - return {"conditional": "database"} - - async def async_inner(): - return inner() - - if return_style == "direct": - return inner() - elif return_style == "callable": - return inner - elif return_style == "async_callable": - return async_inner - else: - return async_inner() - - other_plugin = OtherPlugin() - conditional_plugin = ConditionalPlugin() - pm.register(other_plugin) - pm.register(conditional_plugin) - try: - template = ds_client.ds.get_jinja_environment().from_string( - "{{ other }}:{{ conditional|default('missing') }}" - ) - for view_name, expected in ( - ("database", "present:database"), - ("index", "present:missing"), - ): - rendered = await ds_client.ds.render_template(template, view_name=view_name) - assert rendered == expected - finally: - pm.unregister(conditional_plugin) - pm.unregister(other_plugin) - - -@pytest.mark.asyncio -@pytest.mark.parametrize("invalid_value", [False, 0, "", [], ()]) -async def test_hook_extra_template_vars_invalid(ds_client, invalid_value): - class InvalidPlugin: - @hookimpl - def extra_template_vars(self): - return lambda: invalid_value - - plugin = InvalidPlugin() - pm.register(plugin) - try: - template = ds_client.ds.get_jinja_environment().from_string("test") - with pytest.raises(AssertionError, match="extra_vars is of type"): - await ds_client.ds.render_template(template) - finally: - pm.unregister(plugin) - - def test_plugins_async_template_function(restore_working_directory): with make_app_client( template_dir=str(pathlib.Path(__file__).parent / "test_templates") @@ -902,7 +823,7 @@ def test_hook_register_routes_with_datasette(configured_path): assert response.status_code == 200 assert configured_path.upper() == response.text # Other one should 404 - other_path = next(p for p in ("path1", "path2") if configured_path != p) + other_path = [p for p in ("path1", "path2") if configured_path != p][0] assert client.get(f"/{other_path}/", follow_redirects=True).status_code == 404 @@ -1007,7 +928,7 @@ async def test_plugin_startup_can_add_queries(): await datasette.add_query( "data", "from_startup", - f"select {result.first()[0]}", + "select {}".format(result.first()[0]), source="plugin", ) @@ -1119,7 +1040,7 @@ async def test_hook_handle_exception(ds_client): @pytest.mark.asyncio @pytest.mark.parametrize("param", ("_custom_error", "_custom_error_async")) async def test_hook_handle_exception_custom_response(ds_client, param): - response = await ds_client.get(f"/trigger-error?{param}=1") + response = await ds_client.get("/trigger-error?{}=1".format(param)) assert response.text == param @@ -1373,8 +1294,7 @@ async def test_hook_filters_from_request(ds_client): ds_client.ds.pm.register(ReturnNothingPlugin(), name="ReturnNothingPlugin") response = await ds_client.get("/fixtures/facetable?_nothing=1") - summary = Soup(response.text, "html.parser").select_one(".table-summary") - assert summary.get_text(" ", strip=True) == "0 rows where NOTHING" + assert "0 rows\n where NOTHING" in response.text json_response = await ds_client.get("/fixtures/facetable.json?_nothing=1") assert json_response.json()["rows"] == [] ds_client.ds.pm.unregister(name="ReturnNothingPlugin") @@ -1454,7 +1374,7 @@ async def test_hook_register_actions_no_duplicates(duplicate): # This should error: with pytest.raises(StartupError) as ex: await ds.invoke_startup() - assert f"Duplicate action {duplicate}" in str(ex.value) + assert "Duplicate action {}".format(duplicate) in str(ex.value) @pytest.mark.asyncio @@ -1562,7 +1482,7 @@ async def test_plugin_is_installed(): datasette.pm.register(DummyPlugin(), name="DummyPlugin") response = await datasette.client.get("/-/plugins.json") assert response.status_code == 200 - installed_plugins = {p["name"] for p in response.json()} + installed_plugins = {p["name"] for p in response.json()["plugins"]} assert "DummyPlugin" in installed_plugins finally: diff --git a/tests/test_pr76_fts_policy.py b/tests/test_pr76_fts_policy.py deleted file mode 100644 index 2d4086b3..00000000 --- a/tests/test_pr76_fts_policy.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Policy and compatibility coverage for PR #76, run against the fixed checkout.""" - -import uuid - -import pytest - -from datasette.app import Datasette -from datasette.resources import TableResource -from datasette.utils.sqlite import sqlite3, sqlite_derived_table_dependencies - - -@pytest.mark.parametrize("vocab_name", ["words", "name USING fts4aux", 'quoted"name']) -@pytest.mark.parametrize( - "module,arguments", - [ - ("fts5vocab", "'Search,Index', 'row'"), - ("fts5vocab", "'SEARCH,INDEX', 'col'"), - ("fts5vocab", "'Search,Index', 'instance'"), - ("fts4aux", "'Search,Index'"), - ], -) -def test_vocabulary_dependency_identity(module, arguments, vocab_name): - conn = sqlite3.connect(":memory:") - try: - fts = "fts5" if module == "fts5vocab" else "fts4" - conn.execute(f'create virtual table "Search,Index" using {fts}(body)') - quoted_name = '"' + vocab_name.replace('"', '""') + '"' - conn.execute( - f"create virtual table {quoted_name} USING /* module */ {module}({arguments})" - ) - assert sqlite_derived_table_dependencies(conn)[vocab_name] == "Search,Index" - finally: - conn.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("module", ["fts5", "fts4"]) -@pytest.mark.parametrize("external_content", [False, True], ids=["one-hop", "two-hop"]) -@pytest.mark.parametrize( - "source_allowed,vocab_allowed", [(False, True), (True, False), (True, True)] -) -async def test_vocabulary_immediate_source_permissions( - module, external_content, source_allowed, vocab_allowed -): - ds = Datasette( - memory=True, - config={ - "databases": { - "data": { - "tables": { - "search": { - "permissions": { - "view-table": ( - {"id": "reader"} if source_allowed else False - ) - } - }, - "words": {"permissions": {"view-table": vocab_allowed}}, - } - } - } - }, - ) - db = ds.add_memory_database(uuid.uuid4().hex, name="data") - await db.execute_write("create table documents(body text)") - options = "body, content='documents'" if external_content else "body" - await db.execute_write(f"create virtual table search using {module}({options})") - definition = ( - "fts5vocab('SEARCH', 'row')" if module == "fts5" else "fts4aux('SEARCH')" - ) - await db.execute_write(f"create virtual table words using {definition}") - await ds.invoke_startup() - try: - actor = {"id": "reader"} - expected = source_allowed and vocab_allowed and not external_content - for name in ("words", "WORDS"): - assert ( - await ds.allowed( - action="view-table", - resource=TableResource("data", name), - actor=actor, - ) - is expected - ) - resources = await ds.allowed_resources( - "view-table", parent="data", actor=actor, include_is_private=True - ) - words = [r for r in resources.resources if r.child == "words"] - assert bool(words) is expected - if expected: - assert words[0].private - assert not await ds.allowed( - action="view-table", resource=TableResource("data", "words") - ) - # Dropping the source invalidates dependency metadata and remains denied. - await db.execute_write("drop table search") - assert not await ds.allowed( - action="view-table", resource=TableResource("data", "words"), actor=actor - ) - finally: - ds.close() - - -@pytest.mark.parametrize( - "module,definition", - [ - ("fts5", "fts5vocab('main', 'search', 'row')"), - ("fts4", "fts4aux('main', 'search')"), - ], -) -def test_cross_schema_vocabulary_is_unresolved(module, definition): - conn = sqlite3.connect(":memory:") - try: - conn.execute(f"create virtual table search using {module}(body)") - conn.execute(f"create virtual table temp.words using {definition}") - # Cross-schema ownership is not representable by the current map. - # The source is itself derived, so the immediate-source policy denies it. - assert ( - sqlite_derived_table_dependencies(conn, schema="temp")["words"] == "words" - ) - finally: - conn.close() - - -@pytest.mark.parametrize( - "definition", - [ - """CREATE VIRTUAL TABLE"words"USING"fts5vocab"('search', 'row')""", - """CREATE VIRTUAL TABLE[words]USING[fts5vocab]('search', 'row')""", - """CREATE VIRTUAL TABLE`words`USING`fts5vocab`('search', 'row')""", - ], -) -def test_vocabulary_quoted_token_boundaries(definition): - conn = sqlite3.connect(":memory:") - try: - conn.execute("create virtual table search using fts5(body)") - conn.execute(definition) - assert sqlite_derived_table_dependencies(conn)["words"] == "search" - finally: - conn.close() diff --git a/tests/test_pr76_statistics_policy.py b/tests/test_pr76_statistics_policy.py deleted file mode 100644 index 6ebf10b9..00000000 --- a/tests/test_pr76_statistics_policy.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Statistics access policy and plugin replacement coverage for PR #76.""" - -import uuid - -import pytest - -from datasette import hookimpl -from datasette.app import Datasette -from datasette.permissions import PermissionSQL -from datasette.resources import TableResource - - -@pytest.mark.asyncio -@pytest.mark.parametrize("scope", [None, "global", "database", "table", "root"]) -async def test_statistics_denied_despite_allow_rules(scope): - config = {"databases": {"data": {"tables": {"sqlite_stat1": {}}}}} - grant = {"view-table": True} - if scope == "global": - config["permissions"] = grant - elif scope == "database": - config["databases"]["data"]["permissions"] = grant - elif scope == "table": - config["databases"]["data"]["tables"]["sqlite_stat1"]["permissions"] = grant - ds = Datasette(memory=True, config=config) - ds.root_enabled = scope == "root" - actor = {"id": "root"} if scope == "root" else {"id": "reader"} - db = ds.add_memory_database(uuid.uuid4().hex, name="data") - await db.execute_write("create table items(value text)") - await db.execute_write("create index items_value on items(value)") - await db.execute_write("insert into items values ('example')") - await db.execute_write("analyze") - await ds.invoke_startup() - try: - assert "view-sqlite-statistics" not in ds.actions - for name in ("sqlite_stat1", "SQLITE_STAT1"): - assert not await ds.allowed( - action="view-table", resource=TableResource("data", name), actor=actor - ) - for suffix in ("", ".json", ".csv"): - assert ( - await ds.client.get(f"/data/sqlite_stat1{suffix}", actor=actor) - ).status_code == 403 - resources = await ds.allowed_resources("view-table", parent="data", actor=actor) - assert "sqlite_stat1" not in {r.child for r in resources.resources} - assert "items" in {r.child for r in resources.resources} - finally: - ds.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "table", ["sqlite_stat1", "sqlite_stat2", "sqlite_stat3", "sqlite_stat4"] -) -@pytest.mark.parametrize("default_deny", [False, True]) -async def test_statistics_names_denied(table, default_deny): - ds = Datasette(memory=True, default_deny=default_deny) - ds.root_enabled = True - await ds.invoke_startup() - try: - for name in (table, table.upper()): - assert not await ds.allowed( - action="view-table", - resource=TableResource("_memory", name), - actor={"id": "root"}, - ) - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_plugin_can_replace_statistics_policy(): - class ReplacementPolicy: - @hookimpl - def permission_resources_sql(self, action, actor): - if action == "view-table": - return PermissionSQL( - sql="SELECT 'data' AS parent, 'sqlite_stat1' AS child, :statistics_allowed AS allow, 'custom statistics policy' AS reason", - params={"statistics_allowed": int(actor == {"id": "reader"})}, - ) - - ds = Datasette(memory=True) - db = ds.add_memory_database(uuid.uuid4().hex, name="data") - await db.execute_write("create table items(value text)") - await db.execute_write("analyze") - await ds.invoke_startup() - name = "datasette.default_permissions.sqlite_statistics" - original = ds.pm.unregister(name=name) - assert original is not None - replacement = ReplacementPolicy() - ds.pm.register(replacement, name="test-replacement-statistics-policy") - try: - actor = {"id": "reader"} - assert await ds.allowed( - action="view-table", - resource=TableResource("data", "sqlite_stat1"), - actor=actor, - ) - assert not await ds.allowed( - action="view-table", resource=TableResource("data", "sqlite_stat1") - ) - resources = await ds.allowed_resources( - "view-table", parent="data", actor=actor, include_is_private=True - ) - stats = [r for r in resources.resources if r.child == "sqlite_stat1"] - assert len(stats) == 1 and stats[0].private - assert ( - await ds.client.get("/data/sqlite_stat1.json", actor=actor) - ).status_code == 200 - assert (await ds.client.get("/data/sqlite_stat1.json")).status_code == 403 - finally: - ds.pm.unregister(replacement) - ds.pm.register(original, name=name) - ds.close() diff --git a/tests/test_publish_cloudrun.py b/tests/test_publish_cloudrun.py index aebcaa33..6617bc77 100644 --- a/tests/test_publish_cloudrun.py +++ b/tests/test_publish_cloudrun.py @@ -1,12 +1,10 @@ +from click.testing import CliRunner +from datasette import cli +from unittest import mock import json import os -import textwrap -from unittest import mock - import pytest -from click.testing import CliRunner - -from datasette import cli +import textwrap @pytest.mark.serial @@ -72,7 +70,9 @@ def test_publish_cloudrun_prompts_for_service( ), mock.call(f"gcloud builds submit --tag {tag}", shell=True), mock.call( - f"gcloud run deploy --allow-unauthenticated --platform=managed --image {tag} input-service --max-instances 1", + "gcloud run deploy --allow-unauthenticated --platform=managed --image {} input-service --max-instances 1".format( + tag + ), shell=True, ), ] @@ -107,7 +107,9 @@ def test_publish_cloudrun(mock_call, mock_output, mock_which, tmp_path_factory): ), mock.call(f"gcloud builds submit --tag {tag}", shell=True), mock.call( - f"gcloud run deploy --allow-unauthenticated --platform=managed --image {tag} test --max-instances 1", + "gcloud run deploy --allow-unauthenticated --platform=managed --image {} test --max-instances 1".format( + tag + ), shell=True, ), ] @@ -184,13 +186,13 @@ def test_publish_cloudrun_memory_cpu( tag = f"us-docker.pkg.dev/{mock_output.return_value}/datasette/datasette-test" expected_call = ( "gcloud run deploy --allow-unauthenticated --platform=managed" - f" --image {tag} test" + " --image {} test".format(tag) ) expected_build_call = f"gcloud builds submit --tag {tag}" if memory: - expected_call += f" --memory {memory}" + expected_call += " --memory {}".format(memory) if cpu: - expected_call += f" --cpu {cpu}" + expected_call += " --cpu {}".format(cpu) if timeout: expected_build_call += f" --timeout {timeout}" # max_instances defaults to 1 diff --git a/tests/test_publish_heroku.py b/tests/test_publish_heroku.py index 4302ed94..cab83654 100644 --- a/tests/test_publish_heroku.py +++ b/tests/test_publish_heroku.py @@ -1,11 +1,9 @@ +from click.testing import CliRunner +from datasette import cli +from unittest import mock import os import pathlib -from unittest import mock - import pytest -from click.testing import CliRunner - -from datasette import cli @pytest.mark.serial diff --git a/tests/test_pytest_autoclose_plugin.py b/tests/test_pytest_autoclose_plugin.py index 9b17d24b..3af1aace 100644 --- a/tests/test_pytest_autoclose_plugin.py +++ b/tests/test_pytest_autoclose_plugin.py @@ -20,7 +20,6 @@ def _run_pytest(tmp_path: Path) -> subprocess.CompletedProcess: cwd=str(tmp_path), capture_output=True, text=True, - check=False, ) diff --git a/tests/test_queries.py b/tests/test_queries.py index ebe8b832..bd2e2b95 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -15,29 +15,37 @@ from datasette.utils.sqlite import sqlite3, supports_returning requires_sqlite_returning = pytest.mark.skipif( not supports_returning(), reason="SQLite does not support RETURNING" ) -EXPECTED_CREATE_TABLE_TEMPLATE_SQL = "create table new_table (\n id integer primary key,\n name text\n -- created text default (datetime('now'))\n)" +EXPECTED_CREATE_TABLE_TEMPLATE_SQL = "\n".join( + ( + "create table new_table (", + " id integer primary key,", + " name text", + " -- created text default (datetime('now'))", + ")", + ) +) def _template_option_attributes(html, table): - match = re.search(rf'
', + ''.format( + i, i + ), f'', f'', f'', @@ -834,39 +760,6 @@ async def test_table_html_foreign_key_links(ds_client): ] -@pytest.mark.asyncio -@pytest.mark.parametrize("referenced_table", ("authors", "AuThOrS")) -async def test_table_html_foreign_key_to_missing_table_is_not_linked(referenced_table): - # https://github.com/simonw/datasette/issues/1515 - ds = Datasette([]) - db = ds.add_database( - Database(ds, memory_name="test_foreign_key_to_missing_table"), name="data" - ) - await db.execute_write_script(f""" - create table authors (id integer primary key, name text); - create table books ( - id integer primary key, - author_id integer references {referenced_table}(id), - missing_id integer references missing_table(id) - ); - insert into authors (id, name) values (1, 'Ada'); - insert into books (id, author_id, missing_id) values (1, 1, 7); - """) - response = await ds.client.get("/data/books") - assert response.status_code == 200 - table = Soup(response.text, "html.parser").find("table") - cells = {td["class"][0]: str(td) for td in table.select("tbody tr")[0].select("td")} - assert cells["col-author_id"] == ( - '' - ) - assert cells["col-missing_id"] == '' - # The JSON labels are left alone as well - data = (await ds.client.get("/data/books.json?_labels=on")).json() - assert data["rows"][0]["missing_id"] == 7 - assert data["rows"][0]["author_id"] == {"value": 1, "label": "Ada"} - - @pytest.mark.asyncio async def test_table_html_foreign_key_facets(ds_client): response = await ds_client.get( @@ -1774,7 +1667,7 @@ async def test_row_update_sets_message(): assert response.status_code == 200 assert response.json()["rows"][0]["name"] == long_name assert ds.unsign(response.cookies["ds_messages"], "messages") == [ - [f"Updated row 1 ({truncated_name})", ds.INFO] + ["Updated row 1 ({})".format(truncated_name), ds.INFO] ] finally: ds.close() @@ -1787,9 +1680,9 @@ def test_table_data_uses_base_url(app_client_base_url_prefix): import re soup = Soup(response.text, "html.parser") - table_script = next( + table_script = [ s for s in soup.find_all("script") if "_datasetteTableData" in (s.string or "") - ) + ][0] match = re.search( r"window\._datasetteTableData\s*=\s*({.*?});", table_script.string, @@ -1817,9 +1710,8 @@ def test_table_fragment_custom_table_include(): @pytest.mark.asyncio async def test_table_fragment_uses_render_cell_hook(): - from markupsafe import Markup - from datasette import hookimpl + from markupsafe import Markup class TestRenderCellPlugin: __name__ = "TestRenderCellPlugin" @@ -1827,7 +1719,7 @@ async def test_table_fragment_uses_render_cell_hook(): @hookimpl def render_cell(self, value, column, table, database): if database == "data" and table == "items" and column == "name": - return Markup(f"{value}") + return Markup("{}".format(value)) return None ds = Datasette(memory=True) @@ -2366,16 +2258,18 @@ def test_allow_facet_off(allow_facet): ) async def test_format_of_binary_links(size, title, length_bytes): ds = Datasette() - db_name = f"binary-links-{size}" + db_name = "binary-links-{}".format(size) db = ds.add_memory_database(db_name) - sql = f"select zeroblob({size}) as blob" - await db.execute_write(f"create table blobs as {sql}") - response = await ds.client.get(f"/{db_name}/blobs") + sql = "select zeroblob({}) as blob".format(size) + await db.execute_write("create table blobs as {}".format(sql)) + response = await ds.client.get("/{}/blobs".format(db_name)) assert response.status_code == 200 - expected = f"{title}><Binary: {length_bytes} bytes>" + expected = "{}><Binary: {} bytes>".format(title, length_bytes) assert expected in response.text # And test with arbitrary SQL query too - sql_response = await ds.client.get(f"{db_name}/-/query", params={"sql": sql}) + sql_response = await ds.client.get( + "{}/-/query".format(db_name), params={"sql": sql} + ) assert sql_response.status_code == 200 assert expected in sql_response.text diff --git a/tests/test_table_resource_identity.py b/tests/test_table_resource_identity.py deleted file mode 100644 index 35b4dcc6..00000000 --- a/tests/test_table_resource_identity.py +++ /dev/null @@ -1,420 +0,0 @@ -"""Table permission identities must agree with SQLite identifier resolution.""" - -import uuid -from unittest.mock import AsyncMock - -import pytest - -from datasette import hookimpl -from datasette.app import Datasette -from datasette.default_permissions import restrictions_allow_action -from datasette.permissions import Action, PermissionSQL, _permission_check_cache -from datasette.resources import QueryResource, TableResource -from datasette.utils.actions_sql import explain_permission_for_resource -from datasette.utils.asgi import Forbidden -from datasette.utils.permissions import gather_permission_sql_from_hooks - - -@pytest.mark.asyncio -@pytest.mark.parametrize("kind", ["table", "view"]) -@pytest.mark.parametrize("spelling", ["Inventory", "inventory", "INVENTORY"]) -@pytest.mark.parametrize("allowed", [False, True]) -@pytest.mark.parametrize("rule_spelling", ["Inventory", "iNvEnToRy"]) -async def test_table_permission_identity( - kind, spelling, allowed, rule_spelling, monkeypatch -): - ds = Datasette( - config={ - "permissions": {"view-table": not allowed, "insert-row": not allowed}, - "databases": { - "data": { - "tables": { - rule_spelling: { - "permissions": { - "view-table": allowed, - "insert-row": allowed, - } - } - } - } - }, - } - ) - db = ds.add_memory_database("identity_" + uuid.uuid4().hex, name="data") - cache_token = _permission_check_cache.set({}) - try: - await db.execute_write( - "create table Inventory (id integer primary key)" - if kind == "table" - else "create view Inventory as select 1 as id" - ) - await ds.invoke_startup() - # Identity matching needs no target-schema lookup. Derived-table - # permissions may still check the schema version. All spellings and - # API entry points should share the existing permission result cache. - target_execute = AsyncMock(wraps=db.execute) - monkeypatch.setattr(db, "execute", target_execute) - internal_execute = AsyncMock(wraps=ds.get_internal_database().execute) - monkeypatch.setattr(ds.get_internal_database(), "execute", internal_execute) - resource = TableResource("data", spelling) - assert await ds.allowed_many( - actions=["view-table", "insert-row"], resource=resource - ) == {"view-table": allowed, "insert-row": allowed} - assert await ds.allowed(action="view-table", resource=resource) is allowed - assert await ds.check_visibility(None, "view-table", resource) == ( - allowed, - False, - ) - if allowed: - await ds.ensure_permission(action="view-table", resource=resource) - else: - with pytest.raises(Forbidden): - await ds.ensure_permission(action="view-table", resource=resource) - assert resource.child == spelling # Do not mutate caller-owned resources. - for variant in ("Inventory", "inventory", "INVENTORY"): - assert ( - await ds.allowed( - action="view-table", resource=TableResource("data", variant) - ) - is allowed - ) - assert internal_execute.await_count == 1 - assert all( - call.args[0] == "PRAGMA schema_version" - for call in target_execute.await_args_list - ) - assert all(key[3] == "inventory" for key in _permission_check_cache.get()) - finally: - _permission_check_cache.reset(cache_token) - ds.close() - - -@pytest.mark.asyncio -async def test_other_permission_identities_are_preserved(): - ds = Datasette( - config={ - "databases": { - "data": { - "tables": { - "Äpfel": {"permissions": {"view-table": False}}, - "Future": {"permissions": {"view-table": False}}, - }, - "queries": { - "Report": { - "sql": "select 1", - "permissions": {"view-query": False}, - }, - "report": { - "sql": "select 1", - "permissions": {"view-query": True}, - }, - }, - } - } - } - ) - db = ds.add_memory_database("identity_" + uuid.uuid4().hex, name="data") - try: - await db.execute_write('create table "Äpfel" (id integer primary key)') - await db.execute_write('create table "äpfel" (id integer primary key)') - await db.execute_write("create table Report (id integer primary key)") - await ds.invoke_startup() - # SQLite folds ASCII identifier casing, not Unicode casing. - for name, expected in [ - ("ÄPFEL", False), - ("äPFEL", True), - ("Future", False), - ("future", False), - ]: - assert ( - await ds.allowed( - action="view-table", resource=TableResource("data", name) - ) - is expected - ) - # Query names remain case-sensitive even when a table has the same name. - for name, expected in [("Report", False), ("report", True)]: - assert ( - await ds.allowed( - action="view-query", resource=QueryResource("data", name) - ) - is expected - ) - finally: - ds.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("allow", [True, False, {"id": "reader"}]) -async def test_table_listings_and_explanations(allow): - ds = Datasette( - config={ - "databases": { - "data": { - "tables": { - "inventory": {"permissions": {"view-table": allow}}, - } - } - } - } - ) - db = ds.add_memory_database("identity_" + uuid.uuid4().hex, name="data") - try: - await db.execute_write("create table Inventory (id integer primary key)") - await db.execute_write("create view InventoryView as select id from Inventory") - await ds.invoke_startup() - for actor in (None, {"id": "reader"}): - expected = allow is True or (isinstance(allow, dict) and actor == allow) - explanation = await explain_permission_for_resource( - datasette=ds, - actor=actor, - action="view-table", - parent="data", - child="INVENTORY", - ) - assert explanation["allowed"] is expected - assert explanation["winning_scope"] == "resource" - assert any( - "data/inventory" in rule["reason"] - for rule in explanation["matched_rules"] - ) - page = await ds.allowed_resources( - "view-table", - actor, - parent="data", - include_is_private=True, - include_reasons=True, - limit=1, - ) - resources = [resource async for resource in page.all()] - matching = [r for r in resources if r.child == "Inventory"] - assert bool(matching) is expected - assert len(matching) <= 1 - if matching: - assert matching[0].private is isinstance(allow, dict) - assert any(r.child == "InventoryView" for r in resources) - finally: - ds.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("deny_first", [True, False]) -async def test_case_variant_rules_deny_wins(deny_first): - rules = [("inventory", False), ("INVENTORY", True)] - if not deny_first: - rules.reverse() - ds = Datasette( - config={ - "databases": { - "data": { - "tables": { - name: {"permissions": {"view-table": allow}} - for name, allow in rules - } - } - } - } - ) - db = ds.add_memory_database("identity_" + uuid.uuid4().hex, name="data") - try: - await db.execute_write("create table Inventory (id integer primary key)") - await ds.invoke_startup() - assert not await ds.allowed( - action="view-table", resource=TableResource("data", "Inventory") - ) - assert not ( - await ds.allowed_resources( - "view-table", parent="data", include_is_private=True - ) - ).resources - explanation = await explain_permission_for_resource( - datasette=ds, - actor=None, - action="view-table", - parent="data", - child="Inventory", - ) - assert not explanation["allowed"] - assert any( - rule["effect"] == "allow" and not rule["decisive"] - for rule in explanation["matched_rules"] - ) - assert any( - rule["effect"] == "deny" and rule["decisive"] - for rule in explanation["matched_rules"] - ) - finally: - ds.close() - - -@pytest.mark.asyncio -@pytest.mark.parametrize("config_style", ["allow", "permissions"]) -@pytest.mark.parametrize("allowed", [True, False]) -async def test_case_variant_token_restrictions(config_style, allowed): - table_config = ( - {"allow": allowed} - if config_style == "allow" - else {"permissions": {"view-table": allowed}} - ) - ds = Datasette( - config={"databases": {"data": {"tables": {"Inventory": table_config}}}} - ) - db = ds.add_memory_database("identity_" + uuid.uuid4().hex, name="data") - actor = {"id": "reader", "_r": {"r": {"data": {"inventory": ["vt"]}}}} - try: - await db.execute_write("create table Inventory (id integer primary key)") - await ds.invoke_startup() - assert restrictions_allow_action( - ds, actor["_r"], "view-table", ("data", "INVENTORY") - ) - assert not restrictions_allow_action( - ds, actor["_r"], "view-table", ("Data", "Inventory") - ) - assert ( - await ds.allowed( - action="view-table", - resource=TableResource("data", "INVENTORY"), - actor=actor, - ) - is allowed - ) - page = await ds.allowed_resources("view-table", actor, parent="data") - assert [(r.parent, r.child) for r in page.resources] == ( - [("data", "Inventory")] if allowed else [] - ) - explanation = await explain_permission_for_resource( - datasette=ds, - actor=actor, - action="view-table", - parent="data", - child="Inventory", - ) - assert explanation["restriction_allowed"] - assert explanation["allowed"] is allowed - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_plugin_restriction_intersection_and_dependencies(): - class Plugin: - @hookimpl - def register_actions(self, datasette): - return [ - Action( - name="inspect-inventory", - description="Inspect inventory", - resource_class=TableResource, - also_requires="view-table", - ) - ] - - @hookimpl - def permission_resources_sql(self, action): - if action not in ("view-table", "inspect-inventory"): - return None - return [ - PermissionSQL( - sql="SELECT 'data' AS parent, 'INVENTORY' AS child, 1 AS allow, 'inventory grant' AS reason", - restriction_sql="SELECT 'data' AS parent, 'inventory' AS child", - ), - PermissionSQL( - restriction_sql="SELECT 'data' AS parent, 'InVeNtOrY' AS child" - ), - ] - - ds = Datasette(default_deny=True) - ds.pm.register(Plugin(), name="identity-test") - db = ds.add_memory_database("identity_" + uuid.uuid4().hex, name="data") - try: - await db.execute_write("create table Inventory (id integer primary key)") - await db.execute_write("create table Other (id integer primary key)") - await ds.invoke_startup() - for action in ("view-table", "inspect-inventory"): - assert await ds.allowed( - action=action, resource=TableResource("data", "Inventory") - ) - assert not await ds.allowed( - action=action, resource=TableResource("data", "Other") - ) - resources = ( - await ds.allowed_resources( - action, parent="data", include_is_private=True - ) - ).resources - assert [r.child for r in resources] == ["Inventory"] - explanation = await explain_permission_for_resource( - datasette=ds, - actor=None, - action=action, - parent="data", - child="Inventory", - ) - assert explanation["allowed"] - assert all(item["allowed"] for item in explanation["restrictions"]) - finally: - ds.pm.unregister(name="identity-test") - ds.close() - - -@pytest.mark.asyncio -async def test_shared_plugin_rule_keeps_query_identity_and_original_sql(): - shared = PermissionSQL( - sql="SELECT 'data' AS parent, 'Inventory' AS child, 0 AS allow, 'shared deny' AS reason" - ) - original_sql = shared.sql - - class Plugin: - @hookimpl - def permission_resources_sql(self, action): - if action in ("view-table", "view-query"): - return shared - - ds = Datasette( - config={ - "databases": { - "data": { - "queries": { - "Inventory": "select 1", - "inventory": "select 1", - } - } - } - } - ) - ds.add_memory_database("identity_" + uuid.uuid4().hex, name="data") - ds.pm.register(Plugin(), name="identity-test") - try: - await ds.invoke_startup() - for _ in range(2): - await gather_permission_sql_from_hooks( - datasette=ds, actor=None, action="view-table" - ) - assert shared.sql == original_sql - assert not await ds.allowed( - action="view-table", resource=TableResource("data", "inventory") - ) - assert await ds.allowed( - action="view-query", resource=QueryResource("data", "inventory") - ) - assert not await ds.allowed( - action="view-query", resource=QueryResource("data", "Inventory") - ) - assert await ds.allowed( - action="view-table", resource=TableResource("Data", "Inventory") - ) - assert restrictions_allow_action( - ds, - {"r": {"data": {"Inventory": ["vq"]}}}, - "view-query", - ("data", "Inventory"), - ) - assert not restrictions_allow_action( - ds, - {"r": {"data": {"Inventory": ["vq"]}}}, - "view-query", - ("data", "inventory"), - ) - finally: - ds.pm.unregister(name="identity-test") - ds.close() diff --git a/tests/test_tasks_endpoint.py b/tests/test_tasks_endpoint.py deleted file mode 100644 index be174dbe..00000000 --- a/tests/test_tasks_endpoint.py +++ /dev/null @@ -1,122 +0,0 @@ -""" -Tests for the /-/tasks introspection endpoint. - -/-/tasks exposes datasette._background_tasks (see tests/test_background_tasks.py -for the supervisor machinery itself) the same way /-/threads exposes threading -internals: gated behind the permissions-debug permission, JSON-only. -""" - -import asyncio -import contextlib -import functools - -import pytest - -from datasette.app import Datasette - - -async def example_task(datasette): - pass - - -class ExampleWorker: - async def run(self, datasette): - pass - - async def __call__(self, datasette): - pass - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "func, qualified_name", - [ - (example_task, "example_task"), - (functools.partial(example_task), "example_task"), - (ExampleWorker().run, "ExampleWorker.run"), - (ExampleWorker(), "ExampleWorker.__call__"), - ], -) -async def test_task_function_path(func, qualified_name): - ds = Datasette(memory=True) - ds.root_enabled = True - handle = ds.add_background_task(func, name="custom-name") - try: - response = await ds.client.get("/-/tasks.json", actor={"id": "root"}) - assert response.status_code == 200 - task = response.json()["tasks"][0] - assert task["name"] == "custom-name" - assert task["function"] == f"{__name__}.{qualified_name}" - assert handle.function == task["function"] - assert "plugin" not in task - await handle.task - html = await ds.client.get("/-/tasks", actor={"id": "root"}) - assert html.status_code == 200 - assert task["function"] in html.text - finally: - await ds.invoke_shutdown() - - -@pytest.mark.asyncio -async def test_tasks_requires_permissions_debug(): - ds = Datasette(memory=True) - ds.root_enabled = True - - denied = await ds.client.get("/-/tasks.json") - assert denied.status_code == 403 - - allowed = await ds.client.get("/-/tasks.json", actor={"id": "root"}) - assert allowed.status_code == 200 - data = allowed.json() - assert data["ok"] is True - assert "tasks" in data - assert "launched" in data - - -@pytest.mark.asyncio -async def test_running_and_crashed_task_states(): - ds = Datasette(memory=True) - ds.root_enabled = True - - async def long_running(datasette): - await asyncio.Event().wait() - - async def crashing_task(datasette): - raise RuntimeError("kaboom") - - long_handle = ds.add_background_task(long_running, name="long-runner") - crash_handle = ds.add_background_task(crashing_task, name="crashing_task") - - await ds.start_background_tasks() - - # Let the crashing_task run to completion and its done-callback (which sets - # handle.state = "crashed") actually fire before we read state back out. - await asyncio.wait_for( - asyncio.gather(crash_handle.task, return_exceptions=True), timeout=5 - ) - await asyncio.sleep(0) - - try: - response = await ds.client.get("/-/tasks.json", actor={"id": "root"}) - assert response.status_code == 200 - data = response.json() - assert data["launched"] is True - - by_name = {t["name"]: t for t in data["tasks"]} - assert by_name["long-runner"]["state"] == "running" - assert by_name["long-runner"]["exception"] is None - assert by_name["long-runner"]["started_at"] is not None - - crashed = by_name["crashing_task"] - assert crashed["function"] == ( - f"{__name__}.test_running_and_crashed_task_states..crashing_task" - ) - assert crashed["state"] == "crashed" - assert crashed["exception"] is not None - assert isinstance(crashed["exception"], str) - assert "kaboom" in crashed["exception"] - assert "RuntimeError" in crashed["exception"] - finally: - long_handle.cancel() - with contextlib.suppress(asyncio.CancelledError): - await long_handle.task diff --git a/tests/test_telemetry.py b/tests/test_telemetry.py deleted file mode 100644 index 801876df..00000000 --- a/tests/test_telemetry.py +++ /dev/null @@ -1,1023 +0,0 @@ -import json -import sqlite3 -import subprocess -import sys -import threading -import time - -import pytest -import sqlite_utils -from opentelemetry import context as otel_context_api -from opentelemetry import trace as otel_trace -from opentelemetry.trace import SpanKind, StatusCode - -from datasette.app import Datasette -from datasette.database import Database, QueryInterrupted -from datasette.telemetry import ( - MAX_SQL_LENGTH, - SCHEMA_URL, - sql_attribute, - sql_operation_name, - tracer, -) -from datasette.version import __version__ - -SECRET_PARAM_VALUE = "SUPER_SECRET_PARAM_VALUE_XYZ_123" - -INVALID_SQL = "select this_is_not_valid_sql from nowhere" - -# Bounded so a broken time limit fails rather than hangs, but too slow to -# finish within the millisecond time limits used below. -SLOW_SQL = """ -with recursive counter(x) as ( - select 1 union all select x + 1 from counter where x < 50000000 -) -select max(x) from counter -""" - - -def _db_query_spans(otel_spans): - return [span for span in otel_spans.get_finished_spans() if span.name == "db.query"] - - -def _spans_for_namespace(otel_spans, namespace): - "db.query spans for one database, excluding queries against the internal database." - return [ - span - for span in _db_query_spans(otel_spans) - if span.attributes["db.namespace"] == namespace - ] - - -def _children_named(otel_spans, name, parent_span_context): - "Finished spans called `name` that are direct children of `parent_span_context`." - return [ - span - for span in otel_spans.get_finished_spans() - if span.name == name - and span.parent is not None - and span.parent.span_id == parent_span_context.span_id - and span.parent.trace_id == parent_span_context.trace_id - and span.context.trace_id == parent_span_context.trace_id - ] - - -def _descends_from(span, ancestor_span_context, by_span_id): - "True if `span` reaches `ancestor_span_context` by walking parent links." - seen = set() - current = span - while current.parent is not None: - if current.parent.span_id == ancestor_span_context.span_id: - return current.parent.trace_id == ancestor_span_context.trace_id - if current.parent.span_id in seen: - return False - seen.add(current.parent.span_id) - current = by_span_id.get(current.parent.span_id) - if current is None: - return False - return False - - -def _all_attribute_values(otel_spans): - "Every attribute value on every finished span and span event." - values = [] - for span in otel_spans.get_finished_spans(): - values.extend((span.attributes or {}).values()) - for event in span.events: - values.extend((event.attributes or {}).values()) - return values - - -def test_datasette_package_never_imports_the_sdk(): - """ - Importing datasette does not load the OpenTelemetry SDK. - - conftest.py moves this test to the front of the run by name. - """ - code = ( - "import datasette.app, datasette.database, datasette.telemetry, sys; " - "print([m for m in sys.modules if m.startswith('opentelemetry.sdk')])" - ) - result = subprocess.run( - [sys.executable, "-c", code], capture_output=True, text=True, check=True - ) - assert ( - result.stdout.strip() == "[]" - ), f"datasette imported the OpenTelemetry SDK: {result.stdout.strip()}" - - -@pytest.mark.asyncio -async def test_db_query_span_basic_attributes(ds_client, otel_spans): - response = await ds_client.get("/fixtures/-/query.json?sql=select+1") - assert response.status_code == 200 - - spans = _db_query_spans(otel_spans) - assert spans, "expected at least one db.query span" - span = spans[-1] - - assert span.attributes["db.system"] == "sqlite" - assert span.attributes["db.namespace"] == "fixtures" - assert span.attributes["db.query.text"] == "select 1" - assert span.attributes["datasette.rows_returned"] == 1 - assert span.attributes["datasette.truncated"] is False - assert isinstance(span.attributes["datasette.time_limit_ms"], int) - assert span.status.status_code == StatusCode.UNSET - - -@pytest.mark.asyncio -async def test_truncated_result_sets_truncated_attribute(otel_spans): - "A result cut short by max_returned_rows records truncated=True." - ds = Datasette(memory=True, settings={"max_returned_rows": 5}) - db = ds.add_memory_database("t04_truncated") - results = await db.execute( - "select value from json_each('[1,2,3,4,5,6,7,8,9,10]')", truncate=True - ) - assert results.truncated - - spans = _spans_for_namespace(otel_spans, "t04_truncated") - assert spans - span = spans[-1] - assert span.attributes["datasette.truncated"] is True - assert span.attributes["datasette.rows_returned"] == 5 - - -@pytest.mark.asyncio -async def test_facetable_request_produces_db_query_spans(ds_client, otel_spans): - response = await ds_client.get("/fixtures/facetable.json") - assert response.status_code == 200 - - spans = _db_query_spans(otel_spans) - assert spans, "expected at least one db.query span" - assert all(span.attributes["db.system"] == "sqlite" for span in spans) - # Each span records the SQL or, for callback methods, the callback name: - assert all( - span.attributes.get("db.query.text") - or span.attributes.get("datasette.callback") - for span in spans - ) - assert any(span.attributes.get("db.query.text") for span in spans) - # Rendering the page also queries the internal database, so only some of - # these spans belong to "fixtures". - assert any(span.attributes["db.namespace"] == "fixtures" for span in spans) - - -def test_sql_attribute_truncates_at_2048(): - short_sql = "select 1" - assert sql_attribute(short_sql) == "select 1" - # Surrounding whitespace is stripped: - assert sql_attribute(" select 1\n") == "select 1" - - long_sql = "select 1 -- " + ("x" * 3000) - truncated = sql_attribute(long_sql) - assert len(truncated) == MAX_SQL_LENGTH + len("…[truncated]") - assert truncated.startswith("select 1 -- ") - assert truncated.endswith("…[truncated]") - - -@pytest.mark.asyncio -async def test_db_query_text_is_truncated_in_real_span(ds_client, otel_spans): - # A long trailing comment keeps the SQL valid but over the 2048 character limit - long_sql = "select 1 -- " + ("x" * 3000) - response = await ds_client.get("/fixtures/-/query.json", params={"sql": long_sql}) - assert response.status_code == 200 - - spans = _db_query_spans(otel_spans) - assert spans - assert any(len(span.attributes.get("db.query.text", "")) > 100 for span in spans), ( - "expected the long query to reach a span - otherwise this test would " - "pass even if truncation were never applied" - ) - for span in spans: - recorded = span.attributes.get("db.query.text", "") - assert len(recorded) <= MAX_SQL_LENGTH + len("…[truncated]") - - -@pytest.mark.asyncio -async def test_no_span_attribute_ever_contains_a_parameter_value(ds_client, otel_spans): - response = await ds_client.get( - "/fixtures/-/query.json", - params={"sql": "select :secret", "secret": SECRET_PARAM_VALUE}, - ) - assert response.status_code == 200 - # Confirm the bound parameter value was used by the query: - assert SECRET_PARAM_VALUE in json.dumps(response.json()) - - for value in _all_attribute_values(otel_spans): - if isinstance(value, str): - assert SECRET_PARAM_VALUE not in value - elif isinstance(value, (list, tuple)): - for item in value: - if isinstance(item, str): - assert SECRET_PARAM_VALUE not in item - - spans = _db_query_spans(otel_spans) - assert spans - span = spans[-1] - assert "select :secret" in span.attributes["db.query.text"] - assert span.attributes.get("datasette.param_count") == 1 - - -@pytest.mark.asyncio -async def test_query_interrupted_sets_error_status(otel_spans): - """ - A query that exceeds the sql_time_limit_ms setting is a span error. - - The limit comes from the setting because a shorter custom_time_limit - marks the timeout as expected. - """ - ds = Datasette(memory=True, settings={"sql_time_limit_ms": 20}) - db = ds.add_memory_database("t09_instance_limit_timeout") - with pytest.raises(QueryInterrupted): - await db.execute(SLOW_SQL) - - spans = _spans_for_namespace(otel_spans, "t09_instance_limit_timeout") - assert spans - span = spans[-1] - assert span.status.status_code == StatusCode.ERROR - assert span.attributes["datasette.interrupted"] is True - assert span.events - assert all(event.name == "exception" for event in span.events) - - -async def _expected_timeout_count_span(otel_spans, database_name): - "Make table_counts() time out and return its db.query span." - db = Datasette(memory=True).add_memory_database(database_name) - await db.execute_write("create table big (id integer primary key, t text)") - await db.execute_write_many( - "insert into big (t) values (?)", [["x" * 50] for _ in range(11000)] - ) - # count_limit caps the scan at 10001 rows. Below 20ms sqlite_timelimit() - # checks the limit on every VM instruction, so this reliably exceeds 1ms. - counts = await db.table_counts(1) - assert counts == { - "big": None - }, "the count did not actually time out, so the rest of this test is vacuous" - - spans = [ - span - for span in _spans_for_namespace(otel_spans, database_name) - if "count(*)" in span.attributes.get("db.query.text", "") - ] - assert len(spans) == 1 - return spans[0] - - -@pytest.mark.asyncio -async def test_expected_timeout_is_not_a_span_error(otel_spans): - span = await _expected_timeout_count_span(otel_spans, "t09_expected_timeout") - # Recorded as interrupted, but not as an error: - assert span.attributes["datasette.interrupted"] is True - assert span.status.status_code != StatusCode.ERROR - assert not [event for event in span.events if event.name == "exception"] - - -@pytest.mark.asyncio -async def test_expected_timeout_does_not_error_the_inner_execute_span(otel_spans): - "The db.query.execute child span is not marked as an error either." - span = await _expected_timeout_count_span(otel_spans, "t09_expected_timeout_inner") - children = _children_named(otel_spans, "db.query.execute", span.context) - assert len(children) == 1 - child = children[0] - assert child.status.status_code != StatusCode.ERROR - assert not [event for event in child.events if event.name == "exception"] - - -@pytest.mark.asyncio -async def test_unexpected_timeout_is_still_a_span_error(otel_spans): - "A timeout is an error if custom_time_limit is above sql_time_limit_ms." - ds = Datasette(memory=True, settings={"sql_time_limit_ms": 20}) - db = ds.add_memory_database("t09_custom_limit_ignored") - with pytest.raises(QueryInterrupted): - await db.execute(SLOW_SQL, custom_time_limit=5000) - - spans = _spans_for_namespace(otel_spans, "t09_custom_limit_ignored") - assert spans - span = spans[-1] - # The setting overrides the larger custom_time_limit: - assert span.attributes["datasette.time_limit_ms"] == 20 - assert span.attributes["datasette.interrupted"] is True - assert span.status.status_code == StatusCode.ERROR - assert any(event.name == "exception" for event in span.events) - - children = _children_named(otel_spans, "db.query.execute", span.context) - assert len(children) == 1 - assert children[0].status.status_code == StatusCode.ERROR - - -@pytest.mark.asyncio -async def test_unsuppressed_sql_error_is_a_span_error(ds_client, otel_spans): - db = ds_client.ds.get_database("fixtures") - with pytest.raises(sqlite3.OperationalError): - await db.execute(INVALID_SQL) - - spans = _db_query_spans(otel_spans) - assert spans - span = spans[-1] - assert span.status.status_code == StatusCode.ERROR - assert any(event.name == "exception" for event in span.events) - assert "datasette.sql_error_suppressed" not in span.attributes - - -@pytest.mark.asyncio -async def test_suppressed_sql_error_is_not_a_span_error(ds_client, otel_spans): - "With log_sql_errors=False the error is recorded as suppressed, not a span error." - db = ds_client.ds.get_database("fixtures") - with pytest.raises(sqlite3.OperationalError): - await db.execute(INVALID_SQL, log_sql_errors=False) - - spans = _db_query_spans(otel_spans) - assert spans - span = spans[-1] - assert span.status.status_code == StatusCode.UNSET - assert span.attributes["datasette.sql_error_suppressed"] is True - assert not [event for event in span.events if event.name == "exception"] - - -@pytest.mark.asyncio -async def test_execute_write_produces_db_query_span(otel_spans): - # Named in-memory databases are shared, so each test uses a unique name. - db = Datasette(memory=True).add_memory_database("t03_write_span") - await db.execute_write("create table docs (id integer primary key, name text)") - await db.execute_write("insert into docs (id, name) values (?, ?)", [1, "one"]) - - spans = _spans_for_namespace(otel_spans, "t03_write_span") - assert spans, "expected db.query spans from execute_write()" - span = spans[-1] - - assert span.attributes["db.system"] == "sqlite" - assert span.attributes["db.namespace"] == "t03_write_span" - assert span.attributes["db.query.text"] == ( - "insert into docs (id, name) values (?, ?)" - ) - assert span.attributes["datasette.param_count"] == 2 - - -@pytest.mark.asyncio -async def test_execute_write_script_sets_executescript_attribute(otel_spans): - db = Datasette(memory=True).add_memory_database("t03_write_script_span") - await db.execute_write_script( - "create table docs (id integer primary key);\n" - "insert into docs (id) values (1);" - ) - - spans = _spans_for_namespace(otel_spans, "t03_write_script_span") - assert spans, "expected a db.query span from execute_write_script()" - span = spans[-1] - - assert span.attributes["db.system"] == "sqlite" - assert span.attributes["datasette.executescript"] is True - assert "insert into docs" in span.attributes["db.query.text"] - - -@pytest.mark.asyncio -async def test_execute_write_many_records_param_sets_not_rows_returned(otel_spans): - db = Datasette(memory=True).add_memory_database("t03_write_many_span") - await db.execute_write("create table docs (id integer primary key)") - await db.execute_write_many( - "insert into docs (id) values (?)", [[i] for i in range(1, 6)] - ) - - spans = _spans_for_namespace(otel_spans, "t03_write_many_span") - many_spans = [ - span for span in spans if span.attributes.get("datasette.executemany") is True - ] - assert len(many_spans) == 1 - span = many_spans[0] - - assert span.attributes["datasette.param_sets"] == 5 - assert "datasette.rows_returned" not in span.attributes - - -# --- Context propagation across thread boundaries -------------------------- -# -# These tests check span parentage, not just that the spans exist. - - -@pytest.mark.asyncio -async def test_db_query_execute_parents_to_db_query(ds_client, otel_spans): - # execute_fn() submits to the executor, so db.query.execute is created on - # another thread. - response = await ds_client.get("/fixtures/-/query.json?sql=select+1") - assert response.status_code == 200 - - query_spans = [ - span - for span in _spans_for_namespace(otel_spans, "fixtures") - if span.attributes.get("db.query.text") == "select 1" - ] - assert query_spans, "expected a db.query span for 'select 1'" - query_span = query_spans[-1] - - assert [ - span - for span in otel_spans.get_finished_spans() - if span.name == "db.query.execute" - ], "expected at least one db.query.execute span" - children = _children_named(otel_spans, "db.query.execute", query_span.context) - assert len(children) == 1, "expected exactly one db.query.execute child of db.query" - # db.query.execute runs within db.query; the gap is the thread pool wait. - assert query_span.start_time <= children[0].start_time - assert children[0].end_time <= query_span.end_time - - -@pytest.mark.asyncio -async def test_immutable_database_propagates_context(tmp_path, otel_spans): - # Immutable databases run execute_isolated_fn() on another thread using - # loop.run_in_executor(), not the write thread. - db_path = tmp_path / "t04_immutable.db" - sqlite_utils.Database(str(db_path))["t"].insert({"id": 1}, pk="id") - - ds = Datasette() - db = Database(ds, path=str(db_path), is_mutable=False) - ds.add_database(db, name="t04_immutable") - - def fn(conn): - with tracer.start_as_current_span("t04-child-in-isolated-worker"): - pass - - try: - with tracer.start_as_current_span("t04-parent-on-event-loop") as parent: - parent_context = parent.get_span_context() - await db.execute_isolated_fn(fn) - finally: - ds.remove_database("t04_immutable") - - assert [ - span - for span in otel_spans.get_finished_spans() - if span.name == "t04-child-in-isolated-worker" - ], "expected a span created inside execute_isolated_fn's worker thread" - # Expected chain: event loop parent -> db.query -> worker thread child - query_spans = _children_named(otel_spans, "db.query", parent_context) - assert len(query_spans) == 1 - children = _children_named( - otel_spans, "t04-child-in-isolated-worker", query_spans[0].context - ) - assert len(children) == 1 - - -@pytest.mark.asyncio -async def test_write_spans_parent_to_db_query(otel_spans): - # execute_write() queues a WriteTask for the write thread. - # db.write.queue_wait and db.write.execute are both children of db.query. - db = Datasette(memory=True).add_memory_database("t04_write_spans") - await db.execute_write("create table docs (id integer primary key)") - - query_spans = _spans_for_namespace(otel_spans, "t04_write_spans") - assert query_spans, "expected a db.query span from execute_write()" - query_span = query_spans[-1] - - queue_wait_children = _children_named( - otel_spans, "db.write.queue_wait", query_span.context - ) - execute_children = _children_named( - otel_spans, "db.write.execute", query_span.context - ) - assert len(queue_wait_children) == 1 - assert len(execute_children) == 1 - - execute_span = execute_children[0] - assert execute_span.attributes["datasette.isolated_connection"] is False - assert execute_span.attributes["datasette.transaction"] is True - # The queue wait ends before the write begins. - assert queue_wait_children[0].end_time <= execute_span.start_time - - -@pytest.mark.asyncio -async def test_write_queue_wait_duration_reflects_real_wait(otel_spans): - # db.write.queue_wait runs from task.enqueued_at_ns, captured on the event - # loop, to when the write thread dequeues the task. - ds = Datasette(memory=True) - db = ds.add_memory_database("t04_queue_wait") - await db.execute_write("create table docs (id integer primary key)") - - def slow_write(conn): - time.sleep(0.1) - - # Queue a slow write without waiting for it, then a second write behind it: - _, slow_future = await db._send_to_write_thread(slow_write, block=False) - await db.execute_write("insert into docs (id) values (1)") - await slow_future - - query_spans = [ - span - for span in _spans_for_namespace(otel_spans, "t04_queue_wait") - if span.attributes.get("db.query.text") == "insert into docs (id) values (1)" - ] - assert query_spans, "expected a db.query span for the queued-behind insert" - queue_wait_children = _children_named( - otel_spans, "db.write.queue_wait", query_spans[-1].context - ) - assert len(queue_wait_children) == 1 - duration_ns = queue_wait_children[0].end_time - queue_wait_children[0].start_time - # The slow write sleeps for 100ms - assert duration_ns > 10_000_000, f"queue wait was only {duration_ns}ns" - - -async def _write_spans_from_one_enqueue(otel_spans, name, block): - """ - Run one write through the write thread inside a span, returning - (enqueueing span context, {span name: span}). - - Uses _send_to_write_thread() because execute_write() would add its own - db.query span between the enqueueing span and the write spans. - """ - db = Datasette(memory=True).add_memory_database(name) - await db.execute_write("create table docs (id integer primary key)") - - def insert(conn): - conn.execute("insert into docs (id) values (1)") - - otel_spans.clear() - with tracer.start_as_current_span("enqueueing-span") as enqueuer: - enqueuer_context = enqueuer.get_span_context() - queued = await db._send_to_write_thread(insert, block=block) - if not block: - # Wait for the write after the enqueueing span has ended. The reply - # future resolves once both write spans have been exported. - _, reply_future = queued - await reply_future - - spans = {} - for span in otel_spans.get_finished_spans(): - if span.name in ("db.write.queue_wait", "db.write.execute"): - assert span.name not in spans, f"more than one {span.name} span" - spans[span.name] = span - assert set(spans) == {"db.write.queue_wait", "db.write.execute"} - return enqueuer_context, spans - - -@pytest.mark.asyncio -async def test_blocking_write_spans_still_parent_normally(otel_spans): - # block=True waits for the write, so its spans are children of the - # enqueueing span, with no links. - enqueuer_context, spans = await _write_spans_from_one_enqueue( - otel_spans, "t07_blocking_write", block=True - ) - for name, span in spans.items(): - assert span.parent is not None, f"{name} lost its parent" - assert span.parent.span_id == enqueuer_context.span_id, name - assert span.parent.trace_id == enqueuer_context.trace_id, name - assert span.context.trace_id == enqueuer_context.trace_id, name - assert span.links == (), f"{name} should be parented, not linked" - - -@pytest.mark.asyncio -async def test_nonblocking_write_spans_are_roots_with_a_link(otel_spans): - # block=False returns before the write runs, so the write spans are roots - # linked to the enqueueing span. - enqueuer_context, spans = await _write_spans_from_one_enqueue( - otel_spans, "t07_nonblocking_write", block=False - ) - assert enqueuer_context.is_valid, "test's own enqueueing span was not recorded" - for name, span in spans.items(): - assert span.parent is None, f"{name} is still parented" - # Each write span starts its own trace - assert span.context.trace_id != enqueuer_context.trace_id, name - assert len(span.links) == 1, f"{name} has links {span.links}" - link_context = span.links[0].context - assert link_context.trace_id == enqueuer_context.trace_id, name - assert link_context.span_id == enqueuer_context.span_id, name - # The two write spans are separate roots - assert ( - spans["db.write.queue_wait"].context.trace_id - != spans["db.write.execute"].context.trace_id - ) - - -@pytest.mark.asyncio -async def test_nonblocking_write_link_has_no_attributes(otel_spans): - _, spans = await _write_spans_from_one_enqueue( - otel_spans, "t07_nonblocking_link_attrs", block=False - ) - for name, span in spans.items(): - assert len(span.links) == 1, name - assert dict(span.links[0].attributes or {}) == {}, name - - -@pytest.mark.asyncio -async def test_nonblocking_write_spans_ignore_the_write_threads_ambient_context( - otel_spans, -): - """ - block=False spans ignore any context left attached on the write thread. - - A prepare_connection hook could attach a context and never detach it. - This test does that, then checks the write spans are still roots. - """ - ds = Datasette(memory=True) - db = ds.add_memory_database("t07_ambient_write_thread") - write_thread_name = "_execute_writes for database t07_ambient_write_thread" - real_prepare_connection = ds._prepare_connection - leaked = {} - - def prepare_connection(conn, database): - if threading.current_thread().name == write_thread_name: - # Runs on the write thread before any task is dequeued, and never - # detaches. - span = tracer.start_span("leaked-write-thread-ambient-span") - leaked["span_id"] = span.get_span_context().span_id - otel_context_api.attach(otel_trace.set_span_in_context(span)) - return real_prepare_connection(conn, database) - - ds._prepare_connection = prepare_connection - try: - await db.execute_write("create table docs (id integer primary key)") - - def insert(conn): - conn.execute("insert into docs (id) values (1)") - - otel_spans.clear() - with tracer.start_as_current_span("enqueueing-span") as enqueuer: - enqueuer_context = enqueuer.get_span_context() - _, reply_future = await db._send_to_write_thread(insert, block=False) - await reply_future - finally: - ds._prepare_connection = real_prepare_connection - db.close() - - assert "span_id" in leaked, "the ambient context was never leaked - test is vacuous" - write_spans = [ - span - for span in otel_spans.get_finished_spans() - if span.name in ("db.write.queue_wait", "db.write.execute") - ] - assert len(write_spans) == 2 - for span in write_spans: - assert span.parent is None, ( - f"{span.name} parented to the write thread's leftover ambient " - "context instead of being a root" - ) - assert span.links[0].context.span_id == enqueuer_context.span_id - - -@pytest.mark.asyncio -async def test_suppressed_error_does_not_mark_execute_span(ds_client, otel_spans): - "The inner db.query.execute span also respects log_sql_errors=False." - db = ds_client.ds.get_database("fixtures") - with pytest.raises(sqlite3.OperationalError): - await db.execute(INVALID_SQL, log_sql_errors=False) - - execute_spans = [ - span - for span in otel_spans.get_finished_spans() - if span.name == "db.query.execute" - ] - assert execute_spans - span = execute_spans[-1] - assert span.status.status_code == StatusCode.UNSET - assert not [event for event in span.events if event.name == "exception"] - - -@pytest.mark.asyncio -async def test_invoke_startup_produces_one_trace_not_dozens_of_orphans(otel_spans): - "Spans emitted by invoke_startup() share a single datasette.startup root span." - ds = Datasette(memory=True) - ds.add_memory_database("t05_startup_db") - # Ignore spans from constructing Datasette, which happens before startup - otel_spans.clear() - - # No ambient span, as in the ASGI lifespan path where startup runs before - # any request. - assert ( - not otel_trace.get_current_span().get_span_context().is_valid - ), "this test must run with no ambient span" - - await ds.invoke_startup() - - spans = otel_spans.get_finished_spans() - assert len(spans) > 10, f"expected startup to emit many spans, got {len(spans)}" - - startup_spans = [span for span in spans if span.name == "datasette.startup"] - assert len(startup_spans) == 1 - startup = startup_spans[0] - assert startup.parent is None, "datasette.startup should be a root span" - - trace_ids = {span.context.trace_id for span in spans} - assert trace_ids == {startup.context.trace_id}, ( - f"startup produced {len(trace_ids)} distinct traces; every span it " - "causes should share the datasette.startup trace" - ) - - roots = [span for span in spans if span.parent is None] - assert [span.name for span in roots] == ["datasette.startup"] - - by_span_id = {span.context.span_id: span for span in spans} - - # Internal database reads: - internal_queries = [ - span - for span in spans - if span.name == "db.query" and span.attributes["db.namespace"] == "__INTERNAL__" - ] - assert internal_queries, "expected internal-catalog db.query spans during startup" - assert all( - _descends_from(span, startup.context, by_span_id) for span in internal_queries - ) - - # Internal database writes, which run on the write thread: - write_spans = [span for span in spans if span.name.startswith("db.write.")] - assert write_spans, "expected db.write.* spans during startup" - assert all( - _descends_from(span, startup.context, by_span_id) for span in write_spans - ) - - -# --- Semantic conventions: span kind, scope, db.operation.name ------------- - - -@pytest.mark.asyncio -async def test_db_query_is_client_kind_and_children_are_internal(otel_spans): - """ - db.query spans are CLIENT. Their child spans are INTERNAL because they are - parts of one query rather than separate database calls. - """ - db = Datasette(memory=True).add_memory_database("t06_span_kind") - # Call each of the four SQL string methods: - await db.execute_write("create table docs (id integer primary key)") - await db.execute_write_many( - "insert into docs (id) values (?)", [[i] for i in range(1, 4)] - ) - await db.execute_write_script("insert into docs (id) values (99);") - await db.execute("select id from docs") - - query_spans = _spans_for_namespace(otel_spans, "t06_span_kind") - assert len(query_spans) == 4, "expected a db.query span per entry point" - for span in query_spans: - text = span.attributes["db.query.text"] - assert span.kind == SpanKind.CLIENT, f"db.query for {text!r} should be CLIENT" - - for name in ("db.query.execute", "db.write.execute", "db.write.queue_wait"): - children = [ - span for span in otel_spans.get_finished_spans() if span.name == name - ] - assert children, f"expected at least one {name} span" - for span in children: - assert span.kind == SpanKind.INTERNAL, f"{name} should be INTERNAL" - - -@pytest.mark.asyncio -async def test_instrumentation_scope_declares_version_and_schema_url( - ds_client, otel_spans -): - "The instrumentation scope includes the Datasette version and schema URL." - response = await ds_client.get("/fixtures/-/query.json?sql=select+1") - assert response.status_code == 200 - - spans = _db_query_spans(otel_spans) - assert spans, "expected at least one db.query span" - scope = spans[-1].instrumentation_scope - - assert scope.name == "datasette" - assert scope.version == __version__ - # Uses the literal URL so changing SCHEMA_URL requires updating this test - assert scope.schema_url == "https://opentelemetry.io/schemas/1.29.0" - assert SCHEMA_URL == "https://opentelemetry.io/schemas/1.29.0" - assert __version__, "the scope version must not be empty" - - -def test_db_operation_name_from_leading_keyword(): - assert sql_operation_name("select 1") == "SELECT" - assert sql_operation_name(" insert into x (a) values (1)") == "INSERT" - # A leading CTE reports WITH, not the operation inside it - assert sql_operation_name("with foo as (select 1) select * from foo") == "WITH" - # Unrecognized leading keyword - assert sql_operation_name("gibberish 1") is None - # A parenthesized SELECT or a leading comment also returns None - assert sql_operation_name("(select 1) union select 2") is None - assert sql_operation_name("-- a comment\nselect 1") is None - assert sql_operation_name("") is None - - -@pytest.mark.asyncio -async def test_db_operation_name_on_real_span(ds_client, otel_spans): - response = await ds_client.get("/fixtures/-/query.json?sql=select+1") - assert response.status_code == 200 - - spans = [ - span - for span in _spans_for_namespace(otel_spans, "fixtures") - if span.attributes.get("db.query.text") == "select 1" - ] - assert spans, "expected a db.query span for 'select 1'" - assert spans[-1].attributes["db.operation.name"] == "SELECT" - - -@pytest.mark.asyncio -async def test_execute_write_sets_db_operation_name(otel_spans): - db = Datasette(memory=True).add_memory_database("t06_write_operation") - await db.execute_write("create table docs (id integer primary key)") - await db.execute_write_many( - "insert into docs (id) values (?)", [[i] for i in range(1, 4)] - ) - - spans = _spans_for_namespace(otel_spans, "t06_write_operation") - by_operation = { - span.attributes["db.query.text"]: span.attributes.get("db.operation.name") - for span in spans - } - assert by_operation["create table docs (id integer primary key)"] == "CREATE" - assert by_operation["insert into docs (id) values (?)"] == "INSERT" - - -@pytest.mark.asyncio -async def test_execute_write_script_has_no_operation_name(otel_spans): - """ - Scripts can contain several statements, so db.operation.name is omitted. - - The script starts with `create`, which is on the allowlist, so this fails - if the operation name is extracted anyway. - """ - db = Datasette(memory=True).add_memory_database("t06_script_operation") - await db.execute_write_script( - "create table docs (id integer primary key);\n" - "insert into docs (id) values (1);" - ) - - spans = _spans_for_namespace(otel_spans, "t06_script_operation") - script_spans = [ - span for span in spans if span.attributes.get("datasette.executescript") is True - ] - assert len(script_spans) == 1 - assert "db.operation.name" not in script_spans[0].attributes - - -# --- Callback-style calls: execute_fn / execute_write_fn / execute_isolated_fn - - -@pytest.mark.asyncio -async def test_execute_fn_produces_db_query_span(otel_spans): - db = Datasette(memory=True).add_memory_database("t16_execute_fn") - await db.execute_write("create table t (id integer primary key)") - - def count_rows(conn): - return conn.execute("select count(*) from t").fetchone()[0] - - otel_spans.clear() - assert await db.execute_fn(count_rows) == 0 - - spans = _spans_for_namespace(otel_spans, "t16_execute_fn") - assert len(spans) == 1 - span = spans[0] - assert span.kind == SpanKind.CLIENT - assert span.attributes["db.system"] == "sqlite" - assert ( - span.attributes["datasette.callback"] - == "test_execute_fn_produces_db_query_span..count_rows" - ) - # Callbacks have no SQL text to record or take an operation name from - assert "db.query.text" not in span.attributes - assert "db.operation.name" not in span.attributes - children = _children_named(otel_spans, "db.query.execute", span.context) - assert len(children) == 1 - - -@pytest.mark.asyncio -async def test_execute_fn_lambda_reports_lambda(otel_spans): - db = Datasette(memory=True).add_memory_database("t16_lambda") - otel_spans.clear() - await db.execute_fn(lambda conn: conn.execute("select 1").fetchone()) - spans = _spans_for_namespace(otel_spans, "t16_lambda") - assert len(spans) == 1 - assert spans[0].attributes["datasette.callback"].endswith("") - - -@pytest.mark.asyncio -async def test_execute_write_fn_produces_db_query_span(otel_spans): - db = Datasette(memory=True).add_memory_database("t16_write_fn") - - def create_table(conn): - conn.execute("create table t (id integer primary key)") - - otel_spans.clear() - await db.execute_write_fn(create_table) - - spans = _spans_for_namespace(otel_spans, "t16_write_fn") - assert len(spans) == 1 - span = spans[0] - assert span.kind == SpanKind.CLIENT - assert ( - span.attributes["datasette.callback"] - == "test_execute_write_fn_produces_db_query_span..create_table" - ) - assert "db.query.text" not in span.attributes - # The write-thread spans are this span's children, same as execute_write() - for name in ("db.write.queue_wait", "db.write.execute"): - assert len(_children_named(otel_spans, name, span.context)) == 1, name - - -@pytest.mark.asyncio -async def test_execute_write_fn_callback_name_is_not_the_hook_wrapper(otel_spans): - # _wrap_fn_with_hooks() wraps callbacks that accept track_event - db = Datasette(memory=True).add_memory_database("t16_wrapper_name") - - def create_with_events(conn, track_event): - conn.execute("create table t (id integer primary key)") - - otel_spans.clear() - await db.execute_write_fn(create_with_events) - spans = _spans_for_namespace(otel_spans, "t16_wrapper_name") - assert len(spans) == 1 - assert spans[0].attributes["datasette.callback"] == ( - "test_execute_write_fn_callback_name_is_not_the_hook_wrapper" - "..create_with_events" - ) - - -@pytest.mark.asyncio -async def test_execute_write_fn_nonblocking_spans_link_to_the_new_span(otel_spans): - # With block=False the write thread spans link to the db.query span from - # execute_write_fn(), not to the span that was current when it was called. - db = Datasette(memory=True).add_memory_database("t16_nonblocking") - await db.execute_write("create table docs (id integer primary key)") - - def insert(conn): - conn.execute("insert into docs (id) values (1)") - - otel_spans.clear() - with tracer.start_as_current_span("t16-enqueueing-span") as enqueuer: - enqueuer_context = enqueuer.get_span_context() - await db.execute_write_fn(insert, block=False) - # Writes run in order, so this waits for the non-blocking write to finish - await db.execute_write("insert into docs (id) values (2)") - - query_spans = [ - span - for span in _spans_for_namespace(otel_spans, "t16_nonblocking") - if span.attributes.get("datasette.callback") - ] - assert len(query_spans) == 1 - fn_span_context = query_spans[0].context - linked = [ - span - for span in otel_spans.get_finished_spans() - if span.name in ("db.write.queue_wait", "db.write.execute") and span.links - ] - assert len(linked) == 2 - for span in linked: - assert span.parent is None, f"{span.name} is still parented" - assert span.links[0].context.span_id == fn_span_context.span_id, span.name - assert span.links[0].context.span_id != enqueuer_context.span_id, span.name - - -@pytest.mark.asyncio -async def test_execute_does_not_double_wrap(otel_spans): - # execute() and the SQL string write methods call the private - # _execute_fn() and _execute_write_fn(), so they create one db.query span. - db = Datasette(memory=True).add_memory_database("t16_no_double_wrap") - otel_spans.clear() - await db.execute_write("create table t (id integer primary key)") - assert len(_spans_for_namespace(otel_spans, "t16_no_double_wrap")) == 1 - otel_spans.clear() - await db.execute("select * from t") - spans = _spans_for_namespace(otel_spans, "t16_no_double_wrap") - assert len(spans) == 1 - assert len(_children_named(otel_spans, "db.query.execute", spans[0].context)) == 1 - - -@pytest.mark.asyncio -async def test_execute_isolated_fn_span_on_mutable_and_immutable(tmp_path, otel_spans): - def read_one(conn): - return conn.execute("select 1").fetchone()[0] - - mutable = Datasette(memory=True).add_memory_database("t16_isolated_mutable") - otel_spans.clear() - assert await mutable.execute_isolated_fn(read_one) == 1 - spans = _spans_for_namespace(otel_spans, "t16_isolated_mutable") - assert len(spans) == 1 - assert spans[0].attributes["datasette.callback"].endswith("read_one") - # Mutable databases route through the write thread, so the write spans - # appear as children; immutable ones run on the pool and get none. - assert _children_named(otel_spans, "db.write.execute", spans[0].context) - - db_path = tmp_path / "t16_isolated_immutable.db" - sqlite_utils.Database(str(db_path))["t"].insert({"id": 1}) - ds = Datasette() - immutable = Database(ds, path=str(db_path), is_mutable=False) - ds.add_database(immutable, name="t16_isolated_immutable") - try: - otel_spans.clear() - assert await immutable.execute_isolated_fn(read_one) == 1 - finally: - ds.remove_database("t16_isolated_immutable") - spans = _spans_for_namespace(otel_spans, "t16_isolated_immutable") - assert len(spans) == 1 - assert spans[0].attributes["datasette.callback"].endswith("read_one") - assert not _children_named(otel_spans, "db.write.execute", spans[0].context) - - -@pytest.mark.asyncio -async def test_execute_fn_exception_marks_span_error(otel_spans): - # execute_fn() has no log_sql_errors option, so exceptions are span errors - db = Datasette(memory=True).add_memory_database("t16_fn_error") - - def boom(conn): - raise ValueError("callback failed") - - otel_spans.clear() - with pytest.raises(ValueError): - await db.execute_fn(boom) - spans = _spans_for_namespace(otel_spans, "t16_fn_error") - assert len(spans) == 1 - assert spans[0].status.status_code == StatusCode.ERROR - assert any(event.name == "exception" for event in spans[0].events) diff --git a/tests/test_telemetry_metrics.py b/tests/test_telemetry_metrics.py deleted file mode 100644 index 2e465b82..00000000 --- a/tests/test_telemetry_metrics.py +++ /dev/null @@ -1,483 +0,0 @@ -""" -Tests for the OpenTelemetry metrics emitted by Datasette. Gauge callbacks are -called directly, since the pool gauges have no attributes to tell instances apart. -""" - -import asyncio -import threading -import weakref - -import pytest - -from datasette import telemetry -from datasette.app import Datasette -from datasette.database import Database -from datasette.utils.sqlite import sqlite3 - -pytestmark = pytest.mark.filterwarnings("ignore::ResourceWarning") - - -def observations(callback, datasette=None): - """ - Run a gauge callback, optionally keeping only observations produced by one - Datasette's databases. Returns a list of (attributes dict, value). - """ - results = [] - names = None - if datasette is not None: - names = {db.name for db in telemetry._databases_of(datasette)} - for observation in callback(): - attributes = dict(observation.attributes or {}) - namespace = attributes.get("db.namespace") - if names is not None and namespace is not None and namespace not in names: - continue - results.append((attributes, observation.value)) - return results - - -@pytest.fixture -def metrics_ds(): - "A Datasette with a distinctive thread count and a uniquely named database." - ds = Datasette( - memory=True, - settings={"num_sql_threads": 7}, - ) - ds.add_memory_database("metrics_test_db") - try: - yield ds - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_sql_thread_limit_gauge_reports_num_sql_threads(metrics_ds): - values = [value for _, value in observations(telemetry.observe_sql_thread_limit)] - # Other Datasette instances may also be reporting: - assert 7 in values - - -@pytest.mark.asyncio -async def test_no_thread_gauges_in_non_threaded_mode(): - "Pool gauges skip instances with num_sql_threads=0, which have no pool." - ds = Datasette(memory=True, settings={"num_sql_threads": 0}) - try: - assert ds.executor is None - # Pool gauges have no attributes, so observe only this instance: - original = telemetry._live_datasettes - telemetry._live_datasettes = weakref.WeakSet([ds]) - try: - assert list(telemetry.observe_sql_thread_limit()) == [] - assert list(telemetry.observe_sql_thread_queue_depth()) == [] - finally: - telemetry._live_datasettes = original - # Per-database gauges do not depend on the pool: - assert observations(telemetry.observe_pending_queries, ds) - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_thread_queue_depth_gauge_reports_saturation(): - """ - Queue depth is above zero when reads queue behind num_sql_threads. Also - fails if the private ThreadPoolExecutor._work_queue attribute goes away. - """ - ds = Datasette(memory=True, settings={"num_sql_threads": 1}) - db = ds.add_memory_database("metrics_saturation_db") - entered = threading.Event() - release = threading.Event() - - def blocker(conn): - entered.set() - assert release.wait(timeout=10) - return 1 - - try: - first = asyncio.ensure_future(db.execute_fn(blocker)) - # Wait until the blocker is using the only thread: - await asyncio.get_running_loop().run_in_executor(None, entered.wait, 10) - second = asyncio.ensure_future(db.execute_fn(lambda conn: 2)) - # The second query is queued on a later event loop turn, so poll: - depths = [] - for _ in range(500): - depths = [ - value - for _, value in observations(telemetry.observe_sql_thread_queue_depth) - ] - if any(value >= 1 for value in depths): - break - await asyncio.sleep(0.01) - assert any(value >= 1 for value in depths), depths - release.set() - assert await first == 1 - assert await second == 2 - finally: - release.set() - ds.close() - - -@pytest.mark.asyncio -async def test_pending_queries_gauge_tracks_in_flight_queries(metrics_ds): - db = metrics_ds.get_database("metrics_test_db") - attributes = {"db.namespace": "metrics_test_db"} - - def value(): - points = [ - v - for a, v in observations(telemetry.observe_pending_queries, metrics_ds) - if a == attributes - ] - assert len(points) == 1 - return points[0] - - assert value() == 0 - - # Hold the worker thread until release is set: - release = asyncio.Event() - loop = asyncio.get_running_loop() - entered = asyncio.Event() - - def blocking_fn(conn): - loop.call_soon_threadsafe(entered.set) - asyncio.run_coroutine_threadsafe(release.wait(), loop).result() - return "done" - - task = asyncio.ensure_future(db.execute_fn(blocking_fn)) - await entered.wait() - assert value() == 1, "a query occupying a pool thread must be counted as pending" - release.set() - assert await task == "done" - assert value() == 0, "the count must drop once the query completes" - - -@pytest.mark.asyncio -async def test_write_queue_depth_gauge(metrics_ds): - db = metrics_ds.get_database("metrics_test_db") - attributes = {"db.namespace": "metrics_test_db"} - - def depths(): - return [ - v - for a, v in observations(telemetry.observe_write_queue_depth, metrics_ds) - if a == attributes - ] - - # No observation until the write queue has been created: - assert depths() == [] - - await db.execute_write("create table t (id integer primary key)") - assert depths() == [0], "an idle write queue reports zero, not nothing" - - -@pytest.mark.asyncio -async def test_open_connections_gauge(metrics_ds, tmp_path): - path = str(tmp_path / "conns.db") - sqlite3.connect(path).execute("create table t (id integer primary key)") - db = metrics_ds.add_database(Database(metrics_ds, path=path), name="conns_db") - attributes = {"db.namespace": "conns_db"} - - def open_connections(): - points = [ - v - for a, v in observations(telemetry.observe_open_connections, metrics_ds) - if a == attributes - ] - assert len(points) == 1 - return points[0] - - assert open_connections() == 0 - await db.execute("select 1") - assert open_connections() >= 1, "executing a query opens a tracked connection" - - -@pytest.mark.asyncio -async def test_operation_duration_histogram_read(otel_metrics): - ds = Datasette(memory=True) - ds.add_memory_database("duration_read_db") - try: - db = ds.get_database("duration_read_db") - await db.execute("select 1") - otel_metrics.collect() - point = otel_metrics.point( - "db.client.operation.duration", - {"db.namespace": "duration_read_db", "datasette.operation": "read"}, - ) - assert point.count == 1 - assert point.sum > 0 - assert dict(point.attributes)["db.system"] == "sqlite" - assert "error.type" not in dict(point.attributes) - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_operation_duration_histogram_write(otel_metrics): - ds = Datasette(memory=True) - ds.add_memory_database("duration_write_db") - try: - db = ds.get_database("duration_write_db") - await db.execute_write("create table t (id integer primary key)") - otel_metrics.collect() - point = otel_metrics.point( - "db.client.operation.duration", - {"db.namespace": "duration_write_db", "datasette.operation": "write"}, - ) - assert point.count == 1 - assert point.sum > 0 - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_operation_duration_records_error_type(otel_metrics): - "A failed query is still timed, and is separable from a successful one." - ds = Datasette(memory=True) - ds.add_memory_database("duration_error_db") - try: - db = ds.get_database("duration_error_db") - with pytest.raises(sqlite3.OperationalError): - await db.execute("select * from nope") - otel_metrics.collect() - point = otel_metrics.point( - "db.client.operation.duration", - {"db.namespace": "duration_error_db", "datasette.operation": "read"}, - ) - assert point.count == 1 - assert dict(point.attributes)["error.type"] == "OperationalError" - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_operation_duration_records_write_error_type(otel_metrics): - "A failed write is still timed and records error.type." - ds = Datasette(memory=True) - ds.add_memory_database("duration_write_error_db") - try: - db = ds.get_database("duration_write_error_db") - with pytest.raises(sqlite3.OperationalError): - await db.execute_write("insert into nope values (1)") - otel_metrics.collect() - point = otel_metrics.point( - "db.client.operation.duration", - {"db.namespace": "duration_write_error_db", "datasette.operation": "write"}, - ) - assert point.count == 1 - assert dict(point.attributes)["error.type"] == "OperationalError" - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_write_queue_wait_histogram(otel_metrics): - ds = Datasette(memory=True) - ds.add_memory_database("queue_wait_db") - try: - db = ds.get_database("queue_wait_db") - await db.execute_write("create table t (id integer primary key)") - await db.execute_write("insert into t (id) values (1)") - otel_metrics.collect() - point = otel_metrics.point( - "datasette.write.queue_wait", {"db.namespace": "queue_wait_db"} - ) - assert point.count == 2, "one measurement per write dequeued" - assert point.sum >= 0 - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_interrupted_queries_counter(otel_metrics): - "Queries cancelled by sql_time_limit_ms are counted." - ds = Datasette(memory=True, settings={"sql_time_limit_ms": 1}) - ds.add_memory_database("interrupted_db") - try: - db = ds.get_database("interrupted_db") - from datasette.database import QueryInterrupted - - with pytest.raises(QueryInterrupted): - await db.execute(""" - with recursive counter(x) as ( - select 0 union all select x + 1 from counter - ) - select * from counter - """) - otel_metrics.collect() - point = otel_metrics.point( - "datasette.sql.queries.interrupted", {"db.namespace": "interrupted_db"} - ) - assert point.value == 1 - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_metrics_are_reported_through_the_sdk_for_gauges(otel_metrics): - "Gauge callbacks reach the metric reader as data points." - ds = Datasette(memory=True) - ds.add_memory_database("gauge_pipeline_db") - try: - await ds.get_database("gauge_pipeline_db").execute("select 1") - otel_metrics.collect() - point = otel_metrics.point( - "datasette.sql.queries.pending", {"db.namespace": "gauge_pipeline_db"} - ) - assert point.value == 0 - assert otel_metrics.points("datasette.sql.threads.limit") - finally: - ds.close() - - -def test_closed_datasette_stops_being_observed(): - ds = Datasette(memory=True) - ds.add_memory_database("closed_db") - assert observations(telemetry.observe_pending_queries, ds) - ds.close() - names = [ - attributes.get("db.namespace") - for attributes, _ in observations(telemetry.observe_pending_queries) - ] - assert "closed_db" not in names - - -def test_registry_holds_instances_weakly(): - """ - Registering an instance does not keep it alive. Uses a stand-in object - because an atexit handler in Database.__init__ keeps a real Datasette alive. - """ - import gc - import weakref - - class FakeDatasette: - pass - - fake = FakeDatasette() - telemetry.register_datasette(fake) - assert fake in telemetry._live_instances() - ref = weakref.ref(fake) - del fake - gc.collect() - assert ref() is None - assert not any(isinstance(ds, FakeDatasette) for ds in telemetry._live_instances()) - - -HISTOGRAM_PROBES = [ - # (instrument attribute on telemetry, metric name, isolating attributes) - ( - "sql_operation_duration", - "db.client.operation.duration", - {"db.namespace": "bucket_probe_operation"}, - ), - ( - "write_queue_wait", - "datasette.write.queue_wait", - {"db.namespace": "bucket_probe_queue_wait"}, - ), -] - -# One value in each of six registry buckets. The SDK's default boundaries -# would put the first five in the same bucket. -SPREAD = [0.00005, 0.0003, 0.002, 0.03, 0.8, 7.0] - - -@pytest.mark.parametrize( - "instrument_name,metric_name,attributes", - HISTOGRAM_PROBES, - ids=[metric for _, metric, _ in HISTOGRAM_PROBES], -) -def test_histograms_spread_values_across_buckets( - otel_metrics, instrument_name, metric_name, attributes -): - """ - The registry's bucket boundaries reach the SDK. Values are recorded - directly because real test query durations would all share one bucket. - """ - from datasette.telemetry_registry import METRICS - - metric = next(m for m in METRICS if m == metric_name) - instrument = getattr(telemetry, instrument_name) - for value in SPREAD: - instrument.record(value, attributes) - - otel_metrics.collect() - point = otel_metrics.point(metric_name, attributes) - - assert ( - tuple(point.explicit_bounds) == metric.buckets - ), "the registry's boundaries did not reach the SDK" - assert point.count == len(SPREAD) - occupied = [count for count in point.bucket_counts if count] - assert len(occupied) == len(SPREAD), ( - f"expected each of {SPREAD} in its own bucket, got bucket counts " - f"{list(point.bucket_counts)} for bounds {list(point.explicit_bounds)}" - ) - - -@pytest.mark.asyncio -async def test_operation_duration_histogram_records_execute_fn(otel_metrics): - "execute_fn() reads are recorded in the same histogram as SQL reads." - ds = Datasette(memory=True) - ds.add_memory_database("duration_fn_db") - try: - db = ds.get_database("duration_fn_db") - - def read_one(conn): - return conn.execute("select 1").fetchone()[0] - - assert await db.execute_fn(read_one) == 1 - otel_metrics.collect() - point = otel_metrics.point( - "db.client.operation.duration", - {"db.namespace": "duration_fn_db", "datasette.operation": "read"}, - ) - assert point.count == 1 - assert point.sum > 0 - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_operation_duration_histogram_records_execute_write_fn(otel_metrics): - "execute_write_fn() writes are recorded in the same histogram." - ds = Datasette(memory=True) - ds.add_memory_database("duration_write_fn_db") - try: - db = ds.get_database("duration_write_fn_db") - - def create_table(conn): - conn.execute("create table t (id integer primary key)") - - await db.execute_write_fn(create_table) - otel_metrics.collect() - point = otel_metrics.point( - "db.client.operation.duration", - {"db.namespace": "duration_write_fn_db", "datasette.operation": "write"}, - ) - assert point.count == 1 - assert point.sum > 0 - finally: - ds.close() - - -@pytest.mark.asyncio -async def test_operation_duration_records_callback_error_type(otel_metrics): - "A callback that raises is still timed, with error.type from the exception." - ds = Datasette(memory=True) - ds.add_memory_database("duration_fn_error_db") - try: - db = ds.get_database("duration_fn_error_db") - - def boom(conn): - raise ValueError("callback failed") - - with pytest.raises(ValueError): - await db.execute_fn(boom) - otel_metrics.collect() - point = otel_metrics.point( - "db.client.operation.duration", - {"db.namespace": "duration_fn_error_db", "datasette.operation": "read"}, - ) - assert point.count == 1 - assert dict(point.attributes)["error.type"] == "ValueError" - finally: - ds.close() diff --git a/tests/test_telemetry_registry.py b/tests/test_telemetry_registry.py deleted file mode 100644 index 11a66a13..00000000 --- a/tests/test_telemetry_registry.py +++ /dev/null @@ -1,502 +0,0 @@ -""" -Tests that the spans, attributes and metrics Datasette emits match -datasette/telemetry_registry.py, in both directions. -""" - -import copy -import io -import itertools -import pickle - -import pytest -import pytest_asyncio - -pytest.importorskip("opentelemetry.sdk") - -from opentelemetry.trace import SpanKind - -from datasette import hookimpl -from datasette import telemetry_registry as reg -from datasette.app import Datasette -from datasette.database import QueryInterrupted -from datasette.telemetry_testing import assert_metrics_conform, assert_metrics_covered -from datasette.utils.sqlite import sqlite3 - -# Written out as literals rather than read from the registry, so renaming a -# signal fails these tests. -EXPECTED_ATTRIBUTES = { - "db.query": { - "db.system", - "db.namespace", - "db.query.text", - "datasette.callback", - "db.operation.name", - "datasette.param_count", - "datasette.param_sets", - "datasette.time_limit_ms", - "datasette.rows_returned", - "datasette.truncated", - "datasette.interrupted", - "datasette.sql_error_suppressed", - "datasette.executescript", - "datasette.executemany", - }, - "db.query.execute": set(), - "db.write.queue_wait": set(), - "db.write.execute": { - "datasette.isolated_connection", - "datasette.transaction", - }, - "datasette.startup": set(), -} -EXPECTED_SPANS = set(EXPECTED_ATTRIBUTES) - -# The HTTP request span name is composed at runtime as "{method} {route}", so -# it is checked by shape rather than as a literal. The workload only issues GETs. -EXPECTED_HTTP_SPAN_NAME = "{http.request.method} {http.route}" -EXPECTED_HTTP_METHOD_NAMES = {"GET"} -EXPECTED_HTTP_ATTRIBUTES = { - "http.request.method", - "http.route", - "url.path", - "url.scheme", - "server.address", - "user_agent.original", - "http.response.status_code", - "error.type", - "datasette.internal_client", -} - -# The registry uses the name template for the request span. -EXPECTED_REGISTRY_ATTRIBUTES = dict( - EXPECTED_ATTRIBUTES, **{EXPECTED_HTTP_SPAN_NAME: EXPECTED_HTTP_ATTRIBUTES} -) -EXPECTED_REGISTRY_NAMES = set(EXPECTED_REGISTRY_ATTRIBUTES) - -# Named in-memory databases are shared between instances, so each workload -# uses a unique name. -_names = itertools.count() - - -def _unique(prefix): - return f"{prefix}{next(_names)}" - - -class _BoomPlugin: - "A route that raises, producing a 500 and error.type on the request span." - - __name__ = "TelemetryRegistryBoomPlugin" - - @hookimpl - def register_routes(self): - return [(r"^/-/telemetry-registry-boom$", lambda: 1 / 0)] - - -async def exercise(): - """ - Drive enough of Datasette to emit every registered span and attribute, - including datasette.startup. Returns the instance so the caller can close it. - """ - name = _unique("registry") - ds = Datasette(memory=True) - ds.add_memory_database(name) - # datasette.startup - await ds.invoke_startup() - db = ds.get_database(name) - - # Writes: db.write.queue_wait, db.write.execute, db.query - await db.execute_write("create table t (id integer primary key, v text)") - # datasette.executemany, datasette.param_sets - await db.execute_write_many( - "insert into t (id, v) values (?, ?)", [[i, f"v{i}"] for i in range(30)] - ) - # datasette.executescript - await db.execute_write_script("create table t2 (id integer); drop table t2;") - # datasette.transaction=False - VACUUM cannot run inside a transaction - await db.execute_write("vacuum", transaction=False) - # datasette.isolated_connection=True - await db.execute_isolated_fn(lambda conn: conn.execute("select 1").fetchone()) - - # datasette.callback, using named functions rather than lambdas - def registry_read_callback(conn): - return conn.execute("select count(*) from t").fetchone() - - def registry_write_callback(conn): - conn.execute("insert into t (id, v) values (100, 'callback')") - - await db.execute_fn(registry_read_callback) - await db.execute_write_fn(registry_write_callback) - - # Reads: db.query.execute, datasette.rows_returned, datasette.truncated, - # datasette.param_count, datasette.time_limit_ms - await db.execute("select * from t where id > :n", {"n": 5}) - await db.execute("select * from t", truncate=True) - - # datasette.sql_error_suppressed - with pytest.raises(sqlite3.OperationalError): - await db.execute("select nope from t", log_sql_errors=False) - - # datasette.interrupted: an unbounded recursive CTE always exceeds 1ms - with pytest.raises(QueryInterrupted): - await db.execute( - "with recursive c(x) as (select 0 union all select x+1 from c) " - "select * from c", - custom_time_limit=1, - ) - - # HTTP request spans and their attributes - assert (await ds.client.get(f"/{name}/t?_facet=v")).status_code == 200 - assert (await ds.client.get(f"/{name}/t/1.json")).status_code == 200 - - # error.type on the request span, set by a 5xx response - ds.pm.register(_BoomPlugin(), name="telemetry-registry-boom") - try: - response = await ds.client.get("/-/telemetry-registry-boom") - assert response.status_code == 500 - finally: - ds.pm.unregister(name="telemetry-registry-boom") - return ds - - -@pytest_asyncio.fixture -async def emitted(otel_spans): - """ - Every (span name, span kind, attributes) triple emitted by exercise(). - The kind is needed to resolve the dynamically named request span. - """ - ds = await exercise() - spans = otel_spans.get_finished_spans() - assert spans, "no spans captured - the fixture is not exercising anything" - # str() so failure messages show plain strings, not registry instances - collected = tuple( - ( - str(span.name), - span.kind, - {str(key): value for key, value in (span.attributes or {}).items()}, - ) - for span in spans - ) - ds.close() - return collected - - -def _partition(emitted): - "The statically named spans, and the dynamically named request spans." - static = [record for record in emitted if record[1] is not SpanKind.SERVER] - server = [record for record in emitted if record[1] is SpanKind.SERVER] - return static, server - - -def _keys_by_span(records): - by_span = {} - for name, _kind, attributes in records: - by_span.setdefault(name, set()).update(attributes) - return by_span - - -@pytest.mark.asyncio -async def test_workload_emits_exactly_the_expected_names(emitted): - "Emitted span and attribute names match the expected literals." - static, server = _partition(emitted) - by_span = _keys_by_span(static) - assert set(by_span) == EXPECTED_SPANS - assert by_span == EXPECTED_ATTRIBUTES - - assert server, "the workload made HTTP requests but no SERVER span was emitted" - union = set() - methods = set() - for name, _kind, attributes in server: - union |= set(attributes) - route = attributes.get("http.route") - # Every request in the workload matches a route - assert route, f"the request span {name!r} carries no http.route" - method, _, name_route = name.partition(" ") - assert name_route == route, ( - f"the request span is named {name!r}, which is not the " - f"`{{method}} {{route}}` of {method!r} and {route!r}" - ) - methods.add(method) - assert methods == EXPECTED_HTTP_METHOD_NAMES - assert union == EXPECTED_HTTP_ATTRIBUTES - - -def test_registry_matches_the_expected_names(): - "Registry names match the expected literals." - assert {str(span) for span in reg.SPANS} == EXPECTED_REGISTRY_NAMES - for span in reg.SPANS: - assert { - str(attribute) for attribute in span.attributes - } == EXPECTED_REGISTRY_ATTRIBUTES[str(span)], f"{span} attributes have drifted" - - -@pytest.mark.asyncio -async def test_every_emitted_span_is_registered(emitted): - "A span added without a registry entry would be missing from the docs." - unregistered = sorted( - {name for name, kind, _ in emitted if reg.span_for(name, kind) is None} - ) - assert ( - not unregistered - ), f"these spans are emitted but not in telemetry_registry.SPANS: {unregistered}" - - -@pytest.mark.asyncio -async def test_every_emitted_attribute_is_registered(emitted): - "An attribute added without a registry entry would be missing from the docs." - unregistered = sorted( - { - f"{name} -> {key}" - for name, kind, keys in emitted - for key in keys - if not reg.attribute_allowed(reg.span_for(name, kind), key) - } - ) - assert ( - not unregistered - ), "these span attributes are emitted but not registered: " + ", ".join( - unregistered - ) - - -@pytest.mark.asyncio -async def test_every_registered_span_is_emitted(emitted): - "The docs should not describe a span that is no longer emitted." - # Compare by identity: the request span's registry name never appears on - # the wire. - resolved = {id(reg.span_for(name, kind)) for name, kind, _ in emitted} - missing = sorted(str(span) for span in reg.SPANS if id(span) not in resolved) - assert not missing, ( - f"these spans are documented but never emitted by the workload: {missing}. " - "Either the instrumentation was removed, or exercise() no longer reaches it." - ) - - -@pytest.mark.asyncio -async def test_every_registered_attribute_is_emitted(emitted): - """ - Every registered attribute, including optional ones, is emitted at least - once. If a new attribute only appears in rare cases, extend exercise(). - """ - by_entry = {} - for name, kind, keys in emitted: - entry = reg.span_for(name, kind) - if entry is not None: - by_entry.setdefault(id(entry), set()).update(keys) - missing = [] - for span in reg.SPANS: - emitted_keys = by_entry.get(id(span), set()) - for attribute in span.attributes: - if attribute not in emitted_keys: - missing.append(f"{span} -> {attribute}") - assert not missing, ( - "these attributes are documented but never emitted by the workload: " - + ", ".join(sorted(missing)) - ) - - -def test_registry_has_no_duplicate_names(): - assert len(set(reg.SPANS)) == len(reg.SPANS) - for span in reg.SPANS: - assert len(set(span.attributes)) == len( - span.attributes - ), f"{span} lists an attribute twice" - - -def test_registry_entries_are_documented(): - "Every entry has a description, used to generate the docs." - for span in reg.SPANS: - assert span.description.strip(), f"{span} has no description" - for attribute in span.attributes: - assert attribute.description.strip(), f"{span} -> {attribute} has none" - - -def test_registry_entries_are_usable_as_plain_strings(): - assert isinstance(reg.DB_QUERY, str) - assert isinstance(reg.DB_NAMESPACE, str) - assert reg.DB_QUERY == "db.query" - assert reg.DB_NAMESPACE == "db.namespace" - assert f"{reg.DB_QUERY}.execute" == "db.query.execute" - - -def test_registry_entries_survive_deepcopy_and_pickle(): - """ - A copied or unpickled entry is a plain str. ConsoleMetricExporter - deepcopies metric attributes, which use registry entries as keys. - """ - for entry in (reg.DB_NAMESPACE, reg.DB_QUERY, reg.M_OPERATION_DURATION): - assert copy.deepcopy({entry: 1}) == {str(entry): 1} - assert type(copy.deepcopy(entry)) is str - assert pickle.loads(pickle.dumps(entry)) == str(entry) - # The original entry keeps its metadata - assert entry.description.strip() - - -@pytest.mark.asyncio -async def test_console_metric_exporter_renders_core_metric_points(otel_metrics): - from opentelemetry.sdk.metrics.export import ( - ConsoleMetricExporter, - MetricExportResult, - ) - - name = _unique("registry_console_export") - ds = Datasette(memory=True) - ds.add_memory_database(name) - await ds.invoke_startup() - # Produces a db.client.operation.duration point keyed by DB_NAMESPACE - await ds.get_database(name).execute("select 1") - - data = otel_metrics.reader.get_metrics_data() - assert data is not None, "no metrics captured - nothing to export" - exporter = ConsoleMetricExporter(out=io.StringIO()) - assert exporter.export(data) is MetricExportResult.SUCCESS - ds.close() - - -def test_every_histogram_declares_bucket_boundaries(): - """ - Every histogram declares bucket boundaries, and only histograms do. - OpenTelemetry's defaults are meant for milliseconds, not seconds. - """ - for metric in reg.METRICS: - if metric.kind == reg.HISTOGRAM: - assert metric.buckets, f"{metric} is a histogram with no boundaries" - assert list(metric.buckets) == sorted( - set(metric.buckets) - ), f"{metric} boundaries must be ascending and unique" - assert metric.buckets[0] > 0, f"{metric} has a non-positive boundary" - else: - assert ( - metric.buckets is None - ), f"{metric} is a {metric.kind} and cannot have bucket boundaries" - - -def test_dynamic_span_lookup(): - """ - dynamic=True entries such as the request span match on kind. They never - match without a kind, and never override a registered name. - """ - assert reg.span_for("GET", SpanKind.SERVER) is reg.HTTP_REQUEST - assert reg.span_for("POST /^/(?P[^/]+)$", SpanKind.SERVER) is ( - reg.HTTP_REQUEST - ) - assert reg.span_for("GET") is None - assert reg.span_for("anything at all", SpanKind.INTERNAL) is None - assert reg.span_for("db.query", SpanKind.SERVER) is reg.DB_QUERY - - -def test_span_and_attribute_lookup(): - assert reg.span_for("db.query") is reg.DB_QUERY - assert reg.span_for("datasette.startup") is reg.STARTUP - assert reg.span_for("not.a.datasette.span") is None - assert reg.attribute_allowed(reg.DB_QUERY, "db.namespace") - assert not reg.attribute_allowed(reg.DB_QUERY, "db.namespace.extra") - assert not reg.attribute_allowed(reg.DB_QUERY, "datasette.isolated_connection") - assert not reg.attribute_allowed(None, "db.namespace") - - -# --- Metric conformance ---------------------------------------------------- - - -@pytest_asyncio.fixture -async def emitted_metrics(otel_metrics): - """ - Metric names and (metric name, attribute key) pairs from a broad workload. - Checks use attribute keys rather than values, since other Datasette - instances in the session can also report points. - """ - # Reaches every synchronous metric except datasette.sql.queries.interrupted - ds = await exercise() - - # datasette.sql.queries.interrupted ignores custom_time_limit timeouts, so - # this needs an instance with a low sql_time_limit_ms. - slow_name = _unique("registry_metrics_slow") - slow = Datasette(memory=True, settings={"sql_time_limit_ms": 5}) - slow.add_memory_database(slow_name) - await slow.invoke_startup() - slow_db = slow.get_database(slow_name) - with pytest.raises(QueryInterrupted): - await slow_db.execute( - "with recursive c(x) as (select 0 union all select x+1 from c) " - "select * from c" - ) - - # Collect before closing the instances so the observable gauges report them - otel_metrics.collect() - snapshot = otel_metrics.snapshot - assert snapshot, "no metrics captured - the fixture is not exercising anything" - pairs = set() - for metric_name, points in snapshot.items(): - for point in points: - for key in point.attributes or {}: - pairs.add((metric_name, key)) - ds.close() - slow.close() - return {"names": set(snapshot), "pairs": pairs, "collector": otel_metrics} - - -@pytest.mark.asyncio -async def test_metrics_conform_to_the_registry(emitted_metrics): - """ - Emitted metric names, kinds, units, attribute keys and enum values match - the registry, using the plugin testing helper. - """ - assert_metrics_conform( - reg.METRICS, emitted_metrics["collector"], scope_name="datasette" - ) - - -@pytest.mark.asyncio -async def test_every_registered_metric_is_emitted(emitted_metrics): - assert_metrics_covered( - reg.METRICS, emitted_metrics["collector"], scope_name="datasette" - ) - - -@pytest.mark.asyncio -async def test_every_registered_metric_attribute_is_emitted(emitted_metrics): - "Every registered metric attribute, including optional ones, is emitted." - emitted_keys_by_metric = {} - for metric_name, key in emitted_metrics["pairs"]: - emitted_keys_by_metric.setdefault(metric_name, set()).add(key) - - missing = [] - for metric in reg.METRICS: - if str(metric) not in emitted_metrics["names"]: - # Reported by test_every_registered_metric_is_emitted - continue - emitted_keys = emitted_keys_by_metric.get(str(metric), set()) - for attribute in metric.attributes: - if attribute not in emitted_keys: - missing.append(f"{metric} -> {attribute}") - assert not missing, ( - "these metric attributes are documented but never emitted by the " - "test workload: " + ", ".join(sorted(missing)) - ) - - -def test_prefix_span_lookup(): - "prefix=True matching, which core does not use but plugin registries can." - hook = reg.SpanName("myplugin.hook.", "A hypothetical span family", prefix=True) - spans = reg.SPANS + (hook,) - assert reg.span_for("myplugin.hook.render_cell", spans=spans) is hook - assert reg.span_for("myplugin.hook.anything", spans=spans) is hook - assert reg.span_for("myplugin.hookish", spans=spans) is None - assert reg.span_for("db.query", spans=spans) is reg.DB_QUERY - - -def test_exact_match_wins_over_prefix(): - family = reg.SpanName("db.", "Greedy prefix", prefix=True) - spans = (family,) + reg.SPANS - assert reg.span_for("db.query", spans=spans) is reg.DB_QUERY - assert reg.span_for("db.anything-else", spans=spans) is family - - -def test_attribute_values_enum_enforced(): - outcome = reg.Attribute("myplugin.outcome", "Enum.", values={"ok", "error"}) - open_attr = reg.Attribute("myplugin.note", "Open value set.") - span = reg.SpanName("myplugin.job", "Test span", (outcome, open_attr)) - assert reg.attribute_value_allowed(span, "myplugin.outcome", "ok") - assert not reg.attribute_value_allowed(span, "myplugin.outcome", "surprise") - assert reg.attribute_value_allowed(span, "myplugin.note", "anything at all") - assert not reg.attribute_value_allowed(span, "not.registered", "x") - assert not reg.attribute_value_allowed(None, "myplugin.outcome", "ok") diff --git a/tests/test_telemetry_testing_kit.py b/tests/test_telemetry_testing_kit.py deleted file mode 100644 index ddf3b3f6..00000000 --- a/tests/test_telemetry_testing_kit.py +++ /dev/null @@ -1,320 +0,0 @@ -""" -Tests for datasette.telemetry_testing and the public registry classes, using -a toy plugin registry and instrumentation scope. -""" - -import pytest - -pytest.importorskip("opentelemetry.sdk") - -from opentelemetry import trace as otel_trace - -from datasette import telemetry_registry as reg -from datasette.telemetry import linked_root_span_kwargs -from datasette.telemetry_testing import ( - assert_package_never_imports_sdk, - assert_spans_conform, - assert_spans_covered, -) - -SCOPE = "toyplugin" - -OUTCOME = reg.Attribute( - "toyplugin.outcome", "How the job ended.", values={"ok", "error"} -) -JOB_NAME = reg.Attribute("toyplugin.job", "The job's registered name.") -JOB = reg.SpanName("toyplugin.job.run", "One job execution.", (OUTCOME, JOB_NAME)) -CHAT = reg.SpanName( - "toyplugin.chat ", "One model call, named `toyplugin.chat {model}`.", prefix=True -) -TOY_SPANS = (JOB, CHAT) - -toy_tracer = otel_trace.get_tracer(SCOPE, "0.1") - - -def _toy_spans(otel_spans): - return [ - span - for span in otel_spans.get_finished_spans() - if span.instrumentation_scope and span.instrumentation_scope.name == SCOPE - ] - - -def _run_workload(): - with toy_tracer.start_as_current_span(JOB) as span: - span.set_attribute(OUTCOME, "ok") - span.set_attribute(JOB_NAME, "nightly") - with toy_tracer.start_as_current_span("toyplugin.chat gpt-5"): - pass - - -def test_conformance_passes_for_a_conforming_workload(otel_spans): - _run_workload() - finished = otel_spans.get_finished_spans() - assert_spans_conform(TOY_SPANS, finished, scope_name=SCOPE) - # The chat span matches the CHAT prefix entry: - assert_spans_covered(TOY_SPANS, finished, scope_name=SCOPE) - - -def test_conformance_catches_an_unregistered_span(otel_spans): - with toy_tracer.start_as_current_span("toyplugin.surprise"): - pass - with pytest.raises(AssertionError, match="unregistered span"): - assert_spans_conform( - TOY_SPANS, otel_spans.get_finished_spans(), scope_name=SCOPE - ) - - -def test_conformance_catches_an_unregistered_attribute(otel_spans): - with toy_tracer.start_as_current_span(JOB) as span: - span.set_attribute("toyplugin.stealth", 1) - with pytest.raises(AssertionError, match="unregistered attribute"): - assert_spans_conform( - TOY_SPANS, otel_spans.get_finished_spans(), scope_name=SCOPE - ) - - -def test_conformance_enforces_declared_enums(otel_spans): - with toy_tracer.start_as_current_span(JOB) as span: - span.set_attribute(OUTCOME, "surprise") - with pytest.raises(AssertionError, match="not in the declared enum"): - assert_spans_conform( - TOY_SPANS, otel_spans.get_finished_spans(), scope_name=SCOPE - ) - - -def test_coverage_catches_a_never_emitted_span(otel_spans): - with toy_tracer.start_as_current_span(JOB) as span: - span.set_attribute(OUTCOME, "ok") - span.set_attribute(JOB_NAME, "nightly") - # CHAT never emitted - with pytest.raises(AssertionError, match="never emitted"): - assert_spans_covered( - TOY_SPANS, otel_spans.get_finished_spans(), scope_name=SCOPE - ) - - -def test_scope_filter_ignores_other_scopes(otel_spans): - # Spans from other scopes, including Datasette's own, are ignored: - other = otel_trace.get_tracer("someone-else", "1.0") - with other.start_as_current_span("not.in.the.toy.registry"): - pass - _run_workload() - assert_spans_conform(TOY_SPANS, otel_spans.get_finished_spans(), scope_name=SCOPE) - - -def test_linked_root_span_kwargs_links_without_parenting(otel_spans): - with toy_tracer.start_as_current_span("toyplugin.cause") as cause: - cause_context = cause.get_span_context() - kwargs = linked_root_span_kwargs() - with toy_tracer.start_as_current_span("toyplugin.effect", **kwargs): - pass - effect = next( - span for span in _toy_spans(otel_spans) if span.name == "toyplugin.effect" - ) - assert effect.parent is None, "must be a root, not a child" - assert effect.context.trace_id != cause_context.trace_id - assert len(effect.links) == 1 - assert effect.links[0].context.span_id == cause_context.span_id - - -def test_linked_root_span_kwargs_with_no_current_span(otel_spans): - kwargs = linked_root_span_kwargs() - assert kwargs["links"] == [] - with toy_tracer.start_as_current_span("toyplugin.orphanless", **kwargs): - pass - span = _toy_spans(otel_spans)[0] - assert span.parent is None - assert span.links == () - - -def test_kit_module_itself_never_imports_the_sdk(): - """ - The kit imports the SDK lazily, so plugins can import it at module level. - - conftest.py runs this test first by name. Update it there if you rename it. - """ - assert_package_never_imports_sdk("datasette.telemetry_testing") - - -# --- Metric conformance helpers -------------------------------------------- - -import itertools - -from opentelemetry import metrics as otel_metrics_api - -from datasette.telemetry_testing import ( - assert_metrics_conform, - assert_metrics_covered, -) - -toy_meter = otel_metrics_api.get_meter(SCOPE, "0.1") - -# Gives each test a unique instrument name: -_metric_ids = itertools.count() - - -def _toy_metric_registry(name, kind="Counter", unit="{job}", attributes=None): - return ( - reg.MetricName( - name, - kind, - unit, - "A toy metric.", - attributes if attributes is not None else (OUTCOME,), - ), - ) - - -def test_metrics_conform_passes_and_covers(otel_metrics): - name = f"toyplugin.jobs.{next(_metric_ids)}" - registry = _toy_metric_registry(name) - counter = toy_meter.create_counter(name, unit="{job}", description="Jobs run") - counter.add(1, {OUTCOME: "ok"}) - otel_metrics.collect() - assert_metrics_conform(registry, otel_metrics, scope_name=SCOPE) - assert_metrics_covered(registry, otel_metrics, scope_name=SCOPE) - - -def test_metrics_conform_catches_unregistered_metric(otel_metrics): - name = f"toyplugin.stealth.{next(_metric_ids)}" - counter = toy_meter.create_counter(name, unit="{job}") - counter.add(1) - otel_metrics.collect() - with pytest.raises(AssertionError, match="unregistered metric"): - assert_metrics_conform((), otel_metrics, scope_name=SCOPE) - - -def test_metrics_conform_catches_kind_mismatch(otel_metrics): - name = f"toyplugin.kindclash.{next(_metric_ids)}" - registry = _toy_metric_registry(name, kind="Histogram", unit="{job}") - counter = toy_meter.create_counter(name, unit="{job}") - counter.add(1, {OUTCOME: "ok"}) - otel_metrics.collect() - with pytest.raises(AssertionError, match="registry declares Histogram"): - assert_metrics_conform(registry, otel_metrics, scope_name=SCOPE) - - -def test_metrics_conform_catches_unit_mismatch(otel_metrics): - name = f"toyplugin.unitclash.{next(_metric_ids)}" - registry = _toy_metric_registry(name, unit="s") - counter = toy_meter.create_counter(name, unit="ms") - counter.add(1, {OUTCOME: "ok"}) - otel_metrics.collect() - with pytest.raises(AssertionError, match="unit"): - assert_metrics_conform(registry, otel_metrics, scope_name=SCOPE) - - -def test_metrics_conform_catches_unregistered_attribute(otel_metrics): - name = f"toyplugin.attrclash.{next(_metric_ids)}" - registry = _toy_metric_registry(name) - counter = toy_meter.create_counter(name, unit="{job}") - counter.add(1, {"toyplugin.stealth": "x"}) - otel_metrics.collect() - with pytest.raises(AssertionError, match="unregistered attribute"): - assert_metrics_conform(registry, otel_metrics, scope_name=SCOPE) - - -def test_metrics_conform_enforces_declared_enums(otel_metrics): - name = f"toyplugin.enumclash.{next(_metric_ids)}" - registry = _toy_metric_registry(name) - counter = toy_meter.create_counter(name, unit="{job}") - counter.add(1, {OUTCOME: "surprise"}) - otel_metrics.collect() - with pytest.raises(AssertionError, match="not in the declared enum"): - assert_metrics_conform(registry, otel_metrics, scope_name=SCOPE) - - -def test_metrics_covered_catches_never_collected(otel_metrics): - registered_but_never_created = _toy_metric_registry( - f"toyplugin.ghost.{next(_metric_ids)}" - ) - otel_metrics.collect() - with pytest.raises(AssertionError, match="never collected"): - assert_metrics_covered( - registered_but_never_created, otel_metrics, scope_name=SCOPE - ) - - -def test_metrics_covered_skips_optional_attributes(otel_metrics): - name = f"toyplugin.optattr.{next(_metric_ids)}" - error_type = reg.Attribute("toyplugin.error", "Only on failure.", optional=True) - registry = _toy_metric_registry(name, attributes=(OUTCOME, error_type)) - counter = toy_meter.create_counter(name, unit="{job}") - counter.add(1, {OUTCOME: "ok"}) # No error attribute - otel_metrics.collect() - assert_metrics_covered(registry, otel_metrics, scope_name=SCOPE) - - -def test_metrics_scope_filter_ignores_other_scopes(otel_metrics): - # Metrics from other scopes, including Datasette's own, are ignored: - name = f"toyplugin.scoped.{next(_metric_ids)}" - registry = _toy_metric_registry(name) - counter = toy_meter.create_counter(name, unit="{job}") - counter.add(1, {OUTCOME: "ok"}) - other_meter = otel_metrics_api.get_meter("someone-else-metrics", "1.0") - stranger = other_meter.create_counter(f"stranger.{next(_metric_ids)}", unit="x") - stranger.add(1) - otel_metrics.collect() - assert_metrics_conform(registry, otel_metrics, scope_name=SCOPE) - - -# --- UpDownCounter kind + privacy walk -------------------------------------- - -from datasette.telemetry_testing import assert_no_forbidden_values - - -def test_updown_counter_kind_passes(otel_metrics): - name = f"toyplugin.active.{next(_metric_ids)}" - registry = _toy_metric_registry(name, kind=reg.UPDOWN_COUNTER, unit="{turn}") - updown = toy_meter.create_up_down_counter(name, unit="{turn}") - updown.add(1, {OUTCOME: "ok"}) - otel_metrics.collect() - assert_metrics_conform(registry, otel_metrics, scope_name=SCOPE) - - -def test_counter_registered_as_updown_fails_on_monotonicity(otel_metrics): - name = f"toyplugin.monoclash.{next(_metric_ids)}" - registry = _toy_metric_registry(name, kind=reg.UPDOWN_COUNTER, unit="{job}") - counter = toy_meter.create_counter(name, unit="{job}") - counter.add(1, {OUTCOME: "ok"}) - otel_metrics.collect() - with pytest.raises(AssertionError, match="is_monotonic"): - assert_metrics_conform(registry, otel_metrics, scope_name=SCOPE) - - -def test_forbidden_values_walk_catches_a_leak(otel_spans, otel_metrics): - secret = "sentinel-token-xyzzy" - with toy_tracer.start_as_current_span(JOB) as span: - span.set_attribute(OUTCOME, "ok") - span.set_attribute(JOB_NAME, f"job for {secret}") - with pytest.raises(AssertionError, match="sentinel-token-xyzzy"): - assert_no_forbidden_values( - {secret}, - finished_spans=otel_spans.get_finished_spans(), - scope_name=SCOPE, - ) - - -def test_forbidden_values_walk_passes_a_clean_workload(otel_spans, otel_metrics): - _run_workload() - name = f"toyplugin.clean.{next(_metric_ids)}" - counter = toy_meter.create_counter(name, unit="{job}") - counter.add(1, {OUTCOME: "ok"}) - otel_metrics.collect() - assert_no_forbidden_values( - {"sentinel-token-xyzzy", "alice@example.com", ""}, - finished_spans=otel_spans.get_finished_spans(), - collector=otel_metrics, - scope_name=SCOPE, - ) - - -def test_forbidden_values_walk_checks_metric_attributes(otel_metrics): - secret = "leaky-metric-value" - name = f"toyplugin.leak.{next(_metric_ids)}" - counter = toy_meter.create_counter(name, unit="{job}") - counter.add(1, {"toyplugin.note": secret}) - otel_metrics.collect() - with pytest.raises(AssertionError, match="leaky-metric-value"): - assert_no_forbidden_values({secret}, collector=otel_metrics, scope_name=SCOPE) diff --git a/tests/test_template_context.py b/tests/test_template_context.py index 691c2d64..7923d0e7 100644 --- a/tests/test_template_context.py +++ b/tests/test_template_context.py @@ -10,7 +10,7 @@ from dataclasses import dataclass, field import pytest -from datasette.app import TEMPLATE_BASE_CONTEXT, Datasette +from datasette.app import Datasette, TEMPLATE_BASE_CONTEXT from datasette.extras import ExtraScope from datasette.fixtures import write_fixture_database from datasette.template_contexts import PAGES, documented_context_keys @@ -40,17 +40,17 @@ def test_documented_fields(): @pytest.mark.parametrize("klass", PAGES.values(), ids=lambda klass: klass.__name__) def test_context_class_fields_all_have_help(klass): for context_field in klass.documented_fields(): - assert ( - context_field.help - ), f"{klass.__name__}.{context_field.name} is missing documentation" + assert context_field.help, "{}.{} is missing documentation".format( + klass.__name__, context_field.name + ) @pytest.mark.parametrize("klass", PAGES.values(), ids=lambda klass: klass.__name__) def test_context_class_has_docstring_and_documented_template(klass): - assert klass.__doc__, f"{klass.__name__} is missing a docstring" - assert ( - klass.documented_template - ), f"{klass.__name__} is missing a documented_template" + assert klass.__doc__, "{} is missing a docstring".format(klass.__name__) + assert klass.documented_template, "{} is missing a documented_template".format( + klass.__name__ + ) def test_from_extra_documentation_comes_from_the_extra_class(): @@ -105,7 +105,7 @@ def isolate_extra_template_vars_plugins(): # for the rest of the process. The contract documents plugin-free # Datasette core, so unregister any non-default plugin that adds # template variables via the extra_template_vars hook - from datasette.plugins import DEFAULT_PLUGINS, pm + from datasette.plugins import pm, DEFAULT_PLUGINS hook_plugins = {impl.plugin for impl in pm.hook.extra_template_vars.get_hookimpls()} removed = [] @@ -182,18 +182,18 @@ async def test_template_context_matches_documented_contract( undocumented = actual - documented no_longer_present = documented - actual assert not undocumented, ( - f"Undocumented keys in {page_name} template context: {sorted(undocumented)} - add them to the " - "page's Context class" + "Undocumented keys in {} template context: {} - add them to the " + "page's Context class".format(page_name, sorted(undocumented)) ) assert not no_longer_present, ( - f"Documented keys missing from {page_name} template context: {sorted(no_longer_present)} - this would " - "break custom templates" + "Documented keys missing from {} template context: {} - this would " + "break custom templates".format(page_name, sorted(no_longer_present)) ) def test_base_context_keys_all_have_docs(): for name, doc in TEMPLATE_BASE_CONTEXT.items(): - assert doc, f"Base context key {name} is missing docs" + assert doc, "Base context key {} is missing docs".format(name) def test_template_context_docs_cover_every_documented_key(): @@ -201,14 +201,15 @@ def test_template_context_docs_cover_every_documented_key(): assert docs_path.exists(), "docs/template_context.rst is missing" docs = docs_path.read_text() for name in TEMPLATE_BASE_CONTEXT: - assert f"``{name}``" in docs, name + assert "``{}``".format(name) in docs, name for page_name, klass in PAGES.items(): title = "{} page".format(klass.__name__.removesuffix("Context")) assert title in docs, title for context_field in klass.documented_fields(): + assert "``{}``".format(context_field.name) in docs, "{} ({} page)".format( + context_field.name, page_name + ) assert ( - f"``{context_field.name}``" in docs - ), f"{context_field.name} ({page_name} page)" - assert ( - f"``{context_field.name}`` - ``{context_field.type_name}``" in docs - ), f"{context_field.name} type ({page_name} page)" + "``{}`` - ``{}``".format(context_field.name, context_field.type_name) + in docs + ), "{} type ({} page)".format(context_field.name, page_name) diff --git a/tests/test_token_handler.py b/tests/test_token_handler.py index 10021ddf..f5bbfead 100644 --- a/tests/test_token_handler.py +++ b/tests/test_token_handler.py @@ -2,17 +2,16 @@ Tests for the register_token_handler plugin hook. """ -import pytest - from datasette.app import Datasette from datasette.hookspecs import hookimpl from datasette.plugins import pm from datasette.tokens import ( - SignedTokenHandler, TokenHandler, TokenInvalid, TokenRestrictions, + SignedTokenHandler, ) +import pytest @pytest.fixture diff --git a/tests/test_tracer.py b/tests/test_tracer.py index 21cfa952..9db211d3 100644 --- a/tests/test_tracer.py +++ b/tests/test_tracer.py @@ -1,5 +1,4 @@ import pytest - from .fixtures import make_app_client @@ -76,9 +75,10 @@ async def test_trace_child_tasks_resets_contextvar_on_exception(): from datasette import tracer before = tracer.trace_task_id.get() - with pytest.raises(ValueError), tracer.trace_child_tasks(): - assert tracer.trace_task_id.get() is not None - raise ValueError("simulated error") + with pytest.raises(ValueError): + with tracer.trace_child_tasks(): + assert tracer.trace_task_id.get() is not None + raise ValueError("simulated error") # The contextvar must be reset even though the block raised assert tracer.trace_task_id.get() == before diff --git a/tests/test_utils.py b/tests/test_utils.py index 68d4a504..a535ca93 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,25 +2,22 @@ Tests for various datasette helper functions. """ -import hashlib -import json -import os -import pathlib -import tempfile -from unittest.mock import patch - -import pytest - -from datasette import utils from datasette.app import Datasette +from datasette import utils from datasette.utils.asgi import Request from datasette.utils.sqlite import ( sqlite3, - sqlite_derived_table_dependencies, sqlite_hidden_table_names, sqlite_table_type, supports_returning, ) +import hashlib +import json +import os +import pathlib +import pytest +import tempfile +from unittest.mock import patch @pytest.mark.parametrize( @@ -197,7 +194,7 @@ def test_validate_sql_select_good(good_sql): @pytest.mark.parametrize("open_quote,close_quote", [('"', '"'), ("[", "]")]) def test_detect_fts(open_quote, close_quote): - sql = f""" + sql = """ CREATE TABLE "Dumb_Table" ( "TreeID" INTEGER, "qSpecies" TEXT @@ -212,9 +209,9 @@ def test_detect_fts(open_quote, close_quote): "qCaretaker" TEXT ); CREATE VIEW Test_View AS SELECT * FROM Dumb_Table; - CREATE VIRTUAL TABLE {open_quote}Street_Tree_List_fts{close_quote} USING FTS4 ("qAddress", "qCaretaker", "qSpecies", content={open_quote}Street_Tree_List{close_quote}); + CREATE VIRTUAL TABLE {open}Street_Tree_List_fts{close} USING FTS4 ("qAddress", "qCaretaker", "qSpecies", content={open}Street_Tree_List{close}); CREATE VIRTUAL TABLE r USING rtree(a, b, c); - """ + """.format(open=open_quote, close=close_quote) conn = utils.sqlite3.connect(":memory:") conn.executescript(sql) assert None is utils.detect_fts(conn, "Dumb_Table") @@ -228,8 +225,6 @@ def test_detect_fts(open_quote, close_quote): "identifier,expected", ( ("plain", "plain"), - ("plain\n", '"plain\n"'), - ("select\n", '"select\n"'), ("select", '"select"'), ("has space", '"has space"'), ("has'quote", '"has\'quote"'), @@ -267,8 +262,8 @@ def test_escape_sqlite_prevents_injection(): conn.execute("CREATE TABLE users (id INTEGER, password TEXT)") conn.execute("INSERT INTO users VALUES (1, 'super_secret_password')") malicious = "users] UNION SELECT password FROM users--" - conn.execute(f'CREATE TABLE "{malicious}" (id INTEGER)') - sql = f"select count(*) from {utils.escape_sqlite(malicious)}" + conn.execute('CREATE TABLE "{}" (id INTEGER)'.format(malicious)) + sql = "select count(*) from {}".format(utils.escape_sqlite(malicious)) results = conn.execute(sql).fetchall() conn.close() # The injected UNION must not execute - only the empty malicious table @@ -278,16 +273,16 @@ def test_escape_sqlite_prevents_injection(): @pytest.mark.parametrize("table", ("regular", "has'single quote")) def test_detect_fts_different_table_names(table): - sql = f""" + sql = """ CREATE TABLE [{table}] ( "TreeID" INTEGER, "qSpecies" TEXT ); CREATE VIRTUAL TABLE [{table}_fts] USING FTS4 ("qSpecies", content="{table}"); - """ + """.format(table=table) conn = utils.sqlite3.connect(":memory:") conn.executescript(sql) - assert f"{table}_fts" == utils.detect_fts(conn, table) + assert "{table}_fts".format(table=table) == utils.detect_fts(conn, table) conn.close() @@ -372,46 +367,6 @@ def test_sqlite_hidden_table_names_hides_multiline_content_fts_table(): conn.close() -def test_sqlite_derived_table_dependencies(): - conn = utils.sqlite3.connect(":memory:") - try: - conn.executescript(""" - create table docs(id integer primary key, body text); - create virtual table external_fts5 using fts5( - body, content='docs', content_rowid='id' - ); - create virtual table internal_fts5 using fts5(body); - create virtual table contentless_fts5 using fts5(body, content=''); - create virtual table external_fts4 using fts4(body, content="docs"); - create virtual table internal_fts4 using fts4(body); - create virtual table contentless_fts4 using fts4(body, content=""); - create table [docs, archive](body text); - create virtual table commented_fts5 using fts5( - body, tokenize='porter unicode61', - /* Comments and commas in quoted values must not confuse parsing. */ - content='docs, archive' - ); - create virtual table boxes using rtree(id, minx, maxx, miny, maxy); - """) - - dependencies = sqlite_derived_table_dependencies(conn) - - assert dependencies["external_fts5"] == "docs" - assert dependencies["external_fts4"] == "docs" - assert dependencies["commented_fts5"] == "docs, archive" - assert "contentless_fts5" not in dependencies - assert "contentless_fts4" not in dependencies - assert dependencies["internal_fts5_content"] == "internal_fts5" - assert dependencies["internal_fts4_content"] == "internal_fts4" - assert dependencies["external_fts5_data"] == "external_fts5" - assert dependencies["external_fts4_segments"] == "external_fts4" - assert dependencies["boxes_node"] == "boxes" - assert dependencies["boxes_parent"] == "boxes" - assert dependencies["boxes_rowid"] == "boxes" - finally: - conn.close() - - @pytest.mark.parametrize( "url,expected", [ @@ -735,6 +690,7 @@ def test_resolve_env_secrets(config, expected): [ ({"id": "blah"}, "blah"), ({"id": "blah", "login": "l"}, "l"), + ({"id": "blah", "login": "l"}, "l"), ({"id": "blah", "login": "l", "username": "u"}, "u"), ({"login": "l", "name": "n"}, "n"), ( diff --git a/tests/test_utils_check_callable.py b/tests/test_utils_check_callable.py index 857b73cd..4f72f9ff 100644 --- a/tests/test_utils_check_callable.py +++ b/tests/test_utils_check_callable.py @@ -1,6 +1,5 @@ -import pytest - from datasette.utils.check_callable import check_callable +import pytest class AsyncClass: diff --git a/tests/test_utils_permissions.py b/tests/test_utils_permissions.py index 918dab95..bc3599c2 100644 --- a/tests/test_utils_permissions.py +++ b/tests/test_utils_permissions.py @@ -1,17 +1,14 @@ -from collections.abc import Callable - import pytest - from datasette.app import Datasette from datasette.permissions import PermissionSQL from datasette.utils.permissions import resolve_permissions_from_catalog +from typing import Callable, List @pytest.fixture def db(): ds = Datasette() import tempfile - from datasette.database import Database path = tempfile.mktemp(suffix="demo.db") @@ -130,7 +127,7 @@ def plugin_root_deny_for_all() -> Callable[[str], PermissionSQL]: def plugin_conflicting_same_child_rules( user: str, parent: str, child: str -) -> list[Callable[[str], PermissionSQL]]: +) -> List[Callable[[str], PermissionSQL]]: def allow_provider(action: str) -> PermissionSQL: return PermissionSQL( """ @@ -280,7 +277,9 @@ async def test_alice_global_allow_with_specific_denies_catalog(db): # Alice can see everything except accounting/sales and hr/* assert "/accounting/sales" in res_denied(rows) for r in rows: - if r["parent"] == "hr" or r["resource"] == "/accounting/sales": + if r["parent"] == "hr": + assert r["allow"] == 0 + elif r["resource"] == "/accounting/sales": assert r["allow"] == 0 else: assert r["allow"] == 1 diff --git a/tests/test_utils_sql_analysis.py b/tests/test_utils_sql_analysis.py index 363814d7..979ff9e1 100644 --- a/tests/test_utils_sql_analysis.py +++ b/tests/test_utils_sql_analysis.py @@ -1,7 +1,7 @@ import pytest -from datasette.utils.sql_analysis import analyze_sql_tables from datasette.utils.sqlite import sqlite3 +from datasette.utils.sql_analysis import analyze_sql_tables @pytest.fixture @@ -439,7 +439,7 @@ def test_analyze_attached_database_tables(conn): } -def test_analyze_disables_authorizer_on_error(): +def test_analyze_clears_authorizer_on_error(): class FakeConnection: def __init__(self): self.authorizers = [] @@ -455,5 +455,4 @@ def test_analyze_disables_authorizer_on_error(): with pytest.raises(sqlite3.OperationalError): analyze_sql_tables(conn, "bad SQL") - final_authorizer = conn.authorizers[-1] - assert final_authorizer is None or final_authorizer() == sqlite3.SQLITE_OK + assert conn.authorizers[-1] is None diff --git a/tests/test_write_wrapper.py b/tests/test_write_wrapper.py index 45eea483..88ce5520 100644 --- a/tests/test_write_wrapper.py +++ b/tests/test_write_wrapper.py @@ -3,16 +3,14 @@ Tests for the write_wrapper plugin hook. """ import asyncio -import sqlite3 -import time from dataclasses import dataclass - -import pytest - from datasette.app import Datasette from datasette.events import Event from datasette.hookspecs import hookimpl from datasette.plugins import pm +import pytest +import sqlite3 +import time @dataclass @@ -115,8 +113,7 @@ async def test_write_wrapper_exception_thrown_into_generator(datasette): def wrapper(conn): try: yield - except Exception as e: # noqa: BLE001 - # Test helper deliberately captures whatever the wrapped write raised + except Exception as e: caught["error"] = e return wrapper @@ -235,6 +232,7 @@ async def test_write_wrapper_return_none_skips(datasette): @hookimpl def write_wrapper(datasette, database, request, transaction): log.append("hook-called") + return None pm.register(Plugin(), name="test_skip") try: @@ -341,7 +339,7 @@ async def test_write_wrapper_via_api(tmp_path): "/test/api_test/-/insert", json={"row": {"name": "test"}, "return": True}, headers={ - "Authorization": f"Bearer {token}", + "Authorization": "Bearer {}".format(token), "Content-Type": "application/json", }, ) @@ -351,73 +349,6 @@ async def test_write_wrapper_via_api(tmp_path): pm.unregister(name="test_api") -@pytest.mark.asyncio -@pytest.mark.parametrize("num_sql_threads", (0, 1)) -@pytest.mark.parametrize("in_memory", (True, False)) -@pytest.mark.parametrize( - "operations", - ( - [{"op": "add_column", "args": {"name": "extra", "type": "text"}}], - [{"op": "rename_column", "args": {"name": "id", "to": "renamed_id"}}], - [{"op": "rename_table", "args": {"to": "renamed_t"}}], - [ - {"op": "add_column", "args": {"name": "extra", "type": "text"}}, - {"op": "rename_column", "args": {"name": "id", "to": "renamed_id"}}, - {"op": "rename_table", "args": {"to": "renamed_t"}}, - ], - ), - ids=["add-column", "transform", "rename-table", "combined"], -) -async def test_write_wrapper_can_reject_alter_table_after_write( - tmp_path, num_sql_threads, in_memory, operations -): - """Raising after yield should roll back the schema change.""" - db_path = str(tmp_path / "demo.db") - ds = Datasette( - [] if in_memory else [db_path], - config={"permissions": {"alter-table": True}}, - settings={"num_sql_threads": num_sql_threads}, - ) - db = ( - ds.add_memory_database(db_path, name="demo") - if in_memory - else ds.get_database("demo") - ) - await db.execute_write("CREATE TABLE t (id)") - await db.execute_write("INSERT INTO t (id) VALUES (1)") - before = await db.execute_fn(lambda conn: list(conn.iterdump())) - - class Plugin: - __name__ = "Plugin" - - @staticmethod - @hookimpl - def write_wrapper(database): - def wrapper(conn): - yield - raise ValueError("Rejected after write") - - return wrapper if database == "demo" else None - - pm.register(Plugin(), name="test_reject_alter_table") - try: - response = await ds.client.post( - "/demo/t/-/alter", - json={"operations": operations}, - ) - assert response.status_code == 400 - assert response.json()["errors"] == ["Rejected after write"] - assert await db.execute_fn(lambda conn: list(conn.iterdump())) == before - assert not [ - event - for event in getattr(ds, "_tracked_events", []) - if event.name in ("alter-table", "rename-table") - ] - finally: - pm.unregister(name="test_reject_alter_table") - ds.close() - - @pytest.mark.asyncio async def test_write_wrapper_change_group_pattern(datasette): """Test the motivating use case: activating a change group around a write.""" @@ -535,7 +466,7 @@ async def test_write_wrapper_set_authorizer(datasette, actor, table, should_deny try: request = FakeRequest(actor) if should_deny: - with pytest.raises(sqlite3.DatabaseError, match="not authorized"): + with pytest.raises(Exception): await db.execute_write_fn( lambda conn: conn.execute( f"insert into {table} (value) values ('test')"
{i}{i}a{i}' - f'Ada 17