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