use std::collections::HashMap;
use std::io::{Read, Seek};
use crate::model::{Alignment, Block, Footnote, HeaderFooter, LineSpacing, Paragraph, Run};
use super::numbering::{ListCounters, NumberingInfo};
use super::parse_table_node;
use super::relationships::parse_part_relationships;
use super::runs::parse_runs;
use super::styles::{ParagraphStyle, StylesInfo, ThemeFonts, parse_alignment};
use super::{
ParseContext, WML_NS, collect_block_nodes, extract_indents, parse_paragraph_spacing, wml,
wml_attr,
};
fn resolve_alignment(
ppr: Option<roxmltree::Node>,
para_style: Option<&ParagraphStyle>,
) -> Alignment {
ppr.and_then(|ppr| wml_attr(ppr, "jc"))
.map(parse_alignment)
.or_else(|| para_style.and_then(|s| s.alignment))
.unwrap_or(Alignment::Left)
}
pub(super) fn parse_header_footer_xml<R: Read + Seek>(
xml_content: &str,
ctx: &mut ParseContext<'_, R>,
) -> Option<HeaderFooter> {
let xml = roxmltree::Document::parse(xml_content).ok()?;
let root = xml.root_element();
let mut blocks = Vec::new();
let top_nodes = collect_block_nodes(root);
let mut lists = ListCounters::default();
for node in top_nodes {
if node.tag_name().namespace() != Some(WML_NS) {
continue;
}
match node.tag_name().name() {
"tbl" => {
let table = parse_table_node(node, ctx, &mut lists);
blocks.push(Block::Table(table));
}
"p" => {
let mut para = super::paragraph::build_paragraph(
node,
ctx,
&mut lists,
&super::paragraph::ParagraphOptions::default(),
);
if blocks.is_empty() && para.space_before_auto {
para.space_before = 0.0;
}
blocks.push(Block::Paragraph(para));
}
_ => {}
}
}
(!blocks.is_empty()).then_some(HeaderFooter { blocks })
}
pub(super) fn parse_footnotes<R: Read + Seek>(
zip: &mut zip::ZipArchive<R>,
styles: &StylesInfo,
theme: &ThemeFonts,
numbering: &NumberingInfo,
) -> (HashMap<u32, Footnote>, Option<Paragraph>) {
parse_notes_simple(
zip,
styles,
theme,
numbering,
"word/footnotes.xml",
"footnote",
"FootnoteText",
)
}
pub(super) fn parse_endnotes<R: Read + Seek>(
zip: &mut zip::ZipArchive<R>,
styles: &StylesInfo,
theme: &ThemeFonts,
numbering: &NumberingInfo,
) -> HashMap<u32, Footnote> {
parse_notes_rich(
zip,
styles,
theme,
numbering,
"word/endnotes.xml",
"endnote",
)
}
fn parse_notes_simple<R: Read + Seek>(
zip: &mut zip::ZipArchive<R>,
styles: &StylesInfo,
theme: &ThemeFonts,
numbering: &NumberingInfo,
zip_path: &str,
element_name: &str,
default_style_id: &str,
) -> (HashMap<u32, Footnote>, Option<Paragraph>) {
let mut footnotes = HashMap::new();
let mut separator = None;
let Some(xml_text) = super::read_zip_text(zip, zip_path) else {
return (footnotes, separator);
};
let Ok(xml) = roxmltree::Document::parse(&xml_text) else {
return (footnotes, separator);
};
let root = xml.root_element();
let rels = parse_part_relationships(zip, zip_path);
let mut fn_ctx = ParseContext {
styles,
theme,
rels: &rels,
zip,
numbering,
};
let mut lists = ListCounters::default();
for node in root.children() {
if !node.has_tag_name((WML_NS, element_name)) {
continue;
}
match node.attribute((WML_NS, "type")) {
Some("separator") => {
separator = node
.children()
.find(|c| c.has_tag_name((WML_NS, "p")))
.map(|p| {
super::paragraph::build_paragraph(
p,
&mut fn_ctx,
&mut lists,
&super::paragraph::ParagraphOptions::default(),
)
});
continue;
}
Some(_) => continue,
None => {}
}
let Some(id) = node
.attribute((WML_NS, "id"))
.and_then(|v| v.parse::<u32>().ok())
else {
continue;
};
let mut parse_para = |p: roxmltree::Node, fn_ctx: &mut ParseContext<'_, R>| -> Paragraph {
let ppr = wml(p, "pPr");
let para_style_id = ppr
.and_then(|ppr| wml_attr(ppr, "pStyle"))
.unwrap_or(default_style_id);
let numbered = ppr.is_some_and(|ppr| wml(ppr, "numPr").is_some())
|| fn_ctx
.styles
.paragraph_styles
.get(para_style_id)
.is_some_and(|s| s.num_id.is_some());
if numbered {
let mut para = super::paragraph::build_paragraph(
p,
fn_ctx,
&mut lists,
&super::paragraph::ParagraphOptions::default(),
);
para.line_spacing = para.line_spacing.or(Some(LineSpacing::Auto(1.0)));
para.snap_to_grid = true;
return para;
}
let para_style = fn_ctx
.styles
.paragraph_styles
.get(para_style_id)
.or_else(|| {
fn_ctx
.styles
.paragraph_styles
.get(&fn_ctx.styles.default_paragraph_style_id)
});
let alignment = resolve_alignment(ppr, para_style);
let parsed = parse_runs(p, fn_ctx);
let (sp_before, sp_after, ls) =
parse_paragraph_spacing(ppr, para_style, &fn_ctx.styles.defaults);
let char_fs = para_style
.and_then(|s| s.font_size)
.unwrap_or(styles.defaults.font_size);
let (left, right, hanging, first) =
if let Some(ind) = ppr.and_then(|ppr| wml(ppr, "ind")) {
let (l, r, h, f) = extract_indents(ind, Some(char_fs / 2.0));
if let Some(s) = para_style {
(
l.or(s.indent_left),
r.or(s.indent_right),
h.or(s.indent_hanging),
f.or(s.indent_first_line),
)
} else {
(l, r, h, f)
}
} else if let Some(s) = para_style {
(
s.indent_left,
s.indent_right,
s.indent_hanging,
s.indent_first_line,
)
} else {
(None, None, None, None)
};
let mut tab_stops = super::resolve_tab_stops(ppr, para_style);
super::paragraph::add_hanging_tab_stop(
&mut tab_stops,
left.unwrap_or(0.0),
hanging.unwrap_or(0.0),
);
Paragraph {
runs: parsed.runs,
space_before: sp_before.unwrap_or(styles.defaults.space_before),
space_after: sp_after.unwrap_or(styles.defaults.space_after),
tab_stops,
alignment,
line_spacing: ls.or(Some(styles.defaults.line_spacing)),
snap_to_grid: true,
indent_left: left.unwrap_or(0.0),
indent_right: right.unwrap_or(0.0),
indent_hanging: hanging.unwrap_or(0.0),
indent_first_line: first.unwrap_or(0.0),
..Paragraph::default()
}
};
let mut paragraphs = Vec::new();
for child in node.children() {
if child.has_tag_name((WML_NS, "p")) {
paragraphs.push(parse_para(child, &mut fn_ctx));
} else if child.has_tag_name((WML_NS, "tbl")) {
for tr in child.children().filter(|n| n.has_tag_name((WML_NS, "tr"))) {
let mut cell_paras = tr
.descendants()
.filter(|n| n.has_tag_name((WML_NS, "p")))
.map(|p| parse_para(p, &mut fn_ctx));
let Some(mut merged) = cell_paras.next() else {
continue;
};
for para in cell_paras {
if let Some(first) = para.runs.first() {
merged.runs.push(Run {
text: " ".to_string(),
font_size: first.font_size,
font_name: first.font_name.clone(),
east_asia_font_name: first.east_asia_font_name.clone(),
bold: first.bold,
italic: first.italic,
..Run::default()
});
}
merged.runs.extend(para.runs);
}
paragraphs.push(merged);
}
}
}
if !paragraphs.is_empty() {
footnotes.insert(id, Footnote { paragraphs });
}
}
(footnotes, separator)
}
fn parse_notes_rich<R: Read + Seek>(
zip: &mut zip::ZipArchive<R>,
styles: &StylesInfo,
theme: &ThemeFonts,
numbering: &NumberingInfo,
zip_path: &str,
element_name: &str,
) -> HashMap<u32, Footnote> {
let mut footnotes = HashMap::new();
let Some(xml_text) = super::read_zip_text(zip, zip_path) else {
return footnotes;
};
let rels = parse_part_relationships(zip, zip_path);
let Ok(xml) = roxmltree::Document::parse(&xml_text) else {
return footnotes;
};
let root = xml.root_element();
let mut fn_ctx = ParseContext {
styles,
theme,
rels: &rels,
zip,
numbering,
};
let mut lists = ListCounters::default();
for node in root.children() {
if !node.has_tag_name((WML_NS, element_name)) {
continue;
}
if node.attribute((WML_NS, "type")).is_some() {
continue;
}
let Some(id) = node
.attribute((WML_NS, "id"))
.and_then(|v| v.parse::<u32>().ok())
else {
continue;
};
let mut paragraphs = Vec::new();
for p in node.children().filter(|n| n.has_tag_name((WML_NS, "p"))) {
let mut para = super::paragraph::build_paragraph(
p,
&mut fn_ctx,
&mut lists,
&super::paragraph::ParagraphOptions::default(),
);
if para.line_spacing.is_none() {
para.line_spacing = Some(LineSpacing::Auto(1.0));
}
para.snap_to_grid = true;
paragraphs.push(para);
}
if !paragraphs.is_empty() {
footnotes.insert(id, Footnote { paragraphs });
}
}
footnotes
}