Skip to content

Commit 4a70d7b

Browse files
committed
fix: ensure headers always start on new lines
- Add ensureBlockStart() helper to check accumulated output - Call before rendering block elements (HEADING, PARAGRAPH, CODE_BLOCK, LIST, BLOCKQUOTE, RULE) - Add test case TestPerformMarkdownTaskHeadersOnNewlines - Add HTML fixture reproducing inline-to-block transition edge case - Verify no headers appear immediately after non-newline characters Fixes #4
1 parent 8e2dfdb commit 4a70d7b

3 files changed

Lines changed: 63 additions & 0 deletions

File tree

engine/js/markdown.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,32 @@
303303
return result.replace(/\n{3,}/g, '\n\n').trim();
304304
}
305305

306+
/**
307+
* Ensures a block-level element starts on a new line
308+
* Block elements should be preceded by at least one newline character.
309+
* If the last character in the output buffer is not a newline, add newlines.
310+
* @param {Array} lines - Accumulator for output lines
311+
*/
312+
function ensureBlockStart(lines) {
313+
if (lines.length === 0) return;
314+
315+
// Join all lines to get the current output
316+
const output = lines.join('');
317+
318+
// If output is empty or already ends with newline, no need to add more
319+
if (output.length === 0 || output.endsWith('\n\n')) {
320+
return;
321+
}
322+
323+
// If output ends with one newline, add one more for proper spacing
324+
if (output.endsWith('\n')) {
325+
lines.push('\n');
326+
} else {
327+
// If output doesn't end with newline, add two newlines
328+
lines.push('\n\n');
329+
}
330+
}
331+
306332
/**
307333
* Renders a single AST node
308334
* @param {Object} node - The AST node to render
@@ -317,12 +343,14 @@
317343
break;
318344

319345
case NodeType.HEADING:
346+
ensureBlockStart(lines);
320347
const hashes = '#'.repeat(node.attrs.level);
321348
const headingText = renderInlineChildren(node.children);
322349
lines.push(hashes + ' ' + headingText + '\n\n');
323350
break;
324351

325352
case NodeType.PARAGRAPH:
353+
ensureBlockStart(lines);
326354
const paraText = renderInlineChildren(node.children);
327355
if (paraText.trim().length > 0) {
328356
lines.push(paraText + '\n\n');
@@ -379,11 +407,13 @@
379407
break;
380408

381409
case NodeType.CODE_BLOCK:
410+
ensureBlockStart(lines);
382411
const lang = node.attrs.lang || '';
383412
lines.push('```' + lang + '\n' + node.content + '\n```\n\n');
384413
break;
385414

386415
case NodeType.LIST:
416+
ensureBlockStart(lines);
387417
renderList(node, lines);
388418
break;
389419

@@ -392,6 +422,7 @@
392422
break;
393423

394424
case NodeType.BLOCKQUOTE:
425+
ensureBlockStart(lines);
395426
const quoteLines = [];
396427
renderChildren(node.children, quoteLines);
397428
const quoteText = quoteLines.join('').trim();
@@ -411,6 +442,7 @@
411442
break;
412443

413444
case NodeType.RULE:
445+
ensureBlockStart(lines);
414446
lines.push('---\n\n');
415447
break;
416448
}

engine/task_markdown_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,26 @@ func TestPerformMarkdownTaskBlockquoteWithNestedContent(t *testing.T) {
201201
assert.Contains(t, mr.Content, "> ")
202202
assert.Contains(t, mr.Content, "Quote with")
203203
}
204+
205+
func TestPerformMarkdownTaskHeadersOnNewlines(t *testing.T) {
206+
// Test that headers are always preceded by a newline (except at start)
207+
// This tests that inline content followed by a header has proper separation
208+
server := pagetest.NewTestWebServer("markdown")
209+
task := NewTask("markdown", server.URL)
210+
211+
ctx, cancel := pagetest.NewTestContext()
212+
defer cancel()
213+
214+
mr, err := performMarkdownTask(ctx, &task, virgo.Logger())
215+
216+
require.NoError(t, err)
217+
assert.NotEmpty(t, mr.Content)
218+
219+
// Headers must always be preceded by a newline (except at start)
220+
// This tests that inline content followed by a header has proper separation
221+
assert.Contains(t, mr.Content, "text before heading\n\n# Inline to Block Test")
222+
223+
// Verify no headers appear immediately after inline content (single newline is not enough)
224+
// Headers should have double newlines before them
225+
assert.NotRegexp(t, `[^\n]\n#+ `, mr.Content)
226+
}

internal/pagetest/testdata/markdown/index.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ <h3>Complex Blockquote</h3>
103103
<li>List in quote</li>
104104
</ul>
105105
</blockquote>
106+
107+
<!-- Inline content immediately before block element -->
108+
<h3>Inline to Block Transition</h3>
109+
<div>
110+
<span>text before heading</span>
111+
<h1>Inline to Block Test</h1>
112+
<p>This paragraph follows the heading.</p>
113+
</div>
106114
</main>
107115

108116
<footer>

0 commit comments

Comments
 (0)