easydoc_reader/extractor/
numbering.rs1use std::borrow::Cow;
8use std::collections::HashMap;
9
10use easydoc_core::{DocError, Result};
11use quick_xml::Reader as XmlReader;
12use quick_xml::events::Event;
13
14#[derive(Debug, Clone, Default)]
23pub struct Numbering {
24 pub num_to_abstract: HashMap<u32, u32>,
26 pub abstract_nums: HashMap<u32, AbstractNum>,
28}
29
30#[derive(Debug, Clone, Default)]
32pub struct AbstractNum {
33 pub levels: HashMap<u8, Level>,
35}
36
37#[derive(Debug, Clone, Default)]
39pub struct Level {
40 pub ordered: bool,
43 pub start: Option<u32>,
46 pub ilvl: u8,
48}
49
50impl Numbering {
51 pub fn parse(xml: &str) -> Result<Self> {
57 let mut numbering = Numbering::default();
58 let mut reader = XmlReader::from_reader(xml.as_bytes());
59 reader.config_mut().trim_text(true);
60 let mut buf = Vec::new();
61
62 let mut current_abstract_id: Option<u32> = None;
64 let mut current_ilvl: Option<u8> = None;
65 let mut current_num_fmt: Option<String> = None;
66 let mut current_start: Option<u32> = None;
67
68 let mut current_num_id: Option<u32> = None;
70 let mut in_num: bool = false;
71
72 loop {
73 match reader.read_event_into(&mut buf) {
74 Ok(Event::Eof) => break,
75 Ok(Event::Start(ref start)) => {
76 let name = start.name();
77 let local = name.as_ref();
78 match local {
79 b"w:abstractNum" => {
80 current_abstract_id = extract_u32_attr(start, b"w:abstractNumId");
81 }
82 b"w:lvl" => {
83 current_ilvl = extract_u8_attr(start, b"w:ilvl");
84 current_num_fmt = None;
85 current_start = None;
86 }
87 b"w:numFmt" => {
88 current_num_fmt = extract_val_attr(start);
89 }
90 b"w:start" => {
91 current_start =
92 extract_val_attr(start).and_then(|v| v.parse::<u32>().ok());
93 }
94 b"w:num" => {
95 current_num_id = extract_u32_attr(start, b"w:numId");
96 in_num = true;
97 }
98 b"w:abstractNumId" if in_num => {
99 if let (Some(abstract_id), Some(num_id)) =
101 (extract_val_attr_u32(start), current_num_id)
102 {
103 numbering.num_to_abstract.insert(num_id, abstract_id);
104 }
105 }
106 _ => {}
107 }
108 }
109 Ok(Event::Empty(ref empty)) => {
110 let name = empty.name();
111 let local = name.as_ref();
112 match local {
113 b"w:numFmt" => {
114 current_num_fmt = extract_val_attr(empty);
115 }
116 b"w:start" => {
117 current_start =
118 extract_val_attr(empty).and_then(|v| v.parse::<u32>().ok());
119 }
120 b"w:abstractNumId" if in_num => {
121 if let (Some(abstract_id), Some(num_id)) =
122 (extract_val_attr_u32(empty), current_num_id)
123 {
124 numbering.num_to_abstract.insert(num_id, abstract_id);
125 }
126 }
127 _ => {}
128 }
129 }
130 Ok(Event::End(ref end)) => {
131 let name = end.name();
132 let local = name.as_ref();
133 match local {
134 b"w:lvl" => {
135 if let (Some(abstract_id), Some(ilvl)) =
137 (current_abstract_id, current_ilvl)
138 {
139 let ordered =
140 !matches!(current_num_fmt.as_deref(), Some("bullet") | None);
141 let level = Level {
142 ordered,
143 start: current_start,
144 ilvl,
145 };
146 numbering
147 .abstract_nums
148 .entry(abstract_id)
149 .or_default()
150 .levels
151 .insert(ilvl, level);
152 }
153 current_ilvl = None;
154 current_num_fmt = None;
155 current_start = None;
156 }
157 b"w:num" => {
158 current_num_id = None;
159 in_num = false;
160 }
161 _ => {}
162 }
163 }
164 Err(e) => {
165 return Err(DocError::Format(format!(
166 "XML parse error in numbering: {e}"
167 )));
168 }
169 _ => {}
170 }
171 buf.clear();
172 }
173
174 Ok(numbering)
175 }
176
177 #[must_use]
182 pub fn lookup(&self, num_id: u32, ilvl: u8) -> Option<&Level> {
183 let abstract_id = self.num_to_abstract.get(&num_id)?;
184 let abstract_num = self.abstract_nums.get(abstract_id)?;
185 abstract_num.levels.get(&ilvl)
186 }
187}
188
189fn extract_u32_attr(tag: &quick_xml::events::BytesStart, attr_name: &[u8]) -> Option<u32> {
191 for attr in tag.attributes().flatten() {
192 if attr.key.as_ref() == attr_name {
193 let val = attr
194 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
195 .ok()?;
196 return val.parse::<u32>().ok();
197 }
198 }
199 None
200}
201
202fn extract_u8_attr(tag: &quick_xml::events::BytesStart, attr_name: &[u8]) -> Option<u8> {
204 for attr in tag.attributes().flatten() {
205 if attr.key.as_ref() == attr_name {
206 let val = attr
207 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
208 .ok()?;
209 return val.parse::<u8>().ok();
210 }
211 }
212 None
213}
214
215fn extract_val_attr(tag: &quick_xml::events::BytesStart) -> Option<String> {
217 for attr in tag.attributes().flatten() {
218 if attr.key.as_ref() == b"w:val" {
219 return attr
220 .normalized_value(quick_xml::XmlVersion::Implicit1_0)
221 .ok()
222 .map(Cow::into_owned);
223 }
224 }
225 None
226}
227
228fn extract_val_attr_u32(tag: &quick_xml::events::BytesStart) -> Option<u32> {
230 extract_val_attr(tag).and_then(|v| v.parse::<u32>().ok())
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn parse_numbering_bullet_lists_unordered() {
239 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
240<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
241 <w:abstractNum w:abstractNumId="0">
242 <w:lvl w:ilvl="0">
243 <w:numFmt w:val="bullet"/>
244 <w:lvlText w:val="•"/>
245 </w:lvl>
246 <w:lvl w:ilvl="1">
247 <w:numFmt w:val="bullet"/>
248 <w:lvlText w:val="–"/>
249 </w:lvl>
250 </w:abstractNum>
251 <w:num w:numId="1">
252 <w:abstractNumId w:val="0"/>
253 </w:num>
254</w:numbering>"#;
255
256 let numbering = Numbering::parse(xml).unwrap();
257 let level0 = numbering.lookup(1, 0).unwrap();
258 assert!(!level0.ordered, "bullet should be unordered");
259 let level1 = numbering.lookup(1, 1).unwrap();
260 assert!(!level1.ordered, "bullet should be unordered");
261 }
262
263 #[test]
264 fn parse_numbering_decimal_lists_ordered() {
265 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
266<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
267 <w:abstractNum w:abstractNumId="1">
268 <w:lvl w:ilvl="0">
269 <w:start w:val="1"/>
270 <w:numFmt w:val="decimal"/>
271 <w:lvlText w:val="%1."/>
272 </w:lvl>
273 </w:abstractNum>
274 <w:num w:numId="2">
275 <w:abstractNumId w:val="1"/>
276 </w:num>
277</w:numbering>"#;
278
279 let numbering = Numbering::parse(xml).unwrap();
280 let level = numbering.lookup(2, 0).unwrap();
281 assert!(level.ordered, "decimal should be ordered");
282 assert_eq!(level.start, Some(1));
283 }
284
285 #[test]
286 fn parse_numbering_decimal_with_start_value() {
287 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
288<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
289 <w:abstractNum w:abstractNumId="0">
290 <w:lvl w:ilvl="0">
291 <w:start w:val="5"/>
292 <w:numFmt w:val="decimal"/>
293 <w:lvlText w:val="%1."/>
294 </w:lvl>
295 </w:abstractNum>
296 <w:num w:numId="1">
297 <w:abstractNumId w:val="0"/>
298 </w:num>
299</w:numbering>"#;
300
301 let numbering = Numbering::parse(xml).unwrap();
302 let level = numbering.lookup(1, 0).unwrap();
303 assert!(level.ordered);
304 assert_eq!(level.start, Some(5), "start should be 5");
305 }
306
307 #[test]
308 fn lookup_returns_correct_format_for_num_id() {
309 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
310<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
311 <w:abstractNum w:abstractNumId="0">
312 <w:lvl w:ilvl="0">
313 <w:numFmt w:val="bullet"/>
314 </w:lvl>
315 </w:abstractNum>
316 <w:abstractNum w:abstractNumId="1">
317 <w:lvl w:ilvl="0">
318 <w:start w:val="1"/>
319 <w:numFmt w:val="decimal"/>
320 </w:lvl>
321 </w:abstractNum>
322 <w:num w:numId="1">
323 <w:abstractNumId w:val="0"/>
324 </w:num>
325 <w:num w:numId="2">
326 <w:abstractNumId w:val="1"/>
327 </w:num>
328</w:numbering>"#;
329
330 let numbering = Numbering::parse(xml).unwrap();
331 assert!(!numbering.lookup(1, 0).unwrap().ordered);
333 assert!(numbering.lookup(2, 0).unwrap().ordered);
335 assert!(numbering.lookup(99, 0).is_none());
337 }
338
339 #[test]
340 fn parse_numbering_roman_is_ordered() {
341 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
342<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
343 <w:abstractNum w:abstractNumId="0">
344 <w:lvl w:ilvl="0">
345 <w:start w:val="1"/>
346 <w:numFmt w:val="upperRoman"/>
347 <w:lvlText w:val="%1."/>
348 </w:lvl>
349 <w:lvl w:ilvl="1">
350 <w:start w:val="1"/>
351 <w:numFmt w:val="lowerLetter"/>
352 <w:lvlText w:val="%2)"/>
353 </w:lvl>
354 </w:abstractNum>
355 <w:num w:numId="1">
356 <w:abstractNumId w:val="0"/>
357 </w:num>
358</w:numbering>"#;
359
360 let numbering = Numbering::parse(xml).unwrap();
361 assert!(numbering.lookup(1, 0).unwrap().ordered);
362 assert!(numbering.lookup(1, 1).unwrap().ordered);
363 }
364
365 #[test]
366 fn parse_empty_numbering() {
367 let xml = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
368<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
369</w:numbering>"#;
370
371 let numbering = Numbering::parse(xml).unwrap();
372 assert!(numbering.num_to_abstract.is_empty());
373 assert!(numbering.abstract_nums.is_empty());
374 assert!(numbering.lookup(1, 0).is_none());
375 }
376}