1pub(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'&' => "&",
25 b'<' => "<",
26 b'>' => ">",
27 b'"' => """,
28 b'\'' => "'",
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
38const 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
74pub 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", 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#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct Border {
118 pub val: &'static str,
119 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
144pub fn parse_brc80(bytes: &[u8]) -> Option<Border> {
146 if bytes.len() < 4 {
147 return None;
148 }
149 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
163pub 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
228pub fn parse_shd80(value: u16) -> Option<Shading> {
230 if value == 0xFFFF {
231 return None; }
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
243pub 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
256pub 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
281pub 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}