use super::*;
#[test]
fn section_xml_multiple_blocks_ordering() {
let xml = section_xml(vec![
Block::Heading {
level: 2,
inlines: vec![inline("Section")],
},
Block::Paragraph {
inlines: vec![inline("Body text")],
},
]);
let heading_pos = xml.find("Section").expect("heading text");
let para_pos = xml.find("Body text").expect("para text");
assert!(heading_pos < para_pos, "heading before paragraph: {xml}");
}
#[test]
fn section_xml_heading_has_numeric_style_id_ref() {
for level in 1u8..=6 {
let xml = section_xml(vec![Block::Heading {
level,
inlines: vec![inline("h")],
}]);
let expected = format!(r#"hp:styleIDRef="{level}""#);
assert!(
xml.contains(&expected),
"level {level}: expected numeric styleIDRef={level}, got: {xml}"
);
let old_form = format!(r#"hp:styleIDRef="Heading{level}""#);
assert!(
!xml.contains(&old_form),
"level {level}: string styleIDRef must not appear: {xml}"
);
}
}
#[test]
fn section_xml_code_block_has_numeric_char_pr_id_ref() {
let xml = section_xml(vec![Block::CodeBlock {
language: Some("rust".into()),
code: "let x = 1;".into(),
}]);
let marker = "charPrIDRef=\"";
let start = xml
.find(marker)
.expect("charPrIDRef attribute must be present");
let rest = &xml[start + marker.len()..];
let end = rest.find('"').expect("closing quote");
let value = &rest[..end];
assert!(
value.parse::<u32>().is_ok(),
"charPrIDRef value '{value}' must be a numeric u32, not a string like 'code': {xml}"
);
assert!(
xml.contains("let x = 1;"),
"code content must appear: {xml}"
);
}
#[test]
fn section_xml_paragraphs_have_sequential_ids() {
let xml = section_xml(vec![
Block::Heading {
level: 1,
inlines: vec![inline("First")],
},
Block::Paragraph {
inlines: vec![inline("Second")],
},
Block::CodeBlock {
language: None,
code: "third".into(),
},
]);
assert!(xml.contains(r#"id="0""#), "first block id=0: {xml}");
assert!(xml.contains(r#"id="1""#), "second block id=1: {xml}");
assert!(xml.contains(r#"id="2""#), "third block id=2: {xml}");
let id0_pos = xml.find(r#"id="0""#).expect("id=0 position");
let style_pos = xml
.find(r#"hp:styleIDRef="1""#)
.expect("styleIDRef=1 position");
assert!(
id0_pos < style_pos,
"id=0 must precede styleIDRef on the heading element: {xml}"
);
}
#[test]
fn section_xml_table_wrapped_in_paragraph() {
let cell = |text: &str| TableCell {
blocks: vec![Block::Paragraph {
inlines: vec![inline(text)],
}],
colspan: 1,
rowspan: 1,
};
let xml = section_xml(vec![Block::Table {
col_count: 2,
rows: vec![TableRow {
cells: vec![cell("X"), cell("Y")],
is_header: false,
}],
inner_margin: None,
}]);
assert!(
xml.contains(r#"id="0""#),
"table wrapper p must have id=0: {xml}"
);
assert!(
xml.contains(r#"<hp:run charPrIDRef="0">"#),
"run wrapper: {xml}"
);
assert!(
xml.contains(r#"rowCnt="1""#),
"tbl must have rowCnt=1: {xml}"
);
assert!(
xml.contains(r#"colCnt="2""#),
"tbl must have colCnt=2: {xml}"
);
assert!(xml.contains("<hp:tbl "), "tbl element: {xml}");
let p_pos = xml.find(r#"id="0""#).expect("p wrapper position");
let run_pos = xml
.find(r#"<hp:run charPrIDRef="0">"#)
.expect("run position");
let tbl_pos = xml.find("<hp:tbl ").expect("tbl position");
assert!(p_pos < run_pos, "p must come before run: {xml}");
assert!(run_pos < tbl_pos, "run must come before tbl: {xml}");
assert!(xml.contains(r#"id="1""#), "cell paragraph id=1: {xml}");
}
#[test]
fn section_xml_table_rowcnt_colcnt_attributes() {
let cell = || TableCell {
blocks: vec![],
colspan: 1,
rowspan: 1,
};
let xml = section_xml(vec![Block::Table {
col_count: 3,
rows: vec![
TableRow {
cells: vec![cell(), cell(), cell()],
is_header: false,
},
TableRow {
cells: vec![cell(), cell(), cell()],
is_header: false,
},
TableRow {
cells: vec![cell(), cell(), cell()],
is_header: false,
},
],
inner_margin: None,
}]);
assert!(xml.contains(r#"rowCnt="3""#), "rowCnt must be 3: {xml}");
assert!(xml.contains(r#"colCnt="3""#), "colCnt must be 3: {xml}");
}
#[test]
fn section_xml_inline_code_has_monospace_charpr_id_ref() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![
inline("text "),
Inline {
text: "code_val".into(),
code: true,
..Inline::default()
},
],
}]);
let run_count = xml.matches("<hp:run ").count();
assert!(
run_count >= 2,
"must have at least 2 runs (plain + code): {xml}"
);
assert!(xml.contains("code_val"), "code text missing: {xml}");
let marker = "charPrIDRef=\"";
let ids: Vec<&str> = xml
.match_indices(marker)
.map(|(pos, _)| {
let rest = &xml[pos + marker.len()..];
let end = rest.find('"').unwrap();
&rest[..end]
})
.collect();
assert!(
ids.len() >= 2,
"must have at least 2 charPrIDRef values: {ids:?}"
);
let has_nonzero = ids.iter().any(|&id| id != "0");
assert!(
has_nonzero,
"inline code must produce a non-zero charPrIDRef: {ids:?}"
);
}
#[test]
fn section_xml_blockquote_uses_para_pr_id_ref_1() {
let xml = section_xml(vec![Block::BlockQuote {
blocks: vec![Block::Paragraph {
inlines: vec![inline("indented text")],
}],
}]);
assert!(
xml.contains(r#"paraPrIDRef="1""#),
"blockquote paragraph must use paraPrIDRef=\"1\": {xml}"
);
assert!(
xml.contains("indented text"),
"blockquote content must be preserved: {xml}"
);
}
#[test]
fn section_xml_normal_paragraph_uses_para_pr_id_ref_0() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![inline("normal")],
}]);
assert!(
xml.contains(r#"paraPrIDRef="0""#),
"normal paragraph must use paraPrIDRef=\"0\": {xml}"
);
}
#[test]
fn section_xml_blockquote_and_normal_mixed() {
let xml = section_xml(vec![
Block::Paragraph {
inlines: vec![inline("before")],
},
Block::BlockQuote {
blocks: vec![Block::Paragraph {
inlines: vec![inline("quoted")],
}],
},
Block::Paragraph {
inlines: vec![inline("after")],
},
]);
assert!(
xml.contains(r#"paraPrIDRef="0""#),
"normal paragraphs must use paraPrIDRef=\"0\": {xml}"
);
assert!(
xml.contains(r#"paraPrIDRef="1""#),
"blockquote paragraph must use paraPrIDRef=\"1\": {xml}"
);
let before_pos = xml.find("before").expect("before text");
let quoted_pos = xml.find("quoted").expect("quoted text");
let after_pos = xml.find("after").expect("after text");
assert!(before_pos < quoted_pos, "before must precede quoted: {xml}");
assert!(quoted_pos < after_pos, "quoted must precede after: {xml}");
}
#[test]
fn section_xml_blockquote_multiple_children() {
let xml = section_xml(vec![Block::BlockQuote {
blocks: vec![
Block::Paragraph {
inlines: vec![inline("first quoted")],
},
Block::Paragraph {
inlines: vec![inline("second quoted")],
},
],
}]);
let count = xml.matches(r#"paraPrIDRef="1""#).count();
assert_eq!(
count, 2,
"both paragraphs in blockquote must use paraPrIDRef=\"1\", found {count}: {xml}"
);
assert!(
!xml.contains(r#"paraPrIDRef="0""#),
"no paragraph should use paraPrIDRef=\"0\" when all are quoted: {xml}"
);
}
#[test]
fn section_xml_blockquote_heading_uses_para_pr_id_ref_1() {
let xml = section_xml(vec![Block::BlockQuote {
blocks: vec![Block::Heading {
level: 2,
inlines: vec![inline("Quoted Heading")],
}],
}]);
assert!(
xml.contains(r#"paraPrIDRef="1""#),
"heading inside blockquote must use paraPrIDRef=\"1\": {xml}"
);
assert!(
xml.contains(r#"hp:styleIDRef="2""#),
"heading must still have its styleIDRef: {xml}"
);
}
#[test]
fn section_xml_nested_blockquote_still_uses_para_pr_id_ref_1() {
let xml = section_xml(vec![Block::BlockQuote {
blocks: vec![Block::BlockQuote {
blocks: vec![Block::Paragraph {
inlines: vec![inline("deeply quoted")],
}],
}],
}]);
assert!(
xml.contains(r#"paraPrIDRef="1""#),
"nested blockquote must use paraPrIDRef=\"1\": {xml}"
);
assert!(
xml.contains("deeply quoted"),
"nested blockquote content must be preserved: {xml}"
);
}