diff --git a/docs/changelog/1214.bugfix.rst b/docs/changelog/1214.bugfix.rst new file mode 100644 index 000000000..91c7640b9 --- /dev/null +++ b/docs/changelog/1214.bugfix.rst @@ -0,0 +1,2 @@ +``to_markdown`` keeps block content inside a heading on the heading line and reads a block edge in link text or a +heading as a space. diff --git a/src/turbohtml/_c/serialize/markdown.c b/src/turbohtml/_c/serialize/markdown.c index 1233d23ed..b5d10dfee 100644 --- a/src/turbohtml/_c/serialize/markdown.c +++ b/src/turbohtml/_c/serialize/markdown.c @@ -163,6 +163,7 @@ enum md_leave { MD_LEAVE_TABLE, MD_LEAVE_BLOCKQUOTE, /* mark a quote that opened no block of its own */ MD_LEAVE_BLOCK_END, /* the block inside an inline element is done */ + MD_LEAVE_SPACE, /* a flattened block's end reads as a space */ }; enum md_table_phase { @@ -1751,7 +1752,7 @@ static void md_render_inline_tag(md_ctx *ctx, th_node *node) { return; } if (is_md_block(atom)) { - if (!ctx->inline_only) { + if (!ctx->inline_only && !ctx->in_heading) { if (!ctx->in_cell) { /* the content after the block belongs to a new block, so note where it ends; a cell flattens its blocks onto its one line instead */ @@ -1765,10 +1766,13 @@ static void md_render_inline_tag(md_ctx *ctx, th_node *node) { md_enter_cell_flat(ctx, node, -1); return; } - /* inside link text a block cannot open its own line (a blank line would - split the CommonMark link), so it flattens to inline; its boundary still - reads as a space so adjacent words never fuse */ + /* inside link text or a heading a block cannot open its own line (a blank + line would split the CommonMark link, and a heading is one line), so it + flattens to inline; its boundary still reads as a space so adjacent words + never fuse */ ctx->space_pending = 1; + md_push(ctx, node, MD_WALK_INLINE, MD_LEAVE_SPACE); + return; } md_push(ctx, node, MD_WALK_INLINE, MD_LEAVE_NONE); } @@ -3223,6 +3227,9 @@ static void md_leave(md_ctx *ctx) { case MD_LEAVE_BLOCK_END: ctx->block_ended = 1; break; + case MD_LEAVE_SPACE: + ctx->space_pending = 1; + break; case MD_LEAVE_BLOCKQUOTE: /* a leading quote writes its marker only with its first block; CommonMark reads a lone ">" as an empty quote, so keep one when no block came */ diff --git a/tests/serialize/test_markdown.py b/tests/serialize/test_markdown.py index 32767e4d9..5c8e0975b 100644 --- a/tests/serialize/test_markdown.py +++ b/tests/serialize/test_markdown.py @@ -62,15 +62,23 @@ def md(html: str) -> str: pytest.param("
x
", "### x", id="heading-paragraph-stays-in-heading"), + pytest.param("b
c", "## a b c", id="heading-block-edges-read-as-spaces"), + pytest.param("b
").to_markdown(config) == "a b\n===" + + @pytest.mark.parametrize( ("html", "expected"), [ @@ -272,7 +280,7 @@ def test_code(html: str, expected: str) -> None: id="emphasis-adjacent-in-mtext-html", ), pytest.param( - ";i
", ";i", id="emphasis-close-punct-before-letter"), pytest.param("i;
", "i;", id="emphasis-open-letter-before-punct"), diff --git a/tests/test_fuzz_markdown_structure_generation.py b/tests/test_fuzz_markdown_structure_generation.py index 683d05c17..da4643211 100644 --- a/tests/test_fuzz_markdown_structure_generation.py +++ b/tests/test_fuzz_markdown_structure_generation.py @@ -184,6 +184,9 @@ def test_markdown_unsupported_element() -> None: pytest.param("ab", id="empty-block-breaks-code-line"), pytest.param("
| b |
x
", id="heading-paragraph"), + pytest.param("b
c", id="heading-text-around-block"), + pytest.param("