mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-09-26 04:05:44 +00:00
fix(markdown-editor): keep inline formatting from rich-HTML pastes
An inline-only rich paste (Google Docs/Word/mail single paragraph - <b>, <a href>, styled spans) carries no block tags, so the paste plugin hijacked it into a plain-text markdown parse and silently dropped bold/italic/links. Defer to ProseMirror's native HTML parse when the clipboard has HTML and the text/plain shows no markdown cues - the schema DOMParser keeps those marks. Markdown-looking text (a VS Code copy of markdown source wrapped in syntax-color spans) still parses as markdown, matching load. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c2d04a1d94
commit
773e9de5d8
@@ -31,10 +31,30 @@ describe('shouldParseMarkdownOnPaste — markdown-parse plain text, defer rich s
|
||||
expect(shouldParseMarkdownOnPaste('# x', html, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('still parses when the HTML is only styled-inline (e.g. a VS Code span, no block tags)', () => {
|
||||
it('still parses when the HTML is only styled-inline but the text IS markdown (VS Code markdown copy)', () => {
|
||||
expect(shouldParseMarkdownOnPaste('**x**', '<span style="color:red">**x**</span>', false)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['# heading', '<span># heading</span>'],
|
||||
['- item one\n- item two', '<span>- item one<br>- item two</span>'],
|
||||
['| a | b |', '<span>| a | b |</span>'],
|
||||
['```\ncode\n```', '<span>```</span>'],
|
||||
['> quoted', '<span>> quoted</span>'],
|
||||
['see [docs](https://x.dev)', '<span>see [docs](https://x.dev)</span>'],
|
||||
['1. first', '<span>1. first</span>'],
|
||||
])('parses markdown-looking text %s despite an inline-only HTML wrapper', (text, html) => {
|
||||
expect(shouldParseMarkdownOnPaste(text, html, false)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['hello world', '<b style="font-weight:700">hello world</b>'], // Google Docs bold paragraph
|
||||
['see the docs', '<a href="https://example.com">see the docs</a>'], // inline link
|
||||
['plain sentence', '<span style="font-style:italic">plain sentence</span>'], // Word italic
|
||||
])('defers inline-formatted HTML whose text %s has no markdown (native parse keeps the marks)', (text, html) => {
|
||||
expect(shouldParseMarkdownOnPaste(text, html, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps a paste inside a code context literal', () => {
|
||||
expect(shouldParseMarkdownOnPaste('# x', '', true)).toBe(false);
|
||||
});
|
||||
@@ -78,4 +98,10 @@ describe('MarkdownPaste — the parsed payload matches load (same tuned markdown
|
||||
|
||||
expect(html).not.toContain('<h1>');
|
||||
});
|
||||
|
||||
it('defers inline-formatted HTML with markdown-free text (plugin inserts nothing)', () => {
|
||||
const html = pasteHtml('hello bold world', '<b>hello bold world</b>');
|
||||
|
||||
expect(html).not.toContain('hello bold world');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,13 +3,27 @@ import { Plugin, PluginKey } from '@tiptap/pm/state';
|
||||
|
||||
// @tiptap/markdown parses markdown for load/insertContent but never for the clipboard, so a paste of block
|
||||
// markdown (# ## - 1. | >) would land as literal text (only StarterKit's inline mark paste-rules fire).
|
||||
// Route plain-text pastes through the same markdown layer as load; defer to ProseMirror's own path for rich
|
||||
// sources — an in-editor copy (carries `data-pm-slice`) or web/Office HTML (block tags) — so their fidelity
|
||||
// survives, and keep pastes inside code literal.
|
||||
// Route markdown-looking plain-text pastes through the same markdown layer as load; defer to ProseMirror's
|
||||
// own path for rich sources — an in-editor copy (carries `data-pm-slice`), web/Office HTML (block tags), or
|
||||
// inline-formatted HTML whose text carries no markdown — so their fidelity survives, and keep pastes inside
|
||||
// code literal.
|
||||
const RICH_HTML_BLOCK = /<(?:h[1-6]|ul|ol|li|table|thead|tbody|tr|td|th|blockquote|pre|img|hr)\b/i;
|
||||
|
||||
const MARKDOWN_CUES = [
|
||||
/^#{1,6}\s/m, // heading
|
||||
/\*\*[^*]+\*\*/, // bold
|
||||
/\[.+\]\(.+\)/, // link
|
||||
/^[-*+]\s/m, // bullet item
|
||||
/^\d+\.\s/m, // ordered item
|
||||
/^ {0,3}(?:```|~~~)/m, // fence
|
||||
/^\|/m, // table row
|
||||
/^>\s/m, // blockquote
|
||||
];
|
||||
|
||||
const looksLikeMarkdown = (text: string): boolean => MARKDOWN_CUES.some((cue) => cue.test(text));
|
||||
|
||||
export const shouldParseMarkdownOnPaste = (text: string, html: string, isCodeContext: boolean): boolean => {
|
||||
if (!text.trim()) {
|
||||
if (!text.trim() || isCodeContext) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -17,7 +31,16 @@ export const shouldParseMarkdownOnPaste = (text: string, html: string, isCodeCon
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isCodeContext;
|
||||
// Inline-only rich HTML (a Google Docs/Word/mail single paragraph: <b>, <a href>, styled spans) whose
|
||||
// text/plain carries no markdown: defer to ProseMirror's native HTML parse — it runs the clipboard HTML
|
||||
// through the schema, so bold/italic/links survive as marks. Markdown-looking text still wins the
|
||||
// markdown parse: a VS Code copy of markdown source arrives wrapped in syntax-color spans, and parsing
|
||||
// its text/plain (not the span noise) is the point of this plugin.
|
||||
if (html && !looksLikeMarkdown(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const MarkdownPaste = Extension.create({
|
||||
|
||||
Reference in New Issue
Block a user