use super::*;
#[test]
fn section_xml_empty_section_produces_valid_wrapper() {
let xml = section_xml(vec![]);
assert!(xml.contains("<hs:sec"), "root element missing: {xml}");
assert!(xml.contains("</hs:sec>"), "closing tag missing: {xml}");
assert!(xml.contains(r#"xmlns:hs="http://www.hancom.co.kr/hwpml/2011/section""#));
}
#[test]
fn section_xml_paragraph_plain_text() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![inline("hello world")],
}]);
assert!(xml.contains("<hp:p "), "paragraph open: {xml}");
assert!(xml.contains("</hp:p>"), "paragraph close: {xml}");
assert!(xml.contains("<hp:t>"), "text run open: {xml}");
assert!(xml.contains("hello world"), "text content: {xml}");
}
#[test]
fn section_xml_empty_paragraph() {
let xml = section_xml(vec![Block::Paragraph { inlines: vec![] }]);
assert!(xml.contains("<hp:p "));
assert!(xml.contains("</hp:p>"));
}
#[test]
fn section_xml_heading_level_1() {
let xml = section_xml(vec![Block::Heading {
level: 1,
inlines: vec![inline("Title")],
}]);
assert!(
xml.contains(r#"hp:styleIDRef="1""#),
"h1 style ref must be numeric 1: {xml}"
);
assert!(xml.contains("Title"));
}
#[test]
fn section_xml_heading_level_6() {
let xml = section_xml(vec![Block::Heading {
level: 6,
inlines: vec![inline("Deep")],
}]);
assert!(xml.contains(r#"hp:styleIDRef="6""#), "{xml}");
assert!(xml.contains("Deep"));
}
#[test]
fn section_xml_bold_inline_has_charpr_id_ref() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![bold_inline("strong")],
}]);
assert!(
xml.contains("<hp:charPr "),
"inline charPr must be emitted for bold: {xml}"
);
assert!(
xml.contains(r#"bold="true""#),
"bold attribute must appear on inline charPr: {xml}"
);
assert!(xml.contains("charPrIDRef="), "charPrIDRef: {xml}");
assert!(xml.contains("strong"));
}
#[test]
fn section_xml_italic_inline_has_charpr_id_ref() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![italic_inline("em")],
}]);
assert!(xml.contains("charPrIDRef="), "{xml}");
}
#[test]
fn section_xml_underline_inline_has_charpr_id_ref() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![underline_inline("ul")],
}]);
assert!(xml.contains("charPrIDRef="), "{xml}");
}
#[test]
fn section_xml_strikethrough_inline_has_charpr_id_ref() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![Inline {
text: "del".into(),
strikethrough: true,
..Inline::default()
}],
}]);
assert!(xml.contains("charPrIDRef="), "{xml}");
}
#[test]
fn section_xml_plain_inline_has_no_charpr() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![inline("plain")],
}]);
assert!(
!xml.contains("<hp:charPr"),
"unexpected inline charPr: {xml}"
);
}
#[test]
fn section_xml_plain_inline_has_charpr_id_ref() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![inline("plain")],
}]);
assert!(
xml.contains(r#"charPrIDRef="0""#),
"charPrIDRef missing: {xml}"
);
}
#[test]
fn section_xml_bold_inline_has_nonzero_charpr_id_ref() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![bold_inline("bold")],
}]);
assert!(xml.contains("charPrIDRef="), "charPrIDRef missing: {xml}");
}
#[test]
fn section_xml_paragraph_has_para_pr_id_ref() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![inline("text")],
}]);
assert!(
xml.contains(r#"paraPrIDRef="0""#),
"paraPrIDRef missing: {xml}"
);
}
#[test]
fn section_xml_nested_inlines_bold_then_italic() {
let xml = section_xml(vec![Block::Paragraph {
inlines: vec![bold_inline("B"), italic_inline("I")],
}]);
assert!(xml.contains("charPrIDRef="), "{xml}");
assert!(xml.contains("B"));
assert!(xml.contains("I"));
}
#[test]
fn section_xml_image_block() {
let xml = section_xml(vec![Block::Image {
src: "image001.png".into(),
alt: "a cat".into(),
}]);
assert!(xml.contains("<hp:p "), "{xml}");
assert!(
xml.contains(r#"hp:binaryItemIDRef="image001.png""#),
"{xml}"
);
assert!(xml.contains(r#"alt="a cat""#), "{xml}");
assert!(xml.contains("<hp:img"), "{xml}");
assert!(
xml.contains(r#"<hp:run charPrIDRef="0">"#),
"image run wrapper missing: {xml}"
);
assert!(xml.contains("<hp:pic>"), "hp:pic wrapper missing: {xml}");
assert!(xml.contains("</hp:pic>"), "hp:pic close missing: {xml}");
let run_pos = xml
.find(r#"<hp:run charPrIDRef="0">"#)
.expect("run position");
let pic_pos = xml.find("<hp:pic>").expect("pic position");
let img_pos = xml.find("<hp:img").expect("img position");
assert!(run_pos < pic_pos, "run must precede pic: {xml}");
assert!(pic_pos < img_pos, "pic must precede img: {xml}");
}
#[test]
fn section_xml_table_2x2() {
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("A"), cell("B")],
is_header: false,
},
TableRow {
cells: vec![cell("C"), cell("D")],
is_header: false,
},
],
}]);
assert!(xml.contains("<hp:p "), "p wrapper: {xml}");
assert!(
xml.contains(r#"<hp:run charPrIDRef="0">"#),
"run wrapper: {xml}"
);
assert!(
xml.contains(r#"<hp:tbl rowCnt="2" colCnt="2">"#),
"tbl with rowCnt/colCnt: {xml}"
);
assert!(xml.contains("</hp:tbl>"), "tbl close: {xml}");
assert_eq!(xml.matches("<hp:tr>").count(), 2, "two rows: {xml}");
assert_eq!(xml.matches("<hp:tc>").count(), 4, "four cells: {xml}");
assert!(xml.contains("A"), "{xml}");
assert!(xml.contains("D"), "{xml}");
}
#[test]
fn section_xml_table_colspan_rowspan_present() {
let wide_cell = TableCell {
blocks: vec![],
colspan: 2,
rowspan: 1,
};
let xml = section_xml(vec![Block::Table {
col_count: 2,
rows: vec![TableRow {
cells: vec![wide_cell],
is_header: false,
}],
}]);
assert!(xml.contains("<hp:tc>"), "plain tc emitted: {xml}");
assert!(
xml.contains("colSpan=\"2\""),
"colSpan should appear: {xml}"
);
assert!(
xml.contains("rowSpan=\"1\""),
"rowSpan should appear: {xml}"
);
assert!(xml.contains("<hp:cellAddr"), "cellAddr element: {xml}");
}
#[test]
fn section_xml_table_no_celladdr_for_1x1() {
let cell = TableCell {
blocks: vec![Block::Paragraph {
inlines: vec![inline("normal")],
}],
colspan: 1,
rowspan: 1,
};
let xml = section_xml(vec![Block::Table {
col_count: 1,
rows: vec![TableRow {
cells: vec![cell],
is_header: false,
}],
}]);
assert!(xml.contains("<hp:tc>"), "tc emitted: {xml}");
assert!(
!xml.contains("<hp:cellAddr"),
"cellAddr must NOT appear for 1x1: {xml}"
);
}
#[test]
fn section_xml_math_block() {
let xml = section_xml(vec![Block::Math {
display: true,
tex: r"E = mc^2".into(),
}]);
assert!(xml.contains("<hp:equation>"), "equation open: {xml}");
assert!(xml.contains("</hp:equation>"), "equation close: {xml}");
assert!(xml.contains(r"E = mc^2"), "{xml}");
assert!(
xml.contains(r#"<hp:run charPrIDRef="0">"#),
"equation run wrapper missing: {xml}"
);
let run_pos = xml
.find(r#"<hp:run charPrIDRef="0">"#)
.expect("run position");
let eq_pos = xml.find("<hp:equation>").expect("equation position");
assert!(run_pos < eq_pos, "run must precede equation: {xml}");
}
#[test]
fn section_xml_ordered_list() {
let xml = section_xml(vec![Block::List {
ordered: true,
start: 1,
items: vec![
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("first")],
}],
children: vec![],
},
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("second")],
}],
children: vec![],
},
],
}]);
assert!(xml.contains("first"), "{xml}");
assert!(xml.contains("second"), "{xml}");
assert_eq!(xml.matches("<hp:p ").count(), 2, "{xml}");
}
#[test]
fn section_xml_unordered_list() {
let xml = section_xml(vec![Block::List {
ordered: false,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("bullet")],
}],
children: vec![],
}],
}]);
assert!(xml.contains("bullet"), "{xml}");
}
#[test]
fn section_xml_footnote_block() {
let xml = section_xml(vec![Block::Footnote {
id: "fn1".into(),
content: vec![Block::Paragraph {
inlines: vec![inline("footnote text")],
}],
}]);
assert!(xml.contains("footnote text"), "{xml}");
}
#[test]
fn section_xml_blockquote() {
let xml = section_xml(vec![Block::BlockQuote {
blocks: vec![Block::Paragraph {
inlines: vec![inline("quoted")],
}],
}]);
assert!(xml.contains("quoted"), "{xml}");
assert!(xml.contains("<hp:p "), "{xml}");
}
#[test]
fn section_xml_horizontal_rule() {
let xml = section_xml(vec![Block::HorizontalRule]);
assert!(xml.contains("<hp:p "), "{xml}");
assert!(xml.contains("───"), "{xml}");
}
#[test]
fn section_xml_code_block() {
let xml = section_xml(vec![Block::CodeBlock {
language: Some("rust".into()),
code: "fn main() {}".into(),
}]);
assert!(xml.contains("<hp:p "), "{xml}");
assert!(
!xml.contains(r#"charPrIDRef="code""#),
"charPrIDRef must not be the string 'code': {xml}"
);
assert!(
xml.contains("charPrIDRef="),
"charPrIDRef must be present: {xml}"
);
assert!(xml.contains("fn main() {}"), "{xml}");
}
#[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,
}],
}]);
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#"<hp:tbl rowCnt="1" colCnt="2">"#),
"tbl attrs: {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(r#"<hp:tbl rowCnt="1" colCnt="2">"#)
.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,
},
],
}]);
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:?}"
);
}