1use std::collections::{HashMap, HashSet};
7
8use rdocx_oxml::numbering::{CT_Numbering, ST_LvlSuffix, ST_NumberFormat};
9use rdocx_oxml::properties::{CT_PPr, CT_RPr};
10use rdocx_oxml::styles::{CT_Style, CT_Styles, StyleType};
11
12#[derive(Debug, Clone)]
14pub struct ResolvedParagraph {
15 pub ppr: CT_PPr,
17 pub runs: Vec<ResolvedRun>,
19 pub numbering: Option<ResolvedNumbering>,
21}
22
23#[derive(Debug, Clone)]
25pub struct ResolvedRun {
26 pub rpr: CT_RPr,
28 pub content: Vec<rdocx_oxml::text::RunContent>,
30}
31
32#[derive(Debug, Clone)]
34pub struct ResolvedNumbering {
35 pub marker_text: String,
37 pub marker_rpr: CT_RPr,
39 pub suffix: ST_LvlSuffix,
41}
42
43#[derive(Clone)]
48pub struct NumberingState {
49 counters: HashMap<(u32, u32), u32>,
51}
52
53impl Default for NumberingState {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl NumberingState {
60 pub fn new() -> Self {
61 NumberingState {
62 counters: HashMap::new(),
63 }
64 }
65
66 pub fn advance(&mut self, num_id: u32, ilvl: u32, start: u32) -> u32 {
69 let key = (num_id, ilvl);
70 let counter = self.counters.entry(key).or_insert(start - 1);
71 *counter += 1;
72 let value = *counter;
73
74 for deeper in (ilvl + 1)..=8 {
76 self.counters.remove(&(num_id, deeper));
77 }
78
79 value
80 }
81
82 pub fn current(&self, num_id: u32, ilvl: u32) -> u32 {
84 self.counters.get(&(num_id, ilvl)).copied().unwrap_or(0)
85 }
86}
87
88pub fn resolve_paragraph_properties(style_id: Option<&str>, styles: &CT_Styles) -> CT_PPr {
90 resolve_paragraph_properties_in_table(style_id, styles, None)
91}
92
93pub fn resolve_paragraph_properties_in_table(
96 style_id: Option<&str>,
97 styles: &CT_Styles,
98 table_properties: Option<&CT_PPr>,
99) -> CT_PPr {
100 let mut effective = CT_PPr::default();
101
102 if let Some(ref defaults) = styles.doc_defaults
104 && let Some(ref ppr) = defaults.ppr
105 {
106 effective.merge_from(ppr);
107 }
108
109 if let Some(properties) = table_properties {
110 effective.merge_from(properties);
111 }
112
113 if let Some(sid) = style_id {
115 let chain = collect_style_chain(sid, styles);
116 for style in chain.iter().rev() {
118 if let Some(ref ppr) = style.ppr {
119 effective.merge_from(ppr);
120 }
121 }
122 } else {
123 if let Some(default_style) = styles.get_default(StyleType::Paragraph)
125 && let Some(ref ppr) = default_style.ppr
126 {
127 effective.merge_from(ppr);
128 }
129 }
130
131 effective
132}
133
134pub fn resolve_run_properties(
136 para_style_id: Option<&str>,
137 run_style_id: Option<&str>,
138 styles: &CT_Styles,
139) -> CT_RPr {
140 let mut effective = CT_RPr::default();
141
142 if let Some(ref defaults) = styles.doc_defaults
144 && let Some(ref rpr) = defaults.rpr
145 {
146 effective.merge_from(rpr);
147 }
148
149 let para_sid = para_style_id.or_else(|| {
151 styles
152 .get_default(StyleType::Paragraph)
153 .map(|s| s.style_id.as_str())
154 });
155 if let Some(sid) = para_sid {
156 let chain = collect_style_chain(sid, styles);
157 for style in chain.iter().rev() {
158 if let Some(ref rpr) = style.rpr {
159 effective.merge_from(rpr);
160 }
161 }
162 }
163
164 if let Some(sid) = run_style_id {
166 let chain = collect_style_chain(sid, styles);
167 for style in chain.iter().rev() {
168 if let Some(ref rpr) = style.rpr {
169 effective.merge_from(rpr);
170 }
171 }
172 }
173
174 effective
175}
176
177pub fn level_paragraph_properties(
183 num_id: u32,
184 ilvl: u32,
185 numbering: &CT_Numbering,
186) -> Option<&CT_PPr> {
187 let abs = numbering.get_abstract_num_for(num_id)?;
188 abs.levels.iter().find(|l| l.ilvl == ilvl)?.ppr.as_ref()
189}
190
191pub fn generate_marker(
193 num_id: u32,
194 ilvl: u32,
195 numbering: &CT_Numbering,
196 state: &mut NumberingState,
197) -> Option<ResolvedNumbering> {
198 let abs = numbering.get_abstract_num_for(num_id)?;
199 let lvl = abs.levels.iter().find(|l| l.ilvl == ilvl)?;
200
201 let counter_id = abs.abstract_num_id;
207
208 let num_fmt = lvl.num_fmt.clone().unwrap_or(ST_NumberFormat::Decimal);
209 let start = lvl.start.unwrap_or(1);
210 let lvl_text = lvl.lvl_text.as_deref().unwrap_or("%1.");
211
212 let marker_text = match num_fmt {
213 ST_NumberFormat::Bullet => lvl_text.to_string(),
214 ST_NumberFormat::Other(_) => String::new(),
215 _ => {
216 let count = state.advance(counter_id, ilvl, start);
217 format_lvl_text(lvl_text, num_id, counter_id, ilvl, count, numbering, state)
218 }
219 };
220
221 let marker_rpr = lvl.rpr.clone().unwrap_or_default();
222
223 Some(ResolvedNumbering {
224 marker_text,
225 marker_rpr,
226 suffix: lvl.suffix.unwrap_or(ST_LvlSuffix::Tab),
227 })
228}
229
230fn format_lvl_text(
232 template: &str,
233 num_id: u32,
234 counter_id: u32,
235 current_ilvl: u32,
236 current_count: u32,
237 numbering: &CT_Numbering,
238 state: &NumberingState,
239) -> String {
240 let abs = match numbering.get_abstract_num_for(num_id) {
241 Some(a) => a,
242 None => return template.to_string(),
243 };
244
245 let mut result = template.to_string();
246 for lvl_idx in 0..=8u32 {
247 let placeholder = format!("%{}", lvl_idx + 1);
248 if result.contains(&placeholder) {
249 let count = if lvl_idx == current_ilvl {
250 current_count
251 } else {
252 state.current(counter_id, lvl_idx)
253 };
254 let fmt = abs
255 .levels
256 .iter()
257 .find(|l| l.ilvl == lvl_idx)
258 .and_then(|l| l.num_fmt.clone())
259 .unwrap_or(ST_NumberFormat::Decimal);
260 let formatted = format_number(count, fmt);
261 result = result.replace(&placeholder, &formatted);
262 }
263 }
264 result
265}
266
267fn format_number(n: u32, fmt: ST_NumberFormat) -> String {
269 match fmt {
270 ST_NumberFormat::Decimal => n.to_string(),
271 ST_NumberFormat::UpperRoman => to_roman(n, true),
272 ST_NumberFormat::LowerRoman => to_roman(n, false),
273 ST_NumberFormat::UpperLetter => to_letter(n, true),
274 ST_NumberFormat::LowerLetter => to_letter(n, false),
275 ST_NumberFormat::Ordinal => format!("{n}"),
276 ST_NumberFormat::Bullet | ST_NumberFormat::None | ST_NumberFormat::Other(_) => {
277 String::new()
278 }
279 }
280}
281
282fn to_roman(mut n: u32, upper: bool) -> String {
283 let vals = [
284 (1000, "M"),
285 (900, "CM"),
286 (500, "D"),
287 (400, "CD"),
288 (100, "C"),
289 (90, "XC"),
290 (50, "L"),
291 (40, "XL"),
292 (10, "X"),
293 (9, "IX"),
294 (5, "V"),
295 (4, "IV"),
296 (1, "I"),
297 ];
298 let mut result = String::new();
299 for &(value, numeral) in &vals {
300 while n >= value {
301 result.push_str(numeral);
302 n -= value;
303 }
304 }
305 if upper { result } else { result.to_lowercase() }
306}
307
308fn to_letter(n: u32, upper: bool) -> String {
309 if n == 0 {
310 return String::new();
311 }
312 let base = if upper { b'A' } else { b'a' };
313 let idx = ((n - 1) % 26) as u8;
314 String::from(char::from(base + idx))
315}
316
317fn collect_style_chain<'a>(style_id: &str, styles: &'a CT_Styles) -> Vec<&'a CT_Style> {
319 let mut chain = Vec::new();
320 let mut current_id = Some(style_id.to_string());
321 let mut seen = HashSet::new();
322
323 while let Some(ref sid) = current_id {
324 if !seen.insert(sid.clone()) {
325 break; }
327 if let Some(style) = styles.get_by_id(sid) {
328 chain.push(style);
329 current_id = style.based_on.clone();
330 } else {
331 break;
332 }
333 }
334
335 chain
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use rdocx_oxml::units::{HalfPoint, Twips};
342
343 fn test_styles() -> CT_Styles {
344 let mut styles = CT_Styles::new_default();
345 styles.styles.push(CT_Style {
346 style_id: "Heading2".to_string(),
347 style_type: StyleType::Paragraph,
348 name: Some("heading 2".to_string()),
349 based_on: Some("Heading1".to_string()),
350 next_style: Some("Normal".to_string()),
351 is_default: false,
352 ppr: Some(CT_PPr {
353 space_before: Some(Twips(40)),
354 ..Default::default()
355 }),
356 rpr: Some(CT_RPr {
357 sz: Some(HalfPoint(26)),
358 color: Some("2E74B5".to_string()),
359 ..Default::default()
360 }),
361 table_properties: None,
362 table_properties_original: None,
363 table_properties_xml: None,
364 conditional_table_styles: Vec::new(),
365 extra_xml: Vec::new(),
366 });
367 styles
368 }
369
370 #[test]
371 fn resolve_normal_paragraph() {
372 let styles = test_styles();
373 let ppr = resolve_paragraph_properties(Some("Normal"), &styles);
374 assert_eq!(ppr.space_after, Some(Twips(160)));
375 }
376
377 #[test]
378 fn resolve_heading1() {
379 let styles = test_styles();
380 let ppr = resolve_paragraph_properties(Some("Heading1"), &styles);
381 assert_eq!(ppr.keep_next, Some(true));
382 assert_eq!(ppr.space_before, Some(Twips(240)));
383 assert_eq!(ppr.space_after, Some(Twips(0)));
384 }
385
386 #[test]
387 fn resolve_heading2_inherits_heading1() {
388 let styles = test_styles();
389 let ppr = resolve_paragraph_properties(Some("Heading2"), &styles);
390 assert_eq!(ppr.keep_next, Some(true));
391 assert_eq!(ppr.space_before, Some(Twips(40)));
392 }
393
394 #[test]
395 fn resolve_heading2_rpr() {
396 let styles = test_styles();
397 let rpr = resolve_run_properties(Some("Heading2"), None, &styles);
398 assert_eq!(rpr.font_ascii, Some("Calibri".to_string()));
399 assert_eq!(rpr.sz, Some(HalfPoint(26)));
400 assert_eq!(rpr.bold, Some(true));
401 assert_eq!(rpr.color, Some("2E74B5".to_string()));
402 }
403
404 #[test]
405 fn numbering_decimal_marker() {
406 let mut numbering = CT_Numbering::new();
407 let num_id = numbering.add_numbered_list();
408
409 let mut state = NumberingState::new();
410 let marker1 = generate_marker(num_id, 0, &numbering, &mut state).unwrap();
411 assert_eq!(marker1.marker_text, "1.");
412 let marker2 = generate_marker(num_id, 0, &numbering, &mut state).unwrap();
413 assert_eq!(marker2.marker_text, "2.");
414 }
415
416 #[test]
417 fn numbering_bullet_marker() {
418 let mut numbering = CT_Numbering::new();
419 let num_id = numbering.add_bullet_list();
420
421 let mut state = NumberingState::new();
422 let marker = generate_marker(num_id, 0, &numbering, &mut state).unwrap();
423 assert_eq!(marker.marker_text, "\u{2022}");
424 }
425
426 #[test]
427 fn numbering_sub_level_reset() {
428 let mut numbering = CT_Numbering::new();
429 let num_id = numbering.add_numbered_list();
430
431 let mut state = NumberingState::new();
432 generate_marker(num_id, 0, &numbering, &mut state);
434 generate_marker(num_id, 0, &numbering, &mut state);
435 let sub = generate_marker(num_id, 1, &numbering, &mut state).unwrap();
437 assert_eq!(sub.marker_text, "a.");
438 generate_marker(num_id, 0, &numbering, &mut state);
440 let sub2 = generate_marker(num_id, 1, &numbering, &mut state).unwrap();
441 assert_eq!(sub2.marker_text, "a."); }
443
444 #[test]
445 fn numbering_marker_uses_the_level_suffix() {
446 let mut numbering = CT_Numbering::new();
447 let num_id = numbering.add_numbered_list();
448 numbering.abstract_nums[0].levels[0].suffix = Some(ST_LvlSuffix::Nothing);
449
450 let marker = generate_marker(num_id, 0, &numbering, &mut NumberingState::new())
451 .expect("numbered marker resolves");
452
453 assert_eq!(marker.suffix, ST_LvlSuffix::Nothing);
454 }
455
456 #[test]
457 fn roman_numeral_formatting() {
458 assert_eq!(to_roman(1, true), "I");
459 assert_eq!(to_roman(4, true), "IV");
460 assert_eq!(to_roman(9, true), "IX");
461 assert_eq!(to_roman(14, false), "xiv");
462 }
463
464 #[test]
465 fn letter_formatting() {
466 assert_eq!(to_letter(1, false), "a");
467 assert_eq!(to_letter(26, false), "z");
468 assert_eq!(to_letter(27, false), "a"); assert_eq!(to_letter(1, true), "A");
470 }
471
472 #[test]
479 fn shared_abstract_definition_continues_the_count() {
480 let mut numbering = CT_Numbering::new();
481 let first = numbering.add_numbered_list();
482 let abstract_id = numbering
483 .nums
484 .iter()
485 .find(|n| n.num_id == first)
486 .unwrap()
487 .abstract_num_id;
488
489 let second = numbering.nums.iter().map(|n| n.num_id).max().unwrap() + 1;
491 numbering.nums.push(rdocx_oxml::numbering::CT_Num {
492 num_id: second,
493 abstract_num_id: abstract_id,
494 extra_xml: Vec::new(),
495 extra_attributes: Vec::new(),
496 });
497
498 let mut state = NumberingState::new();
499 let a = generate_marker(first, 0, &numbering, &mut state).unwrap();
500 let b = generate_marker(second, 0, &numbering, &mut state).unwrap();
501 assert_eq!(a.marker_text, "1.");
502 assert_eq!(b.marker_text, "2.", "the count must carry across instances");
503 }
504
505 #[test]
507 fn separate_abstract_definitions_count_independently() {
508 let mut numbering = CT_Numbering::new();
509 let first = numbering.add_numbered_list();
510 let second = numbering.add_numbered_list();
511
512 let mut state = NumberingState::new();
513 let a = generate_marker(first, 0, &numbering, &mut state).unwrap();
514 let b = generate_marker(second, 0, &numbering, &mut state).unwrap();
515 assert_eq!(a.marker_text, "1.");
516 assert_eq!(b.marker_text, "1.");
517 }
518
519 #[test]
521 fn level_paragraph_properties_expose_per_level_indent() {
522 let mut numbering = CT_Numbering::new();
523 let num_id = numbering.add_numbered_list();
524
525 let lvl0 = level_paragraph_properties(num_id, 0, &numbering)
526 .expect("level 0 should carry paragraph properties");
527 let lvl1 = level_paragraph_properties(num_id, 1, &numbering)
528 .expect("level 1 should carry paragraph properties");
529
530 let left0 = lvl0.ind_left.expect("level 0 indent").0;
531 let left1 = lvl1.ind_left.expect("level 1 indent").0;
532 assert!(
533 left1 > left0,
534 "level 1 must indent further than level 0, got {left0} then {left1}"
535 );
536 }
537
538 #[test]
539 fn producer_defined_number_formats_do_not_invent_layout_markers() {
540 assert_eq!(
541 format_number(7, ST_NumberFormat::Other("chicago".to_owned())),
542 ""
543 );
544 assert_eq!(format_number(7, ST_NumberFormat::Decimal), "7");
545
546 let mut numbering = CT_Numbering::new();
547 let num_id = numbering.add_list(&[(ST_NumberFormat::Other("chicago".to_owned()), Some(1))]);
548 let marker = generate_marker(num_id, 0, &numbering, &mut NumberingState::new()).unwrap();
549 assert_eq!(marker.marker_text, "");
550 }
551}