Skip to main content

extend_docx/doc/
lists.rs

1//! List tables ([MS-DOC] 2.9.131 PlfLst / 2.9.130 PlfLfo): list definitions
2//! (LSTF + per-level LVL) and list-format overrides (LFO/LFOLVL), emitted as
3//! word/numbering.xml. A paragraph's sprmPIlfo is a 1-based LFO index, which
4//! maps directly to the synthesized w:numId.
5
6use 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
60/// Parses one LVL structure, returning it plus the new cursor position.
61fn 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    // Defensive: the two grpprls are distinguishable by sprm category; swap if
76    // a writer ordered them the other way around.
77    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)); // (lsid, fSimpleList)
141        }
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    /// Synthesizes word/numbering.xml. Returns None when there are no lists.
222    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; // sprmPIlfo is 1-based
248            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    // Level text: placeholder code units 0x0000-0x0008 become %1-%9.
308    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); // flags
354        lvl.extend_from_slice(&[0u8; 9]); // rgbxchNums
355        lvl.push(0); // ixchFollow = tab
356        lvl.extend_from_slice(&[0u8; 8]); // dxaIndentSav + unused
357        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        // One simple list (lsid 500) with a decimal "%1." level.
372        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; // fSimpleList
377        plf_lst.extend_from_slice(&lstf);
378        let indent: Vec<u8> = vec![0x0F, 0x84, 0xD0, 0x02]; // dxaLeft 720
379        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}