diff --git a/.cargo/config.toml b/.cargo/config.toml index df2c8ac4..b732349c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -28,24 +28,23 @@ TS_RS_IMPORT_EXTENSION = "ts" # ------------------------- # # Link the Microsoft C runtime statically on Windows. The default, dynamic -# linkage, makes every Windows artifact -- the Server executable and the -# VSCode extension's `.node` addon alike -- import `VCRUNTIME140.dll`, which -# Windows does not ship: it arrives only with the Visual C++ redistributable. -# A machine without that redistributable therefore fails to load the addon -# with `ERROR_MOD_NOT_FOUND`, which Windows reports against the addon's own -# path, so the artifact which is present is accused of being missing (see the -# diagnosis in `extensions/VSCode/src/index.js`). The UCRT half of the C -# runtime, named by the `api-ms-win-crt-*` imports, is part of Windows itself -# and needs no such treatment. +# linkage, makes every Windows artifact -- the Server executable and the VSCode +# extension's `.node` addon alike -- import `VCRUNTIME140.dll`, which Windows +# does not ship: it arrives only with the Visual C++ redistributable. A machine +# without that redistributable therefore fails to load the addon with +# `ERROR_MOD_NOT_FOUND`, which Windows reports against the addon's own path, so +# the artifact which is present is accused of being missing (see the diagnosis +# in `extensions/VSCode/src/index.js`). The UCRT half of the C runtime, named by +# the `api-ms-win-crt-*` imports, is part of Windows itself and needs no such +# treatment. # # Nothing crosses the C runtime boundary here. The addon talks to Node through # N-API, which passes no allocations, locales or `FILE*`s between the two, and -# it embeds the whole Server rather than linking to a second Rust artifact -- -# so the usual hazard of a statically linked runtime inside a DLL does not -# arise. +# it embeds the whole Server rather than linking to a second Rust artifact -- so +# the usual hazard of a statically linked runtime inside a DLL does not arise. # -# This applies to the host triple as well as to a cross build, so Cargo hands -# it to build scripts and proc macros too whenever `--target` is absent. +# This applies to the host triple as well as to a cross build, so Cargo hands it +# to build scripts and proc macros too whenever `--target` is absent. # # The `cfg` form covers `i686` and `aarch64` alongside `x86_64` -- the three # MSVC triples the VSCode loader searches for -- so adding one of them to the @@ -54,3 +53,15 @@ TS_RS_IMPORT_EXTENSION = "ts" # included. [target.'cfg(all(windows, target_env = "msvc"))'] rustflags = ["-C", "target-feature=+crt-static"] + +# Security +# -------- +# +# Set a minimum publish age to combat supply-chain attacks. Copied from +# [Dependency Cool-Downs Land in Stable](https://weeklyrust.substack.com/p/dependency-cool-downs-land-in-stable). +# Not yet released, so produces a warning. +[resolver] +incompatible-publish-age = "deny" + +[registry] +global-min-publish-age = "7 days" # or "14 days", "1 week", "6 months" etc. \ No newline at end of file diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh index 95b59c62..0de7b015 100755 --- a/.devcontainer/postCreateCommand.sh +++ b/.devcontainer/postCreateCommand.sh @@ -8,6 +8,6 @@ if [ -x /usr/local/bin/chrome ] && [ ! -e /usr/local/bin/google-chrome ]; then sudo ln -s /usr/local/bin/chrome /usr/local/bin/google-chrome fi -cd server +# ./bt install --dev ./bt build \ No newline at end of file diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 0fe4e90f..3a8a5c09 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -50,9 +50,11 @@ jobs: ## builder ## server ## extensions/VSCode + # On Windows, GitHub Actions runs these commands in PowerShell, which runs + # `bt.ps1`; elsewhere, bash runs `bt`. + # - name: Run tests run: | rustup update - cd server ./bt install --dev ./bt full diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a55a543f..aa66a5d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -144,12 +144,15 @@ jobs: - name: Install dependencies run: | ${{ matrix.packages_install }} + # dist generated this file; the `bt` calls in this step are hand-added, + # which `allow-dirty = ["ci"]` in `dist-workspace.toml` permits. + # - name: Build artifacts run: | - server/bt prerelease + ./bt prerelease # Actually do builds and make zips and whatnot dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json - server/bt postrelease ${{ matrix.dist_args }} ${{ needs.plan.outputs.tag-flag }} + ./bt postrelease ${{ matrix.dist_args }} ${{ needs.plan.outputs.tag-flag }} echo "dist ran successfully" - id: cargo-dist name: Post-build diff --git a/.ignore b/.ignore new file mode 100644 index 00000000..a423113c --- /dev/null +++ b/.ignore @@ -0,0 +1,30 @@ +# Copyright (C) 2026 Bryan A. Jones. +# +# This file is part of the CodeChat Editor. +# +# The CodeChat Editor is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# The CodeChat Editor is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# the CodeChat Editor. If not, see +# [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). +# +# `.ignore` -- Show hidden directories to the CodeChat Editor +# =========================================================== +# +# The CodeChat Editor's project cache walks the project with the +# [ignore](https://docs.rs/ignore) crate, which skips hidden files and +# directories unless an ignore file whitelists them. These directories contain +# fragments gathered elsewhere in the project, such as the calls to `bt` +# gathered in `docs/implementation.md`. ripgrep honors this file as well. +!.github/ +!.devcontainer/ + +# CodeChat Editor lexer: python diff --git a/CHANGELOG.md b/CHANGELOG.md index 13cc1bd6..531c7b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,15 @@ Changelog * No changes. +Version 0.3.1 -- 2026-Oct-21 +---------------------------- + +* Support navigation of links in gather elements, fragments, and cross + references. +* Clicking on a link activates the TinyMCE editor when the link's doc block is + pure HTML, instead of navigating to the link. +* Improved HTML to Markdown conversion. + Version 0.3.0 -- 2026-Oct-09 ---------------------------- diff --git a/CLAUDE.md b/CLAUDE.md index 847a4190..452a0329 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,18 +32,50 @@ void foo(); // since they are separated by a code block. ``` +### Cross-references, gather elements, and fragments + +To refer to text in another section of the current document or any file in this +project: + +1. Assign a unique id for that section: `section + title`; in a code file, this must be placed in a doc block. Any HTML + element with an id can be the target of either a hyperlink or a + cross-reference. +2. Refer to it using a hyperlink or cross-reference. For example, the following + syntax links directly to the `` inside this section's heading: + `[Foo](#cc-sZ0Glsgjhu)` (link text is "Foo") / `` (link text comes from the cross-referenced span: + "Cross-references, gather elements, and fragments"); the same syntax (a bare + id with no file) also links to any file in the project. + +When implementing a specification: + +1. Label each doc block preceding non-contiguous section of code of more than + three lines with a fragment placed at the end of the doc block: ``. +2. Gather these fragments in a subheading following the specification: `

Implementation of [title of this section]

