use super::*;
#[test]
fn golden_comprehensive_document_structure() {
use std::io::Read as _;
let doc = Document {
metadata: Metadata {
title: Some("Golden Test Doc".into()),
author: Some("Test Author".into()),
..Metadata::default()
},
sections: vec![Section {
blocks: vec![
Block::Heading {
level: 1,
inlines: vec![Inline::plain("Main Title")],
},
Block::Paragraph {
inlines: vec![inline("Normal text "), bold_inline("bold text")],
},
Block::Paragraph {
inlines: vec![italic_inline("italic text")],
},
Block::Table {
rows: vec![
TableRow {
cells: vec![
TableCell {
blocks: vec![Block::Paragraph {
inlines: vec![inline("Cell A1")],
}],
colspan: 1,
rowspan: 1,
},
TableCell {
blocks: vec![Block::Paragraph {
inlines: vec![inline("Cell B1")],
}],
colspan: 1,
rowspan: 1,
},
],
is_header: true,
},
TableRow {
cells: vec![
TableCell {
blocks: vec![Block::Paragraph {
inlines: vec![inline("Cell A2")],
}],
colspan: 1,
rowspan: 1,
},
TableCell {
blocks: vec![Block::Paragraph {
inlines: vec![inline("Cell B2")],
}],
colspan: 1,
rowspan: 1,
},
],
is_header: false,
},
],
col_count: 2,
},
Block::CodeBlock {
language: Some("rust".into()),
code: "fn main() {}".into(),
},
Block::HorizontalRule,
Block::BlockQuote {
blocks: vec![Block::Paragraph {
inlines: vec![inline("quoted text")],
}],
},
Block::List {
ordered: false,
start: 1,
items: vec![
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("List item one")],
}],
children: Vec::new(),
},
ListItem {
blocks: vec![Block::Paragraph {
inlines: vec![inline("List item two")],
}],
children: Vec::new(),
},
],
},
],
page_layout: None,
}],
assets: Vec::new(),
};
let tmp = tempfile::NamedTempFile::new().expect("tmp file");
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 section_xml = {
let mut entry = archive
.by_name("Contents/section0.xml")
.expect("section0.xml must exist in HWPX");
let mut buf = String::new();
entry.read_to_string(&mut buf).expect("read section0.xml");
buf
};
assert!(
section_xml.contains(r#"hp:styleIDRef="1""#),
"H1 heading must have hp:styleIDRef=\"1\" in section XML:\n{section_xml}"
);
assert!(
section_xml.contains("<hp:t>Main Title</hp:t>"),
"heading text 'Main Title' must appear in <hp:t>:\n{section_xml}"
);
assert!(
section_xml.contains(r#"bold="true""#),
"bold inline must emit charPr with bold=\"true\":\n{section_xml}"
);
assert!(
section_xml.contains("<hp:t>bold text</hp:t>"),
"bold text content must appear in <hp:t>:\n{section_xml}"
);
assert!(
section_xml.contains(r#"italic="true""#),
"italic inline must emit charPr with italic=\"true\":\n{section_xml}"
);
assert!(
section_xml.contains("<hp:t>italic text</hp:t>"),
"italic text content must appear in <hp:t>:\n{section_xml}"
);
assert!(
section_xml.contains("<hp:t>Normal text </hp:t>"),
"plain text must appear in <hp:t>:\n{section_xml}"
);
assert!(
section_xml.contains(r#"<hp:tbl"#),
"table must emit <hp:tbl> element:\n{section_xml}"
);
assert!(
section_xml.contains(r#"rowCnt="2""#),
"table must have rowCnt=\"2\":\n{section_xml}"
);
assert!(
section_xml.contains(r#"colCnt="2""#),
"table must have colCnt=\"2\":\n{section_xml}"
);
assert!(
section_xml.contains("<hp:tr>"),
"table must contain <hp:tr> rows:\n{section_xml}"
);
assert!(
section_xml.contains("<hp:tc>"),
"table must contain <hp:tc> cells:\n{section_xml}"
);
assert!(
section_xml.contains("<hp:t>Cell A1</hp:t>"),
"table cell text 'Cell A1' must appear:\n{section_xml}"
);
assert!(
section_xml.contains("<hp:t>Cell B2</hp:t>"),
"table cell text 'Cell B2' must appear:\n{section_xml}"
);
assert!(
section_xml.contains("<hp:t>List item one</hp:t>"),
"list item text 'List item one' must appear:\n{section_xml}"
);
assert!(
section_xml.contains("<hp:t>List item two</hp:t>"),
"list item text 'List item two' must appear:\n{section_xml}"
);
assert!(
section_xml.contains("xmlns:hs="),
"section XML must declare hs namespace:\n{section_xml}"
);
assert!(
section_xml.contains("xmlns:hp="),
"section XML must declare hp namespace:\n{section_xml}"
);
let charpr_count = section_xml.matches("<hp:charPr ").count();
assert!(
charpr_count >= 2,
"at least 2 inline <hp:charPr> elements expected (bold + italic), found {charpr_count}:\n{section_xml}"
);
let content_hpf = {
let mut entry = archive
.by_name("Contents/content.hpf")
.expect("content.hpf must exist in HWPX");
let mut buf = String::new();
entry.read_to_string(&mut buf).expect("read content.hpf");
buf
};
assert!(
content_hpf.contains("section0.xml"),
"content.hpf must reference section0.xml:\n{content_hpf}"
);
assert!(
content_hpf.contains("<hp:title>Golden Test Doc</hp:title>"),
"content.hpf must contain document title:\n{content_hpf}"
);
assert!(
content_hpf.contains("<hp:author>Test Author</hp:author>"),
"content.hpf must contain document author:\n{content_hpf}"
);
let header_xml = {
let mut entry = archive
.by_name("Contents/header.xml")
.expect("header.xml must exist in HWPX");
let mut buf = String::new();
entry.read_to_string(&mut buf).expect("read header.xml");
buf
};
assert!(
header_xml.contains("hh:fontface"),
"header.xml must contain fontface declarations:\n{header_xml}"
);
assert!(
header_xml.contains("hh:charPr"),
"header.xml must contain charPr entries:\n{header_xml}"
);
assert!(
header_xml.contains("hh:style"),
"header.xml must contain style entries:\n{header_xml}"
);
assert!(
section_xml.contains("<hp:t>fn main() {}</hp:t>"),
"code block text must appear in <hp:t>:\n{section_xml}"
);
assert!(
section_xml.contains("\u{2500}"),
"horizontal rule must emit box-drawing characters:\n{section_xml}"
);
assert!(
section_xml.contains("<hp:t>quoted text</hp:t>"),
"block quote text must appear in <hp:t>:\n{section_xml}"
);
let mimetype = {
let mut entry = archive
.by_name("mimetype")
.expect("mimetype must exist in HWPX");
let mut buf = String::new();
entry.read_to_string(&mut buf).expect("read mimetype");
buf
};
assert_eq!(
mimetype, "application/hwp+zip",
"mimetype must be exactly 'application/hwp+zip'"
);
archive
.by_name("version.xml")
.expect("version.xml must exist in HWPX");
let container_xml = {
let mut entry = archive
.by_name("META-INF/container.xml")
.expect("META-INF/container.xml must exist in HWPX");
let mut buf = String::new();
entry.read_to_string(&mut buf).expect("read container.xml");
buf
};
assert!(
container_xml.contains("content.hpf"),
"container.xml must reference content.hpf:\n{container_xml}"
);
assert!(
section_xml.contains(r#"paraPrIDRef="1""#),
"block quote paragraph must use paraPrIDRef=\"1\":\n{section_xml}"
);
assert!(
header_xml.contains(r#"<hh:paraPr id="1">"#),
"header.xml must contain paraPr id=\"1\" for blockquote indent:\n{header_xml}"
);
}
#[test]
fn header_xml_contains_blockquote_para_pr() {
let doc = doc_with_section(vec![Block::Paragraph {
inlines: vec![inline("text")],
}]);
let tables = RefTables::build(&doc);
let header =
super::header::generate_header_xml(&doc, &tables).expect("generate_header_xml failed");
assert!(
header.contains(r#"<hh:paraPr id="0">"#),
"header must contain paraPr id=\"0\":\n{header}"
);
assert!(
header.contains(r#"<hh:paraPr id="1">"#),
"header must contain paraPr id=\"1\":\n{header}"
);
assert!(
header.contains(r#"<hh:paraPr id="4">"#),
"header must contain paraPr id=\"4\" for heading spacing:\n{header}"
);
assert!(
header.contains(r#"itemCnt="5""#),
"paraProperties itemCnt must be 5:\n{header}"
);
assert!(
header.contains(r#"<hh:left value="800"/>"#),
"paraPr id=\"1\" must have left margin 800:\n{header}"
);
}
#[test]
fn header_xml_para_pr_0_has_zero_left_margin() {
let doc = doc_with_section(vec![Block::Paragraph {
inlines: vec![inline("text")],
}]);
let tables = RefTables::build(&doc);
let header =
super::header::generate_header_xml(&doc, &tables).expect("generate_header_xml failed");
let first_left_pos = header
.find(r#"<hh:left value="#)
.expect("hh:left must exist");
let first_left_slice = &header[first_left_pos..];
assert!(
first_left_slice.starts_with(r#"<hh:left value="0"/>"#),
"first paraPr (id=0) must have left margin 0:\n{header}"
);
}
#[test]
fn roundtrip_blockquote_content_preserved() {
let tmp = tempfile::NamedTempFile::new().expect("tmp file");
let doc = Document {
metadata: Metadata::default(),
sections: vec![Section {
blocks: vec![Block::BlockQuote {
blocks: vec![Block::Paragraph {
inlines: vec![inline("roundtrip quote")],
}],
}],
page_layout: None,
}],
assets: Vec::new(),
};
write_hwpx(&doc, tmp.path(), None).expect("write_hwpx");
let read_back = read_hwpx(tmp.path()).expect("read_hwpx");
let has_quote_text = read_back
.sections
.iter()
.flat_map(|s| &s.blocks)
.any(|b| match b {
Block::Paragraph { inlines } => inlines.iter().any(|i| i.text == "roundtrip quote"),
Block::BlockQuote { blocks } => blocks.iter().any(|b2| {
matches!(b2, Block::Paragraph { inlines } if inlines.iter().any(|i| i.text == "roundtrip quote"))
}),
_ => false,
});
assert!(
has_quote_text,
"blockquote text must survive HWPX roundtrip; sections: {:?}",
read_back.sections
);
}