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