fix(rich-markdown-editor): eliminate empty-block / boundary-key structural bugs#5517
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Backspace / Enter on empty wrapped blocks ( Verbatim block boundaries ( Paragraph serialization ( Vitest coverage was added for keymap behavior, paragraph guards, empty paragraphs, and existing round-trip fixtures. Reviewed by Cursor Bugbot for commit 3b3cd1a. Configure here. |
Greptile SummaryThis PR fixes a class of structural editing and serialization bugs in the rich-markdown editor across three tiers: empty-wrapper boundary keys (Backspace/Enter on empty list items and blockquotes), verbatim-node boundary isolation, and paragraph leading-character escaping.
Confidence Score: 5/5All three tiers of fixes are narrowly scoped to the rich-markdown editor with no schema-breaking changes and comprehensive round-trip and keymap tests. The handlers are gated by tight pre-conditions and the walk-up delete is bounded to the same-parent chain. The isolating addition and BlockSafeParagraph serializer are confirmed non-destructive by the round-trip fixture suite. Both flagged observations are speculative with plausible explanations in existing code. No files require special attention; all changed files have direct test coverage in the accompanying test files. Important Files Changed
Reviews (2): Last reviewed commit: "fix(rich-markdown-editor): strip leading..." | Re-trigger Greptile |
…item Backspacing an emptied list item in the middle of a list used ProseMirror's default lift, which pulls the item out into a top-level paragraph — splitting one list into two and stranding an empty paragraph (a visible gap). Backspace at the start of an empty list item now joins into the previous block instead, removing the item and keeping a single list. Covers bullet, ordered, and task lists.
Replace the joinBackward empty-list-item Backspace branch with a walk-up-and-delete that removes the whole emptied wrapper, and extend it to task items and blockquotes. joinBackward left a stray empty paragraph inside the previous item (flat lists) or no-op'd entirely on nested items (leaving them stuck); blockquotes were never handled and split in two. Add an Enter handler so an empty non-trailing list/task item is removed rather than split into two lists around a stranded, non-round-trippable empty paragraph; a trailing empty item still exits the list via the default. Covers bullet/ordered/task/blockquote at every position; non-empty items and the double-Enter list-exit are unaffected.
… joins footnoteDef and rawHtmlBlock hold exact source text but were neither isolating nor atom, so a single Backspace/Delete at their boundary let ProseMirror's default join merge their raw markdown into an adjacent paragraph as HTML-escaped prose — silently destroying the node and corrupting saved markdown. Mark the block variants isolating so boundary keys can't cross their edge. Round-trip and in-place editing are unchanged.
…serialization A paragraph beginning with #, -, +, 1., 1), or a bare --- serialized unescaped, so it silently re-parsed into a heading / list / thematic break on the next load (reachable via type-a-marker then undo). The upstream serializer escapes inline delimiters (* _ ` [ ] ~, so * bullets and > quotes already round-trip) but not these block-starting markers. BlockSafeParagraph wraps the paragraph renderer with a leading-marker guard; escaping is idempotent (parsing consumes the backslash) and never over-escapes non-markers like #hashtag or -5.
3dbfdfc to
70bfe19
Compare
… paragraphs -free A paragraph beginning with a 4-space/tab indent re-parsed as an indented code block on the next load. Leading whitespace never renders in a paragraph (CommonMark strips up to three leading spaces; four or more become code), so BlockSafeParagraph now strips it — lossless and idempotent. Composes with the existing leading-marker escaping (e.g. ' # x' → '\# x'). Also locks in that consecutive empty paragraphs serialize via blank lines rather than the upstream ' ' marker: replacing StarterKit's Paragraph already dropped the path, and the round-trip preserves the empty-paragraph count without tripping the read-only safety gate (which flags as a stable-loss pattern). Added tests for both.
|
@cursor review |
|
@greptile review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 3b3cd1a. Configure here.
Summary
Fixes a class of structural editing + serialization bugs in the rich-markdown (
.md) editor, discovered while fixing the reported "deleting a bullet leaves a weird space" bug. A deep audit (parallel subagents + a real-browser ProseMirror harness) found the original fix was not isolated — the same class spanned other boundary keys, node types, and the serializer. This PR eliminates the whole class.Tier 1 — Empty-wrapped-block boundary keys (
keymap.ts)joinBackwardon an empty list item left a stray empty paragraph inside the previous item (flat lists), no-op'd on nested items (leaving them stuck), and never handled blockquotes (which split in two). Replaced with awalk-up-and-deletethat removes the whole emptied wrapper, generalized to list items, task items, and blockquotes at every position. Added an Enter handler: pressing Enter on an empty non-trailing list/task item previously split the list around a stranded, non-round-trippable empty paragraph; it now removes the empty item. A trailing empty item still exits the list via the default (unchanged).Tier 2 — Verbatim node isolation (
raw-markdown-snippet.tsx)footnoteDefandrawHtmlBlockhold exact source text but were neitherisolatingnoratom, so a single Backspace/Delete at their boundary merged their raw markdown into an adjacent paragraph as HTML-escaped prose — silently destroying the node and corrupting saved markdown. Marked the block variantsisolating.Tier 3 — Paragraph serialization guard (
extensions.ts)BlockSafeParagraphguards a paragraph's leading characters so it can't re-parse into a different block on the next load:#,-,+,1.,1), bare---) are backslash-escaped (the upstream serializer escaped inline delimiters — so*bullets and>quotes already round-tripped — but not these).codeBlocknodes, untouched.) empty-paragraph marker: consecutive empty paragraphs now round-trip via blank lines — count-preserved, idempotent, and no longer tripping the read-only safety gate (which flags ).All escaping/stripping is idempotent (parsing consumes it) and never over-escapes non-markers (
#hashtag,-5).Testing
Real-browser ProseMirror harness + committed Vitest (CI):
#/-) still autoformat — confirming the paragraph replacement andisolatingchanges have no side effects.Note
An empty blockquote (a
>with no content) normalizes away to blank lines on reload. This is a benign one-time normalization (idempotent, no data loss, not read-only) — preserving an empty quote has no clean CommonMark representation and would reintroduce the →read-only problem, so normalizing is the correct behavior.