1use std::collections::{BTreeSet, HashMap, HashSet};
2
3use quick_xml::XmlVersion;
4use quick_xml::escape::resolve_predefined_entity;
5use quick_xml::events::{BytesStart, Event};
6use quick_xml::reader::NsReader;
7use unicode_script::{Script, UnicodeScript};
8
9use crate::projection::compatibility::{CompatibilityAction, MarkupCompatibility};
10use crate::projection::namespaces::OoxmlNamespace;
11use crate::projection::review::{
12 AttributedRevision, ReviewDetail, ReviewFactSet, ReviewFactUnknownReason, ReviewPoint,
13 ReviewSpan, RevisionContent, RevisionFactKind, RevisionPayload,
14};
15use crate::projection::structure::{
16 ParagraphProperties, RawBookmarkRange, RawInternalReference, StructuralFactUnknownReason,
17 StyleSheet, TableStyleSelection, TextProperties, TextStyleInput,
18};
19use crate::projection::styles::{
20 parse_alignment, parse_indentation, parse_level_attribute, parse_outline_level_attribute,
21 parse_u32_attribute, semantic_highlight_value, word_style_id,
22};
23use crate::{FormattingCompleteness, FormattingUnknownReason, ParagraphContainer, ProjectionError};
24
25#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26pub struct PackageParagraphId(u32);
27
28impl PackageParagraphId {
29 #[must_use]
30 pub fn parse(value: &str) -> Option<Self> {
31 if value.len() != 8 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
32 return None;
33 }
34 let value = u32::from_str_radix(value, 16).ok()?;
35 (value != 0 && i32::try_from(value).is_ok()).then_some(Self(value))
36 }
37
38 #[must_use]
39 pub const fn value(self) -> u32 {
40 self.0
41 }
42}
43
44#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
45pub enum TextStyle {
46 Bold,
47 Highlight,
48 Superscript,
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct TextFormattingSpan {
53 pub start_utf16: u32,
54 pub end_utf16: u32,
55 pub style: TextStyle,
56}
57
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct ParagraphStructure {
60 pub table_ordinal: usize,
61 pub row: usize,
62 pub column: usize,
63}
64
65#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
66pub enum RevisionView {
67 #[default]
68 Current,
69 Original,
70}
71
72#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
73pub enum TextMaterialization {
74 #[default]
75 WordHost,
76 ReadablePlainText,
77}
78
79#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
80pub enum RevisionUnsupportedReason {
81 IncompatibleParagraphMerge,
82 StructuralTableRevision,
83 UnsupportedRevisionMarkup,
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub enum RevisionProjectionStatus {
88 Complete,
89 Incomplete(Vec<RevisionUnsupportedReason>),
90}
91
92impl RevisionProjectionStatus {
93 #[must_use]
94 pub const fn is_complete(&self) -> bool {
95 matches!(self, Self::Complete)
96 }
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103enum ParagraphMarkRevision {
104 Added,
106 Removed,
108}
109
110impl ParagraphMarkRevision {
111 const fn from_element(name: &[u8]) -> Option<Self> {
112 match name {
113 b"ins" | b"moveTo" => Some(Self::Added),
114 b"del" | b"moveFrom" => Some(Self::Removed),
115 _ => None,
116 }
117 }
118}
119
120#[derive(Clone, Copy, Debug, Eq, PartialEq)]
121pub(super) enum TextControl {
122 Tab,
123 LineBreak,
124 PageBreak,
125 ColumnBreak,
126 CarriageReturn,
127 NoteReference,
128 SoftHyphen,
129 NoBreakHyphen,
130}
131
132impl TextControl {
133 pub(super) const fn materialize(
134 self,
135 materialization: TextMaterialization,
136 ) -> Option<&'static str> {
137 match materialization {
138 TextMaterialization::WordHost => Some(match self {
139 Self::Tab => "\t",
140 Self::LineBreak => "\u{000b}",
141 Self::PageBreak => "\u{000c}",
142 Self::ColumnBreak => "\u{000e}",
143 Self::CarriageReturn => "\r",
144 Self::NoteReference => "\u{0002}",
146 Self::SoftHyphen => "\u{001f}",
147 Self::NoBreakHyphen => "\u{001e}",
148 }),
149 TextMaterialization::ReadablePlainText => match self {
150 Self::Tab => Some("\t"),
151 Self::LineBreak | Self::PageBreak | Self::ColumnBreak | Self::CarriageReturn => {
152 Some("\n")
153 }
154 Self::NoteReference => None,
155 Self::SoftHyphen => Some("\u{00ad}"),
156 Self::NoBreakHyphen => Some("\u{2011}"),
157 },
158 }
159 }
160}
161
162pub(super) struct RawProjectedParagraph {
163 pub ordinal: usize,
164 pub package_paragraph_id: Option<PackageParagraphId>,
165 pub text: String,
166 utf16_len: u32,
167 pub formatting: Vec<TextFormattingSpan>,
168 pub structure: Option<ParagraphStructure>,
169 pub container: ParagraphContainer,
170 pub properties: ParagraphProperties,
171}
172
173pub(super) struct RawDocumentProjection {
174 pub paragraphs: Vec<RawProjectedParagraph>,
175 pub bookmarks: Result<Vec<RawBookmarkRange>, StructuralFactUnknownReason>,
176 pub references: Result<Vec<RawInternalReference>, StructuralFactUnknownReason>,
177 pub formatting_completeness: FormattingCompleteness,
178 pub revision_status: RevisionProjectionStatus,
179 pub review_revisions: Option<ReviewFactSet<AttributedRevision>>,
180 pub review_comment_anchors: HashMap<String, ReviewSpan>,
181}
182
183struct ParagraphBuilder {
184 package_paragraph_id: Option<PackageParagraphId>,
185 text: String,
186 utf16_len: u32,
187 formatting: Vec<TextFormattingSpan>,
188 structure: Option<ParagraphStructure>,
189 container: ParagraphContainer,
190 properties: ParagraphProperties,
191 resolved_text_base: Option<Result<TextProperties, ()>>,
192 paragraph_mark_revision: Option<ParagraphMarkRevision>,
193 paragraph_review_indices: Vec<usize>,
194}
195
196impl ParagraphBuilder {
197 fn append(&mut self, text: &str, styles: TextProperties) -> Result<(), ProjectionError> {
198 let units = u32::try_from(text.encode_utf16().count())
199 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
200 let end = self
201 .utf16_len
202 .checked_add(units)
203 .ok_or(ProjectionError::InvalidDocumentXml)?;
204 self.append_bold(text, styles, end)?;
205 if styles.highlighted == Some(true) {
206 self.append_style(end, TextStyle::Highlight);
207 }
208 if styles.superscript == Some(true) {
209 self.append_style(end, TextStyle::Superscript);
210 }
211 self.text.push_str(text);
212 self.utf16_len = end;
213 Ok(())
214 }
215
216 fn append_bold(
217 &mut self,
218 text: &str,
219 styles: TextProperties,
220 end_utf16: u32,
221 ) -> Result<(), ProjectionError> {
222 if styles.force_complex_script == Some(true) || styles.right_to_left == Some(true) {
223 if styles.complex_script_bold == Some(true) {
224 self.append_style(end_utf16, TextStyle::Bold);
225 }
226 return Ok(());
227 }
228
229 let regular_bold = styles.bold == Some(true);
230 let complex_bold = styles.complex_script_bold == Some(true);
231 if regular_bold == complex_bold {
232 if regular_bold {
233 self.append_style(end_utf16, TextStyle::Bold);
234 }
235 return Ok(());
236 }
237
238 let mut slot = text
239 .chars()
240 .find_map(character_bold_slot)
241 .unwrap_or(BoldSlot::Regular);
242 let mut range_start = self.utf16_len;
243 let mut position = self.utf16_len;
244 for character in text.chars() {
245 let character_slot = character_bold_slot(character).unwrap_or(slot);
246 if character_slot != slot {
247 if slot.enabled(regular_bold, complex_bold) {
248 self.append_style_range(range_start, position, TextStyle::Bold);
249 }
250 range_start = position;
251 slot = character_slot;
252 }
253 let character_units = if character.len_utf16() == 1 { 1 } else { 2 };
254 position = position
255 .checked_add(character_units)
256 .ok_or(ProjectionError::InvalidDocumentXml)?;
257 }
258 if slot.enabled(regular_bold, complex_bold) {
259 self.append_style_range(range_start, position, TextStyle::Bold);
260 }
261 if position != end_utf16 {
262 return Err(ProjectionError::InvalidDocumentXml);
263 }
264 Ok(())
265 }
266
267 fn resolve_text_base(&mut self, styles: &StyleSheet) -> Result<TextProperties, ()> {
268 if let Some(resolved) = self.resolved_text_base {
269 return resolved;
270 }
271 let resolved = styles.resolve_text(TextStyleInput {
272 paragraph: &self.properties,
273 character_style_id: None,
274 direct: TextProperties::default(),
275 });
276 self.resolved_text_base = Some(resolved);
277 resolved
278 }
279
280 fn append_style(&mut self, end_utf16: u32, style: TextStyle) {
281 self.append_style_range(self.utf16_len, end_utf16, style);
282 }
283
284 fn append_style_range(&mut self, start_utf16: u32, end_utf16: u32, style: TextStyle) {
285 if start_utf16 >= end_utf16 {
286 return;
287 }
288 append_formatting_span(
289 &mut self.formatting,
290 TextFormattingSpan {
291 start_utf16,
292 end_utf16,
293 style,
294 },
295 );
296 }
297
298 fn truncate(&mut self, utf8_len: usize, utf16_len: u32) {
299 self.text.truncate(utf8_len);
300 self.utf16_len = utf16_len;
301 self.formatting.retain(|span| span.start_utf16 < utf16_len);
302 for span in &mut self.formatting {
303 span.end_utf16 = span.end_utf16.min(utf16_len);
304 }
305 }
306}
307
308#[derive(Clone, Copy, Eq, PartialEq)]
309enum BoldSlot {
310 Regular,
311 Complex,
312}
313
314impl BoldSlot {
315 const fn enabled(self, regular: bool, complex: bool) -> bool {
316 match self {
317 Self::Regular => regular,
318 Self::Complex => complex,
319 }
320 }
321}
322
323#[allow(clippy::too_many_lines)] fn character_bold_slot(character: char) -> Option<BoldSlot> {
325 match character.script() {
326 Script::Common | Script::Inherited => None,
327 Script::Adlam
333 | Script::Ahom
334 | Script::Arabic
335 | Script::Balinese
336 | Script::Batak
337 | Script::Beria_Erfe
338 | Script::Bengali
339 | Script::Bhaiksuki
340 | Script::Brahmi
341 | Script::Buginese
342 | Script::Buhid
343 | Script::Chakma
344 | Script::Cham
345 | Script::Chorasmian
346 | Script::Cypro_Minoan
347 | Script::Devanagari
348 | Script::Dives_Akuru
349 | Script::Dogra
350 | Script::Duployan
351 | Script::Egyptian_Hieroglyphs
352 | Script::Elymaic
353 | Script::Garay
354 | Script::Grantha
355 | Script::Gujarati
356 | Script::Gunjala_Gondi
357 | Script::Gurung_Khema
358 | Script::Gurmukhi
359 | Script::Hanifi_Rohingya
360 | Script::Hanunoo
361 | Script::Hebrew
362 | Script::Javanese
363 | Script::Kaithi
364 | Script::Kannada
365 | Script::Kawi
366 | Script::Kayah_Li
367 | Script::Kharoshthi
368 | Script::Khmer
369 | Script::Khitan_Small_Script
370 | Script::Khojki
371 | Script::Khudawadi
372 | Script::Kirat_Rai
373 | Script::Lao
374 | Script::Lepcha
375 | Script::Limbu
376 | Script::Mahajani
377 | Script::Makasar
378 | Script::Malayalam
379 | Script::Mandaic
380 | Script::Manichaean
381 | Script::Marchen
382 | Script::Masaram_Gondi
383 | Script::Medefaidrin
384 | Script::Meetei_Mayek
385 | Script::Miao
386 | Script::Modi
387 | Script::Mongolian
388 | Script::Multani
389 | Script::Myanmar
390 | Script::Nag_Mundari
391 | Script::Nandinagari
392 | Script::Newa
393 | Script::Nko
394 | Script::Nyiakeng_Puachue_Hmong
395 | Script::Old_Sogdian
396 | Script::Old_Uyghur
397 | Script::Ol_Onal
398 | Script::Oriya
399 | Script::Pahawh_Hmong
400 | Script::Phags_Pa
401 | Script::Psalter_Pahlavi
402 | Script::Rejang
403 | Script::Saurashtra
404 | Script::Sharada
405 | Script::Siddham
406 | Script::Sidetic
407 | Script::Sinhala
408 | Script::Sogdian
409 | Script::Soyombo
410 | Script::Sundanese
411 | Script::Sunuwar
412 | Script::Syloti_Nagri
413 | Script::Syriac
414 | Script::Tagalog
415 | Script::Tagbanwa
416 | Script::Tai_Le
417 | Script::Tai_Tham
418 | Script::Tai_Viet
419 | Script::Tai_Yo
420 | Script::Tamil
421 | Script::Tangsa
422 | Script::Takri
423 | Script::Telugu
424 | Script::Thaana
425 | Script::Thai
426 | Script::Tibetan
427 | Script::Tifinagh
428 | Script::Tirhuta
429 | Script::Todhri
430 | Script::Tolong_Siki
431 | Script::Toto
432 | Script::Tulu_Tigalari
433 | Script::Vithkuqi
434 | Script::Wancho
435 | Script::Yezidi
436 | Script::Zanabazar_Square => Some(BoldSlot::Complex),
437 _ => Some(BoldSlot::Regular),
438 }
439}
440
441struct RunFrame {
442 text: String,
443 direct_styles: TextProperties,
444 character_style_id: Option<String>,
445 hidden: bool,
446 direct_child_count: usize,
447 property_review_indices: Vec<usize>,
448}
449
450struct RunPropertiesFrame {
451 run_frame: usize,
452 hidden_eligible: bool,
453}
454
455#[derive(Clone, Copy)]
456struct ContainerReview {
457 paragraph_start: usize,
458 property_review: Option<usize>,
459 grid_review: Option<usize>,
460 exception_review: Option<usize>,
461}
462
463struct TableFrame {
464 ordinal: usize,
465 next_row: usize,
466 style_id: Option<String>,
467 review: ContainerReview,
468}
469
470struct RowFrame {
471 table_ordinal: usize,
472 row: usize,
473 next_column: usize,
474 review: ContainerReview,
475}
476
477#[derive(Clone, Copy)]
478struct CellFrame {
479 table_ordinal: usize,
480 row: usize,
481 column: usize,
482 review: ContainerReview,
483}
484
485struct SdtFrame {
486 placeholder: bool,
487 text_utf8_start: usize,
488 text_utf16_start: u32,
489}
490
491#[derive(Clone, Copy)]
492enum PseudoTextKind {
493 Text { preserve_space: bool },
494 MathText,
495 Instruction,
496}
497
498struct PseudoTextFrame {
499 kind: PseudoTextKind,
500 text: String,
501}
502
503impl PseudoTextFrame {
504 const fn text(preserve_space: bool) -> Self {
505 Self {
506 kind: PseudoTextKind::Text { preserve_space },
507 text: String::new(),
508 }
509 }
510
511 const fn instruction() -> Self {
512 Self {
513 kind: PseudoTextKind::Instruction,
514 text: String::new(),
515 }
516 }
517
518 const fn math_text() -> Self {
519 Self {
520 kind: PseudoTextKind::MathText,
521 text: String::new(),
522 }
523 }
524}
525
526#[derive(Clone, Copy)]
527enum FieldSource {
528 Projected(ReviewPoint),
529 Hidden,
530 Unsupported,
531}
532
533struct FieldFrame {
534 source: FieldSource,
535 instruction: String,
536 separated: bool,
537}
538
539#[derive(Clone, Debug)]
540enum BookmarkPoint {
541 Paragraph(ReviewPoint),
542 ParagraphBoundary(usize),
543}
544
545#[derive(Clone, Debug)]
546struct PendingBookmarkRange {
547 id: u32,
548 name: String,
549 start: BookmarkPoint,
550 end: BookmarkPoint,
551}
552
553fn resolve_bookmark_ranges(
554 paragraphs: &[RawProjectedParagraph],
555 ranges: Vec<PendingBookmarkRange>,
556) -> Option<Vec<RawBookmarkRange>> {
557 ranges
558 .into_iter()
559 .map(|range| {
560 let same_boundary = matches!(
561 (&range.start, &range.end),
562 (
563 BookmarkPoint::ParagraphBoundary(start),
564 BookmarkPoint::ParagraphBoundary(end)
565 ) if start == end
566 );
567 let start = resolve_bookmark_point(paragraphs, &range.start, BoundarySide::Start)?;
568 let end = resolve_bookmark_point(
569 paragraphs,
570 &range.end,
571 if same_boundary {
572 BoundarySide::Start
573 } else {
574 BoundarySide::End
575 },
576 )?;
577 bookmark_points_are_ordered(&start, &end).then_some(RawBookmarkRange {
578 id: range.id,
579 name: range.name,
580 start,
581 end,
582 })
583 })
584 .collect()
585}
586
587#[derive(Clone, Copy)]
588enum BoundarySide {
589 Start,
590 End,
591}
592
593fn resolve_bookmark_point(
594 paragraphs: &[RawProjectedParagraph],
595 point: &BookmarkPoint,
596 side: BoundarySide,
597) -> Option<ReviewPoint> {
598 let boundary = match point {
599 BookmarkPoint::Paragraph(point) => return Some(*point),
600 BookmarkPoint::ParagraphBoundary(boundary) => boundary,
601 };
602 if *boundary > paragraphs.len() || paragraphs.is_empty() {
603 return None;
604 }
605 match side {
606 BoundarySide::Start => paragraphs.get(*boundary).map_or_else(
607 || paragraph_end(paragraphs.last()?),
608 |paragraph| {
609 Some(ReviewPoint {
610 paragraph_ordinal: paragraph.ordinal,
611 utf8: 0,
612 utf16: 0,
613 })
614 },
615 ),
616 BoundarySide::End => boundary.checked_sub(1).map_or_else(
617 || {
618 Some(ReviewPoint {
619 paragraph_ordinal: paragraphs.first()?.ordinal,
620 utf8: 0,
621 utf16: 0,
622 })
623 },
624 |paragraph| paragraph_end(paragraphs.get(paragraph)?),
625 ),
626 }
627}
628
629fn paragraph_end(paragraph: &RawProjectedParagraph) -> Option<ReviewPoint> {
630 Some(ReviewPoint {
631 paragraph_ordinal: paragraph.ordinal,
632 utf8: u32::try_from(paragraph.text.len()).ok()?,
633 utf16: paragraph.utf16_len,
634 })
635}
636
637const fn bookmark_points_are_ordered(start: &ReviewPoint, end: &ReviewPoint) -> bool {
638 start.paragraph_ordinal < end.paragraph_ordinal
639 || (start.paragraph_ordinal == end.paragraph_ordinal
640 && start.utf8 <= end.utf8
641 && start.utf16 <= end.utf16)
642}
643
644#[derive(Clone, Copy)]
645enum OwnedRevisionSpan {
646 Paragraph(ReviewSpan),
647 Content(ReviewSpan),
648}
649
650struct RevisionFrame {
651 review_index: Option<usize>,
652 start: Option<ReviewPoint>,
653 suppressed: bool,
654 text: String,
655}
656
657#[derive(Default)]
658enum ReviewRevisionCollection {
659 #[default]
660 Disabled,
661 Complete {
662 maximum_facts: usize,
663 remaining_detail_bytes: usize,
664 revisions: Vec<AttributedRevision>,
665 },
666 LimitExceeded,
667}
668
669#[derive(Clone, Copy)]
670pub(super) struct ReviewProjectionLimits {
671 pub maximum_facts: usize,
672 pub maximum_detail_bytes: usize,
673}
674
675#[derive(Clone, Copy)]
676enum SnapshotOwner {
677 Run { frame: usize, hidden_eligible: bool },
678 Paragraph,
679 ParagraphMark,
680 Table,
681 Row,
682 Cell,
683 Grid,
684 Section,
685 TableExceptions,
686 Unsupported,
687}
688
689#[derive(Clone, Copy)]
690enum SnapshotStatus {
691 AwaitingRoot,
692 Applied,
693 Unsupported,
694}
695
696struct SnapshotFrame {
697 owner: SnapshotOwner,
698 status: SnapshotStatus,
699}
700
701enum Frame {
702 Other,
703 Body,
704 BlockContent,
705 Table(TableFrame),
706 Row(RowFrame),
707 Cell(CellFrame),
708 Paragraph,
709 Run(RunFrame),
710 RunProperties(RunPropertiesFrame),
711 ParagraphProperties,
712 ParagraphMarkProperties,
713 TableProperties,
714 TableGrid,
715 TableRowProperties,
716 TableCellProperties,
717 TablePropertyExceptions,
718 SectionProperties,
719 NumberingProperties,
720 Hyperlink(Option<RawInternalReference>),
721 Revision(RevisionFrame),
722 ChangeSnapshot(SnapshotFrame),
723 SnapshotIgnored,
724 Math,
725 MathRun,
726 Textbox,
727 Sdt(SdtFrame),
728 PseudoText(PseudoTextFrame),
729}
730
731impl Frame {
732 const fn paragraph_context(&self) -> Option<(ParagraphContainer, Option<ParagraphStructure>)> {
733 match self {
734 Self::Body => Some((ParagraphContainer::Body, None)),
735 Self::Cell(cell) => Some((
736 ParagraphContainer::TableCell,
737 Some(ParagraphStructure {
738 table_ordinal: cell.table_ordinal,
739 row: cell.row,
740 column: cell.column,
741 }),
742 )),
743 Self::Other
744 | Self::BlockContent
745 | Self::Table(_)
746 | Self::Row(_)
747 | Self::Paragraph
748 | Self::Run(_)
749 | Self::RunProperties(_)
750 | Self::ParagraphProperties
751 | Self::ParagraphMarkProperties
752 | Self::TableProperties
753 | Self::TableGrid
754 | Self::TableRowProperties
755 | Self::TableCellProperties
756 | Self::TablePropertyExceptions
757 | Self::SectionProperties
758 | Self::NumberingProperties
759 | Self::Hyperlink(_)
760 | Self::Revision(_)
761 | Self::ChangeSnapshot(_)
762 | Self::SnapshotIgnored
763 | Self::Math
764 | Self::MathRun
765 | Self::Textbox
766 | Self::Sdt(_)
767 | Self::PseudoText(_) => None,
768 }
769 }
770}
771
772#[allow(clippy::too_many_lines)] pub(super) fn project_document_xml(
774 xml: &[u8],
775 maximum_paragraphs: usize,
776 revision_view: RevisionView,
777 text_materialization: TextMaterialization,
778 styles: Result<&StyleSheet, FormattingUnknownReason>,
779 review_limits: Option<ReviewProjectionLimits>,
780) -> Result<RawDocumentProjection, ProjectionError> {
781 let mut reader = NsReader::from_reader(xml);
782 reader.config_mut().check_end_names = true;
783 let mut state = ProjectionState {
784 maximum_paragraphs,
785 revision_view,
786 text_materialization,
787 bookmarks_complete: true,
788 references_complete: true,
789 formatting_completeness: FormattingCompleteness::from_styles(styles.map(|_| ())),
790 review_revisions: review_limits.map_or(ReviewRevisionCollection::Disabled, |limits| {
791 ReviewRevisionCollection::Complete {
792 maximum_facts: limits.maximum_facts,
793 remaining_detail_bytes: limits.maximum_detail_bytes,
794 revisions: Vec::new(),
795 }
796 }),
797 ..ProjectionState::default()
798 };
799 let mut compatibility = MarkupCompatibility::default();
800 loop {
801 match reader
802 .read_event()
803 .map_err(|_| ProjectionError::InvalidDocumentXml)?
804 {
805 Event::Start(element) => {
806 let (namespace, _) = reader.resolver().resolve_element(element.name());
807 let namespace = OoxmlNamespace::from_resolved(&namespace);
808 if compatibility.start(&reader, namespace, &element)?
809 == CompatibilityAction::Process
810 {
811 state.start(&reader, namespace, &element)?;
812 }
813 }
814 Event::Empty(element) => {
815 let (namespace, _) = reader.resolver().resolve_element(element.name());
816 let namespace = OoxmlNamespace::from_resolved(&namespace);
817 if compatibility.empty(&reader, namespace, &element)?
818 == CompatibilityAction::Process
819 {
820 state.start(&reader, namespace, &element)?;
821 state.end(styles)?;
822 }
823 }
824 Event::Text(text) => {
825 if !compatibility.is_suppressed()
826 && matches!(state.frames.last(), Some(Frame::PseudoText(_)))
827 {
828 let decoded = text.xml_content(XmlVersion::Implicit1_0);
829 state.append_pseudo_content(&decoded)?;
830 }
831 }
832 Event::CData(text) => {
833 if !compatibility.is_suppressed()
834 && matches!(state.frames.last(), Some(Frame::PseudoText(_)))
835 {
836 let decoded = text.xml_content(XmlVersion::Implicit1_0);
837 state.append_pseudo_content(&decoded)?;
838 }
839 }
840 Event::GeneralRef(reference) => {
841 if !compatibility.is_suppressed()
842 && matches!(state.frames.last(), Some(Frame::PseudoText(_)))
843 {
844 let resolved_character = reference
845 .resolve_char_ref()
846 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
847 let resolved = if let Some(character) = resolved_character {
848 character.to_string()
849 } else {
850 let name = reference.as_ref();
851 resolve_predefined_entity(name)
852 .ok_or(ProjectionError::InvalidDocumentXml)?
853 .to_owned()
854 };
855 state.append_pseudo_content(&resolved)?;
856 }
857 }
858 Event::End(element) => {
859 let (namespace, local_name) = reader.resolver().resolve_element(element.name());
860 if compatibility.end(
861 OoxmlNamespace::from_resolved(&namespace),
862 local_name.as_ref().as_bytes(),
863 )? == CompatibilityAction::Process
864 {
865 state.end(styles)?;
866 }
867 }
868 Event::Eof => break,
869 _ => {}
870 }
871 }
872 if !state.body_seen {
873 return Err(ProjectionError::MissingDocumentBody);
874 }
875 if state.current_paragraph.is_some() || !state.frames.is_empty() || !compatibility.is_complete()
876 {
877 return Err(ProjectionError::InvalidDocumentXml);
878 }
879 let mut bookmarks = if state.bookmarks_complete && state.open_bookmarks.is_empty() {
880 resolve_bookmark_ranges(&state.paragraphs, state.bookmarks)
881 .ok_or(StructuralFactUnknownReason::IncompleteBookmarkRanges)
882 } else {
883 Err(StructuralFactUnknownReason::IncompleteBookmarkRanges)
884 };
885 let paragraph_origins = if state.paragraph_mark_revisions.is_empty() {
886 None
887 } else {
888 normalize_paragraph_revision_view(
889 &mut state.paragraphs,
890 &mut state.paragraph_mark_revisions,
891 state.revision_view,
892 &mut state.revision_unsupported,
893 )?
894 };
895 if let Some(origins) = paragraph_origins {
896 if let Ok(ranges) = &mut bookmarks {
897 for range in ranges {
898 relocate_block_point(&origins, &mut range.start)?;
899 relocate_block_point(&origins, &mut range.end)?;
900 }
901 }
902 for reference in &mut state.references {
903 relocate_block_point(&origins, &mut reference.source)?;
904 }
905 state.review_comment_anchors.retain(|_, anchor| {
906 relocate_review_span(&origins, *anchor).is_some_and(|relocated| {
907 *anchor = relocated;
908 true
909 })
910 });
911 if let ReviewRevisionCollection::Complete { revisions, .. } = &mut state.review_revisions {
912 for revision in revisions {
913 let ReviewDetail::Known(content) = &mut revision.content else {
914 continue;
915 };
916 match relocate_review_span(&origins, content.span) {
917 Some(span) => content.span = span,
918 None => {
919 revision.content =
920 ReviewDetail::Unknown(ReviewFactUnknownReason::UnsupportedLocation);
921 }
922 }
923 }
924 }
925 }
926 let references = if state.references_complete && state.fields.is_empty() {
927 Ok(state.references)
928 } else {
929 Err(StructuralFactUnknownReason::UnsupportedInternalReferences)
930 };
931 Ok(RawDocumentProjection {
932 paragraphs: state.paragraphs,
933 bookmarks,
934 references,
935 formatting_completeness: state.formatting_completeness,
936 revision_status: if state.revision_unsupported.is_empty() {
937 RevisionProjectionStatus::Complete
938 } else {
939 RevisionProjectionStatus::Incomplete(state.revision_unsupported.into_iter().collect())
940 },
941 review_revisions: match state.review_revisions {
942 ReviewRevisionCollection::Disabled => None,
943 ReviewRevisionCollection::Complete { revisions, .. } => {
944 Some(ReviewFactSet::Known(revisions))
945 }
946 ReviewRevisionCollection::LimitExceeded => Some(ReviewFactSet::Unknown(
947 ReviewFactUnknownReason::ResourceLimit,
948 )),
949 },
950 review_comment_anchors: state.review_comment_anchors,
951 })
952}
953
954#[derive(Default)]
955struct BookmarkQueue {
956 starts: Vec<Option<(String, BookmarkPoint)>>,
957 cursor: usize,
958}
959
960#[derive(Default)]
961struct ProjectionState {
962 frames: Vec<Frame>,
963 paragraphs: Vec<RawProjectedParagraph>,
964 current_paragraph: Option<ParagraphBuilder>,
965 body_seen: bool,
966 next_table_ordinal: usize,
967 maximum_paragraphs: usize,
968 open_bookmarks: HashMap<u32, BookmarkQueue>,
969 bookmarks: Vec<PendingBookmarkRange>,
970 references: Vec<RawInternalReference>,
971 bookmarks_complete: bool,
972 references_complete: bool,
973 formatting_completeness: FormattingCompleteness,
974 fields: Vec<FieldFrame>,
975 paragraph_mark_revisions: HashMap<usize, ParagraphMarkRevision>,
976 revision_view: RevisionView,
977 text_materialization: TextMaterialization,
978 revision_unsupported: BTreeSet<RevisionUnsupportedReason>,
979 review_revisions: ReviewRevisionCollection,
980 open_review_comment_anchors: HashMap<String, ReviewPoint>,
981 review_comment_anchors: HashMap<String, ReviewSpan>,
982 invalid_review_comment_anchors: HashSet<String>,
983}
984
985impl ProjectionState {
986 #[allow(clippy::too_many_lines)] fn start(
988 &mut self,
989 reader: &NsReader<&[u8]>,
990 namespace: OoxmlNamespace,
991 element: &BytesStart<'_>,
992 ) -> Result<(), ProjectionError> {
993 let local_name = element.local_name();
994 let name = if namespace == OoxmlNamespace::Wordprocessing {
995 local_name.as_ref().as_bytes()
996 } else {
997 b""
998 };
999 let direct_run_child = if let Some(Frame::Run(run)) = self.frames.last_mut() {
1000 let child = run.direct_child_count;
1001 run.direct_child_count = run
1002 .direct_child_count
1003 .checked_add(1)
1004 .ok_or(ProjectionError::InvalidDocumentXml)?;
1005 let frame_index = self
1006 .frames
1007 .len()
1008 .checked_sub(1)
1009 .ok_or(ProjectionError::InvalidDocumentXml)?;
1010 Some((frame_index, child))
1011 } else {
1012 None
1013 };
1014
1015 if name == b"body" {
1016 if self.body_seen {
1017 return Err(ProjectionError::InvalidDocumentXml);
1018 }
1019 self.body_seen = true;
1020 self.frames.push(Frame::Body);
1021 return Ok(());
1022 }
1023 if !self.inside_body() {
1024 self.frames.push(Frame::Other);
1025 return Ok(());
1026 }
1027 let inside_snapshot = self.inside_change_snapshot();
1028 if inside_snapshot && self.start_snapshot_child(name)? {
1029 return Ok(());
1030 }
1031 let attributed_revision = if inside_snapshot {
1032 None
1033 } else {
1034 self.record_attributed_revision(reader, element, name)?
1035 };
1036 if name == b"txbxContent" {
1037 self.frames.push(Frame::Textbox);
1038 return Ok(());
1039 }
1040 if self.inside_textbox() {
1041 if matches!(
1042 name,
1043 b"commentRangeStart" | b"commentRangeEnd" | b"commentReference"
1044 ) {
1045 self.suppress_review_comment_anchor(reader, element)?;
1046 }
1047 self.frames.push(Frame::Other);
1048 return Ok(());
1049 }
1050 match name {
1051 b"commentRangeStart" => self.start_review_comment_anchor(reader, element)?,
1052 b"commentRangeEnd" => self.end_review_comment_anchor(reader, element)?,
1053 b"commentReference" => self.record_review_comment_reference(reader, element)?,
1054 _ => {}
1055 }
1056 if is_change_snapshot(name) {
1057 if let Some(review_index) = attributed_revision {
1058 match (name, self.frames.last()) {
1059 (b"rPrChange", Some(Frame::RunProperties(properties))) => {
1060 let run_frame = properties.run_frame;
1061 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1062 run.property_review_indices.push(review_index);
1063 } else if self.inside_paragraph_mark_properties()
1064 && let Some(paragraph) = self.current_paragraph.as_mut()
1065 {
1066 paragraph.paragraph_review_indices.push(review_index);
1067 }
1068 }
1069 (b"pPrChange", Some(Frame::ParagraphProperties)) => {
1070 if let Some(paragraph) = self.current_paragraph.as_mut() {
1071 paragraph.paragraph_review_indices.push(review_index);
1072 }
1073 }
1074 _ => self.record_container_property_revision(name, review_index)?,
1075 }
1076 }
1077 let owner = self.snapshot_owner(name);
1078 self.frames.push(Frame::ChangeSnapshot(SnapshotFrame {
1079 owner,
1080 status: SnapshotStatus::AwaitingRoot,
1081 }));
1082 return Ok(());
1083 }
1084 if is_unsupported_revision_markup(name) {
1085 self.revision_unsupported
1086 .insert(RevisionUnsupportedReason::UnsupportedRevisionMarkup);
1087 self.frames.push(Frame::Other);
1088 return Ok(());
1089 }
1090 if namespace == OoxmlNamespace::OfficeMath {
1091 let frame = match local_name.as_ref().as_bytes() {
1092 b"oMath" | b"oMathPara" if self.current_paragraph.is_some() => {
1093 self.formatting_completeness.mark_formatting_unread();
1096 Frame::Math
1097 }
1098 b"r" if self.frames.iter().any(|frame| matches!(frame, Frame::Math)) => {
1099 Frame::MathRun
1100 }
1101 b"t" if matches!(self.frames.last(), Some(Frame::MathRun)) => {
1102 Frame::PseudoText(PseudoTextFrame::math_text())
1103 }
1104 _ => Frame::Other,
1105 };
1106 self.frames.push(frame);
1107 return Ok(());
1108 }
1109
1110 let frame = match name {
1111 b"tbl" => {
1112 let ordinal = self.next_table_ordinal;
1113 self.next_table_ordinal = self
1114 .next_table_ordinal
1115 .checked_add(1)
1116 .ok_or(ProjectionError::InvalidDocumentXml)?;
1117 Frame::Table(TableFrame {
1118 ordinal,
1119 next_row: 0,
1120 style_id: None,
1121 review: ContainerReview {
1122 paragraph_start: self.paragraphs.len(),
1123 property_review: None,
1124 grid_review: None,
1125 exception_review: None,
1126 },
1127 })
1128 }
1129 b"tr" => {
1130 let Some(table) = enclosing_table(&mut self.frames) else {
1131 self.frames.push(Frame::Other);
1132 return Ok(());
1133 };
1134 let row = table.next_row;
1135 table.next_row = table
1136 .next_row
1137 .checked_add(1)
1138 .ok_or(ProjectionError::InvalidDocumentXml)?;
1139 Frame::Row(RowFrame {
1140 table_ordinal: table.ordinal,
1141 row,
1142 next_column: 0,
1143 review: ContainerReview {
1144 paragraph_start: self.paragraphs.len(),
1145 property_review: None,
1146 grid_review: None,
1147 exception_review: None,
1148 },
1149 })
1150 }
1151 b"tc" => {
1152 let Some(row) = enclosing_row(&mut self.frames) else {
1153 self.frames.push(Frame::Other);
1154 return Ok(());
1155 };
1156 let column = row.next_column;
1157 row.next_column = row
1158 .next_column
1159 .checked_add(1)
1160 .ok_or(ProjectionError::InvalidDocumentXml)?;
1161 Frame::Cell(CellFrame {
1162 table_ordinal: row.table_ordinal,
1163 row: row.row,
1164 column,
1165 review: ContainerReview {
1166 paragraph_start: self.paragraphs.len(),
1167 property_review: None,
1168 grid_review: None,
1169 exception_review: None,
1170 },
1171 })
1172 }
1173 b"p" => {
1174 if self.current_paragraph.is_some() {
1175 self.frames.push(Frame::Other);
1176 return Ok(());
1177 }
1178 if self.paragraphs.len() >= self.maximum_paragraphs {
1179 return Err(ProjectionError::TooManyParagraphs);
1180 }
1181 let package_paragraph_id = attribute_2010(reader, element, b"paraId")?
1182 .as_deref()
1183 .and_then(PackageParagraphId::parse);
1184 let (container, structure) = self
1185 .frames
1186 .iter()
1187 .rev()
1188 .find_map(Frame::paragraph_context)
1189 .ok_or(ProjectionError::InvalidDocumentXml)?;
1190 self.current_paragraph = Some(ParagraphBuilder {
1191 package_paragraph_id,
1192 text: String::new(),
1193 utf16_len: 0,
1194 formatting: Vec::new(),
1195 structure,
1196 container,
1197 properties: ParagraphProperties {
1198 table_style: self
1199 .frames
1200 .iter()
1201 .rev()
1202 .find_map(|frame| {
1203 if let Frame::Table(table) = frame {
1204 Some(table.style_id.clone().map_or(
1205 TableStyleSelection::Default,
1206 TableStyleSelection::Explicit,
1207 ))
1208 } else {
1209 None
1210 }
1211 })
1212 .unwrap_or_default(),
1213 ..ParagraphProperties::default()
1214 },
1215 resolved_text_base: None,
1216 paragraph_mark_revision: None,
1217 paragraph_review_indices: Vec::new(),
1218 });
1219 Frame::Paragraph
1220 }
1221 b"r" if self.current_paragraph.is_some() => Frame::Run(RunFrame {
1222 text: String::new(),
1223 direct_styles: TextProperties::default(),
1224 character_style_id: None,
1225 hidden: false,
1226 direct_child_count: 0,
1227 property_review_indices: Vec::new(),
1228 }),
1229 b"rPr" => {
1230 let Some((run_frame, child_index)) = direct_run_child else {
1231 self.frames.push(Frame::RunProperties(RunPropertiesFrame {
1232 run_frame: usize::MAX,
1233 hidden_eligible: false,
1234 }));
1235 return Ok(());
1236 };
1237 Frame::RunProperties(RunPropertiesFrame {
1238 run_frame,
1239 hidden_eligible: child_index == 0,
1240 })
1241 }
1242 b"sdtContent" | b"customXml" => Frame::BlockContent,
1243 b"pPr" => Frame::ParagraphProperties,
1244 b"tblStyle" if matches!(self.frames.last(), Some(Frame::TableProperties)) => {
1245 if let Some(table) = enclosing_table(&mut self.frames) {
1246 table.style_id = word_style_id(attribute(reader, element, b"val")?);
1247 }
1248 Frame::Other
1249 }
1250 b"tblPr" => Frame::TableProperties,
1251 b"tblGrid" => Frame::TableGrid,
1252 b"trPr" => Frame::TableRowProperties,
1253 b"tcPr" => Frame::TableCellProperties,
1254 b"tblPrEx" => Frame::TablePropertyExceptions,
1255 b"sectPr" => Frame::SectionProperties,
1256 b"pStyle" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1257 if let Some(paragraph) = self.current_paragraph.as_mut() {
1258 paragraph.properties.style_id =
1259 word_style_id(attribute(reader, element, b"val")?);
1260 paragraph.resolved_text_base = None;
1261 }
1262 Frame::Other
1263 }
1264 b"ind" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1265 if let Some(paragraph) = self.current_paragraph.as_mut() {
1266 paragraph.properties.indentation = parse_indentation(reader, element)?;
1267 }
1268 Frame::Other
1269 }
1270 b"jc" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1274 if let Some(paragraph) = self.current_paragraph.as_mut() {
1275 paragraph.properties.alignment = Some(parse_alignment(reader, element)?);
1276 }
1277 Frame::Other
1278 }
1279 b"outlineLvl" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1280 if let Some(paragraph) = self.current_paragraph.as_mut() {
1281 paragraph.properties.outline_level =
1282 Some(parse_outline_level_attribute(reader, element)?);
1283 }
1284 Frame::Other
1285 }
1286 b"numPr" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1287 if let Some(paragraph) = self.current_paragraph.as_mut() {
1288 paragraph.properties.numbering.present = true;
1289 }
1290 Frame::NumberingProperties
1291 }
1292 b"numId" if matches!(self.frames.last(), Some(Frame::NumberingProperties)) => {
1293 if let Some(paragraph) = self.current_paragraph.as_mut() {
1294 paragraph.properties.numbering.num_id =
1295 Some(parse_u32_attribute(reader, element)?);
1296 }
1297 Frame::Other
1298 }
1299 b"ilvl" if matches!(self.frames.last(), Some(Frame::NumberingProperties)) => {
1300 if let Some(paragraph) = self.current_paragraph.as_mut() {
1301 paragraph.properties.numbering.level =
1302 Some(parse_level_attribute(reader, element)?);
1303 }
1304 Frame::Other
1305 }
1306 b"bookmarkStart" => {
1307 self.start_bookmark(reader, element)?;
1308 Frame::Other
1309 }
1310 b"bookmarkEnd" => {
1311 self.end_bookmark(reader, element)?;
1312 Frame::Other
1313 }
1314 b"hyperlink" => {
1315 if self.pseudo_text_is_suppressed() {
1316 self.frames.push(Frame::Hyperlink(None));
1317 return Ok(());
1318 }
1319 let anchor = attribute(reader, element, b"anchor")?;
1320 let reference =
1321 anchor
1322 .clone()
1323 .filter(|anchor| !anchor.is_empty())
1324 .and_then(|anchor| {
1325 self.current_point().map(|source| RawInternalReference {
1326 reference_id: anchor,
1327 source,
1328 })
1329 });
1330 if anchor.is_some() && reference.is_none() {
1331 self.references_complete = false;
1332 }
1333 Frame::Hyperlink(reference)
1334 }
1335 b"fldSimple" => {
1336 self.simple_field(reader, element)?;
1337 Frame::Other
1338 }
1339 b"fldChar" => {
1340 self.field_character(reader, element)?;
1341 Frame::Other
1342 }
1343 b"instrText" | b"delInstrText" => Frame::PseudoText(PseudoTextFrame::instruction()),
1344 b"ins" | b"del" | b"moveFrom" | b"moveTo" => {
1345 if self.inside_table_row_properties() {
1346 self.revision_unsupported
1347 .insert(RevisionUnsupportedReason::StructuralTableRevision);
1348 Frame::Other
1349 } else if self.inside_paragraph_mark_properties() {
1350 if let Some(paragraph) = self.current_paragraph.as_mut() {
1351 paragraph.paragraph_mark_revision =
1352 ParagraphMarkRevision::from_element(name)
1353 .or(paragraph.paragraph_mark_revision);
1354 paragraph
1355 .paragraph_review_indices
1356 .extend(attributed_revision);
1357 }
1358 Frame::Other
1359 } else {
1360 Frame::Revision(RevisionFrame {
1361 review_index: attributed_revision,
1362 start: self.current_review_point(),
1363 suppressed: revision_is_suppressed(name, self.revision_view),
1364 text: String::new(),
1365 })
1366 }
1367 }
1368 b"cellIns" | b"cellDel" | b"cellMerge" => {
1369 self.revision_unsupported
1370 .insert(RevisionUnsupportedReason::StructuralTableRevision);
1371 Frame::Other
1372 }
1373 b"sdt" => {
1374 let (text_utf8_start, text_utf16_start) =
1375 self.current_paragraph.as_ref().map_or((0, 0), |paragraph| {
1376 (paragraph.text.len(), paragraph.utf16_len)
1377 });
1378 Frame::Sdt(SdtFrame {
1379 placeholder: false,
1380 text_utf8_start,
1381 text_utf16_start,
1382 })
1383 }
1384 b"showingPlcHdr" => {
1385 if on_off_element_enabled(reader, element)? {
1386 self.mark_placeholder();
1387 }
1388 Frame::Other
1389 }
1390 b"vanish" => {
1391 if on_off_element_enabled(reader, element)? {
1392 self.mark_hidden_run();
1393 }
1394 Frame::Other
1395 }
1396 b"rStyle" => {
1397 let style_id = word_style_id(attribute(reader, element, b"val")?);
1398 if matches!(style_id.as_deref(), Some("HideTWBInt" | "HideTWBExt")) {
1399 self.mark_hidden_run();
1400 }
1401 self.set_run_character_style(style_id);
1402 Frame::Other
1403 }
1404 b"b" => {
1405 self.mark_bold_run(on_off_element_enabled(reader, element)?);
1406 Frame::Other
1407 }
1408 b"bCs" => {
1409 self.mark_complex_script_bold_run(on_off_element_enabled(reader, element)?);
1410 Frame::Other
1411 }
1412 b"cs" => {
1413 self.mark_complex_script_run(on_off_element_enabled(reader, element)?);
1414 Frame::Other
1415 }
1416 b"rtl" => {
1417 self.mark_right_to_left_run(on_off_element_enabled(reader, element)?);
1418 Frame::Other
1419 }
1420 b"highlight" => {
1421 if let Some(highlighted) = attribute(reader, element, b"val")?
1422 .as_deref()
1423 .and_then(semantic_highlight_value)
1424 {
1425 self.mark_highlighted_run(highlighted);
1426 }
1427 Frame::Other
1428 }
1429 b"vertAlign" => {
1430 match attribute(reader, element, b"val")?.as_deref() {
1431 Some("superscript") => self.mark_superscript_run(true),
1432 Some("baseline" | "subscript") => self.mark_superscript_run(false),
1433 _ => {}
1434 }
1435 Frame::Other
1436 }
1437 b"t" | b"delText" => {
1438 Frame::PseudoText(PseudoTextFrame::text(preserves_xml_space(reader, element)?))
1439 }
1440 b"tab" | b"ptab" => {
1441 self.append_text_control(TextControl::Tab)?;
1442 Frame::Other
1443 }
1444 b"br" => {
1445 let control = match attribute(reader, element, b"type")?.as_deref() {
1446 Some("page") => TextControl::PageBreak,
1447 Some("column") => TextControl::ColumnBreak,
1448 _ => TextControl::LineBreak,
1449 };
1450 self.append_text_control(control)?;
1451 Frame::Other
1452 }
1453 b"cr" => {
1454 self.append_text_control(TextControl::CarriageReturn)?;
1455 Frame::Other
1456 }
1457 b"footnoteReference" | b"endnoteReference" => {
1458 let visible =
1459 attribute(reader, element, b"customMarkFollows")?.is_none_or(|value| {
1460 matches!(value.to_ascii_lowercase().as_str(), "0" | "false" | "off")
1461 });
1462 if visible {
1463 self.append_text_control(TextControl::NoteReference)?;
1464 }
1465 Frame::Other
1466 }
1467 b"softHyphen" => {
1468 self.append_text_control(TextControl::SoftHyphen)?;
1469 Frame::Other
1470 }
1471 b"noBreakHyphen" => {
1472 self.append_text_control(TextControl::NoBreakHyphen)?;
1473 Frame::Other
1474 }
1475 b"sym" => {
1476 if let Some(value) = attribute(reader, element, b"char")? {
1477 let font = attribute(reader, element, b"font")?;
1478 self.append_pseudo_text(&symbol_text(&value, font.as_deref()))?;
1479 }
1480 Frame::Other
1481 }
1482 _ => Frame::Other,
1483 };
1484 self.frames.push(frame);
1485 Ok(())
1486 }
1487
1488 fn end(
1489 &mut self,
1490 styles: Result<&StyleSheet, FormattingUnknownReason>,
1491 ) -> Result<(), ProjectionError> {
1492 let frame = self
1493 .frames
1494 .pop()
1495 .ok_or(ProjectionError::InvalidDocumentXml)?;
1496 match frame {
1497 Frame::Paragraph => self.finish_paragraph(styles)?,
1498 Frame::Table(table) => self.finish_container_review(table.review)?,
1499 Frame::Row(row) => self.finish_container_review(row.review)?,
1500 Frame::Cell(cell) => self.finish_container_review(cell.review)?,
1501 Frame::Hyperlink(Some(reference)) => self.references.push(reference),
1502 Frame::PseudoText(frame) => match frame.kind {
1503 PseudoTextKind::Text { preserve_space } => {
1504 let text = if preserve_space {
1505 frame.text.as_str()
1506 } else {
1507 trim_xml_whitespace(&frame.text)
1508 };
1509 self.append_pseudo_text(text)?;
1510 }
1511 PseudoTextKind::MathText => self.append_pseudo_text(&frame.text)?,
1512 PseudoTextKind::Instruction => self.append_field_instruction(&frame.text),
1513 },
1514 Frame::Run(run) => {
1515 let start = self.current_review_point();
1516 if !run.hidden
1517 && !self.pseudo_text_is_suppressed()
1518 && let Some(paragraph) = self.current_paragraph.as_mut()
1519 {
1520 let mut effective = match styles {
1521 Ok(styles) => {
1522 let resolved = if run.character_style_id.is_none()
1523 && run.direct_styles == TextProperties::default()
1524 {
1525 paragraph.resolve_text_base(styles)
1526 } else {
1527 styles.resolve_text(TextStyleInput {
1528 paragraph: ¶graph.properties,
1529 character_style_id: run.character_style_id.as_deref(),
1530 direct: run.direct_styles,
1531 })
1532 };
1533 resolved.unwrap_or_else(|()| {
1534 self.formatting_completeness.mark_styles_unread();
1535 run.direct_styles
1536 })
1537 }
1538 Err(_) => run.direct_styles,
1539 };
1540 effective.highlighted = run.direct_styles.highlighted;
1542 paragraph.append(&run.text, effective)?;
1543 }
1544 if let (Some(start), Some(end)) = (start, self.current_review_point()) {
1545 self.locate_owned_revisions(
1546 &run.property_review_indices,
1547 OwnedRevisionSpan::Content(ReviewSpan { start, end }),
1548 )?;
1549 }
1550 }
1551 Frame::ChangeSnapshot(snapshot) => {
1552 if self.revision_view == RevisionView::Original
1553 && !matches!(snapshot.status, SnapshotStatus::Applied)
1554 {
1555 self.revision_unsupported
1556 .insert(RevisionUnsupportedReason::UnsupportedRevisionMarkup);
1557 }
1558 }
1559 Frame::Revision(revision) => self.finish_attributed_revision(revision)?,
1560 _ => {}
1561 }
1562 Ok(())
1563 }
1564
1565 fn snapshot_owner(&self, name: &[u8]) -> SnapshotOwner {
1566 match (name, self.frames.last()) {
1567 (b"rPrChange", Some(Frame::RunProperties(properties))) => {
1568 if matches!(self.frames.get(properties.run_frame), Some(Frame::Run(_))) {
1569 SnapshotOwner::Run {
1570 frame: properties.run_frame,
1571 hidden_eligible: properties.hidden_eligible,
1572 }
1573 } else if self.inside_paragraph_mark_properties() {
1574 SnapshotOwner::ParagraphMark
1575 } else {
1576 SnapshotOwner::Unsupported
1577 }
1578 }
1579 (b"pPrChange", Some(Frame::ParagraphProperties))
1580 if self.current_paragraph.is_some() =>
1581 {
1582 SnapshotOwner::Paragraph
1583 }
1584 (b"tblPrChange", Some(Frame::TableProperties))
1585 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Table(_))) =>
1586 {
1587 SnapshotOwner::Table
1588 }
1589 (b"trPrChange", Some(Frame::TableRowProperties))
1590 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Row(_))) =>
1591 {
1592 SnapshotOwner::Row
1593 }
1594 (b"tcPrChange", Some(Frame::TableCellProperties))
1595 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Cell(_))) =>
1596 {
1597 SnapshotOwner::Cell
1598 }
1599 (b"tblGridChange", Some(Frame::TableGrid))
1600 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Table(_))) =>
1601 {
1602 SnapshotOwner::Grid
1603 }
1604 (b"sectPrChange", Some(Frame::SectionProperties))
1605 if matches!(
1606 self.frames.iter().rev().nth(1),
1607 Some(Frame::Body | Frame::ParagraphProperties)
1608 ) =>
1609 {
1610 SnapshotOwner::Section
1611 }
1612 (b"tblPrExChange", Some(Frame::TablePropertyExceptions))
1613 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Row(_))) =>
1614 {
1615 SnapshotOwner::TableExceptions
1616 }
1617 _ => SnapshotOwner::Unsupported,
1618 }
1619 }
1620
1621 fn start_snapshot_child(&mut self, name: &[u8]) -> Result<bool, ProjectionError> {
1622 if self.revision_view == RevisionView::Current {
1623 self.frames.push(Frame::SnapshotIgnored);
1624 return Ok(true);
1625 }
1626 if let Some(Frame::ChangeSnapshot(snapshot)) = self.frames.last_mut() {
1627 let owner = snapshot.owner;
1628 let matches_root = matches!(
1629 (owner, name),
1630 (
1631 SnapshotOwner::Run { .. } | SnapshotOwner::ParagraphMark,
1632 b"rPr"
1633 ) | (SnapshotOwner::Paragraph, b"pPr")
1634 | (SnapshotOwner::Table, b"tblPr")
1635 | (SnapshotOwner::Row, b"trPr")
1636 | (SnapshotOwner::Cell, b"tcPr")
1637 | (SnapshotOwner::Grid, b"tblGrid")
1638 | (SnapshotOwner::Section, b"sectPr")
1639 | (SnapshotOwner::TableExceptions, b"tblPrEx")
1640 );
1641 if !matches_root || !matches!(snapshot.status, SnapshotStatus::AwaitingRoot) {
1642 snapshot.status = SnapshotStatus::Unsupported;
1643 self.frames.push(Frame::SnapshotIgnored);
1644 return Ok(true);
1645 }
1646 snapshot.status = SnapshotStatus::Applied;
1647 let frame = self.restore_snapshot_owner(owner)?;
1648 self.frames.push(frame);
1649 return Ok(true);
1650 }
1651 if matches!(self.frames.last(), Some(Frame::ParagraphMarkProperties))
1652 && matches!(name, b"moveFrom" | b"moveTo")
1653 {
1654 self.revision_unsupported
1655 .insert(RevisionUnsupportedReason::UnsupportedRevisionMarkup);
1656 self.frames.push(Frame::SnapshotIgnored);
1657 return Ok(true);
1658 }
1659 let supported_property = matches!(
1660 (self.frames.last(), name),
1661 (
1662 Some(Frame::RunProperties(_)),
1663 b"rStyle"
1664 | b"b"
1665 | b"bCs"
1666 | b"cs"
1667 | b"rtl"
1668 | b"highlight"
1669 | b"vertAlign"
1670 | b"vanish"
1671 ) | (
1672 Some(Frame::ParagraphProperties),
1673 b"pStyle" | b"ind" | b"jc" | b"outlineLvl" | b"numPr"
1674 ) | (Some(Frame::NumberingProperties), b"numId" | b"ilvl")
1675 | (Some(Frame::TableProperties), b"tblStyle")
1676 | (Some(Frame::ParagraphMarkProperties), b"ins" | b"del")
1677 );
1678 if supported_property {
1679 return Ok(false);
1680 }
1681 self.frames.push(Frame::SnapshotIgnored);
1682 Ok(true)
1683 }
1684
1685 fn restore_snapshot_owner(&mut self, owner: SnapshotOwner) -> Result<Frame, ProjectionError> {
1686 match owner {
1687 SnapshotOwner::Run {
1688 frame,
1689 hidden_eligible,
1690 } => {
1691 let Some(Frame::Run(run)) = self.frames.get_mut(frame) else {
1692 return Err(ProjectionError::InvalidDocumentXml);
1693 };
1694 run.direct_styles = TextProperties::default();
1695 run.character_style_id = None;
1696 run.hidden = false;
1697 Ok(Frame::RunProperties(RunPropertiesFrame {
1698 run_frame: frame,
1699 hidden_eligible,
1700 }))
1701 }
1702 SnapshotOwner::Paragraph => {
1703 let paragraph = self
1705 .current_paragraph
1706 .as_mut()
1707 .ok_or(ProjectionError::InvalidDocumentXml)?;
1708 paragraph.properties = ParagraphProperties {
1709 table_style: std::mem::take(&mut paragraph.properties.table_style),
1710 ..ParagraphProperties::default()
1711 };
1712 paragraph.resolved_text_base = None;
1713 Ok(Frame::ParagraphProperties)
1714 }
1715 SnapshotOwner::Table => {
1716 let table =
1717 enclosing_table(&mut self.frames).ok_or(ProjectionError::InvalidDocumentXml)?;
1718 table.style_id = None;
1719 Ok(Frame::TableProperties)
1720 }
1721 SnapshotOwner::Row => Ok(Frame::TableRowProperties),
1722 SnapshotOwner::Cell => Ok(Frame::TableCellProperties),
1723 SnapshotOwner::Grid => Ok(Frame::TableGrid),
1724 SnapshotOwner::Section => Ok(Frame::SectionProperties),
1725 SnapshotOwner::TableExceptions => Ok(Frame::TablePropertyExceptions),
1726 SnapshotOwner::ParagraphMark => {
1727 let paragraph = self
1728 .current_paragraph
1729 .as_mut()
1730 .ok_or(ProjectionError::InvalidDocumentXml)?;
1731 paragraph.paragraph_mark_revision = None;
1732 Ok(Frame::ParagraphMarkProperties)
1733 }
1734 SnapshotOwner::Unsupported => Err(ProjectionError::InvalidDocumentXml),
1735 }
1736 }
1737
1738 fn record_container_property_revision(
1739 &mut self,
1740 name: &[u8],
1741 review_index: usize,
1742 ) -> Result<(), ProjectionError> {
1743 let valid_parent = matches!(
1744 (name, self.frames.last()),
1745 (b"tblPrChange", Some(Frame::TableProperties))
1746 | (b"tblGridChange", Some(Frame::TableGrid))
1747 | (b"trPrChange", Some(Frame::TableRowProperties))
1748 | (b"tcPrChange", Some(Frame::TableCellProperties))
1749 | (b"tblPrExChange", Some(Frame::TablePropertyExceptions))
1750 );
1751 if !valid_parent {
1752 return Ok(());
1753 }
1754 let review = self
1755 .frames
1756 .iter_mut()
1757 .rev()
1758 .find_map(|frame| match (name, frame) {
1759 (b"tblPrChange" | b"tblGridChange", Frame::Table(table)) => Some(&mut table.review),
1760 (b"trPrChange" | b"tblPrExChange", Frame::Row(row)) => Some(&mut row.review),
1761 (b"tcPrChange", Frame::Cell(cell)) => Some(&mut cell.review),
1762 _ => None,
1763 });
1764 if let Some(review) = review {
1765 let slot = match name {
1766 b"tblGridChange" => &mut review.grid_review,
1767 b"tblPrExChange" => &mut review.exception_review,
1768 _ => &mut review.property_review,
1769 };
1770 if slot.replace(review_index).is_some() {
1771 return Err(ProjectionError::InvalidDocumentXml);
1772 }
1773 }
1774 Ok(())
1775 }
1776
1777 fn finish_container_review(&mut self, review: ContainerReview) -> Result<(), ProjectionError> {
1778 if review.paragraph_start >= self.paragraphs.len() {
1779 return Ok(());
1780 }
1781 let end = self
1782 .paragraphs
1783 .last()
1784 .and_then(paragraph_end)
1785 .ok_or(ProjectionError::InvalidDocumentXml)?;
1786 for review_index in [
1787 review.property_review,
1788 review.grid_review,
1789 review.exception_review,
1790 ]
1791 .into_iter()
1792 .flatten()
1793 {
1794 self.locate_owned_revisions(
1795 std::slice::from_ref(&review_index),
1796 OwnedRevisionSpan::Content(ReviewSpan {
1797 start: paragraph_start(review.paragraph_start),
1798 end,
1799 }),
1800 )?;
1801 }
1802 Ok(())
1803 }
1804
1805 fn finish_paragraph(
1806 &mut self,
1807 styles: Result<&StyleSheet, FormattingUnknownReason>,
1808 ) -> Result<(), ProjectionError> {
1809 let mut paragraph = self
1810 .current_paragraph
1811 .take()
1812 .ok_or(ProjectionError::InvalidDocumentXml)?;
1813 if styles.is_ok_and(|styles| paragraph.resolve_text_base(styles).is_err()) {
1814 self.formatting_completeness.mark_styles_unread();
1815 }
1816 let ordinal = self.paragraphs.len();
1817 if let Some(revision) = paragraph.paragraph_mark_revision {
1818 self.paragraph_mark_revisions.insert(ordinal, revision);
1819 }
1820 let mark = ReviewPoint {
1821 paragraph_ordinal: ordinal,
1822 utf8: u32::try_from(paragraph.text.len())
1823 .map_err(|_| ProjectionError::InvalidDocumentXml)?,
1824 utf16: paragraph.utf16_len,
1825 };
1826 self.locate_owned_revisions(
1827 ¶graph.paragraph_review_indices,
1828 OwnedRevisionSpan::Paragraph(ReviewSpan {
1829 start: paragraph_start(ordinal),
1830 end: mark,
1831 }),
1832 )?;
1833 self.paragraphs.push(RawProjectedParagraph {
1834 ordinal: self.paragraphs.len(),
1835 package_paragraph_id: paragraph.package_paragraph_id,
1836 text: paragraph.text,
1837 utf16_len: paragraph.utf16_len,
1838 formatting: paragraph.formatting,
1839 structure: paragraph.structure,
1840 container: paragraph.container,
1841 properties: paragraph.properties,
1842 });
1843 Ok(())
1844 }
1845
1846 fn record_attributed_revision(
1847 &mut self,
1848 reader: &NsReader<&[u8]>,
1849 element: &BytesStart<'_>,
1850 name: &[u8],
1851 ) -> Result<Option<usize>, ProjectionError> {
1852 let Some(kind) = revision_fact_kind(name) else {
1853 return Ok(None);
1854 };
1855 let ReviewRevisionCollection::Complete {
1856 maximum_facts,
1857 revisions,
1858 ..
1859 } = &mut self.review_revisions
1860 else {
1861 return Ok(None);
1862 };
1863 if revisions.len() >= *maximum_facts {
1864 self.review_revisions = ReviewRevisionCollection::LimitExceeded;
1865 return Ok(None);
1866 }
1867 let review_index = revisions.len();
1868 revisions.push(AttributedRevision {
1869 kind,
1870 author: attribute(reader, element, b"author")?.unwrap_or_default(),
1871 date: attribute(reader, element, b"date")?,
1872 revision_id: attribute(reader, element, b"id")?,
1873 content: ReviewDetail::Unknown(ReviewFactUnknownReason::UnsupportedLocation),
1874 });
1875 Ok(Some(review_index))
1876 }
1877
1878 fn finish_attributed_revision(
1879 &mut self,
1880 revision: RevisionFrame,
1881 ) -> Result<(), ProjectionError> {
1882 let (Some(review_index), Some(start), Some(end)) = (
1883 revision.review_index,
1884 revision.start,
1885 self.current_review_point(),
1886 ) else {
1887 return Ok(());
1888 };
1889 let ReviewRevisionCollection::Complete { revisions, .. } = &mut self.review_revisions
1890 else {
1891 return Ok(());
1892 };
1893 let attributed = revisions
1894 .get_mut(review_index)
1895 .ok_or(ProjectionError::InvalidDocumentXml)?;
1896 attributed.content = ReviewDetail::Known(RevisionContent {
1897 span: ReviewSpan { start, end },
1898 payload: RevisionPayload::from_text(revision.text),
1899 });
1900 Ok(())
1901 }
1902
1903 fn locate_owned_revisions(
1904 &mut self,
1905 review_indices: &[usize],
1906 owner: OwnedRevisionSpan,
1907 ) -> Result<(), ProjectionError> {
1908 let ReviewRevisionCollection::Complete { revisions, .. } = &mut self.review_revisions
1909 else {
1910 return Ok(());
1911 };
1912 for &review_index in review_indices {
1913 let revision = revisions
1914 .get_mut(review_index)
1915 .ok_or(ProjectionError::InvalidDocumentXml)?;
1916 let span = match owner {
1917 OwnedRevisionSpan::Content(span) => span,
1918 OwnedRevisionSpan::Paragraph(span) => {
1919 if revision.kind == RevisionFactKind::ParagraphPropertiesChange {
1920 span
1921 } else {
1922 ReviewSpan {
1923 start: span.end,
1924 end: span.end,
1925 }
1926 }
1927 }
1928 };
1929 let payload = match revision.kind {
1930 RevisionFactKind::RunPropertiesChange
1931 | RevisionFactKind::ParagraphPropertiesChange
1932 | RevisionFactKind::TablePropertiesChange
1933 | RevisionFactKind::TablePropertiesExceptionChange
1934 | RevisionFactKind::TableRowPropertiesChange
1935 | RevisionFactKind::TableCellPropertiesChange
1936 | RevisionFactKind::TableGridChange => RevisionPayload::FormattingOnly,
1937 _ => RevisionPayload::ParagraphMark,
1938 };
1939 revision.content = ReviewDetail::Known(RevisionContent { span, payload });
1940 }
1941 Ok(())
1942 }
1943
1944 fn inside_body(&self) -> bool {
1945 self.frames.iter().any(|frame| matches!(frame, Frame::Body))
1946 }
1947
1948 fn inside_textbox(&self) -> bool {
1949 self.frames
1950 .iter()
1951 .any(|frame| matches!(frame, Frame::Textbox))
1952 }
1953
1954 fn inside_change_snapshot(&self) -> bool {
1955 self.frames
1956 .iter()
1957 .any(|frame| matches!(frame, Frame::ChangeSnapshot(_)))
1958 }
1959
1960 fn inside_paragraph_mark_properties(&self) -> bool {
1961 self.current_paragraph.is_some()
1962 && self
1963 .frames
1964 .iter()
1965 .any(|frame| matches!(frame, Frame::ParagraphProperties))
1966 && !self
1967 .frames
1968 .iter()
1969 .any(|frame| matches!(frame, Frame::Run(_)))
1970 }
1971
1972 fn inside_table_row_properties(&self) -> bool {
1973 self.frames
1974 .iter()
1975 .any(|frame| matches!(frame, Frame::TableRowProperties))
1976 }
1977
1978 fn pseudo_text_is_suppressed(&self) -> bool {
1979 self.frames.iter().any(|frame| {
1980 matches!(
1981 frame,
1982 Frame::ParagraphProperties
1983 | Frame::RunProperties(_)
1984 | Frame::ChangeSnapshot(_)
1985 | Frame::Textbox
1986 ) || matches!(frame, Frame::Revision(revision) if revision.suppressed)
1987 || matches!(frame, Frame::Sdt(sdt) if sdt.placeholder)
1988 })
1989 }
1990
1991 fn pseudo_text_is_structurally_suppressed(&self) -> bool {
1992 self.frames.iter().any(|frame| {
1993 matches!(
1994 frame,
1995 Frame::ParagraphProperties
1996 | Frame::RunProperties(_)
1997 | Frame::ChangeSnapshot(_)
1998 | Frame::Textbox
1999 ) || matches!(frame, Frame::Sdt(sdt) if sdt.placeholder)
2000 })
2001 }
2002
2003 fn append_pseudo_text(&mut self, text: &str) -> Result<(), ProjectionError> {
2004 if self.current_paragraph.is_none()
2005 || self.pseudo_text_is_structurally_suppressed()
2006 || self
2007 .frames
2008 .iter()
2009 .rev()
2010 .any(|frame| matches!(frame, Frame::Run(run) if run.hidden))
2011 {
2012 return Ok(());
2013 }
2014 self.append_review_text(text);
2015 if self.pseudo_text_is_suppressed() {
2016 return Ok(());
2017 }
2018 if let Some(run) = self.frames.iter_mut().rev().find_map(|frame| match frame {
2019 Frame::Run(run) => Some(run),
2020 _ => None,
2021 }) {
2022 run.text.push_str(text);
2023 return Ok(());
2024 }
2025 self.current_paragraph
2026 .as_mut()
2027 .ok_or(ProjectionError::InvalidDocumentXml)?
2028 .append(text, TextProperties::default())
2029 }
2030
2031 fn append_review_text(&mut self, text: &str) {
2032 let ReviewRevisionCollection::Complete {
2033 remaining_detail_bytes,
2034 ..
2035 } = &mut self.review_revisions
2036 else {
2037 return;
2038 };
2039 let mut limit_exceeded = false;
2040 for frame in &mut self.frames {
2041 if let Frame::Revision(revision) = frame
2042 && revision.review_index.is_some()
2043 {
2044 let Some(remaining) = remaining_detail_bytes.checked_sub(text.len()) else {
2045 limit_exceeded = true;
2046 break;
2047 };
2048 *remaining_detail_bytes = remaining;
2049 revision.text.push_str(text);
2050 }
2051 }
2052 if limit_exceeded {
2053 self.review_revisions = ReviewRevisionCollection::LimitExceeded;
2054 }
2055 }
2056
2057 fn current_review_point(&self) -> Option<ReviewPoint> {
2058 let paragraph = self.current_paragraph.as_ref()?;
2059 let run_text = if self.pseudo_text_is_suppressed() {
2060 None
2061 } else {
2062 self.frames.iter().rev().find_map(|frame| match frame {
2063 Frame::Run(run) if !run.hidden => Some(run.text.as_str()),
2064 _ => None,
2065 })
2066 };
2067 let run_utf8 = run_text.map_or(0, str::len);
2068 let run_utf16 = run_text.map_or(0, |text| text.encode_utf16().count());
2069 Some(ReviewPoint {
2070 paragraph_ordinal: self.paragraphs.len(),
2071 utf8: u32::try_from(paragraph.text.len().checked_add(run_utf8)?).ok()?,
2072 utf16: paragraph
2073 .utf16_len
2074 .checked_add(u32::try_from(run_utf16).ok()?)?,
2075 })
2076 }
2077
2078 fn next_paragraph_start_review_point(&self) -> Option<ReviewPoint> {
2079 if self.current_paragraph.is_some() {
2080 return self.current_review_point();
2081 }
2082 self.inside_body().then_some(ReviewPoint {
2083 paragraph_ordinal: self.paragraphs.len(),
2084 utf8: 0,
2085 utf16: 0,
2086 })
2087 }
2088
2089 fn previous_paragraph_end_review_point(&self) -> Option<ReviewPoint> {
2090 if self.current_paragraph.is_some() {
2091 return self.current_review_point();
2092 }
2093 self.paragraphs.last().and_then(paragraph_end)
2094 }
2095
2096 fn review_comment_id(
2097 reader: &NsReader<&[u8]>,
2098 element: &BytesStart<'_>,
2099 ) -> Result<Option<String>, ProjectionError> {
2100 attribute(reader, element, b"id")
2101 }
2102
2103 fn start_review_comment_anchor(
2104 &mut self,
2105 reader: &NsReader<&[u8]>,
2106 element: &BytesStart<'_>,
2107 ) -> Result<(), ProjectionError> {
2108 let (Some(comment_id), Some(point)) = (
2109 Self::review_comment_id(reader, element)?,
2110 self.next_paragraph_start_review_point(),
2111 ) else {
2112 return Ok(());
2113 };
2114 if self.invalid_review_comment_anchors.contains(&comment_id) {
2115 return Ok(());
2116 }
2117 if self
2118 .open_review_comment_anchors
2119 .insert(comment_id.clone(), point)
2120 .is_some()
2121 {
2122 self.open_review_comment_anchors.remove(&comment_id);
2123 self.review_comment_anchors.remove(&comment_id);
2124 self.invalid_review_comment_anchors.insert(comment_id);
2125 }
2126 Ok(())
2127 }
2128
2129 fn suppress_review_comment_anchor(
2130 &mut self,
2131 reader: &NsReader<&[u8]>,
2132 element: &BytesStart<'_>,
2133 ) -> Result<(), ProjectionError> {
2134 let Some(comment_id) = Self::review_comment_id(reader, element)? else {
2135 return Ok(());
2136 };
2137 self.open_review_comment_anchors.remove(&comment_id);
2138 self.review_comment_anchors.remove(&comment_id);
2139 self.invalid_review_comment_anchors.insert(comment_id);
2140 Ok(())
2141 }
2142
2143 fn end_review_comment_anchor(
2144 &mut self,
2145 reader: &NsReader<&[u8]>,
2146 element: &BytesStart<'_>,
2147 ) -> Result<(), ProjectionError> {
2148 let (Some(comment_id), Some(end)) = (
2149 Self::review_comment_id(reader, element)?,
2150 self.previous_paragraph_end_review_point(),
2151 ) else {
2152 return Ok(());
2153 };
2154 if self.invalid_review_comment_anchors.contains(&comment_id) {
2155 return Ok(());
2156 }
2157 let Some(start) = self.open_review_comment_anchors.remove(&comment_id) else {
2158 self.review_comment_anchors.remove(&comment_id);
2159 self.invalid_review_comment_anchors.insert(comment_id);
2160 return Ok(());
2161 };
2162 if self
2163 .review_comment_anchors
2164 .insert(comment_id.clone(), ReviewSpan { start, end })
2165 .is_some()
2166 {
2167 self.review_comment_anchors.remove(&comment_id);
2168 self.invalid_review_comment_anchors.insert(comment_id);
2169 }
2170 Ok(())
2171 }
2172
2173 fn record_review_comment_reference(
2174 &mut self,
2175 reader: &NsReader<&[u8]>,
2176 element: &BytesStart<'_>,
2177 ) -> Result<(), ProjectionError> {
2178 let (Some(comment_id), Some(point)) = (
2179 Self::review_comment_id(reader, element)?,
2180 self.current_review_point(),
2181 ) else {
2182 return Ok(());
2183 };
2184 if self.invalid_review_comment_anchors.contains(&comment_id) {
2185 return Ok(());
2186 }
2187 if !self.review_comment_anchors.contains_key(&comment_id)
2188 && !self.open_review_comment_anchors.contains_key(&comment_id)
2189 {
2190 self.review_comment_anchors.insert(
2191 comment_id,
2192 ReviewSpan {
2193 start: point,
2194 end: point,
2195 },
2196 );
2197 }
2198 Ok(())
2199 }
2200
2201 fn append_pseudo_content(&mut self, text: &str) -> Result<(), ProjectionError> {
2202 let Some(Frame::PseudoText(frame)) = self.frames.last_mut() else {
2203 return Err(ProjectionError::InvalidDocumentXml);
2204 };
2205 frame.text.push_str(text);
2206 Ok(())
2207 }
2208
2209 fn append_text_control(&mut self, control: TextControl) -> Result<(), ProjectionError> {
2210 if let Some(text) = control.materialize(self.text_materialization) {
2211 self.append_pseudo_text(text)?;
2212 }
2213 Ok(())
2214 }
2215
2216 fn append_field_instruction(&mut self, text: &str) {
2217 let Some(field) = self.fields.last_mut() else {
2218 self.references_complete = false;
2219 return;
2220 };
2221 if matches!(field.source, FieldSource::Hidden) {
2222 return;
2223 }
2224 if field.separated || field.instruction.len().saturating_add(text.len()) > 4096 {
2225 self.references_complete = false;
2226 return;
2227 }
2228 field.instruction.push_str(text);
2229 }
2230
2231 fn simple_field(
2232 &mut self,
2233 reader: &NsReader<&[u8]>,
2234 element: &BytesStart<'_>,
2235 ) -> Result<(), ProjectionError> {
2236 let instruction = attribute(reader, element, b"instr")?;
2237 let source = self.current_field_source();
2238 if matches!(source, FieldSource::Hidden) {
2239 return Ok(());
2240 }
2241 let Some(instruction) = instruction.filter(|value| value.len() <= 4096) else {
2242 self.references_complete = false;
2243 return Ok(());
2244 };
2245 self.record_field_reference(&instruction, source);
2246 Ok(())
2247 }
2248
2249 fn field_character(
2250 &mut self,
2251 reader: &NsReader<&[u8]>,
2252 element: &BytesStart<'_>,
2253 ) -> Result<(), ProjectionError> {
2254 match attribute(reader, element, b"fldCharType")?.as_deref() {
2255 Some("begin") => self.fields.push(FieldFrame {
2256 source: self.current_field_source(),
2257 instruction: String::new(),
2258 separated: false,
2259 }),
2260 Some("separate") => {
2261 let Some(mut field) = self.fields.pop() else {
2262 self.references_complete = false;
2263 return Ok(());
2264 };
2265 if field.separated {
2266 self.references_complete = false;
2267 } else {
2268 self.record_field_reference(&field.instruction, field.source);
2269 field.separated = true;
2270 }
2271 self.fields.push(field);
2272 }
2273 Some("end") => {
2274 let Some(field) = self.fields.pop() else {
2275 self.references_complete = false;
2276 return Ok(());
2277 };
2278 if !field.separated {
2279 self.record_field_reference(&field.instruction, field.source);
2280 }
2281 }
2282 _ => self.references_complete = false,
2283 }
2284 Ok(())
2285 }
2286
2287 fn current_field_source(&self) -> FieldSource {
2288 if self.pseudo_text_is_suppressed() {
2289 return FieldSource::Hidden;
2290 }
2291 self.current_review_point()
2292 .map_or(FieldSource::Unsupported, FieldSource::Projected)
2293 }
2294
2295 fn record_field_reference(&mut self, instruction: &str, source: FieldSource) {
2296 if matches!(source, FieldSource::Hidden) {
2297 return;
2298 }
2299 match internal_reference_target(instruction) {
2300 Ok(Some(reference_id)) => {
2301 let FieldSource::Projected(source) = source else {
2302 self.references_complete = false;
2303 return;
2304 };
2305 self.references.push(RawInternalReference {
2306 reference_id,
2307 source,
2308 });
2309 }
2310 Ok(None) => {}
2311 Err(()) => self.references_complete = false,
2312 }
2313 }
2314
2315 fn current_point(&self) -> Option<ReviewPoint> {
2316 if self.current_paragraph.is_some() {
2317 return self.current_review_point();
2318 }
2319 if !matches!(self.frames.last(), Some(Frame::Body)) {
2320 return None;
2321 }
2322 self.paragraphs
2323 .last()
2324 .map_or(Some(paragraph_start(0)), paragraph_end)
2325 }
2326
2327 fn current_bookmark_point(&self) -> Option<BookmarkPoint> {
2328 if self.current_paragraph.is_some() {
2329 return self.current_point().map(BookmarkPoint::Paragraph);
2330 }
2331 matches!(
2332 self.frames.last(),
2333 Some(
2334 Frame::Body
2335 | Frame::BlockContent
2336 | Frame::Table(_)
2337 | Frame::Row(_)
2338 | Frame::Cell(_)
2339 )
2340 )
2341 .then(|| BookmarkPoint::ParagraphBoundary(self.paragraphs.len()))
2342 }
2343
2344 fn start_bookmark(
2345 &mut self,
2346 reader: &NsReader<&[u8]>,
2347 element: &BytesStart<'_>,
2348 ) -> Result<(), ProjectionError> {
2349 let id = attribute(reader, element, b"id")?.and_then(|value| value.parse::<u32>().ok());
2350 let name = attribute(reader, element, b"name")?;
2351 let point = self.current_bookmark_point();
2352 let (Some(id), Some(name), Some(point)) = (id, name, point) else {
2353 self.bookmarks_complete = false;
2354 return Ok(());
2355 };
2356 if name.is_empty() {
2357 self.bookmarks_complete = false;
2358 return Ok(());
2359 }
2360 self.open_bookmarks
2361 .entry(id)
2362 .or_default()
2363 .starts
2364 .push(Some((name, point)));
2365 Ok(())
2366 }
2367
2368 fn end_bookmark(
2369 &mut self,
2370 reader: &NsReader<&[u8]>,
2371 element: &BytesStart<'_>,
2372 ) -> Result<(), ProjectionError> {
2373 let id = attribute(reader, element, b"id")?.and_then(|value| value.parse::<u32>().ok());
2374 let point = self.current_bookmark_point();
2375 let (Some(id), Some(end)) = (id, point) else {
2376 self.bookmarks_complete = false;
2377 return Ok(());
2378 };
2379 let Some(queue) = self.open_bookmarks.get_mut(&id) else {
2380 self.bookmarks_complete = false;
2381 return Ok(());
2382 };
2383 let Some((name, start)) = queue.starts.get_mut(queue.cursor).and_then(Option::take) else {
2384 self.bookmarks_complete = false;
2385 return Ok(());
2386 };
2387 queue.cursor = queue
2388 .cursor
2389 .checked_add(1)
2390 .ok_or(ProjectionError::InvalidDocumentXml)?;
2391 if queue.cursor == queue.starts.len() {
2392 self.open_bookmarks.remove(&id);
2393 }
2394 self.bookmarks.push(PendingBookmarkRange {
2395 id,
2396 name,
2397 start,
2398 end,
2399 });
2400 Ok(())
2401 }
2402
2403 fn mark_placeholder(&mut self) {
2404 let Some(sdt) = self.frames.iter_mut().rev().find_map(|frame| match frame {
2405 Frame::Sdt(sdt) => Some(sdt),
2406 _ => None,
2407 }) else {
2408 return;
2409 };
2410 sdt.placeholder = true;
2411 if let Some(paragraph) = self.current_paragraph.as_mut() {
2412 paragraph.truncate(sdt.text_utf8_start, sdt.text_utf16_start);
2413 }
2414 }
2415
2416 fn mark_hidden_run(&mut self) {
2417 let Some((run_frame, hidden_eligible)) =
2418 self.frames.iter().rev().find_map(|frame| match frame {
2419 Frame::RunProperties(properties) => {
2420 Some((properties.run_frame, properties.hidden_eligible))
2421 }
2422 _ => None,
2423 })
2424 else {
2425 return;
2426 };
2427 if !hidden_eligible || run_frame == usize::MAX {
2428 return;
2429 }
2430 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2431 run.hidden = true;
2432 }
2433 }
2434
2435 fn set_run_character_style(&mut self, style_id: Option<String>) {
2436 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2437 return;
2438 };
2439 let run_frame = properties.run_frame;
2440 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2441 run.character_style_id = style_id;
2442 }
2443 }
2444
2445 fn mark_bold_run(&mut self, enabled: bool) {
2446 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2447 return;
2448 };
2449 let run_frame = properties.run_frame;
2450 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2451 run.direct_styles.bold = Some(enabled);
2452 }
2453 }
2454
2455 fn mark_complex_script_bold_run(&mut self, enabled: bool) {
2456 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2457 return;
2458 };
2459 let run_frame = properties.run_frame;
2460 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2461 run.direct_styles.complex_script_bold = Some(enabled);
2462 }
2463 }
2464
2465 fn mark_complex_script_run(&mut self, enabled: bool) {
2466 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2467 return;
2468 };
2469 let run_frame = properties.run_frame;
2470 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2471 run.direct_styles.force_complex_script = Some(enabled);
2472 }
2473 }
2474
2475 fn mark_right_to_left_run(&mut self, enabled: bool) {
2476 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2477 return;
2478 };
2479 let run_frame = properties.run_frame;
2480 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2481 run.direct_styles.right_to_left = Some(enabled);
2482 }
2483 }
2484
2485 fn mark_highlighted_run(&mut self, enabled: bool) {
2486 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2487 return;
2488 };
2489 let run_frame = properties.run_frame;
2490 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2491 run.direct_styles.highlighted = Some(enabled);
2492 }
2493 }
2494
2495 fn mark_superscript_run(&mut self, enabled: bool) {
2496 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2497 return;
2498 };
2499 let run_frame = properties.run_frame;
2500 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2501 run.direct_styles.superscript = Some(enabled);
2502 }
2503 }
2504}
2505
2506const fn revision_fact_kind(name: &[u8]) -> Option<RevisionFactKind> {
2507 match name {
2508 b"ins" => Some(RevisionFactKind::Insertion),
2509 b"del" => Some(RevisionFactKind::Deletion),
2510 b"moveFrom" => Some(RevisionFactKind::MoveFrom),
2511 b"moveTo" => Some(RevisionFactKind::MoveTo),
2512 b"cellIns" => Some(RevisionFactKind::CellInsertion),
2513 b"cellDel" => Some(RevisionFactKind::CellDeletion),
2514 b"cellMerge" => Some(RevisionFactKind::CellMerge),
2515 b"pPrChange" => Some(RevisionFactKind::ParagraphPropertiesChange),
2516 b"rPrChange" => Some(RevisionFactKind::RunPropertiesChange),
2517 b"sectPrChange" => Some(RevisionFactKind::SectionPropertiesChange),
2518 b"tblPrChange" => Some(RevisionFactKind::TablePropertiesChange),
2519 b"tblPrExChange" => Some(RevisionFactKind::TablePropertiesExceptionChange),
2520 b"trPrChange" => Some(RevisionFactKind::TableRowPropertiesChange),
2521 b"tcPrChange" => Some(RevisionFactKind::TableCellPropertiesChange),
2522 b"tblGridChange" => Some(RevisionFactKind::TableGridChange),
2523 b"customXmlDelRangeStart" => Some(RevisionFactKind::CustomXmlDeletionRangeStart),
2524 b"customXmlDelRangeEnd" => Some(RevisionFactKind::CustomXmlDeletionRangeEnd),
2525 b"customXmlInsRangeStart" => Some(RevisionFactKind::CustomXmlInsertionRangeStart),
2526 b"customXmlInsRangeEnd" => Some(RevisionFactKind::CustomXmlInsertionRangeEnd),
2527 b"customXmlMoveFromRangeStart" => Some(RevisionFactKind::CustomXmlMoveFromRangeStart),
2528 b"customXmlMoveFromRangeEnd" => Some(RevisionFactKind::CustomXmlMoveFromRangeEnd),
2529 b"customXmlMoveToRangeStart" => Some(RevisionFactKind::CustomXmlMoveToRangeStart),
2530 b"customXmlMoveToRangeEnd" => Some(RevisionFactKind::CustomXmlMoveToRangeEnd),
2531 _ => None,
2532 }
2533}
2534
2535fn enclosing_table(frames: &mut [Frame]) -> Option<&mut TableFrame> {
2536 for frame in frames.iter_mut().rev() {
2537 match frame {
2538 Frame::Table(table) => return Some(table),
2539 Frame::Row(_) | Frame::Cell(_) | Frame::Paragraph | Frame::Run(_) => return None,
2540 _ => {}
2541 }
2542 }
2543 None
2544}
2545
2546fn enclosing_row(frames: &mut [Frame]) -> Option<&mut RowFrame> {
2547 for frame in frames.iter_mut().rev() {
2548 match frame {
2549 Frame::Row(row) => return Some(row),
2550 Frame::Table(_) | Frame::Cell(_) | Frame::Paragraph | Frame::Run(_) => return None,
2551 _ => {}
2552 }
2553 }
2554 None
2555}
2556
2557const fn revision_is_suppressed(name: &[u8], view: RevisionView) -> bool {
2558 match view {
2559 RevisionView::Current => matches!(name, b"del" | b"moveFrom"),
2560 RevisionView::Original => matches!(name, b"ins" | b"moveTo"),
2561 }
2562}
2563
2564const fn is_change_snapshot(name: &[u8]) -> bool {
2565 matches!(
2566 name,
2567 b"pPrChange"
2568 | b"rPrChange"
2569 | b"sectPrChange"
2570 | b"tblPrChange"
2571 | b"tblPrExChange"
2572 | b"trPrChange"
2573 | b"tcPrChange"
2574 | b"tblGridChange"
2575 )
2576}
2577
2578const fn is_unsupported_revision_markup(name: &[u8]) -> bool {
2579 matches!(
2580 name,
2581 b"customXmlDelRangeStart"
2582 | b"customXmlDelRangeEnd"
2583 | b"customXmlInsRangeStart"
2584 | b"customXmlInsRangeEnd"
2585 | b"customXmlMoveFromRangeStart"
2586 | b"customXmlMoveFromRangeEnd"
2587 | b"customXmlMoveToRangeStart"
2588 | b"customXmlMoveToRangeEnd"
2589 )
2590}
2591
2592const fn paragraph_break_is_removed(
2593 revision: Option<ParagraphMarkRevision>,
2594 view: RevisionView,
2595) -> bool {
2596 matches!(
2597 (revision, view),
2598 (Some(ParagraphMarkRevision::Removed), RevisionView::Current)
2599 | (Some(ParagraphMarkRevision::Added), RevisionView::Original)
2600 )
2601}
2602
2603#[inline(never)]
2605fn append_formatting_span(spans: &mut Vec<TextFormattingSpan>, span: TextFormattingSpan) {
2606 if let Some(adjacent) = spans
2607 .iter_mut()
2608 .rev()
2609 .find(|previous| previous.style == span.style)
2610 && adjacent.end_utf16 == span.start_utf16
2611 {
2612 adjacent.end_utf16 = span.end_utf16;
2613 return;
2614 }
2615 spans.push(span);
2616}
2617
2618fn normalize_paragraph_revision_view(
2622 paragraphs: &mut Vec<RawProjectedParagraph>,
2623 paragraph_mark_revisions: &mut HashMap<usize, ParagraphMarkRevision>,
2624 view: RevisionView,
2625 unsupported: &mut BTreeSet<RevisionUnsupportedReason>,
2626) -> Result<Option<Vec<ReviewPoint>>, ProjectionError> {
2627 let mut normalized: Vec<RawProjectedParagraph> = Vec::with_capacity(paragraphs.len());
2628 let mut origins = Vec::with_capacity(paragraphs.len());
2629 let mut merge_previous = false;
2630 let mut merged_any = false;
2631
2632 for (source_ordinal, mut paragraph) in std::mem::take(paragraphs).into_iter().enumerate() {
2633 let merge_next =
2634 paragraph_break_is_removed(paragraph_mark_revisions.remove(&source_ordinal), view);
2635 if merge_previous {
2636 let previous = normalized
2637 .last_mut()
2638 .ok_or(ProjectionError::InvalidDocumentXml)?;
2639 if previous.structure != paragraph.structure {
2640 unsupported.insert(RevisionUnsupportedReason::IncompatibleParagraphMerge);
2641 paragraph.ordinal = normalized.len();
2642 origins.push(paragraph_start(paragraph.ordinal));
2643 normalized.push(paragraph);
2644 merge_previous = merge_next;
2645 continue;
2646 }
2647 let utf16_offset = previous.utf16_len;
2648 origins.push(ReviewPoint {
2649 paragraph_ordinal: previous.ordinal,
2650 utf8: u32::try_from(previous.text.len())
2651 .map_err(|_| ProjectionError::InvalidDocumentXml)?,
2652 utf16: utf16_offset,
2653 });
2654 for mut span in paragraph.formatting {
2655 span.start_utf16 = span
2656 .start_utf16
2657 .checked_add(utf16_offset)
2658 .ok_or(ProjectionError::InvalidDocumentXml)?;
2659 span.end_utf16 = span
2660 .end_utf16
2661 .checked_add(utf16_offset)
2662 .ok_or(ProjectionError::InvalidDocumentXml)?;
2663 append_formatting_span(&mut previous.formatting, span);
2664 }
2665 previous.text.push_str(¶graph.text);
2666 previous.utf16_len = previous
2667 .utf16_len
2668 .checked_add(paragraph.utf16_len)
2669 .ok_or(ProjectionError::InvalidDocumentXml)?;
2670 merge_previous = merge_next;
2671 merged_any = true;
2672 continue;
2673 }
2674
2675 paragraph.ordinal = normalized.len();
2676 origins.push(paragraph_start(paragraph.ordinal));
2677 normalized.push(paragraph);
2678 merge_previous = merge_next;
2679 }
2680
2681 *paragraphs = normalized;
2682 Ok(merged_any.then_some(origins))
2683}
2684
2685const fn paragraph_start(paragraph_ordinal: usize) -> ReviewPoint {
2686 ReviewPoint {
2687 paragraph_ordinal,
2688 utf8: 0,
2689 utf16: 0,
2690 }
2691}
2692
2693fn relocate_review_point(origins: &[ReviewPoint], point: ReviewPoint) -> Option<ReviewPoint> {
2696 let origin = origins.get(point.paragraph_ordinal)?;
2697 Some(ReviewPoint {
2698 paragraph_ordinal: origin.paragraph_ordinal,
2699 utf8: origin.utf8.checked_add(point.utf8)?,
2700 utf16: origin.utf16.checked_add(point.utf16)?,
2701 })
2702}
2703
2704fn relocate_block_point(
2705 origins: &[ReviewPoint],
2706 point: &mut ReviewPoint,
2707) -> Result<(), ProjectionError> {
2708 *point = relocate_review_point(origins, *point).ok_or(ProjectionError::InvalidDocumentXml)?;
2709 Ok(())
2710}
2711
2712fn relocate_review_span(origins: &[ReviewPoint], span: ReviewSpan) -> Option<ReviewSpan> {
2713 Some(ReviewSpan {
2714 start: relocate_review_point(origins, span.start)?,
2715 end: relocate_review_point(origins, span.end)?,
2716 })
2717}
2718
2719fn internal_reference_target(instruction: &str) -> Result<Option<String>, ()> {
2720 let tokens = field_tokens(instruction)?;
2721 let Some(command) = tokens.first() else {
2722 return Err(());
2723 };
2724 if ["REF", "PAGEREF", "NOTEREF"]
2725 .iter()
2726 .any(|candidate| command.eq_ignore_ascii_case(candidate))
2727 {
2728 return tokens
2729 .get(1)
2730 .filter(|target| !target.is_empty() && !target.starts_with('\\'))
2731 .cloned()
2732 .map(Some)
2733 .ok_or(());
2734 }
2735 if command.eq_ignore_ascii_case("HYPERLINK") {
2736 for (index, token) in tokens.iter().enumerate().skip(1) {
2737 if token.eq_ignore_ascii_case("\\l") {
2738 return tokens
2739 .get(index.checked_add(1).ok_or(())?)
2740 .filter(|target| !target.is_empty())
2741 .cloned()
2742 .map(Some)
2743 .ok_or(());
2744 }
2745 }
2746 }
2747 Ok(None)
2748}
2749
2750fn field_tokens(instruction: &str) -> Result<Vec<String>, ()> {
2751 let mut tokens = Vec::new();
2752 let mut token = String::new();
2753 let mut quoted = false;
2754 for character in instruction.chars() {
2755 match character {
2756 '"' => quoted = !quoted,
2757 whitespace if whitespace.is_whitespace() && !quoted => {
2758 if !token.is_empty() {
2759 tokens.push(std::mem::take(&mut token));
2760 }
2761 }
2762 _ => token.push(character),
2763 }
2764 }
2765 if quoted {
2766 return Err(());
2767 }
2768 if !token.is_empty() {
2769 tokens.push(token);
2770 }
2771 Ok(tokens)
2772}
2773
2774fn attribute(
2775 reader: &NsReader<&[u8]>,
2776 element: &BytesStart<'_>,
2777 name: &[u8],
2778) -> Result<Option<String>, ProjectionError> {
2779 for attribute in element.attributes() {
2780 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2781 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2782 if OoxmlNamespace::from_resolved(&namespace) == OoxmlNamespace::Wordprocessing
2783 && local_name.as_ref().as_bytes() == name
2784 {
2785 return attribute
2786 .normalized_value(XmlVersion::Implicit1_0)
2787 .map(|value| Some(value.into_owned()))
2788 .map_err(|_| ProjectionError::InvalidDocumentXml);
2789 }
2790 }
2791 Ok(None)
2792}
2793
2794fn preserves_xml_space(
2795 reader: &NsReader<&[u8]>,
2796 element: &BytesStart<'_>,
2797) -> Result<bool, ProjectionError> {
2798 const XML_NAMESPACE: &[u8] = b"http://www.w3.org/XML/1998/namespace";
2799
2800 for attribute in element.attributes() {
2801 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2802 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2803 if matches!(
2804 namespace,
2805 quick_xml::name::ResolveResult::Bound(namespace)
2806 if namespace.as_ref().as_bytes() == XML_NAMESPACE
2807 ) && local_name.as_ref().as_bytes() == b"space"
2808 {
2809 let value = attribute
2810 .normalized_value(XmlVersion::Implicit1_0)
2811 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
2812 return match value.as_ref() {
2813 "default" => Ok(false),
2814 "preserve" => Ok(true),
2815 _ => Err(ProjectionError::InvalidDocumentXml),
2816 };
2817 }
2818 }
2819 Ok(false)
2820}
2821
2822fn on_off_element_enabled(
2823 reader: &NsReader<&[u8]>,
2824 element: &BytesStart<'_>,
2825) -> Result<bool, ProjectionError> {
2826 Ok(attribute(reader, element, b"val")?
2827 .is_none_or(|value| !matches!(value.to_ascii_lowercase().as_str(), "0" | "false" | "off")))
2828}
2829
2830fn trim_xml_whitespace(value: &str) -> &str {
2831 value.trim_matches(['\u{0009}', '\u{000a}', '\u{000d}', '\u{0020}'])
2832}
2833
2834fn attribute_2010(
2835 reader: &NsReader<&[u8]>,
2836 element: &BytesStart<'_>,
2837 name: &[u8],
2838) -> Result<Option<String>, ProjectionError> {
2839 for attribute in element.attributes() {
2840 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2841 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2842 if OoxmlNamespace::from_resolved(&namespace) == OoxmlNamespace::Wordprocessing2010
2843 && local_name.as_ref().as_bytes() == name
2844 {
2845 return attribute
2846 .normalized_value(XmlVersion::Implicit1_0)
2847 .map(|value| Some(value.into_owned()))
2848 .map_err(|_| ProjectionError::InvalidDocumentXml);
2849 }
2850 }
2851 Ok(None)
2852}
2853
2854fn symbol_text(value: &str, font: Option<&str>) -> String {
2855 let prefix = value
2856 .chars()
2857 .take_while(char::is_ascii_hexdigit)
2858 .collect::<String>();
2859 let Some(code) = (!prefix.is_empty())
2860 .then(|| u32::from_str_radix(&prefix, 16).ok())
2861 .flatten()
2862 else {
2863 return value.to_owned();
2864 };
2865 if let Some(replacement) = normalized_font_symbol(font, code) {
2866 return replacement.to_owned();
2867 }
2868 char::from_u32(code).unwrap_or('\u{fffd}').to_string()
2869}
2870
2871struct FontSymbolMapping {
2872 font: &'static str,
2873 encoded: u32,
2874 unicode: &'static str,
2875}
2876
2877const FONT_SYMBOL_MAPPINGS: &[FontSymbolMapping] = &[FontSymbolMapping {
2884 font: "Wingdings",
2885 encoded: 0xF06C,
2886 unicode: "●",
2887}];
2888
2889fn normalized_font_symbol(font: Option<&str>, encoded: u32) -> Option<&'static str> {
2890 let font = font?;
2891 FONT_SYMBOL_MAPPINGS
2892 .iter()
2893 .find(|mapping| mapping.encoded == encoded && mapping.font.eq_ignore_ascii_case(font))
2894 .map(|mapping| mapping.unicode)
2895}
2896
2897#[cfg(test)]
2898mod collision_tests {
2899 use super::*;
2900 use proptest::prelude::*;
2901 use std::fmt::Write as _;
2902
2903 fn project_bookmarks(body: &str) -> Result<RawDocumentProjection, TestCaseError> {
2904 let xml = format!(
2905 r#"<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p>{body}</w:p></w:body></w:document>"#
2906 );
2907 project_document_xml(
2908 xml.as_bytes(),
2909 32,
2910 RevisionView::Current,
2911 TextMaterialization::WordHost,
2912 Err(FormattingUnknownReason::DocumentPartOnly),
2913 None,
2914 )
2915 .map_err(|error| TestCaseError::fail(format!("unexpected fixture error: {error:?}")))
2916 }
2917
2918 proptest! {
2919 #[test]
2920 fn balanced_duplicate_ranges_retain_all_content(id in 0_u32..100_000, count in 1_usize..24, overlap in any::<bool>()) {
2921 let end = format!(r#"<w:bookmarkEnd w:id="{id}"/>"#);
2922 let text = r"<w:r><w:t>Content</w:t></w:r>";
2923 let body = if overlap {
2924 let mut starts = String::new();
2925 for index in 0..count {
2926 write!(starts, r#"<w:bookmarkStart w:id="{id}" w:name="target{index}"/>"#).map_err(|error| TestCaseError::fail(format!("fixture XML write failed: {error:?}")))?;
2927 }
2928 format!("{starts}{text}{}", end.repeat(count))
2929 } else {
2930 let mut ranges = String::new();
2931 for index in 0..count {
2932 write!(ranges, r#"<w:bookmarkStart w:id="{id}" w:name="target{index}"/>{text}{end}"#).map_err(|error| TestCaseError::fail(format!("fixture XML write failed: {error:?}")))?;
2933 }
2934 ranges
2935 };
2936 let output = project_bookmarks(&body)?;
2937 let bookmarks = output.bookmarks.map_err(|error| TestCaseError::fail(format!("unexpected fixture error: {error:?}")))?;
2938 prop_assert_eq!(bookmarks.len(), count);
2939 prop_assert_eq!(&output.paragraphs.first().ok_or_else(|| TestCaseError::fail("fixture has one paragraph"))?.text, &"Content".repeat(if overlap { 1 } else { count }));
2940 for (index, bookmark) in bookmarks.iter().enumerate() {
2941 prop_assert_eq!(bookmark.id, id);
2942 prop_assert_eq!(&bookmark.name, &format!("target{index}"));
2943 }
2944 }
2945 }
2946
2947 #[test]
2948 fn unmatched_colliding_ranges_remain_unknown() -> Result<(), TestCaseError> {
2949 let start = r#"<w:bookmarkStart w:id="37" w:name="target"/>"#;
2950 let end = r#"<w:bookmarkEnd w:id="37"/>"#;
2951 prop_assert!(
2952 project_bookmarks(&format!("{start}{start}{end}"))?
2953 .bookmarks
2954 .is_err(),
2955 "unmatched colliding range must remain unknown"
2956 );
2957 prop_assert!(
2958 project_bookmarks(&format!("{start}{end}{end}"))?
2959 .bookmarks
2960 .is_err(),
2961 "unmatched colliding range must remain unknown"
2962 );
2963 Ok(())
2964 }
2965}