// The package-level read surface: body PLUS sections and header/footer
// stories. `read_docx_package` is a NEW entry point — `read_docx` /
// `read_docx_with_messages` and `DocxReadResult` are frozen public API and
// stay byte-identical (the shared assembly lives in `DocumentParts`).
//
// The reader stays tolerant here, matching the body reader's philosophy:
// a dangling header reference or an unparsable part becomes a Warning and
// the reference is skipped — `docx validate` is the strict gate.
///|
/// Everything `read_docx_with_messages` returns, plus sections and
/// header/footer stories.
pub(all) struct DocxPackageResult {
document : DocumentElement
headers : Array[@document.HeaderFooterPart]
footers : Array[@document.HeaderFooterPart]
sections : Array[@document.DocumentSection]
messages : Array[Message]
} derive(Debug, Eq)
///|
/// Reads DOCX bytes into the package-level representation: the body
/// document tree (identical to `read_docx_with_messages`), header/footer
/// parts (each a block-level story, deduplicated by part path in
/// first-reference order), and the sections that reference them.
pub fn read_docx_package(
docx : BytesView,
external_file_access? : Bool = false,
read_external_file? : (String) -> Bytes? = no_external_file_reader,
) -> DocxPackageResult raise DocxError {
let zip = open_zip(docx)
let external_files = external_file_access_options(
external_file_access, read_external_file,
)
let parts = DocumentParts::build(zip, external_files~)
let document_result = parts.read_document()
let collector = HeaderFooterCollector::new(parts)
let sections = collect_sections(parts.root, collector)
let messages : Array[Message] = []
messages.append(parts.notes_result.messages)
messages.append(parts.comments_result.messages)
messages.append(document_result.messages)
messages.append(collector.messages)
{
document: document_result.document,
headers: collector.headers,
footers: collector.footers,
sections,
messages: @core.dedupe_messages(messages),
}
}
///|
/// Scans the body for section definitions: a paragraph-level `w:sectPr`
/// (inside `w:pPr`) closes a section after that paragraph; the body-final
/// `w:sectPr` closes the last section. Direct body paragraphs are counted
/// in document order.
/// One scanned section plus the variants it DECLARED explicitly (readable
/// or not): an explicit-but-unreadable reference must still block
/// inheritance of that variant — the author overrode it, we just could not
/// read the override.
priv struct SectionScan {
section : @document.DocumentSection
declared_headers : Array[String]
declared_footers : Array[String]
}
///|
fn collect_sections(
root : XmlElement,
collector : HeaderFooterCollector,
) -> Array[@document.DocumentSection] raise DocxError {
let scans : Array[SectionScan] = []
guard root.first("w:body") is Some(body) else {
// Defensive only: the public entry point reads the body first and
// raises on a missing one, so this branch is unreachable from
// read_docx_package. Kept so the section invariant ("each body range
// maps to exactly one section") holds for any caller.
return [{ ends_after_paragraph: None, headers: [], footers: [] }]
}
let mut paragraph_index = 0
let mut trailing_sect_pr : XmlElement? = None
for node in body.children {
guard node is XmlElement(element) else { continue }
match element.name {
"w:p" => {
paragraph_index += 1
match paragraph_sect_pr(element) {
Some(sect_pr) =>
scans.push(collector.section(sect_pr, Some(paragraph_index)))
None => ()
}
}
"w:sectPr" => trailing_sect_pr = Some(element)
_ => ()
}
}
match trailing_sect_pr {
Some(sect_pr) => scans.push(collector.section(sect_pr, None))
// A document without a body-final sectPr still has a final section
// running to the body end.
None =>
scans.push({
section: { ends_after_paragraph: None, headers: [], footers: [] },
declared_headers: [],
declared_footers: [],
})
}
apply_header_inheritance(scans)
scans.map(scan => scan.section)
}
///|
/// OOXML header/footer inheritance: a section without an explicit
/// reference for a variant uses the previous section's (effective)
/// reference for that variant. The projection exposes EFFECTIVE
/// associations, so an agent reading `sections[1].headers` sees what the
/// section actually renders with.
fn apply_header_inheritance(scans : Array[SectionScan]) -> Unit {
for i in 1.. Unit {
for candidate in previous {
if declared.search(candidate.variant) is Some(_) {
continue
}
let mut present = false
for existing in current {
if existing.variant == candidate.variant {
present = true
break
}
}
if !present {
current.push({ variant: candidate.variant, part: candidate.part })
}
}
}
///|
fn paragraph_sect_pr(paragraph : XmlElement) -> XmlElement? {
match paragraph.first("w:pPr") {
Some(properties) => properties.first("w:sectPr")
None => None
}
}
///|
/// Resolves header/footer references and reads their parts, deduplicating
/// by resolved part path (first-reference order).
priv struct HeaderFooterCollector {
parts : DocumentParts
headers : Array[@document.HeaderFooterPart]
footers : Array[@document.HeaderFooterPart]
header_index_by_path : StableStringMap[Int]
footer_index_by_path : StableStringMap[Int]
messages : Array[Message]
}
///|
fn HeaderFooterCollector::new(parts : DocumentParts) -> HeaderFooterCollector {
{
parts,
headers: [],
footers: [],
header_index_by_path: SortedMap([]),
footer_index_by_path: SortedMap([]),
messages: [],
}
}
///|
fn HeaderFooterCollector::section(
self : HeaderFooterCollector,
sect_pr : XmlElement,
ends_after_paragraph : Int?,
) -> SectionScan raise DocxError {
let headers : Array[@document.SectionRef] = []
let footers : Array[@document.SectionRef] = []
let declared_headers : Array[String] = []
let declared_footers : Array[String] = []
for node in sect_pr.children {
guard node is XmlElement(element) else { continue }
let is_header = element.name == "w:headerReference"
let is_footer = element.name == "w:footerReference"
if !is_header && !is_footer {
continue
}
let variant = element.attributes.get("w:type").unwrap_or("default")
if variant != "default" && variant != "first" && variant != "even" {
raise InvalidXml(
message="header/footer reference w:type must be 'default', 'first', or 'even'",
)
}
let already_declared = if is_header {
declared_headers.search(variant) is Some(_)
} else {
declared_footers.search(variant) is Some(_)
}
if already_declared {
raise InvalidXml(
message="a section must not declare the same header/footer variant more than once",
)
}
// Declared regardless of resolvability: an explicit override blocks
// inheritance even when its part cannot be read.
if is_header {
declared_headers.push(variant)
} else {
declared_footers.push(variant)
}
guard element.attributes.get("r:id") is Some(rel_id) else {
push_reader_message(
self.messages,
self.parts.diagnostics,
Warning("A header/footer reference without r:id was ignored"),
)
continue
}
match self.resolve_part(rel_id, is_header~) {
Some(index) => {
let section_ref = @document.SectionRef::{ variant, part: index }
if is_header {
headers.push(section_ref)
} else {
footers.push(section_ref)
}
}
None => ()
}
}
{
section: { ends_after_paragraph, headers, footers },
declared_headers,
declared_footers,
}
}
///|
/// Returns the part index in `headers`/`footers`, reading the part on its
/// first reference. A dangling id or malformed part warns and yields None;
/// bounded resource exhaustion always propagates.
fn HeaderFooterCollector::resolve_part(
self : HeaderFooterCollector,
rel_id : String,
is_header~ : Bool,
) -> Int? raise DocxError {
let kind = if is_header { "header" } else { "footer" }
let path = resolve_header_footer_reference_part(
self.parts.zip,
self.parts.relationship_index,
self.parts.base_path,
rel_id,
is_header,
) catch {
_ => {
// The package projection remains tolerant. Annotation-aware mutation
// runs the same resolver without this catch and therefore fails closed.
push_reader_warning_parts(self.messages, self.parts.diagnostics, [
"A ", kind, " reference through invalid relationship ", rel_id, " was ignored",
])
return None
}
}
let index_by_path = if is_header {
self.header_index_by_path
} else {
self.footer_index_by_path
}
match index_by_path.get(path) {
Some(index) => Some(index)
None => {
// A malformed part or malformed part-rels must not abort the read —
// outline/text/get all flow through here, and the body-only surface
// never raised for a broken header. Warn and skip instead, while
// preserving hard resource ceilings as typed failures.
let body = self.read_part_story(path, kind) catch {
ResourceLimit(..) as error => raise error
_ => {
push_reader_warning_parts(self.messages, self.parts.diagnostics, [
"A ", kind, " part could not be read and was ignored: ", path,
])
return None
}
}
let stories = if is_header { self.headers } else { self.footers }
stories.push({ body, })
let index = stories.length() - 1
index_by_path[path] = index
Some(index)
}
}
}
///|
/// Reads one header/footer part's block content with a body reader bound to
/// the PART'S OWN relationships (images and hyperlinks inside headers
/// resolve through `word/_rels/headerN.xml.rels`, not the document's).
fn HeaderFooterCollector::read_part_story(
self : HeaderFooterCollector,
path : String,
kind : String,
) -> Array[DocumentElement] raise DocxError {
self.parts.content_types.require_annotation_part(
self.parts.zip,
Some(path),
kind,
)
let (root_namespace_uri, root) = read_xml_part_with_root_namespace_uri(
self.parts.zip,
path,
xml_budget?=self.parts.xml_budget,
)
let expected_root = "w:" + annotation_story_root_local_name(kind)
let wordprocessing_root = match root_namespace_uri {
Some(uri) => wordprocessing_dialect(uri) is Some(_)
None => false
}
if root.name != expected_root || !wordprocessing_root {
raise InvalidXml(message=annotation_story_root_error_message(path, kind))
}
let part_relationships = read_relationships(
self.parts.zip,
relationships_path_for_part(self.parts.zip, path),
xml_budget?=self.parts.xml_budget,
)
let reader = BodyReader::{
styles: self.parts.styles,
zip: self.parts.zip,
relationship_index: RelationshipIndex::build(part_relationships),
content_types: self.parts.content_types,
base_path: split_zip_path(self.parts.zip.logical_path(path).unwrap_or(path)).0,
numbering: self.parts.numbering,
messages: [],
diagnostics: self.parts.diagnostics,
external_files: self.parts.external_files,
read_images: true,
}
let body = reader.read_children(root.children)
self.messages.append(reader.messages)
body
}