From 27c37d9edbb5f1d225c3272f8a238b05ae362633 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Fri, 16 Oct 2026 17:12:15 -0500 Subject: [PATCH 01/18] Add: an "Open link" context menu item for links in non-editable content. TinyMCE's link plugin offers no context menu for non-editable content, so links in math, cross-references, fragments, and gather lists couldn't be followed by a right click or long press. Specify this in the cache spec's new "Links in hydrated content" section. Test it in a Markdown document and in a Python file which links to another file and within itself. Tests for known bugs are ignored: links within the current file don't scroll to their target, MathJax's own context menu takes a right click on math, and a right click on a doc block which isn't yet a TinyMCE editor never shows TinyMCE's menu. Co-Authored-By: Claude Opus 5.5 --- client/src/tinymce-config.mts | 103 +++- docs/implementation.md | 35 +- server/src/processing/cache-spec.md | 80 +-- .../test_open_non_editable_link/other.md | 3 + .../test_open_non_editable_link/test.md | 3 + server/tests/overall/overall_5.rs | 547 +++++++++++++++++- 6 files changed, 697 insertions(+), 74 deletions(-) create mode 100644 server/tests/fixtures/overall/overall_5/test_open_non_editable_link/other.md create mode 100644 server/tests/fixtures/overall/overall_5/test_open_non_editable_link/test.md diff --git a/client/src/tinymce-config.mts b/client/src/tinymce-config.mts index ca65e97b..5c0338be 100644 --- a/client/src/tinymce-config.mts +++ b/client/src/tinymce-config.mts @@ -153,32 +153,101 @@ 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. + contextmenu: `link image table ${NON_EDITABLE_LINK_MENU}`, }, ); // 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(); + }, + }); + addNonEditableLinkMenu(editor); + }; // Use these combined options to // [init](https://www.tiny.cloud/docs/tinymce/latest/apis/tinymce.root/#init) // TinyMCE. return await tinymce.init(combinedOptions); }; + +// Links in non-editable content +// ----------------------------- +// +// The Server hydrates cross-references, fragments, gather lists, and math as +// non-editable content, but the context menu of TinyMCE's link plugin offers +// nothing for a non-editable element; see . +// This context menu fills that gap with a single "Open link" item. 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 item. +const NON_EDITABLE_LINK_MENU = "noneditablelink"; +const OPEN_NON_EDITABLE_LINK_ITEM = "opennoneditablelink"; + +const addNonEditableLinkMenu = ( + // The editor whose context menu gains this item. + editor: Editor, +) => { + // The link which the context menu was most recently shown for. + let menuLink: HTMLAnchorElement | null = null; + + editor.ui.registry.addContextMenu(NON_EDITABLE_LINK_MENU, { + update: ( + // The element right-clicked or long-pressed, or the start of the + // selection when the keyboard opens the menu. + element, + ) => { + // An editable link already has the link plugin's menu. + menuLink = editor.dom.isEditable(element) + ? null + : editor.dom.getParent(element, "a[href]"); + return menuLink === null ? "" : OPEN_NON_EDITABLE_LINK_ITEM; + }, + }); + + // Follow the link exactly as a Ctrl+click does, by sending it one: the link + // plugin handles that click, but doesn't export the routine it uses to do + // so. The plugin checks for Cmd on macOS and iOS but Ctrl elsewhere, so the + // click holds both. + editor.ui.registry.addMenuItem(OPEN_NON_EDITABLE_LINK_ITEM, { + text: "Open link", + icon: "new-tab", + onAction: () => { + // Content the IDE sent while the menu was open may have replaced + // the link. TinyMCE never sees a click on a link outside the + // editor, which leaves the browser to follow it, so skip it. + if (menuLink === null || !editor.getBody().contains(menuLink)) { + return; + } + menuLink.dispatchEvent( + new MouseEvent("click", { + bubbles: true, + // TinyMCE cancels this click to keep the browser from + // following the link as well. + cancelable: true, + ctrlKey: true, + metaKey: true, + }), + ); + }, + }); +}; diff --git a/docs/implementation.md b/docs/implementation.md index ed2321cb..29df8689 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 @@ -362,12 +358,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 +389,7 @@ with descriptions of each setting. * Tabs vs spaces; newline type * Substitutions -Core development priorities ------------------------------------------------------------------- +## Core development priorities 1. Bug fixes 2. Book support diff --git a/server/src/processing/cache-spec.md b/server/src/processing/cache-spec.md index a0dc014a..820a7bc6 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,23 @@ 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 exactly as a Ctrl+click does. +TinyMCE's link plugin already provides the other ways to follow these links, so +the Client doesn't implement them: + +* 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/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/overall/overall_5.rs b/server/tests/overall/overall_5.rs index e4b9a9cd..49a84610 100644 --- a/server/tests/overall/overall_5.rs +++ b/server/tests/overall/overall_5.rs @@ -26,16 +26,23 @@ // ------- // // ### 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 thirtyfour::{ By, Key, WebDriver, error::WebDriverError, prelude::{ElementQueryable, ElementWaitable}, }; +use tokio::time::sleep; // ### Local use crate::common::{ @@ -828,3 +835,541 @@ 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. See `addNonEditableLinkMenu` 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 +); + +// In a Markdown document, using a stand-in for the content the cache hydrates. +async fn test_open_non_editable_link_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + perform_loadfile( + &codechat_server, + &test_dir, + "test.md", + Some(( + "\n".to_string(), + 0.0, + )), + false, + 6.0, + ) + .await; + + // Target the iframe containing the Client. + select_codechat_iframe(&driver).await; + + open_link_from_context_menu(&driver, &format!("{DOC_BLOCK_CSS} a")).await?; + let mut client_id = INITIAL_CLIENT_MESSAGE_ID; + assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.md")).await; + + 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 doesn't make a doc block a TinyMCE editor, so TinyMCE never shows its context 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).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 +} + +// #### Support for these tests +// +// 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 + "# +); + +// Create a project in `test_dir` whose `test.py` contains a link of each kind +// which the cache hydrates. Every link sits near the start of `test.py`, within +// the first screen, while every 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 + "# + ) + .to_string(); + for i in 0..100 { + let _ = writeln!(test_py, "filler_{i} = {i}"); + } + test_py.push_str(indoc!( + r#" + #

Test target

+ f = 6 + # A fragment gathered at the + # start of this file. + g = 7 + #

Far gather

