diff --git a/src/render/markdown/anchors.rs b/src/render/markdown/anchors.rs index d56a21cb..d2e73a23 100644 --- a/src/render/markdown/anchors.rs +++ b/src/render/markdown/anchors.rs @@ -50,7 +50,10 @@ pub(crate) fn resolve_anchors(doc: &Document) -> AnchorMap { // inside the heading content) to those slugs. walk_blocks(&doc.blocks, &mut |block| { if let Block::Heading { anchor, content, .. } = block { - let Some(slug) = ids.claim(gfm_slug(&inlines_to_plain_text(content))) else { + // A line break renders as a space in a heading, and the slug is + // taken from the rendered text. + let text = inlines_to_plain_text(content).replace('\n', " "); + let Some(slug) = ids.claim(gfm_slug(&text)) else { return; }; let mut bind = |id: &str| { diff --git a/src/render/markdown/tests.rs b/src/render/markdown/tests.rs index 0a7b19a4..3594061f 100644 --- a/src/render/markdown/tests.rs +++ b/src/render/markdown/tests.rs @@ -742,6 +742,24 @@ fn heading_coincident_anchor_uses_slug() { assert_eq!(md, "## Section Two\n\n[go](#section-two)\n"); } +#[test] +fn heading_line_break_slugs_as_the_space_it_renders() { + // A heading renders a line break as a space, so the slug a link to it + // uses must separate the words the same way. + let md = doc(vec![ + Block::Paragraph(vec![Inline::Link { + content: vec![Inline::plain("go")], + target: LinkTarget::Anchor("toc1".into()), + }]), + Block::Heading { + level: 1, + anchor: Some("toc1".into()), + content: vec![Inline::plain("Part One"), Inline::LineBreak, Inline::plain("Overview")], + }, + ]); + assert_eq!(md, "[go](#part-one-overview)\n\n# Part One Overview\n"); +} + #[test] fn duplicate_heading_slugs_deduped() { let md = doc(vec![