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 pub(super) fn is_empty(self) -> bool {
46 self == Self::default()
47 }
48
49 pub(super) 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)]
163pub struct ParagraphOutlineLevelFact {
164 pub paragraph_ordinal: usize,
165 pub outline_level: u8,
166}
167
168#[derive(Clone, Debug, Eq, PartialEq)]
169pub struct NumberingHierarchyFact {
170 pub paragraph_ordinal: usize,
171 pub parent_paragraph_ordinal: Option<usize>,
172 pub child_paragraph_ordinals: Vec<usize>,
173}
174
175#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
176pub enum SpanCoverage {
177 Complete,
178 ContinuesBefore,
179 ContinuesAfter,
180 ContinuesBeforeAndAfter,
181}
182
183#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
184pub struct StructuralSpan {
185 pub start_utf8: u32,
186 pub end_utf8: u32,
187 pub start_utf16: u32,
188 pub end_utf16: u32,
189 pub coverage: SpanCoverage,
190}
191
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub struct BookmarkFact {
194 pub paragraph_ordinal: usize,
195 pub bookmark_id: u32,
196 pub name: String,
197 pub span: StructuralSpan,
198}
199
200#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
201pub enum InternalReferenceRole {
202 Source,
203 Target,
204}
205
206#[derive(Clone, Debug, Eq, PartialEq)]
207pub struct InternalReferenceFact {
208 pub paragraph_ordinal: usize,
209 pub reference_id: String,
210 pub role: InternalReferenceRole,
211 pub span: StructuralSpan,
212}
213
214#[derive(Clone, Debug, Eq, PartialEq)]
215pub struct DocumentStructureFacts {
216 pub indentation: StructuralFactSet<ParagraphIndentationFact>,
217 pub numbering_hierarchy: StructuralFactSet<NumberingHierarchyFact>,
218 pub bookmarks: StructuralFactSet<BookmarkFact>,
219 pub internal_references: StructuralFactSet<InternalReferenceFact>,
220 pub outline_levels: StructuralFactSet<ParagraphOutlineLevelFact>,
221}
222
223#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
224pub(super) struct NumberingProperties {
225 pub num_id: Option<u32>,
226 pub level: Option<u8>,
227 pub present: bool,
228}
229
230impl NumberingProperties {
231 pub(super) fn inherit(self, child: Self) -> Self {
232 if !child.present {
233 return self;
234 }
235 Self {
236 num_id: child.num_id.or(self.num_id),
237 level: child.level.or(self.level),
238 present: true,
239 }
240 }
241}
242
243#[derive(Clone, Debug, Default, Eq, PartialEq)]
244pub(super) struct ParagraphProperties {
245 pub style_id: Option<String>,
246 pub outline_level: Option<u8>,
247 pub indentation: ParagraphIndentation,
248 pub numbering: NumberingProperties,
249}
250
251#[derive(Clone, Debug)]
252pub(super) struct RawBlockPoint {
253 pub paragraph: usize,
254 pub utf8: u32,
255 pub utf16: u32,
256}
257
258#[derive(Clone, Debug)]
259pub(super) struct RawBookmarkRange {
260 pub id: u32,
261 pub name: String,
262 pub start: RawBlockPoint,
263 pub end: RawBlockPoint,
264}
265
266#[derive(Clone, Debug)]
267pub(super) struct RawInternalReference {
268 pub reference_id: String,
269 pub source: RawBlockPoint,
270}
271
272#[derive(Clone, Debug)]
273pub(super) struct StyleDefinition {
274 pub based_on: Option<String>,
275 pub properties: ParagraphProperties,
276 pub text_properties: TextProperties,
277 pub kind: StyleKind,
278}
279
280#[derive(Clone, Copy, Debug, Eq, PartialEq)]
281pub(super) enum StyleKind {
282 Character,
283 Paragraph,
284}
285
286#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
287pub(super) struct TextProperties {
288 pub bold: Option<bool>,
289 pub complex_script_bold: Option<bool>,
290 pub force_complex_script: Option<bool>,
291 pub right_to_left: Option<bool>,
292 pub highlighted: Option<bool>,
293 pub superscript: Option<bool>,
294}
295
296impl TextProperties {
297 pub(super) const fn inherit(self, child: Self) -> Self {
298 Self {
299 bold: if child.bold.is_some() {
300 child.bold
301 } else {
302 self.bold
303 },
304 complex_script_bold: if child.complex_script_bold.is_some() {
305 child.complex_script_bold
306 } else {
307 self.complex_script_bold
308 },
309 force_complex_script: if child.force_complex_script.is_some() {
310 child.force_complex_script
311 } else {
312 self.force_complex_script
313 },
314 right_to_left: if child.right_to_left.is_some() {
315 child.right_to_left
316 } else {
317 self.right_to_left
318 },
319 highlighted: if child.highlighted.is_some() {
320 child.highlighted
321 } else {
322 self.highlighted
323 },
324 superscript: if child.superscript.is_some() {
325 child.superscript
326 } else {
327 self.superscript
328 },
329 }
330 }
331
332 pub(super) fn inherit_style(self, child: Self) -> Self {
333 let mut inherited = self.inherit(child);
334 inherited.bold = inherit_style_toggle(self.bold, child.bold);
335 inherited.complex_script_bold =
336 inherit_style_toggle(self.complex_script_bold, child.complex_script_bold);
337 inherited
338 }
339}
340
341fn inherit_style_toggle(inherited: Option<bool>, child: Option<bool>) -> Option<bool> {
342 match child {
343 Some(true) => Some(!inherited.unwrap_or(false)),
344 Some(false) | None => inherited,
345 }
346}
347
348#[derive(Clone, Debug, Default)]
349pub(super) struct StyleSheet {
350 pub default_style_id: Option<String>,
351 pub document_defaults: ParagraphProperties,
352 pub document_text_defaults: TextProperties,
353 pub styles: HashMap<String, StyleDefinition>,
354 resolved_character_text: HashMap<String, TextProperties>,
355 resolved_paragraph_styles: HashMap<String, ResolvedParagraphStyle>,
356}
357
358#[derive(Clone, Copy, Debug, Default)]
359struct ResolvedParagraphStyle {
360 text: TextProperties,
361 properties: ResolvedParagraphStyleProperties,
362}
363
364impl ResolvedParagraphStyle {
365 fn inherit(self, child: &StyleDefinition) -> Self {
366 Self {
367 text: self.text.inherit_style(child.text_properties),
368 properties: self.properties.inherit(&child.properties),
369 }
370 }
371}
372
373#[derive(Clone, Copy, Debug, Default)]
374struct ResolvedParagraphStyleProperties {
375 indentation: ParagraphIndentation,
376 numbering: NumberingProperties,
377 outline_level: Option<u8>,
378}
379
380impl ResolvedParagraphStyleProperties {
381 fn inherit(self, child: &ParagraphProperties) -> Self {
382 Self {
383 indentation: self.indentation.inherit(child.indentation),
384 numbering: self.numbering.inherit(child.numbering),
385 outline_level: child.outline_level.or(self.outline_level),
386 }
387 }
388}
389
390#[derive(Clone, Debug)]
391struct ResolvedParagraphProperties {
392 indentation: Result<ParagraphIndentation, StructuralFactUnknownReason>,
393 numbering: NumberingProperties,
394 outline_level: Option<u8>,
395}
396
397impl StyleSheet {
398 pub(super) fn prepare_styles(&mut self) {
399 (self.resolved_character_text, _) = self.resolve_character_text_styles();
400 (self.resolved_paragraph_styles, _) = self.resolve_paragraph_styles();
401 }
402
403 pub(super) fn resolve_text(
404 &self,
405 paragraph_style_id: Option<&str>,
406 character_style_id: Option<&str>,
407 direct: TextProperties,
408 ) -> Result<TextProperties, ()> {
409 let paragraph_style_id = paragraph_style_id.or(self.default_style_id.as_deref());
410 let paragraph = paragraph_style_id
411 .map(|style_id| {
412 self.resolved_paragraph_styles
413 .get(style_id)
414 .map(|style| style.text)
415 .ok_or(())
416 })
417 .transpose()?
418 .unwrap_or_default();
419 let character = character_style_id
420 .map(|style_id| {
421 self.resolved_character_text
422 .get(style_id)
423 .copied()
424 .ok_or(())
425 })
426 .transpose()?
427 .unwrap_or_default();
428 let mut effective = self
429 .document_text_defaults
430 .inherit(paragraph)
431 .inherit(character)
432 .inherit(direct);
433 effective.bold = Some(resolve_word_toggle(
434 self.document_text_defaults.bold,
435 paragraph.bold,
436 character.bold,
437 direct.bold,
438 ));
439 effective.complex_script_bold = Some(resolve_word_toggle(
440 self.document_text_defaults.complex_script_bold,
441 paragraph.complex_script_bold,
442 character.complex_script_bold,
443 direct.complex_script_bold,
444 ));
445 Ok(effective)
446 }
447
448 fn resolve_character_text_styles(&self) -> (HashMap<String, TextProperties>, usize) {
449 let mut resolved = HashMap::new();
450 let mut unresolved = HashSet::new();
451 let mut resolution_steps = 0usize;
452 for root_id in self.styles.iter().filter_map(|(style_id, style)| {
453 (style.kind == StyleKind::Character).then_some(style_id)
454 }) {
455 if resolved.contains_key(root_id) || unresolved.contains(root_id) {
456 continue;
457 }
458 let mut chain = Vec::new();
459 let mut seen = HashSet::new();
460 let mut current = Some(root_id.as_str());
461 let mut inherited = TextProperties::default();
462 let mut valid = true;
463 while let Some(style_id) = current {
464 resolution_steps = resolution_steps.saturating_add(1);
465 if let Some(properties) = resolved.get(style_id).copied() {
466 inherited = properties;
467 break;
468 }
469 if unresolved.contains(style_id) {
470 valid = false;
471 break;
472 }
473 if !seen.insert(style_id) {
474 valid = false;
475 break;
476 }
477 let Some(style) = self.styles.get(style_id) else {
478 valid = !chain.is_empty();
481 break;
482 };
483 if style.kind != StyleKind::Character {
484 valid = !chain.is_empty();
487 break;
488 }
489 chain.push(style_id);
490 current = style.based_on.as_deref();
491 }
492 if !valid {
493 unresolved.extend(chain.into_iter().map(str::to_owned));
494 continue;
495 }
496 for style_id in chain.into_iter().rev() {
497 if let Some(style) = self.styles.get(style_id) {
499 inherited = inherited.inherit_style(style.text_properties);
500 resolved.insert(style_id.to_owned(), inherited);
501 }
502 }
503 }
504 debug_assert!(
505 resolution_steps <= self.styles.len().saturating_mul(2).saturating_add(1),
506 "memoized style resolution exceeded its linear work budget"
507 );
508 (resolved, resolution_steps)
509 }
510
511 fn resolve_paragraph_styles(&self) -> (HashMap<String, ResolvedParagraphStyle>, usize) {
512 let mut resolved = HashMap::new();
513 let mut unresolved = HashSet::new();
514 let mut resolution_steps = 0usize;
515 for root_id in self.styles.iter().filter_map(|(style_id, style)| {
516 (style.kind == StyleKind::Paragraph).then_some(style_id)
517 }) {
518 if resolved.contains_key(root_id) || unresolved.contains(root_id) {
519 continue;
520 }
521 let mut chain = Vec::new();
522 let mut seen = HashSet::new();
523 let mut current = Some(root_id.as_str());
524 let mut inherited = ResolvedParagraphStyle::default();
525 let mut valid = true;
526 while let Some(style_id) = current {
527 resolution_steps = resolution_steps.saturating_add(1);
528 if let Some(properties) = resolved.get(style_id).copied() {
529 inherited = properties;
530 break;
531 }
532 if unresolved.contains(style_id) || !seen.insert(style_id) {
533 valid = false;
534 break;
535 }
536 let Some(style) = self.styles.get(style_id) else {
537 valid = !chain.is_empty();
538 break;
539 };
540 if style.kind != StyleKind::Paragraph {
541 valid = !chain.is_empty();
542 break;
543 }
544 chain.push(style_id);
545 current = style.based_on.as_deref();
546 }
547 if !valid {
548 unresolved.extend(chain.into_iter().map(str::to_owned));
549 continue;
550 }
551 for style_id in chain.into_iter().rev() {
552 if let Some(style) = self.styles.get(style_id) {
553 inherited = inherited.inherit(style);
554 resolved.insert(style_id.to_owned(), inherited);
555 }
556 }
557 }
558 debug_assert!(
559 resolution_steps <= self.styles.len().saturating_mul(2).saturating_add(1),
560 "memoized paragraph style resolution exceeded its linear work budget"
561 );
562 (resolved, resolution_steps)
563 }
564
565 pub(super) fn paragraph_uses_numbering(
566 &self,
567 direct: &ParagraphProperties,
568 ) -> Result<bool, StructuralFactUnknownReason> {
569 self.resolve(
570 direct,
571 Err(StructuralFactUnknownReason::UnsupportedNumbering),
572 )
573 .map(|resolved| resolved.numbering.present && resolved.numbering.num_id != Some(0))
574 }
575
576 fn resolve(
577 &self,
578 direct: &ParagraphProperties,
579 numbering_catalog: Result<&NumberingCatalog, StructuralFactUnknownReason>,
580 ) -> Result<ResolvedParagraphProperties, StructuralFactUnknownReason> {
581 let initial_style_id = direct.style_id.as_ref().or(self.default_style_id.as_ref());
582 let style = initial_style_id
583 .map(|style_id| {
584 self.resolved_paragraph_styles
585 .get(style_id)
586 .map(|style| style.properties)
587 .ok_or(StructuralFactUnknownReason::UnsupportedStyles)
588 })
589 .transpose()?
590 .unwrap_or_default();
591 let mut numbering = self.document_defaults.numbering.inherit(style.numbering);
592 let mut outline_level = style.outline_level.or(self.document_defaults.outline_level);
593 let inherited_numbering = numbering;
594 numbering = numbering.inherit(direct.numbering);
595 outline_level = direct.outline_level.or(outline_level);
596
597 let numbered = numbering.present && numbering.num_id != Some(0);
598 let numbering_is_direct = direct.numbering.present;
599 let numbering_removed = numbering_is_direct
600 && direct.numbering.num_id == Some(0)
601 && inherited_numbering.present
602 && inherited_numbering.num_id != Some(0);
603 let indentation = if numbered {
604 numbering
605 .num_id
606 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering)
607 .and_then(|num_id| {
608 numbering_catalog.and_then(|catalog| {
609 catalog.indentation(num_id, numbering.level.unwrap_or(0))
610 })
611 })
612 } else {
613 Ok(ParagraphIndentation::default())
614 }
615 .map(|level_indentation| {
616 let mut indentation = if numbering_removed {
617 ParagraphIndentation::default()
618 } else {
619 self.document_defaults.indentation
620 };
621 if !numbering_is_direct && !numbering_removed {
622 indentation = indentation.inherit(level_indentation);
623 }
624 if !numbering_removed {
625 indentation = indentation.inherit(style.indentation);
626 }
627 if numbering_is_direct {
628 indentation = indentation.inherit(level_indentation);
629 }
630 if numbered {
631 indentation.inherit_numbered_direct(direct.indentation)
632 } else {
633 indentation.inherit(direct.indentation)
634 }
635 });
636 Ok(ResolvedParagraphProperties {
637 indentation,
638 numbering,
639 outline_level,
640 })
641 }
642}
643
644fn resolve_word_toggle(
645 document_default: Option<bool>,
646 paragraph: Option<bool>,
647 character: Option<bool>,
648 direct: Option<bool>,
649) -> bool {
650 if let Some(value) = direct {
651 return value;
652 }
653 document_default.unwrap_or(false) ^ paragraph.unwrap_or(false) ^ character.unwrap_or(false)
654}
655
656#[derive(Clone, Copy)]
657pub(super) struct RawStructureInput<'a> {
658 pub paragraph_texts: &'a [&'a str],
659 pub properties: &'a [&'a ParagraphProperties],
660 pub bookmarks: Result<&'a [RawBookmarkRange], StructuralFactUnknownReason>,
661 pub references: Result<&'a [RawInternalReference], StructuralFactUnknownReason>,
662}
663
664struct StructuralFactBudget {
665 remaining: usize,
666}
667
668impl StructuralFactBudget {
669 const fn new(maximum_facts: usize) -> Self {
670 Self {
671 remaining: maximum_facts,
672 }
673 }
674
675 fn consume(&mut self, facts: usize) -> Result<(), ProjectionError> {
676 self.remaining = self
677 .remaining
678 .checked_sub(facts)
679 .ok_or(ProjectionError::TooManyStructuralFacts)?;
680 Ok(())
681 }
682}
683
684pub(super) fn materialize_structure(
685 input: RawStructureInput<'_>,
686 styles: Result<&StyleSheet, StructuralFactUnknownReason>,
687 numbering: Result<&NumberingCatalog, StructuralFactUnknownReason>,
688 maximum_facts: usize,
689) -> Result<DocumentStructureFacts, ProjectionError> {
690 let mut fact_budget = StructuralFactBudget::new(maximum_facts);
691 let resolved = styles.and_then(|styles| {
692 input
693 .properties
694 .iter()
695 .map(|properties| styles.resolve(properties, numbering))
696 .collect::<Result<Vec<_>, _>>()
697 });
698
699 let (indentation, numbering_hierarchy, outline_levels) = match resolved {
700 Ok(properties) => {
701 let indentation = properties
702 .iter()
703 .map(|properties| properties.indentation)
704 .collect::<Result<Vec<_>, _>>()
705 .map_or_else(StructuralFactSet::Unknown, |indentations| {
706 StructuralFactSet::Known(
707 indentations
708 .into_iter()
709 .enumerate()
710 .filter(|(_, indentation)| !indentation.is_empty())
711 .map(|(paragraph_ordinal, value)| ParagraphIndentationFact {
712 paragraph_ordinal,
713 value,
714 })
715 .collect(),
716 )
717 });
718 (
719 indentation,
720 materialize_numbering(&properties),
721 StructuralFactSet::Known(
722 properties
723 .iter()
724 .enumerate()
725 .filter_map(|(paragraph_ordinal, properties)| {
726 properties.outline_level.map(|outline_level| {
727 ParagraphOutlineLevelFact {
728 paragraph_ordinal,
729 outline_level,
730 }
731 })
732 })
733 .collect(),
734 ),
735 )
736 }
737 Err(reason) => (
738 StructuralFactSet::Unknown(reason),
739 StructuralFactSet::Unknown(reason),
740 StructuralFactSet::Unknown(reason),
741 ),
742 };
743 if let StructuralFactSet::Known(facts) = &indentation {
744 fact_budget.consume(facts.len())?;
745 }
746 if let StructuralFactSet::Known(facts) = &numbering_hierarchy {
747 fact_budget.consume(facts.len())?;
748 }
749 if let StructuralFactSet::Known(facts) = &outline_levels {
750 fact_budget.consume(facts.len())?;
751 }
752
753 let bookmarks = match input.bookmarks {
754 Ok(ranges) => StructuralFactSet::Known(materialize_bookmarks(
755 input.paragraph_texts,
756 ranges,
757 &mut fact_budget,
758 )?),
759 Err(reason) => StructuralFactSet::Unknown(reason),
760 };
761 let internal_references = match (input.references, &bookmarks) {
762 (Ok([]), _) => StructuralFactSet::Known(Vec::new()),
763 (Ok(references), StructuralFactSet::Known(bookmark_facts)) => StructuralFactSet::Known(
764 materialize_references(references, bookmark_facts, &mut fact_budget)?,
765 ),
766 (Err(reason), _) => StructuralFactSet::Unknown(reason),
767 (_, StructuralFactSet::Unknown(_)) => {
768 StructuralFactSet::Unknown(StructuralFactUnknownReason::IncompleteBookmarkRanges)
769 }
770 };
771
772 Ok(DocumentStructureFacts {
773 indentation,
774 numbering_hierarchy,
775 bookmarks,
776 internal_references,
777 outline_levels,
778 })
779}
780
781fn materialize_numbering(
782 properties: &[ResolvedParagraphProperties],
783) -> StructuralFactSet<NumberingHierarchyFact> {
784 let mut stacks: HashMap<u32, [Option<usize>; 9]> = HashMap::new();
785 let mut parents = vec![None; properties.len()];
786 let mut children = vec![Vec::new(); properties.len()];
787
788 for (ordinal, paragraph_properties) in properties.iter().enumerate() {
789 if !paragraph_properties.numbering.present
790 || paragraph_properties.numbering.num_id == Some(0)
791 {
792 continue;
793 }
794 let Some(num_id) = paragraph_properties.numbering.num_id else {
795 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
796 };
797 let level = usize::from(paragraph_properties.numbering.level.unwrap_or(0));
798 if level >= 9 {
799 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
800 }
801 let stack = stacks.entry(num_id).or_insert([None; 9]);
802 if level > 0
803 && let Some(parent) = stack
804 .get(..level)
805 .and_then(|ancestors| ancestors.iter().rev().flatten().next().copied())
806 {
807 let Some(parent_slot) = parents.get_mut(ordinal) else {
808 return StructuralFactSet::Unknown(
809 StructuralFactUnknownReason::UnsupportedNumbering,
810 );
811 };
812 *parent_slot = Some(parent);
813 let Some(parent_children) = children.get_mut(parent) else {
814 return StructuralFactSet::Unknown(
815 StructuralFactUnknownReason::UnsupportedNumbering,
816 );
817 };
818 parent_children.push(ordinal);
819 }
820 let Some(level_slot) = stack.get_mut(level) else {
821 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
822 };
823 *level_slot = Some(ordinal);
824 let descendant_start = level.saturating_add(1);
825 let Some(descendant_slots) = stack.get_mut(descendant_start..) else {
826 return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
827 };
828 for slot in descendant_slots {
829 *slot = None;
830 }
831 }
832
833 StructuralFactSet::Known(
834 properties
835 .iter()
836 .enumerate()
837 .filter_map(|(ordinal, _)| {
838 let parent = parents.get(ordinal).copied().flatten();
839 let paragraph_children = children.get(ordinal)?;
840 (parent.is_some() || !paragraph_children.is_empty()).then(|| {
841 NumberingHierarchyFact {
842 paragraph_ordinal: ordinal,
843 parent_paragraph_ordinal: parent,
844 child_paragraph_ordinals: paragraph_children.clone(),
845 }
846 })
847 })
848 .collect(),
849 )
850}
851
852fn materialize_bookmarks(
853 texts: &[&str],
854 ranges: &[RawBookmarkRange],
855 fact_budget: &mut StructuralFactBudget,
856) -> Result<Vec<BookmarkFact>, ProjectionError> {
857 let mut facts = Vec::new();
858 for range in ranges {
859 segment_range(texts, &range.start, &range.end, |paragraph, span| {
860 fact_budget.consume(1)?;
861 facts.push(BookmarkFact {
862 paragraph_ordinal: paragraph,
863 bookmark_id: range.id,
864 name: range.name.clone(),
865 span,
866 });
867 Ok(())
868 })?;
869 }
870 facts.sort_by(|left, right| {
871 (
872 left.paragraph_ordinal,
873 left.span.start_utf8,
874 left.span.end_utf8,
875 left.bookmark_id,
876 left.name.as_str(),
877 left.span.coverage,
878 )
879 .cmp(&(
880 right.paragraph_ordinal,
881 right.span.start_utf8,
882 right.span.end_utf8,
883 right.bookmark_id,
884 right.name.as_str(),
885 right.span.coverage,
886 ))
887 });
888 Ok(facts)
889}
890
891fn materialize_references(
892 references: &[RawInternalReference],
893 bookmarks: &[BookmarkFact],
894 fact_budget: &mut StructuralFactBudget,
895) -> Result<Vec<InternalReferenceFact>, ProjectionError> {
896 let mut facts = Vec::new();
897 let mut targets_by_name: HashMap<&str, Vec<&BookmarkFact>> = HashMap::new();
898 for bookmark in bookmarks {
899 targets_by_name
900 .entry(&bookmark.name)
901 .or_default()
902 .push(bookmark);
903 }
904 let mut emitted_target_names = HashSet::new();
905 for reference in references {
906 fact_budget.consume(1)?;
907 facts.push(InternalReferenceFact {
908 paragraph_ordinal: reference.source.paragraph,
909 reference_id: reference.reference_id.clone(),
910 role: InternalReferenceRole::Source,
911 span: StructuralSpan {
912 start_utf8: reference.source.utf8,
913 end_utf8: reference.source.utf8,
914 start_utf16: reference.source.utf16,
915 end_utf16: reference.source.utf16,
916 coverage: SpanCoverage::Complete,
917 },
918 });
919 if emitted_target_names.insert(reference.reference_id.as_str())
920 && let Some(targets) = targets_by_name.get(reference.reference_id.as_str())
921 {
922 for bookmark in targets {
923 fact_budget.consume(1)?;
924 facts.push(InternalReferenceFact {
925 paragraph_ordinal: bookmark.paragraph_ordinal,
926 reference_id: reference.reference_id.clone(),
927 role: InternalReferenceRole::Target,
928 span: bookmark.span,
929 });
930 }
931 }
932 }
933 facts.sort_by(|left, right| {
934 (
935 left.paragraph_ordinal,
936 left.span.start_utf8,
937 left.span.end_utf8,
938 left.reference_id.as_str(),
939 left.role,
940 left.span.coverage,
941 )
942 .cmp(&(
943 right.paragraph_ordinal,
944 right.span.start_utf8,
945 right.span.end_utf8,
946 right.reference_id.as_str(),
947 right.role,
948 right.span.coverage,
949 ))
950 });
951 Ok(facts)
952}
953
954fn segment_range(
955 texts: &[&str],
956 start: &RawBlockPoint,
957 end: &RawBlockPoint,
958 mut visit: impl FnMut(usize, StructuralSpan) -> Result<(), ProjectionError>,
959) -> Result<(), ProjectionError> {
960 if start.paragraph > end.paragraph || end.paragraph >= texts.len() {
961 return Err(ProjectionError::InvalidDocumentXml);
962 }
963 if start.paragraph == end.paragraph && (start.utf8 > end.utf8 || start.utf16 > end.utf16) {
964 return Err(ProjectionError::InvalidDocumentXml);
965 }
966 for (paragraph, text) in texts
967 .iter()
968 .enumerate()
969 .take(end.paragraph.saturating_add(1))
970 .skip(start.paragraph)
971 {
972 let text_utf8 =
973 u32::try_from(text.len()).map_err(|_| ProjectionError::InvalidDocumentXml)?;
974 let text_utf16 = u32::try_from(text.encode_utf16().count())
975 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
976 let (start_utf8, start_utf16) = if paragraph == start.paragraph {
977 (start.utf8, start.utf16)
978 } else {
979 (0, 0)
980 };
981 let (end_utf8, end_utf16) = if paragraph == end.paragraph {
982 (end.utf8, end.utf16)
983 } else {
984 (text_utf8, text_utf16)
985 };
986 if start_utf8 > text_utf8
987 || end_utf8 > text_utf8
988 || start_utf16 > text_utf16
989 || end_utf16 > text_utf16
990 {
991 return Err(ProjectionError::InvalidDocumentXml);
992 }
993 let continues_before = paragraph > start.paragraph;
994 let continues_after = paragraph < end.paragraph;
995 let coverage = match (continues_before, continues_after) {
996 (false, false) => SpanCoverage::Complete,
997 (true, false) => SpanCoverage::ContinuesBefore,
998 (false, true) => SpanCoverage::ContinuesAfter,
999 (true, true) => SpanCoverage::ContinuesBeforeAndAfter,
1000 };
1001 visit(
1002 paragraph,
1003 StructuralSpan {
1004 start_utf8,
1005 end_utf8,
1006 start_utf16,
1007 end_utf16,
1008 coverage,
1009 },
1010 )?;
1011 }
1012 Ok(())
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017 use super::{
1018 ParagraphProperties, StyleDefinition, StyleKind, StyleSheet, TextProperties,
1019 inherit_style_toggle,
1020 };
1021
1022 const WORD_STYLE_LIMIT: usize = 4_079;
1023
1024 fn style(based_on: Option<String>, kind: StyleKind) -> StyleDefinition {
1025 StyleDefinition {
1026 based_on,
1027 properties: ParagraphProperties::default(),
1028 text_properties: TextProperties {
1029 bold: Some(true),
1030 ..TextProperties::default()
1031 },
1032 kind,
1033 }
1034 }
1035
1036 #[test]
1037 fn style_toggle_truth_table_preserves_false_and_toggles_true() {
1038 let cases = [
1039 (&[][..], None),
1040 (&[false][..], None),
1041 (&[true][..], Some(true)),
1042 (&[true, false][..], Some(true)),
1043 (&[true, true][..], Some(false)),
1044 (&[true, false, true][..], Some(false)),
1045 (&[true, true, true][..], Some(true)),
1046 ];
1047
1048 for (levels, expected) in cases {
1049 let actual = levels.iter().fold(None, |inherited, &level| {
1050 inherit_style_toggle(inherited, Some(level))
1051 });
1052 assert_eq!(actual, expected, "style levels {levels:?}");
1053 }
1054 }
1055
1056 #[test]
1057 fn style_resolution_stays_within_its_linear_work_budget() -> Result<(), &'static str> {
1058 let ids = (0..WORD_STYLE_LIMIT)
1059 .map(|index| format!("style-{index}"))
1060 .collect::<Vec<_>>();
1061
1062 let mut paragraph_chain = StyleSheet::default();
1063 let mut paragraph_parent = None;
1064 for id in &ids {
1065 paragraph_chain.styles.insert(
1066 id.clone(),
1067 style(paragraph_parent.clone(), StyleKind::Paragraph),
1068 );
1069 paragraph_parent = Some(id.clone());
1070 }
1071 let (resolved_paragraphs, paragraph_steps) = paragraph_chain.resolve_paragraph_styles();
1072 assert_eq!(resolved_paragraphs.len(), WORD_STYLE_LIMIT);
1073 assert!(paragraph_steps <= WORD_STYLE_LIMIT.saturating_mul(2).saturating_add(1));
1074
1075 let mut character_chain = StyleSheet::default();
1076 let mut character_parent = None;
1077 for id in &ids {
1078 character_chain.styles.insert(
1079 id.clone(),
1080 style(character_parent.clone(), StyleKind::Character),
1081 );
1082 character_parent = Some(id.clone());
1083 }
1084 let (resolved_characters, character_steps) =
1085 character_chain.resolve_character_text_styles();
1086 assert_eq!(resolved_characters.len(), WORD_STYLE_LIMIT);
1087 assert!(character_steps <= WORD_STYLE_LIMIT.saturating_mul(2).saturating_add(1));
1088
1089 let mut cycle = StyleSheet::default();
1090 let first = ids.first().ok_or("the fixture must be non-empty")?;
1091 for (index, id) in ids.iter().enumerate() {
1092 let successor = ids.get(index.saturating_add(1)).unwrap_or(first);
1093 cycle.styles.insert(
1094 id.clone(),
1095 style(Some(successor.clone()), StyleKind::Paragraph),
1096 );
1097 }
1098 let (cycle_resolved, cycle_steps) = cycle.resolve_paragraph_styles();
1099 assert!(cycle_resolved.is_empty());
1100 assert!(cycle_steps <= WORD_STYLE_LIMIT.saturating_add(1));
1101 Ok(())
1102 }
1103}