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_alignment, parse_indentation, parse_level_attribute, parse_outline_level_attribute,
21 parse_u32_attribute, semantic_highlight_value, word_style_id,
22};
23use crate::{FormattingProjectionStatus, FormattingUnknownReason, ProjectionError};
24
25#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26pub struct PackageParagraphId(u32);
27
28impl PackageParagraphId {
29 #[must_use]
30 pub fn parse(value: &str) -> Option<Self> {
31 if value.len() != 8 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
32 return None;
33 }
34 let value = u32::from_str_radix(value, 16).ok()?;
35 (value != 0 && i32::try_from(value).is_ok()).then_some(Self(value))
36 }
37
38 #[must_use]
39 pub const fn value(self) -> u32 {
40 self.0
41 }
42}
43
44#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
45pub enum TextStyle {
46 Bold,
47 Highlight,
48 Superscript,
49}
50
51#[derive(Clone, Debug, Eq, PartialEq)]
52pub struct TextFormattingSpan {
53 pub start_utf16: u32,
54 pub end_utf16: u32,
55 pub style: TextStyle,
56}
57
58#[derive(Clone, Debug, Eq, PartialEq)]
59pub struct ParagraphStructure {
60 pub table_ordinal: usize,
61 pub row: usize,
62 pub column: usize,
63}
64
65#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
66pub enum RevisionView {
67 #[default]
68 Current,
69 Original,
70}
71
72#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
73pub enum TextMaterialization {
74 #[default]
75 WordHost,
76 ReadablePlainText,
77}
78
79#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
80pub enum RevisionUnsupportedReason {
81 IncompatibleParagraphMerge,
82 StructuralTableRevision,
83 UnsupportedRevisionMarkup,
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub enum RevisionProjectionStatus {
88 Complete,
89 Incomplete(Vec<RevisionUnsupportedReason>),
90}
91
92impl RevisionProjectionStatus {
93 #[must_use]
94 pub const fn is_complete(&self) -> bool {
95 matches!(self, Self::Complete)
96 }
97}
98
99#[derive(Clone, Copy, Debug, Eq, PartialEq)]
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"jc" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1074 if let Some(paragraph) = self.current_paragraph.as_mut() {
1075 paragraph.properties.alignment = Some(parse_alignment(reader, element)?);
1076 }
1077 Frame::Other
1078 }
1079 b"outlineLvl" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1080 if let Some(paragraph) = self.current_paragraph.as_mut() {
1081 paragraph.properties.outline_level =
1082 Some(parse_outline_level_attribute(reader, element)?);
1083 }
1084 Frame::Other
1085 }
1086 b"numPr" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
1087 if let Some(paragraph) = self.current_paragraph.as_mut() {
1088 paragraph.properties.numbering.present = true;
1089 }
1090 Frame::NumberingProperties
1091 }
1092 b"numId" if matches!(self.frames.last(), Some(Frame::NumberingProperties)) => {
1093 if let Some(paragraph) = self.current_paragraph.as_mut() {
1094 paragraph.properties.numbering.num_id =
1095 Some(parse_u32_attribute(reader, element)?);
1096 }
1097 Frame::Other
1098 }
1099 b"ilvl" if matches!(self.frames.last(), Some(Frame::NumberingProperties)) => {
1100 if let Some(paragraph) = self.current_paragraph.as_mut() {
1101 paragraph.properties.numbering.level =
1102 Some(parse_level_attribute(reader, element)?);
1103 }
1104 Frame::Other
1105 }
1106 b"bookmarkStart" => {
1107 self.start_bookmark(reader, element)?;
1108 Frame::Other
1109 }
1110 b"bookmarkEnd" => {
1111 self.end_bookmark(reader, element)?;
1112 Frame::Other
1113 }
1114 b"hyperlink" => {
1115 let anchor = attribute(reader, element, b"anchor")?;
1116 let reference =
1117 anchor
1118 .clone()
1119 .filter(|anchor| !anchor.is_empty())
1120 .and_then(|anchor| {
1121 self.current_point().map(|source| RawInternalReference {
1122 reference_id: anchor,
1123 source,
1124 })
1125 });
1126 if anchor.is_some() && reference.is_none() {
1127 self.references_complete = false;
1128 }
1129 Frame::Hyperlink(reference)
1130 }
1131 b"fldSimple" => {
1132 self.simple_field(reader, element)?;
1133 Frame::Other
1134 }
1135 b"fldChar" => {
1136 self.field_character(reader, element)?;
1137 Frame::Other
1138 }
1139 b"instrText" | b"delInstrText" => Frame::PseudoText(PseudoTextFrame::instruction()),
1140 b"ins" | b"del" | b"moveFrom" | b"moveTo" => {
1141 if self.inside_table_row_properties() {
1142 self.revision_unsupported
1143 .insert(RevisionUnsupportedReason::StructuralTableRevision);
1144 Frame::Other
1145 } else if self.inside_paragraph_mark_properties() && matches!(name, b"ins" | b"del")
1146 {
1147 if let Some(paragraph) = self.current_paragraph.as_mut() {
1148 paragraph.paragraph_mark_revision = Some(if name == b"ins" {
1149 ParagraphMarkRevision::Insertion
1150 } else {
1151 ParagraphMarkRevision::Deletion
1152 });
1153 }
1154 Frame::Other
1155 } else {
1156 Frame::Revision(RevisionFrame {
1157 review_index: attributed_revision,
1158 start: self.current_review_point(),
1159 suppressed: revision_is_suppressed(name, self.revision_view),
1160 text: String::new(),
1161 })
1162 }
1163 }
1164 b"cellIns" | b"cellDel" | b"cellMerge" => {
1165 self.revision_unsupported
1166 .insert(RevisionUnsupportedReason::StructuralTableRevision);
1167 Frame::Other
1168 }
1169 b"sdt" => {
1170 let (text_utf8_start, text_utf16_start) =
1171 self.current_paragraph.as_ref().map_or((0, 0), |paragraph| {
1172 (paragraph.text.len(), paragraph.utf16_len)
1173 });
1174 Frame::Sdt(SdtFrame {
1175 placeholder: false,
1176 text_utf8_start,
1177 text_utf16_start,
1178 })
1179 }
1180 b"showingPlcHdr" => {
1181 if on_off_element_enabled(reader, element)? {
1182 self.mark_placeholder();
1183 }
1184 Frame::Other
1185 }
1186 b"vanish" => {
1187 if on_off_element_enabled(reader, element)? {
1188 self.mark_hidden_run();
1189 }
1190 Frame::Other
1191 }
1192 b"rStyle" => {
1193 let style_id = word_style_id(attribute(reader, element, b"val")?);
1194 if matches!(style_id.as_deref(), Some("HideTWBInt" | "HideTWBExt")) {
1195 self.mark_hidden_run();
1196 }
1197 self.set_run_character_style(style_id);
1198 Frame::Other
1199 }
1200 b"b" => {
1201 self.mark_bold_run(on_off_element_enabled(reader, element)?);
1202 Frame::Other
1203 }
1204 b"bCs" => {
1205 self.mark_complex_script_bold_run(on_off_element_enabled(reader, element)?);
1206 Frame::Other
1207 }
1208 b"cs" => {
1209 self.mark_complex_script_run(on_off_element_enabled(reader, element)?);
1210 Frame::Other
1211 }
1212 b"rtl" => {
1213 self.mark_right_to_left_run(on_off_element_enabled(reader, element)?);
1214 Frame::Other
1215 }
1216 b"highlight" => {
1217 if let Some(highlighted) = attribute(reader, element, b"val")?
1218 .as_deref()
1219 .and_then(semantic_highlight_value)
1220 {
1221 self.mark_highlighted_run(highlighted);
1222 }
1223 Frame::Other
1224 }
1225 b"vertAlign" => {
1226 match attribute(reader, element, b"val")?.as_deref() {
1227 Some("superscript") => self.mark_superscript_run(true),
1228 Some("baseline" | "subscript") => self.mark_superscript_run(false),
1229 _ => {}
1230 }
1231 Frame::Other
1232 }
1233 b"t" | b"delText" => {
1234 Frame::PseudoText(PseudoTextFrame::text(preserves_xml_space(reader, element)?))
1235 }
1236 b"tab" | b"ptab" => {
1237 self.append_text_control(TextControl::Tab)?;
1238 Frame::Other
1239 }
1240 b"br" => {
1241 let control = match attribute(reader, element, b"type")?.as_deref() {
1242 Some("page") => TextControl::PageBreak,
1243 Some("column") => TextControl::ColumnBreak,
1244 _ => TextControl::LineBreak,
1245 };
1246 self.append_text_control(control)?;
1247 Frame::Other
1248 }
1249 b"cr" => {
1250 self.append_text_control(TextControl::CarriageReturn)?;
1251 Frame::Other
1252 }
1253 b"footnoteReference" => {
1254 let visible =
1255 attribute(reader, element, b"customMarkFollows")?.is_none_or(|value| {
1256 matches!(value.to_ascii_lowercase().as_str(), "0" | "false" | "off")
1257 });
1258 if visible {
1259 self.append_text_control(TextControl::FootnoteReference)?;
1260 }
1261 Frame::Other
1262 }
1263 b"softHyphen" => {
1264 self.append_text_control(TextControl::SoftHyphen)?;
1265 Frame::Other
1266 }
1267 b"noBreakHyphen" => {
1268 self.append_text_control(TextControl::NoBreakHyphen)?;
1269 Frame::Other
1270 }
1271 b"sym" => {
1272 if let Some(value) = attribute(reader, element, b"char")? {
1273 let font = attribute(reader, element, b"font")?;
1274 self.append_pseudo_text(&symbol_text(&value, font.as_deref()))?;
1275 }
1276 Frame::Other
1277 }
1278 _ => Frame::Other,
1279 };
1280 self.frames.push(frame);
1281 Ok(())
1282 }
1283
1284 fn end(
1285 &mut self,
1286 styles: Result<&StyleSheet, FormattingUnknownReason>,
1287 ) -> Result<(), ProjectionError> {
1288 let frame = self
1289 .frames
1290 .pop()
1291 .ok_or(ProjectionError::InvalidDocumentXml)?;
1292 match frame {
1293 Frame::Paragraph => {
1294 let mut paragraph = self
1295 .current_paragraph
1296 .take()
1297 .ok_or(ProjectionError::InvalidDocumentXml)?;
1298 if styles.is_ok_and(|styles| paragraph.resolve_text_base(styles).is_err()) {
1299 self.formatting_status = FormattingProjectionStatus::Incomplete(
1300 FormattingUnknownReason::UnsupportedStyles,
1301 );
1302 }
1303 if styles.is_ok_and(|styles| {
1304 styles
1305 .paragraph_uses_numbering(¶graph.properties)
1306 .unwrap_or(false)
1307 }) {
1308 self.formatting_status = FormattingProjectionStatus::Incomplete(
1312 FormattingUnknownReason::UnsupportedStyles,
1313 );
1314 }
1315 if paragraph.structure.is_some()
1316 && self.formatting_status == FormattingProjectionStatus::Complete
1317 {
1318 self.formatting_status = FormattingProjectionStatus::Incomplete(
1322 FormattingUnknownReason::UnsupportedStyles,
1323 );
1324 }
1325 let ordinal = self.paragraphs.len();
1326 if let Some(revision) = paragraph.paragraph_mark_revision {
1327 self.paragraph_mark_revisions.insert(ordinal, revision);
1328 }
1329 self.paragraphs.push(RawProjectedParagraph {
1330 ordinal: self.paragraphs.len(),
1331 package_paragraph_id: paragraph.package_paragraph_id,
1332 text: paragraph.text,
1333 utf16_len: paragraph.utf16_len,
1334 formatting: paragraph.formatting,
1335 structure: paragraph.structure,
1336 properties: paragraph.properties,
1337 });
1338 }
1339 Frame::Hyperlink(Some(reference)) => self.references.push(reference),
1340 Frame::PseudoText(frame) => match frame.kind {
1341 PseudoTextKind::Text { preserve_space } => {
1342 let text = if preserve_space {
1343 frame.text.as_str()
1344 } else {
1345 trim_xml_whitespace(&frame.text)
1346 };
1347 self.append_pseudo_text(text)?;
1348 }
1349 PseudoTextKind::MathText => self.append_pseudo_text(&frame.text)?,
1350 PseudoTextKind::Instruction => self.append_field_instruction(&frame.text),
1351 },
1352 Frame::Run(run) if !run.hidden && !self.pseudo_text_is_suppressed() => {
1353 if let Some(paragraph) = self.current_paragraph.as_mut() {
1354 let effective = match styles {
1355 Ok(styles) => {
1356 let resolved = if run.character_style_id.is_none()
1357 && run.direct_styles == TextProperties::default()
1358 {
1359 paragraph.resolve_text_base(styles)
1360 } else {
1361 styles.resolve_text(
1362 paragraph.properties.style_id.as_deref(),
1363 run.character_style_id.as_deref(),
1364 run.direct_styles,
1365 )
1366 };
1367 resolved.unwrap_or_else(|()| {
1368 self.formatting_status = FormattingProjectionStatus::Incomplete(
1369 FormattingUnknownReason::UnsupportedStyles,
1370 );
1371 run.direct_styles
1372 })
1373 }
1374 Err(_) => run.direct_styles,
1375 };
1376 paragraph.append(&run.text, effective)?;
1377 }
1378 }
1379 Frame::Revision(revision) => self.finish_attributed_revision(revision)?,
1380 _ => {}
1381 }
1382 Ok(())
1383 }
1384
1385 fn record_attributed_revision(
1386 &mut self,
1387 reader: &NsReader<&[u8]>,
1388 element: &BytesStart<'_>,
1389 name: &[u8],
1390 ) -> Result<Option<usize>, ProjectionError> {
1391 let Some(kind) = revision_fact_kind(name) else {
1392 return Ok(None);
1393 };
1394 let ReviewRevisionCollection::Complete {
1395 maximum_facts,
1396 revisions,
1397 ..
1398 } = &mut self.review_revisions
1399 else {
1400 return Ok(None);
1401 };
1402 if revisions.len() >= *maximum_facts {
1403 self.review_revisions = ReviewRevisionCollection::LimitExceeded;
1404 return Ok(None);
1405 }
1406 let review_index = revisions.len();
1407 revisions.push(AttributedRevision {
1408 kind,
1409 author: attribute(reader, element, b"author")?.unwrap_or_default(),
1410 date: attribute(reader, element, b"date")?,
1411 revision_id: attribute(reader, element, b"id")?,
1412 content: ReviewDetail::Unknown(ReviewFactUnknownReason::UnsupportedLocation),
1413 });
1414 Ok(Some(review_index))
1415 }
1416
1417 fn finish_attributed_revision(
1418 &mut self,
1419 revision: RevisionFrame,
1420 ) -> Result<(), ProjectionError> {
1421 let (Some(review_index), Some(start), Some(end)) = (
1422 revision.review_index,
1423 revision.start,
1424 self.current_review_point(),
1425 ) else {
1426 return Ok(());
1427 };
1428 let ReviewRevisionCollection::Complete { revisions, .. } = &mut self.review_revisions
1429 else {
1430 return Ok(());
1431 };
1432 let attributed = revisions
1433 .get_mut(review_index)
1434 .ok_or(ProjectionError::InvalidDocumentXml)?;
1435 attributed.content = ReviewDetail::Known(RevisionContent {
1436 span: ReviewSpan { start, end },
1437 formatting_only: revision.text.is_empty(),
1438 text: revision.text,
1439 });
1440 Ok(())
1441 }
1442
1443 fn inside_body(&self) -> bool {
1444 self.frames.iter().any(|frame| matches!(frame, Frame::Body))
1445 }
1446
1447 fn inside_textbox(&self) -> bool {
1448 self.frames
1449 .iter()
1450 .any(|frame| matches!(frame, Frame::Textbox))
1451 }
1452
1453 fn inside_change_snapshot(&self) -> bool {
1454 self.frames
1455 .iter()
1456 .any(|frame| matches!(frame, Frame::ChangeSnapshot))
1457 }
1458
1459 fn inside_paragraph_mark_properties(&self) -> bool {
1460 self.current_paragraph.is_some()
1461 && self
1462 .frames
1463 .iter()
1464 .any(|frame| matches!(frame, Frame::ParagraphProperties))
1465 && !self
1466 .frames
1467 .iter()
1468 .any(|frame| matches!(frame, Frame::Run(_)))
1469 }
1470
1471 fn inside_table_row_properties(&self) -> bool {
1472 self.frames
1473 .iter()
1474 .any(|frame| matches!(frame, Frame::TableRowProperties))
1475 }
1476
1477 fn pseudo_text_is_suppressed(&self) -> bool {
1478 self.frames.iter().any(|frame| {
1479 matches!(
1480 frame,
1481 Frame::ParagraphProperties
1482 | Frame::RunProperties(_)
1483 | Frame::ChangeSnapshot
1484 | Frame::Textbox
1485 ) || matches!(frame, Frame::Revision(revision) if revision.suppressed)
1486 || matches!(frame, Frame::Sdt(sdt) if sdt.placeholder)
1487 })
1488 }
1489
1490 fn pseudo_text_is_structurally_suppressed(&self) -> bool {
1491 self.frames.iter().any(|frame| {
1492 matches!(
1493 frame,
1494 Frame::ParagraphProperties
1495 | Frame::RunProperties(_)
1496 | Frame::ChangeSnapshot
1497 | Frame::Textbox
1498 ) || matches!(frame, Frame::Sdt(sdt) if sdt.placeholder)
1499 })
1500 }
1501
1502 fn append_pseudo_text(&mut self, text: &str) -> Result<(), ProjectionError> {
1503 if self.current_paragraph.is_none()
1504 || self.pseudo_text_is_structurally_suppressed()
1505 || self
1506 .frames
1507 .iter()
1508 .rev()
1509 .any(|frame| matches!(frame, Frame::Run(run) if run.hidden))
1510 {
1511 return Ok(());
1512 }
1513 self.append_review_text(text);
1514 if self.pseudo_text_is_suppressed() {
1515 return Ok(());
1516 }
1517 if let Some(run) = self.frames.iter_mut().rev().find_map(|frame| match frame {
1518 Frame::Run(run) => Some(run),
1519 _ => None,
1520 }) {
1521 run.text.push_str(text);
1522 return Ok(());
1523 }
1524 self.current_paragraph
1525 .as_mut()
1526 .ok_or(ProjectionError::InvalidDocumentXml)?
1527 .append(text, TextProperties::default())
1528 }
1529
1530 fn append_review_text(&mut self, text: &str) {
1531 let ReviewRevisionCollection::Complete {
1532 remaining_detail_bytes,
1533 ..
1534 } = &mut self.review_revisions
1535 else {
1536 return;
1537 };
1538 let mut limit_exceeded = false;
1539 for frame in &mut self.frames {
1540 if let Frame::Revision(revision) = frame
1541 && revision.review_index.is_some()
1542 {
1543 let Some(remaining) = remaining_detail_bytes.checked_sub(text.len()) else {
1544 limit_exceeded = true;
1545 break;
1546 };
1547 *remaining_detail_bytes = remaining;
1548 revision.text.push_str(text);
1549 }
1550 }
1551 if limit_exceeded {
1552 self.review_revisions = ReviewRevisionCollection::LimitExceeded;
1553 }
1554 }
1555
1556 fn current_review_point(&self) -> Option<ReviewPoint> {
1557 let paragraph = self.current_paragraph.as_ref()?;
1558 let run_text = self.frames.iter().rev().find_map(|frame| match frame {
1559 Frame::Run(run) if !run.hidden && !self.pseudo_text_is_suppressed() => {
1560 Some(run.text.as_str())
1561 }
1562 _ => None,
1563 });
1564 let run_utf8 = run_text.map_or(0, str::len);
1565 let run_utf16 = run_text.map_or(0, |text| text.encode_utf16().count());
1566 Some(ReviewPoint {
1567 paragraph_ordinal: self.paragraphs.len(),
1568 utf8: u32::try_from(paragraph.text.len().checked_add(run_utf8)?).ok()?,
1569 utf16: paragraph
1570 .utf16_len
1571 .checked_add(u32::try_from(run_utf16).ok()?)?,
1572 })
1573 }
1574
1575 fn next_paragraph_start_review_point(&self) -> Option<ReviewPoint> {
1576 if self.current_paragraph.is_some() {
1577 return self.current_review_point();
1578 }
1579 self.inside_body().then_some(ReviewPoint {
1580 paragraph_ordinal: self.paragraphs.len(),
1581 utf8: 0,
1582 utf16: 0,
1583 })
1584 }
1585
1586 fn previous_paragraph_end_review_point(&self) -> Option<ReviewPoint> {
1587 if self.current_paragraph.is_some() {
1588 return self.current_review_point();
1589 }
1590 let paragraph = self.paragraphs.last()?;
1591 Some(ReviewPoint {
1592 paragraph_ordinal: paragraph.ordinal,
1593 utf8: u32::try_from(paragraph.text.len()).ok()?,
1594 utf16: paragraph.utf16_len,
1595 })
1596 }
1597
1598 fn review_comment_id(
1599 reader: &NsReader<&[u8]>,
1600 element: &BytesStart<'_>,
1601 ) -> Result<Option<String>, ProjectionError> {
1602 attribute(reader, element, b"id")
1603 }
1604
1605 fn start_review_comment_anchor(
1606 &mut self,
1607 reader: &NsReader<&[u8]>,
1608 element: &BytesStart<'_>,
1609 ) -> Result<(), ProjectionError> {
1610 let (Some(comment_id), Some(point)) = (
1611 Self::review_comment_id(reader, element)?,
1612 self.next_paragraph_start_review_point(),
1613 ) else {
1614 return Ok(());
1615 };
1616 if self.invalid_review_comment_anchors.contains(&comment_id) {
1617 return Ok(());
1618 }
1619 if self
1620 .open_review_comment_anchors
1621 .insert(comment_id.clone(), point)
1622 .is_some()
1623 {
1624 self.open_review_comment_anchors.remove(&comment_id);
1625 self.review_comment_anchors.remove(&comment_id);
1626 self.invalid_review_comment_anchors.insert(comment_id);
1627 }
1628 Ok(())
1629 }
1630
1631 fn suppress_review_comment_anchor(
1632 &mut self,
1633 reader: &NsReader<&[u8]>,
1634 element: &BytesStart<'_>,
1635 ) -> Result<(), ProjectionError> {
1636 let Some(comment_id) = Self::review_comment_id(reader, element)? else {
1637 return Ok(());
1638 };
1639 self.open_review_comment_anchors.remove(&comment_id);
1640 self.review_comment_anchors.remove(&comment_id);
1641 self.invalid_review_comment_anchors.insert(comment_id);
1642 Ok(())
1643 }
1644
1645 fn end_review_comment_anchor(
1646 &mut self,
1647 reader: &NsReader<&[u8]>,
1648 element: &BytesStart<'_>,
1649 ) -> Result<(), ProjectionError> {
1650 let (Some(comment_id), Some(end)) = (
1651 Self::review_comment_id(reader, element)?,
1652 self.previous_paragraph_end_review_point(),
1653 ) else {
1654 return Ok(());
1655 };
1656 if self.invalid_review_comment_anchors.contains(&comment_id) {
1657 return Ok(());
1658 }
1659 let Some(start) = self.open_review_comment_anchors.remove(&comment_id) else {
1660 self.review_comment_anchors.remove(&comment_id);
1661 self.invalid_review_comment_anchors.insert(comment_id);
1662 return Ok(());
1663 };
1664 if self
1665 .review_comment_anchors
1666 .insert(comment_id.clone(), ReviewSpan { start, end })
1667 .is_some()
1668 {
1669 self.review_comment_anchors.remove(&comment_id);
1670 self.invalid_review_comment_anchors.insert(comment_id);
1671 }
1672 Ok(())
1673 }
1674
1675 fn record_review_comment_reference(
1676 &mut self,
1677 reader: &NsReader<&[u8]>,
1678 element: &BytesStart<'_>,
1679 ) -> Result<(), ProjectionError> {
1680 let (Some(comment_id), Some(point)) = (
1681 Self::review_comment_id(reader, element)?,
1682 self.current_review_point(),
1683 ) else {
1684 return Ok(());
1685 };
1686 if self.invalid_review_comment_anchors.contains(&comment_id) {
1687 return Ok(());
1688 }
1689 if !self.review_comment_anchors.contains_key(&comment_id)
1690 && !self.open_review_comment_anchors.contains_key(&comment_id)
1691 {
1692 self.review_comment_anchors.insert(
1693 comment_id,
1694 ReviewSpan {
1695 start: point,
1696 end: point,
1697 },
1698 );
1699 }
1700 Ok(())
1701 }
1702
1703 fn append_pseudo_content(&mut self, text: &str) -> Result<(), ProjectionError> {
1704 let Some(Frame::PseudoText(frame)) = self.frames.last_mut() else {
1705 return Err(ProjectionError::InvalidDocumentXml);
1706 };
1707 frame.text.push_str(text);
1708 Ok(())
1709 }
1710
1711 fn append_text_control(&mut self, control: TextControl) -> Result<(), ProjectionError> {
1712 if let Some(text) = control.materialize(self.text_materialization) {
1713 self.append_pseudo_text(text)?;
1714 }
1715 Ok(())
1716 }
1717
1718 fn append_field_instruction(&mut self, text: &str) {
1719 let Some(field) = self.fields.last_mut() else {
1720 self.references_complete = false;
1721 return;
1722 };
1723 if field.separated || field.instruction.len().saturating_add(text.len()) > 4096 {
1724 self.references_complete = false;
1725 return;
1726 }
1727 field.instruction.push_str(text);
1728 }
1729
1730 fn simple_field(
1731 &mut self,
1732 reader: &NsReader<&[u8]>,
1733 element: &BytesStart<'_>,
1734 ) -> Result<(), ProjectionError> {
1735 let instruction = attribute(reader, element, b"instr")?;
1736 let source = self.current_point();
1737 let Some(instruction) = instruction.filter(|value| value.len() <= 4096) else {
1738 self.references_complete = false;
1739 return Ok(());
1740 };
1741 self.record_field_reference(&instruction, source);
1742 Ok(())
1743 }
1744
1745 fn field_character(
1746 &mut self,
1747 reader: &NsReader<&[u8]>,
1748 element: &BytesStart<'_>,
1749 ) -> Result<(), ProjectionError> {
1750 match attribute(reader, element, b"fldCharType")?.as_deref() {
1751 Some("begin") => self.fields.push(FieldFrame {
1752 source: self.current_field_point(),
1753 instruction: String::new(),
1754 separated: false,
1755 }),
1756 Some("separate") => {
1757 let Some(mut field) = self.fields.pop() else {
1758 self.references_complete = false;
1759 return Ok(());
1760 };
1761 if field.separated {
1762 self.references_complete = false;
1763 } else {
1764 self.record_field_reference(&field.instruction, field.source.clone());
1765 field.separated = true;
1766 }
1767 self.fields.push(field);
1768 }
1769 Some("end") => {
1770 let Some(field) = self.fields.pop() else {
1771 self.references_complete = false;
1772 return Ok(());
1773 };
1774 if !field.separated {
1775 self.record_field_reference(&field.instruction, field.source);
1776 }
1777 }
1778 _ => self.references_complete = false,
1779 }
1780 Ok(())
1781 }
1782
1783 fn current_field_point(&self) -> Option<RawBlockPoint> {
1784 let paragraph = self.current_paragraph.as_ref()?;
1785 if self.pseudo_text_is_suppressed() {
1786 return None;
1787 }
1788 if self.frames.iter().rev().find_map(|frame| match frame {
1789 Frame::Run(run) => Some(!run.text.is_empty()),
1790 _ => None,
1791 }) == Some(true)
1792 {
1793 return None;
1794 }
1795 Some(RawBlockPoint {
1796 paragraph: self.paragraphs.len(),
1797 utf8: u32::try_from(paragraph.text.len()).ok()?,
1798 utf16: paragraph.utf16_len,
1799 })
1800 }
1801
1802 fn record_field_reference(&mut self, instruction: &str, source: Option<RawBlockPoint>) {
1803 match internal_reference_target(instruction) {
1804 Ok(Some(reference_id)) => {
1805 let Some(source) = source else {
1806 self.references_complete = false;
1807 return;
1808 };
1809 self.references.push(RawInternalReference {
1810 reference_id,
1811 source,
1812 });
1813 }
1814 Ok(None) => {}
1815 Err(()) => self.references_complete = false,
1816 }
1817 }
1818
1819 fn current_point(&self) -> Option<RawBlockPoint> {
1820 if let Some(paragraph) = self.current_paragraph.as_ref() {
1821 if self.pseudo_text_is_suppressed()
1822 || self
1823 .frames
1824 .iter()
1825 .any(|frame| matches!(frame, Frame::Run(_)))
1826 {
1827 return None;
1828 }
1829 return Some(RawBlockPoint {
1830 paragraph: self.paragraphs.len(),
1831 utf8: u32::try_from(paragraph.text.len()).ok()?,
1832 utf16: paragraph.utf16_len,
1833 });
1834 }
1835 if !matches!(self.frames.last(), Some(Frame::Body)) {
1836 return None;
1837 }
1838 self.paragraphs.last().map_or(
1839 Some(RawBlockPoint {
1840 paragraph: 0,
1841 utf8: 0,
1842 utf16: 0,
1843 }),
1844 |paragraph| {
1845 Some(RawBlockPoint {
1846 paragraph: paragraph.ordinal,
1847 utf8: u32::try_from(paragraph.text.len()).ok()?,
1848 utf16: u32::try_from(paragraph.text.encode_utf16().count()).ok()?,
1849 })
1850 },
1851 )
1852 }
1853
1854 fn current_bookmark_point(&self) -> Option<BookmarkPoint> {
1855 if self.pseudo_text_is_suppressed() {
1856 return None;
1857 }
1858 if self.current_paragraph.is_some() {
1859 return self.current_point().map(BookmarkPoint::Paragraph);
1860 }
1861 matches!(
1862 self.frames.last(),
1863 Some(Frame::Body | Frame::Table(_) | Frame::Row(_) | Frame::Cell(_))
1864 )
1865 .then(|| BookmarkPoint::ParagraphBoundary(self.paragraphs.len()))
1866 }
1867
1868 fn start_bookmark(
1869 &mut self,
1870 reader: &NsReader<&[u8]>,
1871 element: &BytesStart<'_>,
1872 ) -> Result<(), ProjectionError> {
1873 let id = attribute(reader, element, b"id")?.and_then(|value| value.parse::<u32>().ok());
1874 let name = attribute(reader, element, b"name")?;
1875 let point = self.current_bookmark_point();
1876 let (Some(id), Some(name), Some(point)) = (id, name, point) else {
1877 self.bookmarks_complete = false;
1878 return Ok(());
1879 };
1880 if name.is_empty()
1881 || !self.seen_bookmark_ids.insert(id)
1882 || self.open_bookmarks.insert(id, (name, point)).is_some()
1883 {
1884 self.bookmarks_complete = false;
1885 }
1886 Ok(())
1887 }
1888
1889 fn end_bookmark(
1890 &mut self,
1891 reader: &NsReader<&[u8]>,
1892 element: &BytesStart<'_>,
1893 ) -> Result<(), ProjectionError> {
1894 let id = attribute(reader, element, b"id")?.and_then(|value| value.parse::<u32>().ok());
1895 let point = self.current_bookmark_point();
1896 let (Some(id), Some(end)) = (id, point) else {
1897 self.bookmarks_complete = false;
1898 return Ok(());
1899 };
1900 let Some((name, start)) = self.open_bookmarks.remove(&id) else {
1901 self.bookmarks_complete = false;
1902 return Ok(());
1903 };
1904 self.bookmarks.push(PendingBookmarkRange {
1905 id,
1906 name,
1907 start,
1908 end,
1909 });
1910 Ok(())
1911 }
1912
1913 fn mark_placeholder(&mut self) {
1914 let Some(sdt) = self.frames.iter_mut().rev().find_map(|frame| match frame {
1915 Frame::Sdt(sdt) => Some(sdt),
1916 _ => None,
1917 }) else {
1918 return;
1919 };
1920 sdt.placeholder = true;
1921 if let Some(paragraph) = self.current_paragraph.as_mut() {
1922 paragraph.truncate(sdt.text_utf8_start, sdt.text_utf16_start);
1923 }
1924 }
1925
1926 fn mark_hidden_run(&mut self) {
1927 let Some((run_frame, hidden_eligible)) =
1928 self.frames.iter().rev().find_map(|frame| match frame {
1929 Frame::RunProperties(properties) => {
1930 Some((properties.run_frame, properties.hidden_eligible))
1931 }
1932 _ => None,
1933 })
1934 else {
1935 return;
1936 };
1937 if !hidden_eligible || run_frame == usize::MAX {
1938 return;
1939 }
1940 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1941 run.hidden = true;
1942 }
1943 }
1944
1945 fn set_run_character_style(&mut self, style_id: Option<String>) {
1946 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
1947 return;
1948 };
1949 let run_frame = properties.run_frame;
1950 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1951 run.character_style_id = style_id;
1952 }
1953 }
1954
1955 fn mark_bold_run(&mut self, enabled: bool) {
1956 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
1957 return;
1958 };
1959 let run_frame = properties.run_frame;
1960 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1961 run.direct_styles.bold = Some(enabled);
1962 }
1963 }
1964
1965 fn mark_complex_script_bold_run(&mut self, enabled: bool) {
1966 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
1967 return;
1968 };
1969 let run_frame = properties.run_frame;
1970 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1971 run.direct_styles.complex_script_bold = Some(enabled);
1972 }
1973 }
1974
1975 fn mark_complex_script_run(&mut self, enabled: bool) {
1976 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
1977 return;
1978 };
1979 let run_frame = properties.run_frame;
1980 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1981 run.direct_styles.force_complex_script = Some(enabled);
1982 }
1983 }
1984
1985 fn mark_right_to_left_run(&mut self, enabled: bool) {
1986 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
1987 return;
1988 };
1989 let run_frame = properties.run_frame;
1990 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1991 run.direct_styles.right_to_left = Some(enabled);
1992 }
1993 }
1994
1995 fn mark_highlighted_run(&mut self, enabled: bool) {
1996 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
1997 return;
1998 };
1999 let run_frame = properties.run_frame;
2000 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2001 run.direct_styles.highlighted = Some(enabled);
2002 }
2003 }
2004
2005 fn mark_superscript_run(&mut self, enabled: bool) {
2006 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
2007 return;
2008 };
2009 let run_frame = properties.run_frame;
2010 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
2011 run.direct_styles.superscript = Some(enabled);
2012 }
2013 }
2014}
2015
2016const fn revision_fact_kind(name: &[u8]) -> Option<RevisionFactKind> {
2017 match name {
2018 b"ins" => Some(RevisionFactKind::Insertion),
2019 b"del" => Some(RevisionFactKind::Deletion),
2020 b"moveFrom" => Some(RevisionFactKind::MoveFrom),
2021 b"moveTo" => Some(RevisionFactKind::MoveTo),
2022 b"cellIns" => Some(RevisionFactKind::CellInsertion),
2023 b"cellDel" => Some(RevisionFactKind::CellDeletion),
2024 b"cellMerge" => Some(RevisionFactKind::CellMerge),
2025 b"pPrChange" => Some(RevisionFactKind::ParagraphPropertiesChange),
2026 b"rPrChange" => Some(RevisionFactKind::RunPropertiesChange),
2027 b"sectPrChange" => Some(RevisionFactKind::SectionPropertiesChange),
2028 b"tblPrChange" => Some(RevisionFactKind::TablePropertiesChange),
2029 b"trPrChange" => Some(RevisionFactKind::TableRowPropertiesChange),
2030 b"tcPrChange" => Some(RevisionFactKind::TableCellPropertiesChange),
2031 b"tblGridChange" => Some(RevisionFactKind::TableGridChange),
2032 b"customXmlDelRangeStart" => Some(RevisionFactKind::CustomXmlDeletionRangeStart),
2033 b"customXmlDelRangeEnd" => Some(RevisionFactKind::CustomXmlDeletionRangeEnd),
2034 b"customXmlInsRangeStart" => Some(RevisionFactKind::CustomXmlInsertionRangeStart),
2035 b"customXmlInsRangeEnd" => Some(RevisionFactKind::CustomXmlInsertionRangeEnd),
2036 b"customXmlMoveFromRangeStart" => Some(RevisionFactKind::CustomXmlMoveFromRangeStart),
2037 b"customXmlMoveFromRangeEnd" => Some(RevisionFactKind::CustomXmlMoveFromRangeEnd),
2038 b"customXmlMoveToRangeStart" => Some(RevisionFactKind::CustomXmlMoveToRangeStart),
2039 b"customXmlMoveToRangeEnd" => Some(RevisionFactKind::CustomXmlMoveToRangeEnd),
2040 _ => None,
2041 }
2042}
2043
2044fn enclosing_table(frames: &mut [Frame]) -> Option<&mut TableFrame> {
2045 for frame in frames.iter_mut().rev() {
2046 match frame {
2047 Frame::Table(table) => return Some(table),
2048 Frame::Row(_) | Frame::Cell(_) | Frame::Paragraph | Frame::Run(_) => return None,
2049 _ => {}
2050 }
2051 }
2052 None
2053}
2054
2055fn enclosing_row(frames: &mut [Frame]) -> Option<&mut RowFrame> {
2056 for frame in frames.iter_mut().rev() {
2057 match frame {
2058 Frame::Row(row) => return Some(row),
2059 Frame::Table(_) | Frame::Cell(_) | Frame::Paragraph | Frame::Run(_) => return None,
2060 _ => {}
2061 }
2062 }
2063 None
2064}
2065
2066fn revision_is_suppressed(name: &[u8], view: RevisionView) -> bool {
2067 match view {
2068 RevisionView::Current => matches!(name, b"del" | b"moveFrom"),
2069 RevisionView::Original => matches!(name, b"ins" | b"moveTo"),
2070 }
2071}
2072
2073fn is_change_snapshot(name: &[u8]) -> bool {
2074 matches!(
2075 name,
2076 b"pPrChange"
2077 | b"rPrChange"
2078 | b"sectPrChange"
2079 | b"tblPrChange"
2080 | b"trPrChange"
2081 | b"tcPrChange"
2082 | b"tblGridChange"
2083 )
2084}
2085
2086fn is_unsupported_revision_markup(name: &[u8]) -> bool {
2087 matches!(
2088 name,
2089 b"customXmlDelRangeStart"
2090 | b"customXmlDelRangeEnd"
2091 | b"customXmlInsRangeStart"
2092 | b"customXmlInsRangeEnd"
2093 | b"customXmlMoveFromRangeStart"
2094 | b"customXmlMoveFromRangeEnd"
2095 | b"customXmlMoveToRangeStart"
2096 | b"customXmlMoveToRangeEnd"
2097 )
2098}
2099
2100const fn paragraph_break_is_removed(
2101 revision: Option<ParagraphMarkRevision>,
2102 view: RevisionView,
2103) -> bool {
2104 matches!(
2105 (revision, view),
2106 (Some(ParagraphMarkRevision::Deletion), RevisionView::Current)
2107 | (
2108 Some(ParagraphMarkRevision::Insertion),
2109 RevisionView::Original
2110 )
2111 )
2112}
2113
2114fn normalize_paragraph_revision_view(
2115 paragraphs: &mut Vec<RawProjectedParagraph>,
2116 paragraph_mark_revisions: &mut HashMap<usize, ParagraphMarkRevision>,
2117 view: RevisionView,
2118 unsupported: &mut BTreeSet<RevisionUnsupportedReason>,
2119) -> Result<bool, ProjectionError> {
2120 let mut normalized: Vec<RawProjectedParagraph> = Vec::with_capacity(paragraphs.len());
2121 let mut merge_previous = false;
2122 let mut merged_any = false;
2123
2124 for (source_ordinal, mut paragraph) in std::mem::take(paragraphs).into_iter().enumerate() {
2125 let merge_next =
2126 paragraph_break_is_removed(paragraph_mark_revisions.remove(&source_ordinal), view);
2127 if merge_previous {
2128 let previous = normalized
2129 .last_mut()
2130 .ok_or(ProjectionError::InvalidDocumentXml)?;
2131 if previous.structure != paragraph.structure {
2132 unsupported.insert(RevisionUnsupportedReason::IncompatibleParagraphMerge);
2133 paragraph.ordinal = normalized.len();
2134 normalized.push(paragraph);
2135 merge_previous = merge_next;
2136 continue;
2137 }
2138 let utf16_offset = previous.utf16_len;
2139 for mut span in paragraph.formatting {
2140 span.start_utf16 = span
2141 .start_utf16
2142 .checked_add(utf16_offset)
2143 .ok_or(ProjectionError::InvalidDocumentXml)?;
2144 span.end_utf16 = span
2145 .end_utf16
2146 .checked_add(utf16_offset)
2147 .ok_or(ProjectionError::InvalidDocumentXml)?;
2148 previous.formatting.push(span);
2149 }
2150 previous.text.push_str(¶graph.text);
2151 previous.utf16_len = previous
2152 .utf16_len
2153 .checked_add(paragraph.utf16_len)
2154 .ok_or(ProjectionError::InvalidDocumentXml)?;
2155 merge_previous = merge_next;
2156 merged_any = true;
2157 continue;
2158 }
2159
2160 paragraph.ordinal = normalized.len();
2161 normalized.push(paragraph);
2162 merge_previous = merge_next;
2163 }
2164
2165 *paragraphs = normalized;
2166 Ok(merged_any)
2167}
2168
2169fn internal_reference_target(instruction: &str) -> Result<Option<String>, ()> {
2170 let tokens = field_tokens(instruction)?;
2171 let Some(command) = tokens.first() else {
2172 return Err(());
2173 };
2174 if ["REF", "PAGEREF", "NOTEREF"]
2175 .iter()
2176 .any(|candidate| command.eq_ignore_ascii_case(candidate))
2177 {
2178 return tokens
2179 .get(1)
2180 .filter(|target| !target.is_empty() && !target.starts_with('\\'))
2181 .cloned()
2182 .map(Some)
2183 .ok_or(());
2184 }
2185 if command.eq_ignore_ascii_case("HYPERLINK") {
2186 for (index, token) in tokens.iter().enumerate().skip(1) {
2187 if token.eq_ignore_ascii_case("\\l") {
2188 return tokens
2189 .get(index.checked_add(1).ok_or(())?)
2190 .filter(|target| !target.is_empty())
2191 .cloned()
2192 .map(Some)
2193 .ok_or(());
2194 }
2195 }
2196 }
2197 Ok(None)
2198}
2199
2200fn field_tokens(instruction: &str) -> Result<Vec<String>, ()> {
2201 let mut tokens = Vec::new();
2202 let mut token = String::new();
2203 let mut quoted = false;
2204 for character in instruction.chars() {
2205 match character {
2206 '"' => quoted = !quoted,
2207 whitespace if whitespace.is_whitespace() && !quoted => {
2208 if !token.is_empty() {
2209 tokens.push(std::mem::take(&mut token));
2210 }
2211 }
2212 _ => token.push(character),
2213 }
2214 }
2215 if quoted {
2216 return Err(());
2217 }
2218 if !token.is_empty() {
2219 tokens.push(token);
2220 }
2221 Ok(tokens)
2222}
2223
2224fn attribute(
2225 reader: &NsReader<&[u8]>,
2226 element: &BytesStart<'_>,
2227 name: &[u8],
2228) -> Result<Option<String>, ProjectionError> {
2229 for attribute in element.attributes() {
2230 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2231 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2232 if OoxmlNamespace::from_resolved(&namespace) == OoxmlNamespace::Wordprocessing
2233 && local_name.as_ref() == name
2234 {
2235 return attribute
2236 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
2237 .map(|value| Some(value.into_owned()))
2238 .map_err(|_| ProjectionError::InvalidDocumentXml);
2239 }
2240 }
2241 Ok(None)
2242}
2243
2244fn preserves_xml_space(
2245 reader: &NsReader<&[u8]>,
2246 element: &BytesStart<'_>,
2247) -> Result<bool, ProjectionError> {
2248 const XML_NAMESPACE: &[u8] = b"http://www.w3.org/XML/1998/namespace";
2249
2250 for attribute in element.attributes() {
2251 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2252 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2253 if matches!(
2254 namespace,
2255 quick_xml::name::ResolveResult::Bound(namespace)
2256 if namespace.as_ref() == XML_NAMESPACE
2257 ) && local_name.as_ref() == b"space"
2258 {
2259 let value = attribute
2260 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
2261 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
2262 return match value.as_ref() {
2263 "default" => Ok(false),
2264 "preserve" => Ok(true),
2265 _ => Err(ProjectionError::InvalidDocumentXml),
2266 };
2267 }
2268 }
2269 Ok(false)
2270}
2271
2272fn on_off_element_enabled(
2273 reader: &NsReader<&[u8]>,
2274 element: &BytesStart<'_>,
2275) -> Result<bool, ProjectionError> {
2276 Ok(attribute(reader, element, b"val")?
2277 .is_none_or(|value| !matches!(value.to_ascii_lowercase().as_str(), "0" | "false" | "off")))
2278}
2279
2280fn trim_xml_whitespace(value: &str) -> &str {
2281 value.trim_matches(['\u{0009}', '\u{000a}', '\u{000d}', '\u{0020}'])
2282}
2283
2284fn attribute_2010(
2285 reader: &NsReader<&[u8]>,
2286 element: &BytesStart<'_>,
2287 name: &[u8],
2288) -> Result<Option<String>, ProjectionError> {
2289 for attribute in element.attributes() {
2290 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
2291 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2292 if OoxmlNamespace::from_resolved(&namespace) == OoxmlNamespace::Wordprocessing2010
2293 && local_name.as_ref() == name
2294 {
2295 return attribute
2296 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
2297 .map(|value| Some(value.into_owned()))
2298 .map_err(|_| ProjectionError::InvalidDocumentXml);
2299 }
2300 }
2301 Ok(None)
2302}
2303
2304fn symbol_text(value: &str, font: Option<&str>) -> String {
2305 let prefix = value
2306 .chars()
2307 .take_while(char::is_ascii_hexdigit)
2308 .collect::<String>();
2309 let Some(code) = (!prefix.is_empty())
2310 .then(|| u32::from_str_radix(&prefix, 16).ok())
2311 .flatten()
2312 else {
2313 return value.to_owned();
2314 };
2315 if let Some(replacement) = normalized_font_symbol(font, code) {
2316 return replacement.to_owned();
2317 }
2318 char::from_u32(code).unwrap_or('\u{fffd}').to_string()
2319}
2320
2321struct FontSymbolMapping {
2322 font: &'static str,
2323 encoded: u32,
2324 unicode: &'static str,
2325}
2326
2327const FONT_SYMBOL_MAPPINGS: &[FontSymbolMapping] = &[FontSymbolMapping {
2334 font: "Wingdings",
2335 encoded: 0xF06C,
2336 unicode: "●",
2337}];
2338
2339fn normalized_font_symbol(font: Option<&str>, encoded: u32) -> Option<&'static str> {
2340 let font = font?;
2341 FONT_SYMBOL_MAPPINGS
2342 .iter()
2343 .find(|mapping| mapping.encoded == encoded && mapping.font.eq_ignore_ascii_case(font))
2344 .map(|mapping| mapping.unicode)
2345}