From 8a942e6d802436840af3318f6ec9be56a070ca3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 12 Oct 2026 03:15:30 -0400 Subject: [PATCH] Remove CEF: the web content crate, ElyMark's page, their packaging, licences and tests (C14.5) PRD and agents.md name the native route; ElyMail's body will not use CEF or Electron. ElyMark's package carries the KaTeX fonts' OFL. Co-Authored-By: Claude Opus 5.5 (1M context) --- .cargo/config.toml | 4 - Cargo.lock | 248 +- Cargo.toml | 2 - PRD.md | 51 +- README.md | 5 +- agents.md | 47 +- apps/ely-mark/web/build.mjs | 89 - apps/ely-mark/web/index.html | 11 - apps/ely-mark/web/package-lock.json | 4871 ----------------- apps/ely-mark/web/package.json | 34 - apps/ely-mark/web/src/bridge.js | 10 - apps/ely-mark/web/src/code.js | 44 - apps/ely-mark/web/src/diagram.js | 34 - apps/ely-mark/web/src/diagrams.js | 90 - apps/ely-mark/web/src/diff.js | 53 - apps/ely-mark/web/src/export.css | 72 - apps/ely-mark/web/src/failure.js | 11 - apps/ely-mark/web/src/find.js | 138 - apps/ely-mark/web/src/icons.js | 12 - apps/ely-mark/web/src/image.js | 14 - apps/ely-mark/web/src/look.js | 22 - apps/ely-mark/web/src/main.css | 330 -- apps/ely-mark/web/src/main.js | 342 -- apps/ely-mark/web/src/math.js | 100 - apps/ely-mark/web/src/preserve.js | 149 - apps/ely-mark/web/src/render.js | 109 - apps/ely-mark/web/src/source.js | 109 - apps/ely-mark/web/src/style.js | 53 - apps/ely-mark/web/src/tables.js | 35 - apps/ely-mark/web/src/theme.js | 42 - apps/ely-mark/web/src/visual.js | 339 -- crates/ely-platform/src/apps.rs | 50 +- crates/ely-platform/src/storage.rs | 20 +- crates/ely-ui/src/i18n/settings.rs | 2 - crates/ely-ui/src/settings/storage.rs | 5 +- crates/ely-web-content/Cargo.toml | 32 - crates/ely-web-content/examples/webprobe.rs | 317 -- .../ely-web-content/src/bin/ely-web-helper.rs | 9 - crates/ely-web-content/src/edit.rs | 92 - crates/ely-web-content/src/engine.rs | 356 -- crates/ely-web-content/src/fence.rs | 88 - crates/ely-web-content/src/handlers.rs | 482 -- crates/ely-web-content/src/input.rs | 344 -- crates/ely-web-content/src/keys.rs | 189 - crates/ely-web-content/src/lib.rs | 19 - crates/ely-web-content/src/mac.rs | 105 - crates/ely-web-content/src/print.rs | 39 - crates/ely-web-content/src/surface.rs | 278 - crates/ely-web-content/src/tokens.rs | 77 - crates/ely-web-content/src/view.rs | 381 -- docs/accessibility.md | 6 +- docs/compatibility.md | 8 +- docs/help.md | 2 +- docs/licenses.md | 5 +- docs/perf/README.md | 2 +- packaging/licenses.sh | 7 +- packaging/linux/package.sh | 13 +- packaging/macos/bundle.sh | 60 +- packaging/macos/entitlements/gpu.plist | 7 - packaging/macos/entitlements/plugin.plist | 8 - packaging/macos/entitlements/renderer.plist | 7 - packaging/stage.sh | 11 - scripts/build-mark-content.sh | 10 - scripts/ci.sh | 2 - scripts/engines-key.sh | 2 +- scripts/fetch-cef.sh | 15 - scripts/fetch-references.sh | 4 - tests/scripts/comments-rules.sh | 14 +- tests/scripts/comments.py | 3 +- tests/scripts/hardened-macos.sh | 36 - tests/scripts/linux-distros.sh | 2 +- tests/scripts/linux-glibc.sh | 4 +- tests/scripts/linux-mark-idle.sh | 105 - tests/scripts/references.sh | 5 +- tests/scripts/release-macos.sh | 11 +- tests/scripts/storage.sh | 42 +- tests/scripts/webview.sh | 77 - 77 files changed, 105 insertions(+), 10718 deletions(-) delete mode 100644 apps/ely-mark/web/build.mjs delete mode 100644 apps/ely-mark/web/index.html delete mode 100644 apps/ely-mark/web/package-lock.json delete mode 100644 apps/ely-mark/web/package.json delete mode 100644 apps/ely-mark/web/src/bridge.js delete mode 100644 apps/ely-mark/web/src/code.js delete mode 100644 apps/ely-mark/web/src/diagram.js delete mode 100644 apps/ely-mark/web/src/diagrams.js delete mode 100644 apps/ely-mark/web/src/diff.js delete mode 100644 apps/ely-mark/web/src/export.css delete mode 100644 apps/ely-mark/web/src/failure.js delete mode 100644 apps/ely-mark/web/src/find.js delete mode 100644 apps/ely-mark/web/src/icons.js delete mode 100644 apps/ely-mark/web/src/image.js delete mode 100644 apps/ely-mark/web/src/look.js delete mode 100644 apps/ely-mark/web/src/main.css delete mode 100644 apps/ely-mark/web/src/main.js delete mode 100644 apps/ely-mark/web/src/math.js delete mode 100644 apps/ely-mark/web/src/preserve.js delete mode 100644 apps/ely-mark/web/src/render.js delete mode 100644 apps/ely-mark/web/src/source.js delete mode 100644 apps/ely-mark/web/src/style.js delete mode 100644 apps/ely-mark/web/src/tables.js delete mode 100644 apps/ely-mark/web/src/theme.js delete mode 100644 apps/ely-mark/web/src/visual.js delete mode 100644 crates/ely-web-content/Cargo.toml delete mode 100644 crates/ely-web-content/examples/webprobe.rs delete mode 100644 crates/ely-web-content/src/bin/ely-web-helper.rs delete mode 100644 crates/ely-web-content/src/edit.rs delete mode 100644 crates/ely-web-content/src/engine.rs delete mode 100644 crates/ely-web-content/src/fence.rs delete mode 100644 crates/ely-web-content/src/handlers.rs delete mode 100644 crates/ely-web-content/src/input.rs delete mode 100644 crates/ely-web-content/src/keys.rs delete mode 100644 crates/ely-web-content/src/lib.rs delete mode 100644 crates/ely-web-content/src/mac.rs delete mode 100644 crates/ely-web-content/src/print.rs delete mode 100644 crates/ely-web-content/src/surface.rs delete mode 100644 crates/ely-web-content/src/tokens.rs delete mode 100644 crates/ely-web-content/src/view.rs delete mode 100644 packaging/macos/entitlements/gpu.plist delete mode 100644 packaging/macos/entitlements/plugin.plist delete mode 100644 packaging/macos/entitlements/renderer.plist delete mode 100755 scripts/build-mark-content.sh delete mode 100755 scripts/fetch-cef.sh delete mode 100755 tests/scripts/hardened-macos.sh delete mode 100755 tests/scripts/linux-mark-idle.sh delete mode 100755 tests/scripts/webview.sh diff --git a/.cargo/config.toml b/.cargo/config.toml index 40cb15b..35c67ad 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,2 @@ [target.'cfg(all(windows, target_env = "msvc"))'] rustflags = ["-C", "target-feature=+crt-static"] - -[env] -# cef-dll-sys builds against the CEF that scripts/fetch-cef.sh fetched. -CEF_PATH = { value = "engines/cef", relative = true } diff --git a/Cargo.lock b/Cargo.lock index dcd2f8d..f838866 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -374,7 +374,7 @@ version = "0.38.0+1.3.281" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" dependencies = [ - "libloading 0.8.9", + "libloading", ] [[package]] @@ -966,29 +966,6 @@ dependencies = [ "shlex 2.0.1", ] -[[package]] -name = "cef" -version = "154.3.0+154.0.32" -source = "git+https://github.com/tauri-apps/cef-rs?rev=a712ad9bacd234c050631a95b9745b27d346b7cc#a712ad9bacd234c050631a95b9745b27d346b7cc" -dependencies = [ - "cef-dll-sys", - "libloading 0.9.0", - "objc2 0.6.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "cef-dll-sys" -version = "154.3.0+154.0.32" -source = "git+https://github.com/tauri-apps/cef-rs?rev=a712ad9bacd234c050631a95b9745b27d346b7cc#a712ad9bacd234c050631a95b9745b27d346b7cc" -dependencies = [ - "anyhow", - "cc", - "cmake", - "download-cef", - "serde_json", -] - [[package]] name = "cesu8" version = "1.1.0" @@ -1069,49 +1046,9 @@ checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" dependencies = [ "glob", "libc", - "libloading 0.8.9", + "libloading", ] -[[package]] -name = "clap" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "clap_lex" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" - [[package]] name = "clipboard-win" version = "5.4.1" @@ -1121,15 +1058,6 @@ dependencies = [ "error-code", ] -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "cmov" version = "0.5.4" @@ -1256,18 +1184,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.16.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96a4956774c13c126a8b5af4daa79384f4d826534c95a02d76afb39e2ab64e3" -dependencies = [ - "encode_unicode", - "libc", - "unicode-width", - "windows-sys 0.61.2", -] - [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -1328,29 +1244,10 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" dependencies = [ - "percent-encoding", "time", "version_check", ] -[[package]] -name = "cookie_store" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" -dependencies = [ - "cookie", - "document-features", - "idna", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "time", - "url", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -1892,7 +1789,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading 0.8.9", + "libloading", ] [[package]] @@ -1925,25 +1822,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "download-cef" -version = "3.0.0" -source = "git+https://github.com/tauri-apps/cef-rs?rev=a712ad9bacd234c050631a95b9745b27d346b7cc#a712ad9bacd234c050631a95b9745b27d346b7cc" -dependencies = [ - "bzip2", - "clap", - "fs-err", - "indicatif", - "regex", - "semver", - "serde", - "serde_json", - "sha1_smol", - "tar", - "thiserror 2.0.21", - "ureq", -] - [[package]] name = "dpi" version = "0.1.2" @@ -2306,26 +2184,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "ely-web-content" -version = "0.1.3" -dependencies = [ - "anyhow", - "cef", - "ely-gpui-component", - "ely-platform", - "ely-ui", - "futures", - "gpui-pre", - "image", - "libc", - "log", - "objc2 0.6.4", - "serde_json", - "tempfile", - "windows-sys 0.60.2", -] - [[package]] name = "embed-resource" version = "3.0.11" @@ -2349,12 +2207,6 @@ dependencies = [ "phf 0.13.1", ] -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "encoding_rs" version = "0.8.42" @@ -2585,16 +2437,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.13" @@ -2754,15 +2596,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "fs-err" -version = "3.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c95b673b8f6f7235229ae11c5642d81b04c2e64c1e2fb417bc0cf73ca45f29" -dependencies = [ - "autocfg", -] - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -3898,19 +3731,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indicatif" -version = "0.18.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" -dependencies = [ - "console", - "portable-atomic", - "unicode-width", - "unit-prefix", - "web-time", -] - [[package]] name = "inotify" version = "0.11.5" @@ -4225,7 +4045,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" dependencies = [ "libc", - "libloading 0.8.9", + "libloading", "pkg-config", ] @@ -4335,16 +4155,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "libloading" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" -dependencies = [ - "cfg-if", - "windows-link 0.2.1", -] - [[package]] name = "libm" version = "0.2.16" @@ -7145,17 +6955,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "socks" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b" -dependencies = [ - "byteorder", - "libc", - "winapi", -] - [[package]] name = "spin" version = "0.9.9" @@ -7262,12 +7061,6 @@ dependencies = [ "quote", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum" version = "0.27.2" @@ -7524,17 +7317,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "tar" -version = "0.4.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "tauri-winrt-notification" version = "0.8.1" @@ -8002,12 +7784,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unit-prefix" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" - [[package]] name = "universal-hash" version = "0.6.1" @@ -8031,15 +7807,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" dependencies = [ "base64 0.23.1", - "cookie_store", "flate2", "log", "percent-encoding", "rustls", "rustls-pki-types", - "serde", - "serde_json", - "socks", "ureq-proto", "utf8-zero", "webpki-roots", @@ -8629,7 +8401,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.9", + "libloading", "log", "naga", "ndk-sys", @@ -9422,16 +9194,6 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - [[package]] name = "xcb" version = "1.7.1" diff --git a/Cargo.toml b/Cargo.toml index cf2570c..75a48f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,6 @@ objc2-core-media = { version = "0.3", default-features = false, features = ["std objc2-core-video = { version = "0.3", default-features = false, features = ["std", "CVBuffer", "CVImageBuffer", "CVPixelBuffer"] } objc2-core-foundation = { version = "0.3", default-features = false, features = ["std", "CFString", "CFCGTypes"] } core-video = "0.5" -cef = { git = "https://github.com/tauri-apps/cef-rs", rev = "a712ad9bacd234c050631a95b9745b27d346b7cc", default-features = false } core-foundation = "0.10" objc2 = "0.6" security-framework = "3.7" @@ -61,7 +60,6 @@ ely-lok = { path = "crates/ely-lok" } ely-office = { path = "crates/ely-office" } ely-pdf-engine = { path = "crates/ely-pdf-engine" } ely-platform = { path = "crates/ely-platform" } -ely-web-content = { path = "crates/ely-web-content" } ely-ui = { path = "crates/ely-ui" } [workspace.lints.clippy] diff --git a/PRD.md b/PRD.md index 46bf609..e711cd9 100644 --- a/PRD.md +++ b/PRD.md @@ -24,10 +24,10 @@ Ely Office 提供八个桌面应用。用户可以写文档、计算表格、制 | ElyPDF | PDF 阅读、编辑、批注、表单和页面处理 | MuPDF;OCR 复用 Tesseract | PDF | | ElyMail | 邮件收发、本地存储和搜索 | MailCore 2,加本地同步与存储 | 邮件、附件、草稿 | | ElyTree | Git 图形化版本管理 | Git CLI | 本地 Git 仓库 | -| ElyMark | 本地 Markdown 编辑 | Milkdown 与 CodeMirror 6;参考 MarkText、ColaMD 的编辑与文件处理方式 | Markdown 与本地附件 | +| ElyMark | 本地 Markdown 编辑 | Ely-GPUI-Components 的 Markdown 文档组件,markdown-rs 解析;参考 MarkText、ColaMD、Markion | Markdown 与本地附件 | | ElyAgent | 独立的 Agent 配置与会话管理 | 嵌入的 Kigi CLI Rust 代码 | 配置、会话记录、运行状态 | -LOK、MuPDF 和 GPUI 是已确定的方向。MailCore 2、Markdown 编辑框架、本地 HTML 宿主和下文的发行默认值,是本 PRD 采用的工程基线,仍须通过三端接入验收。不得把未测试的接口写成已支持功能。 +LOK、MuPDF 和 GPUI 是已确定的方向。MailCore 2、Markdown 文档组件、邮件正文渲染和下文的发行默认值,是本 PRD 采用的工程基线,仍须通过三端接入验收。不得把未测试的接口写成已支持功能。 ElyAgent 参考 Magpie 的独立工具定位,但不要求复制其全部功能。它不是其他七个应用的聊天侧栏,也不是整个套件的控制中枢。[S19] @@ -35,7 +35,7 @@ ElyAgent 参考 Magpie 的独立工具定位,但不要求复制其全部功能 1. 首个正式版交付全部八个应用及本文定义的功能,不发布只有入口和占位界面的套件。 2. Doc、Table、Show 共用 LOK 适配实现。PDF 使用 MuPDF。不开设替代内核路线。 -3. GPUI 负责应用界面。专业内容区域可以使用内核渲染结果或本地 Web 内容区域,但外观和行为必须遵守 Ely 规范。 +3. GPUI 负责应用界面。专业内容区域可以使用内核渲染结果,但外观和行为必须遵守 Ely 规范;不用 CEF 与 Electron。 4. 应用可以独立启动、更新和卸载。安装 ElyMark、ElyTree 或 ElyAgent 不得强制安装 LOK。 5. 用户文件采用通用格式。应用数据库只保存索引、配置和运行记录,不取代用户文件。 6. 不建设云端文档平台、微服务系统、统一业务数据库或跨应用 Agent 编排系统。 @@ -377,7 +377,7 @@ XFA 动态表单、任意嵌入脚本执行、专业印前分色及 PDF 到 Offi 提供多账号邮件客户端,不建设邮件托管服务。MailCore 2 负责 IMAP、POP、SMTP 和邮件解析等能力。Ely 负责账号流程、本地存储、搜索、同步队列和界面。MailCore 2 官方列出 C++ API 与三端支持,但发行前仍须验证实际构建和依赖。[S10] -默认布局为账号与文件夹、邮件列表、阅读区。写信可在独立窗口打开。HTML 正文和富文本撰写区使用本地内容宿主,不能把正文 HTML 解析当成完整页面渲染。 +默认布局为账号与文件夹、邮件列表、阅读区。写信可在独立窗口打开。HTML 正文和富文本撰写区不用 CEF 与 Electron,渲染方式随 ElyMail 任务另定;不能把正文 HTML 解析当成完整页面渲染。 ### 12.2 功能要求 @@ -474,9 +474,9 @@ Git 托管、PR 审核平台、Issue 系统、CI 平台和远程开发环境不 提供以本地 Markdown 文件为中心的编辑器。支持可视化编辑、源码编辑和只读预览。文件可由其他编辑器、Git 或 Agent 修改,不要求导入专有知识库。 -默认使用 Milkdown 承担可视化编辑,CodeMirror 6 承担源码编辑。两者接入同一个文件会话,只允许当前模式提交修改,不同时维护两份独立可写文档。Milkdown 的 Markdown 与编辑模型转换需要单独处理源码保留,不能把序列化输出默认视为无损原文。[S14][S15][S16] +界面用 Ely-GPUI-Components 的 Markdown 文档组件,全部原生 GPUI:全文只在一个源码编辑器里,可视模式按块渲染,按下的块以原文编辑;源码与预览共用同一份文本。解析用 markdown-rs,公式用 RaTeX,Mermaid 用 mermaid-rs-renderer。文本即文件原文,不经编辑模型序列化。[S14][S15][S16] -参考 MarkText 的 Markdown 编辑行为、ColaMD 的本地文件同步和内容保留规则,不复制其整个应用窗口,不把 Electron 作为 Ely 的桌面外壳。当前 ColaMD 仍包含 Milkdown 与 CodeMirror 相关依赖,但具体实现必须按选定提交核对,不凭包名推断编辑行为。[S17][S18] +参考 MarkText 的 Markdown 编辑行为、ColaMD 的本地文件同步和内容保留规则、Markion 的 GPUI 原生 Markdown 编辑,不复制其整个应用窗口。不用 Electron,不用 CEF 或其他网页内容宿主。[S17][S18] ### 14.2 功能要求 @@ -605,7 +605,6 @@ ely-office/ ely-lok/ # LOK 接入;仅 Doc/Table/Show 使用 ely-pdf-engine/ # MuPDF 与 OCR 接入 ely-mail-engine/ # 邮件协议、同步和本地存储 - ely-web-content/ # 本地 HTML 与 Markdown 内容宿主 assets/ tests/ packaging/ @@ -625,23 +624,19 @@ C/C++ 异常、原生错误与资源释放在 FFI 边界处理,不能跨 Rust 邮件同步、Git 命令、OCR、缩略图与索引构建在后台任务中执行,不阻塞 UI 事件循环。后台指应用运行期间的本地任务,不表示默认安装常驻系统服务。 -### 17.3 本地 Web 内容宿主 +### 17.3 内容渲染 -Mail 的 HTML 正文和富文本撰写、Mark 的编辑与预览需要成熟 Web 内容渲染。本 PRD 的默认实现采用 CEF 离屏内容区域,只在这两个应用引入。CEF 提供离屏画面和输入事件接口,适合接入自绘界面,但 GPUI 的实际接入仍须三端验证。[S22] +不用 CEF,不用 Electron(用户 2026-10-11 定)。Mark 的编辑与预览全部由 GPUI 绘制(§14.1)。Mail 的 HTML 正文和富文本撰写的渲染方式随 ElyMail 任务另定,定前不写成已有。 -CEF 只处理内容区域,不绘制应用标题栏、菜单、文件树和设置页面。不引入 Electron 应用外壳,不运行面向浏览器的本地文档服务器。CEF 必需的辅助进程随应用发行。 +Ely 主题 token 用于内容区。键盘、焦点、选择、缩放、右键菜单和输入法进入统一适配规则。对话框和应用菜单由 GPUI 展示。 -Ely 主题 token 同步到内容区样式。键盘、焦点、选择、缩放、右键菜单和输入法进入统一适配规则。对话框和应用菜单由 GPUI 展示,不出现框架默认主题。 - -邮件阅读环境禁用脚本及文件桥接。Markdown 编辑环境只运行随包代码,通过受限桥接访问当前用户打开的文件与资源。外部文档内容不能调用任意本地命令。 - -不同时维护 CEF、系统 WebView 和全原生 Markdown 三种路线。若 CEF 原型不能满足三端要求,先修订该接入决策并更新 PRD,不能在应用内部临时混用多套未验收宿主。 +邮件阅读不运行正文脚本。Markdown 文档里的 HTML 不运行;数学与图只渲染保存的源码。外部文档内容不能调用任意本地命令。 ### 17.4 不建设的技术设施 不引入 Kubernetes、Docker 运行依赖、远程数据库、消息中间件、事件溯源系统、通用插件市场、统一知识图谱、分布式搜索或独立账号中心。 -应用间不做 RPC 服务发现。Agent 不以独立后台服务器为前提。Git 不同时接入多个 Git 内核。Markdown 不叠加第三个编辑内核。新抽象必须服务于已有的重复需求。 +应用间不做 RPC 服务发现。Agent 不以独立后台服务器为前提。Git 不同时接入多个 Git 内核。Markdown 只有一个解析内核(markdown-rs)。新抽象必须服务于已有的重复需求。 ## 18. 数据、配置与隐私 @@ -735,7 +730,7 @@ Doc、Table、Show 的 PDF 输出由 LOK 完成,不要求安装 ElyPDF。ElyMa | PERF-08 | 资源释放 | 连续打开关闭 20 次后,活动文档对象与句柄回到基线,内存无持续单向增长 | | PERF-09 | 多会话 | 4 个 Agent 会话同时流式输出时,其他会话选择和日志滚动仍满足交互目标 | -不提前宣传固定内存占用或比其他 Office 更快。发布报告列出单应用、内核子进程及内容宿主的总内存,不能只测 GPUI 主进程。 +不提前宣传固定内存占用或比其他 Office 更快。发布报告列出单应用与内核子进程的总内存,不能只测 GPUI 主进程。 ### 20.2 稳定性 @@ -769,7 +764,7 @@ Doc、Table、Show 的 PDF 输出由 LOK 完成,不要求安装 ElyPDF。ElyMa | LibreOffice/LOK | 保留 MPL 2.0 及附带依赖的相应声明;发布对受约束文件的修改与所需资料 [S5] | | MailCore 2 | 保留其许可、版权和免责条款,并检查其传递依赖 [S24] | | Git | 作为独立程序分发并遵守其 GPLv2 等实际适用条款;提供所发行版本的源码取得方式 [S23] | -| Markdown 与内容宿主 | 保留 Milkdown、CodeMirror、CEF 及其依赖的通知;逐项检查实际发行包 [S14][S16][S22] | +| Markdown | 保留 markdown-rs、RaTeX(含随带字体)、mermaid-rs-renderer 及其依赖的许可;逐项检查实际发行包 [S14][S15][S16] | | OCR、字体、词典和媒体组件 | 记录每个资源的来源、版本和再分发条件,不从系统目录随意拷贝受限资源 | MuPDF 的 AGPL 路线允许符合条件的开源集成;“项目开源”本身不足以证明整个发行包已经符合条件。是否收费与源码义务分开处理。[S8][S9] @@ -843,13 +838,13 @@ UI 自动测试、内核测试和手工交互测试互相补充。三端的输 | LOK 高级功能 | 母版、放映、图表、修订、输入法、打印和无障碍的接口映射及用例 | 补齐桥接或维护必要上游补丁,不跳回上游原生 UI | | MuPDF 编辑 | 字体、对象修改、表单、删除及保存的样本结果 | 修复对象编辑与输出;不得改为视觉覆盖冒充编辑 | | MailCore 2 | 三端可重复构建、TLS、OAuth、同步和 MIME 测试 | 处理依赖及适配问题;替换后端必须显式修订选型 | -| 本地 Web 内容 | CEF 与 GPUI 的输入、焦点、缩放、弹层和辅助技术测试 | 接入未通过不能以独立 Electron 窗口代替 | +| Markdown 原生编辑 | 可视、源码、预览的输入、焦点、缩放、输入法和辅助技术测试 | 未通过不能以 CEF、WebView 或 Electron 代替 | | Markdown 原文保留 | 无编辑保存、模式切换、局部修改和外部冲突的字节级差异 | 修复映射与保存;未知语法回到原文模式,不删除 | | 邮箱服务注册 | 正式 OAuth 配置、适用审核状态与用户流程 | 不将未获许可的内置登录描述为可用 | | Agent 后端差异 | 每个预设后端的版本、启动、控制、日志和恢复能力表 | 按真实能力显示,禁止模拟未提供的接口 | | 打包与许可 | 三端依赖清单、源码对应关系、资源再分发检查 | 发行前解决,不以“整个项目开源”代替检查 | -选择版本时记录源码提交、编译选项和补丁。升级 LOK、MuPDF、GPUI、CEF 或编辑框架后运行相关回归测试。不要跟随默认分支自动升级正式构建。 +选择版本时记录源码提交、编译选项和补丁。升级 LOK、MuPDF、GPUI 或 Markdown 解析与渲染库后运行相关回归测试。不要跟随默认分支自动升级正式构建。 ## 24. 交付物与完成定义 @@ -908,14 +903,14 @@ UI 自动测试、内核测试和手工交互测试互相补充。三端的输 [S13] Git 官方文档,git-status 与 porcelain 输出格式。 `https://git-scm.com/docs/git-status` -[S14] Milkdown 官方仓库,框架结构与许可证。 -`https://github.com/Milkdown/milkdown` +[S14] markdown-rs 官方仓库,CommonMark、GFM 与扩展语法解析,许可证。 +`https://github.com/wooorm/markdown-rs` -[S15] Milkdown 官方 Transformer 文档,编辑模型与 Markdown AST 转换。 -`https://milkdown.dev/docs/api/transformer` +[S15] RaTeX 官方仓库,LaTeX 公式排版与许可证。 +`https://github.com/erweixin/RaTeX` -[S16] CodeMirror 官方开发入口与仓库迁移说明;应用依赖按实际发布包锁定。 -`https://github.com/codemirror/dev` +[S16] mermaid-rs-renderer 官方仓库,Mermaid 图的 Rust 渲染与许可证。 +`https://github.com/1jehuang/mermaid-rs-renderer` [S17] MarkText 官方仓库,编辑模式、格式能力与许可证。 `https://github.com/marktext/marktext` @@ -933,10 +928,6 @@ UI 自动测试、内核测试和手工交互测试互相补充。三端的输 [S21] Tesseract 官方 API 示例与本地 OCR 接入。 `https://tesseract-ocr.github.io/tessdoc/APIExample.html` -[S22] Chromium Embedded Framework 官方说明与离屏渲染接入。 -`https://github.com/chromiumembedded/cef` -`https://chromiumembedded.github.io/cef/general_usage.html` - [S23] Git 官方 COPYING 文件。 `https://raw.githubusercontent.com/git/git/master/COPYING` diff --git a/README.md b/README.md index 2d5cbb2..426895c 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Offline-first desktop apps in Rust and GPUI. Files stay in your folders; no acco | ElyShow | Presentations (PPTX, ODP) | LibreOfficeKit, system media | | ElyPDF | PDF reading and editing, OCR, signatures | MuPDF, Tesseract | | ElyTree | Git client | Git | -| ElyMark | Markdown | Milkdown, CodeMirror in CEF | +| ElyMark | Markdown | markdown-rs, RaTeX, mermaid-rs-renderer | | ElyMail, ElyAgent | Second release | — | ## Build (macOS) @@ -20,8 +20,7 @@ scripts/fetch-references.sh # upstream sources at pinned commits scripts/build-libreoffice.sh # patched LibreOffice 26.2.6.3 (about an hour) scripts/build-mupdf.sh # patched MuPDF 1.28.5 scripts/build-git.sh && scripts/fetch-git-lfs.sh -scripts/fetch-tessdata.sh && scripts/fetch-cef.sh -scripts/build-mark-content.sh # ElyMark's page +scripts/fetch-tessdata.sh cargo build -p ely-lok --bins -p ely-pdf-engine --bins cargo run -p ely-doc # or ely-table, ely-show, ely-pdf, ely-tree, ely-mark ``` diff --git a/agents.md b/agents.md index 26c168c..6f15e86 100644 --- a/agents.md +++ b/agents.md @@ -14,7 +14,7 @@ | Doc / Table / Show | LibreOfficeKit,独立工作进程 | | Show 媒体 | 各平台原生:macOS AVFoundation,Windows Media Foundation,Linux GStreamer(LOK 不放媒体) | | PDF | MuPDF(C 接口),Tesseract OCR,独立工作进程 | -| Mail | MailCore 2 + SQLite;正文用 CEF 离屏内容区 | +| Mail | MailCore 2 + SQLite;正文渲染随其任务另定(不用 CEF 与 Electron) | | Tree | Git CLI,参数数组启动 | | Mark | 组件库 `documents::MarkdownDocument`(GPUI,原文即文本,无序列化);markdown-rs 解析,RaTeX 公式,mermaid-rs-renderer 图 | | Agent | Kigi CLI 的 Rust 代码直接嵌入(不用 ACP),沿用其提供商登录与模型配置 | @@ -22,10 +22,10 @@ ## 目录 ```text -apps/ 八个应用,各自一个二进制;ely-mark/web 是其网页源码(锁定版本,esbuild 构建) -crates/ ely-ui ely-platform ely-ipc(应用与引擎 worker 之间的帧);ely-files ely-lok ely-office(文档应用共用的外壳:标签、会话、保存、关闭询问、冲突、密码;引擎为 LOK 或 MuPDF)ely-pdf-engine(MuPDF worker 与客户端)ely-web-content(CEF 离屏页面,ElyMark 与 ElyMail 共用);ely-mail-engine 随其任务建 +apps/ 八个应用,各自一个二进制 +crates/ ely-ui ely-platform ely-ipc(应用与引擎 worker 之间的帧);ely-files ely-lok ely-office(文档应用共用的外壳:标签、会话、保存、关闭询问、冲突、密码;引擎为 LOK 或 MuPDF)ely-pdf-engine(MuPDF worker 与客户端);ely-mail-engine 随其任务建 reference/ 上游源码,只读,不属于本项目,不审查 -engines/ 下载的二进制与引擎构建产物,不入库,不审查;mupdf-src 是 reference/mupdf 加 patches/mupdf 的构建副本;content/<应用> 是网页内容的构建产物 +engines/ 下载的二进制与引擎构建产物,不入库,不审查;mupdf-src 是 reference/mupdf 加 patches/mupdf 的构建副本 patches/ 对上游的补丁,随源码发行(libreoffice、mupdf) vendor/ crates.io 上游 crate 的副本加本项目的修复,经工作区 `[patch.crates-io]` 换入(gpui-pre、gpui-pre-windows);首个提交为原样,其后是修复 packaging/ 三端打包脚本,apps.tsv 是八个应用的二进制名、名字、桌面分类与发布批次;icons/ 由图标母版渲染各端格式;libreoffice/ely.xcd 是内核的默认配置层 @@ -71,12 +71,8 @@ site/ 官网(Vite 8、pnpm、TypeScript,无框架;等轴 SVG 积木 | gpui 无障碍只有 `aria_value` 等,无文本选区与插入点:LOK 文档的光标与选区到不了辅助技术 | `LokView` 只桥接当前段落文本(`Role::MultilineTextInput`)。上游待办:gpui 补文本选区 AX | | 选中对象的框与手柄:LOK 不画进图块 | `LokView` 按图形选区画框与 8 个手柄;按住手柄拖动经 `setGraphicSelection` 交内核(缩放;裁剪模式下裁剪) | | 虚拟化 PDF 页面栈(`DocumentViewer` 一次铺全部页,不合 PERF-05) | `crates/ely-pdf-engine`、`apps/ely-pdf` | -| CEF 离屏内容视图 | `crates/ely-web-content` | -| CEF 页面的无障碍:离屏渲染的页面不进系统 AX 树(CEF 另给 AX 事件,GPUI 无从挂接) | 未桥接;页面文字在辅助技术里不可见。上游待办:gpui 可挂外部 AX 子树 | | 组件库的大纲项(`lists::Outline`)与提示(`feedback::Alert`)无无障碍角色:屏幕阅读器读不到 | 上游待办;ElyMark 的搜索失败改为结果列表里的行。`feedback::Banner` 已修(fac20ab:危险与警告为 alert,其余为 status,名为其文字) | -| `documents::ZenMode` 把正文夹进 Sm 宽的列,与 ElyMark 的正文宽度设置冲突 | ElyMark 专注模式自己收起标签条、左栏与状态栏,正文宽度由页面设置 | -| CEF 页面的弹出层(`select` 下拉、日期框)在离屏模式另画一层 | 不画;Ely 的页面不用原生 `select`,用自绘菜单 | -| macOS 的 CEF 离屏输入不带 AppKit 编辑命令:Blink 只凭它们处理 Cmd+方向键与 Cmd+Backspace | `ely-web-content` 的 `edit.rs`:页面没处理这些键时照 AppKit 标准键位执行 | +| `documents::ZenMode` 把正文夹进 Sm 宽的列,与 ElyMark 的正文宽度设置冲突 | ElyMark 不用它,专注模式原生重做(C14.12) | | 表格公式栏与名称框(网格是 LOK 图块,`tables::Spreadsheet` 不用) | `apps/ely-table` | | Doc/Table/Show 工具栏(`documents::FixedFormatToolbar` 绑 `TextInput`,不接 LOK) | 各 app,用 `shell::Toolbar` 组合 | | CSV 导入的编码、分隔符、引号、列类型(`misc::CsvImporter` 只读逗号 CSV、映射到属主字段,文案写死英文) | `apps/ely-table` 的 `text.rs`,内核按选项导入 | @@ -125,7 +121,6 @@ site/ 官网(Vite 8、pnpm、TypeScript,无框架;等轴 SVG 积木 scripts/doctor.sh 工具链版本 scripts/fetch-references.sh 取上游源码到 reference/ scripts/fetch-tessdata.sh 取 OCR 语言数据到 engines/tessdata(钉提交,核对散列) -scripts/fetch-cef.sh 取 CEF minimal 包到 engines/cef(随 cef-rs 钉的版本,核对 SHA-1) scripts/build-git.sh 构建钉住的 Git 到 engines/git(ElyTree 随包) scripts/fetch-git-lfs.sh 取 git-lfs 放进 engines/git(核对散列) scripts/review.sh ID 轮次 路径 审查一条任务 @@ -152,7 +147,6 @@ packaging/icons/masters.py 写出 assets/icons 的全部 SVG 母版(图标 packaging/icons/build.sh 由 assets/icons 的母版渲染 icns、png、ico(只在 macOS) tests/scripts/icons.sh 八个图标齐全、未过期,macOS 为 squircle,其余无底形 tests/scripts/release-macos.sh dist 里首批六个 .app:以访达的方式打开文件(启动与运行中),引擎都取自包内,带齐许可,不含系统会登记的内嵌包,每个二进制要求的系统不高于应用所写;源码包齐全 -tests/scripts/hardened-macos.sh ELY_SIGN_IDENTITY 签名(hardened runtime)的 ElyMark 照样画出页面 tests/engines/*.sh 引擎探测(LOK、MuPDF、libetpan);LOK 探测本构建的内核,不挂载官方镜像 scripts/inert-lo.sh 路径.app 构建出的 LibreOffice.app 去掉文档类型、URL 类型、快速查看与聚焦插件并注销(build-libreoffice.sh 调用) scripts/extract-official-lo.sh 用 7zz 把官方镜像的 Frameworks 与 Resources 解到 engines/lo-official(不挂载,无包,“未打补丁即拒绝”的单测用) @@ -164,7 +158,6 @@ tests/scripts/doc-review.sh 真窗口 ElyDoc:无日期的批注逐条写明 tests/scripts/theme.sh 真窗口 ElyTable 以深、浅两种外观启动,截窗口量网格亮度(需屏幕录制权限) tests/scripts/lag.sh ely-doc 真窗口键入,从调试日志算输入落地、图块、按键到新图块的分位数(需辅助功能权限);ELY_LAG_LOG=路径 留下日志 tests/scripts/pdf-idle.sh 真窗口 ElyPDF 无文件启动时无 worker 子进程 -scripts/build-mark-content.sh 按锁文件构建 ElyMark 页面到 engines/content/ely-mark(不跑安装脚本) tests/scripts/comments-rules.sh comments.py 对长注释与跨行注释报错 tests/scripts/bad-settings.sh 通用设置读不出时 ElyDoc 照样启动,文件不被覆盖 tests/scripts/one-instance.sh 第二份 ElyDoc 被拒,第一份的工作目录不动 @@ -172,7 +165,7 @@ tests/scripts/update.sh 装好的 ElyTree 从本机服务的新版下载 tests/scripts/settings.sh 真窗口:ElyPDF 设置里选深色,ElyTree 即刻变深;作者名写入、不丢外观;ElyTree 拒绝非 Git 程序;设置文件读不出时提示且不覆盖;主窗口重开回到原处(需辅助功能与屏幕录制权限) tests/scripts/privacy.sh 真窗口 ElyDoc:键入的文字不进日志;worker 的日志进应用日志;设置里的诊断先列出所含内容,导出的文件去掉家目录与姓名、不含正文(需辅助功能权限) tests/scripts/panels.sh 真窗口 ElyDoc:左侧栏默认不宽于 230、拖动内侧边缘变宽、宽度存入设置、重启后照旧(需辅助功能权限) -tests/scripts/storage.sh 真窗口 ElyMark:设置里的存储列出各类大小,日志当场清空,缓存下次启动前清掉,恢复点数写入设置(需辅助功能权限) +tests/scripts/storage.sh 真窗口 ElyMark:设置里的存储列出各类大小,日志当场清空,恢复副本下次启动前清掉,恢复点数写入设置(需辅助功能权限) tests/scripts/recovery.sh 真窗口 ElyDoc:恢复点、崩溃后副本、连续两次崩溃、只杀引擎后重新打开(需辅助功能权限) tests/scripts/pdf-recovery.sh 真窗口 ElyPDF:加锁文件的封存恢复点、空闲引擎退出后重开时再问密码(需辅助功能权限) tests/scripts/autosave.sh 真窗口 ElyDoc:自动保存开关、停手即写、外部改动不覆盖(需辅助功能权限) @@ -181,7 +174,6 @@ tests/scripts/show-fullscreen.sh 真窗口 ElyShow:F5 放映的窗口盖住 tests/scripts/types.sh packaging/types.tsv 的文件类型在 Linux 桌面项、Windows 安装程序与 macOS 文档类型里一致,从不设为默认 tests/scripts/layout.sh 真窗口:六个应用在 640、800、1100 宽下无元素越出窗口左右,留截图(需辅助功能与屏幕录制权限) tests/scripts/ooxml.py <用例> <文件> 不经 LibreOffice 读 DOCX、XLSX、PPTX 并按自算的预期核对(往返用例调用) -tests/scripts/webview.sh 真窗口 CEF 页面:输入法组字与提交、键入、焦点、主题变量、缩放、不可离开所在文件夹 cd site && pnpm deploy 官网构建并经 wrangler 发到 Cloudflare Workers(只有静态资源),地址 https://ely-office.zhangyanghaha0407.workers.dev cd site && pnpm test && pnpm e2e 官网单测(几何、道路连通)与 Playwright 端到端(悬停、聚焦、按压、命中归属) cd site/video/scenes && npm run verify 发布视频:群像与离场两个渲染器在切场处屏幕角点一致,离场全程各屏播自己的录屏帧(真页面渲染) @@ -215,7 +207,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-05(用户定):ElyAgent 的后端与内置模型的登录配置以 Kigi CLI(https://git.zacharyzhang.com/ZacharyZhang-NY/Kigi-CLI,Rust)为基础,以 Rust 依赖直接嵌入,不用 ACP;沿用其提供商登录(订阅账号或 API 密钥)与模型配置。 - 2026-10-05(用户定):八个应用都不许卡顿:键入、选择、滚动、文档渲染都要即时(GPUI 的 120 FPS 手感),树莓派 3 上也要流畅。输入延迟与帧时间是验收项,要实测;界面线程不等内核、不持 worker 锁做同步请求,空闲不做事。 - 2026-10-05:LOK 的输入与事件路径(实测后定)。原样:视图的事件监听长轮询 worker(持 worker 锁最多 50 毫秒,worker 读线程也随之阻塞最多 250 毫秒),按键排在锁后,键入锁等待中位 290 毫秒、最长 9 秒;输入队列在界面执行器上逐条跳回界面线程,每键约一帧,连续键入积压到 1 至 2 秒。现在:worker 有新事件即主动推送(`tag::EVENTS`,推送线程在输出锁下取批,批次即发送时状态),客户端读线程把回复与事件分开,事件存入 `client::Feed`,视图以异步方式等它(`Feed::next`,有新批次时唤醒,不占线程、不碰 worker 锁;原先每个打开的文档阻塞一个后台线程最多一秒,文档多于核数时会饿死输入与图块;监听任务归视图所有,视图释放即停);每个视图一条后台通道(`view/lane.rs`),输入优先、图块其次,连续执行,界面线程只收落地结果;文档尺寸与页框只在内核报尺寸变化时在后台读;批次弄脏的已显示图块即刻重绘,不等下一帧;视图的 `Reported` 事件只在内核回报或输入落地时发出,滚动与图块到达不触发应用的观察钩子;编辑与光标移动后应用要读的内核列表(ElyDoc 导航、字数、审阅,ElyTable 工作表与几何、单元格规则、格式面板)经 `ely_lok::read_off` 在后台读,一次一个,结果按发起者核对。实测(macOS,`tests/scripts/lag.sh`):按打字速度键入时输入落地 0 至 1 毫秒,图块 1 至 3 毫秒,按键到新图块 ElyDoc 中位 19 毫秒、ElyTable 3 毫秒;连续快速键入(脚本一次送出近百字)时,各次运行在 3 至 225 毫秒的中位之间摆动:新文档首次键入时内核有一次 150 至 230 毫秒的冷启动(首块图块与拼写等初始化),同一批字排在它之后;原为中位 1 秒、p95 1.9 秒。树莓派 3 未实测(本机无设备)。界面线程不碰 worker:应用对内核的读取、保存与另存为的导出、导出 PDF、比较、邮件合并都经 `Tabs::read_in`(或 `ely_lok::read_off`)在后台执行,回到窗口时按发起的标签与读取时的状态核对;保存导出期间标签状态为“正在保存”,同一标签的后续保存排队到它结束;另存为在写入前按文件身份再查一次目标是否已被别的标签持有;导出期间又有编辑时,“保存后关闭”不关闭,回到关闭询问;关闭标签时的撤销历史也在后台读;邮件合并预览与比较在发起时就保留自己的标签号与工作目录;读取失败不立即重试,等下一次状态变化;载入时一并在后台读版面与撤销历史(`ely_lok::layout`,`LokView::open` 接收版面)。窗口关闭回调先不放行,在后台逐个读标签的撤销历史并关闭(有改动的照常询问),最后一个标签关闭后移除窗口;内核文档的关闭也在后台(2026-10-06 起,原先同步读历史、在界面线程持 worker 锁关闭)。 -- 2026-10-04(用户定):八个应用的内容区一律随系统深浅色:Doc 页面、Table 网格、Show 幻灯片、PDF 页面、邮件正文、Mark 编辑区、Agent 输出。自动颜色随之翻转,文件里写明的颜色照原样;打印与导出 PDF 仍为浅色。各引擎(LOK、MuPDF、CEF)接入应用主题,不留引擎默认色。LOK:`ely.xcd` 定义 `COLOR_SCHEME_ELY_DARK` 与 `COLOR_SCHEME_ELY_LIGHT`(文档底色、自动文字色、网格、链接等取 Ely 默认色板;内核自带的深色方案颜色留空、按应用外观取默认值,传方案名不改外观,所以不用它),`LokView` 在主题深浅变化时经有序队列发 `.uno:ChangeTheme`(不算编辑),页面底色深色取 `surface`、浅色取 `paper`;Omarchy 色板与这两套固定色可能略有差别。导出 PDF 实测仍为白底。图块作废时旧图仍显示,新图到达后替换,编辑与主题切换都不闪。 +- 2026-10-04(用户定):八个应用的内容区一律随系统深浅色:Doc 页面、Table 网格、Show 幻灯片、PDF 页面、邮件正文、Mark 编辑区、Agent 输出。自动颜色随之翻转,文件里写明的颜色照原样;打印与导出 PDF 仍为浅色。各引擎(LOK、MuPDF)接入应用主题,不留引擎默认色。LOK:`ely.xcd` 定义 `COLOR_SCHEME_ELY_DARK` 与 `COLOR_SCHEME_ELY_LIGHT`(文档底色、自动文字色、网格、链接等取 Ely 默认色板;内核自带的深色方案颜色留空、按应用外观取默认值,传方案名不改外观,所以不用它),`LokView` 在主题深浅变化时经有序队列发 `.uno:ChangeTheme`(不算编辑),页面底色深色取 `surface`、浅色取 `paper`;Omarchy 色板与这两套固定色可能略有差别。导出 PDF 实测仍为白底。图块作废时旧图仍显示,新图到达后替换,编辑与主题切换都不闪。 - 2026-10-01:主题。`Appearance` 为系统、浅色、深色。系统档在有 Omarchy 时跟随 Omarchy(`ThemeSettings::omarchy_sync`)。读 Omarchy 的 `$HOME/.local/state/omarchy/current/theme/colors.toml`(Omarchy 写死此路径,不随 `XDG_STATE_HOME`),目录由 `ELY_OMARCHY_DIR` 可改。读法与回退链照 `reference/omarchy/bin/omarchy-theme-color`:松散引号、`colorN`、`purple`、`selection_background`、`theme_type`、`light.mode`、亮度和 382 比。缺的色相用 Ely 自己的色并警告。色板照组件库 `A11yChecker` 的规则收口:正文、状态色、代码色在各自的表面上 4.5:1,焦点与图表色在页面上 3:1;先拉近前景再拉到纯黑白,最后逐项核对,核对不过的主题整套忽略。切换时 Omarchy 先暂存 `next-theme` 再整体替换 `theme`,用文件监视跟随,不用它的钩子。新主题无 `colors.toml` 时退回 Ely 色板。`ELY_APPEARANCE`、`ELY_OMARCHY_SYNC`、`ELY_REDUCE_MOTION` 是覆盖开关,设置页接入前先用环境变量。 - 2026-10-05:组件库 8658999(组件库重写历史后的 main,加本项目三个提交):属主不经 `Close` 移除的浮层(一起移除的嵌套浮层同样,回到最外层打开前之处)(对话框动作只清自身状态、异步校验通过后才关)照样把焦点交回打开前之处;原先焦点落在根上,键入无处可去,须先点一下网格。同批带入上游的 ColumnShell、输入法组字时光标重新闪烁、`Icon::from_path`、MIT 许可。旧 pin 2f02c82 已不在 main 上,只能按完整哈希取得。`Select::label` 与 `TextInput::label` 给控件无障碍名称而不绘制;对话框里描述列表旁的控件以此命名,开关用自身的 `label`。7186179 加 `NumberInput::label`(经内部文本框的 `TextInput::set_label`),同批带入上游的 rustfmt 修正。80c46b1:`TemplatePicker` 的列表是一个 Tab 停靠点,上下、Home、End 移动,回车使用,行带列表项语义,搜索提示、全部、使用、无匹配的文案进内建中英目录。7df3045:`SelectableList` 在 AX 树里是列表(`label` 命名),`ListItem` 是列表项(标题为名,带说明、选中、在可选列表中的序号与总数);原先行无角色,不在 AX 树里。af54877:`NumberInput::blank` 显示占位文字而非数字(混合选区),空着回车或失焦不提交,键入后照常。408f2b0:`CommentSidebar` 与 `RelativeTime` 的文案进内建中英目录;属主不处理“解决”时(无 `on_resolve`)不分未解决与已解决,一列显示全部。bcf0378:`CommentSidebar` 的批注列是列表,每条是 Tab 停靠点(列表项语义,名为作者与正文,带选中、序号与总数),回车或空格选中。71bfc93:`init_for_tests`(`test-support` 下)装主题与键位、不要资源,应用单测可渲染 Ely 组件。617a8d6:`FindWidget` 在查询框里按 Esc 即关闭(调 `on_close`)。2ce3368:`ContextMenu` 可定宿主大小。3d08744:`DescriptionList` 的值占满所在列,其中的文本框随之全宽;同批带入上游的图表、分页改动。a6b1bb0:`SignaturePad` 的“在此签名”与“清除”进内建中英目录;同批带入上游 P01 至 P15(各组件的越界与失效状态记日志而不 panic)。490b022:`ChangesList` 分列冲突、已暂存、改动、未跟踪、已忽略(各有其动作:冲突与已忽略只可打开),标题与提示进内建中英目录;同批带入上游 P16 至 P20。7bb53f4(ElyTree 用):`ChangesList` 行数可缺、按区报按下、当前行;`DiffViewer` 外算的段、块按钮、可选的变动行、逐行读给辅助技术;`CommitList`、`FileHistory`、`BlameView`、`ThreeWayMerge` 只画可见行,`CommitList` 近尾加载;`DescriptionList::text`;`SegmentedControl` 各段为单选;`BranchList`、`TagList` 可选中、只画属主处理的行内动作;`marked`、`conflicts`、`resolve` 按标记宽度(`MARKER` 为默认 7),a284c71 起只按 LF 断行;c5799cc:`StashList` 文案进内建中英目录,行可选(`selected`、`on_pick`),只画属主处理的行内动作;7fab072 至 3741d5d:分支、标签、储藏的行分两行(上行名称与行内动作,下行说明与行尾文字),窄时名称与说明先让;行内动作一律显示(原先只在悬停时显示,键盘停在其上时看不见),改动列表同;各 git 行带透明的 1 像素边框,焦点框才画得出(原先 `focus_ring` 无边框宽度,键盘焦点不可见);同批带入上游 P21 至 P29。4f8f3bd:`Changed::lines` 可缺(二进制文件、文件夹不计行数,不显示);37c5187 缩短其注释。19b0b29:`DiffViewer::from_stretches`(别处算好的差异,如 Git 自己的)、`hunk_action`(每块标题上一个按钮)、`selected` 与 `on_line`(改动行可按,带 Shift),`diff::mark_words` 给外来的行标出改动的词,“行内”“并排”“N 行未改动”进内建中英目录;30fea9e:有块按钮时各行同高,底色相接。edd2c12:`ChangesList` 每次按下带分区(`ChangeSection`),行是 Tab 停靠点(列表项语义),`selected` 标出当前文件。2e080b8:`SegmentedControl` 每段是单选按钮(名为其文字,带选中状态),原先不在 AX 树里。e226d79:差异查看器的行模型与配色移入 `git/rows.rs`(文件不超过 500 行);7b807a9 缩短其注释。51d2aea:`CommitList`、`FileHistory`、`BlameView` 只画视口内的行(`uniform_list`,填满所在的框;`CommitList` 收 `Rc` 的提交与 `lanes` 算好的图,`on_end` 在接近末尾时要下一页),各行是 Tab 停靠点(列表项语义,选中状态;Blame 每段首行可按,名为作者、时间与摘要),`FileHistory` 的行数可缺;`CommitInput` 的“提交到”“修改最近提交”进内建中英目录。e6220ad 与 3fc5a46:提交行窄时作者、提交号与时间先让,摘要至少 6rem。d9cbe99:`DescriptionList::text`(标签与文字值,行的无障碍名为“标签: 值”),提交行的无障碍名带上引用。fb7ee42 与 fcc45cc:`DiffViewer` 只读时每行也是标签(“行号 + 文字”),块头为标题,折叠行有名;`BlameView` 每行为“行号 文字”,段首按钮的名略去空的部分。3d0a3de 至 949d776:差异与内建目录的注释缩短。829ef68:`ThreeWayMerge` 只画视口内的行(填满所在的框;每行三侧各一行,冲突处一行带三个选择按钮,各行同高;不再附结果与统计,结果由属主显示),文案进内建中英目录,每行与冲突行在 AX 树里有文字;`merging::conflicts` 认 diff3 的 `|||||||` 段,`resolve` 取当前侧时不带基底段;`BranchList` 与 `TagList` 的行可选(`selected`、`on_pick`,Tab 停靠点、列表项语义)。63d4a45 至 922cd53:`BranchList` 只画属主处理的行内动作;`merging::marked` 按标记文字里 Git 的分组得出各段(diff3 段为基底);窄时行尾文字先让,行内动作不被挤出。 2ce3368:`ContextMenu` 实现 `Styled`,样式定其宿主大小,其中的虚拟列表才能撑满(原宿主无尺寸,`uniform_list` 高度为零)。 - 2026-10-05:ElyTable 透视表与图表(C09.4,TABLE-10、11)。补丁 `0019-lok-calc-pivot-tables.patch` 与 `0020-lok-calc-charts.patch`:`.uno:DataPilotExec` 收 `Pivot`(JSON:源区域、行、列、筛选、值字段与函数 sum/count/average/max/min),光标在透视表上即原地修改,否则从源区域在新工作表创建;行、列、筛选里的空名表示内核的数据布局字段(多个值字段横排或竖排的位置),读出与写回都保留,已在表中的行、列、筛选字段保留各自的小计函数,读出的布局原样写回不改表;照内核布局对话框的步骤(`ConvertOrientation`,保留各字段的成员、排序与版式),新表的筛选按钮、钻取、展开收起都关(同对话框初值);放到新工作表时插表与透视表是两步撤销(同桌面版)。刷新 `.uno:RecalcPivotTable` 在内核里本就不进撤销(它改缓存)。`.uno:InsertObjectChart` 收 `Chart`(JSON:类型 column/bar/line/scatter/pie、标题、图例、X/Y 轴标题;`other` 保持原类型),LOK 下不开图表向导、插入为一步撤销、插入后图表选中;给出的区域每段都要解析得了、所指工作表都要存在,否则插入与修改都拒绝、文档不动(内核原会改用示例数据,修改时 `ChartModel::setArguments` 抛出未捕获的异常使 worker 退出);`.uno:ChangeChartData` 也收 `Chart`,区域与外观合成一步撤销(撤销保存整个图表的图示与主标题的副本,堆积等外观以外的设置一并恢复)(列表动作带当前视图号,否则 LOK 拒绝撤销别的视图的动作)。读取:`.uno:Chart`(选中图表的对象名、区域、表头、外观)、`.uno:PivotTable`(光标处透视表的名称、位置、源区域、字段与布局),`.uno:SelectedRange` 加 `data`(选区内数据的绝对区域)。界面在“插入”菜单:数据透视表、刷新数据透视表、图表;对话框在光标处有透视表、或有选中图表时为修改,否则以选中的数据(单格扩到数据区)新建,源与图表区域取 `data`,不含空行。为对话框发起的读取(`Tabs::ask_in`)回来时,发起的工作簿已不是当前标签即丢弃,不为隐藏的工作簿开对话框(ElyTable 全部对话框、合并询问)。没有数据时恢复原光标(`select.data` 会把单格标成区域,回车随之只在格内打转)。公式语法是 Excel A1,区域写 `Sheet1!$A$1:$C$5`。 @@ -229,13 +221,12 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-06:ElyShow 输出与演示检查(C10.6,SHOW-13、14,QA-12)。补丁 `0037-pdf-impress-handouts.patch`:PDF 导出的 `ExportHandouts`(每页张数 1、2、3、4、6、9)照打印的讲义版式(`SdPage::CalculateHandoutAreas`,3 张带笔记线)把 `PageRange` 内未隐藏的幻灯片排到讲义页上;其他张数不出页,导出失败。讲义版式会改文档的讲义页,所以讲义与图片都在另存的 ODP 副本上做(`output::on_copy`,做完关闭并删除)。`Pdf::handouts` 经 worker 的 `EXPORT_PDF` 传入。导出对话框:内容 PDF、图片、讲义,范围全部、当前、从–到,讲义每页张数(默认 3);打印对话框:幻灯片或讲义,生成同一 PDF 后交系统打印面板。图片每张未隐藏的幻灯片一个 PNG,宽 1920,副本设浅色方案,文件名 `名称-序号.png`,所选文件夹里任一目标已存在即整体不写。PPTX/ODP 由保存与另存为负责。补丁 `0038-lok-impress-slide-check.patch`:读 `.uno:SlideCheck`(每张幻灯片与母版:文字所用字体族、链接图片与 OLE 链接及是否缺失、内核不能显示的对象:读不出的图片、无数据的 OLE;缺失的链接图片只作为链接报一次)。演示检查以内核报的缺失字体(`FONTS_MISSING`)为准,按所用字体定位到幻灯片或母版;媒体缺失与媒体链接取自 `.uno:SlideShow`;点一行转到所在第一张幻灯片。系统打印面板只经 `ely_office::print_later` 打开:它在前台任务里、GPUI 放开应用之后运行(面板的模态循环会回调 GPUI,在实体更新里打开即借用冲突崩溃),三个应用共用。图片输出先以 `create_new` 占下全部目标(不跟随符号链接),任一冲突或失败都删去本次占下的文件。输出与检查的对话框由后台读取打开,回来时须仍是最近一次请求(`Deck::ask_dialog`、`dialog_due`)且没有别的对话框;对话框种类或当前标签一有变化,此前的请求即作废(`watch_dialogs`,随文档通知)。图片的张数与隐藏取自写出的副本本身;PNG 写完显式 flush。读不出的图片按载入后的状态判定(`NONE` 或解码失败的 `Default` 占位);OLE 链接检查源文件是否在。 - 2026-10-06(用户定):ElyPDF 签名与权限。验证签名时证书信任取操作系统的信任库(macOS 钥匙串的信任设置;Windows、Linux 随其构建)。可视签名三种来源:手写、键入姓名、图片文件;可视签名不是数字签名。只凭用户密码打开、带所有者限制的文件照限制办(打印、复制、编辑等被拦并说明原因),输入所有者密码可解除;改保护须所有者权限。 - 2026-10-06(用户定):OCR 随包语言数据用 tessdata_fast,英文、简体中文、繁体中文(eng、chi_sim、chi_tra)。 -- 2026-10-06(用户定):ElyMail 在八个应用里最后做:任务表中 C12 移到 C15 之后、C16 之前;CEF 内容宿主随 ElyMark 先建(C14.0),邮件编号依次前移。 +- 2026-10-06(用户定):ElyMail 在八个应用里最后做:任务表中 C12 移到 C15 之后、C16 之前;邮件编号依次前移。 - 2026-10-06(用户定):`ely-office` 的外壳不再只服务 LOK:`Host` 带关联类型 `Engine`(`ely_office::Engine`:后台载入、视图、撤销历史的标记、写出、关闭,视图的落定、编辑计数、待落地、焦点与事件),`Tab` 与 `Core` 按引擎取文档、视图与 worker 类型。LOK 的实现在 `ely_office::lok`(`Lok`、`start`、只对 LOK 的 `Commands` 即 `run_active`/`run_in`;查找替换、面板、图表仍只限 LOK);MuPDF 的在 `apps/ely-pdf/src/engine.rs`(`Pdf`;worker 首次载入才起,`Mupdf::get`)。`Kind::macros` 为 `None` 表示该格式无宏,不扫文件。ElyPDF 每个标签一个 `PdfView` 实体(页面、图块、文字、选择、找到的词都在里面,自己绘制),`Viewer` 只管标签条、书签栏、查找框与状态栏;打开、密码、关闭询问与保存走外壳。外壳的密码框:对话框开着时后到的加锁文件排队(`Core::locked`),对话框关闭(含取消、提交)后下一个再问;每次询问一个身份(按输入框实体),连续询问的 Esc 各归各的;提交时对话框先关、再载入。ElyDoc 的加锁测试原先清的是应用自己的对话框字段,改为清外壳的。 -- 2026-10-07:本地 Web 内容宿主(C14.0,§17.3)。`crates/ely-web-content`:CEF 154.0.32(Chromium 154.0.8037.58,cef-rs a712ad9,`default-features = false`),`scripts/fetch-cef.sh` 取 minimal 包到 `engines/cef`,`.cargo/config.toml` 的 `CEF_PATH` 让构建用它。进程:应用的 `main` 先调 `ely_web_content::subprocess()`(参数里有 `--type=` 即为 CEF 子进程,载入框架后 `execute_process`,不碰 GPUI);开发构建子进程即应用本身的二进制;macOS 包里是五个辅助应用(`<二进制名> Helper`、`(GPU)`、`(Renderer)`、`(Plugin)`、`(Alerts)`,`Contents/Frameworks`,`LSUIElement`,可执行文件为 `ely-web-helper`),框架在 `Contents/Frameworks`,`packaging/macos/bundle.sh` 对依赖 ely-web-content 的应用自动放入并先签内层;查找次序 `ELY_CEF`、包内、辅助应用所在、构建时的 `engines/cef`。启动:`start` 在 GPUI 里只做准备(载入框架、给 NSApp 的类补上 Chromium 要的 `isHandlingSendEvent`、`setHandlingSendEvent:` 与包住 `sendEvent:`,并加 `CrAppProtocol`、`CrAppControlProtocol`),`initialize` 与建浏览器都在消息泵任务里、GPUI 的更新之外进行(实测:在更新里同步调 CEF,CEF 转动 AppKit 的循环,GPUI 报 RefCell 已借用、整窗不画);浏览器在泵里同步建(`browser_host_create_browser_sync`;原为异步,建好之前视图已释放时浏览器无人关闭),视图要么在建之前已走(不建),要么有浏览器可关。凡在 GPUI 里对 CEF 的调用(输入、焦点、尺寸、脚本、缩放、重新载入、关闭)都排进主线程的队列,由泵在下一轮、更新之外依次执行(实测:在实体释放时同步关浏览器,CEF 转动 AppKit 的循环去校验菜单,GPUI 的菜单回调遇已借用的应用状态而中止,约三成复现)。泵:外部消息泵,按 `on_schedule_message_pump_work` 的时间,另照 cefclient 每轮后以 1/30 秒兜底(实测:只按请求时第一轮后 CEF 再不请求,页面不载入);空闲时 CPU 约 1.5%。离屏渲染须用 Alloy 风格(`runtime_style`;Chrome 风格不出画面)。同一缓存目录只允许一个实例,第二个启动时报出占用它的进程号。画面:`on_paint` 的 BGRA 直接成 GPUI 图像,上一帧在下一次绘制时从图集释放(`drop_image`);光标随页面。输入:鼠标点击、移动、滚轮(行按 40 点),键照 cefclient 的 macOS 做法:键盘按下带字符与系统键码发 `KEYDOWN` 再发 `CHAR`(退格、回车、方向等用 AppKit 的字符),可打出字的键等 GPUI 的输入法回调后以所打的字发同样两个事件,多字或组字经 `ime_commit_text`/`ime_set_composition`(无替换范围时用 CEF 的“无范围”,两端为最大值;传空指针 CEF 不收);松开带同一字符(不带时 Chromium 当作又按一次,实测退格删两个字);拷贝、剪切、粘贴、全选走框架命令。焦点随 GPUI 的焦点给页面。页面与应用之间:CEF 的消息路由,页面调 `window.elyQuery({request, onSuccess, onFailure})`(`onSuccess` 与 `onFailure` 都必须有,缺一路由即不发;页面载入时路由未就绪,早到的查询丢失),应用收到 `Event::Query(请求, 回复)`;应用以 `run_js` 调页面。主题:每次载入与主题变化时把调色板、代码色、字体与字号比例、深浅写成 `:root` 的 `--ely-*` 变量与 `color-scheme`。缩放 `set_zoom(倍数)`。页面只许载入所在内容文件夹里的文件:按真实路径判定(`..`、百分号转义、指向外面的符号链接都算外面,文件须存在),导航与每个资源请求(脚本、图片、样式、请求等)都过这道门,资源另许 `data:` 与本页的 `blob:`,其余一律取消;另在每个 CEF 进程以 `--proxy-server=127.0.0.1:9` 与 `--proxy-bypass-list=<-loopback>` 把一切网络(含本机回环与 WebSocket,后者不经资源回调)送往不通的代理;WebRTC 走 UDP 不经代理,故另以 `--force-webrtc-ip-handling-policy=disable_non_proxied_udp` 只许它经代理,并在每个页面上下文建立时、页面脚本之前删去 `RTCPeerConnection` 等构造(渲染进程的 `on_context_created`);页面的地址由真实路径逐字节转义生成(文件名含 `#`、`?` 照样载入);用户点出的链接以 `Event::Leave` 交给应用;`WebView::open_inert` 开的页面不运行脚本(`BrowserSettings.javascript` 关),ElyMark 的打印与导出 PDF 暂存页用它,文档里的 HTML 脚本不运行(M-15);页面的警告与错误只记行号(警告文字与来源都可由文档伪造,不进日志);页面自己的诊断经 `elyQuery` 交应用记录。载入失败(主框架,停止不算)、渲染进程退出、CEF 起不来都报 `Event::Failed`,页面区改显示组件库的错误视图与“重新载入”(浏览器在即重载,否则重建;CEF 起不来时只说明),载入成功才报 `Event::Loaded`。浏览器关闭时断开它与表面的互相持有,页面的内存随之释放(释放时写日志)。真窗口(macOS,开发构建与 `dist/ElyMark.app`):页面以应用深色主题画出,点选、键入中英文、回车、退格、方向键、拷贝粘贴、`elyQuery` 往返;`tests/scripts/webview.sh` 以输入法回调组“你好”、键入、读回焦点、主题变量与缩放 1.25,核对页面跳不出所在文件夹、外面的脚本不执行、本机 HTTP 收不到请求、`#` 名的页面载入、载入前被删的页面报失败、开关 20 个页面、5 个在建浏览器之前丢弃、10 个在打开后 0 至 27 毫秒内丢弃,都在保持前释放内存、杀掉渲染进程后页面区不再显示旧画面、经辅助功能按下“Reload”后页面重新画出(截窗口量标记色),WebRTC 不存在且本机 UDP 收不到 STUN,并截窗口量页面画出的标记色(去掉绘制、去掉断开持有、去掉代理或不显示故障时该脚本都失败);需屏幕录制与辅助功能权限,缺即报失败。未验证:Windows 与 Linux;辅助技术(见缺口);树莓派(R-06)。 - 2026-10-07:ElyTree 恢复与清理、外部工具、操作规则(C13.6,TREE-13、14,§13.3,QA-19、20)。仓库页第二行:在终端中打开(macOS `open -a Terminal`,其他平台报未构建)、在访达中显示(`reveal_path`)、清理未跟踪的文件…;页底“引用日志”列出 HEAD 最近 200 次移动(`reflog show -z`,位置、Git 写的原因、时间),选中即中间显示该提交(同提交详情,`Whose::Reflog`),上方“在此新建分支…”(找回删掉的分支)与“把当前分支移到此…”:有已暂存的改动即拒绝(`reset --keep` 会丢掉它们,请先提交或取消暂存);否则在后台算出离开该分支的提交,对话框写明分支、从哪到哪、离开的提交(引用日志仍可找回)与“工作区改动保留”,确认后 `reset --keep`(Git 在改动冲突时拒绝;没有任何按钮做硬重置),执行前在写队列里核对 HEAD 仍在预览时的分支(完整引用名,或仍游离)与提交、仍无暂存,任一变了即什么也不做。引用日志的时间是该次移动的时间(`--date=unix` 下的 `%gd`),不是提交时间;选中按事件本身(提交、原因、时间与它在全部引用日志中自最早起的序号,后者新移动不改,同一秒同原因的两次也分得开),同一提交的多条不会一起选中;之后有新的移动时该事件的位置后移,选中与详情跟着它,事件已不在引用日志里即不显示。清理:对话框先由 `git clean -n -d`(勾“包括被忽略的文件”另加 `-x`)得出 Git 会删除的路径(`core.quotePath=false`,其余 C 引号照 Git 规则还原),其中的文件夹当场展开为其中的文件(`ls-files --others`,同一忽略规则,另一仓库不入),对话框逐个列出文件与文件夹;确认后先逐段查每个所列路径:仍在仓库里、途中没有符号链接、文件仍是文件、文件夹仍是文件夹(任一变了即整体拒绝,什么也不删),再以 `git clean -f [-x] -- 所列文件` 删去(只给文件,Git 不递归;此后被跟踪了的文件 Git 留下,也不穿过链接),最后自深而浅删去所列文件夹中已空的(预览后在其中新出现的文件不动,文件夹随之留下),每次都问。文件标题栏多出“在编辑器中打开”(用户在“仓库›选择编辑器…”选的程序,存在数据目录 `tools.json`,字段类型不对与读不出同样报错且不覆盖;macOS 的 .app 经 `open -a`,在后台等它结束,失败即显示其原话;未选时先问)与 Markdown 的“在 ElyMark 中打开”(`ely_platform::apps::open`:按安装布局找同级的应用,macOS 的 `名称.app/Contents/MacOS`,Windows 的 `Ely Office\名称`,开发构建的同一目录;找不到即报错)。锁(§13.3):任何写操作因 Git 的锁文件失败(`….lock': File exists`,或配置文件的 `could not lock config file …: File exists`)时,对话框说明另一个 Git 正在工作、Ely 从不删除锁文件,“重试”把同一写操作重新排队。QA-19:测试都以独立的 Git 命令核对索引、提交、引用与工作区。QA-20:冲突、钩子失败、凭据被拒(本机 401 服务器,显示 Git 的 “terminal prompts disabled” 原话)、锁、变基中断后中止、取消(停止取回)都有窗口测试,结束时仓库状态以 Git 读回核对。切换到系统 Git 目前经 `ELY_GIT`(设置页随 C18.1)。真窗口(macOS):仓库页工具行、清理预览对话框、文件标题栏的编辑器与 ElyMark 按钮,按下后同级的 ely-mark 以该文件启动。未验证:Windows 与 Linux 的终端;ElyMark 打开文件的内容(C14)。 - 2026-10-07:ElyTree 储藏、远程、工作树、子模块、LFS(C13.5,TREE-10 至 12,§13.3)。左栏第四页“仓库”:上方取回(`fetch --progress --all`)、拉取(`pull --progress`,照用户的 pull 设置)、推送、强制推送…;储藏(`stash list -z`,新的在前;“储藏改动…”问说明与是否包括未跟踪的文件,`stash push`),远程(名称与地址,`remote` 加 `config --get-regexp` 读出取回与推送地址),工作树(`worktree list --porcelain -z`,行名为文件夹名,有失效的可“清理”即 `worktree prune`),子模块(索引里的 gitlink 与文件夹自身 `.git` 的 HEAD:未检出、在记录的提交上、在别的提交上、冲突),LFS 文件(`git lfs ls-files --long --json`:工作树里是内容、对象在本机而工作树是指针、只有指针而对象不在本机);没有子模块或 LFS 文件时不列。各列表分别读出,一个读不出只在该处显示 Git 原话;切到此页时读,此后每次状态重读时重读,只取最新一次。选中一项,中间显示:储藏为提交详情的同一套(`hist::Picked`,历史与储藏共用读取与面板):改动的文件(对第一个父提交)与储藏的未跟踪文件(第三个父提交,各自对空树),选文件看差异,上方应用、弹出、删除…;远程为地址与推送地址,取回、修改…(改名 `remote rename`、改地址 `remote set-url`)、移除…(先问);工作树为路径与分支或提交、锁定与失效原因,打开(作为标签)、移除…(先问,主工作树与当前的不给);子模块为记录的与检出的提交,“检出记录的提交”(`submodule update --init`,经远程运行)与打开。用户输入的名称、地址、路径前一律加 `--`(推送的远程名同样)。表单与选择对话框(新建分支、标签、改名、身份、储藏、远程、工作树、选远程与上游)记下打开时的仓库,提交时写进那个仓库,不随当前标签变;储藏列表按每条三个字段读(说明可为空)。储藏的应用、弹出、删除按 `stash@{n}` 执行,执行前在写队列里核对它仍是所见的那个储藏(另有储藏插入即拒绝,什么也不做);弹出遇冲突时 Git 保留储藏,对话框显示 Git 原话。删除储藏、移除远程与工作树每次都问。分支页的本地分支多出“设置上游…”(列出远程分支,`branch --set-upstream-to=refs/remotes/名`)与“取消上游”。推送:有上游即 `push --progress`;没有上游时后台读出远程,问推到哪个远程(`push --set-upstream -- 远程 refs/heads/分支:refs/heads/分支`),没有远程或 HEAD 游离即说明。强制推送(§13.3):后台读出当前分支、上游的远程与远端分支、上次取回时远端的提交(租约)、此刻 HEAD 的提交(源)与远端有而源没有的提交,对话框写明远程、分支与将被覆盖的提交(或写明没有),确认后推送的就是所示的源提交(`push --force-with-lease=远端分支:租约提交 -- 远程 源提交:远端分支`),确认前本地分支再动也不改变推送内容;远端自上次取回后变了即被拒绝;没有上游或游离即说明不能。远程命令(取回、拉取、推送、子模块更新)是写队列里的一种(`Write::Remote`),经 `running::Running` 运行:Git 在自己的会话里(`setsid`,SSH 找不到终端,不会挂在终端提示上;凭据照用 credential helper 与 ssh-agent),进度逐行显示在中间上方的提示条,可停止(向整个进程组发 SIGTERM,Git 自行清理;已结束的不再发);用户停止时提示“已停止”、清掉其后的排队写,不当作失败;失败时对话框显示 Git 的 stdout 与 stderr(以回车结尾的进度行被下一行改写,只留最后一次;以换行结尾的行全部保留,钩子的每一行都在);拉取遇冲突照合并类命令判定为停下。关窗与退出时停下所有远程命令与克隆。克隆改用同一运行器。真窗口(macOS):仓库页五类列表、储藏详情与文件、工作树详情、分支行。未验证:Windows 与 Linux(停止远程命令在非 Unix 上报未构建);真实网络与 HTTPS、SSH 凭据;LFS 对象的下载(只显示状态)。 - 2026-10-07(用户定):跟随组件库最新的 main。组件库 K01 把 gpui 从 zed 仓库的提交换成 crates.io 的 `gpui-pre` =0.3.8(`gpui-pre-platform` 同版本),K02 让 Tab 绑在 FocusScope 的 ElyFocus 上下文下、更深的上下文先得;本项目工作区的 `gpui`、`gpui_platform` 随之改为这两个包(以 `package =` 保留原名,源码不变),组件库 pin 到 dabc1eb(其上加本项目一个提交:树与树行的无障碍角色)。升级后全工作区编译无改动。 -- 2026-10-08:首版打包(R1.1)。图标(用户定:logo-design 技能出方案,选 C“实心几何”;macOS 标准 squircle,Linux 与 Windows 无底形,八个同一套设计):母版在 `assets/icons`(我按圆角矩形、圆与 30°/45° 斜线在 256 网格上画;每个颜色对白与 `#1E1E1E` 都不低于 3:1,ElyMark 原定的石板灰在深色上 1.75:1,改为琥珀 `#B87A00`;ElyTable 在 48 像素以下用简化的一版),`packaging/icons/build.sh` 以 AppKit 渲染出 icns(macOS)、512 像素 png(Linux 的 AppImage 与桌面文件)、16 至 256 像素 ico(Windows 安装程序、开始菜单快捷方式与卸载项);生成物入库,Linux 与 Windows 构建不需要 macOS。系统交给应用的文件:`ely_ui::run` 收下启动参数与 `on_open_urls`(访达的打开请求,启动前到达的也在内),应用先取 `launch_files`(有则不开空白文档;ElyMark 有则不恢复上次的标签),再以 `on_open` 处理运行中的请求;六个应用都走这一条(ElyDoc、ElyTable、ElyShow 原先不收启动参数)。macOS 包:LOK 应用只带内核,按 LibreOffice 自己的布局并入本应用:内核的库进 `Contents/Frameworks`(合并库构建留下的“invalid - merged lib”占位文件不拷,其余非代码文件即报错),内核的资料进 `Contents/Resources`,另有 `MacOS/ely-lok-worker`;`install_dir` 先找包内(`Frameworks/libmergedlo.dylib`)。不带 `LibreOffice.app` 本身:原先整份嵌入时,其快速查看与聚焦插件、`soffice` 等启动程序与文档类型被系统登记,系统设置里出现“LibreOffice 扩展”(用户发现;已注销本机七处登记);测试也不再运行 LibreOffice 应用(样本改为 `tests/fixtures/office` 的固定文件,运行开发构建的 `soffice` 会让系统重新登记它)。ElyPDF 带 `MacOS/ely-pdf-worker` 与 `Resources/tessdata`(三种语言),OCR 数据先找应用旁再找包内;ElyTree 带 `Resources/git`,其中每个 Mach-O 先签(Git 以 `INSTALL_SYMLINKS` 构建,内建命令是指向同一程序的相对链接;原先是硬链接,拷进包后变成约 150 份副本、689 MB;以 `MACOSX_DEPLOYMENT_TARGET=14.0` 构建,原先随本机 SDK 要求 macOS 27);ElyMark 用同级 ElyTree 包里的 Git。文档类型(`packaging/macos/documents.sh`)一律 `Alternate`,不抢系统默认:Doc 为 docx、odt、doc、rtf;Table 为 xlsx、ods、xls、csv、tsv;Show 为 pptx、odp、ppt;PDF;Markdown;Tree 为文件夹(查看者)。worker、内核、OCR 数据、CEF 与页面的所在路径都写日志,`tests/scripts/release-macos.sh` 据此核对都在包内。许可(§21.2、§21.3):每个包的 `Resources/Licenses` 有 Ely 的 AGPL 全文(仓库根 `LICENSE`)、`rust-crates.txt`(`packaging/crates.py`:本机目标下链进各二进制的全部 crate 与其许可文件,无许可文件的取 `packaging/licenses` 里该表达式的标准文本并写明作者,都没有即失败)与所带引擎的许可:LibreOffice 的 LICENSE、NOTICE、CREDITS;MuPDF 的 COPYING 与其编进去的每个第三方及每组内嵌字体(按构建实际嵌入的)的许可、OpenSSL、tessdata(Apache-2.0,按提交与散列取)、Great Vibes 的 OFL;Git 的 COPYING 与 LGPL-2.1、git-lfs 的 LICENSE.md(发布包里没有,按标签与散列取);CEF 的 LICENSE 与 Chromium 的 CREDITS、页面的第三方通知;缺任何一份即打包失败(`packaging/licenses.sh`)。`release.sh` 另出 `ElyOffice-<版本>-source.tar.gz`:自有源码、锁文件、上游补丁与构建脚本(上游各自钉在提交,取得方式即其中的 fetch 脚本)。正式签名(hardened runtime)时 CEF 的渲染与 GPU 辅助程序带 `allow-jit`,插件辅助程序带 `allow-unsigned-executable-memory` 与 `disable-library-validation`(`packaging/macos/entitlements`;原先不带,V8 保留不到代码区,页面载入失败);`tests/scripts/hardened-macos.sh` 以 `ELY_SIGN_IDENTITY` 签 ElyMark 并核对页面画出。ElyTable 同时收到多个文本文件(访达一次交来多份 CSV)时,读完的导入排队,没有对话框开着才逐个询问(原先后到的一份替换前一份,前一份静默丢失)。`packaging/macos/release.sh` 按 `apps.tsv` 的批次出各应用的 dmg;本机只有 Apple Development 证书,无 Developer ID,未公证。未验证:Intel 包(只建了 arm64)、公证、Linux 与 Windows 的包(本机不能建)。 +- 2026-10-08:首版打包(R1.1)。图标(用户定:logo-design 技能出方案,选 C“实心几何”;macOS 标准 squircle,Linux 与 Windows 无底形,八个同一套设计):母版在 `assets/icons`(我按圆角矩形、圆与 30°/45° 斜线在 256 网格上画;每个颜色对白与 `#1E1E1E` 都不低于 3:1,ElyMark 原定的石板灰在深色上 1.75:1,改为琥珀 `#B87A00`;ElyTable 在 48 像素以下用简化的一版),`packaging/icons/build.sh` 以 AppKit 渲染出 icns(macOS)、512 像素 png(Linux 的 AppImage 与桌面文件)、16 至 256 像素 ico(Windows 安装程序、开始菜单快捷方式与卸载项);生成物入库,Linux 与 Windows 构建不需要 macOS。系统交给应用的文件:`ely_ui::run` 收下启动参数与 `on_open_urls`(访达的打开请求,启动前到达的也在内),应用先取 `launch_files`(有则不开空白文档;ElyMark 有则不恢复上次的标签),再以 `on_open` 处理运行中的请求;六个应用都走这一条(ElyDoc、ElyTable、ElyShow 原先不收启动参数)。macOS 包:LOK 应用只带内核,按 LibreOffice 自己的布局并入本应用:内核的库进 `Contents/Frameworks`(合并库构建留下的“invalid - merged lib”占位文件不拷,其余非代码文件即报错),内核的资料进 `Contents/Resources`,另有 `MacOS/ely-lok-worker`;`install_dir` 先找包内(`Frameworks/libmergedlo.dylib`)。不带 `LibreOffice.app` 本身:原先整份嵌入时,其快速查看与聚焦插件、`soffice` 等启动程序与文档类型被系统登记,系统设置里出现“LibreOffice 扩展”(用户发现;已注销本机七处登记);测试也不再运行 LibreOffice 应用(样本改为 `tests/fixtures/office` 的固定文件,运行开发构建的 `soffice` 会让系统重新登记它)。ElyPDF 带 `MacOS/ely-pdf-worker` 与 `Resources/tessdata`(三种语言),OCR 数据先找应用旁再找包内;ElyTree 带 `Resources/git`,其中每个 Mach-O 先签(Git 以 `INSTALL_SYMLINKS` 构建,内建命令是指向同一程序的相对链接;原先是硬链接,拷进包后变成约 150 份副本、689 MB;以 `MACOSX_DEPLOYMENT_TARGET=14.0` 构建,原先随本机 SDK 要求 macOS 27);ElyMark 用同级 ElyTree 包里的 Git。文档类型(`packaging/macos/documents.sh`)一律 `Alternate`,不抢系统默认:Doc 为 docx、odt、doc、rtf;Table 为 xlsx、ods、xls、csv、tsv;Show 为 pptx、odp、ppt;PDF;Markdown;Tree 为文件夹(查看者)。worker、内核、OCR 数据的所在路径都写日志,`tests/scripts/release-macos.sh` 据此核对都在包内。许可(§21.2、§21.3):每个包的 `Resources/Licenses` 有 Ely 的 AGPL 全文(仓库根 `LICENSE`)、`rust-crates.txt`(`packaging/crates.py`:本机目标下链进各二进制的全部 crate 与其许可文件,无许可文件的取 `packaging/licenses` 里该表达式的标准文本并写明作者,都没有即失败)与所带引擎的许可:LibreOffice 的 LICENSE、NOTICE、CREDITS;MuPDF 的 COPYING 与其编进去的每个第三方及每组内嵌字体(按构建实际嵌入的)的许可、OpenSSL、tessdata(Apache-2.0,按提交与散列取)、Great Vibes 的 OFL;Git 的 COPYING 与 LGPL-2.1、git-lfs 的 LICENSE.md(发布包里没有,按标签与散列取);ElyMark 带 RaTeX 嵌入的 KaTeX 字体的 OFL 与说明;缺任何一份即打包失败(`packaging/licenses.sh`)。`release.sh` 另出 `ElyOffice-<版本>-source.tar.gz`:自有源码、锁文件、上游补丁与构建脚本(上游各自钉在提交,取得方式即其中的 fetch 脚本)。ElyTable 同时收到多个文本文件(访达一次交来多份 CSV)时,读完的导入排队,没有对话框开着才逐个询问(原先后到的一份替换前一份,前一份静默丢失)。`packaging/macos/release.sh` 按 `apps.tsv` 的批次出各应用的 dmg;本机只有 Apple Development 证书,无 Developer ID,未公证。未验证:Intel 包(只建了 arm64)、公证、Linux 与 Windows 的包(本机不能建)。 - 2026-10-08(用户定):深色主题不用纯黑或近黑,底色取柔和的灰 `#252525`。组件库 fd0efc7:深色色板 bg `#252525`、surface `#2c2c2c`、sunken `#202020`、overlay `#323232`、hover `#383838`、active 与 border `#3f3f3f`、border_strong `#555555`;为让说明文字在较亮的 hover 与 active 上仍有 4.5:1,muted 提到 `#c4c2c0`、subtle 与代码注释提到 `#acaaa8`;色盲友好图表的紫与橙改为 `#9d8dec`、`#ffd93d`(3:1 且各色两两分得开)。`ely.xcd` 的深色方案随之:页面 `#2c2c2c`、页外 `#252525`、边界与网格 `#555555`、域底纹 `#3f3f3f`。跨应用同步外观、设置里的浅色/深色/跟随系统、Omarchy 下随 Omarchy 主题,本来已有(共用 `settings.json` 加文件监视;`settings.sh` 核对 ElyPDF 选深色后 ElyTree 即刻变深);Omarchy 的读法与 OmaPhoto 相同(`~/.local/state/omarchy/current/theme/colors.toml`、`mode` 与具名颜色)。 - 2026-10-08:图像的显存与内存(C20.2 长时测试查出)。GPUI 把每张画过的 `RenderImage` 留在窗口的图集里,直到调 `window.drop_image`;LOK 的图块、PDF 的图块与缩略图、演示的缩略图、动画预览与放映图层原先都不释放,ElyDoc 编辑保存 60 轮后应用进程涨到约 2 GB(内核不涨)。组件库 `primitives::Retired`:不再画的图先交给它,下一次绘制时释放(`release` 只放别处已不持有的,`release_all` 给自知不再画的属主,`append` 接过关闭者的);LokView、PdfView 在替换、挤出、清空图块时交出旧图,绘制前释放;ElyShow 每个标签一份,关闭的标签与结束的预览交给 Deck;放映按窗口各一份(观众屏、演讲者视图),离场中的幻灯片还在画时其图层等它结束。修后同样 100 轮在 340 至 440 MB 之间起落。 - 2026-10-08:性能基准(C20.1,§20.1)。`tests/perf/bench.py` 在发行构建上测 PERF-01 至 08(每项 30 次,P50、P95,含全部子进程的内存),样本由 `tests/perf/samples.py` 按固定种子生成到 `engines/bench`,记录写到 `docs/perf/<日期>-<机型>.md`(格式见其 README);`ELY_STALLS=1` 开主线程忙碌记录(只供测量,空闲的应用照样不做事)。实测改动:Doc、Table、Show 原先要等内核启动完(约 2.5 至 3.9 秒)才开窗口,ElyDoc 日常启动 P95 3.9 秒不达标;现 `Worker::launch` 只起进程,内核的版本与图块核对在第一个请求前做(`Worker::ready`,任何请求都先过它,未核对的 worker 收不到请求),`ely_office::start` 在后台核对并设作者名,失败即 `startup::fail`;窗口约 0.15 秒可操作,空白文档在内核首次载入后出现(中位约 2.7 秒,其中约 2.2 秒是内核首次载入文档)。启动参数改为按当前目录取绝对路径(原相对路径成了 `file://相对` 而打不开)。补丁 `0039-skia-macos-no-unused-font-manager.patch`:macOS Skia 首次画字时以全部系统字体再建一个无人使用的 `SkFontMgr`(约 370 个字体枚举加排序,主线程约 175 毫秒),新文档的第一个字与打开文档的首块带字图块因此迟到;去掉后 PERF-03 的 10 次对比 P95 由 2.6 秒降到 1.5 秒。PERF-02 的计时原以输入落地时的代际为基准,内核的作废事件先于落地回报到达时,该次编辑的图块不计,钟一直走到下一次按键(记下的是按键间隔约 250 毫秒);现按编辑的修订认定(`view/clock.rs`:修订不低于该编辑的批次所升的代际,其图块即为所求),实测中位 2 毫秒、P95 5 毫秒。PERF-08:关闭的标签原把其图块留在 GPUI 的图集里(每个文档约 20 MB 显存,40 次新建关闭后应用涨约 250 MB);`Engine::retire` 交出关闭视图的全部图像(LOK 图块、PDF 图块与缩略图、ElyMark 页面的画面),外壳在窗口的下一帧释放,修后 40 次在 454 至 462 MB 之间。历史首屏改为从按下“历史”计(原从进程启动计,含打开仓库)。R1 后基准更严:PERF-03、04 到视图收下的第一块图块(`lok: tile (x, y) shown`),其间有图块画失败即整次作废;DOCX 样本原以单独一段的分页符分页,内核只排出 60 余页(`Heading1` 段上的 `pageBreakBefore` 同样丢页),改为分页符放在标题段内,PERF-03 之前先核对恰为 100 页(视图记录页数);PERF-02 每键须有自己的计时,缺即失败;PERF-08 循环前记基线,记内核中的文档数(worker 每次载入关闭写 `N documents open`),文档数须回到基线,文件数自第一次之后不变(第一次打开模板对话框时进程一次性载入两个系统字体与两个 AppKit 本地化表)。worker 的启动核对失败后保持失败,此后一切请求被拒(原核对失败的状态与成功相同,第二次即放行)。空白文档就绪由约 2.5 秒降到约 1.0 秒,发生在本次重建内核之后,与 0039 无关(对比实测),原因未查明。 @@ -243,23 +234,15 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-08:兼容清单与往返验证(C19.1,§19.1、§19.2)。`docs/compatibility.md` 是随版本发布的兼容清单(版本、内核与补丁、验证机,各应用的编辑保存、导入受限与输出,已验证与限制),版本或格式行为变了即同步改它。往返用例在 `crates/ely-lok/src/client/tests/independent.rs`:Ely 编辑、保存、关闭、再打开、再编辑、再保存,再由 `tests/scripts/ooxml.py` 检查;它不用 LibreOffice,自己解 OOXML,预期值自己算(整数、日期序号、错误值、文本严格相等,浮点按用例声明的容差),DOCX 另交 macOS 的 `textutil`;没有 python3 时报 SKIP。与 Microsoft Office 的交叉检查未做(本机没有 Office)。 - 2026-10-08:恢复副本、引擎停止与自动保存(C06.5,§6.2、§6.3)。Doc、Table、Show、PDF 共用 `ely_office::Recovering`:每 10 秒为有改动、已落定、不在导出的标签经 `Engine::snapshot`(写副本,文档不动)写一个恢复点(`ely_files::Recovery`,限额取存储设置;加密文档封存),索引在数据目录 `recovery/index.json`;崩溃标记照 ElyMark 的 `Crash`,下次启动把崩溃留下的最新点开成未命名副本并标为未保存,提示写明只恢复了内容,光标与撤销历史没有。引擎进程退出(`Engine::ended`,客户端 `try_wait`)时 `ely_office::Stopping` 在上方显示提示条(原因、最近一次恢复点的时间)与“重新打开文档”:`Engine::restart` 换上新 worker(LOK 照原配置与作者名重起,PDF 下次打开再起),有改动的标签从最新点开成副本,无改动的从文件重开,有改动却没有恢复点的列出;被放下的标签迟到的失败不再提示,指向它们的关闭询问一并撤下。重开的副本接过原恢复点的文件夹(封存随之),旧点留到新标签写下新点为止;载入或解封失败的条目留在索引里,下次崩溃后的启动再给。需要密码的副本(加密 PDF 的恢复点照原加密写出)照样询问,答后仍是未命名副本(`Source::Locked`)。引擎是否退出在出错时与每次恢复计时时查,空闲的引擎退出也会显示。恢复索引经 `ely_files::save_atomic` 暂存后替换,写不进时上一份照旧可读;写不进即提示一次、每次计时重试。干净结束后的下次启动只删已接过或正常的条目,没能重开的留着再试。自动保存(设置“文档”页,每个应用一个开关,存在应用的 `settings.json` 的 `autosave`,默认关):只对本应用自有格式(`Kind::save_as`)、无风险(宏、签名、修复)、状态为“已编辑”的标签,编辑计数停 2 秒不变即走普通保存;保存失败、外部改动后状态不再是“已编辑”,不再自动写,等用户处理。Cmd+W 关闭标签是共用命令(`ely_ui::commands::CloseTab`,原来只有 ElyPDF 绑了;文档应用里 Cmd+W 落进 LOK,内核按自己的 Ctrl+W 关掉框架,worker 下一次绘制或读取即在已释放的文档上段错误);worker 另拒绝内核关闭框架与退出的快捷键(Mod1 加 W、F4、Q)。真窗口(macOS):`recovery.sh`(崩溃后副本回来、关闭询问;只杀引擎后提示条、重新打开、文字回来、关闭询问),`autosave.sh`(关时不写,开关存下,开后停手即写,外部改动后不覆盖)。未验证:Table、Show、PDF 的真窗口恢复(同一外壳);Windows 与 Linux。 - 2026-10-08(用户定):首版发布六个应用(ElyDoc、ElyTable、ElyShow、ElyPDF、ElyTree、ElyMark);ElyMail 与 ElyAgent 第二批发布。ElyMark(C14)做完即开始首版的完整测试与打包,C12、C15 留到第二批。 -- 2026-10-08:数据管理(C18.3,§18.3)。设置的“存储”页(每个应用):本应用数据目录里的日志(`ely.log`)、网页内容缓存(`web`,只 ElyMark 显示)、恢复副本(`recovery`)各自的大小与清除(`ely_platform::storage`);日志当场清空(日志器以追加打开,截短不坏句柄),缓存与恢复副本在用着,写下记号(`.clear-web`、`.clear-recovery`),下次启动在设置与窗口之前删去(删不掉即启动失败、写明原因,记号留着下次再试,不打开没删掉的数据);每次清除先问,写明删什么、何时删;每个文档保留的恢复点(10、20、50)与空间(32、64、256 MB)存在应用设置的 `recovery`(严格读,不合即报错),ElyMark 写恢复点时照用(C06.5 的文档应用同)。日志超过 8 MB 时,启动时只留后 4 MB、从整行起。数据库的升级前备份与失败只读只属 ElyMail(第二批),首版的六个应用没有数据库;邮件与 Agent 历史导出同属第二批。组件库 ea4ed4c:`SettingsRow` 是以标题命名、以说明描述的组(原先行标题不进 AX 树,两个同名按钮分不出);待清除的状态以禁用的按钮写明。 +- 2026-10-08:数据管理(C18.3,§18.3)。设置的“存储”页(每个应用):本应用数据目录里的日志(`ely.log`)、恢复副本(`recovery`)各自的大小与清除(`ely_platform::storage`);日志当场清空(日志器以追加打开,截短不坏句柄),恢复副本在用着,写下记号(`.clear-recovery`),下次启动在设置与窗口之前删去(删不掉即启动失败、写明原因,记号留着下次再试,不打开没删掉的数据);每次清除先问,写明删什么、何时删;每个文档保留的恢复点(10、20、50)与空间(32、64、256 MB)存在应用设置的 `recovery`(严格读,不合即报错),ElyMark 写恢复点时照用(C06.5 的文档应用同)。日志超过 8 MB 时,启动时只留后 4 MB、从整行起。数据库的升级前备份与失败只读只属 ElyMail(第二批),首版的六个应用没有数据库;邮件与 Agent 历史导出同属第二批。组件库 ea4ed4c:`SettingsRow` 是以标题命名、以说明描述的组(原先行标题不进 AX 树,两个同名按钮分不出);待清除的状态以禁用的按钮写明。 - 2026-10-08(用户定:图标照 Papirus 重画):每个应用是 Papirus 192 网格上的一个大的扁平物件(页、讲台、信封、终端),主体一种饱和色,折角为其浅色,内容为白色、至多一个点缀色,主体与内容带 Papirus 的柔和投影(下移 4 或 2、模糊、20% 黑)。macOS 为浅色 squircle 上的同一物件(占主体 86%),Linux 与 Windows 只有物件。48 像素以下用无投影、无小细节的简化版。母版由 `packaging/icons/masters.py` 写出,配色见 `assets/icons/README.md`。Papirus 图标包(`assets/papirus_icons-master.zip`,用户放入)只作参照,不随包。 - 2026-10-08(用户要求核对各系统的文件关联):文件类型只写在 `packaging/types.tsv`(应用、扩展名、MIME;`/` 为文件夹)。Linux 的桌面项由它写 `MimeType=`(原先没有,桌面环境从不提供 Ely 打开文件);Windows 安装程序由 `packaging/windows/types.sh` 生成的 NSIS 宏注册(原先没有):HKCU 下每个扩展名的 `OpenWithProgids` 与 `Applications\<程序>\SupportedTypes`,ProgID `ElyOffice.<名称>` 带图标与打开命令,ElyTree 在文件夹右键菜单加“Open in ElyTree”;只进“打开方式”,从不改默认(同 macOS 的 Alternate),卸载时删去。makensis 在本机编过 ElyDoc 与 ElyTree 的安装程序;Windows 与 Linux 上实际打开文件未验证。 - 2026-10-08(用户定:窗口缩窄时控件挤成一团、越出边框,不可接受):窄窗口的版面。外壳一次排两侧栏(`shell/panel.rs` 的 `room`,有单测):内容区至少留 360,放不下时两侧先缩到各自最窄,再先藏右栏、后藏左栏,窗口变宽即回来,用户拖的宽度不变。组件库 6d95b24 至 69265b4:`ScrollArea::vertical` 只纵向滚动、内容不得宽于区域(侧栏一律用它,原先横向不受限,整栏按自然宽度排后被裁);`PropertyGrid` 每行可折行,放不下时控件到标签下方,标签列 104;全部图标的 `SegmentedControl` 与 `Tabs` 放不下全部文字时只显示图标,文字作提示与无障碍名(宽度取上一帧量得的条宽与文字排版宽度);`StatusBar` 两侧与各项可缩,文字以省略号结尾;`Banner` 文字折行;`Button` 不再拒绝收缩,只在无法折行时缩并以省略号结尾;未聚焦的单行文本框显示文字开头(原插入点在末尾,看到的是尾部);`Switch::name` 只给无障碍名(行已有标签时不再重复画字);`CommitInput` 的勾选与按钮一行可折。各应用固定宽度的控件框(`w_32` 等)改为占满、以原宽为上限。`tests/scripts/layout.sh`:六个应用各开样本,640、800、1100 宽下读 AX 树,任何元素越出窗口左右即失败,并留截图供目视(栏内裁切 AX 看不出)。缩放(用户问为何 Doc、Table、Show 不能缩放;视图早有 `set_zoom`,界面从未接上):`ely_office::Zooming`,Cmd+= / Cmd+- / Cmd+0,“视图”菜单三项,状态栏右侧 `ZoomControl` 的属主模式(组件 69265b4:显示属主给的百分比、按下交属主;原只缩放整个界面);Cmd+滚轮与触控板捏合以指针处为中心缩放(`view/zoom.rs`,档位 25% 至 400%);ElyShow 的 Cmd+0 回到适合幻灯片,手动缩放即停止适合。缩放只在内存里随标签,不跨重启。 - 2026-10-08(用户定:侧栏太占地方,要可调):侧栏宽度(C05.6)。外壳的左右侧栏(`shell/panel.rs`,八个应用共用)默认左 220、右 320(原取 `sidebar_width` 240 与 `sheet_size` 380;右 320 是 ElyDoc 属性栏不截断控件的最窄宽度,实测 280 截断数值框);内侧边缘是组件库的 `layout::resize_edge`,拖动改宽,左至少 160、右至少 280(原 220,属性栏放不下)、至多窗口的 45%;拖动停 0.5 秒后存入应用设置的 `panels`(`settings::panels`),退出时还没存的当场写入(窗口位置同),启动时严格读回(不是正数即在设置窗口写明并用默认),写入失败也在设置窗口写明;开合动画期间不显示拖动边。 - 2026-10-08:凭据与私密内容(C18.2,§18.2)。恢复密钥(加密文档的恢复副本用)存在系统凭据库(keyring:钥匙串、Windows 凭据、Secret Service);凭据库不可用或拒绝时改用本次运行的随机密钥,只在内存(`RecoveryKey { key, lasting }`,`lasting` 为假即提示恢复不跨重启),从不写成普通文件;用旧密钥封存的恢复点读不开即报“无法恢复”,不导出明文。PDF 打开密码与证书密码只在 worker 内存里、文档关闭即弃。日志:worker 的 stderr 经 `ely_ipc::relay` 按其级别并入应用日志(原先从访达启动时 worker 的日志丢失),两个 worker 与应用一样读 `ELY_LOG`、默认 info(LOK worker 原读 `RUST_LOG`,平时只出错误);日志调用不打印正文、密码、密钥;引擎(worker、内核、MuPDF)给的错误串可能引用文档,日志只记请求号与错误类别,全文只经回复交给界面;worker 的 stderr 只放行其结构化日志记录与崩溃栈帧,引擎自己打印的文字不进日志(只计数)。worker 启动失败也写成结构化记录,最后一条错误由 `Relay::last_error` 带进客户端的报错。诊断导出时姓名来源(账户名、设置)读不出即不导出并写明原因;日志尾部从整行开始。诊断(设置里的“诊断”,每个应用):先列出要导出的内容(本应用日志的最新 4 MB、macOS 上 7 天内本应用与其 worker 的崩溃报告,列表在 AX 树里),开关“去掉路径与个人姓名”默认开(凡路径与 URL 整段换成 ``:有引号到引号为止,否则到行末;分段标题只写文件名;账户全名、登录名与作者名按从长到短换成 ``),导出为一个文本文件(`ely_platform::diagnostics`),不上传。Git 服务的凭据照用用户自己的 credential helper 与 ssh-agent;打开文件不发任何请求。未验证:Windows 与 Linux 的凭据库与崩溃报告位置(那两端诊断只含日志)。 -- 2026-10-08:配置(C18.1,§18.1)。通用偏好在 `<本机数据目录>/ElyOffice/settings.json`,各应用的在其数据目录的 `settings.json`;都经 `ely_platform::settings::Store`:带 `version`(本构建为 1;更新的版本或读不通即报错,不覆盖),写入在同目录的 `.settings.lock`(`ely_platform::lock::FileLock`,flock 或 LockFileEx,由 ely-files 移来共用)下先重读、只改所写的键、未知字段原样留下,临时文件 fsync 后原子替换;多个应用同时写不丢字段(单测八路并发)。通用字段(`settings::Shared`,类型不对即报错):外观(跟随系统、浅色、深色)、语言(跟随系统、English、简体中文;下次启动生效,设置里写明)、动效(跟随系统、减少、完整)、跟随 Omarchy(只在有 Omarchy 时显示)、作者名(空即账户名;`user::name` 先取它,ElyPDF 的批注、LOK 的批注与修订都用它)。环境变量 `ELY_APPEARANCE`、`ELY_OMARCHY_SYNC`、`ELY_REDUCE_MOTION`、`ELY_LOCALE` 仍优先。`ely_ui::settings` 启动时读入(读不出即用默认值、记下原因),以 `notify` 监视文件夹,别的应用写入后即刻重读:外观、Omarchy、动效当场换,作者名经 `observe_global` 在后台交给 LOK worker(`set_author`)。设置窗口(应用菜单“设置…”,Cmd+,,每个应用一个)用组件库 `SettingsLayout`、`SettingsSection`、`SettingsRow`、`ThemeSelector`(组件 489ef31 起三项文字进内建中英目录)、`SegmentedControl`、`Switch`、`forms::Input`:通用;Doc、Table、Show、PDF 另有“文档”(作者名,失焦或回车写入);ElyTree 另有“Git”(Git 程序,空即随带的;写入前在后台核对 2.40 或更新,不是即在框下以 Banner 说明、不写;`ely_platform::git::locate` 次序为 `ELY_GIT`、此设置、随带的,版本核对移入 `ely_platform::git::system_version`,下次启动 ElyTree 生效)。文件读不出时窗口上方以 Banner 写明原因,改动不保存。作者名与 Git 程序每次提交都编号,写入经 `Store::set_if` 在文件锁下核对仍是最新的提交才落盘(等锁期间改了主意,旧写入作废);写入失败另以 Banner 写明,键入的值留着可重试;作者框在未编辑时跟随别的应用写入的名字。版本核对先看 `--version` 的退出状态(`git::version_of`)。各应用主窗口的位置、大小与最大化、全屏存在应用的设置里(`window`,停止移动 0.5 秒后写),启动时所记之处的中心仍在某块显示器上才用。另:ElyMark(CEF)直接启动的应用继承了 Chromium 忽略或屏蔽的 SIGTERM,测试的 pkill 杀不掉、留下进程(实测数个钟头的残留);`apps::open` 在 exec 前把 SIGTERM、SIGINT、SIGHUP、SIGQUIT 复原并清空信号屏蔽(单测:启动者忽略并屏蔽 SIGTERM 时子进程照样被终止)。未验证:Windows 与 Linux 的锁与窗口位置。 -- 2026-10-08:进程与状态核对(C17.1,§17.2、§17.4)。帧(`ely-ipc`)由长度、标签、请求号与内容组成:客户端每个请求一个递增的请求号,worker 的回复带回同一个,不符即报错(`check_reply`;原先只靠一问一答的次序配对,丢一个回复即整串错位而不自知);LOK 主动推送的事件请求号为 0(`UNASKED`);PDF worker 的版本戳包含 `ely-ipc` 的源码。帧超过上限(64 MB)发送前即报错,什么也不写;worker 的结果超限时回一个错误,worker 不退出。LOK 的输出读不下去(坏帧、提前结束)时,原因经回复通道交给等待的请求,客户端停下并回收 worker 再报出(原先请求一直等进程结束)。会话:LOK 每次载入一个会话号(递增,原先用内核文档指针,释放后的地址被下一份文档复用,旧句柄读到新文档、事件被当作已关的丢掉);PDF 的 `Doc` 带打开它的 worker 会话,客户端发送前核对,别的 worker 的旧句柄被拒;worker 的文档号带进程前缀(进程号与启动时刻),别的进程的号在此不存在。修订(PDF):`Doc` 记着读到时的历史标记(标记相同即文档相同,撤销回到原标记),指向页、对象、位置的请求带上它,worker 在动内核之前核对,不符即以 `stale:` 拒绝;整份文档的请求(页数、保存、撤销、元数据、附件列表与添加、保护等)带“任意”;按键读取或移除附件带标记。ElyPDF 的编辑在排队时记下当时的句柄,落地后按新标记换句柄,过期的读取照旧按版本丢弃(画不出的块不因过期记为失败),过期的编辑提示“文档已改动,未执行”:排在别的编辑后面的编辑因此被拒(它对着的是前一个编辑落地之前的页面),再按一次即可。LOK 的修订(R2):worker 给每份文档记一个修订,会改文档的请求(键、鼠标按下与松开、拖动、文字、命令、粘贴、剪切、函数补全;不按键的鼠标移动不算)在执行前加一;按编号指向工作表、幻灯片、对象、参考线、制表位、行列的命令(参数里有 `Index`、`nTabNumber`、`TabNo`、`Part`、`Row`、`Col`、`Column` 或区域的四角,值不是 -1;-1 是新建)先发 `EXPECT` 带读到时的修订,不符即以 `stale:` 拒绝,内核不动;同一次读取得出的一组命令(对象列表的多选)只核对一次,在一次 worker 调用里依次执行(`Worker::run_all`、`LokView::run_all_at`、`Commands::run_all_in`)。`LokView::run` 拒绝带编号的命令,须经 `run_at`;外壳的 `run_in` 对带编号的命令用 `Host::read_at`(ElyTable、ElyShow 为其读取时同一次 worker 调用里取的 `Worker::now`,ElyDoc 为视图所见:推送的事件批次带修订,每个输入落地后视图另取一次修订);`Tabs::read_in` 也给读取闭包这个句柄(ElyPDF 为视图当前的句柄)。ElyTable、ElyShow 在视图所见修订新于其读取时重读,所以键入之后按编号的命令不会一直过期;记着编号的对话框(删除与重命名工作表、删除重复项、幻灯片备注栏键入期间)在读取时记下修订,确认按它执行(`Commands::run_on`),其间文档结构变了即拒绝;ElyDoc 属性栏按视图所见的修订发(删除所示的制表位)。输入失败时视图照样取回修订(请求在执行前已计数),之后的编号命令不会一直过期。过期的拒绝提示“文档在此间已改动,请重试”(`office.error.changed`)。MuPDF 打开中途(加锁检查、日志、修复)失败即释放已打开的文档与修复副本(原先每次失败留下整个文档;MuPDF 的存储另有 256 MB 上限的缓存,满后不再增长)。FFI:MuPDF 每个调用在垫片的 `fz_try`/`fz_catch` 里,返回码加错误信息;LibreOffice 只经 LOK 的 C 接口;worker 崩溃时客户端报出退出状态。工作进程:LOK 随应用窗口启动(启动即开空白文档),MuPDF 在打开第一个 PDF 时启动;不建进程池。后台:Git、OCR、缩略图、文件夹搜索、导出与系统打开都在后台任务里。不建设 §17.4 所列设施(无 RPC 服务发现、无常驻服务、Git 只有一个内核、Markdown 只有 Milkdown 与 CodeMirror)。 +- 2026-10-08:配置(C18.1,§18.1)。通用偏好在 `<本机数据目录>/ElyOffice/settings.json`,各应用的在其数据目录的 `settings.json`;都经 `ely_platform::settings::Store`:带 `version`(本构建为 1;更新的版本或读不通即报错,不覆盖),写入在同目录的 `.settings.lock`(`ely_platform::lock::FileLock`,flock 或 LockFileEx,由 ely-files 移来共用)下先重读、只改所写的键、未知字段原样留下,临时文件 fsync 后原子替换;多个应用同时写不丢字段(单测八路并发)。通用字段(`settings::Shared`,类型不对即报错):外观(跟随系统、浅色、深色)、语言(跟随系统、English、简体中文;下次启动生效,设置里写明)、动效(跟随系统、减少、完整)、跟随 Omarchy(只在有 Omarchy 时显示)、作者名(空即账户名;`user::name` 先取它,ElyPDF 的批注、LOK 的批注与修订都用它)。环境变量 `ELY_APPEARANCE`、`ELY_OMARCHY_SYNC`、`ELY_REDUCE_MOTION`、`ELY_LOCALE` 仍优先。`ely_ui::settings` 启动时读入(读不出即用默认值、记下原因),以 `notify` 监视文件夹,别的应用写入后即刻重读:外观、Omarchy、动效当场换,作者名经 `observe_global` 在后台交给 LOK worker(`set_author`)。设置窗口(应用菜单“设置…”,Cmd+,,每个应用一个)用组件库 `SettingsLayout`、`SettingsSection`、`SettingsRow`、`ThemeSelector`(组件 489ef31 起三项文字进内建中英目录)、`SegmentedControl`、`Switch`、`forms::Input`:通用;Doc、Table、Show、PDF 另有“文档”(作者名,失焦或回车写入);ElyTree 另有“Git”(Git 程序,空即随带的;写入前在后台核对 2.40 或更新,不是即在框下以 Banner 说明、不写;`ely_platform::git::locate` 次序为 `ELY_GIT`、此设置、随带的,版本核对移入 `ely_platform::git::system_version`,下次启动 ElyTree 生效)。文件读不出时窗口上方以 Banner 写明原因,改动不保存。作者名与 Git 程序每次提交都编号,写入经 `Store::set_if` 在文件锁下核对仍是最新的提交才落盘(等锁期间改了主意,旧写入作废);写入失败另以 Banner 写明,键入的值留着可重试;作者框在未编辑时跟随别的应用写入的名字。版本核对先看 `--version` 的退出状态(`git::version_of`)。各应用主窗口的位置、大小与最大化、全屏存在应用的设置里(`window`,停止移动 0.5 秒后写),启动时所记之处的中心仍在某块显示器上才用。未验证:Windows 与 Linux 的锁与窗口位置。 +- 2026-10-08:进程与状态核对(C17.1,§17.2、§17.4)。帧(`ely-ipc`)由长度、标签、请求号与内容组成:客户端每个请求一个递增的请求号,worker 的回复带回同一个,不符即报错(`check_reply`;原先只靠一问一答的次序配对,丢一个回复即整串错位而不自知);LOK 主动推送的事件请求号为 0(`UNASKED`);PDF worker 的版本戳包含 `ely-ipc` 的源码。帧超过上限(64 MB)发送前即报错,什么也不写;worker 的结果超限时回一个错误,worker 不退出。LOK 的输出读不下去(坏帧、提前结束)时,原因经回复通道交给等待的请求,客户端停下并回收 worker 再报出(原先请求一直等进程结束)。会话:LOK 每次载入一个会话号(递增,原先用内核文档指针,释放后的地址被下一份文档复用,旧句柄读到新文档、事件被当作已关的丢掉);PDF 的 `Doc` 带打开它的 worker 会话,客户端发送前核对,别的 worker 的旧句柄被拒;worker 的文档号带进程前缀(进程号与启动时刻),别的进程的号在此不存在。修订(PDF):`Doc` 记着读到时的历史标记(标记相同即文档相同,撤销回到原标记),指向页、对象、位置的请求带上它,worker 在动内核之前核对,不符即以 `stale:` 拒绝;整份文档的请求(页数、保存、撤销、元数据、附件列表与添加、保护等)带“任意”;按键读取或移除附件带标记。ElyPDF 的编辑在排队时记下当时的句柄,落地后按新标记换句柄,过期的读取照旧按版本丢弃(画不出的块不因过期记为失败),过期的编辑提示“文档已改动,未执行”:排在别的编辑后面的编辑因此被拒(它对着的是前一个编辑落地之前的页面),再按一次即可。LOK 的修订(R2):worker 给每份文档记一个修订,会改文档的请求(键、鼠标按下与松开、拖动、文字、命令、粘贴、剪切、函数补全;不按键的鼠标移动不算)在执行前加一;按编号指向工作表、幻灯片、对象、参考线、制表位、行列的命令(参数里有 `Index`、`nTabNumber`、`TabNo`、`Part`、`Row`、`Col`、`Column` 或区域的四角,值不是 -1;-1 是新建)先发 `EXPECT` 带读到时的修订,不符即以 `stale:` 拒绝,内核不动;同一次读取得出的一组命令(对象列表的多选)只核对一次,在一次 worker 调用里依次执行(`Worker::run_all`、`LokView::run_all_at`、`Commands::run_all_in`)。`LokView::run` 拒绝带编号的命令,须经 `run_at`;外壳的 `run_in` 对带编号的命令用 `Host::read_at`(ElyTable、ElyShow 为其读取时同一次 worker 调用里取的 `Worker::now`,ElyDoc 为视图所见:推送的事件批次带修订,每个输入落地后视图另取一次修订);`Tabs::read_in` 也给读取闭包这个句柄(ElyPDF 为视图当前的句柄)。ElyTable、ElyShow 在视图所见修订新于其读取时重读,所以键入之后按编号的命令不会一直过期;记着编号的对话框(删除与重命名工作表、删除重复项、幻灯片备注栏键入期间)在读取时记下修订,确认按它执行(`Commands::run_on`),其间文档结构变了即拒绝;ElyDoc 属性栏按视图所见的修订发(删除所示的制表位)。输入失败时视图照样取回修订(请求在执行前已计数),之后的编号命令不会一直过期。过期的拒绝提示“文档在此间已改动,请重试”(`office.error.changed`)。MuPDF 打开中途(加锁检查、日志、修复)失败即释放已打开的文档与修复副本(原先每次失败留下整个文档;MuPDF 的存储另有 256 MB 上限的缓存,满后不再增长)。FFI:MuPDF 每个调用在垫片的 `fz_try`/`fz_catch` 里,返回码加错误信息;LibreOffice 只经 LOK 的 C 接口;worker 崩溃时客户端报出退出状态。工作进程:LOK 随应用窗口启动(启动即开空白文档),MuPDF 在打开第一个 PDF 时启动;不建进程池。后台:Git、OCR、缩略图、文件夹搜索、导出与系统打开都在后台任务里。不建设 §17.4 所列设施(无 RPC 服务发现、无常驻服务、Git 只有一个内核、Markdown 只有 markdown-rs)。 - 2026-10-08:应用之间的连接(C16.1,§16;首版只做六个应用之间的三项,邮件与 Agent 的三项随第二批)。启动别的 Ely 应用(`ely_platform::apps`):先找与本应用同一安装或构建的同级(`.app` 旁、Windows 的 `Ely Office\名称`、开发构建同一目录),再找 macOS 登记过的(按包标识 `AppId::bundle_id`,`NSWorkspace`);`.app` 一律经 `open -a` 交给 Launch Services,正在运行的应用经其打开请求收下文件(不另起一个),其余直接启动。应用切换:每个应用的应用菜单有“打开 Ely 应用”子菜单,只列已安装的其他 Ely 应用(`commands::switch_to`,八个无参数动作),启动失败即提示原因。导出后查看:导出、提取、拆分、删除标记内容、清理、保护、签名与附件另存成功后,提示带“打开”:PDF 在已安装的 ElyPDF 打开(作为新标签,不当作原编辑会话),否则与其他文件、文件夹一样交系统默认程序(gpui `open_with_system`);ElyPDF 打开失败即提示原因,不改交系统。ElyMark 导出缺图、图或公式时的警告同样带“打开”。Markdown 查看版本:ElyMark 状态栏的 Git 项把文件本身交给 ElyTree,ElyTree 收到文件即打开其所在仓库并显示该文件的历史(`Tree::open_path` 按文件所在文件夹开仓库,再以仓库内路径 `file_log`);不自动提交,不建仓库;未安装 ElyTree 时提示从发行包安装。真窗口(macOS):`mark-output.sh` 核对 ElyTree 打开该文件的历史、导出警告的“打开”在 ElyPDF 打开 PDF;`release-macos.sh` 从 ElyDoc 的应用菜单启动 ElyMark。AppKit 自己的窗口恢复关掉(`startup::no_window_restore`,`ApplePersistenceIgnoreState`):Ely 自己恢复会话;原先应用被强退后再启动,AppKit 先弹模态“是否重新打开窗口”,访达交来的文件被挡在后面(打包测试实测)。窗口测试的 `seen` 不看系统菜单(苹果菜单的“最近使用的项目”会列出别处打开过的同名文件)。未验证:Windows 与 Linux 的同级查找与系统打开;系统默认程序打开(会在本机弹出别的应用,未自动测)。 -- 2026-10-08:ElyMark 的单行 `$$…$$`(用户以 Pandoc 示例发现):remark-math 把它读成行内公式,序列化只写一个 `$`,与原文不同,整段退成原文块。现按原文记下每个行内公式的 `$` 数(`math.js`),两个及以上以显示模式排出(同 GitHub),写回照原数;导出同样以显示模式。Pandoc 专有的表格、定义列表、行块不是 CommonMark/GFM,照 §14.3 以原文块保留(`---- ----` 在 CommonMark 里是分隔线)。 -- 2026-10-08:ElyMark 书写视图、输出、Git 配合、大文件(C14.7,MARK-09、11、13、14,PERF-07)。书写视图:“视图”菜单的专注模式(⇧⌘F,收起标签条、左栏与状态栏;可视模式里光标所在的顶层块之外淡到 0.35,源码里当前行之外同),打字机滚动(插入点所在行保持在视口中部;上下各留半屏,首尾行也能居中),正文宽度窄、中、宽(36、48、64rem),字体无衬线、衬线、等宽,文字大小(⌘= ⌘- ⌘0,70% 至 200%,每档 10%)。全部在页面里以 CSS 实现(`look.js`:根元素的 data 属性与 `--ely-text-scale`),Markdown 不变;设置存在数据目录的 `look.json`(读不出即报错并用默认值),菜单随之打勾,启动时恢复;淡入淡出遵守系统的减少动效。组件库的 `ZenMode` 不用(见缺口)。输出(“文件›导出…”⌘E 一个对话框选 HTML、HTML 与资源文件夹、PDF、纯文本;“打印…”⌘P):页面以 unified(remark-parse、remark-gfm、remark-math、remark-rehype、rehype-katex、rehype-stringify,版本写死)把编辑器此刻的全文(与懒挂载、大文件无关,不截断)渲染成 HTML,Mermaid 用浅色主题画成 SVG(画不出的照原代码块并在导出后提示);front matter 不进输出。HTML 内嵌导出样式表(构建时单独产出 `export.css`:KaTeX 样式与 woff2 字体内嵌、浅色排版)与图片(文档相对路径、百分号转义、file URL 读成 data URL;网址照原样;读不到的图片照原链接并提示);HTML 与资源文件夹写 `名称.html` 与 `名称_files/`(样式表与图片副本,同名图片按序号区分;文件夹已在即拒绝,写坏即删去);纯文本以 hast-util-to-text 按块换行,公式写 TeX;PDF 先把同一份内嵌 HTML 暂存到工作目录,在不显示的 CEF 视图里由 Chromium 打印(`WebView::print_pdf`,结束经 `Event::Printed` 报回),再交到所选位置;打印同一份 PDF 交系统打印面板(`ely_office::print_later`)。导出都经 `ely_files::export`(不覆盖打开着的文件)。富文本复制用 Chromium 自己的复制(剪贴板带 HTML)。Git(MARK-13,不做第二套客户端):Git 的查找与运行环境移入 `ely_platform::git`(`locate`:`ELY_GIT`,应用旁,ElyTree 包内的 Git,本构建的 `engines/git`;`command`:只读不抢锁等同一套环境变量),ElyTree 照用;ElyMark 在后台对当前文件问 `rev-parse --show-toplevel` 与 `status --porcelain=v2 --ignored`,状态栏显示未改动、有改动、未跟踪、已忽略、有冲突(问不出即“未知”,提示里是原因;不在仓库里不显示),按一下以仓库根打开 ElyTree(`ely_platform::apps::open`);切换标签、每次保存(指纹变化)、外部改动与窗口回到前台时重问。大文件(MARK-14):超过 2 MiB 的文本以源码打开,上方提示条写明大小与“可视与预览已关闭,保存保留全部内容”,切到可视或预览即提示不切;保存与导出都是全文。PERF-07 实测(macOS,release):1 MB 文档自启动到可视可编辑 1.4 秒(首次冷启动 2.0 秒);4 MB 在可视下约 5 秒,故按源码打开。真窗口(macOS,`tests/scripts/mark-output.sh`):Git 状态未改动→键入自动保存后有改动→按下打开 ElyTree 于该仓库;专注模式收起标签、存入 `look.json`、重启仍在,文件不变;四种导出的内容(KaTeX、SVG、内嵌图片、全文;资源文件夹的样式与图片;PDF 以 pdftotext 读出文字;纯文本含 TeX);打印生成 PDF;3.3 MB 文件以源码打开、⌘1 不切、末尾编辑后全文保存、纯文本导出含最后一行;`ELY_CLIPBOARD_TEST=1` 时可视全选复制带 HTML。`mark-content.sh` 的 look 步核对淡出、窄列、衬线与插入点居中。去掉专注收起、图片内嵌、大文件阈值、Git 状态或截断文本导出时各自失败。窗口测试须在英文输入法下运行(中文输入法会把模拟键入收进组字)。导出时每张图定为一个 `src`(去掉 `srcset`、`sizes` 与 `picture` 的 `source`,无 `src` 时取 `srcset` 的首个地址);公式的 KaTeX 错误随导出提示(行号与 KaTeX 的说明),不当作完整导出。引用正文的提示走外壳的 `toast_unlogged`,日志只记计数与行号;组件库 28a6bba 起 `Toaster` 只记提示的编号与级别,不记标题。未验证:Windows 与 Linux;Windows 的 Git 路径在 ElyTree 包内的位置。 -- 2026-10-07:ElyMark 外部变更、自动保存与会话恢复(C14.6,MARK-08、12,§6.2、§6.3、§14.4,QA-22)。监视:一个 `notify` 监视器盖住各标签文件所在的文件夹(不递归),200 毫秒内的一串改动合成一次查看;按会话指纹(长度与修改时间)比对,自己的保存不触发。文件改了:读到稳定的字节(读前读后指纹相同,最多五次),无未保存内容(含页面还没送回的编辑)即原地换成磁盘的文字(`ely.refresh`:插入点所在的文件行与滚动尽量保持,时间线从这份文字重新开始),会话重载指纹并以新文件身份登记;有未保存内容即不动磁盘与编辑器,状态为“外部已改”,自动保存停止,上方提示条“比较…”。比较:组件库 `git::regions` 对“上次载入或保存时”的文字、你的与磁盘上的三方合并,`ThreeWayMerge` 逐个冲突选边(编辑器的文字先落定;比较期间磁盘再改即重读重比,选定前核对所读内容的指纹,不符即提示再比较一次、不写入),可“用磁盘上的”“保留你的”(立即覆盖写入)或冲突都选完后“用合并的”(磁盘的版本成为新的基底,合并结果作为一步可撤销的编辑,随后自动保存)。路径不见了:等 300 毫秒(原子替换的空档)仍不在,即在原文件夹里按文件身份(设备与 inode,不认符号链接)找改名后的文件(`Sessions::renamed_in`),找到即标签跟过去(与 F2 改名同一条路);找不到即为已删:内容留着,提示条“另存为…”,不再自动保存、不重建文件。读不出(权限等)或监视器出错:提示条写明原因与“再查一次”;“视图›检查磁盘上的文件”随时手动查看(网络目录等不保证有事件)。自动保存(§6.2):有路径的文件停手 1 秒后保存(`ely_files::PolicyEngine`),外部改动或删除后停止,选定处理后重新开始;失败照常留为未保存。会话(MARK-12):数据目录的 `session.json` 记各标签的路径(未命名为空)、名称、模式、插入点所在的文件行与滚动;每 10 秒有未保存内容且有变化的标签写一个恢复点(`ely_files::Recovery`,每标签一个文件夹,20 点 7 天);退出(Cmd+Q 或关窗)开始时先记下当时的标签;`session.lock` 写进程号,正常退出删去它与全部恢复点;下次启动锁还在且进程已不在即为崩溃(`ely_files::alive`)。启动时打开记下的文件,标签显示后换成记下的模式与位置;崩溃后的未保存内容从最新恢复点放回,一律开成未命名副本(有路径的文件照磁盘打开,从不先覆盖原件,§6.3;原件已不在也照样开副本并提示哪个文件没能打开)、标为未保存,并提示“恢复了某时未保存的内容;撤销历史未恢复”(§6.3 区分内容与历史)。显式“不保存”后正常退出,下次不当作崩溃,未命名的内容不回来。真窗口(macOS,`tests/scripts/mark-outside.sh`):原子替换后干净标签换成新文字、键入后自动保存;未保存时外部写入不被覆盖,比较三栏、保留你的写回;互不冲突的两处改动合并后都在;外部改名后标签名跟随、保存写到新名、旧名不重现;改成无权限读即提示、恢复权限后“再查一次”重载;删除后不再自动保存、不重建;退出后重开回到源码模式与原来的行;未命名文档写过恢复点后 `kill -9`,重开内容回来且关闭时问是否保存,选不保存后再开不回来。去掉干净重载、改名跟随、删除时停自动保存、读错提示、崩溃内容、退出时记下标签或恢复的位置时各自失败。启动时先显示可视再换到记下的模式(一次多余的绘制,已知)。启动参数给了文件时只开它们与崩溃恢复的副本,不连带上次的标签。“保留你的”以所比较的指纹为基线(`Session::rebase`)走普通保存,之后的外部改动照样冲突;自动保存撞上外部改动时交给比较(`Host::conflicted`),不弹外壳的冲突框;外壳在询问取消、保存面板取消、保存失败、冲突询问取消时都调 `Host::close_cancelled`,关闭标签时调 `Host::tab_closed`(ElyMark 即刻删其恢复点并更新清单);每个标签只在首次显示时恢复一次;缺列的位置读作 0。组件库 f756043:脏标签与非当前标签的关闭按钮透明而非隐藏,辅助技术可按。外壳冲突询问的“保留我的”以此刻磁盘的指纹为基线走普通保存(导出期间再改照样冲突;文件已不在时直接写),“从磁盘重新载入”先结束挂起的关闭;退出途中首次保存的标签以新路径记入清单。取消退出(未保存询问点取消)后清单照常跟随(`Host::close_cancelled`);关闭或放弃的标签的恢复点在下一次保存会话时删去。位置记文件行与行内列。“不截断导出”随导出在 C14.7。未验证:Windows 与 Linux(`alive` 的 Windows 实现未构建);网络目录。 -- 2026-10-07:ElyMark 编辑工具与附件(C14.5,MARK-06、07)。查找替换(Cmd+F):组件库 `FindWidget` 盖在页面右上,查询与替换是两个 `TextInput`,带区分大小写、全词、正则三个开关;可视用 `prosemirror-search`,源码用 `@codemirror/search`;总数与当前序号由页面按不重叠的命中报回;正则写错时框内显示 JS 的原话(按两个编辑器所用的 `gmu` 标志编译);空的或无效的查询不执行查找命令(CodeMirror 对它会打开自带的面板);零宽命中按码点前进(落在代理对中间时 Unicode 正则会往回答,原先反复收集同一处直至页面崩溃);预览不替换;换模式或换标签后按同一查询重查。查找框整窗一个,各标签的页面各记其查询:下一处、替换每次都带上框里此刻的查询与替换文字,标签一换即按框里的重查。查询变了从当前命中的起点往后找,键入替换文字不移动命中。替换框回车替换一处。缩进:源码 Tab 与 Shift-Tab 为 `indentWithTab`;可视在列表里 Tab 下沉、Shift-Tab 上提,不在列表时吞掉;代码块同源码。WebView 在自己的键位上下文绑定 Tab 与 Shift-Tab 交给页面(组件库 K02 起更深的上下文先得),焦点不离开编辑器。列表续写:可视为 Milkdown 自带,源码为 lang-markdown 的回车续写。“表格”菜单:插入表格(3×3)、上下加行、左右加列、删行、删列(prosemirror-tables);插入后插入点落在新表第一格(Milkdown 在段末插入时把插入点留在段里,之后的行列操作落空);插入点不在表格里、或在源码与预览里即提示。代码高亮见 C14.3。附件:拖放图片文件或粘贴剪贴板里的 PNG,复制到文档旁的 `assets/`:拖放保留原名、重名加序号,粘贴名为 `pasted-毫秒.png`;都以 `create_new` 占名,从不覆盖已有文件,写坏即删去半成品(`ely_files::import_attachment` 原先先查后拷,其间出现的文件会被覆盖);在插入点写入相对链接,名称按百分号转义,源码里替代文字的方括号与反斜杠加转义;复制期间文档另存到别的文件夹即不写链接,提示图片留在原文件夹的 assets;未命名文档、预览与非图片文件拒绝并说明。粘贴经 `WebView::intercept_paste`:宿主先看剪贴板,图片自己处理,其余照常交给页面。macOS:CEF 离屏输入不带 AppKit 编辑命令,而 Blink 在 macOS 上只凭这些命令处理 Cmd+方向键与 Cmd+Backspace(Option 组合 Blink 自己处理),可视编辑器里这些键原来不动;`ely-web-content` 的键盘处理器(`edit.rs`)在页面没处理这些键时照 AppKit 标准键位以 `Selection.modify` 移动或扩选到行首行尾、文首文末,Cmd+Backspace 与 Cmd+Delete 删到行首、行尾;CodeMirror 自己处理的键不经此。组件库 8f4729c 与 aa0c99d:开关按钮(`ToggleButton`、`ToggleGroup` 的各项)在 AX 树里是按钮,名为其文字或提示,带按下状态。真窗口(macOS,`tests/scripts/mark-tools.sh`):可视全部替换,源码正则替换,Tab 与 Shift-Tab,列表续写,Cmd+Down 后插入表格、加行加列、删列,拖放图片(自带的拖动源)复制到 assets 并写入转义的链接;`ELY_CLIPBOARD_TEST=1` 时粘贴图片同样落地;复制被拖住(FIFO)时经菜单另存到别处,不写链接;两个标签各设查询后回到前一个全部替换按框里的;键入替换文字后替换一处落在原命中;源码拖入 `a]b.png` 写成 `![a\]b](…)`。去掉 AppKit 键、正则、Tab 绑定、插入点修正、文件夹核对、起点查找、标签跟随或转义时各自失败。`tests/scripts/comments.py` 另查 Swift 测试助手与页面的 JS 源码。本机有全局快捷键占用 Cmd+Shift+S 与 Cmd+Shift+Z(空白 AppKit 窗口也收不到),所以测试经菜单另存。未验证:Windows 与 Linux。 -- 2026-10-07:ElyMark 导航与搜索(C14.4,MARK-05、MARK-10,MARK-01 的打开文件夹)。左栏三页(分段控件):文件、大纲、搜索。打开(面板可选文件与文件夹,启动参数同)文件夹即在后台按其真实路径列出其下的 Markdown 文件(`folder.rs`:`.md`、`.markdown`,不进点开头的文件与文件夹,不跟随链接,只在所开的文件夹里,不扫整盘),以组件库 `lists::FileTree` 显示(只画可见行,可过滤),选中按回车或双击打开;窗口回到前台、改名之后即重列,列表变了即重搜文件夹。没开文件夹时文件页是最近列表(`ely_files::recent`:数据目录的 `recent.json`,新的在前,留 20 个,读不出即报错且不覆盖;ElyTree 的同一套移入此处共用),打开成功的文件与列出的文件夹记入,按真实路径(同一文件只一条);标签的路径变了(改名、另存为)即记入新路径,旧路径已不存在即移出。大纲:页面在显示后与每次送出全文时报出当前编辑器的标题(可视与预览取文档里的标题节点,源码取 CodeMirror 的语法树,ATX 与 Setext),以 `lists::Outline` 显示,原文块里的标题也在内(以 remark 读其源码,引用、列表中的标题同样列出,跳转落在标题文字上),选一项即把插入点放到该标题并把键盘焦点交给文档(预览滚到该处)。搜索:本文件或文件夹(打开了文件夹才有),按字面、不分大小写逐行找,打开着的文件按其当前文字(含未保存的),其余读盘,读不出或不是 UTF-8 的文件以“某文件未能搜索”的行列在结果最前(带原因),最多 500 条;本文件的结果随当前标签:切换标签即重搜,较早的搜索迟到即弃;结果列出行文字与“文件:行”,选中回车即打开(已开即转到,按真实路径认标签)并把插入点放到该行的那一处:源码定位到该行的命中,可视按写出时每个块在全文里的位置(`preserve.js` 的写出同时给出各块的区间)找到该行所在的块,再按块内此前命中的个数落到块中对应的命中(此前的行先经 Milkdown 解析,按编辑器所见的文字计数,链接地址等不算,与块内查找同按不重叠计;原文块照源码计数;该行所见文字没有查询词时——命中只在链接地址等处——落在该行开头);行号自文件开头算,可视与预览先减去 front matter 的行数,落在 front matter 里即滚到它。页面未显示完就要求的定位等显示后再做。多标签由外壳。真窗口(macOS,`tests/scripts/mark-places.sh`):文件树不列 .txt、点选回车打开,大纲跳到二级标题后键入落在标题里,文件夹搜索找到两个文件的行、不搜 .txt、选中结果打开并在该行键入,最近列表重启后还在、从中打开文件夹。组件库 dabc1eb 起树与树行有无障碍角色(树、树项,名称、层级、选中、展开);大纲项仍没有(测试按位置点选)。未验证:Windows 与 Linux。 -- 2026-10-07:ElyMark 编辑模式、文件与语法(C14.3,MARK-01 至 04,§14.3)。模式:可视、源码、预览,“视图”菜单与 Cmd+1/2/3,状态栏右侧分段控件;每个标签各记其模式,页面回落到源码时标签随之。切换在页面里进行(`ely.mode`):先取当前编辑器的全文并立即送出待送的全文,再以同一文字显示新模式;每次切换都新建目标模式的编辑器(不再保留隐藏的编辑器)。本文件只有一条时间线:带着编辑离开一个模式时,把离开时的全文记入时间线;编辑器自己的撤销用尽(各看其撤销深度,Milkdown 的历史键位排在我们的之后)时再按撤销,回到时间线上一条全文、以当前模式重建(另一段的编辑作为一步撤销),重做用尽时向后走;只有真正的编辑会截掉时间线上其后的部分,撤销、重做的跳转不算编辑;离开时编辑器还有可重做的步骤,就把全部重做后的文字记在时间线前方,回来后重做即到那里。模式请求按序号只认最新的:编辑器构建期间再要的模式记下,构建完且仍是最新请求时显示它,此后的新请求清掉它;较旧的显示回报回来时序号不符即退出,不碰排队的请求。预览每次新建、只读(正文与代码块)。切换本身不改文件。文件:新建(空的未命名 `untitled.md`,保存走另存为)、打开、保存、另存为(外壳)、重命名(F2,“文件”菜单):`commands::RENAME` 与 `commands::RENAMING`(含重命名的文件命令表),`document_root`/`set_document_menus` 改收文件命令表;ElyMark 的 `rename.rs` 在原文件夹里改名(只有它用,不放外壳),`ely_files::rename_new` 不覆盖已有文件(macOS `renamex_np` 加 `RENAME_EXCL`,Linux `renameat2` 加 `RENAME_NOREPLACE`,Windows `MoveFileExW` 不带替换;后两者未构建验证),名称须不含斜杠且扩展名为可保存的格式,未命名或正在保存的标签不改名,改名完成时标签须仍开着且仍是原路径,否则只记日志、不动登记;同文件登记记下打开时的文件身份,原路径已不存在而身份相同即为同一文件(改名间隙里打开新名字会转到原标签),改名后以 `Sessions::track` 迁移登记(不关再开);外壳每次保存成功后也以它更新登记的路径与身份(原子保存换了文件);标签名与 `Session::renamed` 的路径随之,内容与指纹不变。文件 URL:Windows 的驱动器路径写成 `file:///C:/…`、UNC 写成 `file://主机/…`,去掉 `\\?\` 前缀,围栏反向换回 Windows 路径再取真实路径(字符串转换有单测,Windows 上未运行)。CEF 的请求处理移入 `fence.rs`(`handlers.rs` 原超过 500 行)。文档旁的图片:页面 `` 设为文件所在文件夹,`WebView::allow_images` 让该文件夹只放行图片请求(资源类型为图片;导航、脚本仍只限内容文件夹),另存为或改名后随文件移动(已显示的相对图片不重载,已知限制);Mermaid 的包按脚本自身位置载入,不受 `` 影响。Milkdown 7.22.2 的图片解析把无标题图片的 `null` 标题交给要求字符串的属性,ProseMirror 拒绝、Milkdown 记错误后丢掉整张图;`image.js` 改为空字符串。实测(macOS,`tests/scripts/mark-modes.sh`):文档旁的图片画出(截窗口量色),三种模式往返不改文件,预览不收键入,可视里键入后经预览回来撤销得回原样,源码里的改动回到可视并保存,改名到已有文件被拒、已有文件不动,改名到新名后文件移动、继续编辑保存到新文件,新建打开空的可视文档(读外壳写下的 `untitled.md` 须为空),可视键入、源码键入、回可视后两次撤销回到两者之前、重做回来(撤销跳转当作编辑、丢掉时间线时失败)(去掉预览只读、去掉保留编辑器、去掉旧编辑器的链接时测试失败);`mark-content.sh` 在同一段脚本里连要两种模式,最后显示后者(去掉记下的模式时失败);可视已显示、其回报未答时再要源码,最后为源码(不核对最新请求时失败)。“直接打开文件夹”随文件树在 C14.4。未验证:Windows 与 Linux。 -- 2026-10-07:ElyMark 文件会话与原文保留(C14.2,§14.3,QA-21)。ElyMark 用 `ely-office` 的外壳(标签、打开、保存、另存为、关闭询问、冲突),引擎 `Mark`(`apps/ely-mark/src/engine.rs`):载入读字节,UTF-8 带不带 BOM 都可,UTF-16 与非 UTF-8 即报错并指出字节位置(`text.rs`);编辑器拿到去掉 BOM、CRLF 换成 LF 的文本;写出时文本与载入时相同即原样写回原字节,否则补回 BOM 并按载入时多数的换行(CRLF 多于单独 LF 即 CRLF;混用换行的文件编辑后统一成多数的那种,已知限制)。外壳的 `Engine::open` 改为收窗口、返回 `io::Result`(网页视图要窗口,页面开不了即为打开失败)。每个标签一个页面;页面每次改动立即报 `{"kind":"changed","version"}`,停手 300 毫秒后报该版本的全文 `{"kind":"text"}`;外壳的“已落定”即最新改动的全文已到,保存等它;历史标记为全文的长度与散列,撤销回原样即为未修改;外壳保存时先写出再读标记,两者之间页面可能送来新全文,所以写出时记下所写文字的标记,紧接的读标记交出它,之后的编辑仍算未保存。可视模式保留原文(`preserve.js`):照 Milkdown 自己的 remark 流程(`parse` 加 `runSync`)读出原文各顶层块的位置,与编辑器的顶层块一一对应(数目不同或有块无位置即不能对应;空文件 Milkdown 仍有一个空段落,照空处理)。每个顶层块单独序列化,与原文逐字相同的才是可视块,否则换成原文块(`ely_raw`:等宽、可编辑的纯文字,写出即其文字,不进撤销历史的一次替换);块间 Milkdown 不显示的原文(如链接定义:Milkdown 把引用链接内联、删去定义)也成原文块,所以没有藏起来的原文。序列化的标记照该文档自己多数的写法(`style.js`:无序与有序列表标记、强调、加粗、代码围栏、分隔线、Setext;以 remark-parse 统计后设 `remarkStringifyOptionsCtx`),写法一致的文档几乎全为可视块,与多数写法不同的块成原文块。写出时按块做 Myers 差异(`diff.js`;ProseMirror 未改的块是同一对象,否则按内容比较),未改的块与块间原文逐字照抄,只有改动的块用 Milkdown 序列化。YAML front matter 不进可视编辑器,以原文显示在上方,写出时原样接回。不能对应的文档直接以源码模式打开(可编辑,无损),并报 `fallback`。打开后插入点在文档开头。页面每次载入是一代(应用计数,随 `ely.show` 传入),回报带代数,别的代的回报丢弃;载入时版本计数归零,重载后显示最后收到的全文。故障视图按“重新载入”后焦点回到页面(原先留在消失的按钮上,键入无处可去)。普通 Markdown 读文件的权限错误不交外壳当作加密文件(外壳把 PermissionDenied 当作要密码),照系统原话报出;外壳打开失败另写日志。实测(macOS,`tests/scripts/mark-session.sh`):BOM、CRLF、front matter、Setext 标题、`1)` 列表、HTML 块、`~`删除线、硬换行、链接定义与 `[[...]]` 的文件,不改保存逐字节相同;在首段键入一字保存,除该段多一个字外逐字节相同(整篇序列化、去掉 BOM 时该测试失败);段落里的 `[[...]]`、引用里的 Setext 与 `[[...]]`、只有链接定义的文件都以原文块显示,键入一字只多一字;在链接定义前向前删除不移动、不修剪它;渲染进程被杀、按重新载入后再键入的字照样保存;无读权限的文件报“Permission denied”、不问密码(不用原文块、不按代丢弃回报时测试失败)。未验证:Windows 与 Linux;模式切换与撤销(C14.3)。 -- 2026-10-07:ElyMark 内容包(C14.1,§14、§17.3)。网页源码在 `apps/ely-mark/web`:版本全部写死,`package-lock.json` 锁定;`scripts/build-mark-content.sh` 以 `npm ci --ignore-scripts` 安装、esbuild 打成经典脚本(file:// 下模块脚本被 Chromium 拦下)到 `engines/content/ely-mark`,附 `THIRD-PARTY-NOTICES.txt`(按打进包的每个包取其许可文件;没有许可文件的只许 MIT,以清单的作者与 MIT 原文写出,构建时列出)。包:Milkdown 7.22.2(commonmark、gfm 含脚注与任务、history、代码块组件)、CodeMirror 6(代码块与源码模式,语言取 language-data,全部打进包)、KaTeX 0.19.0(字体随包)、Mermaid 12.1.0(单独的 `diagram.js`,首次遇到图时才载入;`securityLevel: strict`,不在页面上留错误图)。数学按 Crepe 的做法(remark-math,块公式为 LaTeX 代码块)。只渲染文档里保存的源码;公式与图出错时显示错误(公式带位置,图带行号)与原文,不清空代码块。页面接口 `ely.show(markdown, mode, texts)`,mode 为 visual、source、preview;每次显示一个新容器与序号,较早的显示若后完成即销毁其编辑器、不报告,页面上始终只有最后一次的编辑器;预览时正文与代码块都只读;显示后经 `elyQuery` 报 `{"kind":"shown"}`。颜色全部取 `--ely-*`(CodeMirror 的高亮、Mermaid 的主题变量),随系统深浅色:Milkdown 把预览净化后拷进页面(不保留我们给的元素),且每次调用给的回调都是新函数,所以 Mermaid 预览先返回带编号的空占位,绘制结果只在该占位仍在页面上时经回调落下(改语言、改文字、卸载都换掉占位,旧结果即弃);绘制在全页一条队列里依次进行,每次绘制带代次,调用 Mermaid 前与落下前都核对属主、占位与代次,失效即跳过(编辑器销毁后排着的绘制不再执行);`:root` 的样式一变,凡占位仍在页面上的图(含正在绘制的)都以新颜色重画,此前的结果作废,首次落下的就是新颜色。记录归各编辑器所有,编辑器销毁即释放,占位已离开页面的记录在下一次绘制或换主题时清掉。Milkdown 把视口外的代码块换成占位(懒挂载),滚入视口时再建。界面文字由应用给(`mark.*`)。打包时 `bundle.sh` 把 `engines/content/<应用>` 放进 `Resources/content`,缺即失败;应用先找包内,再找本构建的 `engines/content`,都没有即报错。ElyMark 启动后显示一个空的可视文档(文件会话在 C14.2)。实测(macOS,`tests/scripts/mark-content.sh`):三种模式下标题、任务、表格、脚注、行内与块公式、代码块(逐块滚入视口,须可见、未被遮挡)、Mermaid 图(不小于 100×80)与两种错误,切换深浅色后图的颜色随之变,连续两次显示只留并只报后一次,截窗口背景色占比低于 0.97,KaTeX 字体已载入,无越界请求;`.app` 包内页面显示。未验证:Windows 与 Linux;树莓派上 2.5 MB 主脚本的载入时间。 - 2026-10-07:ElyTree 分支、标签、合并、变基、冲突(C13.4,TREE-07 至 09,§13.3)。Git 一律以 `GIT_EDITOR=true`、`GIT_SEQUENCE_EDITOR=true` 运行(没有终端可开编辑器,合并与撤销照 Git 自己拟的信息);失败时 stderr 为空即报 stdout(合并的冲突写在 stdout)。左栏第三页“分支”:本地分支(当前在前)、远程分支、标签(新的在前),`for-each-ref` 读出(附注标签取所指提交与标签自身的说明,`origin/HEAD` 不算分支,上游已删即不显示领先落后),上方“新建分支…”“新建标签…”;选中一个,中间列出提交、日期、上游与说明,以及适用的动作:切换(远程分支为 `switch --track refs/remotes/名`,不被同名标签劫走)、合并到当前分支、把当前分支变基到此、在此新建分支、重命名、删除(`branch -d`,Git 拒绝时报原话)、强制删除(`-D`)、删除标签。历史页提交详情多出拣选、撤销提交(合并提交以 `-m 1`)、在此新建分支、在此新建标签。新建分支可选“并切换过去”(`switch -c`);标签有说明即附注标签(`tag -a -m`,签名照用户的 `tag.gpgSign`)。变基、删除、强制删除、删除标签、中止、跳过每次都先问(每个确认只对这一次,§13.3),合并、拣选、撤销、继续不问。进行中的操作按 Git 目录里的 `rebase-merge`/`rebase-apply`、`MERGE_HEAD`、`CHERRY_PICK_HEAD`、`REVERT_HEAD` 判定(每次状态重读时读,只取最新一次),中间上方的提示写明正在把谁并到谁(变基时“当前侧”是新基底、“另一侧”是正在重放的提交),给继续(`<操作> --continue`)、跳过(合并无此项)、中止。命令失败时,只有合并、变基、拣选、撤销,且 Git 的输出有以 `CONFLICT (` 起头的行、失败后索引仍有未合并条目(`ls-files --unmerged`),才提示“因冲突停下”;其余(如钩子拒绝、冲突中 `checkout --ours` 的钩子失败)对话框显示 Git 的原话。失败的报错带上 stdout 与 stderr 两者(拣选的 CONFLICT 行在 stdout,钩子多写 stderr)。分支与标签的名称取自完整引用名(`refs/heads/`、`refs/remotes/`、`refs/tags/` 之后),同名的分支与标签各归各的;合并与变基用完整引用。分支行内的切换与删除接到同样的入口(删除照样先问)。冲突文件:中间上方是三侧与 `ThreeWayMerge` 的块级选择,各块照工作树文件里 Git 写下的标记分组(不另算一套合并;标记宽度取 `check-attr conflict-marker-size`,照 Git 的 `strtol_i` 读,不是正整数即 7;行须恰为该宽度,长短不同的同形行照正文;只按 LF 断行,同 Git,正文里的独立 CR 不算行界),基底取自 diff3 标记,没有时取 Git 自己 `merge-file --zdiff3 --marker-size` 对三个索引段的结果(块数相同时,只填没有基底的块,diff3 里空的基底照空);下方是可编辑的结果(`CodeEditor`,初为工作树里的带标记文件);选一侧即在结果里解决对应的那一块标记(其余手改照留),结果里的标记数与未选的冲突数对不上时(已手改过)不再按块选,提示在结果里改;“保存”只写工作树文件(经同一仓库的写队列,与 Git 的写操作不重叠;落地时按仓库与路径记下;路径已成符号链接即拒绝,不顺着链接写),“标为已解决”只做 `git add`(须先保存;结果里还有标记时先问),两者分开(§13.3);另有“用当前侧的文件”“用传入侧的文件”(`checkout --ours/--theirs`),一侧没有该文件时还有“以删除解决…”(`git rm`,先问)。符号链接、子模块、任一侧含 NUL(Git 视为二进制)、不是 UTF-8 或缺少的冲突整份解决:写明情形,给“用当前侧的文件”“用传入侧的文件”(只给有该文件的一侧)、“标为已解决”与“以删除解决…”。重读时结果有未保存的手改即保留并提示。真窗口(macOS):合并冲突的提示条、三侧与按块取另一侧后结果更新。未验证:Windows 与 Linux;交互式变基(不提供)。 - 2026-10-07:ElyTree 提交、历史、Blame(C13.3,TREE-05、06)。左栏分“改动”与“历史”两页(`navigation::Tabs`)。提交:改动页下方是 `git::CommitInput`(摘要与说明两个框,摘要满 50 变黄、72 变红),上面一行是 Git 设置里的身份(每次状态重读与窗口回到前台时重读,只取最新一次;`user.name `,未设即写明,点它开身份对话框:姓名、邮箱(两个输入框以其标签为无障碍名),可勾“用于所有仓库”写 `--global`,否则写本仓库;空着即提示不写)与签名(`commit.gpgsign` 为真时写明以 `gpg.format` 签名,默认 openpgp),签名照用户自己的 Git 与工具,Ely 不另设。提交框按仓库各存一份(换标签时存下当前的、换上另一个的:摘要、说明、修改最近提交与其草稿),提交为 `commit --cleanup=whitespace -F -`(信息经标准输入,`#` 开头的行照留,用户的 `commit.cleanup` 不删它们,不经 shell),“修改最近一次提交”勾上即显示为勾选、读出上次的信息填入(读回前不能提交;先存下已键入的,取消勾选时放回;每次勾选或取消、换仓库都使在途的读取作废,读回时须仍是发起的仓库与那一次),提交成功后只在提交框仍是所提交的内容时清空(提交期间又键入的留着,别的仓库的提交框不动);钩子或签名失败时以对话框显示 Git 的全部原话,不只说失败。提交同其他写操作排队。历史:`log --topo-order --exclude=refs/stash --all`,每页 2000 个,滚到距末尾 20 行内读下一页;每次状态重读后比对 `show-ref --head`,引用变了(含外部提交、切分支、取回)即从头重读;读取期间来的比对记下,读完再比;引用读不出时历史栏显示 Git 的原话,之后读通即重读并清掉;提交图由 `git::lanes` 按已读的全部提交算出;时间按本机时区写到分钟(不用相对时间,长列表不必随时刷新)。搜索:在“提交信息”(`--grep`)、“作者”(`--author`)、“改动的文字”(`-S`)中按字面、不分大小写找,或按提交号(`rev-parse --verify`,前缀可);搜索时不画图。选中一个提交:中间上方是摘要、说明、作者与提交者(不同时才列)、完整提交号、父提交(可点,转到父提交)、签名状态(`%G?` 按字母译出,带签名者)、引用;下方左列是相对第一个父提交改动的文件(`diff --name-status -M` 与 `--numstat`,根提交对空树,空树由 `hash-object -t tree` 算,不写死 SHA-1),右边是所选文件的差异(与改动区同一读法:Git 自己的差异,图片、二进制、非 UTF-8 同样处理,无块的纯重命名显示为无改动)。改动区差异与历史页所选文件的标题栏都有“文件历史”与“逐行追溯”(改动区限已跟踪的文件):文件历史为 `log --follow --numstat -z`,跨重命名,每个提交带当时的路径与行数,选中即显示该提交里这个文件的差异(状态按两侧是否存在判定);逐行追溯为工作树文件的 `blame --porcelain`,未提交的行写“尚未提交”,每段首行可按,转到历史页的该提交;条色按提交时间新旧。读取都在后台,按发起时的仓库、提交与文件核对;文件历史与逐行追溯每次打开有窗口内唯一的序号,迟到的结果即弃。提交详情的作者、提交者、提交号、签名、引用、摘要与说明在 AX 树里有文字。真窗口(macOS)经 AX:历史页、提交图、引用、提交详情、文件列表与差异。未验证:Windows 与 Linux;GPG 与 SSH 实际签名(只读设置,签名由用户的 Git 完成)。 - 2026-10-07:ElyTree 差异与暂存(C13.2,TREE-03、04)。差异一律取 Git 自己的输出(`-c diff.suppressBlankEmpty=false diff --no-color --no-ext-diff --no-textconv -U3`,前缀写明 `a/`、`b/`,覆盖用户的 noprefix 等设置;空白的上下文行照样带前导空格):已暂存为 `--cached -M`(HEAD 对索引,即提交前的实际索引内容),改动为索引对工作树,未跟踪为 `--no-index` 对 `/dev/null`(退出码 1 即有差异);Git 的清理过滤(换行转换等)由它自己做,按块、按行暂存的补丁对得上索引。块之间未改动的行从旧侧的 blob 取(`cat-file blob`,已暂存为 `HEAD:旧路径`,改动为 `:路径`),可展开;行号按块头推算,旧文件比差异短即报错。改动的词按每对删去与加入的行标出(Unicode 词)。空白:精确、忽略空白变化(`-b`)、忽略全部空白(`-w`);行内与并排。按块或按行:只在精确比较、且是普通内容改动时(无新建、删除、重命名、复制、权限变化的头),已暂存区为取消暂存(`apply --cached --reverse`),改动区为暂存(`apply --cached`);未跟踪、重命名等整份暂存,界面写明。补丁由所选行切出:暂存时未选的删去行留作上下文、未选的加入行不写;取消暂存时反之;块头按“范围前的行数”换算,空范围照 Git 的写法;“无换行结尾”按切出后两侧的实际行尾:无换行的行在哪一侧后面还有行,那一侧就带上换行(所选的删去或加入行同样),两侧不同的上下文行拆成一删一加;`apply` 带 `--whitespace=nowarn`,用户的 `apply.whitespace` 不改所选的字节。写入前在后台重读同一条差异,与所选时读到的字节不同即拒绝(“文件在显示后改了,未暂存”),索引不动;同一仓库的写仍排队。所选行按住 Shift 取同一块内的范围;差异一变(任何重读得到不同的输出)所选即清空;每次读取有窗口内唯一的序号,换文件后旧文件迟到的读取即弃。图片(png、jpg、gif、webp、bmp、tiff、ico)以 `BeforeAfter` 左右拖动对比,一侧缺失时只显示另一侧;其他二进制文件,以及两侧任一侧不全是 UTF-8 的文件(差异以外的字节也算),显示两侧大小,整份暂存;文件与符号链接互换时 Git 为同一路径打出两段(删去与新增),两段都显示(不折叠:两段没有共同的旧文),整份暂存;工作树侧照 Git 读:符号链接读其自身的路径,不跟随;未跟踪的文件夹与冲突文件(冲突在 C13.4 处理)只说明。单击文件即在中间显示其差异(已忽略的文件以系统程序打开),标题栏另有“打开文件”。外部改动后状态重读,正显示的差异随之重读;文件不再列在原分区即收起。真窗口(macOS):改动区选两行暂存,索引恰为所选,已暂存区显示实际索引内容,并排视图;AX 树中各行、块按钮与分段可按。 @@ -278,7 +261,6 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-09(用户报:ElyPDF 改字报“这种字体画不出 V”):文档自带的子集字体常无可用的 Unicode 表(LibreOffice 导出的子集表为占位,0x00 起逐一对应字形号),按 Unicode 找字形一律失败。现以“文档自带字体”写字时直接用页面上的那个字体对象(`shim/ely_pen.c`;`pdf_load_font` 按对象缓存,与文字层的字体同一对象),以它自己的编码写:按文字提取的读法(ToUnicode,缺项或坏值退回 cid_to_ucs,同 pdf-op-run.c)倒出 Unicode 到编码的表,排版取它的宽度(`pdf_lookup_hmtx`),原有的 ToUnicode 与宽度不动,两个字共用一个字形时各写各的编码(原先按字形号写、重建 ToUnicode,共用字形的字被改写,且 CMap 漏写条目数,PDFKit 读不出字)。删去原文字块后页面资源里可能已无该字体,故按对象号以新名字重新引用。只支持编码即 CID 的字体(简单字体与 Identity-H),其余拒绝并说明;页面上没有的字仍报缺字。排版按同一套宽度量出的框,比较时留 0.01 点的余量(原块宽度即这些宽度之和,浮点误差曾使末字折行)。 - 2026-10-09:ElyTable 的自动筛选(R2.9)。内核会重新推算未命名的自动筛选区:每次打开筛选列表时向下扩到紧邻的数据行(`ExtendDataArea`、`ExtendBackColorArea`,为接上后来追加的行),命令从单格找到它时按连续数据区重算(`ScDocShell::GetDBData`:应用条件、关闭筛选及其撤销);选区下方紧邻的合计行因此进了列表、随数据一起被隐藏,存后重开的筛选区同样会扩。补丁 `0040-lok-calc-autofilter-keeps-its-rows.patch`:LOK 下已有的自动筛选区保持设定时的行(同 Excel),不再隐式扩展(光标在区内时;光标在同表另一块数据里即按那一块);单格开筛选时仍取整个数据区。代价:紧贴在筛选区下方追加的行不自动并入,需重新选定后再开筛选。 - 2026-10-09:ElyTree 的差异按文件类型着色(R2.12)。组件库 7f2ac3e 至 b0dbfd4:`DiffViewer`(建立时按路径定下读法) 按路径的扩展名判定散文与纯文本(md、markdown、txt、rst、adoc、org、csv、tsv、log 等,无扩展名同,不着色),代码按语言着色(组件库 b0dbfd4 的 `forms::syntax`:Rust、JavaScript/TypeScript、类 C、Python、Ruby、Shell、YAML、INI、SQL、Lua、Haskell、Lisp、Erlang、TeX 与其他 `#` 语言各有行注释记号、关键字与常量;Shell 与 YAML 的 `#` 只在词首起注释;Makefile、Dockerfile、CMakeLists.txt 先按文件名认),不是完整的语法分析器(字符串插值、块注释、多行字符串照通用词法)(原先一律套 `//` 的通用词法,Markdown 的 `#`、Python 的注释都被当作代码上色)。同批带入上游的块编辑器跨块选择、浮层滚轮、输入法候选框位置、插入点按设备像素对齐与格式修正。 -- 2026-10-09:ElyMark 的表格(R2.13)。表格的空格与分隔行写法有许多种(`|a|b|`、`|---|`、对齐空格),逐字重现做不到,原先凡写不回原样的表格都成了原文块。现在一块的序列化与原文在把其中每张表格化成规范形式(各列对齐方式加各格的原文,`tables.js` 的 `canon`)后相同,即为可视块(`preserve.js` 的 `kept`),引用与列表里的表格同样;格内有 Milkdown 会改写的写法(`[[…]]`、引用式链接等)即不同,整块仍为原文块,与段落的规则一致。Milkdown 的列表项必须以段落开头,表格作列表项的第一块时无法放进编辑器,照原文块保留。未改的表格照旧逐字写回,改过的表格按文档多数表格的写法重写(`style.js` 按表格的行节点投票:单元格两侧有无空格、是否按列对齐,经 remark-gfm 的 `tableCellPadding` 与 `tablePipeAlign`;原先投票的解析器不认 GFM,表格从未计票),分隔行随之重写(如 `|---|` 成 `| - |`)。 - 2026-10-09:ElyTable 新插图表的位置(R2.10)。内核按“定位区域”把图表放在其旁、并挤回当前可见区;带参数的请求(Ely 经 `FN_PARAM_5` 给区域)原把定位区域取为光标格,图表压在数据上或旁边的另一块数据上。补丁 `0041-lok-calc-chart-beside-its-data.patch`:LOK 下有请求区域时,图表放进数据区右侧(隔一列)第一块够大、无合并格的空单元格(逐列向右找,跨过旁边的数据块),右侧没有依次找下方、左侧、上方,再按行带找全表第一块够大的空处(尺寸从候选格起累加,表尾也快),不受可见区限制;空处按图表所在的工作表查,数据在别的表时从光标起找;整张表没有空处即插入失败、文档不动;`LokView` 在新选中对象(原先无选中)时把它滚入视口,拖动中的对象不滚动。 - 2026-10-09:ElyPDF 编辑与缩放不闪白(R2.6)。旧图块不再随编辑或比例变化即刻丢弃:编辑落地时全部当前图块、比例变化时离开视图的同页图块,连同当时的页面尺寸进入“底图”(`apps/ely-pdf/src/backdrop.rs`),按其在页面中的比例位置画在该页新图块之下,直到该页在视口里的新图块到齐、或页面尺寸与旋转变了才释放;每页画什么由 `PdfView::pieces` 决定,绘制与测试同用它。只有改内容的编辑保留底图并沿用已测的页面尺寸(前 100 页以外不再被占位尺寸覆盖);增删、移动、旋转、裁剪页面与撤销重做(`PdfView::edit_pages`)清空底图,因同一序号可能已是另一页。关闭标签时底图随图块交给窗口释放。原先比较整份尺寸表(超过 100 页的混合纸张文档尺寸表后段为占位,比较失败即全部清空)且只认同比例的图块(编辑后右栏收起、比例变化即清空),都会闪白。 - 2026-10-09:文档的缩放锚点(R2.7)。窄于视口的文档居中,缩放原按视口坐标求锚点、漏了居中的偏移,锚点随缩放漂移;现锚点先减去缩放前的偏移,缩放后按新的偏移(`LokView::slack`,绘制同用)换回。按键与菜单的缩放(无指针)另保持首页左缘在视口内;指针与捏合的缩放保持指针下的点不动(文档边界以外即止)。 @@ -286,7 +268,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-09:作者名与读不出的设置(缺陷清单 X-02)。签名用的名字取启动时读入的通用设置(`ely_ui::settings::current`;文件读不出时为默认值,设置窗口另以 Banner 写明),`ely_platform::user::named(作者名)` 为空即用账户名;不再在引擎启动时重读文件(原先 `settings.json` 读不出时 Doc、Table、Show 以致命错误退出)。LOK worker 记下设定的作者名,引擎重启照用它。`tests/scripts/bad-settings.sh` 核对读不出的设置下 ElyDoc 照样启动、文件不被覆盖。 - 2026-10-09:每个应用只跑一份(缺陷清单 X-03)。`ely_ui::run` 在写日志之前取应用数据目录的 `instance.lock`(`ely_platform::instance::hold`;Unix 为 fcntl 记录锁,进程结束即放,被拒时以 `F_GETLK` 向系统问出持锁的进程号,不另存进程号文件,同时启动的几份也报得准;Windows 为 LockFileEx,不报进程号);已有一份在跑时第二份不启动,报“已在运行(进程号)”。原先第二份启动即清空第一份的工作目录、把它的恢复点当作干净结束删掉、改写崩溃锁。macOS 经访达打开文件本就交给运行中的那份;`open -n` 与 Linux 每文件一个进程时第二份被拒,文件须在已开的窗口里打开(未做转交)。`tests/scripts/one-instance.sh` 核对。 - 2026-10-10:ElyMark 的链接(M-05,我定,同常见编辑器):可视模式单击链接是编辑,Cmd 单击(Windows、Linux 为 Ctrl)交应用;预览单击即交应用;应用照 ElyPDF:http、https、mailto 交系统打开,其余提示拒绝。 -- 2026-10-10:Windows 与 Linux 的构建环境(E-01)。Windows:MSVC(VS 2022,v143);MuPDF 由 `build-mupdf.sh` 以 MSBuild 编上游 `platform/win32` 的 `libmupdf`(静态运行库 /MT,同 Rust 的 `crt-static`;`HAVE_LIBCRYPTO`,`libmupdf.lib` 内含第三方、Tesseract 与 PKCS#7);静态 OpenSSL 取自钉在 2026.07.29 的 vcpkg(`engines/vcpkg`,`openssl:x64-windows-static`);Git 为 Git for Windows 的 MinGit 2.56.0(钉散列,`engines/git/cmd/git.exe`,LFS 在 `ucrt64/libexec/git-core`);CEF 照 cef-dll-sys 放在可执行文件旁(Windows、Linux 都是),不另载入;`%TEMP%` 下的程序被系统的应用控制拦下,构建与探针都不放那里;cmake 与 ninja 须在 PATH;LibreOffice 的 configure 只认 VS 自带的 CMake(组件 `VC.CMake.Project`)。仓库与 `reference/` 一律 LF(`core.autocrlf false`、`core.eol lf`;`fetch-references.sh` 对每个上游照设)。Linux:WSL 的 Ubuntu 26.04;静态 libcrypto 另需其 pkg-config 列出的库(jitterentropy、zstd、z)。LibreOffice 在 Linux 照 Collabora 的 LOKit 配置构建(无图形后端、cairo RGBA、自带各库)。 +- 2026-10-10:Windows 与 Linux 的构建环境(E-01)。Windows:MSVC(VS 2022,v143);MuPDF 由 `build-mupdf.sh` 以 MSBuild 编上游 `platform/win32` 的 `libmupdf`(静态运行库 /MT,同 Rust 的 `crt-static`;`HAVE_LIBCRYPTO`,`libmupdf.lib` 内含第三方、Tesseract 与 PKCS#7);静态 OpenSSL 取自钉在 2026.07.29 的 vcpkg(`engines/vcpkg`,`openssl:x64-windows-static`);Git 为 Git for Windows 的 MinGit 2.56.0(钉散列,`engines/git/cmd/git.exe`,LFS 在 `ucrt64/libexec/git-core`);`%TEMP%` 下的程序被系统的应用控制拦下,构建与探针都不放那里;cmake 与 ninja 须在 PATH;LibreOffice 的 configure 只认 VS 自带的 CMake(组件 `VC.CMake.Project`)。仓库与 `reference/` 一律 LF(`core.autocrlf false`、`core.eol lf`;`fetch-references.sh` 对每个上游照设)。Linux:WSL 的 Ubuntu 26.04;静态 libcrypto 另需其 pkg-config 列出的库(jitterentropy、zstd、z)。LibreOffice 在 Linux 照 Collabora 的 LOKit 配置构建(无图形后端、cairo RGBA、自带各库)。 - 2026-10-10:Windows 与 Linux 的内核与 worker(W-01、K-01,用户定:0.1.3 先三端发布)。内核只认合并库(`ffi::KERNEL`:`libmergedlo.dylib`、`libmergedlo.so`、`mergedlo.dll`;`sofficeapp` 在合并构建里是文本占位);Windows 载入时一并搜索该 DLL 所在的 `program/`。查找次序照旧,Windows 与 Linux 的包内位置是应用旁的 `libreoffice/program`,开发构建为 `engines/lo-src/instdir/program`。worker 的主循环:macOS 照旧以 GCD;Linux 以 unipoll,`runLoop` 的轮询回调里每次答一个请求,内核的唤醒回调往同一队列放信号(`worker/unipoll.rs`;回调的数据指针须非空,VCL 见空即不调用);Windows 也以 unipoll:LOK 不另起主线程,worker 的主线程跑 `runLoop`(Windows 的消息循环),请求经只收消息的窗口投到这条线程上执行(`worker/messages.rs`),同 macOS 的 GCD;原先 LOK 自起主线程、worker 在另一线程作答,载入表格即死锁。Windows 的循环不调轮询回调,但须传非空的回调,`isUnipoll` 只看它。补丁 `0043-lok-callbacks-flush-inline-everywhere.patch`:回调队列在各平台都即时冲出(原只 macOS),否则请求里等的命令结果与状态要等下一轮主循环才到。`0042-svx-boost-json-needs-no-library.patch`:svxcore 与 sd 定义 `BOOST_ALL_NO_LIB`,MSVC 不再按 Boost 的自动链接索要 `libboost_json`、`libboost_container`。Linux 的内核以 cairo 画图块(worker 设 `SAL_DISABLESKIA`;构建去掉 `--enable-cairo-rgba`,自带的 cairo 要清掉重建),字节为 BGRA,同 macOS 与 Windows;`getTileMode` 只看 cairo 开关,不可信。worker 启动核对在三端都画一段红字,按 BGRA 找不到红色即拒绝。`scripts/apply-lo-patches.sh` 的临时库设 `core.autocrlf false`(Windows 默认转换使任何前缀都对不上)。Windows 的 LibreOffice 在 Cygwin 下构建(`build-libreoffice.sh` 的 CYGWIN 分支:去掉 Unix 专用开关,加 VS 2022,用 `/opt/lo/bin` 的 make,清掉 Git Bash 带进来的 `MSYSTEM`);构建目录与 Cygwin 排除在 Defender 扫描之外(否则并行 configure 删不掉 `conftest.dir`)。英文词典扩展里的 Lightproof(Python 写的语法检查)在构建后删去:macOS 与 Linux 的内核不带 Python,它本不生效;Windows 必带内置 Python(Cygwin 下 configure 不许去掉),第二次用同一配置启动时初始化它,worker 挂起。worker 以 `SAL_NOLOCK_PROFILE` 运行,`ely.xcd` 关掉 LibreOffice 的文档锁(`Common/Misc/UseLocking`):被强杀的 worker 留下的锁会让下次启动或载入弹出交互而挂起;每个应用只有一个 worker 用自己的配置,冲突由 Ely 自己核对。三端发布(CI-01):`.gitea/workflows/release.yml` 在打标签时于三台运行器上执行 `scripts/ci.sh`:macOS 在 Mac mini 的常驻克隆里借用开发克隆 `~/Documents/GitHub/ElyOfficeSuite` 的引擎(`ELY_SHARE`;2026-10-11 起:从零构建 LibreOffice 超过 act_runner 默认的 3 小时,v0.1.3 的 Mac 任务被杀、无 dmg),Windows 在本机运行器上把 `engines` 与 `reference` 接到开发克隆,Linux 由同一台 Windows 运行器经 WSL 构建(服务器上的 Linux 运行器是一次性容器,建不了内核);各自打包后上传到该标签的发布。Windows 为 NSIS 安装程序,Linux 为 AppImage、deb、rpm 与 Arch 包(引擎构建与打包都经 `packaging/linux/container.sh` 进 Ubuntu 26.04 容器,WSL 里须有 docker;`ELY_SHARE` 的目录一并挂进容器),都由 `packaging/stage.sh` 分装引擎;每个平台都上传本提交的源码包(`packaging/source.sh`);更新清单只含 macOS(Windows 与 Linux 的自动安装未做)。 - 2026-10-11:gpui 的 Windows 窗口关闭(W-03 冒烟查出)。`gpui-pre-windows` 0.3.8 在窗口关闭时报三条错:系统已销毁窗口(ElyTree 的关窗交给系统)后 Drop 仍对旧句柄 `RevokeDragDrop` 与 `DestroyWindow`(拖放处理器因此不释放,连带窗口状态);销毁时的失活回报排在异步任务里,执行时窗口已移除(“window not found”);退出后后台任务向已销毁的平台窗口投递(`Invalid window handle`)。修复在 `vendor/gpui-pre-windows`:`WM_DESTROY` 时记下已销毁、在句柄仍有效时撤销拖放、去掉失活回调,Drop 见已销毁即不再动句柄;平台窗口已不在时投递失败只记 debug(应用已退出),其余投递失败照报。三个应用关窗与退出的日志无错。 - 2026-10-11(用户报:Windows 启动带黑色命令行窗口、“打开方式”里无图标、点 X 不退出、表格全黑无网格线):四处根因。命令行窗口:应用是控制台子系统;发行构建改为 `windows_subsystem = "windows"`(开发构建照旧有控制台看日志),子进程(两个 worker、Git、`cmd start`)一律经 `ely_ipc::windowless` 以 `CREATE_NO_WINDOW` 启动。图标:exe 里只有 gpui 的清单资源;各应用共用构建脚本 `packaging/windows/icon.rs`,把 `assets/icons/<应用>.ico` 编成资源 1(gpui 的窗口图标也取资源 1)。点 X 不退出:窗口其实弹了“保存更改”询问。`ely.xcd` 深色方案的 `CalcNotesBackground` 与浅色不同,Calc 在配色变化时改写批注样式并记一步撤销(`ScDetectiveFunc::UpdateAllComments`),未动的表格切到深色即算已修改;两套方案的批注底色改为同一值(LibreOffice 默认的浅黄),切换主题不再改文档,三端同此(`sheet.rs` 的 `a_theme_switch_is_no_edit`)。全黑:Win32 的虚拟设备没有 `SetSizeUsingBuffer`(基类默认返回假,断言在发行构建里不在),给客户缓冲区的设备停在 1×1 像素,Calc 图块只画了一个像素,放映图层同空;补丁 `0044-lok-win32-paints-then-copies.patch` 让 Windows 走 macOS 的路:按全尺寸画进设备,再拷进客户缓冲区(`doc_paintTile` 与 `SlideshowLayerRenderer`)。Writer 自设设备尺寸,原本不受影响。`tests/scripts/lo-patches.sh` 的临时库也设 `core.autocrlf false`(原先在 Windows 上恒失败)。 @@ -294,7 +276,6 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-11:Linux 标题栏按钮(核查 Windows 按钮问题时查出)。macOS 的红绿灯由系统画,不受影响;Linux 为客户端装饰,按钮由组件库画在标题栏的拖动区里,拖动区对任何按下都准备移动窗口,按住按钮时指针动一像素即开始移动窗口,合成器接管指针,松开到不了按钮,点击落空。组件库 3eb4d34:`drag_region` 只在按下未被里面的按钮认领(按钮以 `prevent_default` 认领)时才准备移动;`shell/title_tests.rs` 核对晃动的按下点得动 Linux 的关闭、标题栏空处照样移动窗口(去掉修复时前者失败)。未在真 Linux 桌面上点过。 - 2026-10-11(用户报:ElyTree 改动列表里文件名长时盖住行尾的打开按钮):组件库 bd01470:`ChangesList` 行里的文件名原为不缩的盒子,长名越过行宽压在按钮下;现可缩、以省略号结尾,先于按钮让位;`git/tests/narrow.rs` 核对 260 宽时文件名止于按钮之前(原布局时失败)。 - 2026-10-11(用户定:ElyTree 状态栏去掉 Git 的版本与路径那一项):所用 Git 的版本与路径只记日志。同时查出 Windows 上 `std::fs::canonicalize` 给出 `\\?\C:\…` 形式,仓库根、Git 目录、工作树与文件的真实路径一律改用 `dunce::canonicalize`(能写成普通路径即普通路径,同一文件仍是同一真实路径),状态栏的仓库路径随之为普通形式;ElyTree 的单测在 Windows 上由 44 个失败降到 8 个,余下 8 个改前同样失败(如写入 Windows 不许的文件名),未处理。 -- 2026-10-11:ElyMark 的 AppImage(0.1.3 的 Linux 发布查出)。`linuxdeploy` 把可执行文件的 RUNPATH 改成 `$ORIGIN/../lib`,并把它找得到的 `libcef.so` 拷进 `usr/lib`:原先 `libcef.so` 与 CEF 的资料在 `usr/bin`,结果是两份 1.45 GB 的内核、程序载入 `usr/lib` 那份、CEF 在它旁边找不到 ICU 数据而退出(从未在 AppImage 里启动过)。现在 `package.sh` 把从 `engines/cef` 分装来的全部条目移进 `usr/lib`(按名字对照 CEF 文件夹,清单仍只在 `stage.sh`),以 `LD_LIBRARY_PATH=AppDir/usr/lib` 运行 `linuxdeploy`,`libcef.so` 只此一份、依赖照样带上,CEF 按所载 `libcef.so` 的文件夹取资料(`ely-web-content` 的 `open`)。ElyMark 的 AppImage 由 715 MB 降到 423 MB;WSL 里启动后 CEF 载入、窗口打开(WSL 里各应用都不出首帧,未在真桌面上验证)。 - 2026-10-09(用户定):代码与发布从 GitHub 搬到自建的 Gitea(git.zacharyzhang.com;GitHub 账号被封,推送与发布都被拒)。公开仓库 `ZacharyZhang-NY/ElyOffice`(原样推上 main、缺陷清单分支与 v0.1.0、v0.1.1 标签),组件库为同站的 `ZacharyZhang-NY/Ely-GPUI-Components`(Cargo 的 git 依赖与 `fetch-references.sh` 随之改址,钉的提交不变)。v0.1.1 的六个 dmg 与两个源码包重新发在 Gitea 的 v0.1.1 发布里。官网的下载与源码链接指向 Gitea(`releases/latest` 会跳到最新发布页)。`.github/workflows` 删去(它们靠 GitHub 的 macOS 机器与 `gh`,发布改在本机以 `packaging/macos/release.sh` 构建、经 Gitea API 上传)。发布视频里写的地址已在源文件改为 Gitea,已发布的成片仍是旧地址,重渲染后才更新。上游项目(LibreOffice、MuPDF、git-lfs、tessdata 等)照旧从它们自己的 GitHub 地址取。 - 2026-10-11(用户报:Omarchy 上六个 AppImage 都打不开):`gpui-pre-platform` 对 `gpui-pre-linux` 关了默认特性,Wayland 与 X11 只经它自己的 `wayland`、`x11` 特性打开;工作区原只开 `font-kit`,Linux 上 gpui 退成无头平台,日志照写“window open”,合成器里从无窗口。工作区的 `gpui_platform` 加开两者;`tests/scripts/linux-backends.sh` 核对特性,在 Hyprland 上核对 Wayland 与 X11 两路都映射出窗口、截图里有均匀的深色页面,tesseract 读出起始页的 “Open a repository”(不交缓冲的窗口 Hyprland 不映射;不画字形时读不出)。 - 2026-10-11:Linux 的 inotify 把打开文件报成事件(notify 的 `Access`),macOS 的 FSEvents 不报;四处文件监视原把任何事件当作改动,重读本身又触发下一次:Omarchy 主题与通用设置在第一次改动后、ElyTree 打开仓库起(`git status` 读仓库),空闲时每 250 毫秒重读。四处都经 `ely_ui::watching::only_read` 略去读取(打开、读、只读关闭),写后关闭照算改动(经内存映射的写入只报 CLOSE_WRITE,不报 MODIFY);`tests/scripts/linux-idle.sh` 以 inotifywait 从外面数空闲时的读取与 git status 次数。 @@ -303,4 +284,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-11:Linux 包的实测(L1.6)。本机(Omarchy,Hyprland 0.56)装上六个 Arch 包与六个 AppImage,`linux-installed.sh` 两种都过;deb、rpm、Arch 包在 Ubuntu 26.04、Fedora 44、Arch 容器里装上、全部 ELF 找得到库;deb 与 rpm 的六个应用在各自容器的 Xvfb 下画出窗口(`linux-distros.sh`)。截图按窗口(`grim -T` 加 Hyprland 的 `stableId`):按区域截时用户切换工作区或别的窗口盖住即截错。ElyMark 的文件监视略去读取后不会自激:重读前先比指纹(长度与修改时间),不打开文件。 - 2026-10-11(用户定:替换 v0.1.3 的 Linux 包):v0.1.3 发布里原有的六个 AppImage(无头,打不开窗口)已删去,换成本机容器里建出的六个应用各四种包(共 24 个),出自 L1.7 的提交(晚于该标签);上传后逐个核对大小,ElyTree 的 AppImage 下载回来散列相同。`package.sh` 把每个 RUNPATH 按所在 ELF 解析(`$ORIGIN` 或 `${ORIGIN}` 开头,规范化后须在 AppDir 内),否则拒绝(如未修正的 cairo):CI 用的是别处建好的引擎,旧构建会在这里报错而不是带进包里。替换的 Linux 包对应的源码随之上传到同一发布(`source.sh`,文件名带构建提交号),发布说明写明 Linux 包出自哪个提交。 - 2026-10-11:`ci.sh` 用 `ELY_SHARE` 时,CI 克隆里的 `engines`、`reference` 须是指向共享克隆的链接(Windows 为目录联接,不查);是本克隆自建的目录或指向别处即报错,不混用。`.gitignore` 的 `/engines`、`/reference` 不带斜杠,链接也被忽略(`source.sh` 要求工作区干净)。 -- 2026-10-11(用户定:ElyMark 不用 CEF,不用 Electron):ElyMark 改为原生。界面用组件库的 Markdown 组件(`documents::MarkdownEditor` 一族,缺的在组件库补并推到 Gitea),解析用 markdown-rs(`markdown` 1.0.0),公式用 RaTeX,Mermaid 用 mermaid-rs-renderer,PDF 用 MuPDF Story;CEF 随之删去,ElyMail 的正文也不用 CEF 与 Electron。进度在 `tasks/TASKS.md` 的 C14。组件库 fcad0ff 与 508cc55:`MarkdownRenderer`、`MarkdownEditor`、Markdown 着色、块范围与链接查找改读 markdown-rs 的语法树(GitHub 风格加公式),引用式链接与图片按定义解析,图片把所在段落分开,单行 `$$…$$` 为块公式,软换行(LF、CRLF、CR)读作空格,嵌套在引用与列表里的定义同样解析,强调与链接里的图片仍是图片(链接里的图片可按);`BlockEditor` 的导入导出不属 ElyMark,仍用 pulldown-cmark。markdown-rs 读嵌套图片时只留外层的说明。组件库 d5ecc5a:块公式经 `typography::Latex` 由 RaTeX 排出(字形为轮廓,按窗口缩放画),```` ```mermaid ```` 块经 `documents::Diagram` 由 mermaid-rs-renderer 画出(Mermaid 自己的深浅主题,底色与字色取 Ely 色板),都作为 gpui 资源在后台排版,RaTeX 或 Mermaid 对已解析的输入 panic 时显示为错误;`MarkdownRenderer::base` 给出文档所在文件夹,相对与 file 地址的图片从中读取并按自身大小、不超过栏宽显示,`data:` 地址解码,网络图片不载入(写明)。组件库 ab9a14c:每个公式、图与图片元素经 `primitives::held` 持有其资源,按资源计数持有者(8b9820b;画出的图存在共享的记录里,9036c92 起由最后离开的持有者释放,哪怕它自己还没收到图),最后一个持有者换了内容或离开窗口才从 gpui 资源缓存移除、在本帧之后从图集释放,共用同一内容的元素(含 gpui 缓存的视图)不受影响;公式按字形实际范围取画框,字形不被裁;公式失败显示原因、RaTeX 给的位置与原文,日志不含公式;`file:` 地址去掉查询与片段,`localhost` 为本机,Windows 上别的主机为共享路径,他处拒绝;Mermaid 的节点与饼图文字取 Ely 的前景色。组件库 2b6f551:`documents::MarkdownDocument` 是文件级的 Markdown 文档:全文与撤销在一个 `CodeEditor` 里(`CodeEditor::highlighter` 以 `markdown_highlights` 着色,无行号);可视以 gpui `list` 逐块画出文件的顶层块(链接定义、HTML、front matter 照原文显示,每个字节都在块里或块间),按下的块开成源码(`TextInput`),其键入即刻算作文档的文字,写回为一步撤销,上下键越过块边进入相邻块,Esc 写回;Cmd(他处 Ctrl)按下交给链接;预览只读;源码为编辑器本身;128 KiB 以内当场解析,更长的在后台解析,解析中按下块不打开(日志);新解析保留两端相同的块,滚动不跳。`CodeEditor` 的撤销历史最多留 64 MiB 文字,超出先丢最旧的步;`CodeEditor::reset` 换文字且撤销重新开始;`MarkdownMode` 加 `Preview`。组件库 fc181b4:一个插入点通用于各模式(编辑器的选区,写回的块移动它);`set_caret` 与 `set_top_line` 分别设插入点与视图,换模式保持顶行;按下的块在前一块写回后仍是所按的块(按偏移换算,结构变了即不开并记日志);块是 Tab 停靠点,Enter 或空格打开;预览不收按下、撤销与重做;撤销与重做每次记录、输入法组字、撤销、重做后都限在 64 MiB 内(先丢最旧的撤销,再丢最远的重做)。组件库 433c38f:只有对当下文字的解析才能打开块(解析的文字与编辑器相同),否则拒绝;要求的顶行在其文字解析后才落定:先到所在块,下一帧按块高的比例到块内位置,再有新的要求即接着放下一个;源码与列表之间切换保持顶行,可视与预览共用一个列表,切换不动滚动。组件库 abac8b4:顶行的要求带编号,迟到的帧回调只完成自己的那个;文字一换即按新文字重新放,放时按当下文字的行数夹住;`reveal` 是同样的要求(插入点设在该行,该行放到顶上),新文字解析后才落定。 +- 2026-10-11(用户定:ElyMark 不用 CEF,不用 Electron):ElyMark 改为原生。界面用组件库的 Markdown 组件(`documents::MarkdownEditor` 一族,缺的在组件库补并推到 Gitea),解析用 markdown-rs(`markdown` 1.0.0),公式用 RaTeX,Mermaid 用 mermaid-rs-renderer,PDF 用 MuPDF Story;CEF 随之删去,ElyMail 的正文也不用 CEF 与 Electron。进度在 `tasks/TASKS.md` 的 C14。组件库 fcad0ff 与 508cc55:`MarkdownRenderer`、`MarkdownEditor`、Markdown 着色、块范围与链接查找改读 markdown-rs 的语法树(GitHub 风格加公式),引用式链接与图片按定义解析,图片把所在段落分开,单行 `$$…$$` 为块公式,软换行(LF、CRLF、CR)读作空格,嵌套在引用与列表里的定义同样解析,强调与链接里的图片仍是图片(链接里的图片可按);`BlockEditor` 的导入导出不属 ElyMark,仍用 pulldown-cmark。markdown-rs 读嵌套图片时只留外层的说明。组件库 d5ecc5a:块公式经 `typography::Latex` 由 RaTeX 排出(字形为轮廓,按窗口缩放画),```` ```mermaid ```` 块经 `documents::Diagram` 由 mermaid-rs-renderer 画出(Mermaid 自己的深浅主题,底色与字色取 Ely 色板),都作为 gpui 资源在后台排版,RaTeX 或 Mermaid 对已解析的输入 panic 时显示为错误;`MarkdownRenderer::base` 给出文档所在文件夹,相对与 file 地址的图片从中读取并按自身大小、不超过栏宽显示,`data:` 地址解码,网络图片不载入(写明)。组件库 ab9a14c:每个公式、图与图片元素经 `primitives::held` 持有其资源,按资源计数持有者(8b9820b;画出的图存在共享的记录里,9036c92 起由最后离开的持有者释放,哪怕它自己还没收到图),最后一个持有者换了内容或离开窗口才从 gpui 资源缓存移除、在本帧之后从图集释放,共用同一内容的元素(含 gpui 缓存的视图)不受影响;公式按字形实际范围取画框,字形不被裁;公式失败显示原因、RaTeX 给的位置与原文,日志不含公式;`file:` 地址去掉查询与片段,`localhost` 为本机,Windows 上别的主机为共享路径,他处拒绝;Mermaid 的节点与饼图文字取 Ely 的前景色。组件库 2b6f551:`documents::MarkdownDocument` 是文件级的 Markdown 文档:全文与撤销在一个 `CodeEditor` 里(`CodeEditor::highlighter` 以 `markdown_highlights` 着色,无行号);可视以 gpui `list` 逐块画出文件的顶层块(链接定义、HTML、front matter 照原文显示,每个字节都在块里或块间),按下的块开成源码(`TextInput`),其键入即刻算作文档的文字,写回为一步撤销,上下键越过块边进入相邻块,Esc 写回;Cmd(他处 Ctrl)按下交给链接;预览只读;源码为编辑器本身;128 KiB 以内当场解析,更长的在后台解析,解析中按下块不打开(日志);新解析保留两端相同的块,滚动不跳。`CodeEditor` 的撤销历史最多留 64 MiB 文字,超出先丢最旧的步;`CodeEditor::reset` 换文字且撤销重新开始;`MarkdownMode` 加 `Preview`。组件库 fc181b4:一个插入点通用于各模式(编辑器的选区,写回的块移动它);`set_caret` 与 `set_top_line` 分别设插入点与视图,换模式保持顶行;按下的块在前一块写回后仍是所按的块(按偏移换算,结构变了即不开并记日志);块是 Tab 停靠点,Enter 或空格打开;预览不收按下、撤销与重做;撤销与重做每次记录、输入法组字、撤销、重做后都限在 64 MiB 内(先丢最旧的撤销,再丢最远的重做)。组件库 433c38f:只有对当下文字的解析才能打开块(解析的文字与编辑器相同),否则拒绝;要求的顶行在其文字解析后才落定:先到所在块,下一帧按块高的比例到块内位置,再有新的要求即接着放下一个;源码与列表之间切换保持顶行,可视与预览共用一个列表,切换不动滚动。组件库 abac8b4:顶行的要求带编号,迟到的帧回调只完成自己的那个;文字一换即按新文字重新放,放时按当下文字的行数夹住;`reveal` 是同样的要求(插入点设在该行,该行放到顶上),新文字解析后才落定。组件库 f59c9f7:文字超过读取上限(2 MiB)即丢掉旧的读取,大纲与块为空。ElyMark(C14.4):视图即一个 `MarkdownDocument`,文本即文件原文(BOM 与 CRLF 照 `text.rs` 存回),新建、打开、保存、另存、重命名经外壳照旧;视图在建成后的下一轮更新向外壳报一次载入(文档在 `new` 里当场解析,外壳此时还未订阅);文字超过 2 MiB 即只用源码并转去源码(随当前文字,不只看打开时);网页实现的查找替换、表格、附件、书写视图的应用、导出与打印删去,在 C14.9、C14.10、C14.12、C14.14 原生重做。C14.5 删去 CEF:`crates/ely-web-content`、`apps/ely-mark/web`、`scripts/fetch-cef.sh`、`scripts/build-mark-content.sh`、macOS 包里的辅助应用与其授权、Linux AppImage 的 `libcef` 安排、CEF 与网页依赖的许可、存储设置里的网页缓存、`webview.sh`、`hardened-macos.sh`、`linux-mark-idle.sh`、参考源码 cef、cef-rs、milkdown、codemirror-dev;`apps::open` 不再复原信号(只为 Chromium 屏蔽的 SIGTERM 而设)。旧 C14.1 至 C14.7、R2.13 与 ElyMark AppImage 的记录(CEF 实现)删去,在 git 历史;仍在的应用行为(外部变更与比较、会话、改名、Git 状态、最近与文件夹、`look.json`)在 C14.8 至 C14.16 逐条核对后重记。用户以前的 ElyMark 数据目录里可能留着 `web` 缓存文件夹,Ely 不再读写它。 diff --git a/apps/ely-mark/web/build.mjs b/apps/ely-mark/web/build.mjs deleted file mode 100644 index bda6635..0000000 --- a/apps/ely-mark/web/build.mjs +++ /dev/null @@ -1,89 +0,0 @@ -// Builds the page into the folder given; no network. -import { build } from 'esbuild' -import { copyFile, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises' -import { join } from 'node:path' - -const out = process.argv[2] -if (!out) throw new Error('usage: node build.mjs ') -await rm(out, { recursive: true, force: true }) -await mkdir(out, { recursive: true }) - -const built = await build({ - entryPoints: ['src/main.js', 'src/diagram.js', 'src/main.css'], - outdir: out, - bundle: true, - minify: true, - format: 'iife', - target: 'chrome140', - loader: { '.woff2': 'file', '.woff': 'file', '.ttf': 'file' }, - assetNames: 'fonts/[name]-[hash]', - legalComments: 'none', - metafile: true, - logLevel: 'warning', -}) -// Exports carry one stylesheet: fonts inline, woff2 alone. -const exported = await build({ - entryPoints: ['src/export.css'], - outdir: out, - bundle: true, - minify: true, - loader: { '.woff2': 'dataurl', '.woff': 'empty', '.ttf': 'empty' }, - legalComments: 'none', - metafile: true, - logLevel: 'warning', -}) -const sheet = join(out, 'export.css') -// The dropped woff and ttf left empty sources. -await writeFile(sheet, (await readFile(sheet, 'utf8')).replace(/,url\(\) format\("(?:woff|truetype)"\)/g, '')) -await copyFile('index.html', join(out, 'index.html')) - -/** Each bundled package's folder under node_modules. */ -function packages(inputs) { - const found = new Set() - for (const input of inputs) { - const at = input.lastIndexOf('node_modules/') - if (at < 0) continue - const parts = input.slice(at + 13).split('/') - const name = parts[0].startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0] - found.add(input.slice(0, at + 13) + name) - } - return [...found].sort() -} - -/** Name, version and license; the file, else the manifest's. */ -async function notice(folder) { - const manifest = JSON.parse(await readFile(join(folder, 'package.json'), 'utf8')) - const head = `${manifest.name} ${manifest.version} (${manifest.license})` - const file = (await readdir(folder)).find((name) => /^(licen[cs]e|copying)/i.test(name)) - if (file) return `${head}\n\n${(await readFile(join(folder, file), 'utf8')).trim()}\n` - const author = manifest.author?.name ?? manifest.author - if (manifest.license !== 'MIT' || !author) throw new Error(`${manifest.name}: no license file`) - console.log(`${manifest.name} ships no license file; MIT from its manifest`) - return `${head}\n\nCopyright (c) ${author}\n\n${MIT}\n` -} - -const MIT = `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.` - -const folders = packages([ - ...Object.keys(built.metafile.inputs), - ...Object.keys(exported.metafile.inputs), -]) -const notices = await Promise.all(folders.map(notice)) -await writeFile(join(out, 'THIRD-PARTY-NOTICES.txt'), notices.join('\n' + '-'.repeat(72) + '\n\n')) -console.log(`built ${folders.length} packages into ${out}`) diff --git a/apps/ely-mark/web/index.html b/apps/ely-mark/web/index.html deleted file mode 100644 index 11548e1..0000000 --- a/apps/ely-mark/web/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - -
- - diff --git a/apps/ely-mark/web/package-lock.json b/apps/ely-mark/web/package-lock.json deleted file mode 100644 index 02f7975..0000000 --- a/apps/ely-mark/web/package-lock.json +++ /dev/null @@ -1,4871 +0,0 @@ -{ - "name": "ely-mark-content", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "ely-mark-content", - "dependencies": { - "@codemirror/commands": "6.11.1", - "@codemirror/lang-markdown": "6.5.2", - "@codemirror/language": "6.12.4", - "@codemirror/language-data": "6.5.2", - "@codemirror/search": "6.7.2", - "@codemirror/state": "6.7.6", - "@codemirror/view": "6.43.13", - "@lezer/highlight": "1.2.5", - "@milkdown/kit": "7.22.2", - "codemirror": "6.0.2", - "hast-util-from-dom": "5.0.1", - "hast-util-to-text": "4.0.2", - "katex": "0.19.0", - "mermaid": "12.1.0", - "prosemirror-search": "1.1.1", - "rehype-katex": "7.0.1", - "rehype-stringify": "10.0.1", - "remark-gfm": "4.0.1", - "remark-math": "6.0.0", - "remark-parse": "11.0.0", - "remark-rehype": "11.1.2", - "unified": "11.0.5", - "unist-util-visit": "5.1.0", - "vfile": "6.0.3" - }, - "devDependencies": { - "esbuild": "0.28.2" - } - }, - "node_modules/@antfu/install-pkg": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.1.0.tgz", - "integrity": "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw==", - "license": "MIT", - "dependencies": { - "package-manager-detector": "^1.8.0", - "tinyexec": "^1.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", - "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@braintree/sanitize-url": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", - "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", - "license": "MIT" - }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-13.2.0.tgz", - "integrity": "sha512-bA9dvWlhAUcHkNwU+8w9yiUxJPlj/Lm3VqS7MD+NbIR/7LmpNLuK/AWNFdCo/T3SK5ND0/UHf3IJvl/Jn/i6yg==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "13.2.0", - "@chevrotain/types": "13.2.0" - } - }, - "node_modules/@chevrotain/gast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-13.2.0.tgz", - "integrity": "sha512-bCARdAsG+eEbOveSXcOAC7pVxA04I1ionzeMOwIYf5NX7SaS7zgn4Dgug2BSqoL+iyKeMNSorQjsFSHJHsCNwg==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "13.2.0" - } - }, - "node_modules/@chevrotain/regexp-to-ast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-13.2.0.tgz", - "integrity": "sha512-kxpUkLtiaK09ph+dib+Yvm+3sWSnuwlo25gqfKRgTPNmF6H7uW4gVkMjaRdtnN783S9sM6vUNWh9CsxYX4QwKw==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/types": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", - "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-13.2.0.tgz", - "integrity": "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q==", - "license": "Apache-2.0" - }, - "node_modules/@codemirror/autocomplete": { - "version": "6.20.3", - "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", - "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@codemirror/commands": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.11.1.tgz", - "integrity": "sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.7.0", - "@codemirror/view": "^6.27.0", - "@lezer/common": "^1.1.0" - } - }, - "node_modules/@codemirror/lang-angular": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.4.tgz", - "integrity": "sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==", - "license": "MIT", - "dependencies": { - "@codemirror/lang-html": "^6.0.0", - "@codemirror/lang-javascript": "^6.1.2", - "@codemirror/language": "^6.0.0", - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.3.3" - } - }, - "node_modules/@codemirror/lang-cpp": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz", - "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/cpp": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-css": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", - "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.0.2", - "@lezer/css": "^1.1.7" - } - }, - "node_modules/@codemirror/lang-go": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz", - "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.6.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/go": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-html": { - "version": "6.4.12", - "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.12.tgz", - "integrity": "sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/lang-css": "^6.0.0", - "@codemirror/lang-javascript": "^6.0.0", - "@codemirror/language": "^6.4.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0", - "@lezer/css": "^1.1.0", - "@lezer/html": "^1.3.12" - } - }, - "node_modules/@codemirror/lang-java": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz", - "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/java": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-javascript": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", - "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.6.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.17.0", - "@lezer/common": "^1.0.0", - "@lezer/javascript": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-jinja": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-jinja/-/lang-jinja-6.0.1.tgz", - "integrity": "sha512-P5kyHLObzjtbGj16h+hyvZTxJhSjBEeSx4wMjbnAf3b0uwTy2+F0zGjMZL4PQOm/mh2eGZ5xUDVZXgwP783Nsw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/lang-html": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.2.0", - "@lezer/lr": "^1.4.0" - } - }, - "node_modules/@codemirror/lang-json": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz", - "integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/json": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-less": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz", - "integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==", - "license": "MIT", - "dependencies": { - "@codemirror/lang-css": "^6.2.0", - "@codemirror/language": "^6.0.0", - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-liquid": { - "version": "6.3.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.3.tgz", - "integrity": "sha512-U9EnyzWuAx1TX3MZSKDXydKH734n1Aogk/ijKdW+jdIwmbf9zSPQYOIKggC/b2PTRjQb7ddvMktmxnfTLuv/SQ==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/lang-html": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.3.1" - } - }, - "node_modules/@codemirror/lang-markdown": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.2.tgz", - "integrity": "sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.7.1", - "@codemirror/lang-html": "^6.0.0", - "@codemirror/language": "^6.3.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.2.1", - "@lezer/markdown": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-php": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz", - "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==", - "license": "MIT", - "dependencies": { - "@codemirror/lang-html": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/php": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-python": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz", - "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.3.2", - "@codemirror/language": "^6.8.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.2.1", - "@lezer/python": "^1.1.4" - } - }, - "node_modules/@codemirror/lang-rust": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz", - "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/rust": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-sass": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", - "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", - "license": "MIT", - "dependencies": { - "@codemirror/lang-css": "^6.2.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.0.2", - "@lezer/sass": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-sql": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz", - "integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-vue": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", - "integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==", - "license": "MIT", - "dependencies": { - "@codemirror/lang-html": "^6.0.0", - "@codemirror/lang-javascript": "^6.1.2", - "@codemirror/language": "^6.0.0", - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.3.1" - } - }, - "node_modules/@codemirror/lang-wast": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz", - "integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0", - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-xml": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", - "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.4.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", - "@lezer/common": "^1.0.0", - "@lezer/xml": "^1.0.0" - } - }, - "node_modules/@codemirror/lang-yaml": { - "version": "6.1.3", - "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.3.tgz", - "integrity": "sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.2.0", - "@lezer/lr": "^1.0.0", - "@lezer/yaml": "^1.0.0" - } - }, - "node_modules/@codemirror/language": { - "version": "6.12.4", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz", - "integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.5.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0", - "style-mod": "^4.0.0" - } - }, - "node_modules/@codemirror/language-data": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.2.tgz", - "integrity": "sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==", - "license": "MIT", - "dependencies": { - "@codemirror/lang-angular": "^0.1.0", - "@codemirror/lang-cpp": "^6.0.0", - "@codemirror/lang-css": "^6.0.0", - "@codemirror/lang-go": "^6.0.0", - "@codemirror/lang-html": "^6.0.0", - "@codemirror/lang-java": "^6.0.0", - "@codemirror/lang-javascript": "^6.0.0", - "@codemirror/lang-jinja": "^6.0.0", - "@codemirror/lang-json": "^6.0.0", - "@codemirror/lang-less": "^6.0.0", - "@codemirror/lang-liquid": "^6.0.0", - "@codemirror/lang-markdown": "^6.0.0", - "@codemirror/lang-php": "^6.0.0", - "@codemirror/lang-python": "^6.0.0", - "@codemirror/lang-rust": "^6.0.0", - "@codemirror/lang-sass": "^6.0.0", - "@codemirror/lang-sql": "^6.0.0", - "@codemirror/lang-vue": "^0.1.1", - "@codemirror/lang-wast": "^6.0.0", - "@codemirror/lang-xml": "^6.0.0", - "@codemirror/lang-yaml": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/legacy-modes": "^6.4.0" - } - }, - "node_modules/@codemirror/legacy-modes": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.4.tgz", - "integrity": "sha512-/cZr6qZyl08iYNLGsJ862CXXNI51LryRFRE40ejgoIjXZz0C1rGkD3/Ek5jM/8w1ceRjqtt4qx/KLMh4zBTgew==", - "license": "MIT", - "dependencies": { - "@codemirror/language": "^6.0.0" - } - }, - "node_modules/@codemirror/lint": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz", - "integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.42.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/search": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.2.tgz", - "integrity": "sha512-gUYkYhT2+n/+VGZ+8EzE5WFkYZUZYm1VOKDudIsNqh42uRVQJ0a6Yss9sdKT3MeOYfuL1N6AZA57oza0Oyr0LA==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.37.0", - "crelt": "^1.0.5" - } - }, - "node_modules/@codemirror/state": { - "version": "6.7.6", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.6.tgz", - "integrity": "sha512-kAz+AncRtKuIknedxT1bq4XwXv4UowhbkHU1myPrtVb/jZtImWuV5BXzv5vK6i3kYACsdiZiQKFQQ5Mq7elW8w==", - "license": "MIT", - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.43.13", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.13.tgz", - "integrity": "sha512-sihaFrUzAsYBQsL9J2t69y8nfMQGwcYmggAZsk+kjPbjYZMyuf2hU8tUNTZ+P+isb6XRr8JE22TZlJxBoVdH1A==", - "license": "MIT", - "dependencies": { - "@codemirror/state": "^6.7.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", - "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", - "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", - "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", - "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", - "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", - "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", - "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", - "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", - "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", - "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", - "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", - "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", - "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", - "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", - "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", - "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", - "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", - "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", - "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", - "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", - "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", - "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", - "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", - "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", - "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", - "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@floating-ui/core": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", - "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", - "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.8.0", - "@floating-ui/utils": "^0.2.12" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", - "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", - "license": "MIT" - }, - "node_modules/@iconify/types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", - "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "license": "MIT" - }, - "node_modules/@iconify/utils": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.7.tgz", - "integrity": "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w==", - "license": "MIT", - "dependencies": { - "@antfu/install-pkg": "^2.0.1", - "@iconify/types": "^2.0.0", - "import-meta-resolve": "^4.2.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", - "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", - "license": "MIT" - }, - "node_modules/@lezer/common": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.3.tgz", - "integrity": "sha512-H0iErY4e43LpXbYDyBci5W4v/RwTgGV3YzYOlJPiGZ8RY8w51kVE+xLn1tg3Z6UmSfyETvtsUK3r7YsgDQEI7Q==", - "license": "MIT" - }, - "node_modules/@lezer/cpp": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.6.tgz", - "integrity": "sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/css": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.8.tgz", - "integrity": "sha512-EJn1zcL9qoDptief6ipWKZKLiOpXkxSe0+t8CH9oiMVcZlq7NBWrjCqnc/41EIjeo/ITj1gFFiATdTkaJDL+Og==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.3.0" - } - }, - "node_modules/@lezer/go": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz", - "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.3.0" - } - }, - "node_modules/@lezer/highlight": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.5.tgz", - "integrity": "sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.3.0" - } - }, - "node_modules/@lezer/html": { - "version": "1.3.13", - "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", - "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/java": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.4.tgz", - "integrity": "sha512-gQylJ2xHV2xXsVqtnAcJrcROvLZJaQiC0tR//+q0k961BxqvyYttusPkJ8d9T1fPDJKHJW2etUH8XbIn9jhVUg==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/javascript": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.6.tgz", - "integrity": "sha512-sEvOqpl4vJQca0NugSQK02vQPbc5KrvoojnYGoqqFgFO0MtcYhwGiHKVEB+LuXYJnfyXo4QPA8ztbrEa4Fw5NA==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.1.3", - "@lezer/lr": "^1.3.0" - } - }, - "node_modules/@lezer/json": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz", - "integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/lr": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", - "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.0.0" - } - }, - "node_modules/@lezer/markdown": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.7.2.tgz", - "integrity": "sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.5.0", - "@lezer/highlight": "^1.0.0" - } - }, - "node_modules/@lezer/php": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.6.tgz", - "integrity": "sha512-QJ2xNXPmsm/Z3IpThq8fnJrEIVpxhsIoj6GZBW0JYEd/l9zxpJZW216X4fzqQY4vgpdGIumypvI4uQjd4tnYtw==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.1.0" - } - }, - "node_modules/@lezer/python": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.19.tgz", - "integrity": "sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/rust": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.3.tgz", - "integrity": "sha512-XxErOjZzQ7yJt1agUT4fu9qQvESZ3acgoxpPaPTPOiUx+duCjaVAtZGFIgphkHxlN05djdVAIOy/wItShMEjqQ==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/sass": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.1.0.tgz", - "integrity": "sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/xml": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.6.tgz", - "integrity": "sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.0.0" - } - }, - "node_modules/@lezer/yaml": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz", - "integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==", - "license": "MIT", - "dependencies": { - "@lezer/common": "^1.2.0", - "@lezer/highlight": "^1.0.0", - "@lezer/lr": "^1.4.0" - } - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.4.tgz", - "integrity": "sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==", - "license": "MIT" - }, - "node_modules/@mermaid-js/parser": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-2.0.1.tgz", - "integrity": "sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==", - "license": "MIT", - "dependencies": { - "@chevrotain/types": "~13.2.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@milkdown/components": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/components/-/components-7.22.2.tgz", - "integrity": "sha512-qlXh+KvMeELXdy2h0ndIVKffbuAr2mkuVYbEDuyTBffRC00SfRKOVf+ObZBKpvAoQKo18iDS4tCHFMmkzr29EA==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.5.1", - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/exception": "7.22.2", - "@milkdown/plugin-diff": "7.22.2", - "@milkdown/plugin-tooltip": "7.22.2", - "@milkdown/preset-commonmark": "7.22.2", - "@milkdown/preset-gfm": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/transformer": "7.22.2", - "@milkdown/utils": "7.22.2", - "@types/lodash-es": "^4.17.12", - "clsx": "^2.0.0", - "dompurify": "^3.2.5", - "lodash-es": "^4.17.21", - "nanoid": "^6.0.0", - "unist-util-visit": "^5.0.0", - "vue": "^3.5.20" - }, - "peerDependencies": { - "@codemirror/language": "^6", - "@codemirror/state": "^6", - "@codemirror/view": "^6" - } - }, - "node_modules/@milkdown/core": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/core/-/core-7.22.2.tgz", - "integrity": "sha512-grJ0enYKFwfV9Ur2GLiDtzOi7kzSqhCueBudqgfmmlx3HVGXwg78/CXsYDhn8cNFqpPSA0TMhdxIpBFKxXOMKA==", - "license": "MIT", - "dependencies": { - "@milkdown/ctx": "7.22.2", - "@milkdown/exception": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/transformer": "7.22.2", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.3" - } - }, - "node_modules/@milkdown/ctx": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/ctx/-/ctx-7.22.2.tgz", - "integrity": "sha512-w2kkMfBIxTpXtnjqltxIiMhflfZzyPN+q31ZwLtwUZjgTpiPCHS/ROUOH1U891mKNUKcD10lOFfOQSS959yIqg==", - "license": "MIT", - "dependencies": { - "@milkdown/exception": "7.22.2" - } - }, - "node_modules/@milkdown/exception": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/exception/-/exception-7.22.2.tgz", - "integrity": "sha512-uS4Gx6xE/iTSKslverVWIWQ8RN5B+NXxP8BLSATnlwDKiYlgBYZYLbvSx632VM9j5Qsd5YuKRGt9k3dXe54EWw==", - "license": "MIT" - }, - "node_modules/@milkdown/kit": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/kit/-/kit-7.22.2.tgz", - "integrity": "sha512-omjaInj/xiuVnSchr9vm7Kv+ORgFtmdmk0xUbeCEj7ptpCl0aS71vTFjuuc/FKSpTxcJ2ptHa52mrR6MvK8Bhw==", - "license": "MIT", - "dependencies": { - "@milkdown/components": "7.22.2", - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/exception": "7.22.2", - "@milkdown/plugin-block": "7.22.2", - "@milkdown/plugin-clipboard": "7.22.2", - "@milkdown/plugin-cursor": "7.22.2", - "@milkdown/plugin-diff": "7.22.2", - "@milkdown/plugin-history": "7.22.2", - "@milkdown/plugin-indent": "7.22.2", - "@milkdown/plugin-listener": "7.22.2", - "@milkdown/plugin-slash": "7.22.2", - "@milkdown/plugin-streaming": "7.22.2", - "@milkdown/plugin-tooltip": "7.22.2", - "@milkdown/plugin-trailing": "7.22.2", - "@milkdown/plugin-upload": "7.22.2", - "@milkdown/preset-commonmark": "7.22.2", - "@milkdown/preset-gfm": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/transformer": "7.22.2", - "@milkdown/utils": "7.22.2" - } - }, - "node_modules/@milkdown/plugin-block": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-block/-/plugin-block-7.22.2.tgz", - "integrity": "sha512-9rwkvGsM+zP+rlxIVjZVQX86nUe7/vBc38HwcYTqEEaXAuVcw1BJ8dcpvvlbsywlZqKlazyFJ32uf+X2+tFfnw==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.5.1", - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/utils": "7.22.2", - "@types/lodash-es": "^4.17.12", - "lodash-es": "^4.17.21" - } - }, - "node_modules/@milkdown/plugin-clipboard": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-clipboard/-/plugin-clipboard-7.22.2.tgz", - "integrity": "sha512-6Zr5r7hURwZKvKi7ox+7lrET7ktRgxcBSvrB/XwmfxN69F8M+vILJFlW+ZZ2ezm3x81pu3kpAsvxZJN7d7ol2g==", - "license": "MIT", - "dependencies": { - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/utils": "7.22.2" - } - }, - "node_modules/@milkdown/plugin-cursor": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-cursor/-/plugin-cursor-7.22.2.tgz", - "integrity": "sha512-lTjYXmhYFxlB6JLa+UMI3dvKQJog+I58Gtpl1qn26ynG+jC24bGLNNzj9H7/EaN1yKGevnvl/Hq6EhCjnnUXtg==", - "license": "MIT", - "dependencies": { - "@milkdown/ctx": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/utils": "7.22.2", - "prosemirror-drop-indicator": "^0.1.0" - } - }, - "node_modules/@milkdown/plugin-diff": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-diff/-/plugin-diff-7.22.2.tgz", - "integrity": "sha512-lIAPV1Xh9M3kwXyXZ57CYQBMPQanxSgz3r0n3QKxpCR0LEZnCBZMF61CMsD2c+gcGNG9drnmfFQJLXAhEq2hjw==", - "license": "MIT", - "dependencies": { - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/transformer": "7.22.2", - "@milkdown/utils": "7.22.2" - } - }, - "node_modules/@milkdown/plugin-history": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-history/-/plugin-history-7.22.2.tgz", - "integrity": "sha512-30jM0TWWbG2gwd9QtbrXHfNi737sr6Q5O26eZqIPsmDVFAfqm+ZEd5cpJWngcQ3C5GflvWxyPC+xPEqY97B9AA==", - "license": "MIT", - "dependencies": { - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/utils": "7.22.2" - } - }, - "node_modules/@milkdown/plugin-indent": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-indent/-/plugin-indent-7.22.2.tgz", - "integrity": "sha512-U5W1ZAFV0VmP4YBn192fD/Uk1qnf4q+boWEgtHcXL84hb0EwbXqXHsTTkW8SjKHGQchnT8cQyDRomfbVw/4Q2A==", - "license": "MIT", - "dependencies": { - "@milkdown/ctx": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/utils": "7.22.2" - } - }, - "node_modules/@milkdown/plugin-listener": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-listener/-/plugin-listener-7.22.2.tgz", - "integrity": "sha512-KXsfm2f1tmHtBZHTIvaVsu/Y6TVdgmxJBacooCE6R8tcB9Segj24ICNy0/0+t3i85GUTr1rzx+2rGmpj5WhNZA==", - "license": "MIT", - "dependencies": { - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/prose": "7.22.2", - "@types/lodash-es": "^4.17.12", - "lodash-es": "^4.17.21" - } - }, - "node_modules/@milkdown/plugin-slash": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-slash/-/plugin-slash-7.22.2.tgz", - "integrity": "sha512-4hL/dj6fCs3MOFKGHpXIXAAvu/Hm9fxG2tv2XOZ6Z3ps63z9q8xMSTyuFx1cV9U0z+3J64LnKaQ1lgwzviwD9A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.5.1", - "@milkdown/ctx": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/utils": "7.22.2", - "@types/lodash-es": "^4.17.12", - "lodash-es": "^4.17.21" - } - }, - "node_modules/@milkdown/plugin-streaming": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-streaming/-/plugin-streaming-7.22.2.tgz", - "integrity": "sha512-fxB2veftnm490aE1TPCJuTftiZSfTR7CZ2JEVmgMQHGy4HApIqJ9MqOyMevR23GSX/Qef9rWsZJNQRgI+9VazA==", - "license": "MIT", - "dependencies": { - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/plugin-diff": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/utils": "7.22.2" - } - }, - "node_modules/@milkdown/plugin-tooltip": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-tooltip/-/plugin-tooltip-7.22.2.tgz", - "integrity": "sha512-bMT5Kj+sNliKHzaH9o/7JdUAnmoTkTjanG7muRBw6vo9oQVoeSpWuRrZaE6qC25mQoUzCeqrGukMRtVhnwSyhg==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.5.1", - "@milkdown/ctx": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/utils": "7.22.2", - "@types/lodash-es": "^4.17.12", - "lodash-es": "^4.17.21" - } - }, - "node_modules/@milkdown/plugin-trailing": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-trailing/-/plugin-trailing-7.22.2.tgz", - "integrity": "sha512-VH5oVcT4Nvb7MbmJpQdqgTZmaBDtRsUmrbIEewMsyDMCF6WWiV9Ykxxg4S+D/BKZdyQkooWUTd97pP/XGN3eEw==", - "license": "MIT", - "dependencies": { - "@milkdown/ctx": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/utils": "7.22.2" - } - }, - "node_modules/@milkdown/plugin-upload": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/plugin-upload/-/plugin-upload-7.22.2.tgz", - "integrity": "sha512-fkBiyvQt25IX9DxIeiT5kScCAv2uo/94wwXUEOtEFs+F2RtvbiXHRXx6xzrymepT3L6TYGchxTyKSEWOOLe/qg==", - "license": "MIT", - "dependencies": { - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/exception": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/utils": "7.22.2" - } - }, - "node_modules/@milkdown/preset-commonmark": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/preset-commonmark/-/preset-commonmark-7.22.2.tgz", - "integrity": "sha512-+dy6qG7kcHsuh63saIhP4fg2wdqYMrJWvXZH7CxGDFMg1GMVbBeCPtOHlhO+2CssCKgbwctelbHRvdEHZYRmIw==", - "license": "MIT", - "dependencies": { - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/exception": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/transformer": "7.22.2", - "@milkdown/utils": "7.22.2", - "remark-inline-links": "^7.0.0", - "unist-util-visit": "^5.0.0", - "unist-util-visit-parents": "^6.0.1" - } - }, - "node_modules/@milkdown/preset-gfm": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/preset-gfm/-/preset-gfm-7.22.2.tgz", - "integrity": "sha512-G9NJ0seKnuM6k6SM9rXzXmT8zfemFcVJn+yekrqPkQ2VniwX+cJDHG3s7+cWedPVRFOMzKvpHKMkaPSO34IZtg==", - "license": "MIT", - "dependencies": { - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/exception": "7.22.2", - "@milkdown/preset-commonmark": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/transformer": "7.22.2", - "@milkdown/utils": "7.22.2", - "prosemirror-safari-ime-span": "^1.0.1", - "remark-gfm": "^4.0.1" - } - }, - "node_modules/@milkdown/prose": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/prose/-/prose-7.22.2.tgz", - "integrity": "sha512-sh23B62WiA9+rRwDV1OhC34bpOPRGKbDWDdYxkJkMn+YGUj5HXhNb6hJmqDSqTdPcB2WQTF+WNVbrtde/jkVPg==", - "license": "MIT", - "dependencies": { - "@milkdown/exception": "7.22.2", - "prosemirror-changeset": "^2.3.1", - "prosemirror-commands": "^1.7.1", - "prosemirror-dropcursor": "^1.8.2", - "prosemirror-gapcursor": "^1.4.0", - "prosemirror-history": "^1.5.0", - "prosemirror-inputrules": "^1.5.1", - "prosemirror-keymap": "^1.2.3", - "prosemirror-model": "^1.25.4", - "prosemirror-schema-list": "^1.5.1", - "prosemirror-state": "^1.4.4", - "prosemirror-tables": "^1.8.1", - "prosemirror-transform": "^1.10.5", - "prosemirror-view": "^1.41.3" - } - }, - "node_modules/@milkdown/transformer": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/transformer/-/transformer-7.22.2.tgz", - "integrity": "sha512-RP2NkKFKHwzUidPvi7V9le7o68KwCuMdFCxkvwX5eaxaxHEmH6v4p5GKoCnjU3+SWUc4XOPKXA15/b6Byg7TuA==", - "license": "MIT", - "dependencies": { - "@milkdown/exception": "7.22.2", - "@milkdown/prose": "7.22.2", - "remark": "^15.0.1", - "unified": "^11.0.3" - } - }, - "node_modules/@milkdown/utils": { - "version": "7.22.2", - "resolved": "https://registry.npmjs.org/@milkdown/utils/-/utils-7.22.2.tgz", - "integrity": "sha512-lRKIaX+kFZycqHxc2xtIwgPdp5BWlSKJKCljZ8Uiih98uaNWQvFlpfzc5Bp4+XJS6BEq2K3ZVILUKa5eWJofWw==", - "license": "MIT", - "dependencies": { - "@milkdown/core": "7.22.2", - "@milkdown/ctx": "7.22.2", - "@milkdown/exception": "7.22.2", - "@milkdown/prose": "7.22.2", - "@milkdown/transformer": "7.22.2", - "nanoid": "^6.0.0" - } - }, - "node_modules/@ocavue/utils": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@ocavue/utils/-/utils-1.8.0.tgz", - "integrity": "sha512-BD1ro+6Kllkmr/at+o32kvnLrVooUbk9gw3JAPaoAZXx+A5GhZ6Laa+UA38ifhG3vaamx+jcgEwX22BmTSMUVA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ocavue" - } - }, - "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "license": "MIT" - }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", - "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", - "license": "MIT", - "dependencies": { - "@types/d3-dsv": "*" - } - }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", - "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", - "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", - "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", - "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", - "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/katex": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", - "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", - "license": "MIT" - }, - "node_modules/@types/lodash": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", - "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", - "license": "MIT" - }, - "node_modules/@types/lodash-es": { - "version": "4.17.12", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", - "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", - "license": "MIT", - "dependencies": { - "@types/lodash": "*" - } - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "license": "MIT", - "optional": true - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", - "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", - "license": "ISC" - }, - "node_modules/@upsetjs/venn.js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", - "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", - "license": "MIT", - "optionalDependencies": { - "d3-selection": "^3.0.0", - "d3-transition": "^3.0.1" - } - }, - "node_modules/@vue/compiler-core": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.43.tgz", - "integrity": "sha512-zdiLhnbe1QQqgDT8xZMpNmyqZ3qlI+/Q/FHQco57Kwl/b05HhCzN6eVGN9QU9rbga4CrS0H5SYY8VGHZCt/1Hg==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@vue/shared": "3.5.43", - "entities": "^7.0.1", - "estree-walker": "^2.0.2", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-dom": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.43.tgz", - "integrity": "sha512-PEZoAk3NQmsn/ejMzSOCyTYqwGqczrWm70PuhBKjjv1+TCoQAaO/zOqNwjV+honlNstT5ILxtc+8r8UUfj+iEQ==", - "license": "MIT", - "dependencies": { - "@vue/compiler-core": "3.5.43", - "@vue/shared": "3.5.43" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.43.tgz", - "integrity": "sha512-FCbrG3XNCRl+js3huuKx4IVHBLTvMkJhVepjbxSPu1gn4yWLaYtGNQdjJGZaMytXB6qb76qQDDmDSLy/vkmleQ==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@vue/compiler-core": "3.5.43", - "@vue/compiler-dom": "3.5.43", - "@vue/compiler-ssr": "3.5.43", - "@vue/shared": "3.5.43", - "estree-walker": "^2.0.2", - "magic-string": "^0.30.21", - "postcss": "^8.5.28", - "source-map-js": "^1.2.1" - } - }, - "node_modules/@vue/compiler-ssr": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.43.tgz", - "integrity": "sha512-GF62orf7KiJX9RqrHNGrYBudsQGD0OhJ5nDs90O8UiDuS40+YMYomiXu6w6EuvtXuRDc3MSNis3EaaSKAVSWpg==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.43", - "@vue/shared": "3.5.43" - } - }, - "node_modules/@vue/reactivity": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.43.tgz", - "integrity": "sha512-G/c9GyOZNI2jVaaS6OX1EF1SSFSv7H0ERqNTl4+DTFMlZmB5eVAB53aLQNam/7NL2NPtaDD7RdVrzf8uJzMuOA==", - "license": "MIT", - "dependencies": { - "@vue/shared": "3.5.43" - } - }, - "node_modules/@vue/runtime-core": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.43.tgz", - "integrity": "sha512-hU6U6VnVhBGQDpvlnnDlIB8ZGJBiOcgk2lh/0InltHiz3D8oSkluvuvY+do1G2H3+udeKFsmaBlgVYP7gXQzEw==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.43", - "@vue/shared": "3.5.43" - } - }, - "node_modules/@vue/runtime-dom": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.43.tgz", - "integrity": "sha512-Bb2Jc0YjjJdMt1SJmb9b2L/IWd3I8lIT9x9eS/xvvP9CiVgna0ffua74xKRmt4/uSJ+0r4iN8ex1jrqkhQGWQw==", - "license": "MIT", - "dependencies": { - "@vue/reactivity": "3.5.43", - "@vue/runtime-core": "3.5.43", - "@vue/shared": "3.5.43", - "csstype": "^3.2.3" - } - }, - "node_modules/@vue/server-renderer": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.43.tgz", - "integrity": "sha512-l2Ygjv9NehV94PSBxNWsAHC0j/eIIKbn92mBuWXAPGLnn6HfJ6MH5ubsd+Nk0YoZ5FRuxWI1P2VSoh+dbPQhCQ==", - "license": "MIT", - "dependencies": { - "@vue/compiler-ssr": "3.5.43", - "@vue/runtime-dom": "3.5.43", - "@vue/shared": "3.5.43" - } - }, - "node_modules/@vue/shared": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.43.tgz", - "integrity": "sha512-uksS7YGMR5NZyr4JNq0Rp+QyLns0ueaz20KwzIPW9R0LH1Vnt4E+XUM29PNseEbf1www2gOuhuDi5AKOIXag9Q==", - "license": "MIT" - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chevrotain": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz", - "integrity": "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA==", - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "13.2.0", - "@chevrotain/gast": "13.2.0", - "@chevrotain/regexp-to-ast": "13.2.0", - "@chevrotain/types": "13.2.0", - "@chevrotain/utils": "13.2.0" - }, - "engines": { - "node": ">=22.0.0" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/codemirror": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", - "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", - "license": "MIT", - "dependencies": { - "@codemirror/autocomplete": "^6.0.0", - "@codemirror/commands": "^6.0.0", - "@codemirror/language": "^6.0.0", - "@codemirror/lint": "^6.0.0", - "@codemirror/search": "^6.0.0", - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", - "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", - "license": "MIT", - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/cose-base": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", - "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", - "license": "MIT", - "dependencies": { - "layout-base": "^1.0.0" - } - }, - "node_modules/crelt": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz", - "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==", - "license": "MIT" - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" - }, - "node_modules/cytoscape": { - "version": "3.34.3", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", - "integrity": "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/cytoscape-cose-bilkent": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", - "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", - "license": "MIT", - "dependencies": { - "cose-base": "^1.0.0" - }, - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, - "node_modules/cytoscape-fcose": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", - "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", - "license": "MIT", - "dependencies": { - "cose-base": "^2.2.0" - }, - "peerDependencies": { - "cytoscape": "^3.2.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/cose-base": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", - "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", - "license": "MIT", - "dependencies": { - "layout-base": "^2.0.0" - } - }, - "node_modules/cytoscape-fcose/node_modules/layout-base": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", - "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", - "license": "MIT" - }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-sankey": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", - "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-array": "1 - 2", - "d3-shape": "^1.2.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-array": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", - "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", - "license": "BSD-3-Clause", - "dependencies": { - "internmap": "^1.0.0" - } - }, - "node_modules/d3-sankey/node_modules/d3-path": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", - "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", - "license": "BSD-3-Clause" - }, - "node_modules/d3-sankey/node_modules/d3-shape": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", - "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", - "license": "BSD-3-Clause", - "dependencies": { - "d3-path": "1" - } - }, - "node_modules/d3-sankey/node_modules/internmap": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", - "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", - "license": "ISC" - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dagre-d3-es": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", - "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", - "license": "MIT", - "dependencies": { - "d3": "^7.9.0", - "lodash-es": "^4.17.21" - } - }, - "node_modules/dayjs": { - "version": "1.11.23", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", - "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", - "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/delaunator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", - "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dompurify": { - "version": "3.4.16", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", - "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", - "license": "(MPL-2.0 OR Apache-2.0)", - "optionalDependencies": { - "@types/trusted-types": "^2.0.7" - } - }, - "node_modules/elkjs": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", - "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", - "license": "EPL-2.0" - }, - "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/es-toolkit": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", - "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks", - "tests/types", - "tests/browser-compat" - ] - }, - "node_modules/esbuild": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", - "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.2", - "@esbuild/android-arm": "0.28.2", - "@esbuild/android-arm64": "0.28.2", - "@esbuild/android-x64": "0.28.2", - "@esbuild/darwin-arm64": "0.28.2", - "@esbuild/darwin-x64": "0.28.2", - "@esbuild/freebsd-arm64": "0.28.2", - "@esbuild/freebsd-x64": "0.28.2", - "@esbuild/linux-arm": "0.28.2", - "@esbuild/linux-arm64": "0.28.2", - "@esbuild/linux-ia32": "0.28.2", - "@esbuild/linux-loong64": "0.28.2", - "@esbuild/linux-mips64el": "0.28.2", - "@esbuild/linux-ppc64": "0.28.2", - "@esbuild/linux-riscv64": "0.28.2", - "@esbuild/linux-s390x": "0.28.2", - "@esbuild/linux-x64": "0.28.2", - "@esbuild/netbsd-arm64": "0.28.2", - "@esbuild/netbsd-x64": "0.28.2", - "@esbuild/openbsd-arm64": "0.28.2", - "@esbuild/openbsd-x64": "0.28.2", - "@esbuild/openharmony-arm64": "0.28.2", - "@esbuild/sunos-x64": "0.28.2", - "@esbuild/win32-arm64": "0.28.2", - "@esbuild/win32-ia32": "0.28.2", - "@esbuild/win32-x64": "0.28.2" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/hachure-fill": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", - "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", - "license": "MIT" - }, - "node_modules/hast-util-from-dom": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", - "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", - "license": "ISC", - "dependencies": { - "@types/hast": "^3.0.0", - "hastscript": "^9.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", - "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.1.0", - "hast-util-from-parse5": "^8.0.0", - "parse5": "^7.0.0", - "vfile": "^6.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-html-isomorphic": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", - "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-from-dom": "^5.0.0", - "hast-util-from-html": "^2.0.0", - "unist-util-remove-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-is-element": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", - "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-html": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", - "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-text": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", - "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "hast-util-is-element": "^3.0.0", - "unist-util-find-after": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/import-meta-resolve": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", - "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/katex": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.19.0.tgz", - "integrity": "sha512-v6Tznz3zJ7u3niRCoDTsumM2+HA2XXcCu+WAacCeHD2z3p9A9Ks987o5FzfTGBN0e8A0vjEgIDvLbICrXpdw/Q==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^15.0.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/khroma": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", - "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" - }, - "node_modules/layout-base": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", - "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", - "license": "MIT" - }, - "node_modules/lodash-es": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", - "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/marked": { - "version": "16.4.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", - "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", - "license": "MIT", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/mdast-util-definitions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", - "integrity": "sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.3.tgz", - "integrity": "sha512-xPgpDNl0/OXHsI7TlaIs22lWnH3KlpvZdXG1ET/m/YT2Hhdkx8lJV4kLphE6l9simyM4KPDFBwas08BgEob6Jw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz", - "integrity": "sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.1.tgz", - "integrity": "sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-math": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", - "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "longest-streak": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.1.0", - "unist-util-remove-position": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", - "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.0.tgz", - "integrity": "sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mermaid": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.1.0.tgz", - "integrity": "sha512-wlVCp+8eTupfCeeFvoZNNiTuHrvag0P2jz/ILgb/f/6jkVokUefOcujefi8qUe/j2asHiSePncVsz/xzzA80LQ==", - "license": "MIT", - "dependencies": { - "@braintree/sanitize-url": "^7.1.2", - "@iconify/utils": "^3.0.2", - "@mermaid-js/parser": "^2.0.1", - "@types/d3": "^7.4.3", - "@upsetjs/venn.js": "^2.0.0", - "chevrotain": "~13.2.0", - "cytoscape": "^3.34.0", - "cytoscape-cose-bilkent": "^4.1.0", - "cytoscape-fcose": "^2.2.0", - "d3": "^7.9.0", - "d3-sankey": "^0.12.3", - "dagre-d3-es": "7.0.14", - "dayjs": "^1.11.21", - "dompurify": "^3.4.12", - "elkjs": "^0.9.3", - "es-toolkit": "^1.45.1", - "katex": "^0.16.47", - "khroma": "^2.1.0", - "marked": "^16.3.0", - "roughjs": "^4.6.6", - "stylis": "^4.3.6", - "ts-dedent": "^2.2.0", - "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/mermaid/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/mermaid/node_modules/katex": { - "version": "0.16.47", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/micromark": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", - "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-edit-map": "^1.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", - "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.1.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-edit-map": "^1.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.3" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", - "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-math": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", - "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", - "license": "MIT", - "dependencies": { - "@types/katex": "^0.16.0", - "devlop": "^1.0.0", - "katex": "^0.16.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-math/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/micromark-extension-math/node_modules/katex": { - "version": "0.16.47", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", - "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-edit-map": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", - "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", - "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-6.0.2.tgz", - "integrity": "sha512-hIvVOb9Qr1p/9CNNLOe0OlWvgx/ziTsVwKN/GDSsuArN2K0wDiafP3s0SWmsm7o2BdnyS8Z9Z64NFBUbn6ziBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.js" - }, - "engines": { - "node": "^22 || ^24 || >=26" - } - }, - "node_modules/orderedmap": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", - "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", - "license": "MIT" - }, - "node_modules/package-manager-detector": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", - "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", - "license": "MIT" - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/path-data-parser": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", - "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/points-on-curve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", - "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", - "license": "MIT" - }, - "node_modules/points-on-path": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", - "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", - "license": "MIT", - "dependencies": { - "path-data-parser": "0.1.0", - "points-on-curve": "0.2.0" - } - }, - "node_modules/postcss": { - "version": "8.5.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", - "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.19", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss/node_modules/nanoid": { - "version": "3.3.20", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", - "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/property-information": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", - "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/prosemirror-changeset": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.4.tgz", - "integrity": "sha512-Gyuz9jYdrAeA9I7AholyOes9KUmv/lEwsC5O9s5xF5VB3EgTPsXMGUxz4+H1B+wKVPdZbvRcg1Cp3x8qX+8ofA==", - "license": "MIT", - "dependencies": { - "prosemirror-transform": "^1.0.0" - } - }, - "node_modules/prosemirror-commands": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz", - "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.0.0", - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.10.2" - } - }, - "node_modules/prosemirror-drop-indicator": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/prosemirror-drop-indicator/-/prosemirror-drop-indicator-0.1.4.tgz", - "integrity": "sha512-YaRB1pZmU5GCorPVWbc9dbhbwqr4iMBO/AjPu4BTKHCUzxEDUXje2dUyoxOHib/z4uyPUZTJz64h7mHDJZeSzA==", - "license": "MIT", - "dependencies": { - "@ocavue/utils": "^1.6.0", - "prosemirror-model": "^1.25.7", - "prosemirror-state": "^1.4.4", - "prosemirror-view": "^1.41.8" - }, - "funding": { - "url": "https://github.com/sponsors/ocavue" - } - }, - "node_modules/prosemirror-dropcursor": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.4.tgz", - "integrity": "sha512-VmKxhcA6a+h5oPSYmfF1ZNeQqPbyYaIxqZsKqeYCyTlPtrjUgNaHL3P8vVaO19oqAV0zuaM/axkowZHrB1P3Tg==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.1.0", - "prosemirror-view": "^1.1.0" - } - }, - "node_modules/prosemirror-gapcursor": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", - "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", - "license": "MIT", - "dependencies": { - "prosemirror-keymap": "^1.0.0", - "prosemirror-model": "^1.0.0", - "prosemirror-state": "^1.0.0", - "prosemirror-view": "^1.0.0" - } - }, - "node_modules/prosemirror-history": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.1.tgz", - "integrity": "sha512-onlwnqKvgtFg6srZOmDOAhlAy1FIHQpOExORybX0S+DvJepE5tPALrB9iGet/2sqqqrelSnB10OoiOJqAvu2DA==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.2.2", - "prosemirror-transform": "^1.0.0", - "prosemirror-view": "^1.31.0", - "rope-sequence": "^1.3.0" - } - }, - "node_modules/prosemirror-inputrules": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", - "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.0.0" - } - }, - "node_modules/prosemirror-keymap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", - "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.0.0", - "w3c-keyname": "^2.2.0" - } - }, - "node_modules/prosemirror-model": { - "version": "1.25.12", - "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.12.tgz", - "integrity": "sha512-Ue2gTmXMa7EhpLNhC7J+h4+ykD8ha12K6rrZFFKKJHBForfIStw5gJ6Zrf1mqqAa9NmpmEB4wp9bKmA4eUVjWg==", - "license": "MIT", - "dependencies": { - "orderedmap": "^2.0.0" - } - }, - "node_modules/prosemirror-safari-ime-span": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/prosemirror-safari-ime-span/-/prosemirror-safari-ime-span-1.0.2.tgz", - "integrity": "sha512-QJqD8s1zE/CuK56kDsUhndh5hiHh/gFnAuPOA9ytva2s85/ZEt2tNWeALTJN48DtWghSKOmiBsvVn2OlnJ5H2w==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.4.3", - "prosemirror-view": "^1.33.8" - }, - "funding": { - "url": "https://github.com/sponsors/ocavue" - } - }, - "node_modules/prosemirror-schema-list": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", - "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.0.0", - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.7.3" - } - }, - "node_modules/prosemirror-search": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prosemirror-search/-/prosemirror-search-1.1.1.tgz", - "integrity": "sha512-1dbxbF1YYlP9mYJnFbfdOb9+RTdFHeZ/GwgEyHD4jg9szA8EVitMCnfXUBYLmFun500FMZBkRvcV0gp5BpLw4Q==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.21.0", - "prosemirror-state": "^1.4.3", - "prosemirror-view": "^1.33.6" - } - }, - "node_modules/prosemirror-state": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", - "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.0.0", - "prosemirror-transform": "^1.0.0", - "prosemirror-view": "^1.27.0" - } - }, - "node_modules/prosemirror-tables": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", - "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", - "license": "MIT", - "dependencies": { - "prosemirror-keymap": "^1.2.3", - "prosemirror-model": "^1.25.4", - "prosemirror-state": "^1.4.4", - "prosemirror-transform": "^1.10.5", - "prosemirror-view": "^1.41.4" - } - }, - "node_modules/prosemirror-transform": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.2.tgz", - "integrity": "sha512-PE/aY0HEY4zczvmqrilgkUK/WautF0chvMqkmM/iN5/aPHvwrCWaQZjS6CIZp+K84YrvPFqrL+YArrfJX8xX7g==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.21.0" - } - }, - "node_modules/prosemirror-view": { - "version": "1.42.6", - "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.6.tgz", - "integrity": "sha512-3g25f6hKV2bBxo3puH3EcYWmmVNSH0iWmNsMVT7sugDSpA025jgpOLarcDWr39SdzPjYMaiYtRdNtXialPSjEQ==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.25.12", - "prosemirror-state": "^1.0.0", - "prosemirror-transform": "^1.1.0" - } - }, - "node_modules/rehype-katex": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", - "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/katex": "^0.16.0", - "hast-util-from-html-isomorphic": "^2.0.0", - "hast-util-to-text": "^4.0.0", - "katex": "^0.16.0", - "unist-util-visit-parents": "^6.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/rehype-katex/node_modules/katex": { - "version": "0.16.47", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", - "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", - "funding": [ - "https://opencollective.com/katex", - "https://github.com/sponsors/katex" - ], - "license": "MIT", - "dependencies": { - "commander": "^8.3.0" - }, - "bin": { - "katex": "cli.js" - } - }, - "node_modules/rehype-stringify": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", - "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-to-html": "^9.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", - "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-inline-links": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/remark-inline-links/-/remark-inline-links-7.0.0.tgz", - "integrity": "sha512-4uj1pPM+F495ySZhTIB6ay2oSkTsKgmYaKk/q5HIdhX2fuyLEegpjWa0VdJRJ01sgOqAFo7MBKdDUejIYBMVMQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-definitions": "^6.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-math": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", - "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-math": "^3.0.0", - "micromark-extension-math": "^3.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/robust-predicates": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", - "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", - "license": "Unlicense" - }, - "node_modules/rope-sequence": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", - "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", - "license": "MIT" - }, - "node_modules/roughjs": { - "version": "4.6.6", - "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", - "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", - "license": "MIT", - "dependencies": { - "hachure-fill": "^0.5.2", - "path-data-parser": "^0.1.0", - "points-on-curve": "^0.2.0", - "points-on-path": "^0.2.1" - } - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/source-map-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", - "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/style-mod": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.4.tgz", - "integrity": "sha512-XXWIQt633/EpAFx8aZDOTjBzrCaGmhvEQlQo6MVPfa2OzO2cWo+4hV9h+6UkHYlXGfy+ODXKUdP7Pthmcu5ATw==", - "license": "MIT" - }, - "node_modules/stylis": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", - "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", - "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/ts-dedent": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", - "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", - "license": "MIT", - "engines": { - "node": ">=6.10" - } - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-find-after": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", - "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", - "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", - "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", - "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/uuid": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", - "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", - "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vue": { - "version": "3.5.43", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.43.tgz", - "integrity": "sha512-o5qZoksdnjIKvW1srZ3ab7pcDNYAerBjRe54D0LBLfRdCYFrSgBHVXokMas35czQc0//lmx4/tuY4ZNQ+Rf2Ng==", - "license": "MIT", - "dependencies": { - "@vue/compiler-dom": "3.5.43", - "@vue/compiler-sfc": "3.5.43", - "@vue/runtime-dom": "3.5.43", - "@vue/server-renderer": "3.5.43", - "@vue/shared": "3.5.43" - }, - "peerDependencies": { - "typescript": "*" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", - "license": "MIT" - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/apps/ely-mark/web/package.json b/apps/ely-mark/web/package.json deleted file mode 100644 index 7cb964c..0000000 --- a/apps/ely-mark/web/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "ely-mark-content", - "private": true, - "type": "module", - "dependencies": { - "@codemirror/commands": "6.11.1", - "@codemirror/lang-markdown": "6.5.2", - "@codemirror/language": "6.12.4", - "@codemirror/language-data": "6.5.2", - "@codemirror/search": "6.7.2", - "@codemirror/state": "6.7.6", - "@codemirror/view": "6.43.13", - "@lezer/highlight": "1.2.5", - "@milkdown/kit": "7.22.2", - "codemirror": "6.0.2", - "hast-util-from-dom": "5.0.1", - "hast-util-to-text": "4.0.2", - "katex": "0.19.0", - "mermaid": "12.1.0", - "prosemirror-search": "1.1.1", - "rehype-katex": "7.0.1", - "rehype-stringify": "10.0.1", - "remark-gfm": "4.0.1", - "remark-math": "6.0.0", - "remark-parse": "11.0.0", - "remark-rehype": "11.1.2", - "unified": "11.0.5", - "unist-util-visit": "5.1.0", - "vfile": "6.0.3" - }, - "devDependencies": { - "esbuild": "0.28.2" - } -} diff --git a/apps/ely-mark/web/src/bridge.js b/apps/ely-mark/web/src/bridge.js deleted file mode 100644 index 7570bf6..0000000 --- a/apps/ely-mark/web/src/bridge.js +++ /dev/null @@ -1,10 +0,0 @@ -/** One request to the app, as a promise. */ -export function ask(request) { - return new Promise((resolve, reject) => { - window.elyQuery({ - request: JSON.stringify(request), - onSuccess: resolve, - onFailure: (code, message) => reject(new Error(`${code}: ${message}`)), - }) - }) -} diff --git a/apps/ely-mark/web/src/code.js b/apps/ely-mark/web/src/code.js deleted file mode 100644 index e905f75..0000000 --- a/apps/ely-mark/web/src/code.js +++ /dev/null @@ -1,44 +0,0 @@ -// Code blocks in CodeMirror; LaTeX and Mermaid preview. -import { basicSetup, EditorView } from 'codemirror' -import { EditorState } from '@codemirror/state' -import { keymap } from '@codemirror/view' -import { indentWithTab } from '@codemirror/commands' -import { languages } from '@codemirror/language-data' -import { codeBlockConfig } from '@milkdown/kit/component/code-block' -import { theme } from './theme.js' -import { icons } from './icons.js' -import { render } from './math.js' - -/** Previews for one editor's code blocks. */ -function previews(owner) { - return (language, source, apply) => { - const kind = source.length === 0 ? '' : language.toLowerCase() - switch (kind) { - case 'latex': - return render(source, true, document.createElement('div')) - case 'mermaid': - return owner.show(source, apply) - default: - return null - } - } -} - -/** Milkdown's code block config; read-only in preview. */ -export function code(texts, editable, owner) { - const locked = editable ? [] : [EditorState.readOnly.of(true), EditorView.editable.of(false)] - return (ctx) => - ctx.update(codeBlockConfig.key, (prev) => ({ - ...prev, - ...icons, - extensions: [basicSetup, keymap.of([indentWithTab]), theme, ...locked], - languages, - searchPlaceholder: texts.searchLanguage, - noResultText: texts.noResult, - copyText: texts.copy, - previewLabel: texts.preview, - previewLoading: texts.loading, - previewToggleButton: (only) => (only ? texts.edit : texts.hide), - renderPreview: previews(owner), - })) -} diff --git a/apps/ely-mark/web/src/diagram.js b/apps/ely-mark/web/src/diagram.js deleted file mode 100644 index 67b232d..0000000 --- a/apps/ely-mark/web/src/diagram.js +++ /dev/null @@ -1,34 +0,0 @@ -// Mermaid, loaded on first use; only saved source renders. -import mermaid from 'mermaid' - -let count = 0 - -/** The theme from the page's Ely tokens. */ -function variables() { - const style = getComputedStyle(document.documentElement) - const token = (name) => style.getPropertyValue(`--ely-${name}`).trim() - return { - background: token('bg'), - primaryColor: token('surface'), - primaryTextColor: token('fg'), - primaryBorderColor: token('border'), - lineColor: token('fg-muted'), - textColor: token('fg'), - fontFamily: token('font'), - } -} - -/** `source` as SVG; `paper` draws Mermaid's light theme for export. */ -window.elyDiagram = async (source, look) => { - const paper = look === 'paper' - mermaid.initialize({ - startOnLoad: false, - securityLevel: 'strict', - suppressErrorRendering: true, - theme: paper ? 'default' : 'base', - themeVariables: paper ? {} : variables(), - }) - count += 1 - const { svg } = await mermaid.render(`ely-diagram-${count}`, source) - return svg -} diff --git a/apps/ely-mark/web/src/diagrams.js b/apps/ely-mark/web/src/diagrams.js deleted file mode 100644 index 6707fa0..0000000 --- a/apps/ely-mark/web/src/diagrams.js +++ /dev/null @@ -1,90 +0,0 @@ -// Mermaid previews; each editor owns its records. -import { failure } from './failure.js' - -// Beside this script; the page's base is the document's folder. -const here = document.currentScript.src - -/** The Mermaid bundle, added once as a classic script. */ -let loading -export function bundle() { - loading ??= new Promise((resolve, reject) => { - const script = document.createElement('script') - script.src = new URL('diagram.js', here).href - script.onload = () => resolve(window.elyDiagram) - script.onerror = () => reject(new Error('diagram.js did not load')) - document.head.append(script) - }) - return loading -} - -let queue = Promise.resolve() -let count = 0 -const owners = new Set() - -// New tokens mean a new theme; every owner redraws. -new MutationObserver(() => { - for (const owner of owners) owner.redraw() -}).observe(document.documentElement, { attributes: true, attributeFilter: ['style'] }) - -/** A block's diagram, by its id on the page. */ -const box = (id, inner) => `
${inner}
` -const placed = (id) => document.querySelector(`[data-diagram="${id}"]`) !== null -// After the code block's next render. -const settle = () => new Promise((done) => setTimeout(done)) - -/** One editor's diagrams; a result lands only on its placeholder. */ -export function diagrams() { - const records = new Map() - - /** Drops diagrams whose placeholder has left the page. */ - const sweep = () => { - for (const [id, record] of records) { - if (record.settled && !placed(id)) records.delete(id) - } - } - - /** Each draw has a turn; a later draw voids earlier. */ - const draw = (id, record) => { - const turn = ++record.turn - const live = () => records.get(id) === record && record.turn === turn && placed(id) - queue = queue.then(async () => { - await settle() - if (!live()) return - try { - const render = await bundle() - if (!live()) return - const svg = await render(record.source) - await settle() - if (live()) record.apply(box(id, svg)) - } catch (error) { - await settle() - if (!live()) return - records.delete(id) - record.apply(failure(record.source, error.message)) - } - }) - } - - const owner = { - /** The placeholder for `source`; its diagram follows. */ - show(source, apply) { - sweep() - const id = `${++count}` - const record = { source, apply, settled: false, turn: 0 } - records.set(id, record) - settle().then(() => (record.settled = true)) - draw(id, record) - return box(id, '') - }, - redraw() { - sweep() - for (const [id, record] of records) draw(id, record) - }, - release() { - records.clear() - owners.delete(owner) - }, - } - owners.add(owner) - return owner -} diff --git a/apps/ely-mark/web/src/diff.js b/apps/ely-mark/web/src/diff.js deleted file mode 100644 index 850136a..0000000 --- a/apps/ely-mark/web/src/diff.js +++ /dev/null @@ -1,53 +0,0 @@ -// Myers' diff over two lists. - -/** Equal pairs between `a` and `b`, in order. */ -export function common(a, b, same) { - const n = a.length - const m = b.length - const max = n + m - const reach = new Map([[1, 0]]) - const trace = [] - for (let d = 0; d <= max; d++) { - trace.push(new Map(reach)) - for (let k = -d; k <= d; k += 2) { - const down = k === -d || (k !== d && reach.get(k - 1) < reach.get(k + 1)) - let x = down ? reach.get(k + 1) : reach.get(k - 1) + 1 - let y = x - k - while (x < n && y < m && same(a[x], b[y])) { - x += 1 - y += 1 - } - reach.set(k, x) - if (x >= n && y >= m) return walk(trace, n, m) - } - } - throw new Error('diff did not end') -} - -/** Back through the trace: each equal pair, in order. */ -function walk(trace, n, m) { - const pairs = [] - let x = n - let y = m - for (let d = trace.length - 1; d > 0; d--) { - const reach = trace[d] - const k = x - y - const down = k === -d || (k !== d && reach.get(k - 1) < reach.get(k + 1)) - const from = down ? k + 1 : k - 1 - const fromX = reach.get(from) - const fromY = fromX - from - while (x > fromX + (down ? 0 : 1) && y > fromY + (down ? 1 : 0)) { - x -= 1 - y -= 1 - pairs.push([x, y]) - } - x = fromX - y = fromY - } - while (x > 0 && y > 0) { - x -= 1 - y -= 1 - pairs.push([x, y]) - } - return pairs.reverse() -} diff --git a/apps/ely-mark/web/src/export.css b/apps/ely-mark/web/src/export.css deleted file mode 100644 index 4ed2eea..0000000 --- a/apps/ely-mark/web/src/export.css +++ /dev/null @@ -1,72 +0,0 @@ -/* Exports: KaTeX with its fonts inline, on light paper. */ -@import 'katex/dist/katex.min.css'; - -:root { - color-scheme: light; -} - -body { - margin: 0 auto; - max-width: 48rem; - padding: 2rem 1.5rem; - background: #fff; - color: #1f2328; - font: 16px / 1.6 -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif; -} - -a { - color: #0969da; -} - -pre, -code { - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; - font-size: 0.875em; -} - -pre { - overflow-x: auto; - padding: 0.75rem 1rem; - border-radius: 6px; - background: #f6f8fa; -} - -blockquote { - margin: 0; - padding: 0 1rem; - border-left: 3px solid #d0d7de; - color: #59636e; -} - -table { - border-collapse: collapse; -} - -th, -td { - padding: 0.25rem 0.75rem; - border: 1px solid #d0d7de; -} - -img, -figure.diagram svg { - max-width: 100%; -} - -figure.diagram { - margin: 1rem 0; - text-align: center; -} - -@media print { - body { - max-width: none; - padding: 0; - } - - pre, - table, - figure { - break-inside: avoid; - } -} diff --git a/apps/ely-mark/web/src/failure.js b/apps/ely-mark/web/src/failure.js deleted file mode 100644 index 8668b73..0000000 --- a/apps/ely-mark/web/src/failure.js +++ /dev/null @@ -1,11 +0,0 @@ -/** The source as saved, and why it did not render. */ -export function failure(source, message, at) { - const box = document.createElement('div') - box.className = 'ely-failure' - const why = document.createElement('p') - why.textContent = at === undefined ? message : `${message} (${at})` - const code = document.createElement('pre') - code.textContent = source - box.append(why, code) - return box -} diff --git a/apps/ely-mark/web/src/find.js b/apps/ely-mark/web/src/find.js deleted file mode 100644 index 7d154b0..0000000 --- a/apps/ely-mark/web/src/find.js +++ /dev/null @@ -1,138 +0,0 @@ -// Find and replace in either editor (MARK-06). -import { - SearchQuery as TextQuery, - findNext as nextText, - findPrevious as previousText, - replaceAll as allText, - replaceNext as oneText, - setSearchQuery, -} from '@codemirror/search' -import { - SearchQuery as DocQuery, - findNext as nextDoc, - findPrev as previousDoc, - replaceAll as allDoc, - replaceNext as oneDoc, - setSearchState, -} from 'prosemirror-search' -import { TextSelection } from '@milkdown/kit/prose/state' - -/** Why a pattern fails under the editors' `gmu` flags. */ -function broken(spec) { - if (!spec.regexp || !spec.search) return null - try { - new RegExp(spec.search, 'gmu') - return null - } catch (error) { - return error.message - } -} - -/** Whether two specs find the same hits. */ -export function sameQuery(a, b) { - return ['search', 'caseSensitive', 'regexp', 'wholeWord'].every((key) => a[key] === b[key]) -} - -/** One character's width at `pos`: two for a surrogate pair. */ -function width(doc, pos) { - const next = doc.textBetween(pos, Math.min(pos + 2, doc.content.size), '\n', '\n') - return next.codePointAt(0) > 0xffff ? 2 : 1 -} - -/** Where the selection stands among `ranges`. */ -function standing(ranges, from, to) { - const at = ranges.findIndex((range) => range.from === from && range.to === to) - return { current: at < 0 ? null : at, total: ranges.length } -} - -/** Find in Milkdown's view; `prosemirror-search` is installed there. */ -export function docFinder(view) { - let spec = { search: '' } - const query = () => new DocQuery(spec) - const ranges = () => { - const found = [] - const asked = query() - if (!asked.valid) return found - const { doc } = view.state - for (let at = 0; ;) { - const hit = asked.findNext(view.state, at) - // Mid-pair, a Unicode pattern answers before `at`. - if (!hit || hit.from < at) return found - found.push(hit) - at = hit.to > hit.from ? hit.to : hit.to + width(doc, hit.to) - } - } - const status = () => { - const error = broken(spec) - if (error) return { error } - const { from, to } = view.state.selection - return standing(ranges(), from, to) - } - const run = (command) => command(view.state, view.dispatch) - return { - seek(next) { - spec = next - const { from } = view.state.selection - const tr = setSearchState(view.state.tr, query()) - // From the hit's start: the hit stays while typing. - tr.setSelection(TextSelection.near(tr.doc.resolve(from))) - view.dispatch(tr) - if (query().valid) run(nextDoc) - return status() - }, - step(forward) { - if (query().valid) run(forward ? nextDoc : previousDoc) - return status() - }, - replace(all) { - if (query().valid) run(all ? allDoc : oneDoc) - return status() - }, - clear() { - spec = { search: '' } - view.dispatch(setSearchState(view.state.tr, query())) - }, - } -} - -/** Find in CodeMirror; its `search()` extension is installed there. */ -export function textFinder(view) { - let spec = { search: '' } - const valid = () => new TextQuery(spec).valid - const ranges = () => { - const found = [] - const asked = new TextQuery(spec) - if (!asked.valid) return found - const cursor = asked.getCursor(view.state) - for (let next = cursor.next(); !next.done; next = cursor.next()) found.push(next.value) - return found - } - const status = () => { - const error = broken(spec) - if (error) return { error } - const { from, to } = view.state.selection.main - return standing(ranges(), from, to) - } - return { - seek(next) { - spec = next - const selection = { anchor: view.state.selection.main.from } - view.dispatch({ effects: setSearchQuery.of(new TextQuery(spec)), selection }) - // CodeMirror opens its own panel for invalid queries. - if (valid()) nextText(view) - return status() - }, - step(forward) { - if (valid()) (forward ? nextText : previousText)(view) - return status() - }, - replace(all) { - if (valid()) (all ? allText : oneText)(view) - return status() - }, - clear() { - spec = { search: '' } - view.dispatch({ effects: setSearchQuery.of(new TextQuery(spec)) }) - }, - } -} diff --git a/apps/ely-mark/web/src/icons.js b/apps/ely-mark/web/src/icons.js deleted file mode 100644 index 68ab6ac..0000000 --- a/apps/ely-mark/web/src/icons.js +++ /dev/null @@ -1,12 +0,0 @@ -// Code block icons, drawn in the text color. -const icon = (path) => - `${path}` - -export const icons = { - expandIcon: icon(''), - searchIcon: icon(''), - clearSearchIcon: icon(''), - copyIcon: icon( - '', - ), -} diff --git a/apps/ely-mark/web/src/image.js b/apps/ely-mark/web/src/image.js deleted file mode 100644 index 74a740b..0000000 --- a/apps/ely-mark/web/src/image.js +++ /dev/null @@ -1,14 +0,0 @@ -// Untitled images: remark gives null, the schema wants text. -import { imageSchema } from '@milkdown/kit/preset/commonmark' - -export const image = imageSchema.extendSchema((prev) => (ctx) => { - const base = prev(ctx) - return { - ...base, - parseMarkdown: { - match: base.parseMarkdown.match, - runner: (state, node, type) => - state.addNode(type, { src: node.url, alt: node.alt ?? '', title: node.title ?? '' }), - }, - } -}) diff --git a/apps/ely-mark/web/src/look.js b/apps/ely-mark/web/src/look.js deleted file mode 100644 index 298c103..0000000 --- a/apps/ely-mark/web/src/look.js +++ /dev/null @@ -1,22 +0,0 @@ -// The writing view: focus, typewriter, width, font, size (MARK-09). - -let typewriter = false - -/** Applies `look`; the Markdown never changes. */ -export function apply(look) { - const root = document.documentElement - root.dataset.focus = look.focus - root.dataset.width = look.width - root.dataset.font = look.font - root.dataset.typewriter = look.typewriter - typewriter = look.typewriter - const sheet = - document.getElementById('ely-look') ?? - document.head.appendChild(Object.assign(document.createElement('style'), { id: 'ely-look' })) - sheet.textContent = `:root { --ely-text-scale: ${look.size / 100}; }` -} - -/** Typewriter scrolling: the caret's line stays mid-view. */ -export function follow(editor) { - if (typewriter) editor?.center?.() -} diff --git a/apps/ely-mark/web/src/main.css b/apps/ely-mark/web/src/main.css deleted file mode 100644 index 6862cfe..0000000 --- a/apps/ely-mark/web/src/main.css +++ /dev/null @@ -1,330 +0,0 @@ -@import 'katex/dist/katex.min.css'; -@import '@milkdown/kit/prose/view/style/prosemirror.css'; - -html, -body { - margin: 0; - height: 100%; - background: var(--ely-bg); - color: var(--ely-fg); - font: calc(16px * var(--ely-font-scale, 1) * var(--ely-text-scale, 1)) / 1.6 var(--ely-font), - sans-serif; -} - -::selection { - background: var(--ely-selection); -} - -a { - color: var(--ely-link); -} - -#document { - box-sizing: border-box; - max-width: 48rem; - margin: 0 auto; - padding: 2rem 1.5rem; -} - -:root[data-width='narrow'] #document { - max-width: 36rem; -} - -:root[data-width='wide'] #document { - max-width: 64rem; -} - -:root[data-font='serif'] #document:not([data-mode='source']) { - font-family: ui-serif, Georgia, serif; -} - -:root[data-font='mono'] #document:not([data-mode='source']) { - font-family: var(--ely-mono), monospace; -} - -/* Typewriter: room to center the first and last lines. */ -:root[data-typewriter='true'] #document:not([data-mode='source']) { - padding-block: 50vh; -} - -:root[data-typewriter='true'] .cm-content { - padding-block: 50vh; -} - -/* Focus mode: the caret's block, or line, stays bright. */ -:root[data-focus='true'] .milkdown .ProseMirror > :not(.ely-current), -:root[data-focus='true'] .cm-line:not(.cm-activeLine) { - opacity: 0.35; -} - -@media (prefers-reduced-motion: no-preference) { - .milkdown .ProseMirror > *, - .cm-line { - transition: opacity 150ms; - } -} - -#document[data-mode='source'] { - max-width: none; - height: 100%; - padding: 0; -} - -#document[data-mode='source'] > div, -#document[data-mode='source'] .cm-editor { - height: 100%; -} - -.milkdown .ProseMirror { - outline: none; -} - -.milkdown blockquote { - margin: 0; - padding-left: 1rem; - border-left: 3px solid var(--ely-border); - color: var(--ely-fg-muted); -} - -.milkdown code { - font-family: var(--ely-mono), monospace; - font-size: 0.875em; - background: var(--ely-sunken); - border-radius: 4px; - padding: 0 0.25em; -} - -.milkdown table { - border-collapse: collapse; -} - -.milkdown th, -.milkdown td { - border: 1px solid var(--ely-border); - padding: 0.25rem 0.75rem; -} - -.milkdown li > p, -.milkdown th > p, -.milkdown td > p { - margin: 0.25em 0; -} - -.milkdown li[data-item-type='task'] { - display: flex; - gap: 0.5em; - list-style: none; -} - -/* A text-sized box, centred on the first line. */ -.milkdown li[data-item-type='task']::before { - content: ''; - flex: none; - box-sizing: border-box; - width: 1em; - height: 1em; - margin-top: calc(0.25em + (1lh - 1em) / 2); - border: 0.09em solid var(--ely-fg-muted); - border-radius: 0.2em; -} - -.milkdown li[data-item-type='task'][data-checked='true']::before { - border-color: var(--ely-link); - background: var(--ely-link); - box-shadow: inset 0 0 0 0.18em var(--ely-bg); -} - -.milkdown hr { - border: none; - border-top: 1px solid var(--ely-border); -} - -.milkdown img { - max-width: 100%; -} - -.milkdown-code-block { - display: block; - position: relative; - margin: 0.5rem 0; - padding: 0.5rem 1rem 1rem; - background: var(--ely-surface); - border: 1px solid var(--ely-border); - border-radius: 6px; -} - -.milkdown-code-block.selected { - outline: 1px solid var(--ely-focus); -} - -.milkdown-code-block .hidden { - display: none !important; -} - -.milkdown-code-block .tools { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.5rem; - font-size: 0.75rem; -} - -.milkdown-code-block button, -.milkdown-code-block .language-button { - display: flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - border: 1px solid var(--ely-border); - border-radius: 4px; - background: var(--ely-sunken); - color: var(--ely-fg-muted); - font: inherit; - cursor: pointer; -} - -[data-mode='preview'] .milkdown-code-block .language-button { - pointer-events: none; -} - -.milkdown-code-block .tools-button-group { - display: flex; - gap: 4px; -} - -.milkdown-code-block button:hover, -.milkdown-code-block .language-button:hover { - background: var(--ely-hover); -} - -.milkdown-code-block .tools svg, -.milkdown-code-block .language-picker svg { - width: 14px; - height: 14px; -} - -.ely-diagram svg { - max-width: 100%; - height: auto; -} - -.milkdown-code-block .language-picker { - position: absolute; - z-index: 1; - padding-top: 4px; -} - -.milkdown-code-block .list-wrapper { - width: 15rem; - padding-top: 0.5rem; - background: var(--ely-surface); - border: 1px solid var(--ely-border); - border-radius: 6px; -} - -.milkdown-code-block .search-box { - display: flex; - align-items: center; - gap: 6px; - margin: 0 0.5rem 0.5rem; - padding: 4px 8px; - border: 1px solid var(--ely-border); - border-radius: 4px; -} - -.milkdown-code-block .search-box input { - width: 100%; - border: none; - outline: none; - background: transparent; - color: var(--ely-fg); - font: inherit; -} - -.milkdown-code-block .search-icon { - display: none; -} - -.milkdown-code-block .language-list { - max-height: 20rem; - overflow-y: auto; - margin: 0; - padding: 0; -} - -.milkdown-code-block .language-list-item { - display: flex; - gap: 8px; - padding: 2px 1rem; - cursor: pointer; -} - -.milkdown-code-block .language-list-item:hover, -.milkdown-code-block .language-list-item:focus-visible { - outline: none; - background: var(--ely-hover); -} - -.milkdown-code-block .language-list-item.no-result { - cursor: default; - color: var(--ely-fg-subtle); -} - -.milkdown-code-block .preview-panel .preview-divider { - height: 1px; - margin: 0.5rem 0; - background: var(--ely-border); -} - -.milkdown-code-block .preview-panel .preview-label { - margin: 0.5rem 0; - font-size: 0.75rem; - color: var(--ely-fg-muted); -} - -.milkdown-code-block .preview { - overflow-x: auto; - text-align: center; -} - -.ely-failure { - text-align: left; - color: var(--ely-danger); -} - -.ely-failure pre { - margin: 0; - white-space: pre-wrap; - color: var(--ely-fg); - font-family: var(--ely-mono), monospace; -} - -.ely-front { - margin: 0 0 1rem; - padding: 0.5rem 1rem; - border: 1px solid var(--ely-border); - border-radius: 6px; - color: var(--ely-fg-muted); - font-family: var(--ely-mono), monospace; - white-space: pre-wrap; -} - -.ely-raw { - margin: 0.5rem 0; - padding: 0.25rem 0.5rem; - background: var(--ely-sunken); - border-radius: 4px; - font-family: var(--ely-mono), monospace; - font-size: 0.875em; - white-space: pre-wrap; -} - -.ProseMirror-search-match, -.cm-searchMatch { - background: var(--ely-selection); -} - -.ProseMirror-active-search-match, -.cm-searchMatch-selected { - outline: 1px solid var(--ely-focus); -} diff --git a/apps/ely-mark/web/src/main.js b/apps/ely-mark/web/src/main.js deleted file mode 100644 index b9e4c0e..0000000 --- a/apps/ely-mark/web/src/main.js +++ /dev/null @@ -1,342 +0,0 @@ -// The page the app drives: one document, three modes. -import { visual } from './visual.js' -import { source } from './source.js' -import { ask } from './bridge.js' -import { sameQuery } from './find.js' -import { apply, follow } from './look.js' -import { html, text } from './render.js' - -let texts = {} -let latest = 0 -let version = 0 -// The app's count of page loads; reports carry it. -let generation = 0 -let timer -let pending = null -/** The editor shown. */ -let current = null -// A mode asked for while an editor builds. -let asked = null -// The text no editor could show; a mode retries it. -let broken = null -// A place to show once the editor is built. -let revealing = null -// The search in force, kept across modes. -let seeking = null -/** The file's texts where a mode with edits ended (§14.3). */ -let times = [] -let at = 0 - -/** YAML front matter, kept out of the visual editor. */ -function split(text) { - const front = /^---\n[\s\S]*?\n(?:---|\.\.\.)[ \t]*(?:\n|$)/.exec(text) - return front ? [front[0], text.slice(front[0].length)] : ['', text] -} - -const tell = (said) => ask(said).catch((error) => console.error(`ely: ${error.message}`)) - -/** Sends the waiting text now. */ -function flush() { - clearTimeout(timer) - if (pending) { - tell(pending()) - outline() - } - pending = null -} - -/** A link the user opens; the app decides. */ -const linked = (href) => tell({ kind: 'link', generation, href }) - -/** The shown editor's headings, for the app's outline. */ -function outline() { - if (current) tell({ kind: 'outline', generation, items: current.outline() }) -} - -/** A new text: told now, its words once typing rests. */ -function revised(text) { - version += 1 - const now = version - tell({ kind: 'changed', version: now, generation }) - clearTimeout(timer) - pending = () => ({ kind: 'text', version: now, generation, text: text() }) - timer = setTimeout(flush, 300) -} - -/** An edit: later texts in the timeline are gone. */ -function changed(text) { - times.length = at + 1 - revised(text) -} - -/** An editor for `mode`; a failed one leaves no holder. */ -async function build(text, mode) { - const holder = document.createElement('div') - holder.hidden = true - document.getElementById('document').append(holder) - try { - return await fill(holder, text, mode) - } catch (error) { - holder.remove() - throw error - } -} - -/** Builds in `holder`; visual falls back to source. */ -async function fill(holder, text, mode) { - const entry = { mode, holder, fallback: false } - if (mode !== 'source') { - const [front, body] = split(text) - if (front) { - const raw = document.createElement('pre') - raw.className = 'ely-front' - raw.textContent = front - holder.append(raw) - } - const place = document.createElement('div') - holder.append(place) - const edited = (write) => changed(() => front + write()) - let made - try { - made = await visual(place, body, mode === 'visual', texts, edited, edge, linked) - } catch (error) { - // A broken editor falls back to source and says so. - entry.broke = error.name - } - if (made) { - // Lines count from the file's top, front matter included. - const above = front ? front.split('\n').length - 1 : 0 - const line = (n, query) => { - if (n > above) return made.line(n - above, query) - holder.firstChild.scrollIntoView({ block: 'center' }) - return true - } - const redone = () => { - const ahead = made.redone() - return ahead === null ? null : front + ahead - } - const where = () => { - const { line, column } = made.where() - return { line: above + line, column } - } - const at = (n, column) => (n > above ? made.at(n - above, column) : line(n, '')) - return { ...entry, ...made, text: () => front + made.text(), redone, line, where, at } - } - // Blocks the editor cannot map open as source. - holder.replaceChildren() - Object.assign(entry, { mode: 'source', fallback: true }) - } - return { ...entry, ...source(holder, text, changed, edge) } -} - -function drop(entry) { - entry.close() - entry.holder.remove() -} - -/** Shows `text` as `want`; the newest request alone lands. */ -async function show(text, want) { - const turn = ++latest - asked = null - broken = null - if (current) drop(current) - current = null - let entry - try { - entry = await build(text, want) - } catch (error) { - if (turn !== latest) return - // Source failed too: tell the app, keep the text. - broken = text - tell({ kind: 'failed', generation, what: error.name }) - const next = asked - asked = null - return next && mode(next) - } - if (turn !== latest) return drop(entry) - current = entry - entry.holder.hidden = false - document.getElementById('document').dataset.mode = entry.mode - entry.focus() - await ask({ kind: 'shown', mode: entry.mode, fallback: entry.fallback, broke: entry.broke ?? null }) - // A newer request owns what is queued now. - if (turn !== latest) return - outline() - if (seeking) found(entry.find.seek(seeking)) - const place = revealing - revealing = null - if (place) reveal(place) - const next = asked - asked = null - if (next && next !== entry.mode) return mode(next) -} - -/** Undo or redo past an editor's own history. */ -function edge(direction) { - const to = direction === 'undo' ? at - 1 : at + 1 - if (!current || to < 0 || to >= times.length) return false - flush() - at = to - const text = times[at] - revised(() => text) - show(text, current.mode) - return true -} - -/** Relative links and images resolve under `href`. */ -function base(href) { - const link = - document.querySelector('base') ?? document.head.appendChild(document.createElement('base')) - link.href = href -} - -/** Puts the caret on `place.line`, the page at `place.scroll`. */ -function settle(place) { - if (!current || !place) return - current.at(place.line, place.column) - document.scrollingElement.scrollTop = place.scroll -} - -/** A document, after each page load; `place` restores a session. */ -async function open(text, want, words, load, folder, place) { - texts = words - generation = load - times = [text] - at = 0 - base(folder) - await show(text, want) - settle(place) -} - -/** Changed on disk: the caret and scroll stay near. */ -async function refresh(text, load) { - const place = current && { ...current.where(), scroll: document.scrollingElement.scrollTop } - clearTimeout(timer) - pending = null - generation = load - version = 0 - times = [text] - at = 0 - await show(text, current?.mode ?? 'visual') - settle(place) -} - -/** `text` in place of the editor's, as one edit. */ -function adopt(text) { - if (current) flush() - times.length = at + 1 - times.push(text) - at += 1 - revised(() => text) - return show(text, current?.mode ?? 'visual') -} - -let placing -/** The caret's line and the scroll, once they rest. */ -function placed() { - clearTimeout(placing) - placing = setTimeout(() => { - if (!current) return - const scroll = document.scrollingElement.scrollTop - tell({ kind: 'place', generation, ...current.where(), scroll }) - }, 500) -} -document.addEventListener('selectionchange', () => { - placed() - follow(current) -}) -window.addEventListener('scroll', placed, { passive: true }) - -/** Another mode for the same text; edits are sent first. */ -function mode(next) { - if (!current) { - if (broken !== null) return show(broken, next) - asked = next - return - } - const text = current.text() - const ahead = current.redone?.() ?? null - flush() - // Leaving a mode with edits marks the timeline. - if (text !== times[at]) { - times.length = at + 1 - times.push(text) - at += 1 - } - // Undone steps stay ahead, for redo. - if (ahead !== null && ahead !== text) { - times.length = at + 1 - times.push(ahead) - } - return show(text, next) -} - -/** Shows `{ heading }` or `{ line, query }`. */ -function reveal(place) { - if (!current) { - revealing = place - return - } - const shown = - 'heading' in place ? current.heading(place.heading) : current.line(place.line, place.query) - // The app logs a line number, never the query. - if (shown === false) tell({ kind: 'missed', generation, line: place.line ?? null }) -} - -/** The app's find box hears where the search stands. */ -const found = (status) => tell({ kind: 'found', generation, ...status }) - -/** Searches for `spec`: search, caseSensitive, regexp, wholeWord, replace. */ -function find(spec) { - seeking = spec - if (current) found(current.find.seek(spec)) -} - -/** The box's spec rules; a new query starts afresh. */ -function step({ spec, forward }) { - const fresh = !seeking || !sameQuery(seeking, spec) - find(spec) - if (current && !fresh) found(current.find.step(forward)) -} - -function replace({ spec, all }) { - find(spec) - if (current && current.mode !== 'preview') found(current.find.replace(all)) -} - -function unfind() { - seeking = null - current?.find.clear() -} - -/** The writing view's settings (MARK-09). */ -function look(settings) { - apply(settings) - follow(current) -} - -/** The whole text as `kind`: html or text, tagged `ask`. */ -async function render({ kind, ask: asked }) { - try { - if (!current) throw new Error('no document is shown') - const [, body] = split(current.text()) - const result = kind === 'text' ? { text: await text(body) } : await html(body) - tell({ kind: 'rendered', generation, ask: asked, ...result }) - } catch (error) { - tell({ kind: 'rendered', generation, ask: asked, error: error.message }) - } -} - -/** A table step; the app hears whether it ran. */ -function table(op) { - const done = current?.table?.(op) ?? false - tell({ kind: 'table', generation, op, done }) -} - -/** Image links at the caret; preview takes none. */ -function insert(images) { - const done = current?.insert?.(images) ?? false - tell({ kind: 'inserted', generation, done }) -} - -window.ely = { open, refresh, adopt, look, render, mode, base, reveal, find, step, replace, unfind, table, insert } diff --git a/apps/ely-mark/web/src/math.js b/apps/ely-mark/web/src/math.js deleted file mode 100644 index b096d07..0000000 --- a/apps/ely-mark/web/src/math.js +++ /dev/null @@ -1,100 +0,0 @@ -// KaTeX math, from Milkdown Crepe's latex feature. -import katex from 'katex' -import remarkMath from 'remark-math' -import { visit } from 'unist-util-visit' -import { $nodeSchema, $remark } from '@milkdown/kit/utils' -import { codeBlockSchema } from '@milkdown/kit/preset/commonmark' -import { failure } from './failure.js' - -/** Saved source as HTML; a bad one shows where. */ -export function render(source, display, dom) { - try { - katex.render(source, dom, { displayMode: display, throwOnError: true }) - } catch (error) { - const at = error instanceof katex.ParseError ? error.position : undefined - dom.replaceChildren(failure(source, error.message, at)) - } - return dom -} - -// remark-math, plus each inline formula's `$` count from the source. -const remark = $remark('remarkMath', () => function (options) { - remarkMath.call(this, options) - return (tree, file) => { - const text = String(file.value) - visit(tree, 'inlineMath', (node) => { - let at = node.position?.start.offset ?? 0 - let fence = 0 - while (text[at++] === '$') fence++ - node.data = { ...node.data, fence: Math.max(fence, 1) } - }) - } -}) - -// Block math becomes a code block in language LaTeX. -const blocks = $remark( - 'remarkMathBlock', - () => () => (tree) => - visit(tree, 'math', (node, index, parent) => { - parent.children.splice(index, 1, { - type: 'code', - lang: 'LaTeX', - value: node.value, - position: node.position, - }) - }), -) - -const block = codeBlockSchema.extendSchema((prev) => (ctx) => { - const base = prev(ctx) - return { - ...base, - toMarkdown: { - match: base.toMarkdown.match, - runner: (state, node) => { - if ((node.attrs.language ?? '').toLowerCase() !== 'latex') { - return base.toMarkdown.runner(state, node) - } - state.addNode('math', undefined, node.content.firstChild?.text || '') - }, - }, - } -}) - -const inline = $nodeSchema('math_inline', () => ({ - group: 'inline', - inline: true, - atom: true, - attrs: { value: { default: '' }, fence: { default: 1 } }, - parseDOM: [ - { - tag: 'span[data-type="math_inline"]', - getAttrs: (dom) => ({ value: dom.dataset.value ?? '', fence: Number(dom.dataset.fence ?? 1) }), - }, - ], - toDOM: (node) => { - const dom = document.createElement('span') - dom.dataset.type = 'math_inline' - dom.dataset.value = node.attrs.value - dom.dataset.fence = node.attrs.fence - // `$$…$$` displays, as GitHub shows it. - return render(node.attrs.value, node.attrs.fence > 1, dom) - }, - parseMarkdown: { - match: (node) => node.type === 'inlineMath', - runner: (state, node, type) => - state.addNode(type, { value: node.value, fence: node.data?.fence ?? 1 }), - }, - toMarkdown: { - match: (node) => node.type.name === 'math_inline', - runner: (state, node) => { - const { value, fence } = node.attrs - if (fence === 1) return state.addNode('inlineMath', undefined, value) - // The stringifier knows one `$`; the source's own count stays. - const dollars = '$'.repeat(fence) - state.addNode('html', undefined, `${dollars}${value}${dollars}`) - }, - }, -})) - -export const math = [remark, blocks, block, inline].flat() diff --git a/apps/ely-mark/web/src/preserve.js b/apps/ely-mark/web/src/preserve.js deleted file mode 100644 index 10cbbd3..0000000 --- a/apps/ely-mark/web/src/preserve.js +++ /dev/null @@ -1,149 +0,0 @@ -// Blocks Milkdown cannot write back stay raw text (§14.3). -import { editorViewCtx, remarkCtx, schemaCtx, serializerCtx } from '@milkdown/kit/core' -import { canon } from './tables.js' -import { Fragment } from '@milkdown/kit/prose/model' -import { Selection } from '@milkdown/kit/prose/state' -import { $nodeSchema } from '@milkdown/kit/utils' -import { common } from './diff.js' - -/** Source shown and written as it is. */ -export const raw = $nodeSchema('ely_raw', () => ({ - group: 'block', - content: 'text*', - marks: '', - code: true, - defining: true, - isolating: true, - parseDOM: [{ tag: 'pre.ely-raw', preserveWhitespace: 'full' }], - toDOM: () => ['pre', { class: 'ely-raw' }, 0], - parseMarkdown: { match: () => false, runner: () => {} }, - toMarkdown: { - match: (node) => node.type.name === 'ely_raw', - runner: (state, node) => state.addNode('html', undefined, node.textContent), - }, -})) - -/** Each block's span, as Milkdown reads it; null if unplaced. */ -function spans(ctx, text) { - const remark = ctx.get(remarkCtx) - const tree = remark.runSync(remark.parse(text), text) - const at = tree.children.map( - (node) => node.position && [node.position.start.offset, node.position.end.offset], - ) - return at.every(Boolean) ? at : null -} - -/** The text in `from`..`to` past its outer whitespace. */ -function inner(text, from, to) { - const piece = text.slice(from, to) - const start = piece.search(/\S/) - if (start < 0) return null - return [from + start, from + piece.trimEnd().length] -} - -/** Raw blocks in place; edits then rewrite only their blocks. */ -export function faithful(ctx, original) { - const view = ctx.get(editorViewCtx) - const parsed = view.state.doc - const at = spans(ctx, original) - // No blocks: Milkdown still holds one empty paragraph. - const blank = at?.length === 0 && parsed.childCount === 1 && parsed.textContent === '' - if (!at || (at.length !== parsed.childCount && !blank)) return null - const schema = ctx.get(schemaCtx) - const serializer = ctx.get(serializerCtx) - const written = (nodes) => serializer(schema.topNodeType.create(null, nodes)).replace(/\n+$/, '') - // Table spacing is free; every cell's source must match. - const kept = (node, text) => { - const own = written([node]) - return own === text || canon(own) === canon(text) - } - const rawOf = ([from, to]) => { - const text = original.slice(from, to) - return schema.nodes.ely_raw.create(null, text ? schema.text(text) : null) - } - const nodes = [] - const places = [] - // Text Milkdown drops between blocks, as raw blocks. - const gap = (from, to) => { - const place = inner(original, from, to) - if (!place) return - nodes.push(rawOf(place)) - places.push(place) - } - let last = 0 - if (!blank) - parsed.forEach((node, _, index) => { - gap(last, at[index][0]) - nodes.push(kept(node, original.slice(...at[index])) ? node : rawOf(at[index])) - places.push(at[index]) - last = at[index][1] - }) - gap(last, original.length) - if (nodes.length) { - const swap = view.state.tr.replaceWith(0, parsed.content.size, Fragment.from(nodes)) - swap.setSelection(Selection.atStart(swap.doc)) - view.dispatch(swap.setMeta('addToHistory', false)) - } - const first = view.state.doc - const before = [] - first.forEach((node) => before.push(node)) - /** Where each fresh block sits in `text` from `at`. */ - const found = (nodes, text, at, spans) => { - let from = 0 - for (const node of nodes) { - const own = written([node]) - const place = text.indexOf(own, from) - spans.push(place < 0 ? null : [at + place, at + place + own.length]) - if (place >= 0) from = place + own.length - } - } - // The text and each block's span in it. - return (doc) => { - if (doc.eq(first)) return { text: original, spans: places } - const after = [] - doc.forEach((node) => after.push(node)) - if (!places.length) { - const fresh = written(after) - const spans = [] - found(after, fresh, original.length, spans) - return { text: `${original}${fresh}\n`, spans } - } - const pairs = common(before, after, (x, y) => x === y || x.eq(y)) - pairs.push([before.length, after.length]) - let out = '' - let cursor = 0 - let [i, j] = [0, 0] - const spans = [] - for (const [x, y] of pairs) { - if (x > i || y > j) { - const fresh = y > j ? written(after.slice(j, y)) : '' - let start, end, text - if (x > i && fresh) { - ;[start, end, text] = [places[i][0], places[x - 1][1], fresh] - } else if (x > i && i > 0) { - // Removed blocks take the gap before them. - ;[start, end, text] = [places[i - 1][1], places[x - 1][1], ''] - } else if (x > i && x < places.length) { - ;[start, end, text] = [places[0][0], places[x][0], ''] - } else if (x > i) { - ;[start, end, text] = [places[0][0], places[x - 1][1], ''] - } else if (i > 0) { - ;[start, end, text] = [places[i - 1][1], places[i - 1][1], `\n\n${fresh}`] - } else { - ;[start, end, text] = [places[0][0], places[0][0], `${fresh}\n\n`] - } - out += original.slice(cursor, start) - found(after.slice(j, y), text, out.length, spans) - out += text - cursor = end - } - // Unchanged blocks keep their bytes, shifted. - if (x < places.length) { - const shift = out.length - cursor - spans.push([places[x][0] + shift, places[x][1] + shift]) - } - ;[i, j] = [x + 1, y + 1] - } - return { text: out + original.slice(cursor), spans } - } -} diff --git a/apps/ely-mark/web/src/render.js b/apps/ely-mark/web/src/render.js deleted file mode 100644 index 20d1dd2..0000000 --- a/apps/ely-mark/web/src/render.js +++ /dev/null @@ -1,109 +0,0 @@ -// The whole document as one page or as text (MARK-11). -import { unified } from 'unified' -import remarkParse from 'remark-parse' -import remarkGfm from 'remark-gfm' -import remarkMath from 'remark-math' -import remarkRehype from 'remark-rehype' -import rehypeKatex from 'rehype-katex' -import { fromDom } from 'hast-util-from-dom' -import rehypeStringify from 'rehype-stringify' -import { toText } from 'hast-util-to-text' -import { visit } from 'unist-util-visit' -import { VFile } from 'vfile' -import { bundle } from './diagrams.js' - -const markdown = () => unified().use(remarkParse).use(remarkGfm).use(remarkMath) - -/** Mermaid drawn for paper; a broken one stays code. */ -async function draw(tree) { - const blocks = [] - visit(tree, 'code', (node) => { - if (node.lang === 'mermaid') blocks.push(node) - }) - const failed = [] - if (!blocks.length) return failed - const paint = await bundle() - for (const node of blocks) { - try { - const svg = await paint(node.value, 'paper') - Object.assign(node, { type: 'html', value: `
${svg}
` }) - } catch { - // Lines only: Mermaid's message quotes the document. - failed.push(node.position.start.line) - } - } - return failed -} - -/** A `srcset`'s first URL, split as the spec does. */ -function first(srcset) { - const url = srcset.replace(/^[\s,]+/, '').split(/\s/)[0] - return url.replace(/,+$/, '') -} - -/** One `src` per image, swapped for the app's mark. */ -function marked(html) { - const doc = new DOMParser().parseFromString(html, 'text/html') - const token = `ely-image-${crypto.randomUUID()}-` - // Responsive sources go: the page carries one file per image. - for (const source of doc.querySelectorAll('picture > source')) source.remove() - const images = [] - for (const image of doc.querySelectorAll('img')) { - const srcset = image.getAttribute('srcset') - image.removeAttribute('srcset') - image.removeAttribute('sizes') - const src = image.getAttribute('src') || (srcset && first(srcset)) - if (!src) continue - images.push(src) - image.setAttribute('src', token + (images.length - 1)) - } - return { html: doc.body.innerHTML, images, token } -} - -/** KaTeX's failures: line and its message, never logged. */ -function formulas(file) { - return file.messages - .filter((message) => message.source === 'rehype-katex') - .map((message) => ({ - line: message.line, - error: message.cause?.rawMessage ?? message.cause?.name ?? message.reason - })) -} - -/** `body` as HTML, images marked; the failed diagrams and formulas. */ -export async function html(body) { - const tree = markdown().parse(body) - displayed(tree, body) - const failed = await draw(tree) - const page = unified() - .use(remarkRehype, { allowDangerousHtml: true }) - .use(rehypeKatex) - .use(rehypeStringify, { allowDangerousHtml: true }) - const file = new VFile() - const done = page.stringify(await page.run(tree, file), file) - return { ...marked(done), failed, formulas: formulas(file) } -} - -/** `$$…$$` inline displays, as in the editor. */ -function displayed(tree, body) { - visit(tree, 'inlineMath', (node) => { - const at = node.position?.start.offset ?? 0 - if (body.startsWith('$$', at)) { - node.data.hProperties = { className: ['language-math', 'math-display'] } - } - }) -} - -/** `body` as plain text: blocks on lines, math as TeX. */ -export async function text(body) { - const page = unified().use(remarkRehype, { allowDangerousHtml: true }) - const hast = await page.run(markdown().parse(body)) - // Raw HTML alone goes through the DOM; blocks keep lines. - visit(hast, 'raw', (node, index, parent) => { - const parsed = new DOMParser().parseFromString(node.value, 'text/html').body - const shown = fromDom(parsed).children - parent.children.splice(index, 1, ...shown) - return index + shown.length - }) - return toText(hast) -} diff --git a/apps/ely-mark/web/src/source.js b/apps/ely-mark/web/src/source.js deleted file mode 100644 index aed9443..0000000 --- a/apps/ely-mark/web/src/source.js +++ /dev/null @@ -1,109 +0,0 @@ -// CodeMirror 6: the Markdown source. -import { basicSetup, EditorView } from 'codemirror' -import { Prec } from '@codemirror/state' -import { indentWithTab, redo, redoDepth, undoDepth } from '@codemirror/commands' -import { keymap } from '@codemirror/view' -import { markdown as language } from '@codemirror/lang-markdown' -import { languages } from '@codemirror/language-data' -import { ensureSyntaxTree } from '@codemirror/language' -import { theme } from './theme.js' -import { search } from '@codemirror/search' -import { textFinder } from './find.js' - -/** Alt text that cannot close its brackets. */ -const escape = (alt) => alt.replace(/[\\[\]]/g, '\\$&') - -export function source(root, markdown, changed, edge) { - const view = new EditorView({ - doc: markdown, - extensions: [ - basicSetup, - language({ codeLanguages: languages }), - EditorView.lineWrapping, - theme, - search(), - // Tab indents; it never leaves the editor. - keymap.of([indentWithTab]), - // Past their history, undo and redo reach the page. - Prec.low( - keymap.of([ - { key: 'Mod-z', run: (view) => undoDepth(view.state) === 0 && edge('undo') }, - { key: 'Mod-Shift-z', run: (view) => redoDepth(view.state) === 0 && edge('redo') }, - { key: 'Mod-y', run: (view) => redoDepth(view.state) === 0 && edge('redo') }, - ]), - ), - EditorView.updateListener.of((update) => { - if (update.docChanged) changed(() => update.state.doc.toString()) - }), - ], - parent: root, - }) - // ATX and setext headings from the parse, with their starts. - const headings = () => { - const found = [] - const tree = ensureSyntaxTree(view.state, view.state.doc.length, 200) - tree?.iterate({ - enter: ({ name, from, to }) => { - const level = /^(?:ATX|Setext)Heading(\d)$/.exec(name)?.[1] - if (!level) return - const head = view.state.doc.sliceString(from, to).split('\n')[0] - const title = head.replace(/^#{1,6}[ \t]*/, '').replace(/[ \t]+#+[ \t]*$/, '') - found.push({ level: Number(level), title: title.trim(), from }) - return false - }, - }) - return found - } - const reveal = (from) => { - view.dispatch({ selection: { anchor: from }, scrollIntoView: true }) - view.focus() - } - return { - // Images at the caret, as Markdown. - insert: (images) => { - const text = images.map(({ src, alt }) => `![${escape(alt)}](${src})`).join('\n') - view.dispatch(view.state.replaceSelection(text)) - view.focus() - return true - }, - find: textFinder(view), - outline: () => headings().map(({ level, title }) => ({ level, title })), - heading: (key) => { - const found = headings()[key] - if (found) reveal(found.from) - return Boolean(found) - }, - line: (n, query) => { - if (n < 1 || n > view.state.doc.lines) return false - const line = view.state.doc.line(n) - const column = line.text.toLowerCase().indexOf(query.toLowerCase()) - reveal(line.from + Math.max(0, column)) - return true - }, - close: () => view.destroy(), - center: () => { - const { head } = view.state.selection.main - view.dispatch({ effects: EditorView.scrollIntoView(head, { y: 'center' }) }) - }, - where: () => { - const { head } = view.state.selection.main - const line = view.state.doc.lineAt(head) - return { line: line.number, column: head - line.from } - }, - at: (n, column) => { - if (n < 1 || n > view.state.doc.lines) return false - const line = view.state.doc.line(n) - reveal(line.from + Math.min(column, line.length)) - return true - }, - text: () => view.state.doc.toString(), - // The text once every undone step is redone. - redone: () => { - let state = view.state - if (!redoDepth(state)) return null - while (redoDepth(state)) redo({ state, dispatch: (tr) => (state = tr.state) }) - return state.doc.toString() - }, - focus: () => view.focus(), - } -} diff --git a/apps/ely-mark/web/src/style.js b/apps/ely-mark/web/src/style.js deleted file mode 100644 index 3f4a3c3..0000000 --- a/apps/ely-mark/web/src/style.js +++ /dev/null @@ -1,53 +0,0 @@ -// The document's own markers, so blocks write back unchanged. -import { unified } from 'unified' -import remarkParse from 'remark-parse' -import remarkGfm from 'remark-gfm' -import { visit } from 'unist-util-visit' - -/** Writing options most of `text` already uses; tables' for remark-gfm. */ -export function style(text) { - const votes = {} - const vote = (key, value) => { - votes[key] ??= new Map() - votes[key].set(value, (votes[key].get(value) ?? 0) + 1) - } - visit(unified().use(remarkParse).use(remarkGfm).parse(text), (node) => { - const at = node.position?.start.offset - if (at === undefined) return - const head = text.slice(at, at + 12) - switch (node.type) { - case 'listItem': { - const ordered = /^\d+([.)])/.exec(head) - if (ordered) vote('bulletOrdered', ordered[1]) - else if (/^[*+-]/.test(head)) vote('bullet', head[0]) - break - } - case 'emphasis': - case 'strong': - vote(node.type, head[0]) - break - case 'code': - if (/^[`~]/.test(head)) vote('fence', head[0]) - break - case 'thematicBreak': - vote('rule', head[0]) - break - case 'heading': - if (node.depth <= 2) vote('setext', head[0] !== '#') - break - case 'table': { - // Row nodes leave out quote and list prefixes. - const rows = node.children.map((row) => text.slice(row.position.start.offset, row.position.end.offset).trim()) - const padded = /\| /.test(rows[0]) - const cells = rows.flatMap((row) => row.replace(/^\||\|$/g, '').split('|')) - const inner = (cell) => (padded ? cell.replace(/^ | $/g, '') : cell) - vote('tableCellPadding', padded) - // Aligned cells carry spaces beyond the padding. - vote('tablePipeAlign', cells.some((cell) => /^\s|\s$/.test(inner(cell)))) - break - } - } - }) - const most = (key) => [...votes[key].entries()].sort((a, b) => b[1] - a[1])[0][0] - return Object.fromEntries(Object.keys(votes).map((key) => [key, most(key)])) -} diff --git a/apps/ely-mark/web/src/tables.js b/apps/ely-mark/web/src/tables.js deleted file mode 100644 index 0563a67..0000000 --- a/apps/ely-mark/web/src/tables.js +++ /dev/null @@ -1,35 +0,0 @@ -// Tables compare by alignment and cell source, not spacing. -import { unified } from 'unified' -import remarkParse from 'remark-parse' -import remarkGfm from 'remark-gfm' -import { SKIP, visit } from 'unist-util-visit' - -const reader = unified().use(remarkParse).use(remarkGfm) - -/** A cell's own source, without its padding. */ -function cell(text, node) { - const kids = node.children - return kids.length ? text.slice(kids[0].position.start.offset, kids.at(-1).position.end.offset) : '' -} - -/** `text` with each table's spacing made canonical. */ -export function canon(text) { - const tables = [] - visit(reader.parse(text), 'table', (node) => { - tables.push(node) - return SKIP - }) - let out = text - // Later tables first, so earlier offsets hold. - for (const table of tables.reverse()) { - const width = table.children[0].children.length - // GFM fills a short row with empty cells. - const rows = table.children.map((row) => { - const cells = row.children.map((node) => cell(text, node)) - return [...cells, ...Array(Math.max(0, width - cells.length)).fill('')].join('|') - }) - const shape = [table.align.join(','), ...rows].join('\n') - out = `${out.slice(0, table.position.start.offset)}\u0000${shape}\u0000${out.slice(table.position.end.offset)}` - } - return out -} diff --git a/apps/ely-mark/web/src/theme.js b/apps/ely-mark/web/src/theme.js deleted file mode 100644 index d9305f2..0000000 --- a/apps/ely-mark/web/src/theme.js +++ /dev/null @@ -1,42 +0,0 @@ -// CodeMirror colors from the page's Ely tokens. -import { HighlightStyle, syntaxHighlighting } from '@codemirror/language' -import { EditorView } from '@codemirror/view' -import { tags } from '@lezer/highlight' - -const token = (name) => `var(--ely-${name})` - -const colors = HighlightStyle.define([ - { tag: [tags.keyword, tags.operatorKeyword, tags.modifier], color: token('keyword') }, - { tag: [tags.string, tags.regexp, tags.special(tags.string)], color: token('string') }, - { tag: [tags.number, tags.bool, tags.null, tags.atom], color: token('number') }, - { tag: [tags.comment, tags.meta], color: token('comment') }, - { - tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], - color: token('function'), - }, - { tag: [tags.typeName, tags.className, tags.namespace], color: token('type') }, - { tag: [tags.tagName, tags.heading], color: token('tag'), fontWeight: 'bold' }, - { tag: [tags.attributeName, tags.propertyName], color: token('attribute') }, - { - tag: [tags.punctuation, tags.bracket, tags.processingInstruction], - color: token('punctuation'), - }, - { tag: [tags.link, tags.url], color: token('link') }, - { tag: tags.emphasis, fontStyle: 'italic' }, - { tag: tags.strong, fontWeight: 'bold' }, - { tag: tags.strikethrough, textDecoration: 'line-through' }, -]) - -const frame = EditorView.theme({ - '&': { color: token('fg'), backgroundColor: 'transparent' }, - '.cm-content': { caretColor: token('fg'), fontFamily: 'var(--ely-mono), monospace' }, - '.cm-cursor': { borderLeftColor: token('fg') }, - '.cm-gutters': { backgroundColor: 'transparent', color: token('fg-subtle'), border: 'none' }, - '.cm-activeLine, .cm-activeLineGutter': { backgroundColor: token('hover') }, - '&.cm-focused .cm-selectionBackground, .cm-selectionBackground': { - backgroundColor: token('selection'), - }, - '&.cm-focused': { outline: 'none' }, -}) - -export const theme = [frame, syntaxHighlighting(colors)] diff --git a/apps/ely-mark/web/src/visual.js b/apps/ely-mark/web/src/visual.js deleted file mode 100644 index ad9a055..0000000 --- a/apps/ely-mark/web/src/visual.js +++ /dev/null @@ -1,339 +0,0 @@ -// Milkdown: visual editing, or read-only preview. -import { - Editor, - defaultValueCtx, - remarkStringifyOptionsCtx, - editorViewCtx, - editorViewOptionsCtx, - parserCtx, - remarkCtx, - rootCtx, -} from '@milkdown/kit/core' -import { commonmark } from '@milkdown/kit/preset/commonmark' -import { gfm } from '@milkdown/kit/preset/gfm' -import { history } from '@milkdown/kit/plugin/history' -import { codeBlockComponent } from '@milkdown/kit/component/code-block' -import { Plugin, TextSelection } from '@milkdown/kit/prose/state' -import { Decoration, DecorationSet } from '@milkdown/kit/prose/view' -import { keymap } from '@milkdown/kit/prose/keymap' -import { liftListItem, sinkListItem } from '@milkdown/kit/prose/schema-list' -import { redo, redoDepth, undoDepth } from '@milkdown/kit/prose/history' -import { $prose, callCommand } from '@milkdown/kit/utils' -import { insertTableCommand, remarkGFMPlugin } from '@milkdown/kit/preset/gfm' -import { - addColumnAfter, - addColumnBefore, - addRowAfter, - addRowBefore, - deleteColumn, - deleteRow, -} from '@milkdown/kit/prose/tables' -import { visit } from 'unist-util-visit' -import { image } from './image.js' -import { math } from './math.js' -import { code } from './code.js' -import { diagrams } from './diagrams.js' -import { faithful, raw } from './preserve.js' -import { docFinder } from './find.js' -import { search } from 'prosemirror-search' -import { style } from './style.js' - -/** An editor over `markdown`; null when blocks cannot map. */ -export async function visual(root, markdown, editable, texts, changed, edge, linked) { - const owner = diagrams() - let write = () => ({ text: markdown, spans: [] }) - // Setting up raw blocks is no edit. - let ready = false - const watch = $prose( - () => - new Plugin({ - view: () => ({ - update: (view, before) => { - if (ready && !view.state.doc.eq(before.doc)) changed(() => write(view.state.doc).text) - }, - }), - }), - ) - // Focus mode dims all blocks but the caret's. - const current = $prose( - () => - new Plugin({ - props: { - decorations: (state) => { - const { $from } = state.selection - if ($from.depth < 1) return null - const start = $from.before(1) - const block = Decoration.node(start, start + $from.node(1).nodeSize, { - class: 'ely-current', - }) - return DecorationSet.create(state.doc, [block]) - }, - }, - }), - ) - // Preview: a click opens. Editor: Cmd (Ctrl elsewhere). - const mac = navigator.platform.startsWith('Mac') - const links = $prose( - () => - new Plugin({ - props: { - handleDOMEvents: { - click: (view, event) => { - const anchor = event.target.closest?.('a') - if (!anchor || !view.dom.contains(anchor)) return false - event.preventDefault() - if (editable && !(mac ? event.metaKey : event.ctrlKey)) return true - // The DOM href is sanitized; the mark is not. - const node = view.state.doc.nodeAt(view.posAtDOM(anchor, 0)) - const mark = node?.marks.find((mark) => mark.type.name === 'link') - if (!mark) throw new Error('ely: a link without its mark') - linked(mark.attrs.href) - return true - }, - }, - }, - }), - ) - // Past their history, undo and redo reach the page. - const ends = $prose(() => - keymap({ - 'Mod-z': (state) => undoDepth(state) === 0 && edge('undo'), - 'Mod-Shift-z': (state) => redoDepth(state) === 0 && edge('redo'), - 'Mod-y': (state) => redoDepth(state) === 0 && edge('redo'), - // Tab moves list items; it never leaves the editor. - Tab: (state, dispatch) => sinkListItem(state.schema.nodes.list_item)(state, dispatch) || true, - 'Shift-Tab': (state, dispatch) => - liftListItem(state.schema.nodes.list_item)(state, dispatch) || true, - }), - ) - const made = Editor.make() - .config((ctx) => { - ctx.set(rootCtx, root) - ctx.set(defaultValueCtx, markdown) - const { tableCellPadding, tablePipeAlign, ...marks } = style(markdown) - ctx.update(remarkStringifyOptionsCtx, (prev) => ({ ...prev, ...marks })) - ctx.set(remarkGFMPlugin.options.key, { tableCellPadding, tablePipeAlign }) - ctx.update(editorViewOptionsCtx, (prev) => ({ ...prev, editable: () => editable })) - }) - .use(commonmark) - .use(gfm) - .use(history) - .use(image) - .use(math) - .use(raw) - .config(code(texts, editable, owner)) - .use(codeBlockComponent) - .use(watch) - .use(current) - .use(ends) - .use(links) - .use($prose(() => search())) - .create() - const editor = await made.catch((error) => { - owner.release() - throw error - }) - const view = editor.action((ctx) => ctx.get(editorViewCtx)) - const close = () => { - editor.destroy() - owner.release() - } - const keep = editor.action((ctx) => faithful(ctx, markdown)) - if (!keep) { - close() - return null - } - write = keep - ready = true - // The text once every undone step is redone. - const redone = () => { - let state = view.state - if (!redoDepth(state)) return null - while (redoDepth(state)) redo(state, (tr) => (state = state.apply(tr))) - return write(state.doc).text - } - // Headings, raw ones too, by the title their source gives. - const [remark, parse] = editor.action((ctx) => [ctx.get(remarkCtx), ctx.get(parserCtx)]) - /** An mdast node's words. */ - const words = (node) => node.value ?? (node.children ?? []).map(words).join('') - // Headings; inside raw blocks, as remark reads their source. - const headings = () => { - const found = [] - view.state.doc.descendants((node, pos) => { - if (node.type.name === 'heading') - found.push({ level: node.attrs.level, title: node.textContent, pos }) - if (node.type.name !== 'ely_raw') return true - visit(remark.parse(node.textContent), 'heading', (heading) => { - const start = (heading.children[0] ?? heading).position.start.offset - found.push({ level: heading.depth, title: words(heading), pos, at: pos + 1 + start }) - }) - return false - }) - return found - } - // `exact`: the caret at `pos`; else in that block. - const reveal = (pos, exact) => { - if (editable) { - const doc = view.state.doc - const place = exact - ? TextSelection.create(doc, pos) - : TextSelection.near(doc.resolve(pos + 1)) - view.dispatch(view.state.tr.setSelection(place).scrollIntoView()) - return view.focus() - } - const { node } = view.domAtPos(pos + 1) - ;(node.nodeType === 1 ? node : node.parentElement).scrollIntoView({ block: 'center' }) - } - /** `node`'s text, and where a character of it sits. */ - const reading = (node, start) => { - const chunks = [] - // Text and line breaks, as `textContent` reads them. - node.descendants((child, at) => { - const words = child.isText - ? child.text - : child.isLeaf - ? child.type.spec.leafText?.(child) - : null - if (words) chunks.push({ text: words.toLowerCase(), at: start + 1 + at }) - }) - const at = (offset) => { - for (const [index, chunk] of chunks.entries()) { - const last = index === chunks.length - 1 - if (offset < chunk.text.length || (last && offset === chunk.text.length)) - return chunk.at + offset - offset -= chunk.text.length - } - return null - } - return { joined: chunks.map((chunk) => chunk.text).join(''), at } - } - // Non-overlapping hits, the same rule as `split`. - const hits = (text, needle) => text.split(needle).length - 1 - /** The block holding file line `n`: its index and start. */ - const blockOf = (n) => { - const { text, spans } = write(view.state.doc) - let offset = 0 - for (let k = 1; k < n && offset >= 0; k++) offset = text.indexOf('\n', offset) + 1 - if (n < 1 || offset < 0) return null - const index = spans.findIndex((span) => span && span[1] >= offset) - if (index < 0) return null - let pos = 0 - for (let k = 0; k < index; k++) pos += view.state.doc.child(k).nodeSize - return { text, spans, offset, index, pos } - } - /** Line `n`: the query's place in its block. */ - const line = (n, query) => { - const block = blockOf(n) - if (!block) return false - const { text, spans, offset, index, pos } = block - const needle = query.toLowerCase() - const node = view.state.doc.child(index) - // Text before and through the line, as shown. - const shown = (end) => { - const source = text.slice(spans[index][0], Math.max(spans[index][0], end)) - return (node.type.name === 'ely_raw' ? source : parse(source).textContent).toLowerCase() - } - const ends = text.indexOf('\n', offset) - const before = shown(offset) - const through = shown(ends < 0 ? text.length : ends) - const { joined, at } = reading(node, pos) - const k = needle ? hits(before, needle) : 0 - let found = -1 - if (needle && hits(through, needle) > k) { - let from = 0 - for (let seen = 0; seen <= k; seen++) { - found = joined.indexOf(needle, from) - if (found < 0) break - from = found + needle.length - } - } - // A hit hidden, as in a link: the line's start. - const place = found >= 0 ? at(found) : at(before.length) - reveal(place ?? pos, place !== null) - return true - } - /** The caret: its block's file line, and how far in. */ - const where = () => { - const { text, spans } = write(view.state.doc) - const { from } = view.state.selection - let [start, pos] = [0, 0] - view.state.doc.forEach((node, offset, index) => { - if (offset <= from && spans[index]) [start, pos] = [spans[index][0], offset] - }) - return { line: text.slice(0, start).split('\n').length, column: from - pos } - } - /** The caret back where `where` read it. */ - const at = (n, column) => { - const block = blockOf(n) - if (!block) return false - const size = view.state.doc.child(block.index).nodeSize - reveal(block.pos + Math.min(Math.max(column, 1), size - 1) - 1, false) - return true - } - // Rows and columns; false when the caret is outside tables. - const tables = { - 'row-above': addRowBefore, - 'row-below': addRowAfter, - 'column-left': addColumnBefore, - 'column-right': addColumnAfter, - 'delete-row': deleteRow, - 'delete-column': deleteColumn, - } - const tablesIn = (doc) => { - const found = [] - doc.descendants((node, pos) => { - if (node.type.name === 'table') found.push({ node, pos }) - }) - return found - } - // Milkdown may leave the caret before the new table. - const newTable = () => { - const old = new Set(tablesIn(view.state.doc).map(({ node }) => node)) - editor.action(callCommand(insertTableCommand.key, { row: 3, col: 3 })) - const made = tablesIn(view.state.doc).find(({ node }) => !old.has(node)) - if (!made) throw new Error('the inserted table is missing') - const cell = TextSelection.near(view.state.doc.resolve(made.pos + 1)) - view.dispatch(view.state.tr.setSelection(cell).scrollIntoView()) - return true - } - const table = (op) => { - if (!editable) return false - const done = op === 'insert' ? newTable() : tables[op](view.state, view.dispatch) - view.focus() - return done - } - /** Images at the caret: `{ src, alt }` each. */ - const insert = (images) => { - if (!editable) return false - const image = view.state.schema.nodes.image - let tr = view.state.tr - for (const { src, alt } of images) - tr = tr.replaceSelectionWith(image.create({ src, alt, title: '' }), false) - view.dispatch(tr.scrollIntoView()) - view.focus() - return true - } - return { - insert, - table, - find: docFinder(view), - close, - text: () => write(view.state.doc).text, - redone, - focus: () => editable && view.focus(), - outline: () => headings().map(({ level, title }) => ({ level, title })), - heading: (key) => { - const found = headings()[key] - if (found) reveal(found.at ?? found.pos, found.at !== undefined) - return Boolean(found) - }, - line, - where, - at, - // The caret's line to the middle of the view. - center: () => { - const { top } = view.coordsAtPos(view.state.selection.head) - window.scrollBy(0, top - window.innerHeight / 2) - }, - } -} diff --git a/crates/ely-platform/src/apps.rs b/crates/ely-platform/src/apps.rs index 0293ce9..46d44f4 100644 --- a/crates/ely-platform/src/apps.rs +++ b/crates/ely-platform/src/apps.rs @@ -83,34 +83,11 @@ pub fn open(app: AppId, files: &[PathBuf]) -> io::Result<()> { } let mut command = Command::new(&found); command.args(files); - let child = fresh_signals(&mut command).spawn()?; + let child = command.spawn()?; log::info!("apps: started {} as {}", app.name(), child.id()); Ok(()) } -/// The started app takes signals as from a shell. -#[cfg(unix)] -fn fresh_signals(command: &mut Command) -> &mut Command { - use std::os::unix::process::CommandExt as _; - // Chromium's ignored or blocked signals would carry over. - unsafe { - command.pre_exec(|| { - for signal in [libc::SIGTERM, libc::SIGINT, libc::SIGHUP, libc::SIGQUIT] { - libc::signal(signal, libc::SIG_DFL); - } - let mut none: libc::sigset_t = std::mem::zeroed(); - libc::sigemptyset(&mut none); - libc::pthread_sigmask(libc::SIG_SETMASK, &none, std::ptr::null_mut()); - Ok(()) - }) - } -} - -#[cfg(not(unix))] -fn fresh_signals(command: &mut Command) -> &mut Command { - command -} - /// Opens `path` in the default app; a refusal says why. pub fn open_default(path: &Path) -> io::Result<()> { let mut command = if cfg!(target_os = "macos") { @@ -170,28 +147,3 @@ mod tests { )))); } } - -#[cfg(all(test, unix))] -mod signals { - use std::os::unix::process::ExitStatusExt as _; - - /// A starter that ignores and blocks SIGTERM, as Chromium may. - #[test] - fn a_started_app_still_stops_on_sigterm() { - unsafe { - libc::signal(libc::SIGTERM, libc::SIG_IGN); - let mut term: libc::sigset_t = std::mem::zeroed(); - libc::sigemptyset(&mut term); - libc::sigaddset(&mut term, libc::SIGTERM); - libc::pthread_sigmask(libc::SIG_BLOCK, &term, std::ptr::null_mut()); - } - let mut command = std::process::Command::new("/bin/sleep"); - command.arg("30"); - let mut child = super::fresh_signals(&mut command).spawn().unwrap(); - std::thread::sleep(std::time::Duration::from_millis(200)); - unsafe { libc::kill(child.id() as i32, libc::SIGTERM) }; - let status = child.wait().unwrap(); - unsafe { libc::signal(libc::SIGTERM, libc::SIG_DFL) }; - assert_eq!(status.signal(), Some(libc::SIGTERM), "{status:?}"); - } -} diff --git a/crates/ely-platform/src/storage.rs b/crates/ely-platform/src/storage.rs index 8a07264..b754b84 100644 --- a/crates/ely-platform/src/storage.rs +++ b/crates/ely-platform/src/storage.rs @@ -11,18 +11,15 @@ use crate::AppId; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Kind { Log, - /// The web content cache (ElyMark). - Cache, Recovery, } impl Kind { - pub const ALL: [Kind; 3] = [Kind::Log, Kind::Cache, Kind::Recovery]; + pub const ALL: [Kind; 2] = [Kind::Log, Kind::Recovery]; fn place(self) -> &'static str { match self { Kind::Log => "ely.log", - Kind::Cache => "web", Kind::Recovery => "recovery", } } @@ -152,15 +149,14 @@ mod tests { #[test] fn marked_kinds_go_at_start_and_others_stay() { let dir = tempfile::tempdir().unwrap(); - std::fs::create_dir_all(dir.path().join("web/Cache")).unwrap(); - std::fs::write(dir.path().join("web/Cache/blob"), vec![0u8; 4096]).unwrap(); std::fs::create_dir_all(dir.path().join("recovery/a")).unwrap(); - std::fs::write(dir.path().join("recovery/a/point"), b"text").unwrap(); - assert_eq!(size(&dir.path().join("web")).unwrap(), 4096); - std::fs::write(dir.path().join(Kind::Cache.mark()), b"").unwrap(); + std::fs::write(dir.path().join("recovery/a/point"), vec![0u8; 4096]).unwrap(); + std::fs::write(dir.path().join("ely.log"), b"line").unwrap(); + assert_eq!(size(&dir.path().join("recovery")).unwrap(), 4096); + std::fs::write(dir.path().join(Kind::Recovery.mark()), b"").unwrap(); clear_marked_in(dir.path()).unwrap(); - assert!(!dir.path().join("web").exists(), "the cache went"); - assert!(dir.path().join("recovery/a/point").exists(), "unmarked kinds stay"); - assert!(!dir.path().join(Kind::Cache.mark()).exists(), "the mark is spent"); + assert!(!dir.path().join("recovery").exists(), "the copies went"); + assert!(dir.path().join("ely.log").exists(), "the log stays"); + assert!(!dir.path().join(Kind::Recovery.mark()).exists(), "the mark is spent"); } } diff --git a/crates/ely-ui/src/i18n/settings.rs b/crates/ely-ui/src/i18n/settings.rs index 0b030e4..3bc3aab 100644 --- a/crates/ely-ui/src/i18n/settings.rs +++ b/crates/ely-ui/src/i18n/settings.rs @@ -32,7 +32,6 @@ pub(super) const EN: &[(&str, &str)] = &[ ("settings.git.bad", "{path} is not Git 2.40 or later: {error}"), ("settings.storage", "Storage"), ("settings.storage.log", "This app's log"), - ("settings.storage.cache", "Web content cache"), ("settings.storage.recovery", "Recovery copies"), ("settings.storage.clear", "Clear"), ("settings.storage.clear_later", "Clear at next start"), @@ -92,7 +91,6 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[ ("settings.git.bad", "{path} 不是 2.40 或更新的 Git:{error}"), ("settings.storage", "存储"), ("settings.storage.log", "本应用的日志"), - ("settings.storage.cache", "网页内容缓存"), ("settings.storage.recovery", "恢复副本"), ("settings.storage.clear", "清除"), ("settings.storage.clear_later", "下次启动时清除"), diff --git a/crates/ely-ui/src/settings/storage.rs b/crates/ely-ui/src/settings/storage.rs index 7f710f0..82179ca 100644 --- a/crates/ely-ui/src/settings/storage.rs +++ b/crates/ely-ui/src/settings/storage.rs @@ -74,9 +74,7 @@ impl SettingsView { None => {} Some(Err(error)) => section = section.row(Banner::new("storage-unread", Severity::Danger, error.clone())), Some(Ok(usage)) => { - // Only ElyMark keeps web content. - let shown = usage.iter().filter(|(kind, ..)| *kind != Kind::Cache || self.app == ely_platform::AppId::Mark); - for (kind, bytes, clearing) in shown { + for (kind, bytes, clearing) in usage { section = section.row(row(*kind, *bytes, *clearing, this.clone(), cx)); } } @@ -188,7 +186,6 @@ impl SettingsView { fn name(kind: Kind, cx: &App) -> SharedString { match kind { Kind::Log => text(cx, "settings.storage.log", &[]), - Kind::Cache => text(cx, "settings.storage.cache", &[]), Kind::Recovery => text(cx, "settings.storage.recovery", &[]), } } diff --git a/crates/ely-web-content/Cargo.toml b/crates/ely-web-content/Cargo.toml deleted file mode 100644 index 1aaa46b..0000000 --- a/crates/ely-web-content/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "ely-web-content" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -publish.workspace = true - -[dependencies] -anyhow.workspace = true -cef.workspace = true -ely-gpui-component.workspace = true -ely-ui.workspace = true -futures.workspace = true -gpui.workspace = true -image.workspace = true -libc.workspace = true -log.workspace = true -serde_json.workspace = true - -[target.'cfg(target_os = "macos")'.dependencies] -objc2.workspace = true - -[target.'cfg(windows)'.dependencies] -windows-sys.workspace = true - -[lints] -workspace = true - -[dev-dependencies] -ely-platform.workspace = true -tempfile.workspace = true diff --git a/crates/ely-web-content/examples/webprobe.rs b/crates/ely-web-content/examples/webprobe.rs deleted file mode 100644 index 23051ee..0000000 --- a/crates/ely-web-content/examples/webprobe.rs +++ /dev/null @@ -1,317 +0,0 @@ -//! A live WebView: input, fence, failures, release, paint. - -use std::{ - net::{TcpListener, UdpSocket}, - path::{Path, PathBuf}, - time::Duration, -}; - -use ely_platform::AppId; -use ely_web_content::{Event, WebView}; -use gpui::{ - App, AppContext as _, AsyncWindowContext, Bounds, Context, ElementInputHandler, Entity, - Focusable as _, InputHandler as _, IntoElement, KeyUpEvent, Keystroke, ParentElement as _, - PlatformInput, Render, Styled as _, WeakEntity, Window, div, -}; - -/// How many views open and close to show their release. -const CHURN: usize = 20; -/// Views dropped before the pump makes their browsers. -const UNMADE: usize = 5; -/// Views dropped within 3 ms steps of opening. -const EARLY: u64 = 10; - -struct Probe { - view: Entity, - /// Pages beside the main one, each with its own test. - others: Vec>, - /// The ratio before zoom, to compare after. - before: Option, - /// Fenced off: nothing may reach it. - server: TcpListener, - /// WebRTC's way out, by UDP; nothing may reach it either. - udp: UdpSocket, - churned: usize, - crashed: bool, - fixtures: PathBuf, - work: PathBuf, -} - -fn typed(text: &str) -> Keystroke { - Keystroke { - key_char: Some(text.into()), - ..Keystroke::parse(text).expect("a keystroke") - } -} - -/// Waits, then runs `then` with the probe and its window. -fn later( - this: WeakEntity, - ms: u64, - window: &mut Window, - cx: &mut Context, - then: impl FnOnce(&mut Probe, &mut Window, &mut Context) + 'static, -) { - cx.spawn_in(window, async move |_, cx: &mut AsyncWindowContext| { - cx.background_executor() - .timer(Duration::from_millis(ms)) - .await; - let _ = this.update_in(cx, then); - }) - .detach(); -} - -impl Probe { - fn main_heard( - &mut self, - _: &Entity, - event: &Event, - window: &mut Window, - cx: &mut Context, - ) { - match event { - // Held so the gate sees the page drawn again. - Event::Loaded if self.crashed => { - println!("probe: recovered"); - let this = cx.weak_entity(); - later(this, 4000, window, cx, |_, _, cx| cx.quit()); - } - Event::Loaded => { - let port = self.server.local_addr().expect("an address").port(); - let image = format!( - "new Image().src = 'http://127.0.0.1:{port}/pixel?x=1'; \ - try {{ new WebSocket('ws://127.0.0.1:{port}/socket'); }} catch (e) {{}} \ - try {{ const peer = new RTCPeerConnection({{ iceServers: \ - [{{ urls: 'stun:127.0.0.1:{udp}' }}] }}); peer.createDataChannel('x'); \ - peer.createOffer().then((offer) => peer.setLocalDescription(offer)); \ - }} catch (e) {{}}", - udp = self.udp.local_addr().expect("an address").port() - ); - self.view.read(cx).run_js(&image); - self.view.read(cx).run_js("report('loaded')"); - } - Event::Query(said, reply) => { - reply.ok("ok"); - let read: serde_json::Value = serde_json::from_str(said).expect("json"); - self.step(&read, window, cx); - } - Event::Leave(url) => println!("probe: leave {url}"), - Event::Printed(path, ok) => println!("probe: printed {} {ok}", path.display()), - // Recovery comes from the shown Reload button. - Event::Failed(why) => { - println!("probe: main failed"); - eprintln!("probe: because {why}"); - self.crashed = true; - } - } - } - - fn step(&mut self, read: &serde_json::Value, window: &mut Window, cx: &mut Context) { - let this = cx.weak_entity(); - match read["step"].as_str() { - Some("loaded") => { - self.before = read["ratio"].as_f64(); - println!("probe: outside={}", read["outside"]); - self.edit(window, cx); - } - Some("edited") => { - println!("probe: value={}", read["value"].as_str().unwrap_or("?")); - println!( - "probe: focused={}", - read["focused"].as_str().unwrap_or("none") - ); - println!("probe: fg={}", read["fg"].as_str().unwrap_or("")); - let reached = self.server.accept().is_ok(); - println!( - "probe: network={}", - if reached { "reached" } else { "refused" } - ); - let heard = self.udp.recv(&mut [0u8; 2048]).is_ok(); - println!("probe: udp={}", if heard { "reached" } else { "silent" }); - println!("probe: rtc={}", read["rtc"].as_str().unwrap_or("?")); - println!( - "probe: network={}", - if reached { "reached" } else { "refused" } - ); - self.view.read(cx).set_zoom(1.25); - later(this, 800, window, cx, |probe, _, cx| { - probe.view.read(cx).run_js("report('zoomed')"); - }); - } - Some("zoomed") => { - let zoom = read["ratio"].as_f64().unwrap_or(0.0) / self.before.unwrap_or(1.0); - println!("probe: zoom={zoom:.2}"); - // A page cannot leave its bundled folder. - self.view - .read(cx) - .run_js("location.href = 'https://example.com/'"); - later(this, 800, window, cx, |probe, _, cx| { - probe.view.read(cx).run_js("report('stayed')"); - }); - } - Some("stayed") => { - println!("probe: stayed"); - self.churn(window, cx); - } - other => println!("probe: unknown step {other:?}"), - } - } - - /// Compose 你好, commit it, then type A. - fn edit(&mut self, window: &mut Window, cx: &mut Context) { - let view = self.view.clone(); - // Input runs outside this update; GPUI redraws during it. - cx.spawn_in(window, async move |_, cx| { - let typed_in = cx.update(|window, cx| { - let focus = view.read(cx).focus_handle(cx); - window.focus(&focus, cx); - let mut ime = ElementInputHandler::new(Bounds::default(), view.clone()); - ime.replace_and_mark_text_in_range(None, "ni", Some(2..2), window, cx); - ime.replace_and_mark_text_in_range(Some(0..2), "你好", None, window, cx); - ime.replace_text_in_range(None, "你好", window, cx); - let stroke = typed("a"); - window.dispatch_keystroke(stroke.clone(), cx); - window.dispatch_event(PlatformInput::KeyUp(KeyUpEvent { keystroke: stroke }), cx); - }); - if typed_in.is_err() { - return; - } - cx.background_executor() - .timer(Duration::from_millis(500)) - .await; - view.update(cx, |view, _| view.run_js("report('edited')")); - }) - .detach(); - } - - /// Opens and drops pages; each must free its memory. - fn churn(&mut self, window: &mut Window, cx: &mut Context) { - let page = self.fixtures.join("probe.html"); - if self.churned == CHURN { - println!("probe: closed {CHURN}"); - for _ in 0..UNMADE { - drop(WebView::open(&page, &self.fixtures, window, cx).expect("a view")); - } - for step in 0..EARLY { - let view = WebView::open(&page, &self.fixtures, window, cx).expect("a view"); - self.others.push(view.clone()); - later(cx.weak_entity(), step * 3, window, cx, move |probe, _, _| { - probe.others.retain(|other| *other != view); - }); - } - println!("probe: dropped {UNMADE} unmade, {EARLY} early"); - let this = cx.weak_entity(); - later(this, 2000, window, cx, |_, _, _| { - // The log marks what freed before the hold. - log::info!("probe: hold"); - println!("probe: hold"); - }); - return; - } - self.churned += 1; - let view = WebView::open(&page, &self.fixtures, window, cx).expect("a view"); - cx.subscribe_in(&view, window, |probe, view, event, window, cx| { - if matches!(event, Event::Loaded) { - probe.others.retain(|other| other != view); - probe.churn(window, cx); - } - }) - .detach(); - self.others.push(view); - } - - /// A page named like a URL, one gone, one inert. - fn edges(&mut self, window: &mut Window, cx: &mut Context) { - let page = self.fixtures.join("inert.html"); - let inert = WebView::open_inert(&page, &self.fixtures, window, cx).expect("a view"); - cx.subscribe_in(&inert, window, |_, _, event, _, _| match event { - Event::Loaded => println!("probe: inert loaded"), - Event::Query(_, reply) => { - println!("probe: inert ran"); - reply.ok(""); - } - _ => {} - }) - .detach(); - // The same page with scripts: it must run. - let scripted = WebView::open(&page, &self.fixtures, window, cx).expect("a view"); - cx.subscribe_in(&scripted, window, |_, _, event, window, cx| { - if let Event::Query(_, reply) = event { - println!("probe: scripted ran"); - reply.ok(""); - later(cx.weak_entity(), 2000, window, cx, |_, _, _| println!("probe: inert checked")); - } - }) - .detach(); - self.others.extend([inert, scripted]); - let hash = self.work.join("hash"); - std::fs::create_dir_all(&hash).expect("makes"); - std::fs::copy( - self.fixtures.join("probe.html"), - hash.join("name#part.html"), - ) - .expect("copies"); - let named = WebView::open(&hash.join("name#part.html"), &hash, window, cx).expect("a view"); - cx.subscribe_in(&named, window, |_, _, event, _, _| match event { - Event::Loaded => println!("probe: hash loaded"), - Event::Failed(_) => println!("probe: hash failed"), - _ => {} - }) - .detach(); - let gone = self.work.join("gone"); - std::fs::create_dir_all(&gone).expect("makes"); - let page = gone.join("page.html"); - std::fs::copy(self.fixtures.join("probe.html"), &page).expect("copies"); - let missing = WebView::open(&page, &gone, window, cx).expect("a view"); - std::fs::remove_file(&page).expect("removes"); - cx.subscribe_in(&missing, window, |_, _, event, _, _| match event { - Event::Loaded => println!("probe: missing loaded"), - Event::Failed(_) => println!("probe: missing failed"), - _ => {} - }) - .detach(); - self.others.extend([named, missing]); - } -} - -impl Render for Probe { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - div().size_full().child(self.view.clone()) - } -} - -fn open(app: AppId, window: &mut Window, cx: &mut App) -> Entity { - let work = tempfile::tempdir().expect("a temp dir").keep(); - ely_web_content::start(&work, cx).expect("CEF starts"); - let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/web"); - let fixtures = std::fs::canonicalize(fixtures).expect("fixtures"); - let view = WebView::open(&fixtures.join("probe.html"), &fixtures, window, cx).expect("a view"); - let server = TcpListener::bind("127.0.0.1:0").expect("a port"); - server.set_nonblocking(true).expect("nonblocking"); - let udp = UdpSocket::bind("127.0.0.1:0").expect("a port"); - udp.set_nonblocking(true).expect("nonblocking"); - let probe = cx.new(|cx| { - cx.subscribe_in(&view, window, Probe::main_heard).detach(); - let mut probe = Probe { - view, - others: Vec::new(), - before: None, - server, - udp, - churned: 0, - crashed: false, - fixtures, - work, - }; - probe.edges(window, cx); - probe - }); - ely_ui::root(app, Some(probe.into()), window, cx) -} - -fn main() { - if let Some(code) = ely_web_content::subprocess() { - std::process::exit(code); - } - ely_ui::run(AppId::Mark, open); -} diff --git a/crates/ely-web-content/src/bin/ely-web-helper.rs b/crates/ely-web-content/src/bin/ely-web-helper.rs deleted file mode 100644 index 4084f41..0000000 --- a/crates/ely-web-content/src/bin/ely-web-helper.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! CEF's helper processes inside an app bundle. - -fn main() { - let Some(code) = ely_web_content::subprocess() else { - eprintln!("ely-web-helper: started without --type; CEF starts it"); - std::process::exit(2); - }; - std::process::exit(code); -} diff --git a/crates/ely-web-content/src/edit.rs b/crates/ely-web-content/src/edit.rs deleted file mode 100644 index a9b5eeb..0000000 --- a/crates/ely-web-content/src/edit.rs +++ /dev/null @@ -1,92 +0,0 @@ -//! AppKit's Command edit keys; off-screen Blink never gets them. - -use cef::*; - -const SHIFT: u32 = 1 << 1; -const CONTROL: u32 = 1 << 2; -const ALT: u32 = 1 << 3; -const COMMAND: u32 = 1 << 7; - -/// What AppKit's standard bindings do, as page script. -pub(crate) fn script(windows_key: i32, modifiers: u32) -> Option { - if modifiers & COMMAND == 0 || modifiers & (CONTROL | ALT) != 0 { - return None; - } - let alter = if modifiers & SHIFT != 0 { - "extend" - } else { - "move" - }; - let (direction, unit) = match windows_key { - 0x25 => ("backward", "lineboundary"), - 0x27 => ("forward", "lineboundary"), - 0x26 => ("backward", "documentboundary"), - 0x28 => ("forward", "documentboundary"), - // Delete to the line's start or end. - 0x08 | 0x2E if modifiers & SHIFT == 0 => { - let (direction, command) = match windows_key { - 0x08 => ("backward", "delete"), - _ => ("forward", "forwardDelete"), - }; - return Some(format!( - "{{const s=getSelection();if(s.isCollapsed)s.modify('extend','{direction}','lineboundary');document.execCommand('{command}')}}" - )); - } - _ => return None, - }; - Some(format!( - "{{const s=getSelection();s.modify('{alter}','{direction}','{unit}');s.focusNode?.parentElement?.scrollIntoView({{block:'nearest'}})}}" - )) -} - -wrap_keyboard_handler! { - pub(crate) struct Keys; - - impl KeyboardHandler { - fn on_key_event( - &self, - browser: Option<&mut Browser>, - event: Option<&KeyEvent>, - _os_event: *mut u8, - ) -> ::std::os::raw::c_int { - let (Some(browser), Some(event)) = (browser, event) else { return 0 }; - let down = matches!(event.type_, KeyEventType::RAWKEYDOWN | KeyEventType::KEYDOWN); - if !down || event.focus_on_editable_field == 0 { - return 0; - } - let Some(script) = script(event.windows_key_code, event.modifiers) else { return 0 }; - let Some(frame) = browser.focused_frame() else { return 0 }; - log::debug!("web: AppKit edit key {}", event.windows_key_code); - frame.execute_java_script(Some(&CefString::from(script.as_str())), None, 0); - 1 - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn command_keys_only() { - assert!( - script(0x28, COMMAND) - .unwrap() - .contains("'move','forward','documentboundary'") - ); - assert!( - script(0x25, COMMAND | SHIFT) - .unwrap() - .contains("'extend','backward','lineboundary'") - ); - assert!( - script(0x08, COMMAND) - .unwrap() - .contains("execCommand('delete')") - ); - assert!(script(0x2E, COMMAND).unwrap().contains("forwardDelete")); - assert_eq!(script(0x28, 0), None); - assert_eq!(script(0x28, COMMAND | ALT), None); - assert_eq!(script(0x41, COMMAND), None); - } -} diff --git a/crates/ely-web-content/src/engine.rs b/crates/ely-web-content/src/engine.rs deleted file mode 100644 index fdbd106..0000000 --- a/crates/ely-web-content/src/engine.rs +++ /dev/null @@ -1,356 +0,0 @@ -//! CEF itself: the framework, its processes, its message pump. - -use std::{ - path::{Path, PathBuf}, - sync::{Mutex, OnceLock}, - time::Duration, -}; - -use anyhow::Result; -use cef::{args::Args, *}; -use futures::{FutureExt as _, StreamExt as _, channel::mpsc}; -use gpui::{App, AsyncApp, Global, WeakEntity}; - -use crate::{handlers, surface::Shared, view::WebView}; - -/// The framework beside the app, else the fetched one. -#[cfg(target_os = "macos")] -fn framework_dir() -> Result { - use anyhow::Context as _; - const NAME: &str = "Chromium Embedded Framework.framework"; - let exe = std::env::current_exe()?; - let mut places: Vec = Vec::new(); - if let Some(dir) = std::env::var_os("ELY_CEF") { - places.push(PathBuf::from(dir).join(NAME)); - } - if let Some(contents) = exe.parent().and_then(Path::parent) { - places.push(contents.join("Frameworks").join(NAME)); - // A helper sits in Frameworks/ Helper.app/Contents/MacOS. - if let Some(frameworks) = contents.parent().and_then(Path::parent) { - places.push(frameworks.join(NAME)); - } - } - if let Some(built) = option_env!("CEF_PATH") { - places.push(Path::new(built).join(NAME)); - } - places - .into_iter() - .find(|place| place.is_dir()) - .with_context(|| format!("no {NAME} beside {} or in engines/cef", exe.display())) -} - -static LOADED: OnceLock> = OnceLock::new(); - -/// Loads CEF once and sets its API version. -fn load() -> Result { - LOADED - .get_or_init(|| { - let dir = open()?; - // Versioned calls exit the process until this runs. - let _ = api_hash(sys::CEF_API_VERSION_LAST, 0); - log::info!("web: CEF loaded from {}", dir.display()); - Ok(dir) - }) - .clone() - .map_err(anyhow::Error::msg) -} - -/// Loads the framework; its folder. -#[cfg(target_os = "macos")] -fn open() -> std::result::Result { - let dir = framework_dir().map_err(|error| format!("{error:#}"))?; - let library = dir.join("Chromium Embedded Framework"); - let name = std::ffi::CString::new(library.as_os_str().as_encoded_bytes()).map_err(|error| error.to_string())?; - if unsafe { load_library(Some(&*name.as_ptr().cast())) } != 1 { - return Err(format!("CEF did not load from {}", library.display())); - } - Ok(dir) -} - -/// The folder of the libcef.dll this process linked. -#[cfg(windows)] -fn open() -> std::result::Result { - use std::os::windows::ffi::OsStringExt as _; - use windows_sys::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW}; - let name: Vec = "libcef.dll".encode_utf16().chain([0]).collect(); - let module = unsafe { GetModuleHandleW(name.as_ptr()) }; - if module.is_null() { - return Err(format!("libcef.dll is not loaded: {}", std::io::Error::last_os_error())); - } - let mut path = vec![0u16; 32_768]; - let len = unsafe { GetModuleFileNameW(module, path.as_mut_ptr(), path.len() as u32) } as usize; - if len == 0 || len == path.len() { - return Err(format!("libcef.dll has no path: {}", std::io::Error::last_os_error())); - } - let file = PathBuf::from(std::ffi::OsString::from_wide(&path[..len])); - file.parent().map(Path::to_path_buf).ok_or_else(|| format!("{} has no folder", file.display())) -} - -/// The folder of the libcef.so this process linked. -#[cfg(target_os = "linux")] -fn open() -> std::result::Result { - use std::os::unix::ffi::OsStrExt as _; - let mut info: libc::Dl_info = unsafe { std::mem::zeroed() }; - let entry = sys::cef_api_hash as *const libc::c_void; - if unsafe { libc::dladdr(entry, &mut info) } == 0 || info.dli_fname.is_null() { - return Err("libcef.so is not loaded".into()); - } - let name = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) }; - let file = PathBuf::from(std::ffi::OsStr::from_bytes(name.to_bytes())); - file.parent().map(Path::to_path_buf).ok_or_else(|| format!("{} has no folder", file.display())) -} - -/// In a CEF helper process, runs it to its end. -pub fn subprocess() -> Option { - // CEF's own parser needs the framework; plain args do not. - if !std::env::args().any(|arg| arg.starts_with("--type=")) { - return None; - } - if let Err(error) = load() { - eprintln!("web: {error:#}"); - return Some(1); - } - let args = Args::new(); - let mut app = handlers::app(); - let code = execute_process( - Some(args.as_main_args()), - Some(&mut app), - std::ptr::null_mut(), - ); - Some(code) -} - -/// The longest the pump waits between turns, as in cefclient. -const FALLBACK: Duration = Duration::from_millis(1000 / 30); - -/// Pump work CEF asked for, from any thread. -static WAKE: Mutex>> = Mutex::new(None); - -/// CEF wants its loop run within `delay_ms`. -pub(crate) fn schedule(delay_ms: i64) { - if let Some(wake) = WAKE.lock().expect("the pump lock").as_ref() { - let _ = wake.unbounded_send(delay_ms); - } -} - -/// The live views the pump hands frames to. -#[derive(Default)] -pub(crate) struct Engine { - pub views: Vec>, -} - -impl Global for Engine {} - -thread_local! { - /// CEF calls asked inside GPUI's updates, run after them. - static LATER: std::cell::RefCell>> = Default::default(); -} - -/// Runs `work` on the pump's next turn. -pub(crate) fn later(work: impl FnOnce() + 'static) { - LATER.with_borrow_mut(|queue| queue.push(Box::new(work))); - schedule(0); -} - -/// In asked order; work may ask for more. -fn run_later() { - loop { - let batch = LATER.with_borrow_mut(std::mem::take); - if batch.is_empty() { - return; - } - for work in batch { - work(); - } - } -} - -/// Browsers asked for, made on the pump's next turn. -static PENDING: Mutex> = Mutex::new(Vec::new()); - -/// Why CEF could not start; every view is told. -static FAILED: Mutex> = Mutex::new(None); - -pub(crate) fn failed() -> Option { - FAILED.lock().expect("the start lock").clone() -} - -/// Queues a browser for `surface` at `url`. -pub(crate) fn ask_browser(surface: Shared, url: String) { - PENDING - .lock() - .expect("the pending lock") - .push((surface, url)); - schedule(0); -} - -/// Outside GPUI's updates: CEF may spin the run loop. -fn make_browsers() { - let pending = std::mem::take(&mut *PENDING.lock().expect("the pending lock")); - for (surface, url) in pending { - if surface.lock().expect("the surface lock").closed { - continue; - } - // Off-screen pages work only in CEF's Alloy style. - let info = WindowInfo { - windowless_rendering_enabled: true as _, - runtime_style: RuntimeStyle::ALLOY, - ..Default::default() - }; - let scripts = match surface.lock().expect("the surface lock").scripts { - true => State::ENABLED, - false => State::DISABLED, - }; - let settings = BrowserSettings { - windowless_frame_rate: 60, - javascript: scripts, - ..Default::default() - }; - // Made at once: no view ever waits on a browser. - let made = browser_host_create_browser_sync( - Some(&info), - Some(&mut handlers::client(&surface)), - Some(&url.as_str().into()), - Some(&settings), - None, - None, - ); - match made { - Some(browser) => { - handlers::adopt(&browser, surface.clone()); - surface.lock().expect("the surface lock").browser = Some(browser); - } - None => { - surface.lock().expect("the surface lock").failed = - Some(format!("CEF made no browser for {url}")) - } - } - } -} - -/// Starts CEF in this process; once per app. -pub fn start(data: &Path, cx: &mut App) -> Result<()> { - if cx.has_global::() { - return Ok(()); - } - crate::input::bind(cx); - let framework = load()?; - #[cfg(target_os = "macos")] - crate::mac::adopt_app_protocol()?; - let exe = std::env::current_exe()?; - // Bundled, CEF finds its helper apps; else this binary helps. - let bundled = cfg!(target_os = "macos") - && framework.parent().is_some_and(|dir| { - let name = exe.file_name().unwrap_or_default().to_string_lossy(); - dir.join(format!("{name} Helper.app")).is_dir() - }); - let helper = if bundled { - String::new() - } else { - exe.to_string_lossy().into_owned() - }; - let cache = data.join("web"); - std::fs::create_dir_all(&cache)?; - let settings = Settings { - windowless_rendering_enabled: true as _, - external_message_pump: true as _, - no_sandbox: true as _, - browser_subprocess_path: helper.as_str().into(), - #[cfg(target_os = "macos")] - framework_dir_path: framework.to_string_lossy().as_ref().into(), - #[cfg(not(target_os = "macos"))] - resources_dir_path: framework.to_string_lossy().as_ref().into(), - #[cfg(not(target_os = "macos"))] - locales_dir_path: framework.join("locales").to_string_lossy().as_ref().into(), - root_cache_path: cache.to_string_lossy().as_ref().into(), - persist_session_cookies: false as _, - log_severity: LogSeverity::WARNING, - ..Default::default() - }; - let (wake, mut asked) = mpsc::unbounded(); - *WAKE.lock().expect("the pump lock") = Some(wake); - cx.set_global(Engine::default()); - cx.spawn(async move |cx: &mut AsyncApp| { - // Not inside an update: CEF may run AppKit's loop here. - let args = Args::new(); - let mut app = handlers::app(); - let started = initialize( - Some(args.as_main_args()), - Some(&settings), - Some(&mut app), - std::ptr::null_mut(), - ); - if started != 1 { - let why = held(&cache).unwrap_or_else(|| "CEF did not initialize".into()); - log::error!("web: {why}"); - *FAILED.lock().expect("the start lock") = Some(why); - cx.update(crate::view::deliver); - return; - } - log::info!("web: CEF started, cache {}", cache.display()); - let quitting = cx.update(|cx| cx.on_app_quit(|_| async { stop() })); - quitting.detach(); - let mut due: Option = Some(Duration::ZERO); - loop { - let timer = match due { - Some(delay) => cx.background_executor().timer(delay).boxed_local(), - None => futures::future::pending().boxed_local(), - }; - futures::select_biased! { - delay = asked.next() => match delay { - Some(delay) => { - let delay = Duration::from_millis(delay.max(0) as u64); - due = Some(due.map_or(delay, |now| now.min(delay))); - continue; - } - None => return, - }, - _ = timer.fuse() => {} - } - run_later(); - make_browsers(); - do_message_loop_work(); - cx.update(crate::view::deliver); - // As cefclient: CEF does not ask for all work. - due = Some(FALLBACK); - } - }) - .detach(); - Ok(()) -} - -/// Stops CEF as the app quits. -fn stop() { - WAKE.lock().expect("the pump lock").take(); - shutdown(); - log::info!("web: CEF stopped"); -} - -/// Why CEF would refuse this cache: another app holds it. -#[cfg(unix)] -fn held(cache: &Path) -> Option { - let lock = std::fs::read_link(cache.join("SingletonLock")).ok()?; - let lock = lock.to_string_lossy(); - let pid: i32 = lock.rsplit('-').next()?.parse().ok()?; - // Only a live process holds it. - let alive = unsafe { libc::kill(pid, 0) } == 0; - alive.then(|| format!("another copy (process {pid}) is using {}", cache.display())) -} - -/// Chromium's Windows lock names no process. -#[cfg(windows)] -fn held(_cache: &Path) -> Option { - None -} - -#[cfg(test)] -mod tests { - #[test] - fn cef_loads_with_its_api_version() { - let dir = super::load().expect("CEF loads"); - #[cfg(not(target_os = "macos"))] - assert!(dir.join("locales").is_dir(), "{} holds CEF's files", dir.display()); - // Without the version this call exits the process. - assert!(cef::command_line_create().is_some()); - } -} diff --git a/crates/ely-web-content/src/fence.rs b/crates/ely-web-content/src/fence.rs deleted file mode 100644 index 830e0d3..0000000 --- a/crates/ely-web-content/src/fence.rs +++ /dev/null @@ -1,88 +0,0 @@ -//! The fence: where pages go, what they load. - -use cef::{wrapper::message_router::MessageRouterBrowserSideHandlerCallbacks, *}; - -use crate::{handlers::router, surface::Shared}; - -wrap_request_handler! { - pub(crate) struct Requests { - surface: Shared, - } - - impl RequestHandler { - fn on_before_browse( - &self, - browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - request: Option<&mut Request>, - user_gesture: ::std::os::raw::c_int, - _is_redirect: ::std::os::raw::c_int, - ) -> ::std::os::raw::c_int { - let url = request.map(|r| CefString::from(&r.url()).to_string()).unwrap_or_default(); - let mut surface = self.surface.lock().expect("the surface lock"); - // Content stays bundled; a clicked link leaves for the system. - if !surface.navigable(&url) { - if user_gesture == 1 { - surface.leaving.push(url); - } - return 1; - } - drop(surface); - router().on_before_browse(browser.map(|b| b.clone()), frame.map(|f| f.clone())); - 0 - } - - fn resource_request_handler( - &self, - _browser: Option<&mut Browser>, - _frame: Option<&mut Frame>, - _request: Option<&mut Request>, - _is_navigation: ::std::os::raw::c_int, - _is_download: ::std::os::raw::c_int, - _request_initiator: Option<&CefString>, - _disable_default_handling: Option<&mut ::std::os::raw::c_int>, - ) -> Option { - Some(Resources::new(self.surface.clone())) - } - - fn on_render_process_terminated( - &self, - browser: Option<&mut Browser>, - status: TerminationStatus, - error_code: ::std::os::raw::c_int, - _error_string: Option<&CefString>, - ) { - router().on_render_process_terminated(browser.map(|b| b.clone())); - self.surface.lock().expect("the surface lock").failed = - Some(format!("the page stopped: {status:?} ({error_code})")); - } - } -} - -wrap_resource_request_handler! { - struct Resources { - surface: Shared, - } - - impl ResourceRequestHandler { - // Every request, scripts and images too, meets the fence. - fn on_before_resource_load( - &self, - _browser: Option<&mut Browser>, - _frame: Option<&mut Frame>, - request: Option<&mut Request>, - _callback: Option<&mut Callback>, - ) -> ReturnValue { - let Some(request) = request else { - return ReturnValue::CANCEL; - }; - let url = CefString::from(&request.url()).to_string(); - let image = request.resource_type() == ResourceType::IMAGE; - if self.surface.lock().expect("the surface lock").loadable(&url, image) { - return ReturnValue::CONTINUE; - } - log::warn!("web: a request outside the allowed folders was refused"); - ReturnValue::CANCEL - } - } -} diff --git a/crates/ely-web-content/src/handlers.rs b/crates/ely-web-content/src/handlers.rs deleted file mode 100644 index 9d87ef2..0000000 --- a/crates/ely-web-content/src/handlers.rs +++ /dev/null @@ -1,482 +0,0 @@ -//! What CEF calls back: per process, and per browser. - -use std::{ - collections::HashMap, - sync::{Arc, Mutex, OnceLock}, -}; - -use cef::{ - wrapper::message_router::{ - BrowserSideCallback, BrowserSideHandler, BrowserSideRouter, MessageRouterBrowserSide, - MessageRouterBrowserSideHandlerCallbacks, MessageRouterConfig, MessageRouterRendererSide, - MessageRouterRendererSideHandlerCallbacks, RendererSideRouter, - }, - *, -}; - -use crate::{fence::Requests, surface::Shared}; - -/// The page calls `window.elyQuery({request, onSuccess, onFailure})`. -fn config() -> MessageRouterConfig { - MessageRouterConfig { - js_query_function: "elyQuery".into(), - js_cancel_function: "elyQueryCancel".into(), - ..Default::default() - } -} - -/// Each browser's surface, by CEF's browser id. -fn surfaces() -> &'static Mutex> { - static SURFACES: OnceLock>> = OnceLock::new(); - SURFACES.get_or_init(Default::default) -} - -pub(crate) fn adopt(browser: &Browser, surface: Shared) { - surfaces() - .lock() - .expect("the surface lock") - .insert(browser.identifier(), surface); -} - -fn surface_of(browser: Option<&Browser>) -> Option { - let id = browser?.identifier(); - surfaces() - .lock() - .expect("the surface lock") - .get(&id) - .cloned() -} - -/// Queries go to the view whose page asked. -struct Queries; - -impl BrowserSideHandler for Queries { - fn on_query_str( - &self, - browser: Option, - _frame: Option, - _query_id: i64, - request: &str, - _persistent: bool, - callback: Arc>, - ) -> bool { - let Some(surface) = surface_of(browser.as_ref()) else { - return false; - }; - surface - .lock() - .expect("the surface lock") - .queries - .push((request.to_string(), callback)); - true - } -} - -pub(crate) fn router() -> &'static Arc { - static ROUTER: OnceLock> = OnceLock::new(); - ROUTER.get_or_init(|| { - let router = BrowserSideRouter::new(config()); - router.add_handler(Arc::new(Queries), true); - router - }) -} - -fn renderer_router() -> &'static Arc { - static ROUTER: OnceLock> = OnceLock::new(); - ROUTER.get_or_init(|| RendererSideRouter::new(config())) -} - -/// WebRTC reaches the network by UDP; pages lose it. -const NO_PEERS: &str = "for (const name of ['RTCPeerConnection', 'webkitRTCPeerConnection', \ - 'RTCDataChannel', 'RTCSessionDescription', 'RTCIceCandidate', 'RTCRtpSender', \ - 'RTCRtpReceiver', 'RTCRtpTransceiver']) delete globalThis[name];"; - -wrap_app! { - struct ElyApp; - - impl App { - fn on_before_command_line_processing( - &self, - _process_type: Option<&CefString>, - command_line: Option<&mut CommandLine>, - ) { - let Some(line) = command_line else { return }; - // No keychain prompt; the content keeps no secrets. - line.append_switch(Some(&"use-mock-keychain".into())); - line.append_switch(Some(&"disable-extensions".into())); - // Offline pages: all traffic meets a dead proxy. - line.append_switch_with_value(Some(&"proxy-server".into()), Some(&"127.0.0.1:9".into())); - line.append_switch_with_value( - Some(&"proxy-bypass-list".into()), - Some(&"<-loopback>".into()), - ); - // WebRTC sends UDP past proxies; it may use proxies only. - line.append_switch_with_value( - Some(&"force-webrtc-ip-handling-policy".into()), - Some(&"disable_non_proxied_udp".into()), - ); - } - - fn browser_process_handler(&self) -> Option { - Some(Pump::new()) - } - - fn render_process_handler(&self) -> Option { - Some(Renderer::new()) - } - } -} - -pub(crate) fn app() -> cef::App { - ElyApp::new() -} - -wrap_browser_process_handler! { - struct Pump; - - impl BrowserProcessHandler { - fn on_schedule_message_pump_work(&self, delay_ms: i64) { - crate::engine::schedule(delay_ms); - } - } -} - -wrap_render_process_handler! { - struct Renderer; - - impl RenderProcessHandler { - fn on_context_created( - &self, - browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - context: Option<&mut V8Context>, - ) { - // Before any page script: no peer connections at all. - if let Some(context) = context.as_ref() { - let code = CefString::from(NO_PEERS); - let (mut value, mut thrown) = (None, None); - if context.eval(Some(&code), None, 0, Some(&mut value), Some(&mut thrown)) != 1 { - eprintln!("web: WebRTC could not be removed from a page"); - } - } - renderer_router().on_context_created( - browser.map(|b| b.clone()), - frame.map(|f| f.clone()), - context.map(|c| c.clone()), - ); - } - - fn on_context_released( - &self, - browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - context: Option<&mut V8Context>, - ) { - renderer_router().on_context_released( - browser.map(|b| b.clone()), - frame.map(|f| f.clone()), - context.map(|c| c.clone()), - ); - } - - fn on_process_message_received( - &self, - browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - source_process: ProcessId, - message: Option<&mut ProcessMessage>, - ) -> ::std::os::raw::c_int { - renderer_router().on_process_message_received( - browser.map(|b| b.clone()), - frame.map(|f| f.clone()), - Some(source_process), - message.map(|m| m.clone()), - ) as _ - } - } -} - -wrap_render_handler! { - struct Paint { - surface: Shared, - } - - impl RenderHandler { - fn view_rect(&self, _browser: Option<&mut Browser>, rect: Option<&mut Rect>) { - let Some(rect) = rect else { return }; - let surface = self.surface.lock().expect("the surface lock"); - // CEF needs a page at least one point square. - rect.width = (surface.size.0.round() as i32).max(1); - rect.height = (surface.size.1.round() as i32).max(1); - } - - fn screen_info( - &self, - _browser: Option<&mut Browser>, - screen_info: Option<&mut ScreenInfo>, - ) -> ::std::os::raw::c_int { - let Some(info) = screen_info else { return 0 }; - info.device_scale_factor = self.surface.lock().expect("the surface lock").scale; - 1 - } - - fn on_paint( - &self, - _browser: Option<&mut Browser>, - type_: PaintElementType, - _dirty_rects: Option<&[Rect]>, - buffer: *const u8, - width: ::std::os::raw::c_int, - height: ::std::os::raw::c_int, - ) { - // Popups (select menus) are not drawn; pages avoid them. - if type_ != PaintElementType::VIEW || buffer.is_null() || width <= 0 || height <= 0 { - return; - } - let length = width as usize * height as usize * 4; - let bgra = unsafe { std::slice::from_raw_parts(buffer, length) }.to_vec(); - self.surface.lock().expect("the surface lock").frame = - Some((width as u32, height as u32, bgra)); - } - - fn on_ime_composition_range_changed( - &self, - _browser: Option<&mut Browser>, - _selected_range: Option<&Range>, - character_bounds: Option<&[Rect]>, - ) { - let bounds = character_bounds.unwrap_or_default(); - self.surface.lock().expect("the surface lock").ime = bounds - .iter() - .map(|r| (r.x as f32, r.y as f32, r.width as f32, r.height as f32)) - .collect(); - } - } -} - -wrap_display_handler! { - struct Display { - surface: Shared, - } - - impl DisplayHandler { - fn on_console_message( - &self, - _browser: Option<&mut Browser>, - level: LogSeverity, - _message: Option<&CefString>, - _source: Option<&CefString>, - line: ::std::os::raw::c_int, - ) -> ::std::os::raw::c_int { - // Words and sources a document may forge stay out. - if level.get_raw() >= LogSeverity::WARNING.get_raw() { - log::warn!("web: the page warned at line {line}"); - } - 0 - } - - fn on_cursor_change( - &self, - _browser: Option<&mut Browser>, - _cursor: CursorHandle, - type_: CursorType, - _custom_cursor_info: Option<&CursorInfo>, - ) -> ::std::os::raw::c_int { - self.surface.lock().expect("the surface lock").cursor = type_; - 1 - } - } -} - -wrap_load_handler! { - struct Load { - surface: Shared, - } - - impl LoadHandler { - fn on_load_start( - &self, - _browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - _transition_type: TransitionType, - ) { - if frame.is_some_and(|frame| frame.is_main() == 1) { - self.surface.lock().expect("the surface lock").errored = false; - } - } - - fn on_load_error( - &self, - _browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - error_code: Errorcode, - error_text: Option<&CefString>, - failed_url: Option<&CefString>, - ) { - // A stopped load is no failure; a fenced one is. - if frame.is_none_or(|frame| frame.is_main() != 1) || error_code == Errorcode::ABORTED { - return; - } - let text = error_text.map(CefString::to_string).unwrap_or_default(); - let url = failed_url.map(CefString::to_string).unwrap_or_default(); - let mut surface = self.surface.lock().expect("the surface lock"); - surface.errored = true; - surface.failed = Some(format!("{text} ({}) {url}", error_code.get_raw())); - } - - fn on_load_end( - &self, - _browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - _http_status_code: ::std::os::raw::c_int, - ) { - if frame.is_some_and(|frame| frame.is_main() == 1) { - let mut surface = self.surface.lock().expect("the surface lock"); - surface.loaded = !surface.errored; - } - } - } -} - -wrap_life_span_handler! { - struct Life { - surface: Shared, - } - - impl LifeSpanHandler { - fn on_after_created(&self, browser: Option<&mut Browser>) { - if let Some(browser) = browser { - log::info!("web: browser {} made", browser.identifier()); - } - } - - fn on_before_close(&self, browser: Option<&mut Browser>) { - let browser = browser.map(|b| b.clone()); - router().on_before_close(browser.clone()); - if let Some(browser) = browser { - surfaces().lock().expect("the surface lock").remove(&browser.identifier()); - } - // The browser holds these handlers; they hold the surface. - self.surface.lock().expect("the surface lock").browser = None; - } - } -} - -wrap_client! { - struct Handlers { - paint: RenderHandler, - display: DisplayHandler, - load: LoadHandler, - life: LifeSpanHandler, - requests: RequestHandler, - keys: Option, - } - - impl Client { - fn keyboard_handler(&self) -> Option { - self.keys.clone() - } - - fn render_handler(&self) -> Option { - Some(self.paint.clone()) - } - - fn display_handler(&self) -> Option { - Some(self.display.clone()) - } - - fn load_handler(&self) -> Option { - Some(self.load.clone()) - } - - fn life_span_handler(&self) -> Option { - Some(self.life.clone()) - } - - fn request_handler(&self) -> Option { - Some(self.requests.clone()) - } - - fn on_process_message_received( - &self, - browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - source_process: ProcessId, - message: Option<&mut ProcessMessage>, - ) -> ::std::os::raw::c_int { - router().on_process_message_received( - browser.map(|b| b.clone()), - frame.map(|f| f.clone()), - source_process, - message.map(|m| m.clone()), - ) as _ - } - } -} - -/// A browser's handlers, all sharing its surface. -pub(crate) fn client(surface: &Shared) -> cef::Client { - Handlers::new( - Paint::new(surface.clone()), - Display::new(surface.clone()), - Load::new(surface.clone()), - Life::new(surface.clone()), - Requests::new(surface.clone()), - keys(), - ) -} - -#[cfg(target_os = "macos")] -fn keys() -> Option { - Some(crate::edit::Keys::new()) -} - -/// Elsewhere Chromium binds these keys itself. -#[cfg(not(target_os = "macos"))] -fn keys() -> Option { - None -} - - -#[cfg(test)] -mod tests { - use std::sync::{Arc, Mutex, OnceLock}; - - use cef::{CefString, ImplDisplayHandler, LogSeverity}; - - struct Kept; - - fn kept() -> &'static Mutex> { - static KEPT: OnceLock>> = OnceLock::new(); - KEPT.get_or_init(Mutex::default) - } - - impl log::Log for Kept { - fn enabled(&self, _: &log::Metadata) -> bool { - true - } - - fn log(&self, record: &log::Record) { - kept().lock().expect("the kept lock").push(record.args().to_string()); - } - - fn flush(&self) {} - } - - #[test] - fn a_pages_words_and_sources_never_reach_the_log() { - log::set_boxed_logger(Box::new(Kept)).expect("the only logger"); - log::set_max_level(log::LevelFilter::Trace); - let surface = crate::surface::Surface::new(std::env::temp_dir(), 1.0); - let display = super::Display::new(Arc::new(Mutex::new(surface))); - let said = CefString::from("ely: M11_SECRET"); - let at = CefString::from("file:///content/M11_SECRET.js"); - for level in [LogSeverity::WARNING, LogSeverity::ERROR, LogSeverity::INFO] { - display.on_console_message(None, level, Some(&said), Some(&at), 7); - } - let records = kept().lock().expect("the kept lock").clone(); - let warned = records.iter().filter(|said| *said == "web: the page warned at line 7"); - assert_eq!(warned.count(), 2, "warnings and errors stay seen: {records:#?}"); - assert!(!records.iter().any(|said| said.contains("M11_SECRET")), "{records:#?}"); - } -} diff --git a/crates/ely-web-content/src/input.rs b/crates/ely-web-content/src/input.rs deleted file mode 100644 index 12d005f..0000000 --- a/crates/ely-web-content/src/input.rs +++ /dev/null @@ -1,344 +0,0 @@ -//! GPUI's mouse, keys, focus and IME, as CEF events. - -use std::ops::Range; - -use cef::*; -use gpui::{ - App, Bounds, Context, Div, Entity, EntityInputHandler, InteractiveElement as _, KeyDownEvent, - KeyUpEvent, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, - Point, ScrollDelta, ScrollWheelEvent, UTF16Selection, Window, point, px, size, -}; - -use crate::{keys, view::WebView}; - -gpui::actions!(web, [PageTab, PageBackTab]); - -/// The page's key context: Tab stays in the page. -const CONTEXT: &str = "WebView"; - -/// Tab and Shift-Tab go to the page, not to focus. -pub(crate) fn bind(cx: &mut App) { - cx.bind_keys([ - gpui::KeyBinding::new("tab", PageTab, Some(CONTEXT)), - gpui::KeyBinding::new("shift-tab", PageBackTab, Some(CONTEXT)), - ]); -} - -/// CEF's `cef_event_flags_t` for these modifiers. -pub(crate) fn flags(modifiers: &Modifiers) -> u32 { - let mut flags = 0; - for (on, bit) in [ - (modifiers.shift, 1 << 1), - (modifiers.control, 1 << 2), - (modifiers.alt, 1 << 3), - (modifiers.platform, 1 << 7), - ] { - if on { - flags |= bit; - } - } - flags -} - -fn button(button: MouseButton) -> Option<(MouseButtonType, u32)> { - match button { - MouseButton::Left => Some((MouseButtonType::LEFT, 1 << 4)), - MouseButton::Middle => Some((MouseButtonType::MIDDLE, 1 << 5)), - MouseButton::Right => Some((MouseButtonType::RIGHT, 1 << 6)), - _ => None, - } -} - -impl WebView { - /// A window point as a page point. - fn at(&self, position: Point, modifiers: u32) -> MouseEvent { - let local = position - self.bounds.origin; - MouseEvent { - x: f32::from(local.x).round() as i32, - y: f32::from(local.y).round() as i32, - modifiers, - } - } - - fn mouse_down(&mut self, event: &MouseDownEvent, window: &mut Window, cx: &mut Context) { - window.focus(&self.focus, cx); - let Some((kind, held)) = button(event.button) else { - return; - }; - let at = self.at(event.position, flags(&event.modifiers) | held); - let count = event.click_count as i32; - self.on_host(move |host| host.send_mouse_click_event(Some(&at), kind, false as _, count)); - } - - fn mouse_up(&mut self, event: &MouseUpEvent, _: &mut Window, _: &mut Context) { - let Some((kind, _)) = button(event.button) else { - return; - }; - let at = self.at(event.position, flags(&event.modifiers)); - let count = event.click_count as i32; - self.on_host(move |host| host.send_mouse_click_event(Some(&at), kind, true as _, count)); - } - - fn mouse_move(&mut self, event: &MouseMoveEvent, _: &mut Window, _: &mut Context) { - let held = event - .pressed_button - .and_then(button) - .map_or(0, |(_, bit)| bit); - let at = self.at(event.position, flags(&event.modifiers) | held); - self.on_host(move |host| host.send_mouse_move_event(Some(&at), false as _)); - } - - fn scroll(&mut self, event: &ScrollWheelEvent, _: &mut Window, _: &mut Context) { - // Lines as Chromium counts them: forty points. - let delta = match event.delta { - ScrollDelta::Pixels(delta) => (f32::from(delta.x), f32::from(delta.y)), - ScrollDelta::Lines(delta) => (delta.x * 40.0, delta.y * 40.0), - }; - let at = self.at(event.position, flags(&event.modifiers)); - let (x, y) = (delta.0.round() as i32, delta.1.round() as i32); - self.on_host(move |host| host.send_mouse_wheel_event(Some(&at), x, y)); - } - - fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context) { - let stroke = &event.keystroke; - // Edit commands Chromium only takes from its own menu. - let edit = stroke.key.clone(); - if keys::edit_chord(&stroke.modifiers) && matches!(edit.as_str(), "c" | "x" | "v" | "a") { - // The owner may take a paste, such as an image. - if edit == "v" && self.paste.clone().is_some_and(|take| take(window, cx)) { - return cx.stop_propagation(); - } - self.on_browser(move |browser| { - let Some(frame) = browser.main_frame() else { - return; - }; - match edit.as_str() { - "c" => frame.copy(), - "x" => frame.cut(), - "v" => frame.paste(), - _ => frame.select_all(), - } - }); - return cx.stop_propagation(); - } - let Some(code) = keys::code(&stroke.key) else { - return log::debug!("web: no key code for {}", stroke.key); - }; - let event = KeyEvent { - modifiers: flags(&stroke.modifiers), - windows_key_code: code.windows, - native_key_code: code.native, - ..Default::default() - }; - let chord = stroke.modifiers.control || stroke.modifiers.platform; - match (keys::character(&stroke.key), &stroke.key_char) { - // Typed text arrives by the input handler, which sends it. - (None, Some(_)) if !chord => self.typing = Some(event), - (character, typed) => { - let character = character - .or_else(|| typed.as_deref().and_then(|t| t.encode_utf16().next())) - .unwrap_or(0); - self.press(event, character); - } - } - } - - /// Tab, with Shift when `back`, down and up. - fn tab(&mut self, back: bool) { - let code = keys::code("tab").expect("tab has a key code"); - let event = KeyEvent { - modifiers: if back { 1 << 1 } else { 0 }, - windows_key_code: code.windows, - native_key_code: code.native, - ..Default::default() - }; - self.press(event.clone(), 0x09); - let up = KeyEvent { - type_: KeyEventType::KEYUP, - character: 0x09, - unmodified_character: 0x09, - ..event - }; - self.on_host(move |host| host.send_key_event(Some(&up))); - } - - /// A key down and its character, as cefclient sends them. - fn press(&self, event: KeyEvent, character: u16) { - let mut event = KeyEvent { - type_: KeyEventType::KEYDOWN, - character, - unmodified_character: character, - ..event - }; - self.on_host(move |host| { - host.send_key_event(Some(&event)); - event.type_ = KeyEventType::CHAR; - host.send_key_event(Some(&event)); - }); - } - - fn key_up(&mut self, event: &KeyUpEvent, _: &mut Window, _: &mut Context) { - let stroke = &event.keystroke; - let Some(code) = keys::code(&stroke.key) else { - return; - }; - // An up without its character reads as another down. - let character = keys::character(&stroke.key) - .or_else(|| { - stroke - .key_char - .as_deref() - .and_then(|t| t.encode_utf16().next()) - }) - .or_else(|| stroke.key.encode_utf16().next()) - .unwrap_or(0); - let up = KeyEvent { - type_: KeyEventType::KEYUP, - modifiers: flags(&stroke.modifiers), - windows_key_code: code.windows, - native_key_code: code.native, - character, - unmodified_character: character, - ..Default::default() - }; - self.on_host(move |host| host.send_key_event(Some(&up))); - } -} - -/// The view's listeners on its outer element. -pub(crate) fn listen(element: Div, cx: &mut Context) -> Div { - element - .key_context(CONTEXT) - .on_action(cx.listener(|view, _: &PageTab, _, _| view.tab(false))) - .on_action(cx.listener(|view, _: &PageBackTab, _, _| view.tab(true))) - .on_mouse_down(MouseButton::Left, cx.listener(WebView::mouse_down)) - .on_mouse_down(MouseButton::Right, cx.listener(WebView::mouse_down)) - .on_mouse_down(MouseButton::Middle, cx.listener(WebView::mouse_down)) - .on_mouse_up(MouseButton::Left, cx.listener(WebView::mouse_up)) - .on_mouse_up(MouseButton::Right, cx.listener(WebView::mouse_up)) - .on_mouse_up(MouseButton::Middle, cx.listener(WebView::mouse_up)) - .on_mouse_move(cx.listener(WebView::mouse_move)) - .on_scroll_wheel(cx.listener(WebView::scroll)) - .on_key_down(cx.listener(WebView::key_down)) - .on_key_up(cx.listener(WebView::key_up)) -} - -/// CEF's page focus follows the view's. -pub(crate) fn follow_focus(view: &Entity, window: &mut Window, cx: &mut App) { - view.update(cx, |this, cx| { - let focus = this.focus.clone(); - cx.on_focus(&focus, window, |view, _, _| { - view.on_host(|host| host.set_focus(true as _)); - }) - .detach(); - cx.on_blur(&focus, window, |view, _, _| { - view.on_host(|host| host.set_focus(false as _)); - }) - .detach(); - }); -} - -/// CEF's "no range": both ends at the maximum. -const NO_RANGE: cef::Range = cef::Range { - from: u32::MAX, - to: u32::MAX, -}; - -fn utf16(text: &str) -> Vec { - text.encode_utf16().collect() -} - -impl EntityInputHandler for WebView { - /// The IME sees none of the page's text. - fn text_for_range( - &mut self, - _: Range, - _: &mut Option>, - _: &mut Window, - _: &mut Context, - ) -> Option { - None - } - - fn selected_text_range( - &mut self, - _: bool, - _: &mut Window, - _: &mut Context, - ) -> Option { - None - } - - fn marked_text_range(&self, _: &mut Window, _: &mut Context) -> Option> { - self.composing.clone() - } - - fn unmark_text(&mut self, _: &mut Window, _: &mut Context) { - if self.composing.take().is_some() { - self.on_host(|host| host.ime_finish_composing_text(false as _)); - } - } - - fn replace_text_in_range( - &mut self, - _: Option>, - text: &str, - _: &mut Window, - _: &mut Context, - ) { - let typed = self.typing.take(); - let was = self.composing.take(); - let units = utf16(text); - // One character, no composition: a key press, as cefclient. - if let (Some(event), None, [character]) = (typed, was, units.as_slice()) { - return self.press(event, *character); - } - let text = CefString::from(text); - self.on_host(move |host| host.ime_commit_text(Some(&text), Some(&NO_RANGE), 0)); - } - - fn replace_and_mark_text_in_range( - &mut self, - _: Option>, - new_text: &str, - new_selected_range: Option>, - _: &mut Window, - _: &mut Context, - ) { - let length = utf16(new_text).len(); - self.composing = (length > 0).then_some(0..length); - let selection = new_selected_range.unwrap_or(length..length); - let range = cef::Range { - from: selection.start as _, - to: selection.end as _, - }; - let text = CefString::from(new_text); - self.on_host(move |host| { - host.ime_set_composition(Some(&text), None, Some(&NO_RANGE), Some(&range)) - }); - } - - fn bounds_for_range( - &mut self, - range: Range, - element: Bounds, - _: &mut Window, - _: &mut Context, - ) -> Option> { - let surface = self.surface.lock().expect("the surface lock"); - let (x, y, w, h) = *surface.ime.get(range.start).or(surface.ime.last())?; - Some(Bounds::new( - element.origin + point(px(x), px(y)), - size(px(w), px(h)), - )) - } - - fn character_index_for_point( - &mut self, - _: Point, - _: &mut Window, - _: &mut Context, - ) -> Option { - None - } -} diff --git a/crates/ely-web-content/src/keys.rs b/crates/ely-web-content/src/keys.rs deleted file mode 100644 index e0c89de..0000000 --- a/crates/ely-web-content/src/keys.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! GPUI key names as the codes Chromium reads. - -use gpui::Modifiers; - -/// Windows' virtual key, and macOS's hardware key. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Code { - pub windows: i32, - pub native: i32, -} - -/// Letters, then digits, as macOS numbers its keys. -const MAC_LETTERS: [i32; 26] = [ - 0, 11, 8, 2, 14, 3, 5, 4, 34, 38, 40, 37, 46, 45, 31, 35, 12, 15, 1, 17, 32, 9, 13, 7, 16, 6, -]; -const MAC_DIGITS: [i32; 10] = [29, 18, 19, 20, 21, 23, 22, 26, 28, 25]; -const MAC_F: [i32; 12] = [122, 120, 99, 118, 96, 97, 98, 100, 101, 109, 103, 111]; - -/// Named keys: GPUI's name, Windows, macOS. -const NAMED: &[(&str, i32, i32)] = &[ - ("backspace", 0x08, 51), - ("tab", 0x09, 48), - ("enter", 0x0D, 36), - ("escape", 0x1B, 53), - ("space", 0x20, 49), - ("pageup", 0x21, 116), - ("pagedown", 0x22, 121), - ("end", 0x23, 119), - ("home", 0x24, 115), - ("left", 0x25, 123), - ("up", 0x26, 126), - ("right", 0x27, 124), - ("down", 0x28, 125), - ("delete", 0x2E, 117), - (";", 0xBA, 41), - ("=", 0xBB, 24), - (",", 0xBC, 43), - ("-", 0xBD, 27), - (".", 0xBE, 47), - ("/", 0xBF, 44), - ("`", 0xC0, 50), - ("[", 0xDB, 33), - ("\\", 0xDC, 42), - ("]", 0xDD, 30), - ("'", 0xDE, 39), -]; - -pub fn code(key: &str) -> Option { - if let Some(&(_, windows, native)) = NAMED.iter().find(|(name, _, _)| *name == key) { - return Some(Code { windows, native }); - } - let mut chars = key.chars(); - let (Some(c), None) = (chars.next(), chars.next()) else { - let n: usize = key.strip_prefix('f')?.parse().ok()?; - let native = *MAC_F.get(n.checked_sub(1)?)?; - return Some(Code { - windows: 0x6F + n as i32, - native, - }); - }; - match c { - 'a'..='z' => Some(Code { - windows: c.to_ascii_uppercase() as i32, - native: MAC_LETTERS[(c as u8 - b'a') as usize], - }), - '0'..='9' => Some(Code { - windows: c as i32, - native: MAC_DIGITS[(c as u8 - b'0') as usize], - }), - _ => None, - } -} - -/// The character AppKit gives keys that type none. -#[cfg(target_os = "macos")] -pub fn character(key: &str) -> Option { - let n: Option = key.strip_prefix('f').and_then(|n| n.parse().ok()); - Some(match key { - "backspace" => 0x7F, - "tab" => 0x09, - "enter" => 0x0D, - "escape" => 0x1B, - "up" => 0xF700, - "down" => 0xF701, - "left" => 0xF702, - "right" => 0xF703, - "delete" => 0xF728, - "home" => 0xF729, - "end" => 0xF72B, - "pageup" => 0xF72C, - "pagedown" => 0xF72D, - _ => match n { - Some(n @ 1..=12) => 0xF703 + n, - _ => return None, - }, - }) -} - -/// The control character other systems type, if any. -#[cfg(not(target_os = "macos"))] -pub fn character(key: &str) -> Option { - Some(match key { - "backspace" => 0x08, - "tab" => 0x09, - "enter" => 0x0D, - "escape" => 0x1B, - _ => return None, - }) -} - -/// The chord for cut, copy, paste and select-all here. -pub fn edit_chord(modifiers: &Modifiers) -> bool { - let primary = if cfg!(target_os = "macos") { - modifiers.platform && !modifiers.control - } else { - modifiers.control && !modifiers.platform - }; - primary && !modifiers.alt && !modifiers.shift -} - -#[cfg(test)] -mod tests { - use super::{Code, code}; - - #[test] - fn keys_carry_both_codes() { - assert_eq!( - code("a"), - Some(Code { - windows: 0x41, - native: 0 - }) - ); - assert_eq!( - code("z"), - Some(Code { - windows: 0x5A, - native: 6 - }) - ); - assert_eq!( - code("0"), - Some(Code { - windows: 0x30, - native: 29 - }) - ); - assert_eq!( - code("enter"), - Some(Code { - windows: 0x0D, - native: 36 - }) - ); - assert_eq!( - code("f1"), - Some(Code { - windows: 0x70, - native: 122 - }) - ); - assert_eq!( - code("f12"), - Some(Code { - windows: 0x7B, - native: 111 - }) - ); - assert_eq!( - code("["), - Some(Code { - windows: 0xDB, - native: 33 - }) - ); - assert_eq!(code("f13"), None); - assert_eq!(code("é"), None); - } - - #[cfg(target_os = "macos")] - #[test] - fn keys_without_text_carry_appkit_characters() { - assert_eq!(super::character("backspace"), Some(0x7F)); - assert_eq!(super::character("up"), Some(0xF700)); - assert_eq!(super::character("f1"), Some(0xF704)); - assert_eq!(super::character("f12"), Some(0xF70F)); - assert_eq!(super::character("a"), None); - } -} diff --git a/crates/ely-web-content/src/lib.rs b/crates/ely-web-content/src/lib.rs deleted file mode 100644 index a766968..0000000 --- a/crates/ely-web-content/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Bundled pages: CEF draws off screen, GPUI shows. - -#[cfg(target_os = "macos")] -mod edit; -mod engine; -mod fence; -mod handlers; -mod input; -mod keys; -#[cfg(target_os = "macos")] -mod mac; -mod print; -mod surface; -mod tokens; -mod view; - -pub use engine::{start, subprocess}; -pub use surface::file_url; -pub use view::{Event, Reply, WebView}; diff --git a/crates/ely-web-content/src/mac.rs b/crates/ely-web-content/src/mac.rs deleted file mode 100644 index 7ff9eca..0000000 --- a/crates/ely-web-content/src/mac.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! Chromium's app protocol on GPUI's own `NSApplication` class. - -use std::{cell::Cell, ffi::CStr}; - -use anyhow::{Result, bail}; -use objc2::{ - ffi, - runtime::{AnyClass, AnyObject, Bool, Imp, Sel}, - sel, -}; - -thread_local! { - /// NSApp lives on the main thread; one flag serves it. - static SENDING: Cell = const { Cell::new(false) }; -} - -extern "C-unwind" fn is_handling(_this: *mut AnyObject, _cmd: Sel) -> Bool { - Bool::new(SENDING.get()) -} - -extern "C-unwind" fn set_handling(_this: *mut AnyObject, _cmd: Sel, on: Bool) { - SENDING.set(on.as_bool()); -} - -extern "C-unwind" fn send_event(this: *mut AnyObject, _cmd: Sel, event: *mut AnyObject) { - let was = SENDING.replace(true); - let base = AnyClass::get(c"NSApplication").expect("AppKit has NSApplication"); - unsafe { - let _: () = objc2::msg_send![super(&*this, base), sendEvent: event]; - } - SENDING.set(was); -} - -fn add(class: &AnyClass, name: Sel, imp: Imp, types: &CStr) -> Result<()> { - let added = unsafe { - ffi::class_addMethod( - class as *const AnyClass as *mut AnyClass, - name, - imp, - types.as_ptr(), - ) - }; - if !added.as_bool() { - bail!("{} already has -{name}", class.name().to_string_lossy()); - } - Ok(()) -} - -/// Gives NSApp's class what CEF checks before it runs. -pub fn adopt_app_protocol() -> Result<()> { - let Some(app) = (unsafe { objc2_app_kit_nsapp() }) else { - bail!("no NSApp yet"); - }; - let class = unsafe { &*ffi::object_getClass(app.cast()) }; - if class.responds_to(sel!(isHandlingSendEvent)) { - return Ok(()); - } - unsafe { - add( - class, - sel!(isHandlingSendEvent), - std::mem::transmute(is_handling as *const ()), - c"c@:", - )?; - add( - class, - sel!(setHandlingSendEvent:), - std::mem::transmute(set_handling as *const ()), - c"v@:c", - )?; - add( - class, - sel!(sendEvent:), - std::mem::transmute(send_event as *const ()), - c"v@:@", - )?; - } - // Chromium checks the two `Cr` protocols; CEF's own is header-only. - for (name, needed) in [ - (c"CrAppProtocol", true), - (c"CrAppControlProtocol", true), - (c"CefAppProtocol", false), - ] { - let protocol = unsafe { ffi::objc_getProtocol(name.as_ptr()) }; - match (protocol.is_null(), needed) { - (true, true) => bail!("CEF did not register {}", name.to_string_lossy()), - (true, false) => log::info!("web: {} is not registered", name.to_string_lossy()), - (false, _) => unsafe { - ffi::class_addProtocol(class as *const AnyClass as *mut AnyClass, protocol); - }, - } - } - log::info!( - "web: {} takes CefAppProtocol", - class.name().to_string_lossy() - ); - Ok(()) -} - -/// The shared application, if made. -unsafe fn objc2_app_kit_nsapp() -> Option<*mut AnyObject> { - let class = AnyClass::get(c"NSApplication")?; - let app: *mut AnyObject = unsafe { objc2::msg_send![class, sharedApplication] }; - (!app.is_null()).then_some(app) -} diff --git a/crates/ely-web-content/src/print.rs b/crates/ely-web-content/src/print.rs deleted file mode 100644 index 6109a62..0000000 --- a/crates/ely-web-content/src/print.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! A page as PDF, printed by Chromium itself (MARK-11). - -use std::path::{Path, PathBuf}; - -use cef::*; - -use crate::{surface::Shared, view::WebView}; - -wrap_pdf_print_callback! { - struct Printed { - surface: Shared, - } - - impl PdfPrintCallback { - fn on_pdf_print_finished(&self, path: Option<&CefString>, ok: ::std::os::raw::c_int) { - let path = PathBuf::from(path.map(CefString::to_string).unwrap_or_default()); - log::info!("web: printed {} ok {}", path.display(), ok != 0); - self.surface.lock().expect("the surface lock").printed.push((path, ok != 0)); - } - } -} - -impl WebView { - /// Prints the page to `path`; `Event::Printed` tells how. - pub fn print_pdf(&self, path: &Path) { - let (path, surface) = ( - CefString::from(path.to_string_lossy().as_ref()), - self.surface.clone(), - ); - self.on_host(move |host| { - let settings = PdfPrintSettings { - print_background: 1, - ..Default::default() - }; - let mut done = Printed::new(surface); - host.print_to_pdf(Some(&path), Some(&settings), Some(&mut done)); - }); - } -} diff --git a/crates/ely-web-content/src/surface.rs b/crates/ely-web-content/src/surface.rs deleted file mode 100644 index af5fe39..0000000 --- a/crates/ely-web-content/src/surface.rs +++ /dev/null @@ -1,278 +0,0 @@ -//! What CEF's handlers leave the view; the page fence. - -use std::{ - path::{Path, PathBuf}, - sync::{Arc, Mutex}, -}; - -use cef::{wrapper::message_router::BrowserSideCallback, *}; - -pub(crate) type Shared = Arc>; - -pub(crate) struct Surface { - /// Points, as GPUI lays the view out. - pub size: (f32, f32), - pub scale: f32, - pub frame: Option<(u32, u32, Vec)>, - /// Composing characters' boxes, in points. - pub ime: Vec<(f32, f32, f32, f32)>, - pub cursor: CursorType, - pub loaded: bool, - /// The main frame's load failed since it started. - pub errored: bool, - pub queries: Vec<(String, Arc>)>, - pub leaving: Vec, - /// PDFs printed: where, and whether it worked. - pub printed: Vec<(PathBuf, bool)>, - /// Why the page cannot show: load, renderer or start. - pub failed: Option, - /// Made on the pump's turn, after the view. - pub browser: Option, - /// The view went before its browser came. - pub closed: bool, - /// Whether the page's scripts run. - pub scripts: bool, - /// The only folder pages load from, real path. - root: PathBuf, - /// A folder images alone may also come from. - pub images: Option, -} - -impl Surface { - pub fn new(root: PathBuf, scale: f32) -> Self { - Surface { - size: (1.0, 1.0), - scale, - frame: None, - ime: Vec::new(), - cursor: CursorType::POINTER, - loaded: false, - errored: false, - queries: Vec::new(), - leaving: Vec::new(), - printed: Vec::new(), - failed: None, - browser: None, - closed: false, - scripts: true, - root, - images: None, - } - } - - /// A file under `root`, by its real path. - fn holds(root: &Path, url: &str) -> bool { - let Some(path) = url.strip_prefix("file://") else { - return false; - }; - let path = path.split(['?', '#']).next().unwrap_or_default(); - let Ok(path) = String::from_utf8(decoded(path)) else { - return false; - }; - #[cfg(windows)] - let path = windows_path(&path); - std::fs::canonicalize(path).is_ok_and(|real| real.starts_with(root)) - } - - /// Where the page may go: its own files, or blank. - pub fn navigable(&self, url: &str) -> bool { - url == "about:blank" || Self::holds(&self.root, url) - } - - /// What a page loads: its files, data, allowed images. - pub fn loadable(&self, url: &str, image: bool) -> bool { - let pictured = image - && self - .images - .as_deref() - .is_some_and(|dir| Self::holds(dir, url)); - self.navigable(url) - || url.starts_with("data:") - || url.starts_with("blob:file://") - || pictured - } -} - -impl Drop for Surface { - fn drop(&mut self) { - log::info!("web: a page's memory is freed"); - } -} - -/// `%XX` decoded; a file URL's path. -fn decoded(text: &str) -> Vec { - let bytes = text.as_bytes(); - let (mut out, mut at) = (Vec::with_capacity(bytes.len()), 0); - while at < bytes.len() { - let hex = bytes - .get(at + 1..at + 3) - .and_then(|pair| u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()); - match (bytes[at], hex) { - (b'%', Some(byte)) => { - out.push(byte); - at += 3; - } - (byte, _) => { - out.push(byte); - at += 1; - } - } - } - out -} - -/// Bytes as URL text, reserved ones escaped. -fn escaped(bytes: &[u8]) -> String { - let mut out = String::new(); - for &byte in bytes { - match byte { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' => { - out.push(byte as char) - } - _ => out.push_str(&format!("%{byte:02X}")), - } - } - out -} - -/// A path as a file URL, every reserved byte escaped. -pub fn file_url(path: &Path) -> String { - #[cfg(windows)] - return windows_url(&path.to_string_lossy()); - #[cfg(not(windows))] - format!("file://{}", escaped(path.as_os_str().as_encoded_bytes())) -} - -/// A drive or UNC path as `file:///C:/…` or `file://host/…`. -#[cfg(any(windows, test))] -fn windows_url(path: &str) -> String { - let path = match path.strip_prefix(r"\\?\UNC\") { - Some(rest) => format!(r"\\{rest}"), - None => path.strip_prefix(r"\\?\").unwrap_or(path).to_owned(), - }; - let slashed = |text: &str| escaped(text.replace('\\', "/").as_bytes()); - match path.strip_prefix(r"\\") { - Some(unc) => { - let (host, rest) = unc.split_once('\\').unwrap_or((unc, "")); - format!("file://{}/{}", slashed(host), slashed(rest)) - } - None if path.get(1..2) == Some(":") => { - format!("file:///{}{}", &path[..2], slashed(&path[2..])) - } - None => format!("file:///{}", slashed(&path)), - } -} - -/// A decoded file URL's rest as a Windows path. -#[cfg(any(windows, test))] -fn windows_path(rest: &str) -> String { - match rest.strip_prefix('/') { - Some(drive) => drive.replace('/', "\\"), - None => format!(r"\\{}", rest.replace('/', "\\")), - } -} - -#[cfg(test)] -mod tests { - use std::path::Path; - - fn surface(root: &Path) -> super::Surface { - super::Surface::new(std::fs::canonicalize(root).expect("real"), 1.0) - } - - #[test] - fn only_real_files_under_the_root_load() { - let dir = tempfile::tempdir().expect("a temp dir"); - let root = dir.path().join("content"); - let outside = dir.path().join("outside"); - std::fs::create_dir_all(&root).expect("makes"); - std::fs::create_dir_all(&outside).expect("makes"); - std::fs::write(root.join("index.html"), "x").expect("writes"); - std::fs::write(root.join("a #b?.html"), "x").expect("writes"); - std::fs::write(outside.join("o.html"), "x").expect("writes"); - let surface = surface(&root); - let url = |path: &Path| super::file_url(&std::fs::canonicalize(path).expect("real")); - assert!(surface.navigable(&url(&root.join("index.html")))); - assert!( - surface.navigable(&url(&root.join("a #b?.html"))), - "reserved bytes" - ); - assert!(surface.navigable("about:blank")); - assert!(!surface.navigable(&url(&outside.join("o.html")))); - let up = format!("{}/../outside/o.html", super::file_url(&surface.root)); - assert!(!surface.navigable(&up)); - assert!(!surface.navigable(&format!("{}/missing.html", super::file_url(&surface.root)))); - assert!(!surface.navigable("https://example.com/")); - assert!( - !surface.navigable("data:text/html,x") - && surface.loadable("data:image/png;base64,x", true) - ); - assert!(!surface.loadable("http://127.0.0.1:9/pixel", true)); - } - - #[test] - fn an_image_folder_admits_images_alone() { - let dir = tempfile::tempdir().expect("a temp dir"); - let (root, docs) = (dir.path().join("content"), dir.path().join("docs")); - std::fs::create_dir_all(&root).expect("makes"); - std::fs::create_dir_all(&docs).expect("makes"); - std::fs::write(docs.join("a.png"), "x").expect("writes"); - let mut surface = surface(&root); - let picture = super::file_url(&std::fs::canonicalize(docs.join("a.png")).expect("real")); - assert!( - !surface.loadable(&picture, true), - "not before it is allowed" - ); - surface.images = Some(std::fs::canonicalize(&docs).expect("real")); - assert!(surface.loadable(&picture, true)); - assert!(!surface.loadable(&picture, false), "a script from there"); - assert!(!surface.navigable(&picture)); - } - - #[cfg(unix)] - #[test] - fn a_link_out_of_the_root_is_outside() { - let dir = tempfile::tempdir().expect("a temp dir"); - let root = dir.path().join("content"); - let outside = dir.path().join("outside"); - std::fs::create_dir_all(&root).expect("makes"); - std::fs::create_dir_all(&outside).expect("makes"); - std::fs::write(outside.join("o.html"), "x").expect("writes"); - std::os::unix::fs::symlink(&outside, root.join("link")).expect("links"); - let surface = surface(&root); - let through = format!("{}/link/o.html", super::file_url(&surface.root)); - assert!(!surface.navigable(&through)); - } - - #[test] - fn windows_paths_make_valid_file_urls_and_back() { - let cases = [ - (r"C:\Users\A b\doc.md", "file:///C:/Users/A%20b/doc.md"), - (r"\\?\C:\notes\doc.md", "file:///C:/notes/doc.md"), - (r"\\server\share\doc.md", "file://server/share/doc.md"), - (r"\\?\UNC\server\share\doc.md", "file://server/share/doc.md"), - ]; - for (path, url) in cases { - assert_eq!(super::windows_url(path), url, "{path}"); - } - assert_eq!( - super::windows_path("/C:/Users/A b/doc.md"), - r"C:\Users\A b\doc.md" - ); - assert_eq!( - super::windows_path("server/share/doc.md"), - r"\\server\share\doc.md" - ); - } - - #[test] - fn percent_escapes_decode_and_bad_ones_stay() { - assert_eq!(super::decoded("a%20b%e4%b8%ad"), "a b中".as_bytes()); - assert_eq!(super::decoded("100%"), b"100%"); - assert_eq!(super::decoded("%zz"), b"%zz"); - assert_eq!( - super::file_url(Path::new("/a b/中#?.html")), - "file:///a%20b/%E4%B8%AD%23%3F.html" - ); - } -} diff --git a/crates/ely-web-content/src/tokens.rs b/crates/ely-web-content/src/tokens.rs deleted file mode 100644 index 41c9d8b..0000000 --- a/crates/ely-web-content/src/tokens.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! The app's theme as CSS variables on the page's root. - -use ely_gpui_component::theme::{ActiveTheme as _, Palette}; -use gpui::{App, Hsla, Rgba}; - -fn css(color: Hsla) -> String { - let rgba = Rgba::from(color); - let byte = |part: f32| (part.clamp(0.0, 1.0) * 255.0).round() as u8; - format!( - "rgba({}, {}, {}, {:.3})", - byte(rgba.r), - byte(rgba.g), - byte(rgba.b), - rgba.a - ) -} - -/// `--ely-*` names and values, palette then type. -pub fn tokens(cx: &App) -> Vec<(String, String)> { - let theme = cx.theme(); - let colors: &Palette = &theme.colors; - let mut out: Vec<(String, String)> = [ - ("bg", colors.bg), - ("surface", colors.surface), - ("sunken", colors.sunken), - ("hover", colors.hover), - ("border", colors.border), - ("fg", colors.fg), - ("fg-muted", colors.fg_muted), - ("fg-subtle", colors.fg_subtle), - ("accent", colors.accent), - ("on-accent", colors.on_accent), - ("focus", colors.focus), - ("link", colors.link), - ("selection", colors.selection), - ("danger", colors.danger), - ("warning", colors.warning), - ("success", colors.success), - ("keyword", colors.syntax.keyword), - ("string", colors.syntax.string), - ("number", colors.syntax.number), - ("comment", colors.syntax.comment), - ("function", colors.syntax.function), - ("type", colors.syntax.type_name), - ("tag", colors.syntax.tag), - ("attribute", colors.syntax.attribute), - ("punctuation", colors.syntax.punctuation), - ] - .into_iter() - .map(|(name, color)| (format!("--ely-{name}"), css(color))) - .collect(); - out.push(("--ely-font".into(), format!("\"{}\"", theme.font_family))); - out.push(("--ely-mono".into(), format!("\"{}\"", theme.mono_family))); - out.push(("--ely-font-scale".into(), theme.font_scale.to_string())); - let scheme = if theme.is_dark() { "dark" } else { "light" }; - out.push(("color-scheme".into(), scheme.into())); - out -} - -/// Script that sets `tokens` on `:root`. -pub fn script(tokens: &[(String, String)]) -> String { - let pairs = serde_json::to_string(tokens).expect("strings serialize"); - format!("for (const [k, v] of {pairs}) document.documentElement.style.setProperty(k, v);") -} - -#[cfg(test)] -mod tests { - #[test] - fn a_script_sets_each_token_as_json() { - let tokens = vec![("--ely-fg".to_string(), "rgba(1, 2, 3, 1.000)".to_string())]; - let script = super::script(&tokens); - assert!( - script.contains(r#"[["--ely-fg","rgba(1, 2, 3, 1.000)"]]"#), - "{script}" - ); - } -} diff --git a/crates/ely-web-content/src/view.rs b/crates/ely-web-content/src/view.rs deleted file mode 100644 index c12e6c5..0000000 --- a/crates/ely-web-content/src/view.rs +++ /dev/null @@ -1,381 +0,0 @@ -//! A CEF page drawn by GPUI: frames in, events out. - -use std::{ - path::{Path, PathBuf}, - sync::{Arc, Mutex}, -}; - -use anyhow::{Result, bail}; -use cef::{wrapper::message_router::BrowserSideCallback, *}; -use ely_gpui_component::{ - buttons::{Button, ButtonVariant}, - feedback::ErrorView, - primitives::IconName, -}; -use gpui::{ - AnyElement, App, AppContext as _, Bounds, Context, CursorStyle, ElementInputHandler, Entity, - EventEmitter, FocusHandle, Focusable, Hitbox, HitboxBehavior, InteractiveElement as _, - IntoElement, ParentElement as _, Pixels, Render, RenderImage, Styled as _, Window, canvas, div, -}; -use image::{Frame, ImageBuffer, Rgba}; - -use crate::{ - engine::Engine, - surface::{Shared, Surface, file_url}, -}; - -/// A page's answer to one `elyQuery`. -#[derive(Clone)] -pub struct Reply(Arc>); - -impl Reply { - pub fn ok(&self, text: &str) { - self.0.lock().expect("the reply lock").success_str(text); - } - - pub fn fail(&self, message: &str) { - self.0.lock().expect("the reply lock").failure(1, message); - } -} - -/// What the page tells its owner. -pub enum Event { - Loaded, - Query(String, Reply), - /// A link to leave by; the owner decides. - Leave(String), - /// The page cannot show: load, start or run failed. - Failed(String), - /// A PDF print ended: where, and whether it worked. - Printed(PathBuf, bool), -} - -pub struct WebView { - pub(crate) surface: Shared, - image: Option>, - /// Replaced frames, freed from GPUI's atlas next paint. - stale: Vec>, - pub(crate) focus: FocusHandle, - pub(crate) bounds: Bounds, - pub(crate) composing: Option>, - /// The key whose text the input handler brings next. - pub(crate) typing: Option, - url: String, - /// Shown instead of the page until reloaded. - fault: Option, - /// Takes a paste before the page; true when it did. - pub(crate) paste: Option, -} - -/// Takes a paste before the page; true when it did. -pub(crate) type Paste = std::rc::Rc bool>; - -impl EventEmitter for WebView {} - -impl Focusable for WebView { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus.clone() - } -} - -impl WebView { - /// Its frames, for a closing owner to free. - pub fn retire_all(&mut self) -> ely_gpui_component::primitives::Retired { - let mut images = ely_gpui_component::primitives::Retired::default(); - for image in self.image.take().into_iter().chain(self.stale.drain(..)) { - images.push(image); - } - images - } - - /// Opens `page`, a file in `root`, in a new browser. - pub fn open(page: &Path, root: &Path, window: &mut Window, cx: &mut App) -> Result> { - Self::opened(page, root, true, window, cx) - } - - /// Opens `page` with its scripts off: shown, never run. - pub fn open_inert(page: &Path, root: &Path, window: &mut Window, cx: &mut App) -> Result> { - Self::opened(page, root, false, window, cx) - } - - fn opened( - page: &Path, - root: &Path, - scripts: bool, - window: &mut Window, - cx: &mut App, - ) -> Result> { - if !cx.has_global::() { - bail!("the web engine has not started"); - } - let root = std::fs::canonicalize(root)?; - let page = std::fs::canonicalize(page)?; - let mut surface = Surface::new(root, window.scale_factor()); - surface.scripts = scripts; - let surface = Arc::new(Mutex::new(surface)); - let url = file_url(&page); - if !surface.lock().expect("the surface lock").navigable(&url) { - bail!("{} is outside its content folder", page.display()); - } - log::info!("web: a browser asked for {url}"); - crate::engine::ask_browser(surface.clone(), url.clone()); - let view = cx.new(|cx| WebView { - surface, - image: None, - stale: Vec::new(), - focus: cx.focus_handle(), - bounds: Bounds::default(), - composing: None, - typing: None, - url, - fault: None, - paste: None, - }); - crate::input::follow_focus(&view, window, cx); - view.update(cx, |_, cx| { - cx.observe_global::(|view, cx| view.theme(cx)) - .detach(); - }); - cx.global_mut::().views.push(view.downgrade()); - Ok(view) - } - - /// The browser, on the pump's next turn, outside any update. - pub(crate) fn on_browser(&self, work: impl FnOnce(&Browser) + 'static) { - let surface = self.surface.clone(); - crate::engine::later(move || { - // Not held while CEF runs: its callbacks lock it too. - let browser = surface.lock().expect("the surface lock").browser.clone(); - if let Some(browser) = browser { - work(&browser); - } - }); - } - - pub(crate) fn on_host(&self, work: impl FnOnce(&BrowserHost) + 'static) { - self.on_browser(move |browser| { - if let Some(host) = browser.host() { - work(&host); - } - }); - } - - /// Lets images under `dir` load; one folder at a time. - pub fn allow_images(&self, dir: &Path) -> Result<()> { - let dir = std::fs::canonicalize(dir)?; - log::info!("web: images may come from {}", dir.display()); - self.surface.lock().expect("the surface lock").images = Some(dir); - Ok(()) - } - - /// `take` sees pastes first; true keeps them from the page. - pub fn intercept_paste(&mut self, take: impl Fn(&mut Window, &mut App) -> bool + 'static) { - self.paste = Some(std::rc::Rc::new(take)); - } - - /// Runs script in the page's main frame. - pub fn run_js(&self, code: &str) { - let code = CefString::from(code); - self.on_browser(move |browser| { - if let Some(frame) = browser.main_frame() { - frame.execute_java_script(Some(&code), None, 0); - } - }); - } - - /// The app's colors and fonts, into the page. - fn theme(&mut self, cx: &mut Context) { - self.run_js(&crate::tokens::script(&crate::tokens::tokens(cx))); - } - - /// Zoom as a factor; CEF counts in 1.2 steps. - pub fn set_zoom(&self, factor: f64) { - let level = factor.ln() / 1.2f64.ln(); - self.on_host(move |host| host.set_zoom_level(level)); - } - - /// Takes what the handlers left; says if anything came. - fn take(&mut self, cx: &mut Context) { - let (frame, events) = { - let mut surface = self.surface.lock().expect("the surface lock"); - let mut events = Vec::new(); - if std::mem::take(&mut surface.loaded) { - events.push(Event::Loaded); - } - for (request, callback) in surface.queries.drain(..) { - events.push(Event::Query(request, Reply(callback))); - } - events.extend(surface.leaving.drain(..).map(Event::Leave)); - events.extend( - surface - .printed - .drain(..) - .map(|(path, ok)| Event::Printed(path, ok)), - ); - events.extend(surface.failed.take().map(Event::Failed)); - if let Some(why) = crate::engine::failed().filter(|_| !surface.closed) { - surface.closed = true; - events.push(Event::Failed(why)); - } - (surface.frame.take(), events) - }; - if let Some((width, height, bgra)) = frame { - match ImageBuffer::, _>::from_raw(width, height, bgra) { - Some(picture) => { - let image = Arc::new(RenderImage::new([Frame::new(picture)])); - self.stale.extend(self.image.replace(image)); - cx.notify(); - } - None => log::error!("web: a {width}×{height} frame was short"), - } - } - for event in events { - match &event { - // Each load starts unthemed; the tokens go in first. - Event::Loaded => { - self.fault = None; - self.theme(cx); - } - Event::Failed(why) => { - log::error!("web: {} failed: {why}", self.url); - self.fault = Some(why.clone()); - } - _ => {} - } - cx.notify(); - cx.emit(event); - } - } - - /// Follows the room GPUI gave; CEF resizes to it. - fn place(&mut self, bounds: Bounds, scale: f32) { - self.bounds = bounds; - let size = (f32::from(bounds.size.width), f32::from(bounds.size.height)); - let (resized, rescaled) = { - let mut surface = self.surface.lock().expect("the surface lock"); - let changed = (surface.size != size, surface.scale != scale); - (surface.size, surface.scale) = (size, scale); - changed - }; - if resized || rescaled { - self.on_host(move |host| { - if rescaled { - host.notify_screen_info_changed(); - } - host.was_resized(); - }); - } - } - - /// Tries the page again; a lost browser is made anew. - pub fn reload(&mut self, cx: &mut Context) { - self.fault = None; - let made = self - .surface - .lock() - .expect("the surface lock") - .browser - .is_some(); - match made { - true => self.on_browser(|browser| browser.reload()), - false => crate::engine::ask_browser(self.surface.clone(), self.url.clone()), - } - cx.notify(); - } - - /// The fault, with a way to try again. - fn fault_view(&self, why: &str, cx: &mut Context) -> AnyElement { - let this = cx.weak_entity(); - let title = ely_ui::i18n::text(cx, "web.failed", &[]); - let again = Button::new("web-reload", ely_ui::i18n::text(cx, "web.reload", &[])) - .variant(ButtonVariant::Primary) - .on_click(move |_, window, cx| { - // The page, not the gone button, takes keys. - this.update(cx, |view, cx| { - view.reload(cx); - window.focus(&view.focus, cx); - }) - .ok(); - }); - let view = - ErrorView::new("web-failed", IconName::TriangleAlert, title).detail(why.to_string()); - // Without a running engine nothing can try again. - match crate::engine::failed() { - Some(_) => view.into_any_element(), - None => view.action(again).into_any_element(), - } - } - - fn cursor(&self) -> CursorStyle { - match self.surface.lock().expect("the surface lock").cursor { - CursorType::HAND => CursorStyle::PointingHand, - CursorType::IBEAM => CursorStyle::IBeam, - CursorType::CROSS => CursorStyle::Crosshair, - CursorType::EASTWESTRESIZE | CursorType::COLUMNRESIZE => CursorStyle::ResizeLeftRight, - CursorType::NORTHSOUTHRESIZE | CursorType::ROWRESIZE => CursorStyle::ResizeUpDown, - CursorType::NOTALLOWED => CursorStyle::OperationNotAllowed, - _ => CursorStyle::Arrow, - } - } -} - -impl Drop for WebView { - fn drop(&mut self) { - self.surface.lock().expect("the surface lock").closed = true; - self.on_host(|host| host.close_browser(true as _)); - } -} - -/// After each pump: every live view takes its news. -pub(crate) fn deliver(cx: &mut App) { - let views = std::mem::take(&mut cx.global_mut::().views); - let alive: Vec<_> = views - .into_iter() - .filter(|view| view.update(cx, |view, cx| view.take(cx)).is_ok()) - .collect(); - cx.global_mut::().views.extend(alive); -} - -impl Render for WebView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - if let Some(why) = self.fault.clone() { - return div() - .size_full() - .child(self.fault_view(&why, cx)) - .into_any_element(); - } - let this = cx.entity(); - let paint = this.clone(); - let element = canvas( - move |bounds, window, cx| { - let scale = window.scale_factor(); - this.update(cx, |view, _| view.place(bounds, scale)); - window.insert_hitbox(bounds, HitboxBehavior::Normal) - }, - move |bounds, hitbox: Hitbox, window, cx| { - let (image, stale, cursor, focus) = paint.update(cx, |view, _| { - let stale = std::mem::take(&mut view.stale); - (view.image.clone(), stale, view.cursor(), view.focus.clone()) - }); - for old in stale { - if let Err(error) = window.drop_image(old) { - log::error!("web: an old frame stayed: {error}"); - } - } - window.set_cursor_style(cursor, &hitbox); - window.handle_input(&focus, ElementInputHandler::new(bounds, paint.clone()), cx); - let Some(image) = image else { return }; - let painted = - window.paint_image(bounds, bounds, Default::default(), image, 0, false); - if let Err(error) = painted { - log::error!("web: a frame was not painted: {error}"); - } - }, - ) - .size_full(); - crate::input::listen(div().size_full(), cx) - .track_focus(&self.focus) - .child(element) - .into_any_element() - } -} diff --git a/docs/accessibility.md b/docs/accessibility.md index 83a22b5..02a3379 100644 --- a/docs/accessibility.md +++ b/docs/accessibility.md @@ -8,7 +8,7 @@ | --- | --- | --- | | 键盘完成打开、编辑、保存、搜索与主要工具 | 共用命令(`ely_ui::commands`:新建、打开、保存、另存为、导出、打印、关闭标签 Cmd+W、命令面板、设置),各应用的查找(Cmd+F)与模式键;`shell.sh`、`doc-shell.sh`、`mark-tools.sh` 以按键驱动 | 拖动类操作(图片手柄、透视表字段、PDF 区域标记)没有键盘替代 | | 焦点框不依赖悬停 | 组件库可按元素都是 Tab 停靠点并带 `focus_ring`(7fab072 起 git 行带透明边框使焦点框可见) | — | -| 内容区与界面间焦点可预测 | Tab 顺序导航→内容→属性→状态(C05.4,`shell.sh`);浮层关闭后焦点回到打开前之处(组件库 8658999);文档(LOK 视图)与网页(ElyMark)把 Tab 留给自己(制表符、跳单元格、缩进),F6 与 Shift+F6 照 Tab 的次序移出与移回(组件库 029632b;原先没有键盘出口) | CEF 页面内的焦点顺序由页面决定 | +| 内容区与界面间焦点可预测 | Tab 顺序导航→内容→属性→状态(C05.4,`shell.sh`);浮层关闭后焦点回到打开前之处(组件库 8658999);文档(LOK 视图)把 Tab 留给自己(制表符、跳单元格、缩进),F6 与 Shift+F6 照 Tab 的次序移出与移回(组件库 029632b;原先没有键盘出口) | ElyMark 原生组件的焦点次序待 C14.16 核对 | | 对比度 4.5:1 | 主题与 Omarchy 色板按组件库 `A11yChecker` 规则收口(正文、状态色 4.5:1,焦点与图表色 3:1),核对不过的主题整套不用(C05.1) | 文档内容的颜色取自文件,照原样显示 | ## 辅助技术 @@ -16,7 +16,7 @@ | 要求 | 依据 | 缺口 | | --- | --- | --- | | 控件有角色与名称 | 组件库核心控件、对话框、列表、树、提交与差异行、设置行(组与说明)、提示(Toast 有角色与名称);真窗口测试都经 AX 树操作(`ax-values`、`ax-press`) | `lists::Outline` 与 `feedback::Alert` 无角色(上游待办);gpui 无 aria-disabled,以说明文字表达 | -| 文档内容可达 | LOK 视图把当前段落(Calc 为当前单元格与输入行)以 `MultilineTextInput` 交给 AX(`lokview.sh` 读到键入后的段落) | gpui 没有文本选区与插入点的 AX:选区、插入点、整篇文档到不了辅助技术;ElyMark 与内容宿主的页面不进 AX 树(CEF 离屏);PDF 页面文字未接 AX | +| 文档内容可达 | LOK 视图把当前段落(Calc 为当前单元格与输入行)以 `MultilineTextInput` 交给 AX(`lokview.sh` 读到键入后的段落) | gpui 没有文本选区与插入点的 AX:选区、插入点、整篇文档到不了辅助技术;ElyMark 原生组件的 AX 待 C14.16 核对;PDF 页面文字未接 AX | | 编辑状态可达 | 保存状态、风险、引擎停止以提示条与状态栏文字表达 | 状态栏文字是否进 AX 树未逐项核对 | ## 国际化 @@ -24,7 +24,7 @@ | 要求 | 依据 | 缺口 | | --- | --- | --- | | 简中与英文界面 | `ely_ui::i18n` 缺键即失败;组件库面板族、对话框、批注栏等内建中英目录;设置可切语言(下次启动生效) | 组件库 `SortBuilder`、`FilterBuilder`、欢迎页等文案仍为英文(上游待办);ElyTable 选区统计以外的内核文字为英文 | -| 中英文输入、组合输入、标点 | LOK 视图与 CEF 页面都经输入法回调处理组字(`lokview.sh`、`webview.sh` 组“你好”);往返用例含中文(`independent.rs`) | 系统拼音输入法实机只做过人工检查,测试以输入法回调模拟 | +| 中英文输入、组合输入、标点 | LOK 视图与 ElyMark 的编辑器都经输入法回调处理组字(`lokview.sh` 组“你好”;组件库的文档测试);往返用例含中文(`independent.rs`) | 系统拼音输入法实机只做过人工检查,测试以输入法回调模拟 | | 混合字体 | 内核按字体回退排版;缺失字体在 ElyDoc 页面上方列出 | 字体缺失环境的分页对照未做(见 `docs/compatibility.md`) | | 非 ASCII 路径 | `paths_keep_spaces_cjk_and_case`(保存);启动参数按当前目录取绝对路径(C20.1 修) | — | | 日期、数字、纸张用显式区域 | 内核载入选项固定 `Language=en-US`(`ely-lok/src/worker.rs` 的 `LOAD_OPTIONS`),公式为 Excel A1 语法;纸张取自文档本身并原样打印(C19.2) | — | diff --git a/docs/compatibility.md b/docs/compatibility.md index 083fed8..f3face4 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -3,7 +3,7 @@ 随发行说明发布,每个版本一份。支持某格式不等于支持它的全部功能;下表只写已验证的内容、已知限制与保存行为。 版本:Ely Office 0.1.3(首版六个应用:ElyDoc、ElyTable、ElyShow、ElyPDF、ElyTree、ElyMark)。日期 2026-10-11。 -内核:LibreOffice 26.2.6.3(8221e31,加 `patches/libreoffice` 0001–0041);MuPDF 1.28.5(8ad45e9,加 `patches/mupdf` 0001–0005);Git 2.56.0(a018953)与 git-lfs 3.8.0;CEF 154.0.32。 +内核:LibreOffice 26.2.6.3(8221e31,加 `patches/libreoffice` 0001–0041);MuPDF 1.28.5(8ad45e9,加 `patches/mupdf` 0001–0005);Git 2.56.0(a018953)与 git-lfs 3.8.0。 验证机:macOS 27.0 arm64。Windows 与 Linux 未验证。本机没有 Microsoft Office:与 Office 的交叉检查未做(待做)。 ## 往返验证 @@ -66,9 +66,9 @@ | | 格式 | | --- | --- | -| 编辑与保存 | MD、Markdown(UTF-8,带不带 BOM;保留换行风格;未改动的块逐字节照原文写回) | -| 导入或受限 | UTF-16 与非 UTF-8 拒绝打开并指出字节位置;Milkdown 不支持的语法以原文块保留;超过 2 MiB 只以源码打开 | -| 输出 | HTML(单文件或带资源文件夹)、PDF、纯文本、打印(macOS) | +| 编辑与保存 | MD、Markdown(UTF-8,带不带 BOM;保留换行风格;文本即原文,只写回所改的字) | +| 导入或受限 | UTF-16 与非 UTF-8 拒绝打开并指出字节位置;markdown-rs 不认的语法照原文显示;超过 2 MiB 只以源码打开 | +| 输出 | 原生重做中(C14.14),此版没有 | ## 第二批 diff --git a/docs/help.md b/docs/help.md index ff3832b..bf9beed 100644 --- a/docs/help.md +++ b/docs/help.md @@ -19,7 +19,7 @@ - 有未保存改动时,每 10 秒在本机数据目录写一个恢复点(加密的文档其恢复点也加密),只留在本机,不上传。 - 应用崩溃后再启动,未保存的内容以未命名副本打开,原文件不动;只恢复内容,光标与撤销历史不恢复。 - 文档引擎单独停止时,窗口上方写明原因与最近一次恢复点的时间,“重新打开文档”起新引擎并逐个重开:有改动的从恢复点、无改动的从文件。 -- 设置“存储”里可看日志、网页缓存、恢复副本的大小,选每个文档保留几个恢复点与多少空间,并清除它们(先问)。 +- 设置“存储”里可看日志、恢复副本的大小,选每个文档保留几个恢复点与多少空间,并清除它们(先问)。 ## 离线边界 diff --git a/docs/licenses.md b/docs/licenses.md index ed4c9a4..1745e44 100644 --- a/docs/licenses.md +++ b/docs/licenses.md @@ -10,7 +10,7 @@ Ely Office 的自有代码以 AGPL-3.0-or-later 发布,全文在仓库根的 ` | 文件 | 内容 | 生成 | | --- | --- | --- | -| `ElyOffice-<版本>-source.tar.gz` | 自有源码、`Cargo.lock`、`rust-toolchain.toml`、ElyMark 网页的 `package-lock.json`、上游补丁(`patches/libreoffice`、`patches/mupdf`)、构建与取上游的脚本(各上游钉在提交,脚本里写明地址与散列)、测试、文档 | `packaging/macos/release.sh` | +| `ElyOffice-<版本>-source.tar.gz` | 自有源码、`Cargo.lock`、`rust-toolchain.toml`、上游补丁(`patches/libreoffice`、`patches/mupdf`)、构建与取上游的脚本(各上游钉在提交,脚本里写明地址与散列)、测试、文档 | `packaging/macos/release.sh` | | `ElyOffice-<版本>-upstream-source.tar.gz` | 实际编译的 MuPDF 源码(钉住的提交加我们的补丁,AGPL)与随包 Git 的源码(v2.56.0,GPLv2) | 同上;`tests/scripts/release-macos.sh` 核对其中是打过补丁的 MuPDF | | 各应用包 `Resources/Licenses/` | Ely 的 AGPL 全文;`rust-crates.txt`(链进该应用各二进制的全部 crate 与其许可文件,无许可文件的取标准文本并写明作者);所带引擎的许可与通知(见下) | `packaging/licenses.sh`,缺任何一份即打包失败 | @@ -26,8 +26,7 @@ Ely Office 的自有代码以 AGPL-3.0-or-later 发布,全文在仓库根的 ` | Great Vibes 字体 | OFL-1.1 | OFL | PDF | | Git 2.56.0 | GPL-2.0(部分 LGPL-2.1) | COPYING、LGPL-2.1;源码在上游源码包 | Tree(Mark 使用同级 Tree 包内的 Git) | | git-lfs 3.8.0 | MIT | LICENSE.md(按标签与散列取得) | Tree | -| CEF 154 / Chromium | BSD 及其依赖各自许可 | CEF LICENSE、Chromium CREDITS | Mark | -| Milkdown、CodeMirror、KaTeX、Mermaid 等网页依赖 | 各自许可(多为 MIT) | `THIRD-PARTY-NOTICES.txt`,按打进包的每个包取其许可文件 | Mark | +| KaTeX 字体(随 RaTeX 嵌入) | OFL-1.1 | OFL、FONT_NOTICE | Mark | | 媒体播放 | 系统框架(AVFoundation),不随包 | — | Show | ## 未尽之处 diff --git a/docs/perf/README.md b/docs/perf/README.md index 9f2bec4..393b85e 100644 --- a/docs/perf/README.md +++ b/docs/perf/README.md @@ -12,6 +12,6 @@ | repo | 10 万个提交 | PERF-06 | | notes.md | 1 MB | PERF-07 | -计时从启动进程起,到日志里出现标记为止(脚本每 10 毫秒读一次日志):PERF-01 到首帧(`ELY_SMOKE=1` 打印 `ready`),首次运行(新的用户目录)与日常启动分记,每次等上一次的进程(含仍在启动的内核)退出后再测;Doc、Table、Show 另记空白文档就绪(`opened Untitled`),它要等内核启动;PERF-03、04 到第一块图块真正画到窗口上(`lok: first tile drawn`,在 `paint_image` 成功之后记;其间内核画图块失败或窗口画图块失败即整次作废),PERF-03 之前先核对样本经内核排版恰为 100 页(`lok: 100 pages`);PERF-05 到第一页的第一块画到窗口上(`pdf: first tile drawn, page 0`;画失败即整次作废);PERF-06 的历史首屏从按下“历史”到显示(`history shows`),查询从按下回车到结果(`searched true`),性能要求的指标是后者;PERF-07 到可视编辑器显示(`mark: shown as visual`)。PERF-02 为 ElyDoc 每次按键到显示该编辑的图块(`lok: edit to tile`,每键须各有一条,缺即失败),并以 `ELY_STALLS=1` 记下主线程连续忙于 100 毫秒以上的次数(`main thread was busy`)。内存为应用及其全部子进程(内核 worker、CEF 辅助进程)的 RSS 之和。PERF-08 为 ElyDoc 新建再关闭文档 40 次:循环前记下基线(总内存、全部句柄的类型与名称、内核中的文档数 `lok-worker: N documents open`),每次关闭后等内核回到基线文档数(等不到或没有这条日志即失败),再按身份比对句柄:比基线多出的只许是 `/System/Library/` 下的只读系统资源(第一次打开模板对话框时一次性载入两个系统字体与 AppKit 的两个本地化表),且此后每次都相同;多出任何别的句柄即失败。记录写基线与每次的内存、句柄数、文档数。 +计时从启动进程起,到日志里出现标记为止(脚本每 10 毫秒读一次日志):PERF-01 到首帧(`ELY_SMOKE=1` 打印 `ready`),首次运行(新的用户目录)与日常启动分记,每次等上一次的进程(含仍在启动的内核)退出后再测;Doc、Table、Show 另记空白文档就绪(`opened Untitled`),它要等内核启动;PERF-03、04 到第一块图块真正画到窗口上(`lok: first tile drawn`,在 `paint_image` 成功之后记;其间内核画图块失败或窗口画图块失败即整次作废),PERF-03 之前先核对样本经内核排版恰为 100 页(`lok: 100 pages`);PERF-05 到第一页的第一块画到窗口上(`pdf: first tile drawn, page 0`;画失败即整次作废);PERF-06 的历史首屏从按下“历史”到显示(`history shows`),查询从按下回车到结果(`searched true`),性能要求的指标是后者;PERF-07 到可视编辑器显示(`mark: shown as visual`)。PERF-02 为 ElyDoc 每次按键到显示该编辑的图块(`lok: edit to tile`,每键须各有一条,缺即失败),并以 `ELY_STALLS=1` 记下主线程连续忙于 100 毫秒以上的次数(`main thread was busy`)。内存为应用及其全部子进程(内核 worker)的 RSS 之和。PERF-08 为 ElyDoc 新建再关闭文档 40 次:循环前记下基线(总内存、全部句柄的类型与名称、内核中的文档数 `lok-worker: N documents open`),每次关闭后等内核回到基线文档数(等不到或没有这条日志即失败),再按身份比对句柄:比基线多出的只许是 `/System/Library/` 下的只读系统资源(第一次打开模板对话框时一次性载入两个系统字体与 AppKit 的两个本地化表),且此后每次都相同;多出任何别的句柄即失败。记录写基线与每次的内存、句柄数、文档数。 记录开头的 JSON 写机型、CPU、核数、内存、GPU、系统、系统字体文件数、各应用发行二进制的散列与样本散列。冷启动未清系统文件缓存(需要管理员权限),“日常启动”即文件缓存已热时的启动。 diff --git a/packaging/licenses.sh b/packaging/licenses.sh index b440006..1cc4bff 100755 --- a/packaging/licenses.sh +++ b/packaging/licenses.sh @@ -21,7 +21,6 @@ put . "$root/LICENSE" case $slug in ely-doc | ely-table | ely-show) crates=(ely-lok) ;; ely-pdf) crates=(ely-pdf-engine) ;; - ely-mark) crates=(ely-web-content) ;; *) crates=() ;; esac python3 "$root/packaging/crates.py" "$slug" ${crates[@]+"${crates[@]}"} >"$out/rust-crates.txt" @@ -81,8 +80,10 @@ case $slug in put GreatVibes "$root/crates/ely-pdf-engine/fonts/OFL.txt" ;; ely-mark) - put CEF "$root/reference/cef/LICENSE.txt" "${ELY_CEF:-$root/engines/cef}/CREDITS.html" - put content "$root/engines/content/ely-mark/THIRD-PARTY-NOTICES.txt" + # RaTeX embeds KaTeX's fonts. + katex=$(cargo metadata --format-version 1 --locked --manifest-path "$root/Cargo.toml" | + python3 -c 'import json, pathlib, sys; print(next(pathlib.Path(p["manifest_path"]).parent for p in json.load(sys.stdin)["packages"] if p["name"] == "ratex-katex-fonts"))') + put KaTeX "$katex/fonts/OFL.txt" "$katex/fonts/FONT_NOTICE.txt" ;; esac # ElyTree carries Git; on Windows and Linux ElyMark does too. diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh index 2f391ef..ccb373e 100755 --- a/packaging/linux/package.sh +++ b/packaging/linux/package.sh @@ -106,20 +106,9 @@ for helper in ely-lok-worker ely-pdf-worker; do deployed+=(--executable "$bin/$helper") fi done -if [ -f "$bin/libcef.so" ]; then - # Binaries here load from usr/lib; CEF's data sits with libcef. - cef=${ELY_CEF:-$root/engines/cef} - mkdir -p "$appdir/usr/lib" - for item in "$bin"/*; do - if [ -e "$cef/${item##*/}" ]; then - mv "$item" "$appdir/usr/lib/" - fi - done - deployed+=(--library "$appdir/usr/lib/libcef.so") -fi # linuxdeploy bundles libraries and fails on a missing one. -LD_LIBRARY_PATH=$appdir/usr/lib "$deploy" --appimage-extract-and-run --appdir "$appdir" "${deployed[@]}" \ +"$deploy" --appimage-extract-and-run --appdir "$appdir" "${deployed[@]}" \ --desktop-file "$work/$slug.desktop" --icon-file "$icon" mkdir -p "$dist" diff --git a/packaging/macos/bundle.sh b/packaging/macos/bundle.sh index 3addf56..f836a45 100755 --- a/packaging/macos/bundle.sh +++ b/packaging/macos/bundle.sh @@ -66,7 +66,7 @@ if uses ely-lok; then echo "bundle.sh: $binary is not code; Frameworks holds only code" >&2 exit 1 fi - done < <(find "$app/Contents/Frameworks" -type f -not -path "*/Chromium Embedded Framework.framework/*") + done < <(find "$app/Contents/Frameworks" -type f) fi if uses ely-pdf-engine; then tessdata=${ELY_TESSDATA:-$root/engines/tessdata} @@ -84,53 +84,6 @@ if [ "$slug" = ely-tree ]; then file -b "$binary" | grep -q '^Mach-O' && inner+=("$binary") done < <(find "$app/Contents/Resources/git" -type f -perm -111) fi -# Web content apps carry CEF and their bundled pages. -framework= -helpers=() -if grep -q '^ely-web-content' "$root/apps/$slug/Cargo.toml"; then - cef=${ELY_CEF:-$root/engines/cef} - [ -d "$cef/Chromium Embedded Framework.framework" ] || { - echo "bundle.sh: no CEF at $cef; run scripts/fetch-cef.sh" >&2 - exit 1 - } - mkdir -p "$app/Contents/Frameworks" - framework="$app/Contents/Frameworks/Chromium Embedded Framework.framework" - cp -R "$cef/Chromium Embedded Framework.framework" "$framework" - case $profile in - release) cargo build --manifest-path "$root/Cargo.toml" --release -p ely-web-content --bin ely-web-helper ;; - debug) cargo build --manifest-path "$root/Cargo.toml" -p ely-web-content --bin ely-web-helper ;; - esac - # Chromium looks for these five, named after the app's binary. - for kind in "" " (GPU)" " (Renderer)" " (Plugin)" " (Alerts)"; do - helper="$slug Helper$kind" - contents="$app/Contents/Frameworks/$helper.app/Contents" - mkdir -p "$contents/MacOS" - cp "$root/target/$profile/ely-web-helper" "$contents/MacOS/$helper" - id=$(echo "${slug#ely-}.helper$kind" | tr -d '()' | tr ' ' '.' | tr 'A-Z' 'a-z') - cat >"$contents/Info.plist" < - - - - CFBundleExecutable$helper - CFBundleIdentifier$prefix.$id - CFBundleName$helper - CFBundlePackageTypeAPPL - CFBundleShortVersionString$version - LSMinimumSystemVersion14.0 - LSUIElement1 - - -HELPER - helpers+=("$app/Contents/Frameworks/$helper.app") - done - content=$root/engines/content/$slug - [ -f "$content/index.html" ] || { - echo "bundle.sh: no page at $content; run its scripts/build-*-content.sh" >&2 - exit 1 - } - cp -R "$content" "$app/Contents/Resources/content" -fi "$root/packaging/licenses.sh" "$slug" "$app/Contents/Resources/Licenses" cat >"$app/Contents/Info.plist" < @@ -153,18 +106,11 @@ $("$root/packaging/macos/documents.sh" "$slug") PLIST # Nested code is signed before the app that holds it. -for target in ${inner[@]+"${inner[@]}"} ${framework:+"$framework"} ${helpers[@]+"${helpers[@]}"} "$app"; do +for target in ${inner[@]+"${inner[@]}"} "$app"; do if [ "$identity" = - ]; then codesign --force --sign - "$target" else - # Hardened runtime: V8 in CEF's helpers needs its entitlements. - entitled=() - case $target in - *" Helper (Renderer).app") entitled=(--entitlements "$root/packaging/macos/entitlements/renderer.plist") ;; - *" Helper (GPU).app") entitled=(--entitlements "$root/packaging/macos/entitlements/gpu.plist") ;; - *" Helper (Plugin).app") entitled=(--entitlements "$root/packaging/macos/entitlements/plugin.plist") ;; - esac - codesign --force --options runtime --timestamp ${entitled[@]+"${entitled[@]}"} --sign "$identity" "$target" + codesign --force --options runtime --timestamp --sign "$identity" "$target" fi done echo "$app" diff --git a/packaging/macos/entitlements/gpu.plist b/packaging/macos/entitlements/gpu.plist deleted file mode 100644 index 22c09be..0000000 --- a/packaging/macos/entitlements/gpu.plist +++ /dev/null @@ -1,7 +0,0 @@ - - - - - com.apple.security.cs.allow-jit - - diff --git a/packaging/macos/entitlements/plugin.plist b/packaging/macos/entitlements/plugin.plist deleted file mode 100644 index 1d0155d..0000000 --- a/packaging/macos/entitlements/plugin.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.cs.allow-unsigned-executable-memory - com.apple.security.cs.disable-library-validation - - diff --git a/packaging/macos/entitlements/renderer.plist b/packaging/macos/entitlements/renderer.plist deleted file mode 100644 index 22c09be..0000000 --- a/packaging/macos/entitlements/renderer.plist +++ /dev/null @@ -1,7 +0,0 @@ - - - - - com.apple.security.cs.allow-jit - - diff --git a/packaging/stage.sh b/packaging/stage.sh index d163d0d..311351e 100755 --- a/packaging/stage.sh +++ b/packaging/stage.sh @@ -65,16 +65,5 @@ if [ "$slug" = ely-tree ] || [ "$slug" = ely-mark ]; then need "$root/engines/git" "scripts/build-git.sh and scripts/fetch-git-lfs.sh" cp -R "$root/engines/git" "$out/git" fi -if uses ely-web-content; then - cef=${ELY_CEF:-$root/engines/cef} - need "$cef/icudtl.dat" scripts/fetch-cef.sh - # CEF's runtime sits beside the app, as cef-dll-sys has it. - find "$cef" -maxdepth 1 -type f \( -name '*.dll' -o -name '*.so' -o -name '*.so.*' -o -name '*.pak' \ - -o -name '*.bin' -o -name '*.dat' -o -name '*.json' -o -name chrome-sandbox \) -exec cp {} "$out/" \; - cp -R "$cef/locales" "$out/" - content=$root/engines/content/$slug - need "$content/index.html" "its scripts/build-*-content.sh" - cp -R "$content" "$out/content" -fi "$root/packaging/licenses.sh" "$slug" "$out/licenses" echo "$out" diff --git a/scripts/build-mark-content.sh b/scripts/build-mark-content.sh deleted file mode 100755 index 790d56e..0000000 --- a/scripts/build-mark-content.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash -# Builds ElyMark's page, pinned by the lockfile, into engines/content. -set -euo pipefail - -root=$(cd "$(dirname "$0")/.." && pwd) -web=$root/apps/ely-mark/web -cd "$web" -# Install scripts never run; esbuild's binary is its own package. -npm ci --ignore-scripts --no-audit --no-fund -node build.mjs "$root/engines/content/ely-mark" diff --git a/scripts/ci.sh b/scripts/ci.sh index 20d5ef8..4ee05f2 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -40,11 +40,9 @@ if [ -n "${ELY_SHARE:-}" ]; then else scripts/fetch-references.sh ${inside[@]+"${inside[@]}"} scripts/build-mupdf.sh - ${inside[@]+"${inside[@]}"} scripts/fetch-cef.sh ${inside[@]+"${inside[@]}"} scripts/build-git.sh ${inside[@]+"${inside[@]}"} scripts/fetch-git-lfs.sh ${inside[@]+"${inside[@]}"} scripts/fetch-tessdata.sh - ${inside[@]+"${inside[@]}"} scripts/build-mark-content.sh ${inside[@]+"${inside[@]}"} scripts/build-libreoffice.sh fi diff --git a/scripts/engines-key.sh b/scripts/engines-key.sh index e536d3f..17e5792 100755 --- a/scripts/engines-key.sh +++ b/scripts/engines-key.sh @@ -4,5 +4,5 @@ set -euo pipefail root=$(cd "$(dirname "$0")/.." && pwd) cd "$root" find scripts/fetch-references.sh scripts/apply-lo-patches.sh scripts/build-libreoffice.sh scripts/build-mupdf.sh \ - scripts/build-git.sh scripts/fetch-git-lfs.sh scripts/fetch-tessdata.sh scripts/fetch-cef.sh \ + scripts/build-git.sh scripts/fetch-git-lfs.sh scripts/fetch-tessdata.sh \ packaging/libreoffice patches -type f | LC_ALL=C sort | xargs shasum -a 256 | shasum -a 256 | cut -c1-12 diff --git a/scripts/fetch-cef.sh b/scripts/fetch-cef.sh deleted file mode 100755 index f9cc9ef..0000000 --- a/scripts/fetch-cef.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -# CEF's minimal build at cef-rs's pin, SHA-1 checked, into engines/cef. -set -euo pipefail - -root=$(cd "$(dirname "$0")/.." && pwd) -[ -d "$root/reference/cef-rs" ] || { echo "fetch-cef: run scripts/fetch-references.sh first" >&2; exit 1; } -# Not the system temp: Windows may block programs there. -mkdir -p "$root/engines" -work=$(mktemp -d "$root/engines/cef-export.XXXXXX") -trap 'rm -rf "$work"' EXIT -cd "$root/reference/cef-rs" -# The exporter checks each archive against CEF's own index. -CARGO_TARGET_DIR="$work" cargo run -q -p export-cef-dir -- --force "$root/engines/cef" -rm -f "$root"/engines/cef_binary_*.tar.bz2 -cat "$root/engines/cef/archive.json" diff --git a/scripts/fetch-references.sh b/scripts/fetch-references.sh index 295590a..774ccef 100755 --- a/scripts/fetch-references.sh +++ b/scripts/fetch-references.sh @@ -48,10 +48,6 @@ fetch tesseract https://github.com/tesseract-ocr/tesseract.git db0ec62f81b0737fb fetch mailcore2 https://github.com/MailCore/mailcore2.git 1b48bab20b2492fa8ed3965abcbbe099d4ce9d92 git -C "$ref/mailcore2" submodule update --init --depth 1 --recursive fetch libetpan https://github.com/dinhviethoa/libetpan.git 4d478625c4c43721191ae1bd7aed387a66c28ec0 -fetch cef https://github.com/chromiumembedded/cef.git ff57d4eae16d36457895f2de115a71d502e85a08 -fetch cef-rs https://github.com/tauri-apps/cef-rs.git a712ad9bacd234c050631a95b9745b27d346b7cc -fetch milkdown https://github.com/Milkdown/milkdown.git bbb8bcbbb8ce0aa16f92567c23f6236747be9b3b -fetch codemirror-dev https://github.com/codemirror/dev.git c010426d06689a7115aa9df08425126b6d3ead2f fetch marktext https://github.com/marktext/marktext.git b29cb5621e12cedb801c1cd886d9005159feaa2e fetch colamd https://github.com/marswaveai/ColaMD.git 5e0f3c3584bb771180afe1a4670efbaf28dee65b fetch magpie https://github.com/yetone/magpie.git e4c8316e3fa9d73683079df3f9142561265bc1bb diff --git a/tests/scripts/comments-rules.sh b/tests/scripts/comments-rules.sh index 4a4ecb8..f2a21b4 100755 --- a/tests/scripts/comments-rules.sh +++ b/tests/scripts/comments-rules.sh @@ -10,17 +10,7 @@ fail() { exit 1 } check() { python3 "$root/tests/scripts/comments.py" "$work" >/dev/null 2>&1; } -mkdir -p "$work/apps/a/web/src" "$work/tests/scripts" -printf '// One short line.\nconst a = 1\n' >"$work/apps/a/web/src/a.js" -check || fail "a short comment failed" -printf '/* A comment\n spanning lines. */\n' >"$work/apps/a/web/src/a.js" -! check || fail "a multi-line JS block comment passed" -printf 'const a = 1 /* A comment\n spanning lines. */\n' >"$work/apps/a/web/src/a.js" -! check || fail "an inline multi-line JS block comment passed" -printf '// One two three four five six seven eight nine ten eleven.\n' >"$work/apps/a/web/src/a.js" -! check || fail "an eleven-word JS comment passed" -rm "$work/apps/a/web/src/a.js" -mkdir -p "$work/site/src" +mkdir -p "$work/site/src" "$work/tests/scripts" for ext in ts css; do printf '/* A comment\n spanning lines. */\n' >"$work/site/src/a.$ext" ! check || fail "a multi-line .$ext block comment passed" @@ -30,6 +20,8 @@ for ext in ts css; do check || fail "a short .$ext comment failed" rm "$work/site/src/a.$ext" done +printf 'const a = 1 /* A comment\n spanning lines. */\n' >"$work/site/src/a.ts" +! check || fail "an inline multi-line .ts block comment passed" printf 'const glob = "/*.css";\n' >"$work/site/src/a.ts" check || fail "a /* inside a .ts string failed" printf 'const a = 1; // One two three four five six seven eight nine ten eleven.\n' >"$work/site/src/a.ts" diff --git a/tests/scripts/comments.py b/tests/scripts/comments.py index 885da47..e07ea97 100755 --- a/tests/scripts/comments.py +++ b/tests/scripts/comments.py @@ -20,7 +20,6 @@ PATTERNS = [ "crates/**/*.h", "patches/**/*.patch", "tests/**/*.swift", - "apps/*/web/src/**/*.js", "site/src/**/*.ts", "site/src/**/*.css", "site/test/**/*.ts", @@ -37,7 +36,7 @@ LINE_COMMENT = { } # Scanned by tokens: strings skipped, trailing comments found. -SCANNED = {".js": True, ".ts": True, ".css": False} +SCANNED = {".ts": True, ".css": False} def scan(text, slashes): diff --git a/tests/scripts/hardened-macos.sh b/tests/scripts/hardened-macos.sh deleted file mode 100755 index 7f8eb51..0000000 --- a/tests/scripts/hardened-macos.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -# ElyMark signed with hardened runtime still draws its page. -set -euo pipefail - -root=$(cd "$(dirname "$0")/../.." && pwd) -[ "$(uname)" = Darwin ] || { - echo "SKIP hardened-macos: macOS only" - exit 0 -} -[ -n "${ELY_SIGN_IDENTITY:-}" ] || { - echo "SKIP hardened-macos: set ELY_SIGN_IDENTITY to a signing identity" - exit 0 -} -work=$(cd "$(mktemp -d)" && pwd -P) -trap 'pkill -f "$work/ElyMark.app/Contents/MacOS/" 2>/dev/null || true; sleep 1; rm -rf "$work"' EXIT -fail() { - echo "FAIL: $*" >&2 - exit 1 -} -ELY_DIST=$work "$root/packaging/macos/bundle.sh" ely-mark debug >"$work/bundle.log" 2>&1 || - fail "bundle: $(tail -n 3 "$work/bundle.log")" -app=$work/ElyMark.app -codesign --verify --deep --strict "$app" || fail "the signature does not verify" -codesign -d --entitlements - "$app/Contents/Frameworks/ely-mark Helper (Renderer).app" 2>/dev/null | - grep -q allow-jit || fail "the renderer helper has no JIT entitlement" -printf '# Hardened\n' >"$work/t.md" -open -n -a "$app" --env "HOME=$work/home" --env ELY_LOG=info --env ELY_NO_DIALOG=1 "$work/t.md" -log="$work/home/Library/Application Support/ElyOffice/ely-mark/ely.log" -for _ in $(seq 240); do - grep -q 'mark: shown as' "$log" 2>/dev/null && { - echo "hardened-macos: the signed page draws" - exit 0 - } - sleep 0.25 -done -fail "the page never showed: $(grep -E 'web:|mark:' "$log" 2>/dev/null | tail -n 5)" diff --git a/tests/scripts/linux-distros.sh b/tests/scripts/linux-distros.sh index 3a5abd8..8263c6f 100755 --- a/tests/scripts/linux-distros.sh +++ b/tests/scripts/linux-distros.sh @@ -31,7 +31,7 @@ docker build -q -t ely-smoke-fedora - >/dev/null <<'EOF' FROM fedora:44@sha256:43b29f65a41eb9c35e1cd5323e3bdf3b655c2357a9f4f1ff2f9c2798e5045d80 RUN dnf install -y -q xorg-x11-server-Xvfb mesa-vulkan-drivers xwd xwininfo && dnf clean all EOF -# As the work dir's owner: Git and CEF refuse root. +# As the work dir's owner: Git refuses others' repos. inside='set -euo pipefail slug=$1 work=$2 file=$3 shift 3 diff --git a/tests/scripts/linux-glibc.sh b/tests/scripts/linux-glibc.sh index a0ef368..19993ea 100755 --- a/tests/scripts/linux-glibc.sh +++ b/tests/scripts/linux-glibc.sh @@ -17,7 +17,7 @@ done release=$root/target/ubuntu/release lo=$root/engines/lo-src/instdir/program required=("$release"/ely-{doc,table,show,pdf,tree,mark,lok-worker,pdf-worker} "$lo/libmergedlo.so" - "$root/engines/git/bin/git" "$root/engines/cef/libcef.so") + "$root/engines/git/bin/git") for file in "${required[@]}"; do [ -f "$file" ] || fail "no $file; build in packaging/linux/container.sh" magic=$(head -c 4 "$file" | od -An -c | tr -d ' ') || fail "cannot read $file" @@ -26,7 +26,7 @@ done list=$(mktemp) trap 'rm -f "$list"' EXIT find -L "$release"/ely-{doc,table,show,pdf,tree,mark,lok-worker,pdf-worker} "$lo" "$root/engines/git" \ - "$root/engines/cef" -type f -print0 >"$list" || fail "cannot list every file" + -type f -print0 >"$list" || fail "cannot list every file" checked=0 while IFS= read -r -d '' file; do magic=$(head -c 4 "$file" | od -An -c | tr -d ' ') || fail "cannot read $file" diff --git a/tests/scripts/linux-mark-idle.sh b/tests/scripts/linux-mark-idle.sh deleted file mode 100755 index 2c325aa..0000000 --- a/tests/scripts/linux-mark-idle.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env bash -# Idle on Linux: ElyMark rereads no theme, settings or file. -set -euo pipefail - -root=$(cd "$(dirname "$0")/../.." && pwd) -fail() { - echo "FAIL: $*" >&2 - exit 1 -} -[ "$(uname)" = Linux ] || { - echo "SKIP: inotify is Linux" - exit 0 -} -[ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ] || { - echo "SKIP: no display" - exit 0 -} -for tool in inotifywait jq hyprctl grim magick tesseract; do - command -v "$tool" >/dev/null || { - echo "SKIP: no $tool" - exit 0 - } -done -[ -f "$root/engines/cef/libcef.so" ] || fail "no engines/cef; run scripts/fetch-cef.sh" -[ -f "$root/engines/content/ely-mark/index.html" ] || fail "no ElyMark page; run scripts/build-mark-content.sh" -app=$(cargo build -q --locked --message-format=json --manifest-path "$root/Cargo.toml" -p ely-mark --bin ely-mark | - jq -r 'select(.reason == "compiler-artifact" and .target.name == "ely-mark") | .executable // empty') -[ -x "$app" ] || fail "Cargo reported no ElyMark binary" - -work=$(mktemp -d) -pid= -spy= -trap '[ -z "$spy" ] || kill "$spy" 2>/dev/null; [ -z "$pid" ] || stop_app "$pid"; rm -rf "$work"' EXIT -# shellcheck source=tests/scripts/linux-common.sh -. "$root/tests/scripts/linux-common.sh" -current=$work/omarchy/current -mkdir -p "$current/theme" "$work/data/ElyOffice" "$work/notes" -cp "$root/crates/ely-ui/tests/fixtures/omarchy/catppuccin.toml" "$current/theme/colors.toml" -echo catppuccin >"$current/theme.name" -echo '{"version": 1}' >"$work/data/ElyOffice/settings.json" -note=$work/notes/note.md -printf '# Idle\n\nNothing moves.\n' >"$note" - -LD_LIBRARY_PATH="$root/engines/cef" ELY_OMARCHY_DIR="$current" ELY_SMOKE=hold ELY_LOG=info HOME="$work" \ - XDG_DATA_HOME="$work/data" setsid "$app" "$note" 2>"$work/app.err" & -pid=$! -seen() { grep -c "$1" "$work/app.err" || true; } -logged() { # pattern, what, count before - local before=${3:-0} - for _ in $(seq 120); do - [ "$(seen "$1")" -gt "$before" ] && return - kill -0 "$pid" 2>/dev/null || fail "ElyMark quit: $(tail -n 5 "$work/app.err")" - sleep 0.25 - done - fail "ElyMark missed $2" -} -logged 'mark: watching 1 folders' "its file's folder" -logged 'omarchy catppuccin applied' "the Omarchy theme" -sleep 3 - -# Four quiet seconds: nothing opens the theme, settings or note. -idle() { # when - inotifywait -q -m -e open --format '%w%f' "$current" "$current/theme" "$work/data/ElyOffice" "$work/notes" \ - >"$work/opens" 2>/dev/null & - spy=$! - for _ in 1 2 3 4 5; do - sleep 1 - kill -0 "$pid" 2>/dev/null || fail "ElyMark quit while $1: $(tail -n 5 "$work/app.err")" - done - kill "$spy" - spy= - local opens - opens=$(grep -c -E '(colors\.toml|theme\.name|settings\.json|note\.md)$' "$work/opens" || true) - [ "$opens" -eq 0 ] || fail "ElyMark $1 opened files $opens times: $(sort "$work/opens" | uniq -c)" - echo "linux: ElyMark $1 reads nothing" -} -idle "idle after start" - -mkdir "$current/next-theme" -cp "$root/crates/ely-ui/tests/fixtures/omarchy/gruvbox.toml" "$current/next-theme/colors.toml" -rm -rf "$current/theme" -mv "$current/next-theme" "$current/theme" -echo gruvbox >"$current/theme.name" -logged 'omarchy gruvbox applied' "Omarchy's theme swap" -echo '{"version": 1, "author": "Tester"}' >"$work/data/ElyOffice/settings.json" -logged 'settings: changed on disk' "a settings write" -changed=$(seen 'changed outside') -printf '# Idle\n\nSomething moved.\n' >"$note" -logged 'changed outside' "an outside edit" "$changed" -# The window shows the new words. -shown= -for _ in $(seq 20); do - kill -0 "$pid" 2>/dev/null || fail "ElyMark quit: $(tail -n 5 "$work/app.err")" - id=$(hyprctl clients -j | jq -r --argjson pid "$pid" '[.[] | select(.pid == $pid and .mapped)][0].stableId // empty') - if [ -n "$id" ]; then - grim -T "$id" "$work/shot.png" 2>"$work/grim.err" || fail "grim failed: $(cat "$work/grim.err")" - magick "$work/shot.png" -colorspace Gray -negate "$work/ocr.png" - text=$(tesseract "$work/ocr.png" - 2>"$work/ocr.err") || fail "tesseract failed: $(cat "$work/ocr.err")" - grep -q 'moved' <<<"$text" && shown=1 && break - fi - sleep 0.5 -done -[ -n "$shown" ] || fail "ElyMark does not show the outside edit" -sleep 2 -idle "idle after changes" diff --git a/tests/scripts/references.sh b/tests/scripts/references.sh index e55780d..04d638b 100755 --- a/tests/scripts/references.sh +++ b/tests/scripts/references.sh @@ -4,7 +4,7 @@ set -euo pipefail root=$(cd "$(dirname "$0")/../.." && pwd) script=$root/scripts/fetch-references.sh -names="Ely-GPUI-Components zed libreoffice-core mupdf git tesseract mailcore2 libetpan cef cef-rs milkdown codemirror-dev marktext colamd magpie agent-client-protocol markion markdown-rs omarchy" +names="Ely-GPUI-Components zed libreoffice-core mupdf git tesseract mailcore2 libetpan marktext colamd magpie agent-client-protocol markion markdown-rs omarchy" fail() { echo "FAIL: $*" >&2 @@ -29,8 +29,7 @@ for header in \ tesseract/include/tesseract/capi.h \ mailcore2/CMakeLists.txt \ libetpan/src/low-level/gmail/mailgmail.h \ - libetpan/src/low-level/activesync/mailactivesync.h \ - cef-rs/Cargo.toml; do + libetpan/src/low-level/activesync/mailactivesync.h; do [ -s "$root/reference/$header" ] || fail "missing reference/$header" done diff --git a/tests/scripts/release-macos.sh b/tests/scripts/release-macos.sh index 3d7b354..0fdd626 100755 --- a/tests/scripts/release-macos.sh +++ b/tests/scripts/release-macos.sh @@ -72,7 +72,7 @@ while IFS=$'\t' read -r slug name; do ely-doc | ely-table | ely-show) engines=(LibreOffice) ;; ely-pdf) engines=(MuPDF MuPDF/fonts/urw MuPDF/fonts/sil MuPDF/fonts/noto MuPDF/fonts/droid MuPDF/fonts/han OpenSSL tessdata GreatVibes) ;; ely-tree) engines=(Git git-lfs) ;; - ely-mark) engines=(CEF content) ;; + ely-mark) engines=(KaTeX) ;; esac for part in LICENSE rust-crates.txt "${engines[@]}"; do [ -n "$(find "$licenses/$part" -type f -size +0 2>/dev/null)" ] || fail "$name lacks the licences of $part" @@ -83,9 +83,8 @@ while IFS=$'\t' read -r slug name; do [ -n "$(find "$family" -type f -size +0 \( -iname 'licen[cs]e*' -o -iname 'copying*' -o -iname 'notice*' -o -iname 'ofl*' \))" ] || fail "$name lacks the licence of the ${family##*/} fonts" done - # Nothing inside registers with macOS but CEF's helpers. - extra=$(find "$app/Contents" \( -name '*.app' -o -name '*.appex' -o -name '*.mdimporter' -o -name '*.qlgenerator' \) | - grep -v "/Frameworks/$slug Helper" || true) + # Nothing inside registers with macOS. + extra=$(find "$app/Contents" \( -name '*.app' -o -name '*.appex' -o -name '*.mdimporter' -o -name '*.qlgenerator' \)) [ -z "$extra" ] || fail "$name carries bundles macOS registers: $extra" # No binary may need a newer macOS than the app. least=$(/usr/libexec/PlistBuddy -c "Print :LSMinimumSystemVersion" "$app/Contents/Info.plist") @@ -125,10 +124,6 @@ while IFS=$'\t' read -r slug name; do # Built-ins stay links to one binary, not copies. [ "$(du -sm "$app/Contents/Resources/git" | cut -f1)" -lt 100 ] || fail "ElyTree's Git copies its built-ins" ;; - ely-mark) - grep -qF "web: CEF loaded from $app/Contents/Frameworks" "$log" || fail "ElyMark used another CEF" - grep -qF "mark: page from $app/Contents/Resources/content" "$log" || fail "ElyMark used another page" - ;; esac # A running app takes Finder's next file. open -a "$app" "$files/$second" diff --git a/tests/scripts/storage.sh b/tests/scripts/storage.sh index ba77573..f6c88af 100755 --- a/tests/scripts/storage.sh +++ b/tests/scripts/storage.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Storage: sizes shown; log cleared now, cache at restart. +# Storage: sizes shown; log cleared now, copies at restart. set -euo pipefail root=$(cd "$(dirname "$0")/../.." && pwd) @@ -48,18 +48,18 @@ quit() { for _ in $(seq 40); do kill -0 "$pid" 2>/dev/null || break; sleep 0.25; done wait "$pid" 2>/dev/null || true } -mkdir -p "$data/web" -# A known 2 MiB in the cache for the row. -dd if=/dev/zero of="$data/web/ely-sample" bs=1024 count=2048 2>/dev/null +mkdir -p "$data/recovery" +# A known 2 MiB in the copies for the row. +dd if=/dev/zero of="$data/recovery/ely-sample" bs=1024 count=2048 2>/dev/null # A log long enough that clearing it shows. head -c 8192 /dev/zero | tr '\0' 'x' >"$data/ely.log" start keys 'keystroke "," using command down' seen "title=Storage" || fail "no settings window" "$work/ax-press" "$pid" Storage >/dev/null -seen "title=Web content cache" || fail "the storage page lists no cache" -shown=$(kb "Web content cache") -[ "${shown:-0}" -ge 2048 ] || fail "the cache shows ${shown:-nothing} KB, not at least 2048" +seen "title=Recovery copies" || fail "the storage page lists no copies" +shown=$(kb "Recovery copies") +[ "${shown:-0}" -ge 2048 ] || fail "the copies show ${shown:-nothing} KB, not at least 2048" shown=$(kb "This app's log") [ "${shown:-0}" -ge 8 ] || fail "the log shows ${shown:-nothing} KB, not at least 8" # Each clear asks first; nothing goes before the yes. @@ -70,11 +70,11 @@ seen "title=Delete" || fail "clearing the log did not ask" sleep 1 [ "$(wc -c <"$data/ely.log")" -lt 4096 ] || fail "the log was not cleared" "$work/ax-press" "$pid" "Clear at next start" AXButton >/dev/null -seen "title=Delete" || fail "clearing the cache did not ask" -[ ! -e "$data/.clear-web" ] || fail "the cache was marked before the yes" +seen "title=Delete" || fail "clearing the copies did not ask" +[ ! -e "$data/.clear-recovery" ] || fail "the copies were marked before the yes" "$work/ax-press" "$pid" Delete AXButton >/dev/null -seen "Clears at next start" || fail "the cache clear is not shown as pending" -[ -e "$data/ely-sample" ] || [ -d "$data/web" ] || fail "the cache went while in use" +seen "Clears at next start" || fail "the copies clear is not shown as pending" +[ -e "$data/recovery/ely-sample" ] || fail "the copies went while in use" # Ten points kept per document, once twelve were made. "$work/ax-press" "$pid" 10 >/dev/null for _ in $(seq 20); do grep -q '"keep": 10' "$data/settings.json" 2>/dev/null && break; sleep 0.25; done @@ -82,8 +82,9 @@ grep -q '"keep": 10' "$data/settings.json" || fail "the recovery limit is not ke osascript -e "tell application \"System Events\" to click button 1 of (first window of (first process whose unix id is $pid) whose name is \"Settings\")" >/dev/null sleep 1 # An untitled document: never autosaved, so it keeps recovery points. +tabs=$(grep -c "as tab" "$data/ely.log" || true) keys 'keystroke "n" using command down' -for _ in $(seq 40); do grep -q "mark: shown" "$data/ely.log" && break; sleep 0.25; done +for _ in $(seq 40); do [ "$(grep -c "as tab" "$data/ely.log")" -gt "$tabs" ] && break; sleep 0.25; done sleep 2 # Twelve points written, one edit each. written() { grep -c "recovery: a point at" "$data/ely.log" || true; } @@ -146,22 +147,21 @@ grep -q '"keep": 20' "$data/settings.json" || fail "the retried limit is not kep sleep 0.5 "$work/ax-values" "$pid" | grep -q "Not done" && fail "the failure stays after a good save" quit -touch "$data/web/ely-marker" +touch "$data/recovery/ely-marker" start -[ ! -e "$data/web/ely-marker" ] || fail "the cache was not cleared at the start" +[ ! -e "$data/recovery/ely-marker" ] || fail "the copies were not cleared at the start" quit # A failed clear stops the start; the mark stays. -mkdir -p "$data/web/locked" -touch "$data/web/locked/kept" "$data/.clear-web" -chmod 555 "$data/web/locked" +mkdir -p "$data/recovery/locked" +touch "$data/recovery/locked/kept" "$data/.clear-recovery" +chmod 555 "$data/recovery/locked" status=0 ELY_SMOKE=hold ELY_LOCALE=en ELY_NO_DIALOG=1 HOME="$home" perl -e 'alarm 60; exec @ARGV' "$root/target/debug/ely-mark" >"$work/out" 2>&1 || status=$? -chmod 755 "$data/web/locked" +chmod 755 "$data/recovery/locked" [ "$status" = 1 ] || fail "a failed clear started anyway ($status)" grep -q "could not be deleted" "$work/out" || fail "the failed clear is not told: $(tail -3 "$work/out")" -grep -q "web: CEF" "$work/out" && fail "the cache opened after a failed clear" -[ -e "$data/.clear-web" ] && [ -e "$data/web/locked/kept" ] || fail "the failed clear lost its mark or data" +[ -e "$data/.clear-recovery" ] && [ -e "$data/recovery/locked/kept" ] || fail "the failed clear lost its mark or data" start -[ ! -e "$data/web/locked/kept" ] || fail "the clear was not tried again" +[ ! -e "$data/recovery/locked/kept" ] || fail "the clear was not tried again" quit echo "ok: storage" diff --git a/tests/scripts/webview.sh b/tests/scripts/webview.sh deleted file mode 100755 index 36d4785..0000000 --- a/tests/scripts/webview.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env bash -# A live WebView: input, fence, failures, release, paint. -set -euo pipefail - -root=$(cd "$(dirname "$0")/../.." && pwd) -[ "$(uname)" = Darwin ] || { echo "SKIP: macOS only"; exit 0; } -[ -d "${ELY_CEF:-$root/engines/cef}/Chromium Embedded Framework.framework" ] || { - echo "SKIP: no CEF; run scripts/fetch-cef.sh" - exit 0 -} -work=$(mktemp -d) -probe= -trap '[ -n "$probe" ] && kill "$probe" 2>/dev/null; rm -rf "$work"' EXIT -fail() { - echo "FAIL: $*" >&2 - exit 1 -} - -cargo build -q -p ely-web-content --example webprobe --manifest-path "$root/Cargo.toml" -swiftc -O -o "$work/share" "$root/tests/scripts/window-share.swift" -swiftc -O -o "$work/press" "$root/tests/scripts/ax-press.swift" -ELY_LOG=info ELY_LOCALE=en ELY_SMOKE=hold HOME="$work" XDG_DATA_HOME="$work/data" \ - perl -e 'alarm 150; exec @ARGV' "$root/target/debug/examples/webprobe" >"$work/out" 2>"$work/err" & -probe=$! -wait_for() { - for _ in $(seq 400); do - grep -qxF "$1" "$work/out" && return 0 - kill -0 "$probe" 2>/dev/null || break - sleep 0.25 - done - fail "never saw '$1': $(cat "$work/out") $(tail -n 5 "$work/err")" -} -wait_for "probe: hold" -[ "$("$work/press" $$ none)" != untrusted ] || fail "needs Accessibility permission" -# The page's magenta marker: share of the window. -marker() { - local share - share=$("$work/share" "$probe" 255,0,170) - case $share in - denied) fail "needs Screen Recording permission" ;; - none) fail "the probe has no window" ;; - esac - echo "$share" -} -share=$(marker) -awk -v s="$share" 'BEGIN { exit !(s > 0.01) }' || fail "the page is not painted (marker share $share)" -# A killed renderer: the fault shows, its button reloads. -pkill -P "$probe" -f -- "--type=renderer" || fail "no renderer to stop" -wait_for "probe: main failed" -sleep 1 -gone=$(marker) -awk -v s="$gone" 'BEGIN { exit !(s < 0.001) }' || fail "the dead page still shows (marker share $gone)" -pressed= -for _ in $(seq 20); do - [ "$("$work/press" "$probe" Reload)" = pressed ] && pressed=1 && break - sleep 0.25 -done -[ -n "$pressed" ] || fail "no Reload button in the fault view" -wait_for "probe: recovered" -sleep 1.5 -back=$(marker) -awk -v s="$back" 'BEGIN { exit !(s > 0.01) }' || fail "the page did not come back (marker share $back)" -wait "$probe" || fail "the probe did not exit cleanly: $(tail -n 5 "$work/err")" -probe= -out=$(cat "$work/out") -for want in "probe: value=你好a" "probe: focused=text" "probe: zoom=1.25" "probe: stayed" \ - "probe: outside=false" "probe: network=refused" "probe: udp=silent" "probe: rtc=undefined" "probe: main failed" "probe: hash loaded" "probe: missing failed" \ - "probe: closed 20" "probe: dropped 5 unmade, 10 early" "probe: inert loaded" "probe: scripted ran" "probe: inert checked"; do - grep -qxF "$want" <<<"$out" || fail "expected '$want', got: $out" -done -! grep -q "probe: inert ran" <<<"$out" || fail "an inert page ran a script: $out" -grep -qE '^probe: fg=rgba\([0-9]+, [0-9]+, [0-9]+, ' <<<"$out" || fail "no theme token: $out" -# Freed before the hold: 20 closed, 5 unmade, 10 early. -freed=$(awk "/probe: hold/ { exit } /a page's memory is freed/ { n++ } END { print n + 0 }" "$work/err") -[ "$freed" -ge 35 ] || fail "only $freed of 35 closed pages freed their memory" -! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")" -echo "ok: webview (marker share $share, $freed pages freed)"