+ h = 8 + "# + )); + // 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(); +} + +// Create the project `write_link_project` describes, then show its `test.py` +// in the Client. +async fn load_link_project( + codechat_server: &CodeChatEditorServerLog, + driver: &WebDriver, + 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( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + 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?; + + // 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?; + let link_css = &format!("#TinyMCE-inst:not(.CodeChat-doc-hidden) {link_css}"); + + match destination { + LinkDestination::OtherFile => { + open_link_from_context_menu(&driver, link_css).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).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(()) +} + +// Right-click the link `link_css` selects, then choose "Open link", which must +// be the only item on the context menu. +async fn open_link_from_context_menu( + driver: &WebDriver, + // CSS selecting the link to follow. + link_css: &str, +) -> Result<(), WebDriverError> { + let link = driver + .query(By::Css(link_css)) + .desc("the link to follow") + .first() + .await?; + driver + .action_chain() + .context_click_element(&link) + .perform() + .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?; + assert_eq!(menu_items.len(), 1); + assert_eq!(menu_items[0].text().await?, "Open link"); + menu_items[0].click().await?; + Ok(()) +} + +// Check that the Client's next request asks the IDE to open `file`. +async fn assert_requests_file( + 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( + 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( + driver: &WebDriver, + // The element's id. + id: &str, +) -> 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() +} From 82176f7ec4bc0111c6833877f60b374042098d43 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Sat, 17 Oct 2026 21:43:21 -0500 Subject: [PATCH 02/18] Add: make links in doc blocks not holding TinyMCE behave as they do in TinyMCE. A plain click, tap, or keyboard focus on a link moves TinyMCE into its doc block without following the link; a Ctrl+click (Cmd+click on macOS) follows it. Co-Authored-By: Claude Opus 5.5 --- client/src/CodeChatEditor.mts | 65 ++++--- client/src/CodeMirror-integration.mts | 122 +++++++++++- docs/implementation.md | 35 +++- server/tests/overall/overall_5.rs | 268 +++++++++++++++++++++++++- 4 files changed, 444 insertions(+), 46 deletions(-) diff --git a/client/src/CodeChatEditor.mts b/client/src/CodeChatEditor.mts index 055f63a3..80ce3bf5 100644 --- a/client/src/CodeChatEditor.mts +++ b/client/src/CodeChatEditor.mts @@ -647,37 +647,48 @@ const onNavigate = (navigateEvent: NavigateEvent) => { // 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. + // these. Skip a click another handler has already canceled, such as one on + // a link in a doc block which doesn't hold TinyMCE (see `Links in doc + // blocks` in `CodeMirror-integration.mts`). if ( + !event.defaultPrevented && event.target instanceof HTMLAnchorElement && - event.target.target === "_blank" + event.target.target === "_blank" && + followNewTabLink(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(); + } +}; + +// Follow a link which the browser would open in a new tab. +export const followNewTabLink = ( + // The link to follow. + link: HTMLAnchorElement, + // True if this followed the link, so the caller must cancel the click + // which would otherwise open it in a new tab; false to leave that click to + // the browser. +): boolean => { + // If it's to a CodeChat Editor file, then load it as such. + if (link.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? + saveThenNavigate(new URL(link.href)); + return true; + } + // 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")) { + parent.window.CodeChatEditorFramework.webSocketComm.sendMessage({ + OpenUrl: link.href, + }); + return true; } + return false; }; // Save the current document, then navigate to the provided URL, which must be a diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index 9050dae2..962623a7 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -85,6 +85,7 @@ import { tinymceInstance, tinymce, init, + followNewTabLink, } from "./CodeChatEditor.mjs"; import { CodeChatForWeb, @@ -834,6 +835,106 @@ 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; +}; + +// TinyMCE follows a clicked link when `tinymce.util.VK.metaKeyPressed` accepts +// the click, 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 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. +const onDocBlockLinkClick = ( + // A click anywhere in CodeMirror. + event: MouseEvent, +) => { + const link = linkOutsideTinymce(event.target); + if (link === null) { + return; + } + if (!isFollowClick(event) || followNewTabLink(link)) { + event.preventDefault(); + } +}; + // Keyboard navigation between code and doc blocks // ----------------------------------------------- // @@ -1195,10 +1296,12 @@ 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, as when Chromium + // focuses a pressed link (see `onDocBlockLinkMousedown`), on an + // element inside it. 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; @@ -1213,6 +1316,17 @@ export const DocBlockPlugin = ViewPlugin.fromClass( if (isTinymce) { // 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 +1836,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/docs/implementation.md b/docs/implementation.md index 29df8689..1fd91b50 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -196,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 @@ -494,13 +490,40 @@ 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 through +`followNewTabLink`, as TinyMCE's links do: a link to a CodeChat Editor file +loads through `saveThenNavigate`, while an external link opens through the IDE +in VSCode or in a new tab in a browser. 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. + +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/server/tests/overall/overall_5.rs b/server/tests/overall/overall_5.rs index 49a84610..a3627d3b 100644 --- a/server/tests/overall/overall_5.rs +++ b/server/tests/overall/overall_5.rs @@ -38,7 +38,7 @@ use dunce::canonicalize; use indoc::indoc; use pretty_assertions::assert_eq; use thirtyfour::{ - By, Key, WebDriver, + By, Key, WebDriver, WebElement, error::WebDriverError, prelude::{ElementQueryable, ElementWaitable}, }; @@ -836,7 +836,9 @@ async fn test_named_anchor_round_trip_core( Ok(()) } -// ### Links in non-editable content +// ### 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. See `addNonEditableLinkMenu` in @@ -884,7 +886,7 @@ async fn test_open_non_editable_link_core( make_test!( test_open_non_editable_link_py_unfocused, test_open_non_editable_link_py_unfocused_core, - ignore = "a right click doesn't make a doc block a TinyMCE editor, so TinyMCE never shows its context menu" + 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( @@ -1048,6 +1050,143 @@ async fn test_open_non_editable_link_py_gather_same_file_core( .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(()) +} + // #### Support for these tests // // Where following a link should lead. @@ -1079,10 +1218,22 @@ const LINK_PROJECT_OTHER_PY: &str = indoc!( "# ); -// Create a project in `test_dir` whose `test.py` contains a link of each kind -// which the cache hydrates. Every link sits near the start of `test.py`, within -// the first screen, while every target in `test.py` itself follows 100 lines of -// filler, so that following a link to one of them must scroll. +// CSS selecting the plain link in the `test.py` which `write_link_project` +// creates. +const PLAIN_LINK_CSS: &str = "a[href='other.py']"; + +// 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 and a +// link of each kind which the cache hydrates. Every link sits near the start of +// `test.py`, within the first screen, while every 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, @@ -1102,6 +1253,8 @@ fn write_link_project( # 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 "# ) .to_string(); @@ -1111,12 +1264,12 @@ fn write_link_project( test_py.push_str(indoc!( r#" #

Test target

- f = 6 + g = 7 # A fragment gathered at the # start of this file. - g = 7 - #

Far gather

h = 8 + #

Far gather

