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