diff --git a/.gitattributes b/.gitattributes index dfdb8b7..5347599 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,6 @@ *.sh text eol=lf + +# Bundled third-party files are kept byte-for-byte as upstream ships them: +# no line-ending conversion on any OS, so app/ui/vendor/VENDOR.json's SHA-256 +# values match every checkout (tests/test_vendor_manifest.py). +app/ui/vendor/** -text diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e2dc94..d247b5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,34 @@ jobs: if: runner.os == 'Linux' run: node tests/desktop_download_ui.test.js + - name: Test save folder setting UI + if: runner.os == 'Linux' + run: node tests/save_folder_ui.test.js + + - name: Test preview links and release notes UI + if: runner.os == 'Linux' + run: node tests/preview_links_ui.test.js + + - name: Test theme toggle UI + if: runner.os == 'Linux' + run: node tests/theme_toggle_ui.test.js + + - name: Test Settings drawer UI + if: runner.os == 'Linux' + run: node tests/settings_drawer_ui.test.js + + - name: Test layout width setting UI + if: runner.os == 'Linux' + run: node tests/layout_width_ui.test.js + + - name: Test preview text size UI + if: runner.os == 'Linux' + run: node tests/text_size_ui.test.js + + - name: Test math formulas UI + if: runner.os == 'Linux' + run: node tests/math_render_ui.test.js + # installer.iss is otherwise only compiled by release.yml, after the tag is # pushed, so a mistake in its Pascal [Code] cost a version number. Compile # it here against a stand-in bundle (only the script is being checked). diff --git a/AGENTS.md b/AGENTS.md index f504ff1..76e9f40 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,8 @@ the real `markitdown` public API (`MarkItDown.convert_local()` / `convert_uri()` Core UX contract (do not break when changing things): **drop a file, paste a URL, or click to browse → conversion starts immediately → the -resulting `.md` is auto-saved to `~/Downloads` with no export step.** +resulting `.md` is auto-saved to the save folder (`~/Downloads` unless +another folder is chosen in Settings) with no export step.** ## Commands @@ -41,7 +42,7 @@ python -c "import urllib.request, time; time.sleep(2); res = urllib.request.urlo # Test suite (CI runs exactly this) and the UI unit tests (Node, no npm) python -m pytest tests/ -q -node tests/auto_engine_ui.test.js && node tests/preview_word_count.test.js && node tests/glm_ocr_ui.test.js && node tests/desktop_download_ui.test.js +node tests/auto_engine_ui.test.js && node tests/preview_word_count.test.js && node tests/glm_ocr_ui.test.js && node tests/desktop_download_ui.test.js && node tests/save_folder_ui.test.js && node tests/preview_links_ui.test.js && node tests/theme_toggle_ui.test.js && node tests/settings_drawer_ui.test.js && node tests/layout_width_ui.test.js && node tests/text_size_ui.test.js && node tests/math_render_ui.test.js # Lint (also run in CI) pip install ruff @@ -59,19 +60,20 @@ item = QueueItem(source="/path/to/file.txt", kind=SourceKind.FILE, display_name= print(convert_item(item, ConversionOptions())) ``` -Any manual local testing that writes into the real `~/Downloads` (e.g. via -`auto_save_markdown()`) must clean up its own test files afterward — -nothing test-related should be left in the user's actual Downloads folder. +Any manual local testing that writes into the real `~/Downloads` or the chosen +save folder (e.g. via `auto_save_markdown()`) must clean up its own test files +afterward — nothing test-related should be left in the user's actual folders. ## Architecture **Unified Domain-Driven Architecture (`app/`)**: - `app/core/` — Core conversion logic & engine adapters: - - `converter.py` — Maps options to real `MarkItDown(...)` constructor kwargs. Dispatches `convert_local()` or `convert_uri()`. `auto_save_markdown()` / `get_downloads_dir()` / `unique_download_path()` implement collision-safe Downloads auto-save. + - `converter.py` — Maps options to real `MarkItDown(...)` constructor kwargs. Dispatches `convert_local()` or `convert_uri()`. `auto_save_markdown()` / `get_downloads_dir()` / `unique_download_path()` implement collision-safe auto-save into a given folder (Downloads by default). The file is created with `O_EXCL`, so a file or link that appears at the chosen name is never written through; the next free name is used, and a failed write removes its own partial file. - `queue_model.py` — `EngineKind` enum (`markitdown`, `docling`, `markit`, `glm_ocr`, and `auto`, a routing mode that always resolves to one of the concrete engines), `QueueItem` dataclass. - `engines/` — Engine adapters (`docling_engine.py`, `markit_engine.py`). - `pdf_probe.py` — One cheap pdfium pass over a PDF: per-page text layer, image coverage, and the scan-likely classifier that tells scanned pages from cover photos. Imports `pypdfium2` lazily, never raises (returns `None`), serialises pdfium with a lock. + - `save_location.py` — The save folder. Stored as `save_dir` in `settings.json` (`""` = Downloads) and set only by the desktop folder dialog (`choose_save_folder` in `runner.py`); it is deliberately absent from `SettingsPayload`, so no HTTP request can set it. `save_markdown()` writes there and falls back to Downloads with a sentence saying why (missing folder: never re-created; folder or a parent replaced by a link or junction since it was chosen: not followed, detected because the stored path no longer resolves to itself; write error: `OSError`/`ValueError`); the choice is kept. A window that attached to an already-running InkDoc refuses to change it, since that process does the saving. `folder_state()` feeds `GET /settings` and the Download dialog's starting folder. - `quality_check.py` — Missing-text check: compares a PDF conversion's Markdown with the PDF's own text layer and builds the user-facing warning and suggestion. Read-only; never fails a conversion. - `auto_engine.py` — The Auto engine: pure routing (`decide`, `should_escalate`, `pick_result`, `glm_time_gate`, `AUTO_ROUTES`), OCR availability ("only what is on disk"; GLM-OCR first, then Docling), and the async `run_auto`, which takes each engine's lane *before* using a thread and may re-convert once with the OCR engine. GLM-OCR is used only for files estimated to finish within `GLM_OCR_TIME_LIMIT_S` (5 min); otherwise Docling. - `jobs.py` — In-memory job registry behind `/convert/progress/{job_id}` and `/convert/cancel/{job_id}` (phase labels and cooperative cancel; engine-neutral). @@ -82,19 +84,32 @@ nothing test-related should be left in the user's actual Downloads folder. - `app/server/` — Embedded REST API backend: - `server.py` — FastAPI REST API endpoints (`/convert/file`, `/convert/url`, `/convert/batch`, `/convert/progress/{job_id}`, `/convert/cancel/{job_id}`, `/health`, `/extensions`), dual-mounts `/static` and `/ui`. Lanes: Docling (1), GLM-OCR (1), default (2) and a probe lane (1) for pdfium work, all acquired in async code before `asyncio.to_thread`. `/engines/{name}/…` dispatches `glm_ocr` to `GlmOcrManager`; `/convert/estimate` gives GLM-OCR's page count and time estimate. Explicit engines convert → auto-save → check; Auto probes → converts → checks → (maybe re-converts) → auto-saves once. Response metadata is built in one place, `_conversion_payload`. - `app/ui/` — **The single shared web & desktop UI**: - - `index.html` — Application DOM structure, dropzone, engine pills, live preview pane, settings popover. + - `index.html` — Application DOM structure, dropzone, engine pills, live preview pane, Settings drawer (General / Engines / Add-ons / Updates / About; it sits below the header so the title bar stays usable, and every engine prompt opens it on Engines). - `style.css` — Inkbench design system: obsidian dark & light themes, glowing engine selector pills, responsive layout. - - `app.js` — Client-side logic: drag-and-drop, API conversion, live markdown preview (`marked.js`), auto-save toggle, engine switching. + - `app.js` — Client-side logic: drag-and-drop, API conversion, live markdown preview (`marked.js`), auto-save toggle, engine switching. Every preview parse goes through `markdownToHtml`: with `render_math` off, or no `$`, `\(` or `\[` in the document, it is the global `marked.parse`, unchanged; otherwise a separate `marked.Marked` instance with the math rules. `typesetMath` lazy-loads KaTeX and draws formulas in slices. + - `vendor/` — Bundled third-party JS/CSS/fonts (marked, DOMPurify, highlight.js, KaTeX). Never fetched at runtime; every file pinned by SHA-256 in `vendor/VENDOR.json` (`tests/test_vendor_manifest.py`); never line-ending converted (`.gitattributes`). + - **Add-ons** (Settings section): built-in add-ons are bundled code switched by a setting (math formulas → `render_math`). Downloadable add-ons may only ever be data (models, fonts, dictionaries) through the signed `AddonManager` catalogue, never JavaScript that runs in the page. The existing `addon*` element ids, `/addons/*` routes and `AddonManager` belong to the Docling recognition model, not to this section. - `logo.svg`, `favicon.svg` — Brand assets. - `app/desktop/runner.py` — Desktop application runner: - Finds an available local loopback port (`13118` or ephemeral). - Starts FastAPI in a background daemon thread (`uvicorn.Server`). - Opens a native desktop window via `pywebview` (Microsoft Edge WebView2 on Windows) pointing to `http://127.0.0.1:/InkDoc`. + - Gives the page nine functions as `pywebview.api.*` through `window.expose(*_page_functions(...))`: title-bar controls, `open_external_link`, `save_markdown` (the Download button's Save dialog), and `choose_save_folder` / `reset_save_folder` (the only way the save folder is set). Never pass an object as `js_api`: pywebview looks up `js_api` attributes by any name the page sends, underscore-named ones included, while exposed functions are found by exact name only. `tests/test_desktop_save.py` checks the nine names match `app.js` and that nothing else is reachable. Methods that write files or settings also refuse any page not served by InkDoc itself. + - In the desktop app, links inside converted documents and release notes never load in the window (`handleContentLinkClick` in `app.js`): web links open in the system browser, `#` links scroll, everything else is ignored. - Handles graceful shutdown when the window is closed. **Threading and Execution Model**: - Conversions never block the main loop. In desktop mode, FastAPI runs in a background daemon thread (`uvicorn.Server`) while `pywebview` runs on the main GUI thread. - In the browser/UI layer (`app/ui/app.js`), document conversions are dispatched via asynchronous `fetch()` requests to `/convert/file` or `/convert/url`. Each queue item updates reactively without freezing the user interface. -- Files are collision-safely auto-saved to `~/Downloads` by the backend (`auto_save_markdown()`) without requiring any manual export step. +- Files are collision-safely auto-saved to the save folder (`~/Downloads` by default) by the backend (`save_location.save_markdown()`) without requiring any manual export step. + +## Known limitations (security) + +Accepted for now, each needing write access to the folder involved or a local process: +- The save folder is checked for links a moment before the file is created, and only the file name itself is protected from links at creation (`O_EXCL`); a parent folder swapped in between is still followed. +- The default Downloads folder is checked with `is_symlink()` only, which does not detect Windows junctions. +- After a failed write, the partial file is removed by name. +- `unique_download_path`'s containment check compares path prefixes without a separator (file names are sanitised, so it cannot be escaped today). +- In attach mode the window trusts any loopback server on the port whose `/health` looks like InkDoc's. ## CI/CD (all in `.github/workflows/`) diff --git a/API.md b/API.md index 8a7653f..216c4bb 100644 --- a/API.md +++ b/API.md @@ -40,7 +40,7 @@ Convert an uploaded document to Markdown. - `'markit'`: Broad-format specialist (EPUB, YAML, Jupyter Notebooks). - `'glm_ocr'` (also `'glm-ocr'`): the downloadable [GLM-OCR](#glm-ocr) model, for PDFs and images only. `409 engine_not_installed` until it is downloaded in Settings. - `'auto'`: InkDoc picks the engine per file (see [Auto engine](#auto-engine)) and reports its choice in `auto`. - - `save_to_downloads` *(bool, optional, default: `false`)*: Automatically save `.md` output directly to `~/Downloads` with collision-safe naming. The file is named after the uploaded file (`report.pdf` → `report.md`). + - `save_to_downloads` *(bool, optional, default: `false`)*: Automatically save `.md` output to the save folder with collision-safe naming. The save folder is `~/Downloads` unless another folder was chosen in the desktop app's Settings; a request cannot choose it. If the chosen folder is missing or can't be written to, the file is saved to `~/Downloads` and `save_fallback` says why. The file is named after the uploaded file (`report.pdf` → `report.md`). - `response_format` *(string, optional, `'text'` | `'json'`, default: `'text'`)*. - `enable_plugins` *(bool, optional, default: `false`)*: Enable MarkItDown plugins. - `keep_data_uris` *(bool, optional, default: `false`)*: Retain base64 data URIs for images. @@ -84,9 +84,10 @@ curl -F "file=@dataset.xlsx" "http://localhost:13118/convert/file?response_forma "Page 1 is a full-page image; any text inside it wasn't extracted."], "engine": "docling", "output_tokens": 5120 }, - "warnings": [], // notes that are not failures (GLM-OCR: a page cut short, GPU fallback) + "warnings": [], // notes that are not failures (GLM-OCR: a page cut short, GPU fallback, save_fallback) "markdown": "…", - "saved_to_downloads": "C:\\Users\\you\\Downloads\\report.md" + "saved_to_downloads": "C:\\Users\\you\\Downloads\\report.md", // where the file was saved (any save folder), or null + "save_fallback": null // e.g. "Saved to Downloads because the save folder D:\\Notes was not found." } ``` @@ -133,7 +134,7 @@ Convert multiple files in a single HTTP request. - `check_quality` *(bool, optional)* - `job_id` *(string, optional)*: each file reports progress under `-` (0-based). -Each entry in `results` carries the same `engine_*`, `fallback*`, `auto` and `quality` fields as a single conversion. +Each entry in `results` carries the same `engine_*`, `fallback*`, `auto`, `quality` and `save_fallback` fields as a single conversion. **cURL Example**: ```bash @@ -278,10 +279,16 @@ Query or update application engine preferences. { "fallback_to_markitdown": true, "quality_check_enabled": true, - "glm_ocr_download_source": "auto" + "glm_ocr_download_source": "auto", + "layout_width": "standard", + "preview_text_size": 100, + "render_math": true } ``` - `quality_check_enabled` (default `true`) turns the missing-text check on or off for requests that do not pass `check_quality`. `glm_ocr_download_source` is `auto` (official source first, InkDoc mirror as fallback) or `mirror_only`. + `quality_check_enabled` (default `true`) turns the missing-text check on or off for requests that do not pass `check_quality`. `glm_ocr_download_source` is `auto` (official source first, InkDoc mirror as fallback) or `mirror_only`. `layout_width` (default `standard`) is how much of a wide window the UI uses: `standard` (up to 1400px), `wide` (up to 1920px) or `full`; any other value is refused with `422`. The UI applies it from the next page load. `preview_text_size` (default `100`) is the document preview's text size in percent: `85`, `90`, `100`, `110`, `120`, `135` or `150`; anything else is refused with `422`. `render_math` (default `true`) draws LaTeX math in the UI preview with the bundled KaTeX; it never changes conversion output, saved files or API responses. +* **Save folder** (`GET` only): the response includes `save_folder`, for example + `{"path": "C:\\Users\\you\\Downloads", "chosen": "D:\\Notes", "custom": true, "problem": "This folder was not found. Files are saved to Downloads until it is back."}`. + `path` is where auto-save writes now; `chosen` is the folder picked in Settings (`""` for Downloads). The save folder can only be changed in the desktop app's folder dialog: `POST /settings` ignores `save_dir`, and no other request can set it. --- @@ -354,6 +361,7 @@ Conversion responses include engine diagnostic headers: - `X-Quality-Warning`: `true` or `false` when a PDF was checked for missing text. - `X-Quality-Missing-Pct`: Estimated share of the PDF's text missing from the output, when measured. - `X-Engine-Warnings`: Notes that are not failures (`warnings` in JSON), joined with ` | `. +- `X-Save-Fallback`: Present when auto-save could not use the chosen save folder and saved to `~/Downloads` instead (`save_fallback` in JSON). Like the other text headers it is limited to 200 characters and non-Latin-1 characters become `?`; use `response_format=json` for the exact text. Reasons are reduced to one line of at most 200 characters. diff --git a/CHANGELOG.md b/CHANGELOG.md index 50fa83d..dff7fd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.2.0] - TBD + +### Changed +- **Settings** now opens as a wider panel on the right with four sections: + **General**, **Engines**, **Updates** and **About**. "Check for updates + daily" has moved to Updates, and About has a short description of InkDoc. + The panel closes from the gear, the **X**, **Esc** or a click outside it. + The title bar stays usable while it is open, and dragging a file onto the + window closes it so the file lands in the drop zone. Prompts to install or + fix an engine open it on Engines. While it is open, notifications appear + beside it instead of over its buttons. +- In Settings → General, thin lines now separate the unrelated preferences + (saving, the Docling fallback and the PDF check), so each description sits + clearly with its own switch. +- Switching between the dark and light theme now reveals the new theme as + a circle growing from the theme button, as on the website. Where the + system's web engine doesn't support it, or "reduce motion" is on, the + theme switches instantly as before. + +### Security +- In the desktop app, links inside converted documents and release notes no + longer open inside InkDoc's window: web links open in your browser, and + other links are ignored. +- The page can now call only the nine desktop functions it uses; nothing else + in the desktop app is reachable from page scripts. +- If the save folder's location can't be checked, files are saved to + Downloads instead. +- Release notes are shown as plain text if the HTML sanitiser is unavailable. +- The preview's HTML sanitiser, DOMPurify, is updated from 3.1.6 to 3.4.16, + picking up the upstream security fixes released since. + +### Added +- **Save folder** setting (Settings → General): choose where auto-save + writes, instead of always using Downloads. **Change…** opens your system's + folder dialog, the choice is remembered, and the desktop **Download** button's + Save dialog opens there too. If the folder is missing or can't be written to + (for example an unplugged drive), files are saved to Downloads and InkDoc + says so; the folder is used again once it is back. The folder can only be + chosen in the desktop app: no web page or API request can change it. API + responses report a fallback in the new `save_fallback` field + ([#6](https://github.com/AbdoslamB/InkDoc/issues/6)). +- **Layout width** setting (Settings → General → Display): **Standard** (up to + 1,400 px, as before), **Wide** (up to 1,920 px) or **Full** (the whole + window). On large screens, tables, code and images get the extra room while + paragraphs keep a readable line length; windows narrower than 1,400 px look + the same with any choice. It is remembered between launches of the desktop + app and applied before the window first draws, so the layout never jumps. + `GET`/`POST /settings` report and accept it as `layout_width`. +- **Preview text size** (Settings → General → Display): **A−** and **A+** make + the converted document's text smaller or larger in seven steps (85% to 150%), + in both the Rendered and Raw tabs, with a live sample right below the + buttons. Headings, code and tables keep their proportions, and 100% is + exactly the size used before. It is remembered between launches like the + layout width; `GET`/`POST /settings` use `preview_text_size` (85, 90, 100, + 110, 120, 135 or 150). +- **Math formulas** in the preview, and a new **Settings → Add-ons** section + where it can be switched off. LaTeX from Docling's formula recognition and + GLM-OCR (`$…$`, `$$…$$`, `\(…\)`, `\[…\]`) is drawn as real formulas + with KaTeX instead of showing as mangled text. KaTeX is bundled, works + offline and is loaded only for documents that contain math, so other + documents, startup and saved files are unchanged. Prices such as $5 and + code are left alone; if ordinary text with $ signs is ever shown as math, + turn **Math formulas** off. `GET`/`POST /settings` use `render_math`. + +### Fixed +- A wide table in the preview now scrolls sideways on its own instead of + moving the whole document, and large pictures shrink to fit the preview. +- Every switch in Settings can now be reached with Tab and flipped with + Space, and keyboard focus in Settings is visible on macOS and Linux too. + ## [1.1.1] - 2026-10-14 ### Fixed @@ -160,7 +230,8 @@ Initial release. - Windows installer (`inkdoc-setup.exe`), portable executable (`inkdoc.exe`) and portable ZIP; macOS (Apple Silicon) bundle; Linux (x86_64) bundle. -[Unreleased]: https://github.com/AbdoslamB/InkDoc/compare/v1.1.1...HEAD +[Unreleased]: https://github.com/AbdoslamB/InkDoc/compare/v1.2.0...HEAD +[1.2.0]: https://github.com/AbdoslamB/InkDoc/compare/v1.1.1...v1.2.0 [1.1.1]: https://github.com/AbdoslamB/InkDoc/compare/v1.1.0...v1.1.1 [1.1.0]: https://github.com/AbdoslamB/InkDoc/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/AbdoslamB/InkDoc/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ea0125..50cc0fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,8 @@ The codebase follows a domain-driven structure: inkdoc/ ├── app/ │ ├── core/ # Conversion business logic & engine adapters -│ │ ├── converter.py # MarkItDown wrapper, option mapping & Downloads auto-save +│ │ ├── converter.py # MarkItDown wrapper, option mapping & collision-safe auto-save +│ │ ├── save_location.py # Save folder setting (Downloads by default) & its fallback │ │ ├── queue_model.py # EngineKind enum, QueueItem data definitions │ │ └── engines/ # IBM Docling, Markit & GLM-OCR engine adapters │ ├── server/ # Embedded FastAPI backend @@ -151,6 +152,14 @@ node tests/docs_release_selector.test.js node tests/enrichment_addon_ui.test.js node tests/glm_ocr_ui.test.js node tests/preview_word_count.test.js +node tests/desktop_download_ui.test.js +node tests/save_folder_ui.test.js +node tests/preview_links_ui.test.js +node tests/theme_toggle_ui.test.js +node tests/settings_drawer_ui.test.js +node tests/layout_width_ui.test.js +node tests/text_size_ui.test.js +node tests/math_render_ui.test.js # 5. Verify code style with ruff ruff check . @@ -181,6 +190,17 @@ Any pull request that adds or upgrades a dependency, a model, or a bundled or do Dependabot pull requests need the same check before merging. +### Bundled UI libraries (`app/ui/vendor/`) + +The page's JavaScript, CSS and fonts (marked, DOMPurify, highlight.js, KaTeX) are bundled, never fetched at runtime, and pinned by SHA-256 in [`app/ui/vendor/VENDOR.json`](app/ui/vendor/VENDOR.json). Dependabot does not see them. To update one: + +1. Download the exact version (`npm pack @`) into an empty folder and check that the tarball's sha512 matches `npm view @ dist.integrity`. Prefer a release that has been out for at least two weeks. +2. Copy only the files `VENDOR.json` lists for that library, unchanged. +3. Update the library's `version`, `upstream_integrity` and file hashes in `VENDOR.json`, its row in `THIRD_PARTY_NOTICES.md`, and its file in `LICENSES/` (see above). +4. Run `python -m pytest tests/test_vendor_manifest.py`, then the UI tests and a manual look at the preview. + +Files in `app/ui/vendor/` are never line-ending converted (see `.gitattributes`), so their hashes match on every OS. + --- ## 7. Security Policy diff --git a/LICENSES/DOMPurify-Apache-2.0-OR-MPL-2.0.txt b/LICENSES/DOMPurify-Apache-2.0-OR-MPL-2.0.txt index e268feb..ed54ef9 100644 --- a/LICENSES/DOMPurify-Apache-2.0-OR-MPL-2.0.txt +++ b/LICENSES/DOMPurify-Apache-2.0-OR-MPL-2.0.txt @@ -1,6 +1,7 @@ -DOMPurify 3.1.6: HTML sanitiser for the preview, bundled at app/ui/vendor/purify.min.js. +DOMPurify 3.4.16: HTML sanitiser for the preview, bundled at app/ui/vendor/purify.min.js. DOMPurify is offered under Apache-2.0 or MPL-2.0, at the recipient's choice. -Source: https://github.com/cure53/DOMPurify/blob/3.1.6/LICENSE (fetched 2026-10-01) +Source: https://github.com/cure53/DOMPurify/blob/3.4.16/LICENSE and LICENSE-MPL +(the npm package's licence field is "(MPL-2.0 OR Apache-2.0)"; texts unchanged since 3.1.6) ------------------------------------------------------------------------------ DOMPurify diff --git a/LICENSES/KaTeX-MIT.txt b/LICENSES/KaTeX-MIT.txt new file mode 100644 index 0000000..0e2b00c --- /dev/null +++ b/LICENSES/KaTeX-MIT.txt @@ -0,0 +1,27 @@ +KaTeX 0.19.0: math formulas in the preview, bundled at app/ui/vendor/katex/ +(katex.min.js, katex.min.css and the 20 KaTeX_*.woff2 fonts in fonts/). +Source: https://github.com/KaTeX/KaTeX/blob/v0.19.0/LICENSE (fetched 2026-10-14). +The fonts come from https://github.com/KaTeX/katex-fonts, which is also MIT licensed. +------------------------------------------------------------------------------ + +The MIT License (MIT) + +Copyright (c) 2013-2020 Khan Academy and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 884080f..e95ce5e 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ A unified desktop workbench and local REST API that bridges **Microsoft MarkItDo InkDoc desktop workbench demonstration -Drop a file, directory, or URL → conversion starts instantly → clean Markdown is auto-saved to ~/Downloads with collision-safe naming. +Drop a file, directory, or URL → conversion starts instantly → clean Markdown is auto-saved to ~/Downloads (or a folder you choose) with collision-safe naming. @@ -83,7 +83,7 @@ Optional engines are downloaded by the app, not installed with it, and live in t | macOS | `~/Library/Application Support/InkDoc/engines` | | Linux | `~/.local/share/inkdoc/engines` (or `$XDG_DATA_HOME/inkdoc/engines`) | -The Windows uninstaller asks whether to remove them (Docling and GLM-OCR, with their size) and keeps them by default, so a reinstall finds them ready. On macOS and Linux, delete the folder above by hand, or remove an engine first from **Settings → Processing Engines → Remove**. +The Windows uninstaller asks whether to remove them (Docling and GLM-OCR, with their size) and keeps them by default, so a reinstall finds them ready. On macOS and Linux, delete the folder above by hand, or remove an engine first from **Settings → Engines → Remove**. @@ -162,7 +162,7 @@ gh attestation verify inkdoc-setup.exe --repo AbdoslamB/inkdoc
Single Source of Truth Interface
- The native desktop executable (Edge WebView2 on Windows, WebKit on macOS/Linux) and the local web workbench (http://127.0.0.1:13118/InkDoc) run the exact same user interface without code divergence. + The native desktop executable (Edge WebView2 on Windows, WebKit on macOS/Linux) and the local web workbench (http://127.0.0.1:13118/InkDoc) run the exact same user interface without code divergence. On large screens, Settings → General → Layout width (Standard, Wide or Full) gives tables, code and images more room, and A− / A+ set the preview's text size. LaTeX formulas from Docling and GLM-OCR are drawn as real math (bundled KaTeX, works offline; switch it off in Settings → Add-ons). @@ -171,7 +171,7 @@ gh attestation verify inkdoc-setup.exe --repo AbdoslamB/inkdoc
Zero-Friction Auto-Save
- Documents convert asynchronously and are automatically saved to ~/Downloads the moment they finish, with collision-safe numbered suffixes (e.g., report (1).md). No manual export required. + Documents convert asynchronously and are automatically saved to ~/Downloads, or to a folder you pick in Settings, the moment they finish, with collision-safe numbered suffixes (e.g., report (1).md). No manual export required. If the chosen folder is missing or read-only, the file goes to Downloads and InkDoc tells you. @@ -200,7 +200,7 @@ flowchart LR DL --> OUT MK --> OUT GL --> OUT - OUT --> SAVE[("💾 ~/Downloads
collision-safe naming")] + OUT --> SAVE[("💾 Save folder (~/Downloads by default)
collision-safe naming")] classDef engine stroke:#10b981,stroke-width:2px; class MD,DL,MK,GL engine; @@ -225,7 +225,7 @@ flowchart LR [GLM-OCR](https://huggingface.co/zai-org/GLM-OCR) (zai-org, 0.9B parameters, MIT license) is a small vision-language model built for OCR. InkDoc runs it locally through [llama.cpp](https://github.com/ggml-org/llama.cpp)'s `llama-server` in a separate process, so it works on a normal laptop with no Python ML stack. -- **One click, after a clear warning.** Settings → Processing Engines → GLM-OCR → **Download** first shows the download size (~1.45 GB) and that processing is much slower than the other engines, and only downloads after you confirm. +- **One click, after a clear warning.** Settings → Engines → GLM-OCR → **Download** first shows the download size (~1.45 GB) and that processing is much slower than the other engines, and only downloads after you confirm. - **Official source first, mirror fallback.** The model comes from Hugging Face and the runtime from the llama.cpp releases. If either fails for any reason (blocked, down, rate limited, or a wrong hash), the same files come from InkDoc's own GitHub release. Every file is checked against SHA-256 hashes pinned in InkDoc, and the runtime files are re-hashed before every launch. - **Tested before use.** The install is only complete when the model reads a known test image on your computer, which also measures its speed there. - **Speed.** On a laptop CPU (Ryzen 9 4900HS) expect roughly 15–40 seconds per page; with **GPU acceleration** (Settings, Vulkan on Windows/Linux for NVIDIA, AMD and Intel graphics, Metal on Apple Silicon) about 4–7 seconds per page on the same laptop's integrated Radeon. Long jobs show *"Page 7 / 30 · ~8 min left"* with a **Cancel** button, and InkDoc asks before starting a job longer than 5 minutes. @@ -313,7 +313,7 @@ InkDoc ships with an embedded REST API for automated pipelines, agents, and deve | Endpoint | What it does | | :--- | :--- | -| `POST /convert/file` | Upload a file (multipart). Choose an engine with `?engine=` (`markitdown`, `docling`, `markit`, `glm_ocr` or `auto`) and auto-save with `?save_to_downloads=true`. JSON responses include the missing-text check (`quality`) and Auto's choice (`auto`). | +| `POST /convert/file` | Upload a file (multipart). Choose an engine with `?engine=` (`markitdown`, `docling`, `markit`, `glm_ocr` or `auto`) and auto-save with `?save_to_downloads=true` (into the save folder chosen in Settings, `~/Downloads` by default). JSON responses include the missing-text check (`quality`) and Auto's choice (`auto`). | | `POST /convert/url` | Convert a web page from a JSON body: `{"url": "...", "save_to_downloads": true}`. | | `GET /convert/progress/{job_id}` | Live phase of a conversion started with `?job_id=`; `POST /convert/cancel/{job_id}` stops it. | | `GET /docs` | Interactive OpenAPI (Swagger) UI. | @@ -403,6 +403,14 @@ node tests/docs_release_selector.test.js node tests/enrichment_addon_ui.test.js node tests/glm_ocr_ui.test.js node tests/preview_word_count.test.js +node tests/desktop_download_ui.test.js +node tests/save_folder_ui.test.js +node tests/preview_links_ui.test.js +node tests/theme_toggle_ui.test.js +node tests/settings_drawer_ui.test.js +node tests/layout_width_ui.test.js +node tests/text_size_ui.test.js +node tests/math_render_ui.test.js # 4. Verify code style and formatting with Ruff ruff check . @@ -441,6 +449,7 @@ InkDoc is built on excellent open-source work. Code and model weights are listed | Local REST API | [`fastapi`](https://github.com/fastapi/fastapi) | Code | Sebastián Ramírez | MIT | | Desktop window | [`pywebview`](https://github.com/r0x0r/pywebview) | Code | Roman Sirokov | BSD-3-Clause | | Markdown preview | [`marked`](https://github.com/markedjs/marked) | Code | MarkedJS, Christopher Jeffrey | MIT | +| Math formulas in the preview | [`KaTeX`](https://github.com/KaTeX/KaTeX) | Code and fonts | Khan Academy and other contributors | MIT | | Prototype foundation | Early open-source concept | — | Andres Torres | MIT | EasyOCR is not included: InkDoc's Docling pack uses RapidOCR. The full list of components, including all bundled Python packages, is in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index a09126a..683c0e2 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -34,8 +34,9 @@ This file was last checked against upstream sources on 2026-10-01. Where an upst | PyInstaller bootloader | Starts the packaged executable | [pyinstaller/pyinstaller](https://github.com/pyinstaller/pyinstaller) | PyInstaller Development Team | GPL-2.0-or-later with the Bootloader Exception | Bundled | [python-packages-desktop.txt](LICENSES/python-packages-desktop.txt) | | All other Python packages (78 in total, including numpy, pandas, lxml, Pillow, cryptography, certifi) | Dependencies of the above | See text | See text | MIT, BSD, Apache-2.0, PSF-2.0, MPL-2.0 (certifi) and others, listed per package | Bundled | [python-packages-desktop.txt](LICENSES/python-packages-desktop.txt) | | marked 15.0.12 | Markdown preview | [markedjs/marked](https://github.com/markedjs/marked) | MarkedJS; Christopher Jeffrey; John Gruber (see text) | MIT (and a BSD-style notice for Markdown) | Bundled | [marked-MIT.txt](LICENSES/marked-MIT.txt) | -| DOMPurify 3.1.6 | Sanitising the preview | [cure53/DOMPurify](https://github.com/cure53/DOMPurify) | Dr.-Ing. Mario Heiderich, Cure53 | Apache-2.0 or MPL-2.0, at your choice | Bundled | [DOMPurify-Apache-2.0-OR-MPL-2.0.txt](LICENSES/DOMPurify-Apache-2.0-OR-MPL-2.0.txt) | +| DOMPurify 3.4.16 | Sanitising the preview | [cure53/DOMPurify](https://github.com/cure53/DOMPurify) | Dr.-Ing. Mario Heiderich, Cure53 | Apache-2.0 or MPL-2.0, at your choice | Bundled | [DOMPurify-Apache-2.0-OR-MPL-2.0.txt](LICENSES/DOMPurify-Apache-2.0-OR-MPL-2.0.txt) | | highlight.js 11.12.0 (custom build) | Code highlighting in the preview | [highlightjs/highlight.js](https://github.com/highlightjs/highlight.js) | Ivan Sagalaev | BSD-3-Clause | Bundled | [highlight.js-BSD-3-Clause.txt](LICENSES/highlight.js-BSD-3-Clause.txt) | +| KaTeX 0.19.0 (code and fonts) | Math formulas in the preview, loaded only for documents that contain math | [KaTeX/KaTeX](https://github.com/KaTeX/KaTeX), fonts from [KaTeX/katex-fonts](https://github.com/KaTeX/katex-fonts) | Khan Academy and other contributors | MIT | Bundled | [KaTeX-MIT.txt](LICENSES/KaTeX-MIT.txt) | The package list in [python-packages-desktop.txt](LICENSES/python-packages-desktop.txt) was generated from a clean Windows build environment. macOS builds also bundle pyobjc packages, and Linux builds bundle PyGObject and GTK/WebKitGTK libraries from the build machine. Those are not yet listed (see [CONTRIBUTING.md](CONTRIBUTING.md#licenses-for-new-dependencies-models-and-binaries)). diff --git a/app/core/converter.py b/app/core/converter.py index e3057c4..05a9320 100644 --- a/app/core/converter.py +++ b/app/core/converter.py @@ -156,6 +156,9 @@ def unique_download_path(directory: Path, base_name: str) -> Path: return candidate +_MAX_CREATE_ATTEMPTS = 100 + + def auto_save_markdown(item: QueueItem, markdown_text: str, downloads_dir: Path | None = None) -> Path: """Write `markdown_text` to the Downloads folder using a name derived from the item's source, avoiding collisions and symlinks. Returns the path written to.""" @@ -164,9 +167,28 @@ def auto_save_markdown(item: QueueItem, markdown_text: str, downloads_dir: Path raise ValueError("Security violation: Downloads directory cannot be a symbolic link.") base_name = sanitize_filename(default_basename(item)) - path = unique_download_path(directory, base_name) - path.write_text(markdown_text, encoding="utf-8") - return path + # O_EXCL creates the file only if nothing is at that name, a link included, + # so a file or link that appears after unique_download_path() looked is + # never written through or overwritten: the next free name is used instead. + # O_BINARY leaves line endings to the text layer, as write_text() did. + flags = (os.O_WRONLY | os.O_CREAT | os.O_EXCL + | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)) + for _ in range(_MAX_CREATE_ATTEMPTS): + path = unique_download_path(directory, base_name) + try: + fd = os.open(path, flags, 0o666) + except FileExistsError: + continue + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + f.write(markdown_text) + except Exception: + # Only our own new file: disk full, drive unplugged, text that + # cannot be encoded. + path.unlink(missing_ok=True) + raise + return path + raise FileExistsError(f"Could not find a free name for {base_name}.md") # Extensions markitdown's built-in converters recognize (used to filter # recursive folder scans). PlainTextConverter/HtmlConverter also catch many @@ -240,6 +262,9 @@ class ConversionOptions: # and was cut short, the GPU falling back to the CPU). Replaced, never # mutated in place, since dataclasses.replace shares the list. engine_warnings: list[str] = field(default_factory=list) + # Set when auto-save could not use the chosen save folder and wrote to + # Downloads instead (app/core/save_location.py); the sentence says why. + save_fallback: str | None = None # Progress/cancel handle (app/core/jobs.JobHandle) for engines that report # per page. Not a result: set by the server, read by the engine. diff --git a/app/core/engine_manager.py b/app/core/engine_manager.py index e649374..9390e78 100644 --- a/app/core/engine_manager.py +++ b/app/core/engine_manager.py @@ -95,8 +95,24 @@ class InstallProgress: "glm_ocr_download_source": "auto", "glm_ocr_catalogue_seen": 0, "glm_ocr_measured_spp": {}, + # Auto-save folder; "" is Downloads. Set only through the desktop folder + # dialog (app/core/save_location.py), never through POST /settings. + "save_dir": "", + # How much of a wide window the UI uses: "standard" (1400px, as before), + # "wide" (1920px) or "full". Kept here rather than in browser storage, which + # the desktop app's web view does not keep between launches. + "layout_width": "standard", + # Text size of the document preview (Rendered and Raw tabs), in percent. + # Only the interface's own steps are accepted; 100 is the size used before. + "preview_text_size": 100, + # Built-in add-on (Settings → Add-ons): draw LaTeX math in the preview with + # the bundled KaTeX. Off means the preview is parsed exactly as before. + "render_math": True, } +LAYOUT_WIDTHS = ("standard", "wide", "full") +PREVIEW_TEXT_SIZES = (85, 90, 100, 110, 120, 135, 150) + GLM_OCR_ENGINE = "glm_ocr" diff --git a/app/core/save_location.py b/app/core/save_location.py new file mode 100644 index 0000000..8f4c17b --- /dev/null +++ b/app/core/save_location.py @@ -0,0 +1,144 @@ +"""The save folder: where auto-save writes converted Markdown. + +Downloads by default. A different folder can be chosen in the desktop app's +Settings, through the system's folder dialog only: the choice is stored in +settings.json under "save_dir" and is deliberately absent from the HTTP +settings payload, so no web page or API request can make InkDoc write +somewhere else. + +A chosen folder that is missing or cannot be written to never loses a file: +the Markdown goes to Downloads and the caller gets a sentence saying so. The +choice itself is kept, so a folder on a drive that was unplugged works again +once it is back. +""" +from __future__ import annotations + +import json +import logging +import os +import tempfile +from pathlib import Path +from typing import Any + +from app.core import converter +from app.core.engine_manager import EngineManager +from app.core.queue_model import QueueItem + +logger = logging.getLogger(__name__) + +SAVE_DIR_KEY = "save_dir" + + +def configured_save_dir() -> Path | None: + """The folder chosen in Settings, or None for the default (Downloads). + + Only an absolute path counts; anything else can only come from a + hand-edited settings.json and is treated as no choice. + """ + value = EngineManager.get_instance().get_settings().get(SAVE_DIR_KEY) + if not isinstance(value, str) or not value or not os.path.isabs(value): + return None + return Path(value) + + +def _redirected(folder: Path) -> bool: + """True when the folder no longer resolves to itself. + + The dialog stores the resolved path, so a difference means the folder, or a + folder above it, was replaced by a link or junction after it was chosen. + is_symlink() alone would miss both a junction and a replaced parent. + """ + try: + real = folder.resolve() + except Exception: + # Unsure (a link loop raises RuntimeError on Python 3.10-3.12): treat it + # as redirected, so the file goes to Downloads rather than somewhere unknown. + return True + return os.path.normcase(str(real)) != os.path.normcase(str(folder)) + + +def folder_state() -> dict[str, Any]: + """What Settings shows: the folder files go to now, and why if it is not the chosen one. + + Cheap checks only (two stats); nothing is written. The real test is the + write itself, in save_markdown(). + """ + downloads = converter.get_downloads_dir() + chosen = configured_save_dir() + if chosen is None: + return {"path": str(downloads), "chosen": "", "custom": False, "problem": ""} + if not chosen.is_dir(): + problem = "This folder was not found. Files are saved to Downloads until it is back." + elif _redirected(chosen): + problem = "This folder is now a link to another place. Files are saved to Downloads instead." + elif not os.access(chosen, os.W_OK): + problem = "InkDoc can't write to this folder. Files are saved to Downloads instead." + else: + problem = "" + return { + "path": str(downloads if problem else chosen), + "chosen": str(chosen), + "custom": True, + "problem": problem, + } + + +def check_usable(folder: Path) -> str: + """Why `folder` cannot be the save folder, or "" when it can. + + Tries an actual write: permission bits say little on Windows (ACLs) and + nothing about macOS privacy prompts or read-only mounts. + """ + if not folder.is_absolute(): + return "Choose a full folder path." + if not folder.is_dir(): + return "That folder doesn't exist." + try: + fd, probe = tempfile.mkstemp(prefix=".inkdoc-write-test-", dir=str(folder)) + os.close(fd) + os.unlink(probe) + except OSError: + return "InkDoc can't write to that folder." + return "" + + +def store_save_dir(value: str) -> bool: + """Store the save folder ("" for Downloads). False when it could not be saved for next time. + + update_settings() keeps the value in memory even when the file write fails, + so the file is read back to tell the user the truth. + """ + mgr = EngineManager.get_instance() + mgr.update_settings({SAVE_DIR_KEY: value}) + try: + stored = json.loads(mgr.get_settings_file_path().read_text(encoding="utf-8")) + except (OSError, ValueError): + return False + return isinstance(stored, dict) and stored.get(SAVE_DIR_KEY, "") == value + + +def save_markdown(item: QueueItem, markdown_text: str) -> tuple[Path, str | None]: + """Auto-save to the save folder; fall back to Downloads, with a sentence saying why. + + A missing folder is never written to: auto_save_markdown() would re-create + it, which would quietly bring back a deleted folder or a path on an + unplugged drive. If Downloads fails too, that error is raised as before. + """ + chosen = configured_save_dir() + if chosen is None: + return converter.auto_save_markdown(item, markdown_text), None + + if not chosen.is_dir(): + reason = "was not found" + elif _redirected(chosen): + reason = "is a link to another place" + else: + try: + return converter.auto_save_markdown(item, markdown_text, chosen), None + except (OSError, ValueError) as exc: + logger.warning("Could not write to the save folder (%s); saved to Downloads.", type(exc).__name__) + logger.debug("Save folder %s: %s", chosen, exc) + reason = "can't be written to" + + path = converter.auto_save_markdown(item, markdown_text) + return path, f"Saved to Downloads because the save folder {chosen} {reason}." diff --git a/app/desktop/runner.py b/app/desktop/runner.py index 7f9c984..7afe2c8 100644 --- a/app/desktop/runner.py +++ b/app/desktop/runner.py @@ -53,13 +53,14 @@ import uvicorn -from app.core.converter import get_downloads_dir, sanitize_filename +from app.core.converter import sanitize_filename +from app.core.save_location import check_usable, folder_state, store_save_dir from app.server.server import app def _default_save_dir() -> Path: - """Folder the Download button's Save dialog opens in.""" - return get_downloads_dir() + """Folder the Download button's Save dialog opens in: the save folder, Downloads by default.""" + return Path(folder_state()["path"]) def _url_origin(url: str | None) -> str: @@ -67,6 +68,17 @@ def _url_origin(url: str | None) -> str: return f"{parsed.scheme}://{parsed.netloc}" if parsed.scheme and parsed.netloc else "" +def _dialog_path(result) -> str: + """The one path a file dialog returned, or "" when it was cancelled. + + A tuple on Windows, GTK and Qt (Qt cancels with ("",)); a plain string from + the macOS Save panel. + """ + if isinstance(result, str): + return result + return result[0] if result else "" + + # A str from JSON has no surrogate pairs, so any surrogate in it is unpaired. _LONE_SURROGATE = re.compile("[\ud800-\udfff]") @@ -337,17 +349,24 @@ def _setup_window_chrome(title: str, controls: WindowControls, window, origin: s class WindowControls: - """Exposed to the page as pywebview.api, for the custom title bar buttons.""" + """Window controls, the Download button's save and the save folder. + + The page reaches these only through _page_functions(), never this object. + """ def __init__(self) -> None: self._window = None self._chrome = None self._hwnd = 0 self._app_origin = "" + self._owns_server = True - def attach(self, window, app_url: str = "") -> None: + def _attach(self, window, app_url: str = "", owns_server: bool = True) -> None: + """Private (pywebview exposes every public method to the page): it holds the + origin the file and settings methods trust.""" self._window = window self._app_origin = _url_origin(app_url) + self._owns_server = owns_server def attach_chrome(self, hwnd: int) -> None: """Subclass the native window so the shell treats our drawn title bar as @@ -459,9 +478,7 @@ def save_markdown(self, filename: str, content: str) -> dict: if not window: return {"status": "error", "message": "The app window is not ready."} try: - # pywebview gives the bridge to any page the window shows, including one - # reached through a link in a converted document; only InkDoc's own may save. - if not self._app_origin or _url_origin(window.get_current_url()) != self._app_origin: + if not self._on_app_page(window): return {"status": "error", "message": "Saving is only available from InkDoc itself."} import webview @@ -475,8 +492,7 @@ def save_markdown(self, filename: str, content: str) -> dict: save_filename=name, file_types=("Markdown files (*.md)",), ) - # A tuple on Windows, GTK and Qt; a plain string on macOS. - chosen = result if isinstance(result, str) else (result[0] if result else "") + chosen = _dialog_path(result) if not chosen: return {"status": "cancelled"} @@ -509,6 +525,128 @@ def save_markdown(self, filename: str, content: str) -> dict: except Exception as exc: return {"status": "error", "message": str(exc) or type(exc).__name__} + def choose_save_folder(self) -> dict: + """Pick the save folder in the system's folder dialog and remember it. + + The only way the save folder is ever set: the page asks, but the path + comes from the dialog, never from JavaScript, and is checked by writing + a test file before it is stored. Returns {"status": "saved", "folder"} + (plus "warning" when it could not be kept for next time), + {"status": "cancelled"} or {"status": "error", "message"}. + """ + window = self._window + if not window: + return {"status": "error", "message": "The app window is not ready."} + try: + refused = self._save_folder_refusal(window) + if refused: + return refused + + import webview + + chosen = _dialog_path( + window.create_file_dialog(webview.FileDialog.FOLDER, directory=str(_default_save_dir())) + ) + if not chosen: + return {"status": "cancelled"} + + # The real path, so a link picked in the dialog is stored as its target. + folder = Path(chosen).resolve() + problem = check_usable(folder) + if problem: + return {"status": "error", "message": problem} + return self._stored(str(folder)) + except Exception as exc: + return {"status": "error", "message": str(exc) or type(exc).__name__} + + def reset_save_folder(self) -> dict: + """Save to Downloads again. Same return shape as choose_save_folder().""" + window = self._window + if not window: + return {"status": "error", "message": "The app window is not ready."} + try: + refused = self._save_folder_refusal(window) + if refused: + return refused + return self._stored("") + except Exception as exc: + return {"status": "error", "message": str(exc) or type(exc).__name__} + + def _save_folder_refusal(self, window) -> dict | None: + if not self._on_app_page(window): + return {"status": "error", "message": "Settings can only be changed from InkDoc itself."} + if not self._owns_server: + # This window joined an InkDoc that was already running; that process + # does the saving and would never see a folder stored from here. + return { + "status": "error", + "message": "InkDoc is already open in another window. Change the save folder there.", + } + return None + + def _stored(self, value: str) -> dict: + result = {"status": "saved", "folder": None} + if not store_save_dir(value): + result["warning"] = "The folder is used until InkDoc closes, but it couldn't be saved for next time." + result["folder"] = folder_state() + return result + + def _on_app_page(self, window) -> bool: + """pywebview gives the bridge to any page the window shows, including one + reached through a link in a converted document; only InkDoc's own may use + the methods that write files or settings. + """ + return bool(self._app_origin) and _url_origin(window.get_current_url()) == self._app_origin + + +def _page_functions(controls: WindowControls) -> tuple: + """The functions the page can call as pywebview.api., one per method app.js uses. + + Registered with window.expose() instead of passing `controls` as js_api: + pywebview finds an exposed function by its exact name only, while a js_api + object would also let the page reach underscore-named helpers and the + objects they hold. + """ + + def open_external_link(url): + return controls.open_external_link(url) + + def toggle_maximize_window(): + return controls.toggle_maximize_window() + + def set_titlebar_button_rect(left, top, right, bottom): + return controls.set_titlebar_button_rect(left, top, right, bottom) + + def set_titlebar_colors(bg, bg_hover, fg, fg_hover): + return controls.set_titlebar_colors(bg, bg_hover, fg, fg_hover) + + def minimize_window(): + return controls.minimize_window() + + def close_window(): + return controls.close_window() + + def save_markdown(filename, content): + return controls.save_markdown(filename, content) + + def choose_save_folder(): + return controls.choose_save_folder() + + def reset_save_folder(): + return controls.reset_save_folder() + + return ( + open_external_link, + toggle_maximize_window, + set_titlebar_button_rect, + set_titlebar_colors, + minimize_window, + close_window, + save_markdown, + choose_save_folder, + reset_save_folder, + ) + _desktop_window = None @@ -675,11 +813,11 @@ def _daily_check() -> None: # make the whole surface draggable, so selecting text or dragging a file onto # the drop zone would move the window instead. easy_drag=False, - # Exposed on every platform, not just the frameless custom titlebar: - # open_external_link() is also used for regular in-page links. - js_api=window_controls, ) - window_controls.attach(window, app_url) + window_controls._attach(window, app_url, owns_server=server_thread is not None) + # Exposed on every platform, not just the frameless custom titlebar: + # open_external_link() is also used for regular in-page links. + window.expose(*_page_functions(window_controls)) _desktop_window = window # Warm markitdown only after the page has loaded, so the ~2s import does not diff --git a/app/server/server.py b/app/server/server.py index efd6e3c..4df6dbd 100644 --- a/app/server/server.py +++ b/app/server/server.py @@ -19,7 +19,7 @@ import threading from collections.abc import AsyncIterator from pathlib import Path -from typing import Any +from typing import Any, Literal from urllib.parse import urlparse logger = logging.getLogger("inkdoc.server") @@ -57,15 +57,20 @@ from app.core.converter import ( SUPPORTED_EXTENSIONS, ConversionOptions, - auto_save_markdown, convert_item, get_markitdown_version, ) from app.core.download_utils import SecurityError - from app.core.engine_manager import EngineManager, EngineStatus + from app.core.engine_manager import ( + LAYOUT_WIDTHS, + PREVIEW_TEXT_SIZES, + EngineManager, + EngineStatus, + ) from app.core.engines.docling_engine import get_docling_version, is_docling_available from app.core.engines.markit_engine import get_markit_version, is_markit_available from app.core.queue_model import EngineKind, QueueItem, SourceKind + from app.core.save_location import folder_state, save_markdown from app.core.security import SSRFValidationError, validate_url_for_ssrf from app.core.url_fetcher import ( UrlFetchError, @@ -218,11 +223,19 @@ def _perform_conversion( job.raise_if_cancelled() saved_path_str: str | None = None if save_to_downloads: - saved_path = auto_save_markdown(item, markdown_text) - saved_path_str = str(saved_path) + saved_path_str = _auto_save(item, markdown_text, options) return markdown_text, saved_path_str +def _auto_save(item: QueueItem, markdown_text: str, options: ConversionOptions) -> str: + """Write to the save folder (Downloads if that fails) and note any fallback on `options`.""" + saved_path, fallback = save_markdown(item, markdown_text) + if fallback: + options.save_fallback = fallback + options.engine_warnings = [*options.engine_warnings, fallback] + return str(saved_path) + + def _header_value(text: str) -> str: """A header-safe rendering: one line, at most 200 chars, latin-1 encodable.""" single = re.sub(r"[\r\n]+", " ", text)[:200] @@ -347,8 +360,7 @@ async def _convert_auto( saved_path_str: str | None = None if save_to_downloads: job.set_phase("saving", "Saving…") - saved_path = await asyncio.to_thread(auto_save_markdown, item, markdown_text) - saved_path_str = str(saved_path) + saved_path_str = await asyncio.to_thread(_auto_save, item, markdown_text, options) return markdown_text, saved_path_str @@ -382,6 +394,8 @@ def _conversion_payload(options: ConversionOptions, engine_kind: EngineKind) -> # Notes that are not failures, e.g. a GLM-OCR page cut short because the # model repeated itself, or GPU acceleration falling back to the CPU. "warnings": list(getattr(options, "engine_warnings", None) or []), + # Auto-save wrote to Downloads because the chosen save folder failed. + "save_fallback": getattr(options, "save_fallback", None), } @@ -397,6 +411,9 @@ def _markdown_response(markdown_text: str, payload: dict[str, Any]) -> PlainText resp.headers["X-Auto-Reason"] = _header_value(payload["auto"]["reason"]) if payload.get("warnings"): resp.headers["X-Engine-Warnings"] = _header_value(" | ".join(payload["warnings"])) + if payload.get("save_fallback"): + # Its own header too: in the joined warnings it could be cut off. + resp.headers["X-Save-Fallback"] = _header_value(payload["save_fallback"]) quality = payload["quality"] if quality: resp.headers["X-Quality-Warning"] = str(bool(quality.get("warning"))).lower() @@ -740,6 +757,27 @@ class SettingsPayload(BaseModel): "as fallback) or 'mirror_only' (for networks that block Hugging Face)." ), ) + layout_width: Literal["standard", "wide", "full"] | None = Field( + default=None, + description=( + "How much of a wide window the UI uses: 'standard' (up to 1400px), 'wide' " + "(up to 1920px) or 'full'. Applied from the next page load on." + ), + ) + preview_text_size: Literal[85, 90, 100, 110, 120, 135, 150] | None = Field( + default=None, + description=( + "Text size of the document preview in percent: 85, 90, 100 (the default), " + "110, 120, 135 or 150. Applied from the next page load on." + ), + ) + render_math: bool | None = Field( + default=None, + description=( + r"Draw LaTeX math ($...$, $$...$$, \(...\), \[...\]) in the preview with the " + "bundled KaTeX (default true). Never changes saved files or API output." + ), + ) # Resource and DoS protection limits @@ -830,6 +868,40 @@ async def _save_upload_to_temp(file: UploadFile, ext: str) -> str: return tmp_path +def _initial_layout_width() -> str: + """The saved layout width, or "standard" for anything unexpected. + + It is written into a script on the page, so only one of the known values + may ever come out of here, whatever a hand-edited settings.json holds. + """ + try: + value = EngineManager.get_instance().get_settings().get("layout_width") + except Exception: + return "standard" + return value if value in LAYOUT_WIDTHS else "standard" + + +def _initial_preview_text_size() -> int: + """The saved preview text size, or 100 for anything unexpected (see above).""" + try: + value = EngineManager.get_instance().get_settings().get("preview_text_size") + except Exception: + return 100 + # bool is an int in Python; True must not pass for 1. + if isinstance(value, bool) or value not in PREVIEW_TEXT_SIZES: + return 100 + return int(value) + + +def _initial_render_math() -> str: + """'off' only when math rendering was switched off; anything else is 'on'.""" + try: + value = EngineManager.get_instance().get_settings().get("render_math") + except Exception: + return "on" + return "off" if value is False else "on" + + def _serve_bench_html() -> HTMLResponse: index_file = UI_DIR / "index.html" if index_file.exists(): @@ -851,11 +923,19 @@ def _serve_bench_html() -> HTMLResponse: if custom_titlebar else "" ) + # The layout width and preview text size likewise, or the page would + # render at the defaults and then jump. + layout_width = _initial_layout_width() + text_size = _initial_preview_text_size() + render_math = _initial_render_math() token_injection = ( f'' ) if "" in html_content: @@ -1250,6 +1330,9 @@ def get_settings(): """Retrieve current user settings, plus anything startup had to correct.""" settings = dict(EngineManager.get_instance().get_settings()) settings["addon_reconciliation"] = _ADDON_RECONCILIATION + # The folder auto-save writes to now. Read-only here: it is chosen in the + # desktop app's folder dialog, never through POST /settings. + settings["save_folder"] = folder_state() return settings diff --git a/app/ui/app.js b/app/ui/app.js index 3a6a89c..0330d06 100644 --- a/app/ui/app.js +++ b/app/ui/app.js @@ -485,6 +485,8 @@ document.addEventListener("DOMContentLoaded", () => { const updateErrorBox = document.getElementById("updateErrorBox"); const updateErrorMsg = document.getElementById("updateErrorMsg"); const dailyUpdateToggle = document.getElementById("dailyUpdateToggle"); + const settingsUpdateDot = document.getElementById("settingsUpdateDot"); + const aboutVersion = document.getElementById("aboutVersion"); let updatePollInterval = null; let currentUpdateState = null; @@ -539,30 +541,24 @@ document.addEventListener("DOMContentLoaded", () => { } } - // Opens the Settings popover from anywhere in the page. When called from a - // click, that click must not reach the document-level "click outside closes - // Settings" handler, which would shut the popover again straight away. - function openSettings(e) { - if (e && typeof e.stopPropagation === "function") e.stopPropagation(); - settingsPopover.classList.add("open"); - loadSettings(); - refreshAddonStatus(); - refreshGlm(true); - } - // Bind unified pill clicks allEnginePills.forEach((btn) => { if (btn) { btn.addEventListener("click", (e) => { if (btn.dataset.engine === "docling" && btn.classList.contains("is-installable")) { - openSettings(e); + openSettings(e, "engines"); showToast("Docling is not installed yet. Click 'Install Docling' in Settings to enable it.", "info"); return; } if (btn.dataset.engine === "glm_ocr" && btn.classList.contains("is-installable")) { - openSettings(e); + openSettings(e, "engines"); const card = document.getElementById("engineCardGlmOcr"); - if (card) card.open = true; + if (card) { + card.open = true; + // Its buttons (Download, Verify…) to the middle of the pane: the card + // is the last and tallest, and at the bottom the toast would cover them. + (card.querySelector(".engine-actions") || card).scrollIntoView({ block: "center" }); + } showToast( glmStatus && glmStatus.installed ? "GLM-OCR didn't pass its self-test. Run it again in Settings." @@ -815,24 +811,166 @@ document.addEventListener("DOMContentLoaded", () => { } } - // ─── Settings Popover ──────────────────────────────────────────────────── + // ─── Settings Drawer ───────────────────────────────────────────────────── + // A panel on the right, below the header, with four sections. The gear, the + // X, Escape and a click outside close it; clicks in the confirm dialog and in + // toasts do not, since Settings opens those itself and they sit above it. + // Nothing here runs while it is closed. + const settingsScrim = document.getElementById("settingsScrim"); + const settingsCloseBtn = document.getElementById("settingsCloseBtn"); + const settingsNav = document.getElementById("settingsNav"); + const settingsNavItems = Array.from(document.querySelectorAll("[data-settings-section]")); + const settingsPanes = Array.from(document.querySelectorAll("[data-settings-pane]")); + const appHeader = document.querySelector(".app-header"); + const SETTINGS_FOCUSABLE = 'button, [href], input, select, textarea, summary, [tabindex]:not([tabindex="-1"])'; + let settingsSection = "general"; + let settingsReturnFocus = null; + + function isSettingsOpen() { + return settingsPopover.classList.contains("open"); + } + + // The drawer starts where the header ends, so the title bar stays usable. + // Not rounded: at 125% or 150% display scaling the header ends between + // pixels, and rounding would leave a hairline gap or overlap. + function placeSettings() { + if (!appHeader) return; + const top = Math.max(0, appHeader.getBoundingClientRect().bottom); + document.documentElement.style.setProperty("--settings-top", `${top}px`); + } + + function showSettingsSection(name) { + if (!settingsPanes.some((pane) => pane.dataset.settingsPane === name)) return; + settingsSection = name; + settingsPanes.forEach((pane) => { pane.hidden = pane.dataset.settingsPane !== name; }); + settingsNavItems.forEach((item) => { + if (item.dataset.settingsSection === name) item.setAttribute("aria-current", "page"); + else item.removeAttribute("aria-current"); + }); + } + + // Opens Settings from anywhere in the page: on `section` if given (callers + // outside the gear are all about an engine), otherwise where it was left. + // When called from a click, that click must not reach the document-level + // "click outside closes Settings" handler, which would shut it straight away. + function openSettings(e, section) { + if (e && typeof e.stopPropagation === "function") e.stopPropagation(); + if (section) showSettingsSection(section); + if (!isSettingsOpen()) { + settingsReturnFocus = document.activeElement; + placeSettings(); + settingsPopover.classList.add("open"); + document.documentElement.classList.add("settings-open"); // moves toasts beside it + settingsScrim.hidden = false; + settingsBtn.setAttribute("aria-expanded", "true"); + const current = settingsNavItems.find((item) => item.dataset.settingsSection === settingsSection); + (current || settingsPopover).focus(); + } + loadSettings(); + fetchUpdateStatus(); + refreshAddonStatus(); + refreshGlm(true); + } + + // restoreFocus is false when the user has moved on (clicked elsewhere, or + // dragged a file in): focus then stays where they put it. + function closeSettings({ restoreFocus = true } = {}) { + if (!isSettingsOpen()) return; + settingsPopover.classList.remove("open", "keyboard-nav"); + document.documentElement.classList.remove("settings-open"); + settingsScrim.hidden = true; + settingsBtn.setAttribute("aria-expanded", "false"); + const back = settingsReturnFocus; + settingsReturnFocus = null; + if (!restoreFocus) return; + const usable = back && back !== document.body && back.isConnected && back.getClientRects().length > 0; + (usable ? back : settingsBtn).focus(); + } + settingsBtn.addEventListener("click", (e) => { e.stopPropagation(); - const isOpen = settingsPopover.classList.toggle("open"); - if (isOpen) { - loadSettings(); - fetchUpdateStatus(); - refreshAddonStatus(); - refreshGlm(true); - } + if (isSettingsOpen()) closeSettings(); + else openSettings(e); }); + settingsCloseBtn.addEventListener("click", () => closeSettings()); + + // composedPath() is fixed when the click starts, so a button that re-renders + // its own card still counts as inside. document.addEventListener("click", (e) => { - if (!settingsPopover.contains(e.target) && e.target !== settingsBtn) { - settingsPopover.classList.remove("open"); + if (!isSettingsOpen()) return; + const path = typeof e.composedPath === "function" ? e.composedPath() : [e.target]; + const keepOpen = [settingsPopover, settingsBtn, appDialogOverlay, toastNotification]; + if (path.some((el) => keepOpen.includes(el))) return; + closeSettings({ restoreFocus: e.target === settingsScrim }); + }); + + // A file dragged towards the page is meant for the drop zone under the scrim. + settingsScrim.addEventListener("dragenter", () => closeSettings({ restoreFocus: false })); + + // While the confirm dialog is up, Escape and Tab belong to it. + // Tab is always moved here rather than left to the web engine: with macOS's + // "Keyboard navigation" off (the default), WebKit's own Tab skips buttons and + // would jump to the URL box behind the drawer. + document.addEventListener("keydown", (e) => { + if (!isSettingsOpen() || !appDialogOverlay.hidden) return; + if (e.key === "Escape") { + e.preventDefault(); + closeSettings(); + } else if (e.key === "Tab") { + // A radio group is one stop, on its selected option, as browsers do. + const focusables = Array.from(settingsPopover.querySelectorAll(SETTINGS_FOCUSABLE)) + .filter((el) => !el.disabled && el.getClientRects().length > 0 && !(el.type === "radio" && !el.checked)); + if (focusables.length === 0) return; + e.preventDefault(); + const idx = focusables.indexOf(document.activeElement); + const last = focusables.length - 1; + const next = e.shiftKey + ? (idx <= 0 ? last : idx - 1) + : (idx < 0 || idx === last ? 0 : idx + 1); + settingsPopover.classList.add("keyboard-nav"); + focusables[next].focus(); } }); + // WebKit shows :focus-visible for focus moved by script only if the + // previous focus showed it too, so after a mouse click the Tab handling + // above would never show a focus ring on macOS or Linux. While the drawer + // is used from the keyboard it carries .keyboard-nav, and the CSS shows the + // ring on :focus; a click or tap takes it away again. + settingsPopover.addEventListener("pointerdown", () => settingsPopover.classList.remove("keyboard-nav")); + + // Focused on click too: WebKit on macOS doesn't focus clicked buttons, and + // the arrow keys below start from the focused item. + settingsNavItems.forEach((item) => { + item.addEventListener("click", () => { + showSettingsSection(item.dataset.settingsSection); + item.focus(); + }); + }); + + // Arrow keys move between sections, as in the engine pill group. They start + // from the focused item, which Tab can move away from the shown section. + settingsNav.addEventListener("keydown", (e) => { + const keys = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End"]; + if (!keys.includes(e.key)) return; + e.preventDefault(); + let idx = settingsNavItems.indexOf(document.activeElement); + if (idx < 0) idx = settingsNavItems.findIndex((item) => item.dataset.settingsSection === settingsSection); + if (e.key === "Home") idx = 0; + else if (e.key === "End") idx = settingsNavItems.length - 1; + else if (e.key === "ArrowRight" || e.key === "ArrowDown") idx = (idx + 1) % settingsNavItems.length; + else idx = (idx - 1 + settingsNavItems.length) % settingsNavItems.length; + showSettingsSection(settingsNavItems[idx].dataset.settingsSection); + settingsPopover.classList.add("keyboard-nav"); + settingsNavItems[idx].focus(); + }); + + window.addEventListener("resize", () => { + if (isSettingsOpen()) placeSettings(); + }); + // ─── End settings drawer ───────────────────────────────────────────────── + // ─── Progress Polling for Engine Installation ──────────────────────────── async function pollDoclingProgress() { try { @@ -1802,6 +1940,11 @@ document.addEventListener("DOMContentLoaded", () => { if (doclingFormulaEnrichmentToggle && !doclingFormulaEnrichmentToggle.disabled) { doclingFormulaEnrichmentToggle.checked = Boolean(s.docling_formula_enrichment); } + if (s.layout_width !== undefined) syncLayoutWidth(s.layout_width); + if (s.preview_text_size !== undefined) syncTextSize(s.preview_text_size); + if (s.render_math !== undefined) syncRenderMath(s.render_math); + // Last, so nothing in the save folder row can stop the toggles above. + renderSaveFolder(s.save_folder); } } catch (_) {} } @@ -1926,6 +2069,134 @@ document.addEventListener("DOMContentLoaded", () => { }); } + // ─── Layout width ──────────────────────────────────────────────────────── + // How much of a wide window InkDoc uses (Settings → General → Display). The + // server puts the saved choice on before first paint, so the page + // never jumps on load. It is saved in settings.json, not browser storage, + // which the desktop app's web view clears on every launch. A change applies + // at once and is undone if it can't be saved. + const LAYOUT_WIDTHS = ["standard", "wide", "full"]; + const layoutWidthRadios = Array.from(document.querySelectorAll('input[name="layoutWidth"]')); + let layoutWidth = applyLayoutWidth(document.documentElement.dataset.layoutWidth); + let layoutWidthSaves = 0; // newest save wins; loadSettings waits for it + + function applyLayoutWidth(value) { + const width = LAYOUT_WIDTHS.includes(value) ? value : "standard"; + if (document.documentElement.dataset.layoutWidth !== width) { + document.documentElement.dataset.layoutWidth = width; + } + layoutWidthRadios.forEach((radio) => { radio.checked = radio.value === width; }); + return width; + } + + // From GET /settings, each time Settings opens. Skipped while a change is + // being saved, so a slower read can't put the old choice back. + function syncLayoutWidth(value) { + if (layoutWidthSaves === 0) layoutWidth = applyLayoutWidth(value); + } + + layoutWidthRadios.forEach((radio) => { + radio.addEventListener("change", async () => { + if (!radio.checked || radio.value === layoutWidth) return; + const previous = layoutWidth; + layoutWidth = applyLayoutWidth(radio.value); + const save = ++layoutWidthSaves; + let saved = false; + try { + const resp = await fetchWithToken(`${apiBase}/settings`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ layout_width: layoutWidth }), + }); + saved = resp.ok; + } catch (_) {} + if (save !== layoutWidthSaves) return; // a newer choice is on its way + layoutWidthSaves = 0; + if (!saved) { + layoutWidth = applyLayoutWidth(previous); + showToast("Couldn't save the layout width.", "error"); + } + }); + }); + // ─── End layout width ──────────────────────────────────────────────────── + + // ─── Preview text size ─────────────────────────────────────────────────── + // A− and A+ (Settings → General → Display) step through seven sizes for the + // document preview, Rendered and Raw. As with the layout width, the server + // puts the saved step on before first paint and settings.json keeps + // it. A click applies at once (the live sample and the preview follow through + // CSS); the save waits until the clicking stops, so several clicks are one + // write, and only one save is ever in flight. + const TEXT_SIZES = [85, 90, 100, 110, 120, 135, 150]; + const TEXT_SIZE_SAVE_DELAY_MS = 400; + const btnTextSmaller = document.getElementById("btnTextSmaller"); + const btnTextLarger = document.getElementById("btnTextLarger"); + const textSizeStatus = document.getElementById("textSizeStatus"); + let textSize = applyTextSize(Number(document.documentElement.dataset.textSize)); + let textSizeSaved = textSize; // what settings.json is known to hold + let textSizeTimer = null; + let textSizeSaving = false; + + // aria-disabled rather than disabled at the ends: a disabled button would + // drop keyboard focus the moment it reached the limit. + function applyTextSize(value) { + const size = TEXT_SIZES.includes(value) ? value : 100; + if (document.documentElement.dataset.textSize !== String(size)) { + document.documentElement.dataset.textSize = String(size); + } + if (btnTextSmaller) btnTextSmaller.setAttribute("aria-disabled", String(size === TEXT_SIZES[0])); + if (btnTextLarger) btnTextLarger.setAttribute("aria-disabled", String(size === TEXT_SIZES[TEXT_SIZES.length - 1])); + return size; + } + + function stepTextSize(direction) { + const idx = TEXT_SIZES.indexOf(textSize) + direction; + if (idx < 0 || idx >= TEXT_SIZES.length) return; + textSize = applyTextSize(TEXT_SIZES[idx]); + // No size is shown on screen; screen readers are told. + if (textSizeStatus) textSizeStatus.textContent = `Preview text size ${textSize}%`; + clearTimeout(textSizeTimer); + textSizeTimer = setTimeout(saveTextSize, TEXT_SIZE_SAVE_DELAY_MS); + } + + async function saveTextSize() { + textSizeTimer = null; + if (textSizeSaving) { + textSizeTimer = setTimeout(saveTextSize, TEXT_SIZE_SAVE_DELAY_MS); + return; + } + const value = textSize; + if (value === textSizeSaved) return; + textSizeSaving = true; + let saved = false; + try { + const resp = await fetchWithToken(`${apiBase}/settings`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ preview_text_size: value }), + }); + saved = resp.ok; + } catch (_) {} + textSizeSaving = false; + if (saved) { + textSizeSaved = value; + } else if (textSize === value && !textSizeTimer) { + // Not moved on since: show what is actually saved again. + textSize = applyTextSize(textSizeSaved); + showToast("Couldn't save the text size.", "error"); + } + } + + // From GET /settings, each time Settings opens; never over a pending change. + function syncTextSize(value) { + if (textSizeTimer || textSizeSaving) return; + textSize = textSizeSaved = applyTextSize(Number(value)); + } + + if (btnTextSmaller) btnTextSmaller.addEventListener("click", () => stepTextSize(-1)); + if (btnTextLarger) btnTextLarger.addEventListener("click", () => stepTextSize(1)); + // ─── End preview text size ─────────────────────────────────────────────── + // ─── Update Management UI ───────────────────────────────────────────────── function formatBytes(bytes) { if (!bytes || bytes <= 0) return "0 B"; @@ -1942,8 +2213,14 @@ document.addEventListener("DOMContentLoaded", () => { if (appCurrentVersionBadge) { appCurrentVersionBadge.textContent = `v${s.current_version || "—"}`; } + if (aboutVersion) aboutVersion.textContent = `Version ${s.current_version || "—"}`; const state = s.state || "idle"; + // Marks Settings → Updates while an update waits or is on its way. Left as + // is while checking, so "Check again" doesn't make it blink. + if (settingsUpdateDot && state !== "checking") { + settingsUpdateDot.hidden = !["available", "downloading", "verifying", "ready_to_install", "ready_to_reveal"].includes(state); + } if (state === "checking") { if (btnCheckUpdates) btnCheckUpdates.disabled = true; @@ -1985,9 +2262,13 @@ document.addEventListener("DOMContentLoaded", () => { if (s.release_notes && s.release_notes.trim()) { try { - const rawHtml = marked.parse(s.release_notes); - const cleanHtml = window.DOMPurify ? window.DOMPurify.sanitize(rawHtml) : rawHtml; - if (updateNotesContent) updateNotesContent.innerHTML = cleanHtml; + if (window.DOMPurify) { + const cleanHtml = window.DOMPurify.sanitize(marked.parse(s.release_notes)); + if (updateNotesContent) updateNotesContent.innerHTML = cleanHtml; + } else if (updateNotesContent) { + // No sanitiser loaded: show the notes as text, never as HTML. + updateNotesContent.textContent = s.release_notes; + } if (updateNotesAccordion) updateNotesAccordion.style.display = "block"; } catch (_) { if (updateNotesContent) updateNotesContent.textContent = s.release_notes; @@ -2357,6 +2638,26 @@ document.addEventListener("DOMContentLoaded", () => { } } + // ─── Links in converted documents and release notes ───────────────────── + // In the desktop app a plain link would load the other page inside InkDoc's + // own window. Web links open in the system browser instead, and no other + // link may replace the app; links to a heading in the same document still + // scroll. In a browser tab (Web Bench) links behave as before. + function handleContentLinkClick(e) { + if (!window.pywebview) return; + const link = e.target && e.target.closest ? e.target.closest("a[href]") : null; + if (!link) return; + const href = link.getAttribute("href") || ""; + if (href.startsWith("#")) return; + e.preventDefault(); + if (/^https?:\/\//i.test(href)) { + withWindowApi((api) => Promise.resolve(api.open_external_link(href)).catch(() => {})); + } + } + if (renderedOutput) renderedOutput.addEventListener("click", handleContentLinkClick); + if (updateNotesContent) updateNotesContent.addEventListener("click", handleContentLinkClick); + // ─── End content links ─────────────────────────────────────────────────── + // ─── External Links (Settings) ───────────────────────────────────────────── // Inside the desktop app, route through pywebview so links open in the OS // default browser instead of navigating the app's own window. In a plain @@ -2468,15 +2769,59 @@ document.addEventListener("DOMContentLoaded", () => { // ─── Theme Toggle ──────────────────────────────────────────────────────── const savedTheme = readStorage("inkdoc-theme") || readStorage("markitdown-theme") || "dark"; document.documentElement.setAttribute("data-theme", savedTheme); + // The theme the last click asked for. A View Transition applies it a frame + // later, so a quick second click must not read it back from the page. + let requestedTheme = document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark"; - themeToggle.addEventListener("click", () => { - const current = document.documentElement.getAttribute("data-theme"); - const next = current === "dark" ? "light" : "dark"; - document.documentElement.setAttribute("data-theme", next); - writeStorage("inkdoc-theme", next); + function applyTheme(theme) { + document.documentElement.setAttribute("data-theme", theme); + writeStorage("inkdoc-theme", theme); // The maximise button is drawn natively, so it can't pick up the new // palette from CSS on its own. if (window.__inkdocSyncTitlebar) window.__inkdocSyncTitlebar(); + } + + // The new theme grows as a circle from the button, as on the website, where + // the web engine supports View Transitions. Without them, or with reduced + // motion, it switches at once as before. + themeToggle.addEventListener("click", () => { + requestedTheme = requestedTheme === "dark" ? "light" : "dark"; + const next = requestedTheme; + const root = document.documentElement; + const reduceMotion = typeof window.matchMedia === "function" + && window.matchMedia("(prefers-reduced-motion: reduce)").matches; + if (reduceMotion || typeof document.startViewTransition !== "function") { + applyTheme(next); + return; + } + + const r = themeToggle.getBoundingClientRect(); + const x = r.left + r.width / 2; + const y = r.top + r.height / 2; + const radius = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y)); + // Pause the colour fades, or the circle would reveal half-faded colours. + root.classList.add("theme-switching"); + const finish = () => root.classList.remove("theme-switching"); + + let transition; + try { + transition = document.startViewTransition(() => applyTheme(next)); + } catch (_) { + applyTheme(next); + finish(); + return; + } + // Short on purpose: while a View Transition plays, the engine sends clicks + // and drops to the page background, where they are ignored. + transition.ready + .then(() => { + root.animate( + { clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`] }, + { duration: 350, easing: "cubic-bezier(.2,.8,.2,1)", pseudoElement: "::view-transition-new(root)" } + ); + }) + .catch(() => {}); + transition.finished.then(finish, finish); }); // ─── Auto-save Toggle ──────────────────────────────────────────────────── @@ -2484,8 +2829,8 @@ document.addEventListener("DOMContentLoaded", () => { autoSaveToDownloads = e.target.checked; showToast( autoSaveToDownloads - ? "Converted files will also be saved to ~/Downloads" - : "Auto-save to Downloads disabled", + ? `Converted files will also be saved to ${saveFolderState ? saveFolderState.path : "Downloads"}` + : "Auto-save disabled", "info" ); }); @@ -2831,7 +3176,7 @@ document.addEventListener("DOMContentLoaded", () => { item.errorType = "failed"; selectItem(item.id); showToast(isGlm ? item.error : "Docling is not installed. Install in Settings.", "error"); - openSettings(); + openSettings(null, "engines"); return; } @@ -2866,7 +3211,7 @@ document.addEventListener("DOMContentLoaded", () => { showToast(hint, "info", { duration: 9000, actions: [ - { label: hint.includes("Docling") ? "Install Docling" : "Open Settings", onClick: (e) => openSettings(e) }, + { label: hint.includes("Docling") ? "Install Docling" : "Open Settings", onClick: (e) => openSettings(e, "engines") }, { label: "Don't show again", onClick: () => writeStorage(HIDE_AUTO_HINT_KEY, "1") }, ], }); @@ -2912,6 +3257,7 @@ document.addEventListener("DOMContentLoaded", () => { showToast(`Converted ${label} via ${via}`, "success"); } } + noteSaveFallback(data.save_fallback); } function applyNetworkError(item, err, engineToUse, isUrl) { @@ -3214,6 +3560,363 @@ document.addEventListener("DOMContentLoaded", () => { }); } + // ─── Table scroll boxes ────────────────────────────────────────────────── + // Each rendered table goes in its own box that scrolls sideways, so a wide + // table no longer drags the whole document with it. Nothing is measured: + // one wrapper per table, no layout read, so it stays cheap on long documents. + function wrapTables(root) { + root.querySelectorAll("table").forEach((table) => { + if (table.parentElement && table.parentElement.classList.contains("table-scroll")) return; + const box = document.createElement("div"); + box.className = "table-scroll"; + table.replaceWith(box); + box.appendChild(table); + }); + } + // ─── End table scroll boxes ────────────────────────────────────────────── + + // ─── Math formulas (built-in add-on: KaTeX) ────────────────────────────── + // LaTeX from Docling's formula recognition and GLM-OCR ($…$, $$…$$, \(…\), + // \[…\]) is drawn with KaTeX in the Rendered tab. Switched off in Settings → + // Add-ons, or for a document with no delimiter at all, the Markdown goes + // through the global parser exactly as before. KaTeX is bundled and loaded + // only the first time a document has math; formulas on screen are drawn + // first and the rest in short slices, so the window never blocks. Saved + // files, Copy, Download and the Raw tab always use the Markdown itself. + const KATEX_JS = "/static/vendor/katex/katex.min.js"; + const KATEX_CSS = "/static/vendor/katex/katex.min.css"; + const MATH_HINT = /\$|\\\(|\\\[/; + const MATH_INLINE_MAX = 1000; // characters in one inline formula + const MATH_MAX = 5000; // a display formula longer than this stays text + const MATH_LOOKAHEAD = 512; // one search for an inline formula reads no further + const MATH_DOC_MAX = 2000000; // larger documents are parsed exactly as before + const MATH_SLICE_MS = 6; // KaTeX work per slice… + const MATH_SLICE_MAX = 8; // …and formulas per slice: the browser lays + // out what was inserted after each slice + const MATH_CACHE_MAX = 500; + const KATEX_OPTIONS = { throwOnError: true, trust: false, strict: "ignore", maxSize: 20, maxExpand: 500, output: "htmlAndMathml" }; + // KaTeX's holds the LaTeX again; DOMPurify drops the element but + // would keep its text inside , so drop the text too. + const MATH_SANITIZE = { ADD_FORBID_CONTENTS: ["annotation"] }; + // Placeholders carry this per-session value and only they are drawn, so + // look-alike HTML written into a document (class="math-tex") never is. + const MATH_MARK = mathMark(); + const mathToggle = document.getElementById("mathToggle"); + let renderMath = document.documentElement.dataset.renderMath !== "off"; + let mathParser = null; + let katexLoader = null; + let mathRun = 0; // every render bumps it; older work stops + let mathObserver = null; + let mathSaves = 0; + const mathCache = new Map(); // "D"/"I" + LaTeX → sanitised KaTeX HTML, or null if it failed + + function mathMark() { + const bytes = new Uint8Array(8); + if (window.crypto && typeof window.crypto.getRandomValues === "function") { + window.crypto.getRandomValues(bytes); + } else { + for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256); + } + return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); + } + + function mathPlaceholder(tex, display) { + const tag = display ? "div" : "span"; + const cls = display ? "math-tex math-block" : "math-tex"; + return `<${tag} class="${cls}" data-math="${MATH_MARK}" data-display="${display ? 1 : 0}">${escapeHtml(tex)}`; + } + + // $$…$$ or \[…\] starting a line, on one line (Docling) or across lines + // (GLM-OCR). A blank line inside means it isn't a formula, so a stray $$ + // can never swallow the rest of the document. Every search here is + // bounded (the paragraph, or MATH_MAX), so parsing stays linear: marked + // asks again before every block, and unbounded searches made a large + // document with one $ take tens of seconds (security review, 1.2.0). + const MATH_BLOCK = { + name: "mathBlock", + level: "block", + // marked passes the rest of the current block from its second character + // on, so only the following lines can start a formula, and a blank line + // ends the paragraph it would cut, so nothing past it matters here. + start(src) { + let i = src.indexOf("\n"); + while (i !== -1) { + i++; + let j = i; + while (j < src.length && (src[j] === " " || src[j] === "\t")) j++; + if (j >= src.length || src[j] === "\n") return undefined; + if (j - i <= 3 && (src.startsWith("$$", j) || src.startsWith("\\[", j))) return i; + i = src.indexOf("\n", j); + } + return undefined; + }, + tokenizer(src) { + const m = /^[ \t]{0,3}(\$\$|\\\[)/.exec(src); + if (!m) return undefined; + const close = m[1] === "$$" ? "$$" : "\\]"; + const from = m[0].length; + const end = src.slice(from, from + MATH_MAX + close.length).indexOf(close); + if (end < 0) return undefined; + const body = src.slice(from, from + end); + const tex = body.trim(); + if (!tex || /\n[ \t]*\n/.test(body)) return undefined; + let after = from + end + close.length; + while (src[after] === " " || src[after] === "\t") after++; + if (after < src.length && src[after] !== "\n") return undefined; + if (src[after] === "\n") after++; + return { type: "mathBlock", raw: src.slice(0, after), text: tex }; + }, + renderer(token) { return mathPlaceholder(token.text, true) + "\n"; }, + }; + + // \(…\), $…$ and $$…$$ inside a line. For $, Pandoc's rule, which keeps + // prices out: the opening $ is not followed by a space, the closing $ is + // not preceded by a space nor followed by a digit; here the formula also + // stays on one line and never runs into a backtick, so a price can't pair + // with a $ inside inline code. \$ is always a literal dollar. Code spans + // and code blocks are read by marked before this rule can see them. + const MATH_INLINE = { + name: "mathInline", + level: "inline", + // Looks at most MATH_LOOKAHEAD characters ahead. When no formula can start + // there, it hands marked a point to cut the text at (a space if there is + // one, so links aren't split) and is asked again from there: each call + // costs the same, however long the paragraph. + start(src) { + const limit = Math.min(src.length, MATH_LOOKAHEAD); + let cut = -1; + for (let i = 0; i < limit; i++) { + const c = src[i]; + if (c === "$") return i; + if (c === "\\") { + if (src[i + 1] === "(") return i; + i++; // skip whatever is escaped, \$ included + continue; + } + if (c === " " || c === "\n" || c === "\t") cut = i; + } + if (limit === src.length) return undefined; + return cut > 0 ? cut : limit - 1; + }, + tokenizer(src) { + if (src.startsWith("\\(")) { + const end = src.slice(2, 2 + MATH_INLINE_MAX + 2).indexOf("\\)"); + const tex = end > 0 ? src.slice(2, 2 + end) : ""; + if (!tex.trim() || tex.includes("\n\n")) return undefined; + return { type: "mathInline", raw: src.slice(0, end + 4), text: tex.trim(), display: false }; + } + if (src[0] !== "$") return undefined; + const display = src[1] === "$"; + const open = display ? 2 : 1; + const first = src[open]; + if (!first || first === "$" || /\s/.test(first)) return undefined; + let i = open; + while (i < src.length && i - open <= MATH_INLINE_MAX) { + const c = src[i]; + if (c === "\\") { i += 2; continue; } + if (c === "\n" || c === "`") return undefined; + if (c === "$") break; + i++; + } + if (src[i] !== "$" || i - open > MATH_INLINE_MAX) return undefined; + if (display && src[i + 1] !== "$") return undefined; + const tex = src.slice(open, i); + if (/\s$/.test(tex)) return undefined; + const close = i + (display ? 2 : 1); + if (/\d/.test(src[close] || "")) return undefined; + return { type: "mathInline", raw: src.slice(0, close), text: tex, display }; + }, + renderer(token) { return mathPlaceholder(token.text, token.display); }, + }; + + // The one place the preview's Markdown becomes HTML. + function markdownToHtml(md) { + if (!renderMath || md.length > MATH_DOC_MAX || !MATH_HINT.test(md) || typeof window.marked.Marked !== "function") { + return window.marked.parse(md); + } + if (!mathParser) { + mathParser = new window.marked.Marked({ gfm: true, breaks: true }); + mathParser.use({ extensions: [MATH_BLOCK, MATH_INLINE] }); + } + return mathParser.parse(md); + } + + // Script and stylesheet together: KaTeX's HTML is unreadable without its + // CSS, so if either fails the formulas simply stay as LaTeX text. If the + // script ran but didn't register window.katex (a document can shadow the + // globals its loader checks, e.g. ), the next document may + // try again; a missing file is not retried. + function loadKatex() { + if (katexLoader) return katexLoader; + katexLoader = new Promise((resolve) => { + const done = (ok) => { + const k = window.katex; + const usable = ok && k && typeof k.renderToString === "function"; + if (ok && !usable) katexLoader = null; + resolve(usable ? k : null); + }; + const css = document.createElement("link"); + css.rel = "stylesheet"; + css.href = KATEX_CSS; + const cssLoaded = new Promise((r) => { css.onload = () => r(true); css.onerror = () => r(false); }); + const js = document.createElement("script"); + js.src = KATEX_JS; + js.async = true; + const jsLoaded = new Promise((r) => { js.onload = () => r(true); js.onerror = () => r(false); }); + document.head.appendChild(css); + document.head.appendChild(js); + Promise.all([cssLoaded, jsLoaded]).then(([a, b]) => { + if (!a || !b) console.warn("Math formulas: KaTeX could not be loaded; showing LaTeX as text."); + done(a && b); + }); + }); + return katexLoader; + } + + function typesetOne(node, katex) { + const tex = node.textContent; + const display = node.dataset.display === "1"; + // The parser already keeps to these limits; checked again here because + // KaTeX's output grows with its input and one formula is drawn at once. + if (tex.length > (display ? MATH_MAX : MATH_INLINE_MAX)) { + node.classList.add("math-error"); + node.title = "Too long to render as a formula"; + return; + } + const key = (display ? "D" : "I") + tex; + let html = mathCache.get(key); + if (html === undefined) { + try { + html = window.DOMPurify.sanitize(katex.renderToString(tex, { ...KATEX_OPTIONS, displayMode: display }), MATH_SANITIZE); + } catch (_) { + html = null; + } + if (mathCache.size >= MATH_CACHE_MAX) mathCache.delete(mathCache.keys().next().value); + mathCache.set(key, html); + } + if (html === null) { + node.classList.add("math-error"); + node.title = "Couldn't render this formula"; + return; + } + node.innerHTML = html; + node.classList.add("math-done"); + } + + function cancelMath() { + mathRun++; + if (mathObserver) { + mathObserver.disconnect(); + mathObserver = null; + } + } + + // Formulas on screen (or within 600px of it) first, then the rest in + // document order, in slices of MATH_SLICE_MS. setTimeout rather than + // requestIdleCallback, which WebKit (macOS, Linux) doesn't have. + function typesetMath(root) { + cancelMath(); + const run = mathRun; + if (!renderMath || !window.DOMPurify) return; + const nodes = Array.from(root.querySelectorAll(`.math-tex[data-math="${MATH_MARK}"]`)); + if (nodes.length === 0) return; + loadKatex().then((katex) => { + if (!katex || run !== mathRun) return; + const left = new Set(nodes); + const soon = []; + let next = 0; + let queued = false; + // Only the first report matters: it says which formulas are on screen + // now. After that the rest go in document order anyway, and keeping + // hundreds of targets observed would cost a check on every frame. + if (typeof window.IntersectionObserver === "function") { + mathObserver = new window.IntersectionObserver((entries) => { + entries.forEach((e) => { if (e.isIntersecting && left.has(e.target)) soon.push(e.target); }); + if (mathObserver) mathObserver.disconnect(); + mathObserver = null; + kick(); + }, { root: root.closest(".tab-content"), rootMargin: "600px 0px" }); + nodes.forEach((n) => mathObserver.observe(n)); + } + function take() { + while (soon.length) { + const n = soon.shift(); + if (left.has(n)) return n; + } + while (next < nodes.length) { + const n = nodes[next++]; + if (left.has(n)) return n; + } + return null; + } + function kick() { + if (queued) return; + queued = true; + setTimeout(step, 0); + } + function step() { + queued = false; + if (run !== mathRun) return; + const until = performance.now() + MATH_SLICE_MS; + let count = 0; + do { + const n = take(); + if (!n) { + if (mathObserver) mathObserver.disconnect(); + mathObserver = null; + return; + } + left.delete(n); + typesetOne(n, katex); + } while (++count < MATH_SLICE_MAX && performance.now() < until); + kick(); + } + kick(); + }); + } + + // The switch: off means the preview is parsed exactly as before and KaTeX + // is never loaded. Applies at once (the open document is drawn again) and + // is kept in settings.json; the server puts it on before first paint. + function applyRenderMath(on) { + renderMath = on; + document.documentElement.dataset.renderMath = on ? "on" : "off"; + if (mathToggle) mathToggle.checked = on; + if (activeItemId) selectItem(activeItemId); + } + + // From GET /settings, each time Settings opens; never over a pending save. + function syncRenderMath(value) { + const on = value !== false; + if (mathSaves === 0 && on !== renderMath) applyRenderMath(on); + } + + if (mathToggle) { + mathToggle.checked = renderMath; + mathToggle.addEventListener("change", async () => { + const on = mathToggle.checked; + const before = renderMath; + applyRenderMath(on); + const save = ++mathSaves; + let saved = false; + try { + const resp = await fetchWithToken(`${apiBase}/settings`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ render_math: on }), + }); + saved = resp.ok; + } catch (_) {} + if (save !== mathSaves) return; // a newer change decides + mathSaves = 0; + if (!saved) { + applyRenderMath(before); + showToast("Couldn't save the math setting.", "error"); + } + }); + } + // ─── End math formulas ─────────────────────────────────────────────────── + // ─── Select & Display Item ─────────────────────────────────────────────── const EMPTY_HINT_SVG = ``; @@ -3248,7 +3951,7 @@ document.addEventListener("DOMContentLoaded", () => { function qualityNoticeActions(item) { const action = qualityAction(item.quality); if (!action) return []; - if (action.install) return [{ label: action.label, onClick: (e) => openSettings(e) }]; + if (action.install) return [{ label: action.label, onClick: (e) => openSettings(e, "engines") }]; if (!(item.fileRef || item.sourceUrl)) return []; return [{ label: action.label, onClick: () => reconvertItem(item, action.engine) }]; } @@ -3292,6 +3995,7 @@ document.addEventListener("DOMContentLoaded", () => { } function selectItem(id) { + cancelMath(); // formulas still being drawn belong to the previous preview activeItemId = id; const item = queue.find((i) => i.id === id); if (!item) return; @@ -3425,7 +4129,7 @@ document.addEventListener("DOMContentLoaded", () => { ); } else if (item.auto && item.auto.hint) { showConversionNotice("info", "Tip", item.auto.hint, { - actions: [{ label: item.auto.hint.includes("Docling") ? "Install Docling" : "Open Settings", onClick: (e) => openSettings(e) }], + actions: [{ label: item.auto.hint.includes("Docling") ? "Install Docling" : "Open Settings", onClick: (e) => openSettings(e, "engines") }], details, }); } else if (item.warnings && item.warnings.length) { @@ -3442,7 +4146,7 @@ document.addEventListener("DOMContentLoaded", () => { window.marked.use({ gfm: true, breaks: true }); } } catch (_) {} - const rawHtml = window.marked.parse(md); + const rawHtml = markdownToHtml(md); if (window.DOMPurify) { renderedOutput.innerHTML = window.DOMPurify.sanitize(rawHtml); } else { @@ -3451,7 +4155,9 @@ document.addEventListener("DOMContentLoaded", () => { } else { renderedOutput.innerHTML = renderBasicMarkdown(md); } + wrapTables(renderedOutput); highlightCodeBlocks(renderedOutput); + typesetMath(renderedOutput); renderQueue(); } @@ -3515,6 +4221,72 @@ document.addEventListener("DOMContentLoaded", () => { } // ─── End desktop save ───────────────────────────────────────────────────── + // ─── Save folder ───────────────────────────────────────────────────────── + // Where auto-save writes. Shown everywhere, changed only in the desktop app: + // the path comes from the system's folder dialog (pywebview.api), never from + // this page, and no HTTP request can set it. + const saveFolderPath = document.getElementById("saveFolderPath"); + const saveFolderProblem = document.getElementById("saveFolderProblem"); + const saveFolderHint = document.getElementById("saveFolderHint"); + const btnChangeSaveFolder = document.getElementById("btnChangeSaveFolder"); + const btnResetSaveFolder = document.getElementById("btnResetSaveFolder"); + let saveFolderState = null; + // Each fallback sentence is shown once per session, so a batch of files into + // a missing folder gives one notice rather than one per file. + const shownSaveFallbacks = new Set(); + + function renderSaveFolder(state) { + if (!state || !saveFolderPath) return; + saveFolderState = state; + const shown = state.custom ? state.chosen : state.path; + // The path box is right-to-left so a long path keeps its end visible; the + // marks stop that from moving a leading "/" or drive letter to the other side. + saveFolderPath.textContent = `\u200E${shown}\u200E`; + saveFolderPath.title = shown; + saveFolderProblem.textContent = state.problem || ""; + saveFolderProblem.style.display = state.problem ? "" : "none"; + const desktop = Boolean(window.pywebview); + btnChangeSaveFolder.style.display = desktop ? "" : "none"; + btnResetSaveFolder.style.display = desktop && state.custom ? "" : "none"; + saveFolderHint.style.display = desktop ? "none" : ""; + } + + function changeSaveFolder(method) { + withWindowApi((api) => { + btnChangeSaveFolder.disabled = true; + btnResetSaveFolder.disabled = true; + Promise.resolve() + .then(() => api[method]()) + .then((res) => { + if (res && res.status === "saved") { + renderSaveFolder(res.folder); + shownSaveFallbacks.clear(); + if (res.warning) showToast(res.warning, "info"); + else showToast(`Auto-save now uses ${res.folder.path}`, "success"); + } else if (!res || res.status !== "cancelled") { + showToast(`Could not change the save folder: ${(res && res.message) || "unknown error"}`, "error"); + } + }) + .catch((err) => { + showToast(`Could not change the save folder: ${(err && err.message) || err}`, "error"); + }) + .finally(() => { + btnChangeSaveFolder.disabled = false; + btnResetSaveFolder.disabled = false; + }); + }); + } + + function noteSaveFallback(message) { + if (!message || shownSaveFallbacks.has(message)) return; + shownSaveFallbacks.add(message); + showToast(message, "info"); + } + + if (btnChangeSaveFolder) btnChangeSaveFolder.addEventListener("click", () => changeSaveFolder("choose_save_folder")); + if (btnResetSaveFolder) btnResetSaveFolder.addEventListener("click", () => changeSaveFolder("reset_save_folder")); + // ─── End save folder ───────────────────────────────────────────────────── + btnDownloadMarkdown.addEventListener("click", () => { const item = queue.find((i) => i.id === activeItemId); if (!item || !item.markdown) { diff --git a/app/ui/index.html b/app/ui/index.html index 4b0a2a1..e8bbcf8 100644 --- a/app/ui/index.html +++ b/app/ui/index.html @@ -48,381 +48,498 @@

InkDoc

- +
- -
-
-

Preferences

- - - -

If Docling encounters an error, automatically re-run using MarkItDown. The preview will explicitly indicate when fallback occurred.

- - -

Compares the result with the PDF's embedded text and warns if part of it was lost. Read-only; never changes the saved file.

- - -

Automatically checks GitHub Releases for new signed updates on startup (desktop only).

+ + -
- -
-
diff --git a/app/ui/style.css b/app/ui/style.css index 1aeb54b..79b73cc 100644 --- a/app/ui/style.css +++ b/app/ui/style.css @@ -154,13 +154,54 @@ body { transition: background-color var(--transition-normal), color var(--transition-normal); } +/* Theme switch circle reveal (View Transitions, where supported; app.js). + Only the circle animates: no default cross-fade, and no colour fades + inside it. */ +::view-transition-old(root), +::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; +} + +html.theme-switching, +html.theme-switching * { + transition: none !important; +} + +/* Layout width (Settings → General → Display). The server puts the saved + choice on before first paint; no attribute means Standard. Never + transitioned: animating it would re-lay out a long document every frame. */ +:root { + --layout-max: 1400px; + --preview-scale: 1; +} + +/* Preview text size (Settings → General → Display), one rule per step; the + server puts the saved step on before first paint. Every font size in + the preview is calc( * --preview-scale), so 100% is exactly the + former size. Never transitioned, for the same reason as the width. */ +html[data-text-size="85"] { --preview-scale: 0.85; } +html[data-text-size="90"] { --preview-scale: 0.9; } +html[data-text-size="110"] { --preview-scale: 1.1; } +html[data-text-size="120"] { --preview-scale: 1.2; } +html[data-text-size="135"] { --preview-scale: 1.35; } +html[data-text-size="150"] { --preview-scale: 1.5; } + +html[data-layout-width="wide"] { + --layout-max: 1920px; +} + +html[data-layout-width="full"] { + --layout-max: none; +} + .app-container { display: flex; flex-direction: column; height: 100%; height: 100dvh; max-height: 100dvh; - max-width: 1400px; + max-width: var(--layout-max); margin: 0 auto; padding: 0 24px; /* No overflow:hidden here — .app-header intentionally breaks out to full @@ -364,26 +405,254 @@ body { position: relative; } -.settings-popover { +/* Settings drawer: fixed to the right edge, from the bottom of the header + (--settings-top, set by app.js on open) to the bottom of the window, so the + title bar stays draggable and its buttons stay clickable. Below the confirm + dialog (500) and toasts (999), which Settings itself opens. */ +:root { + --settings-width: clamp(520px, 44vw, 680px); +} + +/* While Settings is open, toasts sit beside it over the dimmed page, so they + never cover its buttons. Only where there is room for both: always in the + desktop app, whose window is at least 960px wide. */ +@media (min-width: 960px) { + .settings-open .toast-notification { + right: calc(var(--settings-width) + 24px); + } +} + +.settings-scrim { + position: fixed; + top: var(--settings-top, 64px); + right: 0; + bottom: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.35); + z-index: 199; + animation: fadeIn 0.15s ease; +} + +.settings-scrim[hidden] { display: none; - position: absolute; - top: calc(100% + 10px); +} + +.settings-drawer { + display: none; + position: fixed; + top: var(--settings-top, 64px); right: 0; - width: 320px; - max-width: calc(100vw - 32px); - max-height: calc(100vh - 70px); - max-height: calc(100dvh - 70px); - overflow-y: auto; + bottom: 0; + width: var(--settings-width); + max-width: 100vw; + flex-direction: column; background-color: var(--elevated); - border: 1px solid var(--rule); - border-radius: 12px; + border-left: 1px solid var(--rule); box-shadow: var(--shadow-popover); z-index: 200; } -.settings-popover.open { - display: block; - animation: fadeSlideIn 0.15s ease; +.settings-drawer:focus { + outline: none; +} + +.settings-drawer.open { + display: flex; + animation: settingsDrawerIn 0.18s ease-out; +} + +@keyframes settingsDrawerIn { + from { + opacity: 0; + transform: translateX(16px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +.settings-drawer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px 10px 18px; + border-bottom: 1px solid var(--rule); + flex-shrink: 0; +} + +.settings-drawer-title { + font-size: 15px; + font-weight: 600; + color: var(--ink); +} + +.settings-close { + width: 30px; + height: 30px; +} + +.settings-drawer-body { + display: grid; + grid-template-columns: 148px minmax(0, 1fr); + flex: 1; + min-height: 0; +} + +.settings-nav { + display: flex; + flex-direction: column; + gap: 2px; + padding: 10px 8px; + border-right: 1px solid var(--rule); + background-color: var(--sunken); + overflow-y: auto; +} + +.settings-nav-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + width: 100%; + padding: 7px 10px; + border: none; + border-radius: 6px; + background: none; + color: var(--muted); + font: inherit; + font-size: 13px; + font-weight: 500; + text-align: left; + cursor: pointer; + transition: background-color var(--transition-fast), color var(--transition-fast); +} + +.settings-nav-item:hover { + color: var(--ink); + background-color: var(--elevated); +} + +.settings-nav-item[aria-current="page"] { + color: var(--mark); + background-color: var(--mark-tint); +} + +.settings-nav-item:focus-visible { + outline: 2px solid var(--mark); + outline-offset: -2px; +} + +/* Shown on Updates while an update is available or on its way. */ +.settings-nav-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background-color: var(--mark); + flex-shrink: 0; +} + +.settings-nav-dot[hidden] { + display: none; +} + +/* Each pane scrolls on its own, so switching back keeps its place. */ +.settings-pane { + grid-column: 2; + grid-row: 1; + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; +} + +.settings-pane[hidden] { + display: none; +} + +/* About */ +.about-head { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +.about-logo { + width: 40px; + height: 40px; + flex-shrink: 0; +} + +.about-name { + font-size: 15px; + font-weight: 600; + color: var(--ink); +} + +.about-version { + font-family: var(--font-mono); + font-size: 11px; + color: var(--faint); +} + +.about-text { + font-size: 13px; + line-height: 1.6; + color: var(--muted); +} + +.about-read-more { + font-size: 12px; + font-weight: 500; + color: var(--mark); + text-decoration: none; + white-space: nowrap; +} + +.about-read-more:hover { + text-decoration: underline; +} + +.about-read-more:focus-visible { + outline: 2px solid var(--mark); + outline-offset: 2px; + border-radius: 2px; +} + +/* Narrow browser tabs (Web Bench; the desktop window is at least 960px wide): + full width, with the sections as a row of tabs above the content. */ +@media (max-width: 640px) { + :root { + --settings-width: 100vw; + } + + .settings-drawer { + border-left: none; + } + + .settings-drawer-body { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr); + } + + .settings-nav { + flex-direction: row; + border-right: none; + border-bottom: 1px solid var(--rule); + overflow-x: auto; + overflow-y: hidden; + } + + .settings-nav-item { + width: auto; + flex-shrink: 0; + } + + .settings-pane { + grid-column: 1; + grid-row: 2; + } } @keyframes fadeSlideIn { @@ -418,6 +687,231 @@ body { line-height: 1.45; } +/* Save folder (Preferences): where auto-save writes. */ +.save-folder { + margin-top: 10px; +} + +.save-folder-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.save-folder-label { + font-size: 13px; + color: var(--ink); +} + +/* Display (General): layout width as a three-way segmented control. Real + radio buttons, hidden with .sr-only, so arrow keys and screen readers + treat it as one group. */ +.settings-display { + border-top: 1px solid var(--rule); +} + +/* Unrelated groups within a section (Preferences: saving, Docling fallback, + PDF check) get a thin line between them, inset by the section's padding so + it reads lighter than the full-width line between sections. */ +.settings-group + .settings-group { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--rule); +} + +.layout-width-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.layout-width-label { + font-size: 13px; + color: var(--ink); +} + +.segmented { + display: inline-flex; + padding: 2px; + gap: 2px; + background-color: var(--sunken); + border: 1px solid var(--rule); + border-radius: 8px; + flex-shrink: 0; +} + +.segmented-option { + position: relative; + cursor: pointer; +} + +.segmented-option span { + display: block; + padding: 4px 12px; + border-radius: 6px; + font-size: 12.5px; + font-weight: 500; + color: var(--muted); + transition: background-color var(--transition-fast), color var(--transition-fast); +} + +.segmented-option:hover span { + color: var(--ink); +} + +.segmented-option input:checked + span { + color: var(--mark); + background-color: var(--mark-tint); +} + +.segmented-option input:focus-visible + span { + outline: 2px solid var(--mark); + outline-offset: -2px; +} + +/* Add-ons: one card per add-on, styled like the engine cards. */ +.addons-intro { + margin-top: 0; + margin-bottom: 12px; +} + +.addon-card { + padding: 12px 14px; + background-color: var(--sunken); + border: 1px solid var(--rule-subtle); + border-radius: 8px; +} + +.addon-card + .addon-card { + margin-top: 8px; +} + +.addon-card-head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.addon-card-title { + font-size: 13px; + font-weight: 600; + color: var(--ink); +} + +.addon-card-meta { + margin-top: 2px; + font-size: 11px; + color: var(--faint); +} + +/* Preview text size (Display): A- and A+ only, with a live sample below. */ +.text-size { + margin-top: 14px; +} + +.text-size-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.text-size-buttons { + display: inline-flex; + padding: 2px; + gap: 2px; + background-color: var(--sunken); + border: 1px solid var(--rule); + border-radius: 8px; + flex-shrink: 0; +} + +.text-size-btn { + min-width: 36px; + height: 26px; + padding: 0 8px; + border: none; + border-radius: 6px; + background: none; + color: var(--muted); + font: inherit; + font-size: 13px; + font-weight: 600; + line-height: 1; + cursor: pointer; + transition: background-color var(--transition-fast), color var(--transition-fast); +} + +.text-size-btn:hover:not([aria-disabled="true"]) { + color: var(--ink); + background-color: var(--elevated); +} + +/* At the smallest or largest size; still focusable, so keyboard focus stays. */ +.text-size-btn[aria-disabled="true"] { + opacity: 0.4; + cursor: default; +} + +.text-size-btn:focus-visible { + outline: 2px solid var(--mark); + outline-offset: -2px; +} + +/* A real piece of preview: it uses .markdown-rendered, so it follows + --preview-scale exactly as the document does. */ +.markdown-rendered.text-size-sample { + height: auto; + margin-top: 10px; + padding: 10px 12px; + background-color: var(--sunken); + border: 1px solid var(--rule-subtle); + border-radius: 8px; + overflow: hidden; +} + +.markdown-rendered.text-size-sample h3 { + margin-top: 0; +} + +.markdown-rendered.text-size-sample p { + margin-bottom: 0; +} + +.save-folder-actions { + display: inline-flex; + gap: 6px; + flex-shrink: 0; +} + +.save-folder-path { + margin-top: 4px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + /* Keep the end of the path (the folder's own name) visible. */ + direction: rtl; + text-align: left; +} + +/* Amber with text, never colour alone; same tones as the missing-text warning. */ +.save-folder-problem { + margin-top: 4px; + font-size: 11px; + line-height: 1.45; + color: #fbbf24; +} + +[data-theme="light"] .save-folder-problem { + color: #92400e; +} + .settings-engines { display: flex; flex-direction: column; @@ -601,25 +1095,6 @@ body { margin-top: 8px; } -.settings-divider { - height: 1px; - background-color: var(--rule); -} - -.settings-link { - display: inline-flex; - align-items: center; - gap: 8px; - color: var(--muted); - font-size: 13px; - text-decoration: none; - transition: color var(--transition-fast); -} - -.settings-link:hover { - color: var(--ink); -} - .settings-version { font-size: 11px; color: var(--faint); @@ -729,6 +1204,7 @@ body { /* Toggle Switch */ .toggle-switch { + position: relative; display: flex; align-items: center; gap: 8px; @@ -738,8 +1214,41 @@ body { color: var(--muted); } +/* Hidden visually but still a real checkbox, so Tab reaches every switch + and Space flips it (display: none took switches out of the keyboard). */ .toggle-switch input { - display: none; + position: absolute; + width: 1px; + height: 1px; + margin: 0; + opacity: 0; + pointer-events: none; +} + +.toggle-switch input:focus-visible + .toggle-slider { + outline: 2px solid var(--mark); + outline-offset: 2px; +} + +/* Settings used from the keyboard (.keyboard-nav, set by app.js): show the + focus ring on :focus. WebKit doesn't treat the drawer's own Tab handling + as keyboard focus after a mouse click, so :focus-visible alone would leave + macOS and Linux keyboard users without a ring. Same rings as above. */ +.settings-drawer.keyboard-nav :focus { + outline: 2px solid var(--mark); + outline-offset: 2px; +} + +.settings-drawer.keyboard-nav .settings-nav-item:focus, +.settings-drawer.keyboard-nav .text-size-btn:focus, +.settings-drawer.keyboard-nav .segmented-option input:focus + span { + outline: 2px solid var(--mark); + outline-offset: -2px; +} + +.settings-drawer.keyboard-nav .toggle-switch input:focus + .toggle-slider { + outline: 2px solid var(--mark); + outline-offset: 2px; } .toggle-slider { @@ -2025,7 +2534,7 @@ body { ═══════════════════════════════ */ .markdown-rendered { color: var(--ink); - font-size: 14px; + font-size: calc(14px * var(--preview-scale)); line-height: 1.7; height: 100%; } @@ -2043,15 +2552,15 @@ body { } .markdown-rendered h1 { - font-size: 24px; + font-size: calc(24px * var(--preview-scale)); } .markdown-rendered h2 { - font-size: 20px; + font-size: calc(20px * var(--preview-scale)); } .markdown-rendered h3 { - font-size: 16px; + font-size: calc(16px * var(--preview-scale)); } .markdown-rendered p { @@ -2065,7 +2574,7 @@ body { .markdown-rendered code { font-family: var(--font-mono); - font-size: 12px; + font-size: calc(12px * var(--preview-scale)); padding: 2px 6px; border-radius: 4px; background-color: var(--sunken); @@ -2085,7 +2594,7 @@ body { background: transparent; padding: 0; color: var(--ink); - font-size: 13px; + font-size: calc(13px * var(--preview-scale)); } /* ─── Syntax highlighting ──────────────────────────────────────────────────── @@ -2199,7 +2708,72 @@ body { width: 100%; border-collapse: collapse; margin: 1.5em 0; - font-size: 13px; + font-size: calc(13px * var(--preview-scale)); +} + +/* Each table scrolls sideways on its own (app.js wraps it), so a wide table + no longer moves the whole document. */ +.markdown-rendered .table-scroll { + overflow-x: auto; + margin: 1.5em 0; +} + +.markdown-rendered .table-scroll > table { + margin: 0; +} + +/* Pictures fit the preview instead of widening it. */ +.markdown-rendered img { + max-width: 100%; + height: auto; +} + +/* Running text keeps today's longest line (the Standard preview is under + 1000px wide) when Wide or Full gives tables, code and images more room. + Headings are left out so their rules still span the page. */ +.markdown-rendered > p, +.markdown-rendered > ul, +.markdown-rendered > ol, +.markdown-rendered > dl, +.markdown-rendered > blockquote { + max-width: calc(1000px * var(--preview-scale)); +} + +/* Markdown puts a picture inside a paragraph; that paragraph is not text. */ +.markdown-rendered > p:has(> img) { + max-width: none; +} + +/* Math formulas (Settings → Add-ons). Until KaTeX has drawn it, or if it + can't, a formula shows its exact LaTeX in the code font. KaTeX sizes + itself in em, so drawn formulas follow the preview text size. */ +.markdown-rendered .math-tex { + font-family: var(--font-mono); + font-size: 0.92em; + color: var(--muted); +} + +.markdown-rendered .math-tex.math-done { + font-family: inherit; + font-size: inherit; + color: inherit; +} + +.markdown-rendered .math-block { + display: block; + margin: 1em 0; + text-align: center; + white-space: pre-wrap; + overflow-x: auto; + overflow-y: hidden; +} + +.markdown-rendered .math-block.math-done { + white-space: normal; +} + +.markdown-rendered .math-block .katex-display { + margin: 0; } .markdown-rendered th, @@ -2228,7 +2802,7 @@ body { border: none; outline: none; font-family: var(--font-mono); - font-size: 13px; + font-size: calc(13px * var(--preview-scale)); color: var(--ink); line-height: 1.6; resize: none; @@ -2822,7 +3396,9 @@ input[type="url"] { .chevron-icon, .dropzone-section.is-collapsed .chevron-icon, .app-dialog-overlay, - .app-dialog { + .app-dialog, + .settings-scrim, + .settings-drawer.open { transition: opacity 80ms ease !important; animation: none !important; transform: none !important; diff --git a/app/ui/vendor/VENDOR.json b/app/ui/vendor/VENDOR.json new file mode 100644 index 0000000..21ee7d8 --- /dev/null +++ b/app/ui/vendor/VENDOR.json @@ -0,0 +1,76 @@ +{ + "_comment": [ + "Every third-party file in app/ui/vendor, with the SHA-256 of the exact bytes shipped.", + "tests/test_vendor_manifest.py fails if a file changes, goes missing, or appears unlisted.", + "To update a library: download the pinned package, compare its sha512 with the npm registry's", + "dist.integrity, copy only the files listed here, then update version, upstream_integrity and", + "the hashes (see CONTRIBUTING.md). Nothing here is ever fetched at runtime." + ], + "libraries": [ + { + "name": "KaTeX", + "version": "0.19.0", + "license": "MIT", + "licence_file": "LICENSES/KaTeX-MIT.txt", + "source": "npm katex@0.19.0: dist/katex.min.js, dist/katex.min.css and the 20 dist/fonts/*.woff2 files (no .woff/.ttf)", + "upstream_integrity": "sha512-v6Tznz3zJ7u3niRCoDTsumM2+HA2XXcCu+WAacCeHD2z3p9A9Ks987o5FzfTGBN0e8A0vjEgIDvLbICrXpdw/Q==", + "files": { + "katex/katex.min.js": "103a53763cc033bba8d175bf3f0ba597c3505c9b6747dd3f2c7bc2a6bfcc8ae7", + "katex/katex.min.css": "d4ab5b8ee16989b070cdb0ea24bd6ad48fc8df1b787f280f29d3ae4f959f2c00", + "katex/fonts/KaTeX_AMS-Regular.woff2": "0cdd387c9590a1a9f9794560022dbb59654a7d86f187aa0c81495ad42d3a7308", + "katex/fonts/KaTeX_Caligraphic-Bold.woff2": "de7701e42cf1f4cf0b766c03fb27977207eee2f4fd5d76fa82188406da43ea4c", + "katex/fonts/KaTeX_Caligraphic-Regular.woff2": "5d53e70ad607c2352162dec9e0923fb54ecdafaccbf604cd8dcf7d00facb989b", + "katex/fonts/KaTeX_Fraktur-Bold.woff2": "74444efd593c005e3f4573b44524704c0af0a937fe911cca9e94068d0d140d3f", + "katex/fonts/KaTeX_Fraktur-Regular.woff2": "51814d270d06ff0255dba0799994fa4d8c84d11f09951d47595f4abb1f3602dc", + "katex/fonts/KaTeX_Main-Bold.woff2": "0f60d1b897938ec918c8ce073092411baf9438f6739465693ff18b0f9d20b021", + "katex/fonts/KaTeX_Main-BoldItalic.woff2": "99cd42a3c072d918f2f44984a807cf7aa16e13545fd0875fc07c6c65f99e715b", + "katex/fonts/KaTeX_Main-Italic.woff2": "97479ca6cce906abc961ecac96faa5f9ca2e61b8e7670d475826bcdee9a7c267", + "katex/fonts/KaTeX_Main-Regular.woff2": "c2342cd8b869e01752a9321dc17213fc40d4d04c79688c1d43f2cf316abd7866", + "katex/fonts/KaTeX_Math-BoldItalic.woff2": "dc47344dbb6cb5b655c8460d561f4df5f501b90c804ad3c6cec65fe322351ab1", + "katex/fonts/KaTeX_Math-Italic.woff2": "7af58c5ec8f132a2ddde9027c6d7814decce4d3b822a11192a42a20e2e973264", + "katex/fonts/KaTeX_SansSerif-Bold.woff2": "e99ae51144bf1232efcc1bfe5add36262c6866b0faab24fa75740e1b98577a62", + "katex/fonts/KaTeX_SansSerif-Italic.woff2": "00b26ac825e2095056396e0553b8ac26d3f8ad158c3826e28b4c45b385c4714a", + "katex/fonts/KaTeX_SansSerif-Regular.woff2": "68e8c73ef42afd3ccec58bf0fba302cce448938e7fc020a5e31f8a952eee1342", + "katex/fonts/KaTeX_Script-Regular.woff2": "036d4e95149b69ff9bcc0cd55771efeb25ffa3947293e69acd78d5ac328c684b", + "katex/fonts/KaTeX_Size1-Regular.woff2": "6b47c40166b6dbe21a5dfca7718413f2147fd2399be1ba605d8ad39cedf25dfe", + "katex/fonts/KaTeX_Size2-Regular.woff2": "d04c54219f9eaec6d4d4fd42dfb28785975a4794d6b2fc71e566b9cd6db842dd", + "katex/fonts/KaTeX_Size3-Regular.woff2": "73d591271b1604960cb10bb90fee021670af7297017e0e98480b332d11f51995", + "katex/fonts/KaTeX_Size4-Regular.woff2": "a4af7d414440a1c1790825cfb700cf9cf43b0f2c4b04f0ebc523011ad9853ec0", + "katex/fonts/KaTeX_Typewriter-Regular.woff2": "71d517d67827787cfabdf186914cc3358eda539e37931941f2b2fd4a21f68c0b" + } + }, + { + "name": "DOMPurify", + "version": "3.4.16", + "license": "MPL-2.0 OR Apache-2.0", + "licence_file": "LICENSES/DOMPurify-Apache-2.0-OR-MPL-2.0.txt", + "source": "npm dompurify@3.4.16: dist/purify.min.js", + "upstream_integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "files": { + "purify.min.js": "2c90a9b46d6463f26038a29b686e82bc91de01fdac9d5229e7cfe3b360134ea2" + } + }, + { + "name": "marked", + "version": "15.0.12", + "license": "MIT", + "licence_file": "LICENSES/marked-MIT.txt", + "source": "npm marked@15.0.12: marked.min.js (byte-identical)", + "upstream_integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "files": { + "marked.min.js": "3e7e7d7feb3e5d58cb6c804f68ab5c24cc7e5eb6270fd6e5cbb9124739217d0c" + } + }, + { + "name": "highlight.js", + "version": "11.12.0", + "license": "BSD-3-Clause", + "licence_file": "LICENSES/highlight.js-BSD-3-Clause.txt", + "source": "custom build (core + 48 grammars); see scripts/build_highlight.md", + "upstream_integrity": null, + "files": { + "highlight.min.js": "aeec7e655752cea6683379f32f2c2c682eb2bdaaf2e707a0ea62734b7e4ee0be" + } + } + ] +} diff --git a/app/ui/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 new file mode 100644 index 0000000..0acaaff Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 new file mode 100644 index 0000000..f390922 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 new file mode 100644 index 0000000..75344a1 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 new file mode 100644 index 0000000..395f28b Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 new file mode 100644 index 0000000..735f694 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Main-Bold.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Main-Bold.woff2 new file mode 100644 index 0000000..ab2ad21 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Main-Bold.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 new file mode 100644 index 0000000..5931794 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Main-Italic.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Main-Italic.woff2 new file mode 100644 index 0000000..b50920e Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Main-Italic.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Main-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Main-Regular.woff2 new file mode 100644 index 0000000..eb24a7b Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Main-Regular.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 new file mode 100644 index 0000000..2965702 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Math-Italic.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Math-Italic.woff2 new file mode 100644 index 0000000..215c143 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Math-Italic.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 b/app/ui/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 new file mode 100644 index 0000000..cfaa3bd Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 b/app/ui/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 new file mode 100644 index 0000000..349c06d Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 new file mode 100644 index 0000000..a90eea8 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Script-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Script-Regular.woff2 new file mode 100644 index 0000000..b3048fc Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Script-Regular.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 new file mode 100644 index 0000000..c5a8462 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 new file mode 100644 index 0000000..e1bccfe Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 new file mode 100644 index 0000000..249a286 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 new file mode 100644 index 0000000..680c130 Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 differ diff --git a/app/ui/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 b/app/ui/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 new file mode 100644 index 0000000..771f1af Binary files /dev/null and b/app/ui/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 differ diff --git a/app/ui/vendor/katex/katex.min.css b/app/ui/vendor/katex/katex.min.css new file mode 100644 index 0000000..056c21a --- /dev/null +++ b/app/ui/vendor/katex/katex.min.css @@ -0,0 +1 @@ +@font-face{font-display:block;font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(fonts/KaTeX_AMS-Regular.woff2) format("woff2"),url(fonts/KaTeX_AMS-Regular.woff) format("woff"),url(fonts/KaTeX_AMS-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Caligraphic-Bold.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Bold.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Caligraphic-Regular.woff2) format("woff2"),url(fonts/KaTeX_Caligraphic-Regular.woff) format("woff"),url(fonts/KaTeX_Caligraphic-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Fraktur-Bold.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Bold.woff) format("woff"),url(fonts/KaTeX_Fraktur-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Fraktur-Regular.woff2) format("woff2"),url(fonts/KaTeX_Fraktur-Regular.woff) format("woff"),url(fonts/KaTeX_Fraktur-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(fonts/KaTeX_Main-Bold.woff2) format("woff2"),url(fonts/KaTeX_Main-Bold.woff) format("woff"),url(fonts/KaTeX_Main-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Main-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Main-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Main-BoldItalic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Main-Italic.woff2) format("woff2"),url(fonts/KaTeX_Main-Italic.woff) format("woff"),url(fonts/KaTeX_Main-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Main-Regular.woff2) format("woff2"),url(fonts/KaTeX_Main-Regular.woff) format("woff"),url(fonts/KaTeX_Main-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(fonts/KaTeX_Math-BoldItalic.woff2) format("woff2"),url(fonts/KaTeX_Math-BoldItalic.woff) format("woff"),url(fonts/KaTeX_Math-BoldItalic.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(fonts/KaTeX_Math-Italic.woff2) format("woff2"),url(fonts/KaTeX_Math-Italic.woff) format("woff"),url(fonts/KaTeX_Math-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:"KaTeX_SansSerif";font-style:normal;font-weight:700;src:url(fonts/KaTeX_SansSerif-Bold.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Bold.woff) format("woff"),url(fonts/KaTeX_SansSerif-Bold.ttf) format("truetype")}@font-face{font-display:block;font-family:"KaTeX_SansSerif";font-style:italic;font-weight:400;src:url(fonts/KaTeX_SansSerif-Italic.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Italic.woff) format("woff"),url(fonts/KaTeX_SansSerif-Italic.ttf) format("truetype")}@font-face{font-display:block;font-family:"KaTeX_SansSerif";font-style:normal;font-weight:400;src:url(fonts/KaTeX_SansSerif-Regular.woff2) format("woff2"),url(fonts/KaTeX_SansSerif-Regular.woff) format("woff"),url(fonts/KaTeX_SansSerif-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Script-Regular.woff2) format("woff2"),url(fonts/KaTeX_Script-Regular.woff) format("woff"),url(fonts/KaTeX_Script-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size1-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size1-Regular.woff) format("woff"),url(fonts/KaTeX_Size1-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size2-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size2-Regular.woff) format("woff"),url(fonts/KaTeX_Size2-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size3-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size3-Regular.woff) format("woff"),url(fonts/KaTeX_Size3-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Size4-Regular.woff2) format("woff2"),url(fonts/KaTeX_Size4-Regular.woff) format("woff"),url(fonts/KaTeX_Size4-Regular.ttf) format("truetype")}@font-face{font-display:block;font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(fonts/KaTeX_Typewriter-Regular.woff2) format("woff2"),url(fonts/KaTeX_Typewriter-Regular.woff) format("woff"),url(fonts/KaTeX_Typewriter-Regular.ttf) format("truetype")}.katex{font:normal 1.21em KaTeX_Main,math,Times New Roman,serif;line-height:1.2;position:relative;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.19.0"}.katex .katex-mathml{border:0;-webkit-clip-path:inset(50%);clip-path:inset(50%);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.katex-newline{display:block}.katex .katex-base{position:relative;white-space:nowrap;width:-webkit-min-content;width:-moz-min-content;width:min-content}.katex .katex-base,.katex .katex-strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .katex-vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .katex-thinbox{display:inline-flex;flex-direction:row;max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline,.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-rule,.katex .katex-underline .underline-line,.katex .mfrac .frac-line{min-height:1px}.katex .mspace{display:inline-block}.katex .katex-smash{display:inline;line-height:0}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.katex-inner,.katex .llap>.katex-inner,.katex .rlap>.katex-inner{position:absolute}.katex .clap>.katex-fix,.katex .llap>.katex-fix,.katex .rlap>.katex-fix{display:inline-block}.katex .llap>.katex-inner{right:0}.katex .clap>.katex-inner,.katex .rlap>.katex-inner{left:0}.katex .clap>.katex-inner>span{margin-left:-50%;margin-right:50%}.katex .katex-rule{border:0 solid;display:inline-block;position:relative}.katex .katex-hline,.katex .katex-overline .overline-line,.katex .katex-underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .katex-hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.katex-root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .katex-sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .katex-sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .katex-sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .katex-sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .katex-sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .katex-sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .katex-sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .katex-sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .katex-sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .katex-sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .katex-sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .katex-sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .katex-sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .katex-sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .katex-sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .katex-sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .katex-sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .katex-sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .katex-sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .katex-sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .katex-sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .katex-sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .katex-sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .katex-sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .katex-sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .katex-sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .katex-sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .katex-sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .katex-sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .katex-sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .katex-sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .katex-sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .katex-sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .katex-sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .katex-sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .katex-sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .katex-sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .katex-sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .katex-sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .katex-sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .katex-sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .katex-sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .katex-sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .katex-sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .katex-sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .katex-sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .katex-sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .katex-sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .katex-sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .katex-sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .katex-sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .katex-sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .katex-sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .katex-sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .katex-sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .katex-sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .katex-sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .katex-sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .katex-sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .katex-sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .katex-sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .katex-sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .katex-sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .katex-sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .katex-sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .katex-sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .katex-sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .katex-sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .katex-sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .katex-sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .katex-sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .katex-sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .katex-sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .katex-sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .katex-sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .katex-sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .katex-sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .katex-sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .katex-sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .katex-sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .katex-sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .katex-sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .katex-sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .katex-sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .katex-sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .katex-sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .katex-sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .katex-sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .katex-sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .katex-sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .katex-sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .katex-sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .katex-sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .katex-sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .katex-sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .katex-sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .katex-sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .katex-sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .katex-sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .katex-sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .katex-sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .katex-sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .katex-sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .katex-sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .katex-sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .katex-sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .katex-sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .katex-sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .katex-sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .katex-sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .katex-sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .katex-sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .katex-sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .katex-sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .katex-sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .katex-sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .katex-sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .katex-sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .katex-sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .katex-sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .katex-sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .katex-accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .katex-accent .accent-body{position:relative}.katex .katex-accent .accent-body:not(.accent-full){width:0}.katex .katex-overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex svg{fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .katex-stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .katex-stretchy:after,.katex .katex-stretchy:before{content:""}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .katex-sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .reflectbox{display:inline-block;transform:scaleX(-1)}.katex .eqn-num:before{content:"(" counter(katexEqnNo) ")";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:"(" counter(mmlEqnNo) ")";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.katex-tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.katex-tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo} diff --git a/app/ui/vendor/katex/katex.min.js b/app/ui/vendor/katex/katex.min.js new file mode 100644 index 0000000..4a6810d --- /dev/null +++ b/app/ui/vendor/katex/katex.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.katex=t():e.katex=t()}("undefined"!=typeof self?self:this,function(){return function(){"use strict";var e={d:function(t,r){for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}},t={};e.d(t,{default:function(){return go}});class r extends Error{constructor(e,t){let n,o,s="KaTeX parse error: "+e;const i=t&&t.loc;if(i&&i.start<=i.end){const e=i.lexer.input;n=i.start,o=i.end,n===e.length?s+=" at end of input: ":s+=" at position "+(n+1)+": ";const t=e.slice(n,o).replace(/[^]/g,"$&\u0332");let r,a;r=n>15?"\u2026"+e.slice(n-15,n):e.slice(0,n),a=o+15e.replace(o,"-$1").toLowerCase(),i={"&":"&",">":">","<":"<",'"':""","'":"'"},a=/[&><"']/g,l=e=>String(e).replace(a,e=>i[e]),c=e=>"ordgroup"===e.type||"color"===e.type?1===e.body.length?c(e.body[0]):e:"font"===e.type?c(e.body):e,h=new Set(["mathord","textord","atom"]),m=e=>h.has(c(e).type),u={displayMode:{type:"boolean",description:"Render math in display mode, which puts the math in display style (so \\int and \\sum are large, for example), and centers the math on the page on its own line.",cli:"-d, --display-mode"},output:{type:{enum:["htmlAndMathml","html","mathml"]},description:"Determines the markup language of the output.",cli:"-F, --format "},leqno:{type:"boolean",description:"Render display math in leqno style (left-justified tags)."},fleqn:{type:"boolean",description:"Render display math flush left."},throwOnError:{type:"boolean",default:!0,cli:"-t, --no-throw-on-error",cliDescription:"Render errors (in the color given by --error-color) instead of throwing a ParseError exception when encountering an error."},errorColor:{type:"string",default:"#cc0000",cli:"-c, --error-color ",cliDescription:"A color string given in the format 'rgb' or 'rrggbb' (no #). This option determines the color of errors rendered by the -t option.",cliProcessor:e=>"#"+e},macros:{type:"object",cli:"-m, --macro ",cliDescription:"Define custom macro of the form '\\foo:expansion' (use multiple -m arguments for multiple macros).",cliDefault:[],cliProcessor:(e,t)=>(t.push(e),t)},minRuleThickness:{type:"number",description:"Specifies a minimum thickness, in ems, for fraction lines, `\\sqrt` top lines, `{array}` vertical lines, `\\hline`, `\\hdashline`, `\\underline`, `\\overline`, and the borders of `\\fbox`, `\\boxed`, and `\\fcolorbox`.",processor:e=>Math.max(0,e),cli:"--min-rule-thickness ",cliProcessor:parseFloat},colorIsTextColor:{type:"boolean",description:"Makes \\color behave like LaTeX's 2-argument \\textcolor, instead of LaTeX's one-argument \\color mode change.",cli:"-b, --color-is-text-color"},strict:{type:[{enum:["warn","ignore","error"]},"boolean","function"],description:"Turn on strict / LaTeX faithfulness mode, which throws an error if the input uses features that are not supported by LaTeX.",cli:"-S, --strict",cliDefault:!1},trust:{type:["boolean","function"],description:"Trust the input, enabling all HTML features such as \\url.",cli:"-T, --trust"},maxSize:{type:"number",default:1/0,description:"If non-zero, all user-specified sizes, e.g. in \\rule{500em}{500em}, will be capped to maxSize ems. Otherwise, elements and spaces can be arbitrarily large",processor:e=>Math.max(0,e),cli:"-s, --max-size ",cliProcessor:parseInt},maxExpand:{type:"number",default:1e3,description:"Limit the number of macro expansions to the specified number, to prevent e.g. infinite macro loops. If set to Infinity, the macro expander will try to fully expand as in LaTeX.",processor:e=>Math.max(0,e),cli:"-e, --max-expand ",cliProcessor:e=>"Infinity"===e?1/0:parseInt(e)},globalGroup:{type:"boolean",cli:!1}};function p(e){if(Object.prototype.hasOwnProperty.call(e,"default")&&void 0!==e.default)return e.default;return function(e){if("string"!=typeof e)return e.enum[0];switch(e){case"boolean":return!1;case"string":return"";case"number":return 0;case"object":return{};default:throw new Error("Unexpected schema type; settings must declare an explicit default.")}}(Array.isArray(e.type)?e.type[0]:e.type)}function d(e,t,r,n){const o=Object.prototype.hasOwnProperty.call(r,t)?r[t]:void 0,s=Object.prototype.hasOwnProperty.call(n,"processor")?n.processor:void 0;e[t]=void 0!==o?s?s(o):o:p(n)}class g{constructor(e){void 0===e&&(e={}),this.displayMode=void 0,this.output=void 0,this.leqno=void 0,this.fleqn=void 0,this.throwOnError=void 0,this.errorColor=void 0,this.macros=void 0,this.minRuleThickness=void 0,this.colorIsTextColor=void 0,this.strict=void 0,this.trust=void 0,this.maxSize=void 0,this.maxExpand=void 0,this.globalGroup=void 0,e=e||{};for(const t of Object.keys(u)){const r=u[t];r&&d(this,t,e,r)}}isTrusted(e){if("url"in e&&e.url&&!e.protocol){const t=(e=>{const t=/^[\x00-\x20]*([^\\/#?]*?)(:|�*58|�*3a|&colon)/i.exec(e);return t?":"!==t[2]?null:/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(t[1])?t[1].toLowerCase():null:"_relative"})(e.url);if(null==t)return!1;e.protocol=t}const t="function"==typeof this.trust?this.trust(e):this.trust;return Boolean(t)}}class f{constructor(e,t,r){this.id=void 0,this.size=void 0,this.cramped=void 0,this.id=e,this.size=t,this.cramped=r}sup(){return b[y[this.id]]}sub(){return b[x[this.id]]}fracNum(){return b[w[this.id]]}fracDen(){return b[v[this.id]]}cramp(){return b[k[this.id]]}text(){return b[z[this.id]]}isTight(){return this.size>=2}}const b=[new f(0,0,!1),new f(1,0,!0),new f(2,1,!1),new f(3,1,!0),new f(4,2,!1),new f(5,2,!0),new f(6,3,!1),new f(7,3,!0)],y=[4,5,4,5,6,7,6,7],x=[5,5,5,5,7,7,7,7],w=[2,3,4,5,6,7,6,7],v=[3,3,5,5,7,7,7,7],k=[1,1,3,3,5,5,7,7],z=[0,1,2,3,2,3,2,3];var S={DISPLAY:b[0],TEXT:b[2],SCRIPT:b[4],SCRIPTSCRIPT:b[6]};const M=[{name:"latin",blocks:[[256,591],[768,879]]},{name:"cyrillic",blocks:[[1024,1279]]},{name:"armenian",blocks:[[1328,1423]]},{name:"brahmic",blocks:[[2304,4255]]},{name:"georgian",blocks:[[4256,4351]]},{name:"cjk",blocks:[[12288,12543],[19968,40879],[65280,65376]]},{name:"hangul",blocks:[[44032,55215]]}];const A=[];function T(e){for(let t=0;t=A[t]&&e<=A[t+1])return!0;return!1}M.forEach(e=>e.blocks.forEach(e=>A.push(...e)));const C=e=>e+" "+e,B=80,q={doubleleftarrow:"M262 157\nl10-10c34-36 62.7-77 86-123 3.3-8 5-13.3 5-16 0-5.3-6.7-8-20-8-7.3\n 0-12.2.5-14.5 1.5-2.3 1-4.8 4.5-7.5 10.5-49.3 97.3-121.7 169.3-217 216-28\n 14-57.3 25-88 33-6.7 2-11 3.8-13 5.5-2 1.7-3 4.2-3 7.5s1 5.8 3 7.5\nc2 1.7 6.3 3.5 13 5.5 68 17.3 128.2 47.8 180.5 91.5 52.3 43.7 93.8 96.2 124.5\n 157.5 9.3 8 15.3 12.3 18 13h6c12-.7 18-4 18-10 0-2-1.7-7-5-15-23.3-46-52-87\n-86-123l-10-10h399738v-40H218c328 0 0 0 0 0l-10-8c-26.7-20-65.7-43-117-69 2.7\n-2 6-3.7 10-5 36.7-16 72.3-37.3 107-64l10-8h399782v-40z\nm8 0v40h399730v-40zm0 194v40h399730v-40z",doublerightarrow:"M399738 392l\n-10 10c-34 36-62.7 77-86 123-3.3 8-5 13.3-5 16 0 5.3 6.7 8 20 8 7.3 0 12.2-.5\n 14.5-1.5 2.3-1 4.8-4.5 7.5-10.5 49.3-97.3 121.7-169.3 217-216 28-14 57.3-25 88\n-33 6.7-2 11-3.8 13-5.5 2-1.7 3-4.2 3-7.5s-1-5.8-3-7.5c-2-1.7-6.3-3.5-13-5.5-68\n-17.3-128.2-47.8-180.5-91.5-52.3-43.7-93.8-96.2-124.5-157.5-9.3-8-15.3-12.3-18\n-13h-6c-12 .7-18 4-18 10 0 2 1.7 7 5 15 23.3 46 52 87 86 123l10 10H0v40h399782\nc-328 0 0 0 0 0l10 8c26.7 20 65.7 43 117 69-2.7 2-6 3.7-10 5-36.7 16-72.3 37.3\n-107 64l-10 8H0v40zM0 157v40h399730v-40zm0 194v40h399730v-40z",leftarrow:"M400000 241H110l3-3c68.7-52.7 113.7-120\n 135-202 4-14.7 6-23 6-25 0-7.3-7-11-21-11-8 0-13.2.8-15.5 2.5-2.3 1.7-4.2 5.8\n-5.5 12.5-1.3 4.7-2.7 10.3-4 17-12 48.7-34.8 92-68.5 130S65.3 228.3 18 247\nc-10 4-16 7.7-18 11 0 8.7 6 14.3 18 17 47.3 18.7 87.8 47 121.5 85S196 441.3 208\n 490c.7 2 1.3 5 2 9s1.2 6.7 1.5 8c.3 1.3 1 3.3 2 6s2.2 4.5 3.5 5.5c1.3 1 3.3\n 1.8 6 2.5s6 1 10 1c14 0 21-3.7 21-11 0-2-2-10.3-6-25-20-79.3-65-146.7-135-202\n l-3-3h399890zM100 241v40h399900v-40z",leftbrace:"M6 548l-6-6v-35l6-11c56-104 135.3-181.3 238-232 57.3-28.7 117\n-45 179-50h399577v120H403c-43.3 7-81 15-113 26-100.7 33-179.7 91-237 174-2.7\n 5-6 9-10 13-.7 1-7.3 1-20 1H6z",leftbraceunder:"M0 6l6-6h17c12.688 0 19.313.3 20 1 4 4 7.313 8.3 10 13\n 35.313 51.3 80.813 93.8 136.5 127.5 55.688 33.7 117.188 55.8 184.5 66.5.688\n 0 2 .3 4 1 18.688 2.7 76 4.3 172 5h399450v120H429l-6-1c-124.688-8-235-61.7\n-331-161C60.687 138.7 32.312 99.3 7 54L0 41V6z",leftgroup:"M400000 80\nH435C64 80 168.3 229.4 21 260c-5.9 1.2-18 0-18 0-2 0-3-1-3-3v-38C76 61 257 0\n 435 0h399565z",leftgroupunder:"M400000 262\nH435C64 262 168.3 112.6 21 82c-5.9-1.2-18 0-18 0-2 0-3 1-3 3v38c76 158 257 219\n 435 219h399565z",leftharpoon:"M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3\n-3.3 10.2-9.5 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5\n-18.3 3-21-1.3-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7\n-196 228-6.7 4.7-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40z",leftharpoonplus:"M0 267c.7 5.3 3 10 7 14h399993v-40H93c3.3-3.3 10.2-9.5\n 20.5-18.5s17.8-15.8 22.5-20.5c50.7-52 88-110.3 112-175 4-11.3 5-18.3 3-21-1.3\n-4-7.3-6-18-6-8 0-13 .7-15 2s-4.7 6.7-8 16c-42 98.7-107.3 174.7-196 228-6.7 4.7\n-10.7 8-12 10-1.3 2-2 5.7-2 11zm100-26v40h399900v-40zM0 435v40h400000v-40z\nm0 0v40h400000v-40z",leftharpoondown:"M7 241c-4 4-6.333 8.667-7 14 0 5.333.667 9 2 11s5.333\n 5.333 12 10c90.667 54 156 130 196 228 3.333 10.667 6.333 16.333 9 17 2 .667 5\n 1 9 1h5c10.667 0 16.667-2 18-6 2-2.667 1-9.667-3-21-32-87.333-82.667-157.667\n-152-211l-3-3h399907v-40zM93 281 H400000 v-40L7 241z",leftharpoondownplus:"M7 435c-4 4-6.3 8.7-7 14 0 5.3.7 9 2 11s5.3 5.3 12\n 10c90.7 54 156 130 196 228 3.3 10.7 6.3 16.3 9 17 2 .7 5 1 9 1h5c10.7 0 16.7\n-2 18-6 2-2.7 1-9.7-3-21-32-87.3-82.7-157.7-152-211l-3-3h399907v-40H7zm93 0\nv40h399900v-40zM0 241v40h399900v-40zm0 0v40h399900v-40z",lefthook:"M400000 281 H103s-33-11.2-61-33.5S0 197.3 0 164s14.2-61.2 42.5\n-83.5C70.8 58.2 104 47 142 47 c16.7 0 25 6.7 25 20 0 12-8.7 18.7-26 20-40 3.3\n-68.7 15.7-86 37-10 12-15 25.3-15 40 0 22.7 9.8 40.7 29.5 54 19.7 13.3 43.5 21\n 71.5 23h399859zM103 281v-40h399897v40z",leftlinesegment:C("M40 281 V428 H0 V94 H40 V241 H400000 v40z"),leftbracketunder:C("M0 0 h120 V290 H399995 v120 H0z"),leftbracketover:C("M0 440 h120 V150 H399995 v-120 H0z"),leftmapsto:C("M40 281 V448H0V74H40V241H400000v40z"),leftToFrom:"M0 147h400000v40H0zm0 214c68 40 115.7 95.7 143 167h22c15.3 0 23\n-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69-70-101l-7-8h399905v-40H95l7-8\nc28.7-32 52-65.7 70-101 10.7-23.3 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 265.3\n 68 321 0 361zm0-174v-40h399900v40zm100 154v40h399900v-40z",longequal:C("M0 50 h400000 v40H0z m0 194h40000v40H0z"),midbrace:"M200428 334\nc-100.7-8.3-195.3-44-280-108-55.3-42-101.7-93-139-153l-9-14c-2.7 4-5.7 8.7-9 14\n-53.3 86.7-123.7 153-211 199-66.7 36-137.3 56.3-212 62H0V214h199568c178.3-11.7\n 311.7-78.3 403-201 6-8 9.7-12 11-12 .7-.7 6.7-1 18-1s17.3.3 18 1c1.3 0 5 4 11\n 12 44.7 59.3 101.3 106.3 170 141s145.3 54.3 229 60h199572v120z",midbraceunder:"M199572 214\nc100.7 8.3 195.3 44 280 108 55.3 42 101.7 93 139 153l9 14c2.7-4 5.7-8.7 9-14\n 53.3-86.7 123.7-153 211-199 66.7-36 137.3-56.3 212-62h199568v120H200432c-178.3\n 11.7-311.7 78.3-403 201-6 8-9.7 12-11 12-.7.7-6.7 1-18 1s-17.3-.3-18-1c-1.3 0\n-5-4-11-12-44.7-59.3-101.3-106.3-170-141s-145.3-54.3-229-60H0V214z",oiintSize1:"M512.6 71.6c272.6 0 320.3 106.8 320.3 178.2 0 70.8-47.7 177.6\n-320.3 177.6S193.1 320.6 193.1 249.8c0-71.4 46.9-178.2 319.5-178.2z\nm368.1 178.2c0-86.4-60.9-215.4-368.1-215.4-306.4 0-367.3 129-367.3 215.4 0 85.8\n60.9 214.8 367.3 214.8 307.2 0 368.1-129 368.1-214.8z",oiintSize2:"M757.8 100.1c384.7 0 451.1 137.6 451.1 230 0 91.3-66.4 228.8\n-451.1 228.8-386.3 0-452.7-137.5-452.7-228.8 0-92.4 66.4-230 452.7-230z\nm502.4 230c0-111.2-82.4-277.2-502.4-277.2s-504 166-504 277.2\nc0 110 84 276 504 276s502.4-166 502.4-276z",oiiintSize1:"M681.4 71.6c408.9 0 480.5 106.8 480.5 178.2 0 70.8-71.6 177.6\n-480.5 177.6S202.1 320.6 202.1 249.8c0-71.4 70.5-178.2 479.3-178.2z\nm525.8 178.2c0-86.4-86.8-215.4-525.7-215.4-437.9 0-524.7 129-524.7 215.4 0\n85.8 86.8 214.8 524.7 214.8 438.9 0 525.7-129 525.7-214.8z",oiiintSize2:"M1021.2 53c603.6 0 707.8 165.8 707.8 277.2 0 110-104.2 275.8\n-707.8 275.8-606 0-710.2-165.8-710.2-275.8C311 218.8 415.2 53 1021.2 53z\nm770.4 277.1c0-131.2-126.4-327.6-770.5-327.6S248.4 198.9 248.4 330.1\nc0 130 128.8 326.4 772.7 326.4s770.5-196.4 770.5-326.4z",rightarrow:"M0 241v40h399891c-47.3 35.3-84 78-110 128\n-16.7 32-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20\n 11 8 0 13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7\n 39-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85\n-40.5-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\n 151.7 139 205zm0 0v40h399900v-40z",rightbrace:"M400000 542l\n-6 6h-17c-12.7 0-19.3-.3-20-1-4-4-7.3-8.3-10-13-35.3-51.3-80.8-93.8-136.5-127.5\ns-117.2-55.8-184.5-66.5c-.7 0-2-.3-4-1-18.7-2.7-76-4.3-172-5H0V214h399571l6 1\nc124.7 8 235 61.7 331 161 31.3 33.3 59.7 72.7 85 118l7 13v35z",rightbraceunder:"M399994 0l6 6v35l-6 11c-56 104-135.3 181.3-238 232-57.3\n 28.7-117 45-179 50H-300V214h399897c43.3-7 81-15 113-26 100.7-33 179.7-91 237\n-174 2.7-5 6-9 10-13 .7-1 7.3-1 20-1h17z",rightgroup:"M0 80h399565c371 0 266.7 149.4 414 180 5.9 1.2 18 0 18 0 2 0\n 3-1 3-3v-38c-76-158-257-219-435-219H0z",rightgroupunder:"M0 262h399565c371 0 266.7-149.4 414-180 5.9-1.2 18 0 18\n 0 2 0 3 1 3 3v38c-76 158-257 219-435 219H0z",rightharpoon:"M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3\n-3.7-15.3-11-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2\n-10.7 0-16.7 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58\n 69.2 92 94.5zm0 0v40h399900v-40z",rightharpoonplus:"M0 241v40h399993c4.7-4.7 7-9.3 7-14 0-9.3-3.7-15.3-11\n-18-92.7-56.7-159-133.7-199-231-3.3-9.3-6-14.7-8-16-2-1.3-7-2-15-2-10.7 0-16.7\n 2-18 6-2 2.7-1 9.7 3 21 15.3 42 36.7 81.8 64 119.5 27.3 37.7 58 69.2 92 94.5z\nm0 0v40h399900v-40z m100 194v40h399900v-40zm0 0v40h399900v-40z",rightharpoondown:"M399747 511c0 7.3 6.7 11 20 11 8 0 13-.8 15-2.5s4.7-6.8\n 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3 8.5-5.8 9.5\n-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3-64.7 57-92 95\n-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 241v40h399900v-40z",rightharpoondownplus:"M399747 705c0 7.3 6.7 11 20 11 8 0 13-.8\n 15-2.5s4.7-6.8 8-15.5c40-94 99.3-166.3 178-217 13.3-8 20.3-12.3 21-13 5.3-3.3\n 8.5-5.8 9.5-7.5 1-1.7 1.5-5.2 1.5-10.5s-2.3-10.3-7-15H0v40h399908c-34 25.3\n-64.7 57-92 95-27.3 38-48.7 77.7-64 119-3.3 8.7-5 14-5 16zM0 435v40h399900v-40z\nm0-194v40h400000v-40zm0 0v40h400000v-40z",righthook:"M399859 241c-764 0 0 0 0 0 40-3.3 68.7-15.7 86-37 10-12 15-25.3\n 15-40 0-22.7-9.8-40.7-29.5-54-19.7-13.3-43.5-21-71.5-23-17.3-1.3-26-8-26-20 0\n-13.3 8.7-20 26-20 38 0 71 11.2 99 33.5 0 0 7 5.6 21 16.7 14 11.2 21 33.5 21\n 66.8s-14 61.2-42 83.5c-28 22.3-61 33.5-99 33.5L0 241z M0 281v-40h399859v40z",rightlinesegment:C("M399960 241 V94 h40 V428 h-40 V281 H0 v-40z"),rightbracketunder:C("M399995 0 h-120 V290 H0 v120 H400000z"),rightbracketover:C("M399995 440 h-120 V150 H0 v-120 H399995z"),rightToFrom:"M400000 167c-70.7-42-118-97.7-142-167h-23c-15.3 0-23 .3-23\n 1 0 1.3 5.3 13.7 16 37 18 35.3 41.3 69 70 101l7 8H0v40h399905l-7 8c-28.7 32\n-52 65.7-70 101-10.7 23.3-16 35.7-16 37 0 .7 7.7 1 23 1h23c24-69.3 71.3-125 142\n-167z M100 147v40h399900v-40zM0 341v40h399900v-40z",twoheadleftarrow:"M0 167c68 40\n 115.7 95.7 143 167h22c15.3 0 23-.3 23-1 0-1.3-5.3-13.7-16-37-18-35.3-41.3-69\n-70-101l-7-8h125l9 7c50.7 39.3 85 86 103 140h46c0-4.7-6.3-18.7-19-42-18-35.3\n-40-67.3-66-96l-9-9h399716v-40H284l9-9c26-28.7 48-60.7 66-96 12.7-23.333 19\n-37.333 19-42h-46c-18 54-52.3 100.7-103 140l-9 7H95l7-8c28.7-32 52-65.7 70-101\n 10.7-23.333 16-35.7 16-37 0-.7-7.7-1-23-1h-22C115.7 71.3 68 127 0 167z",twoheadrightarrow:"M400000 167\nc-68-40-115.7-95.7-143-167h-22c-15.3 0-23 .3-23 1 0 1.3 5.3 13.7 16 37 18 35.3\n 41.3 69 70 101l7 8h-125l-9-7c-50.7-39.3-85-86-103-140h-46c0 4.7 6.3 18.7 19 42\n 18 35.3 40 67.3 66 96l9 9H0v40h399716l-9 9c-26 28.7-48 60.7-66 96-12.7 23.333\n-19 37.333-19 42h46c18-54 52.3-100.7 103-140l9-7h125l-7 8c-28.7 32-52 65.7-70\n 101-10.7 23.333-16 35.7-16 37 0 .7 7.7 1 23 1h22c27.3-71.3 75-127 143-167z",tilde1:"M200 55.538c-77 0-168 73.953-177 73.953-3 0-7\n-2.175-9-5.437L2 97c-1-2-2-4-2-6 0-4 2-7 5-9l20-12C116 12 171 0 207 0c86 0\n 114 68 191 68 78 0 168-68 177-68 4 0 7 2 9 5l12 19c1 2.175 2 4.35 2 6.525 0\n 4.35-2 7.613-5 9.788l-19 13.05c-92 63.077-116.937 75.308-183 76.128\n-68.267.847-113-73.952-191-73.952z",tilde2:"M344 55.266c-142 0-300.638 81.316-311.5 86.418\n-8.01 3.762-22.5 10.91-23.5 5.562L1 120c-1-2-1-3-1-4 0-5 3-9 8-10l18.4-9C160.9\n 31.9 283 0 358 0c148 0 188 122 331 122s314-97 326-97c4 0 8 2 10 7l7 21.114\nc1 2.14 1 3.21 1 4.28 0 5.347-3 9.626-7 10.696l-22.3 12.622C852.6 158.372 751\n 181.476 676 181.476c-149 0-189-126.21-332-126.21z",tilde3:"M786 59C457 59 32 175.242 13 175.242c-6 0-10-3.457\n-11-10.37L.15 138c-1-7 3-12 10-13l19.2-6.4C378.4 40.7 634.3 0 804.3 0c337 0\n 411.8 157 746.8 157 328 0 754-112 773-112 5 0 10 3 11 9l1 14.075c1 8.066-.697\n 16.595-6.697 17.492l-21.052 7.31c-367.9 98.146-609.15 122.696-778.15 122.696\n -338 0-409-156.573-744-156.573z",tilde4:"M786 58C457 58 32 177.487 13 177.487c-6 0-10-3.345\n-11-10.035L.15 143c-1-7 3-12 10-13l22-6.7C381.2 35 637.15 0 807.15 0c337 0 409\n 177 744 177 328 0 754-127 773-127 5 0 10 3 11 9l1 14.794c1 7.805-3 13.38-9\n 14.495l-20.7 5.574c-366.85 99.79-607.3 139.372-776.3 139.372-338 0-409\n -175.236-744-175.236z",vec:"M377 20c0-5.333 1.833-10 5.5-14S391 0 397 0c4.667 0 8.667 1.667 12 5\n3.333 2.667 6.667 9 10 19 6.667 24.667 20.333 43.667 41 57 7.333 4.667 11\n10.667 11 18 0 6-1 10-3 12s-6.667 5-14 9c-28.667 14.667-53.667 35.667-75 63\n-1.333 1.333-3.167 3.5-5.5 6.5s-4 4.833-5 5.5c-1 .667-2.5 1.333-4.5 2s-4.333 1\n-7 1c-4.667 0-9.167-1.833-13.5-5.5S337 184 337 178c0-12.667 15.667-32.333 47-59\nH213l-171-1c-8.667-6-13-12.333-13-19 0-4.667 4.333-11.333 13-20h359\nc-16-25.333-24-45-24-59z",widehat1:"M529 0h5l519 115c5 1 9 5 9 10 0 1-1 2-1 3l-4 22\nc-1 5-5 9-11 9h-2L532 67 19 159h-2c-5 0-9-4-11-9l-5-22c-1-6 2-12 8-13z",widehat2:"M1181 0h2l1171 176c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 220h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widehat3:"M1181 0h2l1171 236c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 280h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widehat4:"M1181 0h2l1171 296c6 0 10 5 10 11l-2 23c-1 6-5 10\n-11 10h-1L1182 67 15 340h-1c-6 0-10-4-11-10l-2-23c-1-6 4-11 10-11z",widecheck1:"M529,159h5l519,-115c5,-1,9,-5,9,-10c0,-1,-1,-2,-1,-3l-4,-22c-1,\n-5,-5,-9,-11,-9h-2l-512,92l-513,-92h-2c-5,0,-9,4,-11,9l-5,22c-1,6,2,12,8,13z",widecheck2:"M1181,220h2l1171,-176c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,153l-1167,-153h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",widecheck3:"M1181,280h2l1171,-236c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,213l-1167,-213h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",widecheck4:"M1181,340h2l1171,-296c6,0,10,-5,10,-11l-2,-23c-1,-6,-5,-10,\n-11,-10h-1l-1168,273l-1167,-273h-1c-6,0,-10,4,-11,10l-2,23c-1,6,4,11,10,11z",baraboveleftarrow:"M400000 620h-399890l3 -3c68.7 -52.7 113.7 -120 135 -202\nc4 -14.7 6 -23 6 -25c0 -7.3 -7 -11 -21 -11c-8 0 -13.2 0.8 -15.5 2.5\nc-2.3 1.7 -4.2 5.8 -5.5 12.5c-1.3 4.7 -2.7 10.3 -4 17c-12 48.7 -34.8 92 -68.5 130\ns-74.2 66.3 -121.5 85c-10 4 -16 7.7 -18 11c0 8.7 6 14.3 18 17c47.3 18.7 87.8 47\n121.5 85s56.5 81.3 68.5 130c0.7 2 1.3 5 2 9s1.2 6.7 1.5 8c0.3 1.3 1 3.3 2 6\ns2.2 4.5 3.5 5.5c1.3 1 3.3 1.8 6 2.5s6 1 10 1c14 0 21 -3.7 21 -11\nc0 -2 -2 -10.3 -6 -25c-20 -79.3 -65 -146.7 -135 -202l-3 -3h399890z\nM100 620v40h399900v-40z M0 241v40h399900v-40zM0 241v40h399900v-40z",rightarrowabovebar:"M0 241v40h399891c-47.3 35.3-84 78-110 128-16.7 32\n-27.7 63.7-33 95 0 1.3-.2 2.7-.5 4-.3 1.3-.5 2.3-.5 3 0 7.3 6.7 11 20 11 8 0\n13.2-.8 15.5-2.5 2.3-1.7 4.2-5.5 5.5-11.5 2-13.3 5.7-27 11-41 14.7-44.7 39\n-84.5 73-119.5s73.7-60.2 119-75.5c6-2 9-5.7 9-11s-3-9-9-11c-45.3-15.3-85-40.5\n-119-75.5s-58.3-74.8-73-119.5c-4.7-14-8.3-27.3-11-40-1.3-6.7-3.2-10.8-5.5\n-12.5-2.3-1.7-7.5-2.5-15.5-2.5-14 0-21 3.7-21 11 0 2 2 10.3 6 25 20.7 83.3 67\n151.7 139 205zm96 379h399894v40H0zm0 0h399904v40H0z",baraboveshortleftharpoon:"M507,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17\nc2,0.7,5,1,9,1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21\nc-32,-87.3,-82.7,-157.7,-152,-211c0,0,-3,-3,-3,-3l399351,0l0,-40\nc-398570,0,-399437,0,-399437,0z M593 435 v40 H399500 v-40z\nM0 281 v-40 H399908 v40z M0 281 v-40 H399908 v40z",rightharpoonaboveshortbar:"M0,241 l0,40c399126,0,399993,0,399993,0\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\nM0 241 v40 H399908 v-40z M0 475 v-40 H399500 v40z M0 475 v-40 H399500 v40z",shortbaraboveleftharpoon:"M7,435c-4,4,-6.3,8.7,-7,14c0,5.3,0.7,9,2,11\nc1.3,2,5.3,5.3,12,10c90.7,54,156,130,196,228c3.3,10.7,6.3,16.3,9,17c2,0.7,5,1,9,\n1c0,0,5,0,5,0c10.7,0,16.7,-2,18,-6c2,-2.7,1,-9.7,-3,-21c-32,-87.3,-82.7,-157.7,\n-152,-211c0,0,-3,-3,-3,-3l399907,0l0,-40c-399126,0,-399993,0,-399993,0z\nM93 435 v40 H400000 v-40z M500 241 v40 H400000 v-40z M500 241 v40 H400000 v-40z",shortrightharpoonabovebar:"M53,241l0,40c398570,0,399437,0,399437,0\nc4.7,-4.7,7,-9.3,7,-14c0,-9.3,-3.7,-15.3,-11,-18c-92.7,-56.7,-159,-133.7,-199,\n-231c-3.3,-9.3,-6,-14.7,-8,-16c-2,-1.3,-7,-2,-15,-2c-10.7,0,-16.7,2,-18,6\nc-2,2.7,-1,9.7,3,21c15.3,42,36.7,81.8,64,119.5c27.3,37.7,58,69.2,92,94.5z\nM500 241 v40 H399408 v-40z M500 435 v40 H400000 v-40z"};class I{constructor(e){this.children=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.children=e,this.classes=[],this.height=0,this.depth=0,this.maxFontSize=0,this.style={}}hasClass(e){return this.classes.includes(e)}toNode(){const e=document.createDocumentFragment();for(let t=0;t{if("toText"in e)return e.toText();throw new Error("Expected MathDomNode with toText, got "+e.constructor.name)}).join("")}}const R={pt:1,mm:7227/2540,cm:7227/254,in:72.27,bp:1.00375,pc:12,dd:1238/1157,cc:14856/1157,nd:685/642,nc:1370/107,sp:1/65536,px:1.00375},O={ex:!0,em:!0,mu:!0},E=function(e){return"string"!=typeof e&&(e=e.unit),e in R||e in O||"ex"===e},H=function(e,t){let r;if(e.unit in R)r=R[e.unit]/t.fontMetrics().ptPerEm/t.sizeMultiplier;else if("mu"===e.unit)r=t.fontMetrics().cssEmPerMu;else{let o;if(o=t.style.isTight()?t.havingStyle(t.style.text()):t,"ex"===e.unit)r=o.fontMetrics().xHeight;else{if("em"!==e.unit)throw new n("Invalid unit: '"+e.unit+"'");r=o.fontMetrics().quad}o!==t&&(r*=o.sizeMultiplier/t.sizeMultiplier)}return Math.min(e.number*r,t.maxSize)},N=function(e){return+e.toFixed(4)+"em"},D=function(e){return e.filter(e=>e).join(" ")},L=function(e){let t="";for(const r of Object.keys(e)){const n=e[r];void 0!==n&&(t+=s(r)+":"+n+";")}return t},F=function(e,t,r){if(this.classes=e||[],this.attributes={},this.height=0,this.depth=0,this.maxFontSize=0,this.style=r||{},t){t.style.isTight()&&this.classes.push("mtight");const e=t.getColor();e&&(this.style.color=e)}},P=function(e){const t=document.createElement(e);t.className=D(this.classes),Object.assign(t.style,this.style);for(const e of Object.keys(this.attributes))t.setAttribute(e,this.attributes[e]);for(let e=0;e/=\x00-\x1f]/,G=function(e){let t="<"+e;this.classes.length&&(t+=' class="'+l(D(this.classes))+'"');const r=L(this.style);r&&(t+=' style="'+l(r)+'"');for(const e of Object.keys(this.attributes)){if(V.test(e))throw new n("Invalid attribute name '"+e+"'");t+=" "+e+'="'+l(this.attributes[e])+'"'}t+=">";for(let e=0;e",t};class j{constructor(e,t,r,n){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.width=void 0,this.maxFontSize=void 0,this.style=void 0,this.italic=void 0,F.call(this,e,r,n),this.children=t||[]}setAttribute(e,t){this.attributes[e]=t}hasClass(e){return this.classes.includes(e)}toNode(){return P.call(this,"span")}toMarkup(){return G.call(this,"span")}}class U{constructor(e,t,r,n){this.children=void 0,this.attributes=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,F.call(this,t,n),this.children=r||[],this.setAttribute("href",e)}setAttribute(e,t){this.attributes[e]=t}hasClass(e){return this.classes.includes(e)}toNode(){return P.call(this,"a")}toMarkup(){return G.call(this,"a")}}class Y{constructor(e,t,r){this.src=void 0,this.alt=void 0,this.classes=void 0,this.height=void 0,this.depth=void 0,this.maxFontSize=void 0,this.style=void 0,this.alt=t,this.src=e,this.classes=["mord"],this.height=0,this.depth=0,this.maxFontSize=0,this.style=r}hasClass(e){return this.classes.includes(e)}toNode(){const e=document.createElement("img");return e.src=this.src,e.alt=this.alt,e.className="mord",Object.assign(e.style,this.style),e}toMarkup(){let e=''+l(this.alt)+'=n[0]&&e<=n[1])return r.name}}return null}(this.text.charCodeAt(0));l&&this.classes.push(l+"_fallback"),/[\xee\xef\xed\xec]/.test(this.text)&&(this.text=X[this.text])}hasClass(e){return this.classes.includes(e)}toNode(){const e=document.createTextNode(this.text);let t=null;return this.italic>0&&(t=document.createElement("span"),t.style.marginRight=N(this.italic)),this.classes.length>0&&(t=t||document.createElement("span"),t.className=D(this.classes)),Object.keys(this.style).length>0&&(t=t||document.createElement("span"),Object.assign(t.style,this.style)),t?(t.appendChild(e),t):e}toMarkup(){let e=!1,t="0&&(r+="margin-right:"+N(this.italic)+";"),r+=L(this.style),r&&(e=!0,t+=' style="'+l(r)+'"');const n=l(this.text);return e?(t+=">",t+=n,t+="",t):n}}class _{constructor(e,t){this.children=void 0,this.attributes=void 0,this.children=e||[],this.attributes=t||{}}toNode(){const e=document.createElementNS("http://www.w3.org/2000/svg","svg");for(const t of Object.keys(this.attributes))e.setAttribute(t,this.attributes[t]);for(let t=0;t':''}}class Z{constructor(e){this.attributes=void 0,this.attributes=e||{}}toNode(){const e=document.createElementNS("http://www.w3.org/2000/svg","line");for(const t of Object.keys(this.attributes))e.setAttribute(t,this.attributes[t]);return e}toMarkup(){let e="","\\gt",!0),oe(se,ae,be,"\u2208","\\in",!0),oe(se,ae,be,"\ue020","\\@not"),oe(se,ae,be,"\u2282","\\subset",!0),oe(se,ae,be,"\u2283","\\supset",!0),oe(se,ae,be,"\u2286","\\subseteq",!0),oe(se,ae,be,"\u2287","\\supseteq",!0),oe(se,le,be,"\u2288","\\nsubseteq",!0),oe(se,le,be,"\u2289","\\nsupseteq",!0),oe(se,ae,be,"\u22a8","\\models"),oe(se,ae,be,"\u2190","\\leftarrow",!0),oe(se,ae,be,"\u2264","\\le"),oe(se,ae,be,"\u2264","\\leq",!0),oe(se,ae,be,"<","\\lt",!0),oe(se,ae,be,"\u2192","\\rightarrow",!0),oe(se,ae,be,"\u2192","\\to"),oe(se,le,be,"\u2271","\\ngeq",!0),oe(se,le,be,"\u2270","\\nleq",!0),oe(se,ae,ye,"\xa0","\\ "),oe(se,ae,ye,"\xa0","\\space"),oe(se,ae,ye,"\xa0","\\nobreakspace"),oe(ie,ae,ye,"\xa0","\\ "),oe(ie,ae,ye,"\xa0"," "),oe(ie,ae,ye,"\xa0","\\space"),oe(ie,ae,ye,"\xa0","\\nobreakspace"),oe(se,ae,ye,"","\\nobreak"),oe(se,ae,ye,"","\\allowbreak"),oe(se,ae,fe,",",","),oe(se,ae,fe,";",";"),oe(se,le,he,"\u22bc","\\barwedge",!0),oe(se,le,he,"\u22bb","\\veebar",!0),oe(se,ae,he,"\u2299","\\odot",!0),oe(se,ae,he,"\u2295","\\oplus",!0),oe(se,ae,he,"\u2297","\\otimes",!0),oe(se,ae,xe,"\u2202","\\partial",!0),oe(se,ae,he,"\u2298","\\oslash",!0),oe(se,le,he,"\u229a","\\circledcirc",!0),oe(se,le,he,"\u22a1","\\boxdot",!0),oe(se,ae,he,"\u25b3","\\bigtriangleup"),oe(se,ae,he,"\u25bd","\\bigtriangledown"),oe(se,ae,he,"\u2020","\\dagger"),oe(se,ae,he,"\u22c4","\\diamond"),oe(se,ae,he,"\u22c6","\\star"),oe(se,ae,he,"\u25c3","\\triangleleft"),oe(se,ae,he,"\u25b9","\\triangleright"),oe(se,ae,ge,"{","\\{"),oe(ie,ae,xe,"{","\\{"),oe(ie,ae,xe,"{","\\textbraceleft"),oe(se,ae,me,"}","\\}"),oe(ie,ae,xe,"}","\\}"),oe(ie,ae,xe,"}","\\textbraceright"),oe(se,ae,ge,"{","\\lbrace"),oe(se,ae,me,"}","\\rbrace"),oe(se,ae,ge,"[","\\lbrack",!0),oe(ie,ae,xe,"[","\\lbrack",!0),oe(se,ae,me,"]","\\rbrack",!0),oe(ie,ae,xe,"]","\\rbrack",!0),oe(se,ae,ge,"(","\\lparen",!0),oe(se,ae,me,")","\\rparen",!0),oe(ie,ae,xe,"<","\\textless",!0),oe(ie,ae,xe,">","\\textgreater",!0),oe(se,ae,ge,"\u230a","\\lfloor",!0),oe(se,ae,me,"\u230b","\\rfloor",!0),oe(se,ae,ge,"\u2308","\\lceil",!0),oe(se,ae,me,"\u2309","\\rceil",!0),oe(se,ae,xe,"\\","\\backslash"),oe(se,ae,xe,"\u2223","|"),oe(se,ae,xe,"\u2223","\\vert"),oe(ie,ae,xe,"|","\\textbar",!0),oe(se,ae,xe,"\u2225","\\|"),oe(se,ae,xe,"\u2225","\\Vert"),oe(ie,ae,xe,"\u2225","\\textbardbl"),oe(ie,ae,xe,"~","\\textasciitilde"),oe(ie,ae,xe,"\\","\\textbackslash"),oe(ie,ae,xe,"^","\\textasciicircum"),oe(se,ae,be,"\u2191","\\uparrow",!0),oe(se,ae,be,"\u21d1","\\Uparrow",!0),oe(se,ae,be,"\u2193","\\downarrow",!0),oe(se,ae,be,"\u21d3","\\Downarrow",!0),oe(se,ae,be,"\u2195","\\updownarrow",!0),oe(se,ae,be,"\u21d5","\\Updownarrow",!0),oe(se,ae,de,"\u2210","\\coprod"),oe(se,ae,de,"\u22c1","\\bigvee"),oe(se,ae,de,"\u22c0","\\bigwedge"),oe(se,ae,de,"\u2a04","\\biguplus"),oe(se,ae,de,"\u22c2","\\bigcap"),oe(se,ae,de,"\u22c3","\\bigcup"),oe(se,ae,de,"\u222b","\\int"),oe(se,ae,de,"\u222b","\\intop"),oe(se,ae,de,"\u222c","\\iint"),oe(se,ae,de,"\u222d","\\iiint"),oe(se,ae,de,"\u220f","\\prod"),oe(se,ae,de,"\u2211","\\sum"),oe(se,ae,de,"\u2a02","\\bigotimes"),oe(se,ae,de,"\u2a01","\\bigoplus"),oe(se,ae,de,"\u2a00","\\bigodot"),oe(se,ae,de,"\u222e","\\oint"),oe(se,ae,de,"\u222f","\\oiint"),oe(se,ae,de,"\u2230","\\oiiint"),oe(se,ae,de,"\u2a06","\\bigsqcup"),oe(se,ae,de,"\u222b","\\smallint"),oe(ie,ae,ue,"\u2026","\\textellipsis"),oe(se,ae,ue,"\u2026","\\mathellipsis"),oe(ie,ae,ue,"\u2026","\\ldots",!0),oe(se,ae,ue,"\u2026","\\ldots",!0),oe(se,ae,ue,"\u22ef","\\@cdots",!0),oe(se,ae,ue,"\u22f1","\\ddots",!0),oe(se,ae,xe,"\u22ee","\\varvdots"),oe(ie,ae,xe,"\u22ee","\\varvdots"),oe(se,ae,ce,"\u02ca","\\acute"),oe(se,ae,ce,"\u02cb","\\grave"),oe(se,ae,ce,"\xa8","\\ddot"),oe(se,ae,ce,"~","\\tilde"),oe(se,ae,ce,"\u02c9","\\bar"),oe(se,ae,ce,"\u02d8","\\breve"),oe(se,ae,ce,"\u02c7","\\check"),oe(se,ae,ce,"^","\\hat"),oe(se,ae,ce,"\u20d7","\\vec"),oe(se,ae,ce,"\u02d9","\\dot"),oe(se,ae,ce,"\u02da","\\mathring"),oe(se,ae,pe,"\ue131","\\@imath"),oe(se,ae,pe,"\ue237","\\@jmath"),oe(se,ae,xe,"\u0131","\u0131"),oe(se,ae,xe,"\u0237","\u0237"),oe(ie,ae,xe,"\u0131","\\i",!0),oe(ie,ae,xe,"\u0237","\\j",!0),oe(ie,ae,xe,"\xdf","\\ss",!0),oe(ie,ae,xe,"\xe6","\\ae",!0),oe(ie,ae,xe,"\u0153","\\oe",!0),oe(ie,ae,xe,"\xf8","\\o",!0),oe(ie,ae,xe,"\xc6","\\AE",!0),oe(ie,ae,xe,"\u0152","\\OE",!0),oe(ie,ae,xe,"\xd8","\\O",!0),oe(ie,ae,ce,"\u02ca","\\'"),oe(ie,ae,ce,"\u02cb","\\`"),oe(ie,ae,ce,"\u02c6","\\^"),oe(ie,ae,ce,"\u02dc","\\~"),oe(ie,ae,ce,"\u02c9","\\="),oe(ie,ae,ce,"\u02d8","\\u"),oe(ie,ae,ce,"\u02d9","\\."),oe(ie,ae,ce,"\xb8","\\c"),oe(ie,ae,ce,"\u02da","\\r"),oe(ie,ae,ce,"\u02c7","\\v"),oe(ie,ae,ce,"\xa8",'\\"'),oe(ie,ae,ce,"\u02dd","\\H"),oe(ie,ae,ce,"\u25ef","\\textcircled");const we={"--":!0,"---":!0,"``":!0,"''":!0};oe(ie,ae,xe,"\u2013","--",!0),oe(ie,ae,xe,"\u2013","\\textendash"),oe(ie,ae,xe,"\u2014","---",!0),oe(ie,ae,xe,"\u2014","\\textemdash"),oe(ie,ae,xe,"\u2018","`",!0),oe(ie,ae,xe,"\u2018","\\textquoteleft"),oe(ie,ae,xe,"\u2019","'",!0),oe(ie,ae,xe,"\u2019","\\textquoteright"),oe(ie,ae,xe,"\u201c","``",!0),oe(ie,ae,xe,"\u201c","\\textquotedblleft"),oe(ie,ae,xe,"\u201d","''",!0),oe(ie,ae,xe,"\u201d","\\textquotedblright"),oe(se,ae,xe,"\xb0","\\degree",!0),oe(ie,ae,xe,"\xb0","\\degree"),oe(ie,ae,xe,"\xb0","\\textdegree",!0),oe(se,ae,xe,"\xa3","\\pounds"),oe(se,ae,xe,"\xa3","\\mathsterling",!0),oe(ie,ae,xe,"\xa3","\\pounds"),oe(ie,ae,xe,"\xa3","\\textsterling",!0),oe(se,le,xe,"\u2720","\\maltese"),oe(ie,le,xe,"\u2720","\\maltese");const ve='0123456789/@."';for(let e=0;e<14;e++){const t=ve.charAt(e);oe(se,ae,xe,t,t)}const ke='0123456789!@*()-=+";:?/.,';for(let e=0;e<25;e++){const t=ke.charAt(e);oe(ie,ae,xe,t,t)}const ze="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";for(let e=0;e<52;e++){const t=ze.charAt(e);oe(se,ae,pe,t,t),oe(ie,ae,xe,t,t)}let Se;oe(se,le,xe,"C","\u2102"),oe(ie,le,xe,"C","\u2102"),oe(se,le,xe,"H","\u210d"),oe(ie,le,xe,"H","\u210d"),oe(se,le,xe,"N","\u2115"),oe(ie,le,xe,"N","\u2115"),oe(se,le,xe,"P","\u2119"),oe(ie,le,xe,"P","\u2119"),oe(se,le,xe,"Q","\u211a"),oe(ie,le,xe,"Q","\u211a"),oe(se,le,xe,"R","\u211d"),oe(ie,le,xe,"R","\u211d"),oe(se,le,xe,"Z","\u2124"),oe(ie,le,xe,"Z","\u2124"),oe(se,ae,pe,"h","\u210e"),oe(ie,ae,pe,"h","\u210e");for(let e=0;e<52;e++){const t=ze.charAt(e);Se=String.fromCharCode(55349,56320+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,56372+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,56424+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,56580+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,56684+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,56736+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,56788+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,56840+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,56944+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),e<26&&(Se=String.fromCharCode(55349,56632+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,56476+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se))}Se=String.fromCharCode(55349,56668),oe(se,ae,pe,"k",Se),oe(ie,ae,xe,"k",Se);for(let e=0;e<10;e++){const t=e.toString();Se=String.fromCharCode(55349,57294+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,57314+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,57324+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se),Se=String.fromCharCode(55349,57334+e),oe(se,ae,pe,t,Se),oe(ie,ae,xe,t,Se)}const Me="\xd0\xde\xfe";for(let e=0;e<3;e++){const t=Me.charAt(e);oe(se,ae,pe,t,t),oe(ie,ae,xe,t,t)}const Ae={mathClass:"mathbf",textClass:"textbf",font:"Main-Bold"},Te={mathClass:"mathnormal",textClass:"textit",font:"Math-Italic"},Ce={mathClass:"boldsymbol",textClass:"boldsymbol",font:"Main-BoldItalic"},Be={mathClass:"",textClass:"",font:""},qe={mathClass:"mathfrak",textClass:"textfrak",font:"Fraktur-Regular"},Ie={mathClass:"mathbb",textClass:"textbb",font:"AMS-Regular"},Re={mathClass:"mathboldfrak",textClass:"textboldfrak",font:"Fraktur-Regular"},Oe={mathClass:"mathsf",textClass:"textsf",font:"SansSerif-Regular"},Ee={mathClass:"mathboldsf",textClass:"textboldsf",font:"SansSerif-Bold"},He={mathClass:"mathitsf",textClass:"textitsf",font:"SansSerif-Italic"},Ne={mathClass:"mathtt",textClass:"texttt",font:"Typewriter-Regular"},De=[Ae,Ae,Te,Te,Ce,Ce,{mathClass:"mathscr",textClass:"textscr",font:"Script-Regular"},Be,Be,Be,qe,qe,Ie,Ie,Re,Re,Oe,Oe,Ee,Ee,He,He,Be,Be,Ne,Ne],Le=[Ae,Be,Oe,Ee,Ne];function Fe(e){const t=e.strict,r=e.errorCode,o=e.errorMsg,s=e.token,i=e.report;let a=t;if("function"==typeof t)if(i)a=t(r,o,s);else try{a=t(r,o,s)}catch(e){a="error"}switch(a){case!0:case"error":if(i)throw new n("LaTeX-incompatible input and strict mode is set to 'error': "+o+" ["+r+"]",s);return!0;case!1:case"ignore":if(!i)return!1;break;default:if("undefined"!=typeof console&&console.warn("LaTeX-incompatible input and strict mode is set to 'warn': "+o+" ["+r+"]"),!i)return!1}}const Pe=function(e,t,r){if(ne[r][e]){const t=ne[r][e].replace;t&&(e=t)}return{value:e,metrics:ee(e,t,r)}},Ve=function(e,t,r,n,o){const s=Pe(e,t,r),i=s.metrics;let a;if(e=s.value,i){let t=i.italic;("text"===r||n&&"mathit"===n.font)&&(t=0),a=new W(e,i.height,i.depth,t,i.skew,i.width,o)}else Fe({strict:n.strict,errorCode:"symbolNotInFont",errorMsg:"No character metrics for '"+e+"' in style '"+t+"' and mode '"+r+"'",report:!0}),a=new W(e,0,0,0,0,0,o);a.maxFontSize=n.sizeMultiplier,n.style.isTight()&&a.classes.push("mtight");const l=n.getColor();return l&&(a.style.color=l),a},Ge=function(e,t,r,n){return void 0===n&&(n=[]),"boldsymbol"===r.font&&Pe(e,"Main-Bold",t).metrics?Ve(e,"Main-Bold",t,r,n.concat(["mathbf"])):"\\"===e||"main"===ne[t][e].font?Ve(e,"Main-Regular",t,r,n):Ve(e,"AMS-Regular",t,r,n.concat(["amsrm"]))},je=function(e,t){const r="mathord"===e.type?"mathord":"textord",o=e.mode,s=e.text,i=["mord"],a=t.font,l=t.fontFamily,c=t.fontWeight,h=t.fontShape,m="math"===o||"text"===o&&!!a,u=m?a:l;let p="",d="";if(55349===s.charCodeAt(0)){const e=(e=>{const t=1024*(e.charCodeAt(0)-55296)+(e.charCodeAt(1)-56320)+65536;if(119808<=t&&t<120484){const e=Math.floor((t-119808)/26);return De[e]}if(120782<=t&&t<=120831){const e=Math.floor((t-120782)/10);return Le[e]}if(120485===t||120486===t)return De[0];if(120486{if(D(e.classes)!==D(t.classes)||e.skew!==t.skew||e.maxFontSize!==t.maxFontSize||0!==e.italic&&e.hasClass("mathnormal"))return!1;if(1===e.classes.length){const t=e.classes[0];if("mbin"===t||"mord"===t)return!1}for(const r of Object.keys(e.style))if(e.style[r]!==t.style[r])return!1;for(const r of Object.keys(t.style))if(e.style[r]!==t.style[r])return!1;return!0},Ye=e=>{for(let t=0;tt&&(t=s.height),s.depth>r&&(r=s.depth),s.maxFontSize>n&&(n=s.maxFontSize)}e.height=t,e.depth=r,e.maxFontSize=n},We=function(e,t,r,n){const o=new j(e,t,r,n);return Xe(o),o},_e=(e,t,r,n)=>new j(e,t,r,n),$e=function(e,t,r){const n=We([e],[],t);return n.height=Math.max(r||t.fontMetrics().defaultRuleThickness,t.minRuleThickness),n.style.borderBottomWidth=N(n.height),n.maxFontSize=1,n},Ze=function(e){const t=new I(e);return Xe(t),t},Ke=function(e,t){return e instanceof I?We([],[e],t):e},Je=function(e,t){const r=function(e){if("individualShift"===e.positionType){const t=e.children,r=[t[0]],n=-t[0].shift-t[0].elem.depth;let o=n;for(let e=1;e{const r=We(["mspace"],[],t),n=H(e,t);return r.style.marginRight=N(n),r},et=(e,t,r)=>{let n,o;switch(e){case"amsrm":n="AMS";break;case"textrm":n="Main";break;case"textsf":n="SansSerif";break;case"texttt":n="Typewriter";break;default:n=e}return o="textbf"===t&&"textit"===r?"BoldItalic":"textbf"===t?"Bold":"textit"===r?"Italic":"Regular",n+"-"+o},tt={mathbf:{variant:"bold",fontName:"Main-Bold"},mathrm:{variant:"normal",fontName:"Main-Regular"},textit:{variant:"italic",fontName:"Main-Italic"},mathit:{variant:"italic",fontName:"Main-Italic"},mathnormal:{variant:"italic",fontName:"Math-Italic"},mathsfit:{variant:"sans-serif-italic",fontName:"SansSerif-Italic"},mathbb:{variant:"double-struck",fontName:"AMS-Regular"},mathcal:{variant:"script",fontName:"Caligraphic-Regular"},mathfrak:{variant:"fraktur",fontName:"Fraktur-Regular"},mathscr:{variant:"script",fontName:"Script-Regular"},mathsf:{variant:"sans-serif",fontName:"SansSerif-Regular"},mathtt:{variant:"monospace",fontName:"Typewriter-Regular"}},rt={vec:["vec",.471,.714],oiintSize1:["oiintSize1",.957,.499],oiintSize2:["oiintSize2",1.472,.659],oiiintSize1:["oiiintSize1",1.304,.499],oiiintSize2:["oiiintSize2",1.98,.659]},nt=function(e,t){const r=rt[e],n=r[0],o=r[1],s=r[2],i=new $(n),a=new _([i],{width:N(o),height:N(s),style:"width:"+N(o),viewBox:"0 0 "+1e3*o+" "+1e3*s,preserveAspectRatio:"xMinYMin"}),l=_e(["katex-overlay"],[a],t);return l.height=s,l.style.height=N(s),l.style.width=N(o),l},ot={number:3,unit:"mu"},st={number:4,unit:"mu"},it={number:5,unit:"mu"},at={mord:{mop:ot,mbin:st,mrel:it,minner:ot},mop:{mord:ot,mop:ot,mrel:it,minner:ot},mbin:{mord:st,mop:st,mopen:st,minner:st},mrel:{mord:it,mop:it,mopen:it,minner:it},mopen:{},mclose:{mop:ot,mbin:st,mrel:it,minner:ot},mpunct:{mord:ot,mop:ot,mrel:it,mopen:ot,mclose:ot,mpunct:ot,minner:ot},minner:{mord:ot,mop:ot,mbin:st,mrel:it,mopen:ot,mpunct:ot,minner:ot}},lt={mord:{mop:ot},mop:{mord:ot,mop:ot},mbin:{},mrel:{},mopen:{},mclose:{mop:ot},mpunct:{},minner:{mop:ot}},ct={},ht={},mt={};function ut(e){const t=e.type,r=e.names,n=e.htmlBuilder,o=e.mathmlBuilder;for(let t=0;t{const r=t.classes[0],n=e.classes[0];"mbin"===r&&bt.has(n)?t.classes[0]="mord":"mbin"===n&&ft.has(r)&&(e.classes[0]="mord")},{node:i},a,l),vt(o,(e,t)=>{var r,n;const o=St(t),i=St(e),a=o&&i?e.hasClass("mtight")?null==(r=lt[o])?void 0:r[i]:null==(n=at[o])?void 0:n[i]:null;if(a)return Qe(a,s)},{node:i},a,l),o},vt=function(e,t,r,n,o){n&&e.push(n);let s=0;for(;sr=>{e.splice(t+1,0,r),s++})(s)}n&&e.pop()},kt=function(e){return e instanceof I||e instanceof U||e instanceof j&&e.hasClass("enclosing")?e:null},zt=function(e,t){const r=kt(e);if(r){const e=r.children;if(e.length){if("right"===t)return zt(e[e.length-1],"right");if("left"===t)return zt(e[0],"left")}}return e},St=function(e,t){if(!e)return null;t&&(e=zt(e,t));const r=e.classes[0];return xt[r]||null},Mt=function(e,t){const r=["nulldelimiter"].concat(e.baseSizingClasses());return We(t.concat(r))},At=function(e,t,r){if(!e)return We();if(ht[e.type]){let n=ht[e.type](e,t);if(r&&t.size!==r.size){n=We(t.sizingClasses(r),[n],t);const e=t.sizeMultiplier/r.sizeMultiplier;n.height*=e,n.depth*=e}return n}throw new n("Got group of unknown type: '"+e.type+"'")};function Tt(e,t){const r=We(["katex-base"],e,t),n=We(["katex-strut"]);return n.style.height=N(r.height+r.depth),r.depth&&(n.style.verticalAlign=N(-r.depth)),r.children.unshift(n),r}function Ct(e,t){let r=null;1===e.length&&"tag"===e[0].type&&(r=e[0].tag,e=e[0].body);const n=wt(e,t,"root");let o;2===n.length&&n[1].hasClass("katex-tag")&&(o=n.pop());const s=[];let i,a=[];for(let e=0;e0&&(s.push(Tt(a,t)),a=[]),s.push(n[e]));a.length>0&&s.push(Tt(a,t)),r?(i=Tt(wt(r,t,!0),t),i.classes=["katex-tag"],s.push(i)):o&&s.push(o);const l=We(["katex-html"],s);if(l.setAttribute("aria-hidden","true"),i){const e=i.children[0];e.style.height=N(l.height+l.depth),l.depth&&(e.style.verticalAlign=N(-l.depth))}return l}function Bt(e){return new I(e)}class qt{constructor(e,t,r){this.type=void 0,this.attributes=void 0,this.children=void 0,this.classes=void 0,this.type=e,this.attributes={},this.children=t||[],this.classes=r||[]}setAttribute(e,t){this.attributes[e]=t}getAttribute(e){return this.attributes[e]}toNode(){const e=document.createElementNS("http://www.w3.org/1998/Math/MathML",this.type);for(const t of Object.entries(this.attributes)){const r=t[0],n=t[1];e.setAttribute(r,n)}this.classes.length>0&&(e.className=D(this.classes));for(let t=0;t0&&(e+=' class ="'+l(D(this.classes))+'"'),e+=">";for(let t=0;t",e}toText(){return this.children.map(e=>e.toText()).join("")}}class It{constructor(e){this.text=void 0,this.text=e}toNode(){return document.createTextNode(this.text)}toMarkup(){return l(this.toText())}toText(){return this.text}}class Rt{constructor(e){this.width=void 0,this.character=void 0,this.width=e,this.character=e>=.05555&&e<=.05556?"\u200a":e>=.1666&&e<=.1667?"\u2009":e>=.2222&&e<=.2223?"\u2005":e>=.2777&&e<=.2778?"\u2005\u200a":e>=-.05556&&e<=-.05555?"\u200a\u2063":e>=-.1667&&e<=-.1666?"\u2009\u2063":e>=-.2223&&e<=-.2222?"\u205f\u2063":e>=-.2778&&e<=-.2777?"\u2005\u2063":null}toNode(){if(this.character)return document.createTextNode(this.character);{const e=document.createElementNS("http://www.w3.org/1998/Math/MathML","mspace");return e.setAttribute("width",N(this.width)),e}}toMarkup(){return this.character?""+this.character+"":''}toText(){return this.character?this.character:" "}}const Ot=new Set(["\\imath","\\jmath"]),Et=new Set(["mrow","mtable"]),Ht=function(e,t,r){var n,o;return ne[t][e]&&ne[t][e].replace&&55349!==e.charCodeAt(0)&&(!Object.prototype.hasOwnProperty.call(we,e)||"tt"!==(null==r||null==(n=r.fontFamily)?void 0:n.slice(4,6))&&"tt"!==(null==r||null==(o=r.font)?void 0:o.slice(4,6)))&&(e=ne[t][e].replace),new It(e)},Nt=function(e){return 1===e.length?e[0]:new qt("mrow",e)},Dt={mathit:"italic",boldsymbol:e=>"textord"===e.type?"bold":"bold-italic",mathbf:"bold",mathbb:"double-struck",mathsfit:"sans-serif-italic",mathfrak:"fraktur",mathscr:"script",mathcal:"script",mathsf:"sans-serif",mathtt:"monospace"},Lt=(e,t)=>{if("text"===e.mode){if("texttt"===t.fontFamily)return"monospace";if("textsf"===t.fontFamily)return"textit"===t.fontShape&&"textbf"===t.fontWeight?"sans-serif-bold-italic":"textit"===t.fontShape?"sans-serif-italic":"textbf"===t.fontWeight?"bold-sans-serif":"sans-serif";if("textit"===t.fontShape&&"textbf"===t.fontWeight)return"bold-italic";if("textit"===t.fontShape)return"italic";if("textbf"===t.fontWeight)return"bold"}const r=t.font;if(!r||"mathnormal"===r)return null;const n=e.mode,o=Dt[r];if(o)return"function"==typeof o?o(e):o;let s=e.text;if(Ot.has(s))return null;if(ne[n][s]){const e=ne[n][s].replace;e&&(s=e)}return ee(s,tt[r].fontName,n)?tt[r].variant:null};function Ft(e){if(!e)return!1;if("mi"===e.type&&1===e.children.length){const t=e.children[0];return t instanceof It&&"."===t.text}if("mo"===e.type&&1===e.children.length&&"true"===e.getAttribute("separator")&&"0em"===e.getAttribute("lspace")&&"0em"===e.getAttribute("rspace")){const t=e.children[0];return t instanceof It&&","===t.text}return!1}const Pt=function(e,t,r){if(1===e.length){const n=Gt(e[0],t);return r&&n instanceof qt&&"mo"===n.type&&(n.setAttribute("lspace","0em"),n.setAttribute("rspace","0em")),[n]}const n=[];let o;for(let r=0;r=1&&("mn"===o.type||Ft(o))){const e=s.children[0];e instanceof qt&&"mn"===e.type&&(e.children=[...o.children,...e.children],n.pop())}else if("mi"===o.type&&1===o.children.length){const e=o.children[0];if(e instanceof It&&"\u0338"===e.text&&("mo"===s.type||"mi"===s.type||"mn"===s.type)){const e=s.children[0];e instanceof It&&e.text.length>0&&(e.text=e.text.slice(0,1)+"\u0338"+e.text.slice(1),n.pop())}}}n.push(s),o=s}return n},Vt=function(e,t,r){return Nt(Pt(e,t,r))},Gt=function(e,t){if(!e)return new qt("mrow");if(mt[e.type])return mt[e.type](e,t);throw new n("Got group of unknown type: '"+e.type+"'")};function jt(e,t,r,n,o){const s=Pt(e,r);let i;i=1===s.length&&s[0]instanceof qt&&Et.has(s[0].type)?s[0]:new qt("mrow",s);const a=new qt("annotation",[new It(t)]);a.setAttribute("encoding","application/x-tex");const l=new qt("semantics",[i,a]),c=new qt("math",[l]);c.setAttribute("xmlns","http://www.w3.org/1998/Math/MathML"),n&&c.setAttribute("display","block");return We([o?"katex":"katex-mathml"],[c])}const Ut=[[1,1,1],[2,1,1],[3,1,1],[4,2,1],[5,2,1],[6,3,1],[7,4,2],[8,6,3],[9,7,6],[10,8,7],[11,10,9]],Yt=[.5,.6,.7,.8,.9,1,1.2,1.44,1.728,2.074,2.488],Xt=function(e,t){return t.size<2?e:Ut[e-1][t.size-1]};class Wt{constructor(e){this.style=void 0,this.color=void 0,this.size=void 0,this.textSize=void 0,this.phantom=void 0,this.font=void 0,this.fontFamily=void 0,this.fontWeight=void 0,this.fontShape=void 0,this.sizeMultiplier=void 0,this.maxSize=void 0,this.minRuleThickness=void 0,this._fontMetrics=void 0,this.strict=void 0,this.style=e.style,this.color=e.color,this.size=e.size||Wt.BASESIZE,this.textSize=e.textSize||this.size,this.phantom=!!e.phantom,this.font=e.font||"",this.fontFamily=e.fontFamily||"",this.fontWeight=e.fontWeight||"",this.fontShape=e.fontShape||"",this.sizeMultiplier=Yt[this.size-1],this.maxSize=e.maxSize,this.minRuleThickness=e.minRuleThickness,this.strict=e.strict,this._fontMetrics=void 0}extend(e){const t={style:this.style,size:this.size,textSize:this.textSize,color:this.color,phantom:this.phantom,font:this.font,fontFamily:this.fontFamily,fontWeight:this.fontWeight,fontShape:this.fontShape,maxSize:this.maxSize,minRuleThickness:this.minRuleThickness,strict:this.strict};return Object.assign(t,e),new Wt(t)}havingStyle(e){return this.style===e?this:this.extend({style:e,size:Xt(this.textSize,e)})}havingCrampedStyle(){return this.havingStyle(this.style.cramp())}havingSize(e){return this.size===e&&this.textSize===e?this:this.extend({style:this.style.text(),size:e,textSize:e,sizeMultiplier:Yt[e-1]})}havingBaseStyle(e){e=e||this.style.text();const t=Xt(Wt.BASESIZE,e);return this.size===t&&this.textSize===Wt.BASESIZE&&this.style===e?this:this.extend({style:e,size:t})}havingBaseSizing(){let e;switch(this.style.id){case 4:case 5:e=3;break;case 6:case 7:e=1;break;default:e=6}return this.extend({style:this.style.text(),size:e})}withColor(e){return this.extend({color:e})}withPhantom(){return this.extend({phantom:!0})}withFont(e){return this.extend({font:e})}withTextFontFamily(e){return this.extend({fontFamily:e,font:""})}withTextFontWeight(e){return this.extend({fontWeight:e,font:""})}withTextFontShape(e){return this.extend({fontShape:e,font:""})}sizingClasses(e){return e.size!==this.size?["katex-sizing","reset-size"+e.size,"size"+this.size]:[]}baseSizingClasses(){return this.size!==Wt.BASESIZE?["katex-sizing","reset-size"+this.size,"size"+Wt.BASESIZE]:[]}fontMetrics(){return this._fontMetrics||(this._fontMetrics=function(e){let t;if(t=e>=5?0:e>=3?1:2,!te[t]){const e=te[t]={cssEmPerMu:J.quad[t]/18};for(const r of Object.keys(J))e[r]=J[r][t]}return te[t]}(this.size)),this._fontMetrics}getColor(){return this.phantom?"transparent":this.color}}Wt.BASESIZE=6;var _t=Wt;const $t=e=>new _t({style:e.displayMode?S.DISPLAY:S.TEXT,maxSize:e.maxSize,minRuleThickness:e.minRuleThickness,strict:e.strict}),Zt=function(e,t){if(t.displayMode){const r=["katex-display"];t.leqno&&r.push("leqno"),t.fleqn&&r.push("fleqn"),e=We(r,[e])}return e},Kt=function(e,t,r){const n=$t(r);let o;if("mathml"===r.output)return jt(e,t,n,r.displayMode,!0);if("html"===r.output){const t=Ct(e,n);o=We(["katex"],[t])}else{const s=jt(e,t,n,r.displayMode,!1),i=Ct(e,n);o=We(["katex"],[s,i])}return Zt(o,r)};const Jt={widehat:"^",widecheck:"\u02c7",widetilde:"~",utilde:"~",overleftarrow:"\u2190",underleftarrow:"\u2190",xleftarrow:"\u2190",overrightarrow:"\u2192",underrightarrow:"\u2192",xrightarrow:"\u2192",underbrace:"\u23df",overbrace:"\u23de",underbracket:"\u23b5",overbracket:"\u23b4",overgroup:"\u23e0",undergroup:"\u23e1",overleftrightarrow:"\u2194",underleftrightarrow:"\u2194",xleftrightarrow:"\u2194",Overrightarrow:"\u21d2",xRightarrow:"\u21d2",overleftharpoon:"\u21bc",xleftharpoonup:"\u21bc",overrightharpoon:"\u21c0",xrightharpoonup:"\u21c0",xLeftarrow:"\u21d0",xLeftrightarrow:"\u21d4",xhookleftarrow:"\u21a9",xhookrightarrow:"\u21aa",xmapsto:"\u21a6",xrightharpoondown:"\u21c1",xleftharpoondown:"\u21bd",xrightleftharpoons:"\u21cc",xleftrightharpoons:"\u21cb",xtwoheadleftarrow:"\u219e",xtwoheadrightarrow:"\u21a0",xlongequal:"=",xtofrom:"\u21c4",xrightleftarrows:"\u21c4",xrightequilibrium:"\u21cc",xleftequilibrium:"\u21cb","\\cdrightarrow":"\u2192","\\cdleftarrow":"\u2190","\\cdlongequal":"="},Qt=function(e){const t=new qt("mo",[new It(Jt[e.replace(/^\\/,"")])]);return t.setAttribute("stretchy","true"),t},er={overrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],overleftarrow:[["leftarrow"],.888,522,"xMinYMin"],underrightarrow:[["rightarrow"],.888,522,"xMaxYMin"],underleftarrow:[["leftarrow"],.888,522,"xMinYMin"],xrightarrow:[["rightarrow"],1.469,522,"xMaxYMin"],"\\cdrightarrow":[["rightarrow"],3,522,"xMaxYMin"],xleftarrow:[["leftarrow"],1.469,522,"xMinYMin"],"\\cdleftarrow":[["leftarrow"],3,522,"xMinYMin"],Overrightarrow:[["doublerightarrow"],.888,560,"xMaxYMin"],xRightarrow:[["doublerightarrow"],1.526,560,"xMaxYMin"],xLeftarrow:[["doubleleftarrow"],1.526,560,"xMinYMin"],overleftharpoon:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoonup:[["leftharpoon"],.888,522,"xMinYMin"],xleftharpoondown:[["leftharpoondown"],.888,522,"xMinYMin"],overrightharpoon:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoonup:[["rightharpoon"],.888,522,"xMaxYMin"],xrightharpoondown:[["rightharpoondown"],.888,522,"xMaxYMin"],xlongequal:[["longequal"],.888,334,"xMinYMin"],"\\cdlongequal":[["longequal"],3,334,"xMinYMin"],xtwoheadleftarrow:[["twoheadleftarrow"],.888,334,"xMinYMin"],xtwoheadrightarrow:[["twoheadrightarrow"],.888,334,"xMaxYMin"],overleftrightarrow:[["leftarrow","rightarrow"],.888,522],overbrace:[["leftbrace","midbrace","rightbrace"],1.6,548],underbrace:[["leftbraceunder","midbraceunder","rightbraceunder"],1.6,548],underleftrightarrow:[["leftarrow","rightarrow"],.888,522],xleftrightarrow:[["leftarrow","rightarrow"],1.75,522],xLeftrightarrow:[["doubleleftarrow","doublerightarrow"],1.75,560],xrightleftharpoons:[["leftharpoondownplus","rightharpoonplus"],1.75,716],xleftrightharpoons:[["leftharpoonplus","rightharpoondownplus"],1.75,716],xhookleftarrow:[["leftarrow","righthook"],1.08,522],xhookrightarrow:[["lefthook","rightarrow"],1.08,522],overlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],underlinesegment:[["leftlinesegment","rightlinesegment"],.888,522],overbracket:[["leftbracketover","rightbracketover"],1.6,440],underbracket:[["leftbracketunder","rightbracketunder"],1.6,410],overgroup:[["leftgroup","rightgroup"],.888,342],undergroup:[["leftgroupunder","rightgroupunder"],.888,342],xmapsto:[["leftmapsto","rightarrow"],1.5,522],xtofrom:[["leftToFrom","rightToFrom"],1.75,528],xrightleftarrows:[["baraboveleftarrow","rightarrowabovebar"],1.75,901],xrightequilibrium:[["baraboveshortleftharpoon","rightharpoonaboveshortbar"],1.75,716],xleftequilibrium:[["shortbaraboveleftharpoon","shortrightharpoonabovebar"],1.75,716]},tr=new Set(["widehat","widecheck","widetilde","utilde"]),rr=function(e,t){const r=function(){let r=4e5;const n=e.label.slice(1);if(tr.has(n)&&"base"in e){const o="ordgroup"===e.base.type?e.base.body.length:1;let s,i,a;if(o>5)"widehat"===n||"widecheck"===n?(s=420,r=2364,a=.42,i=n+"4"):(s=312,r=2340,a=.34,i="tilde4");else{const e=[1,1,2,2,3,3][o];"widehat"===n||"widecheck"===n?(r=[0,1062,2364,2364,2364][e],s=[0,239,300,360,420][e],a=[0,.24,.3,.3,.36,.42][e],i=n+e):(r=[0,600,1033,2339,2340][e],s=[0,260,286,306,312][e],a=[0,.26,.286,.3,.306,.34][e],i="tilde"+e)}const l=new $(i),c=new _([l],{width:"100%",height:N(a),viewBox:"0 0 "+r+" "+s,preserveAspectRatio:"none"});return{span:_e([],[c],t),minWidth:0,height:a}}{const e=[],o=er[n];if(!o)throw new Error('No SVG data for "'+n+'".');const s=o[0],i=o[1],a=o[2],l=a/1e3,c=s.length;let h,m;if(1===c){if(4!==o.length)throw new Error('Expected 4-tuple for single-path SVG data "'+n+'".');h=["hide-tail"],m=[o[3]]}else if(2===c)h=["halfarrow-left","halfarrow-right"],m=["xMinYMin","xMaxYMin"];else{if(3!==c)throw new Error("Correct katexImagesData or update code here to support\n "+c+" children.");h=["brace-left","brace-center","brace-right"],m=["xMinYMin","xMidYMin","xMaxYMin"]}for(let n=0;n0&&(n.style.minWidth=N(o)),n},nr=new Set(["bin","close","inner","open","punct","rel"]),or=new Set(["accent-token","mathord","op-token","spacing","textord"]);function sr(e,t){if(!e||e.type!==t)throw new Error("Expected node of type "+t+", but got "+(e?"node of type "+e.type:String(e)));return e}function ir(e){const t=ar(e);if(!t)throw new Error("Expected node of symbol group type, but got "+(e?"node of type "+e.type:String(e)));return t}function ar(e){return"atom"===e.type||or.has(e.type)?e:null}function lr(e,t,r){let o="";for(const s of e.body)if("textord"===s.type)o+=s.text;else{if(!r||"spacing"!==s.type||" "!==s.text)throw new n(t,e);o+=" "}return o}const cr=e=>{return e instanceof W?e:((t=e)instanceof j||t instanceof U||t instanceof I)&&1===e.children.length?cr(e.children[0]):void 0;var t},hr=(e,t)=>{let r,n,o;e&&"supsub"===e.type?(n=sr(e.base,"accent"),r=n.base,e.base=r,o=function(e){if(e instanceof j)return e;throw new Error("Expected span but got "+String(e)+".")}(At(e,t)),e.base=n):(n=sr(e,"accent"),r=n.base);const s=At(r,t.havingCrampedStyle());let i=0;var a,l;n.isShifty&&m(r)&&(i=null!=(a=null==(l=cr(s))?void 0:l.skew)?a:0);const c="\\c"===n.label;let h,u=c?s.height+s.depth:Math.min(s.height,t.fontMetrics().xHeight);if(n.isStretchy)h=rr(n,t),h=Je({positionType:"firstBaseline",children:[{type:"elem",elem:s},{type:"elem",elem:h,wrapperClasses:["svg-align"],wrapperStyle:i>0?{width:"calc(100% - "+N(2*i)+")",marginLeft:N(2*i)}:void 0}]});else{let e,r;"\\vec"===n.label?(e=nt("vec",t),r=rt.vec[1]):(e=je({type:"textord",mode:n.mode,text:n.label},t),e=function(e){if(e instanceof W)return e;throw new Error("Expected symbolNode but got "+String(e)+".")}(e),e.italic=0,r=e.width,c&&(u+=e.depth)),h=We(["accent-body"],[e]);const o="\\textcircled"===n.label;o&&(h.classes.push("accent-full"),u=s.height);let a=i;o||(a-=r/2),h.style.left=N(a),"\\textcircled"===n.label&&(h.style.top=".2em"),h=Je({positionType:"firstBaseline",children:[{type:"elem",elem:s},{type:"kern",size:-u},{type:"elem",elem:h}]})}const p=We(["mord","katex-accent"],[h],t);return o?(o.children[0]=p,o.height=Math.max(p.height,o.height),o.classes[0]="mord",o):p},mr=new RegExp(["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring"].map(e=>"\\"+e).join("|"));ut({type:"accent",names:["\\acute","\\grave","\\ddot","\\tilde","\\bar","\\breve","\\check","\\hat","\\vec","\\dot","\\mathring","\\widecheck","\\widehat","\\widetilde","\\overrightarrow","\\overleftarrow","\\Overrightarrow","\\overleftrightarrow","\\overgroup","\\overlinesegment","\\overleftharpoon","\\overrightharpoon"],numArgs:1,handler:(e,t)=>{const r=dt(t[0]),n=!mr.test(e.funcName),o=!n||"\\widehat"===e.funcName||"\\widetilde"===e.funcName||"\\widecheck"===e.funcName;return{type:"accent",mode:e.parser.mode,label:e.funcName,isStretchy:n,isShifty:o,base:r}},htmlBuilder:hr,mathmlBuilder:(e,t)=>{const r=e.isStretchy?Qt(e.label):new qt("mo",[Ht(e.label,e.mode)]),n=new qt("mover",[Gt(e.base,t),r]);return n.setAttribute("accent","true"),n}}),ut({type:"accent",names:["\\'","\\`","\\^","\\~","\\=","\\u","\\.",'\\"',"\\c","\\r","\\H","\\v","\\textcircled"],numArgs:1,allowedInText:!0,allowedInMath:!0,argTypes:["primitive"],handler:(e,t)=>{const r=t[0];let n=e.parser.mode;return"math"===n&&(Fe({strict:e.parser.settings.strict,errorCode:"mathVsTextAccents",errorMsg:"LaTeX's accent "+e.funcName+" works only in text mode",report:!0}),n="text"),{type:"accent",mode:n,label:e.funcName,isStretchy:!1,isShifty:!0,base:r}}}),ut({type:"accentUnder",names:["\\underleftarrow","\\underrightarrow","\\underleftrightarrow","\\undergroup","\\underlinesegment","\\utilde"],numArgs:1,handler:(e,t)=>{let r=e.parser,n=e.funcName;const o=t[0];return{type:"accentUnder",mode:r.mode,label:n,base:o}},htmlBuilder:(e,t)=>{const r=At(e.base,t),n=rr(e,t),o="\\utilde"===e.label?.12:0,s=Je({positionType:"top",positionData:r.height,children:[{type:"elem",elem:n,wrapperClasses:["svg-align"]},{type:"kern",size:o},{type:"elem",elem:r}]});return We(["mord","accentunder"],[s],t)},mathmlBuilder:(e,t)=>{const r=Qt(e.label),n=new qt("munder",[Gt(e.base,t),r]);return n.setAttribute("accentunder","true"),n}});const ur=e=>{const t=new qt("mpadded",e?[e]:[]);return t.setAttribute("width","+0.6em"),t.setAttribute("lspace","0.3em"),t};ut({type:"xArrow",names:["\\xleftarrow","\\xrightarrow","\\xLeftarrow","\\xRightarrow","\\xleftrightarrow","\\xLeftrightarrow","\\xhookleftarrow","\\xhookrightarrow","\\xmapsto","\\xrightharpoondown","\\xrightharpoonup","\\xleftharpoondown","\\xleftharpoonup","\\xrightleftharpoons","\\xleftrightharpoons","\\xlongequal","\\xtwoheadrightarrow","\\xtwoheadleftarrow","\\xtofrom","\\xrightleftarrows","\\xrightequilibrium","\\xleftequilibrium","\\\\cdrightarrow","\\\\cdleftarrow","\\\\cdlongequal"],numArgs:1,numOptionalArgs:1,handler(e,t,r){let n=e.parser,o=e.funcName;return{type:"xArrow",mode:n.mode,label:o,body:t[0],below:r[0]}},htmlBuilder(e,t){const r=t.style;let n=t.havingStyle(r.sup());const o=Ke(At(e.body,n,t),t),s="\\x"===e.label.slice(0,2)?"x":"cd";let i;o.classes.push(s+"-arrow-pad"),e.below&&(n=t.havingStyle(r.sub()),i=Ke(At(e.below,n,t),t),i.classes.push(s+"-arrow-pad"));const a=rr(e,t),l=-t.fontMetrics().axisHeight+.5*a.height;let c,h=-t.fontMetrics().axisHeight-.5*a.height-.111;if((o.depth>.25||"\\xleftequilibrium"===e.label)&&(h-=o.depth),i){const e=-t.fontMetrics().axisHeight+i.height+.5*a.height+.111;c=Je({positionType:"individualShift",children:[{type:"elem",elem:o,shift:h},{type:"elem",elem:a,shift:l,wrapperClasses:["svg-align"]},{type:"elem",elem:i,shift:e}]})}else c=Je({positionType:"individualShift",children:[{type:"elem",elem:o,shift:h},{type:"elem",elem:a,shift:l,wrapperClasses:["svg-align"]}]});return We(["mrel","x-arrow"],[c],t)},mathmlBuilder(e,t){const r=Qt(e.label);let n;if(r.setAttribute("minsize","x"===e.label.charAt(0)?"1.75em":"3.0em"),e.body){const o=ur(Gt(e.body,t));if(e.below){const s=ur(Gt(e.below,t));n=new qt("munderover",[r,s,o])}else n=new qt("mover",[r,o])}else if(e.below){const o=ur(Gt(e.below,t));n=new qt("munder",[r,o])}else n=ur(),n=new qt("mover",[r,n]);return n}}),ut({type:"mclass",names:["\\mathord","\\mathbin","\\mathrel","\\mathopen","\\mathclose","\\mathpunct","\\mathinner"],numArgs:1,primitive:!0,handler(e,t){let r=e.parser,n=e.funcName;const o=t[0];return{type:"mclass",mode:r.mode,mclass:"m"+n.slice(5),body:gt(o),isCharacterBox:m(o)}},htmlBuilder:function(e,t){const r=wt(e.body,t,!0);return We([e.mclass],r,t)},mathmlBuilder:function(e,t){let r;const n=Pt(e.body,t);return"minner"===e.mclass?r=new qt("mpadded",n):"mord"===e.mclass?e.isCharacterBox?(r=n[0],r.type="mi"):r=new qt("mi",n):(e.isCharacterBox?(r=n[0],r.type="mo"):r=new qt("mo",n),"mbin"===e.mclass?(r.attributes.lspace="0.22em",r.attributes.rspace="0.22em"):"mpunct"===e.mclass?(r.attributes.lspace="0em",r.attributes.rspace="0.17em"):"mopen"!==e.mclass&&"mclose"!==e.mclass||(r.attributes.lspace="0em",r.attributes.rspace="0em")),r}});const pr=e=>{const t="ordgroup"===e.type&&e.body.length?e.body[0]:e;return"atom"!==t.type||"bin"!==t.family&&"rel"!==t.family?"mord":"m"+t.family};ut({type:"mclass",names:["\\@binrel"],numArgs:2,handler(e,t){return{type:"mclass",mode:e.parser.mode,mclass:pr(t[0]),body:gt(t[1]),isCharacterBox:m(t[1])}}}),ut({type:"mclass",names:["\\stackrel","\\overset","\\underset"],numArgs:2,handler(e,t){let r=e.parser,n=e.funcName;const o=t[1],s=t[0];let i;i="\\stackrel"!==n?pr(o):"mrel";const a={type:"op",mode:o.mode,limits:!0,alwaysHandleSupSub:!0,parentIsSupSub:!1,symbol:!1,suppressBaseShift:"\\stackrel"!==n,body:gt(o)},l="\\underset"===n?{type:"supsub",mode:s.mode,base:a,sub:s}:{type:"supsub",mode:s.mode,base:a,sup:s};return{type:"mclass",mode:r.mode,mclass:i,body:[l],isCharacterBox:m(l)}}}),ut({type:"pmb",names:["\\pmb"],numArgs:1,allowedInText:!0,handler(e,t){return{type:"pmb",mode:e.parser.mode,mclass:pr(t[0]),body:gt(t[0])}},htmlBuilder(e,t){const r=wt(e.body,t,!0),n=We([e.mclass],r,t);return n.style.textShadow="0.02em 0.01em 0.04px",n},mathmlBuilder(e,t){const r=Pt(e.body,t),n=new qt("mstyle",r);return n.setAttribute("style","text-shadow: 0.02em 0.01em 0.04px"),n}});const dr={">":"\\\\cdrightarrow","<":"\\\\cdleftarrow","=":"\\\\cdlongequal",A:"\\uparrow",V:"\\downarrow","|":"\\Vert",".":"no arrow"},gr=()=>({type:"styling",body:[],mode:"math",style:"display",resetFont:!0}),fr=e=>"textord"===e.type&&"@"===e.text,br=(e,t)=>("mathord"===e.type||"atom"===e.type)&&e.text===t;function yr(e,t,r){const n=dr[e];switch(n){case"\\\\cdrightarrow":case"\\\\cdleftarrow":return r.callFunction(n,[t[0]],[t[1]]);case"\\uparrow":case"\\downarrow":{const e={type:"atom",text:n,mode:"math",family:"rel"},o={type:"ordgroup",mode:"math",body:[r.callFunction("\\\\cdleft",[t[0]],[]),r.callFunction("\\Big",[e],[]),r.callFunction("\\\\cdright",[t[1]],[])]};return r.callFunction("\\\\cdparent",[o],[])}case"\\\\cdlongequal":return r.callFunction("\\\\cdlongequal",[],[]);case"\\Vert":{const e={type:"textord",text:"\\Vert",mode:"math"};return r.callFunction("\\Big",[e],[])}default:return{type:"textord",text:" ",mode:"math"}}}ut({type:"cdlabel",names:["\\\\cdleft","\\\\cdright"],numArgs:1,handler(e,t){let r=e.parser,n=e.funcName;return{type:"cdlabel",mode:r.mode,side:n.slice(4),label:t[0]}},htmlBuilder(e,t){const r=t.havingStyle(t.style.sup()),n=Ke(At(e.label,r,t),t);return n.classes.push("cd-label-"+e.side),n.style.bottom=N(.8-n.depth),n.height=0,n.depth=0,n},mathmlBuilder(e,t){let r=new qt("mrow",[Gt(e.label,t)]);return r=new qt("mpadded",[r]),r.setAttribute("width","0"),"left"===e.side&&r.setAttribute("lspace","-1width"),r.setAttribute("voffset","0.7em"),r=new qt("mstyle",[r]),r.setAttribute("displaystyle","false"),r.setAttribute("scriptlevel","1"),r}}),ut({type:"cdlabelparent",names:["\\\\cdparent"],numArgs:1,handler(e,t){return{type:"cdlabelparent",mode:e.parser.mode,fragment:t[0]}},htmlBuilder(e,t){const r=Ke(At(e.fragment,t),t);return r.classes.push("cd-vert-arrow"),r},mathmlBuilder(e,t){return new qt("mrow",[Gt(e.fragment,t)])}}),ut({type:"textord",names:["\\@char"],numArgs:1,allowedInText:!0,handler(e,t){let r=e.parser;const o=lr(sr(t[0],"ordgroup"),"\\@char has non-numeric argument");let s,i=parseInt(o);if(isNaN(i))throw new n("\\@char has non-numeric argument "+o);if(i<0||i>1114111)throw new n("\\@char with invalid code point "+o);return i<=65535?s=String.fromCharCode(i):(i-=65536,s=String.fromCharCode(55296+(i>>10),56320+(1023&i))),{type:"textord",mode:r.mode,text:s}}});ut({type:"color",names:["\\textcolor"],numArgs:2,allowedInText:!0,argTypes:["color","original"],handler(e,t){let r=e.parser;const n=sr(t[0],"color-token").color,o=t[1];return{type:"color",mode:r.mode,color:n,body:gt(o)}},htmlBuilder:(e,t)=>{const r=wt(e.body,t.withColor(e.color),!1);return Ze(r)},mathmlBuilder:(e,t)=>{const r=Pt(e.body,t.withColor(e.color)),n=new qt("mstyle",r);return n.setAttribute("mathcolor",e.color),n}}),ut({type:"color",names:["\\color"],numArgs:1,allowedInText:!0,argTypes:["color"],handler(e,t){let r=e.parser,n=e.breakOnTokenText;const o=sr(t[0],"color-token").color;r.gullet.macros.set("\\current@color",o);const s=r.parseExpression(!0,n);return{type:"color",mode:r.mode,color:o,body:s}}}),ut({type:"cr",names:["\\\\"],numArgs:0,numOptionalArgs:0,allowedInText:!0,handler(e){let t=e.parser;const r="["===t.gullet.future().text?t.parseSizeGroup(!0):null,n=!t.settings.displayMode||!Fe({strict:t.settings.strict,errorCode:"newLineInDisplayMode",errorMsg:"In LaTeX, \\\\ or \\newline does nothing in display mode",report:!1});return{type:"cr",mode:t.mode,newLine:n,size:r&&sr(r,"size").value}},htmlBuilder(e,t){const r=We(["mspace"],[],t);return e.newLine&&(r.classes.push("katex-newline"),e.size&&(r.style.marginTop=N(H(e.size,t)))),r},mathmlBuilder(e,t){const r=new qt("mspace");return e.newLine&&(r.setAttribute("linebreak","newline"),e.size&&r.setAttribute("height",N(H(e.size,t)))),r}});const xr={"\\global":"\\global","\\long":"\\\\globallong","\\\\globallong":"\\\\globallong","\\def":"\\gdef","\\gdef":"\\gdef","\\edef":"\\xdef","\\xdef":"\\xdef","\\let":"\\\\globallet","\\futurelet":"\\\\globalfuture"},wr=e=>{const t=e.text;if(/^(?:[\\{}$&#^_]|EOF)$/.test(t))throw new n("Expected a control sequence",e);return t},vr=(e,t,r,n)=>{let o=e.gullet.macros.get(r.text);null==o&&(r.noexpand=!0,o={tokens:[r],numArgs:0,unexpandable:!e.gullet.isExpandable(r.text)}),e.gullet.macros.set(t,o,n)};ut({type:"internal",names:["\\global","\\long","\\\\globallong"],numArgs:0,allowedInText:!0,handler(e){let t=e.parser,r=e.funcName;t.consumeSpaces();const o=t.fetch();if(xr[o.text])return"\\global"!==r&&"\\\\globallong"!==r||(o.text=xr[o.text]),sr(t.parseFunction(),"internal");throw new n("Invalid token after macro prefix",o)}}),ut({type:"internal",names:["\\def","\\gdef","\\edef","\\xdef"],numArgs:0,allowedInText:!0,primitive:!0,handler(e){let t=e.parser,r=e.funcName,o=t.gullet.popToken();const s=o.text;if(/^(?:[\\{}$&#^_]|EOF)$/.test(s))throw new n("Expected a control sequence",o);let i,a=0;const l=[[]];for(;"{"!==t.gullet.future().text;)if(o=t.gullet.popToken(),"#"===o.text){if("{"===t.gullet.future().text){i=t.gullet.future(),l[a].push("{");break}if(o=t.gullet.popToken(),!/^[1-9]$/.test(o.text))throw new n('Invalid argument number "'+o.text+'"');if(parseInt(o.text)!==a+1)throw new n('Argument number "'+o.text+'" out of order');a++,l.push([])}else{if("EOF"===o.text)throw new n("Expected a macro definition");l[a].push(o.text)}let c=t.gullet.consumeArg().tokens;return i&&c.unshift(i),"\\edef"!==r&&"\\xdef"!==r||(c=t.gullet.expandTokens(c),c.reverse()),t.gullet.macros.set(s,{tokens:c,numArgs:a,delimiters:l},r===xr[r]),{type:"internal",mode:t.mode}}}),ut({type:"internal",names:["\\let","\\\\globallet"],numArgs:0,allowedInText:!0,primitive:!0,handler(e){let t=e.parser,r=e.funcName;const n=wr(t.gullet.popToken());t.gullet.consumeSpaces();const o=(e=>{let t=e.gullet.popToken();return"="===t.text&&(t=e.gullet.popToken()," "===t.text&&(t=e.gullet.popToken())),t})(t);return vr(t,n,o,"\\\\globallet"===r),{type:"internal",mode:t.mode}}}),ut({type:"internal",names:["\\futurelet","\\\\globalfuture"],numArgs:0,allowedInText:!0,primitive:!0,handler(e){let t=e.parser,r=e.funcName;const n=wr(t.gullet.popToken()),o=t.gullet.popToken(),s=t.gullet.popToken();return vr(t,n,s,"\\\\globalfuture"===r),t.gullet.pushToken(s),t.gullet.pushToken(o),{type:"internal",mode:t.mode}}});const kr=function(e,t,r){const n=ee(ne.math[e]&&ne.math[e].replace||e,t,r);if(!n)throw new Error("Unsupported symbol "+e+" and font size "+t+".");return n},zr=function(e,t,r,n){const o=r.havingBaseStyle(t),s=We(n.concat(o.sizingClasses(r)),[e],r),i=o.sizeMultiplier/r.sizeMultiplier;return s.height*=i,s.depth*=i,s.maxFontSize=o.sizeMultiplier,s},Sr=function(e,t,r){const n=t.havingBaseStyle(r),o=(1-t.sizeMultiplier/n.sizeMultiplier)*t.fontMetrics().axisHeight;e.classes.push("delimcenter"),e.style.top=N(o),e.height-=o,e.depth+=o},Mr=function(e,t,r,n,o,s){const i=function(e,t,r,n){return Ve(e,"Size"+t+"-Regular",r,n)}(e,t,o,n),a=zr(We(["delimsizing","size"+t],[i],n),S.TEXT,n,s);return r&&Sr(a,n,S.TEXT),a},Ar=function(e,t,r,n){let o;o="Size1-Regular"===t?"delim-size1":"delim-size4";return{type:"elem",elem:We(["delimsizinginner",o],[We([],[Ve(e,t,r,n)])])}},Tr=function(e,t,r){const n=K["Size4-Regular"][e.charCodeAt(0)]?K["Size4-Regular"][e.charCodeAt(0)][4]:K["Size1-Regular"][e.charCodeAt(0)][4],o=new $("inner",function(e,t){switch(e){case"\u239c":return C("M291 0 H417 V"+t+" H291z");case"\u2223":return C("M145 0 H188 V"+t+" H145z");case"\u2225":return C("M145 0 H188 V"+t+" H145z")+C("M367 0 H410 V"+t+" H367z");case"\u239f":return C("M457 0 H583 V"+t+" H457z");case"\u23a2":return C("M319 0 H403 V"+t+" H319z");case"\u23a5":return C("M263 0 H347 V"+t+" H263z");case"\u23aa":return C("M384 0 H504 V"+t+" H384z");case"\u23d0":return C("M312 0 H355 V"+t+" H312z");case"\u2016":return C("M257 0 H300 V"+t+" H257z")+C("M478 0 H521 V"+t+" H478z");default:return""}}(e,Math.round(1e3*t))),s=new _([o],{width:N(n),height:N(t),style:"width:"+N(n),viewBox:"0 0 "+1e3*n+" "+Math.round(1e3*t),preserveAspectRatio:"xMinYMin"}),i=_e([],[s],r);return i.height=t,i.style.height=N(t),i.style.width=N(n),{type:"elem",elem:i}},Cr={type:"kern",size:-.008},Br=new Set(["|","\\lvert","\\rvert","\\vert"]),qr=new Set(["\\|","\\lVert","\\rVert","\\Vert"]),Ir=function(e,t,r,n,o,s){let i,a,l,c,h="",m=0;i=l=c=e,a=null;let u="Size1-Regular";"\\uparrow"===e?l=c="\u23d0":"\\Uparrow"===e?l=c="\u2016":"\\downarrow"===e?i=l="\u23d0":"\\Downarrow"===e?i=l="\u2016":"\\updownarrow"===e?(i="\\uparrow",l="\u23d0",c="\\downarrow"):"\\Updownarrow"===e?(i="\\Uparrow",l="\u2016",c="\\Downarrow"):Br.has(e)?(l="\u2223",h="vert",m=333):qr.has(e)?(l="\u2225",h="doublevert",m=556):"["===e||"\\lbrack"===e?(i="\u23a1",l="\u23a2",c="\u23a3",u="Size4-Regular",h="lbrack",m=667):"]"===e||"\\rbrack"===e?(i="\u23a4",l="\u23a5",c="\u23a6",u="Size4-Regular",h="rbrack",m=667):"\\lfloor"===e||"\u230a"===e?(l=i="\u23a2",c="\u23a3",u="Size4-Regular",h="lfloor",m=667):"\\lceil"===e||"\u2308"===e?(i="\u23a1",l=c="\u23a2",u="Size4-Regular",h="lceil",m=667):"\\rfloor"===e||"\u230b"===e?(l=i="\u23a5",c="\u23a6",u="Size4-Regular",h="rfloor",m=667):"\\rceil"===e||"\u2309"===e?(i="\u23a4",l=c="\u23a5",u="Size4-Regular",h="rceil",m=667):"("===e||"\\lparen"===e?(i="\u239b",l="\u239c",c="\u239d",u="Size4-Regular",h="lparen",m=875):")"===e||"\\rparen"===e?(i="\u239e",l="\u239f",c="\u23a0",u="Size4-Regular",h="rparen",m=875):"\\{"===e||"\\lbrace"===e?(i="\u23a7",a="\u23a8",c="\u23a9",l="\u23aa",u="Size4-Regular"):"\\}"===e||"\\rbrace"===e?(i="\u23ab",a="\u23ac",c="\u23ad",l="\u23aa",u="Size4-Regular"):"\\lgroup"===e||"\u27ee"===e?(i="\u23a7",c="\u23a9",l="\u23aa",u="Size4-Regular"):"\\rgroup"===e||"\u27ef"===e?(i="\u23ab",c="\u23ad",l="\u23aa",u="Size4-Regular"):"\\lmoustache"===e||"\u23b0"===e?(i="\u23a7",c="\u23ad",l="\u23aa",u="Size4-Regular"):"\\rmoustache"!==e&&"\u23b1"!==e||(i="\u23ab",c="\u23a9",l="\u23aa",u="Size4-Regular");const p=kr(i,u,o),d=p.height+p.depth,g=kr(l,u,o),f=g.height+g.depth,b=kr(c,u,o),y=b.height+b.depth;let x=0,w=1;if(null!==a){const e=kr(a,u,o);x=e.height+e.depth,w=2}const v=d+y+x,k=v+Math.max(0,Math.ceil((t-v)/(w*f)))*w*f;let z=n.fontMetrics().axisHeight;r&&(z*=n.sizeMultiplier);const M=k/2-z,A=[];if(h.length>0){const e=k-d-y,t=Math.round(1e3*k),r=function(e,t){switch(e){case"lbrack":return"M403 1759 V84 H666 V0 H319 V1759 v"+t+" v1759 v84 h347 v-84\nH403z M403 1759 V0 H319 V1759 v"+t+" v1759 v84 h84z";case"rbrack":return"M347 1759 V0 H0 V84 H263 V1759 v"+t+" v1759 H0 v84 H347z\nM347 1759 V0 H263 V1759 v"+t+" v1759 h84z";case"vert":return"M145 15 v585 v"+t+" v585 c2.667,10,9.667,15,21,15\nc10,0,16.667,-5,20,-15 v-585 v"+-t+" v-585 c-2.667,-10,-9.667,-15,-21,-15\nc-10,0,-16.667,5,-20,15z M188 15 H145 v585 v"+t+" v585 h43z";case"doublevert":return"M145 15 v585 v"+t+" v585 c2.667,10,9.667,15,21,15\nc10,0,16.667,-5,20,-15 v-585 v"+-t+" v-585 c-2.667,-10,-9.667,-15,-21,-15\nc-10,0,-16.667,5,-20,15z M188 15 H145 v585 v"+t+" v585 h43z\nM367 15 v585 v"+t+" v585 c2.667,10,9.667,15,21,15\nc10,0,16.667,-5,20,-15 v-585 v"+-t+" v-585 c-2.667,-10,-9.667,-15,-21,-15\nc-10,0,-16.667,5,-20,15z M410 15 H367 v585 v"+t+" v585 h43z";case"lfloor":return"M319 602 V0 H403 V602 v"+t+" v1715 h263 v84 H319z\nMM319 602 V0 H403 V602 v"+t+" v1715 H319z";case"rfloor":return"M319 602 V0 H403 V602 v"+t+" v1799 H0 v-84 H319z\nMM319 602 V0 H403 V602 v"+t+" v1715 H319z";case"lceil":return"M403 1759 V84 H666 V0 H319 V1759 v"+t+" v602 h84z\nM403 1759 V0 H319 V1759 v"+t+" v602 h84z";case"rceil":return"M347 1759 V0 H0 V84 H263 V1759 v"+t+" v602 h84z\nM347 1759 V0 h-84 V1759 v"+t+" v602 h84z";case"lparen":return"M863,9c0,-2,-2,-5,-6,-9c0,0,-17,0,-17,0c-12.7,0,-19.3,0.3,-20,1\nc-5.3,5.3,-10.3,11,-15,17c-242.7,294.7,-395.3,682,-458,1162c-21.3,163.3,-33.3,349,\n-36,557 l0,"+(t+84)+"c0.2,6,0,26,0,60c2,159.3,10,310.7,24,454c53.3,528,210,\n949.7,470,1265c4.7,6,9.7,11.7,15,17c0.7,0.7,7,1,19,1c0,0,18,0,18,0c4,-4,6,-7,6,-9\nc0,-2.7,-3.3,-8.7,-10,-18c-135.3,-192.7,-235.5,-414.3,-300.5,-665c-65,-250.7,-102.5,\n-544.7,-112.5,-882c-2,-104,-3,-167,-3,-189\nl0,-"+(t+92)+"c0,-162.7,5.7,-314,17,-454c20.7,-272,63.7,-513,129,-723c65.3,\n-210,155.3,-396.3,270,-559c6.7,-9.3,10,-15.3,10,-18z";case"rparen":return"M76,0c-16.7,0,-25,3,-25,9c0,2,2,6.3,6,13c21.3,28.7,42.3,60.3,\n63,95c96.7,156.7,172.8,332.5,228.5,527.5c55.7,195,92.8,416.5,111.5,664.5\nc11.3,139.3,17,290.7,17,454c0,28,1.7,43,3.3,45l0,"+(t+9)+"\nc-3,4,-3.3,16.7,-3.3,38c0,162,-5.7,313.7,-17,455c-18.7,248,-55.8,469.3,-111.5,664\nc-55.7,194.7,-131.8,370.3,-228.5,527c-20.7,34.7,-41.7,66.3,-63,95c-2,3.3,-4,7,-6,11\nc0,7.3,5.7,11,17,11c0,0,11,0,11,0c9.3,0,14.3,-0.3,15,-1c5.3,-5.3,10.3,-11,15,-17\nc242.7,-294.7,395.3,-681.7,458,-1161c21.3,-164.7,33.3,-350.7,36,-558\nl0,-"+(t+144)+"c-2,-159.3,-10,-310.7,-24,-454c-53.3,-528,-210,-949.7,\n-470,-1265c-4.7,-6,-9.7,-11.7,-15,-17c-0.7,-0.7,-6.7,-1,-18,-1z";default:throw new Error("Unknown stretchy delimiter.")}}(h,Math.round(1e3*e)),o=new $(h,r),s=N(m/1e3),i=N(t/1e3),a=new _([o],{width:s,height:i,viewBox:"0 0 "+m+" "+t}),l=_e([],[a],n);l.height=t/1e3,l.style.width=s,l.style.height=i,A.push({type:"elem",elem:l})}else{if(A.push(Ar(c,u,o,n)),A.push(Cr),null===a){const e=k-d-y+.016;A.push(Tr(l,e,n))}else{const e=(k-d-y-x)/2+.016;A.push(Tr(l,e,n)),A.push(Cr),A.push(Ar(a,u,o,n)),A.push(Cr),A.push(Tr(l,e,n))}A.push(Cr),A.push(Ar(i,u,o,n))}const T=n.havingBaseStyle(S.TEXT),C=Je({positionType:"bottom",positionData:M,children:A});return zr(We(["delimsizing","mult"],[C],T),S.TEXT,n,s)},Rr=.08,Or=function(e,t,r,n,o){const s=function(e,t,r){t*=1e3;let n="";switch(e){case"sqrtMain":n=function(e,t){return"M95,"+(622+e+t)+"\nc-2.7,0,-7.17,-2.7,-13.5,-8c-5.8,-5.3,-9.5,-10,-9.5,-14\nc0,-2,0.3,-3.3,1,-4c1.3,-2.7,23.83,-20.7,67.5,-54\nc44.2,-33.3,65.8,-50.3,66.5,-51c1.3,-1.3,3,-2,5,-2c4.7,0,8.7,3.3,12,10\ns173,378,173,378c0.7,0,35.3,-71,104,-213c68.7,-142,137.5,-285,206.5,-429\nc69,-144,104.5,-217.7,106.5,-221\nl"+e/2.075+" -"+e+"\nc5.3,-9.3,12,-14,20,-14\nH400000v"+(40+e)+"H845.2724\ns-225.272,467,-225.272,467s-235,486,-235,486c-2.7,4.7,-9,7,-19,7\nc-6,0,-10,-1,-12,-3s-194,-422,-194,-422s-65,47,-65,47z\nM"+(834+e)+" "+t+"h400000v"+(40+e)+"h-400000z"}(t,B);break;case"sqrtSize1":n=function(e,t){return"M263,"+(601+e+t)+"c0.7,0,18,39.7,52,119\nc34,79.3,68.167,158.7,102.5,238c34.3,79.3,51.8,119.3,52.5,120\nc340,-704.7,510.7,-1060.3,512,-1067\nl"+e/2.084+" -"+e+"\nc4.7,-7.3,11,-11,19,-11\nH40000v"+(40+e)+"H1012.3\ns-271.3,567,-271.3,567c-38.7,80.7,-84,175,-136,283c-52,108,-89.167,185.3,-111.5,232\nc-22.3,46.7,-33.8,70.3,-34.5,71c-4.7,4.7,-12.3,7,-23,7s-12,-1,-12,-1\ns-109,-253,-109,-253c-72.7,-168,-109.3,-252,-110,-252c-10.7,8,-22,16.7,-34,26\nc-22,17.3,-33.3,26,-34,26s-26,-26,-26,-26s76,-59,76,-59s76,-60,76,-60z\nM"+(1001+e)+" "+t+"h400000v"+(40+e)+"h-400000z"}(t,B);break;case"sqrtSize2":n=function(e,t){return"M983 "+(10+e+t)+"\nl"+e/3.13+" -"+e+"\nc4,-6.7,10,-10,18,-10 H400000v"+(40+e)+"\nH1013.1s-83.4,268,-264.1,840c-180.7,572,-277,876.3,-289,913c-4.7,4.7,-12.7,7,-24,7\ns-12,0,-12,0c-1.3,-3.3,-3.7,-11.7,-7,-25c-35.3,-125.3,-106.7,-373.3,-214,-744\nc-10,12,-21,25,-33,39s-32,39,-32,39c-6,-5.3,-15,-14,-27,-26s25,-30,25,-30\nc26.7,-32.7,52,-63,76,-91s52,-60,52,-60s208,722,208,722\nc56,-175.3,126.3,-397.3,211,-666c84.7,-268.7,153.8,-488.2,207.5,-658.5\nc53.7,-170.3,84.5,-266.8,92.5,-289.5z\nM"+(1001+e)+" "+t+"h400000v"+(40+e)+"h-400000z"}(t,B);break;case"sqrtSize3":n=function(e,t){return"M424,"+(2398+e+t)+"\nc-1.3,-0.7,-38.5,-172,-111.5,-514c-73,-342,-109.8,-513.3,-110.5,-514\nc0,-2,-10.7,14.3,-32,49c-4.7,7.3,-9.8,15.7,-15.5,25c-5.7,9.3,-9.8,16,-12.5,20\ns-5,7,-5,7c-4,-3.3,-8.3,-7.7,-13,-13s-13,-13,-13,-13s76,-122,76,-122s77,-121,77,-121\ns209,968,209,968c0,-2,84.7,-361.7,254,-1079c169.3,-717.3,254.7,-1077.7,256,-1081\nl"+e/4.223+" -"+e+"c4,-6.7,10,-10,18,-10 H400000\nv"+(40+e)+"H1014.6\ns-87.3,378.7,-272.6,1166c-185.3,787.3,-279.3,1182.3,-282,1185\nc-2,6,-10,9,-24,9\nc-8,0,-12,-0.7,-12,-2z M"+(1001+e)+" "+t+"\nh400000v"+(40+e)+"h-400000z"}(t,B);break;case"sqrtSize4":n=function(e,t){return"M473,"+(2713+e+t)+"\nc339.3,-1799.3,509.3,-2700,510,-2702 l"+e/5.298+" -"+e+"\nc3.3,-7.3,9.3,-11,18,-11 H400000v"+(40+e)+"H1017.7\ns-90.5,478,-276.2,1466c-185.7,988,-279.5,1483,-281.5,1485c-2,6,-10,9,-24,9\nc-8,0,-12,-0.7,-12,-2c0,-1.3,-5.3,-32,-16,-92c-50.7,-293.3,-119.7,-693.3,-207,-1200\nc0,-1.3,-5.3,8.7,-16,30c-10.7,21.3,-21.3,42.7,-32,64s-16,33,-16,33s-26,-26,-26,-26\ns76,-153,76,-153s77,-151,77,-151c0.7,0.7,35.7,202,105,604c67.3,400.7,102,602.7,104,\n606zM"+(1001+e)+" "+t+"h400000v"+(40+e)+"H1017.7z"}(t,B);break;case"sqrtTall":n=function(e,t,r){return"M702 "+(e+t)+"H400000"+(40+e)+"\nH742v"+(r-54-t-e)+"l-4 4-4 4c-.667.7 -2 1.5-4 2.5s-4.167 1.833-6.5 2.5-5.5 1-9.5 1\nh-12l-28-84c-16.667-52-96.667 -294.333-240-727l-212 -643 -85 170\nc-4-3.333-8.333-7.667-13 -13l-13-13l77-155 77-156c66 199.333 139 419.667\n219 661 l218 661zM702 "+t+"H400000v"+(40+e)+"H742z"}(t,B,r)}return n}(e,n,r),i=new $(e,s),a=new _([i],{width:"400em",height:N(t),viewBox:"0 0 400000 "+r,preserveAspectRatio:"xMinYMin slice"});return _e(["hide-tail"],[a],o)},Er=new Set(["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230a","\u230b","\\lceil","\\rceil","\u2308","\u2309","\\surd"]),Hr=new Set(["\\uparrow","\\downarrow","\\updownarrow","\\Uparrow","\\Downarrow","\\Updownarrow","|","\\|","\\vert","\\Vert","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27ee","\u27ef","\\lmoustache","\\rmoustache","\u23b0","\u23b1"]),Nr=new Set(["<",">","\\langle","\\rangle","/","\\backslash","\\lt","\\gt"]),Dr=[0,1.2,1.8,2.4,3],Lr=function(e,t,r,o,s){if("<"===e||"\\lt"===e||"\u27e8"===e?e="\\langle":">"!==e&&"\\gt"!==e&&"\u27e9"!==e||(e="\\rangle"),Er.has(e)||Nr.has(e))return Mr(e,t,!1,r,o,s);if(Hr.has(e))return Ir(e,Dr[t],!1,r,o,s);throw new n("Illegal delimiter: '"+e+"'")},Fr=[{type:"small",style:S.SCRIPTSCRIPT},{type:"small",style:S.SCRIPT},{type:"small",style:S.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4}],Pr=[{type:"small",style:S.SCRIPTSCRIPT},{type:"small",style:S.SCRIPT},{type:"small",style:S.TEXT},{type:"stack"}],Vr=[{type:"small",style:S.SCRIPTSCRIPT},{type:"small",style:S.SCRIPT},{type:"small",style:S.TEXT},{type:"large",size:1},{type:"large",size:2},{type:"large",size:3},{type:"large",size:4},{type:"stack"}],Gr=function(e){if("small"===e.type)return"Main-Regular";if("large"===e.type)return"Size"+e.size+"-Regular";if("stack"===e.type)return"Size4-Regular";{const t=e.type;throw new Error("Add support for delim type '"+t+"' here.")}},jr=function(e,t,r,n){for(let o=Math.min(2,3-n.style.size);ot)return s}return r[r.length-1]},Ur=function(e,t,r,n,o,s){let i;"<"===e||"\\lt"===e||"\u27e8"===e?e="\\langle":">"!==e&&"\\gt"!==e&&"\u27e9"!==e||(e="\\rangle"),i=Nr.has(e)?Fr:Er.has(e)?Vr:Pr;const a=jr(e,t,i,n);return"small"===a.type?function(e,t,r,n,o,s){const i=Ve(e,"Main-Regular",o,n),a=zr(i,t,n,s);return r&&Sr(a,n,t),a}(e,a.style,r,n,o,s):"large"===a.type?Mr(e,a.size,r,n,o,s):Ir(e,t,r,n,o,s)},Yr=function(e,t,r,n,o,s){const i=n.fontMetrics().axisHeight*n.sizeMultiplier,a=5/n.fontMetrics().ptPerEm,l=Math.max(t-i,r+i),c=Math.max(l/500*901,2*l-a);return Ur(e,c,!0,n,o,s)},Xr={"\\bigl":{mclass:"mopen",size:1},"\\Bigl":{mclass:"mopen",size:2},"\\biggl":{mclass:"mopen",size:3},"\\Biggl":{mclass:"mopen",size:4},"\\bigr":{mclass:"mclose",size:1},"\\Bigr":{mclass:"mclose",size:2},"\\biggr":{mclass:"mclose",size:3},"\\Biggr":{mclass:"mclose",size:4},"\\bigm":{mclass:"mrel",size:1},"\\Bigm":{mclass:"mrel",size:2},"\\biggm":{mclass:"mrel",size:3},"\\Biggm":{mclass:"mrel",size:4},"\\big":{mclass:"mord",size:1},"\\Big":{mclass:"mord",size:2},"\\bigg":{mclass:"mord",size:3},"\\Bigg":{mclass:"mord",size:4}},Wr=new Set(["(","\\lparen",")","\\rparen","[","\\lbrack","]","\\rbrack","\\{","\\lbrace","\\}","\\rbrace","\\lfloor","\\rfloor","\u230a","\u230b","\\lceil","\\rceil","\u2308","\u2309","<",">","\\langle","\u27e8","\\rangle","\u27e9","\\lt","\\gt","\\lvert","\\rvert","\\lVert","\\rVert","\\lgroup","\\rgroup","\u27ee","\u27ef","\\lmoustache","\\rmoustache","\u23b0","\u23b1","/","\\backslash","|","\\vert","\\|","\\Vert","\\uparrow","\\Uparrow","\\downarrow","\\Downarrow","\\updownarrow","\\Updownarrow","."]);function _r(e){return"isMiddle"in e}function $r(e,t){const r=ar(e);if(r&&Wr.has(r.text))return r;throw new n(r?"Invalid delimiter '"+r.text+"' after '"+t.funcName+"'":"Invalid delimiter type '"+e.type+"'",e)}function Zr(e){if(!e.body)throw new Error("Bug: The leftright ParseNode wasn't fully parsed.")}ut({type:"delimsizing",names:["\\bigl","\\Bigl","\\biggl","\\Biggl","\\bigr","\\Bigr","\\biggr","\\Biggr","\\bigm","\\Bigm","\\biggm","\\Biggm","\\big","\\Big","\\bigg","\\Bigg"],numArgs:1,argTypes:["primitive"],handler:(e,t)=>{const r=$r(dt(t[0]),e);return{type:"delimsizing",mode:e.parser.mode,size:Xr[e.funcName].size,mclass:Xr[e.funcName].mclass,delim:r.text}},htmlBuilder:(e,t)=>"."===e.delim?We([e.mclass]):Lr(e.delim,e.size,t,e.mode,[e.mclass]),mathmlBuilder:e=>{const t=[];"."!==e.delim&&t.push(Ht(e.delim,e.mode));const r=new qt("mo",t);"mopen"===e.mclass||"mclose"===e.mclass?r.setAttribute("fence","true"):r.setAttribute("fence","false"),r.setAttribute("stretchy","true");const n=N(Dr[e.size]);return r.setAttribute("minsize",n),r.setAttribute("maxsize",n),r}}),ut({type:"leftright-right",names:["\\right"],numArgs:1,primitive:!0,handler:(e,t)=>{const r=e.parser.gullet.macros.get("\\current@color");if(r&&"string"!=typeof r)throw new n("\\current@color set to non-string in \\right");return{type:"leftright-right",mode:e.parser.mode,delim:$r(t[0],e).text,color:r}}}),ut({type:"leftright",names:["\\left"],numArgs:1,primitive:!0,handler:(e,t)=>{const r=$r(t[0],e),n=e.parser;++n.leftrightDepth;const o=n.parseExpression(!1);--n.leftrightDepth,n.expect("\\right",!1);const s=sr(n.parseFunction(),"leftright-right");return{type:"leftright",mode:n.mode,body:o,left:r.text,right:s.delim,rightColor:s.color}},htmlBuilder:(e,t)=>{Zr(e);const r=wt(e.body,t,!0,["mopen","mclose"]);let n,o,s=0,i=0,a=!1;for(let e=0;e{Zr(e);const r=Pt(e.body,t);if("."!==e.left){const t=new qt("mo",[Ht(e.left,e.mode)]);t.setAttribute("fence","true"),r.unshift(t)}if("."!==e.right){const t=new qt("mo",[Ht(e.right,e.mode)]);t.setAttribute("fence","true"),e.rightColor&&t.setAttribute("mathcolor",e.rightColor),r.push(t)}return Nt(r)}}),ut({type:"middle",names:["\\middle"],numArgs:1,primitive:!0,handler:(e,t)=>{const r=$r(t[0],e);if(!e.parser.leftrightDepth)throw new n("\\middle without preceding \\left",r);return{type:"middle",mode:e.parser.mode,delim:r.text}},htmlBuilder:(e,t)=>{let r;return"."===e.delim?r=Mt(t,[]):(r=Lr(e.delim,1,t,e.mode,[]),r.isMiddle={delim:e.delim,options:t}),r},mathmlBuilder:(e,t)=>{const r="\\vert"===e.delim||"|"===e.delim?Ht("|","text"):Ht(e.delim,e.mode),n=new qt("mo",[r]);return n.setAttribute("fence","true"),n.setAttribute("lspace","0.05em"),n.setAttribute("rspace","0.05em"),n}});ut({type:"enclose",names:["\\colorbox"],numArgs:2,allowedInText:!0,argTypes:["color","hbox"],handler(e,t,r){let n=e.parser,o=e.funcName;const s=sr(t[0],"color-token").color,i=t[1];return{type:"enclose",mode:n.mode,label:o,backgroundColor:s,body:i}},htmlBuilder:(e,t)=>{const r=Ke(At(e.body,t),t),n=e.label.slice(1);let o,s,i=t.sizeMultiplier;const a=m(e.body);if("sout"===n)o=We(["katex-stretchy","katex-sout"]),o.height=t.fontMetrics().defaultRuleThickness/i,s=-.5*t.fontMetrics().xHeight;else if("phase"===n){const e=H({number:.6,unit:"pt"},t),n=H({number:.35,unit:"ex"},t);i/=t.havingBaseSizing().sizeMultiplier;const a=r.height+r.depth+e+n;r.style.paddingLeft=N(a/2+e);const c=Math.floor(1e3*a*i),h="M400000 "+(l=c)+" H0 L"+l/2+" 0 l65 45 L145 "+(l-80)+" H400000z",m=new _([new $("phase",h)],{width:"400em",height:N(c/1e3),viewBox:"0 0 400000 "+c,preserveAspectRatio:"xMinYMin slice"});o=_e(["hide-tail"],[m],t),o.style.height=N(a),s=r.depth+e+n}else{let i,l;/cancel/.test(n)?a||r.classes.push("cancel-pad"):"angl"===n?r.classes.push("anglpad"):r.classes.push("boxpad");let c=0;/box/.test(n)?(c=Math.max(t.fontMetrics().fboxrule,t.minRuleThickness),i=t.fontMetrics().fboxsep+("colorbox"===n?0:c),l=i):"angl"===n?(c=Math.max(t.fontMetrics().defaultRuleThickness,t.minRuleThickness),i=4*c,l=Math.max(0,.25-r.depth)):(i=a?.2:0,l=i),o=function(e,t,r,n,o){let s;const i=e.height+e.depth+r+n;if(/fbox|color|angl/.test(t)){if(s=We(["katex-stretchy",t],[],o),"fbox"===t){const e=o.color&&o.getColor();e&&(s.style.borderColor=e)}}else{const e=[];/^[bx]cancel$/.test(t)&&e.push(new Z({x1:"0",y1:"0",x2:"100%",y2:"100%","stroke-width":"0.046em"})),/^x?cancel$/.test(t)&&e.push(new Z({x1:"0",y1:"100%",x2:"100%",y2:"0","stroke-width":"0.046em"}));const r=new _(e,{width:"100%",height:N(i)});s=_e([],[r],o)}return s.height=i,s.style.height=N(i),s}(r,n,i,l,t),/fbox|boxed|fcolorbox/.test(n)?(o.style.borderStyle="solid",o.style.borderWidth=N(c)):"angl"===n&&.049!==c&&(o.style.borderTopWidth=N(c),o.style.borderRightWidth=N(c)),s=r.depth+l,e.backgroundColor&&(o.style.backgroundColor=e.backgroundColor,e.borderColor&&(o.style.borderColor=e.borderColor))}var l;let c;if(e.backgroundColor)c=Je({positionType:"individualShift",children:[{type:"elem",elem:o,shift:s},{type:"elem",elem:r,shift:0}]});else{const e=/cancel|phase/.test(n)?["svg-align"]:[];c=Je({positionType:"individualShift",children:[{type:"elem",elem:r,shift:0},{type:"elem",elem:o,shift:s,wrapperClasses:e}]})}return/cancel/.test(n)&&(c.height=r.height,c.depth=r.depth),/cancel/.test(n)&&!a?We(["mord","cancel-lap"],[c],t):We(["mord"],[c],t)},mathmlBuilder:(e,t)=>{let r;const n=new qt(e.label.includes("colorbox")?"mpadded":"menclose",[Gt(e.body,t)]);switch(e.label){case"\\cancel":n.setAttribute("notation","updiagonalstrike");break;case"\\bcancel":n.setAttribute("notation","downdiagonalstrike");break;case"\\phase":n.setAttribute("notation","phasorangle");break;case"\\sout":n.setAttribute("notation","horizontalstrike");break;case"\\fbox":n.setAttribute("notation","box");break;case"\\angl":n.setAttribute("notation","actuarial");break;case"\\fcolorbox":case"\\colorbox":if(r=t.fontMetrics().fboxsep*t.fontMetrics().ptPerEm,n.setAttribute("width","+"+2*r+"pt"),n.setAttribute("height","+"+2*r+"pt"),n.setAttribute("lspace",r+"pt"),n.setAttribute("voffset",r+"pt"),"\\fcolorbox"===e.label){const r=Math.max(t.fontMetrics().fboxrule,t.minRuleThickness);n.setAttribute("style","border: "+N(r)+" solid "+e.borderColor)}break;case"\\xcancel":n.setAttribute("notation","updiagonalstrike downdiagonalstrike")}return e.backgroundColor&&n.setAttribute("mathbackground",e.backgroundColor),n}}),ut({type:"enclose",names:["\\fcolorbox"],numArgs:3,allowedInText:!0,argTypes:["color","color","hbox"],handler(e,t,r){let n=e.parser,o=e.funcName;const s=sr(t[0],"color-token").color,i=sr(t[1],"color-token").color,a=t[2];return{type:"enclose",mode:n.mode,label:o,backgroundColor:i,borderColor:s,body:a}}}),ut({type:"enclose",names:["\\fbox"],numArgs:1,argTypes:["hbox"],allowedInText:!0,handler(e,t){return{type:"enclose",mode:e.parser.mode,label:"\\fbox",body:t[0]}}}),ut({type:"enclose",names:["\\cancel","\\bcancel","\\xcancel","\\phase"],numArgs:1,handler(e,t){let r=e.parser,n=e.funcName;const o=t[0];return{type:"enclose",mode:r.mode,label:n,body:o}}}),ut({type:"enclose",names:["\\sout"],numArgs:1,allowedInText:!0,handler(e,t){let r=e.parser,n=e.funcName;"math"===r.mode&&Fe({strict:r.settings.strict,errorCode:"mathVsSout",errorMsg:"LaTeX's \\sout works only in text mode",report:!0});const o=t[0];return{type:"enclose",mode:r.mode,label:n,body:o}}}),ut({type:"enclose",names:["\\angl"],numArgs:1,argTypes:["hbox"],allowedInText:!1,handler(e,t){return{type:"enclose",mode:e.parser.mode,label:"\\angl",body:t[0]}}});const Kr={};function Jr(e){let t=e.type,r=e.names,n=e.props,o=e.handler,s=e.htmlBuilder,i=e.mathmlBuilder;const a={type:t,numArgs:n.numArgs||0,allowedInText:!1,numOptionalArgs:0,handler:o};for(let e=0;e{if(!e.parser.settings.displayMode)throw new n("{"+e.envName+"} can be used only in display mode.")},sn=new Set(["gather","gather*"]);function an(e){if(!e.includes("ed"))return!e.includes("*")}function ln(e,t,r){let o=t.hskipBeforeAndAfter,s=t.addJot,i=t.cols,a=t.arraystretch,l=t.colSeparationType,c=t.autoTag,h=t.singleRow,m=t.emptySingleRow,u=t.maxNumCols,p=t.leqno;if(e.gullet.beginGroup(),h||e.gullet.macros.set("\\cr","\\\\\\relax"),!a){const t=e.gullet.expandMacroAsText("\\arraystretch");if(null==t)a=1;else if(a=parseFloat(t),!a||a<0)throw new n("Invalid \\arraystretch: "+t)}e.gullet.beginGroup();let d=[];const g=[d],f=[],b=[],y=null!=c?[]:void 0;function x(){c&&e.gullet.macros.set("\\@eqnsw","1",!0)}function w(){y&&(e.gullet.macros.get("\\df@tag")?(y.push(e.subparse([new rn("\\df@tag")])),e.gullet.macros.set("\\df@tag",void 0,!0)):y.push(Boolean(c)&&"1"===e.gullet.macros.get("\\@eqnsw")))}for(x(),b.push(nn(e));;){const t=e.parseExpression(!1,h?"\\end":"\\\\");e.gullet.endGroup(),e.gullet.beginGroup();let o={type:"ordgroup",mode:e.mode,body:t};r&&(o={type:"styling",mode:e.mode,style:r,resetFont:!0,body:[o]}),d.push(o);const s=e.fetch().text;if("&"===s){if(u&&d.length===u){if(h||l)throw new n("Too many tab characters: &",e.nextToken);Fe({strict:e.settings.strict,errorCode:"textEnv",errorMsg:"Too few columns specified in the {array} column argument.",report:!0})}e.consume()}else{if("\\end"===s){w(),1!==d.length||"styling"!==o.type||1!==o.body.length||"ordgroup"!==o.body[0].type||0!==o.body[0].body.length||!(g.length>1)&&m||Array.isArray(null==y?void 0:y[y.length-1])||c&&!0===(null==y?void 0:y[y.length-1])||g.pop(),b.length0&&(y+=.25),c.push({pos:y,isDashed:e[t]})}for(x(i[0]),r=0;r0&&(u+=b,ce))for(r=0;r=a)continue;var B,q;if(o>0||e.hskipBeforeAndAfter)i=null!=(B=null==(q=c)?void 0:q.pregap)?B:u,0!==i&&(z=We(["arraycolsep"],[]),z.style.width=N(i),k.push(z));const p=[];for(r=0;r0){const e=$e("katex-hline",t,h),r=$e("katex-hdashline",t,h),n=[{type:"elem",elem:O,shift:0}];for(;c.length>0;){const t=c.pop(),o=t.pos-w;t.isDashed?n.push({type:"elem",elem:r,shift:o}):n.push({type:"elem",elem:e,shift:o})}O=Je({positionType:"individualShift",children:n})}if(0===A.length)return We(["mord"],[O],t);{const e=Je({positionType:"individualShift",children:A}),r=We(["katex-tag"],[e],t);return Ze([O,r])}},mn={c:"center ",l:"left ",r:"right "},un=function(e,t){const r=[],n=new qt("mtd",[],["mtr-glue"]),o=new qt("mtd",[],["mml-eqn-num"]);for(let s=0;s0){const t=e.cols;let r="",n=!1,o=0,i=t.length;"separator"===t[0].type&&(a+="top ",o=1),"separator"===t[t.length-1].type&&(a+="bottom ",i-=1);for(let e=o;e0?"left ":"",a+=h[h.length-1].length>0?"right ":"";for(let e=1;e0&&c&&(n=1),r[e]={type:"align",align:t,pregap:n,postgap:0}}return s.colSeparationType=c?"align":"alignat",s};Jr({type:"array",names:["array","darray"],props:{numArgs:1},handler(e,t){const r=(ar(t[0])?[t[0]]:sr(t[0],"ordgroup").body).map(function(e){const t=ir(e).text;if("lcr".includes(t))return{type:"align",align:t};if("|"===t)return{type:"separator",separator:"|"};if(":"===t)return{type:"separator",separator:":"};throw new n("Unknown column alignment: "+t,e)}),o={cols:r,hskipBeforeAndAfter:!0,maxNumCols:r.length};return ln(e.parser,o,cn(e.envName))},htmlBuilder:hn,mathmlBuilder:un}),Jr({type:"array",names:["matrix","pmatrix","bmatrix","Bmatrix","vmatrix","Vmatrix","matrix*","pmatrix*","bmatrix*","Bmatrix*","vmatrix*","Vmatrix*"],props:{numArgs:0},handler(e){const t={matrix:null,pmatrix:["(",")"],bmatrix:["[","]"],Bmatrix:["\\{","\\}"],vmatrix:["|","|"],Vmatrix:["\\Vert","\\Vert"]}[e.envName.replace("*","")];let r="c";const o={hskipBeforeAndAfter:!1,cols:[{type:"align",align:r}]};if("*"===e.envName.charAt(e.envName.length-1)){const t=e.parser;if(t.consumeSpaces(),"["===t.fetch().text){if(t.consume(),t.consumeSpaces(),r=t.fetch().text,!"lcr".includes(r))throw new n("Expected l or c or r",t.nextToken);t.consume(),t.consumeSpaces(),t.expect("]"),t.consume(),o.cols=[{type:"align",align:r}]}}const s=ln(e.parser,o,cn(e.envName)),i=Math.max(0,...s.body.map(e=>e.length));return s.cols=new Array(i).fill({type:"align",align:r}),t?{type:"leftright",mode:e.mode,body:[s],left:t[0],right:t[1],rightColor:void 0}:s},htmlBuilder:hn,mathmlBuilder:un}),Jr({type:"array",names:["smallmatrix"],props:{numArgs:0},handler(e){const t=ln(e.parser,{arraystretch:.5},"script");return t.colSeparationType="small",t},htmlBuilder:hn,mathmlBuilder:un}),Jr({type:"array",names:["subarray"],props:{numArgs:1},handler(e,t){const r=(ar(t[0])?[t[0]]:sr(t[0],"ordgroup").body).map(function(e){const t=ir(e).text;if("lc".includes(t))return{type:"align",align:t};throw new n("Unknown column alignment: "+t,e)});if(r.length>1)throw new n("{subarray} can contain only one column");const o={cols:r,hskipBeforeAndAfter:!1,arraystretch:.5},s=ln(e.parser,o,"script");if(s.body.length>0&&s.body[0].length>1)throw new n("{subarray} can contain only one column");return s},htmlBuilder:hn,mathmlBuilder:un}),Jr({type:"array",names:["cases","dcases","rcases","drcases"],props:{numArgs:0},handler(e){const t=ln(e.parser,{arraystretch:1.2,cols:[{type:"align",align:"l",pregap:0,postgap:1},{type:"align",align:"l",pregap:0,postgap:0}]},cn(e.envName));return{type:"leftright",mode:e.mode,body:[t],left:e.envName.includes("r")?".":"\\{",right:e.envName.includes("r")?"\\}":".",rightColor:void 0}},htmlBuilder:hn,mathmlBuilder:un}),Jr({type:"array",names:["align","align*","aligned","split"],props:{numArgs:0},handler:pn,htmlBuilder:hn,mathmlBuilder:un}),Jr({type:"array",names:["gathered","gather","gather*"],props:{numArgs:0},handler(e){sn.has(e.envName)&&on(e);const t={cols:[{type:"align",align:"c"}],addJot:!0,colSeparationType:"gather",autoTag:an(e.envName),emptySingleRow:!0,leqno:e.parser.settings.leqno};return ln(e.parser,t,"display")},htmlBuilder:hn,mathmlBuilder:un}),Jr({type:"array",names:["alignat","alignat*","alignedat"],props:{numArgs:1},handler:pn,htmlBuilder:hn,mathmlBuilder:un}),Jr({type:"array",names:["equation","equation*"],props:{numArgs:0},handler(e){on(e);const t={autoTag:an(e.envName),emptySingleRow:!0,singleRow:!0,maxNumCols:1,leqno:e.parser.settings.leqno};return ln(e.parser,t,"display")},htmlBuilder:hn,mathmlBuilder:un}),Jr({type:"array",names:["CD"],props:{numArgs:0},handler(e){return on(e),function(e){const t=[];for(e.gullet.beginGroup(),e.gullet.macros.set("\\cr","\\\\\\relax"),e.gullet.beginGroup();;){t.push(e.parseExpression(!1,"\\\\")),e.gullet.endGroup(),e.gullet.beginGroup();const r=e.fetch().text;if("&"!==r&&"\\\\"!==r){if("\\end"===r){0===t[t.length-1].length&&t.pop();break}throw new n("Expected \\\\ or \\cr or \\end",e.nextToken)}e.consume()}let r=[];const o=[r];for(let s=0;sAV".includes(o))throw new n('Expected one of "<>AV=|." after @',i[t]);for(let e=0;e<2;e++){let r=!0;for(let a=t+1;a{let r=e.parser,n=e.funcName;const o=dt(t[0]),s=n in gn?gn[n]:n;return{type:"font",mode:r.mode,font:s.slice(1),body:o}},htmlBuilder:(e,t)=>{const r=e.font,n=t.withFont(r);return At(e.body,n)},mathmlBuilder:(e,t)=>{const r=e.font,n=t.withFont(r);return Gt(e.body,n)}}),ut({type:"mclass",names:["\\boldsymbol","\\bm"],numArgs:1,handler:(e,t)=>{let r=e.parser;const n=t[0];return{type:"mclass",mode:r.mode,mclass:pr(n),body:[{type:"font",mode:r.mode,font:"boldsymbol",body:n}],isCharacterBox:m(n)}}}),ut({type:"font",names:["\\rm","\\sf","\\tt","\\bf","\\it","\\cal"],numArgs:0,allowedInText:!0,handler:(e,t)=>{let r=e.parser,n=e.funcName,o=e.breakOnTokenText;const s=r.mode,i=r.parseExpression(!0,o);return{type:"font",mode:s,font:"math"+n.slice(1),body:{type:"ordgroup",mode:r.mode,body:i}}}});const fn=(e,t)=>{if(!t)return e;return{type:"styling",mode:e.mode,style:t,body:[e]}};ut({type:"genfrac",names:["\\cfrac","\\dfrac","\\frac","\\tfrac","\\dbinom","\\binom","\\tbinom","\\\\atopfrac","\\\\bracefrac","\\\\brackfrac"],numArgs:2,allowedInArgument:!0,handler:(e,t)=>{let r=e.parser,n=e.funcName;const o=t[0],s=t[1];let i,a=null,l=null;switch(n){case"\\cfrac":case"\\dfrac":case"\\frac":case"\\tfrac":i=!0;break;case"\\\\atopfrac":i=!1;break;case"\\dbinom":case"\\binom":case"\\tbinom":i=!1,a="(",l=")";break;case"\\\\bracefrac":i=!1,a="\\{",l="\\}";break;case"\\\\brackfrac":i=!1,a="[",l="]";break;default:throw new Error("Unrecognized genfrac command")}const c="\\cfrac"===n;let h=null;return c||n.startsWith("\\d")?h="display":n.startsWith("\\t")&&(h="text"),fn({type:"genfrac",mode:r.mode,numer:o,denom:s,continued:c,hasBarLine:i,leftDelim:a,rightDelim:l,barSize:null},h)},htmlBuilder:(e,t)=>{const r=t.style,n=r.fracNum(),o=r.fracDen();let s;s=t.havingStyle(n);const i=At(e.numer,s,t);if(e.continued){const e=8.5/t.fontMetrics().ptPerEm,r=3.5/t.fontMetrics().ptPerEm;i.height=i.height0?3*h:7*h,p=t.fontMetrics().denom1):(c>0?(m=t.fontMetrics().num2,u=h):(m=t.fontMetrics().num3,u=3*h),p=t.fontMetrics().denom2),l){const e=t.fontMetrics().axisHeight;m-i.depth-(e+.5*c){const r=new qt("mfrac",[Gt(e.numer,t),Gt(e.denom,t)]);if(e.hasBarLine){if(e.barSize){const n=H(e.barSize,t);r.setAttribute("linethickness",N(n))}}else r.setAttribute("linethickness","0px");if(null!=e.leftDelim||null!=e.rightDelim){const t=[];if(null!=e.leftDelim){const r=new qt("mo",[new It(e.leftDelim.replace("\\",""))]);r.setAttribute("fence","true"),t.push(r)}if(t.push(r),null!=e.rightDelim){const r=new qt("mo",[new It(e.rightDelim.replace("\\",""))]);r.setAttribute("fence","true"),t.push(r)}return Nt(t)}return r}}),ut({type:"infix",names:["\\over","\\choose","\\atop","\\brace","\\brack"],numArgs:0,infix:!0,handler(e){let t,r=e.parser,n=e.funcName,o=e.token;switch(n){case"\\over":t="\\frac";break;case"\\choose":t="\\binom";break;case"\\atop":t="\\\\atopfrac";break;case"\\brace":t="\\\\bracefrac";break;case"\\brack":t="\\\\brackfrac";break;default:throw new Error("Unrecognized infix genfrac command")}return{type:"infix",mode:r.mode,replaceWith:t,token:o}}});const bn=["display","text","script","scriptscript"],yn=function(e){let t=null;return e.length>0&&(t=e,t="."===t?null:t),t};ut({type:"genfrac",names:["\\genfrac"],numArgs:6,allowedInArgument:!0,argTypes:["math","math","size","text","math","math"],handler(e,t){let r=e.parser;const n=t[4],o=t[5],s=dt(t[0]),i="atom"===s.type&&"open"===s.family?yn(s.text):null,a=dt(t[1]),l="atom"===a.type&&"close"===a.family?yn(a.text):null,c=sr(t[2],"size");let h,m=null;c.isBlank?h=!0:(m=c.value,h=m.number>0);let u=null,p=t[3];if("ordgroup"===p.type){if(p.body.length>0){const e=sr(p.body[0],"textord");u=bn[Number(e.text)]}}else p=sr(p,"textord"),u=bn[Number(p.text)];return fn({type:"genfrac",mode:r.mode,numer:n,denom:o,continued:!1,hasBarLine:h,barSize:m,leftDelim:i,rightDelim:l},u)}}),ut({type:"infix",names:["\\above"],numArgs:1,argTypes:["size"],infix:!0,handler(e,t){let r=e.parser,n=(e.funcName,e.token);return{type:"infix",mode:r.mode,replaceWith:"\\\\abovefrac",size:sr(t[0],"size").value,token:n}}}),ut({type:"genfrac",names:["\\\\abovefrac"],numArgs:3,argTypes:["math","size","math"],handler:(e,t)=>{let r=e.parser;e.funcName;const n=t[0],o=sr(t[1],"infix").size;if(!o)throw new Error("\\\\abovefrac expected size, but got "+String(o));const s=t[2],i=o.number>0;return{type:"genfrac",mode:r.mode,numer:n,denom:s,continued:!1,hasBarLine:i,barSize:o,leftDelim:null,rightDelim:null}}});const xn=(e,t)=>{const r=t.style;let n,o;"supsub"===e.type?(n=e.sup?At(e.sup,t.havingStyle(r.sup()),t):At(e.sub,t.havingStyle(r.sub()),t),o=sr(e.base,"horizBrace")):o=sr(e,"horizBrace");const s=At(o.base,t.havingBaseStyle(S.DISPLAY)),i=rr(o,t);let a;if(a=o.isOver?Je({positionType:"firstBaseline",children:[{type:"elem",elem:s},{type:"kern",size:.1},{type:"elem",elem:i,wrapperClasses:["svg-align"]}]}):Je({positionType:"bottom",positionData:s.depth+.1+i.height,children:[{type:"elem",elem:i,wrapperClasses:["svg-align"]},{type:"kern",size:.1},{type:"elem",elem:s}]}),n){const e=We(["minner",o.isOver?"mover":"munder"],[a],t);a=o.isOver?Je({positionType:"firstBaseline",children:[{type:"elem",elem:e},{type:"kern",size:.2},{type:"elem",elem:n}]}):Je({positionType:"bottom",positionData:e.depth+.2+n.height+n.depth,children:[{type:"elem",elem:n},{type:"kern",size:.2},{type:"elem",elem:e}]})}return We(["minner",o.isOver?"mover":"munder"],[a],t)};ut({type:"horizBrace",names:["\\overbrace","\\underbrace","\\overbracket","\\underbracket"],numArgs:1,handler(e,t){let r=e.parser,n=e.funcName;return{type:"horizBrace",mode:r.mode,label:n,isOver:n.includes("\\over"),base:t[0]}},htmlBuilder:xn,mathmlBuilder:(e,t)=>{const r=Qt(e.label);return new qt(e.isOver?"mover":"munder",[Gt(e.base,t),r])}}),ut({type:"href",names:["\\href"],numArgs:2,argTypes:["url","original"],allowedInText:!0,handler:(e,t)=>{let r=e.parser;const n=t[1],o=sr(t[0],"url").url;return r.settings.isTrusted({command:"\\href",url:o})?{type:"href",mode:r.mode,href:o,body:gt(n)}:r.formatUnsupportedCmd("\\href")},htmlBuilder:(e,t)=>{const r=wt(e.body,t,!1);return function(e,t,r,n){const o=new U(e,t,r,n);return Xe(o),o}(e.href,[],r,t)},mathmlBuilder:(e,t)=>{let r=Vt(e.body,t);return r instanceof qt||(r=new qt("mrow",[r])),r.setAttribute("href",e.href),r}}),ut({type:"href",names:["\\url"],numArgs:1,argTypes:["url"],allowedInText:!0,handler:(e,t)=>{let r=e.parser;const n=sr(t[0],"url").url;if(!r.settings.isTrusted({command:"\\url",url:n}))return r.formatUnsupportedCmd("\\url");const o=[];for(let e=0;e{let r=e.parser,o=e.funcName;e.token;const s=sr(t[0],"raw").string,i=t[1];let a;r.settings.strict&&Fe({strict:r.settings.strict,errorCode:"htmlExtension",errorMsg:"HTML extension is disabled on strict mode",report:!0});const l={};switch(o){case"\\htmlClass":l.class=s,a={command:"\\htmlClass",class:s};break;case"\\htmlId":l.id=s,a={command:"\\htmlId",id:s};break;case"\\htmlStyle":l.style=s,a={command:"\\htmlStyle",style:s};break;case"\\htmlData":{const e="{,}",t=[];let r="";for(let n=0;n{const r=wt(e.body,t,!1),n=["enclosing"];e.attributes.class&&n.push(...e.attributes.class.trim().split(/\s+/));const o=We(n,r,t);for(const t of Object.entries(e.attributes)){const e=t[0],r=t[1];"class"!==e&&o.setAttribute(e,r)}return o},mathmlBuilder:(e,t)=>Vt(e.body,t)}),ut({type:"htmlmathml",names:["\\html@mathml"],numArgs:2,allowedInArgument:!0,allowedInText:!0,handler:(e,t)=>({type:"htmlmathml",mode:e.parser.mode,html:gt(t[0]),mathml:gt(t[1])}),htmlBuilder:(e,t)=>{const r=wt(e.html,t,!1);return Ze(r)},mathmlBuilder:(e,t)=>Vt(e.mathml,t)});const wn=function(e){if(/^[-+]? *(\d+(\.\d*)?|\.\d+)$/.test(e))return{number:+e,unit:"bp"};{const t=/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(e);if(!t)throw new n("Invalid size: '"+e+"' in \\includegraphics");const r={number:+(t[1]+t[2]),unit:t[3]};if(!E(r))throw new n("Invalid unit: '"+r.unit+"' in \\includegraphics.");return r}};ut({type:"includegraphics",names:["\\includegraphics"],numArgs:1,numOptionalArgs:1,argTypes:["raw","url"],allowedInText:!1,handler:(e,t,r)=>{let o=e.parser,s={number:0,unit:"em"},i={number:.9,unit:"em"},a={number:0,unit:"em"},l="";if(r[0]){const e=sr(r[0],"raw").string.split(",");for(let t=0;t{const r=H(e.height,t);let n=0;e.totalheight.number>0&&(n=H(e.totalheight,t)-r);let o=0;e.width.number>0&&(o=H(e.width,t));const s={height:N(r+n)};o>0&&(s.width=N(o)),n>0&&(s.verticalAlign=N(-n));const i=new Y(e.src,e.alt,s);return i.height=r,i.depth=n,i},mathmlBuilder:(e,t)=>{const r=new qt("mglyph",[]);r.setAttribute("alt",e.alt);const n=H(e.height,t);let o=0;if(e.totalheight.number>0&&(o=H(e.totalheight,t)-n,r.setAttribute("valign",N(-o))),r.setAttribute("height",N(n+o)),e.width.number>0){const n=H(e.width,t);r.setAttribute("width",N(n))}return r.setAttribute("src",e.src),r}}),ut({type:"kern",names:["\\kern","\\mkern","\\hskip","\\mskip"],numArgs:1,argTypes:["size"],primitive:!0,allowedInText:!0,handler(e,t){let r=e.parser,n=e.funcName;const o=sr(t[0],"size");if(r.settings.strict){const e="m"===n[1],t="mu"===o.value.unit;e?(t||Fe({strict:r.settings.strict,errorCode:"mathVsTextUnits",errorMsg:"LaTeX's "+n+" supports only mu units, not "+o.value.unit+" units",report:!0}),"math"!==r.mode&&Fe({strict:r.settings.strict,errorCode:"mathVsTextUnits",errorMsg:"LaTeX's "+n+" works only in math mode",report:!0})):t&&Fe({strict:r.settings.strict,errorCode:"mathVsTextUnits",errorMsg:"LaTeX's "+n+" doesn't support mu units",report:!0})}return{type:"kern",mode:r.mode,dimension:o.value}},htmlBuilder(e,t){return Qe(e.dimension,t)},mathmlBuilder(e,t){const r=H(e.dimension,t);return new Rt(r)}}),ut({type:"lap",names:["\\mathllap","\\mathrlap","\\mathclap"],numArgs:1,allowedInText:!0,handler:(e,t)=>{let r=e.parser,n=e.funcName;const o=t[0];return{type:"lap",mode:r.mode,alignment:n.slice(5),body:o}},htmlBuilder:(e,t)=>{let r;"clap"===e.alignment?(r=We([],[At(e.body,t)]),r=We(["katex-inner"],[r],t)):r=We(["katex-inner"],[At(e.body,t)]);const n=We(["katex-fix"],[]);let o=We([e.alignment],[r,n],t);const s=We(["katex-strut"]);return s.style.height=N(o.height+o.depth),o.depth&&(s.style.verticalAlign=N(-o.depth)),o.children.unshift(s),o=We(["katex-thinbox"],[o],t),We(["mord","katex-vbox"],[o],t)},mathmlBuilder:(e,t)=>{const r=new qt("mpadded",[Gt(e.body,t)]);if("rlap"!==e.alignment){const t="llap"===e.alignment?"-1":"-0.5";r.setAttribute("lspace",t+"width")}return r.setAttribute("width","0px"),r}}),ut({type:"styling",names:["\\(","$"],numArgs:0,allowedInText:!0,allowedInMath:!1,handler(e,t){let r=e.funcName,n=e.parser;const o=n.mode;n.switchMode("math");const s="\\("===r?"\\)":"$",i=n.parseExpression(!1,s);return n.expect(s),n.switchMode(o),{type:"styling",mode:n.mode,style:"text",resetFont:!0,body:i}}}),ut({type:"text",names:["\\)","\\]"],numArgs:0,allowedInText:!0,allowedInMath:!1,handler(e,t){throw new n("Mismatched "+e.funcName)}});const vn=(e,t)=>{switch(t.style.size){case S.DISPLAY.size:return e.display;case S.TEXT.size:return e.text;case S.SCRIPT.size:return e.script;case S.SCRIPTSCRIPT.size:return e.scriptscript;default:return e.text}};ut({type:"mathchoice",names:["\\mathchoice"],numArgs:4,primitive:!0,handler:(e,t)=>({type:"mathchoice",mode:e.parser.mode,display:gt(t[0]),text:gt(t[1]),script:gt(t[2]),scriptscript:gt(t[3])}),htmlBuilder:(e,t)=>{const r=vn(e,t),n=wt(r,t,!1);return Ze(n)},mathmlBuilder:(e,t)=>{const r=vn(e,t);return Vt(r,t)}});const kn=(e,t,r,n,o,s,i)=>{e=We([],[e]);const a=r&&m(r);let l,c,h;if(t){const e=At(t,n.havingStyle(o.sup()),n);c={elem:e,kern:Math.max(n.fontMetrics().bigOpSpacing1,n.fontMetrics().bigOpSpacing3-e.depth)}}if(r){const e=At(r,n.havingStyle(o.sub()),n);l={elem:e,kern:Math.max(n.fontMetrics().bigOpSpacing2,n.fontMetrics().bigOpSpacing4-e.height)}}if(c&&l){const t=n.fontMetrics().bigOpSpacing5+l.elem.height+l.elem.depth+l.kern+e.depth+i;h=Je({positionType:"bottom",positionData:t,children:[{type:"kern",size:n.fontMetrics().bigOpSpacing5},{type:"elem",elem:l.elem,marginLeft:N(-s)},{type:"kern",size:l.kern},{type:"elem",elem:e},{type:"kern",size:c.kern},{type:"elem",elem:c.elem,marginLeft:N(s)},{type:"kern",size:n.fontMetrics().bigOpSpacing5}]})}else if(l){const t=e.height-i;h=Je({positionType:"top",positionData:t,children:[{type:"kern",size:n.fontMetrics().bigOpSpacing5},{type:"elem",elem:l.elem,marginLeft:N(-s)},{type:"kern",size:l.kern},{type:"elem",elem:e}]})}else{if(!c)return e;{const t=e.depth+i;h=Je({positionType:"bottom",positionData:t,children:[{type:"elem",elem:e},{type:"kern",size:c.kern},{type:"elem",elem:c.elem,marginLeft:N(s)},{type:"kern",size:n.fontMetrics().bigOpSpacing5}]})}}const u=[h];if(l&&0!==s&&!a){const e=We(["mspace"],[],n);e.style.marginRight=N(s),u.unshift(e)}return We(["mop","op-limits"],u,n)},zn=new Set(["\\smallint"]),Sn=(e,t)=>{let r,n,o,s=!1;"supsub"===e.type?(r=e.sup,n=e.sub,o=sr(e.base,"op"),s=!0):o=sr(e,"op");const i=t.style;let a,l,c=!1;if(i.size===S.DISPLAY.size&&o.symbol&&!zn.has(o.name)&&(c=!0),o.symbol){const e=c?"Size2-Regular":"Size1-Regular";let r="";if("\\oiint"!==o.name&&"\\oiiint"!==o.name||(r=o.name.slice(1),o.name="oiint"===r?"\\iint":"\\iiint"),a=Ve(o.name,e,"math",t,["mop","op-symbol",c?"large-op":"small-op"]),l=a.italic,r.length>0){const e=nt(r+"Size"+(c?"2":"1"),t);a=Je({positionType:"individualShift",children:[{type:"elem",elem:a,shift:0},{type:"elem",elem:e,shift:c?.08:0}]}),o.name="\\"+r,a.classes.unshift("mop"),a.italic=l}}else if(o.body){const e=wt(o.body,t,!0);1===e.length&&e[0]instanceof W?(a=e[0],a.classes[0]="mop"):a=We(["mop"],e,t)}else{const e=[];for(let r=1;r{let r=e.parser,n=e.funcName;return 1===n.length&&(n=Mn[n]),{type:"op",mode:r.mode,limits:!0,parentIsSupSub:!1,symbol:!0,name:n}},htmlBuilder:Sn,mathmlBuilder:(e,t)=>{let r;if(e.symbol)r=new qt("mo",[Ht(e.name,e.mode)]),zn.has(e.name)&&r.setAttribute("largeop","false");else if(e.body)r=new qt("mo",Pt(e.body,t));else{r=new qt("mi",[new It(e.name.slice(1))]);const t=new qt("mo",[Ht("\u2061","text")]);r=e.parentIsSupSub?new qt("mrow",[r,t]):Bt([r,t])}return r}}),ut({type:"op",names:["\\mathop"],numArgs:1,primitive:!0,handler:(e,t)=>{let r=e.parser;const n=t[0];return{type:"op",mode:r.mode,limits:!1,parentIsSupSub:!1,symbol:!1,body:gt(n)}}});const An={"\u222b":"\\int","\u222c":"\\iint","\u222d":"\\iiint","\u222e":"\\oint","\u222f":"\\oiint","\u2230":"\\oiiint"};ut({type:"op",names:["\\arcsin","\\arccos","\\arctan","\\arctg","\\arcctg","\\arg","\\ch","\\cos","\\cosec","\\cosh","\\cot","\\cotg","\\coth","\\csc","\\ctg","\\cth","\\deg","\\dim","\\exp","\\hom","\\ker","\\lg","\\ln","\\log","\\sec","\\sin","\\sinh","\\sh","\\tan","\\tanh","\\tg","\\th"],numArgs:0,handler(e){let t=e.parser,r=e.funcName;return{type:"op",mode:t.mode,limits:!1,parentIsSupSub:!1,symbol:!1,name:r}}}),ut({type:"op",names:["\\det","\\gcd","\\inf","\\lim","\\max","\\min","\\Pr","\\sup"],numArgs:0,handler(e){let t=e.parser,r=e.funcName;return{type:"op",mode:t.mode,limits:!0,parentIsSupSub:!1,symbol:!1,name:r}}}),ut({type:"op",names:["\\int","\\iint","\\iiint","\\oint","\\oiint","\\oiiint","\u222b","\u222c","\u222d","\u222e","\u222f","\u2230"],numArgs:0,allowedInArgument:!0,handler(e){let t=e.parser,r=e.funcName;return 1===r.length&&(r=An[r]),{type:"op",mode:t.mode,limits:!1,parentIsSupSub:!1,symbol:!0,name:r}}});const Tn=(e,t)=>{let r,n,o,s,i=!1;if("supsub"===e.type?(r=e.sup,n=e.sub,o=sr(e.base,"operatorname"),i=!0):o=sr(e,"operatorname"),o.body.length>0){const e=o.body.map(e=>{const t="text"in e?e.text:void 0;return"string"==typeof t?{type:"textord",mode:e.mode,text:t}:e}),r=wt(e,t.withFont("mathrm"),!0);for(let e=0;e{let r=e.parser,n=e.funcName;const o=t[0];return{type:"operatorname",mode:r.mode,body:gt(o),alwaysHandleSupSub:"\\operatornamewithlimits"===n,limits:!1,parentIsSupSub:!1}},htmlBuilder:Tn,mathmlBuilder:(e,t)=>{let r=Pt(e.body,t.withFont("mathrm")),n=!0;for(let e=0;ee.toText()).join("");r=[new It(e)]}const o=new qt("mi",r);o.setAttribute("mathvariant","normal");const s=new qt("mo",[Ht("\u2061","text")]);return e.parentIsSupSub?new qt("mrow",[o,s]):Bt([o,s])}}),en("\\operatorname","\\@ifstar\\operatornamewithlimits\\operatorname@"),pt({type:"ordgroup",htmlBuilder(e,t){return e.semisimple?Ze(wt(e.body,t,!1)):We(["mord"],wt(e.body,t,!0),t)},mathmlBuilder(e,t){return Vt(e.body,t,!0)}}),ut({type:"overline",names:["\\overline"],numArgs:1,handler(e,t){let r=e.parser;const n=t[0];return{type:"overline",mode:r.mode,body:n}},htmlBuilder(e,t){const r=At(e.body,t.havingCrampedStyle()),n=$e("overline-line",t),o=t.fontMetrics().defaultRuleThickness,s=Je({positionType:"firstBaseline",children:[{type:"elem",elem:r},{type:"kern",size:3*o},{type:"elem",elem:n},{type:"kern",size:o}]});return We(["mord","katex-overline"],[s],t)},mathmlBuilder(e,t){const r=new qt("mo",[new It("\u203e")]);r.setAttribute("stretchy","true");const n=new qt("mover",[Gt(e.body,t),r]);return n.setAttribute("accent","true"),n}}),ut({type:"phantom",names:["\\phantom"],numArgs:1,allowedInText:!0,handler:(e,t)=>{let r=e.parser;const n=t[0];return{type:"phantom",mode:r.mode,body:gt(n)}},htmlBuilder:(e,t)=>{const r=wt(e.body,t.withPhantom(),!1);return Ze(r)},mathmlBuilder:(e,t)=>{const r=Pt(e.body,t);return new qt("mphantom",r)}}),en("\\hphantom","\\smash{\\phantom{#1}}"),ut({type:"vphantom",names:["\\vphantom"],numArgs:1,allowedInText:!0,handler:(e,t)=>{let r=e.parser;const n=t[0];return{type:"vphantom",mode:r.mode,body:n}},htmlBuilder:(e,t)=>{const r=We(["katex-inner"],[At(e.body,t.withPhantom())]),n=We(["katex-fix"],[]);return We(["mord","rlap"],[r,n],t)},mathmlBuilder:(e,t)=>{const r=Pt(gt(e.body),t),n=new qt("mphantom",r),o=new qt("mpadded",[n]);return o.setAttribute("width","0px"),o}}),ut({type:"raisebox",names:["\\raisebox"],numArgs:2,argTypes:["size","hbox"],allowedInText:!0,handler(e,t){let r=e.parser;const n=sr(t[0],"size").value,o=t[1];return{type:"raisebox",mode:r.mode,dy:n,body:o}},htmlBuilder(e,t){const r=At(e.body,t),n=H(e.dy,t);return Je({positionType:"shift",positionData:-n,children:[{type:"elem",elem:r}]})},mathmlBuilder(e,t){const r=new qt("mpadded",[Gt(e.body,t)]),n=e.dy.number+e.dy.unit;return r.setAttribute("voffset",n),r}});const Cn=(e,t)=>({type:"reflectbox",mode:e.parser.mode,body:t[0]});function Bn(e,t,r){const n=wt(e,t,!1),o=t.sizeMultiplier/r.sizeMultiplier;for(let e=0;e=0?a.setAttribute("height",N(o)):(a.setAttribute("height",N(o)),a.setAttribute("depth",N(-o))),a.setAttribute("voffset",N(o)),a}});const qn=["\\tiny","\\sixptsize","\\scriptsize","\\footnotesize","\\small","\\normalsize","\\large","\\Large","\\LARGE","\\huge","\\Huge"];ut({type:"sizing",names:qn,numArgs:0,allowedInText:!0,handler:(e,t)=>{let r=e.breakOnTokenText,n=e.funcName,o=e.parser;const s=o.parseExpression(!1,r);return{type:"sizing",mode:o.mode,size:qn.indexOf(n)+1,body:s}},htmlBuilder:(e,t)=>{const r=t.havingSize(e.size);return Bn(e.body,r,t)},mathmlBuilder:(e,t)=>{const r=t.havingSize(e.size),n=Pt(e.body,r),o=new qt("mstyle",n);return o.setAttribute("mathsize",N(r.sizeMultiplier)),o}}),ut({type:"smash",names:["\\smash"],numArgs:1,numOptionalArgs:1,allowedInText:!0,handler:(e,t,r)=>{let n=e.parser,o=!1,s=!1;const i=r[0]&&sr(r[0],"ordgroup");if(i){let e;for(let t=0;t{const r=We([],[At(e.body,t)]);if(!e.smashHeight&&!e.smashDepth)return r;if(e.smashHeight&&(r.height=0),e.smashDepth&&(r.depth=0),e.smashHeight&&e.smashDepth)return We(["mord","katex-smash"],[r],t);if(r.children)for(let t=0;t{const r=new qt("mpadded",[Gt(e.body,t)]);return e.smashHeight&&r.setAttribute("height","0px"),e.smashDepth&&r.setAttribute("depth","0px"),r}}),ut({type:"sqrt",names:["\\sqrt"],numArgs:1,numOptionalArgs:1,handler(e,t,r){let n=e.parser;const o=r[0],s=t[0];return{type:"sqrt",mode:n.mode,body:s,index:o}},htmlBuilder(e,t){let r=At(e.body,t.havingCrampedStyle());0===r.height&&(r.height=t.fontMetrics().xHeight),r=Ke(r,t);const n=t.fontMetrics().defaultRuleThickness;let o=n;t.style.idr.height+r.depth+s&&(s=(s+h-r.height-r.depth)/2);const m=a.height-r.height-s-l;r.style.paddingLeft=N(c);const u=Je({positionType:"firstBaseline",children:[{type:"elem",elem:r,wrapperClasses:["svg-align"]},{type:"kern",size:-(r.height+m)},{type:"elem",elem:a},{type:"kern",size:l}]});if(e.index){const r=t.havingStyle(S.SCRIPTSCRIPT),n=At(e.index,r,t),o=.6*(u.height-u.depth),s=Je({positionType:"shift",positionData:-o,children:[{type:"elem",elem:n}]}),i=We(["katex-root"],[s]);return We(["mord","sqrt"],[i,u],t)}return We(["mord","sqrt"],[u],t)},mathmlBuilder(e,t){const r=e.body,n=e.index;return n?new qt("mroot",[Gt(r,t),Gt(n,t)]):new qt("msqrt",[Gt(r,t)])}});const In={display:S.DISPLAY,text:S.TEXT,script:S.SCRIPT,scriptscript:S.SCRIPTSCRIPT};ut({type:"styling",names:["\\displaystyle","\\textstyle","\\scriptstyle","\\scriptscriptstyle"],numArgs:0,allowedInText:!0,primitive:!0,handler(e,t){let r=e.breakOnTokenText,n=e.funcName,o=e.parser;const s=o.parseExpression(!0,r),i=n.slice(1,n.length-5);if(!(i in In))throw new Error("Unknown style: "+i);return{type:"styling",mode:o.mode,style:i,body:s}},htmlBuilder(e,t){const r=In[e.style];let n=t.havingStyle(r);return e.resetFont&&(n=n.withFont("")),Bn(e.body,n,t)},mathmlBuilder(e,t){const r=In[e.style];let n=t.havingStyle(r);e.resetFont&&(n=n.withFont(""));const o=Pt(e.body,n),s=new qt("mstyle",o),i={display:["0","true"],text:["0","false"],script:["1","false"],scriptscript:["2","false"]}[e.style];return s.setAttribute("scriptlevel",i[0]),s.setAttribute("displaystyle",i[1]),s}});pt({type:"supsub",htmlBuilder(e,t){const r=function(e,t){const r=e.base;if(r)return"op"===r.type?r.limits&&(t.style.size===S.DISPLAY.size||r.alwaysHandleSupSub)?Sn:null:"operatorname"===r.type?r.alwaysHandleSupSub&&(t.style.size===S.DISPLAY.size||r.limits)?Tn:null:"accent"===r.type?m(r.base)?hr:null:"horizBrace"===r.type&&!e.sub===r.isOver?xn:null;return null}(e,t);if(r)return r(e,t);const n=e.base,o=e.sup,s=e.sub,i=At(n,t);let a,l;const c=t.fontMetrics();let h=0,u=0;const p=n&&m(n);if(o){const e=t.havingStyle(t.style.sup());a=At(o,e,t),p||(h=i.height-e.fontMetrics().supDrop*e.sizeMultiplier/t.sizeMultiplier)}if(s){const e=t.havingStyle(t.style.sub());l=At(s,e,t),p||(u=i.depth+e.fontMetrics().subDrop*e.sizeMultiplier/t.sizeMultiplier)}let d;d=t.style===S.DISPLAY?c.sup1:t.style.cramped?c.sup3:c.sup2;const g=t.sizeMultiplier,f=N(.5/c.ptPerEm/g);let b,y=null;if(l){const t=e.base&&"op"===e.base.type&&e.base.name&&("\\oiint"===e.base.name||"\\oiiint"===e.base.name);var x;if(i instanceof W||t)y=N(-(null!=(x=i.italic)?x:0))}if(a&&l){h=Math.max(h,d,a.depth+.25*c.xHeight),u=Math.max(u,c.sub2);const e=4*c.defaultRuleThickness;if(h-a.depth-(l.height-u)0&&(h+=t,u-=t)}b=Je({positionType:"individualShift",children:[{type:"elem",elem:l,shift:u,marginRight:f,marginLeft:y},{type:"elem",elem:a,shift:-h,marginRight:f}]})}else if(l){u=Math.max(u,c.sub1,l.height-.8*c.xHeight);b=Je({positionType:"shift",positionData:u,children:[{type:"elem",elem:l,marginLeft:y,marginRight:f}]})}else{if(!a)throw new Error("supsub must have either sup or sub.");h=Math.max(h,d,a.depth+.25*c.xHeight),b=Je({positionType:"shift",positionData:-h,children:[{type:"elem",elem:a,marginRight:f}]})}const w=St(i,"right")||"mord";return We([w],[i,We(["msupsub"],[b])],t)},mathmlBuilder(e,t){let r,n,o=!1;e.base&&"horizBrace"===e.base.type&&(n=!!e.sup,n===e.base.isOver&&(o=!0,r=e.base.isOver)),!e.base||"op"!==e.base.type&&"operatorname"!==e.base.type||(e.base.parentIsSupSub=!0);const s=[Gt(e.base,t)];let i;if(e.sub&&s.push(Gt(e.sub,t)),e.sup&&s.push(Gt(e.sup,t)),o)i=r?"mover":"munder";else if(e.sub)if(e.sup){const r=e.base;i=r&&"op"===r.type&&r.limits&&t.style===S.DISPLAY||r&&"operatorname"===r.type&&r.alwaysHandleSupSub&&(t.style===S.DISPLAY||r.limits)?"munderover":"msubsup"}else{const r=e.base;i=r&&"op"===r.type&&r.limits&&(t.style===S.DISPLAY||r.alwaysHandleSupSub)||r&&"operatorname"===r.type&&r.alwaysHandleSupSub&&(r.limits||t.style===S.DISPLAY)?"munder":"msub"}else{const r=e.base;i=r&&"op"===r.type&&r.limits&&(t.style===S.DISPLAY||r.alwaysHandleSupSub)||r&&"operatorname"===r.type&&r.alwaysHandleSupSub&&(r.limits||t.style===S.DISPLAY)?"mover":"msup"}return new qt(i,s)}}),pt({type:"atom",htmlBuilder(e,t){return Ge(e.text,e.mode,t,["m"+e.family])},mathmlBuilder(e,t){const r=new qt("mo",[Ht(e.text,e.mode)]);if("bin"===e.family){const n=Lt(e,t);"bold-italic"===n&&r.setAttribute("mathvariant",n)}else"punct"===e.family?r.setAttribute("separator","true"):"open"!==e.family&&"close"!==e.family||r.setAttribute("stretchy","false");return r}});const Rn={mi:"italic",mn:"normal",mtext:"normal"};pt({type:"mathord",htmlBuilder(e,t){return je(e,t)},mathmlBuilder(e,t){const r=new qt("mi",[Ht(e.text,e.mode,t)]),n=Lt(e,t)||"italic";return n!==Rn[r.type]&&r.setAttribute("mathvariant",n),r}}),pt({type:"textord",htmlBuilder(e,t){return je(e,t)},mathmlBuilder(e,t){const r=Ht(e.text,e.mode,t),n=Lt(e,t)||"normal";let o;return o="text"===e.mode?new qt("mtext",[r]):/[0-9]/.test(e.text)?new qt("mn",[r]):"\\prime"===e.text?new qt("mo",[r]):new qt("mi",[r]),n!==Rn[o.type]&&o.setAttribute("mathvariant",n),o}});const On=new Map([["\\nobreak","nobreak"],["\\allowbreak","allowbreak"]]),En=new Map([[" ",{}],["\\ ",{}],["~",{className:"nobreak"}],["\\space",{}],["\\nobreakspace",{className:"nobreak"}]]);pt({type:"spacing",htmlBuilder(e,t){const r=En.get(e.text),o=On.get(e.text);if(r){const n=r.className||"";if("text"===e.mode){const r=je(e,t);return r.classes.push(n),r}return We(["mspace",n],[Ge(e.text,e.mode,t)],t)}if(o)return We(["mspace",o],[],t);throw new n('Unknown type of space "'+e.text+'"')},mathmlBuilder(e,t){let r;if(!En.has(e.text)){if(On.has(e.text))return new qt("mspace");throw new n('Unknown type of space "'+e.text+'"')}return r=new qt("mtext",[new It("\xa0")]),r}});const Hn=()=>{const e=new qt("mtd",[]);return e.setAttribute("width","50%"),e};pt({type:"tag",mathmlBuilder(e,t){const r=new qt("mtable",[new qt("mtr",[Hn(),new qt("mtd",[Vt(e.body,t)]),Hn(),new qt("mtd",[Vt(e.tag,t)])])]);return r.setAttribute("width","100%"),r}});const Nn={"\\text":void 0,"\\textrm":"textrm","\\textsf":"textsf","\\texttt":"texttt","\\textnormal":"textrm"},Dn={"\\textbf":"textbf","\\textmd":"textmd"},Ln={"\\textit":"textit","\\textup":"textup"},Fn=(e,t)=>{const r=e.font;return r?Nn[r]?t.withTextFontFamily(Nn[r]):Dn[r]?t.withTextFontWeight(Dn[r]):"\\emph"===r?"textit"===t.fontShape?t.withTextFontShape("textup"):t.withTextFontShape("textit"):t.withTextFontShape(Ln[r]):t};ut({type:"text",names:["\\text","\\textrm","\\textsf","\\texttt","\\textnormal","\\textbf","\\textmd","\\textit","\\textup","\\emph"],numArgs:1,argTypes:["text"],allowedInArgument:!0,allowedInText:!0,handler(e,t){let r=e.parser,n=e.funcName;const o=t[0];return{type:"text",mode:r.mode,body:gt(o),font:n}},htmlBuilder(e,t){const r=Fn(e,t),n=wt(e.body,r,!0);return We(["mord","text"],n,r)},mathmlBuilder(e,t){const r=Fn(e,t);return Vt(e.body,r)}}),ut({type:"underline",names:["\\underline"],numArgs:1,allowedInText:!0,handler(e,t){return{type:"underline",mode:e.parser.mode,body:t[0]}},htmlBuilder(e,t){const r=At(e.body,t),n=$e("underline-line",t),o=t.fontMetrics().defaultRuleThickness,s=Je({positionType:"top",positionData:r.height,children:[{type:"kern",size:o},{type:"elem",elem:n},{type:"kern",size:3*o},{type:"elem",elem:r}]});return We(["mord","katex-underline"],[s],t)},mathmlBuilder(e,t){const r=new qt("mo",[new It("\u203e")]);r.setAttribute("stretchy","true");const n=new qt("munder",[Gt(e.body,t),r]);return n.setAttribute("accentunder","true"),n}}),ut({type:"vcenter",names:["\\vcenter"],numArgs:1,argTypes:["original"],allowedInText:!1,handler(e,t){return{type:"vcenter",mode:e.parser.mode,body:t[0]}},htmlBuilder(e,t){const r=At(e.body,t),n=t.fontMetrics().axisHeight,o=.5*(r.height-n-(r.depth+n));return Je({positionType:"shift",positionData:o,children:[{type:"elem",elem:r}]})},mathmlBuilder(e,t){const r=new qt("mpadded",[Gt(e.body,t)],["vcenter"]);return new qt("mrow",[r])}}),ut({type:"verb",names:["\\verb"],numArgs:0,allowedInText:!0,handler(e,t,r){throw new n("\\verb ended by end of line instead of matching delimiter")},htmlBuilder(e,t){const r=Pn(e),n=[],o=t.havingStyle(t.style.text());for(let t=0;te.body.replace(/ /g,e.star?"\u2423":"\xa0");var Vn=ct;const Gn="[ \r\n\t]",jn="(\\\\[a-zA-Z@]+)"+Gn+"*",Un="[\u0300-\u036f]",Yn=new RegExp(Un+"+$"),Xn="("+Gn+"+)|\\\\(\n|[ \r\t]+\n?)[ \r\t]*|([!-\\[\\]-\u2027\u202a-\ud7ff\uf900-\uffff]"+Un+"*|[\ud800-\udbff][\udc00-\udfff]"+Un+"*|\\\\verb\\*([^]).*?\\4|\\\\verb([^*a-zA-Z]).*?\\5|"+jn+"|\\\\[^\ud800-\udfff])";class Wn{constructor(e,t){this.input=void 0,this.settings=void 0,this.tokenRegex=void 0,this.catcodes=void 0,this.input=e,this.settings=t,this.tokenRegex=new RegExp(Xn,"g"),this.catcodes={"%":14,"~":13}}setCatcode(e,t){this.catcodes[e]=t}lex(){const e=this.input,t=this.tokenRegex.lastIndex;if(t===e.length)return new rn("EOF",new tn(this,t,t));const r=this.tokenRegex.exec(e);if(null===r||r.index!==t)throw new n("Unexpected character: '"+e[t]+"'",new rn(e[t],new tn(this,t,t+1)));const o=r[6]||r[3]||(r[2]?"\\ ":" ");if(14===this.catcodes[o]){const t=e.indexOf("\n",this.tokenRegex.lastIndex);return-1===t?(this.tokenRegex.lastIndex=e.length,Fe({strict:this.settings.strict,errorCode:"commentAtEnd",errorMsg:"% comment has no terminating newline; LaTeX would fail because of commenting the end of math mode (e.g. $)",report:!0})):this.tokenRegex.lastIndex=t+1,this.lex()}return new rn(o,new tn(this,t,this.tokenRegex.lastIndex))}}class _n{constructor(e,t){void 0===e&&(e={}),void 0===t&&(t={}),this.current=void 0,this.builtins=void 0,this.undefStack=void 0,this.current=t,this.builtins=e,this.undefStack=[]}beginGroup(){this.undefStack.push({})}endGroup(){if(0===this.undefStack.length)throw new n("Unbalanced namespace destruction: attempt to pop global namespace; please report this as a bug");const e=this.undefStack.pop();for(const t of Object.keys(e))void 0===e[t]?delete this.current[t]:this.current[t]=e[t]}endGroups(){for(;this.undefStack.length>0;)this.endGroup()}has(e){return Object.prototype.hasOwnProperty.call(this.current,e)||Object.prototype.hasOwnProperty.call(this.builtins,e)}get(e){return Object.prototype.hasOwnProperty.call(this.current,e)?this.current[e]:Object.prototype.hasOwnProperty.call(this.builtins,e)?this.builtins[e]:void 0}set(e,t,r){if(void 0===r&&(r=!1),r){for(let t=0;t0&&(this.undefStack[this.undefStack.length-1][e]=t)}else{const t=this.undefStack[this.undefStack.length-1];t&&!Object.prototype.hasOwnProperty.call(t,e)&&(t[e]=Object.prototype.hasOwnProperty.call(this.current,e)?this.current[e]:void 0)}null==t?delete this.current[e]:this.current[e]=t}}var $n=Qr;en("\\noexpand",function(e){const t=e.popToken();return e.isExpandable(t.text)&&(t.noexpand=!0,t.treatAsRelax=!0),{tokens:[t],numArgs:0}}),en("\\expandafter",function(e){const t=e.popToken();return e.expandOnce(!0),{tokens:[t],numArgs:0}}),en("\\@firstoftwo",function(e){return{tokens:e.consumeArgs(2)[0],numArgs:0}}),en("\\@secondoftwo",function(e){return{tokens:e.consumeArgs(2)[1],numArgs:0}}),en("\\@ifnextchar",function(e){const t=e.consumeArgs(3);e.consumeSpaces();const r=e.future();return 1===t[0].length&&t[0][0].text===r.text?{tokens:t[1],numArgs:0}:{tokens:t[2],numArgs:0}}),en("\\@ifstar","\\@ifnextchar *{\\@firstoftwo{#1}}"),en("\\TextOrMath",function(e){const t=e.consumeArgs(2);return"text"===e.mode?{tokens:t[0],numArgs:0}:{tokens:t[1],numArgs:0}});const Zn={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,a:10,A:10,b:11,B:11,c:12,C:12,d:13,D:13,e:14,E:14,f:15,F:15};en("\\char",function(e){let t,r=e.popToken(),o=0;if("'"===r.text)t=8,r=e.popToken();else if('"'===r.text)t=16,r=e.popToken();else if("`"===r.text)if(r=e.popToken(),"\\"===r.text[0])o=r.text.charCodeAt(1);else{if("EOF"===r.text)throw new n("\\char` missing argument");o=r.text.charCodeAt(0)}else t=10;if(t){if(o=Zn[r.text],null==o||o>=t)throw new n("Invalid base-"+t+" digit "+r.text);let s;for(;null!=(s=Zn[e.future().text])&&s{let s=e.consumeArg().tokens;if(1!==s.length)throw new n("\\newcommand's first argument must be a macro name");const i=s[0].text,a=e.isDefined(i);if(a&&!t)throw new n("\\newcommand{"+i+"} attempting to redefine "+i+"; use \\renewcommand");if(!a&&!r)throw new n("\\renewcommand{"+i+"} when command "+i+" does not yet exist; use \\newcommand");let l=0;if(s=e.consumeArg().tokens,1===s.length&&"["===s[0].text){let t="",r=e.expandNextToken();for(;"]"!==r.text&&"EOF"!==r.text;)t+=r.text,r=e.expandNextToken();if(!t.match(/^\s*[0-9]+\s*$/))throw new n("Invalid number of arguments: "+t);l=parseInt(t),s=e.consumeArg().tokens}return a&&o||e.macros.set(i,{tokens:s,numArgs:l}),""};en("\\newcommand",e=>Kn(e,!1,!0,!1)),en("\\renewcommand",e=>Kn(e,!0,!1,!1)),en("\\providecommand",e=>Kn(e,!0,!0,!0)),en("\\message",e=>{const t=e.consumeArgs(1)[0];return console.log(t.reverse().map(e=>e.text).join("")),""}),en("\\errmessage",e=>{const t=e.consumeArgs(1)[0];return console.error(t.reverse().map(e=>e.text).join("")),""}),en("\\show",e=>{const t=e.popToken(),r=t.text;return console.log(t,e.macros.get(r),Vn[r],ne.math[r],ne.text[r]),""}),en("\\bgroup","{"),en("\\egroup","}"),en("~","\\nobreakspace"),en("\\lq","`"),en("\\rq","'"),en("\\aa","\\r a"),en("\\AA","\\r A"),en("\\textcopyright","\\html@mathml{\\textcircled{c}}{\\char`\xa9}"),en("\\copyright","\\TextOrMath{\\textcopyright}{\\text{\\textcopyright}}"),en("\\textregistered","\\html@mathml{\\textcircled{\\scriptsize R}}{\\char`\xae}"),en("\u212c","\\mathscr{B}"),en("\u2130","\\mathscr{E}"),en("\u2131","\\mathscr{F}"),en("\u210b","\\mathscr{H}"),en("\u2110","\\mathscr{I}"),en("\u2112","\\mathscr{L}"),en("\u2133","\\mathscr{M}"),en("\u211b","\\mathscr{R}"),en("\u212d","\\mathfrak{C}"),en("\u210c","\\mathfrak{H}"),en("\u2128","\\mathfrak{Z}"),en("\\Bbbk","\\Bbb{k}"),en("\\llap","\\mathllap{\\textrm{#1}}"),en("\\rlap","\\mathrlap{\\textrm{#1}}"),en("\\clap","\\mathclap{\\textrm{#1}}"),en("\\mathstrut","\\vphantom{(}"),en("\\underbar","\\underline{\\text{#1}}"),en("\\not",'\\html@mathml{\\mathrel{\\mathrlap\\@not}\\nobreak}{\\char"338}'),en("\\neq","\\html@mathml{\\mathrel{\\not=}}{\\mathrel{\\char`\u2260}}"),en("\\ne","\\neq"),en("\u2260","\\neq"),en("\\notin","\\html@mathml{\\mathrel{{\\in}\\mathllap{/\\mskip1mu}}}{\\mathrel{\\char`\u2209}}"),en("\u2209","\\notin"),en("\u2258","\\html@mathml{\\mathrel{=\\kern{-1em}\\raisebox{0.4em}{$\\scriptsize\\frown$}}}{\\mathrel{\\char`\u2258}}"),en("\u2259","\\html@mathml{\\stackrel{\\tiny\\wedge}{=}}{\\mathrel{\\char`\u2258}}"),en("\u225a","\\html@mathml{\\stackrel{\\tiny\\vee}{=}}{\\mathrel{\\char`\u225a}}"),en("\u225b","\\html@mathml{\\stackrel{\\scriptsize\\star}{=}}{\\mathrel{\\char`\u225b}}"),en("\u225d","\\html@mathml{\\stackrel{\\tiny\\mathrm{def}}{=}}{\\mathrel{\\char`\u225d}}"),en("\u225e","\\html@mathml{\\stackrel{\\tiny\\mathrm{m}}{=}}{\\mathrel{\\char`\u225e}}"),en("\u225f","\\html@mathml{\\stackrel{\\tiny?}{=}}{\\mathrel{\\char`\u225f}}"),en("\u27c2","\\perp"),en("\u203c","\\mathclose{!\\mkern-0.8mu!}"),en("\u220c","\\notni"),en("\u231c","\\ulcorner"),en("\u231d","\\urcorner"),en("\u231e","\\llcorner"),en("\u231f","\\lrcorner"),en("\xa9","\\copyright"),en("\xae","\\textregistered"),en("\\ulcorner",'\\html@mathml{\\@ulcorner}{\\mathop{\\char"231c}}'),en("\\urcorner",'\\html@mathml{\\@urcorner}{\\mathop{\\char"231d}}'),en("\\llcorner",'\\html@mathml{\\@llcorner}{\\mathop{\\char"231e}}'),en("\\lrcorner",'\\html@mathml{\\@lrcorner}{\\mathop{\\char"231f}}'),en("\\vdots","{\\varvdots\\rule{0pt}{15pt}}"),en("\u22ee","\\vdots"),en("\\varGamma","\\mathit{\\Gamma}"),en("\\varDelta","\\mathit{\\Delta}"),en("\\varTheta","\\mathit{\\Theta}"),en("\\varLambda","\\mathit{\\Lambda}"),en("\\varXi","\\mathit{\\Xi}"),en("\\varPi","\\mathit{\\Pi}"),en("\\varSigma","\\mathit{\\Sigma}"),en("\\varUpsilon","\\mathit{\\Upsilon}"),en("\\varPhi","\\mathit{\\Phi}"),en("\\varPsi","\\mathit{\\Psi}"),en("\\varOmega","\\mathit{\\Omega}"),en("\\substack","\\begin{subarray}{c}#1\\end{subarray}"),en("\\colon","\\nobreak\\mskip2mu\\mathpunct{}\\mathchoice{\\mkern-3mu}{\\mkern-3mu}{}{}{:}\\mskip6mu\\relax"),en("\\boxed","\\fbox{$\\displaystyle{#1}$}"),en("\\iff","\\DOTSB\\;\\Longleftrightarrow\\;"),en("\\implies","\\DOTSB\\;\\Longrightarrow\\;"),en("\\impliedby","\\DOTSB\\;\\Longleftarrow\\;"),en("\\dddot","{\\overset{\\raisebox{-0.1ex}{\\normalsize ...}}{#1}}"),en("\\ddddot","{\\overset{\\raisebox{-0.1ex}{\\normalsize ....}}{#1}}");const Jn={",":"\\dotsc","\\not":"\\dotsb","+":"\\dotsb","=":"\\dotsb","<":"\\dotsb",">":"\\dotsb","-":"\\dotsb","*":"\\dotsb",":":"\\dotsb","\\DOTSB":"\\dotsb","\\coprod":"\\dotsb","\\bigvee":"\\dotsb","\\bigwedge":"\\dotsb","\\biguplus":"\\dotsb","\\bigcap":"\\dotsb","\\bigcup":"\\dotsb","\\prod":"\\dotsb","\\sum":"\\dotsb","\\bigotimes":"\\dotsb","\\bigoplus":"\\dotsb","\\bigodot":"\\dotsb","\\bigsqcup":"\\dotsb","\\And":"\\dotsb","\\longrightarrow":"\\dotsb","\\Longrightarrow":"\\dotsb","\\longleftarrow":"\\dotsb","\\Longleftarrow":"\\dotsb","\\longleftrightarrow":"\\dotsb","\\Longleftrightarrow":"\\dotsb","\\mapsto":"\\dotsb","\\longmapsto":"\\dotsb","\\hookrightarrow":"\\dotsb","\\doteq":"\\dotsb","\\mathbin":"\\dotsb","\\mathrel":"\\dotsb","\\relbar":"\\dotsb","\\Relbar":"\\dotsb","\\xrightarrow":"\\dotsb","\\xleftarrow":"\\dotsb","\\DOTSI":"\\dotsi","\\int":"\\dotsi","\\oint":"\\dotsi","\\iint":"\\dotsi","\\iiint":"\\dotsi","\\iiiint":"\\dotsi","\\idotsint":"\\dotsi","\\DOTSX":"\\dotsx"},Qn=new Set(["bin","rel"]);en("\\dots",function(e){let t="\\dotso";const r=e.expandAfterFuture().text;return r in Jn?t=Jn[r]:("\\not"===r.slice(0,4)||r in ne.math&&Qn.has(ne.math[r].group))&&(t="\\dotsb"),t});const eo={")":!0,"]":!0,"\\rbrack":!0,"\\}":!0,"\\rbrace":!0,"\\rangle":!0,"\\rceil":!0,"\\rfloor":!0,"\\rgroup":!0,"\\rmoustache":!0,"\\right":!0,"\\bigr":!0,"\\biggr":!0,"\\Bigr":!0,"\\Biggr":!0,$:!0,";":!0,".":!0,",":!0};en("\\dotso",function(e){return e.future().text in eo?"\\ldots\\,":"\\ldots"}),en("\\dotsc",function(e){const t=e.future().text;return t in eo&&","!==t?"\\ldots\\,":"\\ldots"}),en("\\cdots",function(e){return e.future().text in eo?"\\@cdots\\,":"\\@cdots"}),en("\\dotsb","\\cdots"),en("\\dotsm","\\cdots"),en("\\dotsi","\\!\\cdots"),en("\\dotsx","\\ldots\\,"),en("\\DOTSI","\\relax"),en("\\DOTSB","\\relax"),en("\\DOTSX","\\relax"),en("\\tmspace","\\TextOrMath{\\kern#1#3}{\\mskip#1#2}\\relax"),en("\\,","\\tmspace+{3mu}{.1667em}"),en("\\thinspace","\\,"),en("\\>","\\mskip{4mu}"),en("\\:","\\tmspace+{4mu}{.2222em}"),en("\\medspace","\\:"),en("\\;","\\tmspace+{5mu}{.2777em}"),en("\\thickspace","\\;"),en("\\!","\\tmspace-{3mu}{.1667em}"),en("\\negthinspace","\\!"),en("\\negmedspace","\\tmspace-{4mu}{.2222em}"),en("\\negthickspace","\\tmspace-{5mu}{.277em}"),en("\\enspace","\\kern.5em "),en("\\enskip","\\hskip.5em\\relax"),en("\\quad","\\hskip1em\\relax"),en("\\qquad","\\hskip2em\\relax"),en("\\tag","\\@ifstar\\tag@literal\\tag@paren"),en("\\tag@paren","\\tag@literal{({#1})}"),en("\\tag@literal",e=>{if(e.macros.get("\\df@tag"))throw new n("Multiple \\tag");return"\\gdef\\df@tag{\\text{#1}}"}),en("\\bmod","\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}\\mathbin{\\rm mod}\\mathchoice{\\mskip1mu}{\\mskip1mu}{\\mskip5mu}{\\mskip5mu}"),en("\\pod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern8mu}{\\mkern8mu}{\\mkern8mu}(#1)"),en("\\pmod","\\pod{{\\rm mod}\\mkern6mu#1}"),en("\\mod","\\allowbreak\\mathchoice{\\mkern18mu}{\\mkern12mu}{\\mkern12mu}{\\mkern12mu}{\\rm mod}\\,\\,#1"),en("\\newline","\\\\\\relax"),en("\\TeX","\\textrm{\\html@mathml{T\\kern-.1667em\\raisebox{-.5ex}{E}\\kern-.125emX}{TeX}}");const to=N(K["Main-Regular"]["T".charCodeAt(0)][1]-.7*K["Main-Regular"]["A".charCodeAt(0)][1]);en("\\LaTeX","\\textrm{\\html@mathml{L\\kern-.36em\\raisebox{"+to+"}{\\scriptstyle A}\\kern-.15em\\TeX}{LaTeX}}"),en("\\KaTeX","\\textrm{\\html@mathml{K\\kern-.17em\\raisebox{"+to+"}{\\scriptstyle A}\\kern-.15em\\TeX}{KaTeX}}"),en("\\hspace","\\@ifstar\\@hspacer\\@hspace"),en("\\@hspace","\\hskip #1\\relax"),en("\\@hspacer","\\rule{0pt}{0pt}\\hskip #1\\relax"),en("\\ordinarycolon",":"),en("\\vcentcolon","\\mathrel{\\mathop\\ordinarycolon}"),en("\\dblcolon",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-.9mu}\\vcentcolon}}{\\mathop{\\char"2237}}'),en("\\coloneqq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2254}}'),en("\\Coloneqq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}=}}{\\mathop{\\char"2237\\char"3d}}'),en("\\coloneq",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"3a\\char"2212}}'),en("\\Coloneq",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\mathrel{-}}}{\\mathop{\\char"2237\\char"2212}}'),en("\\eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2255}}'),en("\\Eqqcolon",'\\html@mathml{\\mathrel{=\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"3d\\char"2237}}'),en("\\eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\vcentcolon}}{\\mathop{\\char"2239}}'),en("\\Eqcolon",'\\html@mathml{\\mathrel{\\mathrel{-}\\mathrel{\\mkern-1.2mu}\\dblcolon}}{\\mathop{\\char"2212\\char"2237}}'),en("\\colonapprox",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"3a\\char"2248}}'),en("\\Colonapprox",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\approx}}{\\mathop{\\char"2237\\char"2248}}'),en("\\colonsim",'\\html@mathml{\\mathrel{\\vcentcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"3a\\char"223c}}'),en("\\Colonsim",'\\html@mathml{\\mathrel{\\dblcolon\\mathrel{\\mkern-1.2mu}\\sim}}{\\mathop{\\char"2237\\char"223c}}'),en("\u2237","\\dblcolon"),en("\u2239","\\eqcolon"),en("\u2254","\\coloneqq"),en("\u2255","\\eqqcolon"),en("\u2a74","\\Coloneqq"),en("\\ratio","\\vcentcolon"),en("\\coloncolon","\\dblcolon"),en("\\colonequals","\\coloneqq"),en("\\coloncolonequals","\\Coloneqq"),en("\\equalscolon","\\eqqcolon"),en("\\equalscoloncolon","\\Eqqcolon"),en("\\colonminus","\\coloneq"),en("\\coloncolonminus","\\Coloneq"),en("\\minuscolon","\\eqcolon"),en("\\minuscoloncolon","\\Eqcolon"),en("\\coloncolonapprox","\\Colonapprox"),en("\\coloncolonsim","\\Colonsim"),en("\\simcolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),en("\\simcoloncolon","\\mathrel{\\sim\\mathrel{\\mkern-1.2mu}\\dblcolon}"),en("\\approxcolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\vcentcolon}"),en("\\approxcoloncolon","\\mathrel{\\approx\\mathrel{\\mkern-1.2mu}\\dblcolon}"),en("\\notni","\\html@mathml{\\not\\ni}{\\mathrel{\\char`\u220c}}"),en("\\limsup","\\DOTSB\\operatorname*{lim\\,sup}"),en("\\liminf","\\DOTSB\\operatorname*{lim\\,inf}"),en("\\injlim","\\DOTSB\\operatorname*{inj\\,lim}"),en("\\projlim","\\DOTSB\\operatorname*{proj\\,lim}"),en("\\varlimsup","\\DOTSB\\operatorname*{\\overline{lim}}"),en("\\varliminf","\\DOTSB\\operatorname*{\\underline{lim}}"),en("\\varinjlim","\\DOTSB\\operatorname*{\\underrightarrow{lim}}"),en("\\varprojlim","\\DOTSB\\operatorname*{\\underleftarrow{lim}}"),en("\\gvertneqq","\\html@mathml{\\@gvertneqq}{\u2269}"),en("\\lvertneqq","\\html@mathml{\\@lvertneqq}{\u2268}"),en("\\ngeqq","\\html@mathml{\\@ngeqq}{\u2271}"),en("\\ngeqslant","\\html@mathml{\\@ngeqslant}{\u2271}"),en("\\nleqq","\\html@mathml{\\@nleqq}{\u2270}"),en("\\nleqslant","\\html@mathml{\\@nleqslant}{\u2270}"),en("\\nshortmid","\\html@mathml{\\@nshortmid}{\u2224}"),en("\\nshortparallel","\\html@mathml{\\@nshortparallel}{\u2226}"),en("\\nsubseteqq","\\html@mathml{\\@nsubseteqq}{\u2288}"),en("\\nsupseteqq","\\html@mathml{\\@nsupseteqq}{\u2289}"),en("\\varsubsetneq","\\html@mathml{\\@varsubsetneq}{\u228a}"),en("\\varsubsetneqq","\\html@mathml{\\@varsubsetneqq}{\u2acb}"),en("\\varsupsetneq","\\html@mathml{\\@varsupsetneq}{\u228b}"),en("\\varsupsetneqq","\\html@mathml{\\@varsupsetneqq}{\u2acc}"),en("\\imath","\\html@mathml{\\@imath}{\u0131}"),en("\\jmath","\\html@mathml{\\@jmath}{\u0237}"),en("\\llbracket","\\html@mathml{\\mathopen{[\\mkern-3.2mu[}}{\\mathopen{\\char`\u27e6}}"),en("\\rrbracket","\\html@mathml{\\mathclose{]\\mkern-3.2mu]}}{\\mathclose{\\char`\u27e7}}"),en("\u27e6","\\llbracket"),en("\u27e7","\\rrbracket"),en("\\lBrace","\\html@mathml{\\mathopen{\\{\\mkern-3.2mu[}}{\\mathopen{\\char`\u2983}}"),en("\\rBrace","\\html@mathml{\\mathclose{]\\mkern-3.2mu\\}}}{\\mathclose{\\char`\u2984}}"),en("\u21a4","\\mapsfrom"),en("\\mapsfrom","\\html@mathml{\\mathrel{\\mathreflectbox{\\mapsto}}}{\\mathrel{\\char`\u21a4}}"),en("\u2983","\\lBrace"),en("\u2984","\\rBrace"),en("\\minuso","\\mathbin{\\html@mathml{{\\mathrlap{\\mathchoice{\\kern{0.145em}}{\\kern{0.145em}}{\\kern{0.1015em}}{\\kern{0.0725em}}\\circ}{-}}}{\\char`\u29b5}}"),en("\u29b5","\\minuso"),en("\\darr","\\downarrow"),en("\\dArr","\\Downarrow"),en("\\Darr","\\Downarrow"),en("\\lang","\\langle"),en("\\rang","\\rangle"),en("\\uarr","\\uparrow"),en("\\uArr","\\Uparrow"),en("\\Uarr","\\Uparrow"),en("\\N","\\mathbb{N}"),en("\\R","\\mathbb{R}"),en("\\Z","\\mathbb{Z}"),en("\\alef","\\aleph"),en("\\alefsym","\\aleph"),en("\\Alpha","\\mathrm{A}"),en("\\Beta","\\mathrm{B}"),en("\\bull","\\bullet"),en("\\Chi","\\mathrm{X}"),en("\\clubs","\\clubsuit"),en("\\cnums","\\mathbb{C}"),en("\\Complex","\\mathbb{C}"),en("\\Dagger","\\ddagger"),en("\\diamonds","\\diamondsuit"),en("\\empty","\\emptyset"),en("\\Epsilon","\\mathrm{E}"),en("\\Eta","\\mathrm{H}"),en("\\exist","\\exists"),en("\\harr","\\leftrightarrow"),en("\\hArr","\\Leftrightarrow"),en("\\Harr","\\Leftrightarrow"),en("\\hearts","\\heartsuit"),en("\\image","\\Im"),en("\\infin","\\infty"),en("\\Iota","\\mathrm{I}"),en("\\isin","\\in"),en("\\Kappa","\\mathrm{K}"),en("\\larr","\\leftarrow"),en("\\lArr","\\Leftarrow"),en("\\Larr","\\Leftarrow"),en("\\lrarr","\\leftrightarrow"),en("\\lrArr","\\Leftrightarrow"),en("\\Lrarr","\\Leftrightarrow"),en("\\Mu","\\mathrm{M}"),en("\\natnums","\\mathbb{N}"),en("\\Nu","\\mathrm{N}"),en("\\Omicron","\\mathrm{O}"),en("\\plusmn","\\pm"),en("\\rarr","\\rightarrow"),en("\\rArr","\\Rightarrow"),en("\\Rarr","\\Rightarrow"),en("\\real","\\Re"),en("\\reals","\\mathbb{R}"),en("\\Reals","\\mathbb{R}"),en("\\Rho","\\mathrm{P}"),en("\\sdot","\\cdot"),en("\\sect","\\S"),en("\\spades","\\spadesuit"),en("\\sub","\\subset"),en("\\sube","\\subseteq"),en("\\supe","\\supseteq"),en("\\Tau","\\mathrm{T}"),en("\\thetasym","\\vartheta"),en("\\weierp","\\wp"),en("\\Zeta","\\mathrm{Z}"),en("\\argmin","\\DOTSB\\operatorname*{arg\\,min}"),en("\\argmax","\\DOTSB\\operatorname*{arg\\,max}"),en("\\plim","\\DOTSB\\mathop{\\operatorname{plim}}\\limits"),en("\\bra","\\mathinner{\\langle{#1}|}"),en("\\ket","\\mathinner{|{#1}\\rangle}"),en("\\braket","\\mathinner{\\langle{#1}\\rangle}"),en("\\Bra","\\left\\langle#1\\right|"),en("\\Ket","\\left|#1\\right\\rangle");const ro=e=>t=>{const r=t.consumeArg().tokens,n=t.consumeArg().tokens,o=t.consumeArg().tokens,s=t.consumeArg().tokens,i=t.macros.get("|"),a=t.macros.get("\\|");t.macros.beginGroup();const l=t=>r=>{e&&(r.macros.set("|",i),o.length&&r.macros.set("\\|",a));let s=t;if(!t&&o.length){"|"===r.future().text&&(r.popToken(),s=!0)}return{tokens:s?o:n,numArgs:0}};t.macros.set("|",l(!1)),o.length&&t.macros.set("\\|",l(!0));const c=t.consumeArg().tokens,h=t.expandTokens([...s,...c,...r]);return t.macros.endGroup(),{tokens:h.reverse(),numArgs:0}};en("\\bra@ket",ro(!1)),en("\\bra@set",ro(!0)),en("\\Braket","\\bra@ket{\\left\\langle}{\\,\\middle\\vert\\,}{\\,\\middle\\vert\\,}{\\right\\rangle}"),en("\\Set","\\bra@set{\\left\\{\\:}{\\;\\middle\\vert\\;}{\\;\\middle\\Vert\\;}{\\:\\right\\}}"),en("\\set","\\bra@set{\\{\\,}{\\mid}{}{\\,\\}}"),en("\\angln","{\\angl n}"),en("\\blue","\\textcolor{##6495ed}{#1}"),en("\\orange","\\textcolor{##ffa500}{#1}"),en("\\pink","\\textcolor{##ff00af}{#1}"),en("\\red","\\textcolor{##df0030}{#1}"),en("\\green","\\textcolor{##28ae7b}{#1}"),en("\\gray","\\textcolor{gray}{#1}"),en("\\purple","\\textcolor{##9d38bd}{#1}"),en("\\blueA","\\textcolor{##ccfaff}{#1}"),en("\\blueB","\\textcolor{##80f6ff}{#1}"),en("\\blueC","\\textcolor{##63d9ea}{#1}"),en("\\blueD","\\textcolor{##11accd}{#1}"),en("\\blueE","\\textcolor{##0c7f99}{#1}"),en("\\tealA","\\textcolor{##94fff5}{#1}"),en("\\tealB","\\textcolor{##26edd5}{#1}"),en("\\tealC","\\textcolor{##01d1c1}{#1}"),en("\\tealD","\\textcolor{##01a995}{#1}"),en("\\tealE","\\textcolor{##208170}{#1}"),en("\\greenA","\\textcolor{##b6ffb0}{#1}"),en("\\greenB","\\textcolor{##8af281}{#1}"),en("\\greenC","\\textcolor{##74cf70}{#1}"),en("\\greenD","\\textcolor{##1fab54}{#1}"),en("\\greenE","\\textcolor{##0d923f}{#1}"),en("\\goldA","\\textcolor{##ffd0a9}{#1}"),en("\\goldB","\\textcolor{##ffbb71}{#1}"),en("\\goldC","\\textcolor{##ff9c39}{#1}"),en("\\goldD","\\textcolor{##e07d10}{#1}"),en("\\goldE","\\textcolor{##a75a05}{#1}"),en("\\redA","\\textcolor{##fca9a9}{#1}"),en("\\redB","\\textcolor{##ff8482}{#1}"),en("\\redC","\\textcolor{##f9685d}{#1}"),en("\\redD","\\textcolor{##e84d39}{#1}"),en("\\redE","\\textcolor{##bc2612}{#1}"),en("\\maroonA","\\textcolor{##ffbde0}{#1}"),en("\\maroonB","\\textcolor{##ff92c6}{#1}"),en("\\maroonC","\\textcolor{##ed5fa6}{#1}"),en("\\maroonD","\\textcolor{##ca337c}{#1}"),en("\\maroonE","\\textcolor{##9e034e}{#1}"),en("\\purpleA","\\textcolor{##ddd7ff}{#1}"),en("\\purpleB","\\textcolor{##c6b9fc}{#1}"),en("\\purpleC","\\textcolor{##aa87ff}{#1}"),en("\\purpleD","\\textcolor{##7854ab}{#1}"),en("\\purpleE","\\textcolor{##543b78}{#1}"),en("\\mintA","\\textcolor{##f5f9e8}{#1}"),en("\\mintB","\\textcolor{##edf2df}{#1}"),en("\\mintC","\\textcolor{##e0e5cc}{#1}"),en("\\grayA","\\textcolor{##f6f7f7}{#1}"),en("\\grayB","\\textcolor{##f0f1f2}{#1}"),en("\\grayC","\\textcolor{##e3e5e6}{#1}"),en("\\grayD","\\textcolor{##d6d8da}{#1}"),en("\\grayE","\\textcolor{##babec2}{#1}"),en("\\grayF","\\textcolor{##888d93}{#1}"),en("\\grayG","\\textcolor{##626569}{#1}"),en("\\grayH","\\textcolor{##3b3e40}{#1}"),en("\\grayI","\\textcolor{##21242c}{#1}"),en("\\kaBlue","\\textcolor{##314453}{#1}"),en("\\kaGreen","\\textcolor{##71B307}{#1}");const no={"^":!0,_:!0,"\\limits":!0,"\\nolimits":!0};class oo{constructor(e,t,r){this.settings=void 0,this.expansionCount=void 0,this.lexer=void 0,this.macros=void 0,this.stack=void 0,this.mode=void 0,this.settings=t,this.expansionCount=0,this.feed(e),this.macros=new _n($n,t.macros),this.mode=r,this.stack=[]}feed(e){this.lexer=new Wn(e,this.settings)}switchMode(e){this.mode=e}beginGroup(){this.macros.beginGroup()}endGroup(){this.macros.endGroup()}endGroups(){this.macros.endGroups()}future(){return 0===this.stack.length&&this.pushToken(this.lexer.lex()),this.stack[this.stack.length-1]}popToken(){return this.future(),this.stack.pop()}pushToken(e){this.stack.push(e)}pushTokens(e){this.stack.push(...e)}scanArgument(e){let t,r,n;if(e){if(this.consumeSpaces(),"["!==this.future().text)return null;t=this.popToken();var o=this.consumeArg(["]"]);n=o.tokens,r=o.end}else{var s=this.consumeArg();n=s.tokens,t=s.start,r=s.end}return this.pushToken(new rn("EOF",r.loc)),this.pushTokens(n),new rn("",tn.range(t,r))}consumeSpaces(){for(;;){if(" "!==this.future().text)break;this.stack.pop()}}consumeArg(e){const t=[],r=e&&e.length>0;r||this.consumeSpaces();const o=this.future();let s,i=0,a=0;do{if(s=this.popToken(),t.push(s),"{"===s.text)++i;else if("}"===s.text){if(--i,-1===i)throw new n("Extra }",s)}else if("EOF"===s.text)throw new n("Unexpected end of input in a macro argument, expected '"+(e&&r?e[a]:"}")+"'",s);if(e&&r)if((0===i||1===i&&"{"===e[a])&&s.text===e[a]){if(++a,a===e.length){t.splice(-a,a);break}}else a=0}while(0!==i||r);return"{"===o.text&&"}"===t[t.length-1].text&&(t.pop(),t.shift()),t.reverse(),{tokens:t,start:o,end:s}}consumeArgs(e,t){if(t){if(t.length!==e+1)throw new n("The length of delimiters doesn't match the number of args!");const r=t[0];for(let e=0;ethis.settings.maxExpand)throw new n("Too many expansions: infinite loop or need to increase maxExpand setting")}expandOnce(e){const t=this.popToken(),r=t.text,o=t.noexpand?null:this._getExpansion(r);if(null==o||e&&o.unexpandable){if(e&&null==o&&"\\"===r[0]&&!this.isDefined(r))throw new n("Undefined control sequence: "+r);return this.pushToken(t),!1}this.countExpansion(1);let s=o.tokens;const i=this.consumeArgs(o.numArgs,o.delimiters);if(o.numArgs){s=s.slice();for(let e=s.length-1;e>=0;--e){let t=s[e];if("#"===t.text){if(0===e)throw new n("Incomplete placeholder at end of macro body",t);if(t=s[--e],"#"===t.text)s.splice(e+1,1);else{if(!/^[1-9]$/.test(t.text))throw new n("Not a valid argument number",t);s.splice(e,2,...i[+t.text-1])}}}}return this.pushTokens(s),s.length}expandAfterFuture(){return this.expandOnce(),this.future()}expandNextToken(){for(;;)if(!1===this.expandOnce()){const e=this.stack.pop();return e.treatAsRelax&&(e.text="\\relax"),e}}expandMacro(e){return this.macros.has(e)?this.expandTokens([new rn(e)]):void 0}expandTokens(e){const t=[],r=this.stack.length;for(this.pushTokens(e);this.stack.length>r;)if(!1===this.expandOnce(!0)){const e=this.stack.pop();e.treatAsRelax&&(e.noexpand=!1,e.treatAsRelax=!1),t.push(e)}return this.countExpansion(t.length),t}expandMacroAsText(e){const t=this.expandMacro(e);return t?t.map(e=>e.text).join(""):t}_getExpansion(e){const t=this.macros.get(e);if(null==t)return t;if(1===e.length){const t=this.lexer.catcodes[e];if(null!=t&&13!==t)return}const r="function"==typeof t?t(this):t;if("string"==typeof r){let e=0;if(r.includes("#")){const t=r.replace(/##/g,"");for(;t.includes("#"+(e+1));)++e}const t=new Wn(r,this.settings),n=[];let o=t.lex();for(;"EOF"!==o.text;)n.push(o),o=t.lex();n.reverse();return{tokens:n,numArgs:e}}return r}isDefined(e){return this.macros.has(e)||Object.prototype.hasOwnProperty.call(Vn,e)||Object.prototype.hasOwnProperty.call(ne.math,e)||Object.prototype.hasOwnProperty.call(ne.text,e)||Object.prototype.hasOwnProperty.call(no,e)}isExpandable(e){const t=this.macros.get(e);return null!=t?"string"==typeof t||"function"==typeof t||!t.unexpandable:Object.prototype.hasOwnProperty.call(Vn,e)&&!Vn[e].primitive}}const so=/^[\u208a\u208b\u208c\u208d\u208e\u2080\u2081\u2082\u2083\u2084\u2085\u2086\u2087\u2088\u2089\u2090\u2091\u2095\u1d62\u2c7c\u2096\u2097\u2098\u2099\u2092\u209a\u1d63\u209b\u209c\u1d64\u1d65\u2093\u1d66\u1d67\u1d68\u1d69\u1d6a]/,io=Object.freeze({"\u208a":"+","\u208b":"-","\u208c":"=","\u208d":"(","\u208e":")","\u2080":"0","\u2081":"1","\u2082":"2","\u2083":"3","\u2084":"4","\u2085":"5","\u2086":"6","\u2087":"7","\u2088":"8","\u2089":"9","\u2090":"a","\u2091":"e","\u2095":"h","\u1d62":"i","\u2c7c":"j","\u2096":"k","\u2097":"l","\u2098":"m","\u2099":"n","\u2092":"o","\u209a":"p","\u1d63":"r","\u209b":"s","\u209c":"t","\u1d64":"u","\u1d65":"v","\u2093":"x","\u1d66":"\u03b2","\u1d67":"\u03b3","\u1d68":"\u03c1","\u1d69":"\u03d5","\u1d6a":"\u03c7","\u207a":"+","\u207b":"-","\u207c":"=","\u207d":"(","\u207e":")","\u2070":"0","\xb9":"1","\xb2":"2","\xb3":"3","\u2074":"4","\u2075":"5","\u2076":"6","\u2077":"7","\u2078":"8","\u2079":"9","\u1d2c":"A","\u1d2e":"B","\u1d30":"D","\u1d31":"E","\u1d33":"G","\u1d34":"H","\u1d35":"I","\u1d36":"J","\u1d37":"K","\u1d38":"L","\u1d39":"M","\u1d3a":"N","\u1d3c":"O","\u1d3e":"P","\u1d3f":"R","\u1d40":"T","\u1d41":"U","\u2c7d":"V","\u1d42":"W","\u1d43":"a","\u1d47":"b","\u1d9c":"c","\u1d48":"d","\u1d49":"e","\u1da0":"f","\u1d4d":"g","\u02b0":"h","\u2071":"i","\u02b2":"j","\u1d4f":"k","\u02e1":"l","\u1d50":"m","\u207f":"n","\u1d52":"o","\u1d56":"p","\u02b3":"r","\u02e2":"s","\u1d57":"t","\u1d58":"u","\u1d5b":"v","\u02b7":"w","\u02e3":"x","\u02b8":"y","\u1dbb":"z","\u1d5d":"\u03b2","\u1d5e":"\u03b3","\u1d5f":"\u03b4","\u1d60":"\u03d5","\u1d61":"\u03c7","\u1dbf":"\u03b8"}),ao={"\u0301":{text:"\\'",math:"\\acute"},"\u0300":{text:"\\`",math:"\\grave"},"\u0308":{text:'\\"',math:"\\ddot"},"\u0303":{text:"\\~",math:"\\tilde"},"\u0304":{text:"\\=",math:"\\bar"},"\u0306":{text:"\\u",math:"\\breve"},"\u030c":{text:"\\v",math:"\\check"},"\u0302":{text:"\\^",math:"\\hat"},"\u0307":{text:"\\.",math:"\\dot"},"\u030a":{text:"\\r",math:"\\mathring"},"\u030b":{text:"\\H"},"\u0327":{text:"\\c"}},lo={"\xe1":"a\u0301","\xe0":"a\u0300","\xe4":"a\u0308","\u01df":"a\u0308\u0304","\xe3":"a\u0303","\u0101":"a\u0304","\u0103":"a\u0306","\u1eaf":"a\u0306\u0301","\u1eb1":"a\u0306\u0300","\u1eb5":"a\u0306\u0303","\u01ce":"a\u030c","\xe2":"a\u0302","\u1ea5":"a\u0302\u0301","\u1ea7":"a\u0302\u0300","\u1eab":"a\u0302\u0303","\u0227":"a\u0307","\u01e1":"a\u0307\u0304","\xe5":"a\u030a","\u01fb":"a\u030a\u0301","\u1e03":"b\u0307","\u0107":"c\u0301","\u1e09":"c\u0327\u0301","\u010d":"c\u030c","\u0109":"c\u0302","\u010b":"c\u0307","\xe7":"c\u0327","\u010f":"d\u030c","\u1e0b":"d\u0307","\u1e11":"d\u0327","\xe9":"e\u0301","\xe8":"e\u0300","\xeb":"e\u0308","\u1ebd":"e\u0303","\u0113":"e\u0304","\u1e17":"e\u0304\u0301","\u1e15":"e\u0304\u0300","\u0115":"e\u0306","\u1e1d":"e\u0327\u0306","\u011b":"e\u030c","\xea":"e\u0302","\u1ebf":"e\u0302\u0301","\u1ec1":"e\u0302\u0300","\u1ec5":"e\u0302\u0303","\u0117":"e\u0307","\u0229":"e\u0327","\u1e1f":"f\u0307","\u01f5":"g\u0301","\u1e21":"g\u0304","\u011f":"g\u0306","\u01e7":"g\u030c","\u011d":"g\u0302","\u0121":"g\u0307","\u0123":"g\u0327","\u1e27":"h\u0308","\u021f":"h\u030c","\u0125":"h\u0302","\u1e23":"h\u0307","\u1e29":"h\u0327","\xed":"i\u0301","\xec":"i\u0300","\xef":"i\u0308","\u1e2f":"i\u0308\u0301","\u0129":"i\u0303","\u012b":"i\u0304","\u012d":"i\u0306","\u01d0":"i\u030c","\xee":"i\u0302","\u01f0":"j\u030c","\u0135":"j\u0302","\u1e31":"k\u0301","\u01e9":"k\u030c","\u0137":"k\u0327","\u013a":"l\u0301","\u013e":"l\u030c","\u013c":"l\u0327","\u1e3f":"m\u0301","\u1e41":"m\u0307","\u0144":"n\u0301","\u01f9":"n\u0300","\xf1":"n\u0303","\u0148":"n\u030c","\u1e45":"n\u0307","\u0146":"n\u0327","\xf3":"o\u0301","\xf2":"o\u0300","\xf6":"o\u0308","\u022b":"o\u0308\u0304","\xf5":"o\u0303","\u1e4d":"o\u0303\u0301","\u1e4f":"o\u0303\u0308","\u022d":"o\u0303\u0304","\u014d":"o\u0304","\u1e53":"o\u0304\u0301","\u1e51":"o\u0304\u0300","\u014f":"o\u0306","\u01d2":"o\u030c","\xf4":"o\u0302","\u1ed1":"o\u0302\u0301","\u1ed3":"o\u0302\u0300","\u1ed7":"o\u0302\u0303","\u022f":"o\u0307","\u0231":"o\u0307\u0304","\u0151":"o\u030b","\u1e55":"p\u0301","\u1e57":"p\u0307","\u0155":"r\u0301","\u0159":"r\u030c","\u1e59":"r\u0307","\u0157":"r\u0327","\u015b":"s\u0301","\u1e65":"s\u0301\u0307","\u0161":"s\u030c","\u1e67":"s\u030c\u0307","\u015d":"s\u0302","\u1e61":"s\u0307","\u015f":"s\u0327","\u1e97":"t\u0308","\u0165":"t\u030c","\u1e6b":"t\u0307","\u0163":"t\u0327","\xfa":"u\u0301","\xf9":"u\u0300","\xfc":"u\u0308","\u01d8":"u\u0308\u0301","\u01dc":"u\u0308\u0300","\u01d6":"u\u0308\u0304","\u01da":"u\u0308\u030c","\u0169":"u\u0303","\u1e79":"u\u0303\u0301","\u016b":"u\u0304","\u1e7b":"u\u0304\u0308","\u016d":"u\u0306","\u01d4":"u\u030c","\xfb":"u\u0302","\u016f":"u\u030a","\u0171":"u\u030b","\u1e7d":"v\u0303","\u1e83":"w\u0301","\u1e81":"w\u0300","\u1e85":"w\u0308","\u0175":"w\u0302","\u1e87":"w\u0307","\u1e98":"w\u030a","\u1e8d":"x\u0308","\u1e8b":"x\u0307","\xfd":"y\u0301","\u1ef3":"y\u0300","\xff":"y\u0308","\u1ef9":"y\u0303","\u0233":"y\u0304","\u0177":"y\u0302","\u1e8f":"y\u0307","\u1e99":"y\u030a","\u017a":"z\u0301","\u017e":"z\u030c","\u1e91":"z\u0302","\u017c":"z\u0307","\xc1":"A\u0301","\xc0":"A\u0300","\xc4":"A\u0308","\u01de":"A\u0308\u0304","\xc3":"A\u0303","\u0100":"A\u0304","\u0102":"A\u0306","\u1eae":"A\u0306\u0301","\u1eb0":"A\u0306\u0300","\u1eb4":"A\u0306\u0303","\u01cd":"A\u030c","\xc2":"A\u0302","\u1ea4":"A\u0302\u0301","\u1ea6":"A\u0302\u0300","\u1eaa":"A\u0302\u0303","\u0226":"A\u0307","\u01e0":"A\u0307\u0304","\xc5":"A\u030a","\u01fa":"A\u030a\u0301","\u1e02":"B\u0307","\u0106":"C\u0301","\u1e08":"C\u0327\u0301","\u010c":"C\u030c","\u0108":"C\u0302","\u010a":"C\u0307","\xc7":"C\u0327","\u010e":"D\u030c","\u1e0a":"D\u0307","\u1e10":"D\u0327","\xc9":"E\u0301","\xc8":"E\u0300","\xcb":"E\u0308","\u1ebc":"E\u0303","\u0112":"E\u0304","\u1e16":"E\u0304\u0301","\u1e14":"E\u0304\u0300","\u0114":"E\u0306","\u1e1c":"E\u0327\u0306","\u011a":"E\u030c","\xca":"E\u0302","\u1ebe":"E\u0302\u0301","\u1ec0":"E\u0302\u0300","\u1ec4":"E\u0302\u0303","\u0116":"E\u0307","\u0228":"E\u0327","\u1e1e":"F\u0307","\u01f4":"G\u0301","\u1e20":"G\u0304","\u011e":"G\u0306","\u01e6":"G\u030c","\u011c":"G\u0302","\u0120":"G\u0307","\u0122":"G\u0327","\u1e26":"H\u0308","\u021e":"H\u030c","\u0124":"H\u0302","\u1e22":"H\u0307","\u1e28":"H\u0327","\xcd":"I\u0301","\xcc":"I\u0300","\xcf":"I\u0308","\u1e2e":"I\u0308\u0301","\u0128":"I\u0303","\u012a":"I\u0304","\u012c":"I\u0306","\u01cf":"I\u030c","\xce":"I\u0302","\u0130":"I\u0307","\u0134":"J\u0302","\u1e30":"K\u0301","\u01e8":"K\u030c","\u0136":"K\u0327","\u0139":"L\u0301","\u013d":"L\u030c","\u013b":"L\u0327","\u1e3e":"M\u0301","\u1e40":"M\u0307","\u0143":"N\u0301","\u01f8":"N\u0300","\xd1":"N\u0303","\u0147":"N\u030c","\u1e44":"N\u0307","\u0145":"N\u0327","\xd3":"O\u0301","\xd2":"O\u0300","\xd6":"O\u0308","\u022a":"O\u0308\u0304","\xd5":"O\u0303","\u1e4c":"O\u0303\u0301","\u1e4e":"O\u0303\u0308","\u022c":"O\u0303\u0304","\u014c":"O\u0304","\u1e52":"O\u0304\u0301","\u1e50":"O\u0304\u0300","\u014e":"O\u0306","\u01d1":"O\u030c","\xd4":"O\u0302","\u1ed0":"O\u0302\u0301","\u1ed2":"O\u0302\u0300","\u1ed6":"O\u0302\u0303","\u022e":"O\u0307","\u0230":"O\u0307\u0304","\u0150":"O\u030b","\u1e54":"P\u0301","\u1e56":"P\u0307","\u0154":"R\u0301","\u0158":"R\u030c","\u1e58":"R\u0307","\u0156":"R\u0327","\u015a":"S\u0301","\u1e64":"S\u0301\u0307","\u0160":"S\u030c","\u1e66":"S\u030c\u0307","\u015c":"S\u0302","\u1e60":"S\u0307","\u015e":"S\u0327","\u0164":"T\u030c","\u1e6a":"T\u0307","\u0162":"T\u0327","\xda":"U\u0301","\xd9":"U\u0300","\xdc":"U\u0308","\u01d7":"U\u0308\u0301","\u01db":"U\u0308\u0300","\u01d5":"U\u0308\u0304","\u01d9":"U\u0308\u030c","\u0168":"U\u0303","\u1e78":"U\u0303\u0301","\u016a":"U\u0304","\u1e7a":"U\u0304\u0308","\u016c":"U\u0306","\u01d3":"U\u030c","\xdb":"U\u0302","\u016e":"U\u030a","\u0170":"U\u030b","\u1e7c":"V\u0303","\u1e82":"W\u0301","\u1e80":"W\u0300","\u1e84":"W\u0308","\u0174":"W\u0302","\u1e86":"W\u0307","\u1e8c":"X\u0308","\u1e8a":"X\u0307","\xdd":"Y\u0301","\u1ef2":"Y\u0300","\u0178":"Y\u0308","\u1ef8":"Y\u0303","\u0232":"Y\u0304","\u0176":"Y\u0302","\u1e8e":"Y\u0307","\u0179":"Z\u0301","\u017d":"Z\u030c","\u1e90":"Z\u0302","\u017b":"Z\u0307","\u03ac":"\u03b1\u0301","\u1f70":"\u03b1\u0300","\u1fb1":"\u03b1\u0304","\u1fb0":"\u03b1\u0306","\u03ad":"\u03b5\u0301","\u1f72":"\u03b5\u0300","\u03ae":"\u03b7\u0301","\u1f74":"\u03b7\u0300","\u03af":"\u03b9\u0301","\u1f76":"\u03b9\u0300","\u03ca":"\u03b9\u0308","\u0390":"\u03b9\u0308\u0301","\u1fd2":"\u03b9\u0308\u0300","\u1fd1":"\u03b9\u0304","\u1fd0":"\u03b9\u0306","\u03cc":"\u03bf\u0301","\u1f78":"\u03bf\u0300","\u03cd":"\u03c5\u0301","\u1f7a":"\u03c5\u0300","\u03cb":"\u03c5\u0308","\u03b0":"\u03c5\u0308\u0301","\u1fe2":"\u03c5\u0308\u0300","\u1fe1":"\u03c5\u0304","\u1fe0":"\u03c5\u0306","\u03ce":"\u03c9\u0301","\u1f7c":"\u03c9\u0300","\u038e":"\u03a5\u0301","\u1fea":"\u03a5\u0300","\u03ab":"\u03a5\u0308","\u1fe9":"\u03a5\u0304","\u1fe8":"\u03a5\u0306","\u038f":"\u03a9\u0301","\u1ffa":"\u03a9\u0300"};class co{constructor(e,t){this.mode=void 0,this.gullet=void 0,this.settings=void 0,this.leftrightDepth=void 0,this.nextToken=void 0,this.mode="math",this.gullet=new oo(e,t,this.mode),this.settings=t,this.leftrightDepth=0,this.nextToken=null}expect(e,t){if(void 0===t&&(t=!0),this.fetch().text!==e)throw new n("Expected '"+e+"', got '"+this.fetch().text+"'",this.fetch());t&&this.consume()}consume(){this.nextToken=null}fetch(){return null==this.nextToken&&(this.nextToken=this.gullet.expandNextToken()),this.nextToken}switchMode(e){this.mode=e,this.gullet.switchMode(e)}parse(){this.settings.globalGroup||this.gullet.beginGroup(),this.settings.colorIsTextColor&&this.gullet.macros.set("\\color","\\textcolor");try{const e=this.parseExpression(!1);return this.expect("EOF"),this.settings.globalGroup||this.gullet.endGroup(),e}finally{this.gullet.endGroups()}}subparse(e){const t=this.nextToken;this.consume(),this.gullet.pushToken(new rn("}")),this.gullet.pushTokens(e);const r=this.parseExpression(!1);return this.expect("}"),this.nextToken=t,r}parseExpression(e,t){const r=[];for(;;){"math"===this.mode&&this.consumeSpaces();const n=this.fetch();if(co.endOfExpression.has(n.text))break;if(t&&n.text===t)break;if(e&&Vn[n.text]&&Vn[n.text].infix)break;const o=this.parseAtom(t);if(!o)break;"internal"!==o.type&&r.push(o)}return"text"===this.mode&&this.formLigatures(r),this.handleInfixNodes(r)}handleInfixNodes(e){let t,r=-1;for(let o=0;o=128))return null;this.settings.strict&&(T(t.charCodeAt(0))?"math"===this.mode&&Fe({strict:this.settings.strict,errorCode:"unicodeTextInMathMode",errorMsg:'Unicode text character "'+t[0]+'" used in math mode',report:!0,token:e}):Fe({strict:this.settings.strict,errorCode:"unknownSymbol",errorMsg:'Unrecognized Unicode character "'+t[0]+'" ('+t.charCodeAt(0)+")",report:!0,token:e})),o={type:"textord",mode:"text",loc:tn.range(e),text:t}}var s;if(this.consume(),r)for(let t=0;t1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function R(e){for(let t=0;t/gm),B=a(/\${[\w\W]*}/gm),W=a(/^data-[\-\w.\u00B7-\uFFFF]/),G=a(/^aria-[\-\w]+$/),Y=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),j=a(/^(?:\w+script|data):/i),X=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),q=a(/^html$/i),$=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var K=Object.freeze({__proto__:null,MUSTACHE_EXPR:H,ERB_EXPR:z,TMPLIT_EXPR:B,DATA_ATTR:W,ARIA_ATTR:G,IS_ALLOWED_URI:Y,IS_SCRIPT_OR_DATA:j,ATTR_WHITESPACE:X,DOCTYPE_NAME:q,CUSTOM_ELEMENT:$});const V=1,Z=3,J=7,Q=8,ee=9,te=function(){return"undefined"==typeof window?null:window};var ne=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:te();const o=e=>t(e);if(o.version="3.1.6",o.removed=[],!n||!n.document||n.document.nodeType!==ee)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:b,Element:R,NodeFilter:H,NamedNodeMap:z=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:B,DOMParser:W,trustedTypes:G}=n,j=R.prototype,X=C(j,"cloneNode"),$=C(j,"remove"),ne=C(j,"nextSibling"),oe=C(j,"childNodes"),re=C(j,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ie,ae="";const{implementation:le,createNodeIterator:ce,createDocumentFragment:se,getElementsByTagName:ue}=r,{importNode:me}=a;let pe={};o.isSupported="function"==typeof e&&"function"==typeof re&&le&&void 0!==le.createHTMLDocument;const{MUSTACHE_EXPR:fe,ERB_EXPR:de,TMPLIT_EXPR:he,DATA_ATTR:ge,ARIA_ATTR:Te,IS_SCRIPT_OR_DATA:ye,ATTR_WHITESPACE:Ee,CUSTOM_ELEMENT:_e}=K;let{IS_ALLOWED_URI:Ae}=K,Ne=null;const be=S({},[...L,...D,...v,...x,...M]);let Se=null;const Re=S({},[...I,...U,...P,...F]);let we=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ce=null,Le=null,De=!0,ve=!0,Oe=!1,xe=!0,ke=!1,Me=!0,Ie=!1,Ue=!1,Pe=!1,Fe=!1,He=!1,ze=!1,Be=!0,We=!1,Ge=!0,Ye=!1,je={},Xe=null;const qe=S({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let $e=null;const Ke=S({},["audio","video","img","source","image","track"]);let Ve=null;const Ze=S({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Je="http://www.w3.org/1998/Math/MathML",Qe="http://www.w3.org/2000/svg",et="http://www.w3.org/1999/xhtml";let tt=et,nt=!1,ot=null;const rt=S({},[Je,Qe,et],d);let it=null;const at=["application/xhtml+xml","text/html"];let lt=null,ct=null;const st=r.createElement("form"),ut=function(e){return e instanceof RegExp||e instanceof Function},mt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ct||ct!==e){if(e&&"object"==typeof e||(e={}),e=w(e),it=-1===at.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,lt="application/xhtml+xml"===it?d:f,Ne=E(e,"ALLOWED_TAGS")?S({},e.ALLOWED_TAGS,lt):be,Se=E(e,"ALLOWED_ATTR")?S({},e.ALLOWED_ATTR,lt):Re,ot=E(e,"ALLOWED_NAMESPACES")?S({},e.ALLOWED_NAMESPACES,d):rt,Ve=E(e,"ADD_URI_SAFE_ATTR")?S(w(Ze),e.ADD_URI_SAFE_ATTR,lt):Ze,$e=E(e,"ADD_DATA_URI_TAGS")?S(w(Ke),e.ADD_DATA_URI_TAGS,lt):Ke,Xe=E(e,"FORBID_CONTENTS")?S({},e.FORBID_CONTENTS,lt):qe,Ce=E(e,"FORBID_TAGS")?S({},e.FORBID_TAGS,lt):{},Le=E(e,"FORBID_ATTR")?S({},e.FORBID_ATTR,lt):{},je=!!E(e,"USE_PROFILES")&&e.USE_PROFILES,De=!1!==e.ALLOW_ARIA_ATTR,ve=!1!==e.ALLOW_DATA_ATTR,Oe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,xe=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ke=e.SAFE_FOR_TEMPLATES||!1,Me=!1!==e.SAFE_FOR_XML,Ie=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,He=e.RETURN_DOM_FRAGMENT||!1,ze=e.RETURN_TRUSTED_TYPE||!1,Pe=e.FORCE_BODY||!1,Be=!1!==e.SANITIZE_DOM,We=e.SANITIZE_NAMED_PROPS||!1,Ge=!1!==e.KEEP_CONTENT,Ye=e.IN_PLACE||!1,Ae=e.ALLOWED_URI_REGEXP||Y,tt=e.NAMESPACE||et,we=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ut(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(we.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ut(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(we.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(we.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),ke&&(ve=!1),He&&(Fe=!0),je&&(Ne=S({},M),Se=[],!0===je.html&&(S(Ne,L),S(Se,I)),!0===je.svg&&(S(Ne,D),S(Se,U),S(Se,F)),!0===je.svgFilters&&(S(Ne,v),S(Se,U),S(Se,F)),!0===je.mathMl&&(S(Ne,x),S(Se,P),S(Se,F))),e.ADD_TAGS&&(Ne===be&&(Ne=w(Ne)),S(Ne,e.ADD_TAGS,lt)),e.ADD_ATTR&&(Se===Re&&(Se=w(Se)),S(Se,e.ADD_ATTR,lt)),e.ADD_URI_SAFE_ATTR&&S(Ve,e.ADD_URI_SAFE_ATTR,lt),e.FORBID_CONTENTS&&(Xe===qe&&(Xe=w(Xe)),S(Xe,e.FORBID_CONTENTS,lt)),Ge&&(Ne["#text"]=!0),Ie&&S(Ne,["html","head","body"]),Ne.table&&(S(Ne,["tbody"]),delete Ce.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ie=e.TRUSTED_TYPES_POLICY,ae=ie.createHTML("")}else void 0===ie&&(ie=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(G,c)),null!==ie&&"string"==typeof ae&&(ae=ie.createHTML(""));i&&i(e),ct=e}},pt=S({},["mi","mo","mn","ms","mtext"]),ft=S({},["foreignobject","annotation-xml"]),dt=S({},["title","style","font","a","script"]),ht=S({},[...D,...v,...O]),gt=S({},[...x,...k]),Tt=function(e){p(o.removed,{element:e});try{re(e).removeChild(e)}catch(t){$(e)}},yt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Se[e])if(Fe||He)try{Tt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},Et=function(e){let t=null,n=null;if(Pe)e=""+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===it&&tt===et&&(e=''+e+"");const o=ie?ie.createHTML(e):e;if(tt===et)try{t=(new W).parseFromString(o,it)}catch(e){}if(!t||!t.documentElement){t=le.createDocument(tt,"template",null);try{t.documentElement.innerHTML=nt?ae:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),tt===et?ue.call(t,Ie?"html":"body")[0]:Ie?t.documentElement:i},_t=function(e){return ce.call(e.ownerDocument||e,e,H.SHOW_ELEMENT|H.SHOW_COMMENT|H.SHOW_TEXT|H.SHOW_PROCESSING_INSTRUCTION|H.SHOW_CDATA_SECTION,null)},At=function(e){return e instanceof B&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof z)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Nt=function(e){return"function"==typeof b&&e instanceof b},bt=function(e,t,n){pe[e]&&u(pe[e],(e=>{e.call(o,t,n,ct)}))},St=function(e){let t=null;if(bt("beforeSanitizeElements",e,null),At(e))return Tt(e),!0;const n=lt(e.nodeName);if(bt("uponSanitizeElement",e,{tagName:n,allowedTags:Ne}),e.hasChildNodes()&&!Nt(e.firstElementChild)&&_(/<[/\w]/g,e.innerHTML)&&_(/<[/\w]/g,e.textContent))return Tt(e),!0;if(e.nodeType===J)return Tt(e),!0;if(Me&&e.nodeType===Q&&_(/<[/\w]/g,e.data))return Tt(e),!0;if(!Ne[n]||Ce[n]){if(!Ce[n]&&wt(n)){if(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,n))return!1;if(we.tagNameCheck instanceof Function&&we.tagNameCheck(n))return!1}if(Ge&&!Xe[n]){const t=re(e)||e.parentNode,n=oe(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=X(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,ne(e))}}}return Tt(e),!0}return e instanceof R&&!function(e){let t=re(e);t&&t.tagName||(t={namespaceURI:tt,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!ot[e.namespaceURI]&&(e.namespaceURI===Qe?t.namespaceURI===et?"svg"===n:t.namespaceURI===Je?"svg"===n&&("annotation-xml"===o||pt[o]):Boolean(ht[n]):e.namespaceURI===Je?t.namespaceURI===et?"math"===n:t.namespaceURI===Qe?"math"===n&&ft[o]:Boolean(gt[n]):e.namespaceURI===et?!(t.namespaceURI===Qe&&!ft[o])&&!(t.namespaceURI===Je&&!pt[o])&&!gt[n]&&(dt[n]||!ht[n]):!("application/xhtml+xml"!==it||!ot[e.namespaceURI]))}(e)?(Tt(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!_(/<\/no(script|embed|frames)/i,e.innerHTML)?(ke&&e.nodeType===Z&&(t=e.textContent,u([fe,de,he],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),bt("afterSanitizeElements",e,null),!1):(Tt(e),!0)},Rt=function(e,t,n){if(Be&&("id"===t||"name"===t)&&(n in r||n in st))return!1;if(ve&&!Le[t]&&_(ge,t));else if(De&&_(Te,t));else if(!Se[t]||Le[t]){if(!(wt(e)&&(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,e)||we.tagNameCheck instanceof Function&&we.tagNameCheck(e))&&(we.attributeNameCheck instanceof RegExp&&_(we.attributeNameCheck,t)||we.attributeNameCheck instanceof Function&&we.attributeNameCheck(t))||"is"===t&&we.allowCustomizedBuiltInElements&&(we.tagNameCheck instanceof RegExp&&_(we.tagNameCheck,n)||we.tagNameCheck instanceof Function&&we.tagNameCheck(n))))return!1}else if(Ve[t]);else if(_(Ae,g(n,Ee,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==T(n,"data:")||!$e[e]){if(Oe&&!_(ye,g(n,Ee,"")));else if(n)return!1}else;return!0},wt=function(e){return"annotation-xml"!==e&&h(e,_e)},Ct=function(e){bt("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Se};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=lt(a);let p="value"===a?c:y(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,bt("uponSanitizeAttribute",e,n),p=n.attrValue,Me&&_(/((--!?|])>)|<\/(style|title)/i,p)){yt(a,e);continue}if(n.forceKeepAttr)continue;if(yt(a,e),!n.keepAttr)continue;if(!xe&&_(/\/>/i,p)){yt(a,e);continue}ke&&u([fe,de,he],(e=>{p=g(p,e," ")}));const f=lt(e.nodeName);if(Rt(f,s,p)){if(!We||"id"!==s&&"name"!==s||(yt(a,e),p="user-content-"+p),ie&&"object"==typeof G&&"function"==typeof G.getAttributeType)if(l);else switch(G.getAttributeType(f,s)){case"TrustedHTML":p=ie.createHTML(p);break;case"TrustedScriptURL":p=ie.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),At(e)?Tt(e):m(o.removed)}catch(e){}}}bt("afterSanitizeAttributes",e,null)},Lt=function e(t){let n=null;const o=_t(t);for(bt("beforeSanitizeShadowDOM",t,null);n=o.nextNode();)bt("uponSanitizeShadowNode",n,null),St(n)||(n.content instanceof s&&e(n.content),Ct(n));bt("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(nt=!e,nt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Nt(e)){if("function"!=typeof e.toString)throw A("toString is not a function");if("string"!=typeof(e=e.toString()))throw A("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Ue||mt(t),o.removed=[],"string"==typeof e&&(Ye=!1),Ye){if(e.nodeName){const t=lt(e.nodeName);if(!Ne[t]||Ce[t])throw A("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof b)n=Et("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===V&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Fe&&!ke&&!Ie&&-1===e.indexOf("<"))return ie&&ze?ie.createHTML(e):e;if(n=Et(e),!n)return Fe?null:ze?ae:""}n&&Pe&&Tt(n.firstChild);const c=_t(Ye?e:n);for(;i=c.nextNode();)St(i)||(i.content instanceof s&&Lt(i.content),Ct(i));if(Ye)return e;if(Fe){if(He)for(l=se.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Se.shadowroot||Se.shadowrootmode)&&(l=me.call(a,l,!0)),l}let m=Ie?n.outerHTML:n.innerHTML;return Ie&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&_(q,n.ownerDocument.doctype.name)&&(m="\n"+m),ke&&u([fe,de,he],(e=>{m=g(m,e," ")})),ie&&ze?ie.createHTML(m):m},o.setConfig=function(){mt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Ue=!0},o.clearConfig=function(){ct=null,Ue=!1},o.isValidAttribute=function(e,t,n){ct||mt({});const o=lt(e),r=lt(t);return Rt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(pe[e]=pe[e]||[],p(pe[e],t))},o.removeHook=function(e){if(pe[e])return m(pe[e])},o.removeHooks=function(e){pe[e]&&(pe[e]=[])},o.removeAllHooks=function(){pe={}},o}();return ne})); -//# sourceMappingURL=purify.min.js.map +/*! @license DOMPurify 3.4.16 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.16/LICENSE */ +(function(e,t){typeof exports==`object`&&typeof module<`u`?module.exports=t():typeof define==`function`&&define.amd?define([],t):(e=typeof globalThis<`u`?globalThis:e||self,e.DOMPurify=t())})(this,function(){"use strict";function e(e,t){this.v=e,this.k=t}function t(e,t){(t==null||t>e.length)&&(t=e.length);for(var n=0,r=Array(t);n`u`?null:T(BigInt.prototype.toString),pe=typeof Symbol>`u`?null:T(Symbol.prototype.toString),x=T(Object.prototype.hasOwnProperty),S=T(Object.prototype.toString),C=T(RegExp.prototype.test),w=E(TypeError);function T(e){return function(t){t instanceof RegExp&&(t.lastIndex=0);var n=[...arguments].slice(1);return g(e,t,n)}}function E(e){return function(){return te(e,[...arguments])}}function D(e,t){let n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:oe;if(l&&l(e,null),!v(t))return e;let r=t.length;for(;r--;){let i=t[r];if(typeof i==`string`){let e=n(i);e!==i&&(u(t)||(t[r]=e),i=e)}e[i]=!0}return e}function me(e){for(let t=0;t/g),Ae=m(/\${[\w\W]*/g),je=m(/^data-[\-\w.\u00B7-\uFFFF]+$/),Me=m(/^aria-[\-\w]+$/),Ne=m(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Pe=m(/^(?:\w+script|data):/i),Fe=m(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),Ie=m(/^html$/i),Le=m(/^[a-z][.\w]*(-[.\w]+)+$/i),Re=m(/<[/\w!]/g),ze=m(/<[/\w]/g),Be=m(/<\/no(script|embed|frames)/i),Ve=m(/\/>/i),A={element:1,attribute:2,text:3,cdataSection:4,entityReference:5,entityNode:6,processingInstruction:7,comment:8,document:9,documentType:10,documentFragment:11,notation:12},j=[`style`,`script`,`xmp`,`iframe`,`noembed`,`noframes`,`plaintext`,`noscript`],He=p(D({},j)),Ue=function(){let e={};return _(j,t=>{e[t]=m(RegExp(`])`,`i`))}),p(e)}(),We=function(){return typeof window>`u`?null:window},Ge=function(e,t){if(typeof e!=`object`||typeof e.createPolicy!=`function`)return null;let n=null,r=`data-tt-policy-suffix`;t&&t.hasAttribute(r)&&(n=t.getAttribute(r));let i=`dompurify`+(n?`#`+n:``);try{return e.createPolicy(i,{createHTML(e){return e},createScriptURL(e){return e}})}catch(e){return console.warn(`TrustedTypes policy `+i+` could not be created.`),null}},Ke=function(){return{afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},M=function(e,t,n,r){return x(e,t)&&v(e[t])?D(r.base?O(r.base):{},e[t],r.transform):n},qe=function(e,t,n){let r=x(e,t)?e[t]:void 0;return r&&typeof r==`object`?O(r):n()};function Je(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:We(),t=e=>Je(e);if(t.version=`3.4.16`,t.removed=[],!e||!e.document||e.document.nodeType!==A.document||!e.Element)return t.isSupported=!1,t;let n=e.document,r=n,i=r.currentScript;e.DocumentFragment;let a=e.HTMLTemplateElement,o=e.Node,s=e.Element,l=e.NodeFilter;e.NamedNodeMap===void 0&&(e.NamedNodeMap||e.MozNamedAttrMap),e.HTMLFormElement;let u=e.DOMParser,d=e.trustedTypes,f=s.prototype,ee=k(f,`cloneNode`),g=k(f,`remove`),te=k(f,`removeAttributeNode`),fe=k(f,`nextSibling`),y=k(f,`childNodes`),b=k(f,`parentNode`),pe=k(f,`shadowRoot`),S=k(f,`attributes`),T=o&&o.prototype?k(o.prototype,`nodeType`):null,E=o&&o.prototype?k(o.prototype,`nodeName`):null,me=o&&o.prototype?k(o.prototype,`ownerDocument`):null,j=function(e){return T?T(e):e.nodeType},Ye=function(e){return E?E(e):e.nodeName};if(typeof a==`function`){let e=n.createElement(`template`);e.content&&e.content.ownerDocument&&(n=e.content.ownerDocument)}let N,P=``,Xe,Ze=!1,Qe=0,$e=function(){if(Qe>0)throw w(`A configured TRUSTED_TYPES_POLICY callback (createHTML or createScriptURL) must not call DOMPurify.sanitize, as that causes infinite recursion. Do not pass a policy whose callbacks wrap DOMPurify as TRUSTED_TYPES_POLICY; see the "DOMPurify and Trusted Types" section of the README.`)},F=function(e){$e(),Qe++;try{return N.createHTML(e)}finally{Qe--}},et=function(e){$e(),Qe++;try{return N.createScriptURL(e)}finally{Qe--}},tt=function(){return Ze||(Xe=Ge(d,i),Ze=!0),Xe},nt=n,rt=nt.implementation,it=nt.createNodeIterator,at=nt.createDocumentFragment,ot=nt.getElementsByTagName,st=r.importNode,I=Ke();t.isSupported=typeof c==`function`&&typeof b==`function`&&rt&&rt.createHTMLDocument!==void 0;let ct=Oe,lt=ke,ut=Ae,dt=je,ft=Me,pt=Pe,mt=Fe,ht=Le,gt=Ne,L=null,_t=D({},[..._e,...ve,...ye,...xe,...Ce]),R=null,vt=D({},[...we,...Te,...Ee,...De]),z=Object.seal(h(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),yt=null,bt=null,B=Object.seal(h(null,{tagCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeCheck:{writable:!0,configurable:!1,enumerable:!0,value:null}})),xt=!0,St=!0,Ct=!1,wt=!0,V=!1,H=!0,U=!1,Tt=!1,Et=null,Dt=null,Ot=!1,W=!1,kt=!1,At=!1,jt=!0,Mt=!1,Nt=`user-content-`,Pt=!0,Ft=!1,G={},K=null,It=D({},/* @__PURE__ */ `annotation-xml.audio.colgroup.desc.foreignobject.head.iframe.math.mi.mn.mo.ms.mtext.noembed.noframes.noscript.plaintext.script.selectedcontent.style.svg.template.thead.title.video.xmp`.split(`.`)),Lt=null,Rt=D({},[`audio`,`video`,`img`,`source`,`image`,`track`]),zt=null,Bt=D({},[`alt`,`class`,`for`,`id`,`label`,`name`,`pattern`,`placeholder`,`role`,`summary`,`title`,`value`,`style`,`xmlns`]),Vt=`http://www.w3.org/1998/Math/MathML`,Ht=`http://www.w3.org/2000/svg`,q=`http://www.w3.org/1999/xhtml`,J=q,Ut=!1,Wt=null,Gt=D({},[Vt,Ht,q],se),Kt=p([`mi`,`mo`,`mn`,`ms`,`mtext`]),qt=D({},Kt),Jt=p([`annotation-xml`]),Yt=D({},Jt),Xt=D({},[`title`,`style`,`font`,`a`,`script`]),Zt=null,Qt=[`application/xhtml+xml`,`text/html`],Y=null,$t=null,en=n.createElement(`form`),tn=function(e){return e instanceof RegExp||e instanceof Function},nn=function(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if($t&&$t===e)return;(!e||typeof e!=`object`)&&(e={}),e=O(e),Zt=Qt.indexOf(e.PARSER_MEDIA_TYPE)===-1?`text/html`:e.PARSER_MEDIA_TYPE,Y=Zt===`application/xhtml+xml`?se:oe,L=M(e,`ALLOWED_TAGS`,_t,{transform:Y}),R=M(e,`ALLOWED_ATTR`,vt,{transform:Y}),Wt=M(e,`ALLOWED_NAMESPACES`,Gt,{transform:se}),zt=M(e,`ADD_URI_SAFE_ATTR`,Bt,{transform:Y,base:Bt}),Lt=M(e,`ADD_DATA_URI_TAGS`,Rt,{transform:Y,base:Rt}),K=M(e,`FORBID_CONTENTS`,It,{transform:Y}),yt=M(e,`FORBID_TAGS`,O({}),{transform:Y}),bt=M(e,`FORBID_ATTR`,O({}),{transform:Y}),G=x(e,`USE_PROFILES`)?e.USE_PROFILES&&typeof e.USE_PROFILES==`object`?O(e.USE_PROFILES):e.USE_PROFILES:!1,xt=e.ALLOW_ARIA_ATTR!==!1,St=e.ALLOW_DATA_ATTR!==!1,Ct=e.ALLOW_UNKNOWN_PROTOCOLS||!1,wt=e.ALLOW_SELF_CLOSE_IN_ATTR!==!1,V=e.SAFE_FOR_TEMPLATES||!1,H=e.SAFE_FOR_XML!==!1,U=e.WHOLE_DOCUMENT||!1,W=e.RETURN_DOM||!1,kt=e.RETURN_DOM_FRAGMENT||!1,At=e.RETURN_TRUSTED_TYPE||!1,Ot=e.FORCE_BODY||!1,jt=e.SANITIZE_DOM!==!1,Mt=e.SANITIZE_NAMED_PROPS||!1,Pt=e.KEEP_CONTENT!==!1,Ft=e.IN_PLACE||!1,gt=ge(e.ALLOWED_URI_REGEXP)?e.ALLOWED_URI_REGEXP:Ne,J=typeof e.NAMESPACE==`string`?e.NAMESPACE:q,qt=qe(e,`MATHML_TEXT_INTEGRATION_POINTS`,()=>D({},Kt)),Yt=qe(e,`HTML_INTEGRATION_POINTS`,()=>D({},Jt));let t=qe(e,`CUSTOM_ELEMENT_HANDLING`,()=>h(null));if(z=h(null),x(t,`tagNameCheck`)&&tn(t.tagNameCheck)&&(z.tagNameCheck=t.tagNameCheck),x(t,`attributeNameCheck`)&&tn(t.attributeNameCheck)&&(z.attributeNameCheck=t.attributeNameCheck),x(t,`allowCustomizedBuiltInElements`)&&typeof t.allowCustomizedBuiltInElements==`boolean`&&(z.allowCustomizedBuiltInElements=t.allowCustomizedBuiltInElements),m(z),V&&(St=!1),kt&&(W=!0),G&&(L=D({},Ce),R=h(null),G.html===!0&&(D(L,_e),D(R,we)),G.svg===!0&&(D(L,ve),D(R,Te),D(R,De)),G.svgFilters===!0&&(D(L,ye),D(R,Te),D(R,De)),G.mathMl===!0&&(D(L,xe),D(R,Ee),D(R,De))),B.tagCheck=null,B.attributeCheck=null,x(e,`ADD_TAGS`)&&(typeof e.ADD_TAGS==`function`?B.tagCheck=e.ADD_TAGS:v(e.ADD_TAGS)&&(L===_t&&(L=O(L)),D(L,e.ADD_TAGS,Y))),x(e,`ADD_ATTR`)&&(typeof e.ADD_ATTR==`function`?B.attributeCheck=e.ADD_ATTR:v(e.ADD_ATTR)&&(R===vt&&(R=O(R)),D(R,e.ADD_ATTR,Y))),x(e,`ADD_FORBID_CONTENTS`)&&v(e.ADD_FORBID_CONTENTS)&&(K===It&&(K=O(K)),D(K,e.ADD_FORBID_CONTENTS,Y)),Pt&&(L[`#text`]=!0),U&&D(L,[`html`,`head`,`body`]),L.table&&(D(L,[`tbody`]),delete yt.tbody),e.TRUSTED_TYPES_POLICY){if(typeof e.TRUSTED_TYPES_POLICY.createHTML!=`function`)throw w(`TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.`);if(typeof e.TRUSTED_TYPES_POLICY.createScriptURL!=`function`)throw w(`TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.`);let t=N;N=e.TRUSTED_TYPES_POLICY;try{P=F(``)}catch(e){throw N=t,e}}else e.TRUSTED_TYPES_POLICY===null?(N=void 0,P=``):(N===void 0&&(N=tt()),N&&typeof P==`string`&&(P=F(``)));p&&p(e),$t=e},rn=D({},[...ve,...ye,...be]),an=D({},[...xe,...Se]),on=function(e,t,n){return t.namespaceURI===q?e===`svg`:t.namespaceURI===Vt?e===`svg`&&(n===`annotation-xml`||qt[n]):!!rn[e]},sn=function(e,t,n){return t.namespaceURI===q?e===`math`:t.namespaceURI===Ht?e===`math`&&Yt[n]:!!an[e]},cn=function(e,t,n){return t.namespaceURI===Ht&&!Yt[n]||t.namespaceURI===Vt&&!qt[n]?!1:!an[e]&&(Xt[e]||!rn[e])},ln=function(e){let t=b(e);(!t||!t.tagName)&&(t={namespaceURI:J,tagName:`template`});let n=oe(e.tagName),r=oe(t.tagName);return Wt[e.namespaceURI]?e.namespaceURI===Ht?on(n,t,r):e.namespaceURI===Vt?sn(n,t,r):e.namespaceURI===q?cn(n,t,r):!!(Zt===`application/xhtml+xml`&&Wt[e.namespaceURI]):!1},X=function(e){ie(t.removed,{element:e});try{b(e).removeChild(e)}catch(t){if(g(e),!b(e))throw w(`a node selected for removal could not be detached from its tree and cannot be safely returned; refusing to sanitize in place`)}},un=function(e,t,n){try{te(e,t)}catch(t){try{e.removeAttribute(n)}catch(e){}}},dn=function(e){pn(e);let t=y(e);if(t){let e=[];_(t,t=>{ie(e,t)}),_(e,e=>{try{g(e)}catch(e){}})}let n=S(e);if(n)for(let t=n.length-1;t>=0;--t){let r=n[t],i=r&&r.name;typeof i==`string`&&un(e,r,i)}},Z=function(e,n,r){if(!r)try{r=n.getAttributeNode(e)}catch(e){r=null}ie(t.removed,{attribute:r||null,from:n});try{r?te(n,r):n.removeAttribute(e)}catch(t){try{n.removeAttribute(e)}catch(e){}}if(e===`is`){if(W||kt)try{X(n)}catch(e){}else try{n.setAttribute(e,``)}catch(e){}}},fn=function(e){let t=S(e);if(t)for(let n=t.length-1;n>=0;--n){let r=t[n],i=r&&r.name;typeof i!=`string`||R[Y(i)]||un(e,r,i)}},pn=function(e){let t=[e];for(;t.length>0;){let e=t.pop();j(e)===A.element&&fn(e);let n=y(e);if(n)for(let e=n.length-1;e>=0;--e)t.push(n[e])}},mn=function(e,t){return H?e===`patchsrc`||e===`for`&&t!==`label`&&t!==`output`:!1},hn=function(e){if(!H)return;let t=[e];for(;t.length>0;){let e=t.pop(),n=j(e);if(n===A.processingInstruction||n===A.comment&&C(ze,e.data)){try{g(e)}catch(e){}continue}if(n===A.element){let t=e,n=Y(Ye(e));try{t.hasAttribute&&t.hasAttribute(`patchsrc`)&&t.removeAttribute(`patchsrc`),t.hasAttribute&&t.hasAttribute(`for`)&&mn(`for`,n)&&t.removeAttribute(`for`)}catch(e){}}let r=y(e);if(r)for(let e=r.length-1;e>=0;--e)t.push(r[e])}},gn=function(e){let t=null,r=null;if(Ot)e=``+e;else{let t=ce(e,/^[\r\n\t ]+/);r=t&&t[0]}Zt===`application/xhtml+xml`&&J===q&&(e=``+e+``);let i=N?F(e):e;if(J===q)try{t=new u().parseFromString(i,Zt)}catch(e){}if(!t||!t.documentElement){t=rt.createDocument(J,`template`,null);try{t.documentElement.innerHTML=Ut?P:i}catch(e){}}let a=t.body||t.documentElement;return e&&r&&a.insertBefore(n.createTextNode(r),a.childNodes[0]||null),J===q?ot.call(t,U?`html`:`body`)[0]:U?t.documentElement:a},_n=function(e){let t=me?me(e):e.ownerDocument;return it.call(t||e,e,l.SHOW_ELEMENT|l.SHOW_COMMENT|l.SHOW_TEXT|l.SHOW_PROCESSING_INSTRUCTION|l.SHOW_CDATA_SECTION,null)},vn=function(e){return e=le(e,ct,` `),e=le(e,lt,` `),e=le(e,ut,` `),e},yn=function(e){var t;e.normalize();let n=me?me(e):e.ownerDocument,r=it.call(n||e,e,l.SHOW_TEXT|l.SHOW_COMMENT|l.SHOW_CDATA_SECTION|l.SHOW_PROCESSING_INSTRUCTION,null),i=r.nextNode();for(;i;)i.data=vn(i.data),i=r.nextNode();let a=(t=e.querySelectorAll)==null?void 0:t.call(e,`template`);a&&_(a,e=>{xn(e.content)&&yn(e.content)})},bn=function(e){let t=E?E(e):null;return typeof t!=`string`||Y(t)!==`form`?!1:typeof e.nodeName!=`string`||typeof e.textContent!=`string`||typeof e.removeChild!=`function`||e.attributes!==S(e)||typeof e.removeAttribute!=`function`||typeof e.removeAttributeNode!=`function`||typeof e.getAttributeNode!=`function`||typeof e.setAttribute!=`function`||typeof e.namespaceURI!=`string`||typeof e.insertBefore!=`function`||typeof e.hasChildNodes!=`function`||e.nodeType!==T(e)||e.childNodes!==y(e)},xn=function(e){if(!T||typeof e!=`object`||!e)return!1;try{return T(e)===A.documentFragment}catch(e){return!1}},Sn=function(e){if(!T||typeof e!=`object`||!e)return!1;try{return typeof T(e)==`number`}catch(e){return!1}};function Q(e,n,r){e.length!==0&&_(e,e=>{e.call(t,n,r,$t)})}let Cn=function(e,t){return!!(H&&e.hasChildNodes()&&!Sn(e.firstElementChild)&&C(Re,e.textContent)&&C(Re,e.innerHTML)||H&&e.namespaceURI===q&&He[t]&&(Sn(e.firstElementChild)||typeof e.textContent==`string`&&C(Ue[t],e.textContent))||e.nodeType===A.processingInstruction||H&&e.nodeType===A.comment&&C(ze,e.data))},wn=function(e,t){return e instanceof RegExp?C(e,t):e instanceof Function&&!!e(t,...[...arguments].slice(2))},Tn=function(e,t,n){if(!yt[t]&&An(t)&&wn(z.tagNameCheck,t))return!1;if(Pt&&!K[t]){let t=b(e),r=y(e);if(r&&t){let i=r.length;for(let a=i-1;a>=0;--a){let i=e===n?ee(r[a],!0):r[a];t.insertBefore(i,fe(e))}}}return X(e),!0},En=function(e,t,n,r){return e.length===0?t:t===n||t===r?O(t):t},$=function(e,t){return e===t||b(e)!==null?!1:(Ft&&pn(e),!0)},Dn=function(e,n){if(Q(I.beforeSanitizeElements,e,null),$(e,n))return!0;if(bn(e))return X(e),!0;let r=Y(Ye(e));if(L=En(I.uponSanitizeElement,L,_t,Et),Q(I.uponSanitizeElement,e,{tagName:r,allowedTags:L}),$(e,n))return!0;if(Cn(e,r))return X(e),!0;if(yt[r]||!(B.tagCheck instanceof Function&&B.tagCheck(r))&&!L[r]){let t=Tn(e,r,n);return t===!1&&(Q(I.afterSanitizeElements,e,null),$(e,n))?!0:t}if(j(e)===A.element&&!ln(e)||(r===`noscript`||r===`noembed`||r===`noframes`)&&C(Be,e.innerHTML))return X(e),!0;if(V&&e.nodeType===A.text){let n=vn(e.textContent);e.textContent!==n&&(ie(t.removed,{element:e.cloneNode()}),e.textContent=n)}return Q(I.afterSanitizeElements,e,null),$(e,n)},On=function(e,t,r){if(bt[t]||mn(t,e)||jt&&(t===`id`||t===`name`)&&(r in n||r in en))return!1;let i=R[t]||B.attributeCheck instanceof Function&&B.attributeCheck(t,e);return St&&C(dt,t)||xt&&C(ft,t)?!0:i?zt[t]||C(gt,le(r,mt,``))||(t===`src`||t===`xlink:href`||t===`href`)&&e!==`script`&&ue(r,`data:`)===0&&Lt[e]||Ct&&!C(pt,le(r,mt,``))?!0:!r:An(e)&&wn(z.tagNameCheck,e)&&wn(z.attributeNameCheck,t,e)||t===`is`&&z.allowCustomizedBuiltInElements&&wn(z.tagNameCheck,r)},kn=D({},[`annotation-xml`,`color-profile`,`font-face`,`font-face-format`,`font-face-name`,`font-face-src`,`font-face-uri`,`missing-glyph`]),An=function(e){return!kn[oe(e)]&&C(ht,e)},jn=function(e,t,n,r){if(N&&typeof d==`object`&&typeof d.getAttributeType==`function`&&!n)switch(d.getAttributeType(e,t)){case`TrustedHTML`:return F(r);case`TrustedScriptURL`:return et(r)}return r},Mn=function(e,t,n,r){try{return n?e.setAttributeNS(n,t,r):e.setAttribute(t,r),!bn(e)||(X(e),!1)}catch(n){return Z(t,e),!1}},Nn=function(e,n){if(Q(I.beforeSanitizeAttributes,e,null),$(e,n))return;let r=e.attributes;if(!r||bn(e))return;R=En(I.uponSanitizeAttribute,R,vt,Dt);let i={attrName:``,attrValue:``,keepAttr:!0,allowedAttributes:R,forceKeepAttr:void 0},a=r.length,o=Y(e.nodeName);for(;a--;){let n=r[a],s=n.name,c=n.namespaceURI,l=n.value,u=Y(s),d=l,f=s===`value`?d:de(d),p=!1;if(i.attrName=u,i.attrValue=f,i.keepAttr=!0,i.forceKeepAttr=void 0,Q(I.uponSanitizeAttribute,e,i),f=i.attrValue,Mt&&(u===`id`||u===`name`)&&ue(f,Nt)!==0&&(Z(s,e,n),f=Nt+f,p=!0),H&&C(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i,f)){Z(s,e,n);continue}if(u===`attributename`&&ce(f,`href`)){Z(s,e,n);continue}if(!i.forceKeepAttr){if(!i.keepAttr){Z(s,e,n);continue}if(!wt&&C(Ve,f)){Z(s,e,n);continue}if(V&&(f=vn(f)),!On(o,u,f)){Z(s,e,n);continue}f=jn(o,u,c,f),f!==d&&Mn(e,s,c,f)&&p&&re(t.removed)}}Q(I.afterSanitizeAttributes,e,null),$(e,n)},Pn=function(e){let t=null,n=_n(e);for(Q(I.beforeSanitizeShadowDOM,e,null);t=n.nextNode();)if(Q(I.uponSanitizeShadowNode,t,null),Dn(t,e),Nn(t,e),xn(t.content)&&Pn(t.content),j(t)===A.element){let e=pe(t);xn(e)&&(Fn(e),Pn(e))}Q(I.afterSanitizeShadowDOM,e,null)},Fn=function(e){let t=[{node:e,shadow:null}];for(;t.length>0;){let e=t.pop();if(e.shadow){Pn(e.shadow);continue}let n=e.node,r=j(n)===A.element,i=y(n);if(i)for(let e=i.length-1;e>=0;--e)t.push({node:i[e],shadow:null});if(r){let e=E?E(n):null;if(typeof e==`string`&&Y(e)===`template`){let e=n.content;xn(e)&&t.push({node:e,shadow:null})}}if(r){let e=pe(n);xn(e)&&t.push({node:null,shadow:e},{node:e,shadow:null})}}};return t.sanitize=function(e){let n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},i=null,a=null,o=null,s=null;if(Ut=!e,Ut&&(e=``),typeof e!=`string`&&!Sn(e)&&(e=he(e),typeof e!=`string`))throw w(`dirty is not a string, aborting`);if(!t.isSupported)return e;Tt?(L=Et,R=Dt):nn(n),(I.uponSanitizeElement.length>0||I.uponSanitizeAttribute.length>0)&&(L=O(L)),I.uponSanitizeAttribute.length>0&&(R=O(R)),t.removed=[];let c=Ft&&typeof e!=`string`&&Sn(e);if(c){hn(e);let t=Ye(e);if(typeof t==`string`){let n=Y(t);if(!L[n]||yt[n])throw dn(e),w(`root node is forbidden and cannot be sanitized in-place`)}if(bn(e))throw dn(e),w(`root node is clobbered and cannot be sanitized in-place`);try{Fn(e)}catch(t){throw dn(e),t}}else if(Sn(e))i=gn(``),a=i.ownerDocument.importNode(e,!0),a.nodeType===A.element&&a.nodeName===`BODY`||a.nodeName===`HTML`?i=a:i.appendChild(a),Fn(i);else{if(!W&&!V&&!U&&e.indexOf(`<`)===-1)return N&&At?F(e):e;if(i=gn(e),!i)return W?null:At?P:``}i&&Ot&&X(i.firstChild);let l=c?e:i;try{let e=_n(l);for(;o=e.nextNode();)Dn(o,l),Nn(o,l),xn(o.content)&&Pn(o.content)}catch(n){throw c&&(dn(e),_(t.removed,e=>{e.element&&pn(e.element)})),n}if(c){let n=!1;if(_(t.removed,t=>{t.element&&(t.element===e&&(n=!0),pn(t.element))}),n)throw w(`a node selected for removal could not be safely returned; refusing to sanitize in place`);return V&&yn(e),e}if(W){if(V&&yn(i),kt)for(s=at.call(i.ownerDocument);i.firstChild;)s.appendChild(i.firstChild);else s=i;return(R.shadowroot||R.shadowrootmode)&&(s=st.call(r,s,!0)),s}let u=U?i.outerHTML:i.innerHTML;return U&&L[`!doctype`]&&i.ownerDocument&&i.ownerDocument.doctype&&i.ownerDocument.doctype.name&&C(Ie,i.ownerDocument.doctype.name)&&(u=` +`+u),V&&(u=vn(u)),N&&At?F(u):u},t.setConfig=function(){let e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};nn(e),Tt=!0,Et=L,Dt=R},t.clearConfig=function(){$t=null,Tt=!1,Et=null,Dt=null,N=Xe,P=``},t.isValidAttribute=function(e,t,n){$t||nn({});let r=Y(e),i=Y(t);return On(r,i,n)},t.addHook=function(e,t){typeof t==`function`&&x(I,e)&&ie(I[e],t)},t.removeHook=function(e,t){if(x(I,e)){if(t!==void 0){let n=ne(I[e],t);return n===-1?void 0:ae(I[e],n,1)[0]}return re(I[e])}},t.removeHooks=function(e){x(I,e)&&(I[e]=[])},t.removeAllHooks=function(){I=Ke()},t}return Je()}); +//# sourceMappingURL=purify.min.js.map \ No newline at end of file diff --git a/docs/index.html b/docs/index.html index aed9b16..7b7c22d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2690,8 +2690,8 @@

Everything you need, nothing to set up

Saves itself

-

The moment a conversion finishes, the .md file is in Downloads. No - save dialog, no export button. An existing file is never overwritten.

+

The moment a conversion finishes, the .md file is in Downloads, or in another + folder you pick in Settings. No save dialog, no export button. An existing file is never overwritten.

@@ -2986,7 +2986,7 @@

Linux

a("https://github.com/shift-labs-ai/markit", "Markit") + sep + a("https://huggingface.co/zai-org/GLM-OCR", "GLM-OCR") + and + a("https://github.com/ggml-org/llama.cpp", "llama.cpp"); }; - var ME = a("https://github.com/AbdoslamB", "Abdoslam Baabbad"); + var ME = a("https://abdoslamb.github.io/", "Abdoslam Baabbad"); var SUMS = REPO + "/releases"; var T = { @@ -3011,7 +3011,7 @@

Linux

"demo.kicker": "In action", "demo.title": "See the real app", "demo.lead": "A real recording of InkDoc: add a file, watch it convert, read the preview.", "demo.alt": "InkDoc recording: a file is added, converted, and the Markdown preview appears", "feat.kicker": "Features", "feat.title": "Everything you need, nothing to set up", - "feat.1.t": "Saves itself", "feat.1.b": "The moment a conversion finishes, the .md file is in Downloads. No save dialog, no export button. An existing file is never overwritten.", + "feat.1.t": "Saves itself", "feat.1.b": "The moment a conversion finishes, the .md file is in Downloads, or in another folder you pick in Settings. No save dialog, no export button. An existing file is never overwritten.", "feat.1.folder": "Downloads", "feat.1.new": "new", "feat.2.t": "Preview and edit", "feat.2.b": "Read the rendered result or the raw Markdown, fix anything, then copy it.", "feat.3.t": "Missing-text check", "feat.3.b": "For PDFs, InkDoc compares the result with the PDF's own text and tells you when pages or tables went missing.", @@ -3054,7 +3054,7 @@

Linux

"demo.kicker": "En acción", "demo.title": "Mira la app real", "demo.lead": "Una grabación real de InkDoc: añades un archivo, se convierte y lees la vista previa.", "demo.alt": "Grabación de InkDoc: se añade un archivo, se convierte y aparece la vista previa del Markdown", "feat.kicker": "Funciones", "feat.title": "Todo lo que necesitas, sin configurar nada", - "feat.1.t": "Se guarda solo", "feat.1.b": "En cuanto termina la conversión, el archivo .md está en Descargas. Sin cuadro de guardar ni botón de exportar. Nunca sobrescribe un archivo existente.", + "feat.1.t": "Se guarda solo", "feat.1.b": "En cuanto termina la conversión, el archivo .md está en Descargas, o en otra carpeta que elijas en Ajustes. Sin cuadro de guardar ni botón de exportar. Nunca sobrescribe un archivo existente.", "feat.1.folder": "Descargas", "feat.1.new": "nuevo", "feat.2.t": "Vista previa y edición", "feat.2.b": "Lee el resultado formateado o el Markdown sin procesar, corrige lo que quieras y cópialo.", "feat.3.t": "Detecta texto que falta", "feat.3.b": "En los PDF, InkDoc compara el resultado con el texto del propio PDF y te avisa si faltan páginas o tablas.", @@ -3097,7 +3097,7 @@

Linux

"demo.kicker": "عملياً", "demo.title": "شاهد التطبيق الحقيقي", "demo.lead": "تسجيل حقيقي لـ InkDoc: تضيف ملفاً، فيُحوَّل، ثم تقرأ المعاينة.", "demo.alt": "تسجيل لـ InkDoc: إضافة ملف وتحويله وظهور معاينة Markdown", "feat.kicker": "المزايا", "feat.title": "كل ما تحتاجه، بلا أي إعداد", - "feat.1.t": "يُحفظ تلقائياً", "feat.1.b": "بمجرد انتهاء التحويل يصبح ملف .md في التنزيلات. بلا نافذة حفظ ولا زر تصدير، ولا يكتب فوق أي ملف موجود.", + "feat.1.t": "يُحفظ تلقائياً", "feat.1.b": "بمجرد انتهاء التحويل يصبح ملف .md في التنزيلات، أو في مجلد آخر تختاره من الإعدادات. بلا نافذة حفظ ولا زر تصدير، ولا يكتب فوق أي ملف موجود.", "feat.1.folder": "التنزيلات", "feat.1.new": "جديد", "feat.2.t": "معاينة وتحرير", "feat.2.b": "اقرأ النتيجة المنسّقة أو نص Markdown الخام، وصحّح ما تريد، ثم انسخه.", "feat.3.t": "كشف النص المفقود", "feat.3.b": "في ملفات PDF يقارن InkDoc النتيجة بنص الملف نفسه، وينبّهك إذا سقطت صفحات أو جداول.", @@ -3163,7 +3163,7 @@

Linux

"feat.kicker": "Funktionen", "feat.title": "Alles, was du brauchst – ohne Einrichtung", "feat.1.t": "Speichert sich selbst", - "feat.1.b": "Sobald die Umwandlung fertig ist, liegt die .md-Datei im Download-Ordner. Kein Speichern-Dialog, kein Export-Button. Vorhandene Dateien werden nie überschrieben.", + "feat.1.b": "Sobald die Umwandlung fertig ist, liegt die .md-Datei im Download-Ordner oder in einem anderen Ordner, den du in den Einstellungen wählst. Kein Speichern-Dialog, kein Export-Button. Vorhandene Dateien werden nie überschrieben.", "feat.1.folder": "Downloads", "feat.1.new": "neu", "feat.2.t": "Vorschau und Bearbeitung", @@ -3254,7 +3254,7 @@

Linux

"feat.kicker": "Fonctionnalités", "feat.title": "Tout ce qu’il vous faut, sans rien configurer", "feat.1.t": "Enregistrement automatique", - "feat.1.b": "Dès la fin de la conversion, le fichier .md se trouve dans Téléchargements. Pas de boîte de dialogue, pas de bouton d’export. Un fichier existant n’est jamais écrasé.", + "feat.1.b": "Dès la fin de la conversion, le fichier .md se trouve dans Téléchargements, ou dans un autre dossier choisi dans les Paramètres. Pas de boîte de dialogue, pas de bouton d’export. Un fichier existant n’est jamais écrasé.", "feat.1.folder": "Téléchargements", "feat.1.new": "nouveau", "feat.2.t": "Aperçu et modification", @@ -3345,7 +3345,7 @@

Linux

"feat.kicker": "Fitur", "feat.title": "Semua yang Anda butuhkan, tanpa pengaturan", "feat.1.t": "Tersimpan otomatis", - "feat.1.b": "Begitu konversi selesai, file .md sudah ada di Unduhan. Tanpa dialog simpan, tanpa tombol ekspor. File yang sudah ada tidak pernah ditimpa.", + "feat.1.b": "Begitu konversi selesai, file .md sudah ada di Unduhan, atau di folder lain yang dipilih di Pengaturan. Tanpa dialog simpan, tanpa tombol ekspor. File yang sudah ada tidak pernah ditimpa.", "feat.1.folder": "Unduhan", "feat.1.new": "baru", "feat.2.t": "Pratinjau dan edit", @@ -3436,7 +3436,7 @@

Linux

"feat.kicker": "Özellikler", "feat.title": "İhtiyacınız olan her şey, kurulum gerektirmeden", "feat.1.t": "Kendiliğinden kaydedilir", - "feat.1.b": "Dönüştürme biter bitmez .md dosyası İndirilenler’dedir. Kaydet penceresi yok, dışa aktar düğmesi yok. Var olan bir dosyanın üzerine asla yazılmaz.", + "feat.1.b": "Dönüştürme biter bitmez .md dosyası İndirilenler’de ya da Ayarlar’da seçtiğiniz başka bir klasördedir. Kaydet penceresi yok, dışa aktar düğmesi yok. Var olan bir dosyanın üzerine asla yazılmaz.", "feat.1.folder": "İndirilenler", "feat.1.new": "yeni", "feat.2.t": "Önizleme ve düzenleme", @@ -3527,7 +3527,7 @@

Linux

"feat.kicker": "機能", "feat.title": "必要なものはすべてそろい、設定は不要", "feat.1.t": "自動で保存", - "feat.1.b": "変換が終わった瞬間、.md ファイルはダウンロードフォルダーにあります。保存ダイアログもエクスポートボタンもありません。既存のファイルが上書きされることはありません。", + "feat.1.b": "変換が終わった瞬間、.md ファイルはダウンロードフォルダー(または設定で選んだ別のフォルダー)にあります。保存ダイアログもエクスポートボタンもありません。既存のファイルが上書きされることはありません。", "feat.1.folder": "ダウンロード", "feat.1.new": "新規", "feat.2.t": "プレビューと編集", @@ -3618,7 +3618,7 @@

Linux

"feat.kicker": "功能", "feat.title": "所需功能一应俱全,无需任何设置", "feat.1.t": "自动保存", - "feat.1.b": "转换一完成,.md 文件就已在“下载”文件夹中。没有保存对话框,也没有导出按钮,且绝不会覆盖已有文件。", + "feat.1.b": "转换一完成,.md 文件就已在“下载”文件夹(或你在“设置”中选择的其他文件夹)中。没有保存对话框,也没有导出按钮,且绝不会覆盖已有文件。", "feat.1.folder": "下载", "feat.1.new": "新", "feat.2.t": "预览和编辑", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..abffbe5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,19 @@ +"""Shared test setup. + +Every test gets its own empty settings.json. Without this, a test that saves or +changes a setting would read and write the developer's real one: with a save +folder chosen in the desktop app, auto-save tests would write into that folder. +""" +from __future__ import annotations + +import pytest + + +@pytest.fixture(autouse=True) +def _isolated_settings(tmp_path_factory, monkeypatch): + from app.core.engine_manager import EngineManager + + mgr = EngineManager.get_instance() + settings_file = tmp_path_factory.mktemp("settings") / "settings.json" + monkeypatch.setattr(mgr, "get_settings_file_path", lambda: settings_file) + monkeypatch.setattr(mgr, "_settings_cache", None) diff --git a/tests/layout_width_ui.test.js b/tests/layout_width_ui.test.js new file mode 100644 index 0000000..6a292a1 --- /dev/null +++ b/tests/layout_width_ui.test.js @@ -0,0 +1,226 @@ +// Tests the Layout width setting (Settings > General > Display) and the table +// scroll boxes in app/ui/app.js, plus the CSS and markup they rely on. +// +// The server puts the saved width on before first paint; the page keeps +// the radios in step, applies a change at once, saves it to settings.json and +// undoes it if the save fails. Each rendered table gets its own scroll box. +// +// Follows save_folder_ui.test.js: slice the block out of the source, run it +// against stubs, no npm dependency. +const assert = require("assert"); +const fs = require("fs"); + +const source = fs.readFileSync("app/ui/app.js", "utf8"); +const html = fs.readFileSync("app/ui/index.html", "utf8"); +const css = fs.readFileSync("app/ui/style.css", "utf8"); + +function slice(startMarker, endMarker) { + const start = source.indexOf(startMarker); + const end = source.indexOf(endMarker, start); + if (start < 0 || end < 0) throw new Error(`could not locate ${startMarker.trim()} in app.js`); + return source.slice(start, end); +} +const widthBlock = slice(" // ─── Layout width ", " // ─── End layout width"); +const tableBlock = slice(" // ─── Table scroll boxes ", " // ─── End table scroll boxes"); + +function setup({ initial, respond = () => ({ ok: true }) } = {}) { + const root = { dataset: {}, writes: 0 }; + let current; + Object.defineProperty(root.dataset, "layoutWidth", { + get: () => current, + set: (v) => { current = v; root.writes++; }, + enumerable: true, + }); + if (initial !== undefined) { current = initial; } + const radios = ["standard", "wide", "full"].map((value) => ({ + value, checked: value === "standard", listeners: [], + addEventListener(type, fn) { if (type === "change") this.listeners.push(fn); }, + })); + const posts = []; + const toasts = []; + const pending = []; + const env = { + document: { + documentElement: root, + querySelectorAll: (sel) => (sel === 'input[name="layoutWidth"]' ? radios : []), + }, + apiBase: "/InkDoc", + showToast: (msg, kind) => toasts.push({ msg, kind }), + // Each POST resolves when the test says so, to check ordering. + fetchWithToken: (url, opts) => { + posts.push({ url, body: JSON.parse(opts.body) }); + return new Promise((resolve, reject) => pending.push({ resolve, reject, body: JSON.parse(opts.body) })); + }, + }; + const api = new Function("env", ` + const { document, apiBase, showToast, fetchWithToken } = env; + ${widthBlock} + return { syncLayoutWidth, current: () => layoutWidth }; + `)(env); + const choose = async (value) => { + radios.forEach((r) => { r.checked = r.value === value; }); + const r = radios.find((x) => x.value === value); + const done = Promise.all(r.listeners.map((fn) => fn({ target: r }))); + return done; + }; + const settle = () => new Promise((r) => setImmediate(r)); + const checked = () => radios.filter((r) => r.checked).map((r) => r.value); + return { ...api, root, radios, posts, toasts, pending, choose, settle, checked, respond }; +} + +(async () => { + // The width the server wrote is kept and shown; nothing is rewritten. + { + const t = setup({ initial: "wide" }); + assert.strictEqual(t.current(), "wide"); + assert.deepStrictEqual(t.checked(), ["wide"]); + assert.strictEqual(t.root.writes, 0, "no write when the page already has the right width"); + } + + // No attribute (or a strange one) means Standard. + for (const initial of [undefined, "huge", ""]) { + const t = setup({ initial }); + assert.strictEqual(t.current(), "standard"); + assert.strictEqual(t.root.dataset.layoutWidth, "standard"); + assert.deepStrictEqual(t.checked(), ["standard"]); + } + + // Choosing applies at once, then saves exactly that value. + { + const t = setup(); + const done = t.choose("full"); + assert.strictEqual(t.root.dataset.layoutWidth, "full", "applied before the save returns"); + assert.deepStrictEqual(t.posts, [{ url: "/InkDoc/settings", body: { layout_width: "full" } }]); + t.pending[0].resolve({ ok: true }); + await done; + assert.strictEqual(t.current(), "full"); + assert.deepStrictEqual(t.toasts, [], "no toast when it worked: the page itself shows the change"); + } + + // A failed save (HTTP error or no connection) puts the previous width back and says so. + for (const fail of ["http", "network"]) { + const t = setup({ initial: "wide" }); + const done = t.choose("standard"); + if (fail === "http") t.pending[0].resolve({ ok: false }); + else t.pending[0].reject(new Error("offline")); + await done; + assert.strictEqual(t.root.dataset.layoutWidth, "wide"); + assert.deepStrictEqual(t.checked(), ["wide"]); + assert.deepStrictEqual(t.toasts, [{ msg: "Couldn't save the layout width.", kind: "error" }]); + } + + // Choosing the current width again sends nothing. + { + const t = setup({ initial: "wide" }); + await t.choose("wide"); + assert.deepStrictEqual(t.posts, []); + } + + // Two quick choices: the newest decides, an older failure doesn't undo it. + { + const t = setup(); + const first = t.choose("wide"); + const second = t.choose("full"); + t.pending[0].resolve({ ok: false }); + await first; + assert.strictEqual(t.root.dataset.layoutWidth, "full", "an older failed save doesn't undo a newer choice"); + t.pending[1].resolve({ ok: true }); + await second; + assert.strictEqual(t.current(), "full"); + assert.deepStrictEqual(t.toasts, []); + } + + // Opening Settings re-reads the setting, but never over a save in progress. + { + const t = setup(); + t.syncLayoutWidth("wide"); + assert.strictEqual(t.root.dataset.layoutWidth, "wide"); + const done = t.choose("full"); + t.syncLayoutWidth("wide"); // the read that started before the click + assert.strictEqual(t.root.dataset.layoutWidth, "full"); + t.pending[0].resolve({ ok: true }); + await done; + t.syncLayoutWidth("full"); + assert.strictEqual(t.current(), "full"); + t.syncLayoutWidth("bogus"); + assert.strictEqual(t.current(), "standard", "an unknown value from the server means Standard"); + } + + // Table scroll boxes: one box per table, idempotent, other content untouched. + { + function node(tag, parent = null) { + const n = { + tag, parentElement: parent, children: [], + classList: { contains: (c) => (n.className || "").split(" ").includes(c) }, + replaceWith(other) { + const siblings = n.parentElement.children; + siblings[siblings.indexOf(n)] = other; + other.parentElement = n.parentElement; + n.parentElement = null; + }, + appendChild(child) { child.parentElement = n; n.children.push(child); }, + }; + if (parent) parent.children.push(n); + return n; + } + const root = node("div"); + node("p", root); + const t1 = node("table", root); + node("pre", root); + const t2 = node("table", root); + const all = () => { const out = []; const walk = (n) => n.children.forEach((c) => { if (c.tag === "table") out.push(c); walk(c); }); walk(root); return out; }; + root.querySelectorAll = (sel) => (sel === "table" ? all() : []); + const env = { document: { createElement: (tag) => node(tag) } }; + const { wrapTables } = new Function("env", `const { document } = env; ${tableBlock} return { wrapTables };`)(env); + + wrapTables(root); + assert.deepStrictEqual(root.children.map((c) => c.tag), ["p", "div", "pre", "div"]); + [t1, t2].forEach((tb) => { + assert.strictEqual(tb.parentElement.className, "table-scroll"); + assert.strictEqual(tb.parentElement.parentElement, root); + }); + wrapTables(root); + assert.deepStrictEqual(root.children.map((c) => c.tag), ["p", "div", "pre", "div"], "wrapping twice adds nothing"); + } + + // Wiring in app.js: tables are wrapped on every render, before highlighting; + // Settings re-reads the width (guarded, so the add-on test's slice still runs). + { + const render = source.indexOf(" const rawHtml = markdownToHtml(md);"); + const wrap = source.indexOf(" wrapTables(renderedOutput);", render); + const highlight = source.indexOf(" highlightCodeBlocks(renderedOutput);", render); + assert.ok(render > 0 && wrap > render && highlight > wrap, "wrapTables runs after sanitising, before highlighting"); + const loadStart = source.indexOf(" async function loadSettings() {"); + const loadEnd = source.indexOf("renderSaveFolder(s.save_folder);", loadStart); + assert.ok(source.slice(loadStart, loadEnd).includes("if (s.layout_width !== undefined) syncLayoutWidth(s.layout_width);")); + } + + // Markup: one radio per width, Standard checked by default, in General. + { + const values = [...html.matchAll(//g)]; + assert.deepStrictEqual(values.map((m) => m[1]), ["standard", "wide", "full"]); + assert.deepStrictEqual(values.filter((m) => m[2]).map((m) => m[1]), ["standard"]); + const general = html.slice(html.indexOf('data-settings-pane="general"'), html.indexOf('data-settings-pane="engines"')); + assert.ok(general.includes('id="layoutWidthGroup"') && general.includes('role="radiogroup"')); + } + + // CSS: the three widths, Standard as before, and never animated. + { + assert.ok(/:root \{\s*--layout-max: 1400px;/.test(css)); + assert.ok(/html\[data-layout-width="wide"\] \{\s*--layout-max: 1920px;/.test(css)); + assert.ok(/html\[data-layout-width="full"\] \{\s*--layout-max: none;/.test(css)); + const container = css.slice(css.indexOf(".app-container {"), css.indexOf("}", css.indexOf(".app-container {"))); + assert.ok(container.includes("max-width: var(--layout-max);")); + assert.ok(!/transition/.test(container), "the width must not be animated (long documents would stutter)"); + assert.ok(/\.markdown-rendered \.table-scroll \{\s*overflow-x: auto;/.test(css)); + assert.ok(/\.markdown-rendered img \{\s*max-width: 100%;\s*height: auto;/.test(css)); + assert.ok(/\.markdown-rendered > p,[\s\S]*?max-width: calc\(1000px \* var\(--preview-scale\)\);/.test(css), "text keeps 1000px at 100% and the same line length at other text sizes"); + assert.ok(/\.markdown-rendered > p:has\(> img\) \{\s*max-width: none;/.test(css), + "a paragraph holding a picture is not held to the text width"); + } + + console.log("layout width UI tests passed"); +})().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/tests/math_render_ui.test.js b/tests/math_render_ui.test.js new file mode 100644 index 0000000..813c0f9 --- /dev/null +++ b/tests/math_render_ui.test.js @@ -0,0 +1,482 @@ +// Tests math formulas (Settings > Add-ons, KaTeX) in app/ui/app.js. +// +// Detection runs on the real bundled marked; KaTeX, DOMPurify, timers and +// IntersectionObserver are stubs, so the scheduling can be checked exactly. +// Off, or for a document without a delimiter, the preview must come out of +// the global parser unchanged. KaTeX must never load for such documents. +// +// Follows save_folder_ui.test.js: slice the block out of the source, run it +// against stubs, no npm dependency. +const assert = require("assert"); +const fs = require("fs"); +const vm = require("vm"); + +const source = fs.readFileSync("app/ui/app.js", "utf8"); +const html = fs.readFileSync("app/ui/index.html", "utf8"); + +function slice(startMarker, endMarker) { + const start = source.indexOf(startMarker); + const end = source.indexOf(endMarker, start); + if (start < 0 || end < 0) throw new Error(`could not locate ${startMarker.trim()} in app.js`); + return source.slice(start, end); +} +const block = slice(" // ─── Math formulas (built-in add-on: KaTeX) ", " // ─── End math formulas"); +const escapeStart = source.indexOf(" function escapeHtml(text) {"); +const escapeFn = source.slice(escapeStart, source.indexOf("\n }", escapeStart) + 4); + +// The real bundled marked, loaded the way the page loads it. +function loadMarked() { + const ctx = {}; + ctx.globalThis = ctx; ctx.window = ctx; ctx.self = ctx; + vm.createContext(ctx); + vm.runInContext(fs.readFileSync("app/ui/vendor/marked.min.js", "utf8"), ctx); + ctx.marked.use({ gfm: true, breaks: true }); // as the render function does + return ctx.marked; +} + +function setup({ renderMath = "on", katexOk = true, cssOk = true, purify = true, io = true, msPerFormula = 3, registers = () => true } = {}) { + const marked = loadMarked(); + const root = { dataset: {} }; + if (renderMath !== undefined) root.dataset.renderMath = renderMath; + let now = 0; + let timers = []; + const warnings = []; + const appended = []; + const katexCalls = []; + const katex = { + renderToString(tex, opts) { + katexCalls.push({ tex, opts }); + now += msPerFormula; // each formula "takes" this long + if (tex.includes("\\bad")) throw new Error("ParseError"); + return `${opts.displayMode ? "D" : "I"}:${tex}`; + }, + }; + let ioCallback = null; + const observed = new Set(); + const sanitizeCalls = []; + const win = { + marked, + DOMPurify: purify ? { sanitize: (h, cfg) => { sanitizeCalls.push(cfg); return `[clean]${h}`; } } : undefined, + IntersectionObserver: io ? function (cb) { ioCallback = cb; return { observe: (n) => observed.add(n), unobserve: (n) => observed.delete(n), disconnect: () => observed.clear() }; } : undefined, + }; + function element(tag) { + const el = { tag, onload: null, onerror: null }; + // escapeHtml sets textContent and reads innerHTML; serialise as a browser does. + let text = ""; + Object.defineProperty(el, "textContent", { set: (v) => { text = String(v); }, get: () => text }); + Object.defineProperty(el, "innerHTML", { + get: () => text.replace(/&/g, "&").replace(//g, ">").replace(/ /g, " "), + }); + return el; + } + const doc = { + documentElement: root, + getElementById: () => null, + createElement: element, + head: { + appendChild(el) { + appended.push(el); + setImmediate(() => { + if (el.tag === "script") { + if (katexOk) { if (registers()) win.katex = katex; el.onload && el.onload(); } else { el.onerror && el.onerror(); } + } else if (el.tag === "link") { + cssOk ? el.onload && el.onload() : el.onerror && el.onerror(); + } + }); + }, + }, + }; + const selected = []; + const env = { + window: win, document: doc, + console: { warn: (m) => warnings.push(m) }, + performance: { now: () => now }, + setTimeout: (fn, ms) => { timers.push(fn); return timers.length; }, + selectItem: (id) => selected.push(id), + getActive: () => "item-1", + fetchWithToken: async () => ({ ok: true }), apiBase: "/InkDoc", showToast: () => {}, + }; + const api = new Function("env", ` + const { window, document, console, performance, setTimeout, selectItem, fetchWithToken, apiBase, showToast } = env; + let activeItemId = env.getActive(); + ${escapeFn} + ${block} + return { markdownToHtml, typesetMath, cancelMath, applyRenderMath, syncRenderMath, mark: MATH_MARK, + state: () => ({ renderMath, mathParser, mathCacheSize: mathCache.size }) }; + `)(env); + // Run queued timers (and anything they queue) until none are left. + const flush = async () => { + for (let guard = 0; guard < 1000; guard++) { + await new Promise((r) => setImmediate(r)); + if (!timers.length) { + await new Promise((r) => setImmediate(r)); + if (!timers.length) return; + } + const run = timers; timers = []; + run.forEach((fn) => fn()); + } + throw new Error("timers never settled"); + }; + const stepOnce = async () => { + await new Promise((r) => setImmediate(r)); + const run = timers; timers = []; + run.forEach((fn) => fn()); + }; + const r = (specs) => mathRoot(specs, api.mark); + return { ...api, marked, win, root, appended, katexCalls, warnings, selected, flush, stepOnce, r, sanitizeCalls, + io: () => ioCallback, observed, timersPending: () => timers.length, advance: (ms) => { now += ms; } }; +} + +// A tiny DOM for typesetMath: nodes with class "math-tex" and the session +// marker (spec[2] === false leaves it off, like look-alike HTML in a document). +function mathRoot(specs, mark) { + const nodes = specs.map(([tex, display, marked = true]) => ({ + textContent: tex, + dataset: { display: display ? "1" : "0", math: marked ? mark : "" }, + classes: new Set(["math-tex"]), + classList: { add(c) { this.owner.classes.add(c); } }, + title: "", + set innerHTML(v) { this._html = v; }, + get innerHTML() { return this._html; }, + })); + nodes.forEach((n) => { n.classList.owner = n; }); + return { + nodes, + querySelectorAll: (sel) => (sel === `.math-tex[data-math="${mark}"]` ? nodes.filter((n) => n.dataset.math === mark) : []), + closest: () => ({ scroller: true }), + }; +} + +const hasMath = (out) => /class="math-tex/.test(out); + +(async () => { + // ── Detection: formulas from Docling and GLM-OCR become placeholders ── + { + const t = setup(); + const cases = [ + ["Energy $E = mc^2$ here.", "E = mc^2", false], + ["$$\\sum_{i=1}^{n} x_i^2 = \\frac{a*b}{c*d}$$", "\\sum_{i=1}^{n} x_i^2 = \\frac{a*b}{c*d}", true], + ["$$\n\\mathbf{x}_k = A_{k} \\mathbf{x}_{k-1} \\\\\ny = x^*\n$$", "\\mathbf{x}_k = A_{k} \\mathbf{x}_{k-1} \\\\\ny = x^*", true], + ["Inline \\(a_1 * b_1\\) here.", "a_1 * b_1", false], + ["\\[\n\\frac{a*b}{c*d}\n\\]", "\\frac{a*b}{c*d}", true], + ["see $$x^2$$ inline", "x^2", true], + ["| A | B |\n|---|---|\n| $a_1$ | 2 |", "a_1", false], + ["**$x_i$** bold", "x_i", false], + ["Costs $\\$5$ in TeX", "\\$5", false], + ]; + for (const [md, tex, display] of cases) { + const out = t.markdownToHtml(md); + const want = display ? `data-display="1">` : `data-display="0">`; + assert.ok(hasMath(out) && out.includes(want), `should be math: ${JSON.stringify(md)} -> ${out}`); + const escaped = tex.replace(/&/g, "&").replace(//g, ">"); + assert.ok(out.includes(escaped), `LaTeX kept exactly: ${JSON.stringify(md)} -> ${out}`); + assert.ok(!/|
/.test(out.split('class="math-tex')[1].split(" ${out}`); + } + // Too long for inline: stays text. + assert.ok(!hasMath(t.markdownToHtml("$" + "x".repeat(1001) + "$"))); + } + + // ── Placeholders carry the per-session marker ── + { + const t = setup(); + assert.match(t.mark, /^[0-9a-f]{16}$/); + assert.ok(t.markdownToHtml("a $x$ b").includes(`data-math="${t.mark}"`)); + assert.notStrictEqual(setup().mark, t.mark, "a new marker every session"); + } + + // ── Escaping: a placeholder can never carry markup ── + { + const t = setup(); + const out = t.markdownToHtml("$$ and $a', resp.text, re.S + ) + assert script is not None + assert "document.documentElement.dataset.layoutWidth = 'full';" in script.group(1) + head_end = resp.text.index("") + assert resp.text.index("layoutWidth = 'full'") < head_end, "must run before the body is painted" + + +# ─── Preview text size ──────────────────────────────────────────────────────── +# Same path as the layout width: settings.json, then onto before first paint. + +def _page_text_size(html: str) -> str | None: + match = re.search(r"document\.documentElement\.dataset\.textSize = '(\d+)';", html) + return match.group(1) if match else None + + +def test_text_size_default_is_100(): + from app.core.engine_manager import PREVIEW_TEXT_SIZES + + assert DEFAULT_SETTINGS["preview_text_size"] == 100 + assert PREVIEW_TEXT_SIZES == (85, 90, 100, 110, 120, 135, 150) + assert client.get("/settings").json()["preview_text_size"] == 100 + assert _page_text_size(client.get("/InkDoc").text) == "100" + + +def test_each_text_size_is_saved_and_served(): + from app.core.engine_manager import PREVIEW_TEXT_SIZES + + mgr = EngineManager.get_instance() + for size in PREVIEW_TEXT_SIZES: + resp = client.post("/settings", json={"preview_text_size": size}, headers=TOKEN) + assert resp.status_code == 200 + assert client.get("/settings").json()["preview_text_size"] == size + assert _page_text_size(client.get("/InkDoc").text) == str(size) + stored = json.loads(mgr.get_settings_file_path().read_text(encoding="utf-8")) + assert stored["preview_text_size"] == size + + +def test_text_size_off_the_steps_is_refused(): + for bad in [105, 0, 1, 200, -100, "110", True, 110.5, "x'; alert(1); '"]: + resp = client.post("/settings", json={"preview_text_size": bad}, headers=TOKEN) + assert resp.status_code == 422, bad + assert EngineManager.get_instance().get_settings()["preview_text_size"] == 100 + + +def test_text_size_needs_the_session_token(): + assert client.post("/settings", json={"preview_text_size": 150}).status_code == 403 + assert EngineManager.get_instance().get_settings()["preview_text_size"] == 100 + + +def test_hand_edited_text_size_falls_back_to_100(): + mgr = EngineManager.get_instance() + for stored in [105, "120", True, None, 1.5, "x'; alert(1); '", [110]]: + mgr.update_settings({"preview_text_size": stored}) + html = client.get("/InkDoc").text + assert _page_text_size(html) == "100", stored + assert "alert(1)" not in html + + +def test_width_and_text_size_are_independent(): + client.post("/settings", json={"layout_width": "full", "preview_text_size": 135}, headers=TOKEN) + client.post("/settings", json={"preview_text_size": 90}, headers=TOKEN) + html = client.get("/InkDoc").text + assert _page_width(html) == "full" + assert _page_text_size(html) == "90" + + +# ─── Math formulas (built-in add-on) ───────────────────────────────────────── +# render_math: on unless explicitly switched off; set on before first +# paint so a preview can never be parsed with the wrong setting. + +def _page_render_math(html: str) -> str | None: + match = re.search(r"document\.documentElement\.dataset\.renderMath = '(on|off)';", html) + return match.group(1) if match else None + + +def test_render_math_is_on_by_default(): + assert DEFAULT_SETTINGS["render_math"] is True + assert client.get("/settings").json()["render_math"] is True + assert _page_render_math(client.get("/InkDoc").text) == "on" + + +def test_render_math_off_and_on_again(): + mgr = EngineManager.get_instance() + for value, page in [(False, "off"), (True, "on")]: + resp = client.post("/settings", json={"render_math": value}, headers=TOKEN) + assert resp.status_code == 200 + assert client.get("/settings").json()["render_math"] is value + assert _page_render_math(client.get("/InkDoc").text) == page + stored = json.loads(mgr.get_settings_file_path().read_text(encoding="utf-8")) + assert stored["render_math"] is value + + +def test_render_math_refuses_non_booleans(): + for bad in ["maybe", 2, [True], {"on": True}, "off'; alert(1); '"]: + resp = client.post("/settings", json={"render_math": bad}, headers=TOKEN) + assert resp.status_code == 422, bad + assert EngineManager.get_instance().get_settings()["render_math"] is True + + +def test_render_math_needs_the_session_token(): + assert client.post("/settings", json={"render_math": False}).status_code == 403 + assert EngineManager.get_instance().get_settings()["render_math"] is True + + +def test_hand_edited_render_math_is_on_unless_false(): + """Only an explicit false turns it off; nothing stored is ever written into the page.""" + mgr = EngineManager.get_instance() + for stored, page in [(None, "on"), ("false", "on"), (0, "on"), ("off'; alert(1); '", "on"), (False, "off")]: + mgr.update_settings({"render_math": stored}) + html = client.get("/InkDoc").text + assert _page_render_math(html) == page, stored + assert "alert(1)" not in html + + +def test_page_policy_unchanged_by_math(): + """KaTeX comes from /static under the existing policy: no eval, no other origins.""" + csp = client.get("/InkDoc").headers["content-security-policy"] + assert "script-src 'self' 'nonce-" in csp and "unsafe-eval" not in csp + assert "font-src 'self' https://fonts.gstatic.com data:;" in csp + assert "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;" in csp + + +def test_katex_is_served_from_static(): + for path in ["/static/vendor/katex/katex.min.js", "/static/vendor/katex/katex.min.css", + "/static/vendor/katex/fonts/KaTeX_Main-Regular.woff2"]: + assert client.get(path).status_code == 200, path diff --git a/tests/test_save_location.py b/tests/test_save_location.py new file mode 100644 index 0000000..df9f5eb --- /dev/null +++ b/tests/test_save_location.py @@ -0,0 +1,453 @@ +"""The save folder: where auto-save writes, and its fallback to Downloads. + +Settings live in a temporary settings.json and Downloads is a temporary +folder, so nothing here reads the user's settings or writes to their real +Downloads. +""" +from __future__ import annotations + +import io +import json +import os +import sys +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +import app.server.server as server_module +from app.core import converter, save_location +from app.core.engine_manager import DEFAULT_SETTINGS, EngineManager +from app.core.queue_model import QueueItem, SourceKind +from app.server.server import app + +client = TestClient(app) + + +@pytest.fixture +def env(tmp_path, monkeypatch): + """Isolated settings.json and Downloads; returns (downloads, settings_file).""" + downloads = tmp_path / "Downloads" + downloads.mkdir() + settings_file = tmp_path / "settings.json" + mgr = EngineManager.get_instance() + monkeypatch.setattr(mgr, "get_settings_file_path", lambda: settings_file) + monkeypatch.setattr(mgr, "_settings_cache", None) + monkeypatch.setattr(converter, "get_downloads_dir", lambda: downloads) + monkeypatch.setattr(server_module, "_conversion_semaphores", {}) + return downloads, settings_file + + +def _choose(folder: Path | str) -> None: + EngineManager.get_instance().update_settings({"save_dir": str(folder)}) + + +def _item(name="report.pdf") -> QueueItem: + return QueueItem(source=f"/x/{name}", kind=SourceKind.FILE, display_name=name) + + +# ─── save_location ─────────────────────────────────────────────────────────── + +def test_default_is_downloads(env): + downloads, _ = env + assert DEFAULT_SETTINGS["save_dir"] == "" + assert save_location.folder_state() == { + "path": str(downloads), "chosen": "", "custom": False, "problem": "", + } + path, fallback = save_location.save_markdown(_item(), "# hi") + assert (path.parent, fallback) == (downloads, None) + + +def test_saves_to_chosen_folder(env, tmp_path): + folder = tmp_path / "Notes" + folder.mkdir() + _choose(folder) + + first, fb1 = save_location.save_markdown(_item(), "# one") + second, fb2 = save_location.save_markdown(_item(), "# two") + + assert (fb1, fb2) == (None, None) + assert [first.name, second.name] == ["report.md", "report (1).md"] + assert first.parent == folder and first.read_text(encoding="utf-8") == "# one" + assert save_location.folder_state() == { + "path": str(folder), "chosen": str(folder), "custom": True, "problem": "", + } + + +def test_missing_folder_falls_back_and_is_not_recreated(env, tmp_path): + downloads, settings_file = env + folder = tmp_path / "Gone" + _choose(folder) + + path, fallback = save_location.save_markdown(_item(), "# hi") + + assert path.parent == downloads + assert fallback == f"Saved to Downloads because the save folder {folder} was not found." + assert not folder.exists(), "a deleted save folder must not be re-created" + # The choice is kept, so the folder works again once it is back. + assert json.loads(settings_file.read_text(encoding="utf-8"))["save_dir"] == str(folder) + state = save_location.folder_state() + assert state["path"] == str(downloads) and state["chosen"] == str(folder) + assert "not found" in state["problem"] + + folder.mkdir() + assert save_location.save_markdown(_item(), "# back")[0].parent == folder + + +def test_write_error_falls_back(env, tmp_path, monkeypatch): + downloads, _ = env + folder = tmp_path / "Locked" + folder.mkdir() + _choose(folder) + real = converter.auto_save_markdown + + def failing(item, text, downloads_dir=None): + if downloads_dir == folder: + raise PermissionError("denied") + return real(item, text, downloads_dir) + + monkeypatch.setattr(converter, "auto_save_markdown", failing) + path, fallback = save_location.save_markdown(_item(), "# hi") + + assert path.parent == downloads + assert fallback == f"Saved to Downloads because the save folder {folder} can't be written to." + + +@pytest.mark.skipif( + sys.platform == "win32" or (hasattr(os, "geteuid") and os.geteuid() == 0), + reason="needs POSIX permissions and a non-root user", +) +def test_read_only_folder_falls_back(env, tmp_path): + downloads, _ = env + folder = tmp_path / "ReadOnly" + folder.mkdir() + _choose(folder) + folder.chmod(0o500) + try: + path, fallback = save_location.save_markdown(_item(), "# hi") + assert path.parent == downloads and "can't be written to" in fallback + assert "can't write" in save_location.folder_state()["problem"] + assert save_location.check_usable(folder) == "InkDoc can't write to that folder." + finally: + folder.chmod(0o700) + + +def test_symlinked_folder_falls_back(env, tmp_path): + downloads, _ = env + target = tmp_path / "Target" + target.mkdir() + link = tmp_path / "Link" + try: + link.symlink_to(target, target_is_directory=True) + except (OSError, NotImplementedError): + pytest.skip("symlinks are not available") + _choose(link) + + path, fallback = save_location.save_markdown(_item(), "# hi") + + assert path.parent == downloads + assert fallback == f"Saved to Downloads because the save folder {link} is a link to another place." + assert list(target.iterdir()) == [] + state = save_location.folder_state() + assert state["path"] == str(downloads) and "link" in state["problem"] + + +def test_failed_write_leaves_no_partial_file(env, tmp_path): + # Text that cannot be encoded fails after the file was created; neither + # the chosen folder nor Downloads may keep the empty file. + downloads, _ = env + folder = tmp_path / "Notes" + folder.mkdir() + _choose(folder) + + with pytest.raises(UnicodeEncodeError): + save_location.save_markdown(_item(), "x" + chr(0xD800)) + + assert list(folder.iterdir()) == [] + assert list(downloads.iterdir()) == [] + + +def _symlink(link: Path, target: Path) -> None: + try: + link.symlink_to(target, target_is_directory=target.is_dir()) + except (OSError, NotImplementedError): + pytest.skip("symlinks are not available") + + +def test_replaced_parent_folder_is_not_followed(env, tmp_path): + # The folder above the chosen one is swapped for a link after it was + # chosen; is_symlink() on the folder itself would not notice. + downloads, _ = env + share = tmp_path / "Share" + (share / "Notes").mkdir(parents=True) + _choose((share / "Notes").resolve()) + elsewhere = tmp_path / "Elsewhere" + (elsewhere / "Notes").mkdir(parents=True) + share.rename(tmp_path / "Share-old") + _symlink(share, elsewhere) + + path, fallback = save_location.save_markdown(_item(), "# hi") + + assert path.parent == downloads + assert fallback.endswith("is a link to another place.") + assert list((elsewhere / "Notes").iterdir()) == [] + assert "link" in save_location.folder_state()["problem"] + + +@pytest.mark.parametrize("error", [RuntimeError("Symlink loop"), OSError("ELOOP"), ValueError("bad")]) +def test_unresolvable_folder_falls_back(env, tmp_path, monkeypatch, error): + # resolve() can fail (a link loop raises RuntimeError on Python 3.10-3.12); + # when the check can't tell, nothing is written to the chosen folder. + downloads, _ = env + folder = tmp_path / "Notes" + folder.mkdir() + _choose(folder) + real_resolve = Path.resolve + + def failing(self, *args, **kwargs): + if self == folder: + raise error + return real_resolve(self, *args, **kwargs) + + monkeypatch.setattr(Path, "resolve", failing) + path, fallback = save_location.save_markdown(_item(), "# hi") + + assert path.parent == downloads + assert fallback.endswith("is a link to another place.") + assert list(folder.iterdir()) == [] + assert save_location.folder_state()["problem"] + + +def test_name_taken_after_the_check_is_not_overwritten(env, tmp_path, monkeypatch): + # Something appears at the free name between unique_download_path() and + # the write; the save moves on to the next name instead of replacing it. + folder = tmp_path / "Notes" + folder.mkdir() + _choose(folder) + real = converter.unique_download_path + calls = [] + + def racing(directory, base_name): + path = real(directory, base_name) + if not calls: + path.write_text("someone else's file", encoding="utf-8") + calls.append(path.name) + return path + + monkeypatch.setattr(converter, "unique_download_path", racing) + path, fallback = save_location.save_markdown(_item(), "# mine") + + assert fallback is None + assert path.name == "report (1).md" and path.read_text(encoding="utf-8") == "# mine" + assert (folder / "report.md").read_text(encoding="utf-8") == "someone else's file" + + +def test_link_planted_at_the_name_is_not_followed(env, tmp_path, monkeypatch): + folder = tmp_path / "Notes" + folder.mkdir() + _choose(folder) + victim = tmp_path / "victim.txt" + victim.write_text("precious", encoding="utf-8") + _symlink(folder / "report.md", victim) + real = converter.unique_download_path + first = [] + + def racing(directory, base_name): + if not first: # hand out the planted name once, as a lost race would + first.append(True) + return directory / "report.md" + return real(directory, base_name) + + monkeypatch.setattr(converter, "unique_download_path", racing) + path, _ = save_location.save_markdown(_item(), "# mine") + + assert victim.read_text(encoding="utf-8") == "precious" + assert path.name != "report.md" and path.read_text(encoding="utf-8") == "# mine" + + +def test_downloads_failure_still_raises(env, tmp_path, monkeypatch): + _choose(tmp_path / "Gone") + + def failing(item, text, downloads_dir=None): + raise PermissionError("Downloads is read-only") + + monkeypatch.setattr(converter, "auto_save_markdown", failing) + with pytest.raises(PermissionError): + save_location.save_markdown(_item(), "# hi") + + +def test_check_usable(tmp_path): + good = tmp_path / "Good" + good.mkdir() + a_file = tmp_path / "file.txt" + a_file.write_text("x", encoding="utf-8") + + assert save_location.check_usable(good) == "" + assert list(good.iterdir()) == [], "the write test must clean up" + assert save_location.check_usable(Path("relative/dir")) == "Choose a full folder path." + assert save_location.check_usable(a_file) == "That folder doesn't exist." + assert save_location.check_usable(tmp_path / "missing") == "That folder doesn't exist." + + +def test_relative_or_bad_values_mean_downloads(env): + downloads, _ = env + for value in ("relative/dir", "", None, 42): + EngineManager.get_instance().update_settings({"save_dir": value}) + assert save_location.configured_save_dir() is None + assert save_location.folder_state()["path"] == str(downloads) + + +def test_non_ascii_folder_round_trips(env, tmp_path): + _, settings_file = env + folder = tmp_path / "ملاحظات 📝" + folder.mkdir() + assert save_location.store_save_dir(str(folder)) is True + + EngineManager.get_instance()._settings_cache = None # read the file again + assert save_location.configured_save_dir() == folder + assert save_location.save_markdown(_item(), "# hi")[0].parent == folder + + +def test_store_reports_a_failed_write(env, monkeypatch): + _, settings_file = env + # update_settings() logs and keeps going when the file can't be written. + monkeypatch.setattr( + EngineManager.get_instance(), "get_settings_file_path", + lambda: settings_file.parent / "missing-dir" / "deeper" / "\0bad" / "settings.json", + ) + assert save_location.store_save_dir("/somewhere") is False + + +def test_old_settings_file_loads(env): + _, settings_file = env + settings_file.write_text(json.dumps({"fallback_to_markitdown": True}), encoding="utf-8") + assert EngineManager.get_instance().get_settings()["save_dir"] == "" + assert save_location.configured_save_dir() is None + + +# ─── Server ────────────────────────────────────────────────────────────────── + +def _upload(engine="markitdown", name="My Report.txt", **params): + query = "&".join(f"{k}={v}" for k, v in {"engine": engine, "save_to_downloads": "true", + "response_format": "json", **params}.items()) + return client.post(f"/convert/file?{query}", + files={"file": (name, io.BytesIO(b"hello"), "text/plain")}) + + +@pytest.mark.parametrize("engine", ["markitdown", "auto"]) +def test_conversion_saves_to_chosen_folder(env, tmp_path, engine): + folder = tmp_path / "Notes" + folder.mkdir() + _choose(folder) + + resp = _upload(engine) + + assert resp.status_code == 200, resp.text + data = resp.json() + assert Path(data["saved_to_downloads"]).parent == folder + assert data["save_fallback"] is None + + +@pytest.mark.parametrize("engine", ["markitdown", "auto"]) +def test_conversion_falls_back_and_says_so(env, tmp_path, engine): + downloads, _ = env + folder = tmp_path / "Gone" + _choose(folder) + + data = _upload(engine).json() + + assert Path(data["saved_to_downloads"]).parent == downloads + assert data["save_fallback"] == f"Saved to Downloads because the save folder {folder} was not found." + assert data["save_fallback"] in data["warnings"] + + +def test_text_response_carries_the_fallback(env, tmp_path): + _choose(tmp_path / "Gone") + resp = client.post( + "/convert/file?save_to_downloads=true", + files={"file": ("a.txt", io.BytesIO(b"hello"), "text/plain")}, + ) + assert resp.status_code == 200 + assert "Saved to Downloads because" in resp.headers.get("X-Engine-Warnings", "") + assert resp.headers["X-Save-Fallback"].startswith("Saved to Downloads because the save folder") + + +def test_text_response_has_no_fallback_header_normally(env): + resp = client.post( + "/convert/file?save_to_downloads=true", + files={"file": ("a.txt", io.BytesIO(b"hello"), "text/plain")}, + ) + assert "X-Save-Fallback" not in resp.headers + + +def test_batch_uses_chosen_folder(env, tmp_path): + folder = tmp_path / "Notes" + folder.mkdir() + _choose(folder) + files = [ + ("files", ("one.txt", io.BytesIO(b"one"), "text/plain")), + ("files", ("two.txt", io.BytesIO(b"two"), "text/plain")), + ] + data = client.post("/convert/batch?save_to_downloads=true", files=files).json() + assert sorted(p.name for p in folder.iterdir()) == ["one.md", "two.md"] + assert all(r["save_fallback"] is None for r in data["results"].values()) + + +def test_batch_fallback_is_reported_per_file(env, tmp_path): + downloads, _ = env + folder = tmp_path / "Gone" + _choose(folder) + files = [ + ("files", ("one.txt", io.BytesIO(b"one"), "text/plain")), + ("files", ("two.txt", io.BytesIO(b"two"), "text/plain")), + ] + results = client.post("/convert/batch?save_to_downloads=true", files=files).json()["results"] + + assert sorted(p.name for p in downloads.iterdir()) == ["one.md", "two.md"] + for result in results.values(): + assert result["save_fallback"].endswith("was not found.") + # One note per file: options copied per item must not share the list. + assert result["warnings"].count(result["save_fallback"]) == 1 + assert not folder.exists() + + +def test_no_save_when_not_asked(env, tmp_path): + downloads, _ = env + folder = tmp_path / "Notes" + folder.mkdir() + _choose(folder) + resp = _upload(save_to_downloads="false") + assert resp.json()["saved_to_downloads"] is None + assert list(folder.iterdir()) == [] and list(downloads.iterdir()) == [] + + +def test_get_settings_reports_the_folder(env, tmp_path): + downloads, _ = env + assert client.get("/settings").json()["save_folder"]["path"] == str(downloads) + _choose(tmp_path / "Gone") + state = client.get("/settings").json()["save_folder"] + assert state["custom"] is True and state["problem"] + + +def test_post_settings_cannot_set_the_folder(env, tmp_path): + target = tmp_path / "Elsewhere" + target.mkdir() + headers = {"X-InkDoc-Token": server_module.SESSION_TOKEN} + + resp = client.post("/settings", json={"save_dir": str(target), "quality_check_enabled": True}, + headers=headers) + + assert resp.status_code == 200 + assert EngineManager.get_instance().get_settings()["save_dir"] == "" + assert save_location.configured_save_dir() is None + + +def test_conversion_request_cannot_choose_the_folder(env, tmp_path): + downloads, _ = env + target = tmp_path / "Elsewhere" + target.mkdir() + + resp = _upload(save_dir=str(target), downloads_dir=str(target), directory=str(target)) + + assert Path(resp.json()["saved_to_downloads"]).parent == downloads + assert list(target.iterdir()) == [] diff --git a/tests/test_vendor_manifest.py b/tests/test_vendor_manifest.py new file mode 100644 index 0000000..0393faf --- /dev/null +++ b/tests/test_vendor_manifest.py @@ -0,0 +1,86 @@ +"""Bundled third-party UI files match app/ui/vendor/VENDOR.json exactly. + +Every file the page can load from /static/vendor is pinned here by SHA-256, +so a changed, missing or unlisted file fails CI instead of shipping. The +versions must also agree with the files' own headers and with +THIRD_PARTY_NOTICES.md, which keeps the notices in step with what ships. +""" +from __future__ import annotations + +import hashlib +import json +import re +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +VENDOR = REPO / "app" / "ui" / "vendor" +MANIFEST = VENDOR / "VENDOR.json" + + +def _libraries() -> list[dict]: + return json.loads(MANIFEST.read_text(encoding="utf-8"))["libraries"] + + +def _listed() -> dict[str, str]: + files: dict[str, str] = {} + for lib in _libraries(): + for rel, digest in lib["files"].items(): + assert rel not in files, f"{rel} is listed twice" + files[rel] = digest + return files + + +def test_every_listed_file_matches_its_hash(): + for rel, digest in _listed().items(): + path = VENDOR / rel + assert path.is_file(), f"{rel} is listed in VENDOR.json but missing" + actual = hashlib.sha256(path.read_bytes()).hexdigest() + assert actual == digest, f"{rel} changed: {actual} != {digest}" + + +def test_no_unlisted_files(): + on_disk = { + p.relative_to(VENDOR).as_posix() + for p in VENDOR.rglob("*") + if p.is_file() and p != MANIFEST + } + unlisted = sorted(on_disk - set(_listed())) + assert not unlisted, f"files in app/ui/vendor not in VENDOR.json: {unlisted}" + + +def test_each_library_is_described_and_licensed(): + for lib in _libraries(): + for key in ("name", "version", "license", "source", "files"): + assert lib.get(key), f"{lib.get('name')}: {key} is empty" + assert (REPO / lib["licence_file"]).is_file(), f"{lib['name']}: {lib['licence_file']} missing" + integrity = lib.get("upstream_integrity") + assert integrity is None or integrity.startswith("sha512-"), lib["name"] + + +def test_versions_match_file_headers_and_notices(): + notices = (REPO / "THIRD_PARTY_NOTICES.md").read_text(encoding="utf-8") + header_file = { + "DOMPurify": "purify.min.js", + "marked": "marked.min.js", + "highlight.js": "highlight.min.js", + } + for lib in _libraries(): + name, version = lib["name"], lib["version"] + assert f"{name} {version}" in notices, f"THIRD_PARTY_NOTICES.md does not list {name} {version}" + if name in header_file: + head = (VENDOR / header_file[name]).read_bytes()[:400].decode("utf-8", "replace") + # "v15.0.12" has no word boundary before the 1, so match on digits instead. + assert re.search(rf"(? General > Display) in app/ui/app.js, +// plus the CSS, markup and Preferences grouping it relies on. +// +// A− and A+ step through seven sizes. A click applies at once; the save to +// settings.json waits until the clicking stops, and only one save is ever in +// flight. A failed save puts the saved size back. No size is shown on screen; +// screen readers are told. +// +// Follows save_folder_ui.test.js: slice the block out of the source, run it +// against stubs, no npm dependency. +const assert = require("assert"); +const fs = require("fs"); + +const source = fs.readFileSync("app/ui/app.js", "utf8"); +const html = fs.readFileSync("app/ui/index.html", "utf8"); +const css = fs.readFileSync("app/ui/style.css", "utf8"); + +const start = source.indexOf(" // ─── Preview text size "); +const end = source.indexOf(" // ─── End preview text size", start); +if (start < 0 || end < 0) throw new Error("could not locate the preview text size block in app.js"); +const block = source.slice(start, end); + +const SIZES = [85, 90, 100, 110, 120, 135, 150]; + +function setup({ initial } = {}) { + const root = { dataset: {}, writes: 0 }; + let current = initial; + Object.defineProperty(root.dataset, "textSize", { + get: () => current, + set: (v) => { current = v; root.writes++; }, + }); + function button() { + const attrs = {}; + return { + listeners: [], setAttribute: (k, v) => { attrs[k] = v; }, getAttribute: (k) => attrs[k], + addEventListener(type, fn) { if (type === "click") this.listeners.push(fn); }, + click() { this.listeners.forEach((fn) => fn()); }, + }; + } + const els = { btnTextSmaller: button(), btnTextLarger: button(), textSizeStatus: { textContent: "" } }; + // Manual timers: the test moves time forward. + let now = 0; + let timers = []; + const setTimeoutStub = (fn, ms) => { const t = { fn, at: now + ms }; timers.push(t); return t; }; + const clearTimeoutStub = (t) => { timers = timers.filter((x) => x !== t); }; + const advance = async (ms) => { + now += ms; + for (;;) { + const due = timers.filter((t) => t.at <= now).sort((a, b) => a.at - b.at)[0]; + if (!due) break; + timers = timers.filter((t) => t !== due); + due.fn(); + await new Promise((r) => setImmediate(r)); + } + }; + const posts = []; + const pending = []; + const toasts = []; + const env = { + document: { documentElement: root, getElementById: (id) => els[id] }, + apiBase: "/InkDoc", + showToast: (msg, kind) => toasts.push({ msg, kind }), + fetchWithToken: (url, opts) => { + posts.push(JSON.parse(opts.body)); + return new Promise((resolve, reject) => pending.push({ resolve, reject })); + }, + setTimeout: setTimeoutStub, + clearTimeout: clearTimeoutStub, + }; + const api = new Function("env", ` + const { document, apiBase, showToast, fetchWithToken, setTimeout, clearTimeout } = env; + ${block} + return { syncTextSize, size: () => textSize, saved: () => textSizeSaved }; + `)(env); + const settle = () => new Promise((r) => setImmediate(r)); + const disabled = () => [els.btnTextSmaller.getAttribute("aria-disabled"), els.btnTextLarger.getAttribute("aria-disabled")]; + return { ...api, root, els, posts, pending, toasts, advance, settle, disabled, timers: () => timers }; +} + +(async () => { + // The size the server wrote is kept; nothing is rewritten on load. + { + const t = setup({ initial: "120" }); + assert.strictEqual(t.size(), 120); + assert.strictEqual(t.root.writes, 0); + assert.deepStrictEqual(t.disabled(), ["false", "false"]); + } + + // No attribute, or one that isn't a step, means 100%. + for (const initial of [undefined, "105", "abc", ""]) { + const t = setup({ initial }); + assert.strictEqual(t.size(), 100, String(initial)); + assert.strictEqual(t.root.dataset.textSize, "100"); + } + + // A+ walks up every step and stops at 150%; A− walks back to 85%. + { + const t = setup({ initial: "85" }); + assert.deepStrictEqual(t.disabled(), ["true", "false"], "A− is marked unavailable at the smallest size"); + const seen = [t.size()]; + for (let i = 0; i < 10; i++) { t.els.btnTextLarger.click(); seen.push(t.size()); } + assert.deepStrictEqual([...new Set(seen)], SIZES); + assert.strictEqual(t.root.dataset.textSize, "150"); + assert.deepStrictEqual(t.disabled(), ["false", "true"], "A+ is marked unavailable at the largest size"); + for (let i = 0; i < 10; i++) t.els.btnTextSmaller.click(); + assert.strictEqual(t.size(), 85); + assert.strictEqual(t.els.textSizeStatus.textContent, "Preview text size 85%", "screen readers hear the size"); + } + + // Applied at once; saved once, 400 ms after the last click. + { + const t = setup(); + t.els.btnTextLarger.click(); + t.els.btnTextLarger.click(); + t.els.btnTextLarger.click(); + assert.strictEqual(t.root.dataset.textSize, "135", "each click shows at once"); + await t.advance(399); + assert.deepStrictEqual(t.posts, [], "nothing saved while clicking"); + await t.advance(1); + assert.deepStrictEqual(t.posts, [{ preview_text_size: 135 }], "one save for three clicks"); + t.pending[0].resolve({ ok: true }); + await t.settle(); + assert.strictEqual(t.saved(), 135); + assert.deepStrictEqual(t.toasts, []); + } + + // Back to the saved size before the delay: nothing to save. + { + const t = setup({ initial: "110" }); + t.els.btnTextLarger.click(); + t.els.btnTextSmaller.click(); + await t.advance(500); + assert.deepStrictEqual(t.posts, []); + } + + // A failed save (HTTP error or no connection) shows the saved size again and says so. + for (const fail of ["http", "network"]) { + const t = setup({ initial: "100" }); + t.els.btnTextLarger.click(); + await t.advance(400); + if (fail === "http") t.pending[0].resolve({ ok: false }); + else t.pending[0].reject(new Error("offline")); + await t.settle(); + assert.strictEqual(t.root.dataset.textSize, "100"); + assert.deepStrictEqual(t.toasts, [{ msg: "Couldn't save the text size.", kind: "error" }]); + } + + // Clicks during a save: only one request at a time, and the latest size ends up saved. + { + const t = setup(); + t.els.btnTextLarger.click(); // 110 + await t.advance(400); + assert.deepStrictEqual(t.posts, [{ preview_text_size: 110 }]); + t.els.btnTextLarger.click(); // 120, while 110 is still being saved + await t.advance(400); + assert.strictEqual(t.posts.length, 1, "waits for the first save"); + t.pending[0].resolve({ ok: true }); + await t.settle(); + await t.advance(400); + assert.deepStrictEqual(t.posts, [{ preview_text_size: 110 }, { preview_text_size: 120 }]); + t.pending[1].resolve({ ok: true }); + await t.settle(); + assert.strictEqual(t.saved(), 120); + assert.strictEqual(t.root.dataset.textSize, "120"); + } + + // A failure that the user has already moved past does not undo the newer choice. + { + const t = setup(); + t.els.btnTextLarger.click(); // 110 + await t.advance(400); + t.els.btnTextLarger.click(); // 120 queued + t.pending[0].resolve({ ok: false }); + await t.settle(); + assert.strictEqual(t.root.dataset.textSize, "120"); + assert.deepStrictEqual(t.toasts, []); + } + + // Opening Settings re-reads the size, but never over a pending change. + { + const t = setup(); + t.syncTextSize(135); + assert.strictEqual(t.root.dataset.textSize, "135"); + assert.strictEqual(t.saved(), 135); + t.els.btnTextSmaller.click(); // 120 pending + t.syncTextSize(135); + assert.strictEqual(t.root.dataset.textSize, "120"); + await t.advance(400); + t.syncTextSize(135); // save in flight + assert.strictEqual(t.root.dataset.textSize, "120"); + t.pending[0].resolve({ ok: true }); + await t.settle(); + t.syncTextSize("bogus"); + assert.strictEqual(t.size(), 100, "an unknown value from the server means 100%"); + } + + // Wiring: Settings re-reads the size (guarded, so the add-on test's slice still runs). + { + const loadStart = source.indexOf(" async function loadSettings() {"); + const loadEnd = source.indexOf("renderSaveFolder(s.save_folder);", loadStart); + assert.ok(source.slice(loadStart, loadEnd).includes("if (s.preview_text_size !== undefined) syncTextSize(s.preview_text_size);")); + } + + // Markup: A− and A+ only (no size shown), labelled; a live sample drawn with + // the preview's own class; a status line for screen readers. + { + const display = html.slice(html.indexOf('class="settings-section settings-display"'), html.indexOf('data-settings-pane="engines"')); + const smaller = display.match(/]*id="btnTextSmaller"[^>]*>([\s\S]*?)<\/button>/); + const larger = display.match(/]*id="btnTextLarger"[^>]*>([\s\S]*?)<\/button>/); + assert.ok(smaller && smaller[0].includes('aria-label="Smaller text"') && smaller[1].includes("A−")); + assert.ok(larger && larger[0].includes('aria-label="Larger text"') && larger[1].includes("A+")); + assert.ok(!/\d+\s*%/.test(display.replace(//g, "")), "no percentage is shown"); + assert.ok(/class="markdown-rendered text-size-sample" aria-hidden="true"/.test(display)); + assert.ok(/id="textSizeStatus" role="status" aria-live="polite"/.test(display)); + } + + // Preferences: three groups (saving, Docling fallback, PDF check), each toggle + // in its own, no inline spacing left over. + { + const prefs = html.slice(html.indexOf(">Preferences<"), html.indexOf('class="settings-section settings-display"')); + const groups = prefs.split('
').slice(1); + assert.strictEqual(groups.length, 3); + assert.ok(groups[0].includes('id="autoSaveToggle"') && groups[0].includes('id="btnChangeSaveFolder"')); + assert.ok(groups[1].includes('id="doclingFallbackToggle"')); + assert.ok(groups[2].includes('id="qualityCheckToggle"')); + assert.ok(!/toggle-switch" style="margin-top/.test(prefs)); + assert.ok(/\.settings-group \+ \.settings-group \{[^}]*border-top: 1px solid var\(--rule\);/.test(css), "a line only between groups"); + } + + // CSS: one rule per step, and every preview font size scales from its former value. + { + assert.ok(/:root \{[^}]*--preview-scale: 1;/.test(css)); + for (const step of SIZES.filter((s) => s !== 100)) { + const re = new RegExp(`html\\[data-text-size="${step}"\\] \\{ --preview-scale: ${step / 100}; \\}`); + assert.ok(re.test(css), `rule for ${step}%`); + } + const expected = [ + [".markdown-rendered {", 14], [".markdown-rendered h1 {", 24], [".markdown-rendered h2 {", 20], + [".markdown-rendered h3 {", 16], [".markdown-rendered code {", 12], [".markdown-rendered pre code {", 13], + [".markdown-rendered table {", 13], [".raw-markdown-editor {", 13], + ]; + for (const [selector, px] of expected) { + const at = css.indexOf("\n" + selector); + const rule = css.slice(at, css.indexOf("}", at)); + assert.ok(rule.includes(`font-size: calc(${px}px * var(--preview-scale));`), `${selector} scales from ${px}px`); + assert.ok(!/transition/.test(rule), `${selector} must not animate its size`); + } + // Nothing else in the preview still uses a fixed font size. + const fixed = [...css.matchAll(/\n(\.markdown-rendered[^{]*)\{([^}]*)\}/g)] + .filter((m) => !m[1].includes("text-size-sample")) + .filter((m) => /font-size:\s*\d+px/.test(m[2])); + assert.deepStrictEqual(fixed.map((m) => m[1].trim()), []); + } + + console.log("text size UI tests passed"); +})().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/tests/theme_toggle_ui.test.js b/tests/theme_toggle_ui.test.js new file mode 100644 index 0000000..0b944e6 --- /dev/null +++ b/tests/theme_toggle_ui.test.js @@ -0,0 +1,166 @@ +// Tests the theme toggle in app/ui/app.js. +// +// Where the web engine supports View Transitions, the new theme grows as a +// circle from the button (as on the website). Without them, or with reduced +// motion, the theme switches at once, exactly as before. Either way the theme +// is saved and the natively drawn Windows title bar button is re-synced. +// +// Follows save_folder_ui.test.js: slice the block out of the source, run it +// against stubs, no npm dependency. +const assert = require("assert"); +const fs = require("fs"); + +const source = fs.readFileSync("app/ui/app.js", "utf8"); +const start = source.indexOf(" // ─── Theme Toggle "); +const end = source.indexOf(" // ─── Auto-save Toggle ", start); +if (start < 0 || end < 0) throw new Error("could not locate the theme toggle block in app.js"); +const block = source.slice(start, end); + +function setup({ saved = null, viewTransitions = false, reduceMotion = false, failReady = false, throwOnStart = false } = {}) { + const attrs = {}; + const classes = new Set(); + const storage = saved ? { "inkdoc-theme": saved } : {}; + const animations = []; + const transitions = []; + let syncs = 0; + let listener = null; + + const root = { + getAttribute: (n) => attrs[n] ?? null, + setAttribute: (n, v) => { attrs[n] = v; }, + classList: { add: (c) => classes.add(c), remove: (c) => classes.delete(c) }, + animate: (keyframes, options) => { animations.push({ keyframes, options }); }, + }; + const document = { documentElement: root }; + if (viewTransitions) { + document.startViewTransition = (callback) => { + if (throwOnStart) throw new Error("not allowed"); + // The engine runs the update a little later, after it captures the old view. + const done = Promise.resolve().then(callback); + const t = { + ready: failReady ? done.then(() => Promise.reject(new Error("skipped"))) : done, + finished: done, + }; + t.ready.catch(() => {}); + transitions.push(t); + return t; + }; + } + const window = { + innerWidth: 1200, + innerHeight: 800, + matchMedia: (q) => ({ matches: q === "(prefers-reduced-motion: reduce)" && reduceMotion }), + __inkdocSyncTitlebar: () => { syncs++; }, + }; + const themeToggle = { + addEventListener: (type, fn) => { if (type === "click") listener = fn; }, + getBoundingClientRect: () => ({ left: 1100, top: 10, width: 40, height: 40 }), + }; + + new Function("env", ` + const { window, document, themeToggle, readStorage, writeStorage } = env; + ${block} + `)({ + window, document, themeToggle, + readStorage: (k) => storage[k] ?? null, + writeStorage: (k, v) => { storage[k] = v; }, + }); + + return { + click: () => listener(), + theme: () => attrs["data-theme"], + storage, classes, animations, transitions, + syncs: () => syncs, + }; +} + +const settle = () => new Promise((resolve) => setImmediate(resolve)); + +(async () => { + // Start-up: the saved theme is restored, dark by default. + assert.strictEqual(setup().theme(), "dark"); + assert.strictEqual(setup({ saved: "light" }).theme(), "light"); + + // 1. No View Transitions: switches at once, saved, title bar synced (as before). + { + const t = setup(); + t.click(); + assert.strictEqual(t.theme(), "light", "applied synchronously"); + assert.strictEqual(t.storage["inkdoc-theme"], "light"); + assert.strictEqual(t.syncs(), 1); + assert.strictEqual(t.animations.length, 0); + } + + // 2. Reduced motion: also instant, and no transition is started. + { + const t = setup({ viewTransitions: true, reduceMotion: true }); + t.click(); + assert.strictEqual(t.theme(), "light"); + assert.strictEqual(t.transitions.length, 0, "reduced motion must not start a transition"); + assert.strictEqual(t.syncs(), 1); + } + + // 3. Supported: the circle grows from the button to the farthest corner. + { + const t = setup({ viewTransitions: true }); + t.click(); + assert.ok(t.classes.has("theme-switching"), "colour fades paused during the switch"); + await settle(); + assert.strictEqual(t.theme(), "light"); + assert.strictEqual(t.storage["inkdoc-theme"], "light"); + assert.strictEqual(t.syncs(), 1); + assert.strictEqual(t.animations.length, 1); + const { keyframes, options } = t.animations[0]; + const x = 1120, y = 30; + const radius = Math.hypot(Math.max(x, 1200 - x), Math.max(y, 800 - y)); + assert.deepStrictEqual(keyframes.clipPath, [ + `circle(0px at ${x}px ${y}px)`, + `circle(${radius}px at ${x}px ${y}px)`, + ]); + assert.strictEqual(options.pseudoElement, "::view-transition-new(root)"); + assert.ok(options.duration <= 350, "kept short: clicks are ignored while it plays"); + assert.ok(!t.classes.has("theme-switching"), "fades restored afterwards"); + } + + // 4. The animation is skipped or fails: no error escapes, the theme still applies. + { + const t = setup({ viewTransitions: true, failReady: true }); + t.click(); + await settle(); + assert.strictEqual(t.theme(), "light"); + assert.strictEqual(t.animations.length, 0); + assert.ok(!t.classes.has("theme-switching")); + } + { + const t = setup({ viewTransitions: true, throwOnStart: true }); + t.click(); + assert.strictEqual(t.theme(), "light", "falls back to an instant switch"); + assert.ok(!t.classes.has("theme-switching")); + } + + // 5. Two quick clicks toggle twice, even before the first update has run. + { + const t = setup({ viewTransitions: true }); + t.click(); + t.click(); + await settle(); + assert.strictEqual(t.theme(), "dark"); + assert.strictEqual(t.storage["inkdoc-theme"], "dark"); + } + { + const t = setup({ saved: "light" }); + t.click(); + t.click(); + t.click(); + assert.strictEqual(t.theme(), "dark"); + } + + // The CSS that pauses colour fades during the circle is in place. + const css = fs.readFileSync("app/ui/style.css", "utf8"); + assert.ok(css.includes("html.theme-switching *"), "colour fades must be paused during the switch"); + + console.log("[OK] theme_toggle_ui.test.js passed"); +})().catch((err) => { + console.error(err); + process.exit(1); +});