Skip to main content

stella_docx_kernel/projection/
structure.rs

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