`. + +Each id must be unique throughout the entire project. To create an id: + +1. Randomly generate an id of the form `cc-[10 random ASCII letters or + numbers]`. +2. grep the project to ensure this id is unique. + Architecture ------------ -A Visual Studio Code extension in `extensions/VSCode` exchanges messages with -the CodeChat Editor Server, located in `server/` (also termed the Server), which -also exchanges messages with the CodeChat Editor Client (also termed the Client) -located in `client/`. +A Visual Studio Code extension in `extensions/VSCode` or the standalone server +in `extensions/standalone` exchanges messages with the CodeChat Editor Server, +located in `server/` (also termed the Server), which also exchanges messages +with the CodeChat Editor Client (also termed the Client) located in `client/`. Project build ------------- -All build commands must be executed from the `server/` directory. +Execute these build commands, which run the [`bt`](#cc-1s8bUScadp) script, from +the root of the project. * To build the entire project, execute `./bt build`. * To format and lint the entire project, execute `./bt flint`. @@ -139,6 +171,8 @@ Because comments become doc blocks, these mechanical rules apply: * Denote paragraphs using an empty comment (`//` or `///`), not an empty line. * A comment on the same line as code is never a doc block; it stays part of the code block. +* When commenting out code, use `/*** ... */` to prevent the code from being + treated as a doc block. ### File structure @@ -148,8 +182,8 @@ Source files follow this template (a few predate it; match it in new files): 2. The GPL copyright and license block, copied verbatim from a neighboring file rather than retyped. In Rust it uses plain `//`, so it stays a doc block separate from the title which follows it. -3. A single level-1 heading titling the file: the file name in a monospaced font - -- matching the file's actual name -- then `--`, then a short description. +3. A single level-1 heading titling the file: the file name in backticks -- + matching the file's actual name -- then `--`, then a short description. 4. The file-level design narrative, if the file needs one. 5. A `Modules` section, if the file has submodules, then an `Imports` section. In Rust its subsections are `### Standard library`, `### Third-party`, and @@ -186,7 +220,7 @@ use std::collections::HashMap; ### TypeScript specifics -* Use `//` tags instead of `/**` blocks. +* Use `//` comments instead of `/**` blocks. * Document parameters as described above instead of using `@param` tags. ### Editing existing comments diff --git a/Cargo.lock b/Cargo.lock index 40f2d991..d4d0b77e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,9 +21,9 @@ dependencies = [ [[package]] name = "actix-files" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed87c765e9e5be096cc29b43d04b932209892ec871d05fe255f0ae1ccfed9a06" +checksum = "db96438ec043d438f36bc211e260f0bec3de11dd0921feebe6b776e5a77ff3f9" dependencies = [ "actix-http", "actix-service", @@ -40,13 +40,14 @@ dependencies = [ "percent-encoding", "pin-project-lite", "v_htmlescape", + "winapi-util", ] [[package]] name = "actix-http" -version = "3.18.12" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2ab7d72c0bb26f10df574ea560177b4de2f27ee8f421b2c1e9b75628bd9db23" +checksum = "b796ba3c974e5133f6ca1a2c4e97c5941ea1a46d202df83344afaa18219f1a62" dependencies = [ "actix-codec", "actix-service", @@ -88,7 +89,7 @@ checksum = "367f814ad4afbac74f07df5001214da65f65e185c90ef56c4dd8df23f8695b9b" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -119,9 +120,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.9.8" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5c8e46e9b40172e4cc67adcd172bd42e932a5a0aa7f603eee88e60f70452f22" +checksum = "5e296908c4d6f3a191e6b9873dbdfec8023350a89129d2b470f61d17233931ee" dependencies = [ "actix-rt", "actix-service", @@ -155,9 +156,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.15.0" +version = "4.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbacab3593b6b4f7be815076fc52d60a83c873426824675417e2abdd229e2e36" +checksum = "67756b0fc99e07b8702b9a856e3d6ec036387243c2ce6310070d1e8f02680bb0" dependencies = [ "actix-codec", "actix-http", @@ -192,6 +193,7 @@ dependencies = [ "smallvec", "socket2", "time", + "tokio", "tracing", "url", ] @@ -205,7 +207,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -269,15 +271,15 @@ dependencies = [ [[package]] name = "alloc-no-stdlib" -version = "2.0.4" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" [[package]] name = "alloc-stdlib" -version = "0.2.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" dependencies = [ "alloc-no-stdlib", ] @@ -290,9 +292,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "ammonia" -version = "4.2.1" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "850babe4998b8b9e2dad65a56be4ee1bb977bc3cb3934fa2b28feb2297c30a55" +checksum = "1f45dfa685e1ec33b1bf1a3b93b8047cbdbef41b6040deebbe6bea7720a45574" dependencies = [ "cssparser 0.38.0", "html5ever 0.40.1", @@ -367,9 +369,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arc-swap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +checksum = "380962b3d93b99642e50b9febf72c071e97e8b32d86eae9cf4ef582bb960ae6e" dependencies = [ "rustversion", ] @@ -421,7 +423,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -438,9 +440,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.18.1" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" +checksum = "faac5829c2b74c28f830747e7818ccfb684261b5f48a1118b1e2a13d36dfab13" dependencies = [ "aws-lc-sys", "zeroize", @@ -448,9 +450,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" +checksum = "f1622d8446a2d4b2ce0c7eefc73dd43a99779028d5ee5c2dd8073a658ba8a2bc" dependencies = [ "cc", "cmake", @@ -498,9 +500,9 @@ dependencies = [ [[package]] name = "brotli" -version = "8.0.4" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +checksum = "f8b851b75c23ca7873623d612fe49bd1989aeb03d08fb9432187eb253d3d4c6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -509,9 +511,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "5.0.3" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +checksum = "941cd9bd4ddab83cb46fa5a2d428f1c857b24ac78cb876cf7beb710840934bd7" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -566,9 +568,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a649af8a827553c29fb0cb4bd4a6f1a0dd695bd3232b9bc98bd9c8a3ffbb8b" +checksum = "877bf67ac8e52449eb2f9e98a344ea163beb70dd1f9eebe3cc5357dcfeaee4df" dependencies = [ "find-msvc-tools", "jobserver", @@ -650,7 +652,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -679,7 +681,7 @@ dependencies = [ [[package]] name = "codechat-editor-server" -version = "0.3.0" +version = "0.3.1" dependencies = [ "actix-files", "actix-rt", @@ -740,7 +742,7 @@ dependencies = [ [[package]] name = "codechat-editor-standalone" -version = "0.3.0" +version = "0.3.1" dependencies = [ "actix-http", "actix-rt", @@ -768,7 +770,7 @@ dependencies = [ [[package]] name = "codechat-editor-vscode-extension" -version = "0.3.0" +version = "0.3.1" dependencies = [ "codechat-editor-server", "log", @@ -786,9 +788,9 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" -version = "4.6.8" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" dependencies = [ "bytes", "memchr", @@ -918,9 +920,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" dependencies = [ "cfg-if", ] @@ -999,7 +1001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" dependencies = [ "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -1016,9 +1018,9 @@ checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" [[package]] name = "deranged" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" [[package]] name = "derive_more" @@ -1074,9 +1076,9 @@ dependencies = [ [[package]] name = "dirs" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" dependencies = [ "dirs-sys", ] @@ -1111,7 +1113,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -1211,9 +1213,9 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] name = "errno" @@ -1258,9 +1260,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" [[package]] name = "flate2" @@ -1391,7 +1393,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -1535,7 +1537,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "htmd" version = "0.5.5" -source = "git+https://github.com/bjones1/htmd.git?branch=dom-interface#68e54497edf1be23dbf73fe1bd6bdf6170b729cc" +source = "git+https://github.com/bjones1/htmd.git?branch=dom-interface#2ee01b1603d596df48f8e00261dc10f87dd686e2" dependencies = [ "html5ever 0.39.0", "markup5ever_rcdom", @@ -1619,9 +1621,9 @@ dependencies = [ [[package]] name = "http-range" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" +checksum = "2149c4b4b83390c71fab669730347d578ec6bdf759f2160c581ad8b8819733b4" [[package]] name = "httparse" @@ -1909,9 +1911,9 @@ dependencies = [ [[package]] name = "impl-more" -version = "0.3.8" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c0cddce6b7505483307994f60d97c4b156020e1504d0cf1f05a21b1b325e64" +checksum = "2d3a73c82a0b0747dba739b380c046a140b5ae747234bf701df3460282da7193" [[package]] name = "indexmap" @@ -2143,9 +2145,9 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] name = "libc" -version = "0.2.190" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "libloading" @@ -2285,9 +2287,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.5" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5" +checksum = "4a9f71e3591c7ec44d3e563e87bf14ddc2bada8519dca046e5b0e560cd004e2d" dependencies = [ "hashbrown 0.17.1", ] @@ -2480,7 +2482,7 @@ dependencies = [ "napi-derive-backend", "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -2493,7 +2495,7 @@ dependencies = [ "proc-macro2", "quote", "semver", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -2517,6 +2519,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -3143,9 +3157,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b568374ba38b33a6c627141f891faf16902b08d2db26b8ede1bcb0a15b1919fa" +checksum = "fd47a460a9cbc3b3429c7e22bf0d76653e779319bae45a499ca010c9a990bb23" dependencies = [ "memchr", "psm", @@ -3155,9 +3169,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66e184b924cebaaff20ab2256ca52f12332d528a39aa76553b5d96f92aacf7f" +checksum = "203e46697708f7519ab1608393cf89239cafb46b8e6d08a8ffe359359b11ef44" dependencies = [ "pest", "pest_generator", @@ -3165,22 +3179,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a87478d267e4de54a626af9754f2f0f58e927aac6ed0575fe89bc05ad6851694" +checksum = "e84ae6340a458d3d6cfc73cf328cf3cfe90b2484cd0de25eb66aa954615ed4d5" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] name = "pest_meta" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f986f248b4241ac359b831f6139aaa34e03b08a37b6caf7e201a33f95c869e1" +checksum = "b57f92f32b3dbce6598fd8f5cdf1f06a6072570b8103fa21728fa9036923e710" dependencies = [ "pest", ] @@ -3445,9 +3459,9 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" +checksum = "278e9268c93b9e498a2da46c34d90ba93a3255eb99009646398f34bc73ab36eb" dependencies = [ "bitflags", "memchr", @@ -3747,9 +3761,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.45" +version = "0.23.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" dependencies = [ "aws-lc-rs", "log", @@ -3762,9 +3776,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -3838,9 +3852,9 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", @@ -3871,9 +3885,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" dependencies = [ "windows-sys 0.61.2", ] @@ -3962,15 +3976,16 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] name = "serde_json" -version = "1.0.151" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" dependencies = [ + "foldhash 0.2.0", "indexmap", "itoa", "memchr", @@ -3987,7 +4002,7 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -4028,9 +4043,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" dependencies = [ "cfg-if", "cpufeatures", @@ -4237,9 +4252,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -4263,7 +4278,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -4335,13 +4350,13 @@ dependencies = [ [[package]] name = "thirtyfour" -version = "0.37.7" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df97330642045e09a763b839ebedc6e76eb77bf202dad474898f5a631b4dc46" +checksum = "4ee66d96cfd32e01668efd6ec62caf5f7000438d771a9d673bf0c727bc05fbb6" dependencies = [ "arc-swap", "async-trait", - "base64 0.22.1", + "base64 0.23.1", "bytes", "cfg-if", "const_format", @@ -4369,13 +4384,13 @@ dependencies = [ [[package]] name = "thirtyfour-macros" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d712d0fd1988fb67a3fd89d10ead116544448bad01e43d326bd64dd8e09b5ea4" +checksum = "94196cd236b8087d4059b7cfba1e1c864f20262ecb38aa7b1da9e66d6f07ab77" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] @@ -4415,7 +4430,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -4486,9 +4501,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.2" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -4509,7 +4524,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -4714,9 +4729,9 @@ dependencies = [ [[package]] name = "typed-path" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +checksum = "54ec2755506471f54baf5e4bb5499d8e510cc1caf271270e778a736bd2d7336c" [[package]] name = "typemap-ors" @@ -4753,9 +4768,9 @@ checksum = "0825d2e09dcae814a4273c1e1435979d414ede25b2ee235b1545da6682483765" [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-segmentation" @@ -4945,7 +4960,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", "wasm-bindgen-shared", ] @@ -5033,9 +5048,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "58ec80e73fd8baf57bf0d9615554c75348bda990d6d5bbeab964dd068319fecb" dependencies = [ "rustls-pki-types", ] @@ -5460,24 +5475,24 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.62" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.62" +version = "0.8.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" dependencies = [ "proc-macro2", "quote", @@ -5501,7 +5516,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", "synstructure", ] @@ -5543,19 +5558,20 @@ checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] name = "zip" -version = "8.6.0" +version = "9.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" dependencies = [ "crc32fast", "flate2", "indexmap", "memchr", + "nix", "typed-path", "zopfli", ] @@ -5574,9 +5590,9 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" dependencies = [ "bumpalo", "crc32fast", @@ -5586,27 +5602,27 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.3" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +checksum = "db3ae9bf90af6e40197bef767eeefc46ad6397420fe873830bd85e5fbbfb3c70" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "7.3.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" +checksum = "ded520f16b29e1a9b771c722221b2b0a0398c2316d9bb3c42b458a5414d7434b" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.1.1+zstd.1.5.7" +version = "2.1.2+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeec9eaf2dffbbd09201e23bd0ffcbaa33bb8e9266a10734fd7ed90a85eca078" +checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 78661501..556547a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,7 @@ # the CodeChat Editor. If not, see # [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). # -# `Cargo.toml` -- Workspace manifest for the CodeChat Editor -# ========================================================== +# # `Cargo.toml` -- Workspace manifest for the CodeChat Editor # # Every Rust package in this repository except `builder` is a member of this one # workspace. Sharing one `target/` only avoids recompilation when the packages @@ -64,7 +63,7 @@ homepage = "https://codechat-editor.onrender.com/fw/fsb/opt/render/project/src/R keywords = ["literate programming"] license = "GPL-3.0-only" repository = "https://github.com/bjones1/CodeChat_Editor" -version = "0.3.0" +version = "0.3.1" # Shared dependencies # ------------------- diff --git a/server/bt b/bt similarity index 67% rename from server/bt rename to bt index 47788c5c..0a3e0986 100755 --- a/server/bt +++ b/bt @@ -16,10 +16,13 @@ # You should have received a copy of the GNU General Public License along with # the CodeChat Editor. If not, see # [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). -# -# # `bt` -- Shortcut to run the CodeChat Editor build tool -# See [passing all command-line arguments](https://www.scivision.dev/powershell-pass-all-command-args/). -cargo run --manifest-path=$(dirname "${BASH_SOURCE[0]}")/../builder/Cargo.toml -- "$@" +# `bt` -- Shortcut to run the CodeChat Editor build tool +# ====================================================== +# +# See +# [passing all command-line arguments](https://www.scivision.dev/powershell-pass-all-command-args/). +# +cargo run --manifest-path=$(dirname "${BASH_SOURCE[0]}")/builder/Cargo.toml -- "$@" -# CodeChat Editor lexer: sh. \ No newline at end of file +# CodeChat Editor lexer: sh. diff --git a/server/bt.ps1 b/bt.ps1 similarity index 68% rename from server/bt.ps1 rename to bt.ps1 index 834665b3..cae8b1df 100644 --- a/server/bt.ps1 +++ b/bt.ps1 @@ -15,12 +15,15 @@ # You should have received a copy of the GNU General Public License along with # the CodeChat Editor. If not, see # [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). -# -# # `bt` -- Shortcut to run the CodeChat Editor build tool -# See [passing all command-line arguments](https://www.scivision.dev/powershell-pass-all-command-args/). -$manifest_path = Join-Path -Path $PSScriptRoot -ChildPath ../builder/Cargo.toml +# `bt.ps1` -- Shortcut to run the CodeChat Editor build tool +# ========================================================== +# +# See +# [passing all command-line arguments](https://www.scivision.dev/powershell-pass-all-command-args/). +# +$manifest_path = Join-Path -Path $PSScriptRoot -ChildPath builder/Cargo.toml echo $manifest_path cargo run --manifest-path=$manifest_path -- $args -# CodeChat Editor lexer: python \ No newline at end of file +# CodeChat Editor lexer: python diff --git a/builder/Cargo.lock b/builder/Cargo.lock index d1a211de..035858c0 100644 --- a/builder/Cargo.lock +++ b/builder/Cargo.lock @@ -128,7 +128,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -175,9 +175,9 @@ checksum = "a74858bcfe44b22016cb49337d7b6f04618c58e5dbfdef61b06b8c434324a0bc" [[package]] name = "defmt" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" dependencies = [ "bitflags", "defmt-macros", @@ -185,21 +185,21 @@ dependencies = [ [[package]] name = "defmt-macros" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" dependencies = [ "defmt-parser", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.9", ] [[package]] name = "defmt-parser" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" dependencies = [ "thiserror", ] @@ -295,9 +295,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.190" +version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" [[package]] name = "log" @@ -440,7 +440,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] @@ -462,9 +462,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.7" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d62a2e0561533f2ca2561d0cf27fd9fedb640a1bf2616ff5d5c80d99017faadc" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" dependencies = [ "proc-macro2", "quote", @@ -488,14 +488,14 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.7", + "syn 3.0.9", ] [[package]] name = "unicode-ident" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" [[package]] name = "unicode-width" diff --git a/builder/Cargo.toml b/builder/Cargo.toml index ca89ffe5..72798750 100644 --- a/builder/Cargo.toml +++ b/builder/Cargo.toml @@ -30,7 +30,7 @@ version = "0.1.0" # # This package is deliberately *not* a member of the workspace rooted at the # repository root, which excludes it; this empty table makes it a workspace root -# of its own, See the note in the root `Cargo.toml` for the full explanation. +# of its own; see for the full explanation. # # It must follow `[package]`, which `cargo sort` requires to come first. [workspace] diff --git a/client/package.json5 b/client/package.json5 index b399b07b..91ac6f99 100644 --- a/client/package.json5 +++ b/client/package.json5 @@ -43,7 +43,7 @@ url: 'https://github.com/bjones1/CodeChat_editor', }, type: 'module', - version: '0.3.0', + version: '0.3.1', dependencies: { '@codemirror/commands': '^6.11.1', '@codemirror/lang-cpp': '^6.0.3', @@ -61,46 +61,46 @@ '@codemirror/legacy-modes': '^6.5.5', '@codemirror/lint': '^6.9.7', '@codemirror/state': '^6.7.6', - '@codemirror/view': '^6.43.14', + '@codemirror/view': '^6.43.15', '@hpcc-js/wasm-graphviz': '^1.29.2', '@mathjax/mathjax-newcm-font': '^4.1.3', codemirror: '^6.0.2', mathjax: '^4.1.3', - mermaid: '^12.1.0', + mermaid: '^12.2.0', 'pdfjs-dist': '^6.4.299', - tinymce: '^8.9.3', + tinymce: '^8.10.0', 'toastify-js': '^1.12.0', }, devDependencies: { - '@eslint/css': '^2.0.0', + '@eslint/css': '^2.0.1', '@eslint/js': '^10.0.1', '@types/chai': '^5.2.3', '@types/mocha': '^10.0.10', - '@types/node': '^26.6.4', + '@types/node': '^26.6.5', '@types/toastify-js': '^1.12.4', - '@typescript-eslint/eslint-plugin': '^8.71.1', - '@typescript-eslint/parser': '^8.71.1', + '@typescript-eslint/eslint-plugin': '^8.72.0', + '@typescript-eslint/parser': '^8.72.0', '@typescript/native': 'npm:typescript@^7.0.2', /* The accessibility audit engine driven by the Server's `overall_a11y` browser tests. */ 'axe-core': '^4.14.0', chai: '^6.3.0', esbuild: '^0.28.2', - eslint: '^10.12.0', + eslint: '^10.13.0', 'eslint-config-prettier': '^10.1.8', 'eslint-plugin-import': '^2.32.0', - 'eslint-plugin-prettier': '^5.5.6', + 'eslint-plugin-prettier': '^5.5.7', globals: '^17.13.0', mocha: '^12.0.3', - 'npm-check-updates': '^23.1.0', - prettier: '^3.9.9', + 'npm-check-updates': '^23.1.1', + prettier: '^3.9.10', /* Typescript 7 support for eslint is blocked per [this issue](https://github.com/typescript-eslint/typescript-eslint/issues/10940). Instead, see [Running Typescript 7.0 Side-by-Side with TypeScript 6.0](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0) for a workaround. */ typescript: 'npm:@typescript/typescript6@^6.0.2', - 'typescript-eslint': '^8.71.1', + 'typescript-eslint': '^8.72.0', }, scripts: { test: 'echo "Error: no test specified" && exit 1', diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 69d8f88d..d85237ed 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -57,8 +57,8 @@ importers: specifier: ^6.7.6 version: 6.7.6 '@codemirror/view': - specifier: ^6.43.14 - version: 6.43.14 + specifier: ^6.43.15 + version: 6.43.15 '@hpcc-js/wasm-graphviz': specifier: ^1.29.2 version: 1.29.2 @@ -72,24 +72,24 @@ importers: specifier: ^4.1.3 version: 4.1.3 mermaid: - specifier: ^12.1.0 - version: 12.1.0 + specifier: ^12.2.0 + version: 12.2.0 pdfjs-dist: specifier: ^6.4.299 version: 6.4.299 tinymce: - specifier: ^8.9.3 - version: 8.9.3 + specifier: ^8.10.0 + version: 8.10.0 toastify-js: specifier: ^1.12.0 version: 1.12.0 devDependencies: '@eslint/css': - specifier: ^2.0.0 - version: 2.0.0 + specifier: ^2.0.1 + version: 2.0.1 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.12.0(supports-color@8.1.1)) + version: 10.0.1(eslint@10.13.0(supports-color@8.1.1)) '@types/chai': specifier: ^5.2.3 version: 5.2.3 @@ -97,17 +97,17 @@ importers: specifier: ^10.0.10 version: 10.0.10 '@types/node': - specifier: ^26.6.4 - version: 26.6.4 + specifier: ^26.6.5 + version: 26.6.5 '@types/toastify-js': specifier: ^1.12.4 version: 1.12.4 '@typescript-eslint/eslint-plugin': - specifier: ^8.71.1 - version: 8.71.1(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) + specifier: ^8.72.0 + version: 8.72.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) '@typescript-eslint/parser': - specifier: ^8.71.1 - version: 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) + specifier: ^8.72.0 + version: 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) '@typescript/native': specifier: npm:typescript@^7.0.2 version: typescript@7.0.2 @@ -121,17 +121,17 @@ importers: specifier: ^0.28.2 version: 0.28.2 eslint: - specifier: ^10.12.0 - version: 10.12.0(supports-color@8.1.1) + specifier: ^10.13.0 + version: 10.13.0(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.12.0(supports-color@8.1.1)) + version: 10.1.8(eslint@10.13.0(supports-color@8.1.1)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) + version: 2.32.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) eslint-plugin-prettier: - specifier: ^5.5.6 - version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.12.0(supports-color@8.1.1)))(eslint@10.12.0(supports-color@8.1.1))(prettier@3.9.9) + specifier: ^5.5.7 + version: 5.5.7(eslint-config-prettier@10.1.8(eslint@10.13.0(supports-color@8.1.1)))(eslint@10.13.0(supports-color@8.1.1))(prettier@3.9.10) globals: specifier: ^17.13.0 version: 17.13.0 @@ -139,17 +139,17 @@ importers: specifier: ^12.0.3 version: 12.0.3 npm-check-updates: - specifier: ^23.1.0 - version: 23.1.0 + specifier: ^23.1.1 + version: 23.1.1 prettier: - specifier: ^3.9.9 - version: 3.9.9 + specifier: ^3.9.10 + version: 3.9.10 typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' typescript-eslint: - specifier: ^8.71.1 - version: 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) + specifier: ^8.72.0 + version: 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) packages: @@ -180,8 +180,8 @@ packages: '@chevrotain/utils@13.2.0': resolution: {integrity: sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q==} - '@codemirror/autocomplete@6.20.3': - resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + '@codemirror/autocomplete@6.20.4': + resolution: {integrity: sha512-yF8RhOf2SXlg00+qtupzLhrglcas61boqaONiNcbPIebHCpJocpqTBA6dyrtUrQO47hqyTdeob/3XnYilT7TaQ==} '@codemirror/commands@6.11.1': resolution: {integrity: sha512-O/4hG3SC1YwcmQ0d2UVNDs+AsaNWd1iHVxbTeEBuqH+6bExAiPK3iS/BvpY6rZGURALv4ZD3sIgcCmRvw3ehBg==} @@ -237,8 +237,11 @@ packages: '@codemirror/streamparser@6.0.0': resolution: {integrity: sha512-JTFPF6xrfjt0mnfGPgzcLiMwYmvIyNaoizp8BYzv8FE8KrT6GBGDZ190rtt+O7Wke3gG/MFHyiY9YGNeimof4w==} - '@codemirror/view@6.43.14': - resolution: {integrity: sha512-hC45YnmJNR1rz42OF57BLoQNFMV12/13drnvibDsR2quNDmldcb4WvYVV81hFQg56i9lcqQMEDqilhX2QDcGQQ==} + '@codemirror/view@6.43.15': + resolution: {integrity: sha512-HHsfpqFeHyXLs983pSRe38Xajs0vFWZQ2BuX0m7Ki8w3jZ5iD8cuR3lBz2/5/X53Dhf32SBHKh6rtSTB6tKeGw==} + + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} @@ -418,12 +421,12 @@ packages: resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/css-tree@4.1.1': - resolution: {integrity: sha512-A+s89eP+yDAPkKjnIOfjlkGifg5J7GPn01nWx14UjBVWktfkxOU4975F9ZZ13xNxkGiblNcBaAqgKhnQrW6SWA==} + '@eslint/css-tree@4.1.2': + resolution: {integrity: sha512-rVcxLmwSkEL3aWS3vAgsTRrQHoYE1b0XcgDtKHDJ92XOkyUpcbS/g0tUJKhvCtlEhYzgKBWsgDd+Z/aPhd166w==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/css@2.0.0': - resolution: {integrity: sha512-lODVRbtxOXgcSOWNvv3gE4A8oRTSUHefA0tX9zrWUD57F+5RTLGDX9I8bnHQROO8nWb0hrmke0uO23pTlNfZ0A==} + '@eslint/css@2.0.1': + resolution: {integrity: sha512-1qIQFzuFeTaHF3ewzsj/HUDC9RTDwfBHGWdFvpmOG0R/Tb3Mt+17yltbE10/LK+oglS1fP9+tiG5UzMvWgQY9g==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/js@10.0.1': @@ -530,79 +533,79 @@ packages: resolution: {integrity: sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==} engines: {node: '>=22.12.0'} - '@napi-rs/canvas-android-arm64@1.0.10': - resolution: {integrity: sha512-0sDMvme+7fj6bHERUaBs5SvEyWaORc91VUkaB4RrtG7p6dBrBNGy1of8cyEKDxygd1FlHtW+Thunj8lSixzY7A==} + '@napi-rs/canvas-android-arm64@1.0.12': + resolution: {integrity: sha512-Z8hg9BD7ABaZNhdM8O96IrfBaYLM7KzO/YyKA8eU9BDdc5fZJRTWyOhSAyOv59/ari6bOYa+p96h7zN6wbJRDA==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@napi-rs/canvas-darwin-arm64@1.0.10': - resolution: {integrity: sha512-pdHL1TEfFMrnchRYqmPWXKN7hR4XtOYmzJqnh4XhpfWD4/eeSokOGVzrdFP/iQ3sfr2rf0939gOGq9uCp8BGmg==} + '@napi-rs/canvas-darwin-arm64@1.0.12': + resolution: {integrity: sha512-dRtZCi/u1IOZyRjEDCA73PqgWakf5689K2jfv4uPRYGMJvEqtV+Eq7MIQkBJLmdfUPe7AktSiBZRml2pggnjRQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@napi-rs/canvas-darwin-x64@1.0.10': - resolution: {integrity: sha512-iuuk5plGxAcxurxv2ycF2yR20y5zvaBZ4heQdhPpg3EMFD21PhlY1V+YwSRmYRWzbvKL474UYdw3WIg2qfafyw==} + '@napi-rs/canvas-darwin-x64@1.0.12': + resolution: {integrity: sha512-YwvMgK5NBXwPY1AjZGXfPCG3hp/OFGM0Kw+JZHCOR/FYaeSOftaZRyxaDEGkJQSLEUZZIMNJXKCWgZYSzgWZEA==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@napi-rs/canvas-linux-arm-gnueabihf@1.0.10': - resolution: {integrity: sha512-kH7GFR5Unpm77pKSEsmkqvzeQCRD7/MQ7upmeLLjg4XpQ62dX/R6THFsp6vkF1Pkco2aKUq9KhoLM4W5twdk7g==} + '@napi-rs/canvas-linux-arm-gnueabihf@1.0.12': + resolution: {integrity: sha512-LlOsZy8qh+qJq1euEvDZqa9muACS1fL1Ag38c7J+axAnA8NeXazMtqmUyKb4NVyft6DqbU/MesEj3C0bAFnv4Q==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@napi-rs/canvas-linux-arm64-gnu@1.0.10': - resolution: {integrity: sha512-0/Hj7IwM9pmzdwxiqkp4YLJ8dKqASBrWQ3Y7dWUSU/4dgvCL9O6YNvX7xVUJjonQvdD3IE9ESS/nXa5srQqoVw==} + '@napi-rs/canvas-linux-arm64-gnu@1.0.12': + resolution: {integrity: sha512-nvuqhi85GzaFl/6sid4qf7wwjHuGU6gfIuDyywUb/fB+kp6Fp3h8GyIAKss/QPil3zVO2r8g6O9ah+IOUoBrIw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-arm64-musl@1.0.10': - resolution: {integrity: sha512-fmjKM0P2MLF9O6XzCQsRjhB+jlLzaf6tFQU36hMPMsYup+VThYvpjjhqNLOc/Ee3tHUE3r13H2BSJx/yXz4pFA==} + '@napi-rs/canvas-linux-arm64-musl@1.0.12': + resolution: {integrity: sha512-Bf6+9PoEr9K06P/oyMab7zZOpW90KimbIuNsy/vL0VhZZKsb0B0rCC78cp0jXhK6PIM7v8t6uX1sn7isOZ8KEw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@napi-rs/canvas-linux-riscv64-gnu@1.0.10': - resolution: {integrity: sha512-O/BUrGwrs3pVP9PpD/3++ZiOpjHMJlmiB7aBgswP8rbe4egPfiFkFxmcCczoLvZa72TaQX5lJ8Udedb6rc/Hvg==} + '@napi-rs/canvas-linux-riscv64-gnu@1.0.12': + resolution: {integrity: sha512-j7VcZduLSRx60nv/wbdyH+fyB5QriWGuO8GrvMDlFlt7GNUKAw5lHAHpcuuQW3QwLOe+3P+3qryzyT3sW5pCqQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-x64-gnu@1.0.10': - resolution: {integrity: sha512-48HkZPQeAN/R+9NPpY64tceoyCUW5xYYtHKZnC+BG11qiihXJCbH+xfbgGU+OdYp1Q4s84HDl9ILU0KBK6SBOQ==} + '@napi-rs/canvas-linux-x64-gnu@1.0.12': + resolution: {integrity: sha512-twDb8ZNgoCF8EG6ka+Lk4etU+ea9lv6NiV5ptIf+thz3LgWuHBbt85c+YgL1s05VsHe4WYWK0f+GhR45IuqARg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-x64-musl@1.0.10': - resolution: {integrity: sha512-QYHVi7WZ9v+Wm9OQyHLMTnJ8QiBPVX94BgbCHFJC/4bCFMfvX7fRzo4FJPgkRP0ISz3ulYNow/FCbQOPjzhYjg==} + '@napi-rs/canvas-linux-x64-musl@1.0.12': + resolution: {integrity: sha512-mtZbBeNCdGA0MYQxivPmMOp5JMH80LaJLQeEMg7Nne517fvgGb+DWTJTmoE/qUW8YJ4nkiP2vQyr1XVGrH/Ktg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@napi-rs/canvas-win32-arm64-msvc@1.0.10': - resolution: {integrity: sha512-bWK+YajM/8sL0mMgLtug2s063FVBPzFV4QAn987CQomMKlmzrBY6Trz43KY+24hq7IOa3qNhUAgP/XMEF50ZEQ==} + '@napi-rs/canvas-win32-arm64-msvc@1.0.12': + resolution: {integrity: sha512-HgGRuBUkFL8U99ogwUnTz3SmwUu7WKjmj0eOVWRqVZTPPyfWGEIMr+ycb8oDoNZcKM9vSiF1rpy3r+UIqqaKqw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@napi-rs/canvas-win32-x64-msvc@1.0.10': - resolution: {integrity: sha512-9WtzW85PaIhtB27iXaR/9RrLCqHLbqD3Z8Q+kh17NjFxwQXFXU5vWbYH4vEH+drM307TTyK3hidZgpt09G5T3g==} + '@napi-rs/canvas-win32-x64-msvc@1.0.12': + resolution: {integrity: sha512-SEEfblIAnYeS4UkeFPp9jw6ZWuHxaTqumUuksMgnMXmPLW8Ofb21uu7cujhw+lk4E9V3aWgGAE9f8g+CdoAU0Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@napi-rs/canvas@1.0.10': - resolution: {integrity: sha512-V+qptzsGnPMFxEbhwc6eDNMe5eDrfiBIp8qGpuRjAVIX4Kn2zxvwQ1Xco6Fwe+3tYIe3iLW8LM9D+SvjyJhqbQ==} + '@napi-rs/canvas@1.0.12': + resolution: {integrity: sha512-PSwhRdvizfuiSDlhE4Ld2jl/JqkrbSFFyZO0ycamE6mBpf1H0uUF3Tm8yQaVDeAwCNNDzxjRfrN22KmWLTDwcw==} engines: {node: '>= 10'} '@pkgr/core@0.3.6': @@ -729,8 +732,8 @@ packages: '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} - '@types/node@26.6.4': - resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} + '@types/node@26.6.5': + resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==} '@types/toastify-js@1.12.4': resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==} @@ -738,63 +741,63 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@typescript-eslint/eslint-plugin@8.71.1': - resolution: {integrity: sha512-nNlBf6HuotqMgZvSkWD5hNdemiBIxQ2NmrlgfcUIeZRs+BbakjscCQRq/Hb74EFVnFiSWWdL12JruqwBy3ERDw==} + '@typescript-eslint/eslint-plugin@8.72.0': + resolution: {integrity: sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.71.1 + '@typescript-eslint/parser': ^8.72.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.71.1': - resolution: {integrity: sha512-oM/5sAqz/l1v/zYK1qTOPrn2+JIrBGC1V6uNYucRO5gh6eT9vWJ/RhPbGE59byvZEUQ4VP6XkLBaS5Jti5jpvw==} + '@typescript-eslint/parser@8.72.0': + resolution: {integrity: sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.71.1': - resolution: {integrity: sha512-Oijc9RUsohHjncg6iGi7gCtjnEjYt9FWv9u6ygK8uKcHK5eLjLAkqERQ9BaytbpceOFdk6kifPCS/zAZE+A99g==} + '@typescript-eslint/project-service@8.72.0': + resolution: {integrity: sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.71.1': - resolution: {integrity: sha512-1YUDZXdTnXLlob03SFHBipdyMBsDVm7zoSz0ytTJ39dk5J1TgsTK03VlR/dLXG/RLLUrPcYbD4rp23BmHxHP0w==} + '@typescript-eslint/scope-manager@8.72.0': + resolution: {integrity: sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.71.1': - resolution: {integrity: sha512-oTvkml5SxXhgg+WWKhod5qWQICDXtOB+/Hi9VdPKFAANJ2b7YOgMffgvzXuqog7R6A/JtrzRf128dsxummyWhg==} + '@typescript-eslint/tsconfig-utils@8.72.0': + resolution: {integrity: sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.71.1': - resolution: {integrity: sha512-kH3t3jZCYv2PZSBEWPKTariLrCaoF1CLoPitYFBJOhAGEwWnSRAtGEifrfrpBa4K9JlwWNZotgA5w3/wLSk/FQ==} + '@typescript-eslint/type-utils@8.72.0': + resolution: {integrity: sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.71.1': - resolution: {integrity: sha512-ybYnPTUwg3VQQOFEiBPLs8Jxl1ry2vXdn1kVKguomBbvxpetdu3MKTNDqFNUrlG8ylP9MpZosmXMFYlGQHk1oQ==} + '@typescript-eslint/types@8.72.0': + resolution: {integrity: sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.71.1': - resolution: {integrity: sha512-pSKSEK1JJpHs6KiKVtABCE4iJK5d7FLFzDXQNBQ2oRY7LEd3UwOt3TZemTgu9syTIf56dJWlTn/pdmrO9oKUXQ==} + '@typescript-eslint/typescript-estree@8.72.0': + resolution: {integrity: sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.71.1': - resolution: {integrity: sha512-CBjT6gfAz3DW2j9Q4moFnqt208Aq+506sVGXfpRcyAeqn0EpayL0mjdWoCO2t3RC7GewNrFxdeehuEaPg6DMzQ==} + '@typescript-eslint/utils@8.72.0': + resolution: {integrity: sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.71.1': - resolution: {integrity: sha512-0GxiUGqMU0qXJt58cBsiG3DP8lUmBaFo2vChtUX+CT7PnR3v/VGLLrzVP4K9r1lNiZXjHYefMBbPunplr09v6w==} + '@typescript-eslint/visitor-keys@8.72.0': + resolution: {integrity: sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript/typescript-aix-ppc64@7.0.2': @@ -937,8 +940,8 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + argparse@3.0.3: + resolution: {integrity: sha512-Sn98ftV3KfsO8xfLPHG6oT8y+b0yvbrLgZjgPDXsharNY15tTAjqIsIju4S32nl6/iEUNNE4v+yL2Zhm8ooP3w==} array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} @@ -1027,14 +1030,14 @@ packages: codemirror@6.0.2: resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} - commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1061,8 +1064,8 @@ packages: peerDependencies: cytoscape: ^3.2.0 - cytoscape@3.34.3: - resolution: {integrity: sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==} + cytoscape@3.34.4: + resolution: {integrity: sha512-msY99tMY8iWTg+sIPGfSqDcPNOt4xcj+v/K/7qUFaCUxxlqnMkUYLBjRUzn9W5CU/R2hrmtJa5+UtovrEM2ctg==} engines: {node: '>=0.10'} d3-array@2.12.1: @@ -1261,8 +1264,8 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - dompurify@3.4.16: - resolution: {integrity: sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==} + dompurify@3.4.17: + resolution: {integrity: sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==} dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} @@ -1355,8 +1358,8 @@ packages: '@typescript-eslint/parser': optional: true - eslint-plugin-prettier@5.5.6: - resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + eslint-plugin-prettier@5.5.7: + resolution: {integrity: sha512-WjHyn3GrFd+ZR0rzsLiM4zJZ35PZMOE0SUIxzGmnWNG0126yNfJKJzObmmkYDwHJfGlkz3PSlzyG9ez/LJntwg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -1381,8 +1384,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.12.0: - resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==} + eslint@10.13.0: + resolution: {integrity: sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1547,8 +1550,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.12: - resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==} + ignore@7.0.13: + resolution: {integrity: sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==} engines: {node: '>= 4'} import-meta-resolve@4.2.0: @@ -1683,8 +1686,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - js-yaml@5.4.3: - resolution: {integrity: sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==} + js-yaml@5.5.0: + resolution: {integrity: sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==} hasBin: true json-schema-traverse@0.4.1: @@ -1697,8 +1700,8 @@ packages: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true - katex@0.16.47: - resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + katex@0.18.10: + resolution: {integrity: sha512-/B6p9eY9DX7aHBfpkHdpirDTZ5QH9xTwL9y837PWuzuv41O5jFdqNQwVQEQsimY0/iVNkwBY4+4hMfhQ7d4Dbw==} hasBin: true keyv@5.6.0: @@ -1740,11 +1743,11 @@ packages: mathjax@4.1.3: resolution: {integrity: sha512-BN/8Pkgn7G1pIDYJqd9md+JHsE/jydSYbyOZnSdSA0WziuVO8mRxdYiWFumkVVly/8U+hm9DpIIoWuvySverzw==} - mdn-data@2.35.0: - resolution: {integrity: sha512-k+1+dEIm2z/BEfdvYzT/fIyAGeDBo/1AJNJI4ilFXJRYoxP/3Ds+GP8shAOIq8jYXY9N5nGB3Ag104NsagmLAA==} + mdn-data@2.37.2: + resolution: {integrity: sha512-rz9YOBGItdSbe7HcSArUbhiAv4SB/4DKRYg96CSFS2CcIYMbUvBtZlzA1NCbk2AdgbmYwMqwNblLpR441lLeiQ==} - mermaid@12.1.0: - resolution: {integrity: sha512-wlVCp+8eTupfCeeFvoZNNiTuHrvag0P2jz/ILgb/f/6jkVokUefOcujefi8qUe/j2asHiSePncVsz/xzzA80LQ==} + mermaid@12.2.0: + resolution: {integrity: sha512-w7nr/SFrLO7BaVTs3dQTvYSV6nYSBj0Yy9DjBhsIJtAcHqRzXSq4Pg32ujHZ/AdWsAMNEvvhbm5dt9E2tOiHzw==} engines: {node: '>=22.12.0'} minimatch@10.2.6: @@ -1776,8 +1779,8 @@ packages: resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} engines: {node: '>= 0.4'} - npm-check-updates@23.1.0: - resolution: {integrity: sha512-u6Odx/JDpDXNqXp5pPsVN0RhuWkNrBe4Sz26J7yWJcEadopKhQgcB+bFq2PK35iJY6vc6h1PbnXmnwfiOFQl/w==} + npm-check-updates@23.1.1: + resolution: {integrity: sha512-UzYDvvsQRkmXR59g9SqsQxygYp7AhFpfEcOOGOJaB/P7pF/XlaqIYLK3UjUGfMHH1+aS2nZvJRaIsk2nk0z5ag==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0, npm: '>=10.0.0'} hasBin: true @@ -1875,8 +1878,8 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.9.9: - resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} + prettier@3.9.10: + resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==} engines: {node: '>=14'} hasBin: true @@ -2020,8 +2023,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - synckit@0.11.13: - resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + synckit@0.12.3: + resolution: {integrity: sha512-U+/AXT6LWsxnj2vz9HeUuPWBNgdWxjyIEjEPdbJARC9txmgPzo2VEdqnFo4hHaNyMPtycyVJrSaQYeQv/PoyyA==} engines: {node: ^14.18.0 || >=16.0.0} tinyexec@1.3.1: @@ -2032,8 +2035,8 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tinymce@8.9.3: - resolution: {integrity: sha512-T6Z541dm8R34/rgotB40/j2lwrE3eKARbaoet37/I4d3jq+S7Ul3Q7bNZUQY2WtXOvD9P5aTVbgcYI1nKQoGqg==} + tinymce@8.10.0: + resolution: {integrity: sha512-/e+/B+VdbzO02hLzfr3EaoYKYdr3phC/yitG7I9vpi2NqYvtb04UxO/Ay4y9VPqDaQHyigdFnHv2zuTgF7mnZQ==} toastify-js@1.12.0: resolution: {integrity: sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==} @@ -2071,8 +2074,8 @@ packages: resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} - typescript-eslint@8.71.1: - resolution: {integrity: sha512-oiNVPC3/NbV1FpaHU07l8O6ynqG9v4PAiW2WvTrAmZdR0f2HI/gE668ElMLCK7Xbsu5k2fK5C4fXNiq05j7/nQ==} + typescript-eslint@8.72.0: + resolution: {integrity: sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2098,8 +2101,8 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - uuid@14.0.2: - resolution: {integrity: sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==} + uuid@14.0.3: + resolution: {integrity: sha512-Sc5u1VVln93y1Xnz4R+CunOZc7R9LUcTmhj7Rc7Eg3HVcf7rVn2FOoa812fe0W8p3jD7xeoGSYbfqCGGH2B6xw==} hasBin: true w3c-keyname@2.2.8: @@ -2173,18 +2176,18 @@ snapshots: '@chevrotain/utils@13.2.0': {} - '@codemirror/autocomplete@6.20.3': + '@codemirror/autocomplete@6.20.4': dependencies: '@codemirror/language': 6.13.1 '@codemirror/state': 6.7.6 - '@codemirror/view': 6.43.14 + '@codemirror/view': 6.43.15 '@lezer/common': 1.5.3 '@codemirror/commands@6.11.1': dependencies: '@codemirror/language': 6.13.1 '@codemirror/state': 6.7.6 - '@codemirror/view': 6.43.14 + '@codemirror/view': 6.43.15 '@lezer/common': 1.5.3 '@codemirror/lang-cpp@6.0.3': @@ -2194,7 +2197,7 @@ snapshots: '@codemirror/lang-css@6.3.1': dependencies: - '@codemirror/autocomplete': 6.20.3 + '@codemirror/autocomplete': 6.20.4 '@codemirror/language': 6.13.1 '@codemirror/state': 6.7.6 '@lezer/common': 1.5.3 @@ -2202,7 +2205,7 @@ snapshots: '@codemirror/lang-go@6.0.1': dependencies: - '@codemirror/autocomplete': 6.20.3 + '@codemirror/autocomplete': 6.20.4 '@codemirror/language': 6.13.1 '@codemirror/state': 6.7.6 '@lezer/common': 1.5.3 @@ -2210,12 +2213,12 @@ snapshots: '@codemirror/lang-html@6.4.12': dependencies: - '@codemirror/autocomplete': 6.20.3 + '@codemirror/autocomplete': 6.20.4 '@codemirror/lang-css': 6.3.1 '@codemirror/lang-javascript': 6.2.5 '@codemirror/language': 6.13.1 '@codemirror/state': 6.7.6 - '@codemirror/view': 6.43.14 + '@codemirror/view': 6.43.15 '@lezer/common': 1.5.3 '@lezer/css': 1.3.9 '@lezer/html': 1.3.13 @@ -2227,11 +2230,11 @@ snapshots: '@codemirror/lang-javascript@6.2.5': dependencies: - '@codemirror/autocomplete': 6.20.3 + '@codemirror/autocomplete': 6.20.4 '@codemirror/language': 6.13.1 '@codemirror/lint': 6.9.7 '@codemirror/state': 6.7.6 - '@codemirror/view': 6.43.14 + '@codemirror/view': 6.43.15 '@lezer/common': 1.5.3 '@lezer/javascript': 1.5.6 @@ -2242,7 +2245,7 @@ snapshots: '@codemirror/lang-python@6.2.1': dependencies: - '@codemirror/autocomplete': 6.20.3 + '@codemirror/autocomplete': 6.20.4 '@codemirror/language': 6.13.1 '@codemirror/state': 6.7.6 '@lezer/common': 1.5.3 @@ -2255,7 +2258,7 @@ snapshots: '@codemirror/lang-sql@6.10.0': dependencies: - '@codemirror/autocomplete': 6.20.3 + '@codemirror/autocomplete': 6.20.4 '@codemirror/language': 6.13.1 '@codemirror/state': 6.7.6 '@lezer/common': 1.5.3 @@ -2264,7 +2267,7 @@ snapshots: '@codemirror/lang-yaml@6.1.3': dependencies: - '@codemirror/autocomplete': 6.20.3 + '@codemirror/autocomplete': 6.20.4 '@codemirror/language': 6.13.1 '@codemirror/state': 6.7.6 '@lezer/common': 1.5.3 @@ -2276,7 +2279,7 @@ snapshots: dependencies: '@codemirror/state': 6.7.6 '@codemirror/streamparser': 6.0.0 - '@codemirror/view': 6.43.14 + '@codemirror/view': 6.43.15 '@lezer/common': 1.5.3 '@lezer/highlight': 1.2.5 '@lezer/lr': 1.4.11 @@ -2289,13 +2292,13 @@ snapshots: '@codemirror/lint@6.9.7': dependencies: '@codemirror/state': 6.7.6 - '@codemirror/view': 6.43.14 + '@codemirror/view': 6.43.15 crelt: 1.0.7 '@codemirror/search@6.7.2': dependencies: '@codemirror/state': 6.7.6 - '@codemirror/view': 6.43.14 + '@codemirror/view': 6.43.15 crelt: 1.0.7 '@codemirror/state@6.7.6': @@ -2304,13 +2307,15 @@ snapshots: '@codemirror/streamparser@6.0.0': {} - '@codemirror/view@6.43.14': + '@codemirror/view@6.43.15': dependencies: '@codemirror/state': 6.7.6 crelt: 1.0.7 style-mod: 4.1.4 w3c-keyname: 2.2.8 + '@dual-bundle/import-meta-resolve@4.2.1': {} + '@esbuild/aix-ppc64@0.28.2': optional: true @@ -2389,9 +2394,9 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.12.0(supports-color@8.1.1))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.13.0(supports-color@8.1.1))': dependencies: - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -2412,20 +2417,20 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/css-tree@4.1.1': + '@eslint/css-tree@4.1.2': dependencies: - mdn-data: 2.35.0 + mdn-data: 2.37.2 source-map-js: 1.2.2 - '@eslint/css@2.0.0': + '@eslint/css@2.0.1': dependencies: '@eslint/core': 1.2.1 - '@eslint/css-tree': 4.1.1 + '@eslint/css-tree': 4.1.2 '@eslint/plugin-kit': 0.7.3 - '@eslint/js@10.0.1(eslint@10.12.0(supports-color@8.1.1))': + '@eslint/js@10.0.1(eslint@10.13.0(supports-color@8.1.1))': optionalDependencies: - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) '@eslint/object-schema@3.0.5': {} @@ -2546,52 +2551,52 @@ snapshots: dependencies: '@chevrotain/types': 13.2.0 - '@napi-rs/canvas-android-arm64@1.0.10': + '@napi-rs/canvas-android-arm64@1.0.12': optional: true - '@napi-rs/canvas-darwin-arm64@1.0.10': + '@napi-rs/canvas-darwin-arm64@1.0.12': optional: true - '@napi-rs/canvas-darwin-x64@1.0.10': + '@napi-rs/canvas-darwin-x64@1.0.12': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@1.0.10': + '@napi-rs/canvas-linux-arm-gnueabihf@1.0.12': optional: true - '@napi-rs/canvas-linux-arm64-gnu@1.0.10': + '@napi-rs/canvas-linux-arm64-gnu@1.0.12': optional: true - '@napi-rs/canvas-linux-arm64-musl@1.0.10': + '@napi-rs/canvas-linux-arm64-musl@1.0.12': optional: true - '@napi-rs/canvas-linux-riscv64-gnu@1.0.10': + '@napi-rs/canvas-linux-riscv64-gnu@1.0.12': optional: true - '@napi-rs/canvas-linux-x64-gnu@1.0.10': + '@napi-rs/canvas-linux-x64-gnu@1.0.12': optional: true - '@napi-rs/canvas-linux-x64-musl@1.0.10': + '@napi-rs/canvas-linux-x64-musl@1.0.12': optional: true - '@napi-rs/canvas-win32-arm64-msvc@1.0.10': + '@napi-rs/canvas-win32-arm64-msvc@1.0.12': optional: true - '@napi-rs/canvas-win32-x64-msvc@1.0.10': + '@napi-rs/canvas-win32-x64-msvc@1.0.12': optional: true - '@napi-rs/canvas@1.0.10': + '@napi-rs/canvas@1.0.12': optionalDependencies: - '@napi-rs/canvas-android-arm64': 1.0.10 - '@napi-rs/canvas-darwin-arm64': 1.0.10 - '@napi-rs/canvas-darwin-x64': 1.0.10 - '@napi-rs/canvas-linux-arm-gnueabihf': 1.0.10 - '@napi-rs/canvas-linux-arm64-gnu': 1.0.10 - '@napi-rs/canvas-linux-arm64-musl': 1.0.10 - '@napi-rs/canvas-linux-riscv64-gnu': 1.0.10 - '@napi-rs/canvas-linux-x64-gnu': 1.0.10 - '@napi-rs/canvas-linux-x64-musl': 1.0.10 - '@napi-rs/canvas-win32-arm64-msvc': 1.0.10 - '@napi-rs/canvas-win32-x64-msvc': 1.0.10 + '@napi-rs/canvas-android-arm64': 1.0.12 + '@napi-rs/canvas-darwin-arm64': 1.0.12 + '@napi-rs/canvas-darwin-x64': 1.0.12 + '@napi-rs/canvas-linux-arm-gnueabihf': 1.0.12 + '@napi-rs/canvas-linux-arm64-gnu': 1.0.12 + '@napi-rs/canvas-linux-arm64-musl': 1.0.12 + '@napi-rs/canvas-linux-riscv64-gnu': 1.0.12 + '@napi-rs/canvas-linux-x64-gnu': 1.0.12 + '@napi-rs/canvas-linux-x64-musl': 1.0.12 + '@napi-rs/canvas-win32-arm64-msvc': 1.0.12 + '@napi-rs/canvas-win32-x64-msvc': 1.0.12 optional: true '@pkgr/core@0.3.6': {} @@ -2734,7 +2739,7 @@ snapshots: '@types/mocha@10.0.10': {} - '@types/node@26.6.4': + '@types/node@26.6.5': dependencies: undici-types: 8.9.0 @@ -2743,72 +2748,72 @@ snapshots: '@types/trusted-types@2.0.7': optional: true - '@typescript-eslint/eslint-plugin@8.71.1(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/eslint-plugin@8.72.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/scope-manager': 8.71.1 - '@typescript-eslint/type-utils': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/utils': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/visitor-keys': 8.71.1 - eslint: 10.12.0(supports-color@8.1.1) - ignore: 7.0.12 + '@typescript-eslint/parser': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/type-utils': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/utils': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/visitor-keys': 8.72.0 + eslint: 10.13.0(supports-color@8.1.1) + ignore: 7.0.13 natural-compare: 1.4.0 ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@typescript-eslint/scope-manager': 8.71.1 - '@typescript-eslint/types': 8.71.1 - '@typescript-eslint/typescript-estree': 8.71.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/visitor-keys': 8.71.1 + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/visitor-keys': 8.72.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.71.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': + '@typescript-eslint/project-service@8.72.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.71.1(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.71.1 + '@typescript-eslint/tsconfig-utils': 8.72.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.72.0 debug: 4.4.3(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.71.1': + '@typescript-eslint/scope-manager@8.72.0': dependencies: - '@typescript-eslint/types': 8.71.1 - '@typescript-eslint/visitor-keys': 8.71.1 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/visitor-keys': 8.72.0 - '@typescript-eslint/tsconfig-utils@8.71.1(@typescript/typescript6@6.0.2)': + '@typescript-eslint/tsconfig-utils@8.72.0(@typescript/typescript6@6.0.2)': dependencies: typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/type-utils@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/type-utils@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@typescript-eslint/types': 8.71.1 - '@typescript-eslint/typescript-estree': 8.71.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/utils': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.71.1': {} + '@typescript-eslint/types@8.72.0': {} - '@typescript-eslint/typescript-estree@8.71.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': + '@typescript-eslint/typescript-estree@8.72.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/project-service': 8.71.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/tsconfig-utils': 8.71.1(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.71.1 - '@typescript-eslint/visitor-keys': 8.71.1 + '@typescript-eslint/project-service': 8.72.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/tsconfig-utils': 8.72.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/visitor-keys': 8.72.0 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.6 semver: 7.8.5 @@ -2818,20 +2823,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1)': + '@typescript-eslint/utils@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@8.1.1)) - '@typescript-eslint/scope-manager': 8.71.1 - '@typescript-eslint/types': 8.71.1 - '@typescript-eslint/typescript-estree': 8.71.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - eslint: 10.12.0(supports-color@8.1.1) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0(supports-color@8.1.1)) + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.71.1': + '@typescript-eslint/visitor-keys@8.72.0': dependencies: - '@typescript-eslint/types': 8.71.1 + '@typescript-eslint/types': 8.72.0 eslint-visitor-keys: 5.0.1 '@typescript/typescript-aix-ppc64@7.0.2': @@ -2916,7 +2921,7 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - argparse@2.0.1: {} + argparse@3.0.3: {} array-buffer-byte-length@1.0.2: dependencies: @@ -3034,17 +3039,17 @@ snapshots: codemirror@6.0.2: dependencies: - '@codemirror/autocomplete': 6.20.3 + '@codemirror/autocomplete': 6.20.4 '@codemirror/commands': 6.11.1 '@codemirror/language': 6.13.1 '@codemirror/lint': 6.9.7 '@codemirror/search': 6.7.2 '@codemirror/state': 6.7.6 - '@codemirror/view': 6.43.14 + '@codemirror/view': 6.43.15 - commander@7.2.0: {} + commander@15.0.0: {} - commander@8.3.0: {} + commander@7.2.0: {} concat-map@0.0.1: {} @@ -3064,17 +3069,17 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.3): + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.4): dependencies: cose-base: 1.0.3 - cytoscape: 3.34.3 + cytoscape: 3.34.4 - cytoscape-fcose@2.2.0(cytoscape@3.34.3): + cytoscape-fcose@2.2.0(cytoscape@3.34.4): dependencies: cose-base: 2.2.0 - cytoscape: 3.34.3 + cytoscape: 3.34.4 - cytoscape@3.34.3: {} + cytoscape@3.34.4: {} d3-array@2.12.1: dependencies: @@ -3304,7 +3309,7 @@ snapshots: dependencies: esutils: 2.0.3 - dompurify@3.4.16: + dompurify@3.4.17: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -3441,9 +3446,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.12.0(supports-color@8.1.1)): + eslint-config-prettier@10.1.8(eslint@10.13.0(supports-color@8.1.1)): dependencies: - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) eslint-import-resolver-node@0.3.10(supports-color@8.1.1): dependencies: @@ -3453,17 +3458,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) - eslint: 10.12.0(supports-color@8.1.1) + '@typescript-eslint/parser': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.2.0 @@ -3472,9 +3477,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 - eslint: 10.12.0(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) hasown: 2.0.4 is-core-module: 2.17.0 is-glob: 4.0.3 @@ -3486,20 +3491,20 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/parser': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.12.0(supports-color@8.1.1)))(eslint@10.12.0(supports-color@8.1.1))(prettier@3.9.9): + eslint-plugin-prettier@5.5.7(eslint-config-prettier@10.1.8(eslint@10.13.0(supports-color@8.1.1)))(eslint@10.13.0(supports-color@8.1.1))(prettier@3.9.10): dependencies: - eslint: 10.12.0(supports-color@8.1.1) - prettier: 3.9.9 + eslint: 10.13.0(supports-color@8.1.1) + prettier: 3.9.10 prettier-linter-helpers: 1.0.1 - synckit: 0.11.13 + synckit: 0.12.3 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.12.0(supports-color@8.1.1)) + eslint-config-prettier: 10.1.8(eslint@10.13.0(supports-color@8.1.1)) eslint-scope@9.1.2: dependencies: @@ -3512,9 +3517,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.12.0(supports-color@8.1.1): + eslint@10.13.0(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0(supports-color@8.1.1)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5(supports-color@8.1.1) '@eslint/config-helpers': 0.7.0 @@ -3699,7 +3704,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.12: {} + ignore@7.0.13: {} import-meta-resolve@4.2.0: {} @@ -3833,9 +3838,9 @@ snapshots: isexe@2.0.0: {} - js-yaml@5.4.3: + js-yaml@5.5.0: dependencies: - argparse: 2.0.1 + argparse: 3.0.3 json-schema-traverse@0.4.1: {} @@ -3845,9 +3850,9 @@ snapshots: dependencies: minimist: 1.2.8 - katex@0.16.47: + katex@0.18.10: dependencies: - commander: 8.3.0 + commander: 15.0.0 keyv@5.6.0: dependencies: @@ -3880,9 +3885,9 @@ snapshots: dependencies: '@mathjax/mathjax-newcm-font': 4.1.3 - mdn-data@2.35.0: {} + mdn-data@2.37.2: {} - mermaid@12.1.0: + mermaid@12.2.0: dependencies: '@braintree/sanitize-url': 7.1.2 '@iconify/utils': 3.1.7 @@ -3890,23 +3895,23 @@ snapshots: '@types/d3': 7.4.3 '@upsetjs/venn.js': 2.0.0 chevrotain: 13.2.0 - cytoscape: 3.34.3 - cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.3) - cytoscape-fcose: 2.2.0(cytoscape@3.34.3) + cytoscape: 3.34.4 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.34.4) + cytoscape-fcose: 2.2.0(cytoscape@3.34.4) d3: 7.9.0 d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 dayjs: 1.11.23 - dompurify: 3.4.16 + dompurify: 3.4.17 elkjs: 0.9.3 es-toolkit: 1.52.0 - katex: 0.16.47 + katex: 0.18.10 khroma: 2.1.0 marked: 16.4.2 roughjs: 4.6.6 stylis: 4.4.0 ts-dedent: 2.3.0 - uuid: 14.0.2 + uuid: 14.0.3 minimatch@10.2.6: dependencies: @@ -3930,7 +3935,7 @@ snapshots: glob: 13.0.6 is-path-inside: 4.0.0 is-unicode-supported: 0.1.0 - js-yaml: 5.4.3 + js-yaml: 5.5.0 minimatch: 10.2.6 ms: 2.1.3 picocolors: 1.1.1 @@ -3950,7 +3955,7 @@ snapshots: object.entries: 1.1.9 semver: 6.3.1 - npm-check-updates@23.1.0: {} + npm-check-updates@23.1.1: {} object-inspect@1.13.4: {} @@ -4033,7 +4038,7 @@ snapshots: pdfjs-dist@6.4.299: optionalDependencies: - '@napi-rs/canvas': 1.0.10 + '@napi-rs/canvas': 1.0.12 picocolors@1.1.1: {} @@ -4054,7 +4059,7 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier@3.9.9: {} + prettier@3.9.10: {} punycode@2.3.1: {} @@ -4232,8 +4237,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - synckit@0.11.13: + synckit@0.12.3: dependencies: + '@dual-bundle/import-meta-resolve': 4.2.1 '@pkgr/core': 0.3.6 tinyexec@1.3.1: {} @@ -4243,7 +4249,7 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 - tinymce@8.9.3: {} + tinymce@8.10.0: {} toastify-js@1.12.0: {} @@ -4296,13 +4302,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1): + typescript-eslint@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@typescript-eslint/eslint-plugin': 8.71.1(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/parser': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) - '@typescript-eslint/typescript-estree': 8.71.1(@typescript/typescript6@6.0.2)(supports-color@8.1.1) - '@typescript-eslint/utils': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0(supports-color@8.1.1))(supports-color@8.1.1) - eslint: 10.12.0(supports-color@8.1.1) + '@typescript-eslint/eslint-plugin': 8.72.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/parser': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/typescript-estree': 8.72.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint: 10.13.0(supports-color@8.1.1) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -4345,7 +4351,7 @@ snapshots: dependencies: punycode: 2.3.1 - uuid@14.0.2: {} + uuid@14.0.3: {} w3c-keyname@2.2.8: {} diff --git a/client/src/CodeChatEditor-test.mts b/client/src/CodeChatEditor-test.mts index 15011c55..05312fc1 100644 --- a/client/src/CodeChatEditor-test.mts +++ b/client/src/CodeChatEditor-test.mts @@ -17,10 +17,10 @@ // `CodeChatEditor-test.mts` -- Tests for the CodeChat Editor client // ================================================================= // -// To run tests, change to the `server/` directory then run `./bt client-build` -// to build any changes to the Client, followed by `cargo test --test overall -// test_client`. Or, add a `?test` parameter to any web page served by the -// CodeChat Editor Server. +// To run tests, run [`./bt client-build`](#cc-1s8bUScadp) from the root of the +// project to build any changes to the Client, then run `cargo test --test +// overall test_client` from the `server/` directory. Or, add a `?test` +// parameter to any web page served by the CodeChat Editor Server. // // Imports // ------- @@ -227,9 +227,9 @@ window.CodeChatEditor_test = () => { .querySelector(".cc-fragment-doc-contents")! .getBoundingClientRect().left; // ...and the code on the next line must begin in that same - // column: each line number fills a gutter of the same width, and - // the four spaces which follow the code's match the four the doc - // block is indented by. + // column: each line number fills a gutter of the same width, + // and the four spaces which follow the code's match the four + // the doc block is indented by. const codeText = Array.from(code.childNodes).find( (node) => node.nodeType === Node.TEXT_NODE, ) as Text; diff --git a/client/src/CodeChatEditor.mts b/client/src/CodeChatEditor.mts index 055f63a3..6eb80839 100644 --- a/client/src/CodeChatEditor.mts +++ b/client/src/CodeChatEditor.mts @@ -646,37 +646,78 @@ const onNavigate = (navigateEvent: NavigateEvent) => { // specifically those that TinyMCE generates (since they're always set to open // in a new tab). const onClick = (event: MouseEvent) => { - // TinyMCE by default tries to open all links in a new tab. Look for and fix - // these. + // TinyMCE's link plugin follows a link by clicking a temporary link to the + // same URL, appended to the page outside any doc block, which opens in a + // new tab. Look for and fix these. Leave alone a link in a doc block, where + // a plain click places the cursor whatever the link's target; `Link + // navigation` in [implementation.md](../../docs/implementation.md) lists + // the gestures which follow such a link. if ( + !event.defaultPrevented && event.target instanceof HTMLAnchorElement && - event.target.target === "_blank" + event.target.target === "_blank" && + event.target.closest(".CodeChat-doc-contents") === null && + !isLeftToBrowser(event.target) ) { - // Get the URL from the link. - const url = event.target.href; - - // If it's to a CodeChat Editor file, then load it as such. - if (event.target.origin === window.location.origin) { - // Ignore the "new tab" target, which doesn't make sense when there - // is a 1:1 relationship between the active IDE file and the file - // being edited in the CodeChat Editor. If two tabs are open, which - // is the current file for the IDE? - event.preventDefault(); - saveThenNavigate(new URL(url)); - } else { - // This is navigation to some external link. Let that proceed - // without interruption in a pure browser environment. However, - // VSCode will block navigation, since it's cross-origin (the root - // iframe has no URL, in contrast with the localhost URL of the - // CodeChat Editor Server). In this case, ask the Server to open the - // requested link. - if (window.location.pathname.startsWith("/vsc")) { - event.preventDefault(); - parent.window.CodeChatEditorFramework.webSocketComm.sendMessage( - { OpenUrl: url }, - ); - } - } + event.preventDefault(); + followLink(event.target); + } +}; + +// VSCode shows the Client through the Server's `/vsc` endpoints; see +// [vscode.rs](../../server/src/ide/vscode.rs). +const isVsCode = () => window.location.pathname.startsWith("/vsc"); + +// Return true if the browser, rather than `followLink`, should follow a click +// on this link: an external link outside VSCode. The browser then opens the +// link as the click's modifier keys ask, such as in a background tab on a +// Ctrl+click. +export const isLeftToBrowser = ( + // The clicked link. + link: HTMLAnchorElement, + // True to leave the click to the browser. +): boolean => link.origin !== window.location.origin && !isVsCode(); + +// Follow a link. Every way of following a link in the Client ends here, except +// a click which `isLeftToBrowser` leaves to the browser, so that links act +// alike wherever they are; see `Link navigation` in +// [implementation.md](../../docs/implementation.md). +export const followLink = ( + // The link to follow. + link: HTMLAnchorElement, +) => { + // TinyMCE keeps the `href` as written in `data-mce-href`. + const href = + link.getAttribute("data-mce-href") ?? link.getAttribute("href") ?? ""; + if (href.startsWith("#")) { + // TODO: CodeMirror renders only the part of a file near its viewport, + // so this misses a target elsewhere in the file. Remove this once the + // `_same_file` tests in `server/tests/overall/overall_5.rs` pass + // without `ignore`. + const id = CSS.escape(href.slice(1)); + document.querySelector(`[id="${id}"],[name="${id}"]`)?.scrollIntoView(); + } else if (link.origin === window.location.origin) { + // This is a link to a CodeChat Editor file, so load it as such. Ignore + // the "new tab" target, which doesn't make sense when there is a 1:1 + // relationship between the active IDE file and the file being edited + // in the CodeChat Editor. If two tabs are open, which is the current + // file for the IDE? + saveThenNavigate(new URL(link.href)); + } else if (isVsCode()) { + // This is an external link, which VSCode will block, since it's + // cross-origin (the root iframe has no URL, in contrast with the + // localhost URL of the CodeChat Editor Server). Ask the Server to open + // it instead. + parent.window.CodeChatEditorFramework.webSocketComm.sendMessage({ + OpenUrl: link.href, + }); + } else { + // This is an external link which the browser won't follow, so open it + // here. `isLeftToBrowser` leaves a click on such a link to the browser, + // but the browser follows no link in editable content, TinyMCE cancels + // a click in non-editable content, and neither Alt+Enter nor a context + // menu item is a click on the link. + window.open(link.href, "_blank", "noopener,noreferrer"); } }; diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index 9050dae2..18d71a68 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -85,6 +85,8 @@ import { tinymceInstance, tinymce, init, + followLink, + isLeftToBrowser, } from "./CodeChatEditor.mjs"; import { CodeChatForWeb, @@ -834,6 +836,112 @@ const onDirty = ( }); }; +// Links in doc blocks +// ------------------- +// +// In the doc block holding TinyMCE, a plain click or tap on a link places the +// cursor there, while a Ctrl+click (Cmd+click on macOS, where Ctrl+click opens +// the context menu) follows it. The handlers below give links in every other +// doc block the same behavior; otherwise, the browser follows a link on a plain +// click and opens it in a new tab on a Ctrl+click. See `Link navigation` in +// [implementation.md](../../docs/implementation.md). +// +// `codeMirrorLoad` adds these handlers to the element enclosing CodeMirror, +// since `DocBlockWidget.ignoreEvent` hides mouse events in a doc block from the +// handlers `DocBlockPlugin` defines. +// +// Return the link enclosing `target` if that link sits in a doc block which +// doesn't hold TinyMCE. +const linkOutsideTinymce = ( + // The target of a mouse event. + target: EventTarget | null, + // The link, or `null` if there's none. +): HTMLAnchorElement | null => { + const link = target instanceof Element ? target.closest("a[href]") : null; + return link instanceof HTMLAnchorElement && + link.closest(`.CodeChat-doc-contents:not(#${TINYMCE_INST})`) !== null + ? link + : null; +}; + +// In the doc block holding TinyMCE, the Client follows a clicked link when +// `tinymce.util.VK.metaKeyPressed` accepts the click (see `Following links` in +// [tinymce-config.mts](tinymce-config.mts)), so `isFollowClick` repeats that +// test: TinyMCE loads only once a doc block gains the focus, too late for a +// click which arrives first. On macOS and iOS, which TinyMCE recognizes from the +// user agent much as below, the test requires Cmd; elsewhere, it requires Ctrl +// without Alt, since AltGr, which types characters on many Windows keyboard +// layouts, reports both. +const isMacOsOrIos = /iphone|ipad|mac os x/.test( + navigator.userAgent.toLowerCase(), +); + +const isFollowClick = ( + // A mouse event on a link. + event: MouseEvent, + // True if the event asks to follow the link. +): boolean => (isMacOsOrIos ? event.metaKey : event.ctrlKey && !event.altKey); + +// Chromium gives a pressed link the focus, and the `focusin` handler in +// `DocBlockPlugin` then moves TinyMCE into the link's doc block. However, +// Chromium leaves the cursor where it was when the pressed link isn't editable, +// so a plain press places the cursor where the press landed, for that handler +// to carry into TinyMCE. A Ctrl+press leaves the focus, and therefore TinyMCE, +// where it is, so that the link is still in place when its click arrives. +const onDocBlockLinkMousedown = ( + // A press anywhere in CodeMirror. + event: MouseEvent, +) => { + const link = linkOutsideTinymce(event.target); + if (link === null || event.button !== 0) { + return; + } + if (isFollowClick(event)) { + event.preventDefault(); + return; + } + // Chromium added the standard `caretPositionFromPoint` only in version 128; + // older browsers provide only the non-standard `caretRangeFromPoint`. + const selection = window.getSelection(); + if (typeof document.caretPositionFromPoint === "function") { + const caret = document.caretPositionFromPoint( + event.clientX, + event.clientY, + ); + if (caret !== null) { + selection?.collapse(caret.offsetNode, caret.offset); + } + } else { + const caret = document.caretRangeFromPoint( + event.clientX, + event.clientY, + ); + if (caret !== null) { + selection?.collapse(caret.startContainer, caret.startOffset); + } + } +}; + +// A plain click which arrives before TinyMCE has replaced the pressed link, +// such as a tap's, lands on that link; cancel it. A Ctrl+click lands on the +// link, which its press left in place; follow it, unless that's left to the +// browser. +const onDocBlockLinkClick = ( + // A click anywhere in CodeMirror. + event: MouseEvent, +) => { + const link = linkOutsideTinymce(event.target); + if (link === null) { + return; + } + if (!isFollowClick(event)) { + event.preventDefault(); + } else if (!isLeftToBrowser(link)) { + event.preventDefault(); + followLink(link); + } +}; + // Keyboard navigation between code and doc blocks // ----------------------------------------------- // @@ -1195,10 +1303,13 @@ export const DocBlockPlugin = ViewPlugin.fromClass( ); indentDiv.addEventListener("input", onIndentInput); - // If the target is in the indent, not the contents, then the - // following code isn't needed. + // The focus lands on the contents div or on a focusable element + // inside it, such as a link, which Chromium focuses when + // pressed (see `onDocBlockLinkMousedown`), or typeset math, + // which MathJax's menu makes focusable. If the target is in the + // indent instead, then the following code isn't needed. if ( - !(eventTarget instanceof HTMLDivElement) || + !(eventTarget instanceof Element) || eventTarget.closest(".CodeChat-doc-contents") === null ) { return false; @@ -1209,10 +1320,26 @@ export const DocBlockPlugin = ViewPlugin.fromClass( startAutoUpdateTimer(); // See if this is already a TinyMCE instance; if not, move it - // here. - if (isTinymce) { + // here. Moving TinyMCE rebuilds the contents, so focus on + // embedded interactive content, such as an iframe or a + // ``, must leave TinyMCE where it is. + if ( + isTinymce || + eventTarget.closest("iframe, summary") !== null + ) { // Nothing to do. } else { + // Focus which reaches an element inside the contents from + // the keyboard, such as a link reached with the Tab key, + // leaves the selection where it was, perhaps in another doc + // block; place the cursor at that element's start instead. + const selection = window.getSelection(); + if ( + eventTarget !== contentsDiv && + !contentsDiv.contains(selection?.anchorNode ?? null) + ) { + selection?.collapse(eventTarget, 0); + } // Wait until the focus event completes; this causes the // cursor position (the selection) to be set in the // contenteditable div. Then, save that location. @@ -1722,6 +1849,8 @@ export const codeMirrorLoad = async ( ); const codechatDiv = codechatBody.childNodes[0]; assert(codechatDiv instanceof HTMLDivElement); + codechatDiv.addEventListener("mousedown", onDocBlockLinkMousedown); + codechatDiv.addEventListener("click", onDocBlockLinkClick); currentView = new EditorView({ parent: codechatDiv, state, diff --git a/client/src/tinymce-config.mts b/client/src/tinymce-config.mts index ca65e97b..6bb9b82c 100644 --- a/client/src/tinymce-config.mts +++ b/client/src/tinymce-config.mts @@ -67,6 +67,9 @@ import "tinymce/plugins/visualchars/index.js"; /// import './plugins/powerpaste/plugin'; /// import './plugins/powerpaste/js/wordimport'; +// Import local code. +import { followLink } from "./CodeChatEditor.mjs"; + // Initialize TinyMCE. export const init = async ( // Provide additional editor options; note that the basic setup provided @@ -153,32 +156,193 @@ export const init = async ( "graphviz-graph[scale],wc-mermaid,xref[contenteditable|ref],fragment[contenteditable|following|id]", // Per the [docs](https://www.tiny.cloud/docs/tinymce/latest/content-filtering/#custom_elements), `~` marks tags as an inline element, not a block element. custom_elements: "graphviz-graph,wc-mermaid,~xref,~fragment", + + // Setting the + // [context menu](https://www.tiny.cloud/docs/tinymce/latest/contextmenu/#contextmenu) + // replaces TinyMCE's default list, so this must also name the + // context menus of the plugins imported above, except the link + // plugin's, which `LINK_MENU` replaces. + contextmenu: `${LINK_MENU} image table`, }, ); // Merge in additional setup code. const oldSetup = combinedOptions.setup; - combinedOptions.setup = + combinedOptions.setup = (editor: Editor) => { + oldSetup?.(editor); // Add a "Format as code" button (generated by Gemini). - (editor: Editor) => { - oldSetup?.(editor); - editor.ui.registry.addToggleButton("codeformat", { - text: "<>", - tooltip: "Format as code", - onAction: () => - editor.execCommand("mceToggleFormat", false, "code"), - onSetup: (api) => { - const changed = editor.formatter.formatChanged( - "code", - (state) => api.setActive(state), - ); - return () => changed.unbind(); - }, - }); - }; + editor.ui.registry.addToggleButton("codeformat", { + text: "<>", + tooltip: "Format as code", + onAction: () => + editor.execCommand("mceToggleFormat", false, "code"), + onSetup: (api) => { + const changed = editor.formatter.formatChanged( + "code", + (state) => api.setActive(state), + ); + return () => changed.unbind(); + }, + }); + takeLinkGestures(editor); + addLinkMenu(editor); + }; // Use these combined options to // [init](https://www.tiny.cloud/docs/tinymce/latest/apis/tinymce.root/#init) // TinyMCE. return await tinymce.init(combinedOptions); }; + +// Following links +// --------------- +// +// The Client, rather than TinyMCE's link plugin, follows the links in the +// editor, through `followLink`, so that they act like links everywhere else in +// the Client; see `Link navigation` in +// [implementation.md](../../docs/implementation.md). The plugin follows a link +// on Ctrl+click (Cmd+click on macOS) and Alt+Enter through its own private +// code, not through an item the Client could replace, so the handlers below +// take these gestures before the plugin sees them; handlers added during setup +// run before those the plugin adds when the editor initializes. The link +// context menu below likewise replaces the plugin's. +// +// Each link the code below finds is an HTML link, never an `a` element in +// inline SVG, whose `href` isn't a URL: the Server's sanitizer removes inline +// SVG (see `AMMONIA_OPTIONS` in +// [processing.rs](../../server/src/processing.rs)), replacing the Client's copy +// of any edit which added some. +// +// TODO: the "Open link" button of the plugin's quick link form, which the quick +// toolbar's `quicklink` button opens, still follows a link to an id itself, +// searching only the doc block holding TinyMCE. Route it through `followLink` +// when fixing links to an id elsewhere in the current file (see the TODO in +// `followLink`). +const takeLinkGestures = ( + // The editor whose links the Client follows. + editor: Editor, +) => { + editor.on("click", (event) => { + const link = + event.target instanceof Element + ? event.target.closest("a[href]") + : null; + if (link !== null && tinymce.util.VK.metaKeyPressed(event)) { + // The plugin follows a clicked link even when an earlier handler + // canceled the click, so stop the plugin from seeing it. + event.preventDefault(); + event.stopImmediatePropagation(); + followLink(link); + } + }); + + editor.on("keydown", (event) => { + if ( + event.key === "Enter" && + event.altKey && + !event.ctrlKey && + !event.metaKey && + !event.shiftKey + ) { + const link = selectedLink(editor); + if (link !== null) { + // The plugin ignores a canceled keydown. + event.preventDefault(); + followLink(link); + } + } + }); +}; + +// Return the link which the keyboard follows, through Alt+Enter or the context +// menu: the link holding the start of the selection, or else the first link in +// the selection. A selected non-editable element encloses its links instead of +// sitting in one, so this is its first link. +const selectedLink = ( + // The editor holding the selection. + editor: Editor, + // The link, or `null` if there's none. +): HTMLAnchorElement | null => { + const range = editor.selection.getRng(); + return ( + editor.dom.getParent( + editor.selection.getStart(true), + "a[href]", + ) ?? + editor.dom + .select("a[href]", editor.selection.getNode()) + .find((link) => range.intersectsNode(link)) ?? + null + ); +}; + +// ### The link context menu +// +// This replaces the link plugin's context menu, whose "Open link" follows a +// link to an id itself, and which offers nothing for non-editable content. The +// Server hydrates cross-references, fragments, gather lists, and math as +// non-editable content; see . This menu keeps +// the plugin's items for editing an editable link, and adds its own "Open link" +// for any link. TinyMCE shows its context menus on a long press as well as a +// right click, so this serves touch devices too. +// +// The names under which TinyMCE registers this context menu and its "Open link" +// item. +const LINK_MENU = "codechatlink"; +const OPEN_LINK_ITEM = "codechatopenlink"; + +const addLinkMenu = ( + // The editor whose context menu this is. + editor: Editor, +) => { + // The link which the context menu was most recently shown for. + let menuLink: HTMLAnchorElement | null = null; + // The target of the event which most recently opened the context menu. + // TinyMCE's theme starts listening for these events only once the editor + // initializes, so this listener, added during setup, runs first. + let menuEventTarget: EventTarget | null = null; + editor.on("contextmenu longpress", (event) => { + menuEventTarget = event.target; + }); + + editor.ui.registry.addContextMenu(LINK_MENU, { + update: ( + // The element right-clicked or long-pressed, which is the target + // of the event opening the menu, or the start of the selection + // when TinyMCE judges that the keyboard opened the menu. + element, + ) => { + menuLink = + element === menuEventTarget + ? // A right click or long press beside a link offers no + // "Open link". + editor.dom.getParent( + element, + "a[href]", + ) + : selectedLink(editor); + // Offer the plugin's + // [menu items](https://www.tiny.cloud/docs/tinymce/latest/link/#menu-items) + // for editing a link only in editable content, as the plugin's + // own menu does. + if (editor.dom.isEditable(element)) { + return menuLink === null + ? "link" + : `link unlink ${OPEN_LINK_ITEM}`; + } + return menuLink === null ? "" : OPEN_LINK_ITEM; + }, + }); + + editor.ui.registry.addMenuItem(OPEN_LINK_ITEM, { + text: "Open link", + icon: "new-tab", + onAction: () => { + // Content the IDE sent while the menu was open may have replaced + // the link; don't follow a link which is no longer shown. + if (menuLink !== null && editor.getBody().contains(menuLink)) { + followLink(menuLink); + } + }, + }); +}; diff --git a/docs/accessibility_review.md b/docs/accessibility_review.md index e625de83..3fa442b3 100644 --- a/docs/accessibility_review.md +++ b/docs/accessibility_review.md @@ -35,10 +35,11 @@ This file is written by hand; only its evidence is generated. After changing anything which affects the rendered page, refresh the evidence and then edit the prose against it: -1. Rebuild the Client if any of its TypeScript or CSS changed, by running `./bt - client-build` from `server/`. The tests serve the *bundle*, so a source-only - change stays invisible to them until it's rebuilt. This is the step which is - easy to forget and which silently yields stale findings. +1. Rebuild the Client if any of its TypeScript or CSS changed, by running + [`./bt client-build`](#cc-1s8bUScadp) from the root of the project. The tests + serve the *bundle*, so a source-only change stays invisible to them until + it's rebuilt. This is the step which is easy to forget and which silently + yields stale findings. 2. Run `cargo test --test overall a11y` from `server/`. Each scenario rewrites `server/target/a11y/.json` -- the audit findings, the heading and diff --git a/docs/design.md b/docs/design.md index 3997f3ba..10440d37 100644 --- a/docs/design.md +++ b/docs/design.md @@ -27,13 +27,13 @@ To build from source recommend the 64-bit toolset for Windows. 3. [Install NPM](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) (the Node.js package manager). -4. In the `server/` directory: +4. In the root directory of the project: 1. Run `./bt install --dev`. 2. Run `./bt build`. - 3. Run `cargo run -- start ../README.md`. +5. In the `server/` directory, run `cargo run -- start ../README.md`. -Use `./bt` tool's options update all libraries (`update`), run all tests -(`test`), and more. +Use the [`bt`](#cc-1s8bUScadp) tool's options to update all libraries +(`update`), run all tests (`test`), and more. Vision ------------------------- diff --git a/docs/implementation.md b/docs/implementation.md index ed2321cb..093ce62b 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -16,8 +16,7 @@ You should have received a copy of the GNU General Public License along with the CodeChat Editor. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/). -Implementation -============== +# Implementation ### System architecture @@ -78,15 +77,13 @@ same capture event shape to the local CodeChat server through the existing NAPI-RS bridge. The server hashes raw local file paths, removes any raw path fields from event data before local persistence, writes every event to a durable `capture-spool` FIFO directory under VS Code's global extension storage, and -uploads batches to -`POST /v1/capture/events` using -`Authorization: Bearer `. CodeChat does not read database credentials -from disk and does not connect directly to PostgreSQL; the database schema under -`server/scripts/` documents the server-side table used by CaptureWebService. -The previous local JSON database-secret configuration path is intentionally -removed; any database writer role or password must remain server-side in the -CaptureWebService deployment. -The service endpoint is read only from the user/application-level +uploads batches to `POST /v1/capture/events` using `Authorization: Bearer +`. CodeChat does not read database credentials from disk and does not +connect directly to PostgreSQL; the database schema under `server/scripts/` +documents the server-side table used by CaptureWebService. The previous local +JSON database-secret configuration path is intentionally removed; any database +writer role or password must remain server-side in the CaptureWebService +deployment. The service endpoint is read only from the user/application-level `CodeChatEditor.Capture.ServiceBaseUrl` setting; workspace values are ignored so a repository cannot redirect a stored token. Bearer-token requests require HTTPS, with `http://localhost` and `http://127.0.0.1` allowed only for local @@ -119,8 +116,7 @@ The verified extension-generated event stream includes `session_start`, contract tests submit representative well-formed event batches for optional study/debug/build event types through the same public HTTPS API. -Architecture ------------------------------------------- +## Architecture Overall, the code is something like this: @@ -144,8 +140,8 @@ files. Therefore, the overall strategy is: which implement custom tags which only need local information. For example, a GraphViz custom tag renders graphs based on a description of the graph inside the tag. Likewise, MathJax interprets anything in matching math delimiters - (..., for example), then - transforms it back to text before saving. + (`...`, for example), + then transforms it back to text before saving. * On save, the client sends its text back to the server, which de-transforms custom tags which depend on information from other pages. If de-transforms disagree with the provided text, then re-load the updated text after the save @@ -200,10 +196,6 @@ On save: * Save the file to disk. * If dirty, re-load the file. -### Cache data format - -Documented elsewhere. - ### IDE/editor integration The IDE extension client (IDE for short) and the CodeChat Editor Client (or @@ -269,7 +261,10 @@ The app needs build support because of complexity: to the VSCode extension, then a release published for that platform. So, this project contains Rust code to automate this process -- see the -[builder](../builder/Cargo.toml). +[builder](../builder/Cargo.toml). The `bt` script simplifies invoking the +builder. + +### `bt` (build tool) script Misc topics ----------- @@ -362,12 +357,6 @@ Future work `h1::before {` `counter-reset: var(--section-counter-reset);` `content: var(--section-counter-content);` `}` -#### Example of non-editable text - -
-

