mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-23 02:40:15 +00:00
fix(renderer): drop empty trailing line from blockquote match
The new group-based blockquote rule introduced in this PR captures the
trailing newline in its (?:\n|$) clause. After block.split('\n') that
trailing newline produces an empty final element. The original filter
only dropped lone bare '>' artifacts on the last line, so the empty
final element survived, and the .map(blank → '<br>') step turned it
into a phantom <br> immediately before </blockquote>.
Visible symptom: any blockquote whose source ends with \n (the common
case — a quote followed by another paragraph or end-of-message) renders
with an extra blank line at the bottom of the quote.
Reproducer:
'> Hello\n\nThe rest of the message.'
→ '<blockquote>Hello\n<br></blockquote>\nThe rest of the message.'
^^^ phantom <br>
Fix: replace the single-line filter with a while-loop that pops trailing
lines while they are either empty OR a bare '>'. This matches the
intent the Python test mirror in tests/test_blockquote_rendering.py
already had (the mirror was correct; the JS was not — that's why
the original tests passed despite the bug).
Also add four new regression tests in TestNoPhantomTrailingBr that pin
the no-trailing-<br> invariant for the common shapes:
- input ending with \n
- quote followed by paragraph (the real-world case)
- multi-line quote ending with \n
- quote with blank continuation + trailing \n (internal <br> stays,
trailing <br> does not)
Verified end-to-end with node against the actual JS regex.
244 renderer-adjacent tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+10
-2
@@ -769,8 +769,16 @@ function renderMd(raw){
|
||||
// 3. After the fenced-code pass, lines of > preceding/following code
|
||||
// blocks were left as literals because .+ didn't match empty lines
|
||||
s=s.replace(/((?:^>[^\n]*(?:\n|$))+)/gm,block=>{
|
||||
const inner=block.split('\n')
|
||||
.filter((_,i,a)=>i<a.length-1||_.trim()!='>') // drop lone trailing '>' artifact
|
||||
const lines=block.split('\n');
|
||||
// Drop trailing artifacts: empty string from a trailing \n in the match
|
||||
// (split adds '' after the final \n) and lone bare '>' lines that
|
||||
// weren't intended as content. Without this, a blockquote whose source
|
||||
// ends with \n (the common case — anything followed by another block)
|
||||
// renders with a phantom <br> before </blockquote>.
|
||||
while(lines.length&&(lines[lines.length-1].trim()===''||lines[lines.length-1].trim()==='>')){
|
||||
lines.pop();
|
||||
}
|
||||
const inner=lines
|
||||
.map(l=>l.replace(/^>[ \t]?/,'')) // strip "> " or ">"
|
||||
.map(l=>l.trim()===''?'<br>':inlineMd(l)) // blank lines → <br>, text → inlineMd
|
||||
.join('\n');
|
||||
|
||||
@@ -162,6 +162,46 @@ class TestInlineMarkdownInsideBlockquote:
|
||||
assert "<blockquote>" in out
|
||||
|
||||
|
||||
class TestNoPhantomTrailingBr:
|
||||
"""The fix must drop both empty trailing lines (from a trailing \\n in the
|
||||
match) and bare '>' artifacts. Without this, the common case — a blockquote
|
||||
followed by another paragraph — renders with a phantom <br> right before
|
||||
</blockquote>, leaving a visible blank line at the bottom of the quote.
|
||||
"""
|
||||
|
||||
def test_input_ending_with_newline_no_trailing_br(self):
|
||||
"""`> Hello\\n` must NOT produce `<blockquote>Hello\\n<br></blockquote>`."""
|
||||
out = _apply_blockquote("> Hello\n")
|
||||
assert "<br></blockquote>" not in out, (
|
||||
f"Trailing <br> leaked inside the blockquote (phantom blank line): {out!r}"
|
||||
)
|
||||
|
||||
def test_blockquote_followed_by_paragraph_no_trailing_br(self):
|
||||
"""The common real-world shape: quote + blank line + paragraph."""
|
||||
src = "> Quoted text\n\nNormal paragraph"
|
||||
out = _apply_blockquote(src)
|
||||
assert "<br></blockquote>" not in out, (
|
||||
f"Trailing <br> leaked inside blockquote when followed by paragraph: {out!r}"
|
||||
)
|
||||
|
||||
def test_multiline_quote_ending_with_newline_no_trailing_br(self):
|
||||
out = _apply_blockquote("> Line one\n> Line two\n")
|
||||
assert "<br></blockquote>" not in out, (
|
||||
f"Multi-line quote ending with \\n must not leave a trailing <br>: {out!r}"
|
||||
)
|
||||
|
||||
def test_quote_with_blank_continuation_then_newline(self):
|
||||
"""`> A\\n>\\n> B\\n` — the internal `<br>` for the blank line stays,
|
||||
but the trailing newline must not add a second `<br>` at the end."""
|
||||
out = _apply_blockquote("> A\n>\n> B\n")
|
||||
# Internal <br> for the blank-line continuation is intentional
|
||||
assert "<br>" in out
|
||||
# But there must not be a <br> immediately before the closing tag
|
||||
assert "<br></blockquote>" not in out, (
|
||||
f"Trailing <br> leaked at end of blockquote: {out!r}"
|
||||
)
|
||||
|
||||
|
||||
class TestBlockquoteFollowedByParagraph:
|
||||
"""A blockquote followed by a normal paragraph must not bleed into each other."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user