Skip to content

Support raw HTML <br> line breaks in the Markdown renderer#13854

Closed
fbartho wants to merge 9 commits into
warpdotdev:masterfrom
fbartho:fb/13732-markdown-br-implementation
Closed

Support raw HTML <br> line breaks in the Markdown renderer#13854
fbartho wants to merge 9 commits into
warpdotdev:masterfrom
fbartho:fb/13732-markdown-br-implementation

Conversation

@fbartho

@fbartho fbartho commented Jul 16, 2026

Copy link
Copy Markdown

Closes #13732

The Markdown viewer rendered a raw <br> as literal text, both mid-paragraph and inside GFM pipe-table cells. Inside a pipe-table cell <br> is the only way to author a forced line break at all, so this was the more painful half of the gap.

This teaches the .md inline parser to recognize <br>, <br/>, and <br /> (case-insensitive) and turn them into a real line break, in ordinary paragraphs and inside GFM table cells.

Notes:

  • The break is stored as a newline fragment; the render path already turns an embedded newline into a visual break in both paragraphs and cells, so no rendering code changed. The table serialization paths translate the break back to <br> so an in-cell break does not corrupt row/column structure on round-trip.
  • Attributes (<br class="x">) and malformed forms (<br without >, < br>, </br>) fall through to literal text.
  • Markdown-native hard breaks (trailing double-space / backslash) are unchanged and out of scope.

Tested: new unit tests in markdown_parser pass, clippy and rustfmt clean, and exercised in a local dev build against the issue sample document — paragraphs, consecutive breaks, self-closing/case variants, table cells, and malformed forms all behave as described.

Human-focused Test Document + Screenshot
# `<br>` line breaks (paragraphs and GFM pipe-table cells)

Each `<br>` below should render as a real line break. Malformed forms stay literal.

Mid-paragraph:

First line of the paragraph.<br>
Second line, after a raw `<br>`.

Consecutive breaks (should produce blank lines):

Before the gap.<br><br><br>After a triple `<br>`.

Self-closing and case variants (each breaks):

alpha<br/>bravo<br />charlie<BR>delta

Inside a GFM pipe-table cell (two lines, table intact):

| Feature | Notes                                |
| ------- | ------------------------------------ |
| Export  | Supports CSV.<br>Also supports JSON. |

Trailing `<br>` at the end of a cell (should add a trailing empty line inside the cell, matching GitHub):

| Feature | Notes                  |
| ------- | ---------------------- |
| Import  | Ends with a break.<br> |

Malformed / attributed forms (each should stay literal text, no break):

one</br>two &lt;br three<br class="x">four

Control (unrelated tags unaffected): a <kbd>K</kbd> stays literal.
Screenshot of the above test document

fbartho added 2 commits July 16, 2026 11:11
…v#13732)

Warp's Markdown viewer rendered a raw <br> as literal text in both
paragraphs and GFM pipe-table cells. Recognize <br>, <br/>, and <br />
(case-insensitive) as an inline token that produces a forced line break.

The break is stored as a newline fragment: both the paragraph and
table-cell render paths already turn an embedded newline into a real
visual break, so no rendering changes are needed. The newline/tab/pipe
delimited serialization paths (inline_to_markdown, to_plain_text)
translate the break back to <br> so an in-cell break does not corrupt
table row/column structure.

Attributes (e.g. <br class="x">) and malformed forms (<br with no >,
< br>, </br>) deterministically fall through to literal text.
A raw HTML <br> at the end of a GFM pipe-table cell parses to a trailing
"\n" fragment, but layout_run strips a trailing newline out of the cell
text so the break was silently dropped — the cell collapsed to a single
line instead of rendering the trailing empty line GitHub shows.

measure_table_cells now honors layout_run's has_new_line return: when the
final run ends in a stripped newline, re-append it to the cell text so both
the measure pass and the render pass emit the trailing empty line. Embedded
breaks (interior newline) were never stripped and are unchanged.
@cla-bot cla-bot Bot added the cla-signed label Jul 16, 2026
@github-actions github-actions Bot added the external-contributor Indicates that a PR has been opened by someone outside the Warp team. label Jul 16, 2026
@oz-for-oss

oz-for-oss Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@fbartho

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR teaches the Markdown parser to treat raw <br> variants as hard line breaks in paragraphs and GFM table cells, then preserves trailing table-cell breaks during layout.

Concerns

  • Exporting a table cell containing a parsed <br> can still emit a raw newline in the editor GFM table export path, which corrupts the pipe-table row on copy/export. I left an inline comment on the parser-side serialization change because the corresponding editor table serializer needs the same newline-to-<br> handling.
  • Spec context: no approved or repository spec context was provided for this PR.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread crates/markdown_parser/src/lib.rs
The editor's table export path (BufferMarkdownParser::serialize_table_to_gfm_markdown)
uses a local inline_to_markdown that emitted fragment text unchanged, so an in-cell
hard break (raw HTML <br>, stored as an embedded newline) was written as a raw newline
that split the pipe-table row. Apply the same newline-to-<br> conversion the
markdown_parser serializer already performs, guarded on non-inline-code fragments.
@fbartho

fbartho commented Jul 16, 2026

Copy link
Copy Markdown
Author

Re: Spec Context: the issue was tagged ready-to-implement by a maintainer, and CONTRIBUTING.md suggested I could just go ahead. Was that not the right process?
If it is the right process, maybe the Oz skill/script here should be updated to indicate "No spec needed, issue tagged ready-to-implement"? Is that skill/script editable/open-source? If yes, I can help add that.

Otherwise I've implemented the requested changes (as discussed in the inline comment thread), so I think I'm ready for another /oz-review

@oz-for-oss

oz-for-oss Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@fbartho

I'm re-reviewing this pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss

oz-for-oss Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@fbartho

I'm re-reviewing this pull request in response to a review request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

@oz-for-oss oz-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR teaches the Markdown inline parser to treat raw HTML <br> variants as hard line breaks, adds table serialization support so those breaks round-trip as <br>, and adjusts table-cell measurement for trailing breaks.

Concerns

  • Table cells still drop a <br> when the break is represented as a trailing newline on a non-final styled run, such as a break at the boundary between bold/link text and following plain text.
  • No approved spec context was provided, so there is no spec-drift finding.
  • No security findings.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread crates/editor/src/content/edit.rs Outdated

@oz-for-oss oz-for-oss Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR teaches the Markdown parser to treat raw <br> variants as hard line breaks and updates table serialization/layout tests around preserving those breaks.

Concerns

  • Table-cell rendering drops <br> line breaks when the break is at a style boundary because only the last stripped trailing newline is preserved.

Verdict

Found: 0 critical, 1 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread crates/editor/src/content/edit.rs Outdated
fbartho added 6 commits July 16, 2026 15:20
TableCellOffsetMap::from_inline_and_source walked source char-by-char against rendered text, assuming a 1:1 correspondence. A raw HTML <br> is 4+ source chars but a single rendered newline, so the walk desynced: offsets after an in-cell break mapped past the break instead of to their true source positions, corrupting cursor rects, selection highlights, and clipboard slices for post-break text. Walk each fragment in newline-delimited segments, emitting one linear range per segment and consuming the <br>/<br/>/<br /> token as a source gap between segments.
serialize_table_cell_inline wrote a cell fragment's text verbatim, so an authored hard break (stored as an embedded newline) landed as a raw newline inside <td>. HTML collapses that to a space, silently dropping the in-cell break when copying a table as HTML. Split the fragment on newlines and emit a <br> element between the parts, mirroring the sibling guard in inline_to_markdown (ca6a4dd). Inline code is left verbatim since it can never contain such a newline.
clipboard_table_text_in_range flattened each cell with inline_to_text, which emits an authored hard break (stored as an embedded newline) as a raw newline. Inside the tab/newline-delimited clipboard format that raw newline splits the row, so pasting a copied table back into Warp re-parsed a spurious extra row. Translate the break back to a literal <br> when building each cell slice, scoped to the table-cell context rather than changing inline_to_text globally (other callers may legitimately want the newline).
The parser had no coverage combining <br> with inline style delimiters. Guard that a break inside bold, underline, or a link keeps the surrounding style on both sides of the embedded newline.
The plain-text clipboard flavor is rendered text and already strips styling, so an authored hard break must emit a real newline rather than the literal HTML string "<br>" the user never selected. The HTML flavor still carries the break as a real <br> element for Warp-to-Warp round-trip fidelity; the accepted cost is that a plain-text-only re-paste of such a row may split it, a standard TSV-class limitation.
@fbartho

fbartho commented Jul 16, 2026

Copy link
Copy Markdown
Author

Before re-review I ran a deeper pass over the surfaces this PR touches and hardened three more spots, each with a failing-test-first fix:

  • 0d4b4eb — the table-cell offset map walked raw cell source char-by-char against rendered text, so every cursor/selection/clipboard offset after an in-cell break mapped
    past the break. It now walks fragments in newline-delimited segments and treats the <br> token as a source gap, the same way it already handles markdown markers that exist
    in source but not rendered.
  • 538c961 — copy-as-HTML wrote the break as a literal newline inside <td> (collapses to a space on paste); it now emits a real <br> element, matching the GFM export
    guard.
  • 716e569 + 6b3ea8d — the two clipboard flavors now carry an in-cell break differently by design: the HTML flavor emits a <br> element (Warp-to-Warp paste uses this
    flavor, so it round-trips faithfully), while the plain-text flavor emits a real newline — it's rendered text and shouldn't inject HTML the user never selected. The accepted
    cost, documented in the test, is that a plain-text-only re-paste of such a row can split it — the standard TSV-class limitation.
  • 5b346de — regression guards for <br> combined with bold/underline/links (all pass; the style carries across the break).

Also manually verified in a local build that styled breaks paint no stray underline/bold artifacts on macOS.

/oz-review

@fbartho

fbartho commented Jul 16, 2026

Copy link
Copy Markdown
Author

hey @kevinyang372 I accidentally wasted one of my Oz reviews earlier by double triggering, but I think the PR is finally complete.

Could you please trigger oz again for me? or otherwise review this? Thanks!

@fbartho

fbartho commented Jul 16, 2026

Copy link
Copy Markdown
Author

Closing in favor of #13870.

@fbartho fbartho closed this Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed external-contributor Indicates that a PR has been opened by someone outside the Warp team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Markdown viewer: support <br> for explicit line breaks

1 participant