Skip to main content

rust_docx/doc/
fmt.rs

1//! Shared primitives for mapping binary formatting structures (colors, borders,
2//! shading) onto their OOXML equivalents, plus XML string helpers.
3
4pub(super) fn dttm_to_iso(value: u32) -> Option<String> {
5    let minute = value & 63;
6    let hour = value >> 6 & 31;
7    let day = value >> 11 & 31;
8    let month = value >> 16 & 15;
9    let year = 1900 + (value >> 20 & 511);
10    (minute < 60 && hour < 24 && (1..=31).contains(&day) && (1..=12).contains(&month))
11        .then(|| format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:00"))
12}
13
14pub fn escape_xml(text: &str) -> String {
15    let mut out = String::with_capacity(text.len());
16    escape_xml_into(text, &mut out);
17    out
18}
19
20pub(super) fn escape_xml_into(text: &str, out: &mut String) {
21    let mut start = 0;
22    for (index, byte) in text.bytes().enumerate() {
23        let escaped = match byte {
24            b'&' => "&amp;",
25            b'<' => "&lt;",
26            b'>' => "&gt;",
27            b'"' => "&quot;",
28            b'\'' => "&apos;",
29            _ => continue,
30        };
31        out.push_str(&text[start..index]);
32        out.push_str(escaped);
33        start = index + 1;
34    }
35    out.push_str(&text[start..]);
36}
37
38/// Classic 17-entry Word color palette (ico values 1-16; 0 = auto).
39const ICO_PALETTE: [&str; 17] = [
40    "auto", "000000", "0000FF", "00FFFF", "00FF00", "FF00FF", "FF0000", "FFFF00", "FFFFFF",
41    "000080", "008080", "008000", "800080", "800000", "808000", "808080", "C0C0C0",
42];
43
44pub fn ico_to_hex(ico: u8) -> Option<&'static str> {
45    match ico as usize {
46        0 => None,
47        index if index < ICO_PALETTE.len() => Some(ICO_PALETTE[index]),
48        _ => None,
49    }
50}
51
52pub fn ico_to_highlight(ico: u8) -> Option<&'static str> {
53    Some(match ico {
54        1 => "black",
55        2 => "blue",
56        3 => "cyan",
57        4 => "green",
58        5 => "magenta",
59        6 => "red",
60        7 => "yellow",
61        8 => "white",
62        9 => "darkBlue",
63        10 => "darkCyan",
64        11 => "darkGreen",
65        12 => "darkMagenta",
66        13 => "darkRed",
67        14 => "darkYellow",
68        15 => "darkGray",
69        16 => "lightGray",
70        _ => return None,
71    })
72}
73
74/// COLORREF ([MS-DOC] 2.9.39): bytes R, G, B, fAuto. Returns hex or None for auto.
75pub fn colorref_to_hex(bytes: &[u8]) -> Option<String> {
76    if bytes.len() < 4 || bytes[3] == 0xFF {
77        return None;
78    }
79    Some(format!("{:02X}{:02X}{:02X}", bytes[0], bytes[1], bytes[2]))
80}
81
82fn brc_type_to_val(brc_type: u8) -> Option<&'static str> {
83    Some(match brc_type {
84        0 | 255 => return None,
85        1 => "single",
86        2 => "thick",
87        3 => "double",
88        5 => "single", // hairline
89        6 => "dotted",
90        7 => "dashed",
91        8 => "dotDash",
92        9 => "dotDotDash",
93        10 => "triple",
94        11 => "thinThickSmallGap",
95        12 => "thickThinSmallGap",
96        13 => "thinThickThinSmallGap",
97        14 => "thinThickMediumGap",
98        15 => "thickThinMediumGap",
99        16 => "thinThickThinMediumGap",
100        17 => "thinThickLargeGap",
101        18 => "thickThinLargeGap",
102        19 => "thinThickThinLargeGap",
103        20 => "wave",
104        21 => "doubleWave",
105        22 => "dashSmallGap",
106        23 => "dashDotStroked",
107        24 => "threeDEmboss",
108        25 => "threeDEngrave",
109        26 => "outset",
110        27 => "inset",
111        _ => "single",
112    })
113}
114
115/// A border ready for OOXML emission.
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct Border {
118    pub val: &'static str,
119    /// Eighths of a point (same unit as the binary dptLineWidth and OOXML w:sz).
120    pub sz: u8,
121    pub color: Option<String>,
122    pub space: u8,
123    pub shadow: bool,
124}
125
126impl Border {
127    pub fn to_xml(&self, tag: &str) -> String {
128        let mut xml = format!(
129            r#"<{tag} w:val="{}" w:sz="{}" w:space="{}""#,
130            self.val, self.sz, self.space
131        );
132        xml.push_str(&format!(
133            r#" w:color="{}""#,
134            self.color.as_deref().unwrap_or("auto")
135        ));
136        if self.shadow {
137            xml.push_str(r#" w:shadow="1""#);
138        }
139        xml.push_str("/>");
140        xml
141    }
142}
143
144/// BRC80 ([MS-DOC] 2.9.16): dptLineWidth, brcType, ico, dptSpace:5 fShadow:1 ...
145pub fn parse_brc80(bytes: &[u8]) -> Option<Border> {
146    if bytes.len() < 4 {
147        return None;
148    }
149    // 0xFFFFFFFF means "nil" (inherit / not specified).
150    if bytes[..4] == [0xFF, 0xFF, 0xFF, 0xFF] {
151        return None;
152    }
153    let val = brc_type_to_val(bytes[1])?;
154    Some(Border {
155        val,
156        sz: bytes[0],
157        color: ico_to_hex(bytes[2]).map(|hex| hex.to_string()),
158        space: bytes[3] & 0x1F,
159        shadow: bytes[3] & 0x20 != 0,
160    })
161}
162
163/// Brc ([MS-DOC] 2.9.17, Word 2000+): cv COLORREF, dptLineWidth, brcType,
164/// dptSpace:5 fShadow:1 fFrame:1.
165pub fn parse_brc97(bytes: &[u8]) -> Option<Border> {
166    if bytes.len() < 8 {
167        return None;
168    }
169    let val = brc_type_to_val(bytes[5])?;
170    Some(Border {
171        val,
172        sz: bytes[4],
173        color: colorref_to_hex(&bytes[..4]),
174        space: bytes[6] & 0x1F,
175        shadow: bytes[6] & 0x20 != 0,
176    })
177}
178
179fn ipat_to_val(ipat: u16) -> &'static str {
180    match ipat {
181        0 => "clear",
182        1 => "solid",
183        2 => "pct5",
184        3 => "pct10",
185        4 => "pct20",
186        5 => "pct25",
187        6 => "pct30",
188        7 => "pct40",
189        8 => "pct50",
190        9 => "pct60",
191        10 => "pct70",
192        11 => "pct75",
193        12 => "pct80",
194        13 => "pct90",
195        14 => "horzStripe",
196        15 => "vertStripe",
197        16 => "reverseDiagStripe",
198        17 => "diagStripe",
199        18 => "horzCross",
200        19 => "diagCross",
201        35 => "pct10",
202        _ => "clear",
203    }
204}
205
206#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct Shading {
208    pub val: &'static str,
209    pub color: Option<String>,
210    pub fill: Option<String>,
211}
212
213impl Shading {
214    pub fn to_xml(&self) -> String {
215        format!(
216            r#"<w:shd w:val="{}" w:color="{}" w:fill="{}"/>"#,
217            self.val,
218            self.color.as_deref().unwrap_or("auto"),
219            self.fill.as_deref().unwrap_or("auto"),
220        )
221    }
222
223    pub fn is_visible(&self) -> bool {
224        self.val != "clear" || self.fill.is_some()
225    }
226}
227
228/// Shd80 ([MS-DOC] 2.9.245): icoFore:5 icoBack:5 ipat:6.
229pub fn parse_shd80(value: u16) -> Option<Shading> {
230    if value == 0xFFFF {
231        return None; // ShdNil
232    }
233    let ico_fore = (value & 0x1F) as u8;
234    let ico_back = ((value >> 5) & 0x1F) as u8;
235    let ipat = value >> 10;
236    Some(Shading {
237        val: ipat_to_val(ipat),
238        color: ico_to_hex(ico_fore).map(|hex| hex.to_string()),
239        fill: ico_to_hex(ico_back).map(|hex| hex.to_string()),
240    })
241}
242
243/// SHDOperand ([MS-DOC] 2.9.249): cvFore COLORREF, cvBack COLORREF, ipat u16.
244pub fn parse_shd_operand(bytes: &[u8]) -> Option<Shading> {
245    if bytes.len() < 10 {
246        return None;
247    }
248    let ipat = u16::from_le_bytes([bytes[8], bytes[9]]);
249    Some(Shading {
250        val: ipat_to_val(ipat),
251        color: colorref_to_hex(&bytes[..4]),
252        fill: colorref_to_hex(&bytes[4..8]),
253    })
254}
255
256/// Underline kind (kul) to w:u value.
257pub fn kul_to_val(kul: u8) -> &'static str {
258    match kul {
259        0 => "none",
260        1 => "single",
261        2 => "words",
262        3 => "double",
263        4 => "dotted",
264        6 => "thick",
265        7 => "dash",
266        9 => "dotDash",
267        10 => "dotDotDash",
268        11 => "wave",
269        20 => "dottedHeavy",
270        23 => "dashedHeavy",
271        25 => "dashDotHeavy",
272        26 => "dashDotDotHeavy",
273        27 => "wavyHeavy",
274        39 => "dashLong",
275        43 => "wavyDouble",
276        55 => "dashLongHeavy",
277        _ => "single",
278    }
279}
280
281/// Page-number format (nfc) to w:pgNumType w:fmt value.
282pub fn nfc_to_num_fmt(nfc: u8) -> &'static str {
283    match nfc {
284        0 => "decimal",
285        1 => "upperRoman",
286        2 => "lowerRoman",
287        3 => "upperLetter",
288        4 => "lowerLetter",
289        5 => "ordinal",
290        6 => "cardinalText",
291        7 => "ordinalText",
292        22 => "decimalZero",
293        23 => "bullet",
294        47 => "chosung",
295        _ => "decimal",
296    }
297}