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;
7
8use crate::ProjectionError;
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,
18};
19use crate::projection::styles::{
20 parse_indentation, parse_level_attribute, parse_outline_level_attribute, parse_u32_attribute,
21};
22
23#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
24pub struct PackageParagraphId(u32);
25
26impl PackageParagraphId {
27 #[must_use]
28 pub fn parse(value: &str) -> Option<Self> {
29 if value.len() != 8 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
30 return None;
31 }
32 let value = u32::from_str_radix(value, 16).ok()?;
33 (value != 0 && i32::try_from(value).is_ok()).then_some(Self(value))
34 }
35
36 #[must_use]
37 pub const fn value(self) -> u32 {
38 self.0
39 }
40}
41
42#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
43pub enum TextStyle {
44 Bold,
45 Highlight,
46}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct TextFormattingSpan {
50 pub start_utf16: u32,
51 pub end_utf16: u32,
52 pub style: TextStyle,
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
56pub struct ParagraphStructure {
57 pub table_ordinal: usize,
58 pub row: usize,
59 pub column: usize,
60}
61
62#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
63pub enum RevisionView {
64 #[default]
65 Current,
66 Original,
67}
68
69#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
70pub enum TextMaterialization {
71 #[default]
72 WordHost,
73 ReadablePlainText,
74}
75
76#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
77pub enum RevisionUnsupportedReason {
78 IncompatibleParagraphMerge,
79 StructuralTableRevision,
80 UnsupportedRevisionMarkup,
81}
82
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum RevisionProjectionStatus {
85 Complete,
86 Incomplete(Vec<RevisionUnsupportedReason>),
87}
88
89impl RevisionProjectionStatus {
90 #[must_use]
91 pub const fn is_complete(&self) -> bool {
92 matches!(self, Self::Complete)
93 }
94}
95
96#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97enum ParagraphMarkRevision {
98 Insertion,
99 Deletion,
100}
101
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub(super) enum TextControl {
104 Tab,
105 LineBreak,
106 PageBreak,
107 ColumnBreak,
108 CarriageReturn,
109 FootnoteReference,
110 SoftHyphen,
111 NoBreakHyphen,
112}
113
114impl TextControl {
115 pub(super) const fn materialize(
116 self,
117 materialization: TextMaterialization,
118 ) -> Option<&'static str> {
119 match materialization {
120 TextMaterialization::WordHost => Some(match self {
121 Self::Tab => "\t",
122 Self::LineBreak => "\u{000b}",
123 Self::PageBreak => "\u{000c}",
124 Self::ColumnBreak => "\u{000e}",
125 Self::CarriageReturn => "\r",
126 Self::FootnoteReference => "\u{0002}",
127 Self::SoftHyphen => "\u{001f}",
128 Self::NoBreakHyphen => "\u{001e}",
129 }),
130 TextMaterialization::ReadablePlainText => match self {
131 Self::Tab => Some("\t"),
132 Self::LineBreak | Self::PageBreak | Self::ColumnBreak | Self::CarriageReturn => {
133 Some("\n")
134 }
135 Self::FootnoteReference => None,
136 Self::SoftHyphen => Some("\u{00ad}"),
137 Self::NoBreakHyphen => Some("\u{2011}"),
138 },
139 }
140 }
141}
142
143pub(super) struct RawProjectedParagraph {
144 pub ordinal: usize,
145 pub package_paragraph_id: Option<PackageParagraphId>,
146 pub text: String,
147 utf16_len: u32,
148 pub formatting: Vec<TextFormattingSpan>,
149 pub structure: Option<ParagraphStructure>,
150 pub properties: ParagraphProperties,
151}
152
153pub(super) struct RawDocumentProjection {
154 pub paragraphs: Vec<RawProjectedParagraph>,
155 pub bookmarks: Result<Vec<RawBookmarkRange>, StructuralFactUnknownReason>,
156 pub references: Result<Vec<RawInternalReference>, StructuralFactUnknownReason>,
157 pub revision_status: RevisionProjectionStatus,
158 pub review_revisions: Option<ReviewFactSet<AttributedRevision>>,
159 pub review_comment_anchors: HashMap<String, ReviewSpan>,
160}
161
162struct ParagraphBuilder {
163 package_paragraph_id: Option<PackageParagraphId>,
164 text: String,
165 utf16_len: u32,
166 formatting: Vec<TextFormattingSpan>,
167 structure: Option<ParagraphStructure>,
168 properties: ParagraphProperties,
169 paragraph_mark_revision: Option<ParagraphMarkRevision>,
170}
171
172impl ParagraphBuilder {
173 fn append(&mut self, text: &str, bold: bool, highlighted: bool) -> Result<(), ProjectionError> {
174 let units = u32::try_from(text.encode_utf16().count())
175 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
176 let end = self
177 .utf16_len
178 .checked_add(units)
179 .ok_or(ProjectionError::InvalidDocumentXml)?;
180 if bold {
181 self.append_style(end, TextStyle::Bold);
182 }
183 if highlighted {
184 self.append_style(end, TextStyle::Highlight);
185 }
186 self.text.push_str(text);
187 self.utf16_len = end;
188 Ok(())
189 }
190
191 fn append_style(&mut self, end_utf16: u32, style: TextStyle) {
192 if self.utf16_len >= end_utf16 {
193 return;
194 }
195 if let Some(previous) = self
196 .formatting
197 .iter_mut()
198 .rev()
199 .find(|span| span.style == style)
200 && previous.end_utf16 == self.utf16_len
201 {
202 previous.end_utf16 = end_utf16;
203 return;
204 }
205 self.formatting.push(TextFormattingSpan {
206 start_utf16: self.utf16_len,
207 end_utf16,
208 style,
209 });
210 }
211
212 fn truncate(&mut self, utf8_len: usize, utf16_len: u32) {
213 self.text.truncate(utf8_len);
214 self.utf16_len = utf16_len;
215 self.formatting.retain(|span| span.start_utf16 < utf16_len);
216 for span in &mut self.formatting {
217 span.end_utf16 = span.end_utf16.min(utf16_len);
218 }
219 }
220}
221
222struct RunFrame {
223 text: String,
224 bold: bool,
225 highlighted: bool,
226 hidden: bool,
227 direct_child_count: usize,
228}
229
230struct RunPropertiesFrame {
231 run_frame: usize,
232 hidden_eligible: bool,
233}
234
235struct TableFrame {
236 ordinal: usize,
237 next_row: usize,
238}
239
240struct RowFrame {
241 table_ordinal: usize,
242 row: usize,
243 next_column: usize,
244}
245
246#[derive(Clone, Copy)]
247struct CellFrame {
248 table_ordinal: usize,
249 row: usize,
250 column: usize,
251}
252
253struct SdtFrame {
254 placeholder: bool,
255 text_utf8_start: usize,
256 text_utf16_start: u32,
257}
258
259#[derive(Clone, Copy)]
260enum PseudoTextKind {
261 Text { preserve_space: bool },
262 Instruction,
263}
264
265struct PseudoTextFrame {
266 kind: PseudoTextKind,
267 text: String,
268}
269
270impl PseudoTextFrame {
271 const fn text(preserve_space: bool) -> Self {
272 Self {
273 kind: PseudoTextKind::Text { preserve_space },
274 text: String::new(),
275 }
276 }
277
278 const fn instruction() -> Self {
279 Self {
280 kind: PseudoTextKind::Instruction,
281 text: String::new(),
282 }
283 }
284}
285
286struct FieldFrame {
287 source: Option<RawBlockPoint>,
288 instruction: String,
289 separated: bool,
290}
291
292struct RevisionFrame {
293 review_index: Option<usize>,
294 start: Option<ReviewPoint>,
295 suppressed: bool,
296 text: String,
297}
298
299#[derive(Default)]
300enum ReviewRevisionCollection {
301 #[default]
302 Disabled,
303 Complete {
304 maximum_facts: usize,
305 remaining_detail_bytes: usize,
306 revisions: Vec<AttributedRevision>,
307 },
308 LimitExceeded,
309}
310
311#[derive(Clone, Copy)]
312pub(super) struct ReviewProjectionLimits {
313 pub maximum_facts: usize,
314 pub maximum_detail_bytes: usize,
315}
316
317enum Frame {
318 Other,
319 Body,
320 Table(TableFrame),
321 Row(RowFrame),
322 Cell(CellFrame),
323 Paragraph,
324 Run(RunFrame),
325 RunProperties(RunPropertiesFrame),
326 ParagraphProperties,
327 TableRowProperties,
328 TableCellProperties,
329 NumberingProperties,
330 Hyperlink(Option<RawInternalReference>),
331 Revision(RevisionFrame),
332 ChangeSnapshot,
333 Textbox,
334 Sdt(SdtFrame),
335 PseudoText(PseudoTextFrame),
336}
337
338#[allow(clippy::too_many_lines)] pub(super) fn project_document_xml(
340 xml: &[u8],
341 maximum_paragraphs: usize,
342 revision_view: RevisionView,
343 text_materialization: TextMaterialization,
344 review_limits: Option<ReviewProjectionLimits>,
345) -> Result<RawDocumentProjection, ProjectionError> {
346 let mut reader = NsReader::from_reader(xml);
347 reader.config_mut().check_end_names = true;
348 let mut state = ProjectionState {
349 maximum_paragraphs,
350 revision_view,
351 text_materialization,
352 bookmarks_complete: true,
353 references_complete: true,
354 review_revisions: review_limits.map_or(ReviewRevisionCollection::Disabled, |limits| {
355 ReviewRevisionCollection::Complete {
356 maximum_facts: limits.maximum_facts,
357 remaining_detail_bytes: limits.maximum_detail_bytes,
358 revisions: Vec::new(),
359 }
360 }),
361 ..ProjectionState::default()
362 };
363 let mut compatibility = MarkupCompatibility::default();
364 loop {
365 match reader
366 .read_event()
367 .map_err(|_| ProjectionError::InvalidDocumentXml)?
368 {
369 Event::Start(element) => {
370 let (namespace, _) = reader.resolver().resolve_element(element.name());
371 let namespace = OoxmlNamespace::from_resolved(&namespace);
372 if compatibility.start(&reader, namespace, &element)?
373 == CompatibilityAction::Process
374 {
375 state.start(&reader, namespace, &element)?;
376 }
377 }
378 Event::Empty(element) => {
379 let (namespace, _) = reader.resolver().resolve_element(element.name());
380 let namespace = OoxmlNamespace::from_resolved(&namespace);
381 if compatibility.empty(&reader, namespace, &element)?
382 == CompatibilityAction::Process
383 {
384 state.start(&reader, namespace, &element)?;
385 state.end()?;
386 }
387 }
388 Event::Text(text) => {
389 if !compatibility.is_suppressed()
390 && matches!(state.frames.last(), Some(Frame::PseudoText(_)))
391 {
392 let decoded = text
393 .xml10_content()
394 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
395 state.append_pseudo_content(&decoded)?;
396 }
397 }
398 Event::CData(text) => {
399 if !compatibility.is_suppressed()
400 && matches!(state.frames.last(), Some(Frame::PseudoText(_)))
401 {
402 let decoded = text
403 .xml10_content()
404 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
405 state.append_pseudo_content(&decoded)?;
406 }
407 }
408 Event::GeneralRef(reference) => {
409 if !compatibility.is_suppressed()
410 && matches!(state.frames.last(), Some(Frame::PseudoText(_)))
411 {
412 let resolved_character = reference
413 .resolve_char_ref()
414 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
415 let resolved = if let Some(character) = resolved_character {
416 character.to_string()
417 } else {
418 let name = reference
419 .decode()
420 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
421 resolve_predefined_entity(&name)
422 .ok_or(ProjectionError::InvalidDocumentXml)?
423 .to_owned()
424 };
425 state.append_pseudo_content(&resolved)?;
426 }
427 }
428 Event::End(element) => {
429 let (namespace, local_name) = reader.resolver().resolve_element(element.name());
430 if compatibility.end(
431 OoxmlNamespace::from_resolved(&namespace),
432 local_name.as_ref(),
433 )? == CompatibilityAction::Process
434 {
435 state.end()?;
436 }
437 }
438 Event::Eof => break,
439 _ => {}
440 }
441 }
442 if !state.body_seen {
443 return Err(ProjectionError::MissingDocumentBody);
444 }
445 if state.current_paragraph.is_some() || !state.frames.is_empty() || !compatibility.is_complete()
446 {
447 return Err(ProjectionError::InvalidDocumentXml);
448 }
449 let paragraph_merges = if state.paragraph_mark_revisions.is_empty() {
450 false
451 } else {
452 normalize_paragraph_revision_view(
453 &mut state.paragraphs,
454 &mut state.paragraph_mark_revisions,
455 state.revision_view,
456 &mut state.revision_unsupported,
457 )?
458 };
459 if paragraph_merges {
460 state.bookmarks_complete = false;
461 state.references_complete = false;
462 state.review_comment_anchors.clear();
463 if let ReviewRevisionCollection::Complete { revisions, .. } = &mut state.review_revisions {
464 for revision in revisions {
465 revision.content =
466 ReviewDetail::Unknown(ReviewFactUnknownReason::UnsupportedLocation);
467 }
468 }
469 }
470 let bookmarks = if state.bookmarks_complete
471 && state.open_bookmarks.is_empty()
472 && (state.bookmarks.is_empty() || !state.paragraphs.is_empty())
473 {
474 Ok(state.bookmarks)
475 } else {
476 Err(StructuralFactUnknownReason::IncompleteBookmarkRanges)
477 };
478 let references = if state.references_complete && state.fields.is_empty() {
479 Ok(state.references)
480 } else {
481 Err(StructuralFactUnknownReason::UnsupportedInternalReferences)
482 };
483 Ok(RawDocumentProjection {
484 paragraphs: state.paragraphs,
485 bookmarks,
486 references,
487 revision_status: if state.revision_unsupported.is_empty() {
488 RevisionProjectionStatus::Complete
489 } else {
490 RevisionProjectionStatus::Incomplete(state.revision_unsupported.into_iter().collect())
491 },
492 review_revisions: match state.review_revisions {
493 ReviewRevisionCollection::Disabled => None,
494 ReviewRevisionCollection::Complete { revisions, .. } => {
495 Some(ReviewFactSet::Known(revisions))
496 }
497 ReviewRevisionCollection::LimitExceeded => Some(ReviewFactSet::Unknown(
498 ReviewFactUnknownReason::ResourceLimit,
499 )),
500 },
501 review_comment_anchors: state.review_comment_anchors,
502 })
503}
504
505#[derive(Default)]
506struct ProjectionState {
507 frames: Vec<Frame>,
508 paragraphs: Vec<RawProjectedParagraph>,
509 current_paragraph: Option<ParagraphBuilder>,
510 body_seen: bool,
511 next_table_ordinal: usize,
512 maximum_paragraphs: usize,
513 open_bookmarks: HashMap<u32, (String, RawBlockPoint)>,
514 seen_bookmark_ids: HashSet<u32>,
515 bookmarks: Vec<RawBookmarkRange>,
516 references: Vec<RawInternalReference>,
517 bookmarks_complete: bool,
518 references_complete: bool,
519 fields: Vec<FieldFrame>,
520 paragraph_mark_revisions: HashMap<usize, ParagraphMarkRevision>,
521 revision_view: RevisionView,
522 text_materialization: TextMaterialization,
523 revision_unsupported: BTreeSet<RevisionUnsupportedReason>,
524 review_revisions: ReviewRevisionCollection,
525 open_review_comment_anchors: HashMap<String, ReviewPoint>,
526 review_comment_anchors: HashMap<String, ReviewSpan>,
527 invalid_review_comment_anchors: HashSet<String>,
528}
529
530impl ProjectionState {
531 #[allow(clippy::too_many_lines)] fn start(
533 &mut self,
534 reader: &NsReader<&[u8]>,
535 namespace: OoxmlNamespace,
536 element: &BytesStart<'_>,
537 ) -> Result<(), ProjectionError> {
538 let local_name = element.local_name();
539 let name = if namespace == OoxmlNamespace::Wordprocessing {
540 local_name.as_ref()
541 } else {
542 b""
543 };
544 let direct_run_child = if let Some(Frame::Run(run)) = self.frames.last_mut() {
545 let child = run.direct_child_count;
546 run.direct_child_count = run
547 .direct_child_count
548 .checked_add(1)
549 .ok_or(ProjectionError::InvalidDocumentXml)?;
550 let frame_index = self
551 .frames
552 .len()
553 .checked_sub(1)
554 .ok_or(ProjectionError::InvalidDocumentXml)?;
555 Some((frame_index, child))
556 } else {
557 None
558 };
559
560 if name == b"body" {
561 if self.body_seen {
562 return Err(ProjectionError::InvalidDocumentXml);
563 }
564 self.body_seen = true;
565 self.frames.push(Frame::Body);
566 return Ok(());
567 }
568 if !self.inside_body() {
569 self.frames.push(Frame::Other);
570 return Ok(());
571 }
572 if self.inside_change_snapshot() {
573 self.frames.push(Frame::Other);
574 return Ok(());
575 }
576 let attributed_revision = self.record_attributed_revision(reader, element, name)?;
577 if name == b"txbxContent" {
578 self.frames.push(Frame::Textbox);
579 return Ok(());
580 }
581 if self.inside_textbox() {
582 if matches!(
583 name,
584 b"commentRangeStart" | b"commentRangeEnd" | b"commentReference"
585 ) {
586 self.suppress_review_comment_anchor(reader, element)?;
587 }
588 self.frames.push(Frame::Other);
589 return Ok(());
590 }
591 match name {
592 b"commentRangeStart" => self.start_review_comment_anchor(reader, element)?,
593 b"commentRangeEnd" => self.end_review_comment_anchor(reader, element)?,
594 b"commentReference" => self.record_review_comment_reference(reader, element)?,
595 _ => {}
596 }
597 if is_change_snapshot(name) {
598 if self.revision_view == RevisionView::Original {
599 self.revision_unsupported
600 .insert(RevisionUnsupportedReason::UnsupportedRevisionMarkup);
601 }
602 self.frames.push(Frame::ChangeSnapshot);
603 return Ok(());
604 }
605 if is_unsupported_revision_markup(name) {
606 self.revision_unsupported
607 .insert(RevisionUnsupportedReason::UnsupportedRevisionMarkup);
608 self.frames.push(Frame::Other);
609 return Ok(());
610 }
611
612 let frame = match name {
613 b"tbl" => {
614 let ordinal = self.next_table_ordinal;
615 self.next_table_ordinal = self
616 .next_table_ordinal
617 .checked_add(1)
618 .ok_or(ProjectionError::InvalidDocumentXml)?;
619 Frame::Table(TableFrame {
620 ordinal,
621 next_row: 0,
622 })
623 }
624 b"tr" => {
625 let Some(table) = enclosing_table(&mut self.frames) else {
626 self.frames.push(Frame::Other);
627 return Ok(());
628 };
629 let row = table.next_row;
630 table.next_row = table
631 .next_row
632 .checked_add(1)
633 .ok_or(ProjectionError::InvalidDocumentXml)?;
634 Frame::Row(RowFrame {
635 table_ordinal: table.ordinal,
636 row,
637 next_column: 0,
638 })
639 }
640 b"tc" => {
641 let Some(row) = enclosing_row(&mut self.frames) else {
642 self.frames.push(Frame::Other);
643 return Ok(());
644 };
645 let column = row.next_column;
646 row.next_column = row
647 .next_column
648 .checked_add(1)
649 .ok_or(ProjectionError::InvalidDocumentXml)?;
650 Frame::Cell(CellFrame {
651 table_ordinal: row.table_ordinal,
652 row: row.row,
653 column,
654 })
655 }
656 b"p" => {
657 if self.current_paragraph.is_some() {
658 self.frames.push(Frame::Other);
659 return Ok(());
660 }
661 if self.paragraphs.len() >= self.maximum_paragraphs {
662 return Err(ProjectionError::TooManyParagraphs);
663 }
664 let package_paragraph_id = attribute_2010(reader, element, b"paraId")?
665 .as_deref()
666 .and_then(PackageParagraphId::parse);
667 let structure = self.frames.iter().rev().find_map(|frame| match frame {
668 Frame::Cell(cell) => Some(ParagraphStructure {
669 table_ordinal: cell.table_ordinal,
670 row: cell.row,
671 column: cell.column,
672 }),
673 _ => None,
674 });
675 self.current_paragraph = Some(ParagraphBuilder {
676 package_paragraph_id,
677 text: String::new(),
678 utf16_len: 0,
679 formatting: Vec::new(),
680 structure,
681 properties: ParagraphProperties::default(),
682 paragraph_mark_revision: None,
683 });
684 Frame::Paragraph
685 }
686 b"r" if self.current_paragraph.is_some() => Frame::Run(RunFrame {
687 text: String::new(),
688 bold: false,
689 highlighted: false,
690 hidden: false,
691 direct_child_count: 0,
692 }),
693 b"rPr" => {
694 let Some((run_frame, child_index)) = direct_run_child else {
695 self.frames.push(Frame::RunProperties(RunPropertiesFrame {
696 run_frame: usize::MAX,
697 hidden_eligible: false,
698 }));
699 return Ok(());
700 };
701 Frame::RunProperties(RunPropertiesFrame {
702 run_frame,
703 hidden_eligible: child_index == 0,
704 })
705 }
706 b"pPr" => Frame::ParagraphProperties,
707 b"trPr" => Frame::TableRowProperties,
708 b"tcPr" => Frame::TableCellProperties,
709 b"pStyle" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
710 if let Some(paragraph) = self.current_paragraph.as_mut() {
711 paragraph.properties.style_id = attribute(reader, element, b"val")?;
712 }
713 Frame::Other
714 }
715 b"ind" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
716 if let Some(paragraph) = self.current_paragraph.as_mut() {
717 paragraph.properties.indentation = parse_indentation(reader, element)?;
718 }
719 Frame::Other
720 }
721 b"outlineLvl" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
722 if let Some(paragraph) = self.current_paragraph.as_mut() {
723 paragraph.properties.outline_level =
724 Some(parse_outline_level_attribute(reader, element)?);
725 }
726 Frame::Other
727 }
728 b"numPr" if matches!(self.frames.last(), Some(Frame::ParagraphProperties)) => {
729 if let Some(paragraph) = self.current_paragraph.as_mut() {
730 paragraph.properties.numbering.present = true;
731 }
732 Frame::NumberingProperties
733 }
734 b"numId" if matches!(self.frames.last(), Some(Frame::NumberingProperties)) => {
735 if let Some(paragraph) = self.current_paragraph.as_mut() {
736 paragraph.properties.numbering.num_id =
737 Some(parse_u32_attribute(reader, element)?);
738 }
739 Frame::Other
740 }
741 b"ilvl" if matches!(self.frames.last(), Some(Frame::NumberingProperties)) => {
742 if let Some(paragraph) = self.current_paragraph.as_mut() {
743 paragraph.properties.numbering.level =
744 Some(parse_level_attribute(reader, element)?);
745 }
746 Frame::Other
747 }
748 b"bookmarkStart" => {
749 self.start_bookmark(reader, element)?;
750 Frame::Other
751 }
752 b"bookmarkEnd" => {
753 self.end_bookmark(reader, element)?;
754 Frame::Other
755 }
756 b"hyperlink" => {
757 let anchor = attribute(reader, element, b"anchor")?;
758 let reference =
759 anchor
760 .clone()
761 .filter(|anchor| !anchor.is_empty())
762 .and_then(|anchor| {
763 self.current_point().map(|source| RawInternalReference {
764 reference_id: anchor,
765 source,
766 })
767 });
768 if anchor.is_some() && reference.is_none() {
769 self.references_complete = false;
770 }
771 Frame::Hyperlink(reference)
772 }
773 b"fldSimple" => {
774 self.simple_field(reader, element)?;
775 Frame::Other
776 }
777 b"fldChar" => {
778 self.field_character(reader, element)?;
779 Frame::Other
780 }
781 b"instrText" | b"delInstrText" => Frame::PseudoText(PseudoTextFrame::instruction()),
782 b"ins" | b"del" | b"moveFrom" | b"moveTo" => {
783 if self.inside_table_row_properties() {
784 self.revision_unsupported
785 .insert(RevisionUnsupportedReason::StructuralTableRevision);
786 Frame::Other
787 } else if self.inside_paragraph_mark_properties() && matches!(name, b"ins" | b"del")
788 {
789 if let Some(paragraph) = self.current_paragraph.as_mut() {
790 paragraph.paragraph_mark_revision = Some(if name == b"ins" {
791 ParagraphMarkRevision::Insertion
792 } else {
793 ParagraphMarkRevision::Deletion
794 });
795 }
796 Frame::Other
797 } else {
798 Frame::Revision(RevisionFrame {
799 review_index: attributed_revision,
800 start: self.current_review_point(),
801 suppressed: revision_is_suppressed(name, self.revision_view),
802 text: String::new(),
803 })
804 }
805 }
806 b"cellIns" | b"cellDel" | b"cellMerge" => {
807 self.revision_unsupported
808 .insert(RevisionUnsupportedReason::StructuralTableRevision);
809 Frame::Other
810 }
811 b"sdt" => {
812 let (text_utf8_start, text_utf16_start) =
813 self.current_paragraph.as_ref().map_or((0, 0), |paragraph| {
814 (paragraph.text.len(), paragraph.utf16_len)
815 });
816 Frame::Sdt(SdtFrame {
817 placeholder: false,
818 text_utf8_start,
819 text_utf16_start,
820 })
821 }
822 b"showingPlcHdr" => {
823 if on_off_element_enabled(reader, element)? {
824 self.mark_placeholder();
825 }
826 Frame::Other
827 }
828 b"vanish" => {
829 if on_off_element_enabled(reader, element)? {
830 self.mark_hidden_run();
831 }
832 Frame::Other
833 }
834 b"rStyle" => {
835 if matches!(
836 attribute(reader, element, b"val")?.as_deref(),
837 Some("HideTWBInt" | "HideTWBExt")
838 ) {
839 self.mark_hidden_run();
840 }
841 Frame::Other
842 }
843 b"b" | b"bCs" => {
844 if on_off_element_enabled(reader, element)? {
845 self.mark_bold_run();
846 }
847 Frame::Other
848 }
849 b"highlight" => {
850 if attribute(reader, element, b"val")?
851 .as_deref()
852 .is_some_and(is_semantic_highlight_color)
853 {
854 self.mark_highlighted_run();
855 }
856 Frame::Other
857 }
858 b"t" | b"delText" => {
859 Frame::PseudoText(PseudoTextFrame::text(preserves_xml_space(reader, element)?))
860 }
861 b"tab" | b"ptab" => {
862 self.append_text_control(TextControl::Tab)?;
863 Frame::Other
864 }
865 b"br" => {
866 let control = match attribute(reader, element, b"type")?.as_deref() {
867 Some("page") => TextControl::PageBreak,
868 Some("column") => TextControl::ColumnBreak,
869 _ => TextControl::LineBreak,
870 };
871 self.append_text_control(control)?;
872 Frame::Other
873 }
874 b"cr" => {
875 self.append_text_control(TextControl::CarriageReturn)?;
876 Frame::Other
877 }
878 b"footnoteReference" => {
879 let visible =
880 attribute(reader, element, b"customMarkFollows")?.is_none_or(|value| {
881 matches!(value.to_ascii_lowercase().as_str(), "0" | "false" | "off")
882 });
883 if visible {
884 self.append_text_control(TextControl::FootnoteReference)?;
885 }
886 Frame::Other
887 }
888 b"softHyphen" => {
889 self.append_text_control(TextControl::SoftHyphen)?;
890 Frame::Other
891 }
892 b"noBreakHyphen" => {
893 self.append_text_control(TextControl::NoBreakHyphen)?;
894 Frame::Other
895 }
896 b"sym" => {
897 if let Some(value) = attribute(reader, element, b"char")? {
898 self.append_pseudo_text(&symbol_text(&value))?;
899 }
900 Frame::Other
901 }
902 _ => Frame::Other,
903 };
904 self.frames.push(frame);
905 Ok(())
906 }
907
908 fn end(&mut self) -> Result<(), ProjectionError> {
909 let frame = self
910 .frames
911 .pop()
912 .ok_or(ProjectionError::InvalidDocumentXml)?;
913 match frame {
914 Frame::Paragraph => {
915 let paragraph = self
916 .current_paragraph
917 .take()
918 .ok_or(ProjectionError::InvalidDocumentXml)?;
919 let ordinal = self.paragraphs.len();
920 if let Some(revision) = paragraph.paragraph_mark_revision {
921 self.paragraph_mark_revisions.insert(ordinal, revision);
922 }
923 self.paragraphs.push(RawProjectedParagraph {
924 ordinal: self.paragraphs.len(),
925 package_paragraph_id: paragraph.package_paragraph_id,
926 text: paragraph.text,
927 utf16_len: paragraph.utf16_len,
928 formatting: paragraph.formatting,
929 structure: paragraph.structure,
930 properties: paragraph.properties,
931 });
932 }
933 Frame::Hyperlink(Some(reference)) => self.references.push(reference),
934 Frame::PseudoText(frame) => match frame.kind {
935 PseudoTextKind::Text { preserve_space } => {
936 let text = if preserve_space {
937 frame.text.as_str()
938 } else {
939 trim_xml_whitespace(&frame.text)
940 };
941 self.append_pseudo_text(text)?;
942 }
943 PseudoTextKind::Instruction => self.append_field_instruction(&frame.text),
944 },
945 Frame::Run(run) if !run.hidden && !self.pseudo_text_is_suppressed() => {
946 if let Some(paragraph) = self.current_paragraph.as_mut() {
947 paragraph.append(&run.text, run.bold, run.highlighted)?;
948 }
949 }
950 Frame::Revision(revision) => self.finish_attributed_revision(revision)?,
951 _ => {}
952 }
953 Ok(())
954 }
955
956 fn record_attributed_revision(
957 &mut self,
958 reader: &NsReader<&[u8]>,
959 element: &BytesStart<'_>,
960 name: &[u8],
961 ) -> Result<Option<usize>, ProjectionError> {
962 let Some(kind) = revision_fact_kind(name) else {
963 return Ok(None);
964 };
965 let ReviewRevisionCollection::Complete {
966 maximum_facts,
967 revisions,
968 ..
969 } = &mut self.review_revisions
970 else {
971 return Ok(None);
972 };
973 if revisions.len() >= *maximum_facts {
974 self.review_revisions = ReviewRevisionCollection::LimitExceeded;
975 return Ok(None);
976 }
977 let review_index = revisions.len();
978 revisions.push(AttributedRevision {
979 kind,
980 author: attribute(reader, element, b"author")?.unwrap_or_default(),
981 date: attribute(reader, element, b"date")?,
982 revision_id: attribute(reader, element, b"id")?,
983 content: ReviewDetail::Unknown(ReviewFactUnknownReason::UnsupportedLocation),
984 });
985 Ok(Some(review_index))
986 }
987
988 fn finish_attributed_revision(
989 &mut self,
990 revision: RevisionFrame,
991 ) -> Result<(), ProjectionError> {
992 let (Some(review_index), Some(start), Some(end)) = (
993 revision.review_index,
994 revision.start,
995 self.current_review_point(),
996 ) else {
997 return Ok(());
998 };
999 let ReviewRevisionCollection::Complete { revisions, .. } = &mut self.review_revisions
1000 else {
1001 return Ok(());
1002 };
1003 let attributed = revisions
1004 .get_mut(review_index)
1005 .ok_or(ProjectionError::InvalidDocumentXml)?;
1006 attributed.content = ReviewDetail::Known(RevisionContent {
1007 span: ReviewSpan { start, end },
1008 formatting_only: revision.text.is_empty(),
1009 text: revision.text,
1010 });
1011 Ok(())
1012 }
1013
1014 fn inside_body(&self) -> bool {
1015 self.frames.iter().any(|frame| matches!(frame, Frame::Body))
1016 }
1017
1018 fn inside_textbox(&self) -> bool {
1019 self.frames
1020 .iter()
1021 .any(|frame| matches!(frame, Frame::Textbox))
1022 }
1023
1024 fn inside_change_snapshot(&self) -> bool {
1025 self.frames
1026 .iter()
1027 .any(|frame| matches!(frame, Frame::ChangeSnapshot))
1028 }
1029
1030 fn inside_paragraph_mark_properties(&self) -> bool {
1031 self.current_paragraph.is_some()
1032 && self
1033 .frames
1034 .iter()
1035 .any(|frame| matches!(frame, Frame::ParagraphProperties))
1036 && !self
1037 .frames
1038 .iter()
1039 .any(|frame| matches!(frame, Frame::Run(_)))
1040 }
1041
1042 fn inside_table_row_properties(&self) -> bool {
1043 self.frames
1044 .iter()
1045 .any(|frame| matches!(frame, Frame::TableRowProperties))
1046 }
1047
1048 fn pseudo_text_is_suppressed(&self) -> bool {
1049 self.frames.iter().any(|frame| {
1050 matches!(
1051 frame,
1052 Frame::ParagraphProperties
1053 | Frame::RunProperties(_)
1054 | Frame::ChangeSnapshot
1055 | Frame::Textbox
1056 ) || matches!(frame, Frame::Revision(revision) if revision.suppressed)
1057 || matches!(frame, Frame::Sdt(sdt) if sdt.placeholder)
1058 })
1059 }
1060
1061 fn pseudo_text_is_structurally_suppressed(&self) -> bool {
1062 self.frames.iter().any(|frame| {
1063 matches!(
1064 frame,
1065 Frame::ParagraphProperties
1066 | Frame::RunProperties(_)
1067 | Frame::ChangeSnapshot
1068 | Frame::Textbox
1069 ) || matches!(frame, Frame::Sdt(sdt) if sdt.placeholder)
1070 })
1071 }
1072
1073 fn append_pseudo_text(&mut self, text: &str) -> Result<(), ProjectionError> {
1074 if self.current_paragraph.is_none()
1075 || self.pseudo_text_is_structurally_suppressed()
1076 || self
1077 .frames
1078 .iter()
1079 .rev()
1080 .any(|frame| matches!(frame, Frame::Run(run) if run.hidden))
1081 {
1082 return Ok(());
1083 }
1084 self.append_review_text(text);
1085 if self.pseudo_text_is_suppressed() {
1086 return Ok(());
1087 }
1088 if let Some(run) = self.frames.iter_mut().rev().find_map(|frame| match frame {
1089 Frame::Run(run) => Some(run),
1090 _ => None,
1091 }) {
1092 run.text.push_str(text);
1093 return Ok(());
1094 }
1095 self.current_paragraph
1096 .as_mut()
1097 .ok_or(ProjectionError::InvalidDocumentXml)?
1098 .append(text, false, false)
1099 }
1100
1101 fn append_review_text(&mut self, text: &str) {
1102 let ReviewRevisionCollection::Complete {
1103 remaining_detail_bytes,
1104 ..
1105 } = &mut self.review_revisions
1106 else {
1107 return;
1108 };
1109 let mut limit_exceeded = false;
1110 for frame in &mut self.frames {
1111 if let Frame::Revision(revision) = frame
1112 && revision.review_index.is_some()
1113 {
1114 let Some(remaining) = remaining_detail_bytes.checked_sub(text.len()) else {
1115 limit_exceeded = true;
1116 break;
1117 };
1118 *remaining_detail_bytes = remaining;
1119 revision.text.push_str(text);
1120 }
1121 }
1122 if limit_exceeded {
1123 self.review_revisions = ReviewRevisionCollection::LimitExceeded;
1124 }
1125 }
1126
1127 fn current_review_point(&self) -> Option<ReviewPoint> {
1128 let paragraph = self.current_paragraph.as_ref()?;
1129 let run_text = self.frames.iter().rev().find_map(|frame| match frame {
1130 Frame::Run(run) if !run.hidden && !self.pseudo_text_is_suppressed() => {
1131 Some(run.text.as_str())
1132 }
1133 _ => None,
1134 });
1135 let run_utf8 = run_text.map_or(0, str::len);
1136 let run_utf16 = run_text.map_or(0, |text| text.encode_utf16().count());
1137 Some(ReviewPoint {
1138 paragraph_ordinal: self.paragraphs.len(),
1139 utf8: u32::try_from(paragraph.text.len().checked_add(run_utf8)?).ok()?,
1140 utf16: paragraph
1141 .utf16_len
1142 .checked_add(u32::try_from(run_utf16).ok()?)?,
1143 })
1144 }
1145
1146 fn next_paragraph_start_review_point(&self) -> Option<ReviewPoint> {
1147 if self.current_paragraph.is_some() {
1148 return self.current_review_point();
1149 }
1150 self.inside_body().then_some(ReviewPoint {
1151 paragraph_ordinal: self.paragraphs.len(),
1152 utf8: 0,
1153 utf16: 0,
1154 })
1155 }
1156
1157 fn previous_paragraph_end_review_point(&self) -> Option<ReviewPoint> {
1158 if self.current_paragraph.is_some() {
1159 return self.current_review_point();
1160 }
1161 let paragraph = self.paragraphs.last()?;
1162 Some(ReviewPoint {
1163 paragraph_ordinal: paragraph.ordinal,
1164 utf8: u32::try_from(paragraph.text.len()).ok()?,
1165 utf16: paragraph.utf16_len,
1166 })
1167 }
1168
1169 fn review_comment_id(
1170 reader: &NsReader<&[u8]>,
1171 element: &BytesStart<'_>,
1172 ) -> Result<Option<String>, ProjectionError> {
1173 attribute(reader, element, b"id")
1174 }
1175
1176 fn start_review_comment_anchor(
1177 &mut self,
1178 reader: &NsReader<&[u8]>,
1179 element: &BytesStart<'_>,
1180 ) -> Result<(), ProjectionError> {
1181 let (Some(comment_id), Some(point)) = (
1182 Self::review_comment_id(reader, element)?,
1183 self.next_paragraph_start_review_point(),
1184 ) else {
1185 return Ok(());
1186 };
1187 if self.invalid_review_comment_anchors.contains(&comment_id) {
1188 return Ok(());
1189 }
1190 if self
1191 .open_review_comment_anchors
1192 .insert(comment_id.clone(), point)
1193 .is_some()
1194 {
1195 self.open_review_comment_anchors.remove(&comment_id);
1196 self.review_comment_anchors.remove(&comment_id);
1197 self.invalid_review_comment_anchors.insert(comment_id);
1198 }
1199 Ok(())
1200 }
1201
1202 fn suppress_review_comment_anchor(
1203 &mut self,
1204 reader: &NsReader<&[u8]>,
1205 element: &BytesStart<'_>,
1206 ) -> Result<(), ProjectionError> {
1207 let Some(comment_id) = Self::review_comment_id(reader, element)? else {
1208 return Ok(());
1209 };
1210 self.open_review_comment_anchors.remove(&comment_id);
1211 self.review_comment_anchors.remove(&comment_id);
1212 self.invalid_review_comment_anchors.insert(comment_id);
1213 Ok(())
1214 }
1215
1216 fn end_review_comment_anchor(
1217 &mut self,
1218 reader: &NsReader<&[u8]>,
1219 element: &BytesStart<'_>,
1220 ) -> Result<(), ProjectionError> {
1221 let (Some(comment_id), Some(end)) = (
1222 Self::review_comment_id(reader, element)?,
1223 self.previous_paragraph_end_review_point(),
1224 ) else {
1225 return Ok(());
1226 };
1227 if self.invalid_review_comment_anchors.contains(&comment_id) {
1228 return Ok(());
1229 }
1230 let Some(start) = self.open_review_comment_anchors.remove(&comment_id) else {
1231 self.review_comment_anchors.remove(&comment_id);
1232 self.invalid_review_comment_anchors.insert(comment_id);
1233 return Ok(());
1234 };
1235 if self
1236 .review_comment_anchors
1237 .insert(comment_id.clone(), ReviewSpan { start, end })
1238 .is_some()
1239 {
1240 self.review_comment_anchors.remove(&comment_id);
1241 self.invalid_review_comment_anchors.insert(comment_id);
1242 }
1243 Ok(())
1244 }
1245
1246 fn record_review_comment_reference(
1247 &mut self,
1248 reader: &NsReader<&[u8]>,
1249 element: &BytesStart<'_>,
1250 ) -> Result<(), ProjectionError> {
1251 let (Some(comment_id), Some(point)) = (
1252 Self::review_comment_id(reader, element)?,
1253 self.current_review_point(),
1254 ) else {
1255 return Ok(());
1256 };
1257 if self.invalid_review_comment_anchors.contains(&comment_id) {
1258 return Ok(());
1259 }
1260 if !self.review_comment_anchors.contains_key(&comment_id)
1261 && !self.open_review_comment_anchors.contains_key(&comment_id)
1262 {
1263 self.review_comment_anchors.insert(
1264 comment_id,
1265 ReviewSpan {
1266 start: point,
1267 end: point,
1268 },
1269 );
1270 }
1271 Ok(())
1272 }
1273
1274 fn append_pseudo_content(&mut self, text: &str) -> Result<(), ProjectionError> {
1275 let Some(Frame::PseudoText(frame)) = self.frames.last_mut() else {
1276 return Err(ProjectionError::InvalidDocumentXml);
1277 };
1278 frame.text.push_str(text);
1279 Ok(())
1280 }
1281
1282 fn append_text_control(&mut self, control: TextControl) -> Result<(), ProjectionError> {
1283 if let Some(text) = control.materialize(self.text_materialization) {
1284 self.append_pseudo_text(text)?;
1285 }
1286 Ok(())
1287 }
1288
1289 fn append_field_instruction(&mut self, text: &str) {
1290 let Some(field) = self.fields.last_mut() else {
1291 self.references_complete = false;
1292 return;
1293 };
1294 if field.separated || field.instruction.len().saturating_add(text.len()) > 4096 {
1295 self.references_complete = false;
1296 return;
1297 }
1298 field.instruction.push_str(text);
1299 }
1300
1301 fn simple_field(
1302 &mut self,
1303 reader: &NsReader<&[u8]>,
1304 element: &BytesStart<'_>,
1305 ) -> Result<(), ProjectionError> {
1306 let instruction = attribute(reader, element, b"instr")?;
1307 let source = self.current_point();
1308 let Some(instruction) = instruction.filter(|value| value.len() <= 4096) else {
1309 self.references_complete = false;
1310 return Ok(());
1311 };
1312 self.record_field_reference(&instruction, source);
1313 Ok(())
1314 }
1315
1316 fn field_character(
1317 &mut self,
1318 reader: &NsReader<&[u8]>,
1319 element: &BytesStart<'_>,
1320 ) -> Result<(), ProjectionError> {
1321 match attribute(reader, element, b"fldCharType")?.as_deref() {
1322 Some("begin") => self.fields.push(FieldFrame {
1323 source: self.current_field_point(),
1324 instruction: String::new(),
1325 separated: false,
1326 }),
1327 Some("separate") => {
1328 let Some(mut field) = self.fields.pop() else {
1329 self.references_complete = false;
1330 return Ok(());
1331 };
1332 if field.separated {
1333 self.references_complete = false;
1334 } else {
1335 self.record_field_reference(&field.instruction, field.source.clone());
1336 field.separated = true;
1337 }
1338 self.fields.push(field);
1339 }
1340 Some("end") => {
1341 let Some(field) = self.fields.pop() else {
1342 self.references_complete = false;
1343 return Ok(());
1344 };
1345 if !field.separated {
1346 self.record_field_reference(&field.instruction, field.source);
1347 }
1348 }
1349 _ => self.references_complete = false,
1350 }
1351 Ok(())
1352 }
1353
1354 fn current_field_point(&self) -> Option<RawBlockPoint> {
1355 let paragraph = self.current_paragraph.as_ref()?;
1356 if self.pseudo_text_is_suppressed() {
1357 return None;
1358 }
1359 if self.frames.iter().rev().find_map(|frame| match frame {
1360 Frame::Run(run) => Some(!run.text.is_empty()),
1361 _ => None,
1362 }) == Some(true)
1363 {
1364 return None;
1365 }
1366 Some(RawBlockPoint {
1367 paragraph: self.paragraphs.len(),
1368 utf8: u32::try_from(paragraph.text.len()).ok()?,
1369 utf16: paragraph.utf16_len,
1370 })
1371 }
1372
1373 fn record_field_reference(&mut self, instruction: &str, source: Option<RawBlockPoint>) {
1374 match internal_reference_target(instruction) {
1375 Ok(Some(reference_id)) => {
1376 let Some(source) = source else {
1377 self.references_complete = false;
1378 return;
1379 };
1380 self.references.push(RawInternalReference {
1381 reference_id,
1382 source,
1383 });
1384 }
1385 Ok(None) => {}
1386 Err(()) => self.references_complete = false,
1387 }
1388 }
1389
1390 fn current_point(&self) -> Option<RawBlockPoint> {
1391 if let Some(paragraph) = self.current_paragraph.as_ref() {
1392 if self.pseudo_text_is_suppressed()
1393 || self
1394 .frames
1395 .iter()
1396 .any(|frame| matches!(frame, Frame::Run(_)))
1397 {
1398 return None;
1399 }
1400 return Some(RawBlockPoint {
1401 paragraph: self.paragraphs.len(),
1402 utf8: u32::try_from(paragraph.text.len()).ok()?,
1403 utf16: paragraph.utf16_len,
1404 });
1405 }
1406 if !matches!(self.frames.last(), Some(Frame::Body)) {
1407 return None;
1408 }
1409 self.paragraphs.last().map_or(
1410 Some(RawBlockPoint {
1411 paragraph: 0,
1412 utf8: 0,
1413 utf16: 0,
1414 }),
1415 |paragraph| {
1416 Some(RawBlockPoint {
1417 paragraph: paragraph.ordinal,
1418 utf8: u32::try_from(paragraph.text.len()).ok()?,
1419 utf16: u32::try_from(paragraph.text.encode_utf16().count()).ok()?,
1420 })
1421 },
1422 )
1423 }
1424
1425 fn start_bookmark(
1426 &mut self,
1427 reader: &NsReader<&[u8]>,
1428 element: &BytesStart<'_>,
1429 ) -> Result<(), ProjectionError> {
1430 let id = attribute(reader, element, b"id")?.and_then(|value| value.parse::<u32>().ok());
1431 let name = attribute(reader, element, b"name")?;
1432 let point = self.current_point();
1433 let (Some(id), Some(name), Some(point)) = (id, name, point) else {
1434 self.bookmarks_complete = false;
1435 return Ok(());
1436 };
1437 if name.is_empty()
1438 || !self.seen_bookmark_ids.insert(id)
1439 || self.open_bookmarks.insert(id, (name, point)).is_some()
1440 {
1441 self.bookmarks_complete = false;
1442 }
1443 Ok(())
1444 }
1445
1446 fn end_bookmark(
1447 &mut self,
1448 reader: &NsReader<&[u8]>,
1449 element: &BytesStart<'_>,
1450 ) -> Result<(), ProjectionError> {
1451 let id = attribute(reader, element, b"id")?.and_then(|value| value.parse::<u32>().ok());
1452 let point = self.current_point();
1453 let (Some(id), Some(end)) = (id, point) else {
1454 self.bookmarks_complete = false;
1455 return Ok(());
1456 };
1457 let Some((name, start)) = self.open_bookmarks.remove(&id) else {
1458 self.bookmarks_complete = false;
1459 return Ok(());
1460 };
1461 self.bookmarks.push(RawBookmarkRange {
1462 id,
1463 name,
1464 start,
1465 end,
1466 });
1467 Ok(())
1468 }
1469
1470 fn mark_placeholder(&mut self) {
1471 let Some(sdt) = self.frames.iter_mut().rev().find_map(|frame| match frame {
1472 Frame::Sdt(sdt) => Some(sdt),
1473 _ => None,
1474 }) else {
1475 return;
1476 };
1477 sdt.placeholder = true;
1478 if let Some(paragraph) = self.current_paragraph.as_mut() {
1479 paragraph.truncate(sdt.text_utf8_start, sdt.text_utf16_start);
1480 }
1481 }
1482
1483 fn mark_hidden_run(&mut self) {
1484 let Some((run_frame, hidden_eligible)) =
1485 self.frames.iter().rev().find_map(|frame| match frame {
1486 Frame::RunProperties(properties) => {
1487 Some((properties.run_frame, properties.hidden_eligible))
1488 }
1489 _ => None,
1490 })
1491 else {
1492 return;
1493 };
1494 if !hidden_eligible || run_frame == usize::MAX {
1495 return;
1496 }
1497 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1498 run.hidden = true;
1499 }
1500 }
1501
1502 fn mark_bold_run(&mut self) {
1503 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
1504 return;
1505 };
1506 let run_frame = properties.run_frame;
1507 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1508 run.bold = true;
1509 }
1510 }
1511
1512 fn mark_highlighted_run(&mut self) {
1513 let Some(Frame::RunProperties(properties)) = self.frames.last() else {
1514 return;
1515 };
1516 let run_frame = properties.run_frame;
1517 if let Some(Frame::Run(run)) = self.frames.get_mut(run_frame) {
1518 run.highlighted = true;
1519 }
1520 }
1521}
1522
1523const fn revision_fact_kind(name: &[u8]) -> Option<RevisionFactKind> {
1524 match name {
1525 b"ins" => Some(RevisionFactKind::Insertion),
1526 b"del" => Some(RevisionFactKind::Deletion),
1527 b"moveFrom" => Some(RevisionFactKind::MoveFrom),
1528 b"moveTo" => Some(RevisionFactKind::MoveTo),
1529 b"cellIns" => Some(RevisionFactKind::CellInsertion),
1530 b"cellDel" => Some(RevisionFactKind::CellDeletion),
1531 b"cellMerge" => Some(RevisionFactKind::CellMerge),
1532 b"pPrChange" => Some(RevisionFactKind::ParagraphPropertiesChange),
1533 b"rPrChange" => Some(RevisionFactKind::RunPropertiesChange),
1534 b"sectPrChange" => Some(RevisionFactKind::SectionPropertiesChange),
1535 b"tblPrChange" => Some(RevisionFactKind::TablePropertiesChange),
1536 b"trPrChange" => Some(RevisionFactKind::TableRowPropertiesChange),
1537 b"tcPrChange" => Some(RevisionFactKind::TableCellPropertiesChange),
1538 b"tblGridChange" => Some(RevisionFactKind::TableGridChange),
1539 b"customXmlDelRangeStart" => Some(RevisionFactKind::CustomXmlDeletionRangeStart),
1540 b"customXmlDelRangeEnd" => Some(RevisionFactKind::CustomXmlDeletionRangeEnd),
1541 b"customXmlInsRangeStart" => Some(RevisionFactKind::CustomXmlInsertionRangeStart),
1542 b"customXmlInsRangeEnd" => Some(RevisionFactKind::CustomXmlInsertionRangeEnd),
1543 b"customXmlMoveFromRangeStart" => Some(RevisionFactKind::CustomXmlMoveFromRangeStart),
1544 b"customXmlMoveFromRangeEnd" => Some(RevisionFactKind::CustomXmlMoveFromRangeEnd),
1545 b"customXmlMoveToRangeStart" => Some(RevisionFactKind::CustomXmlMoveToRangeStart),
1546 b"customXmlMoveToRangeEnd" => Some(RevisionFactKind::CustomXmlMoveToRangeEnd),
1547 _ => None,
1548 }
1549}
1550
1551fn enclosing_table(frames: &mut [Frame]) -> Option<&mut TableFrame> {
1552 for frame in frames.iter_mut().rev() {
1553 match frame {
1554 Frame::Table(table) => return Some(table),
1555 Frame::Row(_) | Frame::Cell(_) | Frame::Paragraph | Frame::Run(_) => return None,
1556 _ => {}
1557 }
1558 }
1559 None
1560}
1561
1562fn enclosing_row(frames: &mut [Frame]) -> Option<&mut RowFrame> {
1563 for frame in frames.iter_mut().rev() {
1564 match frame {
1565 Frame::Row(row) => return Some(row),
1566 Frame::Table(_) | Frame::Cell(_) | Frame::Paragraph | Frame::Run(_) => return None,
1567 _ => {}
1568 }
1569 }
1570 None
1571}
1572
1573fn revision_is_suppressed(name: &[u8], view: RevisionView) -> bool {
1574 match view {
1575 RevisionView::Current => matches!(name, b"del" | b"moveFrom"),
1576 RevisionView::Original => matches!(name, b"ins" | b"moveTo"),
1577 }
1578}
1579
1580fn is_change_snapshot(name: &[u8]) -> bool {
1581 matches!(
1582 name,
1583 b"pPrChange"
1584 | b"rPrChange"
1585 | b"sectPrChange"
1586 | b"tblPrChange"
1587 | b"trPrChange"
1588 | b"tcPrChange"
1589 | b"tblGridChange"
1590 )
1591}
1592
1593fn is_unsupported_revision_markup(name: &[u8]) -> bool {
1594 matches!(
1595 name,
1596 b"customXmlDelRangeStart"
1597 | b"customXmlDelRangeEnd"
1598 | b"customXmlInsRangeStart"
1599 | b"customXmlInsRangeEnd"
1600 | b"customXmlMoveFromRangeStart"
1601 | b"customXmlMoveFromRangeEnd"
1602 | b"customXmlMoveToRangeStart"
1603 | b"customXmlMoveToRangeEnd"
1604 )
1605}
1606
1607const fn paragraph_break_is_removed(
1608 revision: Option<ParagraphMarkRevision>,
1609 view: RevisionView,
1610) -> bool {
1611 matches!(
1612 (revision, view),
1613 (Some(ParagraphMarkRevision::Deletion), RevisionView::Current)
1614 | (
1615 Some(ParagraphMarkRevision::Insertion),
1616 RevisionView::Original
1617 )
1618 )
1619}
1620
1621fn normalize_paragraph_revision_view(
1622 paragraphs: &mut Vec<RawProjectedParagraph>,
1623 paragraph_mark_revisions: &mut HashMap<usize, ParagraphMarkRevision>,
1624 view: RevisionView,
1625 unsupported: &mut BTreeSet<RevisionUnsupportedReason>,
1626) -> Result<bool, ProjectionError> {
1627 let mut normalized: Vec<RawProjectedParagraph> = Vec::with_capacity(paragraphs.len());
1628 let mut merge_previous = false;
1629 let mut merged_any = false;
1630
1631 for (source_ordinal, mut paragraph) in std::mem::take(paragraphs).into_iter().enumerate() {
1632 let merge_next =
1633 paragraph_break_is_removed(paragraph_mark_revisions.remove(&source_ordinal), view);
1634 if merge_previous {
1635 let previous = normalized
1636 .last_mut()
1637 .ok_or(ProjectionError::InvalidDocumentXml)?;
1638 if previous.structure != paragraph.structure {
1639 unsupported.insert(RevisionUnsupportedReason::IncompatibleParagraphMerge);
1640 paragraph.ordinal = normalized.len();
1641 normalized.push(paragraph);
1642 merge_previous = merge_next;
1643 continue;
1644 }
1645 let utf16_offset = previous.utf16_len;
1646 for mut span in paragraph.formatting {
1647 span.start_utf16 = span
1648 .start_utf16
1649 .checked_add(utf16_offset)
1650 .ok_or(ProjectionError::InvalidDocumentXml)?;
1651 span.end_utf16 = span
1652 .end_utf16
1653 .checked_add(utf16_offset)
1654 .ok_or(ProjectionError::InvalidDocumentXml)?;
1655 previous.formatting.push(span);
1656 }
1657 previous.text.push_str(¶graph.text);
1658 previous.utf16_len = previous
1659 .utf16_len
1660 .checked_add(paragraph.utf16_len)
1661 .ok_or(ProjectionError::InvalidDocumentXml)?;
1662 merge_previous = merge_next;
1663 merged_any = true;
1664 continue;
1665 }
1666
1667 paragraph.ordinal = normalized.len();
1668 normalized.push(paragraph);
1669 merge_previous = merge_next;
1670 }
1671
1672 *paragraphs = normalized;
1673 Ok(merged_any)
1674}
1675
1676fn internal_reference_target(instruction: &str) -> Result<Option<String>, ()> {
1677 let tokens = field_tokens(instruction)?;
1678 let Some(command) = tokens.first() else {
1679 return Err(());
1680 };
1681 if ["REF", "PAGEREF", "NOTEREF"]
1682 .iter()
1683 .any(|candidate| command.eq_ignore_ascii_case(candidate))
1684 {
1685 return tokens
1686 .get(1)
1687 .filter(|target| !target.is_empty() && !target.starts_with('\\'))
1688 .cloned()
1689 .map(Some)
1690 .ok_or(());
1691 }
1692 if command.eq_ignore_ascii_case("HYPERLINK") {
1693 for (index, token) in tokens.iter().enumerate().skip(1) {
1694 if token.eq_ignore_ascii_case("\\l") {
1695 return tokens
1696 .get(index.checked_add(1).ok_or(())?)
1697 .filter(|target| !target.is_empty())
1698 .cloned()
1699 .map(Some)
1700 .ok_or(());
1701 }
1702 }
1703 }
1704 Ok(None)
1705}
1706
1707fn field_tokens(instruction: &str) -> Result<Vec<String>, ()> {
1708 let mut tokens = Vec::new();
1709 let mut token = String::new();
1710 let mut quoted = false;
1711 for character in instruction.chars() {
1712 match character {
1713 '"' => quoted = !quoted,
1714 whitespace if whitespace.is_whitespace() && !quoted => {
1715 if !token.is_empty() {
1716 tokens.push(std::mem::take(&mut token));
1717 }
1718 }
1719 _ => token.push(character),
1720 }
1721 }
1722 if quoted {
1723 return Err(());
1724 }
1725 if !token.is_empty() {
1726 tokens.push(token);
1727 }
1728 Ok(tokens)
1729}
1730
1731#[must_use]
1734pub fn is_semantic_highlight_color(value: &str) -> bool {
1735 matches!(
1736 value,
1737 "black"
1738 | "blue"
1739 | "cyan"
1740 | "darkBlue"
1741 | "darkCyan"
1742 | "darkGreen"
1743 | "darkMagenta"
1744 | "darkRed"
1745 | "darkYellow"
1746 | "green"
1747 | "magenta"
1748 | "red"
1749 | "yellow"
1750 )
1751}
1752
1753fn attribute(
1754 reader: &NsReader<&[u8]>,
1755 element: &BytesStart<'_>,
1756 name: &[u8],
1757) -> Result<Option<String>, ProjectionError> {
1758 for attribute in element.attributes() {
1759 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
1760 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
1761 if OoxmlNamespace::from_resolved(&namespace) == OoxmlNamespace::Wordprocessing
1762 && local_name.as_ref() == name
1763 {
1764 return attribute
1765 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
1766 .map(|value| Some(value.into_owned()))
1767 .map_err(|_| ProjectionError::InvalidDocumentXml);
1768 }
1769 }
1770 Ok(None)
1771}
1772
1773fn preserves_xml_space(
1774 reader: &NsReader<&[u8]>,
1775 element: &BytesStart<'_>,
1776) -> Result<bool, ProjectionError> {
1777 const XML_NAMESPACE: &[u8] = b"http://www.w3.org/XML/1998/namespace";
1778
1779 for attribute in element.attributes() {
1780 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
1781 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
1782 if matches!(
1783 namespace,
1784 quick_xml::name::ResolveResult::Bound(namespace)
1785 if namespace.as_ref() == XML_NAMESPACE
1786 ) && local_name.as_ref() == b"space"
1787 {
1788 let value = attribute
1789 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
1790 .map_err(|_| ProjectionError::InvalidDocumentXml)?;
1791 return match value.as_ref() {
1792 "default" => Ok(false),
1793 "preserve" => Ok(true),
1794 _ => Err(ProjectionError::InvalidDocumentXml),
1795 };
1796 }
1797 }
1798 Ok(false)
1799}
1800
1801fn on_off_element_enabled(
1802 reader: &NsReader<&[u8]>,
1803 element: &BytesStart<'_>,
1804) -> Result<bool, ProjectionError> {
1805 Ok(attribute(reader, element, b"val")?
1806 .is_none_or(|value| !matches!(value.to_ascii_lowercase().as_str(), "0" | "false" | "off")))
1807}
1808
1809fn trim_xml_whitespace(value: &str) -> &str {
1810 value.trim_matches(['\u{0009}', '\u{000a}', '\u{000d}', '\u{0020}'])
1811}
1812
1813fn attribute_2010(
1814 reader: &NsReader<&[u8]>,
1815 element: &BytesStart<'_>,
1816 name: &[u8],
1817) -> Result<Option<String>, ProjectionError> {
1818 for attribute in element.attributes() {
1819 let attribute = attribute.map_err(|_| ProjectionError::InvalidDocumentXml)?;
1820 let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
1821 if OoxmlNamespace::from_resolved(&namespace) == OoxmlNamespace::Wordprocessing2010
1822 && local_name.as_ref() == name
1823 {
1824 return attribute
1825 .decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder())
1826 .map(|value| Some(value.into_owned()))
1827 .map_err(|_| ProjectionError::InvalidDocumentXml);
1828 }
1829 }
1830 Ok(None)
1831}
1832
1833fn symbol_text(value: &str) -> String {
1834 let prefix = value
1835 .chars()
1836 .take_while(char::is_ascii_hexdigit)
1837 .collect::<String>();
1838 let Some(code) = (!prefix.is_empty())
1839 .then(|| u32::from_str_radix(&prefix, 16).ok())
1840 .flatten()
1841 else {
1842 return value.to_owned();
1843 };
1844 char::from_u32(code).unwrap_or('\u{fffd}').to_string()
1845}