linch_docx_rs/document/
footnotes.rs1use crate::document::Paragraph;
4use crate::error::Result;
5use crate::xml::{get_attr, RawXmlElement, RawXmlNode};
6use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
7use quick_xml::{Reader, Writer};
8use std::io::Cursor;
9
10#[derive(Clone, Debug)]
12pub struct Note {
13 pub id: i32,
15 pub note_type: Option<String>,
17 pub paragraphs: Vec<Paragraph>,
19 pub unknown_children: Vec<RawXmlNode>,
21}
22
23impl Note {
24 pub fn new(id: i32, text: impl Into<String>) -> Self {
26 Note {
27 id,
28 note_type: None,
29 paragraphs: vec![Paragraph::new(text)],
30 unknown_children: Vec::new(),
31 }
32 }
33
34 pub fn text(&self) -> String {
36 self.paragraphs
37 .iter()
38 .map(|p| p.text())
39 .collect::<Vec<_>>()
40 .join("\n")
41 }
42
43 pub fn is_regular(&self) -> bool {
45 self.note_type.is_none() || self.note_type.as_deref() == Some("normal")
46 }
47}
48
49#[derive(Clone, Debug, Default)]
51pub struct Notes {
52 pub notes: Vec<Note>,
54 pub is_footnotes: bool,
56 pub unknown_children: Vec<RawXmlNode>,
58}
59
60impl Notes {
61 pub fn from_xml(xml: &str, is_footnotes: bool) -> Result<Self> {
63 let mut reader = Reader::from_str(xml);
64 reader.config_mut().trim_text(true);
65
66 let mut notes = Notes {
67 is_footnotes,
68 ..Default::default()
69 };
70 let mut buf = Vec::new();
71
72 let note_tag = if is_footnotes {
73 b"footnote".as_slice()
74 } else {
75 b"endnote".as_slice()
76 };
77
78 loop {
79 match reader.read_event_into(&mut buf)? {
80 Event::Start(e) => {
81 let local = e.name().local_name();
82 if local.as_ref() == note_tag {
83 notes.notes.push(parse_note(&mut reader, &e)?);
84 } else if local.as_ref() != b"footnotes" && local.as_ref() != b"endnotes" {
85 let raw = RawXmlElement::from_reader(&mut reader, &e)?;
86 notes.unknown_children.push(RawXmlNode::Element(raw));
87 }
88 }
89 Event::Eof => break,
90 _ => {}
91 }
92 buf.clear();
93 }
94
95 Ok(notes)
96 }
97
98 pub fn to_xml(&self) -> Result<String> {
100 let mut buffer = Cursor::new(Vec::new());
101 let mut writer = Writer::new(&mut buffer);
102
103 writer.write_event(Event::Decl(BytesDecl::new(
104 "1.0",
105 Some("UTF-8"),
106 Some("yes"),
107 )))?;
108
109 let root_tag = if self.is_footnotes {
110 "w:footnotes"
111 } else {
112 "w:endnotes"
113 };
114 let note_tag = if self.is_footnotes {
115 "w:footnote"
116 } else {
117 "w:endnote"
118 };
119
120 let mut start = BytesStart::new(root_tag);
121 start.push_attribute(("xmlns:w", crate::xml::W));
122 start.push_attribute(("xmlns:r", crate::xml::R));
123 writer.write_event(Event::Start(start))?;
124
125 for note in &self.notes {
126 let mut note_start = BytesStart::new(note_tag);
127 note_start.push_attribute(("w:id", note.id.to_string().as_str()));
128 if let Some(ref nt) = note.note_type {
129 note_start.push_attribute(("w:type", nt.as_str()));
130 }
131
132 writer.write_event(Event::Start(note_start))?;
133
134 for para in ¬e.paragraphs {
135 para.write_to(&mut writer)?;
136 }
137
138 for child in ¬e.unknown_children {
139 child.write_to(&mut writer)?;
140 }
141
142 writer.write_event(Event::End(BytesEnd::new(note_tag)))?;
143 }
144
145 for child in &self.unknown_children {
146 child.write_to(&mut writer)?;
147 }
148
149 writer.write_event(Event::End(BytesEnd::new(root_tag)))?;
150
151 let xml_bytes = buffer.into_inner();
152 String::from_utf8(xml_bytes)
153 .map_err(|e| crate::error::Error::InvalidDocument(e.to_string()))
154 }
155
156 pub fn get(&self, id: i32) -> Option<&Note> {
158 self.notes.iter().find(|n| n.id == id)
159 }
160
161 pub fn regular_notes(&self) -> impl Iterator<Item = &Note> {
163 self.notes.iter().filter(|n| n.is_regular())
164 }
165
166 pub fn next_id(&self) -> i32 {
168 self.notes.iter().map(|n| n.id).max().unwrap_or(0) + 1
169 }
170
171 pub fn add(&mut self, text: impl Into<String>) -> i32 {
173 let id = self.next_id();
174 self.notes.push(Note::new(id, text));
175 id
176 }
177}
178
179fn parse_note<R: std::io::BufRead>(reader: &mut Reader<R>, start: &BytesStart) -> Result<Note> {
180 let id = get_attr(start, "w:id")
181 .or_else(|| get_attr(start, "id"))
182 .and_then(|v| v.parse().ok())
183 .unwrap_or(0);
184 let note_type = get_attr(start, "w:type").or_else(|| get_attr(start, "type"));
185
186 let mut note = Note {
187 id,
188 note_type,
189 paragraphs: Vec::new(),
190 unknown_children: Vec::new(),
191 };
192
193 let mut buf = Vec::new();
194 loop {
195 match reader.read_event_into(&mut buf)? {
196 Event::Start(e) => {
197 let local = e.name().local_name();
198 if local.as_ref() == b"p" {
199 note.paragraphs.push(Paragraph::from_reader(reader, &e)?);
200 } else {
201 let raw = RawXmlElement::from_reader(reader, &e)?;
202 note.unknown_children.push(RawXmlNode::Element(raw));
203 }
204 }
205 Event::Empty(e) => {
206 let local = e.name().local_name();
207 if local.as_ref() == b"p" {
208 note.paragraphs.push(Paragraph::from_empty(&e)?);
209 }
210 }
211 Event::End(e) => {
212 let local = e.name().local_name();
213 if local.as_ref() == b"footnote" || local.as_ref() == b"endnote" {
214 break;
215 }
216 }
217 Event::Eof => break,
218 _ => {}
219 }
220 buf.clear();
221 }
222
223 Ok(note)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_parse_footnotes() {
232 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
233<w:footnotes xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
234 <w:footnote w:type="separator" w:id="-1">
235 <w:p><w:r><w:separator/></w:r></w:p>
236 </w:footnote>
237 <w:footnote w:id="1">
238 <w:p>
239 <w:r><w:t>This is footnote 1</w:t></w:r>
240 </w:p>
241 </w:footnote>
242</w:footnotes>"#;
243
244 let notes = Notes::from_xml(xml, true).unwrap();
245 assert_eq!(notes.notes.len(), 2);
246 assert!(!notes.notes[0].is_regular());
247 assert!(notes.notes[1].is_regular());
248 assert_eq!(notes.get(1).unwrap().text(), "This is footnote 1");
249 assert_eq!(notes.regular_notes().count(), 1);
250 }
251
252 #[test]
253 fn test_notes_roundtrip() {
254 let mut notes = Notes {
255 is_footnotes: true,
256 ..Default::default()
257 };
258 let id = notes.add("Test footnote");
259 assert_eq!(id, 1);
260
261 let xml = notes.to_xml().unwrap();
262 let notes2 = Notes::from_xml(&xml, true).unwrap();
263 assert_eq!(notes2.notes.len(), 1);
264 assert_eq!(notes2.get(1).unwrap().text(), "Test footnote");
265 }
266
267 #[test]
268 fn test_endnotes() {
269 let mut notes = Notes {
270 is_footnotes: false,
271 ..Default::default()
272 };
273 notes.add("Endnote content");
274
275 let xml = notes.to_xml().unwrap();
276 assert!(xml.contains("w:endnotes"));
277 assert!(xml.contains("w:endnote"));
278 }
279}