From 5417526573c346fe8aa34b21b2e657a63e4b6fb9 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 11:36:36 +0800 Subject: [PATCH 01/17] feat(muya): expose desktop-required public API accessors (#4381) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): export html/image/sanitize/wordcount utilities from public API Desktop consumes escapeHTML, unescapeHTML, getImageInfo, wordCount, sanitize and generateGithubSlug from the legacy muyajs `muya/lib/utils` tree. The implementations already exist in @muyajs/core's src/utils but were not re-exported from the public entrypoint. Surface them so the desktop migration can drop the muyajs util imports. Part of the muyajs -> @muyajs/core engine migration (stage A10). Co-Authored-By: Claude Opus 4.8 (1M context) * feat(muya): expose clipboard copy/paste on public API Surface copyAsMarkdown / copyAsHtml / pasteAsPlainText on the Muya class, delegating to the internal Clipboard module. The desktop Edit menu drives "Copy as Markdown/HTML" and "Paste as Plain Text" through these, which had no public entrypoint on @muyajs/core. Part of the muyajs -> @muyajs/core engine migration (stage A6). Co-Authored-By: Claude Opus 4.8 (1M context) * feat(muya): expose selection/focus accessors on public API Surface getSelection, hasFocus, blur(unfocus, clearSelection) and hideAllFloatTools on the Muya class. The desktop editor relies on these for the typewriter/selection sync, editor-blur/focus bus actions, and hiding floating tools before exports — all of which previously lived only on the internal selection / ui modules. Part of the muyajs -> @muyajs/core engine migration (stage A3, partial; setCursor lands with the content-loading work). Co-Authored-By: Claude Opus 4.8 (1M context) * feat(muya): expose inline format(type) on public API Surface format(type) on the Muya class so the desktop Format menu (strong/em/u/del/inline_code/link/image/inline_math/sub/sup/mark/clear) can drive inline formatting programmatically. Mirrors the inline format toolbar: narrows the active block to a Format, restores the selection, then toggles the format — previously only reachable through toolbar/ shortcut interaction. Part of the muyajs -> @muyajs/core engine migration (stage A2). Co-Authored-By: Claude Opus 4.8 (1M context) * fix(muya): align blur() with muyajs — clear image selection and hide floats Address Copilot review on #4381. The previous blur() cleared the DOM selection but left editor.selection.selectedImage set and never hid the image transformer/toolbar, so a previously selected inline image could stay visually selected with its resize bar lingering after blur. Match marktext muyajs blur(isRemoveAllRange, unSelect): always hide all float tools and blur the contenteditable node; on unSelect clear the selected inline image (muya has no selectedTableCells in its model, so only the image is cleared). The desktop editor-blur path calls blur(false, true), so the lingering image selection/resize bar is fixed. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- packages/muya/src/index.ts | 4 ++ packages/muya/src/muya.ts | 104 +++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/packages/muya/src/index.ts b/packages/muya/src/index.ts index d3e0c3b329..28425ac404 100644 --- a/packages/muya/src/index.ts +++ b/packages/muya/src/index.ts @@ -25,3 +25,7 @@ export { PreviewToolBar } from './ui/previewToolBar'; export { TableColumnToolbar } from './ui/tableColumnToolbar'; export { TableDragBar } from './ui/tableDragBar'; export { TableRowColumMenu } from './ui/tableRowColumMenu'; +export type { IImageInfo } from './utils/image'; +export { getImageInfo } from './utils/image'; +export { escapeHTML, sanitize, unescapeHTML, wordCount } from './utils/index'; +export { generateGithubSlug } from './utils/slug'; diff --git a/packages/muya/src/muya.ts b/packages/muya/src/muya.ts index ead6551e44..ef68ca85fb 100644 --- a/packages/muya/src/muya.ts +++ b/packages/muya/src/muya.ts @@ -3,6 +3,7 @@ import type { ILocale } from './i18n/types'; import type { ITocItem } from './state/getTOC'; import type { TState } from './state/types'; import type { IMuyaOptions } from './types'; +import Format from './block/base/format'; import { CLASS_NAMES, MUYA_DEFAULT_OPTIONS, @@ -174,6 +175,109 @@ export class Muya { this.editor.selection.selectAll(); } + /** + * Toggle an inline format on the current selection. + * @param type One of strong/em/u/del/inline_code/link/image/inline_math/ + * sub/sup/mark/clear (and html_tag aliases). No-op when the selection is + * not inside a single formattable block. + */ + format(type: string) { + const { selection } = this.editor; + const sel = selection.getSelection(); + if (!sel) + return; + + const { + anchor, + focus, + anchorBlock, + anchorPath, + focusBlock, + focusPath, + isSelectionInSameBlock, + } = sel; + + if (!isSelectionInSameBlock || !(anchorBlock instanceof Format)) + return; + + // Restore the selection before applying the format, mirroring the + // inline format toolbar — the menu/IPC round-trip can drop the live + // DOM selection. + selection.setSelection({ + anchor, + focus, + anchorBlock, + anchorPath, + focusBlock, + focusPath, + }); + + anchorBlock.format(type); + } + + /** + * Return the current selection, or null when the editor has no selection. + */ + getSelection() { + return this.editor.selection.getSelection(); + } + + /** + * Whether the editor (or one of its descendants) currently holds focus. + */ + hasFocus() { + const { activeElement } = document; + + return this.domNode === activeElement || this.domNode.contains(activeElement); + } + + /** + * Blur the editor (mirrors marktext muyajs `blur`). Always hides every + * floating tool and blurs the contenteditable node. + * @param isRemoveAllRange Remove all native selection ranges. + * @param unSelect Clear the selected inline image so its toolbar/resize + * bar do not linger after the editor is blurred. + */ + blur(isRemoveAllRange = false, unSelect = false) { + if (isRemoveAllRange) + document.getSelection()?.removeAllRanges(); + + if (unSelect) + this.editor.selection.selectedImage = null; + + this.editor.activeContentBlock = null; + this.ui.hideAllFloatTools(); + this.domNode.blur(); + } + + /** + * Hide every floating tool/menu (toolbars, pickers, front button, …). + */ + hideAllFloatTools() { + this.ui.hideAllFloatTools(); + } + + /** + * Copy the current document as Markdown to the clipboard. + */ + copyAsMarkdown() { + this.editor.clipboard.copyAsMarkdown(); + } + + /** + * Copy the current selection as rendered HTML to the clipboard. + */ + copyAsHtml() { + this.editor.clipboard.copyAsHtml(); + } + + /** + * Paste the clipboard content as plain text at the current cursor. + */ + pasteAsPlainText() { + this.editor.clipboard.pasteAsPlainText(); + } + destroy() { this.eventCenter.detachAllDomEvents(); this.eventCenter.unsubscribeAll(); From 3a4e76434a6c81f0a8e8b7cc7a81e97d161db68d Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 12:17:22 +0800 Subject: [PATCH 02/17] feat(muya): expose themeable CSS variables and diagram theming (#4382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): expose themeable CSS variables Introduce CSS custom properties so desktop's 32 themes can later drive muya's editor chrome (part of the muyajs -> @muyajs/core migration). Each new variable defaults to muya's current hardcoded / opacity-tier value via `var(--name, )`, so the bundled light theme renders identically. Added in index.css `:root`: - --editor-area-width (800px) for `.mu-container` max-width - --link-color (rgb(20 86 240)) - --h1-color .. --h6-color (var(--editor-color-80)) - --blockquote-text-color (var(--editor-color-50)) - --blockquote-border-color (var(--editor-color-30)) - --hr-color (var(--editor-color-10)) - --strong-color / --em-color / --list-marker-color (inherit) - --button-bg-color-active / --button-border-active / --button-border-focus Usage sites updated in blockSyntax.css (container width, per-heading colors, blockquote text + border, thematic break, list markers) and inlineSyntax.css (link color, strong + em color). Co-Authored-By: Claude Opus 4.8 (1M context) * feat(muya): theme mermaid/vega diagram SVGs Port marktext's legacy diagram-recoloring rules (packages/muyajs/themes/ default.css `figure[data-role] svg …`) into muya's blockSyntax.css, adapted to muya's live DOM (`figure.mu-diagram-block .mu-diagram-preview svg`) and kebab-cased vars (`--editor-bg-color`, `--editor-color`). Mermaid and vega-lite render an inline whose default black-on-white fills/strokes are rewritten to the editor's theme colors, so diagrams follow the active theme (including dark themes that override those vars). Additive and entirely theme-driven; the bundled light theme is unchanged because `--editor-bg-color` is white and `--editor-color` is the editor text color, matching the diagrams' original look. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(muya): correct themeable-vars comment about desktop overrides Address Copilot review on #4382. The comment implied the desktop themes already use kebab-case overrides, but they currently use camelCase (--linkColor, --h1Color, …). Clarify that consumers override per theme and that the desktop themes will be migrated to these kebab-case names as part of the muyajs -> @muyajs/core move. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../muya/src/assets/styles/blockSyntax.css | 48 +++++++++++++++++-- packages/muya/src/assets/styles/index.css | 28 +++++++++++ .../muya/src/assets/styles/inlineSyntax.css | 12 ++++- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/packages/muya/src/assets/styles/blockSyntax.css b/packages/muya/src/assets/styles/blockSyntax.css index a3915a194c..683221179d 100644 --- a/packages/muya/src/assets/styles/blockSyntax.css +++ b/packages/muya/src/assets/styles/blockSyntax.css @@ -4,7 +4,7 @@ .mu-container { box-sizing: border-box; - max-width: 800px; + max-width: var(--editor-area-width, 800px); min-height: 100%; margin: 0 auto; padding: 0 50px 100px; @@ -43,26 +43,32 @@ } .mu-container h1 { + color: var(--h1-color, var(--editor-color-80)); font-size: 1.875em; } .mu-container h2 { + color: var(--h2-color, var(--editor-color-80)); font-size: 1.5em; } .mu-container h3 { + color: var(--h3-color, var(--editor-color-80)); font-size: 1.375em; } .mu-container h4 { + color: var(--h4-color, var(--editor-color-80)); font-size: 1.25em; } .mu-container h5 { + color: var(--h5-color, var(--editor-color-80)); font-size: 1.125em; } .mu-container h6 { + color: var(--h6-color, var(--editor-color-80)); font-size: 1em; } @@ -74,7 +80,7 @@ margin-left: 0; padding: 0 1em; - color: var(--editor-color-50); + color: var(--blockquote-text-color, var(--editor-color-50)); font-size: 1em; } @@ -88,7 +94,7 @@ width: 3px; height: calc(100% - 1em); - background: var(--editor-color-30); + background: var(--blockquote-border-color, var(--editor-color-30)); content: ''; } @@ -111,7 +117,7 @@ width: 100%; height: 1px; - background: var(--editor-color-10); + background: var(--hr-color, var(--editor-color-10)); content: ''; } @@ -311,6 +317,13 @@ ul.mu-bullet-list { list-style: disc outside none; } +/* list markers — default to `inherit` (current text color) so the bundled + light theme is unchanged; themes can recolor the bullets / numbers. */ +ol.mu-order-list > li::marker, +ul.mu-bullet-list > li::marker { + color: var(--list-marker-color, inherit); +} + ul.mu-bullet-list:first-child, ol.mu-order-list:first-child { margin-top: 0; @@ -580,6 +593,33 @@ figure.mu-math-block div.mu-math-preview > svg { width: 100%; } +/* Diagram SVG recoloring — port of marktext's legacy theme block + (packages/muyajs/themes/default.css `figure[data-role] svg …`, ~lines + 689-720) adapted to muya's DOM and kebab-cased vars. Mermaid and + vega-lite render an inline inside `.mu-diagram-preview`; these + rules rewrite the renderer's default black-on-white fills/strokes to the + editor's theme colors so diagrams follow the active theme (including + dark themes that override `--editor-bg-color` / `--editor-color`). + Additive and entirely theme-driven — on the bundled light theme + `--editor-bg-color` is white and `--editor-color` is the editor text + color, matching the diagrams' original look. */ +figure.mu-diagram-block .mu-diagram-preview svg rect[fill='#ffffff'], +figure.mu-diagram-block .mu-diagram-preview svg path[fill='#ffffff'] { + fill: var(--editor-bg-color); +} + +figure.mu-diagram-block .mu-diagram-preview svg text[fill='#000000'], +figure.mu-diagram-block .mu-diagram-preview svg path[fill='#000000'], +figure.mu-diagram-block .mu-diagram-preview svg use[fill='black'], +figure.mu-diagram-block .mu-diagram-preview svg use[fill='#000000'] { + fill: var(--editor-color); +} + +figure.mu-diagram-block .mu-diagram-preview svg rect[stroke='#000000'], +figure.mu-diagram-block .mu-diagram-preview svg path[stroke='#000000'] { + stroke: var(--editor-color); +} + figure.mu-active.mu-math-block > div.mu-math-preview, figure.mu-active.mu-diagram-block > div.mu-diagram-preview { position: absolute; diff --git a/packages/muya/src/assets/styles/index.css b/packages/muya/src/assets/styles/index.css index 9f52537046..643b134299 100644 --- a/packages/muya/src/assets/styles/index.css +++ b/packages/muya/src/assets/styles/index.css @@ -10,6 +10,9 @@ --editor-color-10: rgb(0 0 0 / 10%); --editor-color-04: rgb(0 0 0 / 3%); --editor-bg-color: rgb(255 255 255 / 100%); + + /* width of the centered editing column (`.mu-container` max-width) */ + --editor-area-width: 800px; --delete-color: #ff6969; --icon-color: #6b737b; --code-block-bg-color: rgb(0 0 0 / 3%); @@ -20,10 +23,35 @@ --button-border: 1px solid #dcdfe6; --button-bg-color-hover: linear-gradient(#f9f9f9, #f2f2f2); --button-border-hover: var(--button-border); + + /* active / focus button variants — default to the resting button look so + the bundled light theme is unchanged; desktop themes can override. */ + --button-bg-color-active: var(--button-bg-color); + --button-border-active: var(--button-border); + --button-border-focus: var(--button-border); --float-bg-color: #fff; --float-hover-color: rgb(0 0 0 / 4%); --float-border-color: rgb(0 0 0 / 10%); --float-shadow: rgb(15 15 15 / 3%) 0 0 0 1px, rgb(15 15 15 / 4%) 0 3px 6px, rgb(15 15 15 / 5%) 0 9px 24px; + + /* markdown element colors — defaults mirror the current hardcoded / + opacity-tier values so the bundled light theme renders identically. + Consumers override them per theme. The desktop themes currently use + camelCase names (e.g. --linkColor, --h1Color); they will be migrated + to these kebab-case names as part of the muyajs -> @muyajs/core move. */ + --link-color: rgb(20 86 240); + --h1-color: var(--editor-color-80); + --h2-color: var(--editor-color-80); + --h3-color: var(--editor-color-80); + --h4-color: var(--editor-color-80); + --h5-color: var(--editor-color-80); + --h6-color: var(--editor-color-80); + --blockquote-text-color: var(--editor-color-50); + --blockquote-border-color: var(--editor-color-30); + --strong-color: inherit; + --em-color: inherit; + --list-marker-color: inherit; + --hr-color: var(--editor-color-10); } /* diff --git a/packages/muya/src/assets/styles/inlineSyntax.css b/packages/muya/src/assets/styles/inlineSyntax.css index f38c71fbe9..443a5417ce 100644 --- a/packages/muya/src/assets/styles/inlineSyntax.css +++ b/packages/muya/src/assets/styles/inlineSyntax.css @@ -17,7 +17,7 @@ /* link and auto-link and reference link */ a.mu-inline-rule, span.mu-inline-rule.mu-link { - color: rgb(20 86 240); + color: var(--link-color, rgb(20 86 240)); } /* Note: previously this rule also set `pointer-events: none` to suppress @@ -31,6 +31,16 @@ span.mu-inline-rule.mu-link { color: var(--editor-color-50); } +/* strong and emphasis — default to `inherit` (the surrounding editor color) + so the bundled light theme is unchanged; themes can recolor them. */ +strong.mu-inline-rule { + color: var(--strong-color, inherit); +} + +em.mu-inline-rule { + color: var(--em-color, inherit); +} + /* inline code */ code.mu-inline-rule { margin: 0; From 4bb1c8e2852c5a124ea8a151ae7f362a8b4a6dfd Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 12:18:43 +0800 Subject: [PATCH 03/17] feat(muya): add block duplicate/insert/delete public API (#4384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): add block duplicate/insert/delete public API Expose duplicate(), insertParagraph(location, text) and deleteParagraph() on the Muya class so the desktop Edit/Paragraph menu can manipulate block structure programmatically — previously only reachable through the paragraph front-menu UI. They resolve the target via the active content block's outMostBlock (which survives the menu/IPC round-trip) and reuse the same block primitives as the front menu (ScrollPage.loadBlock().create, insertAfter/insertBefore, remove). Adds happy-dom unit coverage. Part of the muyajs -> @muyajs/core engine migration (stage A1, partial; updateParagraph/createTable/insertImage land next). Co-Authored-By: Claude Opus 4.8 (1M context) * test(muya): preserve/restore window.MUYA_VERSION in block-editing spec Address Copilot review on #4384: mirror the preserve-and-restore pattern used by the other happy-dom specs (getTOC.spec.ts, quick-insert hint.spec.ts) instead of unconditionally deleting window.MUYA_VERSION in afterEach, so the spec does not leak global state across test files. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../muya/src/__tests__/blockEditing.spec.ts | 115 ++++++++++++++++++ packages/muya/src/muya.ts | 81 ++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 packages/muya/src/__tests__/blockEditing.spec.ts diff --git a/packages/muya/src/__tests__/blockEditing.spec.ts b/packages/muya/src/__tests__/blockEditing.spec.ts new file mode 100644 index 0000000000..a3149af12d --- /dev/null +++ b/packages/muya/src/__tests__/blockEditing.spec.ts @@ -0,0 +1,115 @@ +// @vitest-environment happy-dom + +import type Content from '../block/base/content'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Muya } from '../muya'; + +// Coverage for the programmatic block-editing API added for the +// muyajs -> @muyajs/core desktop migration: duplicate / insertParagraph / +// deleteParagraph. These drive the desktop Edit/Paragraph menu actions, +// which previously had no public entrypoint on @muyajs/core. +// +// Block-tree mutations dispatch json1 ops that flush to the document state on +// the next animation frame (see JSONState._emitStateChange), so assertions on +// getState() are wrapped in vi.waitFor to await that flush. + +const bootedHosts: HTMLElement[] = []; +let originalVersion: string | undefined; +let hadVersion = false; + +beforeEach(() => { + hadVersion = 'MUYA_VERSION' in window; + originalVersion = window.MUYA_VERSION; + window.MUYA_VERSION = 'test'; +}); + +afterEach(() => { + while (bootedHosts.length) { + const host = bootedHosts.pop()!; + host.remove(); + } + if (hadVersion) + window.MUYA_VERSION = originalVersion as string; + else + delete (window as Partial).MUYA_VERSION; +}); + +function bootMuya(markdown: string): Muya { + const host = document.createElement('div'); + document.body.appendChild(host); + const muya = new Muya(host, { markdown } as ConstructorParameters[1]); + muya.init(); + bootedHosts.push(muya.domNode); + return muya; +} + +// Block-level ops resolve their target via the active content block's +// outMostBlock — the way the editor tracks the cursor after a click. Set it +// directly to simulate the cursor sitting in the first block. +function placeCursorOnFirstBlock(muya: Muya): Content { + const first = muya.editor.scrollPage!.firstContentInDescendant()!; + muya.editor.activeContentBlock = first; + return first; +} + +describe('muya block editing api', () => { + it('duplicate() copies the current block in place', async () => { + const muya = bootMuya('# Title\n\nbody\n'); + placeCursorOnFirstBlock(muya); + muya.duplicate(); + await vi.waitFor(() => { + const after = muya.getState(); + expect(after.length).toBe(3); + expect(after[0].name).toBe('atx-heading'); + expect(after[1].name).toBe('atx-heading'); + expect(after[2].name).toBe('paragraph'); + }); + }); + + it('insertParagraph() inserts an empty paragraph after by default', async () => { + const muya = bootMuya('# Title\n'); + placeCursorOnFirstBlock(muya); + muya.insertParagraph(); + await vi.waitFor(() => { + const after = muya.getState(); + expect(after.length).toBe(2); + expect(after[0].name).toBe('atx-heading'); + expect(after[1].name).toBe('paragraph'); + }); + }); + + it('insertParagraph("before", text) inserts before with the given text', async () => { + const muya = bootMuya('# Title\n'); + placeCursorOnFirstBlock(muya); + muya.insertParagraph('before', 'intro'); + await vi.waitFor(() => { + const after = muya.getState(); + expect(after.length).toBe(2); + expect(after[0].name).toBe('paragraph'); + expect(after[1].name).toBe('atx-heading'); + }); + expect(muya.getMarkdown()).toContain('intro'); + }); + + it('deleteParagraph() removes the current block and keeps the rest', async () => { + const muya = bootMuya('# Title\n\nbody\n'); + placeCursorOnFirstBlock(muya); + muya.deleteParagraph(); + await vi.waitFor(() => { + const after = muya.getState(); + expect(after.length).toBe(1); + expect(after[0].name).toBe('paragraph'); + }); + }); + + it('deleteParagraph() on the only block leaves a single empty paragraph', async () => { + const muya = bootMuya('# Title\n'); + placeCursorOnFirstBlock(muya); + muya.deleteParagraph(); + await vi.waitFor(() => { + const after = muya.getState(); + expect(after.length).toBe(1); + expect(after[0].name).toBe('paragraph'); + }); + }); +}); diff --git a/packages/muya/src/muya.ts b/packages/muya/src/muya.ts index ef68ca85fb..3801bfa8b0 100644 --- a/packages/muya/src/muya.ts +++ b/packages/muya/src/muya.ts @@ -1,9 +1,13 @@ +import type Content from './block/base/content'; +import type Parent from './block/base/parent'; import type { Listener } from './event/types'; import type { ILocale } from './i18n/types'; import type { ITocItem } from './state/getTOC'; import type { TState } from './state/types'; import type { IMuyaOptions } from './types'; import Format from './block/base/format'; +import { ScrollPage } from './block/scrollPage'; +import emptyStates from './config/emptyStates'; import { CLASS_NAMES, MUYA_DEFAULT_OPTIONS, @@ -14,6 +18,7 @@ import EventCenter from './event/index'; import I18n from './i18n/index'; import { getTOC } from './state/getTOC'; import { Ui } from './ui/ui'; +import { deepClone } from './utils'; import './assets/styles/blockSyntax.css'; import './assets/styles/index.css'; import './assets/styles/inlineSyntax.css'; @@ -278,6 +283,82 @@ export class Muya { this.editor.clipboard.pasteAsPlainText(); } + /** + * The outer-most block at the current cursor — the target for block-level + * operations. Uses the persisted active content block (which survives the + * menu/IPC round-trip), falling back to the selection anchor. + */ + private _outmostBlockAtCursor(): Parent | null { + const content = this.editor.activeContentBlock ?? this.editor.selection.anchorBlock; + + return content?.outMostBlock ?? null; + } + + /** + * Duplicate the block at the current cursor, placing the cursor in the + * copy. No-op when there is no current block. + */ + duplicate() { + const block = this._outmostBlockAtCursor(); + if (!block) + return; + + const state = deepClone(block.getState()); + const dupBlock = ScrollPage.loadBlock(state.name).create(this, state); + block.parent!.insertAfter(dupBlock, block); + dupBlock.lastContentInDescendant()?.setCursor(0, 0, true); + } + + /** + * Insert an empty paragraph relative to the block at the current cursor. + * @param location Insert `before` or `after` the current block (default `after`). + * @param text Initial text of the new paragraph. + */ + insertParagraph(location: 'before' | 'after' = 'after', text = '') { + const block = this._outmostBlockAtCursor(); + if (!block) + return; + + const state = deepClone(emptyStates.paragraph); + state.text = text; + const newBlock = ScrollPage.loadBlock('paragraph').create(this, state); + if (location === 'before') + block.parent!.insertBefore(newBlock, block); + else + block.parent!.insertAfter(newBlock, block); + + newBlock.lastContentInDescendant()?.setCursor(0, 0, true); + } + + /** + * Delete the block at the current cursor, moving the cursor to an adjacent + * block, or to a fresh empty paragraph when it was the only block. + */ + deleteParagraph() { + const block = this._outmostBlockAtCursor(); + if (!block) + return; + + let cursorBlock: Content | null = null; + if (block.prev) { + cursorBlock = block.prev.lastContentInDescendant(); + } + else if (block.next) { + cursorBlock = block.next.firstContentInDescendant(); + } + else { + const newBlock = ScrollPage.loadBlock('paragraph').create( + this, + deepClone(emptyStates.paragraph), + ); + block.parent!.insertAfter(newBlock, block); + cursorBlock = newBlock.lastContentInDescendant(); + } + + block.remove(); + cursorBlock?.setCursor(0, 0, true); + } + destroy() { this.eventCenter.detachAllDomEvents(); this.eventCenter.unsubscribeAll(); From 36700095ad3045a022e6024268dac5b3ffc46194 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 12:20:04 +0800 Subject: [PATCH 04/17] feat(muya): restore flowchart and sequence diagrams (#4385) * feat(muya): render flowchart and sequence diagrams Restore the flowchart and sequence diagram code blocks that the legacy muyajs engine supported but the TypeScript rewrite dropped, for feature parity ahead of the desktop migration. - Add flowchart.js (the package legacy used) plus the snap.svg / underscore / webfontloader runtime deps for sequence diagrams. - Vendor the js-sequence-diagrams library (bramp, BSD) under src/utils/diagram/sequence/. The js-sequence-diagrams npm package is a dead security-holder placeholder, so legacy vendored the source; we do the same. Dropped its dead jison UMD `exports` fallback (the file is ESM) to avoid a Rollup COMMONJS_VARIABLE_IN_ESM warning. - Wire flowchart + sequence into loadRenderer, the live diagram preview (diagramPreview), and the HTML export path (markdownToHtml). Both use the parse(code).drawSVG(target, options) API, matching legacy. - Parse ```flowchart``` / ```sequence``` fences into diagram blocks (markdownToState, IDiagramMeta, getHighlightHtml diagram-type list). - Add the sequenceTheme option ('hand' | 'simple', default 'hand') to IMuyaOptions and MUYA_DEFAULT_OPTIONS, and honor it when rendering sequence diagrams. - Ambient module declarations for flowchart.js, snapsvg-cjs and the vendored sequence module; ignore the vendored third-party files in eslint (kept verbatim). Co-Authored-By: Claude Opus 4.8 (1M context) * feat(muya): add flowchart and sequence insert menu entries - Add Flowchart and Sequence entries to the quick-insert diagram section and wire the insert handler to create diagram blocks of those types. - Add flowchart + sequence icons to the paragraph front-button diagram icon map. Reuse the existing flowchart icon (already present in muya assets) and port the sequence icon from the legacy muyajs pngicon set. - Center exported flowchart / sequence diagram divs (exportStyle.css), matching plantuml / mermaid / vega-lite. Co-Authored-By: Claude Opus 4.8 (1M context) * test(muya): cover flowchart and sequence diagram parity - diagramFlowchartSequence.spec.ts: ```flowchart``` and ```sequence``` fences parse into diagram blocks of the right type and round-trip back to their fence via stateToMarkdown; mermaid/plantuml/vega-lite still parse (no regression); sequenceTheme defaults to 'hand'. - diagramMenuEntries.spec.ts: the quick-insert diagram section exposes flowchart + sequence entries (with icons) alongside the existing three. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- packages/muya/eslint.config.mjs | 5 + packages/muya/package.json | 8 +- packages/muya/src/assets/icons/sequence/1.png | Bin 0 -> 376 bytes packages/muya/src/assets/icons/sequence/2.png | Bin 0 -> 790 bytes packages/muya/src/assets/icons/sequence/3.png | Bin 0 -> 1208 bytes .../muya/src/assets/styles/exportStyle.css | 4 +- .../src/block/extra/diagram/diagramPreview.ts | 13 +- packages/muya/src/config/index.ts | 1 + .../diagramFlowchartSequence.spec.ts | 109 + packages/muya/src/state/markdownToHtml.ts | 19 +- packages/muya/src/state/markdownToState.ts | 4 +- packages/muya/src/state/types.ts | 2 +- packages/muya/src/types.ts | 1 + packages/muya/src/types/index.d.ts | 31 + .../src/ui/paragraphFrontButton/config.ts | 4 + .../__tests__/diagramMenuEntries.spec.ts | 32 + .../src/ui/paragraphQuickInsertMenu/config.ts | 28 +- packages/muya/src/utils/diagram/index.ts | 12 +- .../src/utils/diagram/sequence/danielbd.woff | Bin 0 -> 38300 bytes .../src/utils/diagram/sequence/danielbd.woff2 | Bin 0 -> 33744 bytes .../muya/src/utils/diagram/sequence/index.ts | 9 + .../diagram/sequence/sequence-diagram-snap.js | 1871 +++++++++++++++++ .../diagram/sequence/sequence-diagram.css | 12 + .../muya/src/utils/marked/getHighlightHtml.ts | 2 + pnpm-lock.yaml | 33 +- 25 files changed, 2184 insertions(+), 16 deletions(-) create mode 100755 packages/muya/src/assets/icons/sequence/1.png create mode 100755 packages/muya/src/assets/icons/sequence/2.png create mode 100755 packages/muya/src/assets/icons/sequence/3.png create mode 100644 packages/muya/src/state/__tests__/diagramFlowchartSequence.spec.ts create mode 100644 packages/muya/src/ui/paragraphQuickInsertMenu/__tests__/diagramMenuEntries.spec.ts create mode 100755 packages/muya/src/utils/diagram/sequence/danielbd.woff create mode 100755 packages/muya/src/utils/diagram/sequence/danielbd.woff2 create mode 100644 packages/muya/src/utils/diagram/sequence/index.ts create mode 100644 packages/muya/src/utils/diagram/sequence/sequence-diagram-snap.js create mode 100755 packages/muya/src/utils/diagram/sequence/sequence-diagram.css diff --git a/packages/muya/eslint.config.mjs b/packages/muya/eslint.config.mjs index 1a0a277015..aaad306f4c 100644 --- a/packages/muya/eslint.config.mjs +++ b/packages/muya/eslint.config.mjs @@ -99,6 +99,11 @@ export default antfu( 'e2e/**', 'lib/**', 'docs/**', + // Vendored third-party library (js-sequence-diagrams, bramp, BSD) + // wired to snap.svg. Kept verbatim for feature parity with the + // legacy muyajs engine; not subject to our lint/format rules. + 'src/utils/diagram/sequence/sequence-diagram-snap.js', + 'src/utils/diagram/sequence/sequence-diagram.css', ], }, { diff --git a/packages/muya/package.json b/packages/muya/package.json index 2042267300..c219e77454 100644 --- a/packages/muya/package.json +++ b/packages/muya/package.json @@ -62,6 +62,7 @@ "dompurify": "^3.4.5", "execall": "^3.0.0", "fast-diff": "^1.3.0", + "flowchart.js": "^1.18.0", "fuse.js": "^7.3.0", "html-tags": "^5.1.0", "joplin-turndown-plugin-gfm": "^1.0.12", @@ -76,10 +77,13 @@ "rxjs": "^7.8.2", "snabbdom": "^3.6.3", "snabbdom-to-html": "^7.1.0", + "snapsvg-cjs": "^0.0.6", "turndown": "^7.2.4", + "underscore": "^1.13.8", "vega": "^6.2.0", "vega-embed": "^7.1.0", - "vega-lite": "^6.4.3" + "vega-lite": "^6.4.3", + "webfontloader": "^1.6.28" }, "devDependencies": { "@antfu/eslint-config": "^9.0.0", @@ -88,6 +92,8 @@ "@types/plantuml-encoder": "^1.4.2", "@types/prismjs": "^1.26.6", "@types/turndown": "^5.0.4", + "@types/underscore": "^1.13.0", + "@types/webfontloader": "^1.6.38", "@typescript-eslint/parser": "^8.59.4", "@vitest/coverage-istanbul": "^4.1.6", "commonmark-spec": "^0.31.2", diff --git a/packages/muya/src/assets/icons/sequence/1.png b/packages/muya/src/assets/icons/sequence/1.png new file mode 100755 index 0000000000000000000000000000000000000000..4ebfd401225490a87acd236091fa455bc942df06 GIT binary patch literal 376 zcmV-;0f+vHP)Px$GD$>1R5%gER7*|*K@ja~I17Bj zSQ(_KXSybn24+Zwgh@K}dDYcbU9ZQv_~m)-vDCK8pc@zK%}>;O>a)lyp<}+WBuMv= zw0X5$Z(f=C5g-x0cdx^X_CJG@q5lKrt@~ z?qPI14Lo&jp{oyi+eZ>zkE!mE*;5^WUOzd-j@d!(>;r9X`R`MM;Df%`GPxBwx|^28 zx5lQ@V*sA$O#$fescn|S(Gz2t6U2=bZFJIF;n`?|#7Y}WwLvKY+F;TcPW0J$s8t-I zjbcr5kB3dPG4T)-!JVZ;K~)wWhB(bkDD(fGCuNZ}wb+JLPfva`^YlI}Ew>$bd3*;w WTc&DLB?(Ue0000Px%&`Cr=R9Fe^S3zqOF%(X+Q^BL&g!bUsAK+bj>3`@=T@c+O)Gft!rPx-%S`b$+ zRd4=>3Q7eJJ$UfuFR2bNR96YU7)?H_!d~{p%$ea%tGcob$cp)am(0_g20PlOH87 zQIYHRN{z$jFg$Z$mW-9)-B`Rc$9TF!vT4lgKBVt}Nzs0?eRp@54zbYfT`7y4y#N?s z4X**?pi4POx{O|j5GiAqZ64=rSG1;PFhJWf7i-IoiWW?=3$}7DrrP}nYX=yhZCMUW z$D)oW2dF;LM_jftDHDw1XWz`sDBQU6gmLzfB6CqIlIdWlv$Uon2U_#h8`#KCLWS@J zeOvzxlx$8O4n7UD16a)u$zpwcWf`h<`QGC3*7|c&wC4js1`MzU7m&rHGSuku5P~+A zD8fD?AcN?8-l!@MV(8f%zby{K|70b-Z+)T{z6TTsUkd%)rC9u`r=B? zkf+<51Lm!h)UrhYdQaFh59(=s}} zX*9li?!HYHyMp5~uiwoC+$14R>LV`0*6wCv^@AK`rqk`cK`ANo0(>+K{jGKL)8xZ8 zDNAT$Nxz-*zFG+5+-@c$i}mr9WoU>a^}CsXEJlX%e;Hc)*T7^kBH4eqp=zA%8F-M{ zyqL5xu=1t>e|7qZE9Zm1qW}N^ literal 0 HcmV?d00001 diff --git a/packages/muya/src/assets/icons/sequence/3.png b/packages/muya/src/assets/icons/sequence/3.png new file mode 100755 index 0000000000000000000000000000000000000000..2cbaeba3f00b9fa8baf1f0393e9c8c65d8e0990c GIT binary patch literal 1208 zcmV;p1V{UcP)Px(a!Eu%RA>e5TFq1N4JW|G-y#-uiZWhZam*Ka;v-n@j6 z{My4Kz!;;0cgiR6hwcAiD3lt{@p@X0;Y`ghd?lTDMZ_41SMcb*g+ACj3)AA{ay(;>yDfHmjlL;8@gS( zpc=ubK9x+C=q-<>q2j~|kCCVF%-;!Kyz<<6hKwU7b74um zgFSdkbOh*=xM%%kj9`%lyrBzqgoqg>xQ^pItJmuTqJI!{I)OG@+gxu3DH&LlLmfeR zlf(m`0F03@E}_GZ7N$N2^(EPM79UK$0b{GCN>LVd1m#T|wXX}K$8ooPYEjx42 zQ9KC1US112t<~oFG{_2hF438!r8 z;DFHpo_g>gptHP@2YFQ|CFox*})2$1|-PBNK07HhN8r!BSd%JU`uMBAbQ`4_ZkV@zMaIYm?6~GAH>3fJx%P6P#^ WrXx4If1WP@0000 & { type?: string; lang?: string }; + children?: IStateLike[]; +} + +function generate(markdown: string): IStateLike[] { + return new MarkdownToState({ + footnote: false, + math: false, + isGitlabCompatibilityEnabled: false, + trimUnnecessaryCodeBlockEmptyLines: false, + frontMatter: false, + }).generate(markdown) as unknown as IStateLike[]; +} + +function toMarkdown(states: IStateLike[]): string { + return new ExportMarkdown({ listIndentation: 1 }).generate( + states as unknown as Parameters[0], + ); +} + +// Parity restoration for flowchart + sequence diagrams. The legacy muyajs +// engine rendered ```flowchart``` (flowchart.js) and ```sequence``` +// (js-sequence-diagrams) fenced blocks as diagram blocks; the TS rewrite +// dropped them. These specs lock the parse + round-trip behaviour so the +// two diagram types stay first-class alongside mermaid / plantuml / +// vega-lite. +describe('diagram blocks — flowchart & sequence parity', () => { + it('parses a ```flowchart``` fence as a diagram block of type flowchart', () => { + const md = `\`\`\`flowchart +st=>start: Start +e=>end: End +st->e +\`\`\` +`; + const states = generate(md); + expect(states.length).toBe(1); + expect(states[0].name).toBe('diagram'); + expect(states[0].meta!.type).toBe('flowchart'); + // flowchart is not vega-lite, so the inner code lang stays yaml. + expect(states[0].meta!.lang).toBe('yaml'); + expect(states[0].text).toContain('st=>start: Start'); + }); + + it('parses a ```sequence``` fence as a diagram block of type sequence', () => { + const md = `\`\`\`sequence +Alice->Bob: Hello Bob +Bob-->Alice: Hi Alice +\`\`\` +`; + const states = generate(md); + expect(states.length).toBe(1); + expect(states[0].name).toBe('diagram'); + expect(states[0].meta!.type).toBe('sequence'); + expect(states[0].meta!.lang).toBe('yaml'); + expect(states[0].text).toContain('Alice->Bob: Hello Bob'); + }); + + it('round-trips a flowchart diagram block back to a ```flowchart``` fence', () => { + const md = `\`\`\`flowchart +st=>start: Start +e=>end: End +st->e +\`\`\` +`; + const out = toMarkdown(generate(md)); + expect(out).toContain('```flowchart'); + expect(out).toContain('st=>start: Start'); + expect(out).toContain('st->e'); + }); + + it('round-trips a sequence diagram block back to a ```sequence``` fence', () => { + const md = `\`\`\`sequence +Alice->Bob: Hello Bob +Bob-->Alice: Hi Alice +\`\`\` +`; + const out = toMarkdown(generate(md)); + expect(out).toContain('```sequence'); + expect(out).toContain('Alice->Bob: Hello Bob'); + expect(out).toContain('Bob-->Alice: Hi Alice'); + }); + + it('still parses mermaid / plantuml / vega-lite (no regression)', () => { + for (const type of ['mermaid', 'plantuml', 'vega-lite'] as const) { + const md = `\`\`\`${type}\nfoo\n\`\`\`\n`; + const states = generate(md); + expect(states[0].name).toBe('diagram'); + expect(states[0].meta!.type).toBe(type); + } + }); +}); + +describe('sequenceTheme option', () => { + it('defaults to `hand` in MUYA_DEFAULT_OPTIONS', () => { + expect(MUYA_DEFAULT_OPTIONS.sequenceTheme).toBe('hand'); + }); +}); diff --git a/packages/muya/src/state/markdownToHtml.ts b/packages/muya/src/state/markdownToHtml.ts index b80783222c..c4113dc8fb 100644 --- a/packages/muya/src/state/markdownToHtml.ts +++ b/packages/muya/src/state/markdownToHtml.ts @@ -48,7 +48,7 @@ export class MarkdownToHtml { async renderDiagram() { const selector - = 'code.language-vega-lite, code.language-plantuml'; + = 'code.language-vega-lite, code.language-plantuml, code.language-flowchart, code.language-sequence'; const codes = this._exportContainer!.querySelectorAll(selector); for (const code of codes) { @@ -56,6 +56,10 @@ export class MarkdownToHtml { const functionType = (() => { if (/plantuml/.test(code.className)) return 'plantuml'; + else if (/flowchart/.test(code.className)) + return 'flowchart'; + else if (/sequence/.test(code.className)) + return 'sequence'; else return 'vega-lite'; })(); @@ -75,6 +79,11 @@ export class MarkdownToHtml { theme: 'latimes', // only render light theme }); } + else if (functionType === 'sequence') { + Object.assign(options, { + theme: this.muya?.options.sequenceTheme ?? 'hand', + }); + } try { if (functionType === 'plantuml') { @@ -82,8 +91,14 @@ export class MarkdownToHtml { diagramContainer.innerHTML = ''; diagram.insertImgElement(diagramContainer); } - if (functionType === 'vega-lite') + else if (functionType === 'flowchart' || functionType === 'sequence') { + const diagram = render.parse(rawCode); + diagramContainer.innerHTML = ''; + diagram.drawSVG(diagramContainer, options); + } + else if (functionType === 'vega-lite') { await render(diagramContainer, JSON.parse(rawCode), options); + } } catch { diagramContainer.innerHTML = '< Invalid Diagram >'; diff --git a/packages/muya/src/state/markdownToState.ts b/packages/muya/src/state/markdownToState.ts index 3e4c07e8c3..080c6be930 100644 --- a/packages/muya/src/state/markdownToState.ts +++ b/packages/muya/src/state/markdownToState.ts @@ -159,9 +159,9 @@ export class MarkdownToState { value = value.replace(/\n+$/, '').replace(/^\n+/, ''); } - const diagramMatch = /^(mermaid|vega-lite|plantuml)$/.exec(lang); + const diagramMatch = /^(mermaid|vega-lite|plantuml|flowchart|sequence)$/.exec(lang); if (diagramMatch) { - const diagramType = diagramMatch[1] as 'mermaid' | 'vega-lite' | 'plantuml'; + const diagramType = diagramMatch[1] as 'mermaid' | 'vega-lite' | 'plantuml' | 'flowchart' | 'sequence'; state = { name: 'diagram' as const, text: value, diff --git a/packages/muya/src/state/types.ts b/packages/muya/src/state/types.ts index 0a7723cf67..9e7b41e9f2 100644 --- a/packages/muya/src/state/types.ts +++ b/packages/muya/src/state/types.ts @@ -145,7 +145,7 @@ export interface IFrontmatterState { export interface IDiagramMeta { lang: string; // 'yaml' | 'json'; - type: 'mermaid' | 'plantuml' | 'vega-lite'; + type: 'mermaid' | 'plantuml' | 'vega-lite' | 'flowchart' | 'sequence'; } export interface IDiagramState { diff --git a/packages/muya/src/types.ts b/packages/muya/src/types.ts index aa7368e3a2..fd3f628206 100644 --- a/packages/muya/src/types.ts +++ b/packages/muya/src/types.ts @@ -18,6 +18,7 @@ export interface IMuyaOptions { frontmatterType: string; // '-' | '+' | ';' | '{'; mermaidTheme: string; vegaTheme: string; + sequenceTheme: 'hand' | 'simple'; hideQuickInsertHint: boolean; hideLinkPopup: boolean; autoCheck: boolean; diff --git a/packages/muya/src/types/index.d.ts b/packages/muya/src/types/index.d.ts index d07b43e076..dbe45e3c4a 100644 --- a/packages/muya/src/types/index.d.ts +++ b/packages/muya/src/types/index.d.ts @@ -7,7 +7,38 @@ declare module '*.bmp'; declare module '*.tiff'; declare module '*.css'; declare module '*.css?inline'; +declare module '*.woff'; +declare module '*.woff2'; declare module 'joplin-turndown-plugin-gfm'; declare module 'prismjs/plugins/keep-markup/prism-keep-markup'; declare module 'prismjs/dependencies'; declare module '@marktext/file-icons'; +declare module 'snapsvg-cjs'; + +declare module 'flowchart.js' { + interface IFlowChartDrawOptions { + [key: string]: unknown; + } + interface IFlowChartInstance { + drawSVG: (container: HTMLElement | string, options?: IFlowChartDrawOptions) => void; + clean: () => void; + } + export function parse(input: string): IFlowChartInstance; + const flowchart: { parse: typeof parse }; + export default flowchart; +} + +declare module '*sequence-diagram-snap' { + interface ISequenceDrawOptions { + theme?: 'hand' | 'simple'; + [key: string]: unknown; + } + interface ISequenceDiagramInstance { + drawSVG: (container: HTMLElement | string, options?: ISequenceDrawOptions) => void; + } + interface ISequenceDiagramConstructor { + parse: (input: string) => ISequenceDiagramInstance; + } + const Diagram: ISequenceDiagramConstructor; + export default Diagram; +} diff --git a/packages/muya/src/ui/paragraphFrontButton/config.ts b/packages/muya/src/ui/paragraphFrontButton/config.ts index f8af8b57a6..7970c74199 100644 --- a/packages/muya/src/ui/paragraphFrontButton/config.ts +++ b/packages/muya/src/ui/paragraphFrontButton/config.ts @@ -5,6 +5,7 @@ import type DiagramBlock from '../../block/extra/diagram'; import bulletListIcon from '../../assets/icons/bullet_list/2.png'; import vegaIcon from '../../assets/icons/chart/2.png'; import codeIcon from '../../assets/icons/code/2.png'; +import flowchartIcon from '../../assets/icons/flowchart/2.png'; import footnoteIcon from '../../assets/icons/footnote/2.png'; import frontMatterIcon from '../../assets/icons/front_matter/2.png'; import header1Icon from '../../assets/icons/heading_1/2.png'; @@ -22,6 +23,7 @@ import orderListIcon from '../../assets/icons/order_list/2.png'; import paragraphIcon from '../../assets/icons/paragraph/2.png'; import plantumlIcon from '../../assets/icons/plantuml/2.png'; import quoteIcon from '../../assets/icons/quote_block/2.png'; +import sequenceIcon from '../../assets/icons/sequence/2.png'; import taskListIcon from '../../assets/icons/todolist/2.png'; const HEADING_ICONS = [ @@ -37,6 +39,8 @@ const DIAGRAM_ICONS = { 'plantuml': plantumlIcon, 'mermaid': mermaidIcon, 'vega-lite': vegaIcon, + 'flowchart': flowchartIcon, + 'sequence': sequenceIcon, }; export function getIcon(block: Parent) { diff --git a/packages/muya/src/ui/paragraphQuickInsertMenu/__tests__/diagramMenuEntries.spec.ts b/packages/muya/src/ui/paragraphQuickInsertMenu/__tests__/diagramMenuEntries.spec.ts new file mode 100644 index 0000000000..f832fd3419 --- /dev/null +++ b/packages/muya/src/ui/paragraphQuickInsertMenu/__tests__/diagramMenuEntries.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { MENU_CONFIG } from '../config'; + +// Lock the quick-insert menu's diagram section to all five supported diagram +// engines. flowchart + sequence were restored for parity with the legacy +// muyajs engine; a future refactor of the menu config shouldn't silently drop +// them. +describe('quick-insert menu — diagram entries', () => { + const diagramSection = MENU_CONFIG.find(section => section.name === 'diagrams'); + + it('has a diagrams section', () => { + expect(diagramSection).toBeDefined(); + }); + + it('exposes flowchart and sequence insert entries', () => { + const labels = diagramSection!.children.map(child => child.label); + expect(labels).toContain('diagram flowchart'); + expect(labels).toContain('diagram sequence'); + }); + + it('keeps the existing mermaid / plantuml / vega-lite entries', () => { + const labels = diagramSection!.children.map(child => child.label); + expect(labels).toContain('diagram mermaid'); + expect(labels).toContain('diagram plantuml'); + expect(labels).toContain('diagram vega-lite'); + }); + + it('gives every diagram entry an icon', () => { + for (const child of diagramSection!.children) + expect(child.icon, `${child.label} should have an icon`).toBeTruthy(); + }); +}); diff --git a/packages/muya/src/ui/paragraphQuickInsertMenu/config.ts b/packages/muya/src/ui/paragraphQuickInsertMenu/config.ts index 143d950546..2945f183ae 100644 --- a/packages/muya/src/ui/paragraphQuickInsertMenu/config.ts +++ b/packages/muya/src/ui/paragraphQuickInsertMenu/config.ts @@ -3,6 +3,7 @@ import type { Muya } from '../../index'; import bulletListIcon from '../../assets/icons/bullet_list/2.png'; import vegaIcon from '../../assets/icons/chart/2.png'; import codeIcon from '../../assets/icons/code/2.png'; +import flowchartIcon from '../../assets/icons/flowchart/2.png'; import frontMatterIcon from '../../assets/icons/front_matter/2.png'; import header1Icon from '../../assets/icons/heading_1/2.png'; import header2Icon from '../../assets/icons/heading_2/2.png'; @@ -19,6 +20,7 @@ import orderListIcon from '../../assets/icons/order_list/2.png'; import paragraphIcon from '../../assets/icons/paragraph/2.png'; import plantumlIcon from '../../assets/icons/plantuml/2.png'; import quoteIcon from '../../assets/icons/quote_block/2.png'; +import sequenceIcon from '../../assets/icons/sequence/2.png'; import todoListIcon from '../../assets/icons/todolist/2.png'; import { ScrollPage } from '../../block/scrollPage'; @@ -326,6 +328,18 @@ export const MENU_CONFIG: IQuickInsertMenuItem[] = [ label: 'diagram plantuml', icon: plantumlIcon, }, + { + title: 'Flowchart', + subTitle: 'By flowchart.js', + label: 'diagram flowchart', + icon: flowchartIcon, + }, + { + title: 'Sequence', + subTitle: 'By js-sequence-diagrams', + label: 'diagram sequence', + icon: sequenceIcon, + }, ], }, ]; @@ -460,11 +474,21 @@ export function replaceBlockByLabel({ block, muya, label, text = '' }: { // fall through case 'diagram mermaid': // fall through - case 'diagram plantuml': { + case 'diagram plantuml': + // fall through + case 'diagram flowchart': + // fall through + case 'diagram sequence': { const diagramState = deepClone(emptyStates.diagram); const [name, type] = label.split(' '); - if (type === 'mermaid' || type === 'plantuml' || type === 'vega-lite') { + if ( + type === 'mermaid' + || type === 'plantuml' + || type === 'vega-lite' + || type === 'flowchart' + || type === 'sequence' + ) { diagramState.meta.type = type; diagramState.meta.lang = type === 'vega-lite' ? 'json' : 'yaml'; } diff --git a/packages/muya/src/utils/diagram/index.ts b/packages/muya/src/utils/diagram/index.ts index 0631a63f7c..f464814e11 100644 --- a/packages/muya/src/utils/diagram/index.ts +++ b/packages/muya/src/utils/diagram/index.ts @@ -1,7 +1,7 @@ const rendererCache = new Map(); /** * - * @param {string} name the renderer name:plantuml, mermaid, vega-lite + * @param {string} name the renderer name: plantuml, mermaid, vega-lite, flowchart, sequence */ async function loadRenderer(name: string) { if (!rendererCache.has(name)) { @@ -22,6 +22,16 @@ async function loadRenderer(name: string) { rendererCache.set(name, m.default); break; + case 'flowchart': + m = await import('flowchart.js'); + rendererCache.set(name, m.default); + break; + + case 'sequence': + m = await import('./sequence'); + rendererCache.set(name, m.default); + break; + default: throw new Error(`Unknown diagram name ${name}`); } diff --git a/packages/muya/src/utils/diagram/sequence/danielbd.woff b/packages/muya/src/utils/diagram/sequence/danielbd.woff new file mode 100755 index 0000000000000000000000000000000000000000..aa01f17b930503d23aa19e780fff8795c49207d1 GIT binary patch literal 38300 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 literal 0 HcmV?d00001 diff --git a/packages/muya/src/utils/diagram/sequence/danielbd.woff2 b/packages/muya/src/utils/diagram/sequence/danielbd.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..15ceb491207c9c121ba13eea5e90660736dace54 GIT binary patch literal 33744 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 Q{Ur+Hq1OSBLserF0Qp^#ga7~l literal 0 HcmV?d00001 diff --git a/packages/muya/src/utils/diagram/sequence/index.ts b/packages/muya/src/utils/diagram/sequence/index.ts new file mode 100644 index 0000000000..66d2edaced --- /dev/null +++ b/packages/muya/src/utils/diagram/sequence/index.ts @@ -0,0 +1,9 @@ +import Diagram from './sequence-diagram-snap'; +import './sequence-diagram.css'; + +// Vendored `js-sequence-diagrams` (bramp, BSD) wired to render via snap.svg. +// The upstream `js-sequence-diagrams` npm package is a dead security-holder +// placeholder, so the legacy muyajs engine vendored the library source. We do +// the same here to keep feature parity. The exported `Diagram` exposes +// `parse(code)` returning an object with `drawSVG(container, { theme })`. +export default Diagram; diff --git a/packages/muya/src/utils/diagram/sequence/sequence-diagram-snap.js b/packages/muya/src/utils/diagram/sequence/sequence-diagram-snap.js new file mode 100644 index 0000000000..bd721fd575 --- /dev/null +++ b/packages/muya/src/utils/diagram/sequence/sequence-diagram-snap.js @@ -0,0 +1,1871 @@ +/** js sequence diagrams 2.0.1 + * https://bramp.github.io/js-sequence-diagrams/ + * (c) 2012-2017 Andrew Brampton (bramp.net) + * @license Simplified BSD license. + */ +import _ from 'underscore' +import Snap from 'snapsvg-cjs' +import WebFont from 'webfontloader' + +function Diagram() { + this.title = undefined + this.actors = [] + this.signals = [] +} +/* + * Return an existing actor with this alias, or creates a new one with alias and name. + */ +Diagram.prototype.getActor = function (alias, name) { + alias = alias.trim() + + var i + var actors = this.actors + for (i in actors) { + if (actors[i].alias == alias) { + return actors[i] + } + } + i = actors.push(new Diagram.Actor(alias, name || alias, actors.length)) + return actors[i - 1] +} + +/* + * Parses the input as either a alias, or a "name as alias", and returns the corresponding actor. + */ +Diagram.prototype.getActorWithAlias = function (input) { + input = input.trim() + + // We are lazy and do some of the parsing in javascript :(. TODO move into the .jison file. + var s = /([\s\S]+) as (\S+)$/im.exec(input) + var alias + var name + if (s) { + name = s[1].trim() + alias = s[2].trim() + } else { + name = alias = input + } + return this.getActor(alias, name) +} + +Diagram.prototype.setTitle = function (title) { + this.title = title +} + +Diagram.prototype.addSignal = function (signal) { + this.signals.push(signal) +} + +Diagram.Actor = function (alias, name, index) { + this.alias = alias + this.name = name + this.index = index +} + +Diagram.Signal = function (actorA, signaltype, actorB, message) { + this.type = 'Signal' + this.actorA = actorA + this.actorB = actorB + this.linetype = signaltype & 3 + this.arrowtype = (signaltype >> 2) & 3 + this.message = message +} + +Diagram.Signal.prototype.isSelf = function () { + return this.actorA.index == this.actorB.index +} + +Diagram.Note = function (actor, placement, message) { + this.type = 'Note' + this.actor = actor + this.placement = placement + this.message = message + + if (this.hasManyActors() && actor[0] == actor[1]) { + throw new Error('Note should be over two different actors') + } +} + +Diagram.Note.prototype.hasManyActors = function () { + return _.isArray(this.actor) +} + +Diagram.unescape = function (s) { + // Turn "\\n" into "\n" + return s + .trim() + .replace(/^"(.*)"$/m, '$1') + .replace(/\\n/gm, '\n') +} + +Diagram.LINETYPE = { + SOLID: 0, + DOTTED: 1 +} + +Diagram.ARROWTYPE = { + FILLED: 0, + OPEN: 1 +} + +Diagram.PLACEMENT = { + LEFTOF: 0, + RIGHTOF: 1, + OVER: 2 +} + +// Some older browsers don't have getPrototypeOf, thus we polyfill it +// https://github.com/bramp/js-sequence-diagrams/issues/57 +// https://github.com/zaach/jison/issues/194 +// Taken from http://ejohn.org/blog/objectgetprototypeof/ +if (typeof Object.getPrototypeOf !== 'function') { + /* jshint -W103 */ + if (typeof 'test'.__proto__ === 'object') { + Object.getPrototypeOf = function (object) { + return object.__proto__ + } + } else { + Object.getPrototypeOf = function (object) { + // May break if the constructor has been tampered with + return object.constructor.prototype + } + } + /* jshint +W103 */ +} + +/** The following is included by preprocessor */ +/* parser generated by jison 0.4.15 */ +/* + Returns a Parser object of the following structure: + Parser: { + yy: {} + } + Parser.prototype: { + yy: {}, + trace: function(), + symbols_: {associative list: name ==> number}, + terminals_: {associative list: number ==> name}, + productions_: [...], + performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), + table: [...], + defaultActions: {...}, + parseError: function(str, hash), + parse: function(input), + lexer: { + EOF: 1, + parseError: function(str, hash), + setInput: function(input), + input: function(), + unput: function(str), + more: function(), + less: function(n), + pastInput: function(), + upcomingInput: function(), + showPosition: function(), + test_match: function(regex_match_array, rule_index), + next: function(), + lex: function(), + begin: function(condition), + popState: function(), + _currentRules: function(), + topState: function(), + pushState: function(condition), + options: { + ranges: boolean (optional: true ==> token location info will include a .range[] member) + flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) + backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) + }, + performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), + rules: [...], + conditions: {associative list: name ==> set}, + } + } + token location info (@$, _$, etc.): { + first_line: n, + last_line: n, + first_column: n, + last_column: n, + range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) + } + the parseError function receives a 'hash' object with these members for lexer and parser errors: { + text: (matched text) + token: (the produced terminal token, if any) + line: (yylineno) + } + while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { + loc: (yylloc) + expected: (string describing the set of expected tokens) + recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) + } +*/ +var parser = (function () { + function Parser() { + this.yy = {} + } + var o = function (k, v, o, l) { + for (o = o || {}, l = k.length; l--; o[k[l]] = v); + return o + }, + $V0 = [5, 8, 9, 13, 15, 24], + $V1 = [1, 13], + $V2 = [1, 17], + $V3 = [24, 29, 30], + parser = { + trace: function () {}, + yy: {}, + symbols_: { + error: 2, + start: 3, + document: 4, + EOF: 5, + line: 6, + statement: 7, + NL: 8, + participant: 9, + actor_alias: 10, + signal: 11, + note_statement: 12, + title: 13, + message: 14, + note: 15, + placement: 16, + actor: 17, + over: 18, + actor_pair: 19, + ',': 20, + left_of: 21, + right_of: 22, + signaltype: 23, + ACTOR: 24, + linetype: 25, + arrowtype: 26, + LINE: 27, + DOTLINE: 28, + ARROW: 29, + OPENARROW: 30, + MESSAGE: 31, + $accept: 0, + $end: 1 + }, + terminals_: { + 2: 'error', + 5: 'EOF', + 8: 'NL', + 9: 'participant', + 13: 'title', + 15: 'note', + 18: 'over', + 20: ',', + 21: 'left_of', + 22: 'right_of', + 24: 'ACTOR', + 27: 'LINE', + 28: 'DOTLINE', + 29: 'ARROW', + 30: 'OPENARROW', + 31: 'MESSAGE' + }, + productions_: [ + 0, + [3, 2], + [4, 0], + [4, 2], + [6, 1], + [6, 1], + [7, 2], + [7, 1], + [7, 1], + [7, 2], + [12, 4], + [12, 4], + [19, 1], + [19, 3], + [16, 1], + [16, 1], + [11, 4], + [17, 1], + [10, 1], + [23, 2], + [23, 1], + [25, 1], + [25, 1], + [26, 1], + [26, 1], + [14, 1] + ], + performAction: function (yytext, yyleng, yylineno, yy, yystate, $$, _$) { + /* this == yyval */ + var $0 = $$.length - 1 + switch (yystate) { + case 1: + return yy.parser.yy + + case 4: + break + + case 6: + $$[$0] + break + + case 7: + case 8: + yy.parser.yy.addSignal($$[$0]) + break + + case 9: + yy.parser.yy.setTitle($$[$0]) + break + + case 10: + this.$ = new Diagram.Note($$[$0 - 1], $$[$0 - 2], $$[$0]) + break + + case 11: + this.$ = new Diagram.Note($$[$0 - 1], Diagram.PLACEMENT.OVER, $$[$0]) + break + + case 12: + case 20: + this.$ = $$[$0] + break + + case 13: + this.$ = [$$[$0 - 2], $$[$0]] + break + + case 14: + this.$ = Diagram.PLACEMENT.LEFTOF + break + + case 15: + this.$ = Diagram.PLACEMENT.RIGHTOF + break + + case 16: + this.$ = new Diagram.Signal($$[$0 - 3], $$[$0 - 2], $$[$0 - 1], $$[$0]) + break + + case 17: + this.$ = yy.parser.yy.getActor(Diagram.unescape($$[$0])) + break + + case 18: + this.$ = yy.parser.yy.getActorWithAlias(Diagram.unescape($$[$0])) + break + + case 19: + this.$ = $$[$0 - 1] | ($$[$0] << 2) + break + + case 21: + this.$ = Diagram.LINETYPE.SOLID + break + + case 22: + this.$ = Diagram.LINETYPE.DOTTED + break + + case 23: + this.$ = Diagram.ARROWTYPE.FILLED + break + + case 24: + this.$ = Diagram.ARROWTYPE.OPEN + break + + case 25: + this.$ = Diagram.unescape($$[$0].substring(1)) + } + }, + table: [ + o($V0, [2, 2], { + 3: 1, + 4: 2 + }), + { + 1: [3] + }, + { + 5: [1, 3], + 6: 4, + 7: 5, + 8: [1, 6], + 9: [1, 7], + 11: 8, + 12: 9, + 13: [1, 10], + 15: [1, 12], + 17: 11, + 24: $V1 + }, + { + 1: [2, 1] + }, + o($V0, [2, 3]), + o($V0, [2, 4]), + o($V0, [2, 5]), + { + 10: 14, + 24: [1, 15] + }, + o($V0, [2, 7]), + o($V0, [2, 8]), + { + 14: 16, + 31: $V2 + }, + { + 23: 18, + 25: 19, + 27: [1, 20], + 28: [1, 21] + }, + { + 16: 22, + 18: [1, 23], + 21: [1, 24], + 22: [1, 25] + }, + o([20, 27, 28, 31], [2, 17]), + o($V0, [2, 6]), + o($V0, [2, 18]), + o($V0, [2, 9]), + o($V0, [2, 25]), + { + 17: 26, + 24: $V1 + }, + { + 24: [2, 20], + 26: 27, + 29: [1, 28], + 30: [1, 29] + }, + o($V3, [2, 21]), + o($V3, [2, 22]), + { + 17: 30, + 24: $V1 + }, + { + 17: 32, + 19: 31, + 24: $V1 + }, + { + 24: [2, 14] + }, + { + 24: [2, 15] + }, + { + 14: 33, + 31: $V2 + }, + { + 24: [2, 19] + }, + { + 24: [2, 23] + }, + { + 24: [2, 24] + }, + { + 14: 34, + 31: $V2 + }, + { + 14: 35, + 31: $V2 + }, + { + 20: [1, 36], + 31: [2, 12] + }, + o($V0, [2, 16]), + o($V0, [2, 10]), + o($V0, [2, 11]), + { + 17: 37, + 24: $V1 + }, + { + 31: [2, 13] + } + ], + defaultActions: { + 3: [2, 1], + 24: [2, 14], + 25: [2, 15], + 27: [2, 19], + 28: [2, 23], + 29: [2, 24], + 37: [2, 13] + }, + parseError: function (str, hash) { + if (!hash.recoverable) throw new Error(str) + this.trace(str) + }, + parse: function (input) { + function lex() { + var token + return ( + (token = lexer.lex() || EOF), + 'number' != typeof token && (token = self.symbols_[token] || token), + token + ) + } + var self = this, + stack = [0], + vstack = [null], + lstack = [], + table = this.table, + yytext = '', + yylineno = 0, + yyleng = 0, + recovering = 0, + TERROR = 2, + EOF = 1, + args = lstack.slice.call(arguments, 1), + lexer = Object.create(this.lexer), + sharedState = { + yy: {} + } + for (var k in this.yy) + Object.prototype.hasOwnProperty.call(this.yy, k) && (sharedState.yy[k] = this.yy[k]) + lexer.setInput(input, sharedState.yy), + (sharedState.yy.lexer = lexer), + (sharedState.yy.parser = this), + 'undefined' == typeof lexer.yylloc && (lexer.yylloc = {}) + var yyloc = lexer.yylloc + lstack.push(yyloc) + var ranges = lexer.options && lexer.options.ranges + 'function' == typeof sharedState.yy.parseError + ? (this.parseError = sharedState.yy.parseError) + : (this.parseError = Object.getPrototypeOf(this).parseError) + for ( + var symbol, preErrorSymbol, state, action, r, p, len, newState, expected, yyval = {}; + ; + + ) { + if ( + ((state = stack[stack.length - 1]), + this.defaultActions[state] + ? (action = this.defaultActions[state]) + : ((null !== symbol && 'undefined' != typeof symbol) || (symbol = lex()), + (action = table[state] && table[state][symbol])), + 'undefined' == typeof action || !action.length || !action[0]) + ) { + var errStr = '' + expected = [] + for (p in table[state]) + this.terminals_[p] && p > TERROR && expected.push("'" + this.terminals_[p] + "'") + ;(errStr = lexer.showPosition + ? 'Parse error on line ' + + (yylineno + 1) + + ':\n' + + lexer.showPosition() + + '\nExpecting ' + + expected.join(', ') + + ", got '" + + (this.terminals_[symbol] || symbol) + + "'" + : 'Parse error on line ' + + (yylineno + 1) + + ': Unexpected ' + + (symbol == EOF ? 'end of input' : "'" + (this.terminals_[symbol] || symbol) + "'")), + this.parseError(errStr, { + text: lexer.match, + token: this.terminals_[symbol] || symbol, + line: lexer.yylineno, + loc: yyloc, + expected: expected + }) + } + if (action[0] instanceof Array && action.length > 1) + throw new Error( + 'Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol + ) + switch (action[0]) { + case 1: + stack.push(symbol), + vstack.push(lexer.yytext), + lstack.push(lexer.yylloc), + stack.push(action[1]), + (symbol = null), + preErrorSymbol + ? ((symbol = preErrorSymbol), (preErrorSymbol = null)) + : ((yyleng = lexer.yyleng), + (yytext = lexer.yytext), + (yylineno = lexer.yylineno), + (yyloc = lexer.yylloc), + recovering > 0 && recovering--) + break + + case 2: + if ( + ((len = this.productions_[action[1]][1]), + (yyval.$ = vstack[vstack.length - len]), + (yyval._$ = { + first_line: lstack[lstack.length - (len || 1)].first_line, + last_line: lstack[lstack.length - 1].last_line, + first_column: lstack[lstack.length - (len || 1)].first_column, + last_column: lstack[lstack.length - 1].last_column + }), + ranges && + (yyval._$.range = [ + lstack[lstack.length - (len || 1)].range[0], + lstack[lstack.length - 1].range[1] + ]), + (r = this.performAction.apply( + yyval, + [yytext, yyleng, yylineno, sharedState.yy, action[1], vstack, lstack].concat(args) + )), + 'undefined' != typeof r) + ) + return r + len && + ((stack = stack.slice(0, -1 * len * 2)), + (vstack = vstack.slice(0, -1 * len)), + (lstack = lstack.slice(0, -1 * len))), + stack.push(this.productions_[action[1]][0]), + vstack.push(yyval.$), + lstack.push(yyval._$), + (newState = table[stack[stack.length - 2]][stack[stack.length - 1]]), + stack.push(newState) + break + + case 3: + return !0 + } + } + return !0 + } + }, + lexer = (function () { + var lexer = { + EOF: 1, + parseError: function (str, hash) { + if (!this.yy.parser) throw new Error(str) + this.yy.parser.parseError(str, hash) + }, + // resets the lexer, sets new input + setInput: function (input, yy) { + return ( + (this.yy = yy || this.yy || {}), + (this._input = input), + (this._more = this._backtrack = this.done = !1), + (this.yylineno = this.yyleng = 0), + (this.yytext = this.matched = this.match = ''), + (this.conditionStack = ['INITIAL']), + (this.yylloc = { + first_line: 1, + first_column: 0, + last_line: 1, + last_column: 0 + }), + this.options.ranges && (this.yylloc.range = [0, 0]), + (this.offset = 0), + this + ) + }, + // consumes and returns one char from the input + input: function () { + var ch = this._input[0] + ;(this.yytext += ch), + this.yyleng++, + this.offset++, + (this.match += ch), + (this.matched += ch) + var lines = ch.match(/(?:\r\n?|\n).*/g) + return ( + lines ? (this.yylineno++, this.yylloc.last_line++) : this.yylloc.last_column++, + this.options.ranges && this.yylloc.range[1]++, + (this._input = this._input.slice(1)), + ch + ) + }, + // unshifts one char (or a string) into the input + unput: function (ch) { + var len = ch.length, + lines = ch.split(/(?:\r\n?|\n)/g) + ;(this._input = ch + this._input), + (this.yytext = this.yytext.substr(0, this.yytext.length - len)), + //this.yyleng -= len; + (this.offset -= len) + var oldLines = this.match.split(/(?:\r\n?|\n)/g) + ;(this.match = this.match.substr(0, this.match.length - 1)), + (this.matched = this.matched.substr(0, this.matched.length - 1)), + lines.length - 1 && (this.yylineno -= lines.length - 1) + var r = this.yylloc.range + return ( + (this.yylloc = { + first_line: this.yylloc.first_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.first_column, + last_column: lines + ? (lines.length === oldLines.length ? this.yylloc.first_column : 0) + + oldLines[oldLines.length - lines.length].length - + lines[0].length + : this.yylloc.first_column - len + }), + this.options.ranges && (this.yylloc.range = [r[0], r[0] + this.yyleng - len]), + (this.yyleng = this.yytext.length), + this + ) + }, + // When called from action, caches matched text and appends it on next action + more: function () { + return (this._more = !0), this + }, + // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. + reject: function () { + return this.options.backtrack_lexer + ? ((this._backtrack = !0), this) + : this.parseError( + 'Lexical error on line ' + + (this.yylineno + 1) + + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + + this.showPosition(), + { + text: '', + token: null, + line: this.yylineno + } + ) + }, + // retain first n characters of the match + less: function (n) { + this.unput(this.match.slice(n)) + }, + // displays already matched input, i.e. for error messages + pastInput: function () { + var past = this.matched.substr(0, this.matched.length - this.match.length) + return (past.length > 20 ? '...' : '') + past.substr(-20).replace(/\n/g, '') + }, + // displays upcoming input, i.e. for error messages + upcomingInput: function () { + var next = this.match + return ( + next.length < 20 && (next += this._input.substr(0, 20 - next.length)), + (next.substr(0, 20) + (next.length > 20 ? '...' : '')).replace(/\n/g, '') + ) + }, + // displays the character position where the lexing error occurred, i.e. for error messages + showPosition: function () { + var pre = this.pastInput(), + c = new Array(pre.length + 1).join('-') + return pre + this.upcomingInput() + '\n' + c + '^' + }, + // test the lexed token: return FALSE when not a match, otherwise return token + test_match: function (match, indexed_rule) { + var token, lines, backup + if ( + (this.options.backtrack_lexer && // save context + ((backup = { + yylineno: this.yylineno, + yylloc: { + first_line: this.yylloc.first_line, + last_line: this.last_line, + first_column: this.yylloc.first_column, + last_column: this.yylloc.last_column + }, + yytext: this.yytext, + match: this.match, + matches: this.matches, + matched: this.matched, + yyleng: this.yyleng, + offset: this.offset, + _more: this._more, + _input: this._input, + yy: this.yy, + conditionStack: this.conditionStack.slice(0), + done: this.done + }), + this.options.ranges && (backup.yylloc.range = this.yylloc.range.slice(0))), + (lines = match[0].match(/(?:\r\n?|\n).*/g)), + lines && (this.yylineno += lines.length), + (this.yylloc = { + first_line: this.yylloc.last_line, + last_line: this.yylineno + 1, + first_column: this.yylloc.last_column, + last_column: lines + ? lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length + : this.yylloc.last_column + match[0].length + }), + (this.yytext += match[0]), + (this.match += match[0]), + (this.matches = match), + (this.yyleng = this.yytext.length), + this.options.ranges && + (this.yylloc.range = [this.offset, (this.offset += this.yyleng)]), + (this._more = !1), + (this._backtrack = !1), + (this._input = this._input.slice(match[0].length)), + (this.matched += match[0]), + (token = this.performAction.call( + this, + this.yy, + this, + indexed_rule, + this.conditionStack[this.conditionStack.length - 1] + )), + this.done && this._input && (this.done = !1), + token) + ) + return token + if (this._backtrack) { + // recover context + for (var k in backup) this[k] = backup[k] + return !1 + } + return !1 + }, + // return next match in input + next: function () { + if (this.done) return this.EOF + this._input || (this.done = !0) + var token, match, tempMatch, index + this._more || ((this.yytext = ''), (this.match = '')) + for (var rules = this._currentRules(), i = 0; i < rules.length; i++) + if ( + ((tempMatch = this._input.match(this.rules[rules[i]])), + tempMatch && (!match || tempMatch[0].length > match[0].length)) + ) { + if (((match = tempMatch), (index = i), this.options.backtrack_lexer)) { + if (((token = this.test_match(tempMatch, rules[i])), token !== !1)) return token + if (this._backtrack) { + match = !1 + continue + } + // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) + return !1 + } + if (!this.options.flex) break + } + return match + ? ((token = this.test_match(match, rules[index])), token !== !1 && token) + : '' === this._input + ? this.EOF + : this.parseError( + 'Lexical error on line ' + + (this.yylineno + 1) + + '. Unrecognized text.\n' + + this.showPosition(), + { + text: '', + token: null, + line: this.yylineno + } + ) + }, + // return next match that has a token + lex: function () { + var r = this.next() + return r ? r : this.lex() + }, + // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) + begin: function (condition) { + this.conditionStack.push(condition) + }, + // pop the previously active lexer condition state off the condition stack + popState: function () { + var n = this.conditionStack.length - 1 + return n > 0 ? this.conditionStack.pop() : this.conditionStack[0] + }, + // produce the lexer rule set which is active for the currently active lexer condition state + _currentRules: function () { + return this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1] + ? this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules + : this.conditions.INITIAL.rules + }, + // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available + topState: function (n) { + return ( + (n = this.conditionStack.length - 1 - Math.abs(n || 0)), + n >= 0 ? this.conditionStack[n] : 'INITIAL' + ) + }, + // alias for begin(condition) + pushState: function (condition) { + this.begin(condition) + }, + // return the number of states currently on the stack + stateStackSize: function () { + return this.conditionStack.length + }, + options: { + 'case-insensitive': !0 + }, + performAction: function (yy, yy_, $avoiding_name_collisions, YY_START) { + switch ($avoiding_name_collisions) { + case 0: + return 8 + + case 1: + /* skip whitespace */ + break + + case 2: + /* skip comments */ + break + + case 3: + return 9 + + case 4: + return 21 + + case 5: + return 22 + + case 6: + return 18 + + case 7: + return 15 + + case 8: + return 13 + + case 9: + return 20 + + case 10: + return 24 + + case 11: + return 24 + + case 12: + return 28 + + case 13: + return 27 + + case 14: + return 30 + + case 15: + return 29 + + case 16: + return 31 + + case 17: + return 5 + + case 18: + return 'INVALID' + } + }, + rules: [ + /^(?:[\r\n]+)/i, + /^(?:\s+)/i, + /^(?:#[^\r\n]*)/i, + /^(?:participant\b)/i, + /^(?:left of\b)/i, + /^(?:right of\b)/i, + /^(?:over\b)/i, + /^(?:note\b)/i, + /^(?:title\b)/i, + /^(?:,)/i, + /^(?:[^\->:,\r\n"]+)/i, + /^(?:"[^"]+")/i, + /^(?:--)/i, + /^(?:-)/i, + /^(?:>>)/i, + /^(?:>)/i, + /^(?:[^\r\n]+)/i, + /^(?:$)/i, + /^(?:.)/i + ], + conditions: { + INITIAL: { + rules: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18], + inclusive: !0 + } + } + } + return lexer + })() + return (parser.lexer = lexer), (Parser.prototype = parser), (parser.Parser = Parser), new Parser() +})() + +// CLI bootstrap stripped — this library only runs in the renderer; the +// require('fs')/require('path') path was unreachable in the editor and breaks +// sandboxed bundling. +// jison's UMD `exports.parser/Parser/parse` fallback was also dropped: this +// file is an ESM module (`export default Diagram`), so the CommonJS `exports` +// global is undefined here and Rollup flags it (COMMONJS_VARIABLE_IN_ESM). +// `Diagram.parse` uses the internal `parser` directly, so nothing relies on it. +/** + * jison doesn't have a good exception, so we make one. + * This is brittle as it depends on jison internals + */ +function ParseError(message, hash) { + _.extend(this, hash) + + this.name = 'ParseError' + this.message = message || '' +} +ParseError.prototype = new Error() +Diagram.ParseError = ParseError + +Diagram.parse = function (input) { + // TODO jison v0.4.17 changed their API slightly, so parser is no longer defined: + + // Create the object to track state and deal with errors + parser.yy = new Diagram() + parser.yy.parseError = function (message, hash) { + throw new ParseError(message, hash) + } + + // Parse + var diagram = parser.parse(input) + + // Then clean up the parseError key that a user won't care about + delete diagram.parseError + return diagram +} + +/** js sequence diagrams + * https://bramp.github.io/js-sequence-diagrams/ + * (c) 2012-2017 Andrew Brampton (bramp.net) + * Simplified BSD license. + */ +/*global Diagram, _ */ + +// Following the CSS convention +// Margin is the gap outside the box +// Padding is the gap inside the box +// Each object has x/y/width/height properties +// The x/y should be top left corner +// width/height is with both margin and padding + +// TODO +// Image width is wrong, when there is a note in the right hand col +// Title box could look better +// Note box could look better + +var DIAGRAM_MARGIN = 10 + +var ACTOR_MARGIN = 10 // Margin around a actor +var ACTOR_PADDING = 10 // Padding inside a actor + +var SIGNAL_MARGIN = 5 // Margin around a signal +var SIGNAL_PADDING = 5 // Padding inside a signal + +var NOTE_MARGIN = 10 // Margin around a note +var NOTE_PADDING = 5 // Padding inside a note +var NOTE_OVERLAP = 15 // Overlap when using a "note over A,B" + +var TITLE_MARGIN = 0 +var TITLE_PADDING = 5 + +var SELF_SIGNAL_WIDTH = 20 // How far out a self signal goes + +var PLACEMENT = Diagram.PLACEMENT +var LINETYPE = Diagram.LINETYPE +var ARROWTYPE = Diagram.ARROWTYPE + +var ALIGN_LEFT = 0 +var ALIGN_CENTER = 1 + +function AssertException(message) { + this.message = message +} +AssertException.prototype.toString = function () { + return 'AssertException: ' + this.message +} + +function assert(exp, message) { + if (!exp) { + throw new AssertException(message) + } +} + +if (!String.prototype.trim) { + String.prototype.trim = function () { + return this.replace(/^\s+|\s+$/g, '') + } +} + +Diagram.themes = {} +function registerTheme(name, theme) { + Diagram.themes[name] = theme +} + +/****************** + * Drawing extras + ******************/ + +function getCenterX(box) { + return box.x + box.width / 2 +} + +function getCenterY(box) { + return box.y + box.height / 2 +} + +/****************** + * SVG Path extras + ******************/ + +function clamp(x, min, max) { + if (x < min) { + return min + } + if (x > max) { + return max + } + return x +} + +function wobble(x1, y1, x2, y2) { + assert(_.all([x1, x2, y1, y2], _.isFinite), 'x1,x2,y1,y2 must be numeric') + + // Wobble no more than 1/25 of the line length + var factor = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)) / 25 + + // Distance along line where the control points are + // Clamp between 20% and 80% so any arrow heads aren't angled too much + var r1 = clamp(Math.random(), 0.2, 0.8) + var r2 = clamp(Math.random(), 0.2, 0.8) + + var xfactor = Math.random() > 0.5 ? factor : -factor + var yfactor = Math.random() > 0.5 ? factor : -factor + + var p1 = { + x: (x2 - x1) * r1 + x1 + xfactor, + y: (y2 - y1) * r1 + y1 + yfactor + } + + var p2 = { + x: (x2 - x1) * r2 + x1 - xfactor, + y: (y2 - y1) * r2 + y1 - yfactor + } + + return ( + 'C' + + p1.x.toFixed(1) + + ',' + + p1.y.toFixed(1) + // start control point + ' ' + + p2.x.toFixed(1) + + ',' + + p2.y.toFixed(1) + // end control point + ' ' + + x2.toFixed(1) + + ',' + + y2.toFixed(1) + ) // end point +} + +/** + * Draws a wobbly (hand drawn) rect + */ +function handRect(x, y, w, h) { + assert(_.all([x, y, w, h], _.isFinite), 'x, y, w, h must be numeric') + return ( + 'M' + + x + + ',' + + y + + wobble(x, y, x + w, y) + + wobble(x + w, y, x + w, y + h) + + wobble(x + w, y + h, x, y + h) + + wobble(x, y + h, x, y) + ) +} + +/** + * Draws a wobbly (hand drawn) line + */ +function handLine(x1, y1, x2, y2) { + assert(_.all([x1, x2, y1, y2], _.isFinite), 'x1,x2,y1,y2 must be numeric') + return 'M' + x1.toFixed(1) + ',' + y1.toFixed(1) + wobble(x1, y1, x2, y2) +} + +/****************** + * BaseTheme + ******************/ + +var BaseTheme = function (diagram, options) { + this.init(diagram, options) +} + +_.extend(BaseTheme.prototype, { + // Init called while creating the Theme + init: function (diagram, options) { + this.diagram = diagram + + this.actorsHeight_ = 0 + this.signalsHeight_ = 0 + this.title_ = undefined // hack - This should be somewhere better + }, + + setupPaper: function (container) {}, + + draw: function (container) { + this.setupPaper(container) + + this.layout() + + var titleHeight = this.title_ ? this.title_.height : 0 + var y = DIAGRAM_MARGIN + titleHeight + + this.drawTitle() + this.drawActors(y) + this.drawSignals(y + this.actorsHeight_) + }, + + layout: function () { + // Local copies + var diagram = this.diagram + var font = this.font_ + var actors = diagram.actors + var signals = diagram.signals + + diagram.width = 0 // min width + diagram.height = 0 // min height + + // Setup some layout stuff + if (diagram.title) { + var title = (this.title_ = {}) + var bb = this.textBBox(diagram.title, font) + title.textBB = bb + title.message = diagram.title + + title.width = bb.width + (TITLE_PADDING + TITLE_MARGIN) * 2 + title.height = bb.height + (TITLE_PADDING + TITLE_MARGIN) * 2 + title.x = DIAGRAM_MARGIN + title.y = DIAGRAM_MARGIN + + diagram.width += title.width + diagram.height += title.height + } + + _.each( + actors, + function (a) { + var bb = this.textBBox(a.name, font) + a.textBB = bb + + a.x = 0 + a.y = 0 + a.width = bb.width + (ACTOR_PADDING + ACTOR_MARGIN) * 2 + a.height = bb.height + (ACTOR_PADDING + ACTOR_MARGIN) * 2 + + a.distances = [] + a.paddingRight = 0 + this.actorsHeight_ = Math.max(a.height, this.actorsHeight_) + }, + this + ) + + function actorEnsureDistance(a, b, d) { + assert(a < b, 'a must be less than or equal to b') + + if (a < 0) { + // Ensure b has left margin + b = actors[b] + b.x = Math.max(d - b.width / 2, b.x) + } else if (b >= actors.length) { + // Ensure a has right margin + a = actors[a] + a.paddingRight = Math.max(d, a.paddingRight) + } else { + a = actors[a] + a.distances[b] = Math.max(d, a.distances[b] ? a.distances[b] : 0) + } + } + + _.each( + signals, + function (s) { + // Indexes of the left and right actors involved + var a + var b + + var bb = this.textBBox(s.message, font) + + //var bb = t.attr("text", s.message).getBBox(); + s.textBB = bb + s.width = bb.width + s.height = bb.height + + var extraWidth = 0 + + if (s.type == 'Signal') { + s.width += (SIGNAL_MARGIN + SIGNAL_PADDING) * 2 + s.height += (SIGNAL_MARGIN + SIGNAL_PADDING) * 2 + + if (s.isSelf()) { + // TODO Self signals need a min height + a = s.actorA.index + b = a + 1 + s.width += SELF_SIGNAL_WIDTH + } else { + a = Math.min(s.actorA.index, s.actorB.index) + b = Math.max(s.actorA.index, s.actorB.index) + } + } else if (s.type == 'Note') { + s.width += (NOTE_MARGIN + NOTE_PADDING) * 2 + s.height += (NOTE_MARGIN + NOTE_PADDING) * 2 + + // HACK lets include the actor's padding + extraWidth = 2 * ACTOR_MARGIN + + if (s.placement == PLACEMENT.LEFTOF) { + b = s.actor.index + a = b - 1 + } else if (s.placement == PLACEMENT.RIGHTOF) { + a = s.actor.index + b = a + 1 + } else if (s.placement == PLACEMENT.OVER && s.hasManyActors()) { + // Over multiple actors + a = Math.min(s.actor[0].index, s.actor[1].index) + b = Math.max(s.actor[0].index, s.actor[1].index) + + // We don't need our padding, and we want to overlap + extraWidth = -(NOTE_PADDING * 2 + NOTE_OVERLAP * 2) + } else if (s.placement == PLACEMENT.OVER) { + // Over single actor + a = s.actor.index + actorEnsureDistance(a - 1, a, s.width / 2) + actorEnsureDistance(a, a + 1, s.width / 2) + this.signalsHeight_ += s.height + + return // Bail out early + } + } else { + throw new Error('Unhandled signal type:' + s.type) + } + + actorEnsureDistance(a, b, s.width + extraWidth) + this.signalsHeight_ += s.height + }, + this + ) + + // Re-jig the positions + var actorsX = 0 + _.each( + actors, + function (a) { + a.x = Math.max(actorsX, a.x) + + // TODO This only works if we loop in sequence, 0, 1, 2, etc + _.each(a.distances, function (distance, b) { + // lodash (and possibly others) do not like sparse arrays + // so sometimes they return undefined + if (typeof distance == 'undefined') { + return + } + + b = actors[b] + distance = Math.max(distance, a.width / 2, b.width / 2) + b.x = Math.max(b.x, a.x + a.width / 2 + distance - b.width / 2) + }) + + actorsX = a.x + a.width + a.paddingRight + }, + this + ) + + diagram.width = Math.max(actorsX, diagram.width) + + // TODO Refactor a little + diagram.width += 2 * DIAGRAM_MARGIN + diagram.height += 2 * DIAGRAM_MARGIN + 2 * this.actorsHeight_ + this.signalsHeight_ + + return this + }, + + // TODO Instead of one textBBox function, create a function for each element type, e.g + // layout_title, layout_actor, etc that returns it's bounding box + textBBox: function (text, font) {}, + + drawTitle: function () { + var title = this.title_ + if (title) { + this.drawTextBox(title, title.message, TITLE_MARGIN, TITLE_PADDING, this.font_, ALIGN_LEFT) + } + }, + + drawActors: function (offsetY) { + var y = offsetY + _.each( + this.diagram.actors, + function (a) { + // Top box + this.drawActor(a, y, this.actorsHeight_) + + // Bottom box + this.drawActor(a, y + this.actorsHeight_ + this.signalsHeight_, this.actorsHeight_) + + // Veritical line + var aX = getCenterX(a) + this.drawLine( + aX, + y + this.actorsHeight_ - ACTOR_MARGIN, + aX, + y + this.actorsHeight_ + ACTOR_MARGIN + this.signalsHeight_ + ) + }, + this + ) + }, + + drawActor: function (actor, offsetY, height) { + actor.y = offsetY + actor.height = height + this.drawTextBox(actor, actor.name, ACTOR_MARGIN, ACTOR_PADDING, this.font_, ALIGN_CENTER) + }, + + drawSignals: function (offsetY) { + var y = offsetY + _.each( + this.diagram.signals, + function (s) { + // TODO Add debug mode, that draws padding/margin box + if (s.type == 'Signal') { + if (s.isSelf()) { + this.drawSelfSignal(s, y) + } else { + this.drawSignal(s, y) + } + } else if (s.type == 'Note') { + this.drawNote(s, y) + } + + y += s.height + }, + this + ) + }, + + drawSelfSignal: function (signal, offsetY) { + assert(signal.isSelf(), 'signal must be a self signal') + + var textBB = signal.textBB + var aX = getCenterX(signal.actorA) + + var x = aX + SELF_SIGNAL_WIDTH + SIGNAL_PADDING + var y = offsetY + SIGNAL_PADDING + signal.height / 2 + textBB.y + + this.drawText(x, y, signal.message, this.font_, ALIGN_LEFT) + + var y1 = offsetY + SIGNAL_MARGIN + SIGNAL_PADDING + var y2 = y1 + signal.height - 2 * SIGNAL_MARGIN - SIGNAL_PADDING + + // Draw three lines, the last one with a arrow + this.drawLine(aX, y1, aX + SELF_SIGNAL_WIDTH, y1, signal.linetype) + this.drawLine(aX + SELF_SIGNAL_WIDTH, y1, aX + SELF_SIGNAL_WIDTH, y2, signal.linetype) + this.drawLine(aX + SELF_SIGNAL_WIDTH, y2, aX, y2, signal.linetype, signal.arrowtype) + }, + + drawSignal: function (signal, offsetY) { + var aX = getCenterX(signal.actorA) + var bX = getCenterX(signal.actorB) + + // Mid point between actors + var x = (bX - aX) / 2 + aX + var y = offsetY + SIGNAL_MARGIN + 2 * SIGNAL_PADDING + + // Draw the text in the middle of the signal + this.drawText(x, y, signal.message, this.font_, ALIGN_CENTER) + + // Draw the line along the bottom of the signal + y = offsetY + signal.height - SIGNAL_MARGIN - SIGNAL_PADDING + this.drawLine(aX, y, bX, y, signal.linetype, signal.arrowtype) + }, + + drawNote: function (note, offsetY) { + note.y = offsetY + var actorA = note.hasManyActors() ? note.actor[0] : note.actor + var aX = getCenterX(actorA) + switch (note.placement) { + case PLACEMENT.RIGHTOF: + note.x = aX + ACTOR_MARGIN + break + case PLACEMENT.LEFTOF: + note.x = aX - ACTOR_MARGIN - note.width + break + case PLACEMENT.OVER: + if (note.hasManyActors()) { + var bX = getCenterX(note.actor[1]) + var overlap = NOTE_OVERLAP + NOTE_PADDING + note.x = Math.min(aX, bX) - overlap + note.width = Math.max(aX, bX) + overlap - note.x + } else { + note.x = aX - note.width / 2 + } + break + default: + throw new Error('Unhandled note placement: ' + note.placement) + } + return this.drawTextBox(note, note.message, NOTE_MARGIN, NOTE_PADDING, this.font_, ALIGN_LEFT) + }, + + /** + * Draw text surrounded by a box + */ + drawTextBox: function (box, text, margin, padding, font, align) { + var x = box.x + margin + var y = box.y + margin + var w = box.width - 2 * margin + var h = box.height - 2 * margin + + // Draw inner box + this.drawRect(x, y, w, h) + + // Draw text (in the center) + if (align == ALIGN_CENTER) { + x = getCenterX(box) + y = getCenterY(box) + } else { + x += padding + y += padding + } + + return this.drawText(x, y, text, font, align) + } +}) + +/** js sequence diagrams + * https://bramp.github.io/js-sequence-diagrams/ + * (c) 2012-2017 Andrew Brampton (bramp.net) + * Simplified BSD license. + */ +/*global Diagram, Snap, WebFont _ */ +// TODO Move defintion of font onto the , so it can easily be override at each level +if (typeof Snap != 'undefined') { + var xmlns = 'http://www.w3.org/2000/svg' + + var LINE = { + stroke: '#000000', + 'stroke-width': 2, // BUG TODO This gets set as a style, not as a attribute. Look at eve.on("snap.util.attr"... + fill: 'none' + } + + var RECT = { + stroke: '#000000', + 'stroke-width': 2, + fill: '#fff' + } + + var LOADED_FONTS = {} + + /****************** + * SnapTheme + ******************/ + + var SnapTheme = function (diagram, options, resume) { + _.defaults(options, { + 'css-class': 'simple', + 'font-size': 16, + 'font-family': 'Andale Mono, monospace' + }) + + this.init(diagram, options, resume) + } + + _.extend(SnapTheme.prototype, BaseTheme.prototype, { + init: function (diagram, options, resume) { + BaseTheme.prototype.init.call(this, diagram) + + this.paper_ = undefined + this.cssClass_ = options['css-class'] || undefined + this.font_ = { + 'font-size': options['font-size'], + 'font-family': options['font-family'] + } + + var a = (this.arrowTypes_ = {}) + a[ARROWTYPE.FILLED] = 'Block' + a[ARROWTYPE.OPEN] = 'Open' + + var l = (this.lineTypes_ = {}) + l[LINETYPE.SOLID] = '' + l[LINETYPE.DOTTED] = '6,2' + + var that = this + this.waitForFont(function () { + resume(that) + }) + }, + + // Wait for loading of the font + waitForFont: function (callback) { + var fontFamily = this.font_['font-family'] + + if (typeof WebFont == 'undefined') { + throw new Error('WebFont is required (https://github.com/typekit/webfontloader).') + } + + if (LOADED_FONTS[fontFamily]) { + // If already loaded, just return instantly. + callback() + return + } + + WebFont.load({ + custom: { + families: [fontFamily] // TODO replace this with something that reads the css + }, + classes: false, // No need to place classes on the DOM, just use JS Events + active: function () { + LOADED_FONTS[fontFamily] = true + callback() + }, + inactive: function () { + // If we fail to fetch the font, still continue. + LOADED_FONTS[fontFamily] = true + callback() + } + }) + }, + + addDescription: function (svg, description) { + var desc = document.createElementNS(xmlns, 'desc') + desc.appendChild(document.createTextNode(description)) + svg.appendChild(desc) + }, + + setupPaper: function (container) { + // Container must be a SVG element. We assume it's a div, so lets create a SVG and insert + var svg = document.createElementNS(xmlns, 'svg') + container.appendChild(svg) + + this.addDescription(svg, this.diagram.title || '') + + this.paper_ = Snap(svg) + this.paper_.addClass('sequence') + + if (this.cssClass_) { + this.paper_.addClass(this.cssClass_) + } + + this.beginGroup() + + // TODO Perhaps only include the markers if we actually use them. + var a = (this.arrowMarkers_ = {}) + var arrow = this.paper_.path('M 0 0 L 5 2.5 L 0 5 z') + a[ARROWTYPE.FILLED] = arrow.marker(0, 0, 5, 5, 5, 2.5).attr({ id: 'markerArrowBlock' }) + + arrow = this.paper_.path('M 9.6,8 1.92,16 0,13.7 5.76,8 0,2.286 1.92,0 9.6,8 z') + a[ARROWTYPE.OPEN] = arrow + .marker(0, 0, 9.6, 16, 9.6, 8) + .attr({ markerWidth: '4', id: 'markerArrowOpen' }) + }, + + layout: function () { + BaseTheme.prototype.layout.call(this) + this.paper_.attr({ + width: this.diagram.width + 'px', + height: this.diagram.height + 'px' + }) + }, + + textBBox: function (text, font) { + // TODO getBBox will return the bounds with any whitespace/kerning. This makes some of our aligments screwed up + var t = this.createText(text, font) + var bb = t.getBBox() + t.remove() + return bb + }, + + // For each drawn element, push onto the stack, so it can be wrapped in a single outer element + pushToStack: function (element) { + this._stack.push(element) + return element + }, + + // Begin a group of elements + beginGroup: function () { + this._stack = [] + }, + + // Finishes the group, and returns the element + finishGroup: function () { + var g = this.paper_.group.apply(this.paper_, this._stack) + this.beginGroup() // Reset the group + return g + }, + + createText: function (text, font) { + text = _.invoke(text.split('\n'), 'trim') + var t = this.paper_.text(0, 0, text) + t.attr(font || {}) + if (text.length > 1) { + // Every row after the first, set tspan to be 1.2em below the previous line + t.selectAll('tspan:nth-child(n+2)').attr({ + dy: '1.2em', + x: 0 + }) + } + + return t + }, + + drawLine: function (x1, y1, x2, y2, linetype, arrowhead) { + var line = this.paper_.line(x1, y1, x2, y2).attr(LINE) + if (linetype !== undefined) { + line.attr('strokeDasharray', this.lineTypes_[linetype]) + } + if (arrowhead !== undefined) { + line.attr('markerEnd', this.arrowMarkers_[arrowhead]) + } + return this.pushToStack(line) + }, + + drawRect: function (x, y, w, h) { + var rect = this.paper_.rect(x, y, w, h).attr(RECT) + return this.pushToStack(rect) + }, + + /** + * Draws text with a optional white background + * x,y (int) x,y top left point of the text, or the center of the text (depending on align param) + * text (string) text to print + * font (Object) + * align (string) ALIGN_LEFT or ALIGN_CENTER + */ + drawText: function (x, y, text, font, align) { + var t = this.createText(text, font) + var bb = t.getBBox() + + if (align == ALIGN_CENTER) { + x = x - bb.width / 2 + y = y - bb.height / 2 + } + + // Now move the text into place + // `y - bb.y` because text(..) is positioned from the baseline, so this moves it down. + t.attr({ x: x - bb.x, y: y - bb.y }) + t.selectAll('tspan').attr({ x: x }) + + this.pushToStack(t) + return t + }, + + drawTitle: function () { + this.beginGroup() + BaseTheme.prototype.drawTitle.call(this) + return this.finishGroup().addClass('title') + }, + + drawActor: function (actor, offsetY, height) { + this.beginGroup() + BaseTheme.prototype.drawActor.call(this, actor, offsetY, height) + return this.finishGroup().addClass('actor') + }, + + drawSignal: function (signal, offsetY) { + this.beginGroup() + BaseTheme.prototype.drawSignal.call(this, signal, offsetY) + return this.finishGroup().addClass('signal') + }, + + drawSelfSignal: function (signal, offsetY) { + this.beginGroup() + BaseTheme.prototype.drawSelfSignal.call(this, signal, offsetY) + return this.finishGroup().addClass('signal') + }, + + drawNote: function (note, offsetY) { + this.beginGroup() + BaseTheme.prototype.drawNote.call(this, note, offsetY) + return this.finishGroup().addClass('note') + } + }) + + /****************** + * SnapHandTheme + ******************/ + + var SnapHandTheme = function (diagram, options, resume) { + _.defaults(options, { + 'css-class': 'hand', + 'font-size': 16, + 'font-family': 'danielbd' + }) + + this.init(diagram, options, resume) + } + + // Take the standard SnapTheme and make all the lines wobbly + _.extend(SnapHandTheme.prototype, SnapTheme.prototype, { + drawLine: function (x1, y1, x2, y2, linetype, arrowhead) { + var line = this.paper_.path(handLine(x1, y1, x2, y2)).attr(LINE) + if (linetype !== undefined) { + line.attr('strokeDasharray', this.lineTypes_[linetype]) + } + if (arrowhead !== undefined) { + line.attr('markerEnd', this.arrowMarkers_[arrowhead]) + } + return this.pushToStack(line) + }, + + drawRect: function (x, y, w, h) { + var rect = this.paper_.path(handRect(x, y, w, h)).attr(RECT) + return this.pushToStack(rect) + } + }) + + registerTheme('snapSimple', SnapTheme) + registerTheme('snapHand', SnapHandTheme) +} + +/** js sequence diagrams + * https://bramp.github.io/js-sequence-diagrams/ + * (c) 2012-2017 Andrew Brampton (bramp.net) + * Simplified BSD license. + */ +/*global Diagram, _ */ + +if (typeof Raphael == 'undefined' && typeof Snap == 'undefined') { + throw new Error('Raphael or Snap.svg is required to be included.') +} + +if (_.isEmpty(Diagram.themes)) { + // If you are using stock js-sequence-diagrams you should never see this. This only + // happens if you have removed the built in themes. + throw new Error('No themes were registered. Please call registerTheme(...).') +} + +// Set the default hand/simple based on which theme is available. +Diagram.themes.hand = Diagram.themes.snapHand || Diagram.themes.raphaelHand +Diagram.themes.simple = Diagram.themes.snapSimple || Diagram.themes.raphaelSimple + +/* Draws the diagram. Creates a SVG inside the container + * container (HTMLElement|string) DOM element or its ID to draw on + * options (Object) + */ +Diagram.prototype.drawSVG = function (container, options) { + var defaultOptions = { + theme: 'hand' + } + + options = _.defaults(options || {}, defaultOptions) + + if (!(options.theme in Diagram.themes)) { + throw new Error('Unsupported theme: ' + options.theme) + } + + // TODO Write tests for this check + var div = _.isString(container) ? document.getElementById(container) : container + if (div === null || !div.tagName) { + throw new Error('Invalid container: ' + container) + } + + var Theme = Diagram.themes[options.theme] + new Theme(this, options, function (drawing) { + drawing.draw(div) + }) +} // end of drawSVG + +export default Diagram diff --git a/packages/muya/src/utils/diagram/sequence/sequence-diagram.css b/packages/muya/src/utils/diagram/sequence/sequence-diagram.css new file mode 100755 index 0000000000..244b80da82 --- /dev/null +++ b/packages/muya/src/utils/diagram/sequence/sequence-diagram.css @@ -0,0 +1,12 @@ +/** js sequence diagrams + * https://bramp.github.io/js-sequence-diagrams/ + * (c) 2012-2017 Andrew Brampton (bramp.net) + * Simplified BSD license. + */ +@font-face { + font-family: 'danielbd'; + src: url('danielbd.woff2') format('woff2'), + url('danielbd.woff') format('woff'); + font-weight: normal; + font-style: normal; +} diff --git a/packages/muya/src/utils/marked/getHighlightHtml.ts b/packages/muya/src/utils/marked/getHighlightHtml.ts index ce05e85523..ae7f3707c4 100644 --- a/packages/muya/src/utils/marked/getHighlightHtml.ts +++ b/packages/muya/src/utils/marked/getHighlightHtml.ts @@ -14,6 +14,8 @@ const DIAGRAM_TYPE = [ 'mermaid', 'plantuml', 'vega-lite', + 'flowchart', + 'sequence', ]; function highlight(code: string, lang: string) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80137411b6..d4c0b94d11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -319,6 +319,9 @@ importers: fast-diff: specifier: ^1.3.0 version: 1.3.0 + flowchart.js: + specifier: ^1.18.0 + version: 1.18.0 fuse.js: specifier: ^7.3.0 version: 7.3.0 @@ -361,9 +364,15 @@ importers: snabbdom-to-html: specifier: ^7.1.0 version: 7.1.0 + snapsvg-cjs: + specifier: ^0.0.6 + version: 0.0.6(eve@0.5.4) turndown: specifier: ^7.2.4 version: 7.2.4 + underscore: + specifier: ^1.13.8 + version: 1.13.8 vega: specifier: ^6.2.0 version: 6.2.0 @@ -373,6 +382,9 @@ importers: vega-lite: specifier: ^6.4.3 version: 6.4.3(vega@6.2.0) + webfontloader: + specifier: ^1.6.28 + version: 1.6.28 devDependencies: '@antfu/eslint-config': specifier: ^9.0.0 @@ -392,6 +404,12 @@ importers: '@types/turndown': specifier: ^5.0.4 version: 5.0.6 + '@types/underscore': + specifier: ^1.13.0 + version: 1.13.0 + '@types/webfontloader': + specifier: ^1.6.38 + version: 1.6.38 '@typescript-eslint/parser': specifier: ^8.59.4 version: 8.60.0(eslint@10.4.0(jiti@2.7.0))(typescript@6.0.3) @@ -3825,6 +3843,9 @@ packages: '@types/turndown@5.0.6': resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} + '@types/underscore@1.13.0': + resolution: {integrity: sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -13810,6 +13831,8 @@ snapshots: '@types/turndown@5.0.6': {} + '@types/underscore@1.13.0': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -19748,7 +19771,7 @@ snapshots: dependencies: htmlparser2: 3.10.1 postcss: 8.5.15 - postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) + postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) postcss-image-set-function@8.0.0(postcss@8.5.15): dependencies: @@ -19760,7 +19783,7 @@ snapshots: dependencies: '@babel/core': 7.29.0 postcss: 8.5.15 - postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) + postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) transitivePeerDependencies: - supports-color @@ -19785,7 +19808,7 @@ snapshots: postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15): dependencies: postcss: 8.5.15 - postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) + postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) remark: 10.0.1 unist-util-find-all-after: 1.0.5 @@ -19955,7 +19978,7 @@ snapshots: lodash: 4.18.1 postcss: 8.5.15 - postcss-syntax@0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15): + postcss-syntax@0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15): dependencies: postcss: 8.5.15 optionalDependencies: @@ -21170,7 +21193,7 @@ snapshots: postcss-sass: 0.3.5 postcss-scss: 2.1.1 postcss-selector-parser: 3.1.2 - postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) + postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) postcss-value-parser: 3.3.1 resolve-from: 4.0.0 signal-exit: 3.0.7 From f8b8638d7f69862a5654d2ad9cbb4392a6da5273 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 12:45:18 +0800 Subject: [PATCH 05/17] feat(muya): add ImagePathPicker floating autocomplete UI (#4386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): add ImagePathPicker floating autocomplete UI Port the legacy `imagePicker` plugin (packages/muyajs/lib/ui/imagePicker) to the new TypeScript engine so the desktop migration off packages/muyajs retains the local image-path autocomplete dropdown. - Add `ImagePathPicker` (BaseScrollFloat subclass, pluginName "imagePathPicker") that renders a scrollable, keyboard-navigable list of path suggestions on the `muya-image-picker` event and reports the choice through the supplied callback. - Wire it into ImageEditTool: the src input now calls the new `imagePathAutoComplete(src)` option on keyup and dispatches `muya-image-picker`; keydown routes arrows/Tab/Enter to the open picker. The picker is dismissed when the tool hides. All existing ImageEditTool behavior is preserved. - Export `ImagePathPicker` + `IImagePathSuggestion` from the package entry. - Add happy-dom vitest specs for the picker render/navigation/selection and for the ImageEditTool autocomplete wiring. Icons are rendered via font-icon CSS classes from `iconClass` (the new engine ships no inline SVG assets); see PR notes for the placeholder. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(muya): address ImagePathPicker review feedback (#4386) Five fixes from the Copilot review: - imageEditTool keydown: stopPropagation on Arrow/Tab so the editor's BaseScrollFloat keydown handler (bound on muya.domNode) does not also step the picker, which advanced the active item twice per keypress (the Enter case already stopped propagation). - imageEditTool autocomplete: guard against out-of-order resolution with a monotonic sequence counter (fast typing could emit stale suggestions), and derive the selected path's directory prefix from the CURRENT input value rather than the value captured on keyup. - imagePicker CSS: explicitly set justify-content: flex-start to override codeBlockLanguageSelector's global `.mu-list-picker .item` justify-content: space-between, which spaced icon/text apart. - imagePicker render/getItemElement: key items by a stable data-index instead of data-label (file names can contain quotes/brackets — unsafe in an attribute selector — and duplicate basenames collided). Test updated. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- packages/muya/src/index.ts | 2 + .../imageEditToolAutocomplete.spec.ts | 110 +++++++++++++ packages/muya/src/ui/imageEditTool/index.ts | 155 +++++++++++++++++- .../imagePicker/__tests__/imagePicker.spec.ts | 130 +++++++++++++++ packages/muya/src/ui/imagePicker/index.css | 64 ++++++++ packages/muya/src/ui/imagePicker/index.ts | 134 +++++++++++++++ 6 files changed, 587 insertions(+), 8 deletions(-) create mode 100644 packages/muya/src/ui/imageEditTool/__tests__/imageEditToolAutocomplete.spec.ts create mode 100644 packages/muya/src/ui/imagePicker/__tests__/imagePicker.spec.ts create mode 100644 packages/muya/src/ui/imagePicker/index.css create mode 100644 packages/muya/src/ui/imagePicker/index.ts diff --git a/packages/muya/src/index.ts b/packages/muya/src/index.ts index 28425ac404..aba40c6f3c 100644 --- a/packages/muya/src/index.ts +++ b/packages/muya/src/index.ts @@ -14,6 +14,8 @@ export { CodeBlockLanguageSelector } from './ui/codeBlockLanguageSelector'; export { EmojiSelector } from './ui/emojiSelector'; export { FootnoteTool } from './ui/footnoteTool'; export { ImageEditTool } from './ui/imageEditTool'; +export { ImagePathPicker } from './ui/imagePicker'; +export type { IImagePathSuggestion } from './ui/imagePicker'; export { ImageResizeBar } from './ui/imageResizeBar'; export { ImageToolBar } from './ui/imageToolbar'; export { InlineFormatToolbar } from './ui/inlineFormatToolbar'; diff --git a/packages/muya/src/ui/imageEditTool/__tests__/imageEditToolAutocomplete.spec.ts b/packages/muya/src/ui/imageEditTool/__tests__/imageEditToolAutocomplete.spec.ts new file mode 100644 index 0000000000..6ba590cee2 --- /dev/null +++ b/packages/muya/src/ui/imageEditTool/__tests__/imageEditToolAutocomplete.spec.ts @@ -0,0 +1,110 @@ +// @vitest-environment happy-dom +import type { Muya } from '../../../muya'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { ImageEditTool } from '..'; +import EventCenter from '../../../event'; + +// Verifies the ImageEditTool ↔ ImagePathPicker wiring: as the user types in +// the image src input, the tool should call the host's `imagePathAutoComplete` +// hook and re-dispatch the result through the `muya-image-picker` event that +// the floating picker subscribes to. +// +// We run BaseFloat for real (so the src input is actually rendered) but mock +// the slice of Muya the tool touches. We do NOT call init() or build a block +// tree — the src input render path only needs i18n + the muya-image-selector +// payload. + +function makeFakeMuya(imagePathAutoComplete?: (src: string) => Promise): { + muya: Muya; + eventCenter: EventCenter; +} { + const eventCenter = new EventCenter(); + const editorDomNode = document.createElement('div'); + const editorWrapper = document.createElement('div'); + editorWrapper.appendChild(editorDomNode); + document.body.appendChild(editorWrapper); + + const muya = { + domNode: editorDomNode, + eventCenter, + i18n: { t: (s: string) => s }, + ui: { shownFloat: new Set() }, + options: { imagePathAutoComplete }, + } as unknown as Muya; + + return { muya, eventCenter }; +} + +function stubReference(): HTMLElement { + const el = document.createElement('span'); + el.getBoundingClientRect = () => + ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0, toJSON: () => '' }) as DOMRect; + document.body.appendChild(el); + return el; +} + +function openTool(eventCenter: EventCenter, src: string) { + eventCenter.emit('muya-image-selector', { + block: { replaceImage: vi.fn() }, + reference: stubReference(), + imageInfo: { imageId: 'id', token: { attrs: { src, alt: '', title: '' } } }, + }); +} + +async function nextTick() { + await new Promise(resolve => setTimeout(resolve, 0)); +} + +describe('imageEditTool — imagePathAutoComplete wiring', () => { + let tool: ImageEditTool; + + beforeEach(() => { + document.body.innerHTML = ''; + }); + + afterEach(() => { + tool?.hide(); + vi.restoreAllMocks(); + }); + + it('calls imagePathAutoComplete with the current src value and dispatches muya-image-picker on keyup', async () => { + const suggestions = [{ text: 'photo.png', iconClass: 'icon-image' }]; + const autocomplete = vi.fn().mockResolvedValue(suggestions); + const { muya, eventCenter } = makeFakeMuya(autocomplete); + tool = new ImageEditTool(muya, { imagePathAutoComplete: autocomplete } as never); + + const pickerEvents: unknown[] = []; + eventCenter.subscribe('muya-image-picker', (payload: unknown) => { + pickerEvents.push(payload); + }); + + openTool(eventCenter, '/some/dir/ph'); + + const input = tool.container!.querySelector('input.src') as HTMLInputElement; + expect(input).not.toBeNull(); + + input.dispatchEvent(new KeyboardEvent('keyup', { key: 'h' })); + await nextTick(); + + expect(autocomplete).toHaveBeenCalledWith('/some/dir/ph'); + expect(pickerEvents.length).toBe(1); + const payload = pickerEvents[0] as { list: unknown[]; reference: HTMLElement }; + expect(payload.list).toEqual(suggestions); + expect(payload.reference).toBe(input); + }); + + it('does not call imagePathAutoComplete when the hook is absent', async () => { + const { muya, eventCenter } = makeFakeMuya(undefined); + tool = new ImageEditTool(muya); + + const pickerSpy = vi.fn(); + eventCenter.subscribe('muya-image-picker', pickerSpy); + + openTool(eventCenter, '/some/dir/ph'); + const input = tool.container!.querySelector('input.src') as HTMLInputElement; + input.dispatchEvent(new KeyboardEvent('keyup', { key: 'h' })); + await nextTick(); + + expect(pickerSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/muya/src/ui/imageEditTool/index.ts b/packages/muya/src/ui/imageEditTool/index.ts index ace8dfcdc0..bed2672fce 100644 --- a/packages/muya/src/ui/imageEditTool/index.ts +++ b/packages/muya/src/ui/imageEditTool/index.ts @@ -2,14 +2,16 @@ import type { VNode } from 'snabbdom'; import type Format from '../../block/base/format'; import type { Muya } from '../../index'; import type { ImageToken } from '../../inlineRenderer/types'; +import type { IImagePathSuggestion } from '../imagePicker'; import type { IBaseOptions } from '../types'; import { EVENT_KEYS, isWin, URL_REG } from '../../config'; import { getUniqueId, isHTMLInputElement, isKeyboardEvent } from '../../utils'; -import { query } from '../../utils/dom'; +import { query } from '../../utils/dom'; import { getImageInfo, getImageSrc } from '../../utils/image'; import { h, patch } from '../../utils/snabbdom'; import BaseFloat from '../baseFloat'; +import { ImagePathPicker } from '../imagePicker'; import './index.css'; /** @@ -28,8 +30,15 @@ interface IState { * Image edit tool options */ type Options = { - /** Custom image path picker function */ + /** Custom image path picker function (one-shot native file dialog) */ imagePathPicker?: () => Promise; + /** + * Local image path autocomplete hook. Given the current src input value, + * returns a list of path suggestions to show in the floating + * {@link ImagePathPicker}. Mirrors the legacy `imagePathAutoComplete` + * option — typically backed by a filesystem directory listing. + */ + imagePathAutoComplete?: (src: string) => Promise; /** Image upload action handler */ imageAction?: (state: IState) => Promise; } & IBaseOptions; @@ -70,6 +79,9 @@ export class ImageEditTool extends BaseFloat { /** The block containing the image */ private _block: Format | null = null; + /** Monotonic counter used to drop out-of-order imagePathAutoComplete responses */ + private _autoCompleteSeq = 0; + /** Current editing state */ private _state: IState = { alt: '', @@ -161,16 +173,131 @@ export class ImageEditTool extends BaseFloat { } /** - * Handle Enter key press to confirm changes + * Locate the floating image-path picker if it is currently open. + * Plugins are registered privately on Muya, so we resolve the instance via + * the shared `ui.shownFloat` registry (the same pattern tableColumnToolbar + * uses to find the format picker). Returns null when the picker plugin is + * not registered or not currently shown. + */ + private _getOpenImagePathPicker(): ImagePathPicker | null { + for (const tool of this.muya.ui.shownFloat) { + if (tool instanceof ImagePathPicker && tool.status) + return tool; + } + return null; + } + + /** + * Handle keydown on the src input. + * When the autocomplete picker is open, arrow keys / Tab / Enter drive the + * picker (navigate + choose) instead of confirming. Otherwise Enter + * confirms the change, mirroring the legacy `srcInputKeyDown` behavior. * @param event - Keyboard event */ - private _handleEnter(event: Event) { + private _handleSrcKeyDown(event: Event) { if (!isKeyboardEvent(event)) return; - event.stopPropagation(); - if (event.key === EVENT_KEYS.Enter) - this._handleConfirm(); + const picker = this._getOpenImagePathPicker(); + if (!picker) { + if (event.key === EVENT_KEYS.Enter) { + event.stopPropagation(); + this._handleConfirm(); + } + return; + } + + switch (event.key) { + case EVENT_KEYS.ArrowUp: + event.preventDefault(); + // Stop the editor's BaseScrollFloat keydown handler (bound on + // muya.domNode) from also stepping the picker — otherwise the + // active item advances twice per keypress. + event.stopPropagation(); + picker.step('previous'); + break; + + case EVENT_KEYS.ArrowDown: + case EVENT_KEYS.Tab: + event.preventDefault(); + event.stopPropagation(); + picker.step('next'); + break; + + case EVENT_KEYS.Enter: + event.preventDefault(); + event.stopPropagation(); + if (picker.activeItem) + picker.selectItem(picker.activeItem); + break; + + default: + break; + } + } + + /** + * Handle keyup on the src input. + * Re-queries the `imagePathAutoComplete` hook (debounced via the browser's + * natural keystroke cadence) and dispatches `muya-image-picker` so the + * floating picker refreshes its suggestions. Navigation keys are ignored so + * they don't re-trigger a fetch while the user is moving through the list. + * @param event - Keyboard event + */ + private async _handleSrcKeyUp(event: Event) { + if (!isKeyboardEvent(event) || !this.options.imagePathAutoComplete) + return; + + const { key } = event; + if ( + key === EVENT_KEYS.ArrowUp + || key === EVENT_KEYS.ArrowDown + || key === EVENT_KEYS.Tab + || (key === EVENT_KEYS.Enter + && !this._state.src.endsWith('/') + && !this._state.src.endsWith('\\')) + ) { + return; + } + + const { eventCenter } = this.muya; + const value = this._state.src; + const reference = this.container + ? query('input.src', this.container) + : null; + + // Write the chosen suggestion back into the src input. The new value is + // the directory portion of the current path plus the chosen basename, + // matching the legacy ImageSelector autocomplete UX. + const cb = (item: IImagePathSuggestion) => { + if (!reference) + return; + + const { text } = item; + // Derive the directory prefix from the CURRENT input value — the + // user may have kept typing after the suggestions were fetched, so + // the value captured on keyup can be stale. + const current = reference.value; + let basePath = ''; + const pathSep = current.match(/(?:\/|\\)[^/\\]*$/); + if (pathSep && pathSep[0]) + basePath = current.substring(0, pathSep.index! + 1); + + const newValue = basePath + text; + const len = newValue.length; + reference.value = newValue; + this._state.src = newValue; + reference.focus(); + reference.setSelectionRange(len, len); + }; + + // Guard against out-of-order resolution: if the user types again before + // a slower earlier request resolves, drop the stale response. + const seq = ++this._autoCompleteSeq; + const list = value ? await this.options.imagePathAutoComplete(value) : []; + if (seq !== this._autoCompleteSeq) + return; + eventCenter.emit('muya-image-picker', { reference, list, cb }); } /** @@ -262,6 +389,17 @@ export class ImageEditTool extends BaseFloat { } } + /** + * Hide the tool and dismiss the autocomplete picker alongside it so a + * confirm/close never leaves a dangling suggestions dropdown. + */ + override hide() { + const picker = this._getOpenImagePathPicker(); + if (picker) + picker.hide(); + super.hide(); + } + /** * Handle click on "more" button to open file picker * Updates the src input with selected path @@ -305,7 +443,8 @@ export class ImageEditTool extends BaseFloat { on: { input: event => this._handleSrcInput(event), paste: event => this._handleSrcInput(event), - keydown: event => this._handleEnter(event), + keydown: event => this._handleSrcKeyDown(event), + keyup: event => this._handleSrcKeyUp(event), }, }); diff --git a/packages/muya/src/ui/imagePicker/__tests__/imagePicker.spec.ts b/packages/muya/src/ui/imagePicker/__tests__/imagePicker.spec.ts new file mode 100644 index 0000000000..a0ad75123d --- /dev/null +++ b/packages/muya/src/ui/imagePicker/__tests__/imagePicker.spec.ts @@ -0,0 +1,130 @@ +// @vitest-environment happy-dom +import type { Muya } from '../../../muya'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { ImagePathPicker } from '..'; +import EventCenter from '../../../event'; + +// Smoke + integration tests for the ImagePathPicker floating autocomplete UI. +// +// The picker is a BaseScrollFloat subclass driven entirely by the +// `muya-image-picker` event: the ImageEditTool fetches suggestions from the +// host's `imagePathAutoComplete` hook and dispatches them here. We mock the +// slice of Muya the picker touches (eventCenter, domNode, i18n, ui) and run +// BaseFloat/BaseScrollFloat for real so the snabbdom render path is exercised +// end-to-end in happy-dom. + +function makeFakeMuya(): { muya: Muya; eventCenter: EventCenter } { + const eventCenter = new EventCenter(); + const editorDomNode = document.createElement('div'); + const editorWrapper = document.createElement('div'); + editorWrapper.appendChild(editorDomNode); + document.body.appendChild(editorWrapper); + + const shownFloat = new Set(); + // Mirror Ui.listen so `status` flips when the float shows/hides. + eventCenter.subscribe('muya-float', (tool: unknown, status: boolean) => { + status ? shownFloat.add(tool) : shownFloat.delete(tool); + }); + + const muya = { + domNode: editorDomNode, + eventCenter, + i18n: { t: (s: string) => s }, + ui: { shownFloat }, + options: {}, + } as unknown as Muya; + + return { muya, eventCenter }; +} + +function stubReference(): HTMLElement { + const input = document.createElement('input'); + // BaseFloat computes position off the reference; happy-dom has no layout, + // so a stubbed rect keeps autoUpdate from throwing. + input.getBoundingClientRect = () => + ({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0, toJSON: () => '' }) as DOMRect; + document.body.appendChild(input); + return input; +} + +async function nextTick() { + await new Promise(resolve => setTimeout(resolve, 0)); +} + +describe('imagePathPicker — plugin shape', () => { + it('exposes a stable static pluginName so Muya.use registers it under "imagePathPicker"', () => { + expect(ImagePathPicker.pluginName).toBe('imagePathPicker'); + }); +}); + +describe('imagePathPicker — render on muya-image-picker event', () => { + let muya: Muya; + let eventCenter: EventCenter; + let picker: ImagePathPicker; + + beforeEach(() => { + ({ muya, eventCenter } = makeFakeMuya()); + picker = new ImagePathPicker(muya); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('renders one list item per suggestion and marks the first active', async () => { + const reference = stubReference(); + const list = [ + { text: 'a.png', iconClass: 'icon-image' }, + { text: 'sub', iconClass: 'icon-folder', type: 'directory' }, + ]; + + eventCenter.emit('muya-image-picker', { reference, list, cb: () => {} }); + await nextTick(); + + const items = picker.floatBox!.querySelectorAll('li.item'); + expect(items.length).toBe(2); + expect(items[0].classList.contains('active')).toBe(true); + expect(items[0].querySelector('.text')?.textContent).toBe('a.png'); + // Icon class is rendered when supplied. + expect(items[1].querySelector('.icon-wrapper span.icon-folder')).not.toBeNull(); + expect(picker.status).toBe(true); + }); + + it('hides instead of showing when the suggestion list is empty', async () => { + const reference = stubReference(); + eventCenter.emit('muya-image-picker', { reference, list: [], cb: () => {} }); + await nextTick(); + + expect(picker.status).toBe(false); + }); + + it('invokes the selection callback with the chosen item on click', async () => { + const reference = stubReference(); + const cb = vi.fn(); + const list = [{ text: 'first.png' }, { text: 'second.png' }]; + + eventCenter.emit('muya-image-picker', { reference, list, cb }); + await nextTick(); + + const second = picker.floatBox!.querySelector('[data-index="1"]') as HTMLElement; + second.click(); + + expect(cb).toHaveBeenCalledTimes(1); + expect(cb).toHaveBeenCalledWith(list[1]); + }); + + it('moves the active item with step("next") and selects it via the active item', async () => { + const reference = stubReference(); + const cb = vi.fn(); + const list = [{ text: 'one.png' }, { text: 'two.png' }, { text: 'three.png' }]; + + eventCenter.emit('muya-image-picker', { reference, list, cb }); + await nextTick(); + + picker.step('next'); + expect(picker.activeItem).toBe(list[1]); + + picker.selectItem(picker.activeItem); + expect(cb).toHaveBeenCalledWith(list[1]); + }); +}); diff --git a/packages/muya/src/ui/imagePicker/index.css b/packages/muya/src/ui/imagePicker/index.css new file mode 100644 index 0000000000..9cd40f6684 --- /dev/null +++ b/packages/muya/src/ui/imagePicker/index.css @@ -0,0 +1,64 @@ +.mu-image-picker-wrapper { + z-index: 100000; +} + +.mu-image-picker-wrapper .mu-list-picker { + box-sizing: border-box; + width: 450px; + max-height: 156px; + padding: 8px 0; + overflow-y: auto; + + font-size: 14px; +} + +.mu-image-picker-wrapper .mu-list-picker ul, +.mu-image-picker-wrapper .mu-list-picker li { + margin: 0; + padding: 0; +} + +.mu-image-picker-wrapper .mu-list-picker .item { + display: flex; + align-items: center; + + /* override codeBlockLanguageSelector's global `.mu-list-picker .item` + justify-content: space-between, which would push icon/text apart */ + justify-content: flex-start; + height: 28px; + padding: 0 10px; + + list-style: none; +} + +.mu-image-picker-wrapper .mu-list-picker:hover .active { + background: transparent; +} + +.mu-image-picker-wrapper .mu-list-picker .item:hover, +.mu-image-picker-wrapper .mu-list-picker .item.active { + background-color: var(--float-hover-color); +} + +.mu-image-picker-wrapper .mu-list-picker .item .icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; +} + +.mu-image-picker-wrapper .mu-list-picker .item .icon-wrapper span::before { + font-size: 14px; +} + +.mu-image-picker-wrapper .mu-list-picker .item .text { + flex: 1; + overflow: hidden; + + color: var(--editor-color); + font-size: 14px; + line-height: 28px; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/packages/muya/src/ui/imagePicker/index.ts b/packages/muya/src/ui/imagePicker/index.ts new file mode 100644 index 0000000000..e983fe89f8 --- /dev/null +++ b/packages/muya/src/ui/imagePicker/index.ts @@ -0,0 +1,134 @@ +import type { VNode } from 'snabbdom'; +import type { Muya } from '../../index'; +import { query } from '../../utils/dom'; + +import { h, patch } from '../../utils/snabbdom'; +import BaseScrollFloat from '../baseScrollFloat'; + +import './index.css'; + +/** + * A single path suggestion produced by the `imagePathAutoComplete` hook. + * `text` is the basename rendered (and written back into the src input); + * `iconClass` selects a font-icon class, `type` distinguishes files from + * directories. Extra keys are tolerated so callers can carry metadata. + */ +export interface IImagePathSuggestion { + text: string; + iconClass?: string; + type?: string; + [key: string]: unknown; +} + +/** Payload of the `muya-image-picker` event the ImageEditTool dispatches. */ +interface IImagePickerEvent { + reference: HTMLElement | null; + list: IImagePathSuggestion[]; + cb: (item: IImagePathSuggestion) => void; +} + +const defaultOptions = { + placement: 'bottom-start' as const, + offsetOptions: { + mainAxis: 0, + crossAxis: 0, + alignmentAxis: 0, + }, + showArrow: false, +}; + +/** + * Floating autocomplete dropdown that suggests local image file paths as the + * user edits an image's `src` in the {@link ImageEditTool}. It is a faithful + * port of the legacy `packages/muyajs/lib/ui/imagePicker` plugin: it listens + * for the `muya-image-picker` event, renders a scrollable filtered list, and + * supports arrow-key navigation plus Enter/click to choose. The chosen path is + * written back through the callback supplied in the event payload. + * + * The list itself is produced by the host application via the + * `imagePathAutoComplete` option on the ImageEditTool — muya only renders the + * result and reports the selection. + */ +export class ImagePathPicker extends BaseScrollFloat { + static pluginName = 'imagePathPicker'; + + private _oldVNode: VNode | null = null; + public override renderArray: IImagePathSuggestion[] = []; + public override activeItem: IImagePathSuggestion | null = null; + + constructor(muya: Muya, options = {}) { + const name = 'mu-list-picker'; + const opts = Object.assign({}, defaultOptions, options); + super(muya, name, opts); + this.floatBox!.classList.add('mu-image-picker-wrapper'); + this.listen(); + } + + override listen() { + super.listen(); + const { eventCenter } = this.muya; + eventCenter.on('muya-image-picker', ({ reference, list, cb }: IImagePickerEvent) => { + if (reference && list.length) { + this.show(reference, cb); + this.renderArray = list; + this.activeItem = list[0]; + this.render(); + } + else { + this.hide(); + } + }); + } + + render() { + const { renderArray, _oldVNode: oldVNode, scrollElement, activeItem } = this; + const children = renderArray.map((item, index) => { + const { text, iconClass } = item; + // Icons are font-icon classes (parity placeholder for the legacy + // inline SVGs — see PR notes). When the host omits `iconClass` we + // simply render the text without an icon. + const iconContent = iconClass + ? [h('div.icon-wrapper', h(`span.${iconClass}`))] + : []; + const textEle = h('div.text', text); + const selector = activeItem === item ? 'li.item.active' : 'li.item'; + + return h( + selector, + { + // Index-based lookup — file names can contain quotes/brackets + // (unsafe in an attribute selector) and duplicate basenames + // would otherwise collide on a text-based attribute. + dataset: { + index: String(index), + }, + on: { + click: () => { + this.selectItem(item); + }, + }, + }, + [...iconContent, textEle], + ); + }); + + const vnode = h('ul', children); + + if (oldVNode) + patch(oldVNode, vnode); + else + patch(scrollElement!, vnode); + + this._oldVNode = vnode; + } + + getItemElement(item: IImagePathSuggestion): HTMLElement | null { + const index = this.renderArray.indexOf(item); + if (index < 0) + return null; + + return query(`[data-index="${index}"]`, this.floatBox!); + } +} + +export default ImagePathPicker; From 312182f7788d98493321a6ac5df3d7f05fafa959 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 12:53:23 +0800 Subject: [PATCH 06/17] fix(muya): unbreak lint:css (stylelint 17 compatibility) (#4388) `pnpm -C packages/muya lint:css` crashed on a clean checkout with `TypeError: Cannot read properties of undefined (reading 'unprefixed')`. Root cause: `.stylelintrc` extended `stylelint-config-rational-order/plugin`, whose v0.1.2 bundles its own `stylelint-order@2.2.1` built for `stylelint@^9`. That old rule reads a v9-shaped AST node and breaks on the `stylelint@17` AST the repo actually runs, aborting the whole lint run before it reached any file. Fix: drop the unmaintained `plugin/rational-order` plugin and instead drive the same property ordering through the maintained `stylelint-order@8` (already a devDep) via the native `order/properties-order` rule. The rule's group list is the exact rational-order expectation the old plugin generated for this repo's options (`border-in-box-model: false`, `empty-line-between-groups: true`), so the ordering intent is preserved with no change to any CSS behavior. Ordering stays `severity: warning`, matching the prior config. Removed the now-unused `stylelint-config-rational-order` devDep (prunes its old stylelint@9 / postcss@7 subtree from the lockfile). Also fixed one pre-existing genuine error the working tool finally surfaced: `font-family: 'danielbd'` -> `font-family: danielbd` in the vendored sequence-diagram `@font-face` (the standard `font-family-name-quotes` fix; identical rendering). `pnpm -C packages/muya lint:css` now completes with 0 errors. lint, lint:types, check-circular, test, and test:spec all still pass. Co-authored-by: Claude Opus 4.8 (1M context) --- packages/muya/.stylelintrc | 292 ++- packages/muya/package.json | 1 - .../diagram/sequence/sequence-diagram.css | 2 +- pnpm-lock.yaml | 1908 +---------------- 4 files changed, 284 insertions(+), 1919 deletions(-) diff --git a/packages/muya/.stylelintrc b/packages/muya/.stylelintrc index 05ccf33a41..539f731700 100644 --- a/packages/muya/.stylelintrc +++ b/packages/muya/.stylelintrc @@ -1,8 +1,288 @@ { "extends": ["stylelint-config-standard"], - "plugins": ["stylelint-order", "stylelint-config-rational-order/plugin"], + "plugins": ["stylelint-order"], "rules": { - "order/properties-order": [[], { "severity": "warning" }], + "order/properties-order": [ + [ + { + "groupName": "Special", + "emptyLineBefore": "always", + "properties": [ + "composes", + "@import", + "@extend", + "@mixin", + "@at-root" + ] + }, + { + "groupName": "Positioning", + "emptyLineBefore": "always", + "properties": [ + "position", + "top", + "right", + "bottom", + "left", + "z-index" + ] + }, + { + "groupName": "Box Model", + "emptyLineBefore": "always", + "properties": [ + "display", + "flex", + "flex-basis", + "flex-direction", + "flex-flow", + "flex-grow", + "flex-shrink", + "flex-wrap", + "grid", + "grid-area", + "grid-auto-rows", + "grid-auto-columns", + "grid-auto-flow", + "grid-gap", + "grid-row", + "grid-row-start", + "grid-row-end", + "grid-row-gap", + "grid-column", + "grid-column-start", + "grid-column-end", + "grid-column-gap", + "grid-template", + "grid-template-areas", + "grid-template-rows", + "grid-template-columns", + "gap", + "align-content", + "align-items", + "align-self", + "justify-content", + "justify-items", + "justify-self", + "order", + "float", + "clear", + "box-sizing", + "width", + "min-width", + "max-width", + "height", + "min-height", + "max-height", + "margin", + "margin-top", + "margin-right", + "margin-bottom", + "margin-left", + "padding", + "padding-top", + "padding-right", + "padding-bottom", + "padding-left", + "object-fit", + "object-position", + "overflow", + "overflow-x", + "overflow-y" + ] + }, + { + "groupName": "Typography", + "emptyLineBefore": "always", + "properties": [ + "color", + "font", + "font-weight", + "font-size", + "font-family", + "font-style", + "font-variant", + "font-size-adjust", + "font-stretch", + "font-effect", + "font-emphasize", + "font-emphasize-position", + "font-emphasize-style", + "font-smooth", + "line-height", + "direction", + "letter-spacing", + "white-space", + "text-align", + "text-align-last", + "text-transform", + "text-decoration", + "text-emphasis", + "text-emphasis-color", + "text-emphasis-style", + "text-emphasis-position", + "text-indent", + "text-justify", + "text-outline", + "text-wrap", + "text-overflow", + "text-overflow-ellipsis", + "text-overflow-mode", + "text-orientation", + "text-shadow", + "vertical-align", + "word-wrap", + "word-break", + "word-spacing", + "overflow-wrap", + "tab-size", + "hyphens", + "unicode-bidi", + "columns", + "column-count", + "column-fill", + "column-gap", + "column-rule", + "column-rule-color", + "column-rule-style", + "column-rule-width", + "column-span", + "column-width", + "page-break-after", + "page-break-before", + "page-break-inside", + "src" + ] + }, + { + "groupName": "Visual", + "emptyLineBefore": "always", + "properties": [ + "list-style", + "list-style-position", + "list-style-type", + "list-style-image", + "table-layout", + "empty-cells", + "caption-side", + "background", + "background-color", + "background-image", + "background-repeat", + "background-position", + "background-position-x", + "background-position-y", + "background-size", + "background-clip", + "background-origin", + "background-attachment", + "background-blend-mode", + "border", + "border-color", + "border-style", + "border-width", + "border-top", + "border-top-color", + "border-top-width", + "border-top-style", + "border-right", + "border-right-color", + "border-right-width", + "border-right-style", + "border-bottom", + "border-bottom-color", + "border-bottom-width", + "border-bottom-style", + "border-left", + "border-left-color", + "border-left-width", + "border-left-style", + "border-radius", + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-right-radius", + "border-bottom-left-radius", + "border-image", + "border-image-source", + "border-image-slice", + "border-image-width", + "border-image-outset", + "border-image-repeat", + "border-collapse", + "border-spacing", + "outline", + "outline-width", + "outline-style", + "outline-color", + "outline-offset", + "box-shadow", + "box-decoration-break", + "transform", + "transform-origin", + "transform-style", + "backface-visibility", + "perspective", + "perspective-origin", + "visibility", + "cursor", + "opacity", + "filter", + "isolation", + "backdrop-filter", + "mix-blend-mode" + ] + }, + { + "groupName": "Animation", + "emptyLineBefore": "always", + "properties": [ + "transition", + "transition-delay", + "transition-timing-function", + "transition-duration", + "transition-property", + "animation", + "animation-name", + "animation-duration", + "animation-play-state", + "animation-timing-function", + "animation-delay", + "animation-iteration-count", + "animation-direction", + "animation-fill-mode" + ] + }, + { + "groupName": "Misc", + "emptyLineBefore": "always", + "properties": [ + "appearance", + "content", + "clip", + "clip-path", + "counter-reset", + "counter-increment", + "resize", + "user-select", + "nav-index", + "nav-up", + "nav-right", + "nav-down", + "nav-left", + "pointer-events", + "quotes", + "touch-action", + "will-change", + "zoom", + "fill", + "fill-rule", + "clip-rule", + "stroke" + ] + } + ], + { "severity": "warning", "unspecified": "bottomAlphabetical" } + ], "declaration-empty-line-before": [ "never", { @@ -15,14 +295,6 @@ ] } ], - "plugin/rational-order": [ - true, - { - "border-in-box-model": false, - "empty-line-between-groups": true, - "severity": "warning" - } - ], "unit-no-unknown": [ true, { diff --git a/packages/muya/package.json b/packages/muya/package.json index c219e77454..ce79d543b0 100644 --- a/packages/muya/package.json +++ b/packages/muya/package.json @@ -102,7 +102,6 @@ "happy-dom": "^15.11.7", "madge": "^8.0.0", "stylelint": "^17.11.1", - "stylelint-config-rational-order": "^0.1.2", "stylelint-config-standard": "^40.0.0", "stylelint-order": "^8.1.1", "typescript": "^6.0.3", diff --git a/packages/muya/src/utils/diagram/sequence/sequence-diagram.css b/packages/muya/src/utils/diagram/sequence/sequence-diagram.css index 244b80da82..ea3e66611b 100755 --- a/packages/muya/src/utils/diagram/sequence/sequence-diagram.css +++ b/packages/muya/src/utils/diagram/sequence/sequence-diagram.css @@ -4,7 +4,7 @@ * Simplified BSD license. */ @font-face { - font-family: 'danielbd'; + font-family: danielbd; src: url('danielbd.woff2') format('woff2'), url('danielbd.woff') format('woff'); font-weight: normal; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d4c0b94d11..504321f373 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,9 +434,6 @@ importers: stylelint: specifier: ^17.11.1 version: 17.12.0(typescript@6.0.3) - stylelint-config-rational-order: - specifier: ^0.1.2 - version: 0.1.2 stylelint-config-standard: specifier: ^40.0.0 version: 40.0.0(stylelint@17.12.0(typescript@6.0.3)) @@ -2763,10 +2760,6 @@ packages: '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} - '@mrmlnc/readdir-enhanced@2.2.1': - resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} - engines: {node: '>=4'} - '@napi-rs/wasm-runtime@1.1.4': resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} peerDependencies: @@ -2865,10 +2858,6 @@ packages: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} - '@nodelib/fs.stat@1.1.3': - resolution: {integrity: sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==} - engines: {node: '>= 6'} - '@nodelib/fs.stat@2.0.5': resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} engines: {node: '>= 8'} @@ -3765,9 +3754,6 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} - '@types/glob@7.2.0': - resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -3798,10 +3784,6 @@ packages: '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - '@types/minimatch@6.0.0': - resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} - deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. - '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -3846,22 +3828,12 @@ packages: '@types/underscore@1.13.0': resolution: {integrity: sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==} - '@types/unist@2.0.11': - resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} '@types/verror@1.10.11': resolution: {integrity: sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==} - '@types/vfile-message@2.0.0': - resolution: {integrity: sha512-GpTIuDpb9u4zIO165fUy9+fXcULdD8HFRNli04GehoMVbeNq7D6OBnqSmg3lxZnC+UvgUhEWKxdKiwYUkGltIw==} - deprecated: This is a stub types definition. vfile-message provides its own type definitions, so you do not need this installed. - - '@types/vfile@3.0.2': - resolution: {integrity: sha512-b3nLFGaGkJ9rzOcuXRfHkZMdjsawuDD0ENL9fzTophtBg8FJHSGbH7daXkEpcwy3v7Xol3pAvsmlYyFhR4pqJw==} - '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -4400,10 +4372,6 @@ packages: resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} engines: {node: '>=4'} - ansi-regex@4.1.1: - resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} - engines: {node: '>=6'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -4461,18 +4429,6 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - arr-diff@4.0.0: - resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} - engines: {node: '>=0.10.0'} - - arr-flatten@1.1.0: - resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} - engines: {node: '>=0.10.0'} - - arr-union@3.1.0: - resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} - engines: {node: '>=0.10.0'} - array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -4491,18 +4447,6 @@ packages: array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} - array-union@1.0.2: - resolution: {integrity: sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==} - engines: {node: '>=0.10.0'} - - array-uniq@1.0.3: - resolution: {integrity: sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==} - engines: {node: '>=0.10.0'} - - array-unique@0.3.2: - resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} - engines: {node: '>=0.10.0'} - array.prototype.findlast@1.2.5: resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} @@ -4527,10 +4471,6 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - arrify@1.0.1: - resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} - engines: {node: '>=0.10.0'} - asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -4542,10 +4482,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - assign-symbols@1.0.0: - resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} - engines: {node: '>=0.10.0'} - ast-module-types@6.0.2: resolution: {integrity: sha512-6KuK/7nZ/2Qh7sGuVEiwxjCxzTY2Pdb5mTo5z1e6/J8BA0tvjR7G8vQJKrQMTqwmnA3UPEyKIFX4YUS1DO1Hvw==} engines: {node: '>=18'} @@ -4556,10 +4492,6 @@ packages: ast-v8-to-istanbul@1.0.0: resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} - astral-regex@1.0.0: - resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==} - engines: {node: '>=4'} - astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -4588,11 +4520,6 @@ packages: atoa@1.0.0: resolution: {integrity: sha512-VVE1H6cc4ai+ZXo/CRWoJiHXrA1qfA31DPnx6D20+kSI547hQN5Greh51LQ1baMRMfxO5K5M4ImMtZbZt2DODQ==} - atob@2.1.2: - resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} - engines: {node: '>= 4.5.0'} - hasBin: true - atom-fs@0.2.1: resolution: {integrity: sha512-H+09ux1pNAPUbJqyrZ7lA/CpNmKqZURrYc23QzDUG29A380EYEnzwW99Do5VlT8SmZtyapOMySFesndgvZZGWg==} engines: {atom: '>=1.13.0'} @@ -4607,10 +4534,6 @@ packages: peerDependencies: postcss: 8.5.15 - autoprefixer@9.8.8: - resolution: {integrity: sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==} - hasBin: true - available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -4629,9 +4552,6 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} - bail@1.0.5: - resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==} - bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -4645,10 +4565,6 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - base@0.11.2: - resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} - engines: {node: '>=0.10.0'} - baseline-browser-mapping@2.10.29: resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} engines: {node: '>=6.0.0'} @@ -4696,10 +4612,6 @@ packages: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} - braces@2.3.2: - resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} - engines: {node: '>=0.10.0'} - braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -4744,10 +4656,6 @@ packages: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} - cache-base@1.0.1: - resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} - engines: {node: '>=0.10.0'} - cacheable-lookup@5.0.4: resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} engines: {node: '>=10.6.0'} @@ -4771,39 +4679,13 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} - call-me-maybe@1.0.2: - resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} - - caller-callsite@2.0.0: - resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} - engines: {node: '>=4'} - - caller-path@2.0.0: - resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} - engines: {node: '>=4'} - - callsites@2.0.0: - resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} - engines: {node: '>=4'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase-keys@4.2.0: - resolution: {integrity: sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q==} - engines: {node: '>=4'} - - camelcase@4.1.0: - resolution: {integrity: sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==} - engines: {node: '>=4'} - caniuse-lite@1.0.30001792: resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} - ccount@1.1.0: - resolution: {integrity: sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -4830,27 +4712,15 @@ packages: change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} - character-entities-html4@1.1.4: - resolution: {integrity: sha512-HRcDxZuZqMx3/a+qrzxdBKBPUpxWEq9xw2OPZ3a/174ihfrQKVsFhqtthBInFy1zZ9GgZyFXOatNujm8M+El3g==} - character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - character-entities-legacy@1.1.4: - resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==} - character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - character-entities@1.2.4: - resolution: {integrity: sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==} - character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - character-reference-invalid@1.1.4: - resolution: {integrity: sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==} - chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -4877,10 +4747,6 @@ packages: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} - class-utils@0.3.6: - resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} - engines: {node: '>=0.10.0'} - clean-regexp@1.0.0: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} @@ -4908,10 +4774,6 @@ packages: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} - clone-regexp@1.0.1: - resolution: {integrity: sha512-Fcij9IwRW27XedRIJnSOEupS7RVcXtObJXbcUOX93UCLqqOdRpkvzKywOOSizmEK/Is3S/RHX9dLdfo6R1Q1mw==} - engines: {node: '>=0.10.0'} - clone-regexp@3.0.0: resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==} engines: {node: '>=12'} @@ -4929,13 +4791,6 @@ packages: codemirror@5.65.21: resolution: {integrity: sha512-6teYk0bA0nR3QP0ihGMoxuKzpl5W80FpnHpBJpgy66NK3cZv5b/d/HY8PnRvfSsCG1MTfr92u2WUl+wT0E40mQ==} - collapse-white-space@1.0.6: - resolution: {integrity: sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==} - - collection-visit@1.0.0: - resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} - engines: {node: '>=0.10.0'} - color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -5018,9 +4873,6 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} - component-emitter@1.3.1: - resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -5068,10 +4920,6 @@ packages: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} - copy-descriptor@0.1.1: - resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} - engines: {node: '>=0.10.0'} - core-js-compat@3.49.0: resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} @@ -5084,10 +4932,6 @@ packages: cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} - cosmiconfig@5.2.1: - resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} - engines: {node: '>=4'} - cosmiconfig@9.0.1: resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==} engines: {node: '>=14'} @@ -5174,10 +5018,6 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - currently-unhandled@0.4.1: - resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==} - engines: {node: '>=0.10.0'} - custom-event@1.0.1: resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==} @@ -5371,14 +5211,6 @@ packages: resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} engines: {node: '>=18'} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -5400,24 +5232,12 @@ packages: resolution: {integrity: sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - decamelize-keys@1.1.1: - resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} - engines: {node: '>=0.10.0'} - - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - decimal.js@10.6.0: resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} - decode-uri-component@0.2.2: - resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} - engines: {node: '>=0.10'} - decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} @@ -5448,18 +5268,6 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} - define-property@0.2.5: - resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} - engines: {node: '>=0.10.0'} - - define-property@1.0.0: - resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} - engines: {node: '>=0.10.0'} - - define-property@2.0.2: - resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} - engines: {node: '>=0.10.0'} - delaunator@5.1.0: resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} @@ -5547,10 +5355,6 @@ packages: dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} - dir-glob@2.2.2: - resolution: {integrity: sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==} - engines: {node: '>=4'} - dmg-builder@26.8.1: resolution: {integrity: sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg==} @@ -5573,24 +5377,15 @@ packages: dom-plane@1.0.2: resolution: {integrity: sha512-/tR67G6ZGSciXoZLsD706yLxEXvX3mG/OWE8YNYj3A1yU/RAimtPXzklVTu5Y5xoeMoloA/Y+MaNjQm9apgAww==} - dom-serializer@0.2.2: - resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==} - dom-serializer@2.0.0: resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} dom-set@1.1.1: resolution: {integrity: sha512-sUi2aSvRsK3Ixx++gwX9cnaWk9ZxGVFry8+HnTRVmDimybU5PaiI4wX0o00mVtjFKlQNZLmtGoPTLorYbN0+Rw==} - domelementtype@1.3.1: - resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} - domelementtype@2.3.0: resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - domhandler@2.4.2: - resolution: {integrity: sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==} - domhandler@5.0.3: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} @@ -5601,9 +5396,6 @@ packages: dompurify@3.4.7: resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==} - domutils@1.7.0: - resolution: {integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==} - domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -5611,10 +5403,6 @@ packages: resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} engines: {node: '>=20'} - dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} - dotenv-expand@11.0.7: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} engines: {node: '>=12'} @@ -5708,9 +5496,6 @@ packages: emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} - emoji-regex@7.0.3: - resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} - emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -5736,12 +5521,6 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - entities@1.1.2: - resolution: {integrity: sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==} - - entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -6230,18 +6009,10 @@ packages: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} - execall@1.0.0: - resolution: {integrity: sha512-/J0Q8CvOvlAdpvhfkD/WnTQ4H1eU0exze2nFGPj/RSC7jpQ0NkKe2r28T5eMkhEEs+fzepMZNy1kVRKNlC04nQ==} - engines: {node: '>=0.10.0'} - execall@3.0.0: resolution: {integrity: sha512-FaJeg2uWc8ADWnAnoDbxhAAr4U/j86ujlojnEpnBHQ4FM2viZKNjjyO2O6AKG6+9usZAnqsI4+1+w6vzx0x4uw==} engines: {node: '>=12'} - expand-brackets@2.1.4: - resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} - engines: {node: '>=0.10.0'} - expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -6264,17 +6035,9 @@ packages: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} - extend-shallow@3.0.2: - resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} - engines: {node: '>=0.10.0'} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - extglob@2.0.4: - resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} - engines: {node: '>=0.10.0'} - extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} @@ -6290,10 +6053,6 @@ packages: fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - fast-glob@2.2.7: - resolution: {integrity: sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==} - engines: {node: '>=4.0.0'} - fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -6358,10 +6117,6 @@ packages: file-entry-cache@11.1.3: resolution: {integrity: sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==} - file-entry-cache@4.0.0: - resolution: {integrity: sha512-AVSwsnbV8vH/UVbvgEhf3saVQXORNv0ZzSkvkhQIaia5Tia+JhGTaa/ePUSVoPHQyGayQNmYfkzFi3WZV5zcpA==} - engines: {node: '>=4'} - file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -6382,10 +6137,6 @@ packages: engines: {node: '>=18'} hasBin: true - fill-range@4.0.0: - resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} - engines: {node: '>=0.10.0'} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -6398,10 +6149,6 @@ packages: resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} engines: {node: '>=18'} - find-up@2.1.0: - resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} - engines: {node: '>=4'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -6413,10 +6160,6 @@ packages: find-yarn-workspace-root@2.0.0: resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} - flat-cache@2.0.1: - resolution: {integrity: sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==} - engines: {node: '>=4'} - flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -6424,9 +6167,6 @@ packages: flat-cache@6.1.22: resolution: {integrity: sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==} - flatted@2.0.2: - resolution: {integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==} - flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} @@ -6450,10 +6190,6 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - for-in@1.0.2: - resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} - engines: {node: '>=0.10.0'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -6480,10 +6216,6 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - fragment-cache@0.2.1: - resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} - engines: {node: '>=0.10.0'} - fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} @@ -6572,10 +6304,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stdin@6.0.0: - resolution: {integrity: sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==} - engines: {node: '>=4'} - get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} @@ -6591,10 +6319,6 @@ packages: get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - get-value@2.0.6: - resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} - engines: {node: '>=0.10.0'} - github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -6605,9 +6329,6 @@ packages: github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} - glob-parent@3.1.0: - resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==} - glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -6616,9 +6337,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob-to-regexp@0.3.0: - resolution: {integrity: sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==} - glob@12.0.0: resolution: {integrity: sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==} engines: {node: 20 || >=22} @@ -6665,10 +6383,6 @@ packages: resolution: {integrity: sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==} engines: {node: '>=20'} - globby@9.2.0: - resolution: {integrity: sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==} - engines: {node: '>=6'} - globjoin@0.1.4: resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} @@ -6737,22 +6451,6 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} - has-value@0.3.1: - resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} - engines: {node: '>=0.10.0'} - - has-value@1.0.0: - resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} - engines: {node: '>=0.10.0'} - - has-values@0.1.4: - resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} - engines: {node: '>=0.10.0'} - - has-values@1.0.0: - resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} - engines: {node: '>=0.10.0'} - hashery@1.5.1: resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} engines: {node: '>=20'} @@ -6827,10 +6525,6 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - html-tags@2.0.0: - resolution: {integrity: sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==} - engines: {node: '>=4'} - html-tags@5.1.0: resolution: {integrity: sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==} engines: {node: '>=20.10'} @@ -6841,9 +6535,6 @@ packages: htmlparser2@10.1.0: resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} - htmlparser2@3.10.1: - resolution: {integrity: sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==} - http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -6890,10 +6581,6 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@4.0.6: - resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} - engines: {node: '>= 4'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -6902,18 +6589,10 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - import-fresh@2.0.0: - resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} - engines: {node: '>=4'} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} - import-lazy@3.1.0: - resolution: {integrity: sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==} - engines: {node: '>=6'} - import-meta-resolve@4.2.0: resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} @@ -6929,9 +6608,6 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} - indexes-of@1.0.1: - resolution: {integrity: sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==} - inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -6964,20 +6640,6 @@ packages: resolution: {integrity: sha512-kniTIJmaZYiwa17eTtWIfm0K342seyugl6vuC8DiiyiRAJWAVlLkqGCI0Im0neo0TkXw+pRcKaBPRdcKHnQJ6Q==} engines: {node: '>=0.10.0'} - is-accessor-descriptor@1.0.2: - resolution: {integrity: sha512-AIbwAcazqP3R65dGvqk1V+a+vE5Fg1yu/ZKMOiBWSUIXXiwQkYmXQcVa2O0nh0tSDKDFKxG2mY7dB1Sr4hEP1g==} - engines: {node: '>= 0.4'} - - is-alphabetical@1.0.4: - resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} - - is-alphanumeric@1.0.0: - resolution: {integrity: sha512-ZmRL7++ZkcMOfDuWZuMJyIVLr2keE1o/DeNWh1EmgqGhUcV+9BIVsx0BcSBOHTZqzjs4+dISzr2KAeBEWGgXeA==} - engines: {node: '>=0.10.0'} - - is-alphanumerical@1.0.4: - resolution: {integrity: sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==} - is-arguments@1.2.0: resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} engines: {node: '>= 0.4'} @@ -7004,13 +6666,6 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} - is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - - is-buffer@2.0.5: - resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} - engines: {node: '>=4'} - is-builtin-module@5.0.0: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} @@ -7026,10 +6681,6 @@ packages: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} - is-data-descriptor@1.0.1: - resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} - engines: {node: '>= 0.4'} - is-data-view@1.0.2: resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} engines: {node: '>= 0.4'} @@ -7038,21 +6689,6 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} - is-decimal@1.0.4: - resolution: {integrity: sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==} - - is-descriptor@0.1.8: - resolution: {integrity: sha512-SceYGWXvdqlWa/OnQ5FQuV+NxvNmMRhMw/w9AHkH71hTzveND4BTYgvp16g+oITK47qbOl/3D0bl0iygehWAWQ==} - engines: {node: '>= 0.4'} - - is-descriptor@1.0.4: - resolution: {integrity: sha512-bv5z95W0dDtLfKwDfkTNxaRxmISBD3eQBKJeVxv2AQ7MjuUnDNG7cIQqvFtMOUYhsILWHhMayWdoGqNqYYYjww==} - engines: {node: '>= 0.4'} - - is-directory@0.3.1: - resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} - engines: {node: '>=0.10.0'} - is-docker@2.2.1: resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} engines: {node: '>=8'} @@ -7062,10 +6698,6 @@ packages: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} - is-extendable@1.0.1: - resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} - engines: {node: '>=0.10.0'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -7074,10 +6706,6 @@ packages: resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} engines: {node: '>= 0.4'} - is-fullwidth-code-point@2.0.0: - resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} - engines: {node: '>=4'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -7086,17 +6714,10 @@ packages: resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} engines: {node: '>= 0.4'} - is-glob@3.1.0: - resolution: {integrity: sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==} - engines: {node: '>=0.10.0'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-hexadecimal@1.0.4: - resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} - is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -7113,10 +6734,6 @@ packages: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} - is-number@3.0.0: - resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} - engines: {node: '>=0.10.0'} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -7125,26 +6742,14 @@ packages: resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} engines: {node: '>=0.10.0'} - is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - is-path-inside@4.0.0: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} - is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -7179,10 +6784,6 @@ packages: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} - is-supported-regexp-flag@1.0.1: - resolution: {integrity: sha512-3vcJecUUrpgCqc/ca0aWeNu64UGgxcvO60K/Fkr1N6RSvfGCTU60UKN68JDmKokgba0rFFJs12EnzOQa14ubKQ==} - engines: {node: '>=0.10.0'} - is-symbol@1.1.1: resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} engines: {node: '>= 0.4'} @@ -7215,23 +6816,10 @@ packages: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} - is-whitespace-character@1.0.4: - resolution: {integrity: sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==} - - is-windows@1.0.2: - resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} - engines: {node: '>=0.10.0'} - - is-word-character@1.0.4: - resolution: {integrity: sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==} - is-wsl@2.2.0: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -7261,14 +6849,6 @@ packages: resolution: {integrity: sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==} engines: {node: '>=6.0'} - isobject@2.1.0: - resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} - engines: {node: '>=0.10.0'} - - isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} - istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -7346,9 +6926,6 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-better-errors@1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -7416,14 +6993,6 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - kind-of@3.2.2: - resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} - engines: {node: '>=0.10.0'} - - kind-of@4.0.0: - resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} - engines: {node: '>=0.10.0'} - kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} @@ -7435,9 +7004,6 @@ packages: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} - known-css-properties@0.11.0: - resolution: {integrity: sha512-bEZlJzXo5V/ApNNa5z375mJC6Nrz4vG43UgcSCrg2OHC+yuB6j0iDSrY7RQ/+PRofFB03wNIIt9iXIVLr4wc7w==} - kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -7457,10 +7023,6 @@ packages: lazy-val@1.0.5: resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} - leven@2.1.0: - resolution: {integrity: sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==} - engines: {node: '>=0.10.0'} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -7546,10 +7108,6 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - load-json-file@4.0.0: - resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} - engines: {node: '>=4'} - loader-utils@3.3.1: resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==} engines: {node: '>= 12.13.0'} @@ -7558,10 +7116,6 @@ packages: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} - locate-path@2.0.0: - resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} - engines: {node: '>=4'} - locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -7619,9 +7173,6 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} - longest-streak@2.0.4: - resolution: {integrity: sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -7629,10 +7180,6 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loud-rejection@1.6.0: - resolution: {integrity: sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==} - engines: {node: '>=0.10.0'} - lowercase-keys@2.0.0: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} @@ -7671,31 +7218,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - map-cache@0.2.2: - resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} - engines: {node: '>=0.10.0'} - - map-obj@1.0.1: - resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} - engines: {node: '>=0.10.0'} - - map-obj@2.0.0: - resolution: {integrity: sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==} - engines: {node: '>=4'} - - map-visit@1.0.0: - resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} - engines: {node: '>=0.10.0'} - mapped-disposable@1.0.3: resolution: {integrity: sha512-DpYYRSZjNB6tOg8E4BZ+rNsKe4WxoptwM8+JqHi8R0buJgHLmrmAfJtv4lcqqO1esTtxaPx4FXOR54bPoS9qwg==} - markdown-escapes@1.0.4: - resolution: {integrity: sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==} - - markdown-table@1.1.3: - resolution: {integrity: sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==} - markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -7717,15 +7242,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mathml-tag-names@2.1.3: - resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==} - mathml-tag-names@4.0.0: resolution: {integrity: sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==} - mdast-util-compact@1.0.4: - resolution: {integrity: sha512-3YDMQHI5vRiS2uygEFYaqckibpJtKq5Sj2c8JioeOQBU6INpKbdWzfyLqFFnDwEcEnRFIdMsguzs5pC1Jp4Isg==} - mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -7788,10 +7307,6 @@ packages: resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==} engines: {node: '>=20'} - meow@5.0.0: - resolution: {integrity: sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==} - engines: {node: '>=6'} - merge-descriptors@2.0.0: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} @@ -7896,10 +7411,6 @@ packages: micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - micromatch@3.1.10: - resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} - engines: {node: '>=0.10.0'} - micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -7965,10 +7476,6 @@ packages: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} - minimist-options@3.0.2: - resolution: {integrity: sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==} - engines: {node: '>= 4'} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -7987,10 +7494,6 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - mixin-deep@1.3.2: - resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} - engines: {node: '>=0.10.0'} - mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} @@ -8026,9 +7529,6 @@ packages: resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} engines: {node: '>=10'} - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8040,10 +7540,6 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanomatch@1.2.13: - resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} - engines: {node: '>=0.10.0'} - napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} @@ -8157,13 +7653,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - - normalize-selector@0.2.0: - resolution: {integrity: sha512-dxvWdI8gw6eAvk9BlPffgEoGfM7AdijoCwOEJge3e3ulT2XLgmU7KvvxprOaCu05Q1uGRHmOhHe1r6emZoKyFw==} - normalize-url@6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} @@ -8181,17 +7670,10 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - num2fraction@1.2.2: - resolution: {integrity: sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-copy@0.1.0: - resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} - engines: {node: '>=0.10.0'} - object-deep-merge@2.0.1: resolution: {integrity: sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==} @@ -8211,10 +7693,6 @@ packages: resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} engines: {node: '>= 10'} - object-visit@1.0.1: - resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} - engines: {node: '>=0.10.0'} - object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} @@ -8231,10 +7709,6 @@ packages: resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} engines: {node: '>= 0.4'} - object.pick@1.3.0: - resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} - engines: {node: '>=0.10.0'} - object.values@1.2.1: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} @@ -8305,10 +7779,6 @@ packages: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} - p-limit@1.3.0: - resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} - engines: {node: '>=4'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -8317,10 +7787,6 @@ packages: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-locate@2.0.0: - resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} - engines: {node: '>=4'} - p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} @@ -8329,10 +7795,6 @@ packages: resolution: {integrity: sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - p-try@1.0.0: - resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} - engines: {node: '>=4'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -8346,9 +7808,6 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse-entities@1.2.2: - resolution: {integrity: sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==} - parse-gitignore@2.0.0: resolution: {integrity: sha512-RmVuCHWsfu0QPNW+mraxh/xjQVw/lhUCUru8Zni3Ctq3AoMhpDTq0OVdKS6iesd6Kqb7viCV3isAL43dciOSog==} engines: {node: '>=14'} @@ -8356,10 +7815,6 @@ packages: parse-imports-exports@0.2.4: resolution: {integrity: sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==} - parse-json@4.0.0: - resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} - engines: {node: '>=4'} - parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -8387,10 +7842,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - pascalcase@0.1.1: - resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} - engines: {node: '>=0.10.0'} - patch-package@8.0.1: resolution: {integrity: sha512-VsKRIA8f5uqHQ7NGhwIna6Bx6D9s/1iXlA1hthBVBEbkq+t4kXD0HHt+rJhf/Z+Ci0F/HCB2hvn0qLdLG+Qxlw==} engines: {node: '>=14', npm: '>5'} @@ -8402,13 +7853,6 @@ packages: path-data-parser@0.1.0: resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} - path-dirname@1.0.2: - resolution: {integrity: sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==} - - path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -8442,10 +7886,6 @@ packages: path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - path-type@3.0.0: - resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} - engines: {node: '>=4'} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -8463,9 +7903,6 @@ packages: perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - picocolors@0.2.1: - resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -8477,14 +7914,6 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pify@3.0.0: - resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} - engines: {node: '>=4'} - - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - pinia@3.0.4: resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} peerDependencies: @@ -8538,10 +7967,6 @@ packages: points-on-path@0.2.1: resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} - posix-character-classes@0.1.1: - resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} - engines: {node: '>=0.10.0'} - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -8629,49 +8054,24 @@ packages: peerDependencies: postcss: 8.5.15 - postcss-html@0.36.0: - resolution: {integrity: sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==} - peerDependencies: - postcss: 8.5.15 - postcss-syntax: '>=0.36.0' - postcss-image-set-function@8.0.0: resolution: {integrity: sha512-rEGNkOkNusf4+IuMmfEoIdLuVmvbExGbmG+MIsyV6jR5UaWSoyPcAYHV/PxzVDCmudyF+2Nh/o6Ub2saqUdnuA==} engines: {node: '>=20.19.0'} peerDependencies: postcss: 8.5.15 - postcss-jsx@0.36.4: - resolution: {integrity: sha512-jwO/7qWUvYuWYnpOb0+4bIIgJt7003pgU3P6nETBLaOyBXuTD55ho21xnals5nBrlpTIFodyd3/jBi6UO3dHvA==} - peerDependencies: - postcss: 8.5.15 - postcss-syntax: '>=0.36.0' - postcss-lab-function@8.0.4: resolution: {integrity: sha512-dqcJSzVasdELD9xqJ1wfP95uzP57J6zFd80c7S3AWK127H9zwqR9Kbk5ZgyIfN2DiMStI7Vq8E7ablXNeTvpew==} engines: {node: '>=20.19.0'} peerDependencies: postcss: 8.5.15 - postcss-less@3.1.4: - resolution: {integrity: sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==} - engines: {node: '>=6.14.4'} - postcss-logical@9.0.0: resolution: {integrity: sha512-A4LNd9dk3q/juEUA9Gd8ALhBO3TeOeYurnyHLlf2aAToD94VHR8c5Uv7KNmf8YVRhTxvWsyug4c5fKtARzyIRQ==} engines: {node: '>=20.19.0'} peerDependencies: postcss: 8.5.15 - postcss-markdown@0.36.0: - resolution: {integrity: sha512-rl7fs1r/LNSB2bWRhyZ+lM/0bwKv9fhl38/06gF6mKMo/NPnp55+K1dSTosSVjFZc0e1ppBlu+WT91ba0PMBfQ==} - peerDependencies: - postcss: 8.5.15 - postcss-syntax: '>=0.36.0' - - postcss-media-query-parser@0.2.3: - resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==} - postcss-nesting@14.0.0: resolution: {integrity: sha512-YGFOfVrjxYfeGTS5XctP1WCI5hu8Lr9SmntjfRC+iX5hCihEO+QZl9Ra+pkjqkgoVdDKvb2JccpElcowhZtzpw==} engines: {node: '>=20.19.0'} @@ -8718,40 +8118,18 @@ packages: peerDependencies: postcss: 8.5.15 - postcss-reporter@6.0.1: - resolution: {integrity: sha512-LpmQjfRWyabc+fRygxZjpRxfhRf9u/fdlKf4VHG4TSPbV2XNsuISzYW1KL+1aQzx53CAppa1bKG4APIB/DOXXw==} - engines: {node: '>=6'} - - postcss-resolve-nested-selector@0.1.6: - resolution: {integrity: sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==} - - postcss-safe-parser@4.0.2: - resolution: {integrity: sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==} - engines: {node: '>=6.0.0'} - postcss-safe-parser@7.0.1: resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} engines: {node: '>=18.0'} peerDependencies: postcss: 8.5.15 - postcss-sass@0.3.5: - resolution: {integrity: sha512-B5z2Kob4xBxFjcufFnhQ2HqJQ2y/Zs/ic5EZbCywCkxKd756Q40cIQ/veRDwSrw1BF6+4wUgmpm0sBASqVi65A==} - - postcss-scss@2.1.1: - resolution: {integrity: sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==} - engines: {node: '>=6.0.0'} - postcss-selector-not@9.0.0: resolution: {integrity: sha512-xhAtTdHnVU2M/CrpYOPyRUvg3njhVlKmn2GNYXDaRJV9Ygx4d5OkSkc7NINzjUqnbDFtaKXlISOBeyMXU/zyFQ==} engines: {node: '>=20.19.0'} peerDependencies: postcss: 8.5.15 - postcss-selector-parser@3.1.2: - resolution: {integrity: sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==} - engines: {node: '>=8'} - postcss-selector-parser@7.1.1: resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} @@ -8761,34 +8139,6 @@ packages: peerDependencies: postcss: 8.5.15 - postcss-sorting@4.1.0: - resolution: {integrity: sha512-r4T2oQd1giURJdHQ/RMb72dKZCuLOdWx2B/XhXN1Y1ZdnwXsKH896Qz6vD4tFy9xSjpKNYhlZoJmWyhH/7JUQw==} - engines: {node: '>=6.14.3'} - - postcss-syntax@0.36.2: - resolution: {integrity: sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==} - peerDependencies: - postcss: 8.5.15 - postcss-html: '*' - postcss-jsx: '*' - postcss-less: '*' - postcss-markdown: '*' - postcss-scss: '*' - peerDependenciesMeta: - postcss-html: - optional: true - postcss-jsx: - optional: true - postcss-less: - optional: true - postcss-markdown: - optional: true - postcss-scss: - optional: true - - postcss-value-parser@3.3.1: - resolution: {integrity: sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==} - postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -8892,10 +8242,6 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - quick-lru@1.1.0: - resolution: {integrity: sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA==} - engines: {node: '>=4'} - quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -8942,14 +8288,6 @@ packages: resolution: {integrity: sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==} deprecated: This package is no longer supported. Please use @npmcli/package-json instead. - read-pkg-up@3.0.0: - resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} - engines: {node: '>=4'} - - read-pkg@3.0.0: - resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} - engines: {node: '>=4'} - readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -8962,10 +8300,6 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} - redent@2.0.0: - resolution: {integrity: sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==} - engines: {node: '>=4'} - refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -8974,10 +8308,6 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regex-not@1.0.2: - resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} - engines: {node: '>=0.10.0'} - regex-recursion@5.1.1: resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} @@ -9033,33 +8363,12 @@ packages: remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} - remark-parse@6.0.3: - resolution: {integrity: sha512-QbDXWN4HfKTUC0hHa4teU463KclLAnwpn/FBn87j9cKYJWWawbiLgMfP2Q4XwhxxuuuOxHlw+pSN0OKuJwyVvg==} - remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - remark-stringify@6.0.4: - resolution: {integrity: sha512-eRWGdEPMVudijE/psbIDNcnJLRVx3xhfuEsTDGgH4GsFF91dVhw5nhmnBppafJ7+NWINW6C7ZwWbi30ImJzqWg==} - - remark@10.0.1: - resolution: {integrity: sha512-E6lMuoLIy2TyiokHprMjcWNJ5UxfGQjaMSMhV+f4idM625UjjK4j798+gPs5mfjzDE6vL0oFKVeZM6gZVSVrzQ==} - - repeat-element@1.1.4: - resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} - engines: {node: '>=0.10.0'} - - repeat-string@1.6.1: - resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} - engines: {node: '>=0.10'} - - replace-ext@1.0.0: - resolution: {integrity: sha512-vuNYXC7gG7IeVNBC1xUllqCcZKRbJoSPOBhnTEcAIiKCsbuef6zO3F0Rve3isPMMoNoQRWjQwbAgAjHUHniyEA==} - engines: {node: '>= 0.10'} - require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -9092,10 +8401,6 @@ packages: resolution: {integrity: sha512-YQftIIC4vzO9UMhO/sCgXukNyiwVRCVaxiWskCBy7Zpqkplm8kTAISZ8O1MoKW1ca6xzgLUBjZTcDgypXvXxiQ==} engines: {node: '>=18'} - resolve-from@3.0.0: - resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} - engines: {node: '>=4'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -9103,10 +8408,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve-url@0.2.1: - resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} - deprecated: https://github.com/lydell/resolve-url#deprecated - resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} @@ -9124,10 +8425,6 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} - ret@0.1.15: - resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} - engines: {node: '>=0.12'} - retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -9212,9 +8509,6 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - safe-regex@1.1.0: - resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -9290,10 +8584,6 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} - set-value@2.0.1: - resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} - engines: {node: '>=0.10.0'} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} @@ -9359,10 +8649,6 @@ packages: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} - slice-ansi@2.1.0: - resolution: {integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==} - engines: {node: '>=6'} - slice-ansi@3.0.0: resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} engines: {node: '>=8'} @@ -9385,18 +8671,6 @@ packages: resolution: {integrity: sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==} engines: {node: '>=12.17.0'} - snapdragon-node@2.1.1: - resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} - engines: {node: '>=0.10.0'} - - snapdragon-util@3.0.1: - resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} - engines: {node: '>=0.10.0'} - - snapdragon@0.8.2: - resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} - engines: {node: '>=0.10.0'} - snapsvg-cjs@0.0.6: resolution: {integrity: sha512-7NNvoGrc3BQvWz5rWK1DsD5/Vni4STswz5B3JrBADboQWcN8OBVGjYVJFPT5JkUXb2iVnEflZANhufEpEcTHXw==} peerDependencies: @@ -9409,21 +8683,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-resolve@0.5.3: - resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} - deprecated: See https://github.com/lydell/source-map-resolve#deprecated - source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - source-map-url@0.4.1: - resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} - deprecated: See https://github.com/lydell/source-map-url#deprecated - - source-map@0.5.7: - resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} - engines: {node: '>=0.10.0'} - source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -9459,14 +8721,6 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} - specificity@0.4.1: - resolution: {integrity: sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==} - hasBin: true - - split-string@3.1.0: - resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} - engines: {node: '>=0.10.0'} - sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -9483,13 +8737,6 @@ packages: resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} engines: {node: '>= 6'} - state-toggle@1.0.3: - resolution: {integrity: sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==} - - static-extend@0.1.2: - resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} - engines: {node: '>=0.10.0'} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -9504,10 +8751,6 @@ packages: stream-to-array@2.3.0: resolution: {integrity: sha512-UsZtOYEn4tWU2RGLOXr/o/xjRBftZRlG3dEWoaHr8j4GuypJ3isitGbVyjQKAuMu+xbiop8q224TjiZWc4XTZA==} - string-width@3.1.0: - resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} - engines: {node: '>=6'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -9546,9 +8789,6 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - stringify-entities@1.3.2: - resolution: {integrity: sha512-nrBAQClJAPN2p+uGCVJRPIPakKeKWZ9GtBCmormE7pWOSlHat7+x5A8gx85M7HM5Dt0BP3pP5RhVW77WdbJJ3A==} - stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -9556,10 +8796,6 @@ packages: resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} engines: {node: '>=4'} - strip-ansi@5.2.0: - resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} - engines: {node: '>=6'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -9580,10 +8816,6 @@ packages: resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} engines: {node: '>=6'} - strip-indent@2.0.0: - resolution: {integrity: sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==} - engines: {node: '>=4'} - strip-indent@4.1.1: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} @@ -9605,9 +8837,6 @@ packages: stubborn-utils@1.0.2: resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} - style-search@0.1.0: - resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} - styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} engines: {node: '>= 12.0.0'} @@ -9621,9 +8850,6 @@ packages: babel-plugin-macros: optional: true - stylelint-config-rational-order@0.1.2: - resolution: {integrity: sha512-Qo7ZQaihCwTqijfZg4sbdQQHtugOX/B1/fYh018EiDZHW+lkqH9uHOnsDwDPGZrYJuB6CoyI7MZh2ecw2dOkew==} - stylelint-config-recommended@18.0.0: resolution: {integrity: sha512-mxgT2XY6YZ3HWWe3Di8umG6aBmWmHTblTgu/f10rqFXnyWxjKWwNdjSWkgkwCtxIKnqjSJzvFmPT5yabVIRxZg==} engines: {node: '>=20.19.0'} @@ -9636,12 +8862,6 @@ packages: peerDependencies: stylelint: ^17.0.0 - stylelint-order@2.2.1: - resolution: {integrity: sha512-019KBV9j8qp1MfBjJuotse6MgaZqGVtXMc91GU9MsS9Feb+jYUvUU3Z8XiClqPdqJZQ0ryXQJGg3U3PcEjXwfg==} - engines: {node: '>=6'} - peerDependencies: - stylelint: ^9.10.1 || ^10.0.0 - stylelint-order@8.1.1: resolution: {integrity: sha512-LqsEB6VggJuu5v10RtkrQsBObcdwBE7GuAOlwfc/LR3VL/w8UqKX2BOLIjhyGt0Gne/njo7gRNGiJAKhfmPMNw==} engines: {node: '>=20.19.0'} @@ -9653,11 +8873,6 @@ packages: engines: {node: '>=20.19.0'} hasBin: true - stylelint@9.10.1: - resolution: {integrity: sha512-9UiHxZhOAHEgeQ7oLGwrwoDR8vclBKlSX7r4fH0iuu0SfPwFaLkb1c7Q2j1cqg9P7IDXeAV2TvQML/fRQzGBBQ==} - engines: {node: '>=6'} - hasBin: true - stylis@4.4.0: resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} @@ -9666,9 +8881,6 @@ packages: engines: {node: '>=18'} hasBin: true - sugarss@2.0.0: - resolution: {integrity: sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==} - sumchecker@3.0.1: resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} engines: {node: '>= 8.0'} @@ -9712,10 +8924,6 @@ packages: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} - table@5.4.6: - resolution: {integrity: sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==} - engines: {node: '>=6.0.0'} - table@6.9.0: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} @@ -9796,22 +9004,10 @@ packages: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} - to-object-path@0.3.0: - resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} - engines: {node: '>=0.10.0'} - - to-regex-range@2.1.1: - resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} - engines: {node: '>=0.10.0'} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - to-regex@3.0.2: - resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} - engines: {node: '>=0.10.0'} - to-valid-identifier@1.0.0: resolution: {integrity: sha512-41wJyvKep3yT2tyPqX/4blcfybknGB4D+oETKLs7Q76UiPqRpUJK3hr1nxelyYO0PHKVzJwlu0aCeEAsGI6rpw==} engines: {node: '>=20'} @@ -9846,20 +9042,6 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - trim-newlines@2.0.0: - resolution: {integrity: sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA==} - engines: {node: '>=4'} - - trim-trailing-lines@1.1.4: - resolution: {integrity: sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==} - - trim@0.0.1: - resolution: {integrity: sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==} - deprecated: Use String.prototype.trim() instead - - trough@1.0.5: - resolution: {integrity: sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==} - trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -10000,9 +9182,6 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} - unherit@1.1.3: - resolution: {integrity: sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==} - unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -10017,49 +9196,21 @@ packages: unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unified@7.1.0: - resolution: {integrity: sha512-lbk82UOIGuCEsZhPj8rNAkXSDXd6p0QLzIuSsCdxrqnqU56St4eyOB+AlXsVgVeRmetPTYydIuvFfpDIed8mqw==} - - union-value@1.0.1: - resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} - engines: {node: '>=0.10.0'} - - uniq@1.0.1: - resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==} - - unist-util-find-all-after@1.0.5: - resolution: {integrity: sha512-lWgIc3rrTMTlK1Y0hEuL+k+ApzFk78h+lsaa2gHf63Gp5Ww+mt11huDniuaoq1H+XMK2lIIjjPkncxXcDp3QDw==} - - unist-util-is@3.0.0: - resolution: {integrity: sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==} - unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - unist-util-remove-position@1.1.4: - resolution: {integrity: sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==} - unist-util-remove-position@5.0.0: resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} - unist-util-stringify-position@1.1.2: - resolution: {integrity: sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==} - unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - unist-util-visit-parents@2.1.2: - resolution: {integrity: sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==} - unist-util-visit-parents@6.0.2: resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - unist-util-visit@1.4.1: - resolution: {integrity: sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==} - unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} @@ -10112,10 +9263,6 @@ packages: unrs-resolver@1.12.2: resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} - unset-value@1.0.0: - resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} - engines: {node: '>=0.10.0'} - update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -10125,17 +9272,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - urix@0.1.0: - resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} - deprecated: Please see https://github.com/lydell/urix#deprecated - urlpattern-polyfill@10.1.0: resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} - use@3.1.1: - resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} - engines: {node: '>=0.10.0'} - utf8-byte-length@1.0.5: resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} @@ -10278,21 +9417,12 @@ packages: resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} engines: {node: '>=0.6.0'} - vfile-location@2.0.6: - resolution: {integrity: sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==} - vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} - vfile-message@1.1.1: - resolution: {integrity: sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==} - vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - vfile@3.0.1: - resolution: {integrity: sha512-y7Y3gH9BsUSdD4KzHsuMaCzRjglXN0W2EcMf0gpvu6+SbsGhMje7xDc8AEoeXy6mIwCKMI6BkjMsRjzQbhMEjQ==} - vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} @@ -10608,10 +9738,6 @@ packages: resolution: {integrity: sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==} engines: {node: ^20.17.0 || >=22.9.0} - write@1.0.3: - resolution: {integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==} - engines: {node: '>=4'} - ws@8.20.1: resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} @@ -10624,9 +9750,6 @@ packages: utf-8-validate: optional: true - x-is-string@0.1.0: - resolution: {integrity: sha512-GojqklwG8gpzOVEVki5KudKNoq7MbbjYZCbyWzEz7tyPA7eleiE0+ePwOWQQRb5fm86rD3S8Tc0tSFf3AOv50w==} - xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -10646,10 +9769,6 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -10673,9 +9792,6 @@ packages: engines: {node: '>= 14.6'} hasBin: true - yargs-parser@10.1.0: - resolution: {integrity: sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==} - yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -12859,11 +11975,6 @@ snapshots: '@mixmark-io/domino@2.2.0': {} - '@mrmlnc/readdir-enhanced@2.2.1': - dependencies: - call-me-maybe: 1.0.2 - glob-to-regexp: 0.3.0 - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: '@emnapi/core': 1.10.0 @@ -12935,8 +12046,6 @@ snapshots: '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 - '@nodelib/fs.stat@1.1.3': {} - '@nodelib/fs.stat@2.0.5': {} '@nodelib/fs.walk@1.2.8': @@ -13744,11 +12853,6 @@ snapshots: '@types/geojson@7946.0.16': {} - '@types/glob@7.2.0': - dependencies: - '@types/minimatch': 6.0.0 - '@types/node': 22.19.19 - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -13779,10 +12883,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/minimatch@6.0.0': - dependencies: - minimatch: 10.2.5 - '@types/ms@2.1.0': {} '@types/node-fetch@2.6.13': @@ -13833,23 +12933,11 @@ snapshots: '@types/underscore@1.13.0': {} - '@types/unist@2.0.11': {} - '@types/unist@3.0.3': {} '@types/verror@1.10.11': optional: true - '@types/vfile-message@2.0.0': - dependencies: - vfile-message: 4.0.3 - - '@types/vfile@3.0.2': - dependencies: - '@types/node': 22.19.19 - '@types/unist': 2.0.11 - '@types/vfile-message': 2.0.0 - '@types/web-bluetooth@0.0.21': {} '@types/webfontloader@1.6.38': {} @@ -14594,8 +13682,6 @@ snapshots: ansi-regex@3.0.1: {} - ansi-regex@4.1.1: {} - ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -14673,12 +13759,6 @@ snapshots: aria-query@5.3.2: {} - arr-diff@4.0.0: {} - - arr-flatten@1.1.0: {} - - arr-union@3.1.0: {} - array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -14701,14 +13781,6 @@ snapshots: array-timsort@1.0.3: {} - array-union@1.0.2: - dependencies: - array-uniq: 1.0.3 - - array-uniq@1.0.3: {} - - array-unique@0.3.2: {} - array.prototype.findlast@1.2.5: dependencies: call-bind: 1.0.9 @@ -14760,8 +13832,6 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 - arrify@1.0.1: {} - asap@2.0.6: {} assert-plus@1.0.0: @@ -14769,8 +13839,6 @@ snapshots: assertion-error@2.0.1: {} - assign-symbols@1.0.0: {} - ast-module-types@6.0.2: {} ast-types-flow@0.0.8: {} @@ -14781,8 +13849,6 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 - astral-regex@1.0.0: {} - astral-regex@2.0.0: {} async-exit-hook@2.0.1: {} @@ -14799,8 +13865,6 @@ snapshots: atoa@1.0.0: {} - atob@2.1.2: {} - atom-fs@0.2.1: dependencies: mapped-disposable: 1.0.3 @@ -14819,16 +13883,6 @@ snapshots: postcss: 8.5.15 postcss-value-parser: 4.2.0 - autoprefixer@9.8.8: - dependencies: - browserslist: 4.28.2 - caniuse-lite: 1.0.30001792 - normalize-range: 0.1.2 - num2fraction: 1.2.2 - picocolors: 0.2.1 - postcss: 8.5.15 - postcss-value-parser: 4.2.0 - available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -14849,8 +13903,6 @@ snapshots: axobject-query@4.1.0: {} - bail@1.0.5: {} - bail@2.0.2: {} balanced-match@1.0.2: {} @@ -14859,16 +13911,6 @@ snapshots: base64-js@1.5.1: {} - base@0.11.2: - dependencies: - cache-base: 1.0.1 - class-utils: 0.3.6 - component-emitter: 1.3.1 - define-property: 1.0.0 - isobject: 3.0.1 - mixin-deep: 1.3.2 - pascalcase: 0.1.1 - baseline-browser-mapping@2.10.29: {} batch-processor@1.0.0: {} @@ -14925,21 +13967,6 @@ snapshots: dependencies: balanced-match: 4.0.4 - braces@2.3.2: - dependencies: - arr-flatten: 1.1.0 - array-unique: 0.3.2 - extend-shallow: 2.0.1 - fill-range: 4.0.0 - isobject: 3.0.1 - repeat-element: 1.1.4 - snapdragon: 0.8.2 - snapdragon-node: 2.1.1 - split-string: 3.1.0 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color - braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -14999,18 +14026,6 @@ snapshots: cac@7.0.0: {} - cache-base@1.0.1: - dependencies: - collection-visit: 1.0.0 - component-emitter: 1.3.1 - get-value: 2.0.6 - has-value: 1.0.0 - isobject: 3.0.1 - set-value: 2.0.1 - to-object-path: 0.3.0 - union-value: 1.0.1 - unset-value: 1.0.0 - cacheable-lookup@5.0.4: {} cacheable-request@7.0.4: @@ -15048,32 +14063,10 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 - call-me-maybe@1.0.2: {} - - caller-callsite@2.0.0: - dependencies: - callsites: 2.0.0 - - caller-path@2.0.0: - dependencies: - caller-callsite: 2.0.0 - - callsites@2.0.0: {} - callsites@3.1.0: {} - camelcase-keys@4.2.0: - dependencies: - camelcase: 4.1.0 - map-obj: 2.0.0 - quick-lru: 1.1.0 - - camelcase@4.1.0: {} - caniuse-lite@1.0.30001792: {} - ccount@1.1.0: {} - ccount@2.0.1: {} ced@2.0.0: @@ -15097,20 +14090,12 @@ snapshots: change-case@5.4.4: {} - character-entities-html4@1.1.4: {} - character-entities-html4@2.1.0: {} - character-entities-legacy@1.1.4: {} - character-entities-legacy@3.0.0: {} - character-entities@1.2.4: {} - character-entities@2.0.2: {} - character-reference-invalid@1.1.4: {} - chokidar@5.0.0: dependencies: readdirp: 5.0.0 @@ -15127,13 +14112,6 @@ snapshots: ci-info@4.4.0: {} - class-utils@0.3.6: - dependencies: - arr-union: 3.1.0 - define-property: 0.2.5 - isobject: 3.0.1 - static-extend: 0.1.2 - clean-regexp@1.0.0: dependencies: escape-string-regexp: 1.0.5 @@ -15164,11 +14142,6 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 - clone-regexp@1.0.1: - dependencies: - is-regexp: 1.0.0 - is-supported-regexp-flag: 1.0.1 - clone-regexp@3.0.0: dependencies: is-regexp: 3.1.0 @@ -15193,13 +14166,6 @@ snapshots: codemirror@5.65.21: {} - collapse-white-space@1.0.6: {} - - collection-visit@1.0.0: - dependencies: - map-visit: 1.0.0 - object-visit: 1.0.1 - color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -15256,8 +14222,6 @@ snapshots: compare-versions@6.1.1: {} - component-emitter@1.3.1: {} - concat-map@0.0.1: {} conf@15.1.0: @@ -15299,8 +14263,6 @@ snapshots: dependencies: is-what: 5.5.0 - copy-descriptor@0.1.1: {} - core-js-compat@3.49.0: dependencies: browserslist: 4.28.2 @@ -15316,13 +14278,6 @@ snapshots: dependencies: layout-base: 2.0.1 - cosmiconfig@5.2.1: - dependencies: - import-fresh: 2.0.0 - is-directory: 0.3.1 - js-yaml: 3.14.2 - parse-json: 4.0.0 - cosmiconfig@9.0.1(typescript@6.0.3): dependencies: env-paths: 2.2.1 @@ -15412,10 +14367,6 @@ snapshots: csstype@3.2.3: {} - currently-unhandled@0.4.1: - dependencies: - array-find-index: 1.0.2 - custom-event@1.0.1: {} cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.3): @@ -15643,10 +14594,6 @@ snapshots: dependencies: mimic-function: 5.0.1 - debug@2.6.9: - dependencies: - ms: 2.0.0 - debug@3.2.7: dependencies: ms: 2.1.3 @@ -15657,21 +14604,12 @@ snapshots: debuglog@1.0.1: {} - decamelize-keys@1.1.1: - dependencies: - decamelize: 1.2.0 - map-obj: 1.0.1 - - decamelize@1.2.0: {} - decimal.js@10.6.0: {} decode-named-character-reference@1.3.0: dependencies: character-entities: 2.0.2 - decode-uri-component@0.2.2: {} - decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 @@ -15719,19 +14657,6 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 - define-property@0.2.5: - dependencies: - is-descriptor: 0.1.8 - - define-property@1.0.0: - dependencies: - is-descriptor: 1.0.4 - - define-property@2.0.2: - dependencies: - is-descriptor: 1.0.4 - isobject: 3.0.1 - delaunator@5.1.0: dependencies: robust-predicates: 3.0.3 @@ -15831,10 +14756,6 @@ snapshots: minimatch: 3.1.5 p-limit: 3.1.0 - dir-glob@2.2.2: - dependencies: - path-type: 3.0.0 - dmg-builder@26.8.1(electron-builder-squirrel-windows@26.8.1): dependencies: app-builder-lib: 26.8.1(dmg-builder@26.8.1)(electron-builder-squirrel-windows@26.8.1) @@ -15879,11 +14800,6 @@ snapshots: dependencies: create-point-cb: 1.2.0 - dom-serializer@0.2.2: - dependencies: - domelementtype: 2.3.0 - entities: 2.2.0 - dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 @@ -15896,14 +14812,8 @@ snapshots: is-array: 1.0.1 iselement: 1.1.4 - domelementtype@1.3.1: {} - domelementtype@2.3.0: {} - domhandler@2.4.2: - dependencies: - domelementtype: 1.3.1 - domhandler@5.0.3: dependencies: domelementtype: 2.3.0 @@ -15916,11 +14826,6 @@ snapshots: optionalDependencies: '@types/trusted-types': 2.0.7 - domutils@1.7.0: - dependencies: - dom-serializer: 0.2.2 - domelementtype: 1.3.1 - domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -15931,10 +14836,6 @@ snapshots: dependencies: type-fest: 5.6.0 - dot-prop@5.3.0: - dependencies: - is-obj: 2.0.0 - dotenv-expand@11.0.7: dependencies: dotenv: 16.6.1 @@ -16091,8 +14992,6 @@ snapshots: emoji-regex@10.6.0: {} - emoji-regex@7.0.3: {} - emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} @@ -16115,10 +15014,6 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - entities@1.1.2: {} - - entities@2.2.0: {} - entities@4.5.0: {} entities@6.0.1: {} @@ -17028,26 +15923,10 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - execall@1.0.0: - dependencies: - clone-regexp: 1.0.1 - execall@3.0.0: dependencies: clone-regexp: 3.0.0 - expand-brackets@2.1.4: - dependencies: - debug: 2.6.9 - define-property: 0.2.5 - extend-shallow: 2.0.1 - posix-character-classes: 0.1.1 - regex-not: 1.0.2 - snapdragon: 0.8.2 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color - expand-template@2.0.3: {} expect-type@1.3.0: {} @@ -17093,26 +15972,8 @@ snapshots: dependencies: is-extendable: 0.1.1 - extend-shallow@3.0.2: - dependencies: - assign-symbols: 1.0.0 - is-extendable: 1.0.1 - extend@3.0.2: {} - extglob@2.0.4: - dependencies: - array-unique: 0.3.2 - define-property: 1.0.0 - expand-brackets: 2.1.4 - extend-shallow: 2.0.1 - fragment-cache: 0.2.1 - regex-not: 1.0.2 - snapdragon: 0.8.2 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color - extract-zip@2.0.1: dependencies: debug: 4.4.3 @@ -17130,18 +15991,7 @@ snapshots: fast-diff@1.3.0: {} - fast-glob@2.2.7: - dependencies: - '@mrmlnc/readdir-enhanced': 2.2.1 - '@nodelib/fs.stat': 1.1.3 - glob-parent: 3.1.0 - is-glob: 4.0.3 - merge2: 1.4.1 - micromatch: 3.1.10 - transitivePeerDependencies: - - supports-color - - fast-glob@3.3.1: + fast-glob@3.3.1: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 @@ -17211,10 +16061,6 @@ snapshots: dependencies: flat-cache: 6.1.22 - file-entry-cache@4.0.0: - dependencies: - flat-cache: 2.0.1 - file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -17246,13 +16092,6 @@ snapshots: tsconfig-paths: 4.2.0 typescript: 5.9.3 - fill-range@4.0.0: - dependencies: - extend-shallow: 2.0.1 - is-number: 3.0.0 - repeat-string: 1.6.1 - to-regex-range: 2.1.1 - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -17270,10 +16109,6 @@ snapshots: find-up-simple@1.0.1: {} - find-up@2.1.0: - dependencies: - locate-path: 2.0.0 - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -17288,12 +16123,6 @@ snapshots: dependencies: micromatch: 4.0.8 - flat-cache@2.0.1: - dependencies: - flatted: 2.0.2 - rimraf: 2.6.3 - write: 1.0.3 - flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -17305,8 +16134,6 @@ snapshots: flatted: 3.4.2 hookified: 1.15.1 - flatted@2.0.2: {} - flatted@3.4.2: {} flowchart.js@1.18.0: @@ -17321,8 +16148,6 @@ snapshots: dependencies: is-callable: 1.2.7 - for-in@1.0.2: {} - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -17349,10 +16174,6 @@ snapshots: fraction.js@5.3.4: {} - fragment-cache@0.2.1: - dependencies: - map-cache: 0.2.2 - fresh@2.0.0: {} fs-constants@1.0.0: {} @@ -17446,8 +16267,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stdin@6.0.0: {} - get-stream@5.2.0: dependencies: pump: 3.0.4 @@ -17464,19 +16283,12 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - get-value@2.0.6: {} - github-from-package@0.0.0: {} github-markdown-css@5.9.0: {} github-slugger@2.0.0: {} - glob-parent@3.1.0: - dependencies: - is-glob: 3.1.0 - path-dirname: 1.0.2 - glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -17485,8 +16297,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob-to-regexp@0.3.0: {} - glob@12.0.0: dependencies: foreground-child: 3.3.1 @@ -17552,19 +16362,6 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.4.0 - globby@9.2.0: - dependencies: - '@types/glob': 7.2.0 - array-union: 1.0.2 - dir-glob: 2.2.2 - fast-glob: 2.2.7 - glob: 7.2.3 - ignore: 4.0.6 - pify: 4.0.1 - slash: 2.0.0 - transitivePeerDependencies: - - supports-color - globjoin@0.1.4: {} globrex@0.1.2: {} @@ -17632,25 +16429,6 @@ snapshots: dependencies: has-symbols: 1.1.0 - has-value@0.3.1: - dependencies: - get-value: 2.0.6 - has-values: 0.1.4 - isobject: 2.1.0 - - has-value@1.0.0: - dependencies: - get-value: 2.0.6 - has-values: 1.0.0 - isobject: 3.0.1 - - has-values@0.1.4: {} - - has-values@1.0.0: - dependencies: - is-number: 3.0.0 - kind-of: 4.0.0 - hashery@1.5.1: dependencies: hookified: 1.15.1 @@ -17777,8 +16555,6 @@ snapshots: html-escaper@2.0.2: {} - html-tags@2.0.0: {} - html-tags@5.1.0: {} html-void-elements@3.0.0: {} @@ -17790,15 +16566,6 @@ snapshots: domutils: 3.2.2 entities: 7.0.1 - htmlparser2@3.10.1: - dependencies: - domelementtype: 1.3.1 - domhandler: 2.4.2 - domutils: 1.7.0 - entities: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - http-cache-semantics@4.2.0: {} http-errors@2.0.1: @@ -17857,24 +16624,15 @@ snapshots: ieee754@1.2.1: {} - ignore@4.0.6: {} - ignore@5.3.2: {} ignore@7.0.5: {} - import-fresh@2.0.0: - dependencies: - caller-path: 2.0.0 - resolve-from: 3.0.0 - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - import-lazy@3.1.0: {} - import-meta-resolve@4.2.0: {} imurmurhash@0.1.4: {} @@ -17883,8 +16641,6 @@ snapshots: indent-string@5.0.0: {} - indexes-of@1.0.1: {} - inflight@1.0.6: dependencies: once: 1.4.0 @@ -17912,19 +16668,6 @@ snapshots: irregular-plurals@1.4.0: {} - is-accessor-descriptor@1.0.2: - dependencies: - hasown: 2.0.3 - - is-alphabetical@1.0.4: {} - - is-alphanumeric@1.0.0: {} - - is-alphanumerical@1.0.4: - dependencies: - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - is-arguments@1.2.0: dependencies: call-bound: 1.0.4 @@ -17957,10 +16700,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-buffer@1.1.6: {} - - is-buffer@2.0.5: {} - is-builtin-module@5.0.0: dependencies: builtin-modules: 5.2.0 @@ -17975,10 +16714,6 @@ snapshots: dependencies: hasown: 2.0.3 - is-data-descriptor@1.0.1: - dependencies: - hasown: 2.0.3 - is-data-view@1.0.2: dependencies: call-bound: 1.0.4 @@ -17990,36 +16725,16 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-decimal@1.0.4: {} - - is-descriptor@0.1.8: - dependencies: - is-accessor-descriptor: 1.0.2 - is-data-descriptor: 1.0.1 - - is-descriptor@1.0.4: - dependencies: - is-accessor-descriptor: 1.0.2 - is-data-descriptor: 1.0.1 - - is-directory@0.3.1: {} - is-docker@2.2.1: {} is-extendable@0.1.1: {} - is-extendable@1.0.1: - dependencies: - is-plain-object: 2.0.4 - is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: dependencies: call-bound: 1.0.4 - is-fullwidth-code-point@2.0.0: {} - is-fullwidth-code-point@3.0.0: {} is-generator-function@1.1.2: @@ -18030,16 +16745,10 @@ snapshots: has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 - is-glob@3.1.0: - dependencies: - is-extglob: 2.1.1 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - is-hexadecimal@1.0.4: {} - is-interactive@1.0.0: {} is-map@2.0.3: {} @@ -18051,26 +16760,14 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-number@3.0.0: - dependencies: - kind-of: 3.2.2 - is-number@7.0.0: {} is-obj@1.0.1: {} - is-obj@2.0.0: {} - is-path-inside@4.0.0: {} - is-plain-obj@1.1.0: {} - is-plain-obj@4.1.0: {} - is-plain-object@2.0.4: - dependencies: - isobject: 3.0.1 - is-potential-custom-element-name@1.0.1: {} is-promise@4.0.0: {} @@ -18099,8 +16796,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-supported-regexp-flag@1.0.1: {} - is-symbol@1.1.1: dependencies: call-bound: 1.0.4 @@ -18128,18 +16823,10 @@ snapshots: is-what@5.5.0: {} - is-whitespace-character@1.0.4: {} - - is-windows@1.0.2: {} - - is-word-character@1.0.4: {} - is-wsl@2.2.0: dependencies: is-docker: 2.2.1 - isarray@1.0.0: {} - isarray@2.0.5: {} isbinaryfile@4.0.10: {} @@ -18156,12 +16843,6 @@ snapshots: iso-639-1@3.1.5: {} - isobject@2.1.0: - dependencies: - isarray: 1.0.0 - - isobject@3.0.1: {} - istanbul-lib-coverage@3.2.2: {} istanbul-lib-report@3.0.1: @@ -18254,8 +16935,6 @@ snapshots: json-buffer@3.0.1: {} - json-parse-better-errors@1.0.2: {} - json-parse-even-better-errors@2.3.1: {} json-schema-traverse@0.4.1: {} @@ -18329,14 +17008,6 @@ snapshots: khroma@2.1.0: {} - kind-of@3.2.2: - dependencies: - is-buffer: 1.1.6 - - kind-of@4.0.0: - dependencies: - is-buffer: 1.1.6 - kind-of@6.0.3: {} klaw-sync@6.0.0: @@ -18345,8 +17016,6 @@ snapshots: kleur@4.1.5: {} - known-css-properties@0.11.0: {} - kolorist@1.8.0: {} language-subtag-registry@0.3.23: {} @@ -18361,8 +17030,6 @@ snapshots: lazy-val@1.0.5: {} - leven@2.1.0: {} - levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -18434,13 +17101,6 @@ snapshots: lines-and-columns@1.2.4: {} - load-json-file@4.0.0: - dependencies: - graceful-fs: 4.2.11 - parse-json: 4.0.0 - pify: 3.0.0 - strip-bom: 3.0.0 - loader-utils@3.3.1: {} local-pkg@1.2.1: @@ -18449,11 +17109,6 @@ snapshots: pkg-types: 2.3.1 quansync: 0.2.11 - locate-path@2.0.0: - dependencies: - p-locate: 2.0.0 - path-exists: 3.0.0 - locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -18499,19 +17154,12 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 - longest-streak@2.0.4: {} - longest-streak@3.1.0: {} loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - loud-rejection@1.6.0: - dependencies: - currently-unhandled: 0.4.1 - signal-exit: 3.0.7 - lowercase-keys@2.0.0: {} lru-cache@10.4.3: {} @@ -18559,22 +17207,8 @@ snapshots: dependencies: semver: 7.8.0 - map-cache@0.2.2: {} - - map-obj@1.0.1: {} - - map-obj@2.0.0: {} - - map-visit@1.0.0: - dependencies: - object-visit: 1.0.1 - mapped-disposable@1.0.3: {} - markdown-escapes@1.0.4: {} - - markdown-table@1.1.3: {} - markdown-table@3.0.4: {} marked-highlight@2.2.4(marked@16.4.2): @@ -18590,14 +17224,8 @@ snapshots: math-intrinsics@1.1.0: {} - mathml-tag-names@2.1.3: {} - mathml-tag-names@4.0.0: {} - mdast-util-compact@1.0.4: - dependencies: - unist-util-visit: 1.4.1 - mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -18747,18 +17375,6 @@ snapshots: meow@14.1.0: {} - meow@5.0.0: - dependencies: - camelcase-keys: 4.2.0 - decamelize-keys: 1.1.1 - loud-rejection: 1.6.0 - minimist-options: 3.0.2 - normalize-package-data: 2.5.0 - read-pkg-up: 3.0.0 - redent: 2.0.0 - trim-newlines: 2.0.0 - yargs-parser: 10.1.0 - merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} @@ -18997,24 +17613,6 @@ snapshots: transitivePeerDependencies: - supports-color - micromatch@3.1.10: - dependencies: - arr-diff: 4.0.0 - array-unique: 0.3.2 - braces: 2.3.2 - define-property: 2.0.2 - extend-shallow: 3.0.2 - extglob: 2.0.4 - fragment-cache: 0.2.1 - kind-of: 6.0.3 - nanomatch: 1.2.13 - object.pick: 1.3.0 - regex-not: 1.0.2 - snapdragon: 0.8.2 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color - micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -19074,11 +17672,6 @@ snapshots: dependencies: brace-expansion: 2.1.0 - minimist-options@3.0.2: - dependencies: - arrify: 1.0.1 - is-plain-obj: 1.1.0 - minimist@1.2.8: {} minipass@4.2.8: {} @@ -19091,11 +17684,6 @@ snapshots: mitt@3.0.1: {} - mixin-deep@1.3.2: - dependencies: - for-in: 1.0.2 - is-extendable: 1.0.1 - mkdirp-classic@0.5.3: {} mkdirp@0.5.6: @@ -19130,30 +17718,12 @@ snapshots: mrmime@1.0.1: {} - ms@2.0.0: {} - ms@2.1.3: {} muggle-string@0.4.1: {} nanoid@3.3.12: {} - nanomatch@1.2.13: - dependencies: - arr-diff: 4.0.0 - array-unique: 0.3.2 - define-property: 2.0.2 - extend-shallow: 3.0.2 - fragment-cache: 0.2.1 - is-windows: 1.0.2 - kind-of: 6.0.3 - object.pick: 1.3.0 - regex-not: 1.0.2 - snapdragon: 0.8.2 - to-regex: 3.0.2 - transitivePeerDependencies: - - supports-color - napi-build-utils@2.0.0: {} napi-postinstall@0.3.4: {} @@ -19275,10 +17845,6 @@ snapshots: normalize-path@3.0.0: {} - normalize-range@0.1.2: {} - - normalize-selector@0.2.0: {} - normalize-url@6.1.0: {} normalize-wheel-es@1.2.0: {} @@ -19293,16 +17859,8 @@ snapshots: dependencies: boolbase: 1.0.0 - num2fraction@1.2.2: {} - object-assign@4.1.1: {} - object-copy@0.1.0: - dependencies: - copy-descriptor: 0.1.1 - define-property: 0.2.5 - kind-of: 3.2.2 - object-deep-merge@2.0.1: {} object-inspect@1.13.4: {} @@ -19316,10 +17874,6 @@ snapshots: object-treeify@1.1.33: {} - object-visit@1.0.1: - dependencies: - isobject: 3.0.1 - object.assign@4.1.7: dependencies: call-bind: 1.0.9 @@ -19349,10 +17903,6 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.24.2 - object.pick@1.3.0: - dependencies: - isobject: 3.0.1 - object.values@1.2.1: dependencies: call-bind: 1.0.9 @@ -19459,10 +18009,6 @@ snapshots: p-cancelable@2.1.1: {} - p-limit@1.3.0: - dependencies: - p-try: 1.0.0 - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -19471,10 +18017,6 @@ snapshots: dependencies: yocto-queue: 1.2.2 - p-locate@2.0.0: - dependencies: - p-limit: 1.3.0 - p-locate@5.0.0: dependencies: p-limit: 3.1.0 @@ -19483,8 +18025,6 @@ snapshots: dependencies: p-limit: 4.0.0 - p-try@1.0.0: {} - package-json-from-dist@1.0.1: {} package-manager-detector@1.6.0: {} @@ -19495,26 +18035,12 @@ snapshots: dependencies: callsites: 3.1.0 - parse-entities@1.2.2: - dependencies: - character-entities: 1.2.4 - character-entities-legacy: 1.1.4 - character-reference-invalid: 1.1.4 - is-alphanumerical: 1.0.4 - is-decimal: 1.0.4 - is-hexadecimal: 1.0.4 - parse-gitignore@2.0.0: {} parse-imports-exports@0.2.4: dependencies: parse-statements: 1.0.11 - parse-json@4.0.0: - dependencies: - error-ex: 1.3.4 - json-parse-better-errors: 1.0.2 - parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -19542,8 +18068,6 @@ snapshots: parseurl@1.3.3: {} - pascalcase@0.1.1: {} - patch-package@8.0.1: dependencies: '@yarnpkg/lockfile': 1.1.0 @@ -19565,10 +18089,6 @@ snapshots: path-data-parser@0.1.0: {} - path-dirname@1.0.2: {} - - path-exists@3.0.0: {} - path-exists@4.0.0: {} path-expression-matcher@1.5.0: {} @@ -19593,10 +18113,6 @@ snapshots: path-to-regexp@8.4.2: {} - path-type@3.0.0: - dependencies: - pify: 3.0.0 - pathe@2.0.3: {} pe-library@0.4.1: {} @@ -19607,18 +18123,12 @@ snapshots: perfect-debounce@1.0.0: {} - picocolors@0.2.1: {} - picocolors@1.1.1: {} picomatch@2.3.2: {} picomatch@4.0.4: {} - pify@3.0.0: {} - - pify@4.0.1: {} - pinia@3.0.4(typescript@5.9.3)(vue@3.5.34(typescript@5.9.3)): dependencies: '@vue/devtools-api': 7.7.9 @@ -19677,8 +18187,6 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - posix-character-classes@0.1.1: {} - possible-typed-array-names@1.1.0: {} postcss-attribute-case-insensitive@8.0.0(postcss@8.5.15): @@ -19767,26 +18275,12 @@ snapshots: dependencies: postcss: 8.5.15 - postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15): - dependencies: - htmlparser2: 3.10.1 - postcss: 8.5.15 - postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) - postcss-image-set-function@8.0.0(postcss@8.5.15): dependencies: '@csstools/utilities': 3.0.0(postcss@8.5.15) postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15): - dependencies: - '@babel/core': 7.29.0 - postcss: 8.5.15 - postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) - transitivePeerDependencies: - - supports-color - postcss-lab-function@8.0.4(postcss@8.5.15): dependencies: '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) @@ -19796,24 +18290,11 @@ snapshots: '@csstools/utilities': 3.0.0(postcss@8.5.15) postcss: 8.5.15 - postcss-less@3.1.4: - dependencies: - postcss: 8.5.15 - postcss-logical@9.0.0(postcss@8.5.15): dependencies: postcss: 8.5.15 postcss-value-parser: 4.2.0 - postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15): - dependencies: - postcss: 8.5.15 - postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) - remark: 10.0.1 - unist-util-find-all-after: 1.0.5 - - postcss-media-query-parser@0.2.3: {} - postcss-nesting@14.0.0(postcss@8.5.15): dependencies: '@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1) @@ -19927,43 +18408,15 @@ snapshots: dependencies: postcss: 8.5.15 - postcss-reporter@6.0.1: - dependencies: - chalk: 2.4.2 - lodash: 4.18.1 - log-symbols: 2.2.0 - postcss: 8.5.15 - - postcss-resolve-nested-selector@0.1.6: {} - - postcss-safe-parser@4.0.2: - dependencies: - postcss: 8.5.15 - postcss-safe-parser@7.0.1(postcss@8.5.15): dependencies: postcss: 8.5.15 - postcss-sass@0.3.5: - dependencies: - gonzales-pe: 4.3.0 - postcss: 8.5.15 - - postcss-scss@2.1.1: - dependencies: - postcss: 8.5.15 - postcss-selector-not@9.0.0(postcss@8.5.15): dependencies: postcss: 8.5.15 postcss-selector-parser: 7.1.1 - postcss-selector-parser@3.1.2: - dependencies: - dot-prop: 5.3.0 - indexes-of: 1.0.1 - uniq: 1.0.1 - postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 @@ -19973,23 +18426,6 @@ snapshots: dependencies: postcss: 8.5.15 - postcss-sorting@4.1.0: - dependencies: - lodash: 4.18.1 - postcss: 8.5.15 - - postcss-syntax@0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15): - dependencies: - postcss: 8.5.15 - optionalDependencies: - postcss-html: 0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15) - postcss-jsx: 0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15) - postcss-less: 3.1.4 - postcss-markdown: 0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15) - postcss-scss: 2.1.1 - - postcss-value-parser@3.3.1: {} - postcss-value-parser@4.2.0: {} postcss-values-parser@6.0.2(postcss@8.5.15): @@ -20113,8 +18549,6 @@ snapshots: queue-microtask@1.2.3: {} - quick-lru@1.1.0: {} - quick-lru@5.1.1: {} quote-unquote@1.0.0: {} @@ -20172,17 +18606,6 @@ snapshots: normalize-package-data: 2.5.0 npm-normalize-package-bin: 1.0.1 - read-pkg-up@3.0.0: - dependencies: - find-up: 2.1.0 - read-pkg: 3.0.0 - - read-pkg@3.0.0: - dependencies: - load-json-file: 4.0.0 - normalize-package-data: 2.5.0 - path-type: 3.0.0 - readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -20198,11 +18621,6 @@ snapshots: readdirp@5.0.0: {} - redent@2.0.0: - dependencies: - indent-string: 3.2.0 - strip-indent: 2.0.0 - refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -20218,11 +18636,6 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 - regex-not@1.0.2: - dependencies: - extend-shallow: 3.0.2 - safe-regex: 1.1.0 - regex-recursion@5.1.1: dependencies: regex: 5.1.1 @@ -20324,24 +18737,6 @@ snapshots: transitivePeerDependencies: - supports-color - remark-parse@6.0.3: - dependencies: - collapse-white-space: 1.0.6 - is-alphabetical: 1.0.4 - is-decimal: 1.0.4 - is-whitespace-character: 1.0.4 - is-word-character: 1.0.4 - markdown-escapes: 1.0.4 - parse-entities: 1.2.2 - repeat-string: 1.6.1 - state-toggle: 1.0.3 - trim: 0.0.1 - trim-trailing-lines: 1.1.4 - unherit: 1.1.3 - unist-util-remove-position: 1.1.4 - vfile-location: 2.0.6 - xtend: 4.0.2 - remark-rehype@11.1.2: dependencies: '@types/hast': 3.0.4 @@ -20356,35 +18751,6 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - remark-stringify@6.0.4: - dependencies: - ccount: 1.1.0 - is-alphanumeric: 1.0.0 - is-decimal: 1.0.4 - is-whitespace-character: 1.0.4 - longest-streak: 2.0.4 - markdown-escapes: 1.0.4 - markdown-table: 1.1.3 - mdast-util-compact: 1.0.4 - parse-entities: 1.2.2 - repeat-string: 1.6.1 - state-toggle: 1.0.3 - stringify-entities: 1.3.2 - unherit: 1.1.3 - xtend: 4.0.2 - - remark@10.0.1: - dependencies: - remark-parse: 6.0.3 - remark-stringify: 6.0.4 - unified: 7.1.0 - - repeat-element@1.1.4: {} - - repeat-string@1.6.1: {} - - replace-ext@1.0.0: {} - require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -20406,14 +18772,10 @@ snapshots: resolve-dependency-path@4.0.1: {} - resolve-from@3.0.0: {} - resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} - resolve-url@0.2.1: {} - resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -20439,8 +18801,6 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 - ret@0.1.15: {} - retry@0.12.0: {} reusify@1.1.0: {} @@ -20578,10 +18938,6 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - safe-regex@1.1.0: - dependencies: - ret: 0.1.15 - safer-buffer@2.1.2: {} sanitize-filename@1.6.4: @@ -20675,13 +19031,6 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 - set-value@2.0.1: - dependencies: - extend-shallow: 2.0.1 - is-extendable: 0.1.1 - is-plain-object: 2.0.4 - split-string: 3.1.0 - setprototypeof@1.2.0: {} sharp@0.34.5: @@ -20784,12 +19133,6 @@ snapshots: slash@5.1.0: {} - slice-ansi@2.1.0: - dependencies: - ansi-styles: 3.2.1 - astral-regex: 1.0.0 - is-fullwidth-code-point: 2.0.0 - slice-ansi@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -20820,29 +19163,6 @@ snapshots: snabbdom@3.6.3: {} - snapdragon-node@2.1.1: - dependencies: - define-property: 1.0.0 - isobject: 3.0.1 - snapdragon-util: 3.0.1 - - snapdragon-util@3.0.1: - dependencies: - kind-of: 3.2.2 - - snapdragon@0.8.2: - dependencies: - base: 0.11.2 - debug: 2.6.9 - define-property: 0.2.5 - extend-shallow: 2.0.1 - map-cache: 0.2.2 - source-map: 0.5.7 - source-map-resolve: 0.5.3 - use: 3.1.1 - transitivePeerDependencies: - - supports-color - snapsvg-cjs@0.0.6(eve@0.5.4): dependencies: eve: 0.5.4 @@ -20854,23 +19174,11 @@ snapshots: source-map-js@1.2.1: {} - source-map-resolve@0.5.3: - dependencies: - atob: 2.1.2 - decode-uri-component: 0.2.2 - resolve-url: 0.2.1 - source-map-url: 0.4.1 - urix: 0.1.0 - source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 - source-map-url@0.4.1: {} - - source-map@0.5.7: {} - source-map@0.6.1: {} space-separated-tokens@2.0.2: {} @@ -20910,12 +19218,6 @@ snapshots: speakingurl@14.0.1: {} - specificity@0.4.1: {} - - split-string@3.1.0: - dependencies: - extend-shallow: 3.0.2 - sprintf-js@1.0.3: {} sprintf-js@1.1.3: @@ -20927,13 +19229,6 @@ snapshots: stat-mode@1.0.0: {} - state-toggle@1.0.3: {} - - static-extend@0.1.2: - dependencies: - define-property: 0.2.5 - object-copy: 0.1.0 - statuses@2.0.2: {} std-env@4.1.0: {} @@ -20947,12 +19242,6 @@ snapshots: dependencies: any-promise: 1.3.0 - string-width@3.1.0: - dependencies: - emoji-regex: 7.0.3 - is-fullwidth-code-point: 2.0.0 - strip-ansi: 5.2.0 - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -21024,13 +19313,6 @@ snapshots: dependencies: safe-buffer: 5.2.1 - stringify-entities@1.3.2: - dependencies: - character-entities-html4: 1.1.4 - character-entities-legacy: 1.1.4 - is-alphanumerical: 1.0.4 - is-hexadecimal: 1.0.4 - stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -21042,10 +19324,6 @@ snapshots: is-obj: 1.0.1 is-regexp: 1.0.0 - strip-ansi@5.2.0: - dependencies: - ansi-regex: 4.1.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -21060,8 +19338,6 @@ snapshots: strip-final-newline@2.0.0: {} - strip-indent@2.0.0: {} - strip-indent@4.1.1: {} strip-json-comments@2.0.1: {} @@ -21076,8 +19352,6 @@ snapshots: stubborn-utils@1.0.2: {} - style-search@0.1.0: {} - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.6): dependencies: client-only: 0.0.1 @@ -21085,13 +19359,6 @@ snapshots: optionalDependencies: '@babel/core': 7.29.0 - stylelint-config-rational-order@0.1.2: - dependencies: - stylelint: 9.10.1 - stylelint-order: 2.2.1(stylelint@9.10.1) - transitivePeerDependencies: - - supports-color - stylelint-config-recommended@18.0.0(stylelint@17.12.0(typescript@6.0.3)): dependencies: stylelint: 17.12.0(typescript@6.0.3) @@ -21101,13 +19368,6 @@ snapshots: stylelint: 17.12.0(typescript@6.0.3) stylelint-config-recommended: 18.0.0(stylelint@17.12.0(typescript@6.0.3)) - stylelint-order@2.2.1(stylelint@9.10.1): - dependencies: - lodash: 4.18.1 - postcss: 8.5.15 - postcss-sorting: 4.1.0 - stylelint: 9.10.1 - stylelint-order@8.1.1(stylelint@17.12.0(typescript@6.0.3)): dependencies: postcss: 8.5.15 @@ -21155,68 +19415,12 @@ snapshots: - supports-color - typescript - stylelint@9.10.1: - dependencies: - autoprefixer: 9.8.8 - balanced-match: 1.0.2 - chalk: 2.4.2 - cosmiconfig: 5.2.1 - debug: 4.4.3 - execall: 1.0.0 - file-entry-cache: 4.0.0 - get-stdin: 6.0.0 - global-modules: 2.0.0 - globby: 9.2.0 - globjoin: 0.1.4 - html-tags: 2.0.0 - ignore: 5.3.2 - import-lazy: 3.1.0 - imurmurhash: 0.1.4 - known-css-properties: 0.11.0 - leven: 2.1.0 - lodash: 4.18.1 - log-symbols: 2.2.0 - mathml-tag-names: 2.1.3 - meow: 5.0.0 - micromatch: 3.1.10 - normalize-selector: 0.2.0 - pify: 4.0.1 - postcss: 8.5.15 - postcss-html: 0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15) - postcss-jsx: 0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15) - postcss-less: 3.1.4 - postcss-markdown: 0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15) - postcss-media-query-parser: 0.2.3 - postcss-reporter: 6.0.1 - postcss-resolve-nested-selector: 0.1.6 - postcss-safe-parser: 4.0.2 - postcss-sass: 0.3.5 - postcss-scss: 2.1.1 - postcss-selector-parser: 3.1.2 - postcss-syntax: 0.36.2(postcss-html@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-jsx@0.36.4(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-less@3.1.4)(postcss-markdown@0.36.0(postcss-syntax@0.36.2(postcss@8.5.15))(postcss@8.5.15))(postcss-scss@2.1.1)(postcss@8.5.15) - postcss-value-parser: 3.3.1 - resolve-from: 4.0.0 - signal-exit: 3.0.7 - slash: 2.0.0 - specificity: 0.4.1 - string-width: 3.1.0 - style-search: 0.1.0 - sugarss: 2.0.0 - svg-tags: 1.0.0 - table: 5.4.6 - transitivePeerDependencies: - - supports-color - stylis@4.4.0: {} stylus-lookup@6.1.2: dependencies: commander: 12.1.0 - sugarss@2.0.0: - dependencies: - postcss: 8.5.15 - sumchecker@3.0.1: dependencies: debug: 4.4.3 @@ -21262,13 +19466,6 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 - table@5.4.6: - dependencies: - ajv: 6.15.0 - lodash: 4.18.1 - slice-ansi: 2.1.0 - string-width: 3.1.0 - table@6.9.0: dependencies: ajv: 8.20.0 @@ -21356,26 +19553,10 @@ snapshots: tmp@0.2.5: {} - to-object-path@0.3.0: - dependencies: - kind-of: 3.2.2 - - to-regex-range@2.1.1: - dependencies: - is-number: 3.0.0 - repeat-string: 1.6.1 - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - to-regex@3.0.2: - dependencies: - define-property: 2.0.2 - extend-shallow: 3.0.2 - regex-not: 1.0.2 - safe-regex: 1.1.0 - to-valid-identifier@1.0.0: dependencies: '@sindresorhus/base62': 1.0.0 @@ -21405,14 +19586,6 @@ snapshots: trim-lines@3.0.1: {} - trim-newlines@2.0.0: {} - - trim-trailing-lines@1.1.4: {} - - trim@0.0.1: {} - - trough@1.0.5: {} - trough@2.2.0: {} truncate-utf8-bytes@1.0.2: @@ -21568,11 +19741,6 @@ snapshots: dependencies: pathe: 2.0.3 - unherit@1.1.3: - dependencies: - inherits: 2.0.4 - xtend: 4.0.2 - unicorn-magic@0.3.0: {} unicorn-magic@0.4.0: {} @@ -21589,32 +19757,6 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unified@7.1.0: - dependencies: - '@types/unist': 2.0.11 - '@types/vfile': 3.0.2 - bail: 1.0.5 - extend: 3.0.2 - is-plain-obj: 1.1.0 - trough: 1.0.5 - vfile: 3.0.1 - x-is-string: 0.1.0 - - union-value@1.0.1: - dependencies: - arr-union: 3.1.0 - get-value: 2.0.6 - is-extendable: 0.1.1 - set-value: 2.0.1 - - uniq@1.0.1: {} - - unist-util-find-all-after@1.0.5: - dependencies: - unist-util-is: 3.0.0 - - unist-util-is@3.0.0: {} - unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -21623,34 +19765,20 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-remove-position@1.1.4: - dependencies: - unist-util-visit: 1.4.1 - unist-util-remove-position@5.0.0: dependencies: '@types/unist': 3.0.3 unist-util-visit: 5.1.0 - unist-util-stringify-position@1.1.2: {} - unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-visit-parents@2.1.2: - dependencies: - unist-util-is: 3.0.0 - unist-util-visit-parents@6.0.2: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.1 - unist-util-visit@1.4.1: - dependencies: - unist-util-visit-parents: 2.1.2 - unist-util-visit@5.1.0: dependencies: '@types/unist': 3.0.3 @@ -21716,11 +19844,6 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 - unset-value@1.0.0: - dependencies: - has-value: 0.3.1 - isobject: 3.0.1 - update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -21731,12 +19854,8 @@ snapshots: dependencies: punycode: 2.3.1 - urix@0.1.0: {} - urlpattern-polyfill@10.1.0: {} - use@3.1.1: {} - utf8-byte-length@1.0.5: {} util-deprecate@1.0.2: {} @@ -22023,29 +20142,16 @@ snapshots: extsprintf: 1.4.1 optional: true - vfile-location@2.0.6: {} - vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 vfile: 6.0.3 - vfile-message@1.1.1: - dependencies: - unist-util-stringify-position: 1.1.2 - vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 - vfile@3.0.1: - dependencies: - is-buffer: 2.0.5 - replace-ext: 1.0.0 - unist-util-stringify-position: 1.1.2 - vfile-message: 1.1.1 - vfile@6.0.3: dependencies: '@types/unist': 3.0.3 @@ -22397,14 +20503,8 @@ snapshots: dependencies: signal-exit: 4.1.0 - write@1.0.3: - dependencies: - mkdirp: 0.5.6 - ws@8.20.1: {} - x-is-string@0.1.0: {} - xml-name-validator@4.0.0: {} xml-name-validator@5.0.0: {} @@ -22415,8 +20515,6 @@ snapshots: xmlchars@2.2.0: {} - xtend@4.0.2: {} - y18n@5.0.8: {} yallist@3.1.1: {} @@ -22432,10 +20530,6 @@ snapshots: yaml@2.9.0: {} - yargs-parser@10.1.0: - dependencies: - camelcase: 4.1.0 - yargs-parser@21.1.1: {} yargs-parser@22.0.0: {} From 3a3c092e1e7626575b05ef5e5e2cbabf58bec3a9 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 12:54:17 +0800 Subject: [PATCH 07/17] feat(muya): add cursorCoords + active formats to selection-change (#4387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): add cursorCoords and active formats to selection-change The desktop editor relies on two extras the legacy muyajs put on its selectionChange payload: cursorCoords (the caret's screen rect, used for typewriter-mode scrolling via changes.cursorCoords.y) and the active inline formats at the cursor (to light up the format toolbar). Add both to the selection-change event emitted by Selection.setSelection. Formats are duck-typed off the anchor block's getFormatsInRange to avoid a selection -> format circular import. Adds happy-dom coverage. Part of the muyajs -> @muyajs/core engine migration (stage A8). Co-Authored-By: Claude Opus 4.8 (1M context) * fix(muya): address selection-change review (#4387) - cursorCoords now follows the caret/focus end for forward range selections (getCursorCoords gains a `preferEnd` param, defaulted false so existing callers are unchanged; setSelection passes `direction === 'forward'`). getClientRects returns the first rect by default, which is the selection start — typewriter scrolling should track the caret, not the start. - Strengthen the spec with a non-empty case: cursor inside `**bold**` now asserts a `strong` token is emitted in `formats`, not just that it is an array. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../__tests__/selectionChange.spec.ts | 88 +++++++++++++++++++ packages/muya/src/selection/index.ts | 25 +++++- 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 packages/muya/src/selection/__tests__/selectionChange.spec.ts diff --git a/packages/muya/src/selection/__tests__/selectionChange.spec.ts b/packages/muya/src/selection/__tests__/selectionChange.spec.ts new file mode 100644 index 0000000000..fc2d3d6fed --- /dev/null +++ b/packages/muya/src/selection/__tests__/selectionChange.spec.ts @@ -0,0 +1,88 @@ +// @vitest-environment happy-dom + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { Muya } from '../../muya'; + +// Coverage for the selection-change payload extras added for the +// muyajs -> @muyajs/core desktop migration: `cursorCoords` (typewriter-mode +// scrolling) and `formats` (active inline formats, for lighting up the +// desktop toolbar). The legacy engine put both on its `selectionChange` +// event; the desktop reads `changes.cursorCoords.y` and the format list. + +const bootedHosts: HTMLElement[] = []; +let originalVersion: string | undefined; +let hadVersion = false; + +beforeEach(() => { + hadVersion = 'MUYA_VERSION' in window; + originalVersion = window.MUYA_VERSION; + window.MUYA_VERSION = 'test'; +}); + +afterEach(() => { + while (bootedHosts.length) { + const host = bootedHosts.pop()!; + host.remove(); + } + if (hadVersion) + window.MUYA_VERSION = originalVersion as string; + else + delete (window as Partial).MUYA_VERSION; +}); + +function bootMuya(markdown: string): Muya { + const host = document.createElement('div'); + document.body.appendChild(host); + const muya = new Muya(host, { markdown } as ConstructorParameters[1]); + muya.init(); + bootedHosts.push(muya.domNode); + return muya; +} + +describe('selection-change payload', () => { + it('includes cursorCoords and a formats array', () => { + const muya = bootMuya('hello world\n'); + const first = muya.editor.scrollPage!.firstContentInDescendant()!; + + let payload: Record | null = null; + muya.on('selection-change', (p: unknown) => { + payload = p as Record; + }); + + muya.editor.selection.setSelection({ + anchor: { offset: 0 }, + focus: { offset: 5 }, + block: first, + path: first.path, + }); + + expect(payload).not.toBeNull(); + // cursorCoords is a DOMRect | null (null under happy-dom, which has no + // real layout) — assert the key is present so the desktop typewriter + // path always receives it. + expect(payload!).toHaveProperty('cursorCoords'); + expect(Array.isArray(payload!.formats)).toBe(true); + }); + + it('reports the active inline format when the cursor is inside bold text', () => { + const muya = bootMuya('**bold**\n'); + const first = muya.editor.scrollPage!.firstContentInDescendant()!; + + let payload: Record | null = null; + muya.on('selection-change', (p: unknown) => { + payload = p as Record; + }); + + // `**bold**` — place the selection inside the bolded word (offsets 3–5). + muya.editor.selection.setSelection({ + anchor: { offset: 3 }, + focus: { offset: 5 }, + block: first, + path: first.path, + }); + + expect(payload).not.toBeNull(); + const formats = payload!.formats as Array<{ type: string }>; + expect(formats.some(f => f.type === 'strong')).toBe(true); + }); +}); diff --git a/packages/muya/src/selection/index.ts b/packages/muya/src/selection/index.ts index 25bf5a2ecc..73600e4911 100644 --- a/packages/muya/src/selection/index.ts +++ b/packages/muya/src/selection/index.ts @@ -37,7 +37,7 @@ class Selection { }; } - static getCursorCoords() { + static getCursorCoords(preferEnd = false) { const sel = document.getSelection(); let range; let rect = null; @@ -54,8 +54,11 @@ class Selection { : null; } + // For a forward range selection the caret sits at the END, so + // prefer the last client rect; otherwise the first rect is the + // caret (collapsed cursor or backward selection). if (rects?.length) - rect = rects[0]; + rect = preferEnd ? rects[rects.length - 1] : rects[0]; } } @@ -305,6 +308,22 @@ class Selection { selectedImage, } = this; + // Backport of marktext's `selectionChange` payload extras the desktop + // relies on: `cursorCoords` for typewriter-mode scrolling and the + // active inline formats at the cursor for lighting up the toolbar. + // Follow the caret (focus end) for forward selections so typewriter + // scrolling tracks the cursor rather than the selection start. + const cursorCoords = Selection.getCursorCoords(direction === 'forward'); + // Duck-type the Format block — a value import of Format here would + // create a selection -> format circular dependency. + const anchorBlockRef = this.anchorBlock as Format | null; + const formats + = isSelectionInSameBlock + && anchorBlockRef + && typeof anchorBlockRef.getFormatsInRange === 'function' + ? anchorBlockRef.getFormatsInRange().formats + : []; + this.muya.eventCenter.emit('selection-change', { anchor, focus, @@ -317,6 +336,8 @@ class Selection { direction, type, selectedImage, + cursorCoords, + formats, }); } From a49c7b7111163c2e2777518c5fa8863f30cc8b43 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 13:29:57 +0800 Subject: [PATCH 08/17] feat(muya): implement focus mode (dim inactive blocks) (#4389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): implement focus mode (dim inactive blocks) Focus mode was a declared no-op in @muyajs/core: the `focusMode` option and the `mu-focus-mode` class name existed but nothing applied the class and there was no dimming CSS. - Add `Muya#setFocusMode(bool)` which toggles `mu-focus-mode` on the editor container and stores `options.focusMode`, mirroring legacy muyajs `setFocusMode`. Apply the class at construction (`getContainer`) when `focusMode: true` is passed. - Add dimming CSS to blockSyntax.css: under `.mu-focus-mode`, every top-level block (`.mu-container > *`) fades to `opacity: 0.25` with a smooth transition, and the active block (`.mu-active`, applied to the focused content block's ancestor chain) stays at full opacity — mirroring legacy `.ag-focus-mode`. - Add a happy-dom unit test asserting the class toggles via the constructor and `setFocusMode`, and rewrite the e2e spec (which previously documented the no-op) to assert the class is applied and inactive blocks are dimmed. Co-Authored-By: Claude Opus 4.8 (1M context) * test(muya): make focus-mode e2e opacity assertion non-flaky The opacity assertion read getComputedStyle synchronously right after the click, catching the active block mid-transition (the `opacity 0.2s` fade from 0.25 to 1), so it intermittently saw an interpolated value instead of "1". Switch to Playwright's auto-retrying `toHaveCSS('opacity', ...)` on the active and inactive block locators, which waits for the transition to settle. Verified locally against system Chrome: all 3 focus-mode specs pass. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- packages/muya/e2e/tests/helpers/selectors.ts | 3 + .../muya/e2e/tests/options/focus-mode.spec.ts | 65 ++++++++++---- packages/muya/src/__tests__/focusMode.spec.ts | 87 +++++++++++++++++++ .../muya/src/assets/styles/blockSyntax.css | 15 ++++ packages/muya/src/muya.ts | 22 ++++- 5 files changed, 173 insertions(+), 19 deletions(-) create mode 100644 packages/muya/src/__tests__/focusMode.spec.ts diff --git a/packages/muya/e2e/tests/helpers/selectors.ts b/packages/muya/e2e/tests/helpers/selectors.ts index 1879953806..4e71693f29 100644 --- a/packages/muya/e2e/tests/helpers/selectors.ts +++ b/packages/muya/e2e/tests/helpers/selectors.ts @@ -8,6 +8,9 @@ export const editor = { container: '#editor', root: '.mu-editor', + // Editor root carries this class while focus mode is enabled (toggled by + // `Muya#setFocusMode` / applied at construction for `focusMode: true`). + focusModeRoot: '.mu-editor.mu-focus-mode', paragraph: '.mu-paragraph', atxHeading: '.mu-atx-heading', setextHeading: '.mu-setext-heading', diff --git a/packages/muya/e2e/tests/options/focus-mode.spec.ts b/packages/muya/e2e/tests/options/focus-mode.spec.ts index 138f029811..b27f018210 100644 --- a/packages/muya/e2e/tests/options/focus-mode.spec.ts +++ b/packages/muya/e2e/tests/options/focus-mode.spec.ts @@ -2,52 +2,81 @@ import { expect, test } from '../fixtures/muya'; import { editor } from '../helpers/selectors'; /** - * `focusMode: true` constructor option. + * `focusMode` option + `Muya#setFocusMode`. * - * Current state of the codebase: `focusMode` is declared in - * `IMuyaOptions`, defaulted to `false`, and reserved a class name - * `CLASS_NAMES.MU_FOCUS_MODE` ('mu-focus-mode') — but no code path - * actually applies that class to the DOM when the option is enabled. - * This makes focus-mode currently a no-op option. + * Focus mode dims every top-level block except the one holding the cursor. + * It is driven by the `mu-focus-mode` class on the `.mu-editor` root: + * - applied at construction when `new Muya(el, { focusMode: true })` + * (source: `packages/core/src/muya.ts::getContainer`), + * - toggled at runtime by `muya.setFocusMode(bool)`. + * The dimming itself lives in CSS (`.mu-focus-mode .mu-container > *`). * - * What this spec asserts (the option *is* respected by the constructor): - * 1. `new Muya(container, { focusMode: true })` boots without crashing. - * 2. `muya.options.focusMode === true` after rebuild. - * 3. The active paragraph can still be focused and typed into. - * - * What this spec deliberately does NOT assert: a visual marker on - * non-active paragraphs. There is no such marker today. See BACKLOG - * Phase 5 follow-up — once focus-mode renders a marker class, this - * spec should be tightened to assert it. + * This used to be a no-op (the option was declared and the class reserved but + * never applied); these specs lock in the implemented behavior. */ test.describe('options / focus-mode', () => { - test('focusMode: true — rebuild boots, option reflected, editor usable', async ({ page }) => { + test('focusMode: true — root carries mu-focus-mode, option reflected, editor usable', async ({ page }) => { await page.evaluate(() => { window.__e2e!.rebuildMuya({ focusMode: true }); window.muya!.setContent('# heading\n\nparagraph A\n\nparagraph B\n'); }); + const focusModeOption = await page.evaluate(() => window.muya!.options.focusMode); expect(focusModeOption).toBe(true); + // The class is applied to the editor root at construction. + await expect(page.locator(editor.focusModeRoot)).toBeVisible(); + // Sanity: editor renders multiple blocks and can be focused. await expect(page.locator(editor.atxHeading).first()).toBeVisible(); await expect(page.locator(editor.paragraph).nth(0)).toContainText('paragraph A'); await expect(page.locator(editor.paragraph).nth(1)).toContainText('paragraph B'); - // Click into paragraph B; the editor should remain alive. + // Click into paragraph B; the editor should remain alive and the active + // block keeps full opacity while the others are dimmed. await page.locator(editor.paragraph).nth(1).click(); const focused = await page.evaluate(() => { const active = window.muya!.editor.activeContentBlock; return active != null; }); expect(focused).toBe(true); + + // Exactly one top-level block (the active one) is at full opacity; the + // rest are dimmed to 0.25. Use auto-retrying `toHaveCSS` so the + // `opacity 0.2s` transition has settled before we read the value. + const activeBlock = page.locator('.mu-container > .mu-active'); + await expect(activeBlock).toHaveCount(1); + await expect(activeBlock).toHaveCSS('opacity', '1'); + + const inactiveBlocks = page.locator('.mu-container > :not(.mu-active)'); + const inactiveCount = await inactiveBlocks.count(); + expect(inactiveCount).toBeGreaterThan(0); + for (let i = 0; i < inactiveCount; i++) + await expect(inactiveBlocks.nth(i)).toHaveCSS('opacity', '0.25'); }); - test('focusMode: false (default) — option reflected as false', async ({ page }) => { + test('focusMode: false (default) — option reflected as false, no class', async ({ page }) => { await page.evaluate(() => { window.__e2e!.rebuildMuya({ focusMode: false }); }); + const value = await page.evaluate(() => window.muya!.options.focusMode); expect(value).toBe(false); + + await expect(page.locator(editor.root)).toBeVisible(); + await expect(page.locator(editor.focusModeRoot)).toHaveCount(0); + }); + + test('setFocusMode toggles the class and option at runtime', async ({ page }) => { + await page.evaluate(() => window.__e2e!.rebuildMuya({ focusMode: false })); + await expect(page.locator(editor.focusModeRoot)).toHaveCount(0); + + await page.evaluate(() => window.muya!.setFocusMode(true)); + expect(await page.evaluate(() => window.muya!.options.focusMode)).toBe(true); + await expect(page.locator(editor.focusModeRoot)).toBeVisible(); + + await page.evaluate(() => window.muya!.setFocusMode(false)); + expect(await page.evaluate(() => window.muya!.options.focusMode)).toBe(false); + await expect(page.locator(editor.focusModeRoot)).toHaveCount(0); }); }); diff --git a/packages/muya/src/__tests__/focusMode.spec.ts b/packages/muya/src/__tests__/focusMode.spec.ts new file mode 100644 index 0000000000..87fbb4764a --- /dev/null +++ b/packages/muya/src/__tests__/focusMode.spec.ts @@ -0,0 +1,87 @@ +// @vitest-environment happy-dom + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { Muya } from '../muya'; + +// Coverage for focus mode. marktext muyajs declared a `focusMode` option and +// reserved the `mu-focus-mode` class name but never applied it — focus mode was +// a complete no-op. `Muya#setFocusMode` now toggles `mu-focus-mode` on the +// editor container (and the constructor applies it when `focusMode: true` is +// passed), with the dimming itself driven by CSS in blockSyntax.css. These +// tests lock in the class-toggling contract; the visual dimming is verified by +// the e2e suite. + +const FOCUS_MODE_CLASS = 'mu-focus-mode'; + +const bootedHosts: HTMLElement[] = []; +let originalVersion: string | undefined; +let hadVersion = false; + +beforeEach(() => { + hadVersion = 'MUYA_VERSION' in window; + originalVersion = window.MUYA_VERSION; + window.MUYA_VERSION = 'test'; +}); + +afterEach(() => { + while (bootedHosts.length) { + const host = bootedHosts.pop()!; + host.remove(); + } + if (hadVersion) + window.MUYA_VERSION = originalVersion as string; + else + delete (window as Partial).MUYA_VERSION; +}); + +function bootMuya(options: Partial[1]> = {}): Muya { + const host = document.createElement('div'); + document.body.appendChild(host); + const muya = new Muya(host, { markdown: '# heading\n\nparagraph A\n\nparagraph B\n', ...options } as ConstructorParameters[1]); + muya.init(); + bootedHosts.push(muya.domNode); + return muya; +} + +describe('muya focus mode', () => { + it('does not apply mu-focus-mode by default', () => { + const muya = bootMuya(); + expect(muya.options.focusMode).toBe(false); + expect(muya.domNode.classList.contains(FOCUS_MODE_CLASS)).toBe(false); + }); + + it('applies mu-focus-mode at construction when focusMode: true', () => { + const muya = bootMuya({ focusMode: true }); + expect(muya.options.focusMode).toBe(true); + expect(muya.domNode.classList.contains(FOCUS_MODE_CLASS)).toBe(true); + }); + + it('setFocusMode(false) removes the class and clears the option', () => { + const muya = bootMuya({ focusMode: true }); + expect(muya.domNode.classList.contains(FOCUS_MODE_CLASS)).toBe(true); + + muya.setFocusMode(false); + expect(muya.options.focusMode).toBe(false); + expect(muya.domNode.classList.contains(FOCUS_MODE_CLASS)).toBe(false); + }); + + it('setFocusMode(true) adds the class and sets the option', () => { + const muya = bootMuya(); + expect(muya.domNode.classList.contains(FOCUS_MODE_CLASS)).toBe(false); + + muya.setFocusMode(true); + expect(muya.options.focusMode).toBe(true); + expect(muya.domNode.classList.contains(FOCUS_MODE_CLASS)).toBe(true); + }); + + it('toggling is idempotent (re-adding keeps a single class)', () => { + const muya = bootMuya(); + muya.setFocusMode(true); + muya.setFocusMode(true); + expect(muya.domNode.classList.contains(FOCUS_MODE_CLASS)).toBe(true); + + muya.setFocusMode(false); + muya.setFocusMode(false); + expect(muya.domNode.classList.contains(FOCUS_MODE_CLASS)).toBe(false); + }); +}); diff --git a/packages/muya/src/assets/styles/blockSyntax.css b/packages/muya/src/assets/styles/blockSyntax.css index 683221179d..f2ec99e03b 100644 --- a/packages/muya/src/assets/styles/blockSyntax.css +++ b/packages/muya/src/assets/styles/blockSyntax.css @@ -853,3 +853,18 @@ pre.mu-active.mu-fenced-code::after { .mu-container figure.mu-footnote > i.mu-footnote-backlink:hover { color: var(--theme-color); } + +/* Focus mode (mirrors marktext muyajs `.ag-focus-mode`): when the editor + container carries `mu-focus-mode`, dim every top-level block and restore full + opacity on the active one. `mu-active` is applied to the whole ancestor chain + of the focused content block, so the top-level block containing the cursor + keeps `opacity: 1`. The class is toggled by `Muya#setFocusMode`. */ +.mu-focus-mode .mu-container > * { + opacity: 0.25; + + transition: opacity 0.2s ease-in-out; +} + +.mu-focus-mode .mu-container > .mu-active { + opacity: 1; +} diff --git a/packages/muya/src/muya.ts b/packages/muya/src/muya.ts index 3801bfa8b0..6d9d3eacfa 100644 --- a/packages/muya/src/muya.ts +++ b/packages/muya/src/muya.ts @@ -176,6 +176,21 @@ export class Muya { this.editor.focus(); } + /** + * Toggle focus mode (mirrors marktext muyajs `setFocusMode`). When enabled, + * every top-level block except the one holding the cursor is dimmed via the + * `mu-focus-mode` class on the editor container; the dimming itself lives in + * the stylesheet (`.mu-focus-mode .mu-container > * { opacity }`). + */ + setFocusMode(focusMode: boolean) { + if (focusMode) + this.domNode.classList.add(CLASS_NAMES.MU_FOCUS_MODE); + else + this.domNode.classList.remove(CLASS_NAMES.MU_FOCUS_MODE); + + this.options.focusMode = focusMode; + } + selectAll() { this.editor.selection.selectAll(); } @@ -376,7 +391,7 @@ export class Muya { * [ensureContainerDiv ensure container element is div] */ function getContainer(originContainer: HTMLElement, options: IMuyaOptions) { - const { spellcheckEnabled, hideQuickInsertHint } = options; + const { spellcheckEnabled, hideQuickInsertHint, focusMode } = options; const newContainer = document.createElement('div'); const attrs = originContainer.attributes; // Copy attrs from origin container to new container @@ -387,6 +402,11 @@ function getContainer(originContainer: HTMLElement, options: IMuyaOptions) { if (!hideQuickInsertHint) newContainer.classList.add(CLASS_NAMES.MU_SHOW_QUICK_INSERT_HINT); + // Apply focus mode at construction when initially enabled; `setFocusMode` + // toggles it thereafter. + if (focusMode) + newContainer.classList.add(CLASS_NAMES.MU_FOCUS_MODE); + newContainer.classList.add(CLASS_NAMES.MU_EDITOR); newContainer.setAttribute('contenteditable', 'true'); From c2c4a1e10f6bf568c17626dfbe56b0c8fcd77f3b Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 13:31:11 +0800 Subject: [PATCH 09/17] feat(muya): emit format-click and preview-image interaction events (#4390) * feat(muya): emit format-click interaction events for links and images Desktop is migrating off legacy packages/muyajs to packages/muya (@muyajs/core). The legacy engine emitted custom interaction events that the desktop renderer's editor.vue subscribes to in order to open links and preview images on a Cmd/Ctrl-click. packages/muya had no such emitter, so port the link + image format-click events: - editor/linkMouseEvents.ts: on a Cmd/Ctrl-click of a rendered link, emit `format-click` with { event, formatType: 'link', data } where data is the getLinkInfo payload (superset of the legacy { text, href }). Plain clicks keep their cursor-placement-only behavior (preventDefault unchanged). - selection/index.ts (_handleClickInlineImage): on a Cmd/Ctrl-click of an , emit `format-click` with { event, formatType: 'image', data: } (src resolved via getImageSrc, falling back to the src attribute). The plain-click image-toolbar / transformer / selector path is untouched. The desktop's format-click handler re-checks the OS modifier and opens the link / SimpleImageViewer, so muya stays platform-agnostic and only emits. heading-copy-link is deferred: it needs new heading-hover UI that does not exist in packages/muya yet. Adds a happy-dom vitest spec that subscribes via muya.on('format-click') and simulates modifier-clicks on a rendered link and image, asserting the payload shape and that plain clicks do not emit. Co-Authored-By: Claude Opus 4.8 (1M context) * test(muya): cover Ctrl-click image format-click (#4390) Address Copilot review: the image format-click tests only exercised the macOS metaKey modifier, but the handler emits on (metaKey || ctrlKey). Add a Ctrl-click case for the image path, mirroring the existing link Ctrl-click test. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../src/__tests__/formatClickEvents.spec.ts | 159 ++++++++++++++++++ packages/muya/src/editor/linkMouseEvents.ts | 40 ++++- packages/muya/src/selection/index.ts | 24 ++- 3 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 packages/muya/src/__tests__/formatClickEvents.spec.ts diff --git a/packages/muya/src/__tests__/formatClickEvents.spec.ts b/packages/muya/src/__tests__/formatClickEvents.spec.ts new file mode 100644 index 0000000000..a0ada0a551 --- /dev/null +++ b/packages/muya/src/__tests__/formatClickEvents.spec.ts @@ -0,0 +1,159 @@ +// @vitest-environment happy-dom + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { CLASS_NAMES } from '../config'; +import { Muya } from '../muya'; + +// Coverage for the desktop-migration interaction events ported from legacy +// `packages/muyajs` (`clickCtrl.js` link path + `clickEvent.js` image path): +// +// - `format-click` { event, formatType: 'link', data } on a Cmd/Ctrl-click +// of a rendered link. `data` is the `getLinkInfo` payload (superset of +// the legacy `{ text, href }`). +// - `format-click` { event, formatType: 'image', data: } on a +// Cmd/Ctrl-click of a rendered . +// +// The desktop renderer (`editor.vue`) re-checks the modifier and opens the +// link / image viewer; muya only emits, leaving the plain-click +// cursor-placement / image-toolbar behaviour untouched. + +const bootedHosts: HTMLElement[] = []; +let originalVersion: string | undefined; +let hadVersion = false; + +beforeEach(() => { + hadVersion = 'MUYA_VERSION' in window; + originalVersion = window.MUYA_VERSION; + window.MUYA_VERSION = 'test'; +}); + +afterEach(() => { + while (bootedHosts.length) { + const host = bootedHosts.pop()!; + host.remove(); + } + if (hadVersion) + window.MUYA_VERSION = originalVersion as string; + else + delete (window as Partial).MUYA_VERSION; +}); + +function bootMuya(markdown: string): Muya { + const host = document.createElement('div'); + document.body.appendChild(host); + const muya = new Muya(host, { markdown } as ConstructorParameters[1]); + muya.init(); + bootedHosts.push(muya.domNode); + return muya; +} + +function dispatchClick(target: Element, init: MouseEventInit = {}): void { + target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, ...init })); +} + +describe('format-click on links', () => { + it('emits format-click with the link payload on a Cmd/Ctrl-click', () => { + const muya = bootMuya('[hello](https://example.com)'); + const link = muya.domNode.querySelector(`span.${CLASS_NAMES.MU_LINK}`)!; + expect(link).toBeTruthy(); + + const handler = vi.fn(); + muya.on('format-click', handler); + + dispatchClick(link, { metaKey: true }); + + expect(handler).toHaveBeenCalledTimes(1); + const payload = handler.mock.calls[0][0]; + expect(payload.formatType).toBe('link'); + expect(payload.event).toBeInstanceOf(MouseEvent); + expect(payload.data.href).toBe('https://example.com'); + expect(payload.data.text).toBe('hello'); + }); + + it('also fires for a Ctrl-click (non-macOS modifier)', () => { + const muya = bootMuya('[hello](https://example.com)'); + const link = muya.domNode.querySelector(`span.${CLASS_NAMES.MU_LINK}`)!; + + const handler = vi.fn(); + muya.on('format-click', handler); + + dispatchClick(link, { ctrlKey: true }); + + expect(handler).toHaveBeenCalledTimes(1); + expect(handler.mock.calls[0][0].formatType).toBe('link'); + }); + + it('does NOT emit format-click on a plain (no-modifier) link click', () => { + const muya = bootMuya('[hello](https://example.com)'); + const link = muya.domNode.querySelector(`span.${CLASS_NAMES.MU_LINK}`)!; + + const handler = vi.fn(); + muya.on('format-click', handler); + + dispatchClick(link); + + expect(handler).not.toHaveBeenCalled(); + }); +}); + +describe('format-click on images', () => { + // Boot an image and inject a loaded into the rendered container. + // The async `loadImageAsync` path never resolves in happy-dom (no real + // network / Image decode), so we stand in the the renderer would + // have produced and drive the click through the real handler. + function bootImage(src: string): { muya: Muya; img: HTMLImageElement } { + const muya = bootMuya(`![alt](${src})`); + const wrapper = muya.domNode.querySelector( + `span.${CLASS_NAMES.MU_INLINE_IMAGE}`, + )!; + const container = wrapper.querySelector( + `.${CLASS_NAMES.MU_IMAGE_CONTAINER}`, + )!; + const img = document.createElement('img'); + img.setAttribute('src', src); + container.appendChild(img); + return { muya, img }; + } + + it('emits format-click with the image src on a Cmd/Ctrl-click', () => { + const src = 'https://example.com/x.png'; + const { muya, img } = bootImage(src); + + const handler = vi.fn(); + muya.on('format-click', handler); + + dispatchClick(img, { metaKey: true }); + + expect(handler).toHaveBeenCalledTimes(1); + const payload = handler.mock.calls[0][0]; + expect(payload.formatType).toBe('image'); + expect(payload.event).toBeInstanceOf(MouseEvent); + expect(payload.data).toBe(src); + }); + + it('emits image format-click on a Ctrl-click (non-macOS modifier)', () => { + const src = 'https://example.com/x.png'; + const { muya, img } = bootImage(src); + + const handler = vi.fn(); + muya.on('format-click', handler); + + dispatchClick(img, { ctrlKey: true }); + + expect(handler).toHaveBeenCalledTimes(1); + const payload = handler.mock.calls[0][0]; + expect(payload.formatType).toBe('image'); + expect(payload.data).toBe(src); + }); + + it('does NOT emit format-click on a plain (no-modifier) image click', () => { + const { muya, img } = bootImage('https://example.com/x.png'); + + const handler = vi.fn(); + muya.on('format-click', handler); + + dispatchClick(img); + + expect(handler).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/muya/src/editor/linkMouseEvents.ts b/packages/muya/src/editor/linkMouseEvents.ts index 83ad8af6ae..78f35ed2ac 100644 --- a/packages/muya/src/editor/linkMouseEvents.ts +++ b/packages/muya/src/editor/linkMouseEvents.ts @@ -57,6 +57,13 @@ function findLinkWrapper(target: EventTarget | null): HTMLElement | null { return target.closest(LINK_SELECTOR); } +// True for a Cmd-click (macOS) or Ctrl-click (other platforms). The desktop +// renderer makes the same OS distinction; emitting on either keeps muya +// platform-agnostic and lets the host decide. +function isModifierClick(event: Event): boolean { + return event instanceof MouseEvent && (event.metaKey || event.ctrlKey); +} + function isPopoverTarget(wrapper: HTMLElement): boolean { // HTML `` is always a popover target — no source markers to hide. if (wrapper.classList.contains(CLASS_NAMES.MU_RAW_HTML)) @@ -116,8 +123,39 @@ export function attachLinkMouseHandlers(muya: Muya): void { if (!(event.target instanceof HTMLElement)) return; - if (event.target.closest(ANCHOR_CLICK_SELECTOR)) + // Suppress in-editor navigation for every real anchor variant. Place + // the cursor instead of opening a tab (standard contenteditable + // rich-text pattern). This runs for plain clicks too. + const anchor = event.target.closest(ANCHOR_CLICK_SELECTOR); + if (anchor) event.preventDefault(); + + // Cmd/Ctrl-click a link → ask the host to open it. marktext's + // `clickCtrl.js` dispatched `format-click` with `{ event, formatType: + // 'link', data: { text, href } }`; the desktop renderer gates on the + // modifier itself (`editor.vue` `format-click` handler) and calls + // `FORMAT_LINK_CLICK({ data })`, so the only contract it needs is a + // `data.href`. We gate on the modifier here too so plain clicks keep + // their cursor-placement-only behavior. `getLinkInfo` resolves the + // wrapper that hosts the href even when the IMG/text descendant was + // clicked, and returns a superset (`{ href, raw, text, range }`) of + // the legacy `{ text, href }` payload. + if (!isModifierClick(event)) + return; + + const wrapper = findLinkWrapper(event.target); + if (!wrapper) + return; + + const linkInfo = getLinkInfo(wrapper); + if (!linkInfo || !linkInfo.href) + return; + + eventCenter.emit('format-click', { + event, + formatType: 'link', + data: linkInfo, + }); }; eventCenter.attachDOMEvent(domNode, 'mouseover', overHandler); diff --git a/packages/muya/src/selection/index.ts b/packages/muya/src/selection/index.ts index 73600e4911..5f30f046c6 100644 --- a/packages/muya/src/selection/index.ts +++ b/packages/muya/src/selection/index.ts @@ -8,7 +8,7 @@ import type { Muya } from '../muya'; import type { ICursor, INodeOffset, ISelection } from './types'; import { BLOCK_DOM_PROPERTY, CLASS_NAMES } from '../config'; import { isElement, isHTMLElement, isKeyboardEvent, isMouseEvent } from '../utils'; -import { getImageInfo } from '../utils/image'; +import { getImageInfo, getImageSrc } from '../utils/image'; import { compareParagraphsOrder, findContentDOM, @@ -565,6 +565,28 @@ class Selection { // Handle image click, to select the current image if (isHTMLElement(target) && target.tagName === 'IMG') { + // Cmd/Ctrl-click an image → ask the host to preview it. marktext's + // `clickEvent.js` dispatched `format-click` with `{ event, + // formatType: 'image', data: }`; the desktop renderer + // (`editor.vue` `format-click` handler) gates on the modifier and + // opens a `SimpleImageViewer` with that src string. We resolve the + // src from the token (the same source path the renderer used) + // through `getImageSrc` so relative/file paths become loadable + // URLs, falling back to the rendered 's own `src` attribute. + // The plain-click select/toolbar/transformer path below is left + // untouched. + if (event instanceof MouseEvent && (event.metaKey || event.ctrlKey)) { + const tokenSrc = imageInfo.token.src || imageInfo.token.attrs.src || ''; + const src = getImageSrc(tokenSrc).src || target.getAttribute('src') || ''; + if (src) { + eventCenter.emit('format-click', { + event, + formatType: 'image', + data: src, + }); + } + } + // Handle show image toolbar const rect = imageWrapper .querySelector(`.${CLASS_NAMES.MU_IMAGE_CONTAINER}`) From 166f0149cffd6202887a5778ae6efe031068d014 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 13:51:24 +0800 Subject: [PATCH 10/17] feat(muya): add updateParagraph block-type conversion API (#4391) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): add updateParagraph block-type conversion API Expose updateParagraph(type) on the Muya class so the desktop Paragraph menu can convert the block at the cursor. It accepts the marktext/muyajs label vocabulary (paragraph, heading 1-6, upgrade/degrade heading, blockquote, pre, mathblock, html, hr, table, front-matter, ul-bullet/ol-order/ul-task, loose-list-item, reset-to-paragraph, and the diagram types) and maps onto muya's replaceBlockByLabel, plus: - heading upgrade/degrade level cycling (marktext semantics: paragraph->h6 on upgrade, h6->paragraph on degrade); - list-type conversion that rebuilds the list preserving every item (rather than collapsing to a single-item list of the lead text); - loose/tight toggling of the current list. Reuses the existing _outmostBlockAtCursor cursor resolution and the block primitives the front menu/quick insert already use. Adds happy-dom coverage. Part of the muyajs -> @muyajs/core engine migration (stage A1). Co-Authored-By: Claude Opus 4.8 (1M context) * fix(muya): faithful updateParagraph reset/toggle/guards (#4391) Address Copilot review on #4391 — close legacy-fidelity gaps so the desktop Paragraph menu/command palette behave like muyajs and never drop content: - Accept the command-palette `ol-bullet` label (the menu uses `ol-order`); both now map to an ordered list. - `reset-to-paragraph` no longer collapses structured blocks to a single paragraph: lists and blockquotes unwrap into their child blocks (every item preserved); tables are left untouched; headings/hr/code fall back to a paragraph of their text. - Selecting the active list type toggles the list off by unwrapping each item back into paragraphs (matches the menu's checkbox semantics), instead of a no-op. - Guard hr/table conversions against non-empty content (legacy isAllowedTransformation), so converting a non-empty block no longer silently drops its text. Adds regression tests for ol-bullet, list unwrap on reset + same-type toggle, and the hr content guard. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../src/__tests__/updateParagraph.spec.ts | 183 ++++++++++++++ packages/muya/src/muya.ts | 231 +++++++++++++++++- 2 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 packages/muya/src/__tests__/updateParagraph.spec.ts diff --git a/packages/muya/src/__tests__/updateParagraph.spec.ts b/packages/muya/src/__tests__/updateParagraph.spec.ts new file mode 100644 index 0000000000..c1faa6cd4a --- /dev/null +++ b/packages/muya/src/__tests__/updateParagraph.spec.ts @@ -0,0 +1,183 @@ +// @vitest-environment happy-dom + +import type Content from '../block/base/content'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Muya } from '../muya'; + +// Coverage for muya.updateParagraph — the block-type conversions the desktop +// Paragraph menu drives (added for the muyajs -> @muyajs/core migration). It +// accepts the marktext/muyajs label vocabulary and maps onto muya's +// replaceBlockByLabel + list/heading handling. State flushes on rAF, so +// assertions wait via vi.waitFor. + +const bootedHosts: HTMLElement[] = []; +let originalVersion: string | undefined; +let hadVersion = false; + +beforeEach(() => { + hadVersion = 'MUYA_VERSION' in window; + originalVersion = window.MUYA_VERSION; + window.MUYA_VERSION = 'test'; +}); + +afterEach(() => { + while (bootedHosts.length) { + const host = bootedHosts.pop()!; + host.remove(); + } + if (hadVersion) + window.MUYA_VERSION = originalVersion as string; + else + delete (window as Partial).MUYA_VERSION; +}); + +function bootMuya(markdown: string): Muya { + const host = document.createElement('div'); + document.body.appendChild(host); + const muya = new Muya(host, { markdown } as ConstructorParameters[1]); + muya.init(); + bootedHosts.push(muya.domNode); + return muya; +} + +function placeCursorOnFirstBlock(muya: Muya): Content { + const first = muya.editor.scrollPage!.firstContentInDescendant()!; + muya.editor.activeContentBlock = first; + return first; +} + +// eslint-disable-next-line ts/no-explicit-any +function firstBlock(muya: Muya): any { + return muya.getState()[0]; +} + +describe('muya.updateParagraph()', () => { + it('turns a paragraph into a heading (text preserved)', async () => { + const muya = bootMuya('hello world\n'); + placeCursorOnFirstBlock(muya); + muya.updateParagraph('heading 1'); + await vi.waitFor(() => { + const b = firstBlock(muya); + expect(b.name).toBe('atx-heading'); + expect(b.meta.level).toBe(1); + }); + expect(muya.getMarkdown()).toContain('hello world'); + }); + + it('reset-to-paragraph turns a heading back into a paragraph', async () => { + const muya = bootMuya('## a heading\n'); + placeCursorOnFirstBlock(muya); + muya.updateParagraph('reset-to-paragraph'); + await vi.waitFor(() => { + expect(firstBlock(muya).name).toBe('paragraph'); + }); + }); + + it('upgrade heading cycles paragraph -> h6 and h2 -> h1', async () => { + const muya = bootMuya('plain\n'); + placeCursorOnFirstBlock(muya); + muya.updateParagraph('upgrade heading'); + await vi.waitFor(() => { + expect(firstBlock(muya).name).toBe('atx-heading'); + expect(firstBlock(muya).meta.level).toBe(6); + }); + + const muya2 = bootMuya('## two\n'); + placeCursorOnFirstBlock(muya2); + muya2.updateParagraph('upgrade heading'); + await vi.waitFor(() => { + expect(firstBlock(muya2).meta.level).toBe(1); + }); + }); + + it('degrade heading lowers h1 -> h2', async () => { + const muya = bootMuya('# one\n'); + placeCursorOnFirstBlock(muya); + muya.updateParagraph('degrade heading'); + await vi.waitFor(() => { + expect(firstBlock(muya).meta.level).toBe(2); + }); + }); + + it('turns a paragraph into a blockquote', async () => { + const muya = bootMuya('quote me\n'); + placeCursorOnFirstBlock(muya); + muya.updateParagraph('blockquote'); + await vi.waitFor(() => { + expect(firstBlock(muya).name).toBe('block-quote'); + }); + }); + + it('turns a paragraph into a bullet list', async () => { + const muya = bootMuya('item\n'); + placeCursorOnFirstBlock(muya); + muya.updateParagraph('ul-bullet'); + await vi.waitFor(() => { + expect(firstBlock(muya).name).toBe('bullet-list'); + }); + }); + + it('converts a bullet list to an ordered list, preserving items', async () => { + const muya = bootMuya('- one\n- two\n'); + placeCursorOnFirstBlock(muya); + expect(firstBlock(muya).name).toBe('bullet-list'); + muya.updateParagraph('ol-order'); + await vi.waitFor(() => { + const b = firstBlock(muya); + expect(b.name).toBe('order-list'); + expect(b.children.length).toBe(2); + }); + }); + + it('toggles loose/tight on the current list', async () => { + const muya = bootMuya('- a\n- b\n'); + placeCursorOnFirstBlock(muya); + const before = firstBlock(muya).meta.loose; + muya.updateParagraph('loose-list-item'); + await vi.waitFor(() => { + expect(firstBlock(muya).meta.loose).toBe(!before); + }); + }); + + it('maps the command-palette ol-bullet label to an ordered list', async () => { + const muya = bootMuya('item\n'); + placeCursorOnFirstBlock(muya); + muya.updateParagraph('ol-bullet'); + await vi.waitFor(() => { + expect(firstBlock(muya).name).toBe('order-list'); + }); + }); + + it('reset-to-paragraph unwraps a list into paragraphs, preserving items', async () => { + const muya = bootMuya('- one\n- two\n'); + placeCursorOnFirstBlock(muya); + muya.updateParagraph('reset-to-paragraph'); + await vi.waitFor(() => { + const state = muya.getState(); + expect(state.length).toBe(2); + expect(state.every(b => b.name === 'paragraph')).toBe(true); + }); + expect(muya.getMarkdown()).toContain('one'); + expect(muya.getMarkdown()).toContain('two'); + }); + + it('selecting the active list type toggles the list off, preserving items', async () => { + const muya = bootMuya('- a\n- b\n'); + placeCursorOnFirstBlock(muya); + muya.updateParagraph('ul-bullet'); + await vi.waitFor(() => { + const state = muya.getState(); + expect(state.length).toBe(2); + expect(state.every(b => b.name === 'paragraph')).toBe(true); + }); + }); + + it('does not convert a non-empty paragraph to hr (content guard)', async () => { + const muya = bootMuya('keep me\n'); + placeCursorOnFirstBlock(muya); + muya.updateParagraph('hr'); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + expect(firstBlock(muya).name).toBe('paragraph'); + expect(muya.getMarkdown()).toContain('keep me'); + }); +}); diff --git a/packages/muya/src/muya.ts b/packages/muya/src/muya.ts index 6d9d3eacfa..db31113876 100644 --- a/packages/muya/src/muya.ts +++ b/packages/muya/src/muya.ts @@ -3,7 +3,7 @@ import type Parent from './block/base/parent'; import type { Listener } from './event/types'; import type { ILocale } from './i18n/types'; import type { ITocItem } from './state/getTOC'; -import type { TState } from './state/types'; +import type { IBulletListState, IOrderListState, ITaskListState, TState } from './state/types'; import type { IMuyaOptions } from './types'; import Format from './block/base/format'; import { ScrollPage } from './block/scrollPage'; @@ -17,6 +17,8 @@ import { Editor } from './editor/index'; import EventCenter from './event/index'; import I18n from './i18n/index'; import { getTOC } from './state/getTOC'; +import { isAnyListState, isAtxHeadingState } from './state/types'; +import { replaceBlockByLabel } from './ui/paragraphQuickInsertMenu/config'; import { Ui } from './ui/ui'; import { deepClone } from './utils'; import './assets/styles/blockSyntax.css'; @@ -38,6 +40,36 @@ interface IPlugin { options: Record; } +// Maps the marktext/muyajs paragraph-menu labels the desktop sends through +// `updateParagraph` to the muya `replaceBlockByLabel` vocabulary. +const PARAGRAPH_LABEL_MAP: Record = { + 'paragraph': 'paragraph', + 'hr': 'thematic-break', + 'front-matter': 'frontmatter', + 'table': 'table', + 'mathblock': 'math-block', + 'html': 'html-block', + 'pre': 'code-block', + 'blockquote': 'block-quote', + 'heading 1': 'atx-heading 1', + 'heading 2': 'atx-heading 2', + 'heading 3': 'atx-heading 3', + 'heading 4': 'atx-heading 4', + 'heading 5': 'atx-heading 5', + 'heading 6': 'atx-heading 6', + 'ul-bullet': 'bullet-list', + 'ol-order': 'order-list', + // The desktop command palette emits `ol-bullet` for the ordered-list + // command while the menu emits `ol-order`; accept both. + 'ol-bullet': 'order-list', + 'ul-task': 'task-list', + 'mermaid': 'diagram mermaid', + 'plantuml': 'diagram plantuml', + 'vega-lite': 'diagram vega-lite', + 'flowchart': 'diagram flowchart', + 'sequence': 'diagram sequence', +}; + export class Muya { static plugins: IPlugin[] = []; @@ -374,6 +406,203 @@ export class Muya { cursorBlock?.setCursor(0, 0, true); } + /** + * Convert the block at the cursor to another type, mirroring marktext's + * `updateParagraph`. `type` uses the marktext/muyajs paragraph-menu + * vocabulary: `paragraph`, `heading 1`–`heading 6`, `upgrade heading`, + * `degrade heading`, `blockquote`, `pre`, `mathblock`, `html`, `hr`, + * `table`, `front-matter`, `ul-bullet`/`ol-order`/`ul-task`, + * `loose-list-item`, `reset-to-paragraph`, and the diagram types. + */ + updateParagraph(type: string) { + const block = this._outmostBlockAtCursor(); + if (!block) + return; + + if (type === 'upgrade heading' || type === 'degrade heading') { + this._changeHeadingLevel(block, type); + return; + } + + if (type === 'loose-list-item') { + this._toggleLooseList(block); + return; + } + + // `reset-to-paragraph` returns the current block to plain paragraph + // form; structured containers (lists/blockquote) unwrap to preserve + // every child, tables are left untouched (matches legacy). + if (type === 'reset-to-paragraph') { + this._resetToParagraph(block); + return; + } + + const label = PARAGRAPH_LABEL_MAP[type]; + if (!label) + return; + + if (label.endsWith('-list') && isAnyListState(block.getState())) { + // Selecting the active list type toggles the list off (unwrap each + // item back into paragraphs); a different type converts in place, + // preserving every item. + if (block.blockName === label) + this._unwrapToParagraphs(block); + else + this._convertListType(block, label); + + return; + } + + // Legacy `isAllowedTransformation`: hr/table only replace an empty + // block so user content is never silently dropped. + if ( + (label === 'thematic-break' || label === 'table') + && this._blockLeadingText(block).trim() !== '' + ) { + return; + } + + replaceBlockByLabel({ + block, + muya: this, + label, + text: this._blockLeadingText(block), + }); + } + + /** Return the block at the cursor to plain paragraph form. */ + private _resetToParagraph(block: Parent) { + if (block.blockName === 'table') + return; + + if (isAnyListState(block.getState()) || block.blockName === 'block-quote') { + this._unwrapToParagraphs(block); + return; + } + + replaceBlockByLabel({ + block, + muya: this, + label: 'paragraph', + text: this._blockLeadingText(block), + }); + } + + /** + * Unwrap a structured container (list or blockquote) into the top-level + * blocks it contains, preserving every item. + */ + private _unwrapToParagraphs(block: Parent) { + const state = block.getState(); + let inner: TState[] = []; + if (isAnyListState(state)) + inner = state.children.flatMap(li => deepClone(li.children)); + else if (state.name === 'block-quote') + inner = deepClone(state.children); + + if (!inner.length) + return; + + const parent = block.parent!; + let ref: Parent = block; + let firstNew: Parent | null = null; + for (const childState of inner) { + const newBlock = ScrollPage.loadBlock(childState.name).create(this, childState); + parent.insertAfter(newBlock, ref); + ref = newBlock; + firstNew ??= newBlock; + } + + block.remove(); + firstNew?.firstContentInDescendant()?.setCursor(0, 0, true); + } + + /** Leading text of a block, with the atx hash run stripped for headings. */ + private _blockLeadingText(block: Parent): string { + const text = block.firstContentInDescendant()?.text ?? ''; + + return block.blockName === 'atx-heading' + ? text.replace(/^ {0,3}#{1,6}(?:\s+|$)/, '') + : text; + } + + /** Cycle the heading level (marktext upgrade/degrade semantics). */ + private _changeHeadingLevel(block: Parent, type: 'upgrade heading' | 'degrade heading') { + const state = block.getState(); + const level = isAtxHeadingState(state) ? state.meta.level : 0; + let newLevel = level; + + if (type === 'upgrade heading' && level !== 1) + newLevel = level === 0 ? 6 : level - 1; + else if (type === 'degrade heading' && level !== 0) + newLevel = level === 6 ? 0 : level + 1; + + if (newLevel === level) + return; + + replaceBlockByLabel({ + block, + muya: this, + label: newLevel === 0 ? 'paragraph' : `atx-heading ${newLevel}`, + text: this._blockLeadingText(block), + }); + } + + /** Toggle loose/tight on the list at the cursor. */ + private _toggleLooseList(block: Parent) { + const state = block.getState(); + if (!isAnyListState(state)) + return; + + const newState = deepClone(state); + newState.meta.loose = !newState.meta.loose; + const newBlock = ScrollPage.loadBlock(newState.name).create(this, newState); + block.replaceWith(newBlock); + newBlock.firstContentInDescendant()?.setCursor(0, 0, true); + } + + /** Convert an existing list to another list type, preserving items. */ + private _convertListType(block: Parent, label: string) { + const state = block.getState(); + if (!isAnyListState(state) || block.blockName === label) + return; + + const { bulletListMarker, orderListDelimiter } = this.options; + const loose = !!state.meta.loose; + const childContents: TState[][] = state.children.map(li => deepClone(li.children)); + + let newState: IBulletListState | IOrderListState | ITaskListState; + if (label === 'task-list') { + newState = { + name: 'task-list', + meta: { marker: bulletListMarker, loose }, + children: childContents.map(children => ({ + name: 'task-list-item', + meta: { checked: false }, + children, + })), + }; + } + else if (label === 'order-list') { + newState = { + name: 'order-list', + meta: { delimiter: orderListDelimiter, loose, start: 1 }, + children: childContents.map(children => ({ name: 'list-item', children })), + }; + } + else { + newState = { + name: 'bullet-list', + meta: { marker: bulletListMarker, loose }, + children: childContents.map(children => ({ name: 'list-item', children })), + }; + } + + const newBlock = ScrollPage.loadBlock(label).create(this, newState); + block.replaceWith(newBlock); + newBlock.firstContentInDescendant()?.setCursor(0, 0, true); + } + destroy() { this.eventCenter.detachAllDomEvents(); this.eventCenter.unsubscribeAll(); From e67fdbe7657745a467f71a84ecb872158ff9ef81 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 15:00:38 +0800 Subject: [PATCH 11/17] feat(muya): add runtime setOptions / setFont / setTabSize / setListIndentation (#4393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): add runtime setOptions / setFont / setTabSize / setListIndentation Every desktop Preferences toggle needs editor options to update live; the legacy engine exposed setOptions(options, needRerender) and the setFont/ setTabSize/setListIndentation helpers, which @muyajs/core lacked. setOptions(options, forceRender) merges into muya.options, reflects the container-level ones (spellcheck attribute, quick-insert hint class), and — when forceRender is set — fully re-renders the document from its current state via ScrollPage.updateState so render-affecting options (superSubScript, footnote, disableHtml, frontmatterType, codeBlockLineNumbers, GitLab compatibility, …) take effect. Unlike setContent, this preserves the undo history; the cursor is captured and restored by path across the re-render. setFont/setTabSize update their options; setListIndentation routes through setOptions with a re-render. Adds happy-dom coverage. Part of the muyajs -> @muyajs/core engine migration (stage A4). Co-Authored-By: Claude Opus 4.8 (1M context) * fix(muya): correct setOptions cursor restore + history test (#4393) Address Copilot review: - Cursor restore after forceRender was broken: passing only paths to Selection.setSelection doesn't work (_setCursor needs a concrete block's domNode; a bare queryBlock result is not a Node). Resolve the block at the saved path on the rebuilt tree and call block.setCursor directly, mirroring Editor.updateContents' same-block restore. - The history test was trivial (undo() on an empty stack never throws). Assert history.canUndo() stays true across a forceRender re-render instead — and documents that updateState uses the 'api' source so it neither clears nor pollutes history. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../muya/src/__tests__/setOptions.spec.ts | 102 ++++++++++++++++++ packages/muya/src/muya.ts | 59 ++++++++++ 2 files changed, 161 insertions(+) create mode 100644 packages/muya/src/__tests__/setOptions.spec.ts diff --git a/packages/muya/src/__tests__/setOptions.spec.ts b/packages/muya/src/__tests__/setOptions.spec.ts new file mode 100644 index 0000000000..eac11b5929 --- /dev/null +++ b/packages/muya/src/__tests__/setOptions.spec.ts @@ -0,0 +1,102 @@ +// @vitest-environment happy-dom + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Muya } from '../muya'; + +// Coverage for the runtime option API added for the muyajs -> @muyajs/core +// migration: setOptions / setFont / setTabSize / setListIndentation. Every +// desktop Preferences toggle depends on options updating live. setOptions with +// forceRender re-renders from current state (so render-affecting options take +// effect) WITHOUT clearing undo history, and preserves the document content. + +const bootedHosts: HTMLElement[] = []; +let originalVersion: string | undefined; +let hadVersion = false; + +beforeEach(() => { + hadVersion = 'MUYA_VERSION' in window; + originalVersion = window.MUYA_VERSION; + window.MUYA_VERSION = 'test'; +}); + +afterEach(() => { + while (bootedHosts.length) { + const host = bootedHosts.pop()!; + host.remove(); + } + if (hadVersion) + window.MUYA_VERSION = originalVersion as string; + else + delete (window as Partial).MUYA_VERSION; +}); + +function bootMuya(markdown: string): Muya { + const host = document.createElement('div'); + document.body.appendChild(host); + const muya = new Muya(host, { markdown } as ConstructorParameters[1]); + muya.init(); + bootedHosts.push(muya.domNode); + return muya; +} + +describe('muya runtime options', () => { + it('setOptions merges into muya.options', () => { + const muya = bootMuya('hello\n'); + muya.setOptions({ footnote: true, superSubScript: false }); + expect(muya.options.footnote).toBe(true); + expect(muya.options.superSubScript).toBe(false); + }); + + it('setOptions with forceRender preserves the document content', () => { + const muya = bootMuya('# Heading\n\nsome text\n'); + const before = muya.getMarkdown(); + muya.setOptions({ footnote: true }, true); + // A forced re-render rebuilds the block tree from current state, so the + // serialized document is unchanged. + expect(muya.getMarkdown()).toBe(before); + }); + + it('setOptions with forceRender does not clear the undo history', async () => { + const muya = bootMuya('one\n'); + muya.editor.activeContentBlock = muya.editor.scrollPage!.firstContentInDescendant()!; + // Make an edit so the undo stack is non-empty. + muya.insertParagraph(); + await vi.waitFor(() => { + expect(muya.getState().length).toBe(2); + // the edit was recorded onto the undo stack + expect(muya.editor.history.canUndo()).toBe(true); + }); + + // A forced re-render rebuilds the tree via ScrollPage.updateState, which + // uses the 'api' source (no json-change dispatch), so it neither clears + // the history (unlike setContent) nor pollutes it with re-render ops. + muya.setOptions({ footnote: true }, true); + + expect(muya.editor.history.canUndo()).toBe(true); + }); + + it('setOptions reflects spellcheckEnabled on the container', () => { + const muya = bootMuya('x\n'); + muya.setOptions({ spellcheckEnabled: true }); + expect(muya.domNode.getAttribute('spellcheck')).toBe('true'); + muya.setOptions({ spellcheckEnabled: false }); + expect(muya.domNode.getAttribute('spellcheck')).toBe('false'); + }); + + it('setFont and setTabSize update options', () => { + const muya = bootMuya('x\n'); + muya.setFont({ fontSize: 18, lineHeight: 1.8 }); + expect(muya.options.fontSize).toBe(18); + expect(muya.options.lineHeight).toBe(1.8); + muya.setTabSize(2); + expect(muya.options.tabSize).toBe(2); + }); + + it('setListIndentation updates options and preserves content', () => { + const muya = bootMuya('- a\n- b\n'); + const before = muya.getMarkdown(); + muya.setListIndentation(2); + expect(muya.options.listIndentation).toBe(2); + expect(muya.getMarkdown()).toBe(before); + }); +}); diff --git a/packages/muya/src/muya.ts b/packages/muya/src/muya.ts index db31113876..bdc30711e9 100644 --- a/packages/muya/src/muya.ts +++ b/packages/muya/src/muya.ts @@ -204,6 +204,65 @@ export class Muya { this.editor.setContent(content, autoFocus); } + /** + * Update editor options at runtime (mirrors marktext muyajs `setOptions`): + * merges `options` into `muya.options`, reflects the container-level ones + * (spellcheck, quick-insert hint), and — when `forceRender` is set — fully + * re-renders the document from its current state so render-affecting + * options (superSubScript, footnote, disableHtml, frontmatterType, + * codeBlockLineNumbers, GitLab compatibility, …) take effect. Unlike + * `setContent`, the undo history is preserved; the cursor is restored by path. + */ + setOptions(options: Partial, forceRender = false) { + Object.assign(this.options, options); + + if ('spellcheckEnabled' in options) + this.domNode.setAttribute('spellcheck', options.spellcheckEnabled ? 'true' : 'false'); + + if ('hideQuickInsertHint' in options) { + this.domNode.classList.toggle( + CLASS_NAMES.MU_SHOW_QUICK_INSERT_HINT, + !options.hideQuickInsertHint, + ); + } + + if (!forceRender) + return; + + const selection = this.editor.selection.getSelection(); + this.editor.scrollPage?.updateState(this.getState()); + // Restore the caret on the rebuilt tree by resolving the block at the + // saved path and setting the cursor on it directly. (Passing only a + // path to setSelection does not work — Selection._setCursor needs a + // concrete block's domNode; a bare queryBlock result is not a Node.) + // Mirrors Editor.updateContents' same-block cursor restore. + if (selection && selection.isSelectionInSameBlock) { + const begin = Math.min(selection.anchor.offset, selection.focus.offset); + const end = Math.max(selection.anchor.offset, selection.focus.offset); + const cursorBlock = this.editor.scrollPage?.queryBlock(selection.anchorPath); + if (cursorBlock && cursorBlock.isContent()) + cursorBlock.setCursor(begin, end, true); + } + } + + /** Update the editor font size / line height (mirrors muyajs `setFont`). */ + setFont({ fontSize, lineHeight }: { fontSize?: IMuyaOptions['fontSize']; lineHeight?: IMuyaOptions['lineHeight'] }) { + if (typeof fontSize === 'number') + this.options.fontSize = fontSize; + if (typeof lineHeight === 'number') + this.options.lineHeight = lineHeight; + } + + /** Update the tab size used for indentation. */ + setTabSize(tabSize: IMuyaOptions['tabSize']) { + this.options.tabSize = tabSize; + } + + /** Update list indentation and re-render so it takes effect. */ + setListIndentation(listIndentation: IMuyaOptions['listIndentation']) { + this.setOptions({ listIndentation }, true); + } + focus() { this.editor.focus(); } From c19ef7fc1a2ca3d3340fa52e336d648f140ba942 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 15:10:58 +0800 Subject: [PATCH 12/17] feat(muya): add spellcheck word-replacement API (#4392) * feat(muya): add spellcheck word-replacement API Add `replaceCurrentWordInlineUnsafe(word, replacement)` to @muyajs/core, restoring the legacy muyajs `_replaceCurrentWordInlineUnsafe` API the desktop spell checker relies on. When the user right-clicks a misspelled word, Chromium selects the whole word; choosing a suggestion from the context menu replaces it inline. The new method finds the word at the cursor (using the VSCode-derived word boundaries ported from legacy muyajs), asserts it matches the expected `word`, replaces it through the Content text setter so the change dispatches a json edit op, and places the cursor after the replacement. It is a no-op when there is no active content block / cursor, or when the word at the cursor does not match (guards a Chromium selection mismatch). Co-Authored-By: Claude Opus 4.8 (1M context) * docs(muya): fix legacy source path in spellcheck word-boundary comments The `extractWord` helper and the `WORD_SEPARATORS`/`WORD_DEFINITION` regexes were ported from legacy muyajs `lib/marktext/spellchecker.js` (which carries the VSCode wordHelper attribution), not from `lib/contentState/core.js`. `core.js` only holds the `replaceWordInLine` range-replacement helper, so the previous comments pointed at the wrong file. Correct both references to ease future maintenance. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../src/__tests__/replaceCurrentWord.spec.ts | 125 ++++++++++++++++++ packages/muya/src/block/base/content.ts | 118 +++++++++++++++++ packages/muya/src/muya.ts | 21 +++ 3 files changed, 264 insertions(+) create mode 100644 packages/muya/src/__tests__/replaceCurrentWord.spec.ts diff --git a/packages/muya/src/__tests__/replaceCurrentWord.spec.ts b/packages/muya/src/__tests__/replaceCurrentWord.spec.ts new file mode 100644 index 0000000000..6f788d0aaa --- /dev/null +++ b/packages/muya/src/__tests__/replaceCurrentWord.spec.ts @@ -0,0 +1,125 @@ +// @vitest-environment happy-dom + +import type Content from '../block/base/content'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Muya } from '../muya'; + +// Coverage for muya.replaceCurrentWordInlineUnsafe — the spellcheck word +// replacement API added for the muyajs -> @muyajs/core desktop migration. +// Legacy muyajs exposed `_replaceCurrentWordInlineUnsafe(word, replacement)`; +// the desktop spell checker calls it when the user picks a suggestion from the +// misspelled-word context menu (Chromium has already selected the whole word). +// +// The method finds the word at the cursor, asserts it matches `word`, replaces +// it inline through the text setter (which dispatches a json edit op), and +// places the cursor after the replacement. State flushes on rAF, so markdown +// assertions wait via vi.waitFor. + +const bootedHosts: HTMLElement[] = []; +let originalVersion: string | undefined; +let hadVersion = false; + +beforeEach(() => { + hadVersion = 'MUYA_VERSION' in window; + originalVersion = window.MUYA_VERSION; + window.MUYA_VERSION = 'test'; +}); + +afterEach(() => { + while (bootedHosts.length) { + const host = bootedHosts.pop()!; + host.remove(); + } + if (hadVersion) + window.MUYA_VERSION = originalVersion as string; + else + delete (window as Partial).MUYA_VERSION; +}); + +function bootMuya(markdown: string): Muya { + const host = document.createElement('div'); + document.body.appendChild(host); + const muya = new Muya(host, { markdown } as ConstructorParameters[1]); + muya.init(); + bootedHosts.push(muya.domNode); + return muya; +} + +// Place the cursor inside the first content block at `offset` and mark it as +// the active block — mirrors the editor state after a click lands the caret. +function placeCursorAt(muya: Muya, offset: number): Content { + const first = muya.editor.scrollPage!.firstContentInDescendant()!; + muya.editor.activeContentBlock = first; + first.setCursor(offset, offset, true); + return first; +} + +describe('muya.replaceCurrentWordInlineUnsafe()', () => { + it('replaces the misspelled word at the cursor and updates markdown', async () => { + const muya = bootMuya('teh quick brown fox\n'); + // Cursor sits inside `teh`. + placeCursorAt(muya, 1); + + const ok = muya.replaceCurrentWordInlineUnsafe('teh', 'the'); + expect(ok).toBe(true); + + await vi.waitFor(() => { + expect(muya.getMarkdown()).toContain('the quick brown fox'); + }); + expect(muya.getMarkdown()).not.toContain('teh'); + }); + + it('replaces a word in the middle of the line', async () => { + const muya = bootMuya('the quikc brown fox\n'); + // Cursor inside `quikc` (offset of the `i`). + placeCursorAt(muya, 'the qu'.length); + + const ok = muya.replaceCurrentWordInlineUnsafe('quikc', 'quick'); + expect(ok).toBe(true); + + await vi.waitFor(() => { + expect(muya.getMarkdown()).toContain('the quick brown fox'); + }); + }); + + it('places the cursor after the replacement', () => { + const muya = bootMuya('teh end\n'); + const block = placeCursorAt(muya, 0); + + muya.replaceCurrentWordInlineUnsafe('teh', 'the'); + + const cursor = block.getCursor(); + expect(cursor).not.toBeNull(); + // `the` is 3 chars, so the caret should sit at offset 3. + expect(cursor!.start.offset).toBe(3); + expect(cursor!.end.offset).toBe(3); + }); + + it('is a no-op when the word at the cursor does not match (Chromium mismatch)', async () => { + const muya = bootMuya('teh quick\n'); + placeCursorAt(muya, 1); + + const ok = muya.replaceCurrentWordInlineUnsafe('different', 'the'); + expect(ok).toBe(false); + + await new Promise(resolve => requestAnimationFrame(() => resolve())); + expect(muya.getMarkdown()).toContain('teh quick'); + }); + + it('returns false when there is no active content block', () => { + const muya = bootMuya('teh quick\n'); + muya.editor.activeContentBlock = null; + + expect(muya.replaceCurrentWordInlineUnsafe('teh', 'the')).toBe(false); + }); + + it('returns false when there is no cursor in the active block', () => { + const muya = bootMuya('teh quick\n'); + const first = muya.editor.scrollPage!.firstContentInDescendant()!; + muya.editor.activeContentBlock = first; + // No selection set — getCursor() returns null. + document.getSelection()?.removeAllRanges(); + + expect(muya.replaceCurrentWordInlineUnsafe('teh', 'the')).toBe(false); + }); +}); diff --git a/packages/muya/src/block/base/content.ts b/packages/muya/src/block/base/content.ts index 311327ed5a..8368902005 100644 --- a/packages/muya/src/block/base/content.ts +++ b/packages/muya/src/block/base/content.ts @@ -20,6 +20,82 @@ import { // const debug = logger('block.content:') +// Word boundary regexes ported from legacy muyajs +// (lib/marktext/spellchecker.js), which in turn derive from VSCode's wordHelper. +// Used by `extractWord` to find the word at the cursor for spell-check +// replacement. +const WORD_SEPARATORS = /[`~!@#$%^&*()\-=+[{\]}\\|;:'",.<>/?\s]/g; +const WORD_DEFINITION = /-?\d*\.\d\w*|[^`~!@#$%^&*()\-=+[{\]}\\|;:'",.<>/?\s]+/g; + +/** + * Extract the word at the given offset from the text. + * + * Ported from legacy muyajs `extractWord` (lib/marktext/spellchecker.js). + * + * @param text The line text. + * @param offset Normalized cursor offset (e.g. `ab|c def` -> 2). + * @returns The matched word with its `left`/`right` offsets, or null when the + * cursor is not inside a word. + */ +function extractWord( + text: string, + offset: number, +): { left: number; right: number; word: string } | null { + if (!text || text.length === 0) { + return null; + } + else if (offset < 0) { + offset = 0; + } + else if (offset >= text.length) { + offset = text.length - 1; + } + + // Matches all words starting at a good position. + WORD_DEFINITION.lastIndex = text.lastIndexOf(' ', offset - 1) + 1; + let match: RegExpExecArray | null = null; + let left = -1; + // eslint-disable-next-line no-cond-assign + while ((match = WORD_DEFINITION.exec(text))) { + if (match && match.index <= offset) { + if (WORD_DEFINITION.lastIndex > offset) + left = match.index; + } + else { + break; + } + } + WORD_DEFINITION.lastIndex = 0; + + // Cursor is between two word separators (e.g. `*|*` or ` |*`). + if (left <= -1) + return null; + + // Find word ending. + WORD_SEPARATORS.lastIndex = offset; + match = WORD_SEPARATORS.exec(text); + let right = -1; + if (match) + right = match.index; + + WORD_SEPARATORS.lastIndex = 0; + + // The last word in the string is a special case. + if (right < 0) { + return { + left, + right: text.length, + word: text.slice(left), + }; + } + + return { + left, + right, + word: text.slice(left, right), + }; +} + class Content extends TreeNode { public _text: string; public isComposed: boolean; @@ -432,6 +508,48 @@ class Content extends TreeNode { } } + /** + * Replace the word at/around the current cursor with `replacement`. + * + * Ported from legacy muyajs `ContentState._replaceCurrentWordInlineUnsafe` + * (lib/contentState/marktext.js). Used by the desktop spell checker: right + * clicking a misspelled word selects the whole word via Chromium, and + * choosing a suggestion replaces it inline. `extractWord` mirrors the + * VSCode-derived word boundaries muyajs relied on. + * + * Unsafe: the caller asserts that exactly the word `word` is selected. If + * the word found at the cursor does not match `word` the call is a no-op + * (returns false) — this guards against a Chromium selection mismatch. + * + * @param word The expected word at the cursor; the whole word must be selected. + * @param replacement The replacement text. + * @returns True when the replacement was applied. + */ + replaceCurrentWordInlineUnsafe(word: string, replacement: string): boolean { + const cursor = this.getCursor(); + if (cursor == null) + return false; + + const { text } = this; + // Use the start offset of the (possibly whole-word) selection as the + // probe point, matching the legacy `start.offset` behaviour. + const wordInfo = extractWord(text, cursor.start.offset); + if (wordInfo == null) + return false; + + const { left, right, word: selectedWord } = wordInfo; + if (selectedWord !== word) + return false; + + // Reuse the text setter so the change dispatches a json edit op. + this.text = text.substring(0, left) + replacement + text.substring(right); + + const offset = left + replacement.length; + this.setCursor(offset, offset, true); + + return true; + } + keydownHandler = (event: Event) => { if (!isKeyboardEvent(event)) return; diff --git a/packages/muya/src/muya.ts b/packages/muya/src/muya.ts index bdc30711e9..5d332dd92e 100644 --- a/packages/muya/src/muya.ts +++ b/packages/muya/src/muya.ts @@ -326,6 +326,27 @@ export class Muya { anchorBlock.format(type); } + /** + * Replace the word at the current cursor with `replacement`, then place the + * cursor after the replacement. + * + * Mirrors legacy muyajs `_replaceCurrentWordInlineUnsafe`. The desktop spell + * checker calls this when the user picks a suggestion from the misspelled-word + * context menu (Chromium has already selected the whole word). Unsafe: the + * call is a no-op unless the word at the cursor matches `word`. + * + * @param word The expected (misspelled) word at the cursor. + * @param replacement The replacement word. + * @returns True when the replacement was applied. + */ + replaceCurrentWordInlineUnsafe(word: string, replacement: string): boolean { + const block = this.editor.activeContentBlock; + if (!block) + return false; + + return block.replaceCurrentWordInlineUnsafe(word, replacement); + } + /** * Return the current selection, or null when the editor has no selection. */ From 86830449f22c8a893e1b1dff984e3345d75c1403 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 15:12:12 +0800 Subject: [PATCH 13/17] feat(muya): serialize/restore undo history (#4394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): serialize/restore undo history Add a JSON-serializable view of the History undo/redo stacks so the desktop shell can persist each tab's editing history across tab switches. - History.getHistory() returns a deep, JSON-serializable snapshot of the undo/redo stacks plus the lastRecorded / selectionStack bookkeeping. ot-json1 ops are plain JSON arrays (deep-cloned as-is). The stored selection carries live anchorBlock/focusBlock references, so the snapshot strips them and keeps only the serializable anchorPath / focusPath + offsets. - History.setHistory() restores the snapshot. The restored, path-only selections re-resolve their target block from the path on apply: editor.updateContents and selection._setCursor both fall back to scrollPage.queryBlock(path) when no block instance is present, so the caret restores losslessly. - History.clear() now also resets selectionStack / lastRecorded. - Muya exposes getHistory() / setHistory() / clearHistory() delegating to editor.history, placed immediately after redo(). Adds a happy-dom vitest spec covering the JSON round-trip, the setHistory(getHistory()) + undo() lossless reproduction of prior states, the redo() round-trip, and clearHistory(). Co-Authored-By: Claude Opus 4.8 (1M context) * refactor(muya): type history selections honestly without unsound cast `History._fromSerializableSelection` rebuilt a path-only selection (no live `anchorBlock` / `focusBlock`) and cast it `as unknown as ISelection`, where those block fields are required — an unsound double-cast. The restored selection is consumed by `Editor.updateContents`, which re-resolves the target block from `anchorPath` / `focusPath` via `scrollPage.queryBlock`, so the block instances are genuinely optional at restore time. Introduce a dedicated `IHistorySelection` type (`ISelection` with optional `anchorBlock` / `focusBlock`) in `selection/types.ts` and use it for the history stacks (`IOperation.selection`, `_selectionStack`), the serialize / restore helpers, and `Editor.updateContents`. A full `ISelection` is assignable to it, so live selections from `getSelection()` still fit without a cast, and the restored path-only object now type-checks directly — no `as unknown as X` needed. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../__tests__/historySerialization.spec.ts | 192 ++++++++++++++++++ packages/muya/src/editor/index.ts | 4 +- packages/muya/src/history/index.ts | 143 ++++++++++++- packages/muya/src/muya.ts | 29 +++ packages/muya/src/selection/types.ts | 11 + 5 files changed, 374 insertions(+), 5 deletions(-) create mode 100644 packages/muya/src/__tests__/historySerialization.spec.ts diff --git a/packages/muya/src/__tests__/historySerialization.spec.ts b/packages/muya/src/__tests__/historySerialization.spec.ts new file mode 100644 index 0000000000..255c5f9041 --- /dev/null +++ b/packages/muya/src/__tests__/historySerialization.spec.ts @@ -0,0 +1,192 @@ +// @vitest-environment happy-dom + +import type Content from '../block/base/content'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Muya } from '../muya'; + +// Coverage for the undo-history serialization API added for the +// muyajs -> @muyajs/core desktop migration: getHistory / setHistory / +// clearHistory. The desktop shell persists each tab's undo/redo history +// across tab switches — it reads getHistory() before deactivating a tab and +// restores it via setHistory() when the tab is re-selected. +// +// Block-tree mutations dispatch json1 ops that flow through the History +// recorder on the next animation frame (see JSONState._emitStateChange and +// History._record), so assertions on getState()/getMarkdown() and on the +// recorded stacks are wrapped in vi.waitFor to await that flush. +// +// History._record coalesces ops recorded within `options.delay` (1s) of the +// previous one into a single undo entry, so tests call `cutoff()` between +// edits to force one undo entry per edit and keep stack-depth assertions +// deterministic. The live-DOM selection that History reads while applying an +// undo can point at a block removed by the re-render, so we re-seat the +// cursor on a known-attached block right before each undo()/redo(). + +const bootedHosts: HTMLElement[] = []; +let originalVersion: string | undefined; +let hadVersion = false; + +beforeEach(() => { + hadVersion = 'MUYA_VERSION' in window; + originalVersion = window.MUYA_VERSION; + window.MUYA_VERSION = 'test'; +}); + +afterEach(() => { + while (bootedHosts.length) { + const host = bootedHosts.pop()!; + host.remove(); + } + if (hadVersion) + window.MUYA_VERSION = originalVersion as string; + else + delete (window as Partial).MUYA_VERSION; +}); + +function bootMuya(markdown: string): Muya { + const host = document.createElement('div'); + document.body.appendChild(host); + const muya = new Muya(host, { markdown } as ConstructorParameters[1]); + muya.init(); + bootedHosts.push(muya.domNode); + return muya; +} + +// Block-level ops resolve their target via the active content block's +// outMostBlock — the way the editor tracks the cursor after a click. Set it +// directly to simulate the cursor sitting in the first block, and seat the +// live DOM caret there so History reads a valid selection. +function placeCursorOnFirstBlock(muya: Muya): Content { + const first = muya.editor.scrollPage!.firstContentInDescendant()!; + muya.editor.activeContentBlock = first; + first.setCursor(0, 0, true); + return first; +} + +// Undo entries coalesce within options.delay; cutoff() forces the next edit +// into its own undo entry so depth assertions are deterministic. +function undoDepth(muya: Muya): number { + // @ts-expect-error — reach into the private stack for test assertions. + return muya.editor.history._stack.undo.length; +} + +describe('muya history serialization api', () => { + it('getHistory() returns a JSON-serializable snapshot of the undo/redo stacks', async () => { + const muya = bootMuya('# Title\n'); + placeCursorOnFirstBlock(muya); + muya.insertParagraph('after', 'one'); + await vi.waitFor(() => { + expect(undoDepth(muya)).toBe(1); + }); + + const snapshot = muya.getHistory(); + // It must survive a JSON round-trip with no loss (no live block refs, + // functions, or DOM nodes leaking into the serialized form). + expect(() => JSON.stringify(snapshot)).not.toThrow(); + expect(JSON.parse(JSON.stringify(snapshot))).toEqual(snapshot); + + expect(snapshot.stack.undo).toHaveLength(1); + expect(snapshot.stack.redo).toHaveLength(0); + // The recorded selection is path-only — no live anchorBlock / focusBlock. + const recorded = snapshot.stack.undo[0]; + expect(Array.isArray(recorded.operation)).toBe(true); + if (recorded.selection) { + expect(recorded.selection).not.toHaveProperty('anchorBlock'); + expect(recorded.selection).not.toHaveProperty('focusBlock'); + expect(Array.isArray(recorded.selection.anchorPath)).toBe(true); + } + }); + + it('setHistory(getHistory()) then undo() reproduces the snapshot-point state', async () => { + const muya = bootMuya('# Title\n'); + + // Make two edits, capturing the markdown + a history snapshot after the + // SECOND one — this is the state the desktop persists when a tab is + // deactivated (document + undo stack both at the same point). + placeCursorOnFirstBlock(muya); + muya.insertParagraph('after', 'one'); + await vi.waitFor(() => { + expect(muya.getMarkdown()).toContain('one'); + expect(undoDepth(muya)).toBe(1); + }); + muya.editor.history.cutoff(); + placeCursorOnFirstBlock(muya); + muya.insertParagraph('after', 'two'); + await vi.waitFor(() => { + expect(muya.getMarkdown()).toContain('two'); + expect(undoDepth(muya)).toBe(2); + }); + + const snapshot = muya.getHistory(); + expect(snapshot.stack.undo).toHaveLength(2); + + // Simulate the tab round-trip: the in-memory history is dropped (as it + // is when another tab takes over the editor) while the document stays + // put, then the persisted snapshot is restored. + muya.clearHistory(); + expect(undoDepth(muya)).toBe(0); + muya.setHistory(snapshot); + expect(undoDepth(muya)).toBe(2); + + // Undoing twice against the restored stack must walk the document back + // through "two" then "one" to the original "# Title" — proving the + // restored ops reproduce the prior document states losslessly. + placeCursorOnFirstBlock(muya); + muya.undo(); + await vi.waitFor(() => { + const md = muya.getMarkdown(); + expect(md).toContain('one'); + expect(md).not.toContain('two'); + }); + placeCursorOnFirstBlock(muya); + muya.undo(); + await vi.waitFor(() => { + expect(muya.getMarkdown().trim()).toBe('# Title'); + }); + expect(muya.editor.history.canUndo()).toBe(false); + expect(muya.editor.history.canRedo()).toBe(true); + }); + + it('restored history round-trips through redo() back to the snapshot state', async () => { + const muya = bootMuya('# Title\n'); + placeCursorOnFirstBlock(muya); + muya.insertParagraph('after', 'alpha'); + await vi.waitFor(() => { + expect(muya.getMarkdown()).toContain('alpha'); + expect(undoDepth(muya)).toBe(1); + }); + const markdownAtSnapshot = muya.getMarkdown(); + // Persist exactly as the desktop shell would — through JSON. + const snapshot = JSON.parse(JSON.stringify(muya.getHistory())); + + muya.setHistory(snapshot); + placeCursorOnFirstBlock(muya); + muya.undo(); + await vi.waitFor(() => { + expect(muya.getMarkdown().trim()).toBe('# Title'); + }); + + // redo must restore the exact pre-undo document state, proving the + // serialized op is lossless in both directions. + placeCursorOnFirstBlock(muya); + muya.redo(); + await vi.waitFor(() => { + expect(muya.getMarkdown()).toBe(markdownAtSnapshot); + }); + }); + + it('clearHistory() empties both stacks', async () => { + const muya = bootMuya('# Title\n'); + placeCursorOnFirstBlock(muya); + muya.insertParagraph('after', 'one'); + await vi.waitFor(() => { + expect(undoDepth(muya)).toBe(1); + }); + + muya.clearHistory(); + expect(muya.editor.history.canUndo()).toBe(false); + expect(muya.editor.history.canRedo()).toBe(false); + expect(muya.getHistory().stack.undo).toHaveLength(0); + expect(muya.getHistory().stack.redo).toHaveLength(0); + }); +}); diff --git a/packages/muya/src/editor/index.ts b/packages/muya/src/editor/index.ts index e9cebc1c4f..f49b43c71a 100644 --- a/packages/muya/src/editor/index.ts +++ b/packages/muya/src/editor/index.ts @@ -2,7 +2,7 @@ import type { JSONOp, JSONOpComponent, JSONOpList } from 'ot-json1'; import type Content from '../block/base/content'; import type Format from '../block/base/format'; import type { Muya } from '../muya'; -import type { ISelection } from '../selection/types'; +import type { IHistorySelection } from '../selection/types'; import type { TState } from '../state/types'; import type { Nullable } from '../types'; import * as otText from 'ot-text-unicode'; @@ -162,7 +162,7 @@ export class Editor { firstLeafBlock.setCursor(0, 0, needUpdated); } - updateContents(operations: JSONOp, selection: Nullable, source: string) { + updateContents(operations: JSONOp, selection: Nullable, source: string) { const { muya } = this; // ot-json1 no-op (`null`) is forwarded to dispatch — JSONState // short-circuits internally so listeners still see a json-change diff --git a/packages/muya/src/history/index.ts b/packages/muya/src/history/index.ts index b1acd32baf..929555ecd3 100644 --- a/packages/muya/src/history/index.ts +++ b/packages/muya/src/history/index.ts @@ -1,10 +1,11 @@ import type { JSONOpList } from 'ot-json1'; import type { Muya } from '../muya'; -import type { ISelection } from '../selection/types'; +import type { IHistorySelection } from '../selection/types'; import type { TState } from '../state/types'; import type { Nullable } from '../types'; import * as json1 from 'ot-json1'; import { asDoc } from '../state'; +import { deepClone } from '../utils'; interface IOptions { delay: number; @@ -14,7 +15,7 @@ interface IOptions { interface IOperation { operation: JSONOpList; - selection: Nullable; + selection: Nullable; } interface IStack { @@ -22,6 +23,39 @@ interface IStack { redo: IOperation[]; } +// A JSON-serializable view of an ISelection. The live `anchorBlock` / +// `focusBlock` references are dropped — they are an in-memory optimization +// only. `Selection._setCursor` re-resolves the target block from +// `anchorPath` / `focusPath` via `scrollPage.queryBlock(path)` when no block +// instance is present, so a path-only selection restores the caret losslessly. +interface ISerializableSelection { + anchor: IHistorySelection['anchor']; + focus: IHistorySelection['focus']; + anchorPath: IHistorySelection['anchorPath']; + focusPath: IHistorySelection['focusPath']; + isCollapsed: IHistorySelection['isCollapsed']; + isSelectionInSameBlock: IHistorySelection['isSelectionInSameBlock']; + direction: IHistorySelection['direction']; + type: IHistorySelection['type']; +} + +interface ISerializableOperation { + operation: JSONOpList; + selection: Nullable; +} + +// The public, JSON-serializable shape returned by `getHistory` and accepted by +// `setHistory`. Mirrors the private `_stack` plus the bookkeeping pointers +// (`lastRecorded`, `selectionStack`) needed to round-trip the recording state. +export interface ISerializedHistory { + stack: { + undo: ISerializableOperation[]; + redo: ISerializableOperation[]; + }; + lastRecorded: number; + selectionStack: (Nullable)[]; +} + enum HistoryAction { UNDO = 'undo', REDO = 'redo', @@ -36,7 +70,7 @@ const DEFAULT_OPTIONS = { class History { private _lastRecorded: number = 0; private _ignoreChange: boolean = false; - private _selectionStack: (Nullable)[] = []; + private _selectionStack: (Nullable)[] = []; private _stack: IStack = { undo: [], redo: [], @@ -96,6 +130,109 @@ class History { clear() { this._stack = { undo: [], redo: [] }; + this._selectionStack = []; + this._lastRecorded = 0; + } + + /** + * Return a deep, JSON-serializable snapshot of the undo/redo history. + * + * The ot-json1 ops are plain JSON arrays and are deep-cloned as-is. + * Selections drop their live `anchorBlock` / `focusBlock` references and + * keep only the serializable `anchorPath` / `focusPath` + offsets; the + * caret is re-resolved from those paths on restore (see + * `_toSerializableSelection`). The result can be `JSON.stringify`-d, stored + * on a desktop tab, and handed back to `setHistory` to restore the exact + * undo/redo state — `setHistory(getHistory())` followed by `undo()` + * reproduces the prior document state. + */ + getHistory(): ISerializedHistory { + return { + stack: { + undo: this._stack.undo.map(op => this._toSerializableOperation(op)), + redo: this._stack.redo.map(op => this._toSerializableOperation(op)), + }, + lastRecorded: this._lastRecorded, + selectionStack: this._selectionStack.map(sel => + this._toSerializableSelection(sel), + ), + }; + } + + /** + * Restore a snapshot previously produced by `getHistory`. Replaces the + * undo/redo stacks and recording bookkeeping. The restored selections are + * path-only; `Selection.setSelection` / `_setCursor` resolve the live + * block from the path when the op is later applied by `undo` / `redo`. + */ + setHistory(history: ISerializedHistory) { + this._stack = { + undo: history.stack.undo.map(op => this._fromSerializableOperation(op)), + redo: history.stack.redo.map(op => this._fromSerializableOperation(op)), + }; + this._lastRecorded = history.lastRecorded ?? 0; + this._selectionStack = (history.selectionStack ?? []).map(sel => + this._fromSerializableSelection(sel), + ); + } + + private _toSerializableOperation(op: IOperation): ISerializableOperation { + return { + operation: deepClone(op.operation), + selection: this._toSerializableSelection(op.selection), + }; + } + + private _fromSerializableOperation(op: ISerializableOperation): IOperation { + return { + operation: deepClone(op.operation), + selection: this._fromSerializableSelection(op.selection), + }; + } + + // Strip the live block references and keep only plain paths + offsets. + private _toSerializableSelection( + selection: Nullable, + ): Nullable { + if (selection == null) + return selection; + + return { + anchor: deepClone(selection.anchor), + focus: deepClone(selection.focus), + anchorPath: deepClone(selection.anchorPath), + focusPath: deepClone(selection.focusPath), + isCollapsed: selection.isCollapsed, + isSelectionInSameBlock: selection.isSelectionInSameBlock, + direction: selection.direction, + type: selection.type, + }; + } + + // Rebuild a selection without live block references. The block instances + // are intentionally omitted: the only consumers of a restored selection + // are `editor.updateContents` and `selection._setCursor`, both of which + // re-resolve the target block from `anchorPath` / `focusPath` via + // `scrollPage.queryBlock` when no block instance is present. The return + // type is `IHistorySelection`, whose `anchorBlock` / `focusBlock` are + // optional, so the missing block fields are part of the contract rather + // than an unsound cast over fabricated `ContentBlock` instances. + private _fromSerializableSelection( + selection: Nullable, + ): Nullable { + if (selection == null) + return selection; + + return { + anchor: deepClone(selection.anchor), + focus: deepClone(selection.focus), + anchorPath: deepClone(selection.anchorPath), + focusPath: deepClone(selection.focusPath), + isCollapsed: selection.isCollapsed, + isSelectionInSameBlock: selection.isSelectionInSameBlock, + direction: selection.direction, + type: selection.type, + }; } cutoff() { diff --git a/packages/muya/src/muya.ts b/packages/muya/src/muya.ts index 5d332dd92e..44bd836425 100644 --- a/packages/muya/src/muya.ts +++ b/packages/muya/src/muya.ts @@ -179,6 +179,35 @@ export class Muya { this.editor.history.redo(); } + /** + * Return a JSON-serializable snapshot of the undo/redo history. + * + * Used by the desktop shell to persist each tab's editing history across + * tab switches: read it before deactivating a tab, store it, and hand it + * back to `setHistory` when the tab is re-selected. The ot-json1 ops are + * deep-cloned plain JSON; selections are reduced to their serializable + * paths/offsets (live block references are dropped and re-resolved on + * restore). Lossless round-trip: `setHistory(getHistory())` then `undo()` + * reproduces the prior document state. + */ + getHistory() { + return this.editor.history.getHistory(); + } + + /** + * Restore a history snapshot previously produced by `getHistory`. + */ + setHistory(history: ReturnType) { + this.editor.history.setHistory(history); + } + + /** + * Clear the undo/redo history (e.g. after loading a fresh document). + */ + clearHistory() { + this.editor.history.clear(); + } + /** * Search value in current document. * @param {string} value diff --git a/packages/muya/src/selection/types.ts b/packages/muya/src/selection/types.ts index 7f462857ff..377016291b 100644 --- a/packages/muya/src/selection/types.ts +++ b/packages/muya/src/selection/types.ts @@ -36,3 +36,14 @@ export interface ISelection { direction: string; type: string; } + +// An `ISelection` whose live `anchorBlock` / `focusBlock` references are +// optional. The history stacks store selections that may have lost their block +// instances after a serialize/restore round-trip (those references are an +// in-memory optimization re-resolved from `anchorPath` / `focusPath` on apply). +// A full `ISelection` is assignable to this type, so live selections captured +// via `getSelection()` still fit without any cast. +export type IHistorySelection = Omit & { + anchorBlock?: ContentBlock; + focusBlock?: ContentBlock; +}; From 8421aece9fe72f01e873ec7e66feda6040426d28 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 15:59:07 +0800 Subject: [PATCH 14/17] feat(muya): add invalidateImageCache (#4396) The renderer memoises loaded inline images in `loadImageMap` (skipped on the next render once `isSuccess` is true) and resolved URLs in `urlMap`, so an image whose file changed on disk would keep showing the stale bitmap with no way to flush it. Add `InlineRenderer.invalidateImageCache()` which clears both caches and re-renders every content block (via `scrollPage.breadthFirstTraverse`), so `loadImageAsync` runs afresh for inline images. Expose it publicly as `Muya.invalidateImageCache()`, mirroring legacy muyajs `StateRender.invalidateImageCache` so the desktop migration can call `editor.invalidateImageCache()` after a watched image file changes or on the `mt::invalidate-image-cache` IPC. Co-authored-by: Claude Opus 4.8 (1M context) --- .../__tests__/invalidateImageCache.spec.ts | 117 ++++++++++++++++++ packages/muya/src/inlineRenderer/index.ts | 24 ++++ packages/muya/src/muya.ts | 13 ++ 3 files changed, 154 insertions(+) create mode 100644 packages/muya/src/__tests__/invalidateImageCache.spec.ts diff --git a/packages/muya/src/__tests__/invalidateImageCache.spec.ts b/packages/muya/src/__tests__/invalidateImageCache.spec.ts new file mode 100644 index 0000000000..a61c931fe8 --- /dev/null +++ b/packages/muya/src/__tests__/invalidateImageCache.spec.ts @@ -0,0 +1,117 @@ +// @vitest-environment happy-dom + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Muya } from '../muya'; + +// Coverage for `muya.invalidateImageCache()` — the public API the desktop +// shell calls to force inline images to reload (e.g. after a watched image +// file changes on disk, or on the `mt::invalidate-image-cache` IPC). +// +// The inline renderer memoises loaded images in two maps keyed by src: +// - `loadImageMap` (skipped on the next render once `isSuccess` is true) +// - `urlMap` (resolved/inflight URLs) +// `invalidateImageCache()` clears both and re-renders every content block so +// `loadImageAsync` runs afresh. happy-dom's `Image` never fires load/error +// for a `file://` src, so we seed the caches by hand rather than relying on a +// real load resolving. + +const bootedHosts: HTMLElement[] = []; +let originalVersion: string | undefined; +let hadVersion = false; + +beforeEach(() => { + hadVersion = 'MUYA_VERSION' in window; + originalVersion = window.MUYA_VERSION; + window.MUYA_VERSION = 'test'; +}); + +afterEach(() => { + while (bootedHosts.length) { + const host = bootedHosts.pop()!; + host.remove(); + } + if (hadVersion) + window.MUYA_VERSION = originalVersion as string; + else + delete (window as Partial).MUYA_VERSION; +}); + +function bootMuya(markdown: string): Muya { + const host = document.createElement('div'); + document.body.appendChild(host); + const muya = new Muya(host, { markdown } as ConstructorParameters[1]); + muya.init(); + bootedHosts.push(muya.domNode); + return muya; +} + +function renderer(muya: Muya) { + return muya.editor.inlineRenderer.renderer; +} + +describe('muya.invalidateImageCache()', () => { + it('exposes the method on the Muya instance', () => { + const muya = bootMuya('# hi'); + expect(typeof muya.invalidateImageCache).toBe('function'); + }); + + it('clears the loadImageMap and urlMap caches', () => { + const muya = bootMuya('![alt](/tmp/pic.png)'); + const { loadImageMap, urlMap } = renderer(muya); + + loadImageMap.set('file:///tmp/pic.png', { + id: 'img-1', + isSuccess: true, + url: 'file:///tmp/pic.png', + width: 320, + height: 240, + }); + urlMap.set('file:///tmp/pic.png', 'data:image/png;base64,AAAA'); + + expect(loadImageMap.size).toBe(1); + expect(urlMap.size).toBe(1); + + muya.invalidateImageCache(); + + expect(loadImageMap.size).toBe(0); + expect(urlMap.size).toBe(0); + }); + + it('re-renders content blocks so inline images load again', async () => { + const muya = bootMuya('![alt](/tmp/pic.png)'); + const inlineRenderer = muya.editor.inlineRenderer; + const { loadImageMap } = renderer(muya); + + loadImageMap.set('file:///tmp/pic.png', { + id: 'img-1', + isSuccess: true, + url: 'file:///tmp/pic.png', + }); + + const patchSpy = vi.spyOn(inlineRenderer, 'patch'); + + muya.invalidateImageCache(); + + // The image cache is flushed synchronously... + expect(loadImageMap.size).toBe(0); + // ...and the content block carrying the image is re-patched, which + // re-runs `loadImageAsync` for it. + await vi.waitFor(() => { + expect(patchSpy).toHaveBeenCalled(); + }); + + patchSpy.mockRestore(); + }); + + it('does not throw on a document with no images', () => { + const muya = bootMuya('just a paragraph, no images here'); + expect(() => muya.invalidateImageCache()).not.toThrow(); + expect(renderer(muya).loadImageMap.size).toBe(0); + expect(renderer(muya).urlMap.size).toBe(0); + }); + + it('does not throw on an empty document', () => { + const muya = bootMuya(''); + expect(() => muya.invalidateImageCache()).not.toThrow(); + }); +}); diff --git a/packages/muya/src/inlineRenderer/index.ts b/packages/muya/src/inlineRenderer/index.ts index 99f568d05e..6942058f6c 100644 --- a/packages/muya/src/inlineRenderer/index.ts +++ b/packages/muya/src/inlineRenderer/index.ts @@ -35,6 +35,30 @@ class InlineRenderer { return tokenizer(text, { hasBeginRules, labels, options, highlights }); } + /** + * Flush every cached image and force inline images to reload. + * + * The renderer memoises loaded images in `loadImageMap` (keyed by src, + * skipped on the next render once `isSuccess` is true) and resolved URLs + * in `urlMap`. When an image file changes on disk the cached entry would + * otherwise keep the stale bitmap, so clearing both maps and re-rendering + * every content block re-runs `loadImageAsync`, which loads the source + * afresh. Mirrors legacy muyajs `StateRender.invalidateImageCache`. + */ + invalidateImageCache() { + this.renderer.loadImageMap.clear(); + this.renderer.urlMap.clear(); + + const { scrollPage } = this.muya.editor; + if (!scrollPage) + return; + + scrollPage.breadthFirstTraverse((node) => { + if (node.isContent()) + node.update(); + }); + } + patch(block: Format, cursor?: ICursor, highlights: IHighlight[] = []) { this.collectReferenceDefinitions(); const { domNode } = block; diff --git a/packages/muya/src/muya.ts b/packages/muya/src/muya.ts index 44bd836425..9d54d58de6 100644 --- a/packages/muya/src/muya.ts +++ b/packages/muya/src/muya.ts @@ -418,6 +418,19 @@ export class Muya { this.ui.hideAllFloatTools(); } + /** + * Flush every cached inline image and force them to reload. + * + * The renderer memoises loaded images, so an image whose file changed on + * disk would otherwise keep showing the stale bitmap. Desktop calls this + * after a watched image file changes or on the `mt::invalidate-image-cache` + * IPC; it clears the image caches and re-renders all content blocks so the + * images load afresh. + */ + invalidateImageCache() { + this.editor.inlineRenderer.invalidateImageCache(); + } + /** * Copy the current document as Markdown to the clipboard. */ From baf81fe8da64a017011b1b209ec42a450f62ed7c Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 16:02:58 +0800 Subject: [PATCH 15/17] test(muya): port MarkText regression cases (#4341/#4307/#4190) (#4399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(muya): port MarkText regression cases (#4341/#4307/#4190) Port three MarkText-specific regression cases from the legacy `packages/muyajs` desktop specs into `@muyajs/core`'s own suite to lock in behavioral fidelity for the migration off legacy muyajs. Tests only — no engine changes. - #4341 nested mixed lists (ul-in-ol / ol-in-ul): PASS. The state tree from MarkdownToState nests the differing-type list under the correct list-item (no paragraph collapse), and md -> state -> md round-trips identically. Ported as structural + round-trip assertions. - #4190 table normalization (body row with more/fewer cells than the header): PASS. StateToMarkdown.serializeTable clamps each row to the header column count (extra cell dropped) and never throws. The legacy spec hand-built a malformed block tree for ExportMarkdown.normalizeTable; the muya equivalent hand-builds a malformed ITableState because a GFM round trip can never produce a ragged table. - #4307 CJK strong flanking (`**"加粗"**` against a CJK boundary): documented engine GAP. marked@16 implements the CommonMark flanking rule literally and classifies CJK ideographs / Hangul as "other" (neither whitespace nor punctuation), so `**` adjacent to a CJK char with punctuation-bounded inner content does not open/close emphasis. Legacy muyajs shipped a custom tokenizer that treats CJK as punctuation for flanking; marked does not. The four CJK cases assert the CORRECT (legacy) behavior under `it.fails`, so the suite stays green while the gap exists and flips red the moment the gap closes. Sanity cases that already work are plain `it` so a future fix can't regress them. Co-Authored-By: Claude Opus 4.8 (1M context) * test(muya): drop duplicate tableNormalization spec The serializeTable ragged-row coverage in tableNormalization.spec.ts duplicated the existing serializeTable — row width mismatch suite in stateToMarkdown.spec.ts (both #4222/#4190): same well-formed, extra-cell-dropped, and short-row scenarios. Keeping two suites for one behavior risks them drifting apart, so remove the redundant file and keep the single existing suite as the source of truth. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../state/__tests__/nestedMixedLists.spec.ts | 123 ++++++++++++++++++ .../state/__tests__/strongCjkFlanking.spec.ts | 81 ++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 packages/muya/src/state/__tests__/nestedMixedLists.spec.ts create mode 100644 packages/muya/src/state/__tests__/strongCjkFlanking.spec.ts diff --git a/packages/muya/src/state/__tests__/nestedMixedLists.spec.ts b/packages/muya/src/state/__tests__/nestedMixedLists.spec.ts new file mode 100644 index 0000000000..28326332ec --- /dev/null +++ b/packages/muya/src/state/__tests__/nestedMixedLists.spec.ts @@ -0,0 +1,123 @@ +// @vitest-environment happy-dom + +import type { TState } from '../types'; +import { describe, expect, it } from 'vitest'; +import { MarkdownToState } from '../markdownToState'; +import StateToMarkdown from '../stateToMarkdown'; + +// Regression coverage ported from marktext#4341 (legacy desktop spec +// `test/unit/specs/markdown-nested-mixed-lists.spec.ts`). A list whose type +// differs from its enclosing list item (a `ul` inside an `ol`, or an `ol` +// inside a `ul`) was being rewritten into a paragraph by the legacy muyajs +// lexer, losing the nested-list structure. +// +// The new @muyajs/core engine parses through marked + the state tree rather +// than the legacy ContentState/ExportMarkdown pair, so the equivalent checks +// are: +// - structural: the state tree from `MarkdownToState` carries a nested +// `bullet-list` / `order-list` under the correct outer `list-item` +// (mirrors the legacy `blocks[...].children.find(type === 'ul')` probe). +// - round-trip: `MarkdownToState` → `StateToMarkdown` reproduces the source +// markdown verbatim (mirrors the legacy importMarkdown → ExportMarkdown +// identity assertion). The legacy spec used a 3-space indent for the +// nested `ul` and 2-space for the nested `ol`; the @muyajs/core serializer +// uses a content-aligned indent (marker width + 1) so the expected output +// is regenerated rather than hard-coded against the legacy indentation. + +function toState(markdown: string): TState[] { + return new MarkdownToState({ + footnote: false, + math: true, + isGitlabCompatibilityEnabled: true, + trimUnnecessaryCodeBlockEmptyLines: false, + frontMatter: true, + }).generate(markdown); +} + +function roundTrip(markdown: string): string { + return new StateToMarkdown({ listIndentation: 1 }).generate(toState(markdown)); +} + +// `children` is only present on container states; leaf states (paragraph, +// table.cell, …) have no `children`. Narrow on the property so we stay +// `any`-free while still tolerating a leaf/undefined input. +function children(state: TState | undefined): TState[] { + return state && 'children' in state ? state.children : []; +} + +// First-child text of a list item (its leading paragraph). Leaf states carry a +// `text` field; container states do not. +function firstText(state: TState | undefined): string | undefined { + const first = children(state)[0]; + return first && 'text' in first ? first.text : undefined; +} + +describe('nested mixed lists (#4341)', () => { + it('preserves a bullet list nested inside an ordered list item (round trip)', () => { + const markdown = `1. Eat a carrot. +2. Find an application: + - New + - Open + - Save +`; + // First pass is the identity, and a second pass is stable. + const once = roundTrip(markdown); + expect(once).toBe(markdown); + expect(roundTrip(once)).toBe(once); + }); + + it('preserves an ordered list nested inside a bullet list item (round trip)', () => { + const markdown = `- Outer bullet +- Container item: + 1. First step + 2. Second step + 3. Third step +`; + const once = roundTrip(markdown); + expect(once).toBe(markdown); + expect(roundTrip(once)).toBe(once); + }); + + it('produces a bullet-list state nested inside the second order-list item (not a paragraph)', () => { + const states = toState(`1. Eat a carrot. +2. Find an application: + - New + - Open + - Save +`); + const ol = states.find(s => s.name === 'order-list'); + expect(ol, 'expected a top-level order-list state').toBeDefined(); + + const secondItem = children(ol)[1]; + expect(secondItem.name).toBe('list-item'); + + const nestedList = children(secondItem).find(c => c.name === 'bullet-list'); + expect(nestedList, 'expected a bullet-list nested inside the second order-list item').toBeDefined(); + // The item is exactly [leading paragraph, nested bullet-list] — the + // nested list did NOT collapse into a paragraph (the #4341 failure mode). + expect(children(secondItem).map(c => c.name)).toEqual(['paragraph', 'bullet-list']); + expect(children(nestedList)).toHaveLength(3); + expect(children(nestedList).map(firstText)).toEqual(['New', 'Open', 'Save']); + }); + + it('produces an order-list state nested inside a bullet-list item (not a paragraph)', () => { + const states = toState(`- Outer bullet +- Container item: + 1. First step + 2. Second step + 3. Third step +`); + const ul = states.find(s => s.name === 'bullet-list'); + expect(ul, 'expected a top-level bullet-list state').toBeDefined(); + + const secondItem = children(ul)[1]; + expect(secondItem.name).toBe('list-item'); + + const nestedList = children(secondItem).find(c => c.name === 'order-list'); + expect(nestedList, 'expected an order-list nested inside the second bullet-list item').toBeDefined(); + // The item is exactly [leading paragraph, nested order-list]. + expect(children(secondItem).map(c => c.name)).toEqual(['paragraph', 'order-list']); + expect(children(nestedList)).toHaveLength(3); + expect(children(nestedList).map(firstText)).toEqual(['First step', 'Second step', 'Third step']); + }); +}); diff --git a/packages/muya/src/state/__tests__/strongCjkFlanking.spec.ts b/packages/muya/src/state/__tests__/strongCjkFlanking.spec.ts new file mode 100644 index 0000000000..2722d920fb --- /dev/null +++ b/packages/muya/src/state/__tests__/strongCjkFlanking.spec.ts @@ -0,0 +1,81 @@ +// @vitest-environment happy-dom + +import { describe, expect, it } from 'vitest'; +import { renderToStaticHTML } from '../renderToStaticHTML'; + +// Regression coverage ported from marktext#4307 (legacy desktop spec +// `test/unit/specs/markdown-strong-cjk.spec.ts`). Strong emphasis (`**…**`) +// whose `**` delimiters sit directly against a CJK character should still be +// recognised, even when the emphasised content begins/ends with a punctuation +// character (a quote, bracket, or paren). +// +// WHY THE CJK CASES FAIL ON @muyajs/core (documented engine gap): +// The new engine tokenises inline markdown with marked@16, which implements +// the CommonMark emphasis "flanking" rule literally. CommonMark classifies +// every character as whitespace, (Unicode) punctuation, or "other"; CJK +// ideographs and Hangul are "other". For a left-flanking `**` run, clause +// (2b) requires the character *before* the run to be whitespace or +// punctuation whenever the character *after* the run is punctuation. In +// `例子例子**"加粗"**例子例子` the char after the opening `**` is `"` +// (punctuation) and the char before it is `子` (a CJK ideograph → "other", +// neither whitespace nor punctuation), so the run is not left-flanking and +// marked emits the literal `**`. The same happens at the closing run. +// +// Legacy marktext shipped its own inline tokenizer (muyajs +// `lib/parser/render`) whose `canOpen/canCloseEmphasis` flanking helpers +// treat CJK characters as punctuation, so `**` adjacent to a CJK char with +// punctuation-bounded inner content opens/closes emphasis. marked has no +// such patch, and fixing it requires either patching the dependency or +// shipping a custom inline-emphasis tokenizer extension — out of scope for a +// tests-only fidelity-verification PR. The CJK cases below assert the +// CORRECT (legacy) behavior and are wrapped in `it.fails`, so: +// - the suite stays green while the gap exists, AND +// - the moment the engine starts recognising these (e.g. a marked upgrade +// or a flanking patch lands) the `it.fails` flips red, forcing this file +// to be promoted to a plain `it`. Fidelity can only go up. +// +// This gap is documented in the PR body for #4307 follow-up. + +function rendersStrong(src: string): boolean { + const html = renderToStaticHTML(src, { sanitize: false }); + return //.test(html); +} + +describe('strong emphasis with CJK boundaries (#4307)', () => { + // Cases that already work on @muyajs/core — they lock in the pre-existing + // behavior so any fix to the CJK gap can't regress them. Each emphasised + // run here is bounded by a CJK ideograph or whitespace on the inner side, + // so the flanking rule is satisfied without the legacy CJK-as-punctuation + // patch. + const sanityCases = [ + 'before **"normal"** after', + 'before**normal**after', + '中文**加粗**中文', + ]; + + for (const src of sanityCases) { + it(`recognises strong in: ${src}`, () => { + expect(rendersStrong(src)).toBe(true); + }); + } + + // CJK-boundary cases. These are the #4307 regression cases the legacy + // marktext tokenizer fixed. @muyajs/core (via marked) does NOT recognise + // them — documented engine gap (see file header). The assertion states the + // CORRECT expected behavior; `it.fails` keeps the suite green until the gap + // is closed, at which point it must be converted to a plain `it`. + const cjkGapCases = [ + '例子例子**"加粗"**例子例子', + '日本語**(強調)**日本語', + '한국어**[강조]**한국어', + // Non-BMP CJK (CJK Ext-B): 𠀀 is U+20000, stored as a surrogate pair. + // The flanking boundary check must read the full code point. + '𠀀𠀁**"加粗"**𠀀𠀁', + ]; + + for (const src of cjkGapCases) { + it.fails(`[GAP #4307] should recognise strong in CJK context: ${src}`, () => { + expect(rendersStrong(src)).toBe(true); + }); + } +}); From 945014c0f6e2ccd9a342153e9860b8675bcde19b Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 16:03:32 +0800 Subject: [PATCH 16/17] feat(muya): add createTable / insertImage / setCursor API (#4397) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): add createTable / insertImage / setCursor API Complete the programmatic block-editing surface @muyajs/core exposes for the desktop migration off legacy packages/muyajs, alongside the existing duplicate / insertParagraph / deleteParagraph / updateParagraph methods. - createTable({ rows, columns }): builds a GFM ITableState (first row is the header, every cell empty with align 'none') and replaces the block at the cursor with it, placing the caret in the first cell. Mirrors legacy createTableInFigure/createFigure and muya's table conventions. - insertImage({ src, alt }): writes ![alt](src) at the cursor in the active Format block through the text setter so it dispatches a JSON op (state stays in sync); derives alt from the file name and percent- encodes plain paths the way legacy insertImage / replaceImage do. No-ops when there is no active formattable block. - setCursor(cursor): resolves the target block(s) by path on the live tree and restores the selection the way Editor.updateContents does — block.setCursor for same-block, selection.setSelection with resolved block instances for cross-block (bare paths don't work — _setCursor needs a block's domNode). Accepts the anchor/focus/path and start/end ICursor shapes. Adds a happy-dom vitest spec covering dimensions, cell defaults, caret placement, image markdown/alt/encoding, and same/cross-block cursor resolution plus the no-op guards. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(muya): validate createTable dimensions and tighten insertImage data-URL check Address Copilot review on the createTable / insertImage / setCursor API: - createTable now coerces rows/columns to integers and clamps to a valid GFM shape (rows >= 2, columns >= 1). Garbage input (rows: 0, NaN, Infinity, fractional/negative values) previously built an invalid table state and could crash Table.columnCount (which reads firstChild.firstChild). - insertImage now uses the strict DATA_URL_REG instead of a loose /^data:image// prefix check, aligning with legacy muyajs insertImage and utils/image.ts getImageSrc. A bare/malformed data:image src no longer embeds verbatim; it falls through to the percent-encoded plain-path branch. Promoted the regex to config/index.ts for reuse. Extends createTableImageCursor.spec.ts with clamping/coercion cases and well-formed vs malformed data-URL cases. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../__tests__/createTableImageCursor.spec.ts | 292 ++++++++++++++++++ packages/muya/src/config/index.ts | 6 + packages/muya/src/muya.ts | 158 +++++++++- 3 files changed, 455 insertions(+), 1 deletion(-) create mode 100644 packages/muya/src/__tests__/createTableImageCursor.spec.ts diff --git a/packages/muya/src/__tests__/createTableImageCursor.spec.ts b/packages/muya/src/__tests__/createTableImageCursor.spec.ts new file mode 100644 index 0000000000..59faee0dd4 --- /dev/null +++ b/packages/muya/src/__tests__/createTableImageCursor.spec.ts @@ -0,0 +1,292 @@ +// @vitest-environment happy-dom + +import type Content from '../block/base/content'; +import type Parent from '../block/base/parent'; +import type { ITableState, TState } from '../state/types'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { Muya } from '../muya'; +import { isTableState } from '../state/types'; + +// Coverage for the programmatic editing API added for the muyajs -> +// @muyajs/core desktop migration: createTable / insertImage / setCursor. +// These complete the block-editing surface the desktop drives (table insert, +// image insert from the image tool, and programmatic cursor placement). +// +// Tree/text mutations dispatch json1 ops that flush to the document state on +// the next animation frame (see JSONState._emitStateChange), so assertions on +// getState()/getMarkdown() are wrapped in vi.waitFor to await that flush. + +const bootedHosts: HTMLElement[] = []; +let originalVersion: string | undefined; +let hadVersion = false; + +beforeEach(() => { + hadVersion = 'MUYA_VERSION' in window; + originalVersion = window.MUYA_VERSION; + window.MUYA_VERSION = 'test'; +}); + +afterEach(() => { + while (bootedHosts.length) { + const host = bootedHosts.pop()!; + host.remove(); + } + if (hadVersion) + window.MUYA_VERSION = originalVersion as string; + else + delete (window as Partial).MUYA_VERSION; +}); + +function bootMuya(markdown: string): Muya { + const host = document.createElement('div'); + document.body.appendChild(host); + const muya = new Muya(host, { markdown } as ConstructorParameters[1]); + muya.init(); + bootedHosts.push(muya.domNode); + return muya; +} + +// Place a collapsed caret on the first content block (and mark it active so the +// block-level ops resolve their target the same way the editor does after a +// click). +function placeCursorOnFirstBlock(muya: Muya, offset = 0): Content { + const first = muya.editor.scrollPage!.firstContentInDescendant()!; + first.setCursor(offset, offset, true); + muya.editor.activeContentBlock = first; + return first; +} + +function firstBlock(muya: Muya): TState { + return muya.getState()[0]; +} + +function firstTable(muya: Muya): ITableState { + const b = firstBlock(muya); + if (!isTableState(b)) + throw new Error(`expected a table, got ${b.name}`); + return b; +} + +describe('muya.createTable()', () => { + it('replaces the current block with a table of the requested dimensions', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya); + muya.createTable({ rows: 3, columns: 4 }); + await vi.waitFor(() => { + const b = firstTable(muya); + expect(b.children.length).toBe(3); // rows (header + 2 body) + expect(b.children.every(row => row.children.length === 4)).toBe(true); // columns + }); + }); + + it('builds empty cells with align none', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya); + muya.createTable({ rows: 2, columns: 2 }); + await vi.waitFor(() => { + const cells = firstTable(muya).children.flatMap(row => row.children); + expect(cells.every(c => c.name === 'table.cell')).toBe(true); + expect(cells.every(c => c.text === '')).toBe(true); + expect(cells.every(c => c.meta.align === 'none')).toBe(true); + }); + }); + + it('places the cursor in the first cell of the new table', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya); + muya.createTable({ rows: 2, columns: 2 }); + await vi.waitFor(() => { + expect(firstBlock(muya).name).toBe('table'); + }); + const sel = muya.editor.selection.getSelection(); + expect(sel).not.toBeNull(); + // The caret lands on a table-cell content block. + expect(sel!.anchorBlock.blockName).toBe('table.cell.content'); + }); + + it('is a no-op when there is no current block', () => { + const muya = bootMuya('hello\n'); + muya.editor.activeContentBlock = null; + muya.editor.selection.anchorBlock = null; + expect(() => muya.createTable({ rows: 2, columns: 2 })).not.toThrow(); + expect(firstBlock(muya).name).toBe('paragraph'); + }); + + it('clamps zero/negative dimensions to a valid table (rows >= 2, columns >= 1)', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya); + // rows = 0 would otherwise build a table with no rows and crash + // `Table.columnCount` (which reads `firstChild.firstChild`). + expect(() => muya.createTable({ rows: 0, columns: 0 })).not.toThrow(); + await vi.waitFor(() => { + const b = firstTable(muya); + expect(b.children.length).toBe(2); // header + one body row + expect(b.children.every(row => row.children.length === 1)).toBe(true); + }); + }); + + it('coerces non-finite / fractional dimensions to integers', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya); + expect(() => + muya.createTable({ rows: Number.NaN, columns: Number.POSITIVE_INFINITY }), + ).not.toThrow(); + await vi.waitFor(() => { + const b = firstTable(muya); + // NaN -> clamped to 2 rows; Infinity column count is not finite so it + // also normalises to the minimum of 1 column rather than allocating + // an array of non-integer length. + expect(b.children.length).toBe(2); + expect(b.children.every(row => row.children.length === 1)).toBe(true); + }); + }); + + it('floors fractional dimensions instead of building a ragged table', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya); + muya.createTable({ rows: 3.9, columns: 2.9 }); + await vi.waitFor(() => { + const b = firstTable(muya); + expect(b.children.length).toBe(3); // floor(3.9) + expect(b.children.every(row => row.children.length === 2)).toBe(true); // floor(2.9) + }); + }); +}); + +describe('muya.insertImage()', () => { + it('inserts an inline image at the cursor', async () => { + const muya = bootMuya('hello\n'); + placeCursorOnFirstBlock(muya, 5); // caret at end of "hello" + muya.insertImage({ src: 'https://example.com/cat.png' }); + await vi.waitFor(() => { + const md = muya.getMarkdown(); + expect(md).toContain('https://example.com/cat.png'); + expect(md).toContain('!['); + }); + }); + + it('derives alt text from the file name when none is given', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya, 0); + muya.insertImage({ src: '/tmp/photos/sunset.jpg' }); + await vi.waitFor(() => { + expect(muya.getMarkdown()).toContain('![sunset]('); + }); + }); + + it('uses the provided alt text', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya, 0); + muya.insertImage({ src: 'https://example.com/x.png', alt: 'My Pic' }); + await vi.waitFor(() => { + expect(muya.getMarkdown()).toContain('![My Pic](https://example.com/x.png)'); + }); + }); + + it('percent-encodes spaces in local paths', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya, 0); + muya.insertImage({ src: '/my photos/a b.png', alt: 'pic' }); + await vi.waitFor(() => { + expect(muya.getMarkdown()).toContain('/my%20photos/a%20b.png'); + }); + }); + + it('is a no-op when there is no active formattable block', () => { + const muya = bootMuya('hello\n'); + muya.editor.activeContentBlock = null; + muya.editor.selection.anchorBlock = null; + expect(() => muya.insertImage({ src: 'https://example.com/x.png' })).not.toThrow(); + expect(muya.getMarkdown()).not.toContain('!['); + }); + + it('embeds a well-formed base64 data URL verbatim', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya, 0); + const dataUrl + = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='; + muya.insertImage({ src: dataUrl, alt: 'dot' }); + await vi.waitFor(() => { + expect(muya.getMarkdown()).toContain(`![dot](${dataUrl})`); + }); + }); + + it('does not embed a malformed data: src verbatim (aligns with strict DATA_URL_REG)', async () => { + const muya = bootMuya('\n'); + placeCursorOnFirstBlock(muya, 0); + // `data:image/` prefix with no comma/payload — the old loose + // `^data:image/` check would have embedded it verbatim. It must instead + // fall through to the plain-path branch (spaces and '#' percent-encoded). + const malformed = 'data:image/png not-a-real#payload'; + muya.insertImage({ src: malformed, alt: 'bad' }); + await vi.waitFor(() => { + const md = muya.getMarkdown(); + // Treated as a plain path: spaces and '#' are percent-encoded, so the + // raw malformed string is not present verbatim. + expect(md).not.toContain(`(${malformed})`); + expect(md).toContain('data:image/png%20not-a-real%23payload'); + }); + }); +}); + +describe('muya.setCursor()', () => { + it('positions the caret in the same block (anchor/focus/path shape)', async () => { + const muya = bootMuya('hello world\n'); + const first = muya.editor.scrollPage!.firstContentInDescendant()!; + muya.setCursor({ + anchor: { offset: 3 }, + focus: { offset: 3 }, + anchorPath: first.path, + focusPath: first.path, + }); + await vi.waitFor(() => { + const sel = muya.editor.selection.getSelection(); + expect(sel).not.toBeNull(); + expect(sel!.anchorBlock).toBe(first); + expect(sel!.anchor.offset).toBe(3); + }); + }); + + it('accepts the start/end/path shape', async () => { + const muya = bootMuya('hello world\n'); + const first = muya.editor.scrollPage!.firstContentInDescendant()!; + muya.setCursor({ + start: { offset: 2 }, + end: { offset: 2 }, + path: first.path, + }); + await vi.waitFor(() => { + const sel = muya.editor.selection.getSelection(); + expect(sel!.anchorBlock).toBe(first); + expect(sel!.anchor.offset).toBe(2); + }); + }); + + it('resolves the target block across two paragraphs', async () => { + const muya = bootMuya('first\n\nsecond\n'); + const blocks = muya.editor.scrollPage!; + const secondPara = blocks.find(1) as Parent; + const secondContent = secondPara.firstContentInDescendant()!; + muya.setCursor({ + anchor: { offset: 1 }, + focus: { offset: 1 }, + anchorPath: secondContent.path, + focusPath: secondContent.path, + }); + await vi.waitFor(() => { + const sel = muya.editor.selection.getSelection(); + expect(sel!.anchorBlock).toBe(secondContent); + expect(sel!.anchor.offset).toBe(1); + }); + }); + + it('does not throw and leaves the document intact for an unresolvable path', () => { + const muya = bootMuya('hello\n'); + expect(() => muya.setCursor({ + anchor: { offset: 0 }, + focus: { offset: 0 }, + anchorPath: [99, 'text'], + focusPath: [99, 'text'], + })).not.toThrow(); + }); +}); diff --git a/packages/muya/src/config/index.ts b/packages/muya/src/config/index.ts index ec20700d9c..8569bea9e3 100644 --- a/packages/muya/src/config/index.ts +++ b/packages/muya/src/config/index.ts @@ -388,6 +388,12 @@ export const isWin // http[s] (domain or IPv4 or localhost or IPv6) [port] /not-white-space export const URL_REG = /^http(s)?:\/\/([\w\-.~]+\.[a-z]{2,}|[0-9.]+|localhost|\[[a-f0-9.:]+\])(:\d{1,5})?\/\S+/i; +// A fully-formed base64/percent-encoded image data URL, e.g. +// `data:image/png;base64,iVBORw0KGg...`. Mirrors legacy muyajs `DATA_URL_REG` +// and `utils/image.ts` `getImageSrc`, so a bare `data:image/` prefix is not +// treated as a safe-to-embed source. +export const DATA_URL_REG + = /^data:image\/[\w+-]+(?:;[\w-]+=[\w-]+|;base64)*,[a-zA-Z0-9+/]+={0,2}$/; export const PREVIEW_DOMPURIFY_CONFIG = { // do not forbid `class` because `code` element use class to present language FORBID_ATTR: ['style', 'contenteditable'], diff --git a/packages/muya/src/muya.ts b/packages/muya/src/muya.ts index 9d54d58de6..369a795899 100644 --- a/packages/muya/src/muya.ts +++ b/packages/muya/src/muya.ts @@ -2,15 +2,18 @@ import type Content from './block/base/content'; import type Parent from './block/base/parent'; import type { Listener } from './event/types'; import type { ILocale } from './i18n/types'; +import type { ICursor } from './selection/types'; import type { ITocItem } from './state/getTOC'; -import type { IBulletListState, IOrderListState, ITaskListState, TState } from './state/types'; +import type { IBulletListState, IOrderListState, ITableState, ITaskListState, TState } from './state/types'; import type { IMuyaOptions } from './types'; import Format from './block/base/format'; import { ScrollPage } from './block/scrollPage'; import emptyStates from './config/emptyStates'; import { CLASS_NAMES, + DATA_URL_REG, MUYA_DEFAULT_OPTIONS, + URL_REG, } from './config/index'; import { Editor } from './editor/index'; @@ -528,6 +531,159 @@ export class Muya { cursorBlock?.setCursor(0, 0, true); } + /** + * Insert a GFM table at the current cursor, replacing the block the cursor + * is in (legacy `createTableInFigure`/`createFigure`). The table has `rows` + * rows × `columns` columns with the first row as the header; every cell is + * empty with `align: 'none'`. The cursor lands in the first cell. No-op when + * there is no current block. `rows`/`columns` are coerced to integers and + * clamped to a valid GFM shape (`rows >= 2`, `columns >= 1`) so invalid + * input (e.g. `rows: 0`, non-finite, or fractional values) still yields a + * usable table instead of an invalid state. + */ + createTable({ rows, columns }: { rows: number; columns: number }) { + const block = this._outmostBlockAtCursor(); + if (!block) + return; + + // Coerce and clamp to a valid GFM table shape. A GFM table needs a + // header row plus at least one body row (rows >= 2) and at least one + // column (columns >= 1). Garbage input (NaN/Infinity/floats/negatives) + // is normalised rather than producing an invalid state — `rows = 0` + // would otherwise build a table with no rows and crash `columnCount` + // (which reads `firstChild.firstChild`). + const safeRows = Math.max(2, Number.isFinite(rows) ? Math.floor(rows) : 0); + const safeColumns = Math.max(1, Number.isFinite(columns) ? Math.floor(columns) : 0); + + const makeRow = (): ITableState['children'][number] => ({ + name: 'table.row', + children: Array.from({ length: safeColumns }, () => ({ + name: 'table.cell' as const, + meta: { align: 'none' }, + text: '', + })), + }); + + const state: ITableState = { + name: 'table', + children: Array.from({ length: safeRows }, makeRow), + }; + + const newTable = ScrollPage.loadBlock('table').create(this, state); + block.replaceWith(newTable); + newTable.firstContentInDescendant()?.setCursor(0, 0, true); + } + + /** + * Insert an inline image at the current cursor in the active formattable + * block, mirroring legacy `insertImage`. The `![alt](src)` markdown is + * written through the `Format` block's text setter so it dispatches a JSON + * op (state stays in sync) rather than mutating the DOM directly. No-op when + * there is no active formattable (`Format`) block — e.g. inside a code block + * or with no cursor. + */ + insertImage({ src = '', alt = '' }: { src?: string; alt?: string }) { + const block = this.editor.activeContentBlock ?? this.editor.selection.anchorBlock; + if (!(block instanceof Format)) + return; + + const cursor = block.getCursor(); + if (cursor == null) + return; + + // Derive a sensible alt from the file name when none is provided, + // matching legacy `insertImage`. + if (!alt) { + const match = /[/\\]?([^./\\]+)\.[a-z]+$/i.exec(src); + alt = match?.[1] ?? ''; + } + + // Only percent-encode plain paths; leave full URLs / well-formed data + // URLs as-is. Mirrors legacy `insertImage` / `replaceImage` src + // handling — `DATA_URL_REG` requires the full `data:image/...,` + // shape (the same regex `utils/image.ts` `getImageSrc` uses), so a bare + // `data:image/` prefix is not embedded verbatim and instead falls through + // to the plain-path branch. + let imgUrl: string; + if (URL_REG.test(src)) + imgUrl = encodeURI(src); + else if (DATA_URL_REG.test(src)) + imgUrl = src; + else + imgUrl = src.replace(/ /g, encodeURI(' ')).replace(/#/g, encodeURIComponent('#')); + + const { start, end } = cursor; + const { text } = block; + // When there is a selection, use it as the alt text (legacy behaviour). + const imageAlt = start.offset !== end.offset ? text.substring(start.offset, end.offset) : alt; + const imageText = `![${imageAlt}](${imgUrl})`; + + // The `text` setter diffs against the old value and dispatches a JSON op. + block.text = text.substring(0, start.offset) + imageText + text.substring(end.offset); + // Re-render and place the caret on the alt text (offset of `![`). + block.setCursor(start.offset + 2, start.offset + 2 + imageAlt.length, true); + } + + /** + * Set the cursor programmatically. The desktop passes a cursor like + * `{ anchor, focus, anchorPath, focusPath }` (and may use `{ start, end }` + * / `block` / `path`). Resolves the target block(s) by path on the live tree + * and restores the selection the same way `Editor.updateContents` does — + * `block.setCursor` for the same-block case, `selection.setSelection` with + * resolved block instances for the cross-block case. Passing bare paths to + * `setSelection` does not work (it needs a block's `domNode`), so we always + * resolve and pass the block instance. No-op when the target can't be + * resolved. + */ + setCursor(cursor: ICursor) { + const { scrollPage } = this.editor; + if (!scrollPage) + return; + + // Accept both the `{ anchor, focus, anchorPath, focusPath }` and the + // `{ start, end, path }`/`block` shapes of ICursor. + const anchor = cursor.anchor ?? cursor.start ?? null; + const focus = cursor.focus ?? cursor.end ?? anchor; + const anchorPath = cursor.anchorPath ?? cursor.path; + const focusPath = cursor.focusPath ?? cursor.path ?? anchorPath; + + if (!anchor || !focus) + return; + + // queryBlock mutates its path argument (path.shift()) — pass copies. + const anchorBlock + = cursor.anchorBlock + ?? cursor.block + ?? (anchorPath ? scrollPage.queryBlock([...anchorPath]) : null); + const focusBlock + = cursor.focusBlock + ?? cursor.block + ?? (focusPath ? scrollPage.queryBlock([...focusPath]) : null); + + if (anchorBlock == null || !anchorBlock.isContent()) + return; + + // Same-block, mirror Editor.updateContents' selection-restore. + if (anchorBlock === focusBlock || focusBlock == null) { + const begin = Math.min(anchor.offset, focus.offset); + const last = Math.max(anchor.offset, focus.offset); + anchorBlock.setCursor(begin, last, true); + return; + } + + if (!focusBlock.isContent()) + return; + + this.editor.selection.setSelection({ + anchor, + focus, + anchorBlock, + anchorPath: anchorBlock.path, + focusBlock, + focusPath: focusBlock.path, + }); + } + /** * Convert the block at the cursor to another type, mirroring marktext's * `updateParagraph`. `type` uses the marktext/muyajs paragraph-menu From efb720be54809905f53ffd1426fbd2a49a8e76b7 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Mon, 8 Jun 2026 16:05:17 +0800 Subject: [PATCH 17/17] feat(muya): add clipboardFilePath paste hook (#4398) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(muya): add clipboardFilePath paste hook Port the legacy @muyajs `clipboardFilePath` option to @muyajs/core. When the user pastes and the OS clipboard holds a file (e.g. an image copied from a file manager rather than image bytes), the embedder resolves it to a local path; if that path is a non-empty image file, muya inserts it as an inline image at the cursor instead of running the default text/HTML paste. Returning '' (or omitting the hook) preserves all existing paste behaviour. - Add optional `clipboardFilePath?: () => Promise` to IMuyaOptions. - Add a pure `resolveClipboardImagePath` helper in utils/paste that gates the hook result on IMAGE_EXT_REG (matching the legacy pasteImage guard). - Short-circuit the clipboard paste flow when the hook yields an image path, splicing `![](src)` into the anchor block (src escaped like Format.replaceImage) and placing the cursor after it. - Tests: unit-cover resolveClipboardImagePath and exercise pasteHandler end to end (hook invoked, image inserted, escaping, and fall-through when the hook returns '' / a non-image / is absent). Co-Authored-By: Claude Opus 4.8 (1M context) * fix(muya): snapshot clipboardData before awaiting clipboardFilePath hook `pasteHandler` awaited `resolveClipboardImagePath()` (the async `clipboardFilePath` hook) before reading `event.clipboardData.getData()`. Because the DOM paste listener does not await the handler, execution yields at that first `await` and resumes after event dispatch completes — at which point `event.clipboardData` can be detached and `getData()` returns ''. This silently broke normal text/HTML paste whenever `clipboardFilePath` was configured but resolved to '' / a non-image. Snapshot text/html synchronously at the top of `pasteHandler`, before any `await`, and thread the snapshot through the `!isSelectionInSameBlock` recursion via optional `rawText`/`rawHtml` params so the re-entry never reads a detached clipboard. Mirrors the legacy `@muyajs` `pasteHandler(event, type, rawText, rawHtml)` ordering. Behaviour is unchanged: the file-path hook still takes precedence and inserts an image, and normal paste still works when the hook is absent/empty. Add a regression test proving text/plain still pastes when the hook is present-but-returns-''; update the precedence test to reflect that clipboardData is now read synchronously up front. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../__tests__/clipboardFilePath.spec.ts | 186 ++++++++++++++++++ packages/muya/src/clipboard/index.ts | 75 ++++++- packages/muya/src/types.ts | 13 ++ .../utils/__tests__/getCopyTextType.spec.ts | 54 ++++- packages/muya/src/utils/paste.ts | 27 ++- 5 files changed, 345 insertions(+), 10 deletions(-) create mode 100644 packages/muya/src/clipboard/__tests__/clipboardFilePath.spec.ts diff --git a/packages/muya/src/clipboard/__tests__/clipboardFilePath.spec.ts b/packages/muya/src/clipboard/__tests__/clipboardFilePath.spec.ts new file mode 100644 index 0000000000..8791e99a19 --- /dev/null +++ b/packages/muya/src/clipboard/__tests__/clipboardFilePath.spec.ts @@ -0,0 +1,186 @@ +import type Content from '../../block/base/content'; +import type { Muya } from '../../muya'; +import { describe, expect, it, vi } from 'vitest'; + +// The clipboard module pulls in CodeBlockContent → utils/prism which touches +// `window` at import time. Stub the prism shim so the test can run under Node +// (same stub as copyHandler.spec / getClipboardData.spec). +vi.mock('../../utils/prism/index', () => ({ + default: {}, + walkTokens: () => null, + loadedLanguages: new Set(), + transformAliasToOrigin: (s: string) => s, + loadLanguage: () => null, + search: () => [], +})); + +// Keep the real `resolveClipboardImagePath` (the decision under test) but neuter +// `normalizePastedHTML`, whose DOMPurify call needs a DOM the default `node` +// test environment doesn't provide. Stubbing it lets the fall-through paste +// path run far enough to prove the image hook short-circuited (or didn't). +vi.mock('../../utils/paste', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + normalizePastedHTML: async (html: string) => html, + }; +}); + +const Clipboard = (await import('../index')).default; + +// Ported behaviour from the legacy `@muyajs` `clipboardFilePath` paste hook: +// when the OS clipboard holds a file (e.g. an image copied from a file +// manager), the embedder resolves it to a local path and muya inserts that +// path as an inline image at the cursor instead of running the normal +// text/HTML paste. Returning '' (or omitting the hook) falls through to the +// default paste. + +// A minimal stand-in for the anchor content block. The clipboardFilePath path +// only touches `text`, `getCursor()` and `setCursor()`; the extra `blockName` +// and `getAnchor()` members keep the fall-through text-paste path from +// crashing in the tests that assert the hook did NOT short-circuit. +function makeAnchorBlock(initialText = '', cursor = 0) { + const block = { + text: initialText, + blockName: 'paragraph.content', + getCursor: () => ({ + start: { offset: cursor }, + end: { offset: cursor }, + }), + setCursor: vi.fn(), + getAnchor: () => null, + }; + return block as unknown as Content & { setCursor: ReturnType }; +} + +function makeClipboard( + options: Record, + anchorBlock: Content, +) { + const clipboard = new Clipboard({ options } as unknown as Muya); + Object.defineProperty(clipboard, 'selection', { + get: () => ({ + getSelection: () => ({ + isSelectionInSameBlock: true, + anchorBlock, + }), + }), + }); + return clipboard; +} + +// A clipboard event whose getData returns '' by default. Pass a map keyed by +// MIME type (e.g. { 'text/plain': 'hi' }) to simulate a clipboard that holds +// real text/HTML, proving the synchronous snapshot survives the async hook. +function makePasteEvent(data: Record = {}) { + const getData = vi.fn((type: string) => data[type] ?? ''); + return { + event: { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + clipboardData: { getData }, + } as unknown as ClipboardEvent, + getData, + }; +} + +describe('clipboard.pasteHandler — clipboardFilePath hook', () => { + it('invokes the hook and inserts the resolved path as an inline image', async () => { + const clipboardFilePath = vi.fn().mockResolvedValue('/tmp/shot.png'); + const anchorBlock = makeAnchorBlock('', 0); + const clipboard = makeClipboard({ clipboardFilePath }, anchorBlock); + const { event, getData } = makePasteEvent(); + + await clipboard.pasteHandler(event); + + expect(clipboardFilePath).toHaveBeenCalledOnce(); + expect(anchorBlock.text).toBe('![](/tmp/shot.png)'); + // Cursor lands right after the inserted image markdown. + expect(anchorBlock.setCursor).toHaveBeenCalledWith(18, 18, true); + // text/html is snapshotted synchronously up front (before the async + // hook detaches the clipboard), but the resolved image still + // short-circuits the normal text/HTML paste so nothing else inserts. + expect(getData).toHaveBeenCalled(); + }); + + it('splices the image into existing text at the cursor offset', async () => { + const clipboardFilePath = vi.fn().mockResolvedValue('/tmp/a.png'); + // Cursor between "ab" and "cd". + const anchorBlock = makeAnchorBlock('abcd', 2); + const clipboard = makeClipboard({ clipboardFilePath }, anchorBlock); + const { event } = makePasteEvent(); + + await clipboard.pasteHandler(event); + + expect(anchorBlock.text).toBe('ab![](/tmp/a.png)cd'); + }); + + it('escapes spaces and # in the resolved path', async () => { + const clipboardFilePath = vi + .fn() + .mockResolvedValue('/tmp/my shot#1.png'); + const anchorBlock = makeAnchorBlock('', 0); + const clipboard = makeClipboard({ clipboardFilePath }, anchorBlock); + const { event } = makePasteEvent(); + + await clipboard.pasteHandler(event); + + expect(anchorBlock.text).toBe('![](/tmp/my%20shot%231.png)'); + }); + + it('falls through to the normal paste when the hook returns ""', async () => { + const clipboardFilePath = vi.fn().mockResolvedValue(''); + const anchorBlock = makeAnchorBlock('', 0); + const clipboard = makeClipboard({ clipboardFilePath }, anchorBlock); + const { event, getData } = makePasteEvent(); + + await clipboard.pasteHandler(event); + + expect(clipboardFilePath).toHaveBeenCalledOnce(); + // No image inserted; the text/HTML branch was reached (getData read). + expect(anchorBlock.text).toBe(''); + expect(getData).toHaveBeenCalled(); + }); + + it('pastes the snapshotted text/plain when the hook is present but returns ""', async () => { + // Regression: the hook is configured, so `pasteHandler` awaits it. The + // snapshot of `event.clipboardData` must be taken synchronously BEFORE + // that await — otherwise the detached DataTransfer would yield '' here + // and the paste would silently insert nothing. + const clipboardFilePath = vi.fn().mockResolvedValue(''); + const anchorBlock = makeAnchorBlock('', 0); + const clipboard = makeClipboard({ clipboardFilePath }, anchorBlock); + const { event, getData } = makePasteEvent({ 'text/plain': 'hello world' }); + + await clipboard.pasteHandler(event); + + expect(clipboardFilePath).toHaveBeenCalledOnce(); + expect(getData).toHaveBeenCalledWith('text/plain'); + // The captured text survived the async hook and was pasted in. + expect(anchorBlock.text).toBe('hello world'); + expect(anchorBlock.setCursor).toHaveBeenCalledWith(11, 11, true); + }); + + it('falls through to the normal paste when the resolved path is not an image', async () => { + const clipboardFilePath = vi.fn().mockResolvedValue('/tmp/notes.txt'); + const anchorBlock = makeAnchorBlock('', 0); + const clipboard = makeClipboard({ clipboardFilePath }, anchorBlock); + const { event, getData } = makePasteEvent(); + + await clipboard.pasteHandler(event); + + expect(anchorBlock.text).toBe(''); + expect(getData).toHaveBeenCalled(); + }); + + it('does nothing special when the hook is absent', async () => { + const anchorBlock = makeAnchorBlock('', 0); + const clipboard = makeClipboard({}, anchorBlock); + const { event, getData } = makePasteEvent(); + + await clipboard.pasteHandler(event); + + expect(anchorBlock.text).toBe(''); + expect(getData).toHaveBeenCalled(); + }); +}); diff --git a/packages/muya/src/clipboard/index.ts b/packages/muya/src/clipboard/index.ts index 99353807e4..5983b16a8e 100644 --- a/packages/muya/src/clipboard/index.ts +++ b/packages/muya/src/clipboard/index.ts @@ -15,7 +15,7 @@ import StateToMarkdown from '../state/stateToMarkdown'; import { isAnyListState, isParagraphState } from '../state/types'; import { deepClone, isClipboardEvent, isKeyboardEvent } from '../utils'; import { getClipBoardHtml } from '../utils/marked'; -import { getCopyTextType, isStandaloneTableHtml, normalizePastedHTML } from '../utils/paste'; +import { getCopyTextType, isStandaloneTableHtml, normalizePastedHTML, resolveClipboardImagePath } from '../utils/paste'; import { mergePasteIntoHeading } from './mergePasteIntoHeading'; class Clipboard { @@ -585,7 +585,19 @@ class Clipboard { } // eslint-disable-next-line complexity - async pasteHandler(event: ClipboardEvent): Promise { + async pasteHandler( + event: ClipboardEvent, + // `event.clipboardData` is only valid synchronously while the paste + // event is being dispatched. Once `pasteHandler` yields at its first + // `await` (the `clipboardFilePath` hook), the browser may detach the + // DataTransfer and subsequent `getData()` calls return ''. We snapshot + // text/html synchronously below and thread the snapshot through the + // `!isSelectionInSameBlock` recursion via these optional params so the + // re-entry doesn't read a detached clipboard. Mirrors the legacy + // `@muyajs` `pasteHandler(event, type, rawText, rawHtml)` signature. + rawText?: string, + rawHtml?: string, + ): Promise { event.preventDefault(); event.stopPropagation(); @@ -604,17 +616,35 @@ class Clipboard { const { isSelectionInSameBlock, anchorBlock } = selection; + if (!anchorBlock || !event.clipboardData) + return; + + // Snapshot everything we need from `event.clipboardData` + // synchronously, BEFORE any `await` — after the first yield the + // DataTransfer can be detached and `getData()` returns ''. On the + // `!isSelectionInSameBlock` recursion we reuse the snapshot captured + // by the outer call rather than re-reading the (now possibly + // detached) clipboard. + const text = rawText ?? event.clipboardData.getData('text/plain'); + let html = rawHtml ?? event.clipboardData.getData('text/html'); + if (!isSelectionInSameBlock) { this.cutHandler(); - return this.pasteHandler(event); + return this.pasteHandler(event, text, html); } - if (!anchorBlock || !event.clipboardData) + // When the OS clipboard holds a file (e.g. an image copied from a + // file manager), let the embedder resolve it to a local path and + // insert it as an inline image, short-circuiting the text/HTML paste. + // Ported from the legacy `@muyajs` `clipboardFilePath` hook. + const imagePath = await resolveClipboardImagePath( + muya.options.clipboardFilePath, + ); + if (imagePath) { + this.insertImagePath(anchorBlock, imagePath); return; - - const text = event.clipboardData.getData('text/plain'); - let html = event.clipboardData.getData('text/html'); + } // Support pasted URLs from Firefox. if (URL_REG.test(text) && !/\s/.test(text) && !html) @@ -740,6 +770,37 @@ class Clipboard { } } + /** + * Insert a resolved clipboard file path as an inline image at the cursor. + * + * Inline images in muya are plain markdown text (`![](src)`) on a content + * block; rendering turns the token into an image. We splice the image + * markdown into the anchor block at the current selection (replacing any + * collapsed/expanded range) and place the cursor after it. The src is + * escaped the same way as {@link Format.replaceImage} so spaces and `#` + * survive in the path. + */ + private insertImagePath(anchorBlock: Content, src: string): void { + const cursor = anchorBlock.getCursor(); + if (!cursor) + return; + + const { start, end } = cursor; + const { text: content } = anchorBlock; + const escapedSrc = src + .replace(/ /g, encodeURI(' ')) + .replace(/#/g, encodeURIComponent('#')); + const imageText = `![](${escapedSrc})`; + + anchorBlock.text + = content.substring(0, start.offset) + + imageText + + content.substring(end.offset); + + const offset = start.offset + imageText.length; + anchorBlock.setCursor(offset, offset, true); + } + copyAsMarkdown() { this.copyType = 'copyAsMarkdown'; document.execCommand('copy'); diff --git a/packages/muya/src/types.ts b/packages/muya/src/types.ts index fd3f628206..d7253d4f52 100644 --- a/packages/muya/src/types.ts +++ b/packages/muya/src/types.ts @@ -37,6 +37,19 @@ export interface IMuyaOptions { }; json?: TState[]; markdown?: string; + /** + * Resolve the OS clipboard to a local file path on paste. + * + * When the user pastes and the system clipboard holds a file (for + * example an image copied from a file manager rather than image bytes), + * the embedder resolves it to an absolute path. If this hook is provided + * and returns a non-empty path with an image extension, muya inserts that + * path as an inline image at the cursor instead of running the default + * text/HTML paste. Return `''` to fall through to the normal paste flow. + * + * Ported from the legacy `@muyajs` `clipboardFilePath` option. + */ + clipboardFilePath?: () => Promise; } export type Nullable = T | null | undefined | void; diff --git a/packages/muya/src/utils/__tests__/getCopyTextType.spec.ts b/packages/muya/src/utils/__tests__/getCopyTextType.spec.ts index 8a78f64ade..b03389db9a 100644 --- a/packages/muya/src/utils/__tests__/getCopyTextType.spec.ts +++ b/packages/muya/src/utils/__tests__/getCopyTextType.spec.ts @@ -1,5 +1,5 @@ -import { describe, expect, it } from 'vitest'; -import { getCopyTextType, isStandaloneTableHtml } from '../paste'; +import { describe, expect, it, vi } from 'vitest'; +import { getCopyTextType, isStandaloneTableHtml, resolveClipboardImagePath } from '../paste'; // Regression for marktext commit 067ec485 (#1271). // Some clipboard sources (e.g. Apple Numbers, certain spreadsheet @@ -69,3 +69,53 @@ describe('getCopyTextType — pre-existing classifier behaviour stays put', () = ).toBe('code'); }); }); + +// Ported from the legacy `@muyajs` `clipboardFilePath` paste hook: on paste, +// the embedder may resolve the OS clipboard to a local file path. Only a +// non-empty, image-extension path should short-circuit the normal text/HTML +// paste and be inserted as an inline image. +describe('resolveClipboardImagePath', () => { + it('returns "" when no hook is provided', async () => { + expect(await resolveClipboardImagePath(undefined)).toBe(''); + }); + + it('returns the path when the hook resolves an image file', async () => { + const hook = vi.fn().mockResolvedValue('/tmp/screenshot.png'); + expect(await resolveClipboardImagePath(hook)).toBe('/tmp/screenshot.png'); + expect(hook).toHaveBeenCalledOnce(); + }); + + it('accepts every supported image extension (case-insensitive)', async () => { + for (const path of [ + '/a/b.JPG', + '/a/b.jpeg', + '/a/b.png', + '/a/b.gif', + '/a/b.svg', + '/a/b.webp', + ]) { + expect(await resolveClipboardImagePath(() => Promise.resolve(path))).toBe( + path, + ); + } + }); + + it('tolerates a query string after the extension', async () => { + expect( + await resolveClipboardImagePath(() => Promise.resolve('/a/b.png?x=1')), + ).toBe('/a/b.png?x=1'); + }); + + it('returns "" when the hook resolves an empty string', async () => { + expect(await resolveClipboardImagePath(() => Promise.resolve(''))).toBe(''); + }); + + it('returns "" when the resolved path is not an image', async () => { + expect( + await resolveClipboardImagePath(() => Promise.resolve('/a/b.txt')), + ).toBe(''); + expect( + await resolveClipboardImagePath(() => Promise.resolve('/a/b.pdf')), + ).toBe(''); + }); +}); diff --git a/packages/muya/src/utils/paste.ts b/packages/muya/src/utils/paste.ts index 2931079e5d..ea3edc9b90 100644 --- a/packages/muya/src/utils/paste.ts +++ b/packages/muya/src/utils/paste.ts @@ -1,4 +1,4 @@ -import { PARAGRAPH_TYPES, PREVIEW_DOMPURIFY_CONFIG } from '../config'; +import { IMAGE_EXT_REG, PARAGRAPH_TYPES, PREVIEW_DOMPURIFY_CONFIG } from '../config'; import { sanitize } from '../utils'; const TIMEOUT = 1500; @@ -126,6 +126,31 @@ export function isStandaloneTableHtml(text: string) { return STANDALONE_TABLE_REG.test(text.trim()); } +/** + * Resolve the `clipboardFilePath` paste hook to a usable inline-image path. + * + * Returns the resolved path only when the hook yields a non-empty string that + * looks like an image file (its extension matches {@link IMAGE_EXT_REG}); + * otherwise returns `''` so the caller falls through to the normal text/HTML + * paste. Ported from the legacy `@muyajs` `pasteImage` guard, which inserted + * the resolved path as an image when it matched the same extension regex. + * + * @param hook the `options.clipboardFilePath` callback, if configured + */ +export async function resolveClipboardImagePath( + hook: (() => Promise) | undefined, +): Promise { + if (typeof hook !== 'function') + return ''; + + const path = await hook(); + + if (typeof path === 'string' && path && IMAGE_EXT_REG.test(path)) + return path; + + return ''; +} + /** * * @param {string} html