Skip to main content

stella_docx_kernel/projection/
ooxml.rs

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)] // A single streaming event loop keeps XML parsing one-pass.
349pub(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)] // The frame transition table is one streaming state machine.
542    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(&paragraph.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/// Returns whether an OOXML `ST_HighlightColor` value represents a visible
1757/// semantic marker. Run shading (`w:shd`) is a distinct formatting fact.
1758#[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}