1use std::collections::HashSet;
7
8use super::chp::{Chp, ToggleSet};
9use super::fmt::escape_xml;
10use super::fonts::FontTable;
11use super::pap::Pap;
12
13const ISTD_NIL: u16 = 0x0FFF;
14pub const STK_PARAGRAPH: u8 = 1;
15pub const STK_CHARACTER: u8 = 2;
16
17#[derive(Debug, Clone)]
18pub struct StyleDef {
19 pub istd: u16,
20 pub sti: u16,
21 pub stk: u8,
22 pub istd_base: u16,
23 pub istd_next: u16,
24 pub name: String,
25 pub style_id: String,
26 pub papx: Vec<u8>,
27 pub chpx: Vec<u8>,
28 pub resolved_toggles: ToggleSet,
29}
30
31pub struct StyleSheet {
32 pub styles: Vec<Option<StyleDef>>,
33 pub ftc_standard: [Option<u16>; 3],
35}
36
37fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
38 bytes
39 .get(offset..offset + 2)
40 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
41}
42
43fn style_id_from_name(name: &str, used: &HashSet<String>) -> String {
44 let mut id = String::new();
45 let mut capitalize = true;
46 for ch in name.chars() {
47 if ch.is_ascii_alphanumeric() {
48 if capitalize {
49 id.extend(ch.to_uppercase());
50 capitalize = false;
51 } else {
52 id.push(ch);
53 }
54 } else {
55 capitalize = true;
56 }
57 }
58 if id.is_empty() {
59 id = "Style".to_string();
60 }
61 let mut candidate = id.clone();
62 let mut counter = 1;
63 while used.contains(&candidate) {
64 counter += 1;
65 candidate = format!("{id}{counter}");
66 }
67 candidate
68}
69
70impl StyleSheet {
71 pub fn empty() -> StyleSheet {
72 StyleSheet {
73 styles: Vec::new(),
74 ftc_standard: [None, None, None],
75 }
76 }
77
78 pub fn parse(stsh: &[u8]) -> StyleSheet {
79 let Some(cb_stshi) = read_u16(stsh, 0) else {
80 return StyleSheet::empty();
81 };
82 let cb_stshi = cb_stshi as usize;
83 let stshi = &stsh[2..(2 + cb_stshi).min(stsh.len())];
84 let cstd = read_u16(stshi, 0).unwrap_or(0) as usize;
85 let cb_std_base = read_u16(stshi, 2).unwrap_or(10) as usize;
86 let ftc_standard = [
87 read_u16(stshi, 12),
88 read_u16(stshi, 14),
89 read_u16(stshi, 16),
90 ];
91
92 let mut styles: Vec<Option<StyleDef>> = Vec::with_capacity(cstd);
93 let mut used_ids: HashSet<String> = HashSet::new();
94 let mut pos = 2 + cb_stshi;
95
96 for istd in 0..cstd {
97 let Some(cb_std) = read_u16(stsh, pos) else {
98 break;
99 };
100 let cb_std = cb_std as usize;
101 let record_start = pos + 2;
102 pos = record_start + cb_std;
103 if cb_std % 2 == 1 {
104 pos += 1;
105 }
106 if cb_std == 0 {
107 styles.push(None);
108 continue;
109 }
110 let Some(record) = stsh.get(record_start..record_start + cb_std) else {
111 styles.push(None);
112 continue;
113 };
114 if let Some(mut def) = parse_std(record, cb_std_base, istd as u16) {
115 def.style_id = style_id_from_name(&def.name, &used_ids);
116 used_ids.insert(def.style_id.clone());
117 styles.push(Some(def));
118 } else {
119 styles.push(None);
120 }
121 }
122
123 let mut sheet = StyleSheet {
124 styles,
125 ftc_standard,
126 };
127 sheet.resolve_toggles();
128 sheet
129 }
130
131 fn resolve_toggles(&mut self) {
132 let count = self.styles.len();
133 let mut resolved: Vec<Option<ToggleSet>> = vec![None; count];
134 for istd in 0..count {
135 self.resolve_toggles_for(istd, &mut resolved, 0);
136 }
137 for (istd, toggles) in resolved.into_iter().enumerate() {
138 if let (Some(style), Some(toggles)) = (self.styles[istd].as_mut(), toggles) {
139 style.resolved_toggles = toggles;
140 }
141 }
142 }
143
144 fn resolve_toggles_for(
145 &self,
146 istd: usize,
147 resolved: &mut Vec<Option<ToggleSet>>,
148 depth: usize,
149 ) -> ToggleSet {
150 if depth > 16 {
151 return ToggleSet::default();
152 }
153 if let Some(Some(toggles)) = resolved.get(istd) {
154 return *toggles;
155 }
156 let Some(Some(style)) = self.styles.get(istd) else {
157 return ToggleSet::default();
158 };
159 let mut toggles =
160 if style.istd_base != ISTD_NIL && (style.istd_base as usize) < self.styles.len() {
161 self.resolve_toggles_for(style.istd_base as usize, resolved, depth + 1)
162 } else {
163 ToggleSet::default()
164 };
165 toggles.apply_style_grpprl(&style.chpx);
166 resolved[istd] = Some(toggles);
167 toggles
168 }
169
170 pub fn style(&self, istd: u16) -> Option<&StyleDef> {
171 self.styles
172 .get(istd as usize)
173 .and_then(|slot| slot.as_ref())
174 }
175
176 pub fn style_id(&self, istd: u16) -> Option<String> {
177 self.style(istd).map(|style| style.style_id.clone())
178 }
179
180 pub fn toggles(&self, istd: u16) -> ToggleSet {
181 self.style(istd)
182 .map(|style| style.resolved_toggles)
183 .unwrap_or_default()
184 }
185
186 pub fn run_toggle_base(&self, para_istd: u16, char_istd: Option<u16>) -> ToggleSet {
189 let para = self.toggles(para_istd);
190 match char_istd {
191 Some(istd) if self.style(istd).is_some() => self.toggles(istd),
192 Some(_) => para,
193 None => para,
194 }
195 }
196
197 pub fn to_styles_xml(&self, fonts: &FontTable, default_hps: u16) -> String {
199 let mut xml = String::from(
200 r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">"#,
201 );
202
203 xml.push_str("<w:docDefaults><w:rPrDefault><w:rPr>");
205 if let Some(ftc) = self.ftc_standard[0] {
206 if let Some(name) = fonts.name(ftc) {
207 let escaped = escape_xml(&name);
208 let east_asia = self.ftc_standard[1]
209 .and_then(|ftc| fonts.name(ftc))
210 .map(|name| escape_xml(&name))
211 .unwrap_or_else(|| escaped.clone());
212 let other = self.ftc_standard[2]
213 .and_then(|ftc| fonts.name(ftc))
214 .map(|name| escape_xml(&name))
215 .unwrap_or_else(|| escaped.clone());
216 xml.push_str(&format!(
217 r#"<w:rFonts w:ascii="{escaped}" w:hAnsi="{escaped}" w:eastAsia="{east_asia}" w:cs="{other}"/>"#
218 ));
219 }
220 }
221 xml.push_str(&format!(
222 r#"<w:sz w:val="{default_hps}"/><w:szCs w:val="{default_hps}"/>"#
223 ));
224 xml.push_str("</w:rPr></w:rPrDefault><w:pPrDefault/></w:docDefaults>");
225
226 for style in self.styles.iter().flatten() {
227 if style.stk != STK_PARAGRAPH && style.stk != STK_CHARACTER {
228 continue; }
230 let style_type = if style.stk == STK_PARAGRAPH {
231 "paragraph"
232 } else {
233 "character"
234 };
235 let is_default = style.sti == 0 || style.sti == 65;
236 xml.push_str(&format!(
237 r#"<w:style w:type="{style_type}" w:styleId="{}"{}>"#,
238 escape_xml(&style.style_id),
239 if is_default { r#" w:default="1""# } else { "" }
240 ));
241 xml.push_str(&format!(r#"<w:name w:val="{}"/>"#, escape_xml(&style.name)));
242 if style.istd_base != ISTD_NIL {
243 if let Some(base_id) = self.style_id(style.istd_base) {
244 xml.push_str(&format!(r#"<w:basedOn w:val="{}"/>"#, escape_xml(&base_id)));
245 }
246 }
247 if style.istd_next != style.istd && style.istd_next != ISTD_NIL {
248 if let Some(next_id) = self.style_id(style.istd_next) {
249 xml.push_str(&format!(r#"<w:next w:val="{}"/>"#, escape_xml(&next_id)));
250 }
251 }
252
253 if style.stk == STK_PARAGRAPH && !style.papx.is_empty() {
254 let mut pap = Pap::default();
255 pap.apply_papx(&style.papx);
256 let ppr_inner = pap.to_ppr_inner_xml(&|_| None);
257 if !ppr_inner.is_empty() {
258 xml.push_str(&format!("<w:pPr>{ppr_inner}</w:pPr>"));
259 }
260 }
261 if !style.chpx.is_empty() {
262 let base = if style.istd_base != ISTD_NIL {
263 self.toggles(style.istd_base)
264 } else {
265 ToggleSet::default()
266 };
267 let mut chp = Chp::default();
268 chp.apply_chpx(&style.chpx, &base);
269 let rpr = chp.to_rpr_xml(&|_| None, &|ftc| fonts.name(ftc));
270 xml.push_str(&rpr);
271 }
272 xml.push_str("</w:style>");
273 }
274
275 xml.push_str("</w:styles>");
276 xml
277 }
278}
279
280fn parse_std(record: &[u8], cb_std_base: usize, istd: u16) -> Option<StyleDef> {
283 let word0 = read_u16(record, 0)?;
284 let word1 = read_u16(record, 2)?;
285 let word2 = read_u16(record, 4)?;
286 let sti = word0 & 0x0FFF;
287 let stk = (word1 & 0x000F) as u8;
288 let istd_base = word1 >> 4;
289 let cupx = (word2 & 0x000F) as usize;
290 let istd_next = word2 >> 4;
291
292 let name_offset = cb_std_base;
294 let cch = read_u16(record, name_offset)? as usize;
295 let mut name = String::new();
296 for index in 0..cch {
297 let unit = read_u16(record, name_offset + 2 + index * 2)?;
298 name.push_str(&String::from_utf16_lossy(&[unit]));
299 }
300
301 let mut pos = name_offset + 2 + cch * 2 + 2;
304 if pos % 2 == 1 {
305 pos += 1;
306 }
307
308 let mut papx = Vec::new();
309 let mut chpx = Vec::new();
310 for upx_index in 0..cupx {
311 let cb_upx = read_u16(record, pos)? as usize;
312 let payload_start = pos + 2;
313 let payload = record.get(payload_start..payload_start + cb_upx)?;
314 if stk == STK_PARAGRAPH && upx_index == 0 {
315 if payload.len() >= 2 {
317 papx = payload[2..].to_vec();
318 }
319 } else {
320 if chpx.is_empty() {
322 chpx = payload.to_vec();
323 }
324 }
325 pos = payload_start + cb_upx;
326 if pos % 2 == 1 {
327 pos += 1;
328 }
329 }
330
331 Some(StyleDef {
332 istd,
333 sti,
334 stk,
335 istd_base,
336 istd_next,
337 name,
338 style_id: String::new(),
339 papx,
340 chpx,
341 resolved_toggles: ToggleSet::default(),
342 })
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 fn build_std(
350 sti: u16,
351 stk: u8,
352 istd_base: u16,
353 istd_next: u16,
354 name: &str,
355 papx: &[u8],
356 chpx: &[u8],
357 ) -> Vec<u8> {
358 let mut record = Vec::new();
359 record.extend_from_slice(&sti.to_le_bytes());
360 record.extend_from_slice(&((istd_base << 4) | stk as u16).to_le_bytes());
361 let cupx: u16 = if stk == STK_PARAGRAPH { 2 } else { 1 };
362 record.extend_from_slice(&((istd_next << 4) | cupx).to_le_bytes());
363 record.extend_from_slice(&0u16.to_le_bytes()); record.extend_from_slice(&0u16.to_le_bytes()); let units: Vec<u16> = name.encode_utf16().collect();
366 record.extend_from_slice(&(units.len() as u16).to_le_bytes());
367 for unit in &units {
368 record.extend_from_slice(&unit.to_le_bytes());
369 }
370 record.extend_from_slice(&0u16.to_le_bytes());
371 if record.len() % 2 == 1 {
372 record.push(0);
373 }
374 if stk == STK_PARAGRAPH {
375 let cb = (papx.len() + 2) as u16;
376 record.extend_from_slice(&cb.to_le_bytes());
377 record.extend_from_slice(&0u16.to_le_bytes()); record.extend_from_slice(papx);
379 if record.len() % 2 == 1 {
380 record.push(0);
381 }
382 }
383 record.extend_from_slice(&(chpx.len() as u16).to_le_bytes());
384 record.extend_from_slice(chpx);
385 if record.len() % 2 == 1 {
386 record.push(0);
387 }
388 record
389 }
390
391 fn build_stsh(stds: &[Vec<u8>]) -> Vec<u8> {
392 let mut stshi = Vec::new();
393 stshi.extend_from_slice(&(stds.len() as u16).to_le_bytes());
394 stshi.extend_from_slice(&10u16.to_le_bytes()); stshi.extend_from_slice(&[0u8; 8]); stshi.extend_from_slice(&0u16.to_le_bytes()); stshi.extend_from_slice(&0u16.to_le_bytes());
398 stshi.extend_from_slice(&0u16.to_le_bytes());
399
400 let mut stsh = Vec::new();
401 stsh.extend_from_slice(&(stshi.len() as u16).to_le_bytes());
402 stsh.extend_from_slice(&stshi);
403 for std_record in stds {
404 stsh.extend_from_slice(&(std_record.len() as u16).to_le_bytes());
405 stsh.extend_from_slice(std_record);
406 if std_record.len() % 2 == 1 {
407 stsh.push(0);
408 }
409 }
410 stsh
411 }
412
413 #[test]
414 fn parses_styles_and_resolves_toggles() {
415 let bold_on: Vec<u8> = vec![0x35, 0x08, 0x01];
418 let bold_invert: Vec<u8> = vec![0x35, 0x08, 0x81];
419 let stds = vec![
420 build_std(0, STK_PARAGRAPH, ISTD_NIL, 0, "Normal", &[], &[]),
421 build_std(20, STK_PARAGRAPH, 0, 1, "Strong Para", &[], &bold_on),
422 build_std(21, STK_PARAGRAPH, 1, 2, "Unbold Para", &[], &bold_invert),
423 ];
424 let sheet = StyleSheet::parse(&build_stsh(&stds));
425
426 assert_eq!(sheet.style_id(0).as_deref(), Some("Normal"));
427 assert_eq!(sheet.style_id(1).as_deref(), Some("StrongPara"));
428 assert_eq!(
429 sheet.toggles(1).get(super::super::chp::Toggle::Bold),
430 Some(true)
431 );
432 assert_eq!(
433 sheet.toggles(2).get(super::super::chp::Toggle::Bold),
434 Some(false)
435 );
436
437 let xml = sheet.to_styles_xml(&FontTable::empty(), 20);
438 assert!(xml.contains(r#"<w:style w:type="paragraph" w:styleId="Normal" w:default="1">"#));
439 assert!(xml.contains(r#"<w:basedOn w:val="Normal"/>"#));
440 assert!(xml.contains("<w:b/>"));
441 }
442
443 #[test]
444 fn skips_empty_slots() {
445 let stds = vec![
446 build_std(0, STK_PARAGRAPH, ISTD_NIL, 0, "Normal", &[], &[]),
447 Vec::new(),
448 build_std(
449 65,
450 STK_CHARACTER,
451 ISTD_NIL,
452 2,
453 "Default Paragraph Font",
454 &[],
455 &[],
456 ),
457 ];
458 let mut stsh = build_stsh(&[stds[0].clone()]);
460 stsh[2] = 3;
462 stsh.extend_from_slice(&0u16.to_le_bytes());
463 let record = &stds[2];
464 stsh.extend_from_slice(&(record.len() as u16).to_le_bytes());
465 stsh.extend_from_slice(record);
466
467 let sheet = StyleSheet::parse(&stsh);
468 assert!(sheet.style(1).is_none());
469 assert_eq!(sheet.style_id(2).as_deref(), Some("DefaultParagraphFont"));
470 }
471}