Skip to main content

extend_docx/doc/
pap.rs

1//! Paragraph properties (PAP): style index plus PAPX sprm deltas, emitted as
2//! OOXML `<w:pPr>` direct formatting. Table-related flags (fInTable, fTtp,
3//! itap) drive table reconstruction in the document builder.
4
5use super::fmt::{parse_brc80, parse_brc97, Border, Shading};
6use super::sprm::{self, Sprm, SGC_TAB};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct TabStop {
10    pub dxa: i16,
11    pub jc: u8,
12    pub leader: u8,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct LineSpacing {
17    pub dya_line: i16,
18    pub f_mult: bool,
19}
20
21#[derive(Debug, Default, Clone, PartialEq)]
22pub struct Pap {
23    pub istd: u16,
24    pub jc: Option<u8>,
25    pub dxa_left: Option<i16>,
26    pub dxa_right: Option<i16>,
27    pub dxa_left1: Option<i16>,
28    pub dya_before: Option<u16>,
29    pub dya_after: Option<u16>,
30    pub before_auto: Option<bool>,
31    pub after_auto: Option<bool>,
32    pub lspd: Option<LineSpacing>,
33    pub contextual_spacing: Option<bool>,
34    pub f_keep: Option<bool>,
35    pub f_keep_follow: Option<bool>,
36    pub f_page_break_before: Option<bool>,
37    pub f_widow_control: Option<bool>,
38    pub ilvl: Option<u8>,
39    pub ilfo: Option<u16>,
40    pub outline_lvl: Option<u8>,
41    pub f_bidi: Option<bool>,
42    pub brc_top: Option<Border>,
43    pub brc_left: Option<Border>,
44    pub brc_bottom: Option<Border>,
45    pub brc_right: Option<Border>,
46    pub brc_between: Option<Border>,
47    pub shd: Option<Shading>,
48    pub tabs_deleted: Vec<i16>,
49    pub tabs_added: Vec<TabStop>,
50    // Table structure flags.
51    pub f_in_table: bool,
52    pub f_ttp: bool,
53    pub itap: u32,
54    pub f_inner_table_cell: bool,
55    pub f_inner_ttp: bool,
56    /// Table sprms found in this paragraph's grpprl (row-end paragraphs carry
57    /// the row's TAP definition).
58    pub table_grpprl: Vec<u8>,
59}
60
61impl Pap {
62    pub fn apply_papx(&mut self, grpprl: &[u8]) {
63        for sprm in sprm::iterate(grpprl) {
64            if sprm.sgc() == SGC_TAB {
65                // Collect table sprms verbatim for TAP parsing.
66                self.table_grpprl.extend_from_slice(&sprm_bytes(&sprm));
67                continue;
68            }
69            self.apply_sprm(&sprm);
70        }
71    }
72
73    fn apply_sprm(&mut self, sprm: &Sprm<'_>) {
74        match sprm.opcode {
75            0x4600 => self.istd = sprm.u16_operand(),
76            0x2403 | 0x2461 => self.jc = Some(sprm.byte()),
77            0x2405 => self.f_keep = sprm.bool_operand(),
78            0x2406 => self.f_keep_follow = sprm.bool_operand(),
79            0x2407 => self.f_page_break_before = sprm.bool_operand(),
80            0x2431 => self.f_widow_control = sprm.bool_operand(),
81            0x260A => self.ilvl = Some(sprm.byte()),
82            0x460B => self.ilfo = Some(sprm.u16_operand()),
83            0x840F | 0x845E => self.dxa_left = Some(sprm.i16_operand()),
84            0x840E | 0x845D => self.dxa_right = Some(sprm.i16_operand()),
85            0x8411 | 0x8460 => self.dxa_left1 = Some(sprm.i16_operand()),
86            0x6412 => {
87                if sprm.operand.len() >= 4 {
88                    self.lspd = Some(LineSpacing {
89                        dya_line: i16::from_le_bytes([sprm.operand[0], sprm.operand[1]]),
90                        f_mult: i16::from_le_bytes([sprm.operand[2], sprm.operand[3]]) != 0,
91                    });
92                }
93            }
94            0xA413 => self.dya_before = Some(sprm.u16_operand()),
95            0xA414 => self.dya_after = Some(sprm.u16_operand()),
96            0x245B => self.before_auto = sprm.bool_operand(),
97            0x245C => self.after_auto = sprm.bool_operand(),
98            0x246D => self.contextual_spacing = sprm.bool_operand(),
99            0x2640 => self.outline_lvl = Some(sprm.byte()),
100            0x2441 => self.f_bidi = sprm.bool_operand(),
101            0x2416 => self.f_in_table = sprm.byte() == 1,
102            0x2417 => self.f_ttp = sprm.byte() == 1,
103            0x6649 => self.itap = sprm.u32_operand(),
104            0x244B => self.f_inner_table_cell = sprm.byte() == 1,
105            0x244C => self.f_inner_ttp = sprm.byte() == 1,
106            0x6424 => self.brc_top = parse_brc80(sprm.operand),
107            0x6425 => self.brc_left = parse_brc80(sprm.operand),
108            0x6426 => self.brc_bottom = parse_brc80(sprm.operand),
109            0x6427 => self.brc_right = parse_brc80(sprm.operand),
110            0x6428 => self.brc_between = parse_brc80(sprm.operand),
111            0xC64E => self.brc_top = parse_brc97(sprm.operand),
112            0xC64F => self.brc_left = parse_brc97(sprm.operand),
113            0xC650 => self.brc_bottom = parse_brc97(sprm.operand),
114            0xC651 => self.brc_right = parse_brc97(sprm.operand),
115            0xC652 => self.brc_between = parse_brc97(sprm.operand),
116            0x442D => self.shd = super::fmt::parse_shd80(sprm.u16_operand()),
117            0xC64D => self.shd = super::fmt::parse_shd_operand(sprm.operand),
118            0xC60D | 0xC615 => self.apply_tab_operand(sprm.operand, sprm.opcode == 0xC615),
119            _ => {}
120        }
121    }
122
123    /// PChgTabsPapx / PChgTabs operand: deletions then additions.
124    fn apply_tab_operand(&mut self, operand: &[u8], has_close_array: bool) {
125        let mut pos = 0usize;
126        let Some(&del_count) = operand.first() else {
127            return;
128        };
129        pos += 1;
130        for index in 0..del_count as usize {
131            let offset = pos + index * 2;
132            if offset + 2 > operand.len() {
133                return;
134            }
135            self.tabs_deleted
136                .push(i16::from_le_bytes([operand[offset], operand[offset + 1]]));
137        }
138        pos += del_count as usize * 2;
139        if has_close_array {
140            pos += del_count as usize * 2; // rgdxaClose, tolerance windows — ignored
141        }
142        let Some(&add_count) = operand.get(pos) else {
143            return;
144        };
145        pos += 1;
146        let dxa_base = pos;
147        let tbd_base = pos + add_count as usize * 2;
148        for index in 0..add_count as usize {
149            let dxa_offset = dxa_base + index * 2;
150            let tbd_offset = tbd_base + index;
151            if dxa_offset + 2 > operand.len() || tbd_offset >= operand.len() {
152                return;
153            }
154            let tbd = operand[tbd_offset];
155            self.tabs_added.push(TabStop {
156                dxa: i16::from_le_bytes([operand[dxa_offset], operand[dxa_offset + 1]]),
157                jc: tbd & 0x07,
158                leader: (tbd >> 3) & 0x07,
159            });
160        }
161    }
162
163    /// Emits the inner content of `<w:pPr>` (without numPr/sectPr, which the
164    /// document builder appends contextually). Returns "" when empty.
165    pub fn to_ppr_inner_xml(&self, style_id: &impl Fn(u16) -> Option<String>) -> String {
166        let mut inner = String::new();
167
168        if self.istd != 0 {
169            if let Some(id) = style_id(self.istd) {
170                inner.push_str(&format!(
171                    r#"<w:pStyle w:val="{}"/>"#,
172                    super::fmt::escape_xml(&id)
173                ));
174            }
175        }
176        if let Some(keep) = self.f_keep {
177            inner.push_str(if keep {
178                "<w:keepLines/>"
179            } else {
180                r#"<w:keepLines w:val="0"/>"#
181            });
182        }
183        if let Some(keep_next) = self.f_keep_follow {
184            inner.push_str(if keep_next {
185                "<w:keepNext/>"
186            } else {
187                r#"<w:keepNext w:val="0"/>"#
188            });
189        }
190        if let Some(page_break) = self.f_page_break_before {
191            inner.push_str(if page_break {
192                "<w:pageBreakBefore/>"
193            } else {
194                r#"<w:pageBreakBefore w:val="0"/>"#
195            });
196        }
197        if let Some(widow) = self.f_widow_control {
198            inner.push_str(if widow {
199                "<w:widowControl/>"
200            } else {
201                r#"<w:widowControl w:val="0"/>"#
202            });
203        }
204
205        if let Some(numbering) = self.num_pr_xml() {
206            inner.push_str(&numbering);
207        }
208
209        let borders: Vec<(&str, &Option<Border>)> = vec![
210            ("w:top", &self.brc_top),
211            ("w:left", &self.brc_left),
212            ("w:bottom", &self.brc_bottom),
213            ("w:right", &self.brc_right),
214            ("w:between", &self.brc_between),
215        ];
216        if borders.iter().any(|(_, border)| border.is_some()) {
217            inner.push_str("<w:pBdr>");
218            for (tag, border) in borders {
219                if let Some(border) = border {
220                    inner.push_str(&border.to_xml(tag));
221                }
222            }
223            inner.push_str("</w:pBdr>");
224        }
225
226        if let Some(shd) = &self.shd {
227            inner.push_str(&shd.to_xml());
228        }
229
230        if !self.tabs_deleted.is_empty() || !self.tabs_added.is_empty() {
231            inner.push_str("<w:tabs>");
232            for dxa in &self.tabs_deleted {
233                inner.push_str(&format!(r#"<w:tab w:val="clear" w:pos="{dxa}"/>"#));
234            }
235            for tab in &self.tabs_added {
236                let val = match tab.jc {
237                    1 => "center",
238                    2 => "right",
239                    3 => "decimal",
240                    4 => "bar",
241                    _ => "left",
242                };
243                let leader = match tab.leader {
244                    1 => Some("dot"),
245                    2 => Some("hyphen"),
246                    3 => Some("underscore"),
247                    4 => Some("heavy"),
248                    5 => Some("middleDot"),
249                    _ => None,
250                };
251                let mut tab_xml = format!(r#"<w:tab w:val="{val}" w:pos="{}""#, tab.dxa);
252                if let Some(leader) = leader {
253                    tab_xml.push_str(&format!(r#" w:leader="{leader}""#));
254                }
255                tab_xml.push_str("/>");
256                inner.push_str(&tab_xml);
257            }
258            inner.push_str("</w:tabs>");
259        }
260
261        let has_spacing = self.dya_before.is_some()
262            || self.dya_after.is_some()
263            || self.lspd.is_some()
264            || self.before_auto.is_some()
265            || self.after_auto.is_some();
266        if has_spacing {
267            let mut spacing = String::from("<w:spacing");
268            if let Some(before) = self.dya_before {
269                spacing.push_str(&format!(r#" w:before="{before}""#));
270            }
271            if let Some(automatic) = self.before_auto {
272                spacing.push_str(if automatic {
273                    r#" w:beforeAutospacing="1""#
274                } else {
275                    r#" w:beforeAutospacing="0""#
276                });
277            }
278            if let Some(after) = self.dya_after {
279                spacing.push_str(&format!(r#" w:after="{after}""#));
280            }
281            if let Some(automatic) = self.after_auto {
282                spacing.push_str(if automatic {
283                    r#" w:afterAutospacing="1""#
284                } else {
285                    r#" w:afterAutospacing="0""#
286                });
287            }
288            if let Some(lspd) = self.lspd {
289                let (line, rule) = if lspd.f_mult {
290                    (lspd.dya_line as i32, "auto")
291                } else if lspd.dya_line >= 0 {
292                    (lspd.dya_line as i32, "atLeast")
293                } else {
294                    (-(lspd.dya_line as i32), "exact")
295                };
296                spacing.push_str(&format!(r#" w:line="{line}" w:lineRule="{rule}""#));
297            }
298            spacing.push_str("/>");
299            inner.push_str(&spacing);
300        }
301
302        if self.contextual_spacing == Some(true) {
303            inner.push_str("<w:contextualSpacing/>");
304        }
305
306        if self.dxa_left.is_some() || self.dxa_right.is_some() || self.dxa_left1.is_some() {
307            let mut ind = String::from("<w:ind");
308            if let Some(left) = self.dxa_left {
309                ind.push_str(&format!(r#" w:left="{left}""#));
310            }
311            if let Some(right) = self.dxa_right {
312                ind.push_str(&format!(r#" w:right="{right}""#));
313            }
314            if let Some(first) = self.dxa_left1 {
315                if first >= 0 {
316                    ind.push_str(&format!(r#" w:firstLine="{first}""#));
317                } else {
318                    ind.push_str(&format!(r#" w:hanging="{}""#, -(first as i32)));
319                }
320            }
321            ind.push_str("/>");
322            inner.push_str(&ind);
323        }
324
325        if let Some(jc) = self.jc {
326            let val = match jc {
327                1 => "center",
328                2 => "right",
329                3 => "both",
330                4 => "distribute",
331                _ => "left",
332            };
333            inner.push_str(&format!(r#"<w:jc w:val="{val}"/>"#));
334        }
335
336        if let Some(level) = self.outline_lvl {
337            if level < 9 {
338                inner.push_str(&format!(r#"<w:outlineLvl w:val="{level}"/>"#));
339            }
340        }
341        if self.f_bidi == Some(true) {
342            inner.push_str("<w:bidi/>");
343        }
344
345        inner
346    }
347
348    /// The w:numPr element when list formatting applies. ilfo 0 means "no
349    /// list"; some writers use it to cancel inherited list membership.
350    pub fn num_pr_xml(&self) -> Option<String> {
351        let ilfo = self.ilfo?;
352        if ilfo == 0 || ilfo == 0xF801 {
353            return Some(r#"<w:numPr><w:numId w:val="0"/></w:numPr>"#.to_string());
354        }
355        let ilfo = if ilfo >= 0xF802 {
356            ilfo.wrapping_neg()
357        } else {
358            ilfo
359        };
360        let ilvl = self.ilvl.unwrap_or(0);
361        Some(format!(
362            r#"<w:numPr><w:ilvl w:val="{ilvl}"/><w:numId w:val="{ilfo}"/></w:numPr>"#
363        ))
364    }
365}
366
367fn sprm_bytes(sprm: &Sprm<'_>) -> Vec<u8> {
368    let mut bytes = sprm.opcode.to_le_bytes().to_vec();
369    let spra = (sprm.opcode >> 13) & 0x7;
370    if spra == 6 {
371        if sprm.opcode == 0xD608 || sprm.opcode == 0xD606 {
372            bytes.extend_from_slice(&((sprm.operand.len() + 1) as u16).to_le_bytes());
373        } else {
374            bytes.push(sprm.operand.len() as u8);
375        }
376    }
377    bytes.extend_from_slice(sprm.operand);
378    bytes
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn automatic_spacing_preserves_explicit_false_and_absence() {
387        assert!(!Pap::default()
388            .to_ppr_inner_xml(&|_| None)
389            .contains("Autospacing"));
390        let mut pap = Pap::default();
391        for value in [1u8, 0u8] {
392            pap.apply_papx(&[0x5B, 0x24, value, 0x5C, 0x24, value]);
393            let xml = pap.to_ppr_inner_xml(&|_| None);
394            let spacing = crate::parse::parse_paragraph_spacing_from_xml(&xml).unwrap();
395            assert_eq!(spacing.before_auto_spacing, Some(value != 0));
396            assert_eq!(spacing.after_auto_spacing, Some(value != 0));
397        }
398    }
399
400    #[test]
401    fn applies_indent_and_justification() {
402        let grpprl: Vec<u8> = vec![
403            0x03, 0x24, 0x01, // jc center
404            0x0F, 0x84, 0xD0, 0x02, // dxaLeft 720
405            0x11, 0x84, 0x30, 0xFE, // dxaLeft1 -464 (hanging)
406        ];
407        let mut pap = Pap::default();
408        pap.apply_papx(&grpprl);
409        let xml = pap.to_ppr_inner_xml(&|_| None);
410        assert!(xml.contains(r#"<w:jc w:val="center"/>"#));
411        assert!(xml.contains(r#"w:left="720""#));
412        assert!(xml.contains(r#"w:hanging="464""#));
413    }
414
415    #[test]
416    fn separates_table_sprms() {
417        let grpprl: Vec<u8> = vec![
418            0x16, 0x24, 0x01, // fInTable
419            0x17, 0x24, 0x01, // fTtp
420            0x07, 0x94, 0x40, 0x01, // sprmTDyaRowHeight 320 (sgc=5)
421        ];
422        let mut pap = Pap::default();
423        pap.apply_papx(&grpprl);
424        assert!(pap.f_in_table);
425        assert!(pap.f_ttp);
426        assert_eq!(pap.table_grpprl, vec![0x07, 0x94, 0x40, 0x01]);
427    }
428
429    #[test]
430    fn line_spacing_rules() {
431        let mut pap = Pap::default();
432        pap.lspd = Some(LineSpacing {
433            dya_line: 480,
434            f_mult: true,
435        });
436        let xml = pap.to_ppr_inner_xml(&|_| None);
437        assert!(xml.contains(r#"w:line="480" w:lineRule="auto""#));
438
439        let mut exact = Pap::default();
440        exact.lspd = Some(LineSpacing {
441            dya_line: -240,
442            f_mult: false,
443        });
444        let xml = exact.to_ppr_inner_xml(&|_| None);
445        assert!(xml.contains(r#"w:line="240" w:lineRule="exact""#));
446    }
447}