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