1use std::collections::{HashMap, HashSet};
7
8use rdocx_oxml::numbering::{CT_Numbering, 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}
40
41pub struct NumberingState {
43 counters: HashMap<(u32, u32), u32>,
45}
46
47impl Default for NumberingState {
48 fn default() -> Self {
49 Self::new()
50 }
51}
52
53impl NumberingState {
54 pub fn new() -> Self {
55 NumberingState {
56 counters: HashMap::new(),
57 }
58 }
59
60 pub fn advance(&mut self, num_id: u32, ilvl: u32, start: u32) -> u32 {
63 let key = (num_id, ilvl);
64 let counter = self.counters.entry(key).or_insert(start - 1);
65 *counter += 1;
66 let value = *counter;
67
68 for deeper in (ilvl + 1)..=8 {
70 self.counters.remove(&(num_id, deeper));
71 }
72
73 value
74 }
75
76 pub fn current(&self, num_id: u32, ilvl: u32) -> u32 {
78 self.counters.get(&(num_id, ilvl)).copied().unwrap_or(0)
79 }
80}
81
82pub fn resolve_paragraph_properties(style_id: Option<&str>, styles: &CT_Styles) -> CT_PPr {
84 let mut effective = CT_PPr::default();
85
86 if let Some(ref defaults) = styles.doc_defaults
88 && let Some(ref ppr) = defaults.ppr
89 {
90 effective.merge_from(ppr);
91 }
92
93 if let Some(sid) = style_id {
95 let chain = collect_style_chain(sid, styles);
96 for style in chain.iter().rev() {
98 if let Some(ref ppr) = style.ppr {
99 effective.merge_from(ppr);
100 }
101 }
102 } else {
103 if let Some(default_style) = styles.get_default(StyleType::Paragraph)
105 && let Some(ref ppr) = default_style.ppr
106 {
107 effective.merge_from(ppr);
108 }
109 }
110
111 effective
112}
113
114pub fn resolve_run_properties(
116 para_style_id: Option<&str>,
117 run_style_id: Option<&str>,
118 styles: &CT_Styles,
119) -> CT_RPr {
120 let mut effective = CT_RPr::default();
121
122 if let Some(ref defaults) = styles.doc_defaults
124 && let Some(ref rpr) = defaults.rpr
125 {
126 effective.merge_from(rpr);
127 }
128
129 let para_sid = para_style_id.or_else(|| {
131 styles
132 .get_default(StyleType::Paragraph)
133 .map(|s| s.style_id.as_str())
134 });
135 if let Some(sid) = para_sid {
136 let chain = collect_style_chain(sid, styles);
137 for style in chain.iter().rev() {
138 if let Some(ref rpr) = style.rpr {
139 effective.merge_from(rpr);
140 }
141 }
142 }
143
144 if let Some(sid) = run_style_id {
146 let chain = collect_style_chain(sid, styles);
147 for style in chain.iter().rev() {
148 if let Some(ref rpr) = style.rpr {
149 effective.merge_from(rpr);
150 }
151 }
152 }
153
154 effective
155}
156
157pub fn level_paragraph_properties(
163 num_id: u32,
164 ilvl: u32,
165 numbering: &CT_Numbering,
166) -> Option<&CT_PPr> {
167 let abs = numbering.get_abstract_num_for(num_id)?;
168 abs.levels.iter().find(|l| l.ilvl == ilvl)?.ppr.as_ref()
169}
170
171pub fn generate_marker(
173 num_id: u32,
174 ilvl: u32,
175 numbering: &CT_Numbering,
176 state: &mut NumberingState,
177) -> Option<ResolvedNumbering> {
178 let abs = numbering.get_abstract_num_for(num_id)?;
179 let lvl = abs.levels.iter().find(|l| l.ilvl == ilvl)?;
180
181 let counter_id = abs.abstract_num_id;
187
188 let num_fmt = lvl.num_fmt.unwrap_or(ST_NumberFormat::Decimal);
189 let start = lvl.start.unwrap_or(1);
190 let lvl_text = lvl.lvl_text.as_deref().unwrap_or("%1.");
191
192 let marker_text = if num_fmt == ST_NumberFormat::Bullet {
193 lvl_text.to_string()
194 } else {
195 let count = state.advance(counter_id, ilvl, start);
196 format_lvl_text(lvl_text, num_id, counter_id, ilvl, count, numbering, state)
197 };
198
199 let marker_rpr = lvl.rpr.clone().unwrap_or_default();
200
201 Some(ResolvedNumbering {
202 marker_text,
203 marker_rpr,
204 })
205}
206
207fn format_lvl_text(
209 template: &str,
210 num_id: u32,
211 counter_id: u32,
212 current_ilvl: u32,
213 current_count: u32,
214 numbering: &CT_Numbering,
215 state: &NumberingState,
216) -> String {
217 let abs = match numbering.get_abstract_num_for(num_id) {
218 Some(a) => a,
219 None => return template.to_string(),
220 };
221
222 let mut result = template.to_string();
223 for lvl_idx in 0..=8u32 {
224 let placeholder = format!("%{}", lvl_idx + 1);
225 if result.contains(&placeholder) {
226 let count = if lvl_idx == current_ilvl {
227 current_count
228 } else {
229 state.current(counter_id, lvl_idx)
230 };
231 let fmt = abs
232 .levels
233 .iter()
234 .find(|l| l.ilvl == lvl_idx)
235 .and_then(|l| l.num_fmt)
236 .unwrap_or(ST_NumberFormat::Decimal);
237 let formatted = format_number(count, fmt);
238 result = result.replace(&placeholder, &formatted);
239 }
240 }
241 result
242}
243
244fn format_number(n: u32, fmt: ST_NumberFormat) -> String {
246 match fmt {
247 ST_NumberFormat::Decimal => n.to_string(),
248 ST_NumberFormat::UpperRoman => to_roman(n, true),
249 ST_NumberFormat::LowerRoman => to_roman(n, false),
250 ST_NumberFormat::UpperLetter => to_letter(n, true),
251 ST_NumberFormat::LowerLetter => to_letter(n, false),
252 ST_NumberFormat::Ordinal => format!("{n}"),
253 ST_NumberFormat::Bullet | ST_NumberFormat::None => String::new(),
254 }
255}
256
257fn to_roman(mut n: u32, upper: bool) -> String {
258 let vals = [
259 (1000, "M"),
260 (900, "CM"),
261 (500, "D"),
262 (400, "CD"),
263 (100, "C"),
264 (90, "XC"),
265 (50, "L"),
266 (40, "XL"),
267 (10, "X"),
268 (9, "IX"),
269 (5, "V"),
270 (4, "IV"),
271 (1, "I"),
272 ];
273 let mut result = String::new();
274 for &(value, numeral) in &vals {
275 while n >= value {
276 result.push_str(numeral);
277 n -= value;
278 }
279 }
280 if upper { result } else { result.to_lowercase() }
281}
282
283fn to_letter(n: u32, upper: bool) -> String {
284 if n == 0 {
285 return String::new();
286 }
287 let base = if upper { b'A' } else { b'a' };
288 let idx = ((n - 1) % 26) as u8;
289 String::from(char::from(base + idx))
290}
291
292fn collect_style_chain<'a>(style_id: &str, styles: &'a CT_Styles) -> Vec<&'a CT_Style> {
294 let mut chain = Vec::new();
295 let mut current_id = Some(style_id.to_string());
296 let mut seen = HashSet::new();
297
298 while let Some(ref sid) = current_id {
299 if !seen.insert(sid.clone()) {
300 break; }
302 if let Some(style) = styles.get_by_id(sid) {
303 chain.push(style);
304 current_id = style.based_on.clone();
305 } else {
306 break;
307 }
308 }
309
310 chain
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316 use rdocx_oxml::units::{HalfPoint, Twips};
317
318 fn test_styles() -> CT_Styles {
319 let mut styles = CT_Styles::new_default();
320 styles.styles.push(CT_Style {
321 style_id: "Heading2".to_string(),
322 style_type: StyleType::Paragraph,
323 name: Some("heading 2".to_string()),
324 based_on: Some("Heading1".to_string()),
325 next_style: Some("Normal".to_string()),
326 is_default: false,
327 ppr: Some(CT_PPr {
328 space_before: Some(Twips(40)),
329 ..Default::default()
330 }),
331 rpr: Some(CT_RPr {
332 sz: Some(HalfPoint(26)),
333 color: Some("2E74B5".to_string()),
334 ..Default::default()
335 }),
336 });
337 styles
338 }
339
340 #[test]
341 fn resolve_normal_paragraph() {
342 let styles = test_styles();
343 let ppr = resolve_paragraph_properties(Some("Normal"), &styles);
344 assert_eq!(ppr.space_after, Some(Twips(160)));
345 }
346
347 #[test]
348 fn resolve_heading1() {
349 let styles = test_styles();
350 let ppr = resolve_paragraph_properties(Some("Heading1"), &styles);
351 assert_eq!(ppr.keep_next, Some(true));
352 assert_eq!(ppr.space_before, Some(Twips(240)));
353 assert_eq!(ppr.space_after, Some(Twips(0)));
354 }
355
356 #[test]
357 fn resolve_heading2_inherits_heading1() {
358 let styles = test_styles();
359 let ppr = resolve_paragraph_properties(Some("Heading2"), &styles);
360 assert_eq!(ppr.keep_next, Some(true));
361 assert_eq!(ppr.space_before, Some(Twips(40)));
362 }
363
364 #[test]
365 fn resolve_heading2_rpr() {
366 let styles = test_styles();
367 let rpr = resolve_run_properties(Some("Heading2"), None, &styles);
368 assert_eq!(rpr.font_ascii, Some("Calibri".to_string()));
369 assert_eq!(rpr.sz, Some(HalfPoint(26)));
370 assert_eq!(rpr.bold, Some(true));
371 assert_eq!(rpr.color, Some("2E74B5".to_string()));
372 }
373
374 #[test]
375 fn numbering_decimal_marker() {
376 let mut numbering = CT_Numbering::new();
377 let num_id = numbering.add_numbered_list();
378
379 let mut state = NumberingState::new();
380 let marker1 = generate_marker(num_id, 0, &numbering, &mut state).unwrap();
381 assert_eq!(marker1.marker_text, "1.");
382 let marker2 = generate_marker(num_id, 0, &numbering, &mut state).unwrap();
383 assert_eq!(marker2.marker_text, "2.");
384 }
385
386 #[test]
387 fn numbering_bullet_marker() {
388 let mut numbering = CT_Numbering::new();
389 let num_id = numbering.add_bullet_list();
390
391 let mut state = NumberingState::new();
392 let marker = generate_marker(num_id, 0, &numbering, &mut state).unwrap();
393 assert_eq!(marker.marker_text, "\u{2022}");
394 }
395
396 #[test]
397 fn numbering_sub_level_reset() {
398 let mut numbering = CT_Numbering::new();
399 let num_id = numbering.add_numbered_list();
400
401 let mut state = NumberingState::new();
402 generate_marker(num_id, 0, &numbering, &mut state);
404 generate_marker(num_id, 0, &numbering, &mut state);
405 let sub = generate_marker(num_id, 1, &numbering, &mut state).unwrap();
407 assert_eq!(sub.marker_text, "a.");
408 generate_marker(num_id, 0, &numbering, &mut state);
410 let sub2 = generate_marker(num_id, 1, &numbering, &mut state).unwrap();
411 assert_eq!(sub2.marker_text, "a."); }
413
414 #[test]
415 fn roman_numeral_formatting() {
416 assert_eq!(to_roman(1, true), "I");
417 assert_eq!(to_roman(4, true), "IV");
418 assert_eq!(to_roman(9, true), "IX");
419 assert_eq!(to_roman(14, false), "xiv");
420 }
421
422 #[test]
423 fn letter_formatting() {
424 assert_eq!(to_letter(1, false), "a");
425 assert_eq!(to_letter(26, false), "z");
426 assert_eq!(to_letter(27, false), "a"); assert_eq!(to_letter(1, true), "A");
428 }
429
430 #[test]
437 fn shared_abstract_definition_continues_the_count() {
438 let mut numbering = CT_Numbering::new();
439 let first = numbering.add_numbered_list();
440 let abstract_id = numbering
441 .nums
442 .iter()
443 .find(|n| n.num_id == first)
444 .unwrap()
445 .abstract_num_id;
446
447 let second = numbering.nums.iter().map(|n| n.num_id).max().unwrap() + 1;
449 numbering.nums.push(rdocx_oxml::numbering::CT_Num {
450 num_id: second,
451 abstract_num_id: abstract_id,
452 extra_xml: Vec::new(),
453 extra_attributes: Vec::new(),
454 });
455
456 let mut state = NumberingState::new();
457 let a = generate_marker(first, 0, &numbering, &mut state).unwrap();
458 let b = generate_marker(second, 0, &numbering, &mut state).unwrap();
459 assert_eq!(a.marker_text, "1.");
460 assert_eq!(b.marker_text, "2.", "the count must carry across instances");
461 }
462
463 #[test]
465 fn separate_abstract_definitions_count_independently() {
466 let mut numbering = CT_Numbering::new();
467 let first = numbering.add_numbered_list();
468 let second = numbering.add_numbered_list();
469
470 let mut state = NumberingState::new();
471 let a = generate_marker(first, 0, &numbering, &mut state).unwrap();
472 let b = generate_marker(second, 0, &numbering, &mut state).unwrap();
473 assert_eq!(a.marker_text, "1.");
474 assert_eq!(b.marker_text, "1.");
475 }
476
477 #[test]
479 fn level_paragraph_properties_expose_per_level_indent() {
480 let mut numbering = CT_Numbering::new();
481 let num_id = numbering.add_numbered_list();
482
483 let lvl0 = level_paragraph_properties(num_id, 0, &numbering)
484 .expect("level 0 should carry paragraph properties");
485 let lvl1 = level_paragraph_properties(num_id, 1, &numbering)
486 .expect("level 1 should carry paragraph properties");
487
488 let left0 = lvl0.ind_left.expect("level 0 indent").0;
489 let left1 = lvl1.ind_left.expect("level 1 indent").0;
490 assert!(
491 left1 > left0,
492 "level 1 must indent further than level 0, got {left0} then {left1}"
493 );
494 }
495}