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}