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}