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