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 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
829 .xml10_content()
830 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
831 state.append_pseudo_content(&decoded)?;
832 }
833 }
834 Event::CData(text) => {
835 if !compatibility.is_suppressed()
836 && matches!(state.frames.last(), Some(Frame::PseudoText(_)))
837 {
838 let decoded = text
839 .xml10_content()
840 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
841 state.append_pseudo_content(&decoded)?;
842 }
843 }
844 Event::GeneralRef(reference) => {
845 if !compatibility.is_suppressed()
846 && matches!(state.frames.last(), Some(Frame::PseudoText(_)))
847 {
848 let resolved_character = reference
849 .resolve_char_ref()
850 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
851 let resolved = if let Some(character) = resolved_character {
852 character.to_string()
853 } else {
854 let name = reference
855 .decode()
856 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
857 resolve_predefined_entity(&name)
858 .ok_or(ProjectionError::InvalidDocumentXml)?
859 .to_owned()
860 };
861 state.append_pseudo_content(&resolved)?;
862 }
863 }
864 Event::End(element) => {
865 let (namespace, local_name) = reader.resolver().resolve_element(element.name());
866 if compatibility.end(
867 OoxmlNamespace::from_resolved(&namespace),
868 local_name.as_ref(),
869 )? == CompatibilityAction::Process
870 {
871 state.end(styles)?;
872 }
873 }
874 Event::Eof => break,
875 _ => {}
876 }
877 }
878 if !state.body_seen {
879 return Err(ProjectionError::MissingDocumentBody);
880 }
881 if state.current_paragraph.is_some() || !state.frames.is_empty() || !compatibility.is_complete()
882 {
883 return Err(ProjectionError::InvalidDocumentXml);
884 }
885 let mut bookmarks = if state.bookmarks_complete && state.open_bookmarks.is_empty() {
886 resolve_bookmark_ranges(&state.paragraphs, state.bookmarks)
887 .ok_or(StructuralFactUnknownReason::IncompleteBookmarkRanges)
888 } else {
889 Err(StructuralFactUnknownReason::IncompleteBookmarkRanges)
890 };
891 let paragraph_origins = if state.paragraph_mark_revisions.is_empty() {
892 None
893 } else {
894 normalize_paragraph_revision_view(
895 &mut state.paragraphs,
896 &mut state.paragraph_mark_revisions,
897 state.revision_view,
898 &mut state.revision_unsupported,
899 )?
900 };
901 if let Some(origins) = paragraph_origins {
902 if let Ok(ranges) = &mut bookmarks {
903 for range in ranges {
904 relocate_block_point(&origins, &mut range.start)?;
905 relocate_block_point(&origins, &mut range.end)?;
906 }
907 }
908 for reference in &mut state.references {
909 relocate_block_point(&origins, &mut reference.source)?;
910 }
911 state.review_comment_anchors.retain(|_, anchor| {
912 relocate_review_span(&origins, *anchor).is_some_and(|relocated| {
913 *anchor = relocated;
914 true
915 })
916 });
917 if let ReviewRevisionCollection::Complete { revisions, .. } = &mut state.review_revisions {
918 for revision in revisions {
919 let ReviewDetail::Known(content) = &mut revision.content else {
920 continue;
921 };
922 match relocate_review_span(&origins, content.span) {
923 Some(span) => content.span = span,
924 None => {
925 revision.content =
926 ReviewDetail::Unknown(ReviewFactUnknownReason::UnsupportedLocation);
927 }
928 }
929 }
930 }
931 }
932 let references = if state.references_complete && state.fields.is_empty() {
933 Ok(state.references)
934 } else {
935 Err(StructuralFactUnknownReason::UnsupportedInternalReferences)
936 };
937 Ok(RawDocumentProjection {
938 paragraphs: state.paragraphs,
939 bookmarks,
940 references,
941 formatting_completeness: state.formatting_completeness,
942 revision_status: if state.revision_unsupported.is_empty() {
943 RevisionProjectionStatus::Complete
944 } else {
945 RevisionProjectionStatus::Incomplete(state.revision_unsupported.into_iter().collect())
946 },
947 review_revisions: match state.review_revisions {
948 ReviewRevisionCollection::Disabled => None,
949 ReviewRevisionCollection::Complete { revisions, .. } => {
950 Some(ReviewFactSet::Known(revisions))
951 }
952 ReviewRevisionCollection::LimitExceeded => Some(ReviewFactSet::Unknown(
953 ReviewFactUnknownReason::ResourceLimit,
954 )),
955 },
956 review_comment_anchors: state.review_comment_anchors,
957 })
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, (String, BookmarkPoint)>,
969 seen_bookmark_ids: HashSet<u32>,
970 bookmarks: Vec<PendingBookmarkRange>,
971 references: Vec<RawInternalReference>,
972 bookmarks_complete: bool,
973 references_complete: bool,
974 formatting_completeness: FormattingCompleteness,
975 fields: Vec<FieldFrame>,
976 paragraph_mark_revisions: HashMap<usize, ParagraphMarkRevision>,
977 revision_view: RevisionView,
978 text_materialization: TextMaterialization,
979 revision_unsupported: BTreeSet<RevisionUnsupportedReason>,
980 review_revisions: ReviewRevisionCollection,
981 open_review_comment_anchors: HashMap<String, ReviewPoint>,
982 review_comment_anchors: HashMap<String, ReviewSpan>,
983 invalid_review_comment_anchors: HashSet<String>,
984}
985
986impl ProjectionState {
987 #[allow(clippy::too_many_lines)] fn start(
989 &mut self,
990 reader: &NsReader<&[u8]>,
991 namespace: OoxmlNamespace,
992 element: &BytesStart<'_>,
993 ) -> Result<(), ProjectionError> {
994 let local_name = element.local_name();
995 let name = if namespace == OoxmlNamespace::Wordprocessing {
996 local_name.as_ref()
997 } else {
998 b""
999 };
1000 let direct_run_child = if let Some(Frame::Run(run)) = self.frames.last_mut() {
1001 let child = run.direct_child_count;
1002 run.direct_child_count = run
1003 .direct_child_count
1004 .checked_add(1)
1005 .ok_or(ProjectionError::InvalidDocumentXml)?;
1006 let frame_index = self
1007 .frames
1008 .len()
1009 .checked_sub(1)
1010 .ok_or(ProjectionError::InvalidDocumentXml)?;
1011 Some((frame_index, child))
1012 } else {
1013 None
1014 };
1015
1016 if name == b"body" {
1017 if self.body_seen {
1018 return Err(ProjectionError::InvalidDocumentXml);
1019 }
1020 self.body_seen = true;
1021 self.frames.push(Frame::Body);
1022 return Ok(());
1023 }
1024 if !self.inside_body() {
1025 self.frames.push(Frame::Other);
1026 return Ok(());
1027 }
1028 let inside_snapshot = self.inside_change_snapshot();
1029 if inside_snapshot && self.start_snapshot_child(name)? {
1030 return Ok(());
1031 }
1032 let attributed_revision = if inside_snapshot {
1033 None
1034 } else {
1035 self.record_attributed_revision(reader, element, name)?
1036 };
1037 if name == b"txbxContent" {
1038 self.frames.push(Frame::Textbox);
1039 return Ok(());
1040 }
1041 if self.inside_textbox() {
1042 if matches!(
1043 name,
1044 b"commentRangeStart" | b"commentRangeEnd" | b"commentReference"
1045 ) {
1046 self.suppress_review_comment_anchor(reader, element)?;
1047 }
1048 self.frames.push(Frame::Other);
1049 return Ok(());
1050 }
1051 match name {
1052 b"commentRangeStart" => self.start_review_comment_anchor(reader, element)?,
1053 b"commentRangeEnd" => self.end_review_comment_anchor(reader, element)?,
1054 b"commentReference" => self.record_review_comment_reference(reader, element)?,
1055 _ => {}
1056 }
1057 if is_change_snapshot(name) {
1058 if let Some(review_index) = attributed_revision {
1059 match (name, self.frames.last()) {
1060 (b"rPrChange", Some(Frame::RunProperties(properties))) => {
1061 let run_frame = properties.run_frame;
1062 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1063 run.property_review_indices.push(review_index);
1064 } else if self.inside_paragraph_mark_properties()
1065 && let Some(paragraph) = self.current_paragraph.as_mut()
1066 {
1067 paragraph.paragraph_review_indices.push(review_index);
1068 }
1069 }
1070 (b"pPrChange", Some(Frame::ParagraphProperties)) => {
1071 if let Some(paragraph) = self.current_paragraph.as_mut() {
1072 paragraph.paragraph_review_indices.push(review_index);
1073 }
1074 }
1075 _ => self.record_container_property_revision(name, review_index)?,
1076 }
1077 }
1078 let owner = self.snapshot_owner(name);
1079 self.frames.push(Frame::ChangeSnapshot(SnapshotFrame {
1080 owner,
1081 status: SnapshotStatus::AwaitingRoot,
1082 }));
1083 return Ok(());
1084 }
1085 if is_unsupported_revision_markup(name) {
1086 self.revision_unsupported
1087 .insert(RevisionUnsupportedReason::UnsupportedRevisionMarkup);
1088 self.frames.push(Frame::Other);
1089 return Ok(());
1090 }
1091 if namespace == OoxmlNamespace::OfficeMath {
1092 let frame = match local_name.as_ref() {
1093 b"oMath" | b"oMathPara" if self.current_paragraph.is_some() => {
1094 self.formatting_completeness.mark_formatting_unread();
1097 Frame::Math
1098 }
1099 b"r" if self.frames.iter().any(|frame| matches!(frame, Frame::Math)) => {
1100 Frame::MathRun
1101 }
1102 b"t" if matches!(self.frames.last(), Some(Frame::MathRun)) => {
1103 Frame::PseudoText(PseudoTextFrame::math_text())
1104 }
1105 _ => Frame::Other,
1106 };
1107 self.frames.push(frame);
1108 return Ok(());
1109 }
1110
1111 let frame = match name {
1112 b"tbl" => {
1113 let ordinal = self.next_table_ordinal;
1114 self.next_table_ordinal = self
1115 .next_table_ordinal
1116 .checked_add(1)
1117 .ok_or(ProjectionError::InvalidDocumentXml)?;
1118 Frame::Table(TableFrame {
1119 ordinal,
1120 next_row: 0,
1121 style_id: None,
1122 review: ContainerReview {
1123 paragraph_start: self.paragraphs.len(),
1124 property_review: None,
1125 grid_review: None,
1126 exception_review: None,
1127 },
1128 })
1129 }
1130 b"tr" => {
1131 let Some(table) = enclosing_table(&mut self.frames) else {
1132 self.frames.push(Frame::Other);
1133 return Ok(());
1134 };
1135 let row = table.next_row;
1136 table.next_row = table
1137 .next_row
1138 .checked_add(1)
1139 .ok_or(ProjectionError::InvalidDocumentXml)?;
1140 Frame::Row(RowFrame {
1141 table_ordinal: table.ordinal,
1142 row,
1143 next_column: 0,
1144 review: ContainerReview {
1145 paragraph_start: self.paragraphs.len(),
1146 property_review: None,
1147 grid_review: None,
1148 exception_review: None,
1149 },
1150 })
1151 }
1152 b"tc" => {
1153 let Some(row) = enclosing_row(&mut self.frames) else {
1154 self.frames.push(Frame::Other);
1155 return Ok(());
1156 };
1157 let column = row.next_column;
1158 row.next_column = row
1159 .next_column
1160 .checked_add(1)
1161 .ok_or(ProjectionError::InvalidDocumentXml)?;
1162 Frame::Cell(CellFrame {
1163 table_ordinal: row.table_ordinal,
1164 row: row.row,
1165 column,
1166 review: ContainerReview {
1167 paragraph_start: self.paragraphs.len(),
1168 property_review: None,
1169 grid_review: None,
1170 exception_review: None,
1171 },
1172 })
1173 }
1174 b"p" => {
1175 if self.current_paragraph.is_some() {
1176 self.frames.push(Frame::Other);
1177 return Ok(());
1178 }
1179 if self.paragraphs.len() >= self.maximum_paragraphs {
1180 return Err(ProjectionError::TooManyParagraphs);
1181 }
1182 let package_paragraph_id = attribute_2010(reader, element, b"paraId")?
1183 .as_deref()
1184 .and_then(PackageParagraphId::parse);
1185 let (container, structure) = self
1186 .frames
1187 .iter()
1188 .rev()
1189 .find_map(Frame::paragraph_context)
1190 .ok_or(ProjectionError::InvalidDocumentXml)?;
1191 self.current_paragraph = Some(ParagraphBuilder {
1192 package_paragraph_id,
1193 text: String::new(),
1194 utf16_len: 0,
1195 formatting: Vec::new(),
1196 structure,
1197 container,
1198 properties: ParagraphProperties {
1199 table_style: self
1200 .frames
1201 .iter()
1202 .rev()
1203 .find_map(|frame| {
1204 if let Frame::Table(table) = frame {
1205 Some(table.style_id.clone().map_or(
1206 TableStyleSelection::Default,
1207 TableStyleSelection::Explicit,
1208 ))
1209 } else {
1210 None
1211 }
1212 })
1213 .unwrap_or_default(),
1214 ..ParagraphProperties::default()
1215 },
1216 resolved_text_base: None,
1217 paragraph_mark_revision: None,
1218 paragraph_review_indices: Vec::new(),
1219 });
1220 Frame::Paragraph
1221 }
1222 b"r" if self.current_paragraph.is_some() => Frame::Run(RunFrame {
1223 text: String::new(),
1224 direct_styles: TextProperties::default(),
1225 character_style_id: None,
1226 hidden: false,
1227 direct_child_count: 0,
1228 property_review_indices: Vec::new(),
1229 }),
1230 b"rPr" => {
1231 let Some((run_frame, child_index)) = direct_run_child else {
1232 self.frames.push(Frame::RunProperties(RunPropertiesFrame {
1233 run_frame: usize::MAX,
1234 hidden_eligible: false,
1235 }));
1236 return Ok(());
1237 };
1238 Frame::RunProperties(RunPropertiesFrame {
1239 run_frame,
1240 hidden_eligible: child_index == 0,
1241 })
1242 }
1243 b"sdtContent" | b"customXml" => Frame::BlockContent,
1244 b"pPr" => Frame::ParagraphProperties,
1245 b"tblStyle" if matches!(self.frames.last(), Some(Frame::TableProperties)) => {
1246 if let Some(table) = enclosing_table(&mut self.frames) {
1247 table.style_id = word_style_id(attribute(reader, element, b"val")?);
1248 }
1249 Frame::Other
1250 }
1251 b"tblPr" => Frame::TableProperties,
1252 b"tblGrid" => Frame::TableGrid,
1253 b"trPr" => Frame::TableRowProperties,
1254 b"tcPr" => Frame::TableCellProperties,
1255 b"tblPrEx" => Frame::TablePropertyExceptions,
1256 b"sectPr" => Frame::SectionProperties,
1257 b"pStyle" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1258 if let Some(paragraph) = self.current_paragraph.as_mut() {
1259 paragraph.properties.style_id =
1260 word_style_id(attribute(reader, element, b"val")?);
1261 paragraph.resolved_text_base = None;
1262 }
1263 Frame::Other
1264 }
1265 b"ind" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1266 if let Some(paragraph) = self.current_paragraph.as_mut() {
1267 paragraph.properties.indentation = parse_indentation(reader, element)?;
1268 }
1269 Frame::Other
1270 }
1271 b"jc" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1275 if let Some(paragraph) = self.current_paragraph.as_mut() {
1276 paragraph.properties.alignment = Some(parse_alignment(reader, element)?);
1277 }
1278 Frame::Other
1279 }
1280 b"outlineLvl" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1281 if let Some(paragraph) = self.current_paragraph.as_mut() {
1282 paragraph.properties.outline_level =
1283 Some(parse_outline_level_attribute(reader, element)?);
1284 }
1285 Frame::Other
1286 }
1287 b"numPr" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1288 if let Some(paragraph) = self.current_paragraph.as_mut() {
1289 paragraph.properties.numbering.present = true;
1290 }
1291 Frame::NumberingProperties
1292 }
1293 b"numId" if matches!(self.frames.last(), Some(Frame::NumberingProperties)) => {
1294 if let Some(paragraph) = self.current_paragraph.as_mut() {
1295 paragraph.properties.numbering.num_id =
1296 Some(parse_u32_attribute(reader, element)?);
1297 }
1298 Frame::Other
1299 }
1300 b"ilvl" if matches!(self.frames.last(), Some(Frame::NumberingProperties)) => {
1301 if let Some(paragraph) = self.current_paragraph.as_mut() {
1302 paragraph.properties.numbering.level =
1303 Some(parse_level_attribute(reader, element)?);
1304 }
1305 Frame::Other
1306 }
1307 b"bookmarkStart" => {
1308 self.start_bookmark(reader, element)?;
1309 Frame::Other
1310 }
1311 b"bookmarkEnd" => {
1312 self.end_bookmark(reader, element)?;
1313 Frame::Other
1314 }
1315 b"hyperlink" => {
1316 if self.pseudo_text_is_suppressed() {
1317 self.frames.push(Frame::Hyperlink(None));
1318 return Ok(());
1319 }
1320 let anchor = attribute(reader, element, b"anchor")?;
1321 let reference =
1322 anchor
1323 .clone()
1324 .filter(|anchor| !anchor.is_empty())
1325 .and_then(|anchor| {
1326 self.current_point().map(|source| RawInternalReference {
1327 reference_id: anchor,
1328 source,
1329 })
1330 });
1331 if anchor.is_some() && reference.is_none() {
1332 self.references_complete = false;
1333 }
1334 Frame::Hyperlink(reference)
1335 }
1336 b"fldSimple" => {
1337 self.simple_field(reader, element)?;
1338 Frame::Other
1339 }
1340 b"fldChar" => {
1341 self.field_character(reader, element)?;
1342 Frame::Other
1343 }
1344 b"instrText" | b"delInstrText" => Frame::PseudoText(PseudoTextFrame::instruction()),
1345 b"ins" | b"del" | b"moveFrom" | b"moveTo" => {
1346 if self.inside_table_row_properties() {
1347 self.revision_unsupported
1348 .insert(RevisionUnsupportedReason::StructuralTableRevision);
1349 Frame::Other
1350 } else if self.inside_paragraph_mark_properties() {
1351 if let Some(paragraph) = self.current_paragraph.as_mut() {
1352 paragraph.paragraph_mark_revision =
1353 ParagraphMarkRevision::from_element(name)
1354 .or(paragraph.paragraph_mark_revision);
1355 paragraph
1356 .paragraph_review_indices
1357 .extend(attributed_revision);
1358 }
1359 Frame::Other
1360 } else {
1361 Frame::Revision(RevisionFrame {
1362 review_index: attributed_revision,
1363 start: self.current_review_point(),
1364 suppressed: revision_is_suppressed(name, self.revision_view),
1365 text: String::new(),
1366 })
1367 }
1368 }
1369 b"cellIns" | b"cellDel" | b"cellMerge" => {
1370 self.revision_unsupported
1371 .insert(RevisionUnsupportedReason::StructuralTableRevision);
1372 Frame::Other
1373 }
1374 b"sdt" => {
1375 let (text_utf8_start, text_utf16_start) =
1376 self.current_paragraph.as_ref().map_or((0, 0), |paragraph| {
1377 (paragraph.text.len(), paragraph.utf16_len)
1378 });
1379 Frame::Sdt(SdtFrame {
1380 placeholder: false,
1381 text_utf8_start,
1382 text_utf16_start,
1383 })
1384 }
1385 b"showingPlcHdr" => {
1386 if on_off_element_enabled(reader, element)? {
1387 self.mark_placeholder();
1388 }
1389 Frame::Other
1390 }
1391 b"vanish" => {
1392 if on_off_element_enabled(reader, element)? {
1393 self.mark_hidden_run();
1394 }
1395 Frame::Other
1396 }
1397 b"rStyle" => {
1398 let style_id = word_style_id(attribute(reader, element, b"val")?);
1399 if matches!(style_id.as_deref(), Some("HideTWBInt" | "HideTWBExt")) {
1400 self.mark_hidden_run();
1401 }
1402 self.set_run_character_style(style_id);
1403 Frame::Other
1404 }
1405 b"b" => {
1406 self.mark_bold_run(on_off_element_enabled(reader, element)?);
1407 Frame::Other
1408 }
1409 b"bCs" => {
1410 self.mark_complex_script_bold_run(on_off_element_enabled(reader, element)?);
1411 Frame::Other
1412 }
1413 b"cs" => {
1414 self.mark_complex_script_run(on_off_element_enabled(reader, element)?);
1415 Frame::Other
1416 }
1417 b"rtl" => {
1418 self.mark_right_to_left_run(on_off_element_enabled(reader, element)?);
1419 Frame::Other
1420 }
1421 b"highlight" => {
1422 if let Some(highlighted) = attribute(reader, element, b"val")?
1423 .as_deref()
1424 .and_then(semantic_highlight_value)
1425 {
1426 self.mark_highlighted_run(highlighted);
1427 }
1428 Frame::Other
1429 }
1430 b"vertAlign" => {
1431 match attribute(reader, element, b"val")?.as_deref() {
1432 Some("superscript") => self.mark_superscript_run(true),
1433 Some("baseline" | "subscript") => self.mark_superscript_run(false),
1434 _ => {}
1435 }
1436 Frame::Other
1437 }
1438 b"t" | b"delText" => {
1439 Frame::PseudoText(PseudoTextFrame::text(preserves_xml_space(reader, element)?))
1440 }
1441 b"tab" | b"ptab" => {
1442 self.append_text_control(TextControl::Tab)?;
1443 Frame::Other
1444 }
1445 b"br" => {
1446 let control = match attribute(reader, element, b"type")?.as_deref() {
1447 Some("page") => TextControl::PageBreak,
1448 Some("column") => TextControl::ColumnBreak,
1449 _ => TextControl::LineBreak,
1450 };
1451 self.append_text_control(control)?;
1452 Frame::Other
1453 }
1454 b"cr" => {
1455 self.append_text_control(TextControl::CarriageReturn)?;
1456 Frame::Other
1457 }
1458 b"footnoteReference" | b"endnoteReference" => {
1459 let visible =
1460 attribute(reader, element, b"customMarkFollows")?.is_none_or(|value| {
1461 matches!(value.to_ascii_lowercase().as_str(), "0" | "false" | "off")
1462 });
1463 if visible {
1464 self.append_text_control(TextControl::NoteReference)?;
1465 }
1466 Frame::Other
1467 }
1468 b"softHyphen" => {
1469 self.append_text_control(TextControl::SoftHyphen)?;
1470 Frame::Other
1471 }
1472 b"noBreakHyphen" => {
1473 self.append_text_control(TextControl::NoBreakHyphen)?;
1474 Frame::Other
1475 }
1476 b"sym" => {
1477 if let Some(value) = attribute(reader, element, b"char")? {
1478 let font = attribute(reader, element, b"font")?;
1479 self.append_pseudo_text(&symbol_text(&value, font.as_deref()))?;
1480 }
1481 Frame::Other
1482 }
1483 _ => Frame::Other,
1484 };
1485 self.frames.push(frame);
1486 Ok(())
1487 }
1488
1489 fn end(
1490 &mut self,
1491 styles: Result<&StyleSheet, FormattingUnknownReason>,
1492 ) -> Result<(), ProjectionError> {
1493 let frame = self
1494 .frames
1495 .pop()
1496 .ok_or(ProjectionError::InvalidDocumentXml)?;
1497 match frame {
1498 Frame::Paragraph => self.finish_paragraph(styles)?,
1499 Frame::Table(table) => self.finish_container_review(table.review)?,
1500 Frame::Row(row) => self.finish_container_review(row.review)?,
1501 Frame::Cell(cell) => self.finish_container_review(cell.review)?,
1502 Frame::Hyperlink(Some(reference)) => self.references.push(reference),
1503 Frame::PseudoText(frame) => match frame.kind {
1504 PseudoTextKind::Text { preserve_space } => {
1505 let text = if preserve_space {
1506 frame.text.as_str()
1507 } else {
1508 trim_xml_whitespace(&frame.text)
1509 };
1510 self.append_pseudo_text(text)?;
1511 }
1512 PseudoTextKind::MathText => self.append_pseudo_text(&frame.text)?,
1513 PseudoTextKind::Instruction => self.append_field_instruction(&frame.text),
1514 },
1515 Frame::Run(run) => {
1516 let start = self.current_review_point();
1517 if !run.hidden
1518 && !self.pseudo_text_is_suppressed()
1519 && let Some(paragraph) = self.current_paragraph.as_mut()
1520 {
1521 let mut effective = match styles {
1522 Ok(styles) => {
1523 let resolved = if run.character_style_id.is_none()
1524 && run.direct_styles == TextProperties::default()
1525 {
1526 paragraph.resolve_text_base(styles)
1527 } else {
1528 styles.resolve_text(TextStyleInput {
1529 paragraph: ¶graph.properties,
1530 character_style_id: run.character_style_id.as_deref(),
1531 direct: run.direct_styles,
1532 })
1533 };
1534 resolved.unwrap_or_else(|()| {
1535 self.formatting_completeness.mark_styles_unread();
1536 run.direct_styles
1537 })
1538 }
1539 Err(_) => run.direct_styles,
1540 };
1541 effective.highlighted = run.direct_styles.highlighted;
1543 paragraph.append(&run.text, effective)?;
1544 }
1545 if let (Some(start), Some(end)) = (start, self.current_review_point()) {
1546 self.locate_owned_revisions(
1547 &run.property_review_indices,
1548 OwnedRevisionSpan::Content(ReviewSpan { start, end }),
1549 )?;
1550 }
1551 }
1552 Frame::ChangeSnapshot(snapshot) => {
1553 if self.revision_view == RevisionView::Original
1554 && !matches!(snapshot.status, SnapshotStatus::Applied)
1555 {
1556 self.revision_unsupported
1557 .insert(RevisionUnsupportedReason::UnsupportedRevisionMarkup);
1558 }
1559 }
1560 Frame::Revision(revision) => self.finish_attributed_revision(revision)?,
1561 _ => {}
1562 }
1563 Ok(())
1564 }
1565
1566 fn snapshot_owner(&self, name: &[u8]) -> SnapshotOwner {
1567 match (name, self.frames.last()) {
1568 (b"rPrChange", Some(Frame::RunProperties(properties))) => {
1569 if matches!(self.frames.get(properties.run_frame), Some(Frame::Run(_))) {
1570 SnapshotOwner::Run {
1571 frame: properties.run_frame,
1572 hidden_eligible: properties.hidden_eligible,
1573 }
1574 } else if self.inside_paragraph_mark_properties() {
1575 SnapshotOwner::ParagraphMark
1576 } else {
1577 SnapshotOwner::Unsupported
1578 }
1579 }
1580 (b"pPrChange", Some(Frame::ParagraphProperties))
1581 if self.current_paragraph.is_some() =>
1582 {
1583 SnapshotOwner::Paragraph
1584 }
1585 (b"tblPrChange", Some(Frame::TableProperties))
1586 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Table(_))) =>
1587 {
1588 SnapshotOwner::Table
1589 }
1590 (b"trPrChange", Some(Frame::TableRowProperties))
1591 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Row(_))) =>
1592 {
1593 SnapshotOwner::Row
1594 }
1595 (b"tcPrChange", Some(Frame::TableCellProperties))
1596 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Cell(_))) =>
1597 {
1598 SnapshotOwner::Cell
1599 }
1600 (b"tblGridChange", Some(Frame::TableGrid))
1601 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Table(_))) =>
1602 {
1603 SnapshotOwner::Grid
1604 }
1605 (b"sectPrChange", Some(Frame::SectionProperties))
1606 if matches!(
1607 self.frames.iter().rev().nth(1),
1608 Some(Frame::Body | Frame::ParagraphProperties)
1609 ) =>
1610 {
1611 SnapshotOwner::Section
1612 }
1613 (b"tblPrExChange", Some(Frame::TablePropertyExceptions))
1614 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Row(_))) =>
1615 {
1616 SnapshotOwner::TableExceptions
1617 }
1618 _ => SnapshotOwner::Unsupported,
1619 }
1620 }
1621
1622 fn start_snapshot_child(&mut self, name: &[u8]) -> Result<bool, ProjectionError> {
1623 if self.revision_view == RevisionView::Current {
1624 self.frames.push(Frame::SnapshotIgnored);
1625 return Ok(true);
1626 }
1627 if let Some(Frame::ChangeSnapshot(snapshot)) = self.frames.last_mut() {
1628 let owner = snapshot.owner;
1629 let matches_root = matches!(
1630 (owner, name),
1631 (
1632 SnapshotOwner::Run { .. } | SnapshotOwner::ParagraphMark,
1633 b"rPr"
1634 ) | (SnapshotOwner::Paragraph, b"pPr")
1635 | (SnapshotOwner::Table, b"tblPr")
1636 | (SnapshotOwner::Row, b"trPr")
1637 | (SnapshotOwner::Cell, b"tcPr")
1638 | (SnapshotOwner::Grid, b"tblGrid")
1639 | (SnapshotOwner::Section, b"sectPr")
1640 | (SnapshotOwner::TableExceptions, b"tblPrEx")
1641 );
1642 if !matches_root || !matches!(snapshot.status, SnapshotStatus::AwaitingRoot) {
1643 snapshot.status = SnapshotStatus::Unsupported;
1644 self.frames.push(Frame::SnapshotIgnored);
1645 return Ok(true);
1646 }
1647 snapshot.status = SnapshotStatus::Applied;
1648 let frame = self.restore_snapshot_owner(owner)?;
1649 self.frames.push(frame);
1650 return Ok(true);
1651 }
1652 if matches!(self.frames.last(), Some(Frame::ParagraphMarkProperties))
1653 && matches!(name, b"moveFrom" | b"moveTo")
1654 {
1655 self.revision_unsupported
1656 .insert(RevisionUnsupportedReason::UnsupportedRevisionMarkup);
1657 self.frames.push(Frame::SnapshotIgnored);
1658 return Ok(true);
1659 }
1660 let supported_property = matches!(
1661 (self.frames.last(), name),
1662 (
1663 Some(Frame::RunProperties(_)),
1664 b"rStyle"
1665 | b"b"
1666 | b"bCs"
1667 | b"cs"
1668 | b"rtl"
1669 | b"highlight"
1670 | b"vertAlign"
1671 | b"vanish"
1672 ) | (
1673 Some(Frame::ParagraphProperties),
1674 b"pStyle" | b"ind" | b"jc" | b"outlineLvl" | b"numPr"
1675 ) | (Some(Frame::NumberingProperties), b"numId" | b"ilvl")
1676 | (Some(Frame::TableProperties), b"tblStyle")
1677 | (Some(Frame::ParagraphMarkProperties), b"ins" | b"del")
1678 );
1679 if supported_property {
1680 return Ok(false);
1681 }
1682 self.frames.push(Frame::SnapshotIgnored);
1683 Ok(true)
1684 }
1685
1686 fn restore_snapshot_owner(&mut self, owner: SnapshotOwner) -> Result<Frame, ProjectionError> {
1687 match owner {
1688 SnapshotOwner::Run {
1689 frame,
1690 hidden_eligible,
1691 } => {
1692 let Some(Frame::Run(run)) = self.frames.get_mut(frame) else {
1693 return Err(ProjectionError::InvalidDocumentXml);
1694 };
1695 run.direct_styles = TextProperties::default();
1696 run.character_style_id = None;
1697 run.hidden = false;
1698 Ok(Frame::RunProperties(RunPropertiesFrame {
1699 run_frame: frame,
1700 hidden_eligible,
1701 }))
1702 }
1703 SnapshotOwner::Paragraph => {
1704 let paragraph = self
1706 .current_paragraph
1707 .as_mut()
1708 .ok_or(ProjectionError::InvalidDocumentXml)?;
1709 paragraph.properties = ParagraphProperties {
1710 table_style: std::mem::take(&mut paragraph.properties.table_style),
1711 ..ParagraphProperties::default()
1712 };
1713 paragraph.resolved_text_base = None;
1714 Ok(Frame::ParagraphProperties)
1715 }
1716 SnapshotOwner::Table => {
1717 let table =
1718 enclosing_table(&mut self.frames).ok_or(ProjectionError::InvalidDocumentXml)?;
1719 table.style_id = None;
1720 Ok(Frame::TableProperties)
1721 }
1722 SnapshotOwner::Row => Ok(Frame::TableRowProperties),
1723 SnapshotOwner::Cell => Ok(Frame::TableCellProperties),
1724 SnapshotOwner::Grid => Ok(Frame::TableGrid),
1725 SnapshotOwner::Section => Ok(Frame::SectionProperties),
1726 SnapshotOwner::TableExceptions => Ok(Frame::TablePropertyExceptions),
1727 SnapshotOwner::ParagraphMark => {
1728 let paragraph = self
1729 .current_paragraph
1730 .as_mut()
1731 .ok_or(ProjectionError::InvalidDocumentXml)?;
1732 paragraph.paragraph_mark_revision = None;
1733 Ok(Frame::ParagraphMarkProperties)
1734 }
1735 SnapshotOwner::Unsupported => Err(ProjectionError::InvalidDocumentXml),
1736 }
1737 }
1738
1739 fn record_container_property_revision(
1740 &mut self,
1741 name: &[u8],
1742 review_index: usize,
1743 ) -> Result<(), ProjectionError> {
1744 let valid_parent = matches!(
1745 (name, self.frames.last()),
1746 (b"tblPrChange", Some(Frame::TableProperties))
1747 | (b"tblGridChange", Some(Frame::TableGrid))
1748 | (b"trPrChange", Some(Frame::TableRowProperties))
1749 | (b"tcPrChange", Some(Frame::TableCellProperties))
1750 | (b"tblPrExChange", Some(Frame::TablePropertyExceptions))
1751 );
1752 if !valid_parent {
1753 return Ok(());
1754 }
1755 let review = self
1756 .frames
1757 .iter_mut()
1758 .rev()
1759 .find_map(|frame| match (name, frame) {
1760 (b"tblPrChange" | b"tblGridChange", Frame::Table(table)) => Some(&mut table.review),
1761 (b"trPrChange" | b"tblPrExChange", Frame::Row(row)) => Some(&mut row.review),
1762 (b"tcPrChange", Frame::Cell(cell)) => Some(&mut cell.review),
1763 _ => None,
1764 });
1765 if let Some(review) = review {
1766 let slot = match name {
1767 b"tblGridChange" => &mut review.grid_review,
1768 b"tblPrExChange" => &mut review.exception_review,
1769 _ => &mut review.property_review,
1770 };
1771 if slot.replace(review_index).is_some() {
1772 return Err(ProjectionError::InvalidDocumentXml);
1773 }
1774 }
1775 Ok(())
1776 }
1777
1778 fn finish_container_review(&mut self, review: ContainerReview) -> Result<(), ProjectionError> {
1779 if review.paragraph_start >= self.paragraphs.len() {
1780 return Ok(());
1781 }
1782 let end = self
1783 .paragraphs
1784 .last()
1785 .and_then(paragraph_end)
1786 .ok_or(ProjectionError::InvalidDocumentXml)?;
1787 for review_index in [
1788 review.property_review,
1789 review.grid_review,
1790 review.exception_review,
1791 ]
1792 .into_iter()
1793 .flatten()
1794 {
1795 self.locate_owned_revisions(
1796 std::slice::from_ref(&review_index),
1797 OwnedRevisionSpan::Content(ReviewSpan {
1798 start: paragraph_start(review.paragraph_start),
1799 end,
1800 }),
1801 )?;
1802 }
1803 Ok(())
1804 }
1805
1806 fn finish_paragraph(
1807 &mut self,
1808 styles: Result<&StyleSheet, FormattingUnknownReason>,
1809 ) -> Result<(), ProjectionError> {
1810 let mut paragraph = self
1811 .current_paragraph
1812 .take()
1813 .ok_or(ProjectionError::InvalidDocumentXml)?;
1814 if styles.is_ok_and(|styles| paragraph.resolve_text_base(styles).is_err()) {
1815 self.formatting_completeness.mark_styles_unread();
1816 }
1817 let ordinal = self.paragraphs.len();
1818 if let Some(revision) = paragraph.paragraph_mark_revision {
1819 self.paragraph_mark_revisions.insert(ordinal, revision);
1820 }
1821 let mark = ReviewPoint {
1822 paragraph_ordinal: ordinal,
1823 utf8: u32::try_from(paragraph.text.len())
1824 .map_err(|_| ProjectionError::InvalidDocumentXml)?,
1825 utf16: paragraph.utf16_len,
1826 };
1827 self.locate_owned_revisions(
1828 ¶graph.paragraph_review_indices,
1829 OwnedRevisionSpan::Paragraph(ReviewSpan {
1830 start: paragraph_start(ordinal),
1831 end: mark,
1832 }),
1833 )?;
1834 self.paragraphs.push(RawProjectedParagraph {
1835 ordinal: self.paragraphs.len(),
1836 package_paragraph_id: paragraph.package_paragraph_id,
1837 text: paragraph.text,
1838 utf16_len: paragraph.utf16_len,
1839 formatting: paragraph.formatting,
1840 structure: paragraph.structure,
1841 container: paragraph.container,
1842 properties: paragraph.properties,
1843 });
1844 Ok(())
1845 }
1846
1847 fn record_attributed_revision(
1848 &mut self,
1849 reader: &NsReader<&[u8]>,
1850 element: &BytesStart<'_>,
1851 name: &[u8],
1852 ) -> Result<Option<usize>, ProjectionError> {
1853 let Some(kind) = revision_fact_kind(name) else {
1854 return Ok(None);
1855 };
1856 let ReviewRevisionCollection::Complete {
1857 maximum_facts,
1858 revisions,
1859 ..
1860 } = &mut self.review_revisions
1861 else {
1862 return Ok(None);
1863 };
1864 if revisions.len() >= *maximum_facts {
1865 self.review_revisions = ReviewRevisionCollection::LimitExceeded;
1866 return Ok(None);
1867 }
1868 let review_index = revisions.len();
1869 revisions.push(AttributedRevision {
1870 kind,
1871 author: attribute(reader, element, b"author")?.unwrap_or_default(),
1872 date: attribute(reader, element, b"date")?,
1873 revision_id: attribute(reader, element, b"id")?,
1874 content: ReviewDetail::Unknown(ReviewFactUnknownReason::UnsupportedLocation),
1875 });
1876 Ok(Some(review_index))
1877 }
1878
1879 fn finish_attributed_revision(
1880 &mut self,
1881 revision: RevisionFrame,
1882 ) -> Result<(), ProjectionError> {
1883 let (Some(review_index), Some(start), Some(end)) = (
1884 revision.review_index,
1885 revision.start,
1886 self.current_review_point(),
1887 ) else {
1888 return Ok(());
1889 };
1890 let ReviewRevisionCollection::Complete { revisions, .. } = &mut self.review_revisions
1891 else {
1892 return Ok(());
1893 };
1894 let attributed = revisions
1895 .get_mut(review_index)
1896 .ok_or(ProjectionError::InvalidDocumentXml)?;
1897 attributed.content = ReviewDetail::Known(RevisionContent {
1898 span: ReviewSpan { start, end },
1899 payload: RevisionPayload::from_text(revision.text),
1900 });
1901 Ok(())
1902 }
1903
1904 fn locate_owned_revisions(
1905 &mut self,
1906 review_indices: &[usize],
1907 owner: OwnedRevisionSpan,
1908 ) -> Result<(), ProjectionError> {
1909 let ReviewRevisionCollection::Complete { revisions, .. } = &mut self.review_revisions
1910 else {
1911 return Ok(());
1912 };
1913 for &review_index in review_indices {
1914 let revision = revisions
1915 .get_mut(review_index)
1916 .ok_or(ProjectionError::InvalidDocumentXml)?;
1917 let span = match owner {
1918 OwnedRevisionSpan::Content(span) => span,
1919 OwnedRevisionSpan::Paragraph(span) => {
1920 if revision.kind == RevisionFactKind::ParagraphPropertiesChange {
1921 span
1922 } else {
1923 ReviewSpan {
1924 start: span.end,
1925 end: span.end,
1926 }
1927 }
1928 }
1929 };
1930 let payload = match revision.kind {
1931 RevisionFactKind::RunPropertiesChange
1932 | RevisionFactKind::ParagraphPropertiesChange
1933 | RevisionFactKind::TablePropertiesChange
1934 | RevisionFactKind::TablePropertiesExceptionChange
1935 | RevisionFactKind::TableRowPropertiesChange
1936 | RevisionFactKind::TableCellPropertiesChange
1937 | RevisionFactKind::TableGridChange => RevisionPayload::FormattingOnly,
1938 _ => RevisionPayload::ParagraphMark,
1939 };
1940 revision.content = ReviewDetail::Known(RevisionContent { span, payload });
1941 }
1942 Ok(())
1943 }
1944
1945 fn inside_body(&self) -> bool {
1946 self.frames.iter().any(|frame| matches!(frame, Frame::Body))
1947 }
1948
1949 fn inside_textbox(&self) -> bool {
1950 self.frames
1951 .iter()
1952 .any(|frame| matches!(frame, Frame::Textbox))
1953 }
1954
1955 fn inside_change_snapshot(&self) -> bool {
1956 self.frames
1957 .iter()
1958 .any(|frame| matches!(frame, Frame::ChangeSnapshot(_)))
1959 }
1960
1961 fn inside_paragraph_mark_properties(&self) -> bool {
1962 self.current_paragraph.is_some()
1963 && self
1964 .frames
1965 .iter()
1966 .any(|frame| matches!(frame, Frame::ParagraphProperties))
1967 && !self
1968 .frames
1969 .iter()
1970 .any(|frame| matches!(frame, Frame::Run(_)))
1971 }
1972
1973 fn inside_table_row_properties(&self) -> bool {
1974 self.frames
1975 .iter()
1976 .any(|frame| matches!(frame, Frame::TableRowProperties))
1977 }
1978
1979 fn pseudo_text_is_suppressed(&self) -> bool {
1980 self.frames.iter().any(|frame| {
1981 matches!(
1982 frame,
1983 Frame::ParagraphProperties
1984 | Frame::RunProperties(_)
1985 | Frame::ChangeSnapshot(_)
1986 | Frame::Textbox
1987 ) || matches!(frame, Frame::Revision(revision) if revision.suppressed)
1988 || matches!(frame, Frame::Sdt(sdt) if sdt.placeholder)
1989 })
1990 }
1991
1992 fn pseudo_text_is_structurally_suppressed(&self) -> bool {
1993 self.frames.iter().any(|frame| {
1994 matches!(
1995 frame,
1996 Frame::ParagraphProperties
1997 | Frame::RunProperties(_)
1998 | Frame::ChangeSnapshot(_)
1999 | Frame::Textbox
2000 ) || matches!(frame, Frame::Sdt(sdt) if sdt.placeholder)
2001 })
2002 }
2003
2004 fn append_pseudo_text(&mut self, text: &str) -> Result<(), ProjectionError> {
2005 if self.current_paragraph.is_none()
2006 || self.pseudo_text_is_structurally_suppressed()
2007 || self
2008 .frames
2009 .iter()
2010 .rev()
2011 .any(|frame| matches!(frame, Frame::Run(run) if run.hidden))
2012 {
2013 return Ok(());
2014 }
2015 self.append_review_text(text);
2016 if self.pseudo_text_is_suppressed() {
2017 return Ok(());
2018 }
2019 if let Some(run) = self.frames.iter_mut().rev().find_map(|frame| match frame {
2020 Frame::Run(run) => Some(run),
2021 _ => None,
2022 }) {
2023 run.text.push_str(text);
2024 return Ok(());
2025 }
2026 self.current_paragraph
2027 .as_mut()
2028 .ok_or(ProjectionError::InvalidDocumentXml)?
2029 .append(text, TextProperties::default())
2030 }
2031
2032 fn append_review_text(&mut self, text: &str) {
2033 let ReviewRevisionCollection::Complete {
2034 remaining_detail_bytes,
2035 ..
2036 } = &mut self.review_revisions
2037 else {
2038 return;
2039 };
2040 let mut limit_exceeded = false;
2041 for frame in &mut self.frames {
2042 if let Frame::Revision(revision) = frame
2043 && revision.review_index.is_some()
2044 {
2045 let Some(remaining) = remaining_detail_bytes.checked_sub(text.len()) else {
2046 limit_exceeded = true;
2047 break;
2048 };
2049 *remaining_detail_bytes = remaining;
2050 revision.text.push_str(text);
2051 }
2052 }
2053 if limit_exceeded {
2054 self.review_revisions = ReviewRevisionCollection::LimitExceeded;
2055 }
2056 }
2057
2058 fn current_review_point(&self) -> Option<ReviewPoint> {
2059 let paragraph = self.current_paragraph.as_ref()?;
2060 let run_text = if self.pseudo_text_is_suppressed() {
2061 None
2062 } else {
2063 self.frames.iter().rev().find_map(|frame| match frame {
2064 Frame::Run(run) if !run.hidden => Some(run.text.as_str()),
2065 _ => None,
2066 })
2067 };
2068 let run_utf8 = run_text.map_or(0, str::len);
2069 let run_utf16 = run_text.map_or(0, |text| text.encode_utf16().count());
2070 Some(ReviewPoint {
2071 paragraph_ordinal: self.paragraphs.len(),
2072 utf8: u32::try_from(paragraph.text.len().checked_add(run_utf8)?).ok()?,
2073 utf16: paragraph
2074 .utf16_len
2075 .checked_add(u32::try_from(run_utf16).ok()?)?,
2076 })
2077 }
2078
2079 fn next_paragraph_start_review_point(&self) -> Option<ReviewPoint> {
2080 if self.current_paragraph.is_some() {
2081 return self.current_review_point();
2082 }
2083 self.inside_body().then_some(ReviewPoint {
2084 paragraph_ordinal: self.paragraphs.len(),
2085 utf8: 0,
2086 utf16: 0,
2087 })
2088 }
2089
2090 fn previous_paragraph_end_review_point(&self) -> Option<ReviewPoint> {
2091 if self.current_paragraph.is_some() {
2092 return self.current_review_point();
2093 }
2094 self.paragraphs.last().and_then(paragraph_end)
2095 }
2096
2097 fn review_comment_id(
2098 reader: &NsReader<&[u8]>,
2099 element: &BytesStart<'_>,
2100 ) -> Result<Option<String>, ProjectionError> {
2101 attribute(reader, element, b"id")
2102 }
2103
2104 fn start_review_comment_anchor(
2105 &mut self,
2106 reader: &NsReader<&[u8]>,
2107 element: &BytesStart<'_>,
2108 ) -> Result<(), ProjectionError> {
2109 let (Some(comment_id), Some(point)) = (
2110 Self::review_comment_id(reader, element)?,
2111 self.next_paragraph_start_review_point(),
2112 ) else {
2113 return Ok(());
2114 };
2115 if self.invalid_review_comment_anchors.contains(&comment_id) {
2116 return Ok(());
2117 }
2118 if self
2119 .open_review_comment_anchors
2120 .insert(comment_id.clone(), point)
2121 .is_some()
2122 {
2123 self.open_review_comment_anchors.remove(&comment_id);
2124 self.review_comment_anchors.remove(&comment_id);
2125 self.invalid_review_comment_anchors.insert(comment_id);
2126 }
2127 Ok(())
2128 }
2129
2130 fn suppress_review_comment_anchor(
2131 &mut self,
2132 reader: &NsReader<&[u8]>,
2133 element: &BytesStart<'_>,
2134 ) -> Result<(), ProjectionError> {
2135 let Some(comment_id) = Self::review_comment_id(reader, element)? else {
2136 return Ok(());
2137 };
2138 self.open_review_comment_anchors.remove(&comment_id);
2139 self.review_comment_anchors.remove(&comment_id);
2140 self.invalid_review_comment_anchors.insert(comment_id);
2141 Ok(())
2142 }
2143
2144 fn end_review_comment_anchor(
2145 &mut self,
2146 reader: &NsReader<&[u8]>,
2147 element: &BytesStart<'_>,
2148 ) -> Result<(), ProjectionError> {
2149 let (Some(comment_id), Some(end)) = (
2150 Self::review_comment_id(reader, element)?,
2151 self.previous_paragraph_end_review_point(),
2152 ) else {
2153 return Ok(());
2154 };
2155 if self.invalid_review_comment_anchors.contains(&comment_id) {
2156 return Ok(());
2157 }
2158 let Some(start) = self.open_review_comment_anchors.remove(&comment_id) else {
2159 self.review_comment_anchors.remove(&comment_id);
2160 self.invalid_review_comment_anchors.insert(comment_id);
2161 return Ok(());
2162 };
2163 if self
2164 .review_comment_anchors
2165 .insert(comment_id.clone(), ReviewSpan { start, end })
2166 .is_some()
2167 {
2168 self.review_comment_anchors.remove(&comment_id);
2169 self.invalid_review_comment_anchors.insert(comment_id);
2170 }
2171 Ok(())
2172 }
2173
2174 fn record_review_comment_reference(
2175 &mut self,
2176 reader: &NsReader<&[u8]>,
2177 element: &BytesStart<'_>,
2178 ) -> Result<(), ProjectionError> {
2179 let (Some(comment_id), Some(point)) = (
2180 Self::review_comment_id(reader, element)?,
2181 self.current_review_point(),
2182 ) else {
2183 return Ok(());
2184 };
2185 if self.invalid_review_comment_anchors.contains(&comment_id) {
2186 return Ok(());
2187 }
2188 if !self.review_comment_anchors.contains_key(&comment_id)
2189 && !self.open_review_comment_anchors.contains_key(&comment_id)
2190 {
2191 self.review_comment_anchors.insert(
2192 comment_id,
2193 ReviewSpan {
2194 start: point,
2195 end: point,
2196 },
2197 );
2198 }
2199 Ok(())
2200 }
2201
2202 fn append_pseudo_content(&mut self, text: &str) -> Result<(), ProjectionError> {
2203 let Some(Frame::PseudoText(frame)) = self.frames.last_mut() else {
2204 return Err(ProjectionError::InvalidDocumentXml);
2205 };
2206 frame.text.push_str(text);
2207 Ok(())
2208 }
2209
2210 fn append_text_control(&mut self, control: TextControl) -> Result<(), ProjectionError> {
2211 if let Some(text) = control.materialize(self.text_materialization) {
2212 self.append_pseudo_text(text)?;
2213 }
2214 Ok(())
2215 }
2216
2217 fn append_field_instruction(&mut self, text: &str) {
2218 let Some(field) = self.fields.last_mut() else {
2219 self.references_complete = false;
2220 return;
2221 };
2222 if matches!(field.source, FieldSource::Hidden) {
2223 return;
2224 }
2225 if field.separated || field.instruction.len().saturating_add(text.len()) > 4096 {
2226 self.references_complete = false;
2227 return;
2228 }
2229 field.instruction.push_str(text);
2230 }
2231
2232 fn simple_field(
2233 &mut self,
2234 reader: &NsReader<&[u8]>,
2235 element: &BytesStart<'_>,
2236 ) -> Result<(), ProjectionError> {
2237 let instruction = attribute(reader, element, b"instr")?;
2238 let source = self.current_field_source();
2239 if matches!(source, FieldSource::Hidden) {
2240 return Ok(());
2241 }
2242 let Some(instruction) = instruction.filter(|value| value.len() <= 4096) else {
2243 self.references_complete = false;
2244 return Ok(());
2245 };
2246 self.record_field_reference(&instruction, source);
2247 Ok(())
2248 }
2249
2250 fn field_character(
2251 &mut self,
2252 reader: &NsReader<&[u8]>,
2253 element: &BytesStart<'_>,
2254 ) -> Result<(), ProjectionError> {
2255 match attribute(reader, element, b"fldCharType")?.as_deref() {
2256 Some("begin") => self.fields.push(FieldFrame {
2257 source: self.current_field_source(),
2258 instruction: String::new(),
2259 separated: false,
2260 }),
2261 Some("separate") => {
2262 let Some(mut field) = self.fields.pop() else {
2263 self.references_complete = false;
2264 return Ok(());
2265 };
2266 if field.separated {
2267 self.references_complete = false;
2268 } else {
2269 self.record_field_reference(&field.instruction, field.source);
2270 field.separated = true;
2271 }
2272 self.fields.push(field);
2273 }
2274 Some("end") => {
2275 let Some(field) = self.fields.pop() else {
2276 self.references_complete = false;
2277 return Ok(());
2278 };
2279 if !field.separated {
2280 self.record_field_reference(&field.instruction, field.source);
2281 }
2282 }
2283 _ => self.references_complete = false,
2284 }
2285 Ok(())
2286 }
2287
2288 fn current_field_source(&self) -> FieldSource {
2289 if self.pseudo_text_is_suppressed() {
2290 return FieldSource::Hidden;
2291 }
2292 self.current_review_point()
2293 .map_or(FieldSource::Unsupported, FieldSource::Projected)
2294 }
2295
2296 fn record_field_reference(&mut self, instruction: &str, source: FieldSource) {
2297 if matches!(source, FieldSource::Hidden) {
2298 return;
2299 }
2300 match internal_reference_target(instruction) {
2301 Ok(Some(reference_id)) => {
2302 let FieldSource::Projected(source) = source else {
2303 self.references_complete = false;
2304 return;
2305 };
2306 self.references.push(RawInternalReference {
2307 reference_id,
2308 source,
2309 });
2310 }
2311 Ok(None) => {}
2312 Err(()) => self.references_complete = false,
2313 }
2314 }
2315
2316 fn current_point(&self) -> Option<ReviewPoint> {
2317 if self.current_paragraph.is_some() {
2318 return self.current_review_point();
2319 }
2320 if !matches!(self.frames.last(), Some(Frame::Body)) {
2321 return None;
2322 }
2323 self.paragraphs
2324 .last()
2325 .map_or(Some(paragraph_start(0)), paragraph_end)
2326 }
2327
2328 fn current_bookmark_point(&self) -> Option<BookmarkPoint> {
2329 if self.current_paragraph.is_some() {
2330 return self.current_point().map(BookmarkPoint::Paragraph);
2331 }
2332 matches!(
2333 self.frames.last(),
2334 Some(
2335 Frame::Body
2336 | Frame::BlockContent
2337 | Frame::Table(_)
2338 | Frame::Row(_)
2339 | Frame::Cell(_)
2340 )
2341 )
2342 .then(|| BookmarkPoint::ParagraphBoundary(self.paragraphs.len()))
2343 }
2344
2345 fn start_bookmark(
2346 &mut self,
2347 reader: &NsReader<&[u8]>,
2348 element: &BytesStart<'_>,
2349 ) -> Result<(), ProjectionError> {
2350 let id = attribute(reader, element, b"id")?.and_then(|value| value.parse::<u32>().ok());
2351 let name = attribute(reader, element, b"name")?;
2352 let point = self.current_bookmark_point();
2353 let (Some(id), Some(name), Some(point)) = (id, name, point) else {
2354 self.bookmarks_complete = false;
2355 return Ok(());
2356 };
2357 if name.is_empty()
2358 || !self.seen_bookmark_ids.insert(id)
2359 || self.open_bookmarks.insert(id, (name, point)).is_some()
2360 {
2361 self.bookmarks_complete = false;
2362 }
2363 Ok(())
2364 }
2365
2366 fn end_bookmark(
2367 &mut self,
2368 reader: &NsReader<&[u8]>,
2369 element: &BytesStart<'_>,
2370 ) -> Result<(), ProjectionError> {
2371 let id = attribute(reader, element, b"id")?.and_then(|value| value.parse::<u32>().ok());
2372 let point = self.current_bookmark_point();
2373 let (Some(id), Some(end)) = (id, point) else {
2374 self.bookmarks_complete = false;
2375 return Ok(());
2376 };
2377 let Some((name, start)) = self.open_bookmarks.remove(&id) else {
2378 self.bookmarks_complete = false;
2379 return Ok(());
2380 };
2381 self.bookmarks.push(PendingBookmarkRange {
2382 id,
2383 name,
2384 start,
2385 end,
2386 });
2387 Ok(())
2388 }
2389
2390 fn mark_placeholder(&mut self) {
2391 let Some(sdt) = self.frames.iter_mut().rev().find_map(|frame| match frame {
2392 Frame::Sdt(sdt) => Some(sdt),
2393 _ => None,
2394 }) else {
2395 return;
2396 };
2397 sdt.placeholder = true;
2398 if let Some(paragraph) = self.current_paragraph.as_mut() {
2399 paragraph.truncate(sdt.text_utf8_start, sdt.text_utf16_start);
2400 }
2401 }
2402
2403 fn mark_hidden_run(&mut self) {
2404 let Some((run_frame, hidden_eligible)) =
2405 self.frames.iter().rev().find_map(|frame| match frame {
2406 Frame::RunProperties(properties) => {
2407 Some((properties.run_frame, properties.hidden_eligible))
2408 }
2409 _ => None,
2410 })
2411 else {
2412 return;
2413 };
2414 if !hidden_eligible || run_frame == usize::MAX {
2415 return;
2416 }
2417 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2418 run.hidden = true;
2419 }
2420 }
2421
2422 fn set_run_character_style(&mut self, style_id: Option<String>) {
2423 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2424 return;
2425 };
2426 let run_frame = properties.run_frame;
2427 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2428 run.character_style_id = style_id;
2429 }
2430 }
2431
2432 fn mark_bold_run(&mut self, enabled: bool) {
2433 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2434 return;
2435 };
2436 let run_frame = properties.run_frame;
2437 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2438 run.direct_styles.bold = Some(enabled);
2439 }
2440 }
2441
2442 fn mark_complex_script_bold_run(&mut self, enabled: bool) {
2443 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2444 return;
2445 };
2446 let run_frame = properties.run_frame;
2447 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2448 run.direct_styles.complex_script_bold = Some(enabled);
2449 }
2450 }
2451
2452 fn mark_complex_script_run(&mut self, enabled: bool) {
2453 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2454 return;
2455 };
2456 let run_frame = properties.run_frame;
2457 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2458 run.direct_styles.force_complex_script = Some(enabled);
2459 }
2460 }
2461
2462 fn mark_right_to_left_run(&mut self, enabled: bool) {
2463 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2464 return;
2465 };
2466 let run_frame = properties.run_frame;
2467 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2468 run.direct_styles.right_to_left = Some(enabled);
2469 }
2470 }
2471
2472 fn mark_highlighted_run(&mut self, enabled: bool) {
2473 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2474 return;
2475 };
2476 let run_frame = properties.run_frame;
2477 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2478 run.direct_styles.highlighted = Some(enabled);
2479 }
2480 }
2481
2482 fn mark_superscript_run(&mut self, enabled: bool) {
2483 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2484 return;
2485 };
2486 let run_frame = properties.run_frame;
2487 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2488 run.direct_styles.superscript = Some(enabled);
2489 }
2490 }
2491}
2492
2493const fn revision_fact_kind(name: &[u8]) -> Option<RevisionFactKind> {
2494 match name {
2495 b"ins" => Some(RevisionFactKind::Insertion),
2496 b"del" => Some(RevisionFactKind::Deletion),
2497 b"moveFrom" => Some(RevisionFactKind::MoveFrom),
2498 b"moveTo" => Some(RevisionFactKind::MoveTo),
2499 b"cellIns" => Some(RevisionFactKind::CellInsertion),
2500 b"cellDel" => Some(RevisionFactKind::CellDeletion),
2501 b"cellMerge" => Some(RevisionFactKind::CellMerge),
2502 b"pPrChange" => Some(RevisionFactKind::ParagraphPropertiesChange),
2503 b"rPrChange" => Some(RevisionFactKind::RunPropertiesChange),
2504 b"sectPrChange" => Some(RevisionFactKind::SectionPropertiesChange),
2505 b"tblPrChange" => Some(RevisionFactKind::TablePropertiesChange),
2506 b"tblPrExChange" => Some(RevisionFactKind::TablePropertiesExceptionChange),
2507 b"trPrChange" => Some(RevisionFactKind::TableRowPropertiesChange),
2508 b"tcPrChange" => Some(RevisionFactKind::TableCellPropertiesChange),
2509 b"tblGridChange" => Some(RevisionFactKind::TableGridChange),
2510 b"customXmlDelRangeStart" => Some(RevisionFactKind::CustomXmlDeletionRangeStart),
2511 b"customXmlDelRangeEnd" => Some(RevisionFactKind::CustomXmlDeletionRangeEnd),
2512 b"customXmlInsRangeStart" => Some(RevisionFactKind::CustomXmlInsertionRangeStart),
2513 b"customXmlInsRangeEnd" => Some(RevisionFactKind::CustomXmlInsertionRangeEnd),
2514 b"customXmlMoveFromRangeStart" => Some(RevisionFactKind::CustomXmlMoveFromRangeStart),
2515 b"customXmlMoveFromRangeEnd" => Some(RevisionFactKind::CustomXmlMoveFromRangeEnd),
2516 b"customXmlMoveToRangeStart" => Some(RevisionFactKind::CustomXmlMoveToRangeStart),
2517 b"customXmlMoveToRangeEnd" => Some(RevisionFactKind::CustomXmlMoveToRangeEnd),
2518 _ => None,
2519 }
2520}
2521
2522fn enclosing_table(frames: &mut [Frame]) -> Option<&mut TableFrame> {
2523 for frame in frames.iter_mut().rev() {
2524 match frame {
2525 Frame::Table(table) => return Some(table),
2526 Frame::Row(_) | Frame::Cell(_) | Frame::Paragraph | Frame::Run(_) => return None,
2527 _ => {}
2528 }
2529 }
2530 None
2531}
2532
2533fn enclosing_row(frames: &mut [Frame]) -> Option<&mut RowFrame> {
2534 for frame in frames.iter_mut().rev() {
2535 match frame {
2536 Frame::Row(row) => return Some(row),
2537 Frame::Table(_) | Frame::Cell(_) | Frame::Paragraph | Frame::Run(_) => return None,
2538 _ => {}
2539 }
2540 }
2541 None
2542}
2543
2544fn revision_is_suppressed(name: &[u8], view: RevisionView) -> bool {
2545 match view {
2546 RevisionView::Current => matches!(name, b"del" | b"moveFrom"),
2547 RevisionView::Original => matches!(name, b"ins" | b"moveTo"),
2548 }
2549}
2550
2551fn is_change_snapshot(name: &[u8]) -> bool {
2552 matches!(
2553 name,
2554 b"pPrChange"
2555 | b"rPrChange"
2556 | b"sectPrChange"
2557 | b"tblPrChange"
2558 | b"tblPrExChange"
2559 | b"trPrChange"
2560 | b"tcPrChange"
2561 | b"tblGridChange"
2562 )
2563}
2564
2565fn is_unsupported_revision_markup(name: &[u8]) -> bool {
2566 matches!(
2567 name,
2568 b"customXmlDelRangeStart"
2569 | b"customXmlDelRangeEnd"
2570 | b"customXmlInsRangeStart"
2571 | b"customXmlInsRangeEnd"
2572 | b"customXmlMoveFromRangeStart"
2573 | b"customXmlMoveFromRangeEnd"
2574 | b"customXmlMoveToRangeStart"
2575 | b"customXmlMoveToRangeEnd"
2576 )
2577}
2578
2579const fn paragraph_break_is_removed(
2580 revision: Option<ParagraphMarkRevision>,
2581 view: RevisionView,
2582) -> bool {
2583 matches!(
2584 (revision, view),
2585 (Some(ParagraphMarkRevision::Removed), RevisionView::Current)
2586 | (Some(ParagraphMarkRevision::Added), RevisionView::Original)
2587 )
2588}
2589
2590#[inline(never)]
2592fn append_formatting_span(spans: &mut Vec<TextFormattingSpan>, span: TextFormattingSpan) {
2593 if let Some(adjacent) = spans
2594 .iter_mut()
2595 .rev()
2596 .find(|previous| previous.style == span.style)
2597 && adjacent.end_utf16 == span.start_utf16
2598 {
2599 adjacent.end_utf16 = span.end_utf16;
2600 return;
2601 }
2602 spans.push(span);
2603}
2604
2605fn normalize_paragraph_revision_view(
2609 paragraphs: &mut Vec<RawProjectedParagraph>,
2610 paragraph_mark_revisions: &mut HashMap<usize, ParagraphMarkRevision>,
2611 view: RevisionView,
2612 unsupported: &mut BTreeSet<RevisionUnsupportedReason>,
2613) -> Result<Option<Vec<ReviewPoint>>, ProjectionError> {
2614 let mut normalized: Vec<RawProjectedParagraph> = Vec::with_capacity(paragraphs.len());
2615 let mut origins = Vec::with_capacity(paragraphs.len());
2616 let mut merge_previous = false;
2617 let mut merged_any = false;
2618
2619 for (source_ordinal, mut paragraph) in std::mem::take(paragraphs).into_iter().enumerate() {
2620 let merge_next =
2621 paragraph_break_is_removed(paragraph_mark_revisions.remove(&source_ordinal), view);
2622 if merge_previous {
2623 let previous = normalized
2624 .last_mut()
2625 .ok_or(ProjectionError::InvalidDocumentXml)?;
2626 if previous.structure != paragraph.structure {
2627 unsupported.insert(RevisionUnsupportedReason::IncompatibleParagraphMerge);
2628 paragraph.ordinal = normalized.len();
2629 origins.push(paragraph_start(paragraph.ordinal));
2630 normalized.push(paragraph);
2631 merge_previous = merge_next;
2632 continue;
2633 }
2634 let utf16_offset = previous.utf16_len;
2635 origins.push(ReviewPoint {
2636 paragraph_ordinal: previous.ordinal,
2637 utf8: u32::try_from(previous.text.len())
2638 .map_err(|_| ProjectionError::InvalidDocumentXml)?,
2639 utf16: utf16_offset,
2640 });
2641 for mut span in paragraph.formatting {
2642 span.start_utf16 = span
2643 .start_utf16
2644 .checked_add(utf16_offset)
2645 .ok_or(ProjectionError::InvalidDocumentXml)?;
2646 span.end_utf16 = span
2647 .end_utf16
2648 .checked_add(utf16_offset)
2649 .ok_or(ProjectionError::InvalidDocumentXml)?;
2650 append_formatting_span(&mut previous.formatting, span);
2651 }
2652 previous.text.push_str(¶graph.text);
2653 previous.utf16_len = previous
2654 .utf16_len
2655 .checked_add(paragraph.utf16_len)
2656 .ok_or(ProjectionError::InvalidDocumentXml)?;
2657 merge_previous = merge_next;
2658 merged_any = true;
2659 continue;
2660 }
2661
2662 paragraph.ordinal = normalized.len();
2663 origins.push(paragraph_start(paragraph.ordinal));
2664 normalized.push(paragraph);
2665 merge_previous = merge_next;
2666 }
2667
2668 *paragraphs = normalized;
2669 Ok(merged_any.then_some(origins))
2670}
2671
2672const fn paragraph_start(paragraph_ordinal: usize) -> ReviewPoint {
2673 ReviewPoint {
2674 paragraph_ordinal,
2675 utf8: 0,
2676 utf16: 0,
2677 }
2678}
2679
2680fn relocate_review_point(origins: &[ReviewPoint], point: ReviewPoint) -> Option<ReviewPoint> {
2683 let origin = origins.get(point.paragraph_ordinal)?;
2684 Some(ReviewPoint {
2685 paragraph_ordinal: origin.paragraph_ordinal,
2686 utf8: origin.utf8.checked_add(point.utf8)?,
2687 utf16: origin.utf16.checked_add(point.utf16)?,
2688 })
2689}
2690
2691fn relocate_block_point(
2692 origins: &[ReviewPoint],
2693 point: &mut ReviewPoint,
2694) -> Result<(), ProjectionError> {
2695 *point = relocate_review_point(origins, *point).ok_or(ProjectionError::InvalidDocumentXml)?;
2696 Ok(())
2697}
2698
2699fn relocate_review_span(origins: &[ReviewPoint], span: ReviewSpan) -> Option<ReviewSpan> {
2700 Some(ReviewSpan {
2701 start: relocate_review_point(origins, span.start)?,
2702 end: relocate_review_point(origins, span.end)?,
2703 })
2704}
2705
2706fn internal_reference_target(instruction: &str) -> Result<Option<String>, ()> {
2707 let tokens = field_tokens(instruction)?;
2708 let Some(command) = tokens.first() else {
2709 return Err(());
2710 };
2711 if ["REF", "PAGEREF", "NOTEREF"]
2712 .iter()
2713 .any(|candidate| command.eq_ignore_ascii_case(candidate))
2714 {
2715 return tokens
2716 .get(1)
2717 .filter(|target| !target.is_empty() && !target.starts_with('\\'))
2718 .cloned()
2719 .map(Some)
2720 .ok_or(());
2721 }
2722 if command.eq_ignore_ascii_case("HYPERLINK") {
2723 for (index, token) in tokens.iter().enumerate().skip(1) {
2724 if token.eq_ignore_ascii_case("\\l") {
2725 return tokens
2726 .get(index.checked_add(1).ok_or(())?)
2727 .filter(|target| !target.is_empty())
2728 .cloned()
2729 .map(Some)
2730 .ok_or(());
2731 }
2732 }
2733 }
2734 Ok(None)
2735}
2736
2737fn field_tokens(instruction: &str) -> Result<Vec<String>, ()> {
2738 let mut tokens = Vec::new();
2739 let mut token = String::new();
2740 let mut quoted = false;
2741 for character in instruction.chars() {
2742 match character {
2743 '"' => quoted = !quoted,
2744 whitespace if whitespace.is_whitespace() && !quoted => {
2745 if !token.is_empty() {
2746 tokens.push(std::mem::take(&mut token));
2747 }
2748 }
2749 _ => token.push(character),
2750 }
2751 }
2752 if quoted {
2753 return Err(());
2754 }
2755 if !token.is_empty() {
2756 tokens.push(token);
2757 }
2758 Ok(tokens)
2759}
2760
2761fn attribute(
2762 reader: &NsReader<&[u8]>,
2763 element: &BytesStart<'_>,
2764 name: &[u8],
2765) -> Result<Option<String>, ProjectionError> {
2766 for attribute in element.attributes() {
2767 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2768 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2769 if OoxmlNamespace::from_resolved(&namespace) == OoxmlNamespace::Wordprocessing
2770 && local_name.as_ref() == name
2771 {
2772 return attribute
2773 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
2774 .map(|value| Some(value.into_owned()))
2775 .map_err(|_| ProjectionError::InvalidDocumentXml);
2776 }
2777 }
2778 Ok(None)
2779}
2780
2781fn preserves_xml_space(
2782 reader: &NsReader<&[u8]>,
2783 element: &BytesStart<'_>,
2784) -> Result<bool, ProjectionError> {
2785 const XML_NAMESPACE: &[u8] = b"http://www.w3.org/XML/1998/namespace";
2786
2787 for attribute in element.attributes() {
2788 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2789 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2790 if matches!(
2791 namespace,
2792 quick_xml::name::ResolveResult::Bound(namespace)
2793 if namespace.as_ref() == XML_NAMESPACE
2794 ) && local_name.as_ref() == b"space"
2795 {
2796 let value = attribute
2797 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
2798 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
2799 return match value.as_ref() {
2800 "default" => Ok(false),
2801 "preserve" => Ok(true),
2802 _ => Err(ProjectionError::InvalidDocumentXml),
2803 };
2804 }
2805 }
2806 Ok(false)
2807}
2808
2809fn on_off_element_enabled(
2810 reader: &NsReader<&[u8]>,
2811 element: &BytesStart<'_>,
2812) -> Result<bool, ProjectionError> {
2813 Ok(attribute(reader, element, b"val")?
2814 .is_none_or(|value| !matches!(value.to_ascii_lowercase().as_str(), "0" | "false" | "off")))
2815}
2816
2817fn trim_xml_whitespace(value: &str) -> &str {
2818 value.trim_matches(['\u{0009}', '\u{000a}', '\u{000d}', '\u{0020}'])
2819}
2820
2821fn attribute_2010(
2822 reader: &NsReader<&[u8]>,
2823 element: &BytesStart<'_>,
2824 name: &[u8],
2825) -> Result<Option<String>, ProjectionError> {
2826 for attribute in element.attributes() {
2827 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2828 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2829 if OoxmlNamespace::from_resolved(&namespace) == OoxmlNamespace::Wordprocessing2010
2830 && local_name.as_ref() == name
2831 {
2832 return attribute
2833 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
2834 .map(|value| Some(value.into_owned()))
2835 .map_err(|_| ProjectionError::InvalidDocumentXml);
2836 }
2837 }
2838 Ok(None)
2839}
2840
2841fn symbol_text(value: &str, font: Option<&str>) -> String {
2842 let prefix = value
2843 .chars()
2844 .take_while(char::is_ascii_hexdigit)
2845 .collect::<String>();
2846 let Some(code) = (!prefix.is_empty())
2847 .then(|| u32::from_str_radix(&prefix, 16).ok())
2848 .flatten()
2849 else {
2850 return value.to_owned();
2851 };
2852 if let Some(replacement) = normalized_font_symbol(font, code) {
2853 return replacement.to_owned();
2854 }
2855 char::from_u32(code).unwrap_or('\u{fffd}').to_string()
2856}
2857
2858struct FontSymbolMapping {
2859 font: &'static str,
2860 encoded: u32,
2861 unicode: &'static str,
2862}
2863
2864const FONT_SYMBOL_MAPPINGS: &[FontSymbolMapping] = &[FontSymbolMapping {
2871 font: "Wingdings",
2872 encoded: 0xF06C,
2873 unicode: "●",
2874}];
2875
2876fn normalized_font_symbol(font: Option<&str>, encoded: u32) -> Option<&'static str> {
2877 let font = font?;
2878 FONT_SYMBOL_MAPPINGS
2879 .iter()
2880 .find(|mapping| mapping.encoded == encoded && mapping.font.eq_ignore_ascii_case(font))
2881 .map(|mapping| mapping.unicode)
2882}