+ i = 9 "# )); // A `toc.md` makes this directory a project, so that the cache resolves ids @@ -1286,6 +1439,101 @@ async fn open_link_from_context_menu( 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( + 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( + codechat_server: &CodeChatEditorServerLog, + driver: &WebDriver, + 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?; + let mut actions = driver.action_chain(); + for modifier in modifiers { + actions = actions.key_down(modifier.clone()); + } + actions = actions.click_element(&link); + for modifier in modifiers.iter().rev() { + actions = actions.key_up(modifier.clone()); + } + actions.perform().await?; + Ok(client_id) +} + +// 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( + codechat_server: &CodeChatEditorServerLog, + 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( + 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( codechat_server: &CodeChatEditorServerLog, From 995a220cc64044eac4c090d966b1d2163ffb74d1 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Mon, 19 Oct 2026 20:25:58 -0500 Subject: [PATCH 03/18] Update: use htmd's Conversion API when translating doc blocks to Markdown. A Conversion borrows its HtmlToMarkdown, so build the converter outside HtmlToMarkdownWrapped and lend it in. last and convert now consume the wrapper, since finishing a Conversion consumes it. This needs the htmd update in Cargo.lock, which isn't part of this commit. Co-Authored-By: Claude Opus 5.5 --- server/src/processing.rs | 65 +++++++++++++++++++++------------- server/src/processing/tests.rs | 18 +++++----- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/server/src/processing.rs b/server/src/processing.rs index bf23dbcc..1411ff74 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,20 @@ 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) -> 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, tree: &Rc) -> Result { let mut converted = self.next(tree)?; converted.push_str(&self.last()?); Ok(converted) @@ -670,7 +686,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/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!( " From 3c45648236019d622b7234dcfc57e8d9704ea1d7 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Tue, 20 Oct 2026 21:35:52 -0500 Subject: [PATCH 04/18] Add: a minimum age update policy for cargo. --- .cargo/config.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.cargo/config.toml b/.cargo/config.toml index df2c8ac4..c3776f1f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -54,3 +54,6 @@ TS_RS_IMPORT_EXTENSION = "ts" # included. [target.'cfg(all(windows, target_env = "msvc"))'] rustflags = ["-C", "target-feature=+crt-static"] + +# Security +# -------- From 8de6b735f2792e08fbbc1dd4deea4e0d8cb37f19 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Tue, 20 Oct 2026 21:36:21 -0500 Subject: [PATCH 05/18] Word wrap. --- .cargo/config.toml | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index c3776f1f..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 @@ -57,3 +56,12 @@ 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 From 970573819227f0acb33a9cf8092f92c8986f48f6 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Tue, 20 Oct 2026 21:36:36 -0500 Subject: [PATCH 06/18] Fix: remove incorrect message waits. --- server/tests/overall/overall_4.rs | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) 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; From e0fa05b3fb97569cf197a5a45a92f7b7ca6a5eb4 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Tue, 20 Oct 2026 21:37:23 -0500 Subject: [PATCH 07/18] Freeze for release. --- CHANGELOG.md | 9 + Cargo.lock | 254 ++++++++------- Cargo.toml | 2 +- builder/Cargo.lock | 32 +- client/package.json5 | 26 +- client/pnpm-lock.yaml | 524 ++++++++++++++++--------------- extensions/VSCode/package.json | 16 +- extensions/VSCode/pnpm-lock.yaml | 523 +++++++++++++++--------------- server/Cargo.toml | 4 +- 9 files changed, 721 insertions(+), 669 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13cc1bd6..08e84be1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,15 @@ Changelog * No changes. +Version 0.3.1 -- 2026-Oct-20 +---------------------------- + +* 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/Cargo.lock b/Cargo.lock index 40f2d991..6e294bec 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", ] @@ -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.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" +checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc" 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..c2f2b9f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,7 +64,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/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/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..2067247b 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.11': + resolution: {integrity: sha512-dtJS4FQEfH9OwSYLtMrEnj7T8DzBbJc/97LI/OkiWOYpW/iW5t6R/gcxVrUXS8l45gCVpwMufgucRe/pe0ISqA==} 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.11': + resolution: {integrity: sha512-J/JylWoJTG8OQqZaiqly4IODyAxNofUlZo0wtSBJtHk6AdymbdIkPynAM1JvQaZpPaG5h+msk9QuK8y4i1ooqw==} 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.11': + resolution: {integrity: sha512-nlecCk3xy06eAGC7krZtzNDm9UNTHKuHQYK8ZYegdjzgSL8anGt/eLJZm86OWaNgZkwQwvDqfSmG8Nqe6MPZNQ==} 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.11': + resolution: {integrity: sha512-DKImmG+sTPolE0kGPsYPBxTtOjb3INALp32O2lW2CfSr0bAzSEKKuu+GrQbtilClyoPOahUErS3OrznFDraoWA==} 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.11': + resolution: {integrity: sha512-55sykACGENj5j1rhj+81AO8JbR9HYgGjRZ7/H+pNr4VE1DvINKYNMTcutk8ZmWxk276jAQvUKJ3+Kdj9GXU3lQ==} 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.11': + resolution: {integrity: sha512-6Jz6jys17yXydO+WdJgu7JhoB4K7/w9sWHByzzTmLjQghUx3TDMWvLv+UcCdnWhg4oprET67EMymnrETdeGNAg==} 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.11': + resolution: {integrity: sha512-lf2XeEebWiUYVOVEjLXxF7dfXrBBSEOBn9AqBh39VYXrPt5cDqI+1t58IFSjn8npqkg5jIOs8TQbjKABbFW1gQ==} 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.11': + resolution: {integrity: sha512-iDapw7nFSE5tsmQxcH2+KotJhyfVPmHd6vUOQN/Em+8WNbkZJwJ/rDowlJMFnMl0SYSDSfgKCNUGi6Sn5++Smg==} 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.11': + resolution: {integrity: sha512-26q26AyITo3CGZX4HWFIMgBMA1lu3YN07gxzBm4tW6Hglq/OVyTP50kDrmWuKybCX6dh3m8Ly+c7YhI+sqCgCQ==} 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.11': + resolution: {integrity: sha512-cUO96IdsfvIj2S+2PNcDSkTcLhjPj8pIOEWRV1Kj70MOP4wnE9zSk7N4C1EKHlFGRcgdR5AkbnBZj0dfHK9nbQ==} 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.11': + resolution: {integrity: sha512-DFYp/ZQRdKunI/LPc0UkRXG1vlNjJKOJf8O8Ctb8+wm2Nt2ELg9Eamc31D2VoakkfGo8L+z0Iejz6WtFbQwyGw==} 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.11': + resolution: {integrity: sha512-vt5tog1cr+QKadIddGe+3LrcX0EmjEd0izF717Uwf92mBmgYfq2XRmL2r4bZU40spB69exxhrBn9I91HYEr/7w==} 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.11': optional: true - '@napi-rs/canvas-darwin-arm64@1.0.10': + '@napi-rs/canvas-darwin-arm64@1.0.11': optional: true - '@napi-rs/canvas-darwin-x64@1.0.10': + '@napi-rs/canvas-darwin-x64@1.0.11': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@1.0.10': + '@napi-rs/canvas-linux-arm-gnueabihf@1.0.11': optional: true - '@napi-rs/canvas-linux-arm64-gnu@1.0.10': + '@napi-rs/canvas-linux-arm64-gnu@1.0.11': optional: true - '@napi-rs/canvas-linux-arm64-musl@1.0.10': + '@napi-rs/canvas-linux-arm64-musl@1.0.11': optional: true - '@napi-rs/canvas-linux-riscv64-gnu@1.0.10': + '@napi-rs/canvas-linux-riscv64-gnu@1.0.11': optional: true - '@napi-rs/canvas-linux-x64-gnu@1.0.10': + '@napi-rs/canvas-linux-x64-gnu@1.0.11': optional: true - '@napi-rs/canvas-linux-x64-musl@1.0.10': + '@napi-rs/canvas-linux-x64-musl@1.0.11': optional: true - '@napi-rs/canvas-win32-arm64-msvc@1.0.10': + '@napi-rs/canvas-win32-arm64-msvc@1.0.11': optional: true - '@napi-rs/canvas-win32-x64-msvc@1.0.10': + '@napi-rs/canvas-win32-x64-msvc@1.0.11': optional: true - '@napi-rs/canvas@1.0.10': + '@napi-rs/canvas@1.0.11': 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.11 + '@napi-rs/canvas-darwin-arm64': 1.0.11 + '@napi-rs/canvas-darwin-x64': 1.0.11 + '@napi-rs/canvas-linux-arm-gnueabihf': 1.0.11 + '@napi-rs/canvas-linux-arm64-gnu': 1.0.11 + '@napi-rs/canvas-linux-arm64-musl': 1.0.11 + '@napi-rs/canvas-linux-riscv64-gnu': 1.0.11 + '@napi-rs/canvas-linux-x64-gnu': 1.0.11 + '@napi-rs/canvas-linux-x64-musl': 1.0.11 + '@napi-rs/canvas-win32-arm64-msvc': 1.0.11 + '@napi-rs/canvas-win32-x64-msvc': 1.0.11 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.11 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/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 From 11a63b635b033e3aab49c15d52540a62e02d77d0 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 10:34:25 -0500 Subject: [PATCH 08/18] Fix: keep the Server from closing idle websockets in unit tests. The VSCode unit tests read one websocket of a pair while leaving the other unread, so it can't answer pings; waiting on the cache's watcher could outlast the ping delay, closing the idle websocket. Use a long ping delay under cfg!(test) instead of answering pings on a second websocket. read_message now uses tokio::time::timeout, reports a websocket which ends or errors, and echoes each ping's payload in its pong. Co-Authored-By: Claude Opus 5.5 --- server/src/ide/vscode/tests.rs | 50 +++++++++++++++++++--------------- server/src/webserver.rs | 15 ++++++++-- 2 files changed, 40 insertions(+), 25 deletions(-) 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/webserver.rs b/server/src/webserver.rs index 37f87728..e8e6b142 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -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 long a websocket may go without traffic before this server pings it, +/// then how long to wait 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; From e331a84fdbcdb8090e4eaacd66a9ab5d249c5f23 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 10:34:27 -0500 Subject: [PATCH 09/18] Fix: link handling in doc blocks and the non-editable link menu. * Move TinyMCE into a doc block only when the focus lands on its contents or a link in them, so focusing an embedded iframe or a no longer rebuilds the block. * When the keyboard opens the context menu on a selected non-editable element, "Open link" follows that element's first link. * "Open link" follows the link directly, as the link plugin's gotoLink does, instead of sending a synthetic Ctrl+Meta click. * Use TinyMCE's VK.metaKeyPressed once TinyMCE loads. * Add a test for the keyboard-opened menu, and comment the parameters of the link-test helpers. Co-Authored-By: Claude Opus 5.5 --- client/src/CodeMirror-integration.mts | 35 +++-- client/src/tinymce-config.mts | 53 ++++--- .../other.md | 3 + .../test.md | 3 + server/tests/overall/overall_5.rs | 140 ++++++++++++++---- 5 files changed, 172 insertions(+), 62 deletions(-) create mode 100644 server/tests/fixtures/overall/overall_5/test_open_non_editable_link_keyboard/other.md create mode 100644 server/tests/fixtures/overall/overall_5/test_open_non_editable_link_keyboard/test.md diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index 962623a7..137df40c 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -864,11 +864,12 @@ const linkOutsideTinymce = ( }; // TinyMCE follows a clicked link when `tinymce.util.VK.metaKeyPressed` accepts -// the click, 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 as below, the test requires -// Cmd; elsewhere, it requires Ctrl without Alt, since AltGr, which types -// characters on many Windows keyboard layouts, reports both. +// the click, so `isFollowClick` applies that test. However, TinyMCE loads only +// once a doc block gains the focus, too late for a click which arrives first; +// until then, `isFollowClick` repeats the test. 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(), ); @@ -877,7 +878,9 @@ 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); +): boolean => + tinymce?.util.VK.metaKeyPressed(event) ?? + (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, @@ -1297,12 +1300,16 @@ export const DocBlockPlugin = ViewPlugin.fromClass( indentDiv.addEventListener("input", onIndentInput); // The focus lands on the contents div or, as when Chromium - // focuses a pressed link (see `onDocBlockLinkMousedown`), on an - // element inside it. If the target is in the indent instead, - // then the following code isn't needed. + // focuses a pressed link (see `onDocBlockLinkMousedown`), on a + // link inside it. Ignore focus anywhere else. The indent needs + // none of the following code, and other focusable elements in + // the contents, such as an embedded iframe or a ``, + // must not move TinyMCE here. if ( !(eventTarget instanceof Element) || - eventTarget.closest(".CodeChat-doc-contents") === null + !eventTarget.matches( + ".CodeChat-doc-contents, .CodeChat-doc-contents a[href]", + ) ) { return false; } @@ -1316,10 +1323,10 @@ export const DocBlockPlugin = ViewPlugin.fromClass( if (isTinymce) { // 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. + // Focus which reaches a link from the keyboard, as with the + // Tab key, leaves the selection where it was, perhaps in + // another doc block; place the cursor at the link's start + // instead. const selection = window.getSelection(); if ( eventTarget !== contentsDiv && diff --git a/client/src/tinymce-config.mts b/client/src/tinymce-config.mts index 5c0338be..055cf463 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 { followNewTabLink } from "./CodeChatEditor.mjs"; + // Initialize TinyMCE. export const init = async ( // Provide additional editor options; note that the basic setup provided @@ -219,35 +222,51 @@ const addNonEditableLinkMenu = ( // An editable link already has the link plugin's menu. menuLink = editor.dom.isEditable(element) ? null - : editor.dom.getParent(element, "a[href]"); + : (editor.dom.getParent( + element, + "a[href]", + ) ?? + // The keyboard opens the menu on a selected non-editable + // element, which encloses its links instead of sitting in + // one; take its first link, as Alt+Enter does. + (element === editor.selection.getStart(true) + ? element.querySelector("a[href]") + : null)); return menuLink === null ? "" : OPEN_NON_EDITABLE_LINK_ITEM; }, }); - // Follow the link exactly as a Ctrl+click does, by sending it one: the link - // plugin handles that click, but doesn't export the routine it uses to do - // so. The plugin checks for Cmd on macOS and iOS but Ctrl elsewhere, so the - // click holds both. + // Follow the link exactly as a Ctrl+click does. The link plugin doesn't + // export the routine it uses to do so, `gotoLink`, so this repeats it: a + // link to an id scrolls the element with that id in the editor into view, + // while any other link opens in a new tab, which `followNewTabLink` + // redirects when it can. editor.ui.registry.addMenuItem(OPEN_NON_EDITABLE_LINK_ITEM, { text: "Open link", icon: "new-tab", onAction: () => { // Content the IDE sent while the menu was open may have replaced - // the link. TinyMCE never sees a click on a link outside the - // editor, which leaves the browser to follow it, so skip it. + // the link; don't follow a link which is no longer shown. if (menuLink === null || !editor.getBody().contains(menuLink)) { return; } - menuLink.dispatchEvent( - new MouseEvent("click", { - bubbles: true, - // TinyMCE cancels this click to keep the browser from - // following the link as well. - cancelable: true, - ctrlKey: true, - metaKey: true, - }), - ); + // Like `gotoLink`, prefer the original `href` which TinyMCE keeps + // in `data-mce-href`. + const href = + menuLink.getAttribute("data-mce-href") ?? + menuLink.getAttribute("href") ?? + ""; + if (href.startsWith("#")) { + const id = CSS.escape(href.slice(1)); + const target = editor.dom.select( + `[id="${id}"],[name="${id}"]`, + )[0]; + if (target !== undefined) { + editor.selection.scrollIntoView(target, true); + } + } else if (!followNewTabLink(menuLink)) { + window.open(menuLink.href, "_blank", "noopener,noreferrer"); + } }, }); }; 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_5.rs b/server/tests/overall/overall_5.rs index a3627d3b..9ceaf05f 100644 --- a/server/tests/overall/overall_5.rs +++ b/server/tests/overall/overall_5.rs @@ -841,7 +841,9 @@ async fn test_named_anchor_round_trip_core( // #### 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. See `addNonEditableLinkMenu` in +// only item, "Open link", follows that link. The keyboard opens the same menu +// on a selected non-editable element, following its first link. See +// `addNonEditableLinkMenu` 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!( @@ -849,33 +851,25 @@ make_test!( test_open_non_editable_link_core ); -// In a Markdown document, using a stand-in for the content the cache hydrates. async fn test_open_non_editable_link_core( codechat_server: CodeChatEditorServerLog, driver: WebDriver, test_dir: PathBuf, ) -> Result<(), WebDriverError> { - perform_loadfile( - &codechat_server, - &test_dir, - "test.md", - Some(( - "\n".to_string(), - 0.0, - )), - false, - 6.0, - ) - .await; - - // Target the iframe containing the Client. - select_codechat_iframe(&driver).await; + open_markdown_non_editable_link(codechat_server, driver, test_dir, MenuTrigger::Pointer).await +} - open_link_from_context_menu(&driver, &format!("{DOC_BLOCK_CSS} a")).await?; - let mut client_id = INITIAL_CLIENT_MESSAGE_ID; - assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.md")).await; +make_test!( + test_open_non_editable_link_keyboard, + test_open_non_editable_link_keyboard_core +); - Ok(()) +async fn test_open_non_editable_link_keyboard_core( + codechat_server: CodeChatEditorServerLog, + driver: WebDriver, + test_dir: PathBuf, +) -> Result<(), WebDriverError> { + open_markdown_non_editable_link(codechat_server, driver, test_dir, MenuTrigger::Keyboard).await } // In a source file, unlike a Markdown document, a doc block becomes a TinyMCE @@ -896,7 +890,7 @@ async fn test_open_non_editable_link_py_unfocused_core( ) -> 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).await?; + open_link_from_context_menu(&driver, link_css, MenuTrigger::Pointer).await?; assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.py")).await; Ok(()) @@ -1189,6 +1183,14 @@ async fn test_ctrl_click_non_editable_link_py_unfocused_core( // #### Support for these tests // +// How a test opens TinyMCE's context menu for a link. +enum MenuTrigger { + // A right click on the link. + Pointer, + // The keyboard, with the non-editable element enclosing the link selected. + Keyboard, +} + // Where following a link should lead. enum LinkDestination { // The Client asks the IDE to open `other.py`. @@ -1279,11 +1281,50 @@ fn write_link_project( fs::write(test_dir.join("test.py"), test_py).unwrap(); } +// In a Markdown document, using a stand-in for the content the cache hydrates, +// open the context menu for a link in non-editable content, 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, +) -> Result<(), WebDriverError> { + perform_loadfile( + &codechat_server, + &test_dir, + "test.md", + Some(( + "\n".to_string(), + 0.0, + )), + false, + 6.0, + ) + .await; + + // Target the iframe containing the Client. + select_codechat_iframe(&driver).await; + + open_link_from_context_menu(&driver, &format!("{DOC_BLOCK_CSS} a"), trigger).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, @@ -1355,8 +1396,11 @@ async fn load_link_project( // 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, @@ -1381,7 +1425,7 @@ async fn follow_link_project_link( match destination { LinkDestination::OtherFile => { - open_link_from_context_menu(&driver, link_css).await?; + open_link_from_context_menu(&driver, link_css, MenuTrigger::Pointer).await?; assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.py")) .await; } @@ -1390,7 +1434,7 @@ async fn follow_link_project_link( !is_in_view(&driver, id).await, "#{id} is in view before following the link to it." ); - open_link_from_context_menu(&driver, link_css).await?; + open_link_from_context_menu(&driver, link_css, MenuTrigger::Pointer).await?; let deadline = Instant::now() + TIMEOUT; while !is_in_view(&driver, id).await { assert!( @@ -1411,23 +1455,46 @@ async fn follow_link_project_link( Ok(()) } -// Right-click the link `link_css` selects, then choose "Open link", which must -// be the only item on the context menu. +// Open TinyMCE's context menu for the link `link_css` selects, then choose +// "Open link", which must be the only item on the menu. 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, ) -> Result<(), WebDriverError> { let link = driver .query(By::Css(link_css)) .desc("the link to follow") .first() .await?; - driver - .action_chain() - .context_click_element(&link) - .perform() - .await?; + match trigger { + MenuTrigger::Pointer => { + driver + .action_chain() + .context_click_element(&link) + .perform() + .await?; + } + // macOS has no keyboard shortcut which opens a context menu, so 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. + MenuTrigger::Keyboard => { + driver + .execute( + r#"const editor = tinymce.get(0); + editor.selection.select(arguments[0].closest("[contenteditable='false']")); + editor.getBody().dispatchEvent( + new MouseEvent("contextmenu", { bubbles: true, cancelable: true }), + );"#, + vec![link.to_json()?], + ) + .await?; + } + } let menu_items = driver .query(By::Css(".tox-menu [role='menuitem']")) .desc("the items of TinyMCE's context menu") @@ -1443,6 +1510,7 @@ async fn open_link_from_context_menu( // 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, @@ -1460,8 +1528,11 @@ async fn query_unfocused_link( // 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, @@ -1486,7 +1557,9 @@ async fn click_unfocused_link( // 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. @@ -1519,6 +1592,7 @@ async fn assert_not_followed( // 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, @@ -1536,6 +1610,7 @@ async fn assert_cursor_in( // 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. @@ -1567,6 +1642,7 @@ async fn assert_requests_file( // * 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. @@ -1599,9 +1675,11 @@ async fn next_unhandled_message( // 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( From 869999b9513c1a081463732483d2dc2cd2fd0f80 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 10:34:27 -0500 Subject: [PATCH 10/18] Update: comment the parameters and return values of HtmlToMarkdownWrapped. Co-Authored-By: Claude Opus 5.5 --- server/src/processing.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/src/processing.rs b/server/src/processing.rs index 1411ff74..50ac0564 100644 --- a/server/src/processing.rs +++ b/server/src/processing.rs @@ -658,7 +658,10 @@ impl<'a> HtmlToMarkdownWrapped<'a> { /// 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) -> Result { + 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))? @@ -668,7 +671,12 @@ impl<'a> HtmlToMarkdownWrapped<'a> { /// Convert a document consisting of a single DOM tree from HTML to /// markdown. - fn convert(self, tree: &Rc) -> Result { + 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) From 8d3e93f8d9256c1c9aecb0601c439b0e4c0dfbdc Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 11:34:00 -0500 Subject: [PATCH 11/18] Fix: ping each websocket on a fixed schedule. The ping timer restarted on every pass through the websocket loop, so a steady stream of outgoing messages put off the ping, and with it the detection of a broken websocket, indefinitely. Ping on a fixed interval instead, delaying rather than bursting any missed ticks so that a stalled send can't close the websocket before its pong arrives. Co-Authored-By: Claude Opus 5.5 --- server/src/webserver.rs | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/server/src/webserver.rs b/server/src/webserver.rs index e8e6b142..d3000248 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -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,13 +492,13 @@ pub const REPLY_TIMEOUT_MS: Duration = if cfg!(test) { Duration::from_secs(15) }; -/// How long a websocket may go without traffic before this server pings it, -/// then how long to wait 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. +/// 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 { @@ -1351,11 +1351,20 @@ pub fn client_websocket( let mut is_closing = false; // 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) => { + _ = ping_interval.tick() => { if sent_ping { // If we haven't received the answering pong, the // websocket must be broken. From e9a083fe0de8a09102b824e64da6bac3975c40f1 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 11:34:01 -0500 Subject: [PATCH 12/18] Fix: math focus, the "Open link" menu beside links, and follow clicks. * Move TinyMCE into a doc block for focus anywhere in its contents, such as on typeset math, which MathJax makes focusable; only focus on an iframe or a leaves TinyMCE in place. Send cursor updates for any focus in the contents. * Take a non-editable element's first link for "Open link" only when the keyboard opened the menu, so a right click or long press beside the links no longer follows the first one. * Return to a single test for a link-following click, rather than deferring to TinyMCE's once it loads. * Press a real Shift+F10 in the keyboard test (through the DevTools protocol, since Chrome ignores WebDriver's key actions here), and add a test that a right click beside a link offers no "Open link". * Describe both cases in the cache spec. Co-Authored-By: Claude Opus 5.5 --- client/src/CodeMirror-integration.mts | 47 +++-- client/src/tinymce-config.mts | 17 +- server/src/processing/cache-spec.md | 9 +- .../test.md | 3 + server/tests/overall/overall_5.rs | 188 ++++++++++++++---- 5 files changed, 196 insertions(+), 68 deletions(-) create mode 100644 server/tests/fixtures/overall/overall_5/test_open_non_editable_link_beside/test.md diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index 137df40c..34da9e7d 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -864,12 +864,11 @@ const linkOutsideTinymce = ( }; // TinyMCE follows a clicked link when `tinymce.util.VK.metaKeyPressed` accepts -// the click, so `isFollowClick` applies that test. However, TinyMCE loads only -// once a doc block gains the focus, too late for a click which arrives first; -// until then, `isFollowClick` repeats the test. 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. +// the click, 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(), ); @@ -878,9 +877,7 @@ const isFollowClick = ( // A mouse event on a link. event: MouseEvent, // True if the event asks to follow the link. -): boolean => - tinymce?.util.VK.metaKeyPressed(event) ?? - (isMacOsOrIos ? event.metaKey : event.ctrlKey && !event.altKey); +): 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, @@ -1299,17 +1296,14 @@ export const DocBlockPlugin = ViewPlugin.fromClass( ); indentDiv.addEventListener("input", onIndentInput); - // The focus lands on the contents div or, as when Chromium - // focuses a pressed link (see `onDocBlockLinkMousedown`), on a - // link inside it. Ignore focus anywhere else. The indent needs - // none of the following code, and other focusable elements in - // the contents, such as an embedded iframe or a ``, - // must not move TinyMCE here. + // 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 Element) || - !eventTarget.matches( - ".CodeChat-doc-contents, .CodeChat-doc-contents a[href]", - ) + eventTarget.closest(".CodeChat-doc-contents") === null ) { return false; } @@ -1319,14 +1313,19 @@ 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 a link from the keyboard, as with the - // Tab key, leaves the selection where it was, perhaps in - // another doc block; place the cursor at the link's start - // instead. + // 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 && diff --git a/client/src/tinymce-config.mts b/client/src/tinymce-config.mts index 055cf463..232325a3 100644 --- a/client/src/tinymce-config.mts +++ b/client/src/tinymce-config.mts @@ -212,11 +212,19 @@ const addNonEditableLinkMenu = ( ) => { // 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(NON_EDITABLE_LINK_MENU, { update: ( - // The element right-clicked or long-pressed, or the start of the - // selection when the keyboard opens the menu. + // 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, ) => { // An editable link already has the link plugin's menu. @@ -228,8 +236,9 @@ const addNonEditableLinkMenu = ( ) ?? // The keyboard opens the menu on a selected non-editable // element, which encloses its links instead of sitting in - // one; take its first link, as Alt+Enter does. - (element === editor.selection.getStart(true) + // one; take its first link, as Alt+Enter does. A right + // click or long press beside the links offers nothing. + (element !== menuEventTarget ? element.querySelector("a[href]") : null)); return menuLink === null ? "" : OPEN_NON_EDITABLE_LINK_ITEM; diff --git a/server/src/processing/cache-spec.md b/server/src/processing/cache-spec.md index 820a7bc6..16040a52 100644 --- a/server/src/processing/cache-spec.md +++ b/server/src/processing/cache-spec.md @@ -482,9 +482,12 @@ 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 exactly as a Ctrl+click does. -TinyMCE's link plugin already provides the other ways to follow these links, so -the Client doesn't implement them: +whose only item, "Open link", follows the link exactly 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. TinyMCE's link plugin already provides +the other ways to follow these links, so the Client doesn't implement them: * Ctrl+click (Cmd+click on macOS) follows the clicked link. * Alt+Enter, with the non-editable element selected, follows its first link. 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/overall/overall_5.rs b/server/tests/overall/overall_5.rs index 9ceaf05f..468716e7 100644 --- a/server/tests/overall/overall_5.rs +++ b/server/tests/overall/overall_5.rs @@ -37,6 +37,7 @@ use std::{ use dunce::canonicalize; use indoc::indoc; use pretty_assertions::assert_eq; +use serde_json::json; use thirtyfour::{ By, Key, WebDriver, WebElement, error::WebDriverError, @@ -841,10 +842,10 @@ async fn test_named_anchor_round_trip_core( // #### 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. The keyboard opens the same menu -// on a selected non-editable element, following its first link. See -// `addNonEditableLinkMenu` in -// [tinymce-config.mts](../../../client/src/tinymce-config.mts) and `Links in +// 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 `addNonEditableLinkMenu` +// 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, @@ -852,9 +853,13 @@ make_test!( ); 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 } @@ -865,13 +870,63 @@ make_test!( ); 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)), @@ -1187,10 +1242,17 @@ async fn test_ctrl_click_non_editable_link_py_unfocused_core( enum MenuTrigger { // A right click on the link. Pointer, - // The keyboard, with the non-editable element enclosing the link selected. + // 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`. @@ -1281,35 +1343,58 @@ fn write_link_project( fs::write(test_dir.join("test.py"), test_py).unwrap(); } -// In a Markdown document, using a stand-in for the content the cache hydrates, -// open the context menu for a link in non-editable content, then follow the -// link. -async fn open_markdown_non_editable_link( +// 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. - 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, -) -> Result<(), WebDriverError> { + 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, + codechat_server, + test_dir, "test.md", - Some(( - "\n".to_string(), - 0.0, - )), + Some((MARKDOWN_NON_EDITABLE_LINK.to_string(), 0.0)), false, 6.0, ) .await; // Target the iframe containing the Client. - select_codechat_iframe(&driver).await; + 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).await?; let mut client_id = INITIAL_CLIENT_MESSAGE_ID; assert_requests_file(&codechat_server, &mut client_id, &test_dir.join("other.md")).await; @@ -1464,6 +1549,7 @@ async fn open_link_from_context_menu( link_css: &str, // How to open the context menu. trigger: MenuTrigger, + // WebDriver's errors; failed checks panic instead. ) -> Result<(), WebDriverError> { let link = driver .query(By::Css(link_css)) @@ -1478,21 +1564,49 @@ async fn open_link_from_context_menu( .perform() .await?; } - // macOS has no keyboard shortcut which opens a context menu, so 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. MenuTrigger::Keyboard => { - driver - .execute( - r#"const editor = tinymce.get(0); - editor.selection.select(arguments[0].closest("[contenteditable='false']")); - editor.getBody().dispatchEvent( - new MouseEvent("contextmenu", { bubbles: true, cancelable: true }), - );"#, - vec![link.to_json()?], - ) + // 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 From 0b37f0b3b1d82157088d35e8c994bb2659beb423 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 19:02:07 -0500 Subject: [PATCH 13/18] Update: document cross-references in CLAUDE.md; fix style guide typos. * Describe how to create unique ids, cross-reference them, and gather fragments when implementing a specification. * Mention the standalone server in the architecture overview, and how to comment out code without creating a doc block. * Remove zero-width spaces from the style guide. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 44 +++++++++++++++++++++++++++++++++++++------- docs/style_guide.cpp | 8 ++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 847a4190..32cd5911 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -32,13 +32,41 @@ 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; 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"). + +When implementing a specification: + +1. Label each doc block preceding non-contiguous section of code of more than + three lines with a fragment: ``. +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 ------------- @@ -139,6 +167,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 +178,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 +216,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/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. From 5397de25f6cfdaa6c16b050438556c0162ad0bfb Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 19:02:29 -0500 Subject: [PATCH 14/18] Update: cross-reference the build docs. * Give the workspace note in the root Cargo.toml an id, and refer to it from builder/Cargo.toml through a cross-reference. * Add a heading for the bt script to implementation.md. Co-Authored-By: Claude Opus 5.5 --- Cargo.toml | 3 +-- builder/Cargo.toml | 2 +- docs/implementation.md | 5 ++++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c2f2b9f0..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 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/docs/implementation.md b/docs/implementation.md index 1fd91b50..e21889b4 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -261,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 ----------- From 5a934e67e1a0be957fe27d88f69b972a7b0bc172 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 19:02:30 -0500 Subject: [PATCH 15/18] Fix: read each websocket before its ping timer; fail it on a protocol error. * Poll the websocket loop's branches in order: reading first, so that a tick never judges the websocket broken while the answering pong waits unread, then the ping timer, then outgoing messages. * Close the websocket on a receive error, sending status 1002 as RFC 6455 asks. actix-ws reports the same decode error on every poll, which would otherwise starve the other branches. Co-Authored-By: Claude Opus 5.5 --- server/src/webserver.rs | 58 +++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/server/src/webserver.rs b/server/src/webserver.rs index d3000248..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; @@ -1349,6 +1349,9 @@ 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 @@ -1363,23 +1366,13 @@ pub fn client_websocket( loop { select! { - // 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; - } + // 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() => { @@ -1468,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. @@ -1527,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}"); } From f949b8667f69add11977595095ac45095a3059db Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 19:02:39 -0500 Subject: [PATCH 16/18] Fix: follow every link in the Client through followLink. * Replace followNewTabLink with followLink, which scrolls to a link to an id anywhere in the Client, loads a CodeChat Editor file, or opens an external link through the IDE in VSCode or in a new tab in a browser. isLeftToBrowser leaves a Ctrl+click on an external link outside TinyMCE to the browser. * In the doc block holding TinyMCE, take Ctrl+click and Alt+Enter from the link plugin, and replace its context menu with one which keeps its editing items and adds an "Open link" for any link. * A plain click on a link which opens in a new tab now places the cursor instead of following the link. * Describe link navigation in implementation.md and the cache spec, with a TODO for the quick link form's "Open link" button. * Add tests for Ctrl+click, Alt+Enter, and "Open link" in the doc block holding TinyMCE, including a link to an id in another doc block. Co-Authored-By: Claude Opus 5.5 --- client/src/CodeChatEditor.mts | 84 ++-- client/src/CodeMirror-integration.mts | 25 +- client/src/tinymce-config.mts | 183 ++++++--- docs/implementation.md | 31 +- server/src/processing/cache-spec.md | 11 +- .../test_alt_enter_non_editable_link/other.md | 3 + .../test_alt_enter_non_editable_link/test.md | 3 + .../other.md | 3 + .../test_ctrl_click_non_editable_link/test.md | 3 + server/tests/overall/overall_5.rs | 377 ++++++++++++++++-- 10 files changed, 582 insertions(+), 141 deletions(-) create mode 100644 server/tests/fixtures/overall/overall_5/test_alt_enter_non_editable_link/other.md create mode 100644 server/tests/fixtures/overall/overall_5/test_alt_enter_non_editable_link/test.md create mode 100644 server/tests/fixtures/overall/overall_5/test_ctrl_click_non_editable_link/other.md create mode 100644 server/tests/fixtures/overall/overall_5/test_ctrl_click_non_editable_link/test.md diff --git a/client/src/CodeChatEditor.mts b/client/src/CodeChatEditor.mts index 80ce3bf5..6eb80839 100644 --- a/client/src/CodeChatEditor.mts +++ b/client/src/CodeChatEditor.mts @@ -646,49 +646,79 @@ 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. Skip a click another handler has already canceled, such as one on - // a link in a doc block which doesn't hold TinyMCE (see `Links in doc - // blocks` in `CodeMirror-integration.mts`). + // 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" && - followNewTabLink(event.target) + event.target.closest(".CodeChat-doc-contents") === null && + !isLeftToBrowser(event.target) ) { event.preventDefault(); + followLink(event.target); } }; -// Follow a link which the browser would open in a new tab. -export const followNewTabLink = ( +// 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, - // True if this followed the link, so the caller must cancel the click - // which would otherwise open it in a new tab; false to leave that click to - // the browser. -): boolean => { - // If it's to a CodeChat Editor file, then load it as such. - if (link.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? +) => { + // 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)); - return true; - } - // 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")) { + } 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, }); - return true; + } 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"); } - return false; }; // Save the current document, then navigate to the provided URL, which must be a diff --git a/client/src/CodeMirror-integration.mts b/client/src/CodeMirror-integration.mts index 34da9e7d..18d71a68 100644 --- a/client/src/CodeMirror-integration.mts +++ b/client/src/CodeMirror-integration.mts @@ -85,7 +85,8 @@ import { tinymceInstance, tinymce, init, - followNewTabLink, + followLink, + isLeftToBrowser, } from "./CodeChatEditor.mjs"; import { CodeChatForWeb, @@ -863,12 +864,14 @@ const linkOutsideTinymce = ( : null; }; -// TinyMCE follows a clicked link when `tinymce.util.VK.metaKeyPressed` accepts -// the click, 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. +// 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(), ); @@ -921,7 +924,8 @@ const onDocBlockLinkMousedown = ( // 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. +// 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, @@ -930,8 +934,11 @@ const onDocBlockLinkClick = ( if (link === null) { return; } - if (!isFollowClick(event) || followNewTabLink(link)) { + if (!isFollowClick(event)) { event.preventDefault(); + } else if (!isLeftToBrowser(link)) { + event.preventDefault(); + followLink(link); } }; diff --git a/client/src/tinymce-config.mts b/client/src/tinymce-config.mts index 232325a3..6bb9b82c 100644 --- a/client/src/tinymce-config.mts +++ b/client/src/tinymce-config.mts @@ -68,7 +68,7 @@ import "tinymce/plugins/visualchars/index.js"; /// import './plugins/powerpaste/js/wordimport'; // Import local code. -import { followNewTabLink } from "./CodeChatEditor.mjs"; +import { followLink } from "./CodeChatEditor.mjs"; // Initialize TinyMCE. export const init = async ( @@ -160,8 +160,9 @@ export const init = async ( // 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. - contextmenu: `link image table ${NON_EDITABLE_LINK_MENU}`, + // context menus of the plugins imported above, except the link + // plugin's, which `LINK_MENU` replaces. + contextmenu: `${LINK_MENU} image table`, }, ); @@ -183,7 +184,8 @@ export const init = async ( return () => changed.unbind(); }, }); - addNonEditableLinkMenu(editor); + takeLinkGestures(editor); + addLinkMenu(editor); }; // Use these combined options to @@ -192,22 +194,105 @@ export const init = async ( return await tinymce.init(combinedOptions); }; -// Links in non-editable content -// ----------------------------- +// Following links +// --------------- // -// The Server hydrates cross-references, fragments, gather lists, and math as -// non-editable content, but the context menu of TinyMCE's link plugin offers -// nothing for a non-editable element; see . -// This context menu fills that gap with a single "Open link" item. TinyMCE -// shows its context menus on a long press as well as a right click, so this -// serves touch devices too. +// 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. // -// The names under which TinyMCE registers this context menu and its item. -const NON_EDITABLE_LINK_MENU = "noneditablelink"; -const OPEN_NON_EDITABLE_LINK_ITEM = "opennoneditablelink"; +// 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); + } + }); -const addNonEditableLinkMenu = ( - // The editor whose context menu gains this item. + 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. @@ -220,61 +305,43 @@ const addNonEditableLinkMenu = ( menuEventTarget = event.target; }); - editor.ui.registry.addContextMenu(NON_EDITABLE_LINK_MENU, { + 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, ) => { - // An editable link already has the link plugin's menu. - menuLink = editor.dom.isEditable(element) - ? null - : (editor.dom.getParent( - element, - "a[href]", - ) ?? - // The keyboard opens the menu on a selected non-editable - // element, which encloses its links instead of sitting in - // one; take its first link, as Alt+Enter does. A right - // click or long press beside the links offers nothing. - (element !== menuEventTarget - ? element.querySelector("a[href]") - : null)); - return menuLink === null ? "" : OPEN_NON_EDITABLE_LINK_ITEM; + 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; }, }); - // Follow the link exactly as a Ctrl+click does. The link plugin doesn't - // export the routine it uses to do so, `gotoLink`, so this repeats it: a - // link to an id scrolls the element with that id in the editor into view, - // while any other link opens in a new tab, which `followNewTabLink` - // redirects when it can. - editor.ui.registry.addMenuItem(OPEN_NON_EDITABLE_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)) { - return; - } - // Like `gotoLink`, prefer the original `href` which TinyMCE keeps - // in `data-mce-href`. - const href = - menuLink.getAttribute("data-mce-href") ?? - menuLink.getAttribute("href") ?? - ""; - if (href.startsWith("#")) { - const id = CSS.escape(href.slice(1)); - const target = editor.dom.select( - `[id="${id}"],[name="${id}"]`, - )[0]; - if (target !== undefined) { - editor.selection.scrollIntoView(target, true); - } - } else if (!followNewTabLink(menuLink)) { - window.open(menuLink.href, "_blank", "noopener,noreferrer"); + if (menuLink !== null && editor.getBody().contains(menuLink)) { + followLink(menuLink); } }, }); diff --git a/docs/implementation.md b/docs/implementation.md index e21889b4..a922672e 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -515,12 +515,31 @@ 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 through -`followNewTabLink`, as TinyMCE's links do: a link to a CodeChat Editor file -loads through `saveThenNavigate`, while an external link opens through the IDE -in VSCode or in a new tab in a browser. 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. +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 diff --git a/server/src/processing/cache-spec.md b/server/src/processing/cache-spec.md index 16040a52..f9be25c3 100644 --- a/server/src/processing/cache-spec.md +++ b/server/src/processing/cache-spec.md @@ -482,12 +482,13 @@ 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 exactly 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 +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. TinyMCE's link plugin already provides -the other ways to follow these links, so the Client doesn't implement them: +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. 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/overall/overall_5.rs b/server/tests/overall/overall_5.rs index 468716e7..646ec37f 100644 --- a/server/tests/overall/overall_5.rs +++ b/server/tests/overall/overall_5.rs @@ -844,8 +844,8 @@ async fn test_named_anchor_round_trip_core( // 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 `addNonEditableLinkMenu` -// in [tinymce-config.mts](../../../client/src/tinymce-config.mts) and `Links in +// 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, @@ -945,7 +945,13 @@ async fn test_open_non_editable_link_py_unfocused_core( ) -> 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).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(()) @@ -1236,6 +1242,201 @@ async fn test_ctrl_click_non_editable_link_py_unfocused_core( 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. @@ -1286,6 +1487,14 @@ const LINK_PROJECT_OTHER_PY: &str = indoc!( // 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 @@ -1293,11 +1502,12 @@ const FOLLOW_KEY: Key = if cfg!(target_os = "macos") { Key::Control }; -// Create a project in `test_dir` whose `test.py` contains a plain link and a -// link of each kind which the cache hydrates. Every link sits near the start of -// `test.py`, within the first screen, while every target in `test.py` itself -// follows 100 lines of filler, so that following a link to one of them must -// scroll. +// 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, @@ -1319,6 +1529,10 @@ fn write_link_project( 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(); @@ -1328,12 +1542,12 @@ fn write_link_project( test_py.push_str(indoc!( r#" #