asdf

-
- ### Numbering On a page, the local TOC numbering comes only from heading tags. The CSS (which @@ -399,8 +388,7 @@ with descriptions of each setting. * Tabs vs spaces; newline type * Substitutions -Core development priorities ------------------------------------------------------------------- +## Core development priorities 1. Bug fixes 2. Book support @@ -505,13 +493,59 @@ The CodeChat Editor client supports four modes: * Usual: the usual CodeMirror + TinyMCE editor * View: * For a ReadTheDocs / browsing experience: clicking on links navigates to them - immediately, instead of bringing up a context menu. Still use CodeMirror for - syntax highlighting, collapsing, etc. + immediately, instead of placing the cursor in them as Edit mode does. Still + use CodeMirror for syntax highlighting, collapsing, etc. * Simple viewer: * For text or binary files that aren't supported by the editor. In project mode, this displays the TOC on the left and the file contents in the main area; otherwise, it's only the main area. See: \. +Link navigation +--------------- + +Note: this section is specification; all other sections are documentation. + +In Edit mode, a source file has one TinyMCE editor, which the Client moves into +a doc block when that block gains focus; it stays there until another doc block +gains focus. Since TinyMCE runs inline, nothing marks which block holds it, so +links must behave alike in every doc block. In the block holding TinyMCE, a +click or tap on a link places the cursor (or selects the link's non-editable +element) without following it, while a Ctrl+click (Cmd+click on macOS) follows +it. In any other doc block, the browser would follow the link on a plain click +and open it in a new tab on a Ctrl+click. Instead, the Client makes a plain +click or tap move TinyMCE into that block, placing the cursor where the click +landed, or beside the link's non-editable element, since TinyMCE keeps the +cursor out of non-editable content. A Ctrl+click follows the link, as in the +block holding TinyMCE. Focusing a link from the keyboard, as the Tab key does, +likewise moves TinyMCE into its block, placing the cursor at the link's start. + +The Client, not TinyMCE's link plugin, follows every link, through `followLink`: +a link to an id scrolls the element with that id into view, a link to a CodeChat +Editor file loads through `saveThenNavigate`, and an external link opens through +the IDE in VSCode or in a new tab in a browser. This covers a Ctrl+click in any +doc block; Alt+Enter in the block holding TinyMCE, which follows the link +holding the start of the selection, or else the first link in the selection; and +the "Open link" item of the context menu for a link in the block holding +TinyMCE, whether or not the link is editable (see +). + +The one exception is a Ctrl+click, in a browser, on an external link in a doc +block which doesn't hold TinyMCE: the Client leaves that click to the browser, +which opens the link as the click's modifier keys ask, such as in a background +tab. The browser follows no link in editable content, and TinyMCE cancels a +click in non-editable content, so in the block holding TinyMCE, `followLink` +opens an external link in a new tab itself. + +TODO: the "Open link" button of the link plugin's quick link form, which the +quick toolbar's link button opens, follows a link to an id itself, searching +only the block holding TinyMCE. Route it through `followLink` once `followLink` +finds an id anywhere in the current file (see the TODO in `followLink`). + +TODO: a right click or long press on a link in a doc block which doesn't hold +TinyMCE should show TinyMCE's context menu (see +). Remove this once +`test_open_non_editable_link_py_unfocused` passes without its `ignore`. + Misc ---- diff --git a/docs/style_guide.cpp b/docs/style_guide.cpp index 1d67bd3d..4270285e 100644 --- a/docs/style_guide.cpp +++ b/docs/style_guide.cpp @@ -27,9 +27,9 @@ // CodeChat Editor. For basic use, see the [user manual](../README.md). // // The CodeChat Editor divides code into code blocks and documentation (doc) -// blocks.​ These blocks are separated by newlines; comments on the same line as -// code are not interpreted doc blocks.​ Doc blocks must have 1 space after the -// comment delimiter.​ For example, this paragraph is a doc block; +// blocks. These blocks are separated by newlines; comments on the same line as +// code are not interpreted doc blocks. Doc blocks must have at least one space after the +// comment delimiter. For example, this paragraph is a doc block; const char* CODE_BLOCK = "this is a code block."; // Comments here are NOT part of a doc block. @@ -63,7 +63,7 @@ const char* CODE_BLOCK = // handle. // // **Phase 2 - writing.** As you write code, save links to helpful code -// snippets, documentation, and ideas. ​Update the approach you sketched out as +// snippets, documentation, and ideas. Update the approach you sketched out as // your learn what works (and doesn't) through the development process. Explain // any pieces of code that took significant development or debug time, or which // contain difficult to understand code. diff --git a/extensions/VSCode/package.json b/extensions/VSCode/package.json index 215d8c4a..87a80c10 100644 --- a/extensions/VSCode/package.json +++ b/extensions/VSCode/package.json @@ -41,7 +41,7 @@ "type": "git", "url": "https://github.com/bjones1/CodeChat_Editor" }, - "version": "0.3.0", + "version": "0.3.1", "activationEvents": [ "onCommand:extension.codeChatEditorActivate", "onCommand:extension.codeChatEditorDeactivate", @@ -121,23 +121,23 @@ "@napi-rs/cli": "^3.10.8", "@tybys/wasm-util": "^0.10.4", "@types/escape-html": "^1.0.4", - "@types/node": "^26.6.4", + "@types/node": "^26.6.5", "@types/vscode": "1.61.0", - "@typescript-eslint/eslint-plugin": "^8.71.1", - "@typescript-eslint/parser": "^8.71.1", + "@typescript-eslint/eslint-plugin": "^8.72.0", + "@typescript-eslint/parser": "^8.72.0", "@typescript/native": "npm:typescript@^7.0.2", "@vscode/vsce": "^4.0.0", "chalk": "^6.0.1", "esbuild": "^0.28.2", - "eslint": "^10.12.0", + "eslint": "^10.13.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-prettier": "^5.5.6", + "eslint-plugin-prettier": "^5.5.7", "npm-run-all2": "^9.0.3", - "prettier": "^3.9.9", + "prettier": "^3.9.10", "typescript": "npm:@typescript/typescript6@^6.0.2", - "typescript-eslint": "^8.71.1" + "typescript-eslint": "^8.72.0" }, "optionalDependencies": { "bufferutil": "^4.1.0" diff --git a/extensions/VSCode/pnpm-lock.yaml b/extensions/VSCode/pnpm-lock.yaml index 594ee514..5978ed84 100644 --- a/extensions/VSCode/pnpm-lock.yaml +++ b/extensions/VSCode/pnpm-lock.yaml @@ -20,10 +20,10 @@ importers: version: 1.11.3 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.12.0) + version: 10.0.1(eslint@10.13.0) '@napi-rs/cli': specifier: ^3.10.8 - version: 3.10.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.4) + version: 3.10.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5) '@tybys/wasm-util': specifier: ^0.10.4 version: 0.10.4 @@ -31,17 +31,17 @@ importers: specifier: ^1.0.4 version: 1.0.4 '@types/node': - specifier: ^26.6.4 - version: 26.6.4 + specifier: ^26.6.5 + version: 26.6.5 '@types/vscode': specifier: 1.61.0 version: 1.61.0 '@typescript-eslint/eslint-plugin': - specifier: ^8.71.1 - version: 8.71.1(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0))(@typescript/typescript6@6.0.2)(eslint@10.12.0) + specifier: ^8.72.0 + version: 8.72.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0))(@typescript/typescript6@6.0.2)(eslint@10.13.0) '@typescript-eslint/parser': - specifier: ^8.71.1 - version: 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0) + specifier: ^8.72.0 + version: 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0) '@typescript/native': specifier: npm:typescript@^7.0.2 version: typescript@7.0.2 @@ -55,32 +55,32 @@ importers: specifier: ^0.28.2 version: 0.28.2 eslint: - specifier: ^10.12.0 - version: 10.12.0 + specifier: ^10.13.0 + version: 10.13.0 eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.12.0) + version: 10.1.8(eslint@10.13.0) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0))(eslint@10.12.0) + version: 2.32.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0))(eslint@10.13.0) eslint-plugin-node: specifier: ^11.1.0 - version: 11.1.0(eslint@10.12.0) + version: 11.1.0(eslint@10.13.0) eslint-plugin-prettier: - specifier: ^5.5.6 - version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.12.0))(eslint@10.12.0)(prettier@3.9.9) + specifier: ^5.5.7 + version: 5.5.7(eslint-config-prettier@10.1.8(eslint@10.13.0))(eslint@10.13.0)(prettier@3.9.10) npm-run-all2: specifier: ^9.0.3 version: 9.0.3 prettier: - specifier: ^3.9.9 - version: 3.9.9 + specifier: ^3.9.10 + version: 3.9.10 typescript: specifier: npm:@typescript/typescript6@^6.0.2 version: '@typescript/typescript6@6.0.2' typescript-eslint: - specifier: ^8.71.1 - version: 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0) + specifier: ^8.72.0 + version: 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0) optionalDependencies: bufferutil: specifier: ^4.1.0 @@ -116,16 +116,16 @@ packages: resolution: {integrity: sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==} engines: {node: '>=22.0.0'} - '@azure/identity@4.13.3': - resolution: {integrity: sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A==} + '@azure/identity@4.14.0': + resolution: {integrity: sha512-8utCLdTZn14CiAe0HW4JbtzUMsc0mdDwnjUvStFMrR4IxQP8S2eL9zVAdplpfORVJGYmidZElpeXRYjJQ4UyUw==} engines: {node: '>=22.0.0'} '@azure/logger@1.4.0': resolution: {integrity: sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==} engines: {node: '>=22.0.0'} - '@azure/msal-browser@5.25.0': - resolution: {integrity: sha512-7GyXzDUrdLG5zPDCYJbQdJqbl9a2U9SWUSC4lxr7cXjkBq6diTfWcJsi0UjdGWlqy+EevQg9dSC4g3s3BEp19g==} + '@azure/msal-browser@5.25.1': + resolution: {integrity: sha512-NDW2/bz2Pz71EJAlhSw+jg1kpRIWtrzj0gCzcHuG1u7iLvSOHpUc5lICkDA9x0bbeityxTneiunSBluvS+/y4w==} engines: {node: '>=0.8.0'} '@azure/msal-common@16.14.1': @@ -146,6 +146,9 @@ packages: '@cacheable/utils@2.5.0': resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + '@dual-bundle/import-meta-resolve@4.2.1': + resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==} + '@emnapi/core@1.11.2': resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} @@ -392,8 +395,8 @@ packages: resolution: {integrity: sha512-0Ywt3S8tqM6TWUcsRlBFQ1x+X3/o8kktX0ZiKAnByYb9GFDknnOhse1PVtfiD2BXPI8ar+BWx9PV9R0lQIcygw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/checkbox@5.2.6': - resolution: {integrity: sha512-XkOardciF3tow2NUlY6/Qjk1QqYa+6WpmVLTpxXl97Uv4erczDEZ+xg1j6LqBf0ekcRSUiICQA4nQXHHkVKQ9w==} + '@inquirer/checkbox@5.2.7': + resolution: {integrity: sha512-/+QmGE/9bvE/EobaZDnzYe+T+QWoJrnmxwBQNtoi2OVSUR9d1znYsNyEoUrOZvFSxDYttrhzRIpXpaLkN+iIsg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -401,8 +404,8 @@ packages: '@types/node': optional: true - '@inquirer/confirm@6.3.3': - resolution: {integrity: sha512-ImOBgpl/sJTFp8naeSkNu4S0Sf8F8pxv3/CUx+LoBAdQvCVqFz6rSB2A/oZYsJge3+yphbEDxEkfaRNYq1OU0A==} + '@inquirer/confirm@6.3.4': + resolution: {integrity: sha512-ZwnaZZDa1zxsyVrN32s447ydMnKcw7e8WWNT+R+Qsu0zAn8Rs6yi9d1B80Kob1Ul2cfUi7t8JD2t4/K7uEZpqA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -410,8 +413,8 @@ packages: '@types/node': optional: true - '@inquirer/core@12.0.4': - resolution: {integrity: sha512-M5fB67y7jPObI45vhD82RskWwCSm0scOvenV/I6K8tIi2gfr9DXqQqJ+iw+Wi6SgapikdOiJibYzz1gW47fsDg==} + '@inquirer/core@12.0.5': + resolution: {integrity: sha512-c0ZSnO9oVk/IlEGoZK3u8b3IZQeu9I6jUkXJSQnQEVRXsjKvtAq3AGqFL2LgfJhlzMZne08pZHWRWasIcI3Rew==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -419,8 +422,8 @@ packages: '@types/node': optional: true - '@inquirer/editor@5.3.4': - resolution: {integrity: sha512-imzxQDfw26NpcJAubMj/dE5RjU9Mp80IcT+FzsD6YoxXmL8L1QYnAFdCNv7ccGWh//73+0YP+1kfhwnExdl2VQ==} + '@inquirer/editor@5.3.5': + resolution: {integrity: sha512-6lU3/pPx5z3lHWejFW061MV4qZooBQYv/+EeGhNOPiP/i+hrYsHfxxBnnDGsAfg8YdLUd69YckoeOjtr3QuAUg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -428,8 +431,8 @@ packages: '@types/node': optional: true - '@inquirer/expand@5.1.6': - resolution: {integrity: sha512-JfiLKi7qGuHmcQO/DqR10UFEp054Y1FXZaszP5ZEfHhgHeuFoT5nNFQIXFhed/5/p07SbsV2tIJFwG/dg5+NmQ==} + '@inquirer/expand@5.1.7': + resolution: {integrity: sha512-eiJ31YFCaB6fLoJCqLIn8S6eqUyME6vwr8KcP5h8cxmA+bV9RRVPO+kywoxb7G63HduDXV6sN/SM7o2R+ahEcQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -450,8 +453,8 @@ packages: resolution: {integrity: sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/input@5.1.7': - resolution: {integrity: sha512-Icrs9tuMBCxGI+lFYNO71VFOnqGO8IIrLIGqnTGh07X8jZ2la9QLAYxXcikHZQjjVhIZUtU2xyH89DVN/pPhVg==} + '@inquirer/input@5.1.8': + resolution: {integrity: sha512-psOSTgkL8y/OtgJJYbqJ0oISr2QBE0U1Ro/qQdA/EKjeJiwiUTQgQmq8o4ESNzVOWthIeh3YLD7BCUR9dKnR5g==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -459,8 +462,8 @@ packages: '@types/node': optional: true - '@inquirer/number@4.2.4': - resolution: {integrity: sha512-C6g80U4U2+w2Vxa1CB0MN3a2WnQIfzLSUG0lduO3Au9GsirJsKATZkOwZPK+vZ1BlAlz+/Z5ParfQCS9E0svmg==} + '@inquirer/number@4.2.5': + resolution: {integrity: sha512-lEy7LWsdF5qHx5QjHZqabUqM9P7i5uTMCt8GRzqxmwXXKbapKJDA+ucVflVVPAyj4snD5DTAD4L+sct85XOlOQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -468,8 +471,8 @@ packages: '@types/node': optional: true - '@inquirer/password@5.2.3': - resolution: {integrity: sha512-1Uo2qfI2K3wXbfunMpvwAzoZPIUf5iEETwDGqRVbswoorXGqYQJ/bfIJFeLj+pxjq+e2UFPNOmx+fsIl4Urigw==} + '@inquirer/password@5.2.4': + resolution: {integrity: sha512-T7ir3fflSSVCYEVqNyMBq+pIMsilQRtbZcD5gxn6Ewsin5TJWBxrGBdXveWtmeXVAeoInfhS+BhBUZaL4sFSDg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -477,8 +480,8 @@ packages: '@types/node': optional: true - '@inquirer/prompts@8.7.3': - resolution: {integrity: sha512-DHK20kO+H5k/XcJQpHQUpVQbMYHHhZchlGWhZVMUV4WIuRuBW5744hipOtriE1vMqPLgnIyXFMHWKkIbTepa4Q==} + '@inquirer/prompts@8.7.4': + resolution: {integrity: sha512-SAJt+QhY5rOJAIgZBw9TrHWfgrp6wsb9cqRCyqax+Hl6EiFfRXmD9EsIWkST8yq/dQ8H46Is2Sr/JG7Ls/LQ+Q==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -486,8 +489,8 @@ packages: '@types/node': optional: true - '@inquirer/rawlist@5.3.6': - resolution: {integrity: sha512-2exExhQ5xTBqfN/rRlZkvM1Gm3b0eiLcWe2NqxWxe7qYmnU4M3j/n9TwzfPUb+Q7dAAz9TmBtDWWdF3jbp4ZLA==} + '@inquirer/rawlist@5.3.7': + resolution: {integrity: sha512-XveEe8v3BHj9ec00n+9i8FHjWhXjS6+jCTizYvXCqKUKtgkrq9xid8S5j3mYGW96qfxG3mwNoud3m3v8GDk9Pw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -495,8 +498,8 @@ packages: '@types/node': optional: true - '@inquirer/search@4.3.4': - resolution: {integrity: sha512-xsk0UeFP04jsbEeFMzghjUaH2aHXyt+WykSajQzq/8qyBNHsgGYHea/sFwv5T08kW7Sp+KDX8aF124zPUIs1Ww==} + '@inquirer/search@4.3.5': + resolution: {integrity: sha512-kIUecMugEN2YTvTmff+uYz0nAE0f9RFNDVZknfrh264vAQM9P8tnsXiUp7z8XbGuNIBEtFMJhvc9QquawpC46A==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -504,8 +507,8 @@ packages: '@types/node': optional: true - '@inquirer/select@5.2.6': - resolution: {integrity: sha512-jFu7CFghlr0vpi/326a4wz6BGkJH9X3H/iIu+ahyV2Ja52ktqQE3fC9BYP47JKeSPQYGUv5xquE2Vp9PbepZ3g==} + '@inquirer/select@5.2.7': + resolution: {integrity: sha512-pRDWO4dTqSAvFQdO8yclKMtXNDm9K9HmvWB5UZmT3clw2OlOBNXveHzhyFmO/0d0yJMqlPcNITus1Jtpm6t8VQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1078,69 +1081,69 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/node@26.6.4': - resolution: {integrity: sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==} + '@types/node@26.6.5': + resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==} '@types/vscode@1.61.0': resolution: {integrity: sha512-9k5Nwq45hkRwdfCFY+eKXeQQSbPoA114mF7U/4uJXRBJeGIO7MuJdhF1PnaDN+lllL9iKGQtd6FFXShBXMNaFg==} - '@typescript-eslint/eslint-plugin@8.71.1': - resolution: {integrity: sha512-nNlBf6HuotqMgZvSkWD5hNdemiBIxQ2NmrlgfcUIeZRs+BbakjscCQRq/Hb74EFVnFiSWWdL12JruqwBy3ERDw==} + '@typescript-eslint/eslint-plugin@8.72.0': + resolution: {integrity: sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.71.1 + '@typescript-eslint/parser': ^8.72.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.71.1': - resolution: {integrity: sha512-oM/5sAqz/l1v/zYK1qTOPrn2+JIrBGC1V6uNYucRO5gh6eT9vWJ/RhPbGE59byvZEUQ4VP6XkLBaS5Jti5jpvw==} + '@typescript-eslint/parser@8.72.0': + resolution: {integrity: sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.71.1': - resolution: {integrity: sha512-Oijc9RUsohHjncg6iGi7gCtjnEjYt9FWv9u6ygK8uKcHK5eLjLAkqERQ9BaytbpceOFdk6kifPCS/zAZE+A99g==} + '@typescript-eslint/project-service@8.72.0': + resolution: {integrity: sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.71.1': - resolution: {integrity: sha512-1YUDZXdTnXLlob03SFHBipdyMBsDVm7zoSz0ytTJ39dk5J1TgsTK03VlR/dLXG/RLLUrPcYbD4rp23BmHxHP0w==} + '@typescript-eslint/scope-manager@8.72.0': + resolution: {integrity: sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.71.1': - resolution: {integrity: sha512-oTvkml5SxXhgg+WWKhod5qWQICDXtOB+/Hi9VdPKFAANJ2b7YOgMffgvzXuqog7R6A/JtrzRf128dsxummyWhg==} + '@typescript-eslint/tsconfig-utils@8.72.0': + resolution: {integrity: sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.71.1': - resolution: {integrity: sha512-kH3t3jZCYv2PZSBEWPKTariLrCaoF1CLoPitYFBJOhAGEwWnSRAtGEifrfrpBa4K9JlwWNZotgA5w3/wLSk/FQ==} + '@typescript-eslint/type-utils@8.72.0': + resolution: {integrity: sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.71.1': - resolution: {integrity: sha512-ybYnPTUwg3VQQOFEiBPLs8Jxl1ry2vXdn1kVKguomBbvxpetdu3MKTNDqFNUrlG8ylP9MpZosmXMFYlGQHk1oQ==} + '@typescript-eslint/types@8.72.0': + resolution: {integrity: sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.71.1': - resolution: {integrity: sha512-pSKSEK1JJpHs6KiKVtABCE4iJK5d7FLFzDXQNBQ2oRY7LEd3UwOt3TZemTgu9syTIf56dJWlTn/pdmrO9oKUXQ==} + '@typescript-eslint/typescript-estree@8.72.0': + resolution: {integrity: sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.71.1': - resolution: {integrity: sha512-CBjT6gfAz3DW2j9Q4moFnqt208Aq+506sVGXfpRcyAeqn0EpayL0mjdWoCO2t3RC7GewNrFxdeehuEaPg6DMzQ==} + '@typescript-eslint/utils@8.72.0': + resolution: {integrity: sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.71.1': - resolution: {integrity: sha512-0GxiUGqMU0qXJt58cBsiG3DP8lUmBaFo2vChtUX+CT7PnR3v/VGLLrzVP4K9r1lNiZXjHYefMBbPunplr09v6w==} + '@typescript-eslint/visitor-keys@8.72.0': + resolution: {integrity: sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript/typescript-aix-ppc64@7.0.2': @@ -1345,8 +1348,8 @@ packages: resolution: {integrity: sha512-kKvt3m4uwzqL0wlkPd09CmljPJGOZZ4D0fP65sqFSvPkMRKhNi+74MgIJ5QxE6SxqB4t4KyUFGg8+n5zjo6hew==} engines: {node: '>=22'} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + argparse@3.0.3: + resolution: {integrity: sha512-Sn98ftV3KfsO8xfLPHG6oT8y+b0yvbrLgZjgPDXsharNY15tTAjqIsIju4S32nl6/iEUNNE4v+yL2Zhm8ooP3w==} array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} @@ -1644,8 +1647,8 @@ packages: peerDependencies: eslint: '>=5.16.0' - eslint-plugin-prettier@5.5.6: - resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + eslint-plugin-prettier@5.5.7: + resolution: {integrity: sha512-WjHyn3GrFd+ZR0rzsLiM4zJZ35PZMOE0SUIxzGmnWNG0126yNfJKJzObmmkYDwHJfGlkz3PSlzyG9ez/LJntwg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -1678,8 +1681,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.12.0: - resolution: {integrity: sha512-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA==} + eslint@10.13.0: + resolution: {integrity: sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1849,8 +1852,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.12: - resolution: {integrity: sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg==} + ignore@7.0.13: + resolution: {integrity: sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q==} engines: {node: '>= 4'} imurmurhash@0.1.4: @@ -1918,6 +1921,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -1989,8 +1996,8 @@ packages: resolution: {integrity: sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw==} engines: {node: '>=4'} - js-yaml@5.4.3: - resolution: {integrity: sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==} + js-yaml@5.5.0: + resolution: {integrity: sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==} hasBin: true json-parse-even-better-errors@6.0.0: @@ -2059,8 +2066,8 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - marked@18.1.0: - resolution: {integrity: sha512-PamYXWWWg2nboG3oX5Ffzpy3EFZROqZJK9hSlMmUnekp7TxPs5DcT95vwd46m73/exfKCRVMtgXoPMI4FEJL6w==} + marked@18.1.1: + resolution: {integrity: sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==} engines: {node: '>= 20'} hasBin: true @@ -2149,9 +2156,9 @@ packages: resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} - open@10.2.0: - resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} - engines: {node: '>=18'} + open@11.0.4: + resolution: {integrity: sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==} + engines: {node: '>=20'} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -2199,6 +2206,10 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + powershell-utils@0.2.1: + resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==} + engines: {node: '>=20'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2207,8 +2218,8 @@ packages: resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} engines: {node: '>=6.0.0'} - prettier@3.9.9: - resolution: {integrity: sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==} + prettier@3.9.10: + resolution: {integrity: sha512-skGHTH1Yb9QvaTC4hWnRTzEYtCpApF0UMBPz+t/0ZPzP8ihefo/btKd+QA7pUE0fEwA1SbS/NmhyEcstdXOIdA==} engines: {node: '>=14'} hasBin: true @@ -2223,8 +2234,8 @@ packages: resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} engines: {node: '>=20'} - qs@6.16.0: - resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} + qs@6.16.1: + resolution: {integrity: sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==} engines: {node: '>=0.6'} read-package-json-fast@6.0.0: @@ -2370,8 +2381,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - synckit@0.11.13: - resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + synckit@0.12.3: + resolution: {integrity: sha512-U+/AXT6LWsxnj2vz9HeUuPWBNgdWxjyIEjEPdbJARC9txmgPzo2VEdqnFo4hHaNyMPtycyVJrSaQYeQv/PoyyA==} engines: {node: ^14.18.0 || >=16.0.0} textextensions@6.11.0: @@ -2424,8 +2435,8 @@ packages: typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} - typescript-eslint@8.71.1: - resolution: {integrity: sha512-oiNVPC3/NbV1FpaHU07l8O6ynqG9v4PAiW2WvTrAmZdR0f2HI/gE668ElMLCK7Xbsu5k2fK5C4fXNiq05j7/nQ==} + typescript-eslint@8.72.0: + resolution: {integrity: sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2494,9 +2505,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wsl-utils@0.1.0: - resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} - engines: {node: '>=18'} + wsl-utils@1.0.1: + resolution: {integrity: sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==} + engines: {node: '>=20'} xml2js@0.5.0: resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} @@ -2572,7 +2583,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/identity@4.13.3': + '@azure/identity@4.14.0': dependencies: '@azure/abort-controller': 2.2.0 '@azure/core-auth': 1.11.0 @@ -2582,9 +2593,9 @@ snapshots: '@azure/core-tracing': 1.4.0 '@azure/core-util': 1.14.0 '@azure/logger': 1.4.0 - '@azure/msal-browser': 5.25.0 + '@azure/msal-browser': 5.25.1 '@azure/msal-node': 6.0.1 - open: 10.2.0 + open: 11.0.4 tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -2596,7 +2607,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@azure/msal-browser@5.25.0': + '@azure/msal-browser@5.25.1': dependencies: '@azure/msal-common': 16.14.2 @@ -2621,6 +2632,8 @@ snapshots: hashery: 1.5.1 keyv: 5.6.0 + '@dual-bundle/import-meta-resolve@4.2.1': {} + '@emnapi/core@1.11.2': dependencies: '@emnapi/wasi-threads': 1.2.2 @@ -2744,9 +2757,9 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.12.0)': + '@eslint-community/eslint-utils@4.10.1(eslint@10.13.0)': dependencies: - eslint: 10.12.0 + eslint: 10.13.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -2767,9 +2780,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.12.0)': + '@eslint/js@10.0.1(eslint@10.13.0)': optionalDependencies: - eslint: 10.12.0 + eslint: 10.13.0 '@eslint/object-schema@3.0.5': {} @@ -2796,122 +2809,122 @@ snapshots: '@inquirer/ansi@2.0.9': {} - '@inquirer/checkbox@5.2.6(@types/node@26.6.4)': + '@inquirer/checkbox@5.2.7(@types/node@26.6.5)': dependencies: '@inquirer/ansi': 2.0.9 - '@inquirer/core': 12.0.4(@types/node@26.6.4) + '@inquirer/core': 12.0.5(@types/node@26.6.5) '@inquirer/figures': 2.0.9 - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/type': 4.2.0(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/confirm@6.3.3(@types/node@26.6.4)': + '@inquirer/confirm@6.3.4(@types/node@26.6.5)': dependencies: - '@inquirer/core': 12.0.4(@types/node@26.6.4) - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/core': 12.0.5(@types/node@26.6.5) + '@inquirer/type': 4.2.0(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/core@12.0.4(@types/node@26.6.4)': + '@inquirer/core@12.0.5(@types/node@26.6.5)': dependencies: '@inquirer/ansi': 2.0.9 '@inquirer/figures': 2.0.9 - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/type': 4.2.0(@types/node@26.6.5) cli-width: 4.1.0 fast-wrap-ansi: 0.2.2 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/editor@5.3.4(@types/node@26.6.4)': + '@inquirer/editor@5.3.5(@types/node@26.6.5)': dependencies: - '@inquirer/core': 12.0.4(@types/node@26.6.4) - '@inquirer/external-editor': 3.0.6(@types/node@26.6.4) - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/core': 12.0.5(@types/node@26.6.5) + '@inquirer/external-editor': 3.0.6(@types/node@26.6.5) + '@inquirer/type': 4.2.0(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/expand@5.1.6(@types/node@26.6.4)': + '@inquirer/expand@5.1.7(@types/node@26.6.5)': dependencies: - '@inquirer/core': 12.0.4(@types/node@26.6.4) - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/core': 12.0.5(@types/node@26.6.5) + '@inquirer/type': 4.2.0(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/external-editor@3.0.6(@types/node@26.6.4)': + '@inquirer/external-editor@3.0.6(@types/node@26.6.5)': dependencies: chardet: 2.2.0 iconv-lite: 0.7.3 optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 '@inquirer/figures@2.0.9': {} - '@inquirer/input@5.1.7(@types/node@26.6.4)': + '@inquirer/input@5.1.8(@types/node@26.6.5)': dependencies: - '@inquirer/core': 12.0.4(@types/node@26.6.4) - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/core': 12.0.5(@types/node@26.6.5) + '@inquirer/type': 4.2.0(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/number@4.2.4(@types/node@26.6.4)': + '@inquirer/number@4.2.5(@types/node@26.6.5)': dependencies: - '@inquirer/core': 12.0.4(@types/node@26.6.4) - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/core': 12.0.5(@types/node@26.6.5) + '@inquirer/type': 4.2.0(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/password@5.2.3(@types/node@26.6.4)': + '@inquirer/password@5.2.4(@types/node@26.6.5)': dependencies: '@inquirer/ansi': 2.0.9 - '@inquirer/core': 12.0.4(@types/node@26.6.4) - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/core': 12.0.5(@types/node@26.6.5) + '@inquirer/type': 4.2.0(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 - - '@inquirer/prompts@8.7.3(@types/node@26.6.4)': - dependencies: - '@inquirer/checkbox': 5.2.6(@types/node@26.6.4) - '@inquirer/confirm': 6.3.3(@types/node@26.6.4) - '@inquirer/editor': 5.3.4(@types/node@26.6.4) - '@inquirer/expand': 5.1.6(@types/node@26.6.4) - '@inquirer/input': 5.1.7(@types/node@26.6.4) - '@inquirer/number': 4.2.4(@types/node@26.6.4) - '@inquirer/password': 5.2.3(@types/node@26.6.4) - '@inquirer/rawlist': 5.3.6(@types/node@26.6.4) - '@inquirer/search': 4.3.4(@types/node@26.6.4) - '@inquirer/select': 5.2.6(@types/node@26.6.4) + '@types/node': 26.6.5 + + '@inquirer/prompts@8.7.4(@types/node@26.6.5)': + dependencies: + '@inquirer/checkbox': 5.2.7(@types/node@26.6.5) + '@inquirer/confirm': 6.3.4(@types/node@26.6.5) + '@inquirer/editor': 5.3.5(@types/node@26.6.5) + '@inquirer/expand': 5.1.7(@types/node@26.6.5) + '@inquirer/input': 5.1.8(@types/node@26.6.5) + '@inquirer/number': 4.2.5(@types/node@26.6.5) + '@inquirer/password': 5.2.4(@types/node@26.6.5) + '@inquirer/rawlist': 5.3.7(@types/node@26.6.5) + '@inquirer/search': 4.3.5(@types/node@26.6.5) + '@inquirer/select': 5.2.7(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/rawlist@5.3.6(@types/node@26.6.4)': + '@inquirer/rawlist@5.3.7(@types/node@26.6.5)': dependencies: - '@inquirer/core': 12.0.4(@types/node@26.6.4) - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/core': 12.0.5(@types/node@26.6.5) + '@inquirer/type': 4.2.0(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/search@4.3.4(@types/node@26.6.4)': + '@inquirer/search@4.3.5(@types/node@26.6.5)': dependencies: - '@inquirer/core': 12.0.4(@types/node@26.6.4) + '@inquirer/core': 12.0.5(@types/node@26.6.5) '@inquirer/figures': 2.0.9 - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/type': 4.2.0(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/select@5.2.6(@types/node@26.6.4)': + '@inquirer/select@5.2.7(@types/node@26.6.5)': dependencies: '@inquirer/ansi': 2.0.9 - '@inquirer/core': 12.0.4(@types/node@26.6.4) + '@inquirer/core': 12.0.5(@types/node@26.6.5) '@inquirer/figures': 2.0.9 - '@inquirer/type': 4.2.0(@types/node@26.6.4) + '@inquirer/type': 4.2.0(@types/node@26.6.5) optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 - '@inquirer/type@4.2.0(@types/node@26.6.4)': + '@inquirer/type@4.2.0(@types/node@26.6.5)': optionalDependencies: - '@types/node': 26.6.4 + '@types/node': 26.6.5 '@keyv/bigmap@1.3.1(keyv@5.6.0)': dependencies: @@ -2921,16 +2934,16 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@napi-rs/cli@3.10.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.4)': + '@napi-rs/cli@3.10.8(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.6.5)': dependencies: - '@inquirer/prompts': 8.7.3(@types/node@26.6.4) + '@inquirer/prompts': 8.7.4(@types/node@26.6.5) '@napi-rs/cross-toolchain': 1.0.3 '@napi-rs/wasm-tools': 1.1.0 '@octokit/rest': 22.0.1 clipanion: 4.0.0-rc.4(typanion@3.14.0) colorette: 2.0.20 es-toolkit: 1.52.0 - js-yaml: 5.4.3 + js-yaml: 5.5.0 obug: 3.0.0 semver: 7.8.5 typanion: 3.14.0 @@ -3341,78 +3354,78 @@ snapshots: '@types/json5@0.0.29': {} - '@types/node@26.6.4': + '@types/node@26.6.5': dependencies: undici-types: 8.9.0 '@types/vscode@1.61.0': {} - '@typescript-eslint/eslint-plugin@8.71.1(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0))(@typescript/typescript6@6.0.2)(eslint@10.12.0)': + '@typescript-eslint/eslint-plugin@8.72.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0))(@typescript/typescript6@6.0.2)(eslint@10.13.0)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0) - '@typescript-eslint/scope-manager': 8.71.1 - '@typescript-eslint/type-utils': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0) - '@typescript-eslint/utils': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0) - '@typescript-eslint/visitor-keys': 8.71.1 - eslint: 10.12.0 - ignore: 7.0.12 + '@typescript-eslint/parser': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0) + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/type-utils': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0) + '@typescript-eslint/utils': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0) + '@typescript-eslint/visitor-keys': 8.72.0 + eslint: 10.13.0 + ignore: 7.0.13 natural-compare: 1.4.0 ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0)': + '@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0)': dependencies: - '@typescript-eslint/scope-manager': 8.71.1 - '@typescript-eslint/types': 8.71.1 - '@typescript-eslint/typescript-estree': 8.71.1(@typescript/typescript6@6.0.2) - '@typescript-eslint/visitor-keys': 8.71.1 + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/visitor-keys': 8.72.0 debug: 4.4.3 - eslint: 10.12.0 + eslint: 10.13.0 typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.71.1(@typescript/typescript6@6.0.2)': + '@typescript-eslint/project-service@8.72.0(@typescript/typescript6@6.0.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.71.1(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.71.1 + '@typescript-eslint/tsconfig-utils': 8.72.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.72.0 debug: 4.4.3 typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.71.1': + '@typescript-eslint/scope-manager@8.72.0': dependencies: - '@typescript-eslint/types': 8.71.1 - '@typescript-eslint/visitor-keys': 8.71.1 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/visitor-keys': 8.72.0 - '@typescript-eslint/tsconfig-utils@8.71.1(@typescript/typescript6@6.0.2)': + '@typescript-eslint/tsconfig-utils@8.72.0(@typescript/typescript6@6.0.2)': dependencies: typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/type-utils@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0)': + '@typescript-eslint/type-utils@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0)': dependencies: - '@typescript-eslint/types': 8.71.1 - '@typescript-eslint/typescript-estree': 8.71.1(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0) + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/utils': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0) debug: 4.4.3 - eslint: 10.12.0 + eslint: 10.13.0 ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.71.1': {} + '@typescript-eslint/types@8.72.0': {} - '@typescript-eslint/typescript-estree@8.71.1(@typescript/typescript6@6.0.2)': + '@typescript-eslint/typescript-estree@8.72.0(@typescript/typescript6@6.0.2)': dependencies: - '@typescript-eslint/project-service': 8.71.1(@typescript/typescript6@6.0.2) - '@typescript-eslint/tsconfig-utils': 8.71.1(@typescript/typescript6@6.0.2) - '@typescript-eslint/types': 8.71.1 - '@typescript-eslint/visitor-keys': 8.71.1 + '@typescript-eslint/project-service': 8.72.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/tsconfig-utils': 8.72.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/visitor-keys': 8.72.0 debug: 4.4.3 minimatch: 10.2.6 semver: 7.8.5 @@ -3422,20 +3435,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0)': + '@typescript-eslint/utils@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0) - '@typescript-eslint/scope-manager': 8.71.1 - '@typescript-eslint/types': 8.71.1 - '@typescript-eslint/typescript-estree': 8.71.1(@typescript/typescript6@6.0.2) - eslint: 10.12.0 + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0) + '@typescript-eslint/scope-manager': 8.72.0 + '@typescript-eslint/types': 8.72.0 + '@typescript-eslint/typescript-estree': 8.72.0(@typescript/typescript6@6.0.2) + eslint: 10.13.0 typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.71.1': + '@typescript-eslint/visitor-keys@8.72.0': dependencies: - '@typescript-eslint/types': 8.71.1 + '@typescript-eslint/types': 8.72.0 eslint-visitor-keys: 5.0.1 '@typescript/typescript-aix-ppc64@7.0.2': @@ -3551,7 +3564,7 @@ snapshots: '@vscode/vsce@4.0.0': dependencies: - '@azure/identity': 4.13.3 + '@azure/identity': 4.14.0 '@napi-rs/keyring': 1.3.0 '@secretlint/core': 10.2.2 '@secretlint/secretlint-rule-no-dotenv': 10.2.2 @@ -3564,7 +3577,7 @@ snapshots: commander: 12.1.0 hosted-git-info: 4.1.0 jsonc-parser: 3.3.1 - marked: 18.1.0 + marked: 18.1.1 mime: 1.6.0 minimatch: 10.2.6 parse5: 8.0.1 @@ -3597,7 +3610,7 @@ snapshots: ansi-styles@7.0.0: {} - argparse@2.0.1: {} + argparse@3.0.3: {} array-buffer-byte-length@1.0.2: dependencies: @@ -3941,9 +3954,9 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.12.0): + eslint-config-prettier@10.1.8(eslint@10.13.0): dependencies: - eslint: 10.12.0 + eslint: 10.13.0 eslint-import-resolver-node@0.3.10: dependencies: @@ -3953,23 +3966,23 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0))(eslint-import-resolver-node@0.3.10)(eslint@10.12.0): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0))(eslint-import-resolver-node@0.3.10)(eslint@10.13.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0) - eslint: 10.12.0 + '@typescript-eslint/parser': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0) + eslint: 10.13.0 eslint-import-resolver-node: 0.3.10 transitivePeerDependencies: - supports-color - eslint-plugin-es@3.0.1(eslint@10.12.0): + eslint-plugin-es@3.0.1(eslint@10.13.0): dependencies: - eslint: 10.12.0 + eslint: 10.13.0 eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0))(eslint@10.12.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0))(eslint@10.13.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.2.0 @@ -3978,9 +3991,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 10.12.0 + eslint: 10.13.0 eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0))(eslint-import-resolver-node@0.3.10)(eslint@10.12.0) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0))(eslint-import-resolver-node@0.3.10)(eslint@10.13.0) hasown: 2.0.4 is-core-module: 2.17.0 is-glob: 4.0.3 @@ -3992,30 +4005,30 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0) + '@typescript-eslint/parser': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-node@11.1.0(eslint@10.12.0): + eslint-plugin-node@11.1.0(eslint@10.13.0): dependencies: - eslint: 10.12.0 - eslint-plugin-es: 3.0.1(eslint@10.12.0) + eslint: 10.13.0 + eslint-plugin-es: 3.0.1(eslint@10.13.0) eslint-utils: 2.1.0 ignore: 5.3.2 minimatch: 3.1.5 resolve: 1.22.13 semver: 6.3.1 - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.12.0))(eslint@10.12.0)(prettier@3.9.9): + eslint-plugin-prettier@5.5.7(eslint-config-prettier@10.1.8(eslint@10.13.0))(eslint@10.13.0)(prettier@3.9.10): dependencies: - eslint: 10.12.0 - prettier: 3.9.9 + eslint: 10.13.0 + prettier: 3.9.10 prettier-linter-helpers: 1.0.1 - synckit: 0.11.13 + synckit: 0.12.3 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.12.0) + eslint-config-prettier: 10.1.8(eslint@10.13.0) eslint-scope@9.1.2: dependencies: @@ -4034,9 +4047,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.12.0: + eslint@10.13.0: dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.12.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.13.0) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5 '@eslint/config-helpers': 0.7.0 @@ -4237,7 +4250,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.12: {} + ignore@7.0.13: {} imurmurhash@0.1.4: {} @@ -4311,6 +4324,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-in-ssh@1.0.0: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -4379,9 +4394,9 @@ snapshots: editions: 6.22.0 textextensions: 6.11.0 - js-yaml@5.4.3: + js-yaml@5.5.0: dependencies: - argparse: 2.0.1 + argparse: 3.0.3 json-parse-even-better-errors@6.0.0: {} @@ -4452,7 +4467,7 @@ snapshots: dependencies: yallist: 4.0.0 - marked@18.1.0: {} + marked@18.1.1: {} math-intrinsics@1.1.0: {} @@ -4543,12 +4558,14 @@ snapshots: obug@3.0.0: {} - open@10.2.0: + open@11.0.4: dependencies: default-browser: 5.5.1 define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 is-inside-container: 1.0.0 - wsl-utils: 0.1.0 + powershell-utils: 0.2.1 + wsl-utils: 1.0.1 optionator@0.9.4: dependencies: @@ -4592,13 +4609,15 @@ snapshots: possible-typed-array-names@1.1.0: {} + powershell-utils@0.2.1: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.1: dependencies: fast-diff: 1.3.0 - prettier@3.9.9: {} + prettier@3.9.10: {} proper-lockfile@4.1.2: dependencies: @@ -4612,7 +4631,7 @@ snapshots: dependencies: hookified: 2.2.0 - qs@6.16.0: + qs@6.16.1: dependencies: es-define-property: 1.0.1 side-channel: 1.1.1 @@ -4796,8 +4815,9 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - synckit@0.11.13: + synckit@0.12.3: dependencies: + '@dual-bundle/import-meta-resolve': 4.2.1 '@pkgr/core': 0.3.6 textextensions@6.11.0: @@ -4864,17 +4884,17 @@ snapshots: typed-rest-client@1.8.11: dependencies: - qs: 6.16.0 + qs: 6.16.1 tunnel: 0.0.6 underscore: 1.13.8 - typescript-eslint@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0): + typescript-eslint@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0): dependencies: - '@typescript-eslint/eslint-plugin': 8.71.1(@typescript-eslint/parser@8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0))(@typescript/typescript6@6.0.2)(eslint@10.12.0) - '@typescript-eslint/parser': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0) - '@typescript-eslint/typescript-estree': 8.71.1(@typescript/typescript6@6.0.2) - '@typescript-eslint/utils': 8.71.1(@typescript/typescript6@6.0.2)(eslint@10.12.0) - eslint: 10.12.0 + '@typescript-eslint/eslint-plugin': 8.72.0(@typescript-eslint/parser@8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0))(@typescript/typescript6@6.0.2)(eslint@10.13.0) + '@typescript-eslint/parser': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0) + '@typescript-eslint/typescript-estree': 8.72.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/utils': 8.72.0(@typescript/typescript6@6.0.2)(eslint@10.13.0) + eslint: 10.13.0 typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color @@ -4976,9 +4996,10 @@ snapshots: word-wrap@1.2.5: {} - wsl-utils@0.1.0: + wsl-utils@1.0.1: dependencies: is-wsl: 3.1.1 + powershell-utils: 0.2.1 xml2js@0.5.0: dependencies: diff --git a/server/Cargo.toml b/server/Cargo.toml index 92ec62ec..ed712c77 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -62,7 +62,7 @@ imara-diff = { version = "0.2", features = [] } indoc = "2.0.5" log.workspace = true log4rs = "1.3" -lru = "0.18.5" +lru = "0.19" markup5ever_rcdom = "0.39" mime = "0.3.17" mime_guess = "2.0.5" @@ -105,7 +105,7 @@ chrono = "0.4" futures = "0.3.31" predicates = "3.1.2" pretty_assertions = "1.4.1" -thirtyfour = "0.37" +thirtyfour = "0.38" tokio-tungstenite = "0.30" # Lints diff --git a/server/src/ide/vscode/tests.rs b/server/src/ide/vscode/tests.rs index a7063a1c..728d05dd 100644 --- a/server/src/ide/vscode/tests.rs +++ b/server/src/ide/vscode/tests.rs @@ -39,8 +39,7 @@ use pretty_assertions::assert_eq; use tokio::{ io::{AsyncRead, AsyncWrite}, net::TcpStream, - select, - time::sleep, + time::{sleep, timeout}, }; use tokio_tungstenite::{ MaybeTlsStream, WebSocketStream, connect_async, @@ -70,6 +69,7 @@ use test_utils::{ // Globals // ------- +// // Run a single webserver for all tests. static WEBSERVER_HANDLE: LazyLock>> = LazyLock::new(|| { thread::spawn(|| { @@ -107,23 +107,29 @@ async fn send_message( /// Read a message from a websocket. async fn read_message( + // The websocket to read from. ws_stream: &mut WebSocketStream, + // The message read. ) -> EditorMessage { - let now = SystemTime::now(); - let msg_txt = loop { - let msg = select! { - data = ws_stream.next() => data.unwrap().unwrap(), - () = sleep(Duration::from_secs(6).checked_sub(now.elapsed().unwrap()).unwrap()) => panic!("Timeout waiting for message") - }; - match msg { - Message::Close(_) => panic!("Unexpected close message."), - Message::Ping(_) => ws_stream.send(Message::Pong(vec![].into())).await.unwrap(), - Message::Pong(_) => panic!("Unexpected pong message."), - Message::Text(txt) => break txt, - Message::Binary(_) => panic!("Unexpected binary message."), - Message::Frame(_) => panic!("Unexpected frame message."), + let msg_txt = timeout(Duration::from_secs(6), async { + loop { + let msg = ws_stream + .next() + .await + .expect("The websocket ended without a close message.") + .unwrap_or_else(|err| panic!("Unable to read from the websocket: {err}")); + match msg { + Message::Close(_) => panic!("Unexpected close message."), + Message::Ping(data) => ws_stream.send(Message::Pong(data)).await.unwrap(), + Message::Pong(_) => panic!("Unexpected pong message."), + Message::Text(txt) => break txt, + Message::Binary(_) => panic!("Unexpected binary message."), + Message::Frame(_) => panic!("Unexpected frame message."), + } } - }; + }) + .await + .expect("Timeout waiting for message"); serde_json::from_str(&msg_txt) .unwrap_or_else(|_| panic!("Unable to convert '{msg_txt}' to JSON.")) } @@ -225,8 +231,8 @@ async fn prep_test_core( (temp_dir, test_dir, ws_ide, ws_client) } -/// This calls `prep_test_core` with the current function name. It must be a macro, -/// so that it's called with the test function's name; calling it inside +/// This calls `prep_test_core` with the current function name. It must be a +/// macro, so that it's called with the test function's name; calling it inside /// `prep_test_core` would give the wrong name. macro_rules! prep_test { ($connection_id: ident) => {{ @@ -1410,7 +1416,7 @@ async fn test_vscode_ide_websocket5() { ws_client.close(None).await.unwrap(); loop { match ws_ide.next().await.unwrap().unwrap() { - Message::Ping(_) => ws_ide.send(Message::Pong(vec![].into())).await.unwrap(), + Message::Ping(data) => ws_ide.send(Message::Pong(data)).await.unwrap(), Message::Close(_) => break, _ => panic!("Unexpected message."), } @@ -1431,7 +1437,7 @@ async fn test_vscode_ide_websocket9() { ws_ide.close(None).await.unwrap(); loop { match ws_client.next().await.unwrap().unwrap() { - Message::Ping(_) => ws_client.send(Message::Pong(vec![].into())).await.unwrap(), + Message::Ping(data) => ws_client.send(Message::Pong(data)).await.unwrap(), Message::Close(_) => break, _ => panic!("Unexpected message."), } @@ -1575,8 +1581,8 @@ async fn test_vscode_ide_websocket10() { // // The consumer's initial scan of the project may reach the TOC on either // side of this write; either way, only one commit defines `foo`, outdating - // the current file once. A scan before the write finds no ids in the TOC. - // A scan after it commits the new TOC, which makes the watcher's report a + // the current file once. A scan before the write finds no ids in the TOC. A + // scan after it commits the new TOC, which makes the watcher's report a // no-op: the TOC's metadata then matches the cache's, so the consumer skips // it. fs::write(test_dir.join("toc.md"), "

