Skip to content

fix(convert): move a mark's edge whitespace outside its delimiters - #215

Merged
willkg merged 2 commits into
mainfrom
fix/mark-edge-whitespace
Sep 28, 2026
Merged

willkg merged 2 commits into
mainfrom
fix/mark-edge-whitespace

Conversation

@willkg

@willkg willkg commented Sep 28, 2026

Copy link
Copy Markdown
Member

Fixes #204.

read/export trimmed the whitespace at the edge of a bold, italic or strikethrough span and never put it back. So <strong>bold </strong>next read as **bold**next and published as one word, as on the incident table in #204 ("features for13.5h"). The editor leaves a space typed at the end of a bold run inside the run, so this is a common shape.

What changes

  • Marks (renderMark): whitespace at a mark's edge, Unicode whitespace included, moves outside the delimiters: **bold** next. It can't stay inside, because CommonMark refuses **bold ** as emphasis.
  • Wrappers with no Markdown syntax (a coloured <span>, <u>, <sup>) no longer trim their edges. Without this, coloured bold text would still read as **bold**next.
  • Link text keeps its edge whitespace inside the brackets ([see ](url)here). Markdown allows that and publishes it back.
  • Neighbours that came up while verifying:
    • A mark holding only whitespace rendered as ****; it now renders as the whitespace.
    • A <br /> at a mark's edge was dropped; it now moves out with the rest.
    • a <br /> read as three trailing spaces and then two, so the Markdown wasn't a fixed point.
    • A hard break in a heading ended the heading; it stays a literal <br />.
  • Where moved whitespace meets whitespace beside the mark, one space is kept, since storage would collapse the second on the next publish.

Testing

  • New storage2md/mark-edge-whitespace case: the issue's three rows, a leading space, a no-break space, nested marks, a whitespace-only mark, breaks at a mark's edge, a coloured <span> and <u>, links, headings, and the incident table cell. It also runs through the round-trip fixed-point check.
  • The split-marks golden and TestStorageToMarkdownCoalescesSplitMarks had recorded the bug (**a**[b… for <strong>a </strong><a>b…); both are corrected.
  • Table property test:
    • The generator now puts a space at a mark's edge and follows such a mark directly with text. The edge spaces come from a separate random stream, so every existing seed's table is otherwise unchanged.
    • The model treats a space at a mark's edge as the same inside or outside, and two runs of one mark separated only by a space as one run.
    • Against main's converter it fails at seed 5; with this PR, all 3000 seeds pass, as did a 60s fuzz run (~200k execs).
  • make check passes.

Left for later

  • read: two hard breaks in a row become a paragraph break #213: two hard breaks in a row read back as a paragraph break. This already happens on main. Since this change a <br /> at a bold run's end followed by another <br /> hits it too, where before the first break was simply dropped.
  • read: an empty paragraph before a list in a table cell is lost #214: an empty paragraph before a list in a table cell is lost. Found by the property test when the generator's seeds moved; unrelated to marks.
  • A mark whose text starts or ends with punctuation next to a letter outside it (a**(b)**) still fails CommonMark's other flanking rule and reads back as literal asterisks. This PR doesn't change that behaviour.

read trimmed the whitespace at the edge of a bold, italic or
strikethrough span and did not put it back, so "<strong>bold </strong>
next" read as "**bold**next" and published as one word. The editor
leaves a space typed at the end of a bold run inside the run, so this
shape is common. The whitespace now moves outside the delimiters:
"**bold** next". It cannot stay inside, since CommonMark refuses
"**bold **" as emphasis.

A mark holding only whitespace rendered as "****" and now renders as
the whitespace. A <br /> at a mark's edge was dropped and now moves out
with the rest. Where moved whitespace meets whitespace beside the mark,
one space is kept, and whitespace before a hard break is trimmed, since
storage loses both on the next publish and the Markdown would not be a
fixed point. The last also fixes "a <br />" reading as three trailing
spaces rather than two.

The split-marks golden recorded the bug: its partly-bold link case
lost the space after "a".

Fixes #204.
The whitespace renderMark moved outside a mark was trimmed again by
any wrapper Markdown has no syntax for, so coloured bold text (a
<span> around a <strong>) still read as "**bold**next", and <u>/<sup>
lost their edge spaces outright. Such a wrapper now returns its run
untrimmed and leaves the whitespace to the run around it.

Link text was trimmed the same way, so "<a>see </a>here" read as
"[see](url)here". The whitespace now stays inside the brackets, where
Markdown allows it and publishes it back.

A heading is one line, so a hard break in one, now reachable from a
break at a bold run's edge, ended it and published the rest as a
paragraph. A break in a heading stays a literal <br />.

renderInlineRun builds into a byte slice rather than concatenating
strings, which was quadratic in a paragraph's inline children, and
renderMark trims each edge once.

The table property test now generates a space at a mark's edge, from
a stream of its own so every existing seed's table is otherwise
unchanged, and follows such a mark directly with the next piece of
text. Its model treats a space at a mark's edge as the same inside or
outside, and two runs of one mark separated only by a space as one
run, since both look the same in Confluence. With the converter from
before #204 it fails at seed 5.
@willkg
willkg merged commit fd12269 into main Sep 28, 2026
1 check passed
@willkg
willkg deleted the fix/mark-edge-whitespace branch September 28, 2026 15:06
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.

read: a trailing space inside bold, italic or strikethrough is dropped, joining words

1 participant