Test target

- g = 7 + i = 9 # A fragment gathered at the # start of this file. - h = 8 + j = 10 #

Far gather

- i = 9 + k = 11 "# )); // A `toc.md` makes this directory a project, so that the cache resolves ids @@ -1395,7 +1609,13 @@ async fn open_markdown_non_editable_link( // 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).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; @@ -1493,24 +1713,17 @@ async fn follow_link_project_link( destination: LinkDestination, ) -> Result<(), WebDriverError> { let mut client_id = load_link_project(&codechat_server, &driver, &test_dir, link_css).await?; - - // 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?; - let link_css = &format!("#TinyMCE-inst:not(.CodeChat-doc-hidden) {link_css}"); + 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).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; } @@ -1519,7 +1732,13 @@ async fn follow_link_project_link( !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).await?; + 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!( @@ -1540,8 +1759,8 @@ async fn follow_link_project_link( Ok(()) } -// Open TinyMCE's context menu for the link `link_css` selects, then choose -// "Open link", which must be the only item on the menu. +// 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, @@ -1549,6 +1768,9 @@ async fn open_link_from_context_menu( 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 @@ -1614,9 +1836,24 @@ async fn open_link_from_context_menu( .desc("the items of TinyMCE's context menu") .all_from_selector_required() .await?; - assert_eq!(menu_items.len(), 1); - assert_eq!(menu_items[0].text().await?, "Open link"); - menu_items[0].click().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(()) } @@ -1656,16 +1893,69 @@ async fn click_unfocused_link( ) -> 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(&link); + actions = actions.click_element(element); for modifier in modifiers.iter().rev() { actions = actions.key_up(modifier.clone()); } - actions.perform().await?; - Ok(client_id) + 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 @@ -1813,3 +2103,18 @@ async fn is_in_view( .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() +} From af3e82fe475cccd1b5f84ffc0d92db32c940aeac Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 20:09:16 -0500 Subject: [PATCH 17/18] Update: move the bt scripts to the project root; gather their uses. * Move bt and bt.ps1 from server/ to the project root, and run them from there in CI and the dev container. * Mark both scripts and each call to them with a fragment, and gather these in the bt section of implementation.md. * Add a root .ignore which whitelists .github/ and .devcontainer/: the cache skips hidden directories, so it would otherwise miss the fragments there. * Update the docs which said to run bt from server/, linking their mentions of bt to the gathered section. * In CLAUDE.md, show how to assign an id, and place a fragment at the end of its doc block. Co-Authored-By: Claude Opus 5.5 --- .devcontainer/postCreateCommand.sh | 2 +- .github/workflows/check.yml | 4 +++- .github/workflows/release.yml | 7 +++++-- .ignore | 30 ++++++++++++++++++++++++++++++ CLAUDE.md | 16 ++++++++++------ server/bt => bt | 13 ++++++++----- server/bt.ps1 => bt.ps1 | 13 ++++++++----- client/src/CodeChatEditor-test.mts | 14 +++++++------- docs/accessibility_review.md | 9 +++++---- docs/design.md | 8 ++++---- docs/implementation.md | 2 +- toc.md | 5 +++-- 12 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 .ignore rename server/bt => bt (67%) rename server/bt.ps1 => bt.ps1 (68%) 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/CLAUDE.md b/CLAUDE.md index 32cd5911..452a0329 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,19 +37,22 @@ void foo(); 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; 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. +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"). + "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: ``. + 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]

