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) {
928 (Ok([]), _) => StructuralFactSet::Known(Vec::new()),
929 (Ok(references), StructuralFactSet::Known(bookmark_facts)) => StructuralFactSet::Known(
930 materialize_references(references, bookmark_facts, &mut fact_budget)?,
931 ),
932 (Err(reason), _) => StructuralFactSet::Unknown(reason),
933 (_, StructuralFactSet::Unknown(_)) => {
934 StructuralFactSet::Unknown(StructuralFactUnknownReason::IncompleteBookmarkRanges)
935 }
936 };
937
938 Ok(DocumentStructureFacts {
939 indentation,
940 numbering_hierarchy,
941 bookmarks,
942 internal_references,
943 outline_levels,
944 })
945}
946
947fn materialize_numbering(
948 properties: &[ResolvedParagraphProperties],
949) -> StructuralFactSet<NumberingHierarchyFact> {
950 let mut stacks: HashMap<u32, [Option<usize>; 9]> = HashMap::new();
951 let mut parents = vec![None; properties.len()];
952 let mut children = vec![Vec::new(); properties.len()];
953
954 for (ordinal, paragraph_properties) in properties.iter().enumerate() {
955 if !paragraph_properties.numbering.present
956 || paragraph_properties.numbering.num_id == Some(0)
957 {
958 continue;
959 }
960 let Some(num_id) = paragraph_properties.numbering.num_id else {
961 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
962 };
963 let level = usize::from(paragraph_properties.numbering.level.unwrap_or(0));
964 if level >= 9 {
965 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
966 }
967 let stack = stacks.entry(num_id).or_insert([None; 9]);
968 if level > 0
969 && let Some(parent) = stack
970 .get(..level)
971 .and_then(|ancestors| ancestors.iter().rev().flatten().next().copied())
972 {
973 let Some(parent_slot) = parents.get_mut(ordinal) else {
974 return StructuralFactSet::Unknown(
975 StructuralFactUnknownReason::UnsupportedNumbering,
976 );
977 };
978 *parent_slot = Some(parent);
979 let Some(parent_children) = children.get_mut(parent) else {
980 return StructuralFactSet::Unknown(
981 StructuralFactUnknownReason::UnsupportedNumbering,
982 );
983 };
984 parent_children.push(ordinal);
985 }
986 let Some(level_slot) = stack.get_mut(level) else {
987 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
988 };
989 *level_slot = Some(ordinal);
990 let descendant_start = level.saturating_add(1);
991 let Some(descendant_slots) = stack.get_mut(descendant_start..) else {
992 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
993 };
994 for slot in descendant_slots {
995 *slot = None;
996 }
997 }
998
999 StructuralFactSet::Known(
1000 properties
1001 .iter()
1002 .enumerate()
1003 .filter_map(|(ordinal, _)| {
1004 let parent = parents.get(ordinal).copied().flatten();
1005 let paragraph_children = children.get(ordinal)?;
1006 (parent.is_some() || !paragraph_children.is_empty()).then(|| {
1007 NumberingHierarchyFact {
1008 paragraph_ordinal: ordinal,
1009 parent_paragraph_ordinal: parent,
1010 child_paragraph_ordinals: paragraph_children.clone(),
1011 }
1012 })
1013 })
1014 .collect(),
1015 )
1016}
1017
1018fn materialize_bookmarks(
1019 texts: &[&str],
1020 ranges: &[RawBookmarkRange],
1021 fact_budget: &mut StructuralFactBudget,
1022) -> Result<Vec<BookmarkFact>, ProjectionError> {
1023 let mut facts = Vec::new();
1024 for range in ranges {
1025 segment_range(texts, &range.start, &range.end, |paragraph, span| {
1026 fact_budget.consume(1)?;
1027 facts.push(BookmarkFact {
1028 paragraph_ordinal: paragraph,
1029 bookmark_id: range.id,
1030 name: range.name.clone(),
1031 span,
1032 });
1033 Ok(())
1034 })?;
1035 }
1036 facts.sort_by(|left, right| {
1037 (
1038 left.paragraph_ordinal,
1039 left.span.start_utf8,
1040 left.span.end_utf8,
1041 left.bookmark_id,
1042 left.name.as_str(),
1043 left.span.coverage,
1044 )
1045 .cmp(&(
1046 right.paragraph_ordinal,
1047 right.span.start_utf8,
1048 right.span.end_utf8,
1049 right.bookmark_id,
1050 right.name.as_str(),
1051 right.span.coverage,
1052 ))
1053 });
1054 Ok(facts)
1055}
1056
1057fn materialize_references(
1058 references: &[RawInternalReference],
1059 bookmarks: &[BookmarkFact],
1060 fact_budget: &mut StructuralFactBudget,
1061) -> Result<Vec<InternalReferenceFact>, ProjectionError> {
1062 let mut facts = Vec::new();
1063 let mut targets_by_name: HashMap<&str, Vec<&BookmarkFact>> = HashMap::new();
1064 for bookmark in bookmarks {
1065 targets_by_name
1066 .entry(&bookmark.name)
1067 .or_default()
1068 .push(bookmark);
1069 }
1070 let mut emitted_target_names = HashSet::new();
1071 for reference in references {
1072 fact_budget.consume(1)?;
1073 facts.push(InternalReferenceFact {
1074 paragraph_ordinal: reference.source.paragraph_ordinal,
1075 reference_id: reference.reference_id.clone(),
1076 role: InternalReferenceRole::Source,
1077 span: StructuralSpan {
1078 start_utf8: reference.source.utf8,
1079 end_utf8: reference.source.utf8,
1080 start_utf16: reference.source.utf16,
1081 end_utf16: reference.source.utf16,
1082 coverage: SpanCoverage::Complete,
1083 },
1084 });
1085 if emitted_target_names.insert(reference.reference_id.as_str())
1086 && let Some(targets) = targets_by_name.get(reference.reference_id.as_str())
1087 {
1088 for bookmark in targets {
1089 fact_budget.consume(1)?;
1090 facts.push(InternalReferenceFact {
1091 paragraph_ordinal: bookmark.paragraph_ordinal,
1092 reference_id: reference.reference_id.clone(),
1093 role: InternalReferenceRole::Target,
1094 span: bookmark.span,
1095 });
1096 }
1097 }
1098 }
1099 facts.sort_by(|left, right| {
1100 (
1101 left.paragraph_ordinal,
1102 left.span.start_utf8,
1103 left.span.end_utf8,
1104 left.reference_id.as_str(),
1105 left.role,
1106 left.span.coverage,
1107 )
1108 .cmp(&(
1109 right.paragraph_ordinal,
1110 right.span.start_utf8,
1111 right.span.end_utf8,
1112 right.reference_id.as_str(),
1113 right.role,
1114 right.span.coverage,
1115 ))
1116 });
1117 Ok(facts)
1118}
1119
1120fn segment_range(
1121 texts: &[&str],
1122 start: &ReviewPoint,
1123 end: &ReviewPoint,
1124 mut visit: impl FnMut(usize, StructuralSpan) -> Result<(), ProjectionError>,
1125) -> Result<(), ProjectionError> {
1126 if start.paragraph_ordinal > end.paragraph_ordinal || end.paragraph_ordinal >= texts.len() {
1127 return Err(ProjectionError::InvalidDocumentXml);
1128 }
1129 if start.paragraph_ordinal == end.paragraph_ordinal
1130 && (start.utf8 > end.utf8 || start.utf16 > end.utf16)
1131 {
1132 return Err(ProjectionError::InvalidDocumentXml);
1133 }
1134 for (paragraph, text) in texts
1135 .iter()
1136 .enumerate()
1137 .take(end.paragraph_ordinal.saturating_add(1))
1138 .skip(start.paragraph_ordinal)
1139 {
1140 let text_utf8 =
1141 u32::try_from(text.len()).map_err(|_| ProjectionError::InvalidDocumentXml)?;
1142 let text_utf16 = u32::try_from(text.encode_utf16().count())
1143 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
1144 let (start_utf8, start_utf16) = if paragraph == start.paragraph_ordinal {
1145 (start.utf8, start.utf16)
1146 } else {
1147 (0, 0)
1148 };
1149 let (end_utf8, end_utf16) = if paragraph == end.paragraph_ordinal {
1150 (end.utf8, end.utf16)
1151 } else {
1152 (text_utf8, text_utf16)
1153 };
1154 if start_utf8 > text_utf8
1155 || end_utf8 > text_utf8
1156 || start_utf16 > text_utf16
1157 || end_utf16 > text_utf16
1158 {
1159 return Err(ProjectionError::InvalidDocumentXml);
1160 }
1161 let continues_before = paragraph > start.paragraph_ordinal;
1162 let continues_after = paragraph < end.paragraph_ordinal;
1163 let coverage = match (continues_before, continues_after) {
1164 (false, false) => SpanCoverage::Complete,
1165 (true, false) => SpanCoverage::ContinuesBefore,
1166 (false, true) => SpanCoverage::ContinuesAfter,
1167 (true, true) => SpanCoverage::ContinuesBeforeAndAfter,
1168 };
1169 visit(
1170 paragraph,
1171 StructuralSpan {
1172 start_utf8,
1173 end_utf8,
1174 start_utf16,
1175 end_utf16,
1176 coverage,
1177 },
1178 )?;
1179 }
1180 Ok(())
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185 use super::{
1186 ParagraphProperties, StyleDefinition, StyleKind, StyleSheet, TableStyleFormatting,
1187 TextProperties, inherit_style_toggle,
1188 };
1189
1190 const WORD_STYLE_LIMIT: usize = 4_079;
1191
1192 fn style(based_on: Option<String>, kind: StyleKind) -> StyleDefinition {
1193 StyleDefinition {
1194 based_on,
1195 properties: ParagraphProperties::default(),
1196 text_properties: TextProperties {
1197 bold: Some(true),
1198 ..TextProperties::default()
1199 },
1200 kind,
1201 table_formatting: TableStyleFormatting::Base,
1202 }
1203 }
1204
1205 #[test]
1206 fn style_toggle_truth_table_preserves_false_and_toggles_true() {
1207 let cases = [
1208 (&[][..], None),
1209 (&[false][..], None),
1210 (&[true][..], Some(true)),
1211 (&[true, false][..], Some(true)),
1212 (&[true, true][..], Some(false)),
1213 (&[true, false, true][..], Some(false)),
1214 (&[true, true, true][..], Some(true)),
1215 ];
1216
1217 for (levels, expected) in cases {
1218 let actual = levels.iter().fold(None, |inherited, &level| {
1219 inherit_style_toggle(inherited, Some(level))
1220 });
1221 assert_eq!(actual, expected, "style levels {levels:?}");
1222 }
1223 }
1224
1225 #[test]
1226 fn style_resolution_stays_within_its_linear_work_budget() -> Result<(), &'static str> {
1227 let ids = (0..WORD_STYLE_LIMIT)
1228 .map(|index| format!("style-{index}"))
1229 .collect::<Vec<_>>();
1230
1231 let mut paragraph_chain = StyleSheet::default();
1232 let mut paragraph_parent = None;
1233 for id in &ids {
1234 paragraph_chain.styles.insert(
1235 id.clone(),
1236 style(paragraph_parent.clone(), StyleKind::Paragraph),
1237 );
1238 paragraph_parent = Some(id.clone());
1239 }
1240 let (resolved_paragraphs, paragraph_steps) =
1241 paragraph_chain.resolve_block_styles(StyleKind::Paragraph);
1242 assert_eq!(resolved_paragraphs.len(), WORD_STYLE_LIMIT);
1243 assert!(paragraph_steps <= WORD_STYLE_LIMIT.saturating_mul(2).saturating_add(1));
1244
1245 let mut character_chain = StyleSheet::default();
1246 let mut character_parent = None;
1247 for id in &ids {
1248 character_chain.styles.insert(
1249 id.clone(),
1250 style(character_parent.clone(), StyleKind::Character),
1251 );
1252 character_parent = Some(id.clone());
1253 }
1254 let (resolved_characters, character_steps) =
1255 character_chain.resolve_character_text_styles();
1256 assert_eq!(resolved_characters.len(), WORD_STYLE_LIMIT);
1257 assert!(character_steps <= WORD_STYLE_LIMIT.saturating_mul(2).saturating_add(1));
1258
1259 let mut cycle = StyleSheet::default();
1260 let first = ids.first().ok_or("the fixture must be non-empty")?;
1261 for (index, id) in ids.iter().enumerate() {
1262 let successor = ids.get(index.saturating_add(1)).unwrap_or(first);
1263 cycle.styles.insert(
1264 id.clone(),
1265 style(Some(successor.clone()), StyleKind::Paragraph),
1266 );
1267 }
1268 let (cycle_resolved, cycle_steps) = cycle.resolve_block_styles(StyleKind::Paragraph);
1269 assert!(cycle_resolved.is_empty());
1270 assert!(cycle_steps <= WORD_STYLE_LIMIT.saturating_add(1));
1271 Ok(())
1272 }
1273}