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