1use std::collections::{HashMap, HashSet};
2
3use crate::ProjectionError;
4use crate::projection::numbering::NumberingCatalog;
5use crate::projection::review::ReviewPoint;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum StructuralFactUnknownReason {
9 ParagraphFragment,
10 DocumentPartOnly,
11 StylesPartUnavailable,
12 UnsupportedStyles,
13 UnsupportedNumbering,
14 IncompleteBookmarkRanges,
15 UnsupportedInternalReferences,
16}
17
18#[derive(Clone, Debug, Eq, PartialEq)]
19pub enum StructuralFactSet<T> {
20 Known(Vec<T>),
21 Unknown(StructuralFactUnknownReason),
22}
23
24impl<T> StructuralFactSet<T> {
25 pub fn known(items: impl IntoIterator<Item = T>) -> Self {
26 Self::Known(items.into_iter().collect())
27 }
28}
29
30#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
31pub struct ParagraphIndentation {
32 pub first_line_twips: Option<i32>,
33 pub hanging_twips: Option<i32>,
34 pub left_twips: Option<i32>,
35 pub right_twips: Option<i32>,
36 pub start_twips: Option<i32>,
37 pub end_twips: Option<i32>,
38 pub first_line_chars_hundredths: Option<i32>,
39 pub hanging_chars_hundredths: Option<i32>,
40 pub left_chars_hundredths: Option<i32>,
41 pub right_chars_hundredths: Option<i32>,
42 pub start_chars_hundredths: Option<i32>,
43 pub end_chars_hundredths: Option<i32>,
44}
45
46impl ParagraphIndentation {
47 fn is_empty(self) -> bool {
48 self == Self::default()
49 }
50
51 fn inherit(self, child: Self) -> Self {
52 let child_has_hanging =
53 child.hanging_twips.is_some() || child.hanging_chars_hundredths.is_some();
54 let child_has_first_line = !child_has_hanging
55 && (child.first_line_twips.is_some() || child.first_line_chars_hundredths.is_some());
56 let parent_first_line = if child_has_hanging {
57 (None, None)
58 } else {
59 (self.first_line_twips, self.first_line_chars_hundredths)
60 };
61 let parent_hanging = if child_has_first_line {
62 (None, None)
63 } else {
64 (self.hanging_twips, self.hanging_chars_hundredths)
65 };
66 let child_first_line = if child_has_hanging {
67 (None, None)
68 } else {
69 (child.first_line_twips, child.first_line_chars_hundredths)
70 };
71 let (first_line_twips, first_line_chars_hundredths) = inherit_character_indent(
72 parent_first_line.0,
73 parent_first_line.1,
74 child_first_line.0,
75 child_first_line.1,
76 );
77 let (hanging_twips, hanging_chars_hundredths) = inherit_character_indent(
78 parent_hanging.0,
79 parent_hanging.1,
80 child.hanging_twips,
81 child.hanging_chars_hundredths,
82 );
83 let (mut left_twips, left_chars_hundredths) = inherit_character_indent(
84 self.left_twips,
85 self.left_chars_hundredths,
86 child.left_twips,
87 child.left_chars_hundredths,
88 );
89 let (mut right_twips, right_chars_hundredths) = inherit_character_indent(
90 self.right_twips,
91 self.right_chars_hundredths,
92 child.right_twips,
93 child.right_chars_hundredths,
94 );
95 let (mut start_twips, start_chars_hundredths) = inherit_character_indent(
96 self.start_twips,
97 self.start_chars_hundredths,
98 child.start_twips,
99 child.start_chars_hundredths,
100 );
101 let (mut end_twips, end_chars_hundredths) = inherit_character_indent(
102 self.end_twips,
103 self.end_chars_hundredths,
104 child.end_twips,
105 child.end_chars_hundredths,
106 );
107 if left_chars_hundredths.is_some() || start_chars_hundredths.is_some() {
108 left_twips = None;
109 start_twips = None;
110 }
111 if right_chars_hundredths.is_some() || end_chars_hundredths.is_some() {
112 right_twips = None;
113 end_twips = None;
114 }
115 Self {
116 first_line_twips,
117 hanging_twips,
118 left_twips,
119 right_twips,
120 start_twips,
121 end_twips,
122 first_line_chars_hundredths,
123 hanging_chars_hundredths,
124 left_chars_hundredths,
125 right_chars_hundredths,
126 start_chars_hundredths,
127 end_chars_hundredths,
128 }
129 }
130
131 fn inherit_numbered_direct(self, mut direct: Self) -> Self {
132 if direct.first_line_twips == Some(0) {
133 direct.first_line_twips = None;
134 }
135 if direct.hanging_twips == Some(0) {
136 direct.hanging_twips = None;
137 }
138 self.inherit(direct)
139 }
140}
141
142fn inherit_character_indent(
143 parent_twips: Option<i32>,
144 parent_chars: Option<i32>,
145 child_twips: Option<i32>,
146 child_chars: Option<i32>,
147) -> (Option<i32>, Option<i32>) {
148 match child_chars {
149 Some(0) => (child_twips.or(parent_twips), None),
150 Some(value) => (None, Some(value)),
151 None => parent_chars.filter(|value| *value != 0).map_or_else(
152 || (child_twips.or(parent_twips), None),
153 |value| (None, Some(value)),
154 ),
155 }
156}
157
158#[derive(Clone, Debug, Eq, PartialEq)]
159pub struct ParagraphIndentationFact {
160 pub paragraph_ordinal: usize,
161 pub value: ParagraphIndentation,
162}
163
164#[derive(Clone, Copy, Debug, Eq, PartialEq)]
166pub enum ParagraphAlignmentValue {
167 Center,
168 Justify,
169 Left,
170 Right,
171}
172
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
175pub enum ParagraphAlignmentSource {
176 Direct,
178 Style,
180}
181
182#[derive(Clone, Copy, Debug, Eq, PartialEq)]
184pub struct ParagraphAlignmentFact {
185 pub value: ParagraphAlignmentValue,
186 pub source: ParagraphAlignmentSource,
187}
188
189#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub(super) enum ParagraphAlignmentSetting {
192 Supported(ParagraphAlignmentValue),
193 Unsupported,
198}
199
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
201pub struct ParagraphOutlineLevelFact {
202 pub paragraph_ordinal: usize,
203 pub outline_level: u8,
204}
205
206#[derive(Clone, Debug, Eq, PartialEq)]
207pub struct NumberingHierarchyFact {
208 pub paragraph_ordinal: usize,
209 pub parent_paragraph_ordinal: Option<usize>,
210 pub child_paragraph_ordinals: Vec<usize>,
211}
212
213#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
214pub enum SpanCoverage {
215 Complete,
216 ContinuesBefore,
217 ContinuesAfter,
218 ContinuesBeforeAndAfter,
219}
220
221#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
222pub struct StructuralSpan {
223 pub start_utf8: u32,
224 pub end_utf8: u32,
225 pub start_utf16: u32,
226 pub end_utf16: u32,
227 pub coverage: SpanCoverage,
228}
229
230#[derive(Clone, Debug, Eq, PartialEq)]
231pub struct BookmarkFact {
232 pub paragraph_ordinal: usize,
233 pub bookmark_id: u32,
234 pub name: String,
235 pub span: StructuralSpan,
236}
237
238#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
239pub enum InternalReferenceRole {
240 Source,
241 Target,
242}
243
244#[derive(Clone, Debug, Eq, PartialEq)]
245pub struct InternalReferenceFact {
246 pub paragraph_ordinal: usize,
247 pub reference_id: String,
248 pub role: InternalReferenceRole,
249 pub span: StructuralSpan,
250}
251
252#[derive(Clone, Debug, Eq, PartialEq)]
253pub struct DocumentStructureFacts {
254 pub indentation: StructuralFactSet<ParagraphIndentationFact>,
255 pub numbering_hierarchy: StructuralFactSet<NumberingHierarchyFact>,
256 pub bookmarks: StructuralFactSet<BookmarkFact>,
257 pub internal_references: StructuralFactSet<InternalReferenceFact>,
258 pub outline_levels: StructuralFactSet<ParagraphOutlineLevelFact>,
259}
260
261#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
262pub(super) struct NumberingProperties {
263 pub num_id: Option<u32>,
264 pub level: Option<u8>,
265 pub present: bool,
266}
267
268impl NumberingProperties {
269 fn inherit(self, child: Self) -> Self {
270 if !child.present {
271 return self;
272 }
273 Self {
274 num_id: child.num_id.or(self.num_id),
275 level: child.level.or(self.level),
276 present: true,
277 }
278 }
279}
280
281#[derive(Clone, Debug, Default, Eq, PartialEq)]
282pub(super) enum TableStyleSelection {
283 #[default]
284 OutsideTable,
285 Default,
286 Explicit(String),
287}
288
289#[derive(Clone, Debug, Default, Eq, PartialEq)]
290pub(super) struct ParagraphProperties {
291 pub style_id: Option<String>,
292 pub table_style: TableStyleSelection,
293 pub outline_level: Option<u8>,
294 pub indentation: ParagraphIndentation,
295 pub numbering: NumberingProperties,
296 pub alignment: Option<ParagraphAlignmentSetting>,
297}
298
299#[derive(Clone, Debug)]
300pub(super) struct RawBookmarkRange {
301 pub id: u32,
302 pub name: String,
303 pub start: ReviewPoint,
304 pub end: ReviewPoint,
305}
306
307#[derive(Clone, Debug)]
308pub(super) struct RawInternalReference {
309 pub reference_id: String,
310 pub source: ReviewPoint,
311}
312
313#[derive(Clone, Debug)]
314pub(super) struct StyleDefinition {
315 pub based_on: Option<String>,
316 pub properties: ParagraphProperties,
317 pub text_properties: TextProperties,
318 pub kind: StyleKind,
319 pub table_formatting: TableStyleFormatting,
320}
321
322#[derive(Clone, Copy, Debug, Eq, PartialEq)]
323pub(super) enum StyleKind {
324 Character,
325 Paragraph,
326 Table,
327}
328
329#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
330pub(super) enum TableStyleFormatting {
331 #[default]
332 Base,
333 Conditional,
334}
335
336pub(super) struct TextStyleInput<'a> {
337 pub paragraph: &'a ParagraphProperties,
338 pub character_style_id: Option<&'a str>,
339 pub direct: TextProperties,
340}
341
342#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
343pub(super) struct TextProperties {
344 pub bold: Option<bool>,
345 pub complex_script_bold: Option<bool>,
346 pub force_complex_script: Option<bool>,
347 pub right_to_left: Option<bool>,
348 pub highlighted: Option<bool>,
349 pub superscript: Option<bool>,
350}
351
352impl TextProperties {
353 const fn inherit(self, child: Self) -> Self {
354 Self {
355 bold: if child.bold.is_some() {
356 child.bold
357 } else {
358 self.bold
359 },
360 complex_script_bold: if child.complex_script_bold.is_some() {
361 child.complex_script_bold
362 } else {
363 self.complex_script_bold
364 },
365 force_complex_script: if child.force_complex_script.is_some() {
366 child.force_complex_script
367 } else {
368 self.force_complex_script
369 },
370 right_to_left: if child.right_to_left.is_some() {
371 child.right_to_left
372 } else {
373 self.right_to_left
374 },
375 highlighted: if child.highlighted.is_some() {
376 child.highlighted
377 } else {
378 self.highlighted
379 },
380 superscript: if child.superscript.is_some() {
381 child.superscript
382 } else {
383 self.superscript
384 },
385 }
386 }
387
388 fn inherit_style(self, child: Self) -> Self {
389 let mut inherited = self.inherit(child);
390 inherited.bold = inherit_style_toggle(self.bold, child.bold);
391 inherited.complex_script_bold =
392 inherit_style_toggle(self.complex_script_bold, child.complex_script_bold);
393 inherited
394 }
395}
396
397fn inherit_style_toggle(inherited: Option<bool>, child: Option<bool>) -> Option<bool> {
398 match child {
399 Some(true) => Some(!inherited.unwrap_or(false)),
400 Some(false) | None => inherited,
401 }
402}
403
404#[derive(Clone, Debug, Default)]
405pub(super) struct StyleSheet {
406 pub default_style_id: Option<String>,
407 pub default_table_style_id: Option<String>,
408 pub document_defaults: ParagraphProperties,
409 pub document_text_defaults: TextProperties,
410 pub styles: HashMap<String, StyleDefinition>,
411 resolved_character_text: HashMap<String, TextProperties>,
412 resolved_paragraph_styles: HashMap<String, ResolvedParagraphStyle>,
413 resolved_table_styles: HashMap<String, ResolvedParagraphStyle>,
414}
415
416#[derive(Clone, Copy, Debug, Default)]
417struct ResolvedParagraphStyle {
418 text: TextProperties,
419 properties: ResolvedParagraphStyleProperties,
420}
421
422impl ResolvedParagraphStyle {
423 fn inherit(self, child: &StyleDefinition) -> Self {
424 Self {
425 text: self.text.inherit_style(child.text_properties),
426 properties: self.properties.inherit(&child.properties),
427 }
428 }
429}
430
431#[derive(Clone, Copy, Debug, Default)]
432struct ResolvedParagraphStyleProperties {
433 indentation: ParagraphIndentation,
434 numbering: NumberingProperties,
435 outline_level: Option<u8>,
436 alignment: Option<ParagraphAlignmentSetting>,
437}
438
439impl ResolvedParagraphStyleProperties {
440 fn inherit(self, child: &ParagraphProperties) -> Self {
441 Self {
442 indentation: self.indentation.inherit(child.indentation),
443 numbering: self.numbering.inherit(child.numbering),
444 outline_level: child.outline_level.or(self.outline_level),
445 alignment: child.alignment.or(self.alignment),
446 }
447 }
448}
449
450#[derive(Clone, Debug)]
451struct ResolvedParagraphProperties {
452 indentation: Result<ParagraphIndentation, StructuralFactUnknownReason>,
453 numbering: NumberingProperties,
454 outline_level: Option<u8>,
455}
456
457impl StyleSheet {
458 pub(super) fn prepare_styles(&mut self) {
459 (self.resolved_character_text, _) = self.resolve_character_text_styles();
460 (self.resolved_paragraph_styles, _) = self.resolve_block_styles(StyleKind::Paragraph);
461 (self.resolved_table_styles, _) = self.resolve_block_styles(StyleKind::Table);
462 }
463
464 pub(super) fn resolve_text(
465 &self,
466 TextStyleInput {
467 paragraph: properties,
468 character_style_id,
469 direct,
470 }: TextStyleInput<'_>,
471 ) -> Result<TextProperties, ()> {
472 let table = self
473 .table_style(&properties.table_style)?
474 .map(|style| style.text)
475 .unwrap_or_default();
476 let paragraph = table.inherit_style(
477 self.paragraph_style(properties.style_id.as_deref())?
478 .map(|style| style.text)
479 .unwrap_or_default(),
480 );
481 let character = character_style_id
482 .filter(|style_id| {
483 self.styles
484 .get(*style_id)
485 .is_some_and(|style| style.kind == StyleKind::Character)
486 })
487 .map(|style_id| {
488 self.resolved_character_text
489 .get(style_id)
490 .copied()
491 .ok_or(())
492 })
493 .transpose()?
494 .unwrap_or_default();
495 let mut effective = self
496 .document_text_defaults
497 .inherit(paragraph)
498 .inherit(character)
499 .inherit(direct);
500 effective.bold = Some(resolve_word_toggle(
501 self.document_text_defaults.bold,
502 paragraph.bold,
503 character.bold,
504 direct.bold,
505 ));
506 effective.complex_script_bold = Some(resolve_word_toggle(
507 self.document_text_defaults.complex_script_bold,
508 paragraph.complex_script_bold,
509 character.complex_script_bold,
510 direct.complex_script_bold,
511 ));
512 Ok(effective)
513 }
514
515 fn resolve_character_text_styles(&self) -> (HashMap<String, TextProperties>, usize) {
516 let mut resolved = HashMap::new();
517 let mut unresolved = HashSet::new();
518 let mut resolution_steps = 0usize;
519 for root_id in self.styles.iter().filter_map(|(style_id, style)| {
520 (style.kind == StyleKind::Character).then_some(style_id)
521 }) {
522 if resolved.contains_key(root_id) || unresolved.contains(root_id) {
523 continue;
524 }
525 let mut chain = Vec::new();
526 let mut seen = HashSet::new();
527 let mut current = Some(root_id.as_str());
528 let mut inherited = TextProperties::default();
529 let mut valid = true;
530 while let Some(style_id) = current {
531 resolution_steps = resolution_steps.saturating_add(1);
532 if let Some(properties) = resolved.get(style_id).copied() {
533 inherited = properties;
534 break;
535 }
536 if unresolved.contains(style_id) {
537 valid = false;
538 break;
539 }
540 if !seen.insert(style_id) {
541 valid = false;
542 break;
543 }
544 let Some(style) = self.styles.get(style_id) else {
545 valid = !chain.is_empty();
547 break;
548 };
549 if style.kind != StyleKind::Character {
550 valid = !chain.is_empty();
553 break;
554 }
555 chain.push(style_id);
556 current = style.based_on.as_deref();
557 }
558 if !valid {
559 unresolved.extend(chain.into_iter().map(str::to_owned));
560 continue;
561 }
562 for style_id in chain.into_iter().rev() {
563 if let Some(style) = self.styles.get(style_id) {
565 inherited = inherited.inherit_style(style.text_properties);
566 resolved.insert(style_id.to_owned(), inherited);
567 }
568 }
569 }
570 debug_assert!(
571 resolution_steps <= self.styles.len().saturating_mul(2).saturating_add(1),
572 "memoized style resolution exceeded its linear work budget"
573 );
574 (resolved, resolution_steps)
575 }
576
577 fn resolve_block_styles(
578 &self,
579 kind: StyleKind,
580 ) -> (HashMap<String, ResolvedParagraphStyle>, usize) {
581 let mut resolved = HashMap::new();
582 let mut unresolved = HashSet::new();
583 let mut resolution_steps = 0usize;
584 for root_id in self
585 .styles
586 .iter()
587 .filter_map(|(style_id, style)| (style.kind == kind).then_some(style_id))
588 {
589 if resolved.contains_key(root_id) || unresolved.contains(root_id) {
590 continue;
591 }
592 let mut chain = Vec::new();
593 let mut seen = HashSet::new();
594 let mut current = Some(root_id.as_str());
595 let mut inherited = ResolvedParagraphStyle::default();
596 let mut valid = true;
597 while let Some(style_id) = current {
598 resolution_steps = resolution_steps.saturating_add(1);
599 if let Some(properties) = resolved.get(style_id).copied() {
600 inherited = properties;
601 break;
602 }
603 if unresolved.contains(style_id) || !seen.insert(style_id) {
604 valid = false;
605 break;
606 }
607 let Some(style) = self.styles.get(style_id) else {
608 valid = !chain.is_empty();
609 break;
610 };
611 if style.kind != kind {
612 valid = !chain.is_empty();
613 break;
614 }
615 chain.push(style_id);
616 if kind == StyleKind::Table
617 && style.table_formatting == TableStyleFormatting::Conditional
618 {
619 valid = false;
620 break;
621 }
622 current = style.based_on.as_deref();
623 }
624 if !valid {
625 unresolved.extend(chain.into_iter().map(str::to_owned));
626 continue;
627 }
628 for style_id in chain.into_iter().rev() {
629 if let Some(style) = self.styles.get(style_id) {
630 inherited = inherited.inherit(style);
631 resolved.insert(style_id.to_owned(), inherited);
632 }
633 }
634 }
635 debug_assert!(
636 resolution_steps <= self.styles.len().saturating_mul(2).saturating_add(1),
637 "memoized paragraph style resolution exceeded its linear work budget"
638 );
639 (resolved, resolution_steps)
640 }
641
642 fn table_style(
643 &self,
644 selection: &TableStyleSelection,
645 ) -> Result<Option<&ResolvedParagraphStyle>, ()> {
646 let style_id = match selection {
647 TableStyleSelection::OutsideTable => return Ok(None),
648 TableStyleSelection::Default => self.default_table_style_id.as_deref(),
649 TableStyleSelection::Explicit(id) => Some(id.as_str()),
650 };
651 let Some(style_id) = style_id.filter(|id| {
652 self.styles
653 .get(*id)
654 .is_some_and(|style| style.kind == StyleKind::Table)
655 }) else {
656 return Ok(None);
657 };
658 self.resolved_table_styles.get(style_id).map(Some).ok_or(())
659 }
660
661 fn paragraph_style(
662 &self,
663 explicit_style_id: Option<&str>,
664 ) -> Result<Option<&ResolvedParagraphStyle>, ()> {
665 let Some(style_id) = explicit_style_id
666 .filter(|id| {
667 self.styles
668 .get(*id)
669 .is_some_and(|style| style.kind == StyleKind::Paragraph)
670 })
671 .or(self.default_style_id.as_deref())
672 else {
673 return Ok(None);
674 };
675 self.resolved_paragraph_styles
676 .get(style_id)
677 .map(Some)
678 .ok_or(())
679 }
680
681 fn style_alignment(&self, direct: &ParagraphProperties) -> Option<ParagraphAlignmentSetting> {
684 let table = self.table_style(&direct.table_style).ok()?;
685 self.paragraph_style(direct.style_id.as_deref())
686 .ok()?
687 .and_then(|style| style.properties.alignment)
688 .or_else(|| table.and_then(|style| style.properties.alignment))
689 .or(self.document_defaults.alignment)
690 }
691
692 fn resolve(
693 &self,
694 direct: &ParagraphProperties,
695 numbering_catalog: Result<&NumberingCatalog, StructuralFactUnknownReason>,
696 ) -> Result<ResolvedParagraphProperties, StructuralFactUnknownReason> {
697 let table = self
698 .table_style(&direct.table_style)
699 .map_err(|()| StructuralFactUnknownReason::UnsupportedStyles)?
700 .map(|style| style.properties)
701 .unwrap_or_default();
702 let style = self
703 .paragraph_style(direct.style_id.as_deref())
704 .map_err(|()| StructuralFactUnknownReason::UnsupportedStyles)?
705 .map(|style| style.properties)
706 .unwrap_or_default();
707 let style = ResolvedParagraphStyleProperties {
708 indentation: table.indentation.inherit(style.indentation),
709 numbering: table.numbering.inherit(style.numbering),
710 outline_level: style.outline_level.or(table.outline_level),
711 alignment: style.alignment.or(table.alignment),
712 };
713 let mut numbering = self.document_defaults.numbering.inherit(style.numbering);
714 let mut outline_level = style.outline_level.or(self.document_defaults.outline_level);
715 let inherited_numbering = numbering;
716 numbering = numbering.inherit(direct.numbering);
717 outline_level = direct.outline_level.or(outline_level);
718
719 let numbered = numbering.present && numbering.num_id != Some(0);
720 let numbering_is_direct = direct.numbering.present;
721 let numbering_removed = numbering_is_direct
722 && direct.numbering.num_id == Some(0)
723 && inherited_numbering.present
724 && inherited_numbering.num_id != Some(0);
725 let indentation = if numbered {
726 numbering
727 .num_id
728 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering)
729 .and_then(|num_id| {
730 numbering_catalog.and_then(|catalog| {
731 catalog.indentation(num_id, numbering.level.unwrap_or(0))
732 })
733 })
734 } else {
735 Ok(ParagraphIndentation::default())
736 }
737 .map(|level_indentation| {
738 let mut indentation = if numbering_removed {
739 ParagraphIndentation::default()
740 } else {
741 self.document_defaults.indentation
742 };
743 if !numbering_is_direct && !numbering_removed {
744 indentation = indentation.inherit(level_indentation);
745 }
746 if !numbering_removed {
747 indentation = indentation.inherit(style.indentation);
748 }
749 if numbering_is_direct {
750 indentation = indentation.inherit(level_indentation);
751 }
752 if numbered {
753 indentation.inherit_numbered_direct(direct.indentation)
754 } else {
755 indentation.inherit(direct.indentation)
756 }
757 });
758 Ok(ResolvedParagraphProperties {
759 indentation,
760 numbering,
761 outline_level,
762 })
763 }
764}
765
766pub(super) fn resolve_paragraph_alignment(
777 styles: Result<&StyleSheet, StructuralFactUnknownReason>,
778 direct: &ParagraphProperties,
779) -> Option<ParagraphAlignmentFact> {
780 if let Some(setting) = direct.alignment {
781 return alignment_fact(setting, ParagraphAlignmentSource::Direct);
782 }
783 styles
784 .ok()?
785 .style_alignment(direct)
786 .and_then(|setting| alignment_fact(setting, ParagraphAlignmentSource::Style))
787}
788
789const fn alignment_fact(
790 setting: ParagraphAlignmentSetting,
791 source: ParagraphAlignmentSource,
792) -> Option<ParagraphAlignmentFact> {
793 match setting {
794 ParagraphAlignmentSetting::Supported(value) => {
795 Some(ParagraphAlignmentFact { value, source })
796 }
797 ParagraphAlignmentSetting::Unsupported => None,
798 }
799}
800
801fn resolve_word_toggle(
802 document_default: Option<bool>,
803 paragraph: Option<bool>,
804 character: Option<bool>,
805 direct: Option<bool>,
806) -> bool {
807 if let Some(value) = direct {
808 return value;
809 }
810 document_default.unwrap_or(false) ^ paragraph.unwrap_or(false) ^ character.unwrap_or(false)
811}
812
813#[derive(Clone, Copy)]
814pub(super) struct RawStructureInput<'a> {
815 pub paragraph_texts: &'a [&'a str],
816 pub properties: &'a [&'a ParagraphProperties],
817 pub bookmarks: Result<&'a [RawBookmarkRange], StructuralFactUnknownReason>,
818 pub references: Result<&'a [RawInternalReference], StructuralFactUnknownReason>,
819}
820
821struct StructuralFactBudget {
822 remaining: usize,
823}
824
825impl StructuralFactBudget {
826 const fn new(maximum_facts: usize) -> Self {
827 Self {
828 remaining: maximum_facts,
829 }
830 }
831
832 fn consume(&mut self, facts: usize) -> Result<(), ProjectionError> {
833 self.remaining = self
834 .remaining
835 .checked_sub(facts)
836 .ok_or(ProjectionError::TooManyStructuralFacts)?;
837 Ok(())
838 }
839}
840
841pub(super) fn materialize_structure(
842 input: RawStructureInput<'_>,
843 styles: Result<&StyleSheet, StructuralFactUnknownReason>,
844 numbering: Result<&NumberingCatalog, StructuralFactUnknownReason>,
845 maximum_facts: usize,
846) -> Result<DocumentStructureFacts, ProjectionError> {
847 let mut fact_budget = StructuralFactBudget::new(maximum_facts);
848 let resolved = styles.and_then(|styles| {
849 input
850 .properties
851 .iter()
852 .map(|properties| styles.resolve(properties, numbering))
853 .collect::<Result<Vec<_>, _>>()
854 });
855
856 let (indentation, numbering_hierarchy, outline_levels) = match resolved {
857 Ok(properties) => {
858 let indentation = properties
859 .iter()
860 .map(|properties| properties.indentation)
861 .collect::<Result<Vec<_>, _>>()
862 .map_or_else(StructuralFactSet::Unknown, |indentations| {
863 StructuralFactSet::Known(
864 indentations
865 .into_iter()
866 .enumerate()
867 .filter(|(_, indentation)| !indentation.is_empty())
868 .map(|(paragraph_ordinal, value)| ParagraphIndentationFact {
869 paragraph_ordinal,
870 value,
871 })
872 .collect(),
873 )
874 });
875 (
876 indentation,
877 materialize_numbering(&properties),
878 StructuralFactSet::Known(
879 properties
880 .iter()
881 .enumerate()
882 .filter_map(|(paragraph_ordinal, properties)| {
883 properties.outline_level.map(|outline_level| {
884 ParagraphOutlineLevelFact {
885 paragraph_ordinal,
886 outline_level,
887 }
888 })
889 })
890 .collect(),
891 ),
892 )
893 }
894 Err(reason) => (
895 StructuralFactSet::Unknown(reason),
896 StructuralFactSet::Unknown(reason),
897 StructuralFactSet::Unknown(reason),
898 ),
899 };
900 if let StructuralFactSet::Known(facts) = &indentation {
901 fact_budget.consume(facts.len())?;
902 }
903 if let StructuralFactSet::Known(facts) = &numbering_hierarchy {
904 fact_budget.consume(facts.len())?;
905 }
906 if let StructuralFactSet::Known(facts) = &outline_levels {
907 fact_budget.consume(facts.len())?;
908 }
909
910 let bookmarks = match input.bookmarks {
911 Ok(ranges) => StructuralFactSet::Known(materialize_bookmarks(
912 input.paragraph_texts,
913 ranges,
914 &mut fact_budget,
915 )?),
916 Err(reason) => StructuralFactSet::Unknown(reason),
917 };
918 let internal_references = match (input.references, &bookmarks) {
919 (Ok([]), _) => StructuralFactSet::Known(Vec::new()),
920 (Ok(references), StructuralFactSet::Known(bookmark_facts)) => StructuralFactSet::Known(
921 materialize_references(references, bookmark_facts, &mut fact_budget)?,
922 ),
923 (Err(reason), _) => StructuralFactSet::Unknown(reason),
924 (_, StructuralFactSet::Unknown(_)) => {
925 StructuralFactSet::Unknown(StructuralFactUnknownReason::IncompleteBookmarkRanges)
926 }
927 };
928
929 Ok(DocumentStructureFacts {
930 indentation,
931 numbering_hierarchy,
932 bookmarks,
933 internal_references,
934 outline_levels,
935 })
936}
937
938fn materialize_numbering(
939 properties: &[ResolvedParagraphProperties],
940) -> StructuralFactSet<NumberingHierarchyFact> {
941 let mut stacks: HashMap<u32, [Option<usize>; 9]> = HashMap::new();
942 let mut parents = vec![None; properties.len()];
943 let mut children = vec![Vec::new(); properties.len()];
944
945 for (ordinal, paragraph_properties) in properties.iter().enumerate() {
946 if !paragraph_properties.numbering.present
947 || paragraph_properties.numbering.num_id == Some(0)
948 {
949 continue;
950 }
951 let Some(num_id) = paragraph_properties.numbering.num_id else {
952 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
953 };
954 let level = usize::from(paragraph_properties.numbering.level.unwrap_or(0));
955 if level >= 9 {
956 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
957 }
958 let stack = stacks.entry(num_id).or_insert([None; 9]);
959 if level > 0
960 && let Some(parent) = stack
961 .get(..level)
962 .and_then(|ancestors| ancestors.iter().rev().flatten().next().copied())
963 {
964 let Some(parent_slot) = parents.get_mut(ordinal) else {
965 return StructuralFactSet::Unknown(
966 StructuralFactUnknownReason::UnsupportedNumbering,
967 );
968 };
969 *parent_slot = Some(parent);
970 let Some(parent_children) = children.get_mut(parent) else {
971 return StructuralFactSet::Unknown(
972 StructuralFactUnknownReason::UnsupportedNumbering,
973 );
974 };
975 parent_children.push(ordinal);
976 }
977 let Some(level_slot) = stack.get_mut(level) else {
978 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
979 };
980 *level_slot = Some(ordinal);
981 let descendant_start = level.saturating_add(1);
982 let Some(descendant_slots) = stack.get_mut(descendant_start..) else {
983 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
984 };
985 for slot in descendant_slots {
986 *slot = None;
987 }
988 }
989
990 StructuralFactSet::Known(
991 properties
992 .iter()
993 .enumerate()
994 .filter_map(|(ordinal, _)| {
995 let parent = parents.get(ordinal).copied().flatten();
996 let paragraph_children = children.get(ordinal)?;
997 (parent.is_some() || !paragraph_children.is_empty()).then(|| {
998 NumberingHierarchyFact {
999 paragraph_ordinal: ordinal,
1000 parent_paragraph_ordinal: parent,
1001 child_paragraph_ordinals: paragraph_children.clone(),
1002 }
1003 })
1004 })
1005 .collect(),
1006 )
1007}
1008
1009fn materialize_bookmarks(
1010 texts: &[&str],
1011 ranges: &[RawBookmarkRange],
1012 fact_budget: &mut StructuralFactBudget,
1013) -> Result<Vec<BookmarkFact>, ProjectionError> {
1014 let mut facts = Vec::new();
1015 for range in ranges {
1016 segment_range(texts, &range.start, &range.end, |paragraph, span| {
1017 fact_budget.consume(1)?;
1018 facts.push(BookmarkFact {
1019 paragraph_ordinal: paragraph,
1020 bookmark_id: range.id,
1021 name: range.name.clone(),
1022 span,
1023 });
1024 Ok(())
1025 })?;
1026 }
1027 facts.sort_by(|left, right| {
1028 (
1029 left.paragraph_ordinal,
1030 left.span.start_utf8,
1031 left.span.end_utf8,
1032 left.bookmark_id,
1033 left.name.as_str(),
1034 left.span.coverage,
1035 )
1036 .cmp(&(
1037 right.paragraph_ordinal,
1038 right.span.start_utf8,
1039 right.span.end_utf8,
1040 right.bookmark_id,
1041 right.name.as_str(),
1042 right.span.coverage,
1043 ))
1044 });
1045 Ok(facts)
1046}
1047
1048fn materialize_references(
1049 references: &[RawInternalReference],
1050 bookmarks: &[BookmarkFact],
1051 fact_budget: &mut StructuralFactBudget,
1052) -> Result<Vec<InternalReferenceFact>, ProjectionError> {
1053 let mut facts = Vec::new();
1054 let mut targets_by_name: HashMap<&str, Vec<&BookmarkFact>> = HashMap::new();
1055 for bookmark in bookmarks {
1056 targets_by_name
1057 .entry(&bookmark.name)
1058 .or_default()
1059 .push(bookmark);
1060 }
1061 let mut emitted_target_names = HashSet::new();
1062 for reference in references {
1063 fact_budget.consume(1)?;
1064 facts.push(InternalReferenceFact {
1065 paragraph_ordinal: reference.source.paragraph_ordinal,
1066 reference_id: reference.reference_id.clone(),
1067 role: InternalReferenceRole::Source,
1068 span: StructuralSpan {
1069 start_utf8: reference.source.utf8,
1070 end_utf8: reference.source.utf8,
1071 start_utf16: reference.source.utf16,
1072 end_utf16: reference.source.utf16,
1073 coverage: SpanCoverage::Complete,
1074 },
1075 });
1076 if emitted_target_names.insert(reference.reference_id.as_str())
1077 && let Some(targets) = targets_by_name.get(reference.reference_id.as_str())
1078 {
1079 for bookmark in targets {
1080 fact_budget.consume(1)?;
1081 facts.push(InternalReferenceFact {
1082 paragraph_ordinal: bookmark.paragraph_ordinal,
1083 reference_id: reference.reference_id.clone(),
1084 role: InternalReferenceRole::Target,
1085 span: bookmark.span,
1086 });
1087 }
1088 }
1089 }
1090 facts.sort_by(|left, right| {
1091 (
1092 left.paragraph_ordinal,
1093 left.span.start_utf8,
1094 left.span.end_utf8,
1095 left.reference_id.as_str(),
1096 left.role,
1097 left.span.coverage,
1098 )
1099 .cmp(&(
1100 right.paragraph_ordinal,
1101 right.span.start_utf8,
1102 right.span.end_utf8,
1103 right.reference_id.as_str(),
1104 right.role,
1105 right.span.coverage,
1106 ))
1107 });
1108 Ok(facts)
1109}
1110
1111fn segment_range(
1112 texts: &[&str],
1113 start: &ReviewPoint,
1114 end: &ReviewPoint,
1115 mut visit: impl FnMut(usize, StructuralSpan) -> Result<(), ProjectionError>,
1116) -> Result<(), ProjectionError> {
1117 if start.paragraph_ordinal > end.paragraph_ordinal || end.paragraph_ordinal >= texts.len() {
1118 return Err(ProjectionError::InvalidDocumentXml);
1119 }
1120 if start.paragraph_ordinal == end.paragraph_ordinal
1121 && (start.utf8 > end.utf8 || start.utf16 > end.utf16)
1122 {
1123 return Err(ProjectionError::InvalidDocumentXml);
1124 }
1125 for (paragraph, text) in texts
1126 .iter()
1127 .enumerate()
1128 .take(end.paragraph_ordinal.saturating_add(1))
1129 .skip(start.paragraph_ordinal)
1130 {
1131 let text_utf8 =
1132 u32::try_from(text.len()).map_err(|_| ProjectionError::InvalidDocumentXml)?;
1133 let text_utf16 = u32::try_from(text.encode_utf16().count())
1134 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
1135 let (start_utf8, start_utf16) = if paragraph == start.paragraph_ordinal {
1136 (start.utf8, start.utf16)
1137 } else {
1138 (0, 0)
1139 };
1140 let (end_utf8, end_utf16) = if paragraph == end.paragraph_ordinal {
1141 (end.utf8, end.utf16)
1142 } else {
1143 (text_utf8, text_utf16)
1144 };
1145 if start_utf8 > text_utf8
1146 || end_utf8 > text_utf8
1147 || start_utf16 > text_utf16
1148 || end_utf16 > text_utf16
1149 {
1150 return Err(ProjectionError::InvalidDocumentXml);
1151 }
1152 let continues_before = paragraph > start.paragraph_ordinal;
1153 let continues_after = paragraph < end.paragraph_ordinal;
1154 let coverage = match (continues_before, continues_after) {
1155 (false, false) => SpanCoverage::Complete,
1156 (true, false) => SpanCoverage::ContinuesBefore,
1157 (false, true) => SpanCoverage::ContinuesAfter,
1158 (true, true) => SpanCoverage::ContinuesBeforeAndAfter,
1159 };
1160 visit(
1161 paragraph,
1162 StructuralSpan {
1163 start_utf8,
1164 end_utf8,
1165 start_utf16,
1166 end_utf16,
1167 coverage,
1168 },
1169 )?;
1170 }
1171 Ok(())
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176 use super::{
1177 ParagraphProperties, StyleDefinition, StyleKind, StyleSheet, TableStyleFormatting,
1178 TextProperties, inherit_style_toggle,
1179 };
1180
1181 const WORD_STYLE_LIMIT: usize = 4_079;
1182
1183 fn style(based_on: Option<String>, kind: StyleKind) -> StyleDefinition {
1184 StyleDefinition {
1185 based_on,
1186 properties: ParagraphProperties::default(),
1187 text_properties: TextProperties {
1188 bold: Some(true),
1189 ..TextProperties::default()
1190 },
1191 kind,
1192 table_formatting: TableStyleFormatting::Base,
1193 }
1194 }
1195
1196 #[test]
1197 fn style_toggle_truth_table_preserves_false_and_toggles_true() {
1198 let cases = [
1199 (&[][..], None),
1200 (&[false][..], None),
1201 (&[true][..], Some(true)),
1202 (&[true, false][..], Some(true)),
1203 (&[true, true][..], Some(false)),
1204 (&[true, false, true][..], Some(false)),
1205 (&[true, true, true][..], Some(true)),
1206 ];
1207
1208 for (levels, expected) in cases {
1209 let actual = levels.iter().fold(None, |inherited, &level| {
1210 inherit_style_toggle(inherited, Some(level))
1211 });
1212 assert_eq!(actual, expected, "style levels {levels:?}");
1213 }
1214 }
1215
1216 #[test]
1217 fn style_resolution_stays_within_its_linear_work_budget() -> Result<(), &'static str> {
1218 let ids = (0..WORD_STYLE_LIMIT)
1219 .map(|index| format!("style-{index}"))
1220 .collect::<Vec<_>>();
1221
1222 let mut paragraph_chain = StyleSheet::default();
1223 let mut paragraph_parent = None;
1224 for id in &ids {
1225 paragraph_chain.styles.insert(
1226 id.clone(),
1227 style(paragraph_parent.clone(), StyleKind::Paragraph),
1228 );
1229 paragraph_parent = Some(id.clone());
1230 }
1231 let (resolved_paragraphs, paragraph_steps) =
1232 paragraph_chain.resolve_block_styles(StyleKind::Paragraph);
1233 assert_eq!(resolved_paragraphs.len(), WORD_STYLE_LIMIT);
1234 assert!(paragraph_steps <= WORD_STYLE_LIMIT.saturating_mul(2).saturating_add(1));
1235
1236 let mut character_chain = StyleSheet::default();
1237 let mut character_parent = None;
1238 for id in &ids {
1239 character_chain.styles.insert(
1240 id.clone(),
1241 style(character_parent.clone(), StyleKind::Character),
1242 );
1243 character_parent = Some(id.clone());
1244 }
1245 let (resolved_characters, character_steps) =
1246 character_chain.resolve_character_text_styles();
1247 assert_eq!(resolved_characters.len(), WORD_STYLE_LIMIT);
1248 assert!(character_steps <= WORD_STYLE_LIMIT.saturating_mul(2).saturating_add(1));
1249
1250 let mut cycle = StyleSheet::default();
1251 let first = ids.first().ok_or("the fixture must be non-empty")?;
1252 for (index, id) in ids.iter().enumerate() {
1253 let successor = ids.get(index.saturating_add(1)).unwrap_or(first);
1254 cycle.styles.insert(
1255 id.clone(),
1256 style(Some(successor.clone()), StyleKind::Paragraph),
1257 );
1258 }
1259 let (cycle_resolved, cycle_steps) = cycle.resolve_block_styles(StyleKind::Paragraph);
1260 assert!(cycle_resolved.is_empty());
1261 assert!(cycle_steps <= WORD_STYLE_LIMIT.saturating_add(1));
1262 Ok(())
1263 }
1264}