feat(muya): add clipboardFilePath paste hook by Jocs · Pull Request #4398 · marktext/marktext · GitHub
Skip to content

feat(muya): add clipboardFilePath paste hook - #4398

Merged
Jocs merged 2 commits into
developfrom
feat/muya-clipboard-file-path
Jun 8, 2026
Merged

Jocs merged 2 commits into
developfrom
feat/muya-clipboard-file-path

Conversation

@Jocs

@Jocs Jocs commented Jun 8, 2026

Copy link
Copy Markdown
Member

Why

When the user pastes and the OS clipboard holds a file (e.g. an image copied in a file manager rather than image bytes), the desktop app resolves it to a path and inserts an image. The legacy packages/muyajs engine accepted a clipboardFilePath option (() => Promise<string> returning the file path or '') for exactly this. @muyajs/core (packages/muya) had no such hook, which blocks the desktop migration off the legacy engine.

This ports that hook to @muyajs/core.

What

  • IMuyaOptions: add optional clipboardFilePath?: () => Promise<string>.
  • utils/paste.ts: add a pure, testable resolveClipboardImagePath(hook) helper. It calls the hook and returns the resolved path only when it is a non-empty string whose extension matches IMAGE_EXT_REG — mirroring the legacy pasteImage guard. Otherwise returns ''.
  • clipboard/index.ts (pasteHandler): right after the selection/clipboard-data guard (matching the legacy order where the image path is checked first), consult the hook. If it yields an image path, splice ![](src) into the anchor block at the cursor (src escaped the same way as Format.replaceImage, so spaces and # survive) and return early. Inline images in muya are markdown text on a content block, so this renders as an inline image.

Behavior

  • Hook absent, or returns '', or returns a non-image path → unchanged: the normal text/HTML paste runs exactly as before.
  • Hook returns an image path (.png/.jpg/.jpeg/.gif/.svg/.webp, case-insensitive, query string tolerated) → that path is inserted as an inline image at the cursor, short-circuiting the text/HTML paste.

src/muya.ts is intentionally untouched.

Tests

  • utils/__tests__/getCopyTextType.spec.ts: unit-covers resolveClipboardImagePath (no hook, image path, every supported extension, query string, empty string, non-image).
  • clipboard/__tests__/clipboardFilePath.spec.ts (new): drives pasteHandler end to end with a mocked clipboardFilePath — asserts the hook is invoked, the resolved path is inserted as ![](src) at the cursor, src escaping, the short-circuit (text/HTML getData never read), and fall-through when the hook returns '' / a non-image / is absent. normalizePastedHTML is partially mocked in the fall-through cases since its DOMPurify call needs a DOM the default node test env doesn't provide.

Verify (all green)

Gate Result
lint 0 errors (27 pre-existing warnings in unrelated files)
lint:types pass
check-circular no circular deps
test 453 passed
test:spec 1347 passed

🤖 Generated with Claude Code

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 `![](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) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 8, 2026 07:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR ports the legacy clipboardFilePath paste hook into @muyajs/core (packages/muya) so embedders (e.g., the desktop app) can resolve “clipboard contains a file” pastes to a local image path and insert it as an inline markdown image.

Changes:

  • Adds clipboardFilePath?: () => Promise<string> to IMuyaOptions.
  • Introduces resolveClipboardImagePath() helper (pure + unit-tested) to validate the hook output against IMAGE_EXT_REG.
  • Integrates the hook into Clipboard.pasteHandler to insert ![](<escaped-src>) and short-circuit the normal paste flow, plus adds end-to-end tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/muya/src/utils/paste.ts Adds resolveClipboardImagePath() helper used to validate hook output as an image path.
packages/muya/src/utils/tests/getCopyTextType.spec.ts Adds unit coverage for resolveClipboardImagePath().
packages/muya/src/types.ts Extends IMuyaOptions with the optional clipboardFilePath hook and docs.
packages/muya/src/clipboard/index.ts Calls the hook during paste and inserts an inline image markdown string when it resolves to an image path.
packages/muya/src/clipboard/tests/clipboardFilePath.spec.ts Adds end-to-end pasteHandler tests for the new hook behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/muya/src/clipboard/index.ts Outdated
Comment on lines 616 to 630

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in afa0d31. pasteHandler now snapshots text/plain and text/html from event.clipboardData synchronously at the top, before any await. The clipboardFilePath hook resolution (the first await) now happens only after that snapshot, so the DataTransfer is never read once it may have been detached. The captured values are threaded through the !isSelectionInSameBlock recursion via optional rawText/rawHtml params (mirroring the legacy @muyajs pasteHandler(event, type, rawText, rawHtml) signature), so the re-entry doesn't re-read a possibly-detached clipboard either. Behaviour is unchanged: the file-path hook still takes precedence and inserts an image, and normal text/HTML paste still works when the hook is absent or resolves to ''/non-image. Added a regression test ('pastes the snapshotted text/plain when the hook is present but returns ""') that fails on the old ordering, and updated the precedence test to reflect that clipboardData is now read synchronously up front.

`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>
@Jocs
Jocs merged commit efb720b into develop Jun 8, 2026
9 checks passed
@Jocs
Jocs deleted the feat/muya-clipboard-file-path branch June 8, 2026 08:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants