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 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}