Files
pad/internal/materialize/testdata
xarmian 8d910dc20a fix(editor): bold, italic and strike always serialize to markdown that reads back (BUG-3583) (#2057)
* fix(editor): bold, italic and strike always serialize to markdown that reads back (BUG-3583)

CommonMark lets a `**` / `*` / `~~` run open only where it is left-flanking
and close only where it is right-flanking. A mark whose text starts with
punctuation after a letter, or ends with punctuation before a letter, has no
delimiter that can do both. Measured on the headless editor with the editor's
own parser: every such case lost its mark on reload. tiptap-markdown slides an
unflankable delimiter inward (the punctuation silently loses the mark), and
skips that for a mark at output offset 0, so a bold run starting a block went
out as `**Hello.**World` and read back as literal asterisks.

flankingMarks.ts replaces bold, italic and strike (and re-adds code and
underline, keeping StarterKit's mark order) in the body editor, the
materializer (htmlProseExtensions, so parity holds) and the comment editor:
- each run is written with its delimiter and, when it closes, tested against
  the characters actually around it with markdown-it's scanDelims; a run that
  cannot flank is rewritten to <strong>/<em>/<s>, which parse back to the
  same mark (html: true) and render through renderMarkdown's allow-list;
- tiptap-markdown's sliding is switched off for these marks only (a guarded
  patch on prosemirror-markdown's base markString);
- a textblock whose output could still mis-pair (a run ending in punctuation
  before a letter, or merged same-character runs) is parsed back and
  compared per character; on a mismatch it is re-rendered with the HTML form.
Code needs nothing (measured).

Fuzz (6000 paragraphs of letters, punctuation and spaces under overlapping
marks, links and hard breaks, in paragraphs, quotes and list items): marks
lost 0 (old serializer 211-228 per 1500); text altered 0-1 (old 163-172, the
rest a pre-existing line-start escaping gap); output changed where the old
one already round-tripped: 3 per 1500, all one whitespace placement.
Serialization time on 500 paragraphs: equal to main for ordinary prose;
the extra parse runs only on the textblocks that can fail.

Corpus: a block with the failing shapes is added to the materializer parity
generator; corpus.json regenerated; Go materialize tests pass on the new
bundle. New e2e: typed bold "Hello." + "World" is stored as
<strong>Hello.</strong>World; on main it is stored as **Hello.**World.
(A reload of an open tab does not show the defect on main: it re-seeds from
the op-log. The stored body is what readers and a pruned editor see.)

Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ

* fix(web): backlink snippets drop inline tags; every reader of an HTML mark pinned (BUG-3583)

The lead's check before GO: the fix stores runs markdown delimiters cannot
express as inline HTML, so every other reader of items.content must show it
as formatting or plain text, never literal tags.

- Share pages and their collection views: renderMarkdown's allow-list renders
  <strong>/<em>/<s> (e2e).
- The editor seeding from the body (after an op-log prune): bold (e2e).
- Search palette snippets: already stripped tags (e2e pins it).
- Backlink snippets showed the raw slice, so `<strong>` appeared literally,
  and the slice could cut a tag in two (`…g>Hello.`). Tags and cut fragments
  are now dropped. Red on main in the new e2e, green here.
- Email/notification templates carry no item content; the workspace and
  artifact exports hand out the raw body, where inline HTML is valid
  CommonMark/GFM; nothing syncs bodies to GitHub (read from the code).

Claude-Session: https://claude.ai/code/session_01FAVXA5mJ5BNhKmv95jxQkQ
2026-10-11 08:02:21 -04:00
..