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