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