Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions cpp/parser/GumboNormalizer.c
Original file line number Diff line number Diff line change
Expand Up @@ -548,17 +548,34 @@ static void walk_node(GumboNode *node, buffer_t *out);

static void flatten_bq_node(GumboNode *node, buffer_t *ib, buffer_t *out);

static void flush_inline_p(buffer_t *ib, buffer_t *out,
/** True if buf is empty or contains only ASCII whitespace. */
static bool is_whitespace_only(const char *data, size_t len) {
for (size_t i = 0; i < len; i++) {
unsigned char c = (unsigned char)data[i];
if (c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f')
return false;
}
return true;
}

/**
* Flush buffered inline content as a <p>. Inter-block whitespace (newlines /
* spaces between block tags in pretty-printed HTML) is discarded so it does
* not become empty paragraphs that later serialize as extra <br>s.
*/
static bool flush_inline_p(buffer_t *ib, buffer_t *out,
GumboElement *align_el) {
if (ib->len > 0) {
bool emitted = ib->len > 0 && !is_whitespace_only(ib->data, ib->len);
if (emitted) {
buffer_append_str(out, "<p");
if (align_el)
emit_alignment(align_el, "p", out);
buffer_append_str(out, ">");
buffer_append(out, ib->data, ib->len);
buffer_append_str(out, "</p>");
buffer_clear(ib);
}
buffer_clear(ib);
return emitted;
}

static void flatten_bq_children(GumboNode *node, buffer_t *ib, buffer_t *out) {
Expand Down Expand Up @@ -730,9 +747,8 @@ static void walk_children(GumboNode *node, buffer_t *out) {
!is_blockquote_node(children->data[i])) {
child = children->data[i];
if (is_br_node(child)) {
if (ib.len > 0)
flush_inline_p(&ib, out, NULL);
else
/* Whitespace-only buffer is layout noise; treat like empty → <br> */
if (!flush_inline_p(&ib, out, NULL))
buffer_append_str(out, "<br>");
Comment thread
kacperzolkiewski marked this conversation as resolved.
i++;
continue;
Expand Down
27 changes: 27 additions & 0 deletions cpp/tests/GumboParserTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -571,3 +571,30 @@ TEST(GumboParserTest, TextAlignment) {
"<p style=\"text-align: center\">c</p>"
"<p style=\"text-align: right\">r</p></blockquote>");
}

TEST(GumboParserTest, InterBlockWhitespace) {
// Pretty-printed consecutive paragraphs must not gain empty <p>s from the
// newlines between them (those would later serialize as extra <br>s).
EXPECT_EQ(GumboParser::normalizeHtml(
"<p>Asdasd</p>\n<p>Asdasd</p>\n<p>Asdasda</p>"),
"<p>Asdasd</p><p>Asdasd</p><p>Asdasda</p>");
EXPECT_EQ(GumboParser::normalizeHtml(
"<p>Asdasd</p>\n\n<p>Asdasd</p>\n\n<p>Asdasda</p>"),
"<p>Asdasd</p><p>Asdasd</p><p>Asdasda</p>");
EXPECT_EQ(GumboParser::normalizeHtml(
"<html>\n<p>Asdasd</p>\n<p>Asdasd</p>\n<p>Asdasda</p>\n</html>"),
"<p>Asdasd</p><p>Asdasd</p><p>Asdasda</p>");
EXPECT_EQ(GumboParser::normalizeHtml("<p>Asdasd</p> <p>Asdasd</p>"),
"<p>Asdasd</p><p>Asdasd</p>");

// Significant inline content between blocks is still wrapped in <p>.
EXPECT_EQ(GumboParser::normalizeHtml("<p>a</p> hello <p>b</p>"),
"<p>a</p><p> hello </p><p>b</p>");

// Spaces inside text / between inlines must be preserved.
EXPECT_EQ(GumboParser::normalizeHtml("hello world"), "hello world");
EXPECT_EQ(GumboParser::normalizeHtml("<p>hello world</p>"),
"<p>hello world</p>");
EXPECT_EQ(GumboParser::normalizeHtml("<b>hello</b> <i>world</i>"),
"<b>hello</b> <i>world</i>");
}
28 changes: 28 additions & 0 deletions src/web/__tests__/htmlNormalizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,4 +575,32 @@ describe('htmlNormalizer', () => {
expect(normalizeHtml(input)).toBe(expected);
});
});

describe('InterBlockWhitespace', () => {
// Pretty-printed consecutive paragraphs must not gain empty <p>s from the
// newlines between them (those would later serialize as extra <br>s).
test.each([
[
'<p>Asdasd</p>\n<p>Asdasd</p>\n<p>Asdasda</p>',
'<p>Asdasd</p><p>Asdasd</p><p>Asdasda</p>',
],
[
'<p>Asdasd</p>\n\n<p>Asdasd</p>\n\n<p>Asdasda</p>',
'<p>Asdasd</p><p>Asdasd</p><p>Asdasda</p>',
],
[
'<html>\n<p>Asdasd</p>\n<p>Asdasd</p>\n<p>Asdasda</p>\n</html>',
'<p>Asdasd</p><p>Asdasd</p><p>Asdasda</p>',
],
['<p>Asdasd</p> <p>Asdasd</p>', '<p>Asdasd</p><p>Asdasd</p>'],
// Significant inline content between blocks is still wrapped in <p>.
['<p>a</p> hello <p>b</p>', '<p>a</p><p> hello </p><p>b</p>'],
// Spaces inside text / between inlines must be preserved.
['hello world', 'hello world'],
['<p>hello world</p>', '<p>hello world</p>'],
['<b>hello</b> <i>world</i>', '<b>hello</b> <i>world</i>'],
])('%s → %s', (input, expected) => {
expect(normalizeHtml(input)).toBe(expected);
});
});
});
29 changes: 23 additions & 6 deletions src/web/normalization/htmlNormalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,15 +350,33 @@ function escapeText(s: string): string {

// --- Blockquote content flattening ---

function isWhitespaceOnly(value: string): boolean {
for (let i = 0; i < value.length; i++) {
const c = value.charCodeAt(i);
// space, tab, LF, CR, FF
if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d && c !== 0x0c) {
return false;
}
}
return true;
}

/**
* Flush buffered inline content as a <p>. Inter-block whitespace (newlines /
* spaces between block tags in pretty-printed HTML) is discarded so it does
* not become empty paragraphs that later serialize as extra <br>s.
*/
function flushInlineP(
ib: { buf: string },
out: { buf: string },
attrs = ''
): void {
if (ib.buf.length > 0) {
): boolean {
const emitted = ib.buf.length > 0 && !isWhitespaceOnly(ib.buf);
if (emitted) {
out.buf += `<p${attrs}>${ib.buf}</p>`;
ib.buf = '';
}
ib.buf = '';
return emitted;
}

function flattenBqChildren(
Expand Down Expand Up @@ -525,9 +543,8 @@ function walkChildren(node: Element, out: { buf: string }): void {
break;
}
if (isElement(cur) && isBrNode(cur)) {
if (ib.buf.length > 0) {
flushInlineP(ib, out);
} else {
// Whitespace-only buffer is layout noise; treat like empty → <br>
if (!flushInlineP(ib, out)) {
out.buf += '<br>';
}
i++;
Expand Down
Loading