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 mut 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 effective.highlighted = run.direct_styles.highlighted;
1508 paragraph.append(&run.text, effective)?;
1509 }
1510 if let (Some(start), Some(end)) = (start, self.current_review_point()) {
1511 self.locate_owned_revisions(
1512 &run.property_review_indices,
1513 OwnedRevisionSpan::Content(ReviewSpan { start, end }),
1514 )?;
1515 }
1516 }
1517 Frame::ChangeSnapshot(snapshot) => {
1518 if self.revision_view == RevisionView::Original
1519 && !matches!(snapshot.status, SnapshotStatus::Applied)
1520 {
1521 self.revision_unsupported
1522 .insert(RevisionUnsupportedReason::UnsupportedRevisionMarkup);
1523 }
1524 }
1525 Frame::Revision(revision) => self.finish_attributed_revision(revision)?,
1526 _ => {}
1527 }
1528 Ok(())
1529 }
1530
1531 fn snapshot_owner(&self, name: &[u8]) -> SnapshotOwner {
1532 match (name, self.frames.last()) {
1533 (b"rPrChange", Some(Frame::RunProperties(properties))) => {
1534 if matches!(self.frames.get(properties.run_frame), Some(Frame::Run(_))) {
1535 SnapshotOwner::Run {
1536 frame: properties.run_frame,
1537 hidden_eligible: properties.hidden_eligible,
1538 }
1539 } else if self.inside_paragraph_mark_properties() {
1540 SnapshotOwner::ParagraphMark
1541 } else {
1542 SnapshotOwner::Unsupported
1543 }
1544 }
1545 (b"pPrChange", Some(Frame::ParagraphProperties))
1546 if self.current_paragraph.is_some() =>
1547 {
1548 SnapshotOwner::Paragraph
1549 }
1550 (b"tblPrChange", Some(Frame::TableProperties))
1551 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Table(_))) =>
1552 {
1553 SnapshotOwner::Table
1554 }
1555 (b"trPrChange", Some(Frame::TableRowProperties))
1556 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Row(_))) =>
1557 {
1558 SnapshotOwner::Row
1559 }
1560 (b"tcPrChange", Some(Frame::TableCellProperties))
1561 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Cell(_))) =>
1562 {
1563 SnapshotOwner::Cell
1564 }
1565 (b"tblGridChange", Some(Frame::TableGrid))
1566 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Table(_))) =>
1567 {
1568 SnapshotOwner::Grid
1569 }
1570 (b"sectPrChange", Some(Frame::SectionProperties))
1571 if matches!(
1572 self.frames.iter().rev().nth(1),
1573 Some(Frame::Body | Frame::ParagraphProperties)
1574 ) =>
1575 {
1576 SnapshotOwner::Section
1577 }
1578 (b"tblPrExChange", Some(Frame::TablePropertyExceptions))
1579 if matches!(self.frames.iter().rev().nth(1), Some(Frame::Row(_))) =>
1580 {
1581 SnapshotOwner::TableExceptions
1582 }
1583 _ => SnapshotOwner::Unsupported,
1584 }
1585 }
1586
1587 fn start_snapshot_child(&mut self, name: &[u8]) -> Result<bool, ProjectionError> {
1588 if self.revision_view == RevisionView::Current {
1589 self.frames.push(Frame::SnapshotIgnored);
1590 return Ok(true);
1591 }
1592 if let Some(Frame::ChangeSnapshot(snapshot)) = self.frames.last_mut() {
1593 let owner = snapshot.owner;
1594 let matches_root = matches!(
1595 (owner, name),
1596 (
1597 SnapshotOwner::Run { .. } | SnapshotOwner::ParagraphMark,
1598 b"rPr"
1599 ) | (SnapshotOwner::Paragraph, b"pPr")
1600 | (SnapshotOwner::Table, b"tblPr")
1601 | (SnapshotOwner::Row, b"trPr")
1602 | (SnapshotOwner::Cell, b"tcPr")
1603 | (SnapshotOwner::Grid, b"tblGrid")
1604 | (SnapshotOwner::Section, b"sectPr")
1605 | (SnapshotOwner::TableExceptions, b"tblPrEx")
1606 );
1607 if !matches_root || !matches!(snapshot.status, SnapshotStatus::AwaitingRoot) {
1608 snapshot.status = SnapshotStatus::Unsupported;
1609 self.frames.push(Frame::SnapshotIgnored);
1610 return Ok(true);
1611 }
1612 snapshot.status = SnapshotStatus::Applied;
1613 let frame = self.restore_snapshot_owner(owner)?;
1614 self.frames.push(frame);
1615 return Ok(true);
1616 }
1617 if matches!(self.frames.last(), Some(Frame::ParagraphMarkProperties))
1618 && matches!(name, b"moveFrom" | b"moveTo")
1619 {
1620 self.revision_unsupported
1621 .insert(RevisionUnsupportedReason::UnsupportedRevisionMarkup);
1622 self.frames.push(Frame::SnapshotIgnored);
1623 return Ok(true);
1624 }
1625 let supported_property = matches!(
1626 (self.frames.last(), name),
1627 (
1628 Some(Frame::RunProperties(_)),
1629 b"rStyle"
1630 | b"b"
1631 | b"bCs"
1632 | b"cs"
1633 | b"rtl"
1634 | b"highlight"
1635 | b"vertAlign"
1636 | b"vanish"
1637 ) | (
1638 Some(Frame::ParagraphProperties),
1639 b"pStyle" | b"ind" | b"jc" | b"outlineLvl" | b"numPr"
1640 ) | (Some(Frame::NumberingProperties), b"numId" | b"ilvl")
1641 | (Some(Frame::TableProperties), b"tblStyle")
1642 | (Some(Frame::ParagraphMarkProperties), b"ins" | b"del")
1643 );
1644 if supported_property {
1645 return Ok(false);
1646 }
1647 self.frames.push(Frame::SnapshotIgnored);
1648 Ok(true)
1649 }
1650
1651 fn restore_snapshot_owner(&mut self, owner: SnapshotOwner) -> Result<Frame, ProjectionError> {
1652 match owner {
1653 SnapshotOwner::Run {
1654 frame,
1655 hidden_eligible,
1656 } => {
1657 let Some(Frame::Run(run)) = self.frames.get_mut(frame) else {
1658 return Err(ProjectionError::InvalidDocumentXml);
1659 };
1660 run.direct_styles = TextProperties::default();
1661 run.character_style_id = None;
1662 run.hidden = false;
1663 Ok(Frame::RunProperties(RunPropertiesFrame {
1664 run_frame: frame,
1665 hidden_eligible,
1666 }))
1667 }
1668 SnapshotOwner::Paragraph => {
1669 let paragraph = self
1671 .current_paragraph
1672 .as_mut()
1673 .ok_or(ProjectionError::InvalidDocumentXml)?;
1674 paragraph.properties = ParagraphProperties {
1675 table_style: std::mem::take(&mut paragraph.properties.table_style),
1676 ..ParagraphProperties::default()
1677 };
1678 paragraph.resolved_text_base = None;
1679 Ok(Frame::ParagraphProperties)
1680 }
1681 SnapshotOwner::Table => {
1682 let table =
1683 enclosing_table(&mut self.frames).ok_or(ProjectionError::InvalidDocumentXml)?;
1684 table.style_id = None;
1685 Ok(Frame::TableProperties)
1686 }
1687 SnapshotOwner::Row => Ok(Frame::TableRowProperties),
1688 SnapshotOwner::Cell => Ok(Frame::TableCellProperties),
1689 SnapshotOwner::Grid => Ok(Frame::TableGrid),
1690 SnapshotOwner::Section => Ok(Frame::SectionProperties),
1691 SnapshotOwner::TableExceptions => Ok(Frame::TablePropertyExceptions),
1692 SnapshotOwner::ParagraphMark => {
1693 let paragraph = self
1694 .current_paragraph
1695 .as_mut()
1696 .ok_or(ProjectionError::InvalidDocumentXml)?;
1697 paragraph.paragraph_mark_revision = None;
1698 Ok(Frame::ParagraphMarkProperties)
1699 }
1700 SnapshotOwner::Unsupported => Err(ProjectionError::InvalidDocumentXml),
1701 }
1702 }
1703
1704 fn record_container_property_revision(
1705 &mut self,
1706 name: &[u8],
1707 review_index: usize,
1708 ) -> Result<(), ProjectionError> {
1709 let valid_parent = matches!(
1710 (name, self.frames.last()),
1711 (b"tblPrChange", Some(Frame::TableProperties))
1712 | (b"tblGridChange", Some(Frame::TableGrid))
1713 | (b"trPrChange", Some(Frame::TableRowProperties))
1714 | (b"tcPrChange", Some(Frame::TableCellProperties))
1715 | (b"tblPrExChange", Some(Frame::TablePropertyExceptions))
1716 );
1717 if !valid_parent {
1718 return Ok(());
1719 }
1720 let review = self
1721 .frames
1722 .iter_mut()
1723 .rev()
1724 .find_map(|frame| match (name, frame) {
1725 (b"tblPrChange" | b"tblGridChange", Frame::Table(table)) => Some(&mut table.review),
1726 (b"trPrChange" | b"tblPrExChange", Frame::Row(row)) => Some(&mut row.review),
1727 (b"tcPrChange", Frame::Cell(cell)) => Some(&mut cell.review),
1728 _ => None,
1729 });
1730 if let Some(review) = review {
1731 let slot = match name {
1732 b"tblGridChange" => &mut review.grid_review,
1733 b"tblPrExChange" => &mut review.exception_review,
1734 _ => &mut review.property_review,
1735 };
1736 if slot.replace(review_index).is_some() {
1737 return Err(ProjectionError::InvalidDocumentXml);
1738 }
1739 }
1740 Ok(())
1741 }
1742
1743 fn finish_container_review(&mut self, review: ContainerReview) -> Result<(), ProjectionError> {
1744 if review.paragraph_start >= self.paragraphs.len() {
1745 return Ok(());
1746 }
1747 let end = self
1748 .paragraphs
1749 .last()
1750 .and_then(paragraph_end)
1751 .ok_or(ProjectionError::InvalidDocumentXml)?;
1752 for review_index in [
1753 review.property_review,
1754 review.grid_review,
1755 review.exception_review,
1756 ]
1757 .into_iter()
1758 .flatten()
1759 {
1760 self.locate_owned_revisions(
1761 std::slice::from_ref(&review_index),
1762 OwnedRevisionSpan::Content(ReviewSpan {
1763 start: paragraph_start(review.paragraph_start),
1764 end,
1765 }),
1766 )?;
1767 }
1768 Ok(())
1769 }
1770
1771 fn finish_paragraph(
1772 &mut self,
1773 styles: Result<&StyleSheet, FormattingUnknownReason>,
1774 ) -> Result<(), ProjectionError> {
1775 let mut paragraph = self
1776 .current_paragraph
1777 .take()
1778 .ok_or(ProjectionError::InvalidDocumentXml)?;
1779 if styles.is_ok_and(|styles| paragraph.resolve_text_base(styles).is_err()) {
1780 self.formatting_status =
1781 FormattingProjectionStatus::Incomplete(FormattingUnknownReason::UnsupportedStyles);
1782 }
1783 let ordinal = self.paragraphs.len();
1784 if let Some(revision) = paragraph.paragraph_mark_revision {
1785 self.paragraph_mark_revisions.insert(ordinal, revision);
1786 }
1787 let mark = ReviewPoint {
1788 paragraph_ordinal: ordinal,
1789 utf8: u32::try_from(paragraph.text.len())
1790 .map_err(|_| ProjectionError::InvalidDocumentXml)?,
1791 utf16: paragraph.utf16_len,
1792 };
1793 self.locate_owned_revisions(
1794 ¶graph.paragraph_review_indices,
1795 OwnedRevisionSpan::Paragraph(ReviewSpan {
1796 start: paragraph_start(ordinal),
1797 end: mark,
1798 }),
1799 )?;
1800 self.paragraphs.push(RawProjectedParagraph {
1801 ordinal: self.paragraphs.len(),
1802 package_paragraph_id: paragraph.package_paragraph_id,
1803 text: paragraph.text,
1804 utf16_len: paragraph.utf16_len,
1805 formatting: paragraph.formatting,
1806 structure: paragraph.structure,
1807 properties: paragraph.properties,
1808 });
1809 Ok(())
1810 }
1811
1812 fn record_attributed_revision(
1813 &mut self,
1814 reader: &NsReader<&[u8]>,
1815 element: &BytesStart<'_>,
1816 name: &[u8],
1817 ) -> Result<Option<usize>, ProjectionError> {
1818 let Some(kind) = revision_fact_kind(name) else {
1819 return Ok(None);
1820 };
1821 let ReviewRevisionCollection::Complete {
1822 maximum_facts,
1823 revisions,
1824 ..
1825 } = &mut self.review_revisions
1826 else {
1827 return Ok(None);
1828 };
1829 if revisions.len() >= *maximum_facts {
1830 self.review_revisions = ReviewRevisionCollection::LimitExceeded;
1831 return Ok(None);
1832 }
1833 let review_index = revisions.len();
1834 revisions.push(AttributedRevision {
1835 kind,
1836 author: attribute(reader, element, b"author")?.unwrap_or_default(),
1837 date: attribute(reader, element, b"date")?,
1838 revision_id: attribute(reader, element, b"id")?,
1839 content: ReviewDetail::Unknown(ReviewFactUnknownReason::UnsupportedLocation),
1840 });
1841 Ok(Some(review_index))
1842 }
1843
1844 fn finish_attributed_revision(
1845 &mut self,
1846 revision: RevisionFrame,
1847 ) -> Result<(), ProjectionError> {
1848 let (Some(review_index), Some(start), Some(end)) = (
1849 revision.review_index,
1850 revision.start,
1851 self.current_review_point(),
1852 ) else {
1853 return Ok(());
1854 };
1855 let ReviewRevisionCollection::Complete { revisions, .. } = &mut self.review_revisions
1856 else {
1857 return Ok(());
1858 };
1859 let attributed = revisions
1860 .get_mut(review_index)
1861 .ok_or(ProjectionError::InvalidDocumentXml)?;
1862 attributed.content = ReviewDetail::Known(RevisionContent {
1863 span: ReviewSpan { start, end },
1864 payload: RevisionPayload::from_text(revision.text),
1865 });
1866 Ok(())
1867 }
1868
1869 fn locate_owned_revisions(
1870 &mut self,
1871 review_indices: &[usize],
1872 owner: OwnedRevisionSpan,
1873 ) -> Result<(), ProjectionError> {
1874 let ReviewRevisionCollection::Complete { revisions, .. } = &mut self.review_revisions
1875 else {
1876 return Ok(());
1877 };
1878 for &review_index in review_indices {
1879 let revision = revisions
1880 .get_mut(review_index)
1881 .ok_or(ProjectionError::InvalidDocumentXml)?;
1882 let span = match owner {
1883 OwnedRevisionSpan::Content(span) => span,
1884 OwnedRevisionSpan::Paragraph(span) => {
1885 if revision.kind == RevisionFactKind::ParagraphPropertiesChange {
1886 span
1887 } else {
1888 ReviewSpan {
1889 start: span.end,
1890 end: span.end,
1891 }
1892 }
1893 }
1894 };
1895 let payload = match revision.kind {
1896 RevisionFactKind::RunPropertiesChange
1897 | RevisionFactKind::ParagraphPropertiesChange
1898 | RevisionFactKind::TablePropertiesChange
1899 | RevisionFactKind::TablePropertiesExceptionChange
1900 | RevisionFactKind::TableRowPropertiesChange
1901 | RevisionFactKind::TableCellPropertiesChange
1902 | RevisionFactKind::TableGridChange => RevisionPayload::FormattingOnly,
1903 _ => RevisionPayload::ParagraphMark,
1904 };
1905 revision.content = ReviewDetail::Known(RevisionContent { span, payload });
1906 }
1907 Ok(())
1908 }
1909
1910 fn inside_body(&self) -> bool {
1911 self.frames.iter().any(|frame| matches!(frame, Frame::Body))
1912 }
1913
1914 fn inside_textbox(&self) -> bool {
1915 self.frames
1916 .iter()
1917 .any(|frame| matches!(frame, Frame::Textbox))
1918 }
1919
1920 fn inside_change_snapshot(&self) -> bool {
1921 self.frames
1922 .iter()
1923 .any(|frame| matches!(frame, Frame::ChangeSnapshot(_)))
1924 }
1925
1926 fn inside_paragraph_mark_properties(&self) -> bool {
1927 self.current_paragraph.is_some()
1928 && self
1929 .frames
1930 .iter()
1931 .any(|frame| matches!(frame, Frame::ParagraphProperties))
1932 && !self
1933 .frames
1934 .iter()
1935 .any(|frame| matches!(frame, Frame::Run(_)))
1936 }
1937
1938 fn inside_table_row_properties(&self) -> bool {
1939 self.frames
1940 .iter()
1941 .any(|frame| matches!(frame, Frame::TableRowProperties))
1942 }
1943
1944 fn pseudo_text_is_suppressed(&self) -> bool {
1945 self.frames.iter().any(|frame| {
1946 matches!(
1947 frame,
1948 Frame::ParagraphProperties
1949 | Frame::RunProperties(_)
1950 | Frame::ChangeSnapshot(_)
1951 | Frame::Textbox
1952 ) || matches!(frame, Frame::Revision(revision) if revision.suppressed)
1953 || matches!(frame, Frame::Sdt(sdt) if sdt.placeholder)
1954 })
1955 }
1956
1957 fn pseudo_text_is_structurally_suppressed(&self) -> bool {
1958 self.frames.iter().any(|frame| {
1959 matches!(
1960 frame,
1961 Frame::ParagraphProperties
1962 | Frame::RunProperties(_)
1963 | Frame::ChangeSnapshot(_)
1964 | Frame::Textbox
1965 ) || matches!(frame, Frame::Sdt(sdt) if sdt.placeholder)
1966 })
1967 }
1968
1969 fn append_pseudo_text(&mut self, text: &str) -> Result<(), ProjectionError> {
1970 if self.current_paragraph.is_none()
1971 || self.pseudo_text_is_structurally_suppressed()
1972 || self
1973 .frames
1974 .iter()
1975 .rev()
1976 .any(|frame| matches!(frame, Frame::Run(run) if run.hidden))
1977 {
1978 return Ok(());
1979 }
1980 self.append_review_text(text);
1981 if self.pseudo_text_is_suppressed() {
1982 return Ok(());
1983 }
1984 if let Some(run) = self.frames.iter_mut().rev().find_map(|frame| match frame {
1985 Frame::Run(run) => Some(run),
1986 _ => None,
1987 }) {
1988 run.text.push_str(text);
1989 return Ok(());
1990 }
1991 self.current_paragraph
1992 .as_mut()
1993 .ok_or(ProjectionError::InvalidDocumentXml)?
1994 .append(text, TextProperties::default())
1995 }
1996
1997 fn append_review_text(&mut self, text: &str) {
1998 let ReviewRevisionCollection::Complete {
1999 remaining_detail_bytes,
2000 ..
2001 } = &mut self.review_revisions
2002 else {
2003 return;
2004 };
2005 let mut limit_exceeded = false;
2006 for frame in &mut self.frames {
2007 if let Frame::Revision(revision) = frame
2008 && revision.review_index.is_some()
2009 {
2010 let Some(remaining) = remaining_detail_bytes.checked_sub(text.len()) else {
2011 limit_exceeded = true;
2012 break;
2013 };
2014 *remaining_detail_bytes = remaining;
2015 revision.text.push_str(text);
2016 }
2017 }
2018 if limit_exceeded {
2019 self.review_revisions = ReviewRevisionCollection::LimitExceeded;
2020 }
2021 }
2022
2023 fn current_review_point(&self) -> Option<ReviewPoint> {
2024 let paragraph = self.current_paragraph.as_ref()?;
2025 let run_text = if self.pseudo_text_is_suppressed() {
2026 None
2027 } else {
2028 self.frames.iter().rev().find_map(|frame| match frame {
2029 Frame::Run(run) if !run.hidden => Some(run.text.as_str()),
2030 _ => None,
2031 })
2032 };
2033 let run_utf8 = run_text.map_or(0, str::len);
2034 let run_utf16 = run_text.map_or(0, |text| text.encode_utf16().count());
2035 Some(ReviewPoint {
2036 paragraph_ordinal: self.paragraphs.len(),
2037 utf8: u32::try_from(paragraph.text.len().checked_add(run_utf8)?).ok()?,
2038 utf16: paragraph
2039 .utf16_len
2040 .checked_add(u32::try_from(run_utf16).ok()?)?,
2041 })
2042 }
2043
2044 fn next_paragraph_start_review_point(&self) -> Option<ReviewPoint> {
2045 if self.current_paragraph.is_some() {
2046 return self.current_review_point();
2047 }
2048 self.inside_body().then_some(ReviewPoint {
2049 paragraph_ordinal: self.paragraphs.len(),
2050 utf8: 0,
2051 utf16: 0,
2052 })
2053 }
2054
2055 fn previous_paragraph_end_review_point(&self) -> Option<ReviewPoint> {
2056 if self.current_paragraph.is_some() {
2057 return self.current_review_point();
2058 }
2059 self.paragraphs.last().and_then(paragraph_end)
2060 }
2061
2062 fn review_comment_id(
2063 reader: &NsReader<&[u8]>,
2064 element: &BytesStart<'_>,
2065 ) -> Result<Option<String>, ProjectionError> {
2066 attribute(reader, element, b"id")
2067 }
2068
2069 fn start_review_comment_anchor(
2070 &mut self,
2071 reader: &NsReader<&[u8]>,
2072 element: &BytesStart<'_>,
2073 ) -> Result<(), ProjectionError> {
2074 let (Some(comment_id), Some(point)) = (
2075 Self::review_comment_id(reader, element)?,
2076 self.next_paragraph_start_review_point(),
2077 ) else {
2078 return Ok(());
2079 };
2080 if self.invalid_review_comment_anchors.contains(&comment_id) {
2081 return Ok(());
2082 }
2083 if self
2084 .open_review_comment_anchors
2085 .insert(comment_id.clone(), point)
2086 .is_some()
2087 {
2088 self.open_review_comment_anchors.remove(&comment_id);
2089 self.review_comment_anchors.remove(&comment_id);
2090 self.invalid_review_comment_anchors.insert(comment_id);
2091 }
2092 Ok(())
2093 }
2094
2095 fn suppress_review_comment_anchor(
2096 &mut self,
2097 reader: &NsReader<&[u8]>,
2098 element: &BytesStart<'_>,
2099 ) -> Result<(), ProjectionError> {
2100 let Some(comment_id) = Self::review_comment_id(reader, element)? else {
2101 return Ok(());
2102 };
2103 self.open_review_comment_anchors.remove(&comment_id);
2104 self.review_comment_anchors.remove(&comment_id);
2105 self.invalid_review_comment_anchors.insert(comment_id);
2106 Ok(())
2107 }
2108
2109 fn end_review_comment_anchor(
2110 &mut self,
2111 reader: &NsReader<&[u8]>,
2112 element: &BytesStart<'_>,
2113 ) -> Result<(), ProjectionError> {
2114 let (Some(comment_id), Some(end)) = (
2115 Self::review_comment_id(reader, element)?,
2116 self.previous_paragraph_end_review_point(),
2117 ) else {
2118 return Ok(());
2119 };
2120 if self.invalid_review_comment_anchors.contains(&comment_id) {
2121 return Ok(());
2122 }
2123 let Some(start) = self.open_review_comment_anchors.remove(&comment_id) else {
2124 self.review_comment_anchors.remove(&comment_id);
2125 self.invalid_review_comment_anchors.insert(comment_id);
2126 return Ok(());
2127 };
2128 if self
2129 .review_comment_anchors
2130 .insert(comment_id.clone(), ReviewSpan { start, end })
2131 .is_some()
2132 {
2133 self.review_comment_anchors.remove(&comment_id);
2134 self.invalid_review_comment_anchors.insert(comment_id);
2135 }
2136 Ok(())
2137 }
2138
2139 fn record_review_comment_reference(
2140 &mut self,
2141 reader: &NsReader<&[u8]>,
2142 element: &BytesStart<'_>,
2143 ) -> Result<(), ProjectionError> {
2144 let (Some(comment_id), Some(point)) = (
2145 Self::review_comment_id(reader, element)?,
2146 self.current_review_point(),
2147 ) else {
2148 return Ok(());
2149 };
2150 if self.invalid_review_comment_anchors.contains(&comment_id) {
2151 return Ok(());
2152 }
2153 if !self.review_comment_anchors.contains_key(&comment_id)
2154 && !self.open_review_comment_anchors.contains_key(&comment_id)
2155 {
2156 self.review_comment_anchors.insert(
2157 comment_id,
2158 ReviewSpan {
2159 start: point,
2160 end: point,
2161 },
2162 );
2163 }
2164 Ok(())
2165 }
2166
2167 fn append_pseudo_content(&mut self, text: &str) -> Result<(), ProjectionError> {
2168 let Some(Frame::PseudoText(frame)) = self.frames.last_mut() else {
2169 return Err(ProjectionError::InvalidDocumentXml);
2170 };
2171 frame.text.push_str(text);
2172 Ok(())
2173 }
2174
2175 fn append_text_control(&mut self, control: TextControl) -> Result<(), ProjectionError> {
2176 if let Some(text) = control.materialize(self.text_materialization) {
2177 self.append_pseudo_text(text)?;
2178 }
2179 Ok(())
2180 }
2181
2182 fn append_field_instruction(&mut self, text: &str) {
2183 let Some(field) = self.fields.last_mut() else {
2184 self.references_complete = false;
2185 return;
2186 };
2187 if matches!(field.source, FieldSource::Hidden) {
2188 return;
2189 }
2190 if field.separated || field.instruction.len().saturating_add(text.len()) > 4096 {
2191 self.references_complete = false;
2192 return;
2193 }
2194 field.instruction.push_str(text);
2195 }
2196
2197 fn simple_field(
2198 &mut self,
2199 reader: &NsReader<&[u8]>,
2200 element: &BytesStart<'_>,
2201 ) -> Result<(), ProjectionError> {
2202 let instruction = attribute(reader, element, b"instr")?;
2203 let source = self.current_field_source();
2204 if matches!(source, FieldSource::Hidden) {
2205 return Ok(());
2206 }
2207 let Some(instruction) = instruction.filter(|value| value.len() <= 4096) else {
2208 self.references_complete = false;
2209 return Ok(());
2210 };
2211 self.record_field_reference(&instruction, source);
2212 Ok(())
2213 }
2214
2215 fn field_character(
2216 &mut self,
2217 reader: &NsReader<&[u8]>,
2218 element: &BytesStart<'_>,
2219 ) -> Result<(), ProjectionError> {
2220 match attribute(reader, element, b"fldCharType")?.as_deref() {
2221 Some("begin") => self.fields.push(FieldFrame {
2222 source: self.current_field_source(),
2223 instruction: String::new(),
2224 separated: false,
2225 }),
2226 Some("separate") => {
2227 let Some(mut field) = self.fields.pop() else {
2228 self.references_complete = false;
2229 return Ok(());
2230 };
2231 if field.separated {
2232 self.references_complete = false;
2233 } else {
2234 self.record_field_reference(&field.instruction, field.source);
2235 field.separated = true;
2236 }
2237 self.fields.push(field);
2238 }
2239 Some("end") => {
2240 let Some(field) = self.fields.pop() else {
2241 self.references_complete = false;
2242 return Ok(());
2243 };
2244 if !field.separated {
2245 self.record_field_reference(&field.instruction, field.source);
2246 }
2247 }
2248 _ => self.references_complete = false,
2249 }
2250 Ok(())
2251 }
2252
2253 fn current_field_source(&self) -> FieldSource {
2254 if self.pseudo_text_is_suppressed() {
2255 return FieldSource::Hidden;
2256 }
2257 self.current_review_point()
2258 .map_or(FieldSource::Unsupported, FieldSource::Projected)
2259 }
2260
2261 fn record_field_reference(&mut self, instruction: &str, source: FieldSource) {
2262 if matches!(source, FieldSource::Hidden) {
2263 return;
2264 }
2265 match internal_reference_target(instruction) {
2266 Ok(Some(reference_id)) => {
2267 let FieldSource::Projected(source) = source else {
2268 self.references_complete = false;
2269 return;
2270 };
2271 self.references.push(RawInternalReference {
2272 reference_id,
2273 source,
2274 });
2275 }
2276 Ok(None) => {}
2277 Err(()) => self.references_complete = false,
2278 }
2279 }
2280
2281 fn current_point(&self) -> Option<ReviewPoint> {
2282 if self.current_paragraph.is_some() {
2283 return self.current_review_point();
2284 }
2285 if !matches!(self.frames.last(), Some(Frame::Body)) {
2286 return None;
2287 }
2288 self.paragraphs
2289 .last()
2290 .map_or(Some(paragraph_start(0)), paragraph_end)
2291 }
2292
2293 fn current_bookmark_point(&self) -> Option<BookmarkPoint> {
2294 if self.current_paragraph.is_some() {
2295 return self.current_point().map(BookmarkPoint::Paragraph);
2296 }
2297 matches!(
2298 self.frames.last(),
2299 Some(
2300 Frame::Body
2301 | Frame::BlockContent
2302 | Frame::Table(_)
2303 | Frame::Row(_)
2304 | Frame::Cell(_)
2305 )
2306 )
2307 .then(|| BookmarkPoint::ParagraphBoundary(self.paragraphs.len()))
2308 }
2309
2310 fn start_bookmark(
2311 &mut self,
2312 reader: &NsReader<&[u8]>,
2313 element: &BytesStart<'_>,
2314 ) -> Result<(), ProjectionError> {
2315 let id = attribute(reader, element, b"id")?.and_then(|value| value.parse::<u32>().ok());
2316 let name = attribute(reader, element, b"name")?;
2317 let point = self.current_bookmark_point();
2318 let (Some(id), Some(name), Some(point)) = (id, name, point) else {
2319 self.bookmarks_complete = false;
2320 return Ok(());
2321 };
2322 if name.is_empty()
2323 || !self.seen_bookmark_ids.insert(id)
2324 || self.open_bookmarks.insert(id, (name, point)).is_some()
2325 {
2326 self.bookmarks_complete = false;
2327 }
2328 Ok(())
2329 }
2330
2331 fn end_bookmark(
2332 &mut self,
2333 reader: &NsReader<&[u8]>,
2334 element: &BytesStart<'_>,
2335 ) -> Result<(), ProjectionError> {
2336 let id = attribute(reader, element, b"id")?.and_then(|value| value.parse::<u32>().ok());
2337 let point = self.current_bookmark_point();
2338 let (Some(id), Some(end)) = (id, point) else {
2339 self.bookmarks_complete = false;
2340 return Ok(());
2341 };
2342 let Some((name, start)) = self.open_bookmarks.remove(&id) else {
2343 self.bookmarks_complete = false;
2344 return Ok(());
2345 };
2346 self.bookmarks.push(PendingBookmarkRange {
2347 id,
2348 name,
2349 start,
2350 end,
2351 });
2352 Ok(())
2353 }
2354
2355 fn mark_placeholder(&mut self) {
2356 let Some(sdt) = self.frames.iter_mut().rev().find_map(|frame| match frame {
2357 Frame::Sdt(sdt) => Some(sdt),
2358 _ => None,
2359 }) else {
2360 return;
2361 };
2362 sdt.placeholder = true;
2363 if let Some(paragraph) = self.current_paragraph.as_mut() {
2364 paragraph.truncate(sdt.text_utf8_start, sdt.text_utf16_start);
2365 }
2366 }
2367
2368 fn mark_hidden_run(&mut self) {
2369 let Some((run_frame, hidden_eligible)) =
2370 self.frames.iter().rev().find_map(|frame| match frame {
2371 Frame::RunProperties(properties) => {
2372 Some((properties.run_frame, properties.hidden_eligible))
2373 }
2374 _ => None,
2375 })
2376 else {
2377 return;
2378 };
2379 if !hidden_eligible || run_frame == usize::MAX {
2380 return;
2381 }
2382 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2383 run.hidden = true;
2384 }
2385 }
2386
2387 fn set_run_character_style(&mut self, style_id: Option<String>) {
2388 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2389 return;
2390 };
2391 let run_frame = properties.run_frame;
2392 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2393 run.character_style_id = style_id;
2394 }
2395 }
2396
2397 fn mark_bold_run(&mut self, enabled: bool) {
2398 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2399 return;
2400 };
2401 let run_frame = properties.run_frame;
2402 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2403 run.direct_styles.bold = Some(enabled);
2404 }
2405 }
2406
2407 fn mark_complex_script_bold_run(&mut self, enabled: bool) {
2408 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2409 return;
2410 };
2411 let run_frame = properties.run_frame;
2412 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2413 run.direct_styles.complex_script_bold = Some(enabled);
2414 }
2415 }
2416
2417 fn mark_complex_script_run(&mut self, enabled: bool) {
2418 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2419 return;
2420 };
2421 let run_frame = properties.run_frame;
2422 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2423 run.direct_styles.force_complex_script = Some(enabled);
2424 }
2425 }
2426
2427 fn mark_right_to_left_run(&mut self, enabled: bool) {
2428 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2429 return;
2430 };
2431 let run_frame = properties.run_frame;
2432 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2433 run.direct_styles.right_to_left = Some(enabled);
2434 }
2435 }
2436
2437 fn mark_highlighted_run(&mut self, enabled: bool) {
2438 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2439 return;
2440 };
2441 let run_frame = properties.run_frame;
2442 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2443 run.direct_styles.highlighted = Some(enabled);
2444 }
2445 }
2446
2447 fn mark_superscript_run(&mut self, enabled: bool) {
2448 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2449 return;
2450 };
2451 let run_frame = properties.run_frame;
2452 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2453 run.direct_styles.superscript = Some(enabled);
2454 }
2455 }
2456}
2457
2458const fn revision_fact_kind(name: &[u8]) -> Option<RevisionFactKind> {
2459 match name {
2460 b"ins" => Some(RevisionFactKind::Insertion),
2461 b"del" => Some(RevisionFactKind::Deletion),
2462 b"moveFrom" => Some(RevisionFactKind::MoveFrom),
2463 b"moveTo" => Some(RevisionFactKind::MoveTo),
2464 b"cellIns" => Some(RevisionFactKind::CellInsertion),
2465 b"cellDel" => Some(RevisionFactKind::CellDeletion),
2466 b"cellMerge" => Some(RevisionFactKind::CellMerge),
2467 b"pPrChange" => Some(RevisionFactKind::ParagraphPropertiesChange),
2468 b"rPrChange" => Some(RevisionFactKind::RunPropertiesChange),
2469 b"sectPrChange" => Some(RevisionFactKind::SectionPropertiesChange),
2470 b"tblPrChange" => Some(RevisionFactKind::TablePropertiesChange),
2471 b"tblPrExChange" => Some(RevisionFactKind::TablePropertiesExceptionChange),
2472 b"trPrChange" => Some(RevisionFactKind::TableRowPropertiesChange),
2473 b"tcPrChange" => Some(RevisionFactKind::TableCellPropertiesChange),
2474 b"tblGridChange" => Some(RevisionFactKind::TableGridChange),
2475 b"customXmlDelRangeStart" => Some(RevisionFactKind::CustomXmlDeletionRangeStart),
2476 b"customXmlDelRangeEnd" => Some(RevisionFactKind::CustomXmlDeletionRangeEnd),
2477 b"customXmlInsRangeStart" => Some(RevisionFactKind::CustomXmlInsertionRangeStart),
2478 b"customXmlInsRangeEnd" => Some(RevisionFactKind::CustomXmlInsertionRangeEnd),
2479 b"customXmlMoveFromRangeStart" => Some(RevisionFactKind::CustomXmlMoveFromRangeStart),
2480 b"customXmlMoveFromRangeEnd" => Some(RevisionFactKind::CustomXmlMoveFromRangeEnd),
2481 b"customXmlMoveToRangeStart" => Some(RevisionFactKind::CustomXmlMoveToRangeStart),
2482 b"customXmlMoveToRangeEnd" => Some(RevisionFactKind::CustomXmlMoveToRangeEnd),
2483 _ => None,
2484 }
2485}
2486
2487fn enclosing_table(frames: &mut [Frame]) -> Option<&mut TableFrame> {
2488 for frame in frames.iter_mut().rev() {
2489 match frame {
2490 Frame::Table(table) => return Some(table),
2491 Frame::Row(_) | Frame::Cell(_) | Frame::Paragraph | Frame::Run(_) => return None,
2492 _ => {}
2493 }
2494 }
2495 None
2496}
2497
2498fn enclosing_row(frames: &mut [Frame]) -> Option<&mut RowFrame> {
2499 for frame in frames.iter_mut().rev() {
2500 match frame {
2501 Frame::Row(row) => return Some(row),
2502 Frame::Table(_) | Frame::Cell(_) | Frame::Paragraph | Frame::Run(_) => return None,
2503 _ => {}
2504 }
2505 }
2506 None
2507}
2508
2509fn revision_is_suppressed(name: &[u8], view: RevisionView) -> bool {
2510 match view {
2511 RevisionView::Current => matches!(name, b"del" | b"moveFrom"),
2512 RevisionView::Original => matches!(name, b"ins" | b"moveTo"),
2513 }
2514}
2515
2516fn is_change_snapshot(name: &[u8]) -> bool {
2517 matches!(
2518 name,
2519 b"pPrChange"
2520 | b"rPrChange"
2521 | b"sectPrChange"
2522 | b"tblPrChange"
2523 | b"tblPrExChange"
2524 | b"trPrChange"
2525 | b"tcPrChange"
2526 | b"tblGridChange"
2527 )
2528}
2529
2530fn is_unsupported_revision_markup(name: &[u8]) -> bool {
2531 matches!(
2532 name,
2533 b"customXmlDelRangeStart"
2534 | b"customXmlDelRangeEnd"
2535 | b"customXmlInsRangeStart"
2536 | b"customXmlInsRangeEnd"
2537 | b"customXmlMoveFromRangeStart"
2538 | b"customXmlMoveFromRangeEnd"
2539 | b"customXmlMoveToRangeStart"
2540 | b"customXmlMoveToRangeEnd"
2541 )
2542}
2543
2544const fn paragraph_break_is_removed(
2545 revision: Option<ParagraphMarkRevision>,
2546 view: RevisionView,
2547) -> bool {
2548 matches!(
2549 (revision, view),
2550 (Some(ParagraphMarkRevision::Removed), RevisionView::Current)
2551 | (Some(ParagraphMarkRevision::Added), RevisionView::Original)
2552 )
2553}
2554
2555#[inline(never)]
2557fn append_formatting_span(spans: &mut Vec<TextFormattingSpan>, span: TextFormattingSpan) {
2558 if let Some(adjacent) = spans
2559 .iter_mut()
2560 .rev()
2561 .find(|previous| previous.style == span.style)
2562 && adjacent.end_utf16 == span.start_utf16
2563 {
2564 adjacent.end_utf16 = span.end_utf16;
2565 return;
2566 }
2567 spans.push(span);
2568}
2569
2570fn normalize_paragraph_revision_view(
2574 paragraphs: &mut Vec<RawProjectedParagraph>,
2575 paragraph_mark_revisions: &mut HashMap<usize, ParagraphMarkRevision>,
2576 view: RevisionView,
2577 unsupported: &mut BTreeSet<RevisionUnsupportedReason>,
2578) -> Result<Option<Vec<ReviewPoint>>, ProjectionError> {
2579 let mut normalized: Vec<RawProjectedParagraph> = Vec::with_capacity(paragraphs.len());
2580 let mut origins = Vec::with_capacity(paragraphs.len());
2581 let mut merge_previous = false;
2582 let mut merged_any = false;
2583
2584 for (source_ordinal, mut paragraph) in std::mem::take(paragraphs).into_iter().enumerate() {
2585 let merge_next =
2586 paragraph_break_is_removed(paragraph_mark_revisions.remove(&source_ordinal), view);
2587 if merge_previous {
2588 let previous = normalized
2589 .last_mut()
2590 .ok_or(ProjectionError::InvalidDocumentXml)?;
2591 if previous.structure != paragraph.structure {
2592 unsupported.insert(RevisionUnsupportedReason::IncompatibleParagraphMerge);
2593 paragraph.ordinal = normalized.len();
2594 origins.push(paragraph_start(paragraph.ordinal));
2595 normalized.push(paragraph);
2596 merge_previous = merge_next;
2597 continue;
2598 }
2599 let utf16_offset = previous.utf16_len;
2600 origins.push(ReviewPoint {
2601 paragraph_ordinal: previous.ordinal,
2602 utf8: u32::try_from(previous.text.len())
2603 .map_err(|_| ProjectionError::InvalidDocumentXml)?,
2604 utf16: utf16_offset,
2605 });
2606 for mut span in paragraph.formatting {
2607 span.start_utf16 = span
2608 .start_utf16
2609 .checked_add(utf16_offset)
2610 .ok_or(ProjectionError::InvalidDocumentXml)?;
2611 span.end_utf16 = span
2612 .end_utf16
2613 .checked_add(utf16_offset)
2614 .ok_or(ProjectionError::InvalidDocumentXml)?;
2615 append_formatting_span(&mut previous.formatting, span);
2616 }
2617 previous.text.push_str(¶graph.text);
2618 previous.utf16_len = previous
2619 .utf16_len
2620 .checked_add(paragraph.utf16_len)
2621 .ok_or(ProjectionError::InvalidDocumentXml)?;
2622 merge_previous = merge_next;
2623 merged_any = true;
2624 continue;
2625 }
2626
2627 paragraph.ordinal = normalized.len();
2628 origins.push(paragraph_start(paragraph.ordinal));
2629 normalized.push(paragraph);
2630 merge_previous = merge_next;
2631 }
2632
2633 *paragraphs = normalized;
2634 Ok(merged_any.then_some(origins))
2635}
2636
2637const fn paragraph_start(paragraph_ordinal: usize) -> ReviewPoint {
2638 ReviewPoint {
2639 paragraph_ordinal,
2640 utf8: 0,
2641 utf16: 0,
2642 }
2643}
2644
2645fn relocate_review_point(origins: &[ReviewPoint], point: ReviewPoint) -> Option<ReviewPoint> {
2648 let origin = origins.get(point.paragraph_ordinal)?;
2649 Some(ReviewPoint {
2650 paragraph_ordinal: origin.paragraph_ordinal,
2651 utf8: origin.utf8.checked_add(point.utf8)?,
2652 utf16: origin.utf16.checked_add(point.utf16)?,
2653 })
2654}
2655
2656fn relocate_block_point(
2657 origins: &[ReviewPoint],
2658 point: &mut ReviewPoint,
2659) -> Result<(), ProjectionError> {
2660 *point = relocate_review_point(origins, *point).ok_or(ProjectionError::InvalidDocumentXml)?;
2661 Ok(())
2662}
2663
2664fn relocate_review_span(origins: &[ReviewPoint], span: ReviewSpan) -> Option<ReviewSpan> {
2665 Some(ReviewSpan {
2666 start: relocate_review_point(origins, span.start)?,
2667 end: relocate_review_point(origins, span.end)?,
2668 })
2669}
2670
2671fn internal_reference_target(instruction: &str) -> Result<Option<String>, ()> {
2672 let tokens = field_tokens(instruction)?;
2673 let Some(command) = tokens.first() else {
2674 return Err(());
2675 };
2676 if ["REF", "PAGEREF", "NOTEREF"]
2677 .iter()
2678 .any(|candidate| command.eq_ignore_ascii_case(candidate))
2679 {
2680 return tokens
2681 .get(1)
2682 .filter(|target| !target.is_empty() && !target.starts_with('\\'))
2683 .cloned()
2684 .map(Some)
2685 .ok_or(());
2686 }
2687 if command.eq_ignore_ascii_case("HYPERLINK") {
2688 for (index, token) in tokens.iter().enumerate().skip(1) {
2689 if token.eq_ignore_ascii_case("\\l") {
2690 return tokens
2691 .get(index.checked_add(1).ok_or(())?)
2692 .filter(|target| !target.is_empty())
2693 .cloned()
2694 .map(Some)
2695 .ok_or(());
2696 }
2697 }
2698 }
2699 Ok(None)
2700}
2701
2702fn field_tokens(instruction: &str) -> Result<Vec<String>, ()> {
2703 let mut tokens = Vec::new();
2704 let mut token = String::new();
2705 let mut quoted = false;
2706 for character in instruction.chars() {
2707 match character {
2708 '"' => quoted = !quoted,
2709 whitespace if whitespace.is_whitespace() && !quoted => {
2710 if !token.is_empty() {
2711 tokens.push(std::mem::take(&mut token));
2712 }
2713 }
2714 _ => token.push(character),
2715 }
2716 }
2717 if quoted {
2718 return Err(());
2719 }
2720 if !token.is_empty() {
2721 tokens.push(token);
2722 }
2723 Ok(tokens)
2724}
2725
2726fn attribute(
2727 reader: &NsReader<&[u8]>,
2728 element: &BytesStart<'_>,
2729 name: &[u8],
2730) -> Result<Option<String>, ProjectionError> {
2731 for attribute in element.attributes() {
2732 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2733 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2734 if OoxmlNamespace::from_resolved(&namespace) == OoxmlNamespace::Wordprocessing
2735 && local_name.as_ref() == name
2736 {
2737 return attribute
2738 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
2739 .map(|value| Some(value.into_owned()))
2740 .map_err(|_| ProjectionError::InvalidDocumentXml);
2741 }
2742 }
2743 Ok(None)
2744}
2745
2746fn preserves_xml_space(
2747 reader: &NsReader<&[u8]>,
2748 element: &BytesStart<'_>,
2749) -> Result<bool, ProjectionError> {
2750 const XML_NAMESPACE: &[u8] = b"http://www.w3.org/XML/1998/namespace";
2751
2752 for attribute in element.attributes() {
2753 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2754 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2755 if matches!(
2756 namespace,
2757 quick_xml::name::ResolveResult::Bound(namespace)
2758 if namespace.as_ref() == XML_NAMESPACE
2759 ) && local_name.as_ref() == b"space"
2760 {
2761 let value = attribute
2762 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
2763 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
2764 return match value.as_ref() {
2765 "default" => Ok(false),
2766 "preserve" => Ok(true),
2767 _ => Err(ProjectionError::InvalidDocumentXml),
2768 };
2769 }
2770 }
2771 Ok(false)
2772}
2773
2774fn on_off_element_enabled(
2775 reader: &NsReader<&[u8]>,
2776 element: &BytesStart<'_>,
2777) -> Result<bool, ProjectionError> {
2778 Ok(attribute(reader, element, b"val")?
2779 .is_none_or(|value| !matches!(value.to_ascii_lowercase().as_str(), "0" | "false" | "off")))
2780}
2781
2782fn trim_xml_whitespace(value: &str) -> &str {
2783 value.trim_matches(['\u{0009}', '\u{000a}', '\u{000d}', '\u{0020}'])
2784}
2785
2786fn attribute_2010(
2787 reader: &NsReader<&[u8]>,
2788 element: &BytesStart<'_>,
2789 name: &[u8],
2790) -> Result<Option<String>, ProjectionError> {
2791 for attribute in element.attributes() {
2792 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2793 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2794 if OoxmlNamespace::from_resolved(&namespace) == OoxmlNamespace::Wordprocessing2010
2795 && local_name.as_ref() == name
2796 {
2797 return attribute
2798 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
2799 .map(|value| Some(value.into_owned()))
2800 .map_err(|_| ProjectionError::InvalidDocumentXml);
2801 }
2802 }
2803 Ok(None)
2804}
2805
2806fn symbol_text(value: &str, font: Option<&str>) -> String {
2807 let prefix = value
2808 .chars()
2809 .take_while(char::is_ascii_hexdigit)
2810 .collect::<String>();
2811 let Some(code) = (!prefix.is_empty())
2812 .then(|| u32::from_str_radix(&prefix, 16).ok())
2813 .flatten()
2814 else {
2815 return value.to_owned();
2816 };
2817 if let Some(replacement) = normalized_font_symbol(font, code) {
2818 return replacement.to_owned();
2819 }
2820 char::from_u32(code).unwrap_or('\u{fffd}').to_string()
2821}
2822
2823struct FontSymbolMapping {
2824 font: &'static str,
2825 encoded: u32,
2826 unicode: &'static str,
2827}
2828
2829const FONT_SYMBOL_MAPPINGS: &[FontSymbolMapping] = &[FontSymbolMapping {
2836 font: "Wingdings",
2837 encoded: 0xF06C,
2838 unicode: "●",
2839}];
2840
2841fn normalized_font_symbol(font: Option<&str>, encoded: u32) -> Option<&'static str> {
2842 let font = font?;
2843 FONT_SYMBOL_MAPPINGS
2844 .iter()
2845 .find(|mapping| mapping.encoded == encoded && mapping.font.eq_ignore_ascii_case(font))
2846 .map(|mapping| mapping.unicode)
2847}