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