Skip to main content

linch_docx_rs/document/
header_footer.rs

1//! Header and Footer elements
2
3use crate::document::Paragraph;
4use crate::error::Result;
5use crate::xml::{RawXmlElement, RawXmlNode};
6use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
7use quick_xml::{Reader, Writer};
8use std::io::Cursor;
9
10/// A header or footer part
11#[derive(Clone, Debug, Default)]
12pub struct HeaderFooter {
13    /// Paragraphs in the header/footer
14    pub paragraphs: Vec<Paragraph>,
15    /// Unknown children (preserved for round-trip)
16    pub unknown_children: Vec<RawXmlNode>,
17    /// Whether this is a header (true) or footer (false)
18    pub is_header: bool,
19}
20
21impl HeaderFooter {
22    /// Parse from XML string
23    pub fn from_xml(xml: &str, is_header: bool) -> Result<Self> {
24        let mut reader = Reader::from_str(xml);
25        reader.config_mut().trim_text(true);
26
27        let mut hf = HeaderFooter {
28            is_header,
29            ..Default::default()
30        };
31        let mut buf = Vec::new();
32
33        loop {
34            match reader.read_event_into(&mut buf)? {
35                Event::Start(e) => {
36                    let local = e.name().local_name();
37                    match local.as_ref() {
38                        b"hdr" | b"ftr" => {
39                            // Root element, continue
40                        }
41                        b"p" => {
42                            let para = Paragraph::from_reader(&mut reader, &e)?;
43                            hf.paragraphs.push(para);
44                        }
45                        _ => {
46                            let raw = RawXmlElement::from_reader(&mut reader, &e)?;
47                            hf.unknown_children.push(RawXmlNode::Element(raw));
48                        }
49                    }
50                }
51                Event::Empty(e) => {
52                    let local = e.name().local_name();
53                    if local.as_ref() == b"p" {
54                        let para = Paragraph::from_empty(&e)?;
55                        hf.paragraphs.push(para);
56                    } else {
57                        let raw = RawXmlElement {
58                            name: String::from_utf8_lossy(e.name().as_ref()).to_string(),
59                            attributes: e
60                                .attributes()
61                                .filter_map(|a| a.ok())
62                                .map(|a| {
63                                    (
64                                        String::from_utf8_lossy(a.key.as_ref()).to_string(),
65                                        String::from_utf8_lossy(&a.value).to_string(),
66                                    )
67                                })
68                                .collect(),
69                            children: Vec::new(),
70                            self_closing: true,
71                        };
72                        hf.unknown_children.push(RawXmlNode::Element(raw));
73                    }
74                }
75                Event::Eof => break,
76                _ => {}
77            }
78            buf.clear();
79        }
80
81        Ok(hf)
82    }
83
84    /// Serialize to XML string
85    pub fn to_xml(&self) -> Result<String> {
86        let mut buffer = Cursor::new(Vec::new());
87        let mut writer = Writer::new(&mut buffer);
88
89        writer.write_event(Event::Decl(BytesDecl::new(
90            "1.0",
91            Some("UTF-8"),
92            Some("yes"),
93        )))?;
94
95        let tag = if self.is_header { "w:hdr" } else { "w:ftr" };
96        let mut start = BytesStart::new(tag);
97        start.push_attribute(("xmlns:w", crate::xml::W));
98        start.push_attribute(("xmlns:r", crate::xml::R));
99        writer.write_event(Event::Start(start))?;
100
101        for para in &self.paragraphs {
102            para.write_to(&mut writer)?;
103        }
104
105        for child in &self.unknown_children {
106            child.write_to(&mut writer)?;
107        }
108
109        writer.write_event(Event::End(BytesEnd::new(tag)))?;
110
111        let xml_bytes = buffer.into_inner();
112        String::from_utf8(xml_bytes)
113            .map_err(|e| crate::error::Error::InvalidDocument(e.to_string()))
114    }
115
116    /// Get all text
117    pub fn text(&self) -> String {
118        self.paragraphs
119            .iter()
120            .map(|p| p.text())
121            .collect::<Vec<_>>()
122            .join("\n")
123    }
124
125    /// Add a paragraph
126    pub fn add_paragraph(&mut self, text: impl Into<String>) {
127        self.paragraphs.push(Paragraph::new(text));
128    }
129
130    /// Create a new empty header
131    pub fn new_header() -> Self {
132        HeaderFooter {
133            is_header: true,
134            paragraphs: vec![Paragraph::default()],
135            ..Default::default()
136        }
137    }
138
139    /// Create a new empty footer
140    pub fn new_footer() -> Self {
141        HeaderFooter {
142            is_header: false,
143            paragraphs: vec![Paragraph::default()],
144            ..Default::default()
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn test_parse_header() {
155        let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
156<w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
157  <w:p>
158    <w:r>
159      <w:t>Header Text</w:t>
160    </w:r>
161  </w:p>
162</w:hdr>"#;
163
164        let hf = HeaderFooter::from_xml(xml, true).unwrap();
165        assert!(hf.is_header);
166        assert_eq!(hf.paragraphs.len(), 1);
167        assert_eq!(hf.text(), "Header Text");
168    }
169
170    #[test]
171    fn test_header_roundtrip() {
172        let mut hf = HeaderFooter::new_header();
173        hf.paragraphs.clear();
174        hf.add_paragraph("Test Header");
175
176        let xml = hf.to_xml().unwrap();
177        let hf2 = HeaderFooter::from_xml(&xml, true).unwrap();
178        assert_eq!(hf2.text(), "Test Header");
179    }
180
181    #[test]
182    fn test_footer() {
183        let mut hf = HeaderFooter::new_footer();
184        hf.paragraphs.clear();
185        hf.add_paragraph("Page 1");
186
187        let xml = hf.to_xml().unwrap();
188        assert!(xml.contains("w:ftr"));
189        assert!(!xml.contains("w:hdr"));
190    }
191}