Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
fix(rich-markdown-editor): strict, provenance-aware markdown paste#5463
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Uh oh!
There was an error while loading. Please reload this page.
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
c92eb17
fix(rich-markdown-editor): strict, provenance-aware markdown paste
waleedlatif1 4bf7547
test(rich-markdown-editor): assert code-like pastes are claimed but p…
waleedlatif1 aefb444
fix(rich-markdown-editor): keep paste literal inside inline code too
waleedlatif1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Jump to file
Failed to load files.
Loading
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
42 changes: 35 additions & 7 deletions
42 ...ce/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-paste.test.ts
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
60 changes: 41 additions & 19 deletions
60 ...rkspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/markdown-paste.ts
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,11 +3,12 @@ import { Plugin } from '@tiptap/pm/state' | ||
| import { parseMarkdownToDoc } from './markdown-parse' | ||
| /** | ||
| * Markdown syntax hints. If pasted plain text matches any of these, it's parsed as markdown rather | ||
| * than inserted literally — so a pasted link, image, badge, list, or heading renders as rich content | ||
| * instead of showing its raw `[text](url)` / `# ` source. | ||
| * Structural markdown — strong signals the plain text is genuinely markdown (a link, image, badge, | ||
| * list, heading, blockquote, fenced block, or GFM table). Our parser round-trips these more faithfully | ||
| * than generic HTML→DOM mapping (GFM alignment, escaping, the `./raw-markdown-snippet.ts` constructs), | ||
| * so they are parsed even when the clipboard also carries an HTML sibling. | ||
| */ | ||
| const MARKDOWN_HINTS: ReadonlyArray<RegExp> = [ | ||
| const STRUCTURAL_MARKDOWN_HINTS: ReadonlyArray<RegExp> = [ | ||
| /^#{1,6}\s/m, | ||
| /\*\*[^*]+\*\*/, | ||
| /\[[^\]]*]\([^)]+\)/, | ||
| @@ -18,21 +19,40 @@ const MARKDOWN_HINTS: ReadonlyArray<RegExp> = [ | ||
| /^\|.*\|.*\|/m, | ||
| ] | ||
| function looksLikeMarkdown(text: string): boolean { | ||
| return MARKDOWN_HINTS.some((hint) => hint.test(text)) | ||
| /** | ||
| * Inline marks — weaker markdown signals (`*italic*` / `_italic_`, `~~strike~~`, `` `code` ``) that a | ||
| * rich HTML sibling encodes just as well. Parsed for a plain-text-only paste (so markdown copied from a | ||
| * terminal or `.md` source renders), but deferred to an HTML sibling: its presence means the source was | ||
| * rich, and it may carry structure the plain text can't (a copied table's plain form is tab-separated, | ||
| * not a `| … |` grid, so parsing it would flatten the table). | ||
| */ | ||
| const INLINE_MARK_HINTS: ReadonlyArray<RegExp> = [ | ||
| /\*[^*\n]+\*/, | ||
| /_[^_\n]+_/, | ||
| /~~[^~\n]+~~/, | ||
| /`[^`\n]+`/, | ||
| ] | ||
| function hasAny(hints: ReadonlyArray<RegExp>, text: string): boolean { | ||
| return hints.some((hint) => hint.test(text)) | ||
| } | ||
| /** | ||
| * Parses pasted plain text that looks like markdown into rich content. Pastes inside a code block | ||
| * are left untouched (code is meant to stay literal). | ||
| * Parses pasted plain text that looks like markdown into rich content, via the strict CommonMark | ||
| * parser ({@link parseMarkdownToDoc}, `marked`). Pastes inside a code block or inline code are left | ||
| * untouched (code is meant to stay literal). | ||
| * | ||
| * Provenance decides plain-text-vs-HTML: a `text/html` sibling (copied from a browser, Slack, Notion, | ||
| * GitHub, or this editor) is the signal the source was rich. Structural markdown is still parsed from | ||
| * the plain-text sibling regardless — our parser is more faithful for GFM tables and escaping. But | ||
| * inline-only marks are equally expressible in HTML, so when a rich sibling is present we defer to the | ||
| * DOM path, which preserves structure the plain text can't encode. A plain-text-only clipboard (a | ||
| * terminal, a code editor, a `.md` file) always parses. | ||
| * | ||
| * A clipboard entry that also carries `text/html` (copied from a browser, Slack, Notion, GitHub, | ||
| * or this editor itself) used to always defer entirely to ProseMirror's generic HTML→DOM mapping, | ||
| * even when the `text/plain` sibling was clean markdown our own parser round-trips more faithfully | ||
| * (GFM table alignment, escaping, the constructs `./raw-markdown-snippet.ts` now preserves). Only | ||
| * defer to DOM mapping when the plain-text sibling *doesn't* look like markdown — an HTML clipboard | ||
| * payload with no markdown-shaped plain-text counterpart (a genuinely rich paste from a word | ||
| * processor, a web page selection, …) still goes through the DOM path unchanged. | ||
| * The strictness of the parse matters: `marked` follows CommonMark flanking rules, so `*text*` becomes | ||
| * emphasis but a space-flanked `5 * width * height` stays literal. The editor sets `enablePasteRules: | ||
| * false` so StarterKit's lenient mark paste rules (which would mangle that expression on either path) | ||
| * never run — emphasis is owned by this parser on the plain path and by real HTML tags on the DOM path. | ||
| */ | ||
| export const MarkdownPaste = Extension.create({ | ||
| name: 'markdownPaste', | ||
| @@ -44,11 +64,13 @@ export const MarkdownPaste = Extension.create({ | ||
| props: { | ||
| handlePaste: (_view, event) => { | ||
| if (!editor.isEditable) return false | ||
| if (editor.isActive('codeBlock')) return false | ||
| if (editor.isActive('codeBlock') || editor.isActive('code')) return false | ||
| const text = event.clipboardData?.getData('text/plain') | ||
| if (!text || !looksLikeMarkdown(text)) return false | ||
| // Parse through the chunker (linear) so pasting a large markdown blob can't freeze the | ||
| // main thread the way the underlying superlinear parse would. | ||
| if (!text) return false | ||
| if (!hasAny(STRUCTURAL_MARKDOWN_HINTS, text)) { | ||
| if (!hasAny(INLINE_MARK_HINTS, text)) return false | ||
| if (event.clipboardData?.getData('text/html')) return false | ||
| } | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| const doc = parseMarkdownToDoc(text) | ||
| if (!doc.content?.length) return false | ||
| return editor.commands.insertContent(doc) | ||
1 change: 1 addition & 0 deletions
1 .../[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx
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
1 change: 1 addition & 0 deletions
1 ...e/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field.tsx
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
Oops, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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.
Uh oh!
There was an error while loading. Please reload this page.