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