Skip to content

Commit 3965da1

Browse files
committed
Leave caller-supplied separators alone in the HTML passthrough
Rewriting a bare <br> boundary reached into blockquotes: the paragraph matching is regex-based, not nesting-aware, and a <br> between two paragraphs inside a blockquote is inline content the editor keeps untouched (verified against lexxy v0.9.29). Insert the durable separator only where paragraphs are genuinely adjacent, and leave separators the caller supplied as they came.
1 parent cb57e6d commit 3965da1

2 files changed

Lines changed: 24 additions & 21 deletions

File tree

‎internal/richtext/richtext.go‎

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -398,14 +398,16 @@ func MarkdownToHTML(md string) string {
398398
// distinction), so contiguous paragraphs from HTML input are treated as
399399
// separate paragraphs.
400400
//
401-
// A boundary already carrying an empty separator paragraph (<p><br></p> or
402-
// <p></p>) on either side is left untouched, so running this on Basecamp editor
403-
// output — or on its own output — is a no-op. A boundary separated only by bare
404-
// <br> tags is rewritten to the separator paragraph: the editor drops those on
405-
// the first edit, so leaving them would keep the spacing they express fragile.
406-
// Only directly adjacent <p> blocks are separated; anything else between them,
407-
// such as a heading, list, or attachment, already provides its own break and is
408-
// left alone.
401+
// The transform is byte-preserving apart from the inserted separators and is
402+
// idempotent: a boundary that already carries a separator — a bare <br> between
403+
// the paragraphs, or an empty separator paragraph (<p><br></p> or <p></p>) on
404+
// either side — is left untouched, so running it on already-separated content
405+
// (including Basecamp editor output) is a no-op. Separators the caller supplied
406+
// are left as they came: this matching is not nesting-aware, and a <br> the
407+
// caller put between two paragraphs inside a blockquote is legal inline content
408+
// that survives editing. Only directly adjacent <p> blocks are separated;
409+
// anything else between them, such as a heading, list, or attachment, already
410+
// provides its own break and is left alone.
409411
func insertParagraphSeparators(s string) string {
410412
locs := reP.FindAllStringIndex(s, -1)
411413
if len(locs) < 2 {
@@ -431,8 +433,8 @@ func insertParagraphSeparators(s string) string {
431433

432434
nextStart := locs[i+1][0]
433435
gap := s[end:nextStart]
434-
if !empty[i] && !empty[i+1] && isSeparatorGap(gap) {
435-
b.WriteString(reBR.ReplaceAllString(gap, ""))
436+
if !empty[i] && !empty[i+1] && strings.TrimSpace(gap) == "" {
437+
b.WriteString(gap)
436438
b.WriteString(paragraphSeparator)
437439
cursor = nextStart
438440
}
@@ -441,13 +443,6 @@ func insertParagraphSeparators(s string) string {
441443
return b.String()
442444
}
443445

444-
// isSeparatorGap reports whether the markup between two paragraph blocks holds
445-
// nothing but whitespace and bare <br> tags — i.e. whatever spacing it expresses
446-
// can be replaced by a separator paragraph.
447-
func isSeparatorGap(gap string) bool {
448-
return strings.TrimSpace(reBR.ReplaceAllString(gap, "")) == ""
449-
}
450-
451446
// isEmptyParagraph reports whether a <p>...</p> block has no visible content —
452447
// i.e. it is empty or contains only <br> tags and whitespace, including
453448
// non-breaking-space entities (&nbsp;, &#160;, &#xa0;) that rich text editors

‎internal/richtext/richtext_test.go‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2241,15 +2241,23 @@ func TestMarkdownToHTMLInsertsParagraphSeparators(t *testing.T) {
22412241
expected: "<p>A</p>\n<p><br></p><p>B</p>",
22422242
},
22432243
{
2244-
name: "bare br separator becomes an editor-durable separator paragraph",
2244+
name: "caller-supplied bare br separator is left untouched",
22452245
input: "<p>A</p><br><p>B</p>",
2246-
expected: "<p>A</p><p><br></p><p>B</p>",
2246+
expected: "<p>A</p><br><p>B</p>",
22472247
},
22482248
{
22492249
name: "separator paragraph is left untouched (editor canonical)",
22502250
input: "<p>A</p><p><br></p><p>B</p>",
22512251
expected: "<p>A</p><p><br></p><p>B</p>",
22522252
},
2253+
{
2254+
// A <br> between paragraphs nested in a blockquote is inline content,
2255+
// which the editor keeps — matching is not nesting-aware, so leaving
2256+
// caller-supplied separators alone is what protects it.
2257+
name: "bare br inside a blockquote is left untouched",
2258+
input: "<blockquote><p>A</p><br><p>B</p></blockquote>",
2259+
expected: "<blockquote><p>A</p><br><p>B</p></blockquote>",
2260+
},
22532261
{
22542262
name: "empty paragraph separator is left untouched",
22552263
input: "<p>A</p><p></p><p>B</p>",
@@ -2301,9 +2309,9 @@ func TestMarkdownToHTMLInsertsParagraphSeparators(t *testing.T) {
23012309
expected: "<p></p><p>A</p>",
23022310
},
23032311
{
2304-
name: "mix of contiguous and bare-br boundaries both get separator paragraphs",
2312+
name: "mix of separated and contiguous only fills the gap that lacks a separator",
23052313
input: "<p>A</p><p>B</p><br><p>C</p>",
2306-
expected: "<p>A</p><p><br></p><p>B</p><p><br></p><p>C</p>",
2314+
expected: "<p>A</p><p><br></p><p>B</p><br><p>C</p>",
23072315
},
23082316
}
23092317

0 commit comments

Comments
 (0)