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        Self {
51            first_line_twips: if child.hanging_twips.is_some() {
52                None
53            } else {
54                child.first_line_twips.or(self.first_line_twips)
55            },
56            hanging_twips: if child.first_line_twips.is_some() {
57                None
58            } else {
59                child.hanging_twips.or(self.hanging_twips)
60            },
61            left_twips: child.left_twips.or(self.left_twips),
62            right_twips: child.right_twips.or(self.right_twips),
63            start_twips: child.start_twips.or(self.start_twips),
64            end_twips: child.end_twips.or(self.end_twips),
65            first_line_chars_hundredths: if child.hanging_chars_hundredths.is_some() {
66                None
67            } else {
68                child
69                    .first_line_chars_hundredths
70                    .or(self.first_line_chars_hundredths)
71            },
72            hanging_chars_hundredths: if child.first_line_chars_hundredths.is_some() {
73                None
74            } else {
75                child
76                    .hanging_chars_hundredths
77                    .or(self.hanging_chars_hundredths)
78            },
79            left_chars_hundredths: child.left_chars_hundredths.or(self.left_chars_hundredths),
80            right_chars_hundredths: child.right_chars_hundredths.or(self.right_chars_hundredths),
81            start_chars_hundredths: child.start_chars_hundredths.or(self.start_chars_hundredths),
82            end_chars_hundredths: child.end_chars_hundredths.or(self.end_chars_hundredths),
83        }
84    }
85
86    fn inherit_numbered_direct(self, mut direct: Self) -> Self {
87        if direct.first_line_twips == Some(0) {
88            direct.first_line_twips = None;
89        }
90        if direct.hanging_twips == Some(0) {
91            direct.hanging_twips = None;
92        }
93        if direct.first_line_chars_hundredths == Some(0) {
94            direct.first_line_chars_hundredths = None;
95        }
96        if direct.hanging_chars_hundredths == Some(0) {
97            direct.hanging_chars_hundredths = None;
98        }
99        self.inherit(direct)
100    }
101}
102
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub struct ParagraphIndentationFact {
105    pub paragraph_ordinal: usize,
106    pub value: ParagraphIndentation,
107}
108
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub struct ParagraphOutlineLevelFact {
111    pub paragraph_ordinal: usize,
112    pub outline_level: u8,
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
116pub struct NumberingHierarchyFact {
117    pub paragraph_ordinal: usize,
118    pub parent_paragraph_ordinal: Option<usize>,
119    pub child_paragraph_ordinals: Vec<usize>,
120}
121
122#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
123pub enum SpanCoverage {
124    Complete,
125    ContinuesBefore,
126    ContinuesAfter,
127    ContinuesBeforeAndAfter,
128}
129
130#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
131pub struct StructuralSpan {
132    pub start_utf8: u32,
133    pub end_utf8: u32,
134    pub start_utf16: u32,
135    pub end_utf16: u32,
136    pub coverage: SpanCoverage,
137}
138
139#[derive(Clone, Debug, Eq, PartialEq)]
140pub struct BookmarkFact {
141    pub paragraph_ordinal: usize,
142    pub bookmark_id: u32,
143    pub name: String,
144    pub span: StructuralSpan,
145}
146
147#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
148pub enum InternalReferenceRole {
149    Source,
150    Target,
151}
152
153#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct InternalReferenceFact {
155    pub paragraph_ordinal: usize,
156    pub reference_id: String,
157    pub role: InternalReferenceRole,
158    pub span: StructuralSpan,
159}
160
161#[derive(Clone, Debug, Eq, PartialEq)]
162pub struct DocumentStructureFacts {
163    pub indentation: StructuralFactSet<ParagraphIndentationFact>,
164    pub numbering_hierarchy: StructuralFactSet<NumberingHierarchyFact>,
165    pub bookmarks: StructuralFactSet<BookmarkFact>,
166    pub internal_references: StructuralFactSet<InternalReferenceFact>,
167    pub outline_levels: StructuralFactSet<ParagraphOutlineLevelFact>,
168}
169
170#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
171pub(super) struct NumberingProperties {
172    pub num_id: Option<u32>,
173    pub level: Option<u8>,
174    pub present: bool,
175}
176
177impl NumberingProperties {
178    pub(super) fn inherit(self, child: Self) -> Self {
179        if !child.present {
180            return self;
181        }
182        Self {
183            num_id: child.num_id.or(self.num_id),
184            level: child.level.or(self.level),
185            present: true,
186        }
187    }
188}
189
190#[derive(Clone, Debug, Default, Eq, PartialEq)]
191pub(super) struct ParagraphProperties {
192    pub style_id: Option<String>,
193    pub outline_level: Option<u8>,
194    pub indentation: ParagraphIndentation,
195    pub numbering: NumberingProperties,
196}
197
198#[derive(Clone, Debug)]
199pub(super) struct RawBlockPoint {
200    pub paragraph: usize,
201    pub utf8: u32,
202    pub utf16: u32,
203}
204
205#[derive(Clone, Debug)]
206pub(super) struct RawBookmarkRange {
207    pub id: u32,
208    pub name: String,
209    pub start: RawBlockPoint,
210    pub end: RawBlockPoint,
211}
212
213#[derive(Clone, Debug)]
214pub(super) struct RawInternalReference {
215    pub reference_id: String,
216    pub source: RawBlockPoint,
217}
218
219#[derive(Clone, Debug)]
220pub(super) struct StyleDefinition {
221    pub based_on: Option<String>,
222    pub properties: ParagraphProperties,
223}
224
225#[derive(Clone, Debug, Default)]
226pub(super) struct StyleSheet {
227    pub default_style_id: Option<String>,
228    pub document_defaults: ParagraphProperties,
229    pub styles: HashMap<String, StyleDefinition>,
230}
231
232#[derive(Clone, Debug)]
233struct ResolvedParagraphProperties {
234    indentation: Result<ParagraphIndentation, StructuralFactUnknownReason>,
235    numbering: NumberingProperties,
236    outline_level: Option<u8>,
237}
238
239impl StyleSheet {
240    fn resolve(
241        &self,
242        direct: &ParagraphProperties,
243        numbering_catalog: Result<&NumberingCatalog, StructuralFactUnknownReason>,
244    ) -> Result<ResolvedParagraphProperties, StructuralFactUnknownReason> {
245        let initial_style_id = direct.style_id.as_ref().or(self.default_style_id.as_ref());
246        let mut chain = Vec::new();
247        let mut current = initial_style_id;
248        while let Some(current_style_id) = current {
249            if chain.iter().any(|seen| seen == current_style_id) || chain.len() >= 128 {
250                return Err(StructuralFactUnknownReason::UnsupportedStyles);
251            }
252            let Some(style) = self.styles.get(current_style_id) else {
253                return Err(StructuralFactUnknownReason::UnsupportedStyles);
254            };
255            chain.push(current_style_id.clone());
256            current = style.based_on.as_ref();
257        }
258
259        let mut numbering = self.document_defaults.numbering;
260        let mut outline_level = self.document_defaults.outline_level;
261        for current_style_id in chain.iter().rev() {
262            let style = self
263                .styles
264                .get(current_style_id)
265                .ok_or(StructuralFactUnknownReason::UnsupportedStyles)?;
266            numbering = numbering.inherit(style.properties.numbering);
267            outline_level = style.properties.outline_level.or(outline_level);
268        }
269        let inherited_numbering = numbering;
270        numbering = numbering.inherit(direct.numbering);
271        outline_level = direct.outline_level.or(outline_level);
272
273        let numbered = numbering.present && numbering.num_id != Some(0);
274        let numbering_is_direct = direct.numbering.present;
275        let numbering_removed = numbering_is_direct
276            && direct.numbering.num_id == Some(0)
277            && inherited_numbering.present
278            && inherited_numbering.num_id != Some(0);
279        let indentation = if numbered {
280            numbering
281                .num_id
282                .ok_or(StructuralFactUnknownReason::UnsupportedNumbering)
283                .and_then(|num_id| {
284                    numbering_catalog.and_then(|catalog| {
285                        catalog.indentation(num_id, numbering.level.unwrap_or(0))
286                    })
287                })
288        } else {
289            Ok(ParagraphIndentation::default())
290        }
291        .map(|level_indentation| {
292            let mut indentation = if numbering_removed {
293                ParagraphIndentation::default()
294            } else {
295                self.document_defaults.indentation
296            };
297            if !numbering_is_direct && !numbering_removed {
298                indentation = indentation.inherit(level_indentation);
299            }
300            if !numbering_removed {
301                for current_style_id in chain.iter().rev() {
302                    // The style graph and IDs were validated while constructing the chain.
303                    if let Some(style) = self.styles.get(current_style_id) {
304                        indentation = indentation.inherit(style.properties.indentation);
305                    }
306                }
307            }
308            if numbering_is_direct {
309                indentation = indentation.inherit(level_indentation);
310            }
311            if numbered {
312                indentation.inherit_numbered_direct(direct.indentation)
313            } else {
314                indentation.inherit(direct.indentation)
315            }
316        });
317        Ok(ResolvedParagraphProperties {
318            indentation,
319            numbering,
320            outline_level,
321        })
322    }
323}
324
325#[derive(Clone, Copy)]
326pub(super) struct RawStructureInput<'a> {
327    pub paragraph_texts: &'a [&'a str],
328    pub properties: &'a [&'a ParagraphProperties],
329    pub bookmarks: Result<&'a [RawBookmarkRange], StructuralFactUnknownReason>,
330    pub references: Result<&'a [RawInternalReference], StructuralFactUnknownReason>,
331}
332
333struct StructuralFactBudget {
334    remaining: usize,
335}
336
337impl StructuralFactBudget {
338    const fn new(maximum_facts: usize) -> Self {
339        Self {
340            remaining: maximum_facts,
341        }
342    }
343
344    fn consume(&mut self, facts: usize) -> Result<(), ProjectionError> {
345        self.remaining = self
346            .remaining
347            .checked_sub(facts)
348            .ok_or(ProjectionError::TooManyStructuralFacts)?;
349        Ok(())
350    }
351}
352
353pub(super) fn materialize_structure(
354    input: RawStructureInput<'_>,
355    styles: Result<&StyleSheet, StructuralFactUnknownReason>,
356    numbering: Result<&NumberingCatalog, StructuralFactUnknownReason>,
357    maximum_facts: usize,
358) -> Result<DocumentStructureFacts, ProjectionError> {
359    let mut fact_budget = StructuralFactBudget::new(maximum_facts);
360    let resolved = styles.and_then(|styles| {
361        input
362            .properties
363            .iter()
364            .map(|properties| styles.resolve(properties, numbering))
365            .collect::<Result<Vec<_>, _>>()
366    });
367
368    let (indentation, numbering_hierarchy, outline_levels) = match resolved {
369        Ok(properties) => {
370            let indentation = properties
371                .iter()
372                .map(|properties| properties.indentation)
373                .collect::<Result<Vec<_>, _>>()
374                .map_or_else(StructuralFactSet::Unknown, |indentations| {
375                    StructuralFactSet::Known(
376                        indentations
377                            .into_iter()
378                            .enumerate()
379                            .filter(|(_, indentation)| !indentation.is_empty())
380                            .map(|(paragraph_ordinal, value)| ParagraphIndentationFact {
381                                paragraph_ordinal,
382                                value,
383                            })
384                            .collect(),
385                    )
386                });
387            (
388                indentation,
389                materialize_numbering(&properties),
390                StructuralFactSet::Known(
391                    properties
392                        .iter()
393                        .enumerate()
394                        .filter_map(|(paragraph_ordinal, properties)| {
395                            properties.outline_level.map(|outline_level| {
396                                ParagraphOutlineLevelFact {
397                                    paragraph_ordinal,
398                                    outline_level,
399                                }
400                            })
401                        })
402                        .collect(),
403                ),
404            )
405        }
406        Err(reason) => (
407            StructuralFactSet::Unknown(reason),
408            StructuralFactSet::Unknown(reason),
409            StructuralFactSet::Unknown(reason),
410        ),
411    };
412    if let StructuralFactSet::Known(facts) = &indentation {
413        fact_budget.consume(facts.len())?;
414    }
415    if let StructuralFactSet::Known(facts) = &numbering_hierarchy {
416        fact_budget.consume(facts.len())?;
417    }
418    if let StructuralFactSet::Known(facts) = &outline_levels {
419        fact_budget.consume(facts.len())?;
420    }
421
422    let bookmarks = match input.bookmarks {
423        Ok(ranges) => StructuralFactSet::Known(materialize_bookmarks(
424            input.paragraph_texts,
425            ranges,
426            &mut fact_budget,
427        )?),
428        Err(reason) => StructuralFactSet::Unknown(reason),
429    };
430    let internal_references = match (input.references, &bookmarks) {
431        (Ok([]), _) => StructuralFactSet::Known(Vec::new()),
432        (Ok(references), StructuralFactSet::Known(bookmark_facts)) => StructuralFactSet::Known(
433            materialize_references(references, bookmark_facts, &mut fact_budget)?,
434        ),
435        (Err(reason), _) => StructuralFactSet::Unknown(reason),
436        (_, StructuralFactSet::Unknown(_)) => {
437            StructuralFactSet::Unknown(StructuralFactUnknownReason::IncompleteBookmarkRanges)
438        }
439    };
440
441    Ok(DocumentStructureFacts {
442        indentation,
443        numbering_hierarchy,
444        bookmarks,
445        internal_references,
446        outline_levels,
447    })
448}
449
450fn materialize_numbering(
451    properties: &[ResolvedParagraphProperties],
452) -> StructuralFactSet<NumberingHierarchyFact> {
453    let mut stacks: HashMap<u32, [Option<usize>; 9]> = HashMap::new();
454    let mut parents = vec![None; properties.len()];
455    let mut children = vec![Vec::new(); properties.len()];
456
457    for (ordinal, paragraph_properties) in properties.iter().enumerate() {
458        if !paragraph_properties.numbering.present
459            || paragraph_properties.numbering.num_id == Some(0)
460        {
461            continue;
462        }
463        let Some(num_id) = paragraph_properties.numbering.num_id else {
464            return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
465        };
466        let level = usize::from(paragraph_properties.numbering.level.unwrap_or(0));
467        if level >= 9 {
468            return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
469        }
470        let stack = stacks.entry(num_id).or_insert([None; 9]);
471        if level > 0
472            && let Some(parent) = stack
473                .get(..level)
474                .and_then(|ancestors| ancestors.iter().rev().flatten().next().copied())
475        {
476            let Some(parent_slot) = parents.get_mut(ordinal) else {
477                return StructuralFactSet::Unknown(
478                    StructuralFactUnknownReason::UnsupportedNumbering,
479                );
480            };
481            *parent_slot = Some(parent);
482            let Some(parent_children) = children.get_mut(parent) else {
483                return StructuralFactSet::Unknown(
484                    StructuralFactUnknownReason::UnsupportedNumbering,
485                );
486            };
487            parent_children.push(ordinal);
488        }
489        let Some(level_slot) = stack.get_mut(level) else {
490            return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
491        };
492        *level_slot = Some(ordinal);
493        let descendant_start = level.saturating_add(1);
494        let Some(descendant_slots) = stack.get_mut(descendant_start..) else {
495            return StructuralFactSet::Unknown(StructuralFactUnknownReason::UnsupportedNumbering);
496        };
497        for slot in descendant_slots {
498            *slot = None;
499        }
500    }
501
502    StructuralFactSet::Known(
503        properties
504            .iter()
505            .enumerate()
506            .filter_map(|(ordinal, _)| {
507                let parent = parents.get(ordinal).copied().flatten();
508                let paragraph_children = children.get(ordinal)?;
509                (parent.is_some() || !paragraph_children.is_empty()).then(|| {
510                    NumberingHierarchyFact {
511                        paragraph_ordinal: ordinal,
512                        parent_paragraph_ordinal: parent,
513                        child_paragraph_ordinals: paragraph_children.clone(),
514                    }
515                })
516            })
517            .collect(),
518    )
519}
520
521fn materialize_bookmarks(
522    texts: &[&str],
523    ranges: &[RawBookmarkRange],
524    fact_budget: &mut StructuralFactBudget,
525) -> Result<Vec<BookmarkFact>, ProjectionError> {
526    let mut facts = Vec::new();
527    for range in ranges {
528        segment_range(texts, &range.start, &range.end, |paragraph, span| {
529            fact_budget.consume(1)?;
530            facts.push(BookmarkFact {
531                paragraph_ordinal: paragraph,
532                bookmark_id: range.id,
533                name: range.name.clone(),
534                span,
535            });
536            Ok(())
537        })?;
538    }
539    facts.sort_by(|left, right| {
540        (
541            left.paragraph_ordinal,
542            left.span.start_utf8,
543            left.span.end_utf8,
544            left.bookmark_id,
545            left.name.as_str(),
546            left.span.coverage,
547        )
548            .cmp(&(
549                right.paragraph_ordinal,
550                right.span.start_utf8,
551                right.span.end_utf8,
552                right.bookmark_id,
553                right.name.as_str(),
554                right.span.coverage,
555            ))
556    });
557    Ok(facts)
558}
559
560fn materialize_references(
561    references: &[RawInternalReference],
562    bookmarks: &[BookmarkFact],
563    fact_budget: &mut StructuralFactBudget,
564) -> Result<Vec<InternalReferenceFact>, ProjectionError> {
565    let mut facts = Vec::new();
566    let mut targets_by_name: HashMap<&str, Vec<&BookmarkFact>> = HashMap::new();
567    for bookmark in bookmarks {
568        targets_by_name
569            .entry(&bookmark.name)
570            .or_default()
571            .push(bookmark);
572    }
573    let mut emitted_target_names = HashSet::new();
574    for reference in references {
575        fact_budget.consume(1)?;
576        facts.push(InternalReferenceFact {
577            paragraph_ordinal: reference.source.paragraph,
578            reference_id: reference.reference_id.clone(),
579            role: InternalReferenceRole::Source,
580            span: StructuralSpan {
581                start_utf8: reference.source.utf8,
582                end_utf8: reference.source.utf8,
583                start_utf16: reference.source.utf16,
584                end_utf16: reference.source.utf16,
585                coverage: SpanCoverage::Complete,
586            },
587        });
588        if emitted_target_names.insert(reference.reference_id.as_str())
589            && let Some(targets) = targets_by_name.get(reference.reference_id.as_str())
590        {
591            for bookmark in targets {
592                fact_budget.consume(1)?;
593                facts.push(InternalReferenceFact {
594                    paragraph_ordinal: bookmark.paragraph_ordinal,
595                    reference_id: reference.reference_id.clone(),
596                    role: InternalReferenceRole::Target,
597                    span: bookmark.span,
598                });
599            }
600        }
601    }
602    facts.sort_by(|left, right| {
603        (
604            left.paragraph_ordinal,
605            left.span.start_utf8,
606            left.span.end_utf8,
607            left.reference_id.as_str(),
608            left.role,
609            left.span.coverage,
610        )
611            .cmp(&(
612                right.paragraph_ordinal,
613                right.span.start_utf8,
614                right.span.end_utf8,
615                right.reference_id.as_str(),
616                right.role,
617                right.span.coverage,
618            ))
619    });
620    Ok(facts)
621}
622
623fn segment_range(
624    texts: &[&str],
625    start: &RawBlockPoint,
626    end: &RawBlockPoint,
627    mut visit: impl FnMut(usize, StructuralSpan) -> Result<(), ProjectionError>,
628) -> Result<(), ProjectionError> {
629    if start.paragraph > end.paragraph || end.paragraph >= texts.len() {
630        return Err(ProjectionError::InvalidDocumentXml);
631    }
632    if start.paragraph == end.paragraph && (start.utf8 > end.utf8 || start.utf16 > end.utf16) {
633        return Err(ProjectionError::InvalidDocumentXml);
634    }
635    for (paragraph, text) in texts
636        .iter()
637        .enumerate()
638        .take(end.paragraph.saturating_add(1))
639        .skip(start.paragraph)
640    {
641        let text_utf8 =
642            u32::try_from(text.len()).map_err(|_| ProjectionError::InvalidDocumentXml)?;
643        let text_utf16 = u32::try_from(text.encode_utf16().count())
644            .map_err(|_| ProjectionError::InvalidDocumentXml)?;
645        let (start_utf8, start_utf16) = if paragraph == start.paragraph {
646            (start.utf8, start.utf16)
647        } else {
648            (0, 0)
649        };
650        let (end_utf8, end_utf16) = if paragraph == end.paragraph {
651            (end.utf8, end.utf16)
652        } else {
653            (text_utf8, text_utf16)
654        };
655        if start_utf8 > text_utf8
656            || end_utf8 > text_utf8
657            || start_utf16 > text_utf16
658            || end_utf16 > text_utf16
659        {
660            return Err(ProjectionError::InvalidDocumentXml);
661        }
662        let continues_before = paragraph > start.paragraph;
663        let continues_after = paragraph < end.paragraph;
664        let coverage = match (continues_before, continues_after) {
665            (false, false) => SpanCoverage::Complete,
666            (true, false) => SpanCoverage::ContinuesBefore,
667            (false, true) => SpanCoverage::ContinuesAfter,
668            (true, true) => SpanCoverage::ContinuesBeforeAndAfter,
669        };
670        visit(
671            paragraph,
672            StructuralSpan {
673                start_utf8,
674                end_utf8,
675                start_utf16,
676                end_utf16,
677                coverage,
678            },
679        )?;
680    }
681    Ok(())
682}