use super::*;
#[test]
fn header_xml_contains_numbering_list() {
let doc = doc_with_section(vec![Block::List {
ordered: false,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("item")],
}],
children: vec![],
}],
}]);
let tables = RefTables::build(&doc);
let header =
super::header::generate_header_xml(&doc, &tables).expect("generate_header_xml failed");
assert!(
header.contains("<hh:numberings"),
"header.xml must contain <hh:numberings>:\n{header}"
);
assert!(
header.contains("</hh:numberings>"),
"header.xml must close </hh:numberings>:\n{header}"
);
assert!(
header.contains(r#"<hh:numberings itemCnt="1""#),
"numberings must have itemCnt=\"1\" (ordered only):\n{header}"
);
}
#[test]
fn header_xml_numbering_list_always_present() {
let doc = doc_with_section(vec![Block::Paragraph {
inlines: vec![inline("no lists here")],
}]);
let tables = RefTables::build(&doc);
let header =
super::header::generate_header_xml(&doc, &tables).expect("generate_header_xml failed");
assert!(
header.contains("<hh:numberings"),
"numberings must appear even in documents without lists:\n{header}"
);
}
#[test]
fn header_xml_numbering_id1_is_bullet() {
let doc = doc_with_section(vec![]);
let tables = RefTables::build(&doc);
let header =
super::header::generate_header_xml(&doc, &tables).expect("generate_header_xml failed");
let id1_pos = header
.find(r#"<hh:numbering id="1""#)
.expect("numbering id=1 must exist");
assert!(
!header.contains(r#"<hh:numbering id="2""#),
"numbering id=2 must NOT exist; only one entry is registered:\n{header}"
);
let slice_id1 = &header[id1_pos..];
assert!(
slice_id1.contains(r#"numFormat="DIGIT""#),
"numbering id=1 must have numFormat=\"DIGIT\":\n{slice_id1}"
);
assert!(
slice_id1.contains("%d."),
"numbering id=1 must contain \"%d.\" format string:\n{slice_id1}"
);
}
#[test]
fn header_xml_numbering_id2_is_digit() {
let doc = doc_with_section(vec![]);
let tables = RefTables::build(&doc);
let header =
super::header::generate_header_xml(&doc, &tables).expect("generate_header_xml failed");
let id1_pos = header
.find(r#"<hh:numbering id="1""#)
.expect("numbering id=1 (DIGIT) must exist");
let slice_id1 = &header[id1_pos..];
assert!(
slice_id1.contains(r#"numFormat="DIGIT""#),
"numbering id=1 must have numFormat=\"DIGIT\":\n{slice_id1}"
);
assert!(
slice_id1.contains("%d."),
"numbering id=1 must contain \"%d.\" format string:\n{slice_id1}"
);
}
#[test]
fn header_xml_para_properties_has_four_entries() {
let doc = doc_with_section(vec![]);
let tables = RefTables::build(&doc);
let header =
super::header::generate_header_xml(&doc, &tables).expect("generate_header_xml failed");
assert!(
header.contains(r#"<hh:paraProperties itemCnt="5""#),
"paraProperties must have itemCnt=\"5\":\n{header}"
);
assert!(
header.contains(r#"<hh:paraPr id="0""#),
"paraPr id=0 must exist:\n{header}"
);
assert!(
header.contains(r#"<hh:paraPr id="1""#),
"paraPr id=1 must exist:\n{header}"
);
assert!(
header.contains(r#"<hh:paraPr id="2""#),
"paraPr id=2 must exist:\n{header}"
);
assert!(
header.contains(r#"<hh:paraPr id="3""#),
"paraPr id=3 must exist:\n{header}"
);
assert!(
header.contains(r#"<hh:paraPr id="4""#),
"paraPr id=4 (heading) must exist:\n{header}"
);
}
#[test]
fn section_xml_unordered_list_item_has_num_pr_id_ref_1() {
let xml = section_xml(vec![Block::List {
ordered: false,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("bullet item")],
}],
children: vec![],
}],
}]);
assert!(
!xml.contains("numPrIDRef"),
"unordered list item must NOT have a numPrIDRef attribute (no BULLET numbering):\n{xml}"
);
assert!(
xml.contains("bullet item"),
"item text must be present: {xml}"
);
}
#[test]
fn section_xml_unordered_list_item_has_para_pr_id_ref_2() {
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(r#"paraPrIDRef="2""#),
"unordered list item must have paraPrIDRef=\"2\":\n{xml}"
);
assert!(
!xml.contains(r#"paraPrIDRef="0""#),
"list item must NOT use paraPrIDRef=\"0\":\n{xml}"
);
}
#[test]
fn section_xml_multiple_unordered_items_all_have_num_pr_id_ref_1() {
let xml = section_xml(vec![Block::List {
ordered: false,
start: 1,
items: vec![
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("alpha")],
}],
children: vec![],
},
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("beta")],
}],
children: vec![],
},
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("gamma")],
}],
children: vec![],
},
],
}]);
assert!(
!xml.contains("numPrIDRef"),
"unordered list items must NOT carry numPrIDRef (no BULLET numbering): {xml}"
);
assert!(xml.contains("alpha"), "{xml}");
assert!(xml.contains("beta"), "{xml}");
assert!(xml.contains("gamma"), "{xml}");
}
#[test]
fn section_xml_ordered_list_item_has_num_pr_id_ref_2() {
let xml = section_xml(vec![Block::List {
ordered: true,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("first")],
}],
children: vec![],
}],
}]);
assert!(
xml.contains(r#"numPrIDRef="1""#),
"ordered list item must have numPrIDRef=\"1\" (DIGIT, id=1):\n{xml}"
);
assert!(xml.contains("first"), "item text must be present: {xml}");
}
#[test]
fn section_xml_ordered_list_item_has_para_pr_id_ref_2() {
let xml = section_xml(vec![Block::List {
ordered: true,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("numbered")],
}],
children: vec![],
}],
}]);
assert!(
xml.contains(r#"paraPrIDRef="2""#),
"ordered list item must have paraPrIDRef=\"2\":\n{xml}"
);
}
#[test]
fn section_xml_multiple_ordered_items_all_have_num_pr_id_ref_2() {
let xml = section_xml(vec![Block::List {
ordered: true,
start: 1,
items: vec![
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("one")],
}],
children: vec![],
},
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("two")],
}],
children: vec![],
},
],
}]);
let count = xml.matches(r#"numPrIDRef="1""#).count();
assert_eq!(
count, 2,
"two ordered list items must each have numPrIDRef=\"1\"; found {count}: {xml}"
);
}
#[test]
fn section_xml_ordered_and_unordered_use_different_num_pr_id_refs() {
let xml = section_xml(vec![
Block::List {
ordered: false,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("bullet")],
}],
children: vec![],
}],
},
Block::List {
ordered: true,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("number")],
}],
children: vec![],
}],
},
]);
assert!(
xml.contains(r#"numPrIDRef="1""#),
"ordered list item numPrIDRef=\"1\" must appear:\n{xml}"
);
assert!(
xml.contains("bullet"),
"unordered text 'bullet' must appear: {xml}"
);
assert!(
xml.contains("number"),
"ordered text 'number' must appear: {xml}"
);
}
#[test]
fn section_xml_list_items_produce_correct_paragraph_count() {
let xml = section_xml(vec![Block::List {
ordered: false,
start: 1,
items: vec![
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("a")],
}],
children: vec![],
},
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("b")],
}],
children: vec![],
},
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("c")],
}],
children: vec![],
},
],
}]);
let p_count = xml.matches("<hp:p ").count();
assert_eq!(
p_count, 3,
"three list items must produce three <hp:p> elements; found {p_count}: {xml}"
);
}
#[test]
fn section_xml_empty_list_produces_no_paragraphs() {
let xml = section_xml(vec![Block::List {
ordered: false,
start: 1,
items: vec![],
}]);
assert!(
!xml.contains("<hp:p "),
"empty list must produce no <hp:p> elements:\n{xml}"
);
}
#[test]
fn section_xml_list_item_with_bold_inline_has_both_num_pr_and_charpr() {
let xml = section_xml(vec![Block::List {
ordered: false,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![bold_inline("important")],
}],
children: vec![],
}],
}]);
assert!(
!xml.contains("numPrIDRef"),
"unordered bold list item must NOT have numPrIDRef (no BULLET numbering):\n{xml}"
);
assert!(
xml.contains(r#"bold="true""#),
"bold attribute must be present on inline charPr:\n{xml}"
);
assert!(
xml.contains("important"),
"text content must be present: {xml}"
);
}
#[test]
fn section_xml_list_item_empty_paragraph_no_panic() {
let xml = section_xml(vec![Block::List {
ordered: true,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph { inlines: vec![] }],
children: vec![],
}],
}]);
assert!(
xml.contains(r#"numPrIDRef="1""#),
"empty paragraph ordered list item must have numPrIDRef=\"1\" (DIGIT):\n{xml}"
);
assert!(
xml.contains("<hp:p "),
"paragraph element must be emitted: {xml}"
);
}
#[test]
fn section_xml_nested_list_via_children_uses_para_pr_id_ref_3() {
let xml = section_xml(vec![Block::List {
ordered: false,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("parent item")],
}],
children: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("child item")],
}],
children: vec![],
}],
}],
}]);
assert!(xml.contains("parent item"), "parent item text: {xml}");
assert!(xml.contains("child item"), "child item text: {xml}");
assert!(
xml.contains(r#"paraPrIDRef="2""#),
"parent item must have paraPrIDRef=\"2\":\n{xml}"
);
assert!(
xml.contains(r#"paraPrIDRef="3""#),
"child item must have paraPrIDRef=\"3\":\n{xml}"
);
}
#[test]
fn section_xml_nested_block_list_inside_item_uses_para_pr_id_ref_3() {
let xml = section_xml(vec![Block::List {
ordered: false,
start: 1,
items: vec![ListItem {
blocks: vec![
Block::Paragraph {
inlines: vec![inline("outer item")],
},
Block::List {
ordered: true,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("inner item")],
}],
children: vec![],
}],
},
],
children: vec![],
}],
}]);
assert!(xml.contains("outer item"), "outer item text: {xml}");
assert!(xml.contains("inner item"), "inner item text: {xml}");
assert!(
xml.contains(r#"paraPrIDRef="3""#),
"nested block list items must have paraPrIDRef=\"3\":\n{xml}"
);
}
#[test]
fn section_xml_list_paragraph_ids_are_sequential() {
let xml = section_xml(vec![Block::List {
ordered: false,
start: 1,
items: vec![
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("item 0")],
}],
children: vec![],
},
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("item 1")],
}],
children: vec![],
},
],
}]);
assert!(
xml.contains(r#"id="0""#),
"first list paragraph must be id=0: {xml}"
);
assert!(
xml.contains(r#"id="1""#),
"second list paragraph must be id=1: {xml}"
);
}
#[test]
fn section_xml_list_paragraph_ids_continue_after_preceding_blocks() {
let xml = section_xml(vec![
Block::Paragraph {
inlines: vec![inline("before")],
},
Block::List {
ordered: false,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("list item")],
}],
children: vec![],
}],
},
]);
assert!(
xml.contains(r#"id="0""#),
"normal paragraph must be id=0: {xml}"
);
assert!(xml.contains(r#"id="1""#), "list item must be id=1: {xml}");
}
#[test]
fn roundtrip_ordered_list_text_preserved_in_hwpx() {
let tmp = tempfile::NamedTempFile::new().expect("tmp file");
let doc = Document {
metadata: Metadata::default(),
sections: vec![Section {
blocks: vec![Block::List {
ordered: true,
start: 1,
items: vec![
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("step one")],
}],
children: vec![],
},
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("step two")],
}],
children: vec![],
},
],
}],
page_layout: None,
}],
assets: Vec::new(),
};
write_hwpx(&doc, tmp.path(), None).expect("write_hwpx");
let file = std::fs::File::open(tmp.path()).expect("open zip");
let mut archive = zip::ZipArchive::new(file).expect("parse zip");
let mut entry = archive
.by_name("Contents/section0.xml")
.expect("section0.xml must exist");
let mut xml = String::new();
std::io::Read::read_to_string(&mut entry, &mut xml).expect("read section0.xml");
assert!(
xml.contains("step one"),
"ordered list item 'step one' must survive write: {xml}"
);
assert!(
xml.contains("step two"),
"ordered list item 'step two' must survive write: {xml}"
);
assert!(
xml.contains(r#"numPrIDRef="1""#),
"ordered list items must have numPrIDRef=\"1\" (DIGIT, id=1) in HWPX: {xml}"
);
}
#[test]
fn roundtrip_unordered_list_text_preserved_in_hwpx() {
let tmp = tempfile::NamedTempFile::new().expect("tmp file");
let doc = Document {
metadata: Metadata::default(),
sections: vec![Section {
blocks: vec![Block::List {
ordered: false,
start: 1,
items: vec![
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("alpha")],
}],
children: vec![],
},
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("beta")],
}],
children: vec![],
},
],
}],
page_layout: None,
}],
assets: Vec::new(),
};
write_hwpx(&doc, tmp.path(), None).expect("write_hwpx");
let file = std::fs::File::open(tmp.path()).expect("open zip");
let mut archive = zip::ZipArchive::new(file).expect("parse zip");
let mut entry = archive
.by_name("Contents/section0.xml")
.expect("section0.xml must exist");
let mut xml = String::new();
std::io::Read::read_to_string(&mut entry, &mut xml).expect("read section0.xml");
assert!(
xml.contains("alpha"),
"unordered item 'alpha' must survive: {xml}"
);
assert!(
xml.contains("beta"),
"unordered item 'beta' must survive: {xml}"
);
assert!(
!xml.contains("numPrIDRef"),
"unordered list items must NOT carry numPrIDRef in HWPX (no BULLET numbering): {xml}"
);
}
#[test]
fn roundtrip_header_xml_contains_numbering_list() {
let tmp = tempfile::NamedTempFile::new().expect("tmp file");
let doc = Document {
metadata: Metadata::default(),
sections: vec![Section {
blocks: vec![Block::List {
ordered: false,
start: 1,
items: vec![ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("item")],
}],
children: vec![],
}],
}],
page_layout: None,
}],
assets: Vec::new(),
};
write_hwpx(&doc, tmp.path(), None).expect("write_hwpx");
let file = std::fs::File::open(tmp.path()).expect("open zip");
let mut archive = zip::ZipArchive::new(file).expect("parse zip");
let mut entry = archive
.by_name("Contents/header.xml")
.expect("header.xml must exist");
let mut header = String::new();
std::io::Read::read_to_string(&mut entry, &mut header).expect("read header.xml");
assert!(
header.contains("<hh:numberings"),
"header.xml in HWPX must contain <hh:numberings>:\n{header}"
);
assert!(
!header.contains(r#"numFormat="BULLET""#),
"header.xml must NOT contain BULLET numbering (invalid in OWPML schema):\n{header}"
);
assert!(
header.contains(r#"numFormat="DIGIT""#),
"header.xml must contain DIGIT numbering:\n{header}"
);
}