Foo

\n").unwrap(); diff --git a/server/src/processing.rs b/server/src/processing.rs index bf23dbcc..50ac0564 100644 --- a/server/src/processing.rs +++ b/server/src/processing.rs @@ -54,7 +54,7 @@ use dprint_plugin_markdown::{ format_text, }; use htmd::{ - HtmlToMarkdown, + Conversion, HtmlToMarkdown, options::{BrStyle, LinkStyle, TranslationMode}, }; use html5ever::{ @@ -486,7 +486,8 @@ pub fn codechat_for_web_to_source( return Err(CodechatForWebToSourceError::DocBlocksNotAllowed); } // Translate the HTML document to Markdown. - let converter = HtmlToMarkdownWrapped::new(); + let html_to_markdown = new_html_to_markdown(); + let converter = HtmlToMarkdownWrapped::new(&html_to_markdown); let tree = html_to_dom(&code_mirror.doc, None)?; dehydrating_walk_node(&tree); return converter @@ -581,9 +582,27 @@ fn code_mirror_to_code_doc_blocks(code_mirror: &CodeMirror) -> Vec code_doc_block_arr } -/// This converts HTML to Markdown then word wraps the result. -struct HtmlToMarkdownWrapped { - html_to_markdown: HtmlToMarkdown, +/// Build the HTML to Markdown converter which an [`HtmlToMarkdownWrapped`] +/// borrows. A `Conversion` holds a reference to its converter, so the converter +/// must outlive the wrapper rather than live inside it. +fn new_html_to_markdown() -> HtmlToMarkdown { + // Most of the options don't need to be specified here, since the line + // wrapper will override them. + HtmlToMarkdown::builder() + .options(htmd::options::Options { + link_style: LinkStyle::Inlined, + translation_mode: TranslationMode::Faithful, + // Note that this is ignored in Faithful mode. + br_style: BrStyle::Backslash, + ..Default::default() + }) + .build() +} + +/// This converts a document, supplied as one or more DOM trees, from HTML to +/// Markdown then word wraps the result. +struct HtmlToMarkdownWrapped<'a> { + html_to_markdown_conversion: Conversion<'a>, word_wrap_config: Configuration, } @@ -597,20 +616,13 @@ pub enum HtmlToMarkdownWrappedError { LineWidthOverflow(#[from] std::num::TryFromIntError), } -impl HtmlToMarkdownWrapped { - fn new() -> Self { +impl<'a> HtmlToMarkdownWrapped<'a> { + fn new( + // The converter, typically from `new_html_to_markdown`. + html_to_markdown: &'a HtmlToMarkdown, + ) -> Self { HtmlToMarkdownWrapped { - // Most of the options don't need to be specified here, since the - // line wrapper will override them. - html_to_markdown: HtmlToMarkdown::builder() - .options(htmd::options::Options { - link_style: LinkStyle::Inlined, - translation_mode: TranslationMode::Faithful, - // Note that this is ignored in Faithful mode. - br_style: BrStyle::Backslash, - ..Default::default() - }) - .build(), + html_to_markdown_conversion: html_to_markdown.start_conversion(), // TODO: numbered list formatting should be improved in the dprint // library. word_wrap_config: ConfigurationBuilder::new() @@ -632,7 +644,7 @@ impl HtmlToMarkdownWrapped { /// Convert one item in a stream of HTML to markdown. The HTML must be start /// at the root, not continue a previous incomplete section of the DOM. fn next(&self, tree: &Rc) -> Result { - let converted = self.html_to_markdown.tree_to_markdown(tree); + let converted = self.html_to_markdown_conversion.tree_to_markdown(tree); Ok( format_text(&converted, &self.word_wrap_config, |_, _, _| Ok(None))? // A return value of `None` means the text was unchanged or @@ -643,16 +655,28 @@ impl HtmlToMarkdownWrapped { ) } - fn last(&self) -> Result { - let converted = self.html_to_markdown.finalize_conversion(); + /// End the document, returning the Markdown placed at its end (such as + /// link reference definitions). This consumes the wrapper, since finishing + /// a `Conversion` consumes it. + fn last( + self, + // The word-wrapped Markdown which ends the document. + ) -> Result { + let converted = self.html_to_markdown_conversion.finish(); Ok( format_text(&converted, &self.word_wrap_config, |_, _, _| Ok(None))? .unwrap_or(converted), ) } - /// Convert HTML to markdown. - fn convert(&self, tree: &Rc) -> Result { + /// Convert a document consisting of a single DOM tree from HTML to + /// markdown. + fn convert( + self, + // The DOM tree holding the entire document. + tree: &Rc, + // The document as word-wrapped Markdown. + ) -> Result { let mut converted = self.next(tree)?; converted.push_str(&self.last()?); Ok(converted) @@ -670,7 +694,8 @@ pub fn doc_block_html_to_markdown( // containing only one doc block. dom_location: Option<&(Vec, usize)>, ) -> Result, HtmlToMarkdownWrappedError> { - let mut converter = HtmlToMarkdownWrapped::new(); + let html_to_markdown = new_html_to_markdown(); + let mut converter = HtmlToMarkdownWrapped::new(&html_to_markdown); let mut last_doc_block_index = None; // Only perform marker insertions to a length 1 vec. assert!(dom_location.is_none() || code_doc_block_vec.len() == 1); diff --git a/server/src/processing/cache-spec.md b/server/src/processing/cache-spec.md index a0dc014a..f9be25c3 100644 --- a/server/src/processing/cache-spec.md +++ b/server/src/processing/cache-spec.md @@ -17,15 +17,14 @@ do not. The cache reflects data read directly from disk/IDE; content edited in the Client does not update the cache until it's written to disk/IDE, at which point -the cache must re-process the file. Since the Client is designed around -an autosave principle which updates disk/IDE regularly, there's little gap -between the two. +the cache must re-process the file. Since the Client is designed around an +autosave principle which updates disk/IDE regularly, there's little gap between +the two. The cache hydrates data to the Client; the dehydration routes are responsible for removing all cache hydration artifacts. -Cross references ----------------- +## Cross references A `` is a cross reference to a `Target` or a gather element. The `ref` attribute specifies the destination as an id; the cache then @@ -36,9 +35,9 @@ is only present in the Client. Details: -* This element allows no attribute except for `ref`. Since an `` is - never a `Target`, an `id` attribute on it -- including `id="*"` -- would - mislead, so it replaces the contents with an error message. +* This element allows no attribute except for `ref`. Since an `` is never + a `Target`, an `id` attribute on it -- including `id="*"` -- would mislead, so + it replaces the contents with an error message. * The inner HTML is always taken before cache hydration, to prevent circular dependencies (see also ): @@ -63,8 +62,7 @@ Details: * Cross references to fragments are not allowed; this would create circular dependencies (see ). -Fragments and gather elements ------------------------------ +## Fragments and gather elements A gather element such as `

