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