`. @@ -71,7 +74,8 @@ 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`. 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/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/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 a922672e..093ce62b 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -264,7 +264,7 @@ So, this project contains Rust code to automate this process -- see the [builder](../builder/Cargo.toml). The `bt` script simplifies invoking the builder. -### `bt` (build tool) script +### `bt` (build tool) script Misc topics ----------- 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 ==== From 448ad849bf68eec08822d425b2a7299d044ff407 Mon Sep 17 00:00:00 2001 From: "Bryan A. Jones" Date: Wed, 21 Oct 2026 20:48:27 -0500 Subject: [PATCH 18/18] Freeze for release. --- CHANGELOG.md | 2 +- Cargo.lock | 8 ++-- client/pnpm-lock.yaml | 96 +++++++++++++++++++++---------------------- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08e84be1..531c7b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ Changelog * No changes. -Version 0.3.1 -- 2026-Oct-20 +Version 0.3.1 -- 2026-Oct-21 ---------------------------- * Support navigation of links in gather elements, fragments, and cross diff --git a/Cargo.lock b/Cargo.lock index 6e294bec..d4d0b77e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3459,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", @@ -4501,9 +4501,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.54.0" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c168a474b95bfecfe908a65d4bdffc523144b649a74c09c286afb35d53e0bc" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 2067247b..d85237ed 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -533,79 +533,79 @@ packages: resolution: {integrity: sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==} engines: {node: '>=22.12.0'} - '@napi-rs/canvas-android-arm64@1.0.11': - resolution: {integrity: sha512-dtJS4FQEfH9OwSYLtMrEnj7T8DzBbJc/97LI/OkiWOYpW/iW5t6R/gcxVrUXS8l45gCVpwMufgucRe/pe0ISqA==} + '@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.11': - resolution: {integrity: sha512-J/JylWoJTG8OQqZaiqly4IODyAxNofUlZo0wtSBJtHk6AdymbdIkPynAM1JvQaZpPaG5h+msk9QuK8y4i1ooqw==} + '@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.11': - resolution: {integrity: sha512-nlecCk3xy06eAGC7krZtzNDm9UNTHKuHQYK8ZYegdjzgSL8anGt/eLJZm86OWaNgZkwQwvDqfSmG8Nqe6MPZNQ==} + '@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.11': - resolution: {integrity: sha512-DKImmG+sTPolE0kGPsYPBxTtOjb3INALp32O2lW2CfSr0bAzSEKKuu+GrQbtilClyoPOahUErS3OrznFDraoWA==} + '@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.11': - resolution: {integrity: sha512-55sykACGENj5j1rhj+81AO8JbR9HYgGjRZ7/H+pNr4VE1DvINKYNMTcutk8ZmWxk276jAQvUKJ3+Kdj9GXU3lQ==} + '@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.11': - resolution: {integrity: sha512-6Jz6jys17yXydO+WdJgu7JhoB4K7/w9sWHByzzTmLjQghUx3TDMWvLv+UcCdnWhg4oprET67EMymnrETdeGNAg==} + '@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.11': - resolution: {integrity: sha512-lf2XeEebWiUYVOVEjLXxF7dfXrBBSEOBn9AqBh39VYXrPt5cDqI+1t58IFSjn8npqkg5jIOs8TQbjKABbFW1gQ==} + '@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.11': - resolution: {integrity: sha512-iDapw7nFSE5tsmQxcH2+KotJhyfVPmHd6vUOQN/Em+8WNbkZJwJ/rDowlJMFnMl0SYSDSfgKCNUGi6Sn5++Smg==} + '@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.11': - resolution: {integrity: sha512-26q26AyITo3CGZX4HWFIMgBMA1lu3YN07gxzBm4tW6Hglq/OVyTP50kDrmWuKybCX6dh3m8Ly+c7YhI+sqCgCQ==} + '@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.11': - resolution: {integrity: sha512-cUO96IdsfvIj2S+2PNcDSkTcLhjPj8pIOEWRV1Kj70MOP4wnE9zSk7N4C1EKHlFGRcgdR5AkbnBZj0dfHK9nbQ==} + '@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.11': - resolution: {integrity: sha512-DFYp/ZQRdKunI/LPc0UkRXG1vlNjJKOJf8O8Ctb8+wm2Nt2ELg9Eamc31D2VoakkfGo8L+z0Iejz6WtFbQwyGw==} + '@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.11': - resolution: {integrity: sha512-vt5tog1cr+QKadIddGe+3LrcX0EmjEd0izF717Uwf92mBmgYfq2XRmL2r4bZU40spB69exxhrBn9I91HYEr/7w==} + '@napi-rs/canvas@1.0.12': + resolution: {integrity: sha512-PSwhRdvizfuiSDlhE4Ld2jl/JqkrbSFFyZO0ycamE6mBpf1H0uUF3Tm8yQaVDeAwCNNDzxjRfrN22KmWLTDwcw==} engines: {node: '>= 10'} '@pkgr/core@0.3.6': @@ -2551,52 +2551,52 @@ snapshots: dependencies: '@chevrotain/types': 13.2.0 - '@napi-rs/canvas-android-arm64@1.0.11': + '@napi-rs/canvas-android-arm64@1.0.12': optional: true - '@napi-rs/canvas-darwin-arm64@1.0.11': + '@napi-rs/canvas-darwin-arm64@1.0.12': optional: true - '@napi-rs/canvas-darwin-x64@1.0.11': + '@napi-rs/canvas-darwin-x64@1.0.12': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@1.0.11': + '@napi-rs/canvas-linux-arm-gnueabihf@1.0.12': optional: true - '@napi-rs/canvas-linux-arm64-gnu@1.0.11': + '@napi-rs/canvas-linux-arm64-gnu@1.0.12': optional: true - '@napi-rs/canvas-linux-arm64-musl@1.0.11': + '@napi-rs/canvas-linux-arm64-musl@1.0.12': optional: true - '@napi-rs/canvas-linux-riscv64-gnu@1.0.11': + '@napi-rs/canvas-linux-riscv64-gnu@1.0.12': optional: true - '@napi-rs/canvas-linux-x64-gnu@1.0.11': + '@napi-rs/canvas-linux-x64-gnu@1.0.12': optional: true - '@napi-rs/canvas-linux-x64-musl@1.0.11': + '@napi-rs/canvas-linux-x64-musl@1.0.12': optional: true - '@napi-rs/canvas-win32-arm64-msvc@1.0.11': + '@napi-rs/canvas-win32-arm64-msvc@1.0.12': optional: true - '@napi-rs/canvas-win32-x64-msvc@1.0.11': + '@napi-rs/canvas-win32-x64-msvc@1.0.12': optional: true - '@napi-rs/canvas@1.0.11': + '@napi-rs/canvas@1.0.12': optionalDependencies: - '@napi-rs/canvas-android-arm64': 1.0.11 - '@napi-rs/canvas-darwin-arm64': 1.0.11 - '@napi-rs/canvas-darwin-x64': 1.0.11 - '@napi-rs/canvas-linux-arm-gnueabihf': 1.0.11 - '@napi-rs/canvas-linux-arm64-gnu': 1.0.11 - '@napi-rs/canvas-linux-arm64-musl': 1.0.11 - '@napi-rs/canvas-linux-riscv64-gnu': 1.0.11 - '@napi-rs/canvas-linux-x64-gnu': 1.0.11 - '@napi-rs/canvas-linux-x64-musl': 1.0.11 - '@napi-rs/canvas-win32-arm64-msvc': 1.0.11 - '@napi-rs/canvas-win32-x64-msvc': 1.0.11 + '@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': {} @@ -4038,7 +4038,7 @@ snapshots: pdfjs-dist@6.4.299: optionalDependencies: - '@napi-rs/canvas': 1.0.11 + '@napi-rs/canvas': 1.0.12 picocolors@1.1.1: {}