Bazzy things

` is a `Target` with the `data-gather` attribute. It becomes a list @@ -93,18 +91,18 @@ Details: contents will be replaced by an error message. * Fragments store an HTML rendering of the code and doc blocks they contain, excluding the content produced by hydrating the `` tags, to avoid - duplication and circular dependencies. Cross-references in this rendering - are hydrated in the fragment's file, so their links lead from there; a - gather element re-hydrates each which hydrated to a link so that it leads - from the gather element's file instead. An error message reads the same - anywhere, so it's kept as is. See layer 4 under - : this exclusion is what keeps a gather - element and the fragments it lists from outdating each other forever. The HTML - rendering of a fragment reproduces the layout of the source it came from: each - doc block includes its indent, each line of a code block or the first line of - a doc block is preceded by that line's number, and the two are aligned -- a - doc block and a line of code indented equally in the source begin in the same - column, with the line numbers in a gutter of their own to the left of both. + duplication and circular dependencies. Cross-references in this rendering are + hydrated in the fragment's file, so their links lead from there; a gather + element re-hydrates each which hydrated to a link so that it leads from the + gather element's file instead. An error message reads the same anywhere, so + it's kept as is. See layer 4 under : this + exclusion is what keeps a gather element and the fragments it lists from + outdating each other forever. The HTML rendering of a fragment reproduces the + layout of the source it came from: each doc block includes its indent, each + line of a code block or the first line of a doc block is preceded by that + line's number, and the two are aligned -- a doc block and a line of code + indented equally in the source begin in the same column, with the line numbers + in a gutter of their own to the left of both. * The backlinks a `` hydrates to are derived from the gather elements which list it: for each such element, its containing file, its `id`, and its inner HTML (the link text). A fragment's rendered output therefore depends on @@ -199,9 +197,10 @@ lower-numbered layers: fragment it lists would outdate one another forever. 5. Gather-list hydration depends on layers 1 and 3. It also re-hydrates each `` within a fragment's content (layer 2) which hydrated to a link, so - that it links from the gathering file. That needs no dependency of its own: everything about a target which - this reads is embedded in the fragment's content, so any change to it also - changes that content, which outdates the gathering file. + that it links from the gathering file. That needs no dependency of its own: + everything about a target which this reads is embedded in the fragment's + content, so any change to it also changes that content, which outdates the + gathering file. Only layers 1 and 3 are cached, and layer 3 depends only on layer 1 through layer 2. So a change travels at most two steps (C → B → A), which matches the @@ -228,8 +227,8 @@ differences is: A gather element's `gather_ids` are absent from this list because no dependent reads them: a cross-reference to a gather element renders only its inner HTML. -The fragments a gather element lists do depend on them, through the reverse -edge described below. +The fragments a gather element lists do depend on them, through the reverse edge +described below. This also makes indirection (gather A includes fragment B, whose content cross-references target C) work without extra machinery: a change to C outdates @@ -394,11 +393,10 @@ timestamp tick could leave this metadata unchanged (Git's "racy clean" problem). * If the file exists and doesn't match its cached stamp or if the file's status is `Outdated`: - * If the file is the TOC and doesn't match its cached stamp, send - `TocChanged` to all connections once the processing below is done, even if - it ignores the file: each connection's sidebar renders the TOC from the - text it last loaded, which it must therefore re-load (see `Connections` - below). + * If the file is the TOC and doesn't match its cached stamp, send `TocChanged` + to all connections once the processing below is done, even if it ignores the + file: each connection's sidebar renders the TOC from the text it last + loaded, which it must therefore re-load (see `Connections` below). * If the file is the current file for any of the cache's connections, ignore it. This is handled by the standard IDE to Client translation mechanism. @@ -476,7 +474,27 @@ Overall shutdown sequence: 4. The consumer thread exits when either its consumer rx channel closes or the weak pointer fails to upgrade. -TODO: +## Links in hydrated content + +Some content, such as math, cross-references, fragments, and gather lists (see + and ), sits +inside an element marked `contenteditable=false`. TinyMCE's link plugin offers +no context menu items for non-editable content, so a right click (mouse) or long +press (touch) on one of these links is ignored. Therefore, when the user +performs either of these actions, the Client must show the TinyMCE context menu, +whose only item, "Open link", follows the link as a Ctrl+click does. A right +click or long press on non-editable content beside its links offers no "Open +link". With a non-editable element selected, the keyboard's context menu +shortcut (the Menu key, or Shift+F10) shows the same menu, which follows the +element's first link, as Alt+Enter does. The Client also follows these links in +the other ways it follows any link (see `Link navigation` in +[implementation.md](../../../docs/implementation.md)): + +* Ctrl+click (Cmd+click on macOS) follows the clicked link. +* Alt+Enter, with the non-editable element selected, follows its first link. + +TODO +---- * `SaveOnScan` and search functionality are not implemented yet. This is future work. diff --git a/server/src/processing/tests.rs b/server/src/processing/tests.rs index 552094c0..35996d2f 100644 --- a/server/src/processing/tests.rs +++ b/server/src/processing/tests.rs @@ -53,7 +53,8 @@ use crate::{ cache::{Cache, FileStatus}, code_doc_block_vec_to_source, code_mirror_to_code_doc_blocks, codechat_for_web_to_source, dehydrating_walk_node, diff_code_mirror_doc_blocks, diff_str, doc_block_html_to_markdown, - html_to_dom, hydrate_html, is_css_identifier, markdown_to_html, source_to_codechat_for_web, + html_to_dom, hydrate_html, is_css_identifier, markdown_to_html, new_html_to_markdown, + source_to_codechat_for_web, }, }; use test_utils::{cast, prep_test_dir, test_utils::stringit}; @@ -2292,9 +2293,10 @@ fn dehydrate_html(html: &str) -> io::Result> { #[test] fn test_dehydrate_html_1() { - let converter = HtmlToMarkdownWrapped::new(); + // Each document needs its own conversion, since `convert` consumes it. + let html_to_markdown = new_html_to_markdown(); assert_eq!( - converter + HtmlToMarkdownWrapped::new(&html_to_markdown) .convert( &dehydrate_html(indoc!( " @@ -2317,7 +2319,7 @@ fn test_dehydrate_html_1() { ); assert_eq!( - converter + HtmlToMarkdownWrapped::new(&html_to_markdown) .convert( &dehydrate_html(indoc!( " @@ -2342,7 +2344,7 @@ fn test_dehydrate_html_1() { ); assert_eq!( - converter + HtmlToMarkdownWrapped::new(&html_to_markdown) .convert( &dehydrate_html(indoc!( r#" @@ -2367,7 +2369,7 @@ fn test_dehydrate_html_1() { ); assert_eq!( - converter + HtmlToMarkdownWrapped::new(&html_to_markdown) .convert( &dehydrate_html(indoc!( " @@ -2383,7 +2385,7 @@ fn test_dehydrate_html_1() { ); assert_eq!( - converter + HtmlToMarkdownWrapped::new(&html_to_markdown) .convert( &dehydrate_html(indoc!( r#" @@ -2428,7 +2430,7 @@ fn test_dehydrate_html_1() { // `


