Skip to content

markdown: reflow soft line breaks instead of rendering them as hard breaks - #2947

Merged
huacnlee merged 3 commits into
longbridge:mainfrom
kossoy:fix/soft-break-reflow
Sep 4, 2026
Merged

markdown: reflow soft line breaks instead of rendering them as hard breaks#2947
huacnlee merged 3 commits into
longbridge:mainfrom
kossoy:fix/soft-break-reflow

Conversation

@kossoy

@kossoy kossoy commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

A CommonMark soft break lives inside a Node::Text value as a plain \n. The
renderer treats any \n in a text run as a line break, so a paragraph that is
hard-wrapped in the source renders one visual line per source line instead of
reflowing to the available width.

Reproduction

No-break control: this sentence
continues as a soft wrap (same visual line when width allows).

Renders as two lines even with 1 200 px of free width. Expected one wrapped
line.

Measured on a 3 874-note vault: 10.9 % of notes (421) contain hard-wrapped
prose and rendered wrong.

Fix

Collapse \n to a space in the Node::Text arm. Hard breaks are unaffected:
they arrive as their own Node::Break and never reach this arm. The swap is
length-preserving, so inline mark offsets computed against the text stay valid.

crates/base/src/text/format/markdown.rs, +11 −2. Best reviewed after the
hard-break PR, which adds the Node::Break arm this one relies on for the
distinction.

kossoy and others added 3 commits September 4, 2026 09:50
mdast hands the line ending over exactly as the source wrote it, so a CRLF
document keeps its carriage return inside the `Node::Text` value. Replacing
only the newline left the CR stranded in the middle of the reflowed line,
where it reaches the text run — visible to copy, to measurement, and to
whatever the shaper makes of a bare control character. Before this arm
existed the CR sat against the line break and stayed out of the way, so the
reflow is what exposed it.

Take the CR with the newline, and cover the arm:

- `test_soft_break_reflows_to_space` over LF, CRLF and a lone CR.
- `test_soft_break_reflows_while_hard_break_survives`, which pins the
  invariant the collapse rests on — a hard break arrives as `Node::Break`
  and never as a newline inside `Node::Text`.

Also drop the claim that the swap has to be length-preserving for inline
mark offsets. `parse_paragraph`'s return value is discarded at every
call site; the offsets are recomputed from `merged_text.len()` in
`merge_children_with_mark`. Nothing downstream depends on the length, and
saying otherwise would rule out this fix, which turns two bytes into one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEg8SEHNEgRDcZ9mDj1p6r
@huacnlee
huacnlee enabled auto-merge (squash) September 4, 2026 11:30
@huacnlee

huacnlee commented Sep 4, 2026

Copy link
Copy Markdown
Member

Thank you

@huacnlee
huacnlee merged commit b0a1836 into longbridge:main Sep 4, 2026
9 checks passed
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.

2 participants