use super::tests::first_section_blocks;
use super::tests_inline::parse_with_unsafe_html;
use super::*;
use crate::ir;
#[test]
fn parse_markdown_pagebreak_html_comment_yields_page_break_block() {
let doc = parse_markdown("before\n\n<!-- pagebreak -->\n\nafter\n");
let blocks = first_section_blocks(&doc);
let kinds: Vec<&'static str> = blocks
.iter()
.map(|b| match b {
ir::Block::Paragraph { .. } => "para",
ir::Block::PageBreak => "pb",
_ => "other",
})
.collect();
assert_eq!(
kinds,
vec!["para", "pb", "para"],
"expected exact [para, pb, para] sequence: {blocks:?}"
);
}
#[test]
fn parse_markdown_pagebreak_marker_is_case_insensitive() {
let doc = parse_markdown("text\n\n<!-- PageBreak -->\n\nmore\n");
let blocks = first_section_blocks(&doc);
assert!(
blocks.iter().any(|b| matches!(b, ir::Block::PageBreak)),
"case-insensitive marker should yield PageBreak: {blocks:?}"
);
}
#[test]
fn parse_markdown_unrelated_html_comment_is_not_pagebreak() {
let doc = parse_markdown("text\n\n<!-- not a page break -->\n\nmore\n");
let blocks = first_section_blocks(&doc);
assert!(
!blocks.iter().any(|b| matches!(b, ir::Block::PageBreak)),
"non-pagebreak HTML comment must not yield PageBreak: {blocks:?}"
);
}
#[test]
fn parse_markdown_pagebreak_lookalike_substring_is_rejected() {
let doc = parse_markdown("a\n\n<!-- pagebreakish -->\n\nb\n");
let blocks = first_section_blocks(&doc);
assert!(
!blocks.iter().any(|b| matches!(b, ir::Block::PageBreak)),
"lookalike `pagebreakish` must not match: {blocks:?}"
);
}
#[test]
fn parse_markdown_pagebreak_marker_with_trailing_text_is_rejected() {
let doc = parse_markdown("a\n\n<!-- pagebreak --> trailing\n\nb\n");
let blocks = first_section_blocks(&doc);
assert!(
!blocks.iter().any(|b| matches!(b, ir::Block::PageBreak)),
"marker with trailing text must not match: {blocks:?}"
);
}
#[test]
fn header_footer_markers_parsed_to_section() {
let md = "\
<!-- header -->
Header line
<!-- /header -->
<!-- footer -->
Footer line
<!-- /footer -->
Body paragraph
";
let doc = parse_markdown(md);
let section = doc.sections.first().expect("section must exist");
let body_text: String = section
.blocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
body_text.contains("Body paragraph"),
"body paragraph missing; got: {body_text:?}"
);
assert!(
!body_text.contains("Header line"),
"header text must not appear in body; got: {body_text:?}"
);
assert!(
!body_text.contains("Footer line"),
"footer text must not appear in body; got: {body_text:?}"
);
let header_blocks = section
.header
.as_ref()
.expect("section.header must be Some");
let header_text: String = header_blocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
header_text.contains("Header line"),
"header text not found; got: {header_text:?}"
);
let footer_blocks = section
.footer
.as_ref()
.expect("section.footer must be Some");
let footer_text: String = footer_blocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
footer_text.contains("Footer line"),
"footer text not found; got: {footer_text:?}"
);
}
#[test]
fn header_only_marker() {
let md = "\
<!-- header -->
Just a header
<!-- /header -->
Body text
";
let doc = parse_markdown(md);
let section = doc.sections.first().expect("section must exist");
let header = section
.header
.as_ref()
.expect("section.header must be Some");
let htext: String = header
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
htext.contains("Just a header"),
"header text not found; got: {htext:?}"
);
assert!(
section.footer.is_none(),
"footer must be None when no footer marker present"
);
}
#[test]
fn footer_only_marker() {
let md = "\
<!-- footer -->
Just a footer
<!-- /footer -->
Body text
";
let doc = parse_markdown(md);
let section = doc.sections.first().expect("section must exist");
assert!(
section.header.is_none(),
"header must be None when no header marker present"
);
let footer = section
.footer
.as_ref()
.expect("section.footer must be Some");
let ftext: String = footer
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
ftext.contains("Just a footer"),
"footer text not found; got: {ftext:?}"
);
}
#[test]
fn no_markers_leaves_header_footer_none() {
let doc = parse_markdown("# Heading\n\nParagraph.\n");
let section = doc.sections.first().expect("section must exist");
assert!(
section.header.is_none(),
"header must be None when no markers are present"
);
assert!(
section.footer.is_none(),
"footer must be None when no markers are present"
);
}
#[test]
fn markers_case_insensitive() {
let md = "\
<!-- HEADER -->
Upper case header
<!-- /HEADER -->
<!-- FOOTER -->
Upper case footer
<!-- /FOOTER -->
Body
";
let doc = parse_markdown(md);
let section = doc.sections.first().expect("section must exist");
let header = section
.header
.as_ref()
.expect("header must be Some for HEADER marker");
let htext: String = header
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
htext.contains("Upper case header"),
"case-insensitive HEADER not matched; got: {htext:?}"
);
let footer = section
.footer
.as_ref()
.expect("footer must be Some for FOOTER marker");
let ftext: String = footer
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
ftext.contains("Upper case footer"),
"case-insensitive FOOTER not matched; got: {ftext:?}"
);
}
#[test]
fn header_footer_markers_roundtrip_via_write_then_parse() {
use crate::md::write_markdown;
let header_text = "Running header text";
let footer_text = "Running footer text";
let body_text = "Main body content";
let mut doc = ir::Document::new();
doc.sections.push(ir::Section {
blocks: vec![ir::Block::Paragraph {
inlines: vec![ir::Inline::plain(body_text)],
}],
page_layout: None,
header: Some(vec![ir::Block::Paragraph {
inlines: vec![ir::Inline::plain(header_text)],
}]),
footer: Some(vec![ir::Block::Paragraph {
inlines: vec![ir::Inline::plain(footer_text)],
}]),
header_footer_type: None,
..Default::default()
});
let md = write_markdown(&doc, false);
assert!(
md.contains("<!-- header -->"),
"header open marker missing in MD output; md: {md:?}"
);
assert!(
md.contains("<!-- /header -->"),
"header close marker missing in MD output; md: {md:?}"
);
assert!(
md.contains("<!-- footer -->"),
"footer open marker missing in MD output; md: {md:?}"
);
assert!(
md.contains("<!-- /footer -->"),
"footer close marker missing in MD output; md: {md:?}"
);
let parsed = parse_markdown(&md);
let section = parsed.sections.first().expect("section must exist");
let body: String = section
.blocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
body.contains(body_text),
"body text lost after roundtrip; body: {body:?}"
);
assert!(
!body.contains(header_text),
"header text leaked into body; body: {body:?}"
);
assert!(
!body.contains(footer_text),
"footer text leaked into body; body: {body:?}"
);
let header_blocks = section
.header
.as_ref()
.expect("section.header must be Some");
let htext: String = header_blocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
htext.contains(header_text),
"header text lost after roundtrip; got: {htext:?}"
);
let footer_blocks = section
.footer
.as_ref()
.expect("section.footer must be Some");
let ftext: String = footer_blocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
ftext.contains(footer_text),
"footer text lost after roundtrip; got: {ftext:?}"
);
}
#[test]
fn unclosed_header_marker_falls_back_to_body() {
let md = "\
<!-- header -->
Orphaned header text
Body paragraph
";
let doc = parse_markdown(md);
let section = doc.sections.first().expect("section must exist");
assert!(
section.header.is_none(),
"unclosed header marker must leave section.header as None; got: {:?}",
section.header
);
let body_text: String = section
.blocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
body_text.contains("Orphaned header text"),
"orphaned header content must fall back into body; got: {body_text:?}"
);
assert!(
body_text.contains("Body paragraph"),
"normal body paragraph must be present; got: {body_text:?}"
);
}
#[test]
fn unclosed_footer_marker_falls_back_to_body() {
let md = "\
<!-- footer -->
Orphaned footer text
Body paragraph
";
let doc = parse_markdown(md);
let section = doc.sections.first().expect("section must exist");
assert!(
section.footer.is_none(),
"unclosed footer marker must leave section.footer as None; got: {:?}",
section.footer
);
let body_text: String = section
.blocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
body_text.contains("Orphaned footer text"),
"orphaned footer content must fall back into body; got: {body_text:?}"
);
assert!(
body_text.contains("Body paragraph"),
"normal body paragraph must be present; got: {body_text:?}"
);
}
#[test]
fn empty_header_marker_region() {
let md = "\
<!-- header -->
<!-- /header -->
Body paragraph
";
let doc = parse_markdown(md);
let section = doc.sections.first().expect("section must exist");
assert!(
section.header.is_none(),
"empty header region must produce section.header = None; got: {:?}",
section.header
);
let body_text: String = section
.blocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
assert!(
body_text.contains("Body paragraph"),
"body paragraph must be preserved; got: {body_text:?}"
);
}
#[test]
fn interleaved_markers_body_fallback() {
let md = "\
<!-- header -->
text1
<!-- footer -->
text2
<!-- /header -->
";
let doc = parse_markdown(md);
let section = doc.sections.first().expect("section must exist");
let mut all_text = String::new();
let body_part: String = section
.blocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
all_text.push_str(&body_part);
if let Some(hblocks) = §ion.header {
let hpart: String = hblocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
all_text.push_str(&hpart);
}
if let Some(fblocks) = §ion.footer {
let fpart: String = fblocks
.iter()
.filter_map(|b| {
if let ir::Block::Paragraph { inlines } = b {
Some(inlines.iter().map(|i| i.text.as_str()).collect::<String>())
} else {
None
}
})
.collect();
all_text.push_str(&fpart);
}
assert!(
all_text.contains("text1"),
"text1 must not be lost in interleaved-marker document; all_text: {all_text:?}"
);
assert!(
all_text.contains("text2"),
"text2 must not be lost in interleaved-marker document; all_text: {all_text:?}"
);
}
#[test]
fn ruby_html_parsed_to_inline() {
let inlines = parse_with_unsafe_html("<ruby>漢字<rt>かんじ</rt></ruby>\n");
let found = inlines
.iter()
.any(|i| i.text == "漢字" && i.ruby.as_deref() == Some("かんじ"));
assert!(
found,
"<ruby>漢字<rt>かんじ</rt></ruby>: expected inline with text='漢字' ruby=Some('かんじ'); got {inlines:?}"
);
}
#[test]
fn ruby_roundtrip_via_write_then_parse() {
use crate::md::write_markdown;
let mut doc = ir::Document::new();
doc.sections.push(ir::Section {
blocks: vec![ir::Block::Paragraph {
inlines: vec![ir::Inline {
text: "漢字".into(),
ruby: Some("かんじ".into()),
..ir::Inline::default()
}],
}],
page_layout: None,
header: None,
footer: None,
header_footer_type: None,
..Default::default()
});
let md = write_markdown(&doc, false);
assert!(
md.contains("<ruby>") && md.contains("<rt>"),
"ruby HTML not found in written markdown; md: {md:?}"
);
let mut options = comrak::Options::default();
options.extension.table = true;
options.extension.strikethrough = true;
options.extension.footnotes = true;
options.extension.math_dollars = true;
options.extension.superscript = true;
options.extension.tasklist = true;
options.render.unsafe_ = true;
let arena = comrak::Arena::new();
let root = comrak::parse_document(&arena, &md, &options);
let para = root
.children()
.find(|c| matches!(c.data.borrow().value, NodeValue::Paragraph))
.expect("paragraph not found after roundtrip");
let inlines = collect_inlines(para);
let found = inlines
.iter()
.any(|i| i.text == "漢字" && i.ruby.as_deref() == Some("かんじ"));
assert!(
found,
"ruby annotation lost after roundtrip; inlines: {inlines:?}"
);
}
#[test]
fn ruby_without_rt_produces_no_annotation() {
let inlines = parse_with_unsafe_html("<ruby>text</ruby>\n");
let has_text = inlines.iter().any(|i| i.text.contains("text"));
assert!(
has_text,
"<ruby>text</ruby>: base text 'text' missing; got {inlines:?}"
);
let has_annotation = inlines.iter().any(|i| i.ruby.is_some());
assert!(
!has_annotation,
"<ruby>text</ruby>: unexpected ruby annotation; got {inlines:?}"
);
}
#[test]
fn ruby_with_bold_base() {
let inlines = parse_with_unsafe_html("<ruby>**bold**<rt>anno</rt></ruby>\n");
let found = inlines
.iter()
.any(|i| i.text.contains("bold") && i.ruby.as_deref() == Some("anno"));
assert!(
found,
"<ruby>**bold**<rt>anno</rt></ruby>: expected bold inline with ruby='anno'; got {inlines:?}"
);
}