` by `dehydrating_walk_node`, preserving it as a // non-breaking space. assert_eq!( - converter + HtmlToMarkdownWrapped::new(&html_to_markdown) .convert( &dehydrate_html(indoc!( " diff --git a/server/src/webserver.rs b/server/src/webserver.rs index 37f87728..e90c79b9 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -52,7 +52,7 @@ use actix_web::{ }; use actix_web_httpauth::{extractors::basic::BasicAuth, middleware::HttpAuthentication}; -use actix_ws::AggregatedMessage; +use actix_ws::{AggregatedMessage, CloseCode, CloseReason}; use bytes::Bytes; use dunce::simplified; use futures_util::StreamExt; @@ -79,7 +79,7 @@ use tokio::{ oneshot, }, task::JoinHandle, - time::sleep, + time::{Instant, MissedTickBehavior, interval_at, sleep}, }; use ts_rs::TS; use url::Url; @@ -492,9 +492,18 @@ pub const REPLY_TIMEOUT_MS: Duration = if cfg!(test) { Duration::from_secs(15) }; -/// The time to wait for a pong from the websocket in response to a ping sent by -/// this server. -const WEBSOCKET_PING_DELAY: Duration = Duration::from_secs(2); +/// How often this server pings each websocket, which is also how long it waits +/// for the answering pong before closing the websocket. The unit tests in +/// `ide/vscode/tests.rs` read one websocket of a pair while leaving the other +/// unread, and tungstenite answers a ping only while its websocket is read. +/// Waiting on the cache's watcher can leave a websocket unread for longer than +/// twice the normal delay, so the unit tests use a delay which no wait +/// approaches. +const WEBSOCKET_PING_DELAY: Duration = if cfg!(test) { + Duration::from_secs(10) +} else { + Duration::from_secs(2) +}; /// A few message IDs reserved for use during startup or for sending errors. pub const RESERVED_MESSAGE_ID: f64 = 0.0; @@ -1340,28 +1349,30 @@ pub fn client_websocket( // otherwise, closing represents a network interruption (such as the // computer going to sleep). let mut is_closing = false; + // The status code and description which the Server's Close frame sends + // the websocket's client, if any. + let mut close_reason: Option = None; // True if a ping was sent, but a matching pong wasn't yet received. let mut sent_ping = false; + // Ping on a fixed schedule, rather than after a quiet period, so that + // a steady stream of outgoing messages can't put off the ping, and + // with it the detection of a broken websocket, indefinitely. A send + // which stalls the loop past a tick mustn't produce a burst of + // catch-up ticks, which would close the websocket before its pong + // could arrive. + let mut ping_interval = + interval_at(Instant::now() + WEBSOCKET_PING_DELAY, WEBSOCKET_PING_DELAY); + ping_interval.set_missed_tick_behavior(MissedTickBehavior::Delay); loop { select! { - // Send pings on a regular basis. - () = sleep(WEBSOCKET_PING_DELAY) => { - if sent_ping { - // If we haven't received the answering pong, the - // websocket must be broken. - break; - } - // Send a ping to check that the websocket is still open. - // For example, putting a PC to sleep then waking it breaks - // the websocket, but the server doesn't detect this without - // sending a ping (which then fails). - if let Err(err) = session.ping(&Bytes::new()).await { - error!("Unable to send ping: {err}"); - break; - } - sent_ping = true; - } + // Poll the branches in the order written, rather than at + // random. Reading the websocket comes first, so that a tick + // never judges the websocket broken while the answering pong + // waits unread, as it may after a send stalls the loop. The + // tick comes before outgoing messages, so that a steady stream + // of those can't put off the ping. + biased; // Process a message received from the websocket. Some(msg_wrapped) = aggregated_msg_stream.next() => { @@ -1450,12 +1461,39 @@ pub fn client_websocket( } } } + // A protocol error must + // [fail the connection](https://www.rfc-editor.org/rfc/rfc6455#section-7.1.7), + // sending a Close frame with an + // [appropriate status code](https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1). + // The stream may also report the same error on every + // poll, which, since this branch is polled first, + // would starve the others. Err(err) => { error!("websocket receive error {err:?}"); + close_reason = Some(CloseCode::Protocol.into()); + break; } } } + // Send pings on a regular basis. + _ = ping_interval.tick() => { + if sent_ping { + // If we haven't received the answering pong, the + // websocket must be broken. + break; + } + // Send a ping to check that the websocket is still open. + // For example, putting a PC to sleep then waking it breaks + // the websocket, but the server doesn't detect this without + // sending a ping (which then fails). + if let Err(err) = session.ping(&Bytes::new()).await { + error!("Unable to send ping: {err}"); + break; + } + sent_ping = true; + } + // Forward a message from the processing task to the websocket. Some(m) = to_websocket_rx.recv() => { // Pre-process this message. @@ -1509,7 +1547,7 @@ pub fn client_websocket( } // Shut down the session, to stop any incoming messages. - if let Err(err) = session.close(None).await { + if let Err(err) = session.close(close_reason).await { error!("Unable to close session: {err}"); } diff --git a/server/tests/fixtures/overall/overall_5/test_alt_enter_non_editable_link/other.md b/server/tests/fixtures/overall/overall_5/test_alt_enter_non_editable_link/other.md new file mode 100644 index 00000000..3b1386f5 --- /dev/null +++ b/server/tests/fixtures/overall/overall_5/test_alt_enter_non_editable_link/other.md @@ -0,0 +1,3 @@ +The contents of this file don't matter -- tests will supply the content, +instead of loading it from disk. However, it does need to exist for +`canonicalize` to find the correct path to this file. diff --git a/server/tests/fixtures/overall/overall_5/test_alt_enter_non_editable_link/test.md b/server/tests/fixtures/overall/overall_5/test_alt_enter_non_editable_link/test.md new file mode 100644 index 00000000..3b1386f5 --- /dev/null +++ b/server/tests/fixtures/overall/overall_5/test_alt_enter_non_editable_link/test.md @@ -0,0 +1,3 @@ +The contents of this file don't matter -- tests will supply the content, +instead of loading it from disk. However, it does need to exist for +`canonicalize` to find the correct path to this file. diff --git a/server/tests/fixtures/overall/overall_5/test_ctrl_click_non_editable_link/other.md b/server/tests/fixtures/overall/overall_5/test_ctrl_click_non_editable_link/other.md new file mode 100644 index 00000000..3b1386f5 --- /dev/null +++ b/server/tests/fixtures/overall/overall_5/test_ctrl_click_non_editable_link/other.md @@ -0,0 +1,3 @@ +The contents of this file don't matter -- tests will supply the content, +instead of loading it from disk. However, it does need to exist for +`canonicalize` to find the correct path to this file. diff --git a/server/tests/fixtures/overall/overall_5/test_ctrl_click_non_editable_link/test.md b/server/tests/fixtures/overall/overall_5/test_ctrl_click_non_editable_link/test.md new file mode 100644 index 00000000..3b1386f5 --- /dev/null +++ b/server/tests/fixtures/overall/overall_5/test_ctrl_click_non_editable_link/test.md @@ -0,0 +1,3 @@ +The contents of this file don't matter -- tests will supply the content, +instead of loading it from disk. However, it does need to exist for +`canonicalize` to find the correct path to this file. diff --git a/server/tests/fixtures/overall/overall_5/test_open_non_editable_link/other.md b/server/tests/fixtures/overall/overall_5/test_open_non_editable_link/other.md new file mode 100644 index 00000000..3b1386f5 --- /dev/null +++ b/server/tests/fixtures/overall/overall_5/test_open_non_editable_link/other.md @@ -0,0 +1,3 @@ +The contents of this file don't matter -- tests will supply the content, +instead of loading it from disk. However, it does need to exist for +`canonicalize` to find the correct path to this file. diff --git a/server/tests/fixtures/overall/overall_5/test_open_non_editable_link/test.md b/server/tests/fixtures/overall/overall_5/test_open_non_editable_link/test.md new file mode 100644 index 00000000..3b1386f5 --- /dev/null +++ b/server/tests/fixtures/overall/overall_5/test_open_non_editable_link/test.md @@ -0,0 +1,3 @@ +The contents of this file don't matter -- tests will supply the content, +instead of loading it from disk. However, it does need to exist for +`canonicalize` to find the correct path to this file. diff --git a/server/tests/fixtures/overall/overall_5/test_open_non_editable_link_beside/test.md b/server/tests/fixtures/overall/overall_5/test_open_non_editable_link_beside/test.md new file mode 100644 index 00000000..3b1386f5 --- /dev/null +++ b/server/tests/fixtures/overall/overall_5/test_open_non_editable_link_beside/test.md @@ -0,0 +1,3 @@ +The contents of this file don't matter -- tests will supply the content, +instead of loading it from disk. However, it does need to exist for +`canonicalize` to find the correct path to this file. diff --git a/server/tests/fixtures/overall/overall_5/test_open_non_editable_link_keyboard/other.md b/server/tests/fixtures/overall/overall_5/test_open_non_editable_link_keyboard/other.md new file mode 100644 index 00000000..3b1386f5 --- /dev/null +++ b/server/tests/fixtures/overall/overall_5/test_open_non_editable_link_keyboard/other.md @@ -0,0 +1,3 @@ +The contents of this file don't matter -- tests will supply the content, +instead of loading it from disk. However, it does need to exist for +`canonicalize` to find the correct path to this file. diff --git a/server/tests/fixtures/overall/overall_5/test_open_non_editable_link_keyboard/test.md b/server/tests/fixtures/overall/overall_5/test_open_non_editable_link_keyboard/test.md new file mode 100644 index 00000000..3b1386f5 --- /dev/null +++ b/server/tests/fixtures/overall/overall_5/test_open_non_editable_link_keyboard/test.md @@ -0,0 +1,3 @@ +The contents of this file don't matter -- tests will supply the content, +instead of loading it from disk. However, it does need to exist for +`canonicalize` to find the correct path to this file. diff --git a/server/tests/overall/overall_4.rs b/server/tests/overall/overall_4.rs index 685c7fae..7f83a061 100644 --- a/server/tests/overall/overall_4.rs +++ b/server/tests/overall/overall_4.rs @@ -109,7 +109,7 @@ async fn test_xss_core( let orig_text = "Before after." .to_string(); - let server_id = perform_loadfile( + perform_loadfile( &codechat_server, &test_dir, "test.md", @@ -228,35 +228,6 @@ async fn test_xss_core( "Source written back to the IDE still contains the XSS payload: {inserted}" ); codechat_server.send_result(client_id, None).await.unwrap(); - client_id += MESSAGE_ID_INCREMENT; - - // Editing a doc block prompts the Server to send the Client a re-translated - // version of the document; the Client's acknowledgement comes back here as - // a `Result(Ok)` carrying the Server's ID. - assert_eq!( - codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), - EditorMessage { - id: server_id, - message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)) - } - ); - - // The re-translation settles the cursor, producing a final cursor-only - // update from the Client. - assert_eq!( - codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), - EditorMessage { - id: client_id, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: path_str.clone(), - cursor_position: Some(CursorPosition::Line(1)), - scroll_position: None, - is_re_translation: false, - contents: None, - }) - } - ); - codechat_server.send_result(client_id, None).await.unwrap(); //client_id += MESSAGE_ID_INCREMENT; assert_no_more_messages(&codechat_server).await; diff --git a/server/tests/overall/overall_5.rs b/server/tests/overall/overall_5.rs index e4b9a9cd..646ec37f 100644 --- a/server/tests/overall/overall_5.rs +++ b/server/tests/overall/overall_5.rs @@ -26,16 +26,24 @@ // ------- // // ### Standard library -use std::{fmt::Write, path::PathBuf, time::Duration}; +use std::{ + fmt::Write, + fs, + path::{Path, PathBuf}, + time::{Duration, Instant}, +}; // ### Third-party use dunce::canonicalize; +use indoc::indoc; use pretty_assertions::assert_eq; +use serde_json::json; use thirtyfour::{ - By, Key, WebDriver, + By, Key, WebDriver, WebElement, error::WebDriverError, prelude::{ElementQueryable, ElementWaitable}, }; +use tokio::time::sleep; // ### Local use crate::common::{ @@ -828,3 +836,1285 @@ async fn test_named_anchor_round_trip_core( Ok(()) } + +// ### Links +// +// #### In non-editable content +// +// A right click on a link in non-editable content shows a context menu whose +// only item, "Open link", follows that link, while a right click beside the +// link offers no "Open link". The keyboard opens the same menu on a selected +// non-editable element, following its first link. See `addLinkMenu` in +// [tinymce-config.mts](../../../client/src/tinymce-config.mts) and `Links in +// hydrated content` in [cache-spec.md](../../src/processing/cache-spec.md). +make_test!( + test_open_non_editable_link, + test_open_non_editable_link_core +); + +async fn test_open_non_editable_link_core( + // The Server, through which the test plays the IDE. + codechat_server: CodeChatEditorServerLog, + // The browser showing the Client. + driver: WebDriver, + // The directory holding `test.md` and the `other.md` it links to. + test_dir: PathBuf, + // WebDriver's errors; failed checks panic instead. +) -> Result<(), WebDriverError> { + open_markdown_non_editable_link(codechat_server, driver, test_dir, MenuTrigger::Pointer).await +} + +make_test!( + test_open_non_editable_link_keyboard, + test_open_non_editable_link_keyboard_core +); + +async fn test_open_non_editable_link_keyboard_core( + // The Server, through which the test plays the IDE. + codechat_server: CodeChatEditorServerLog, + // The browser showing the Client. + driver: WebDriver, + // The directory holding `test.md` and the `other.md` it links to. + test_dir: PathBuf, + // WebDriver's errors; failed checks panic instead. +) -> Result<(), WebDriverError> { + open_markdown_non_editable_link(codechat_server, driver, test_dir, MenuTrigger::Keyboard).await +} + +make_test!( + test_open_non_editable_link_beside, + test_open_non_editable_link_beside_core +); + +async fn test_open_non_editable_link_beside_core( + // The Server, through which the test plays the IDE. + codechat_server: CodeChatEditorServerLog, + // The browser showing the Client. + driver: WebDriver, + // The directory holding `test.md`. + test_dir: PathBuf, + // WebDriver's errors; failed checks panic instead. +) -> Result<(), WebDriverError> { + load_markdown_non_editable_link(&codechat_server, &driver, &test_dir).await; + + // Right-click the text preceding the link. Pressing a non-editable element + // selects it, leaving TinyMCE in the state in which the keyboard opens the + // menu, so this checks that the Client doesn't mistake this right click for + // the keyboard. + let non_editable = query_markdown_non_editable(&driver).await?; + let rect = non_editable.rect().await?; + driver + .action_chain() + .move_to_element_with_offset( + &non_editable, + (-rect.width / 2.0 + 8.0) as i64, + (-rect.height / 2.0 + 8.0) as i64, + ) + .context_click() + .perform() + .await?; + // Give a menu time to appear, were the Client to show one. + sleep(SETTLE_TIME).await; + for menu_item in driver + .query(By::Css(".tox-menu [role='menuitem']")) + .nowait() + .all_from_selector() + .await? + { + assert_ne!(menu_item.text().await?, "Open link"); + } + + Ok(()) +} + +// In a source file, unlike a Markdown document, a doc block becomes a TinyMCE +// editor only once it's focused (see `DocBlockPlugin` in +// [CodeMirror-integration.mts](../../../client/src/CodeMirror-integration.mts)), +// and the cache, not the test, supplies the non-editable content. This test +// right-clicks a link in a doc block which isn't yet an editor. +make_test!( + test_open_non_editable_link_py_unfocused, + test_open_non_editable_link_py_unfocused_core, + ignore = "a right click makes a doc block a TinyMCE editor only after the context menu event, so TinyMCE never shows its menu" +); + +async fn test_open_non_editable_link_py_unfocused_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + let link_css = "fragment#test-fragment a"; + let mut client_id = load_link_project(&codechat_server, &driver, &test_dir, link_css).await?; + open_link_from_context_menu( + &driver, + link_css, + MenuTrigger::Pointer, + NON_EDITABLE_LINK_MENU, + ) + .await?; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.py")).await; + + Ok(()) +} + +// Each of the remaining tests clicks into the doc block holding a link, then +// follows that link: a link in math, a cross-reference, a fragment's backlinks, +// or a gather list, leading either to `other.py` or elsewhere in `test.py`. +make_test!( + test_open_non_editable_link_py_math, + test_open_non_editable_link_py_math_core, + ignore = + "MathJax's own context menu handles a right click on math, so TinyMCE never shows its menu" +); + +async fn test_open_non_editable_link_py_math_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + follow_link_project_link( + codechat_server, + driver, + test_dir, + "mjx-container a", + LinkDestination::OtherFile, + ) + .await +} + +make_test!( + test_open_non_editable_link_py_xref, + test_open_non_editable_link_py_xref_core +); + +async fn test_open_non_editable_link_py_xref_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + follow_link_project_link( + codechat_server, + driver, + test_dir, + "xref[ref='other-target'] a", + LinkDestination::OtherFile, + ) + .await +} + +make_test!( + test_open_non_editable_link_py_xref_same_file, + test_open_non_editable_link_py_xref_same_file_core, + ignore = "the Client doesn't follow a link to an id elsewhere in the current file" +); + +async fn test_open_non_editable_link_py_xref_same_file_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + follow_link_project_link( + codechat_server, + driver, + test_dir, + "xref[ref='test-target'] a", + LinkDestination::ThisFile("test-target"), + ) + .await +} + +make_test!( + test_open_non_editable_link_py_fragment, + test_open_non_editable_link_py_fragment_core +); + +async fn test_open_non_editable_link_py_fragment_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + follow_link_project_link( + codechat_server, + driver, + test_dir, + "fragment#test-fragment a", + LinkDestination::OtherFile, + ) + .await +} + +make_test!( + test_open_non_editable_link_py_fragment_same_file, + test_open_non_editable_link_py_fragment_same_file_core, + ignore = "the Client doesn't follow a link to an id elsewhere in the current file" +); + +async fn test_open_non_editable_link_py_fragment_same_file_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + follow_link_project_link( + codechat_server, + driver, + test_dir, + "fragment#test-near-fragment a", + LinkDestination::ThisFile("test-far-gather"), + ) + .await +} + +make_test!( + test_open_non_editable_link_py_gather, + test_open_non_editable_link_py_gather_core +); + +async fn test_open_non_editable_link_py_gather_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + follow_link_project_link( + codechat_server, + driver, + test_dir, + ".cc-gather-items a[href$='#other-fragment']", + LinkDestination::OtherFile, + ) + .await +} + +make_test!( + test_open_non_editable_link_py_gather_same_file, + test_open_non_editable_link_py_gather_same_file_core, + ignore = "the Client doesn't follow a link to an id elsewhere in the current file" +); + +async fn test_open_non_editable_link_py_gather_same_file_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + follow_link_project_link( + codechat_server, + driver, + test_dir, + ".cc-gather-items a[href$='#test-far-fragment']", + LinkDestination::ThisFile("test-far-fragment"), + ) + .await +} + +// #### In a doc block not holding TinyMCE +// +// Links in a doc block which isn't yet a TinyMCE editor act as they do in the +// one which is: a plain click moves TinyMCE into the doc block without +// following the link, while a Ctrl+click (Cmd+click on macOS) follows it. See +// `Link navigation` in [implementation.md](../../../docs/implementation.md). +make_test!( + test_click_link_py_unfocused, + test_click_link_py_unfocused_core +); + +async fn test_click_link_py_unfocused_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + let mut client_id = + click_unfocused_link(&codechat_server, &driver, &test_dir, PLAIN_LINK_CSS, &[]).await?; + let link = + assert_not_followed(&codechat_server, &driver, &mut client_id, PLAIN_LINK_CSS).await?; + // The cursor lands where the click did, in the link. + assert_cursor_in(&driver, &link).await +} + +make_test!( + test_ctrl_click_link_py_unfocused, + test_ctrl_click_link_py_unfocused_core +); + +async fn test_ctrl_click_link_py_unfocused_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + let mut client_id = click_unfocused_link( + &codechat_server, + &driver, + &test_dir, + PLAIN_LINK_CSS, + &[FOLLOW_KEY], + ) + .await?; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.py")).await; + Ok(()) +} + +// As in TinyMCE, a click holding both Ctrl and Alt, which AltGr reports on +// Windows, doesn't follow the link. +make_test!( + test_ctrl_alt_click_link_py_unfocused, + test_ctrl_alt_click_link_py_unfocused_core +); + +async fn test_ctrl_alt_click_link_py_unfocused_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + let mut client_id = click_unfocused_link( + &codechat_server, + &driver, + &test_dir, + PLAIN_LINK_CSS, + &[Key::Control, Key::Alt], + ) + .await?; + assert_not_followed(&codechat_server, &driver, &mut client_id, PLAIN_LINK_CSS).await?; + Ok(()) +} + +// Focusing a link from the keyboard, as the Tab key does, likewise moves +// TinyMCE into the doc block without following the link, placing the cursor at +// the link's start. +make_test!( + test_focus_link_py_unfocused, + test_focus_link_py_unfocused_core +); + +async fn test_focus_link_py_unfocused_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + let mut client_id = + load_link_project(&codechat_server, &driver, &test_dir, PLAIN_LINK_CSS).await?; + let link = query_unfocused_link(&driver, PLAIN_LINK_CSS).await?; + driver + .execute("arguments[0].focus();", vec![link.to_json()?]) + .await?; + let link = + assert_not_followed(&codechat_server, &driver, &mut client_id, PLAIN_LINK_CSS).await?; + assert_cursor_in(&driver, &link).await +} + +// The same, for a link in non-editable content. TinyMCE keeps the cursor out of +// that content, placing it beside the content instead, so these tests don't +// check where the cursor lands. +make_test!( + test_click_non_editable_link_py_unfocused, + test_click_non_editable_link_py_unfocused_core +); + +async fn test_click_non_editable_link_py_unfocused_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + let link_css = "fragment#test-fragment a"; + let mut client_id = + click_unfocused_link(&codechat_server, &driver, &test_dir, link_css, &[]).await?; + assert_not_followed(&codechat_server, &driver, &mut client_id, link_css).await?; + Ok(()) +} + +make_test!( + test_ctrl_click_non_editable_link_py_unfocused, + test_ctrl_click_non_editable_link_py_unfocused_core +); + +async fn test_ctrl_click_non_editable_link_py_unfocused_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + let link_css = "fragment#test-fragment a"; + let mut client_id = click_unfocused_link( + &codechat_server, + &driver, + &test_dir, + link_css, + &[FOLLOW_KEY], + ) + .await?; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.py")).await; + Ok(()) +} + +// #### In the doc block holding TinyMCE +// +// There, the Client takes Ctrl+click (Cmd+click on macOS) and Alt+Enter from +// TinyMCE's link plugin, following links as it does everywhere else. Alt+Enter +// follows the link holding the cursor, or the first link in a selected +// non-editable element. See `takeLinkGestures` in +// [tinymce-config.mts](../../../client/src/tinymce-config.mts) and `Link +// navigation` in [implementation.md](../../../docs/implementation.md). +make_test!(test_ctrl_click_link_py, test_ctrl_click_link_py_core); + +async fn test_ctrl_click_link_py_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + let mut client_id = + load_link_project(&codechat_server, &driver, &test_dir, PLAIN_LINK_CSS).await?; + let link_css = click_into_link_doc_block(&driver, PLAIN_LINK_CSS).await?; + let link = driver + .query(By::Css(link_css)) + .desc("the link to follow") + .first() + .await?; + click_holding(&driver, &link, &[FOLLOW_KEY]).await?; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.py")).await; + Ok(()) +} + +// A plain click on a link which opens in a new tab places the cursor, as on any +// other link. The Server's sanitizer strips a link's `target` (see +// `AMMONIA_OPTIONS` in [processing.rs](../../src/processing.rs)), so this test +// gives the link the target which TinyMCE's link dialog sets for a new window. +make_test!(test_click_new_tab_link_py, test_click_new_tab_link_py_core); + +async fn test_click_new_tab_link_py_core( + // The Server, through which the test plays the IDE. + codechat_server: CodeChatEditorServerLog, + // The browser showing the Client. + driver: WebDriver, + // The directory in which to create the project. + test_dir: PathBuf, + // WebDriver's errors; failed checks panic instead. +) -> Result<(), WebDriverError> { + let mut client_id = + load_link_project(&codechat_server, &driver, &test_dir, PLAIN_LINK_CSS).await?; + let link_css = click_into_link_doc_block(&driver, PLAIN_LINK_CSS).await?; + let link = driver + .query(By::Css(link_css)) + .desc("the link to click") + .first() + .await?; + driver + .execute("arguments[0].target = '_blank';", vec![link.to_json()?]) + .await?; + click_holding(&driver, &link, &[]).await?; + let link = + assert_not_followed(&codechat_server, &driver, &mut client_id, PLAIN_LINK_CSS).await?; + assert_cursor_in(&driver, &link).await +} + +make_test!( + test_ctrl_click_non_editable_link, + test_ctrl_click_non_editable_link_core +); + +async fn test_ctrl_click_non_editable_link_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + load_markdown_non_editable_link(&codechat_server, &driver, &test_dir).await; + let link = driver + .query(By::Css(format!("{DOC_BLOCK_CSS} a"))) + .desc("the link to follow") + .first() + .await?; + click_holding(&driver, &link, &[FOLLOW_KEY]).await?; + let mut client_id = INITIAL_CLIENT_MESSAGE_ID; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.md")).await; + Ok(()) +} + +make_test!(test_alt_enter_link_py, test_alt_enter_link_py_core); + +async fn test_alt_enter_link_py_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + // A plain click on the link places the cursor in it. + let mut client_id = + click_unfocused_link(&codechat_server, &driver, &test_dir, PLAIN_LINK_CSS, &[]).await?; + let link = + assert_not_followed(&codechat_server, &driver, &mut client_id, PLAIN_LINK_CSS).await?; + assert_cursor_in(&driver, &link).await?; + press_alt_enter(&driver).await?; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.py")).await; + Ok(()) +} + +make_test!( + test_alt_enter_non_editable_link, + test_alt_enter_non_editable_link_core +); + +async fn test_alt_enter_non_editable_link_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + load_markdown_non_editable_link(&codechat_server, &driver, &test_dir).await; + // Click the text preceding the link; the press selects the non-editable + // element enclosing it. + let non_editable = query_markdown_non_editable(&driver).await?; + click_element_top_left(&driver, &non_editable).await?; + press_alt_enter(&driver).await?; + let mut client_id = INITIAL_CLIENT_MESSAGE_ID; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.md")).await; + Ok(()) +} + +// The Client's context menu replaces the link plugin's. For an editable link, +// it keeps the plugin's items for editing the link, while its "Open link" +// follows the link as a Ctrl+click does. See `addLinkMenu` in +// [tinymce-config.mts](../../../client/src/tinymce-config.mts). +make_test!(test_open_link_py, test_open_link_py_core); + +async fn test_open_link_py_core( + // The Server, through which the test plays the IDE. + codechat_server: CodeChatEditorServerLog, + // The browser showing the Client. + driver: WebDriver, + // The directory in which to create the project. + test_dir: PathBuf, + // WebDriver's errors; failed checks panic instead. +) -> Result<(), WebDriverError> { + let mut client_id = + load_link_project(&codechat_server, &driver, &test_dir, PLAIN_LINK_CSS).await?; + let link_css = click_into_link_doc_block(&driver, PLAIN_LINK_CSS).await?; + open_link_from_context_menu(&driver, &link_css, MenuTrigger::Pointer, EDITABLE_LINK_MENU) + .await?; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.py")).await; + Ok(()) +} + +// `followLink` looks for a link's id throughout the Client, not just in the doc +// block holding TinyMCE, so "Open link" follows a link to an id in another doc +// block. CodeMirror renders this one, since it's near the link; see the TODO in +// `followLink` for a target which CodeMirror hasn't rendered. +make_test!( + test_open_link_py_near_target, + test_open_link_py_near_target_core +); + +async fn test_open_link_py_near_target_core( + // The Server, through which the test plays the IDE. + codechat_server: CodeChatEditorServerLog, + // The browser showing the Client. + driver: WebDriver, + // The directory in which to create the project. + test_dir: PathBuf, + // WebDriver's errors; failed checks panic instead. +) -> Result<(), WebDriverError> { + let link_css = "a[href='#test-near-target']"; + let mut client_id = load_link_project(&codechat_server, &driver, &test_dir, link_css).await?; + let link_css = click_into_link_doc_block(&driver, link_css).await?; + // Measure only once TinyMCE has moved into the doc block, so that any + // scrolling the move causes can't pass for following the link. + driver + .query(By::Css(&link_css)) + .desc("the link in the doc block holding TinyMCE") + .first() + .await?; + let unscrolled_top = scroll_top(&driver).await?; + open_link_from_context_menu(&driver, &link_css, MenuTrigger::Pointer, EDITABLE_LINK_MENU) + .await?; + // Following a link within the current file mustn't ask the IDE to open a + // file. Waiting for that also gives any scrolling time to finish. + assert_eq!( + next_unhandled_message(&codechat_server, &mut client_id, SETTLE_TIME).await, + None + ); + // The target may have been in view to begin with, but `scrollIntoView` + // then scrolls it to the top, since filler follows it. + assert!( + scroll_top(&driver).await? > unscrolled_top, + "Following the link didn't scroll." + ); + assert!( + is_in_view(&driver, "test-near-target").await, + "Following the link didn't scroll #test-near-target into view." + ); + Ok(()) +} + +// #### Support for these tests +// +// How a test opens TinyMCE's context menu for a link. +enum MenuTrigger { + // A right click on the link. + Pointer, + // A click beside the link, which selects the non-editable element + // enclosing it, then the keyboard's context menu shortcut. + Keyboard, +} + +// The non-editable content which the Markdown tests above place in `test.md`, +// as a stand-in for the content the cache hydrates. Text precedes the link, so +// that a click at the content's top left lands beside the link. +const MARKDOWN_NON_EDITABLE_LINK: &str = + "
See Other
\n"; + +// Where following a link should lead. +enum LinkDestination { + // The Client asks the IDE to open `other.py`. + OtherFile, + // The Client scrolls the element with this id, in the current file, into + // view. + ThisFile(&'static str), +} + +// How long the Client must stay silent before the tests above consider the +// Server's re-translations of the current file finished. +const SETTLE_TIME: Duration = Duration::from_secs(1); + +// How often the tests above re-check the Client while waiting for it. +const POLL_INTERVAL: Duration = Duration::from_millis(100); + +// The other file in the project `write_link_project` creates, holding a target +// of each kind which `test.py` links to. +const LINK_PROJECT_OTHER_PY: &str = indoc!( + r#" + #

