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