Repository navigation
feat(desktop): migrate editor.vue to @muyajs/core engine - #4406
Conversation
Export the TableChessboard UI plugin from the muya core entrypoint so the desktop editor host can register it (the quick-insert table picker). The @muyajs/core workspace dependency and the tsconfig.base.json @muyajs/core paths entry it consumes already landed on develop via #4402. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@muyajs/core exposes MarkdownToHtml(md, muya).generate() but has no equivalent of the legacy muyajs exportStyledHTML (which injects a TOC at the [TOC] marker and wraps the article in a header/footer page table for PDF/print). Add a desktop-side exportStyledHTML wrapper that reproduces that behaviour on top of the engine output so export stays equivalent. Add a hand-written @muyajs/core declaration shim (referenced by the tsconfig paths entry) so vue-tsc resolves the import at the package boundary instead of type-checking the entire muya source tree under the desktop program. Delete it once @muyajs/core ships built type declarations. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR completes the desktop renderer’s migration of the main editor host (editor.vue) from the legacy @marktext/muyajs engine to the TypeScript @muyajs/core engine, updating the editor integration layer, export pipeline, and e2e harness to match the new engine’s DOM/events.
Changes:
- Swaps the editor engine in
editor.vueto@muyajs/core(construction/init, plugin registration, locale wiring, selection/change event adaptation, search payload serialization, and export integration). - Adds a desktop-side
exportStyledHTMLwrapper on top ofMarkdownToHtml.generate()to preserve TOC injection and print/PDF header/footer behavior. - Updates Playwright e2e selectors/recipes from
ag-*tomu-*and adjusts known-gap expectations.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/muya/src/index.ts | Exposes TableChessboard from the @muyajs/core entrypoint for desktop consumption. |
| packages/desktop/src/renderer/src/components/editorWithTabs/editor.vue | Core migration: instantiate/init Muya, register UI plugins, adapt events/payloads, update export/search/scroll behaviors. |
| packages/desktop/src/renderer/src/util/exportHtml.ts | Implements desktop-equivalent styled HTML export (TOC injection + header/footer wrapper) on top of core’s generate(). |
| packages/desktop/src/types/muya-core.d.ts | Expands the desktop-side @muyajs/core type shim (Muya surface, plugins, locales, generate(), utils). |
| packages/desktop/test/e2e/helpers.ts | Updates focus/selection helpers for mu-* DOM and commits selection via synthetic key events. |
| packages/desktop/test/e2e/fixture-render.spec.ts | Updates fixture assertions for core-rendered DOM structure (mu-* classes, link/table rendering differences). |
| packages/desktop/test/e2e/paragraph-blocks.spec.ts | Updates block transform selectors to mu-* equivalents. |
| packages/desktop/test/e2e/crash-update-paragraph.spec.ts | Updates quick-insert trigger/selector strategy for core engine behavior. |
| packages/desktop/test/e2e/crash-range-offset.spec.ts | Updates selectors to match core active-state/paragraph DOM. |
| packages/desktop/test/e2e/issue-4346.spec.ts | Updates paragraph selectors to span.mu-paragraph-content. |
| packages/desktop/test/e2e/issue-4374.spec.ts | Updates paragraph selectors to span.mu-paragraph-content. |
| packages/desktop/test/e2e/plantuml.spec.ts | Updates PlantUML encoding assertion for core’s plantuml-encoder output format. |
| packages/desktop/test/e2e/strong-cjk.spec.ts | Marks the CJK strong-emphasis boundary case as an engine-known gap. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Register the engine UI plugins. The image-edit tool receives the desktop's | ||
| // image callbacks; LinkTools receives the ctrl/cmd-click jump handler. | ||
| Muya.use(TableChessboard) | ||
| Muya.use(ParagraphQuickInsertMenu) | ||
| Muya.use(CodeBlockLanguageSelector) | ||
| Muya.use(EmojiSelector) |
There was a problem hiding this comment.
Good catch — fixed in 2e48ff0. Muya.use(...) appends to the static Muya.plugins array and every init() re-instantiates the full list, so I gated the whole registration block behind a module-level muyaPluginsRegistered flag (registers once per renderer process). The per-plugin option closures (muyaImageAction/jumpClick) only read app-singleton Pinia stores, so capturing them once is correct even across a remount. Verified the quick-insert plugin still works via the #2099 e2e (full suite still 87 passed / 3 skipped).
| // KNOWN GAP (engine #4307): the @muyajs/core markdown parser does not yet | ||
| // recognise strong emphasis when the run is flanked by a CJK char on one side | ||
| // and ASCII punctuation (here the opening quote) on the other. The engine | ||
| // tracks this as a documented `it.fails` in | ||
| // packages/muya/src/state/__tests__/strongCjkFlanking.spec.ts. Until that | ||
| // lands, the desktop renders this as plain text — assert that rather than the | ||
| // wrong (legacy) behaviour, and flip back to `toHaveCount(1)` once the engine | ||
| // fixes #4307. | ||
| test.fixme('CJK + **"x"** renders as bold in WYSIWYG (engine GAP #4307)', async() => { |
There was a problem hiding this comment.
Fixed the comment in 2e48ff0. You're right the old wording was contradictory. The intended strategy is the fixme one: the assertion keeps the DESIRED toHaveCount(1) (bold) expectation and the test is skipped until the engine fixes #4307 — at that point removing .fixme makes it run and pass, so it doubles as a tripwire. I rewrote the comment to say exactly that and to note we intentionally do NOT assert the current plain-text rendering.
|
Build artifacts for PR #4406: Run: https://github.com/marktext/marktext/actions/runs/27141576957
|
Rewire the editor host onto @muyajs/core:
- Construction: build the engine with markRaw(new Muya(el, options)) +
explicit init(); register the new UI plugin set (TableChessboard,
ParagraphQuickInsertMenu, CodeBlockLanguageSelector, EmojiSelector,
ImageEditTool, ImageResizeBar/ToolBar, InlineFormatToolbar,
ParagraphFront*, PreviewToolBar, LinkTools, FootnoteTool, Table*).
- Options/i18n: map the desktop language preference to the engine's
bundled locale objects and pass locale on construct + on
language-changed (editor.locale(...)).
- Events: derive the content-change payload (markdown/wordCount/cursor/
history/TOC/blocks) from the engine's low-level json-change; adapt the
new selection-change shape for the application-menu state builder and
drive format state off the formats it now carries; listen for scroll
on the container directly (engine has no scroll event).
- Methods: setContent (replacing setMarkdown), getScrollContainer via
domNode, viewport-relative caret coords for typewriter/keep-visible
scrolling, copy/paste method remap (copyAsRich -> copyAsHtml), per-tab
engine-history stash for in-session tab switches with a synthetic
desktop-shaped history for save/dirty tracking.
- Search circular-ref fix: muya's search/replace/find return the live
Search instance (circular: Search -> muya -> ... -> ScrollPage) and
each match carries a live block ref. The store deep-clones (JSON) its
payload, which crashed; toSearchMatches() strips it to the plain
{ index, matches, value } the search UI needs before dispatching.
- Blur the editor when the command palette opens to avoid a focus-trap
vs engine-selection deadlock that froze the renderer.
Documented gaps (engine does not yet surface these; editor stays fully
functional): heading-copy-link hover affordance, selection affiliation
chain for block-context menu toggles, and selectedTableCells in
select-all.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Migrate the desktop e2e suite to the @muyajs/core renderer: - Swap ag-* DOM selectors for the engine's mu-* equivalents across the fixture-render, crash, issue-regression, paragraph-blocks and strong-cjk specs, and update helpers.ts (paragraph content lives in span.mu-paragraph-content; selection is committed via a synthetic keyup so the engine updates its active block). - crash-update-paragraph #2099: the quick-insert menu now triggers on '/' (not the legacy '@'); type '/', wait for the float to be shown (it is always attached but parked off-screen until positioned), then pick Header 1. - plantuml: the engine encodes via plantuml-encoder (no '~1' deflate prefix), so assert the bare plantuml-alphabet base64 src. - strong-cjk: mark the CJK + **"x"** flanking case as fixme — a documented @muyajs/core parser gap (#4307, tracked as it.fails in packages/muya/src/state/__tests__/strongCjkFlanking.spec.ts); the CJK + **plain** regression still passes. Full desktop e2e: 87 passed, 3 skipped (all documented gaps), 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
876c4e5 to
2e48ff0
Compare
…aps (#4407) * test(parity): muya engine xfail tests for #4406 gaps PG1,3,5,6,7,8,9,10,11,12,13 Encode 10 of the 15 confirmed muyajs→@muyajs/core functional-parity gaps as vitest `it.fails` regression tests in the engine. Each asserts the desired pre-migration behaviour and fails on develop today (counted as a pass by `it.fails`); when a fix lands the test passes and `it.fails` errors, forcing the fixer to drop the marker. Every test name is prefixed with its gap id (`PGn:`) so fix PRs can grep + flip it. - PG1 selection-change lacks block affiliation / ancestor type - PG3 autoCheck preference not consumed (task-list cascade lost) - PG5 binary/bitmap clipboard image paste not persisted via imageAction - PG6 pasted image FILE bypasses imageAction (insert preference ignored) - PG7 export links core CSS from CDN instead of inlining it - PG8 exported headings carry no slug id (dead TOC anchors) - PG9 copyAsRich writes HTML source not rich text (no copyAsRich path) - PG10 preview-image not emitted (Space on a selected image) - PG11 heading-copy-link not emitted (hover-to-copy affordance gone) - PG12 hideLinkPopup not consumed (link hover popover not gated) - PG13 insertParagraph anchors to outermost not immediate nested block Allow the uppercase `PG` test-title prefix through antfu's `test/prefer-lowercase-title` rule (scoped to spec files) so the greppable gap-id naming coexists with the lint rule; all other test titles still must start lowercase. Suite stays GREEN: 512 pass + 20 expected-fail. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * test(parity): desktop e2e xfail tests for #4406 gaps PG1,2,14,15 Add Playwright `test.fail()` regression specs for the desktop-side parity gaps. Each runs headless and currently fails (the gap), which `test.fail()` counts as a pass; removing `test.fail()` flips the entry green once fixed. Test names are prefixed with the gap id (`PGn:`) for grep-and-flip. - PG1 Paragraph menu does not check the current block type (affiliation gap; caret placed directly in an H1 content span — the engine-unit half lives in paritySelectionChange.spec.ts) - PG2 WYSIWYG caret not restored after a source-mode edit (handleFileChange drops muyaIndexCursor; no index→path cursor conversion in the engine) - PG14 first undo after exiting source mode does not revert the bulk source-mode change as one step - PG15 undo back to on-disk content leaves the tab marked unsaved (synthetic history id never re-matches the saved id) Undo is driven through the same `mt::editor-edit-action` IPC the Edit › Undo menu uses. Suite stays GREEN (4 expected-fail). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * docs(parity): scoreboard index + manual-QA checklist for #4406 gaps Add the visible "how many gaps remain" board (PARITY_SCOREBOARD.md): a table of all 15 muyajs→@muyajs/core parity gaps → severity → test location(s) → status (all xfail now), plus the flip-to-green workflow for fix PRs. Add PARITY_QA.md: precise manual-QA checklists (exact steps + expected vs current result) for the two gaps that cannot be driven headless — PG4 drag-drop image insertion and the OS-clipboard/screenshot half of PG5 binary image paste. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * test(parity): destroy() muya instances in afterEach to detach DOM listeners Address Copilot review: the parity engine specs' cleanup only removed the host DOM node, never calling `muya.destroy()`, so the `document`-level keydown/click listeners selection registers during init leaked across tests — a flake / order- dependence risk as the suite grows. Track booted Muya instances and `destroy()` each in afterEach (detaches all DOM events + removes the node). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
* fix(muya): cascade task-list checkbox state when autoCheck is on (PG3) The autoCheck option was merged into muya.options (muya.ts setOptions) but no engine code consumed it, so the legacy muyajs task-list cascade was lost (parity gap PG3 of #4406). Restore it in the task-list checkbox handler: a user toggle now sets every descendant task item to the same checked state and re-derives each ancestor (checked iff all its siblings are checked), gated on muya.options.autoCheck. Cascaded items mutate state through the TaskListItem.checked setter so each change still dispatches its OT replace op; their checkboxes sync via syncDom. Flips the parityAutoCheck scoreboard specs from it.fails to it. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(muya): gate link hover popover on hideLinkPopup (PG12) The hideLinkPopup option was merged into muya.options but the link-hover emitter (linkMouseEvents.ts#overHandler) emitted muya-link-tools unconditionally, so the popover always appeared on hover even when the user set hideLinkPopup: true (parity gap PG12 of #4406). Read muya.options.hideLinkPopup live in overHandler and return early when set, mirroring legacy muyajs eventHandler/mouseEvent.js. Optional chaining keeps the existing options-less test mock working. Flips the parityHideLinkPopup scoreboard spec from it.fails to it; the positive control stays green. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
The @muyajs/core rewrite (#4406) shipped without any DnD handler, so dropping an image into the document was a no-op — parity gap PG4. Port the legacy muyajs dragDrop/dragDropCtrl behaviour as an engine-level, embedder-agnostic handler. `attachDragDropImageHandlers(muya)` binds dragstart/dragover/drop on the editor container (and dragleave for the ghost) via `eventCenter.attachDOMEvent`, so cleanup rides on `muya.destroy() → detachAllDomEvents()`. It is wired in `Editor.init()` alongside `attachLinkMouseHandlers`. Two drop paths mirror the legacy controller: - web-link image (`text/uri-list`): verify it is an image (extension or content-type sniff) then insert ``. - local image FILE (`dataTransfer.files`): resolve the file to a path via the new embedder `getPathForFile` hook, insert a `` placeholder, persist it through the new `imageAction` option (the same `{ src, alt, title }` contract the imageEditTool plugin consumes), then swap in the returned src. Two optional `IMuyaOptions` hooks are added (mirroring the existing `clipboardFilePath`): `imageAction` and `getPathForFile`. The engine stays free of `window.electron`; the desktop wires these in wave 2. Tested in `dragDropImage.spec.ts`: happy-dom provides a fully working `DataTransfer` (items.add / getAsString / files) and fires getAsString synchronously, so a synthetic `drop` event drives the real handler end-to-end — asserting both drop paths and the no-op-off-target case. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* feat(muya): restore drag-and-drop image insertion (PG4) The @muyajs/core rewrite (#4406) shipped without any DnD handler, so dropping an image into the document was a no-op — parity gap PG4. Port the legacy muyajs dragDrop/dragDropCtrl behaviour as an engine-level, embedder-agnostic handler. `attachDragDropImageHandlers(muya)` binds dragstart/dragover/drop on the editor container (and dragleave for the ghost) via `eventCenter.attachDOMEvent`, so cleanup rides on `muya.destroy() → detachAllDomEvents()`. It is wired in `Editor.init()` alongside `attachLinkMouseHandlers`. Two drop paths mirror the legacy controller: - web-link image (`text/uri-list`): verify it is an image (extension or content-type sniff) then insert ``. - local image FILE (`dataTransfer.files`): resolve the file to a path via the new embedder `getPathForFile` hook, insert a `` placeholder, persist it through the new `imageAction` option (the same `{ src, alt, title }` contract the imageEditTool plugin consumes), then swap in the returned src. Two optional `IMuyaOptions` hooks are added (mirroring the existing `clipboardFilePath`): `imageAction` and `getPathForFile`. The engine stays free of `window.electron`; the desktop wires these in wave 2. Tested in `dragDropImage.spec.ts`: happy-dom provides a fully working `DataTransfer` (items.add / getAsString / files) and fires getAsString synchronously, so a synthetic `drop` event drives the real handler end-to-end — asserting both drop paths and the no-op-off-target case. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * docs(parity): mark PG4 engine half automated, note wave-2 desktop wiring PG4 (drag-drop image insertion) was a manual-QA-only entry because real drag gestures are hard headless. The engine handler now has a synthetic- DataTransfer unit test, so update the scoreboard and QA checklist: - PARITY_QA.md § PG4: describe what is now automated (both drop paths via the live handler), keep the OS-integration steps manual, and document the desktop wave-2 wiring needed for the local-file persistence path (pass `imageAction` / `getPathForFile` into the Muya constructor). - PARITY_SCOREBOARD.md: point PG4 at the new spec, flag the engine half fixed, and drop the remaining-gaps count 15 → 14. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(muya): address PG4 drag-drop review (ghost CSS, no-imageAction, web-image gating) Copilot review on #4413: - Add the `#mu-dragover-ghost` CSS rule (position/height/background) to `assets/styles/index.css`. The new engine had no ghost style, so the drop indicator was invisible — legacy muyajs shipped this rule. - When no `imageAction` hook is configured, insert a clean `` with the raw path verbatim instead of a `` placeholder that would never be swapped (it persists only when imageAction resolves). Matches the documented `imageAction` contract and imageEditTool's direct-replacement behaviour. - Gate the web-link path on the legacy "image dragged from a browser" signature — `text/uri-list` + `text/html` and NO `text/plain` — in both `dragover` and `drop`. A plain hyperlink drag (uri-list + text/plain) is now left to the browser instead of being intercepted and swallowed by `preventDefault()`. Tests updated: web-link drags use the realistic (uri-list + html) payload, plus new cases for the no-imageAction clean insert and the plain-hyperlink pass-through. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
… heading-link, source cursor, saved indicator) (#4415) * fix(desktop): map copyAsRich to the engine copyAsRich method (PG9) The legacy "Copy as Rich Text" command was remapped to copyAsHtml, which blanks text/html and puts the HTML source into text/plain, so pasting into Word/email yielded raw HTML markup as literal text. #4411 added a real Muya.copyAsRich() that writes rendered HTML to text/html and plain text to text/plain; point COPY_PASTE_METHOD_MAP.copyAsRich at it. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * feat(desktop): wire heading-copy-link to copyGithubSlug (PG11) #4414 made the engine attach a hover-to-copy affordance to every heading and emit heading-copy-link { key } (key == the heading's stable slug) on click. Re-subscribe in editor.vue and forward the key to editorStore.copyGithubSlug, which copies `#<githubSlug>` to the clipboard — restoring the heading-anchor copy affordance that was a documented gap after the @muyajs/core migration. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(desktop): match exported-TOC anchors to engine heading ids (PG8) editor.vue now exports via @muyajs/core (#4406), and #4412 injects github-compatible heading ids onto exported headings (deduped in document order with a `-N` suffix). getHtmlToc still slugged via the legacy muyajs Slugger, so `href="#slug"` targets no longer matched the injected ids and TOC / [TOC] links were dead. Swap to @muyajs/core's generateGithubSlug and replicate the engine's whole-document `-N` dedup so the anchors resolve. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(desktop): consume selection-change affiliation for menu state (PG1) adaptSelectionChange hardcoded affiliation:[] and copied changes.type ('Caret'/'Range') into start.type/end.type, so createApplicationMenuState's `start.type === 'span'` and functionType guards never fired — the native Paragraph-menu check marks, loose/task-list toggles, table/code-fence detection, and Format-disable-in-code all went dead after the @muyajs/core migration. #4410 added an `affiliation` chain (outermost-first) plus per-endpoint `anchorBlockInfo`/`focusBlockInfo` (`type: 'span'` + `functionType`) to the selection-change payload. Map them onto the legacy shape: - start/end `.type` and `.block.functionType` from the leaf block info, - affiliation passed through, with a derived `functionType` surfaced on `pre`/`figure` containers so table / code-fence detection lights up. Also fix the consumer's loose-list read: the engine affiliation entry carries `isLooseListItem` on the list block directly, not via a `children` chain. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(desktop): restore saved indicator on undo-to-disk (PG15) makeSyntheticHistory minted an ever-incrementing editSeq id on every json-change (including undo/redo), so after edit-then-undo-to-saved the id never matched lastSavedHistoryId and the tab stayed marked dirty even when its content matched disk. Derive the synthetic id from the engine undo-stack DEPTH instead — a stable position marker that returns to its saved value when an edit is undone back to the baseline. Seed lastSavedHistoryId to 0 (the engine's post-setContent baseline depth, since setContent clears history) so a freshly-loaded, never-saved document clears its dirty indicator when undone back to disk content, mirroring the legacy history-index behaviour. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * feat(muya): add setCursorByOffset for source-mode cursor restore (PG2) The source-code -> WYSIWYG handoff carries only a CodeMirror {line, ch} index cursor; @muyajs/core had no index-offset -> block-path conversion, so the WYSIWYG caret was lost. Add Muya#setCursorByOffset, reproducing the legacy muyajs approach: inject sentinel strings into the current markdown at the line/ch offsets, rebuild the tree (sentinels embed as literal text), find the content blocks they landed in, then rebuild the clean document and set the cursor by the resolved block paths + offsets. Both setContent calls run synchronously so no intermediate paint occurs, and the method is a no-op for stale/unresolvable cursors. Engine helper only — desktop wiring lands in a separate commit. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(desktop): restore WYSIWYG caret after source-mode edit (PG2) handleFileChange dropped the saved muyaIndexCursor on the source-code -> WYSIWYG handoff, so the caret was lost. Consume the new engine Muya#setCursorByOffset: when the tab has no key-based cursor but carries a CodeMirror {line, ch} index cursor, map it onto a block-key cursor so the caret lands where the source-mode cursor was. Restore the per-tab engine history afterwards (setCursorByOffset re-runs setContent internally, which clears history). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * test(desktop): flip parity e2e for PG1/PG2/PG15, defer PG14 Remove the `test.fail()` markers on the PG1 (Paragraph-menu check mark), PG2 (source-mode caret restore), and PG15 (saved indicator on undo-to-disk) parity e2e tests now that the desktop wiring lands — all three genuinely pass. PG14 (first undo after source mode reverts the bulk edit in one step) stays `test.fail()`: recording the source-mode change as a single undo boundary needs a general whole-document json1 diff through Editor.updateContents' pick/drop walker, which only handles specific op shapes and would risk corrupting the document. Deferred with an explanatory note here and in handleFileChange. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * docs(test): reconcile parity scoreboard after wave-2 (14/15 fixed) Update the Status column to the true post-merge state: all seven Wave-1 engine PRs (#4408-#4414) plus the Wave-2 desktop wiring close PG1-PG13 and PG15. Fix the "Gaps remaining" count (11 -> 1), add the PG2 setCursorByOffset engine spec, and document why PG14 is accept-deferred (single-undo-boundary across the source-mode handoff needs a whole-document json1 diff the op walker can't safely apply). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix: address Copilot review on parity wave-2 - editor.vue adaptSelectionChange: restore start/end block.text from the live anchorBlock/focusBlock so SELECTION_CHANGE can still slice the selected text (search prefill); the previous {functionType}-only block dropped it. - editor.vue isIndexCursor: validate both line AND ch are numbers (factored out isIndexPosition) so a missing ch no longer silently clamps to column 0. - muya setCursorByOffset: snapshot getHistory() and restore it after the internal setContent rebuild so the public API is caret-only and does not clear the undo stack; document the behaviour and cover it with a test. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…or (#4417) The #4406 editor.vue rewrite dropped the constructor-level `imageAction` (and the new `getPathForFile`) options. The engine's clipboard and drag-drop handlers read these from `muya.options.*` (clipboard/index.ts:825, editor/dragDropImage.ts:168/215/221) — distinct from the ImageEditTool plugin option. Without them, three already-merged engine image fixes were silently inert on the desktop: - PG4: local-file drag-drop image insertion - PG5: screenshot / binary clipboard paste persistence - PG6: copy-to-assets / upload preference on a pasted image file (raw path inserted) Wire both into the constructor options (web-link drag-drop already worked). The actual gesture paths remain manual-QA — headless e2e cannot drive them, which is exactly why this slipped past the scoreboard. Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
…oll buffer After the @muyajs/core migration (#4406) the legacy `#ag-editor-id` padding no longer matched the new `.mu-container`, so the first block sat ~20px too close to the top and the document lost its 100vh bottom scroll buffer. Restore both on `.editor-component .mu-container`. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* fix(desktop): restore editor first-paragraph top spacing + bottom scroll buffer After the @muyajs/core migration (#4406) the legacy `#ag-editor-id` padding no longer matched the new `.mu-container`, so the first block sat ~20px too close to the top and the document lost its 100vh bottom scroll buffer. Restore both on `.editor-component .mu-container`. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(muya): render toolbar/copy icons by clipping icon boxes with overflow:hidden The drop-shadow icon technique shifts the source PNG off-screen and projects a recoloured silhouette back into view. `overflow: auto` on the icon box let the host app's global `::-webkit-scrollbar` styling force a classic, space-taking scrollbar that filled the ~14px box (rendering as a gray square) and hid the table-column toolbar, code/math copy and several other icons. The muya examples were unaffected because they don't style scrollbars (macOS overlay scrollbars take no space). Switch every icon `i.icon` box to `overflow: hidden`, matching the already -correct inline-format toolbar. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * fix(muya): restore marktext block-level content styling across themes Match the legacy muyajs look on the new `.mu-*` DOM: - paragraph/block spacing back to collapsing `margin: 0.5em 0` (was non-collapsing `padding: 0.5em 0`, which doubled the gap between blocks) - headings to `margin: 1rem 0` - divider (thematic break) to 2px dashed, vertically centred - ordered/bullet/task list indent to 30px - blockquote to `0 30px` padding + 2px bar at left:15px, full height - task-list checked marker to `--theme-color` + selection glow; loose checkbox top back to 0.3em; drop the engine's extra `li padding-left` Colours flow through the existing per-theme camelCase->kebab variable bridge, so this applies uniformly across all themes. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Summary
Finalizes the migration of the MarkText desktop editor host (
editor.vue) from the legacy@marktext/muyajsengine to the TypeScript@muyajs/coreengine. Picks up the prior (incomplete) WIP, makes it fully functional, and verifies it against the real Electron app via the full Playwright e2e suite.Related: continues the
@muyajs/coreadoption started in #4402 (util-file consumption) and #4404 (themeable CSS vars).What changed
Engine swap (
editor.vue)markRaw(new Muya(el, options))+ explicitinit(); registers the new UI plugin set (TableChessboard,ParagraphQuickInsertMenu,CodeBlockLanguageSelector,EmojiSelector,ImageEditTool,ImageResizeBar/ImageToolBar,InlineFormatToolbar,ParagraphFront*,PreviewToolBar,LinkTools,FootnoteTool,Table*).markRawprevents Vue from proxying the engine's live DOM/block references.languagepreference to the engine's bundled locale objects (en/de/es/fr/ja/ko/pt/zhCN/zhTW); passeslocaleon construct and re-applies viaeditor.locale(...)onlanguage-changed.json-change; adapts the newselection-changeshape for the application-menu state builder and drives inline-format menu state off theformatsit now carries; listens forscrollon the container directly (the engine has noscrollevent).setContent(replacingsetMarkdown),getScrollContainerviadomNode, viewport-relative caret coords for typewriter / keep-cursor-visible scrolling, copy/paste remap (copyAsRich→copyAsHtml), and a per-tab engine-history stash for in-session tab switches (with a synthetic desktop-shaped history feeding the store's save/dirty tracking, since the engine history shape is incompatible).Styled-HTML export wrapper + types shim
src/renderer/src/util/exportHtml.ts: reproduces the legacyexportStyledHTMLbehaviour (TOC injection at[TOC], header/footer page-container table for PDF/print) on top ofMarkdownToHtml(md, muya).generate(), which the new engine has no direct equivalent for.src/types/muya-core.d.ts: expands the desktop-side@muyajs/coredeclaration shim (added in feat(desktop): consume @muyajs/core in util files (markdownToHtml/pdf/dompurify/printService/sourceCode/icon) #4402) with the editor instance surface, UI plugins, locales andgenerate()sovue-tscresolves the import at the package boundary without type-checking the whole muya tree.packages/muya/src/index.ts: exportsTableChessboardfrom the engine entrypoint (consumed as the quick-insert table picker).Search circular-ref fix
muya's
search/replace/findreturn the liveSearchinstance (circular:Search → muya → … → ScrollPage), and each match carries a liveblockreference. The editor store deep-clones (JSON.stringify) its payload, which crashed on the circular structure / threw on the live refs.toSearchMatches()strips the result to the plain{ index, matches, value }the search UI needs before dispatching. This is the crash that previously fired on thedocKeyup → emptySearchpath on every keypress.e2e selector + recipe updates (
ag-*→mu-*)ag-*DOM selectors for the engine'smu-*equivalents across fixture-render / crash / issue-regression / paragraph-blocks / strong-cjk specs andhelpers.ts(paragraph content isspan.mu-paragraph-content; selection is committed via a synthetickeyupso the engine updates its active block)./(not the legacy@). The test types/, waits for the float to be shown (it is always attached but parked off-screen attop:-9999until positioned), then picks Header 1.plantuml-encoder(no~1deflate prefix), so the spec asserts the bare plantuml-alphabet base64 src.Verification
Full desktop e2e — every spec passes:
launch,editor-input,inline-format,find-replace,paragraph-blocks,fixture-render(blockquote/link-image/gfm/frontmatter/math/formatted/nested-mixed-lists),crash-range-offset,crash-selection-change,crash-update-paragraph,command-palette,context-isolation,tabs,themes,view-modes,layout-toggles,menu-sanity,ripgrep-search,source-math,plantuml,issue-4346,issue-4374,issue-2800,xss,strong-cjk.A scripted sanity run (type text → select-all → Cmd/Ctrl+B → list context → keypress storm) produced zero console, page, and renderer-process errors — confirming the previously-crashing keyup/search path is fixed.
Documented gaps / follow-ups
These are engine-level or harness limitations carried forward (the editor stays fully functional):
strong-cjkCJK + **"x"**is markedfixme; the@muyajs/coreparser doesn't yet handle strong emphasis flanked by a CJK char on one side and ASCII punctuation on the other. Tracked engine-side as anit.failsinpackages/muya/src/state/__tests__/strongCjkFlanking.spec.ts. TheCJK + **plain**regression still passes.paragraph-blocks) —test.skippre-existing on develop; needs a live cursor in an empty paragraph that the xvfb harness can't reliably drive. Menu-id smoke coverage lives inmenu-sanity.heading-copy-link— the engine doesn't emit this event yet, so the hover-to-copy-heading-anchor affordance is unavailable; TOC navigation andcopyGithubSlug(via command/menu) still work.affiliationchain — the newselection-changepayload doesn't surface the rich block affiliation chain, so block-context menu toggles (list/table awareness) degrade to empty; inline-format toggles still work viaformats.🤖 Generated with Claude Code