Other target

+ a = 1 + #

Other gather

+ b = 2 + # A fragment gathered by `test.py`. + c = 3 + "# +); + +// CSS selecting the plain link in the `test.py` which `write_link_project` +// creates. +const PLAIN_LINK_CSS: &str = "a[href='other.py']"; + +// The labels of the items on TinyMCE's context menu for a link in non-editable +// content: only the Client's "Open link". +const NON_EDITABLE_LINK_MENU: &[&str] = &["Open link"]; + +// The same, for an editable link: the link plugin's items for editing the link, +// then the Client's "Open link". +const EDITABLE_LINK_MENU: &[&str] = &["Link…", "Remove link", "Open link"]; + +// The key which, held during a click on a link, follows that link. +const FOLLOW_KEY: Key = if cfg!(target_os = "macos") { + Key::Command +} else { + Key::Control +}; + +// Create a project in `test_dir` whose `test.py` contains a plain link, a link +// to an id, and a link of each kind which the cache hydrates. Every link +// sits near the start of `test.py`, within the first screen. So does the target +// `test-near-target`, close enough that CodeMirror always renders it; every +// other target in `test.py` itself follows 100 lines of filler, so that +// following a link to one of them must scroll. +fn write_link_project( + // The directory to create the project in. + test_dir: &Path, +) { + let mut test_py = indoc!( + r#" + # Links to `other.py`: the cross-reference + # and the math $\href{other.py}{\text{Other}}$. + a = 1 + # A fragment gathered by `other.py`. + b = 2 + # A fragment gathered at the + # end of this file. + c = 3 + #

Gathered

+ d = 4 + # A link to the end of this file: the cross-reference + # . + e = 5 + # A plain link to [the file other.py](other.py). + f = 6 + # A link to [the target below](#test-near-target). + g = 7 + #

Near target

+ h = 8 + "# + ) + .to_string(); + for i in 0..100 { + let _ = writeln!(test_py, "filler_{i} = {i}"); + } + test_py.push_str(indoc!( + r#" + #

Test target

+ i = 9 + # A fragment gathered at the + # start of this file. + j = 10 + #

Far gather

+ k = 11 + "# + )); + // A `toc.md` makes this directory a project, so that the cache resolves ids + // across its files. + fs::write(test_dir.join("toc.md"), "Contents\n").unwrap(); + fs::write(test_dir.join("other.py"), LINK_PROJECT_OTHER_PY).unwrap(); + fs::write(test_dir.join("test.py"), test_py).unwrap(); +} + +// Show `test.md`, holding `MARKDOWN_NON_EDITABLE_LINK`, in the Client. +async fn load_markdown_non_editable_link( + // The Server, through which the test plays the IDE. + codechat_server: &CodeChatEditorServerLog, + // The browser showing the Client; this selects the Client's iframe. + driver: &WebDriver, + // The directory holding `test.md`. + test_dir: &Path, +) { + perform_loadfile( + codechat_server, + test_dir, + "test.md", + Some((MARKDOWN_NON_EDITABLE_LINK.to_string(), 0.0)), + false, + 6.0, + ) + .await; + + // Target the iframe containing the Client. + select_codechat_iframe(driver).await; +} + +// Find the non-editable element which `load_markdown_non_editable_link` shows. +async fn query_markdown_non_editable( + // The browser, with the Client's iframe selected. + driver: &WebDriver, + // The non-editable element. +) -> Result { + driver + .query(By::Css(format!( + "{DOC_BLOCK_CSS} [contenteditable='false']" + ))) + .desc("the non-editable element") + .first() + .await +} + +// In a Markdown document, open the context menu for the link in +// `MARKDOWN_NON_EDITABLE_LINK`, then follow the link. +async fn open_markdown_non_editable_link( + // The Server, through which the test plays the IDE. + codechat_server: CodeChatEditorServerLog, + // The browser showing the Client. + driver: WebDriver, + // The directory holding `test.md` and the `other.md` it links to. + test_dir: PathBuf, + // How to open the context menu. + trigger: MenuTrigger, + // WebDriver's errors; failed checks panic instead. +) -> Result<(), WebDriverError> { + load_markdown_non_editable_link(&codechat_server, &driver, &test_dir).await; + open_link_from_context_menu( + &driver, + &format!("{DOC_BLOCK_CSS} a"), + trigger, + NON_EDITABLE_LINK_MENU, + ) + .await?; + let mut client_id = INITIAL_CLIENT_MESSAGE_ID; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.md")).await; + + Ok(()) +} + +// Create the project `write_link_project` describes, then show its `test.py` +// in the Client. +async fn load_link_project( + // The Server, through which the test plays the IDE. + codechat_server: &CodeChatEditorServerLog, + // The browser showing the Client; this selects the Client's iframe. + driver: &WebDriver, + // The directory in which to create the project. + test_dir: &Path, + // CSS selecting the link the test will follow. + link_css: &str, + // The id of the next message the Client sends. +) -> Result { + write_link_project(test_dir); + let test_py = canonicalize(test_dir.join("test.py")).unwrap(); + codechat_server + .send_message_current_file(test_py.to_str().unwrap().to_string()) + .await + .unwrap(); + + // The Server translates `test.py` at once, while a walk of the project + // brings `other.py` into the cache in the background; the content hydrated + // from `other.py` then arrives in re-translations of `test.py` (see `Cache + // population and maintenance` in + // [cache-spec.md](../../src/processing/cache-spec.md)). These interleave + // unpredictably with the Server's requests to load `test.py` and the TOC, + // which `perform_loadfile` expects in a fixed order, so answer requests + // as they come instead. + // + // The Client's page can't finish loading until the requests for `test.py` + // and the TOC are answered, and WebDriver waits for it to finish before + // entering its iframe; answer those first. + let mut unrequested = vec![test_py, canonicalize(test_dir.join("toc.md")).unwrap()]; + while !unrequested.is_empty() { + let msg = codechat_server + .get_message_timeout(TIMEOUT) + .await + .unwrap_or_else(|| panic!("The Server never requested {unrequested:?}.")); + match &msg.message { + EditorMessageContents::LoadFile(path, _) => { + codechat_server + .send_result_loadfile(msg.id, None) + .await + .unwrap(); + unrequested.retain(|unrequested_path| unrequested_path != path); + } + EditorMessageContents::Result(Ok(ResultOkTypes::Void)) => {} + _ => panic!("Unexpected message: {msg:#?}"), + } + } + + // Target the iframe containing the Client. + select_codechat_iframe(driver).await; + + // Wait for the link, then for the re-translations to end, so that none + // replaces the link while it's in use. + let mut client_id = INITIAL_CLIENT_MESSAGE_ID; + let deadline = Instant::now() + TIMEOUT; + while !driver.query(By::Css(link_css)).nowait().exists().await? { + assert!( + Instant::now() < deadline, + "The link {link_css} never appeared." + ); + assert_eq!( + next_unhandled_message(codechat_server, &mut client_id, POLL_INTERVAL).await, + None + ); + } + assert_eq!( + next_unhandled_message(codechat_server, &mut client_id, SETTLE_TIME).await, + None + ); + Ok(client_id) +} + +// Open `test.py` from the project `write_link_project` creates, click into the +// doc block holding the link `link_css` selects, follow that link, then check +// that it leads to `destination`. +async fn follow_link_project_link( + // The Server, through which the test plays the IDE. + codechat_server: CodeChatEditorServerLog, + // The browser showing the Client. + driver: WebDriver, + // The directory in which to create the project. + test_dir: PathBuf, + // CSS selecting the link to follow. + link_css: &str, + // Where the link should lead. + destination: LinkDestination, +) -> Result<(), WebDriverError> { + let mut client_id = load_link_project(&codechat_server, &driver, &test_dir, link_css).await?; + let link_css = &click_into_link_doc_block(&driver, link_css).await?; + + match destination { + LinkDestination::OtherFile => { + open_link_from_context_menu( + &driver, + link_css, + MenuTrigger::Pointer, + NON_EDITABLE_LINK_MENU, + ) + .await?; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.py")) + .await; + } + LinkDestination::ThisFile(id) => { + assert!( + !is_in_view(&driver, id).await, + "#{id} is in view before following the link to it." + ); + open_link_from_context_menu( + &driver, + link_css, + MenuTrigger::Pointer, + NON_EDITABLE_LINK_MENU, + ) + .await?; + let deadline = Instant::now() + TIMEOUT; + while !is_in_view(&driver, id).await { + assert!( + Instant::now() < deadline, + "Following the link never scrolled #{id} into view." + ); + sleep(POLL_INTERVAL).await; + } + // Following a link within the current file mustn't ask the IDE to + // open a file. + assert_eq!( + next_unhandled_message(&codechat_server, &mut client_id, SETTLE_TIME).await, + None + ); + } + } + + Ok(()) +} + +// Open TinyMCE's context menu for the link `link_css` selects, check that the +// menu shows `menu_labels`, then choose "Open link". +async fn open_link_from_context_menu( + // The browser, with the Client's iframe selected. + driver: &WebDriver, + // CSS selecting the link to follow. + link_css: &str, + // How to open the context menu. + trigger: MenuTrigger, + // The labels of the menu's items, in order, which must include "Open + // link". + menu_labels: &[&str], + // WebDriver's errors; failed checks panic instead. +) -> Result<(), WebDriverError> { + let link = driver + .query(By::Css(link_css)) + .desc("the link to follow") + .first() + .await?; + match trigger { + MenuTrigger::Pointer => { + driver + .action_chain() + .context_click_element(&link) + .perform() + .await?; + } + MenuTrigger::Keyboard => { + // Click the text preceding the link in the non-editable element + // enclosing it; the press selects that element. + let non_editable = link + .find(By::XPath("ancestor::*[@contenteditable='false'][1]")) + .await?; + click_element_top_left(driver, &non_editable).await?; + // macOS has no keyboard shortcut which opens a context menu, so + // there, send the `contextmenu` event which such a shortcut + // produces elsewhere. TinyMCE treats this event as the keyboard's + // when its `button` isn't 2 (the right button), opening the menu + // on the selection. + if cfg!(target_os = "macos") { + driver + .execute( + r#"tinymce.get(0).getBody().dispatchEvent( + new MouseEvent("contextmenu", { bubbles: true, cancelable: true }), + );"#, + vec![], + ) + .await?; + } else { + // Press Shift+F10. Chrome doesn't open a context menu for + // WebDriver's key actions, but does for a `rawKeyDown` sent + // through the DevTools protocol's + // [Input.dispatchKeyEvent](https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent). + let cdp = driver.cdp(); + for event_type in ["rawKeyDown", "keyUp"] { + cdp.send_raw( + "Input.dispatchKeyEvent", + json!({ + "type": event_type, + // Shift. + "modifiers": 8, + // F10. + "windowsVirtualKeyCode": 121, + "key": "F10", + "code": "F10", + }), + ) + .await?; + } + } + } + } + let menu_items = driver + .query(By::Css(".tox-menu [role='menuitem']")) + .desc("the items of TinyMCE's context menu") + .all_from_selector_required() + .await?; + // An item's text includes its keyboard shortcut, if any; its label + // doesn't. + let mut labels = Vec::new(); + for menu_item in &menu_items { + labels.push( + menu_item + .find(By::Css(".tox-collection__item-label")) + .await? + .text() + .await?, + ); + } + assert_eq!(labels, menu_labels); + let open_link = labels + .iter() + .position(|label| label == "Open link") + .unwrap(); + menu_items[open_link].click().await?; + Ok(()) +} + +// Find the link `link_css` selects in a doc block which doesn't hold TinyMCE. +// Once `load_link_project` returns, nothing has focused a doc block yet, so +// none holds TinyMCE. +async fn query_unfocused_link( + // The browser, with the Client's iframe selected. + driver: &WebDriver, + // CSS selecting the link. + link_css: &str, + // The link. +) -> Result { + driver + .query(By::Css(format!( + ".CodeChat-CodeMirror .CodeChat-doc-contents:not(#TinyMCE-inst) {link_css}" + ))) + .desc("the link, in a doc block not holding TinyMCE") + .first() + .await +} + +// Open `test.py` from the project `write_link_project` creates, then click the +// link `link_css` selects, in a doc block which doesn't hold TinyMCE. +async fn click_unfocused_link( + // The Server, through which the test plays the IDE. + codechat_server: &CodeChatEditorServerLog, + // The browser showing the Client. + driver: &WebDriver, + // The directory in which to create the project. + test_dir: &Path, + // CSS selecting the link to click. + link_css: &str, + // The keys to hold during the click. + modifiers: &[Key], + // The id of the next message the Client sends. +) -> Result { + let client_id = load_link_project(codechat_server, driver, test_dir, link_css).await?; + let link = query_unfocused_link(driver, link_css).await?; + click_holding(driver, &link, modifiers).await?; + Ok(client_id) +} + +// Click into the doc block holding the link `link_css` selects, moving TinyMCE +// there, without following the link. +async fn click_into_link_doc_block( + // The browser, with the Client's iframe selected. + driver: &WebDriver, + // CSS selecting the link. + link_css: &str, + // CSS selecting the link in the doc block, which now holds TinyMCE. +) -> Result { + // Clicking a doc block's top left corner lands on text which precedes + // every link in these doc blocks, so the click focuses the doc block + // without following anything. Focus moves the TinyMCE editor into the doc + // block, replacing its contents. + let doc_block = driver + .query(By::Css(format!( + ".CodeChat-CodeMirror .CodeChat-doc-contents:has({link_css})" + ))) + .desc("the doc block holding the link to follow") + .first() + .await?; + click_element_top_left(driver, &doc_block).await?; + Ok(format!( + "#TinyMCE-inst:not(.CodeChat-doc-hidden) {link_css}" + )) +} + +// Click `element` while holding `modifiers`. +async fn click_holding( + // The browser, with the Client's iframe selected. + driver: &WebDriver, + // The element to click. + element: &WebElement, + // The keys to hold during the click. + modifiers: &[Key], +) -> Result<(), WebDriverError> { + let mut actions = driver.action_chain(); + for modifier in modifiers { + actions = actions.key_down(modifier.clone()); + } + actions = actions.click_element(element); + for modifier in modifiers.iter().rev() { + actions = actions.key_up(modifier.clone()); + } + actions.perform().await +} + +// Press Alt+Enter in the element which has the focus. +async fn press_alt_enter( + // The browser, with the Client's iframe selected. + driver: &WebDriver, +) -> Result<(), WebDriverError> { + driver + .action_chain() + .key_down(Key::Alt) + .key_down(Key::Enter) + .key_up(Key::Enter) + .key_up(Key::Alt) + .perform() + .await +} + +// Check that clicking or focusing the link `link_css` selects moved TinyMCE +// into the doc block holding that link without following the link. +async fn assert_not_followed( + // The Server, through which the test plays the IDE. + codechat_server: &CodeChatEditorServerLog, + // The browser, with the Client's iframe selected. + driver: &WebDriver, + // The id of the next message the Client sends; updated as messages + // arrive. + client_id: &mut f64, + // CSS selecting the link. + link_css: &str, + // The link, now in the doc block holding TinyMCE. +) -> Result { + assert_eq!( + next_unhandled_message(codechat_server, client_id, SETTLE_TIME).await, + None + ); + let link = driver + .query(By::Css(format!( + "#TinyMCE-inst:not(.CodeChat-doc-hidden) {link_css}" + ))) + .desc("the link, in the doc block holding TinyMCE") + .first() + .await?; + let has_focus: bool = driver + .execute( + "return document.activeElement.id === 'TinyMCE-inst';", + Vec::new(), + ) + .await? + .convert()?; + assert!(has_focus, "TinyMCE doesn't have the focus."); + Ok(link) +} + +// Check that the cursor is in `link`. +async fn assert_cursor_in( + // The browser, with the Client's iframe selected. + driver: &WebDriver, + // The link which must hold the cursor. + link: &WebElement, +) -> Result<(), WebDriverError> { + let has_cursor: bool = driver + .execute( + "return arguments[0].contains(document.getSelection().anchorNode);", + vec![link.to_json()?], + ) + .await? + .convert()?; + assert!(has_cursor, "The cursor isn't in the link."); + Ok(()) +} + +// Check that the Client's next request asks the IDE to open `file`. +async fn assert_requests_file( + // The Server, through which the test plays the IDE. + codechat_server: &CodeChatEditorServerLog, + // The id of the next message the Client sends; updated as messages + // arrive. + client_id: &mut f64, + // The file the Client should request. + file: &Path, +) { + let path = canonicalize(file).unwrap(); + let request = next_unhandled_message(codechat_server, client_id, TIMEOUT).await; + assert_eq!( + request, + Some(EditorMessage { + id: *client_id, + message: EditorMessageContents::CurrentFile( + path.to_str().unwrap().to_string(), + Some(true) + ), + }) + ); +} + +// Return the next message the IDE receives, handling those which need no +// attention from the tests above: +// +// * A successful result, such as the Client's acknowledgement of a +// re-translation from the Server. +// * A request to load a file, answered as an IDE without that file open would, +// so that the Server reads it from disk. +// * The Client's report of a new cursor or scroll position, which this +// acknowledges. +async fn next_unhandled_message( + // The Server, through which the test plays the IDE. + codechat_server: &CodeChatEditorServerLog, + // The id of the next message the Client sends; updated as messages + // arrive. + client_id: &mut f64, + // How long to wait for each message. + timeout: Duration, + // The message, or `None` if none arrives within `timeout`. +) -> Option { + while let Some(msg) = codechat_server.get_message_timeout(timeout).await { + match &msg.message { + EditorMessageContents::Result(Ok(ResultOkTypes::Void)) => {} + EditorMessageContents::LoadFile(..) => { + codechat_server + .send_result_loadfile(msg.id, None) + .await + .unwrap(); + } + EditorMessageContents::Update(UpdateMessageContents { contents: None, .. }) => { + assert_eq!(msg.id, *client_id); + codechat_server.send_result(msg.id, None).await.unwrap(); + *client_id += MESSAGE_ID_INCREMENT; + } + _ => return Some(msg), + } + } + None +} + +// Report whether any of the element with the given id is visible in the +// CodeMirror editor. CodeMirror renders only what's near its viewport, so an +// element far from it isn't in the DOM. +async fn is_in_view( + // The browser, with the Client's iframe selected. + driver: &WebDriver, + // The element's id. + id: &str, + // True if any of the element is visible. +) -> bool { + driver + .execute( + &format!( + "const element = document.getElementById('{id}'); + if (element === null) return false; + const rect = element.getBoundingClientRect(); + const scroller = document + .querySelector('.CodeChat-CodeMirror .cm-scroller') + .getBoundingClientRect(); + return rect.bottom > scroller.top && rect.top < scroller.bottom;" + ), + Vec::new(), + ) + .await + .unwrap() + .convert() + .unwrap() +} + +// Return how far, in pixels, the CodeMirror editor has scrolled from its top. +async fn scroll_top( + // The browser, with the Client's iframe selected. + driver: &WebDriver, + // The scroll offset. +) -> Result { + driver + .execute( + "return document.querySelector('.CodeChat-CodeMirror .cm-scroller').scrollTop;", + Vec::new(), + ) + .await? + .convert() +} diff --git a/toc.md b/toc.md index 364ff3d2..cc1343c0 100644 --- a/toc.md +++ b/toc.md @@ -105,8 +105,8 @@ Implementation * Builder * [builder/Cargo.toml](builder/Cargo.toml) * [builder/src/main.rs](builder/src/main.rs) - * [server/bt](server/bt) - shortcut to run the build tool - * [server/bt.ps1](server/bt.ps1) - PowerShell shortcut to run the build tool + * [bt](bt) - shortcut to run the build tool + * [bt.ps1](bt.ps1) - PowerShell shortcut to run the build tool * Continuous integration * [check.yml](.github/workflows/check.yml) * [release.yml](.github/workflows/release.yml) @@ -138,6 +138,7 @@ Implementation * [Cargo.toml](Cargo.toml) - workspace manifest * [config.toml](.cargo/config.toml) - for Rust code coverage * [dist-workspace.toml](dist-workspace.toml) - cargo-dist configuration + * [.ignore](.ignore) - show hidden directories to the CodeChat Editor Misc ====