Skip to main content

stella_docx_kernel/projection/
structure.rs

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                    // Word ignores a missing basedOn target. A missing initially
479                    // selected style is still unsupported.
480                    valid = !chain.is_empty();
481                    break;
482                };
483                if style.kind != StyleKind::Character {
484                    // basedOn inheritance cannot cross style kinds; Word treats
485                    // the current style as a root instead of discarding it.
486                    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                // Every ID in the chain was validated above.
498                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}