1use super::chp::{Chp, ToggleSet};
7use super::fmt::escape_xml;
8use super::fonts::FontTable;
9use super::pap::Pap;
10use super::sprm::{self, SGC_CHR, SGC_PAR};
11
12#[derive(Debug, Clone, Default)]
13pub struct ListLevel {
14 pub start_at: u32,
15 pub nfc: u8,
16 pub jc: u8,
17 pub xst: Vec<u16>,
18 pub ixch_follow: u8,
19 pub papx: Vec<u8>,
20 pub chpx: Vec<u8>,
21}
22
23#[derive(Debug, Clone)]
24pub struct ListData {
25 pub lsid: u32,
26 pub levels: Vec<ListLevel>,
27}
28
29#[derive(Debug, Clone)]
30pub struct LfoLevelOverride {
31 pub ilvl: u8,
32 pub start_at: Option<u32>,
33 pub level: Option<ListLevel>,
34}
35
36#[derive(Debug, Clone)]
37pub struct ListOverride {
38 pub lsid: u32,
39 pub levels: Vec<LfoLevelOverride>,
40}
41
42#[derive(Debug, Default)]
43pub struct ListTables {
44 pub lists: Vec<ListData>,
45 pub lfos: Vec<ListOverride>,
46}
47
48fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
49 bytes
50 .get(offset..offset + 2)
51 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
52}
53
54fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
55 bytes
56 .get(offset..offset + 4)
57 .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
58}
59
60fn parse_lvl(bytes: &[u8], pos: usize) -> Option<(ListLevel, usize)> {
62 let start_at = read_u32(bytes, pos)?;
63 let nfc = *bytes.get(pos + 4)?;
64 let flags = *bytes.get(pos + 5)?;
65 let ixch_follow = *bytes.get(pos + 15)?;
66 let cb_chpx = *bytes.get(pos + 24)? as usize;
67 let cb_papx = *bytes.get(pos + 25)? as usize;
68 let mut cursor = pos + 28;
69
70 let mut papx = bytes.get(cursor..cursor + cb_papx)?.to_vec();
71 cursor += cb_papx;
72 let mut chpx = bytes.get(cursor..cursor + cb_chpx)?.to_vec();
73 cursor += cb_chpx;
74
75 let looks_like = |grpprl: &[u8], sgc: u8| {
78 sprm::iterate(grpprl)
79 .next()
80 .map(|sprm| sprm.sgc() == sgc)
81 .unwrap_or(true)
82 };
83 if !papx.is_empty()
84 && !looks_like(&papx, SGC_PAR)
85 && looks_like(&papx, SGC_CHR)
86 && (chpx.is_empty() || looks_like(&chpx, SGC_PAR))
87 {
88 std::mem::swap(&mut papx, &mut chpx);
89 }
90
91 let cch = read_u16(bytes, cursor)? as usize;
92 cursor += 2;
93 let mut xst = Vec::with_capacity(cch);
94 for index in 0..cch {
95 xst.push(read_u16(bytes, cursor + index * 2)?);
96 }
97 cursor += cch * 2;
98
99 Some((
100 ListLevel {
101 start_at,
102 nfc,
103 jc: flags & 0x03,
104 xst,
105 ixch_follow,
106 papx,
107 chpx,
108 },
109 cursor,
110 ))
111}
112
113impl ListTables {
114 pub fn parse(plf_lst: Option<&[u8]>, plf_lfo: Option<&[u8]>) -> ListTables {
115 let mut tables = ListTables::default();
116 if let Some(plf_lst) = plf_lst {
117 tables.parse_plf_lst(plf_lst);
118 }
119 if let Some(plf_lfo) = plf_lfo {
120 tables.parse_plf_lfo(plf_lfo);
121 }
122 tables
123 }
124
125 fn parse_plf_lst(&mut self, bytes: &[u8]) {
126 let Some(count) = read_u16(bytes, 0) else {
127 return;
128 };
129 let count = count as usize;
130 const LSTF_SIZE: usize = 28;
131 let mut headers = Vec::with_capacity(count);
132 for index in 0..count {
133 let offset = 2 + index * LSTF_SIZE;
134 let Some(lsid) = read_u32(bytes, offset) else {
135 return;
136 };
137 let Some(&flags) = bytes.get(offset + 26) else {
138 return;
139 };
140 headers.push((lsid, flags & 0x01 != 0)); }
142
143 let mut pos = 2 + count * LSTF_SIZE;
144 for (lsid, simple) in headers {
145 let level_count = if simple { 1 } else { 9 };
146 let mut levels = Vec::with_capacity(level_count);
147 for _ in 0..level_count {
148 match parse_lvl(bytes, pos) {
149 Some((level, next)) => {
150 levels.push(level);
151 pos = next;
152 }
153 None => break,
154 }
155 }
156 self.lists.push(ListData { lsid, levels });
157 }
158 }
159
160 fn parse_plf_lfo(&mut self, bytes: &[u8]) {
161 let Some(count) = read_u32(bytes, 0) else {
162 return;
163 };
164 let count = count as usize;
165 const LFO_SIZE: usize = 16;
166 let mut headers = Vec::with_capacity(count);
167 for index in 0..count {
168 let offset = 4 + index * LFO_SIZE;
169 let Some(lsid) = read_u32(bytes, offset) else {
170 return;
171 };
172 let Some(&clfolvl) = bytes.get(offset + 12) else {
173 return;
174 };
175 headers.push((lsid, clfolvl as usize));
176 }
177
178 let mut pos = 4 + count * LFO_SIZE;
179 for (lsid, clfolvl) in headers {
180 if read_u32(bytes, pos).is_none() {
181 break;
182 }
183 pos += 4;
184 let mut levels = Vec::with_capacity(clfolvl);
185 for _ in 0..clfolvl {
186 let Some(start_at) = read_u32(bytes, pos) else {
187 break;
188 };
189 let Some(flags) = read_u32(bytes, pos + 4) else {
190 break;
191 };
192 pos += 8;
193 let ilvl = (flags & 0x0F) as u8;
194 let f_start_at = flags & 0x10 != 0;
195 let f_formatting = flags & 0x20 != 0;
196 let mut level = None;
197 if f_formatting {
198 if let Some((lvl, next)) = parse_lvl(bytes, pos) {
199 level = Some(lvl);
200 pos = next;
201 }
202 }
203 levels.push(LfoLevelOverride {
204 ilvl,
205 start_at: f_start_at.then_some(start_at),
206 level,
207 });
208 }
209 self.lfos.push(ListOverride { lsid, levels });
210 }
211 }
212
213 pub fn is_empty(&self) -> bool {
214 self.lists.is_empty() && self.lfos.is_empty()
215 }
216
217 fn abstract_num_id_for_lsid(&self, lsid: u32) -> Option<usize> {
218 self.lists.iter().position(|list| list.lsid == lsid)
219 }
220
221 pub fn to_numbering_xml(&self, fonts: &FontTable) -> Option<String> {
223 if self.is_empty() {
224 return None;
225 }
226 let mut xml = String::from(
227 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
228 );
229
230 for (index, list) in self.lists.iter().enumerate() {
231 xml.push_str(&format!(r#"<w:abstractNum w:abstractNumId="{index}">"#));
232 xml.push_str(&format!(
233 r#"<w:multiLevelType w:val="{}"/>"#,
234 if list.levels.len() == 1 {
235 "singleLevel"
236 } else {
237 "hybridMultilevel"
238 }
239 ));
240 for (ilvl, level) in list.levels.iter().enumerate() {
241 xml.push_str(&level_xml(ilvl as u8, level, fonts));
242 }
243 xml.push_str("</w:abstractNum>");
244 }
245
246 for (index, lfo) in self.lfos.iter().enumerate() {
247 let num_id = index + 1; let abstract_id = match self.abstract_num_id_for_lsid(lfo.lsid) {
249 Some(id) => id,
250 None => continue,
251 };
252 xml.push_str(&format!(r#"<w:num w:numId="{num_id}">"#));
253 xml.push_str(&format!(r#"<w:abstractNumId w:val="{abstract_id}"/>"#));
254 for level_override in &lfo.levels {
255 xml.push_str(&format!(
256 r#"<w:lvlOverride w:ilvl="{}">"#,
257 level_override.ilvl
258 ));
259 if let Some(start) = level_override.start_at {
260 xml.push_str(&format!(r#"<w:startOverride w:val="{start}"/>"#));
261 }
262 if let Some(level) = &level_override.level {
263 xml.push_str(&level_xml(level_override.ilvl, level, fonts));
264 }
265 xml.push_str("</w:lvlOverride>");
266 }
267 xml.push_str("</w:num>");
268 }
269
270 xml.push_str("</w:numbering>");
271 Some(xml)
272 }
273}
274
275fn level_xml(ilvl: u8, level: &ListLevel, fonts: &FontTable) -> String {
276 let mut xml = format!(r#"<w:lvl w:ilvl="{ilvl}">"#);
277 xml.push_str(&format!(r#"<w:start w:val="{}"/>"#, level.start_at));
278 xml.push_str(&format!(
279 r#"<w:numFmt w:val="{}"/>"#,
280 match level.nfc {
281 0 => "decimal",
282 1 => "upperRoman",
283 2 => "lowerRoman",
284 3 => "upperLetter",
285 4 => "lowerLetter",
286 5 => "ordinal",
287 6 => "cardinalText",
288 7 => "ordinalText",
289 22 => "decimalZero",
290 23 => "bullet",
291 255 => "none",
292 _ => "decimal",
293 }
294 ));
295
296 if level.ixch_follow != 0 {
297 xml.push_str(&format!(
298 r#"<w:suff w:val="{}"/>"#,
299 if level.ixch_follow == 1 {
300 "space"
301 } else {
302 "nothing"
303 }
304 ));
305 }
306
307 let mut text = String::new();
309 for &unit in &level.xst {
310 if unit <= 0x0008 {
311 text.push('%');
312 text.push_str(&(unit + 1).to_string());
313 } else {
314 text.push_str(&String::from_utf16_lossy(&[unit]));
315 }
316 }
317 xml.push_str(&format!(r#"<w:lvlText w:val="{}"/>"#, escape_xml(&text)));
318 xml.push_str(&format!(
319 r#"<w:lvlJc w:val="{}"/>"#,
320 match level.jc {
321 1 => "center",
322 2 => "right",
323 _ => "left",
324 }
325 ));
326
327 if !level.papx.is_empty() {
328 let mut pap = Pap::default();
329 pap.apply_papx(&level.papx);
330 let inner = pap.to_ppr_inner_xml(&|_| None);
331 if !inner.is_empty() {
332 xml.push_str(&format!("<w:pPr>{inner}</w:pPr>"));
333 }
334 }
335 if !level.chpx.is_empty() {
336 let mut chp = Chp::default();
337 chp.apply_chpx(&level.chpx, &ToggleSet::default());
338 xml.push_str(&chp.to_rpr_xml(&|_| None, &|ftc| fonts.name(ftc)));
339 }
340
341 xml.push_str("</w:lvl>");
342 xml
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 fn build_lvl(start: u32, nfc: u8, xst: &[u16], papx: &[u8], chpx: &[u8]) -> Vec<u8> {
350 let mut lvl = Vec::new();
351 lvl.extend_from_slice(&start.to_le_bytes());
352 lvl.push(nfc);
353 lvl.push(0); lvl.extend_from_slice(&[0u8; 9]); lvl.push(0); lvl.extend_from_slice(&[0u8; 8]); lvl.push(chpx.len() as u8);
358 lvl.push(papx.len() as u8);
359 lvl.extend_from_slice(&[0u8; 2]);
360 lvl.extend_from_slice(papx);
361 lvl.extend_from_slice(chpx);
362 lvl.extend_from_slice(&(xst.len() as u16).to_le_bytes());
363 for unit in xst {
364 lvl.extend_from_slice(&unit.to_le_bytes());
365 }
366 lvl
367 }
368
369 #[test]
370 fn parses_simple_list_and_lfo() {
371 let mut plf_lst = Vec::new();
373 plf_lst.extend_from_slice(&1u16.to_le_bytes());
374 let mut lstf = vec![0u8; 28];
375 lstf[..4].copy_from_slice(&500u32.to_le_bytes());
376 lstf[26] = 0x01; plf_lst.extend_from_slice(&lstf);
378 let indent: Vec<u8> = vec![0x0F, 0x84, 0xD0, 0x02]; plf_lst.extend_from_slice(&build_lvl(1, 0, &[0x0000, '.' as u16], &indent, &[]));
380
381 let mut plf_lfo = Vec::new();
382 plf_lfo.extend_from_slice(&1u32.to_le_bytes());
383 let mut lfo = vec![0u8; 16];
384 lfo[..4].copy_from_slice(&500u32.to_le_bytes());
385 plf_lfo.extend_from_slice(&lfo);
386 plf_lfo.extend_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
387
388 let tables = ListTables::parse(Some(&plf_lst), Some(&plf_lfo));
389 assert_eq!(tables.lists.len(), 1);
390 assert_eq!(tables.lists[0].levels.len(), 1);
391 assert_eq!(tables.lfos.len(), 1);
392
393 let xml = tables.to_numbering_xml(&FontTable::empty()).expect("xml");
394 assert!(xml.contains(r#"<w:abstractNum w:abstractNumId="0">"#));
395 assert!(xml.contains(r#"<w:lvlText w:val="%1."/>"#));
396 assert!(xml.contains(r#"<w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>"#));
397 assert!(xml.contains(r#"w:left="720""#));
398 }
399}