[pull] develop from marktext:develop - #47
Merged
Merged
Conversation
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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, <fallback>)`, 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) <noreply@anthropic.com> * 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 <svg> 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`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) <noreply@anthropic.com>
) * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…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 <img>, emit `format-click` with { event, formatType: 'image', data: <src> } (src resolved via getImageSrc, falling back to the <img> 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dentation (#4393) * 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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  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) <noreply@anthropic.com>
* 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) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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<string>` 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 `` 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )