Skip to main content

rdocx_oxml/
text.rs

1//! Text content elements: `CT_P` (paragraph), `CT_R` (run), `CT_Text`.
2
3use std::borrow::Cow;
4use std::cell::Cell;
5use std::collections::HashSet;
6use std::sync::atomic::{AtomicU64, Ordering};
7
8use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
9use quick_xml::name::{Namespace, ResolveResult};
10use quick_xml::{NsReader, Reader, Writer, XmlVersion};
11
12use crate::content_control::{CT_Sdt, SdtContent, SdtOwner};
13use crate::drawing::CT_Drawing;
14use crate::error::{OxmlError, Result};
15use crate::math::OfficeMath;
16use crate::namespace::{R_NS, matches_local_name};
17use crate::numbering::{namespace_bindings, parse_scoped_ppr, word_prefixes_at};
18use crate::properties::{CT_PPr, CT_RPr, is_word_attribute, is_word_element};
19use crate::raw_xml::{capture_element, capture_empty_element};
20use crate::revision::{CT_Revision, RevisionKind};
21use crate::ruby::CT_Ruby;
22use crate::table::{CT_Row, CT_Tbl, CT_Tc, CellContent};
23
24struct CheckedTextOwner {
25    name: Option<Vec<u8>>,
26    content_seen: bool,
27    properties_seen: bool,
28    last_property_slot: Option<u8>,
29    property_names: HashSet<Vec<u8>>,
30    property_leaf: bool,
31    sdt_owner: Option<SdtOwner>,
32}
33
34/// Admit a complete fragment before any typed recursive construction.
35/// The preservation reader may retain unmodelled children. Checked owner
36/// replacement additionally requires qualified roots and modeled p/r order.
37pub(crate) fn validate_word_owner_fragment(xml: &[u8], root: &[u8], checked: bool) -> Result<()> {
38    if checked {
39        oxml_core::xml::validate_strict_xml_1_0(xml)
40            .map_err(|error| OxmlError::InvalidValue(format!("element XML: {error:?}")))?;
41    }
42    let mut reader = NsReader::from_reader(xml);
43    reader.config_mut().trim_text(false);
44    let mut stack = Vec::<CheckedTextOwner>::new();
45    let mut root_seen = false;
46    let mut buffer = Vec::new();
47    loop {
48        let (namespace, event) = reader.read_resolved_event_into(&mut buffer)?;
49        let word = matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == crate::namespace::W_NS.as_bytes()
50            || std::str::from_utf8(uri).ok().and_then(|uri| quick_xml::escape::unescape(uri).ok()).is_some_and(|uri| uri == crate::namespace::W_NS));
51        let mut modeled = word && stack.last().is_none_or(|frame| frame.name.is_some());
52        let mut property_leaf = false;
53        let mut sdt_owner = stack.last().and_then(|frame| frame.sdt_owner);
54        match event {
55            Event::DocType(_) => {
56                return Err(OxmlError::InvalidValue(
57                    "element XML cannot contain a DTD or entity declaration".into(),
58                ));
59            }
60            Event::Start(ref element) | Event::Empty(ref element) => {
61                let name = element.local_name();
62                let local = name.as_ref();
63                if checked && stack.last().is_some_and(|frame| frame.property_leaf) {
64                    return Err(OxmlError::InvalidValue(
65                        "element XML known property leaf cannot contain child elements".into(),
66                    ));
67                }
68                if stack.is_empty() {
69                    if root_seen || local != root || (checked && !word) {
70                        return Err(OxmlError::InvalidValue(format!(
71                            "element XML requires exactly one qualified Word {} root",
72                            String::from_utf8_lossy(root)
73                        )));
74                    }
75                    root_seen = true;
76                }
77                if checked && modeled {
78                    if let Some(frame) = stack.last_mut()
79                        && let Some(parent) = frame.name.as_deref()
80                    {
81                        let content_seen = &mut frame.content_seen;
82                        let properties_seen = &mut frame.properties_seen;
83                        if let Some((slot, repeatable)) =
84                            crate::table::checked_owner_child_slot(parent, local)?
85                        {
86                            if frame.last_property_slot.is_some_and(|last| last > slot)
87                                || (!repeatable && !frame.property_names.insert(local.to_vec()))
88                            {
89                                return Err(OxmlError::InvalidValue("element XML table owner has duplicate or out-of-order known children".into()));
90                            }
91                            frame.last_property_slot = Some(slot);
92                        }
93                        if parent == b"sectPr"
94                            && let Some((slot, repeatable)) =
95                                crate::document::checked_section_child_slot(local)
96                        {
97                            if frame.last_property_slot.is_some_and(|last| last > slot)
98                                || (!repeatable && !frame.property_names.insert(local.to_vec()))
99                            {
100                                return Err(OxmlError::InvalidValue("element XML section has duplicate or out-of-order known children".into()));
101                            }
102                            frame.last_property_slot = Some(slot);
103                        }
104                        let properties = match parent {
105                            b"p" => Some(b"pPr".as_slice()),
106                            b"r" => Some(b"rPr".as_slice()),
107                            _ => None,
108                        };
109                        if let Some(properties) = properties {
110                            if local == properties {
111                                if *content_seen || *properties_seen {
112                                    return Err(OxmlError::InvalidValue(format!(
113                                        "element XML {} must occur once before content",
114                                        String::from_utf8_lossy(properties)
115                                    )));
116                                }
117                                *properties_seen = true;
118                            } else {
119                                *content_seen = true;
120                                if matches!(
121                                    local,
122                                    b"p" | b"tbl" | b"tr" | b"tc" | b"sectPr" | b"pPr" | b"rPr"
123                                ) || (parent == b"r"
124                                    && matches!(local, b"r" | b"hyperlink" | b"fldSimple" | b"sdt"))
125                                {
126                                    return Err(OxmlError::InvalidValue(format!(
127                                        "element XML {} is not a valid direct child of {}",
128                                        String::from_utf8_lossy(local),
129                                        String::from_utf8_lossy(parent)
130                                    )));
131                                }
132                            }
133                        }
134                    }
135                } else if checked
136                    && let Some(frame) = stack.last_mut()
137                    && matches!(frame.name.as_deref(), Some(b"p" | b"r"))
138                {
139                    frame.content_seen = true;
140                }
141                if checked && modeled {
142                    let mut prefixes = Vec::new();
143                    for (prefix, namespace) in reader.resolver().bindings() {
144                        if namespace.as_ref() == crate::namespace::W_NS.as_bytes()
145                            || std::str::from_utf8(namespace.as_ref())
146                                .ok()
147                                .and_then(|uri| quick_xml::escape::unescape(uri).ok())
148                                .is_some_and(|uri| uri == crate::namespace::W_NS)
149                        {
150                            let prefix = match prefix {
151                                quick_xml::name::PrefixDeclaration::Default => "",
152                                quick_xml::name::PrefixDeclaration::Named(prefix) => {
153                                    std::str::from_utf8(prefix)?
154                                }
155                            };
156                            prefixes.push(prefix.to_owned());
157                        }
158                    }
159                    if stack
160                        .last()
161                        .is_some_and(|frame| frame.name.as_deref() == Some(b"sectPr"))
162                    {
163                        for (prefix, namespace) in reader.resolver().bindings() {
164                            let prefix = match prefix {
165                                quick_xml::name::PrefixDeclaration::Default => "",
166                                quick_xml::name::PrefixDeclaration::Named(prefix) => {
167                                    std::str::from_utf8(prefix)?
168                                }
169                            };
170                            prefixes.push(format!(
171                                "\0{prefix}\0{}",
172                                std::str::from_utf8(namespace.as_ref())?
173                            ));
174                        }
175                        property_leaf =
176                            crate::document::validate_checked_section_child(element, &prefixes)?;
177                    }
178                    if let Some(frame) = stack.last_mut() {
179                        if frame.name.as_deref() == Some(b"numPr")
180                            && matches!(local, b"ilvl" | b"numId")
181                        {
182                            let slot = u8::from(local == b"numId");
183                            if !frame.property_names.insert(local.to_vec())
184                                || frame.last_property_slot.is_some_and(|last| last > slot)
185                            {
186                                return Err(OxmlError::InvalidValue("element XML numbering has duplicate or out-of-order properties".into()));
187                            }
188                            frame.last_property_slot = Some(slot);
189                            crate::paragraph_properties::validate_checked_property_child(
190                                element, &prefixes,
191                            )?;
192                            property_leaf = true;
193                        }
194                        if let Some(parent) = frame.name.as_deref()
195                            && matches!(parent, b"tblPr" | b"tblPrEx" | b"tcPr" | b"trPr")
196                        {
197                            property_leaf = crate::table::validate_checked_property_child(
198                                element, &prefixes, parent,
199                            )?;
200                        }
201                        let slot = match frame.name.as_deref() {
202                            Some(b"pPr") => crate::paragraph_properties::ppr_schema_slot(
203                                element.name().as_ref(),
204                                &prefixes,
205                            ),
206                            Some(b"rPr") => crate::run_properties::rpr_schema_slot(
207                                element.name().as_ref(),
208                                &prefixes,
209                            ),
210                            _ => None,
211                        };
212                        if matches!(frame.name.as_deref(), Some(b"pPr" | b"rPr")) {
213                            if let Some(slot) = slot {
214                                let paragraph_properties = frame.name.as_deref() == Some(b"pPr");
215                                if (paragraph_properties
216                                    && (!frame.property_names.insert(local.to_vec())
217                                        || frame
218                                            .last_property_slot
219                                            .is_some_and(|last| last > slot)))
220                                    || (!paragraph_properties
221                                        && frame.property_names.contains(b"rPrChange".as_slice()))
222                                {
223                                    return Err(OxmlError::InvalidValue(format!(
224                                        "element XML {} has duplicate or out-of-order known properties",
225                                        String::from_utf8_lossy(
226                                            frame.name.as_deref().unwrap_or_default()
227                                        )
228                                    )));
229                                }
230                                frame.last_property_slot = Some(slot);
231                                frame.property_names.insert(local.to_vec());
232                                property_leaf = if paragraph_properties {
233                                    !matches!(
234                                        local,
235                                        b"numPr"
236                                            | b"pBdr"
237                                            | b"tabs"
238                                            | b"rPr"
239                                            | b"sectPr"
240                                            | b"pPrChange"
241                                    )
242                                } else {
243                                    local != b"rPrChange"
244                                };
245                                if frame.name.as_deref() == Some(b"pPr") {
246                                    crate::paragraph_properties::validate_checked_property_child(
247                                        element, &prefixes,
248                                    )?;
249                                } else {
250                                    crate::run_properties::validate_checked_property_child(
251                                        element, &prefixes,
252                                    )?;
253                                }
254                            } else {
255                                modeled = false;
256                            }
257                        }
258                    }
259                }
260                if checked
261                    && modeled
262                    && let Some(parent) = stack.last().and_then(|frame| frame.name.as_deref())
263                {
264                    if local == b"sdt" {
265                        sdt_owner = match parent {
266                            b"p" => Some(SdtOwner::Inline),
267                            b"tbl" => Some(SdtOwner::Table),
268                            b"tr" => Some(SdtOwner::Row),
269                            b"tc" => Some(SdtOwner::Cell),
270                            _ => sdt_owner,
271                        };
272                    }
273                    modeled = match parent {
274                        b"p" => {
275                            local == b"pPr" || crate::document::admitted_word_child(parent, local)
276                        }
277                        b"r" => {
278                            local == b"rPr" || crate::document::admitted_word_child(parent, local)
279                        }
280                        b"tbl" | b"tr" | b"tc" => {
281                            crate::table::checked_owner_child_slot(parent, local)?.is_some()
282                        }
283                        b"sectPr" => crate::document::checked_section_child_slot(local).is_some(),
284                        b"sdt" => local == b"sdtContent",
285                        b"sdtContent" => {
286                            local == b"sdt" || sdt_owner.is_some_and(|owner| owner.owns(local))
287                        }
288                        b"hyperlink" | b"fldSimple" | b"ruby" | b"rt" | b"rubyBase" | b"ins"
289                        | b"del" | b"moveFrom" | b"moveTo" => {
290                            crate::document::admitted_word_child(parent, local)
291                        }
292                        _ => modeled,
293                    };
294                }
295                if matches!(event, Event::Start(_)) {
296                    stack.push(CheckedTextOwner {
297                        name: modeled.then(|| local.to_vec()),
298                        content_seen: false,
299                        properties_seen: false,
300                        last_property_slot: None,
301                        property_names: HashSet::new(),
302                        property_leaf,
303                        sdt_owner,
304                    });
305                }
306            }
307            Event::End(_) => {
308                stack.pop().ok_or_else(|| {
309                    OxmlError::InvalidValue("element XML has an unmatched end".into())
310                })?;
311            }
312            Event::Text(ref text)
313                if stack.is_empty()
314                    || (checked
315                        && stack.last().is_some_and(|frame| {
316                            frame.property_leaf
317                                || frame
318                                    .name
319                                    .as_deref()
320                                    .is_some_and(|name| matches!(name, b"p" | b"r"))
321                        })) =>
322            {
323                if text.as_ref().iter().any(|byte| !byte.is_ascii_whitespace()) {
324                    return Err(OxmlError::InvalidValue(
325                        "element XML has text outside an allowed text owner".into(),
326                    ));
327                }
328            }
329            Event::CData(_) | Event::GeneralRef(_)
330                if stack.is_empty()
331                    || (checked
332                        && stack.last().is_some_and(|frame| {
333                            frame.property_leaf
334                                || frame
335                                    .name
336                                    .as_deref()
337                                    .is_some_and(|name| matches!(name, b"p" | b"r"))
338                        })) =>
339            {
340                return Err(OxmlError::InvalidValue(
341                    "element XML has content outside an allowed text owner".into(),
342                ));
343            }
344            Event::Eof => {
345                if !root_seen || !stack.is_empty() {
346                    return Err(OxmlError::InvalidValue(
347                        "element XML requires one complete root".into(),
348                    ));
349                }
350                return Ok(());
351            }
352            _ => {}
353        }
354        buffer.clear();
355    }
356}
357
358static NEXT_FIELD_SOURCE_ID: AtomicU64 = AtomicU64::new(1);
359
360const ROOT_ATTRIBUTES_ELEMENT: &[u8] = b"rdocxRootAttributes";
361pub(crate) const ROOT_ATTRIBUTES_POSITION: usize = usize::MAX;
362const W14_NS: &str = "http://schemas.microsoft.com/office/word/2010/wordml";
363const MC_NS: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
364const WP_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
365pub(crate) const ROOT_R_BINDING: u8 = 1;
366pub(crate) const ROOT_MC_BINDING: u8 = 2;
367pub(crate) const ROOT_WP_BINDING: u8 = 4;
368
369thread_local! {
370    static ROOT_BINDINGS: Cell<u8> = const { Cell::new(0) };
371    #[cfg(test)]
372    static RUN_METADATA_VISITS: Cell<usize> = const { Cell::new(0) };
373}
374
375/// Keep part-root guarantees in scope only while that part is serialized.
376/// Nested serializations restore the caller's guarantees, including on unwind.
377pub(crate) struct RootBindingScope(u8);
378
379pub(crate) fn root_binding_scope(bindings: u8) -> RootBindingScope {
380    RootBindingScope(ROOT_BINDINGS.with(|current| current.replace(bindings)))
381}
382
383impl Drop for RootBindingScope {
384    fn drop(&mut self) {
385        ROOT_BINDINGS.with(|current| current.set(self.0));
386    }
387}
388
389fn namespace_declaration(name: &[u8]) -> bool {
390    name == b"xmlns" || name.starts_with(b"xmlns:")
391}
392
393fn root_attribute_namespace(name: &[u8], bindings: &[(String, String)]) -> Result<Option<String>> {
394    let Some(separator) = name.iter().position(|byte| *byte == b':') else {
395        return Ok(None);
396    };
397    let prefix = &name[..separator];
398    if prefix == b"xml" {
399        return Ok(Some("http://www.w3.org/XML/1998/namespace".to_owned()));
400    }
401    bindings
402        .iter()
403        .find(|(candidate, _)| candidate.as_bytes() == prefix)
404        .map(|(_, namespace)| Some(namespace.clone()))
405        .ok_or_else(|| {
406            OxmlError::InvalidValue(format!(
407                "root attribute prefix `{}` is unbound",
408                String::from_utf8_lossy(prefix)
409            ))
410        })
411}
412
413/// Prefix dependencies of the known namespace-valued root attributes.
414/// Attribute names and opaque literal values use their normal preservation path.
415fn root_attribute_value_prefixes<'a>(
416    namespace: Option<&str>,
417    local: &[u8],
418    value: &'a str,
419) -> Vec<&'a str> {
420    if namespace == Some(crate::namespace::MC_NS) {
421        match local {
422            b"Ignorable" | b"MustUnderstand" => return value.split_ascii_whitespace().collect(),
423            b"ProcessContent" | b"PreserveAttributes" | b"PreserveElements" => {
424                return value
425                    .split_ascii_whitespace()
426                    .filter_map(|token| token.split_once(':').map(|(prefix, _)| prefix))
427                    .collect();
428            }
429            _ => {}
430        }
431    } else if namespace == Some("http://www.w3.org/2001/XMLSchema-instance") && local == b"type" {
432        return value
433            .split_ascii_whitespace()
434            .map(|token| token.split_once(':').map_or("", |(prefix, _)| prefix))
435            .collect();
436    }
437    Vec::new()
438}
439
440pub(crate) fn capture_root_attribute_record(
441    start: &BytesStart<'_>,
442    prefixes: &[String],
443) -> Result<Option<Vec<u8>>> {
444    let mut bindings = namespace_bindings(prefixes);
445    // A plain scope entry names a Word prefix. `word_prefixes_at` adds one
446    // only beside its binding, but the default scope of the public `from_xml`
447    // entrypoints, `CT_P::from_xml` among them, names `w` by convention and
448    // binds nothing. A caller that parses a paragraph cut out of its part in
449    // that scope, as the text-box replacement and template walkers do, reaches
450    // this capture with it, so the Word prefix resolves here instead of
451    // failing on the first `w:rsidR`. An explicit binding always wins.
452    for prefix in prefixes {
453        if !prefix.starts_with('\0') && !bindings.iter().any(|(bound, _)| bound == prefix) {
454            bindings.push((prefix.clone(), crate::namespace::W_NS.to_owned()));
455        }
456    }
457    let mut attributes = Vec::new();
458    let mut expanded = HashSet::new();
459    let mut used_prefixes = Vec::new();
460    for attribute in start.attributes() {
461        let attribute = attribute?;
462        let name = attribute.key.as_ref();
463        let value = attribute
464            .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())?
465            .into_owned();
466        // A namespace declaration is not retained for its own sake. The loop
467        // below re-declares exactly the prefixes these attributes use, and the
468        // alias machinery declares the rest on the elements that use them, so
469        // recording a declaration here would emit it twice.
470        if namespace_declaration(name) {
471            continue;
472        }
473        let namespace = root_attribute_namespace(name, &bindings)?;
474        let local = name.rsplit(|byte| *byte == b':').next().unwrap_or(name);
475        for prefix in root_attribute_value_prefixes(namespace.as_deref(), local, &value) {
476            if prefix == "xml" || !bindings.iter().any(|(bound, _)| bound == prefix) {
477                continue;
478            }
479            if !used_prefixes
480                .iter()
481                .any(|candidate| candidate == prefix.as_bytes())
482            {
483                used_prefixes.push(prefix.as_bytes().to_vec());
484            }
485        }
486        if !expanded.insert((namespace, local.to_vec())) {
487            return Err(OxmlError::InvalidValue(format!(
488                "duplicate expanded root attribute `{}`",
489                String::from_utf8_lossy(local)
490            )));
491        }
492        if let Some(separator) = name.iter().position(|byte| *byte == b':')
493            && let prefix = &name[..separator]
494            && prefix != b"xml"
495            && !used_prefixes.iter().any(|candidate| candidate == prefix)
496        {
497            used_prefixes.push(prefix.to_vec());
498        }
499        attributes.push((std::str::from_utf8(name)?.to_owned(), value));
500    }
501    if attributes.is_empty() {
502        return Ok(None);
503    }
504
505    let mut record = BytesStart::new(std::str::from_utf8(ROOT_ATTRIBUTES_ELEMENT)?);
506    for (name, value) in &attributes {
507        record.push_attribute((name.as_str(), value.as_str()));
508    }
509    for prefix in used_prefixes {
510        let declaration = if prefix.is_empty() {
511            "xmlns".to_owned()
512        } else {
513            format!("xmlns:{}", String::from_utf8_lossy(&prefix))
514        };
515        let namespace = bindings
516            .iter()
517            .find(|(candidate, _)| candidate.as_bytes() == prefix)
518            .map(|(_, namespace)| namespace.as_str())
519            .ok_or_else(|| {
520                OxmlError::InvalidValue(format!(
521                    "root attribute prefix `{}` is unbound",
522                    String::from_utf8_lossy(&prefix)
523                ))
524            })?;
525        record.push_attribute((declaration.as_str(), namespace));
526    }
527    let mut writer = Writer::new(Vec::new());
528    writer.write_event(Event::Empty(record))?;
529    Ok(Some(writer.into_inner()))
530}
531
532#[doc(hidden)]
533pub fn is_root_attribute_record(raw: &[u8]) -> bool {
534    raw.starts_with(b"<rdocxRootAttributes")
535}
536
537pub(crate) fn push_root_attribute_record(
538    target: &mut BytesStart<'_>,
539    raw: &[u8],
540    replaced: Option<(&str, &str)>,
541) -> Result<()> {
542    let mut reader = NsReader::from_reader(raw);
543    let mut buffer = Vec::new();
544    let element = loop {
545        match reader.read_resolved_event_into(&mut buffer)? {
546            (_, Event::Empty(element)) if element.name().as_ref() == ROOT_ATTRIBUTES_ELEMENT => {
547                break element.into_owned();
548            }
549            (_, Event::Eof) => {
550                return Err(OxmlError::InvalidValue(
551                    "invalid retained root-attribute record".to_owned(),
552                ));
553            }
554            _ => {}
555        }
556        buffer.clear();
557    };
558    let existing = target
559        .attributes()
560        .filter_map(|attribute| attribute.ok())
561        .map(|attribute| attribute.key.as_ref().to_vec())
562        .collect::<HashSet<_>>();
563    // The generated owner and its modeled descendants use canonical w.
564    // A retained producer binding must keep its expanded attribute names
565    // without rebinding that generated owner.
566    let is_run = target.name().as_ref() == b"w:r";
567    let mut foreign_bindings = Vec::new();
568    let mut occupied_prefixes = HashSet::<String>::new();
569    for attribute in target.attributes().chain(element.attributes()) {
570        let attribute = attribute?;
571        let name = std::str::from_utf8(attribute.key.as_ref())?;
572        if let Some(prefix) = name.strip_prefix("xmlns:") {
573            occupied_prefixes.insert(prefix.to_owned());
574        } else if let Some((prefix, _)) = name.split_once(':') {
575            occupied_prefixes.insert(prefix.to_owned());
576        }
577        let generated_uri = match name {
578            "xmlns:w" => Some(crate::namespace::W_NS),
579            "xmlns:wp"
580                if is_run
581                    && target.attributes().any(|attribute| {
582                        attribute.is_ok_and(|attribute| {
583                            attribute.key.as_ref() == b"xmlns:wp"
584                                && attribute.value.as_ref() == WP_NS.as_bytes()
585                        })
586                    }) =>
587            {
588                Some(WP_NS)
589            }
590            "xmlns:r"
591                if is_run
592                    && target.attributes().any(|attribute| {
593                        attribute.is_ok_and(|attribute| {
594                            attribute.key.as_ref() == b"xmlns:r"
595                                && attribute.value.as_ref() == R_NS.as_bytes()
596                        })
597                    }) =>
598            {
599                Some(R_NS)
600            }
601            _ => None,
602        };
603        if let Some(generated) = generated_uri {
604            let value = attribute
605                .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?;
606            if value != generated {
607                let prefix = name.strip_prefix("xmlns:").unwrap_or_default();
608                if !foreign_bindings
609                    .iter()
610                    .any(|(existing, _)| existing == prefix)
611                {
612                    foreign_bindings.push((prefix.to_owned(), value.into_owned()));
613                }
614            }
615        }
616    }
617
618    let mut value_prefixes = HashSet::<String>::new();
619    {
620        for attribute in element.attributes() {
621            let attribute = attribute?;
622            let (namespace, local) = reader.resolver().resolve_attribute(attribute.key);
623            let namespace = match namespace {
624                ResolveResult::Bound(Namespace(uri)) => Some(std::str::from_utf8(uri)?),
625                _ => None,
626            };
627            let value = attribute
628                .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?;
629            for prefix in root_attribute_value_prefixes(namespace, local.as_ref(), &value) {
630                value_prefixes.insert(prefix.to_owned());
631                if foreign_bindings
632                    .iter()
633                    .any(|(foreign, _)| foreign == prefix)
634                {
635                    return Err(OxmlError::InvalidValue(format!(
636                        "element XML namespace-valued root attribute referencing foreign {prefix} is unsupported"
637                    )));
638                }
639                if !prefix.is_empty() && prefix != "xml" && !reader.resolver().bindings().any(|(binding, _)| matches!(binding, quick_xml::name::PrefixDeclaration::Named(bound) if bound == prefix.as_bytes())) {
640                    return Err(OxmlError::InvalidValue("element XML namespace-valued root attribute has an unbound prefix".into()));
641                }
642            }
643        }
644    }
645    let mut producer_prefixes = Vec::new();
646    for (foreign, namespace) in foreign_bindings {
647        let mut suffix = 0usize;
648        let prefix = loop {
649            let prefix = if suffix == 0 {
650                "rdocxProducer".to_owned()
651            } else {
652                format!("rdocxProducer{suffix}")
653            };
654            if occupied_prefixes.insert(prefix.clone()) {
655                break prefix;
656            }
657            suffix = suffix.checked_add(1).ok_or_else(|| {
658                OxmlError::InvalidValue("retained producer namespace prefix space exhausted".into())
659            })?;
660        };
661        target.push_attribute((format!("xmlns:{prefix}").as_str(), namespace.as_str()));
662        producer_prefixes.push((foreign, prefix));
663    }
664    for attribute in element.attributes() {
665        let attribute = attribute?;
666        let name = attribute.key.as_ref();
667        let lexical_name = std::str::from_utf8(name)?;
668        if lexical_name.strip_prefix("xmlns:").is_some_and(|prefix| {
669            producer_prefixes
670                .iter()
671                .any(|(foreign, _)| foreign == prefix)
672        }) {
673            continue;
674        }
675        let producer = lexical_name.split_once(':').and_then(|(prefix, local)| {
676            producer_prefixes
677                .iter()
678                .find(|(foreign, _)| foreign == prefix)
679                .map(|(_, alias)| (alias, local))
680        });
681        if existing.contains(name) && producer.is_none() {
682            continue;
683        }
684        // `w14` is bound by the part root that owns the element, the same
685        // assumption the authored `w14:paraId` write already makes. Rebinding
686        // it here would make a reopened save differ from the save it came
687        // from, purely by a declaration that changes nothing. `w` is in scope
688        // already, since the element's own `w:` name needs it, and a writer
689        // that shadows it declares it on the element first. Word writes
690        // `w:rsid*` on nearly every paragraph and run, so repeating `w` here
691        // repeated it hundreds of times in an edited part.
692        if (name == b"xmlns:w14"
693            && attribute.value.as_ref() == W14_NS.as_bytes()
694            && !value_prefixes.contains("w14"))
695            || (name == b"xmlns:w" && attribute.value.as_ref() == crate::namespace::W_NS.as_bytes())
696        {
697            continue;
698        }
699        let root_bindings = ROOT_BINDINGS.with(Cell::get);
700        if (root_bindings & ROOT_R_BINDING != 0
701            && name == b"xmlns:r"
702            && attribute.value.as_ref() == R_NS.as_bytes())
703            || (root_bindings & ROOT_MC_BINDING != 0
704                && name == b"xmlns:mc"
705                && attribute.value.as_ref() == MC_NS.as_bytes())
706            || (root_bindings & ROOT_WP_BINDING != 0
707                && name == b"xmlns:wp"
708                && attribute.value.as_ref() == WP_NS.as_bytes())
709        {
710            continue;
711        }
712        if !namespace_declaration(name)
713            && let Some((namespace, local)) = replaced
714        {
715            let (resolved, resolved_local) = reader.resolver().resolve_attribute(attribute.key);
716            if resolved_local.as_ref() == local.as_bytes()
717                && matches!(resolved, ResolveResult::Bound(Namespace(uri)) if uri == namespace.as_bytes())
718            {
719                continue;
720            }
721        }
722        let name = std::str::from_utf8(name)?;
723        let value =
724            attribute.decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?;
725        if let Some((prefix, local)) = producer {
726            target.push_attribute((format!("{prefix}:{local}").as_str(), value.as_ref()));
727        } else {
728            target.push_attribute((name, value.as_ref()));
729        }
730    }
731    Ok(())
732}
733
734/// Remove `w14:paraId` and `w14:textId` from the retained start-tag
735/// attributes of a paragraph or table row, given its `extra_xml`.
736///
737/// A copy must not share them with its source, and Word assigns new ones to
738/// an element that has none. Every other retained attribute stays, with the
739/// declarations its prefix needs.
740#[doc(hidden)]
741pub fn drop_w14_paragraph_identities(extra_xml: &mut Vec<(usize, Vec<u8>)>) -> Result<()> {
742    let mut index = 0;
743    while index < extra_xml.len() {
744        let (position, raw) = &extra_xml[index];
745        if *position == ROOT_ATTRIBUTES_POSITION && is_root_attribute_record(raw) {
746            match root_attribute_record_without_w14_identities(raw)? {
747                Some(record) => extra_xml[index].1 = record,
748                None => {
749                    extra_xml.remove(index);
750                    continue;
751                }
752            }
753        }
754        index += 1;
755    }
756    Ok(())
757}
758
759/// Return the record without its w14 identities, or `None` when nothing else
760/// is left in it.
761fn root_attribute_record_without_w14_identities(raw: &[u8]) -> Result<Option<Vec<u8>>> {
762    let mut reader = NsReader::from_reader(raw);
763    let mut buffer = Vec::new();
764    let element = loop {
765        match reader.read_resolved_event_into(&mut buffer)? {
766            (_, Event::Empty(element)) if element.name().as_ref() == ROOT_ATTRIBUTES_ELEMENT => {
767                break element.into_owned();
768            }
769            (_, Event::Eof) => {
770                return Err(OxmlError::InvalidValue(
771                    "invalid retained root-attribute record".to_owned(),
772                ));
773            }
774            _ => {}
775        }
776        buffer.clear();
777    };
778    let mut dropped = false;
779    let mut attributes = Vec::new();
780    let mut declarations = Vec::new();
781    for attribute in element.attributes() {
782        let attribute = attribute?;
783        let name = std::str::from_utf8(attribute.key.as_ref())?.to_owned();
784        let value = attribute
785            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
786            .into_owned();
787        if namespace_declaration(name.as_bytes()) {
788            declarations.push((name, value));
789            continue;
790        }
791        let (namespace, local) = reader.resolver().resolve_attribute(attribute.key);
792        if matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == W14_NS.as_bytes())
793            && matches!(local.as_ref(), b"paraId" | b"textId")
794        {
795            dropped = true;
796            continue;
797        }
798        attributes.push((name, value));
799    }
800    if !dropped {
801        return Ok(Some(raw.to_vec()));
802    }
803    if attributes.is_empty() {
804        return Ok(None);
805    }
806    let mut record = BytesStart::new(std::str::from_utf8(ROOT_ATTRIBUTES_ELEMENT)?);
807    for (name, value) in &attributes {
808        record.push_attribute((name.as_str(), value.as_str()));
809    }
810    // The record declares exactly the prefixes its attributes use, so a
811    // declaration only the dropped identities used goes with them.
812    for (name, value) in &declarations {
813        let prefix = name.strip_prefix("xmlns:").unwrap_or_default();
814        if attributes.iter().any(|(attribute, _)| {
815            attribute
816                .split_once(':')
817                .is_some_and(|(used, _)| used == prefix)
818        }) {
819            record.push_attribute((name.as_str(), value.as_str()));
820        }
821    }
822    let mut writer = Writer::new(Vec::new());
823    writer.write_event(Event::Empty(record))?;
824    Ok(Some(writer.into_inner()))
825}
826
827/// Declare the canonical `w14` prefix on the root of a serialized part whose
828/// content uses the prefix while the root does not bind it.
829///
830/// A retained root-attribute record writes `w14:paraId` and `w14:textId`
831/// without their declaration, because Word and python-docx declare `w14` on
832/// the part root. A root rdocx wrote does not, and a producer may declare the
833/// prefix on the element that uses it, so the part declares it here once. A
834/// root that binds `w14` itself is left as it is.
835#[doc(hidden)]
836pub fn declare_w14_on_part_root(xml: &mut Vec<u8>) -> Result<()> {
837    let mut reader = Reader::from_reader(xml.as_slice());
838    let mut buffer = Vec::new();
839    let root_end = loop {
840        match reader.read_event_into(&mut buffer)? {
841            Event::Start(root) => {
842                for attribute in root.attributes() {
843                    if attribute?.key.as_ref() == b"xmlns:w14" {
844                        return Ok(());
845                    }
846                }
847                break reader.buffer_position() as usize;
848            }
849            Event::Empty(_) | Event::Eof => return Ok(()),
850            _ => {}
851        }
852        buffer.clear();
853    };
854    // A qualified name starts after `<`, `</` or the whitespace before an
855    // attribute. Text that happens to match only adds a declaration.
856    let uses_w14 = xml[root_end..].windows(5).any(|window| {
857        matches!(window[0], b'<' | b'/' | b' ' | b'\t' | b'\r' | b'\n') && &window[1..] == b"w14:"
858    });
859    if uses_w14 {
860        let declaration = format!(r#" xmlns:w14="{W14_NS}""#);
861        xml.splice(root_end - 1..root_end - 1, declaration.into_bytes());
862    }
863    Ok(())
864}
865
866/// `CT_Text` — The text content of a run, with optional xml:space="preserve".
867#[derive(Debug, Clone, PartialEq)]
868pub struct CT_Text {
869    pub text: String,
870    pub preserve_space: bool,
871}
872
873impl CT_Text {
874    pub fn new(text: &str) -> Self {
875        CT_Text {
876            text: text.to_string(),
877            preserve_space: text.starts_with(' ') || text.ends_with(' '),
878        }
879    }
880
881    /// Keep the first `at` characters and return the rest.
882    ///
883    /// Either part preserves space when the original did or when the split
884    /// leaves a space at one of its ends.
885    fn split_off_at(&mut self, at: usize) -> CT_Text {
886        let byte = self
887            .text
888            .char_indices()
889            .nth(at)
890            .map_or(self.text.len(), |(byte, _)| byte);
891        let rest = self.text.split_off(byte);
892        let inherited = self.preserve_space;
893        let preserve = |text: &str| inherited || text.starts_with(' ') || text.ends_with(' ');
894        let rest = CT_Text {
895            preserve_space: preserve(&rest),
896            text: rest,
897        };
898        self.preserve_space = preserve(&self.text);
899        rest
900    }
901}
902
903/// A parsed Word field with its stored result and update marker.
904#[derive(Debug, Clone)]
905pub struct Field {
906    pub instruction: FieldInstruction,
907    pub cached_result: String,
908    pub dirty: Option<bool>,
909    locked: Option<bool>,
910    /// Typed legacy form metadata retained below the begin `w:fldChar`.
911    pub legacy_form: Option<LegacyFormFieldData>,
912    legacy_form_parse_error: bool,
913    nested_order: Vec<NestedFieldPosition>,
914    cached_fields: Vec<(std::ops::Range<usize>, Field)>,
915    simple_cached_runs: Option<Vec<CT_R>>,
916    typed_cached_runs: Option<Vec<CT_R>>,
917    typed_cached_comment_ranges: Vec<CommentRangeMarker>,
918    parsed_cached_comment_ranges: Vec<CommentRangeMarker>,
919    parsed_cached_runs: Option<Vec<CT_R>>,
920    source: FieldSource,
921    /// The physical run this field shares with text outside it, if any.
922    span: Option<Box<FieldRunSpan>>,
923}
924
925/// The model runs a physical field span was split into on read.
926///
927/// A run such as `Page {PAGE} of the report` holds text outside the field.
928/// The reader turns it into sibling runs so every consumer sees that text,
929/// and the writer puts the original bytes back while the runs are untouched.
930#[derive(Debug, Clone)]
931struct FieldRunSpan {
932    /// The span's runs in order, `None` where a field of the span sits.
933    runs: Vec<Option<CT_R>>,
934    /// Where this field sits among `runs`.
935    position: usize,
936    /// Which field of the span this is, counted from the first.
937    field_index: usize,
938}
939
940/// The bounded legacy form kind projected from `w:ffData`.
941#[derive(Debug, Clone, Copy, PartialEq, Eq)]
942pub enum LegacyFormFieldKind {
943    TextInput,
944    CheckBox,
945    DropDownList,
946}
947
948/// The current value stored by one legacy form field.
949#[derive(Debug, Clone, PartialEq, Eq)]
950pub enum LegacyFormFieldValue {
951    Text(String),
952    Checked(bool),
953    SelectedIndex(usize),
954}
955
956/// Supported metadata from one structurally valid `w:ffData` subtree.
957#[derive(Debug, Clone, PartialEq, Eq)]
958pub struct LegacyFormFieldData {
959    pub name: Option<String>,
960    pub enabled: bool,
961    pub calculate_on_exit: bool,
962    pub kind: LegacyFormFieldKind,
963    pub value: LegacyFormFieldValue,
964    pub choices: Vec<String>,
965    pub max_length: Option<usize>,
966}
967
968#[derive(Debug, Clone, Copy, PartialEq, Eq)]
969enum NestedFieldPosition {
970    Argument(usize),
971    Switch(usize),
972}
973
974impl Field {
975    /// Construct a field that will be written in the simple field form.
976    pub fn new(instruction: &str, cached_result: &str) -> Self {
977        let instruction = parse_field_instruction(instruction);
978        Self {
979            source: FieldSource::New {
980                original_instruction: instruction.clone(),
981                form: FieldForm::Simple,
982                cached_runs: None,
983                original_cached_result: cached_result.to_owned(),
984                cached_segments: Vec::new(),
985            },
986            instruction,
987            cached_result: cached_result.to_owned(),
988            dirty: None,
989            locked: None,
990            legacy_form: None,
991            legacy_form_parse_error: false,
992            nested_order: Vec::new(),
993            cached_fields: Vec::new(),
994            simple_cached_runs: None,
995            typed_cached_runs: None,
996            typed_cached_comment_ranges: Vec::new(),
997            parsed_cached_comment_ranges: Vec::new(),
998            parsed_cached_runs: None,
999            span: None,
1000        }
1001    }
1002
1003    /// Construct a checked field from a producer-style instruction.
1004    pub fn from_raw(instruction: &str, form: FieldForm, cached_runs: Vec<CT_R>) -> Result<Self> {
1005        validate_raw_field_instruction(instruction)?;
1006        let parsed = parse_field_instruction(instruction);
1007        let mut field = Self::from_instruction(parsed, form, cached_runs)?;
1008        field.instruction.raw = instruction.trim().to_owned();
1009        if let FieldSource::New {
1010            original_instruction,
1011            ..
1012        } = &mut field.source
1013        {
1014            *original_instruction = field.instruction.clone();
1015        }
1016        Ok(field)
1017    }
1018
1019    /// Construct a checked field with typed operands and ordered cached runs.
1020    pub fn from_instruction(
1021        instruction: FieldInstruction,
1022        form: FieldForm,
1023        cached_runs: Vec<CT_R>,
1024    ) -> Result<Self> {
1025        let instruction = FieldInstruction::new(
1026            &instruction.name,
1027            instruction.arguments,
1028            instruction.switches,
1029        )?;
1030        if form == FieldForm::Simple && instruction_contains_nested(&instruction) {
1031            return Err(OxmlError::InvalidValue(
1032                "simple field instructions cannot contain nested fields".into(),
1033            ));
1034        }
1035        validate_cached_field_runs(&cached_runs)?;
1036        let cached_segments = cached_runs
1037            .iter()
1038            .enumerate()
1039            .map(|(run_index, run)| CachedDisplaySegment {
1040                run_index,
1041                text: cached_run_display(run),
1042                properties: run.properties.clone(),
1043            })
1044            .collect::<Vec<_>>();
1045        let cached_result = cached_segments
1046            .iter()
1047            .map(|segment| segment.text.as_str())
1048            .collect::<String>();
1049        let mut field = Self::new(&instruction.raw, &cached_result);
1050        field.instruction = instruction.clone();
1051        field.source = FieldSource::New {
1052            original_instruction: instruction,
1053            form,
1054            cached_runs: Some(cached_runs),
1055            original_cached_result: cached_result,
1056            cached_segments,
1057        };
1058        Ok(field)
1059    }
1060
1061    /// Replace a field's result with checked typed runs while retaining its source controls.
1062    #[doc(hidden)]
1063    pub fn set_cached_runs(&mut self, runs: Vec<CT_R>) -> Result<()> {
1064        if self.locked == Some(true) {
1065            return Err(OxmlError::InvalidValue(
1066                "a locked field cannot replace its cached runs".into(),
1067            ));
1068        }
1069        validate_cached_field_runs(&runs)?;
1070        let mut candidate = self.clone();
1071        candidate.cached_result = runs.iter().map(cached_run_display).collect();
1072        candidate.typed_cached_runs = Some(runs);
1073        candidate.typed_cached_comment_ranges.clear();
1074        candidate.simple_cached_runs = None;
1075        candidate.cached_fields.clear();
1076        let mut writer = Writer::new(Vec::new());
1077        write_run_field(&mut writer, &candidate, None, None)?;
1078        let source = writer.into_inner();
1079        let mut paragraph = FIELD_CACHE_VALIDATION_SCOPE.to_vec();
1080        paragraph.extend(source);
1081        paragraph.extend_from_slice(b"</w:p>");
1082        CT_P::from_xml_fragment(&paragraph)?;
1083        *self = candidate;
1084        Ok(())
1085    }
1086
1087    /// Replace a checked result and its sibling annotation range markers atomically.
1088    #[doc(hidden)]
1089    pub fn set_cached_runs_with_comment_ranges(
1090        &mut self,
1091        runs: Vec<CT_R>,
1092        ranges: Vec<CommentRangeMarker>,
1093    ) -> Result<()> {
1094        validate_cached_comment_ranges(&runs, &ranges)?;
1095        if ranges.is_empty() {
1096            return self.set_cached_runs(runs);
1097        }
1098        let mut candidate = self.clone();
1099        candidate.set_cached_runs(runs)?;
1100        candidate.typed_cached_comment_ranges = ranges;
1101        let mut writer = Writer::new(Vec::new());
1102        write_run_field(&mut writer, &candidate, None, None)?;
1103        let mut paragraph = FIELD_CACHE_VALIDATION_SCOPE.to_vec();
1104        paragraph.extend(writer.into_inner());
1105        paragraph.extend_from_slice(b"</w:p>");
1106        let reparsed = CT_P::from_xml_fragment(&paragraph)?;
1107        let owner = reparsed
1108            .runs()
1109            .into_iter()
1110            .flat_map(|run| &run.content)
1111            .find_map(|content| match content {
1112                RunContent::Field(field) => Some(field),
1113                _ => None,
1114            })
1115            .ok_or_else(|| {
1116                OxmlError::InvalidValue("annotation result did not reopen as a typed field".into())
1117            })?;
1118        if owner.effective_instruction() != candidate.effective_instruction()
1119            || owner.cached_result != candidate.cached_result
1120            || owner.cached_result_comment_ranges().len()
1121                != candidate.typed_cached_comment_ranges.len()
1122        {
1123            return Err(OxmlError::InvalidValue(
1124                "annotation result reopened with a different field owner or range graph".into(),
1125            ));
1126        }
1127        if let Some(runs) = owner.cached_result_runs() {
1128            validate_cached_comment_ranges(runs, owner.cached_result_comment_ranges())?;
1129        } else {
1130            return Err(OxmlError::InvalidValue(
1131                "annotation result has no typed cache after reopen".into(),
1132            ));
1133        }
1134        *self = candidate;
1135        Ok(())
1136    }
1137
1138    /// Return sibling annotation boundaries owned by this field's stored result.
1139    #[doc(hidden)]
1140    pub fn cached_result_comment_ranges(&self) -> &[CommentRangeMarker] {
1141        if self.typed_cache().is_some() {
1142            &self.typed_cached_comment_ranges
1143        } else {
1144            &self.parsed_cached_comment_ranges
1145        }
1146    }
1147
1148    /// Return the ordered typed result runs without replaying cached fields.
1149    #[doc(hidden)]
1150    pub fn cached_result_runs(&self) -> Option<&[CT_R]> {
1151        self.typed_cache()
1152            .or(self.simple_cached_runs.as_deref())
1153            .or(self.parsed_cached_runs.as_deref())
1154            .or(match &self.source {
1155                FieldSource::New { cached_runs, .. } => cached_runs.as_deref(),
1156                _ => None,
1157            })
1158            .filter(|runs| {
1159                runs.iter().map(cached_run_display).collect::<String>() == self.cached_result
1160            })
1161    }
1162
1163    fn typed_cache(&self) -> Option<&[CT_R]> {
1164        self.typed_cached_runs.as_deref().filter(|runs| {
1165            runs.iter().map(cached_run_display).collect::<String>() == self.cached_result
1166        })
1167    }
1168
1169    /// Return the representation this field writes.
1170    pub fn form(&self) -> FieldForm {
1171        if instruction_contains_nested(&self.effective_instruction()) {
1172            return FieldForm::Complex;
1173        }
1174        match &self.source {
1175            FieldSource::New { form, .. } | FieldSource::Parsed { form, .. } => *form,
1176        }
1177    }
1178
1179    /// The field-local lock state, including an absent toggle.
1180    pub fn locked(&self) -> Option<bool> {
1181        self.locked
1182    }
1183
1184    /// Set or remove the field-local lock without changing its instruction or cache.
1185    pub fn set_locked(&mut self, value: Option<bool>) {
1186        self.locked = value;
1187    }
1188
1189    /// Validate the current mutable field before attaching it to a document.
1190    #[doc(hidden)]
1191    pub fn validate_for_attachment(&self) -> Result<()> {
1192        let instruction = self.effective_instruction();
1193        validate_raw_field_instruction(&instruction.raw)?;
1194        FieldInstruction::new(
1195            &instruction.name,
1196            instruction.arguments,
1197            instruction.switches,
1198        )?;
1199        if let Some(runs) = self.typed_cache() {
1200            validate_cached_field_runs(runs)?;
1201        }
1202        if let FieldSource::New {
1203            cached_runs: Some(runs),
1204            ..
1205        } = &self.source
1206        {
1207            validate_cached_field_runs(runs)?;
1208        }
1209        let original_cached_result = match &self.source {
1210            FieldSource::New {
1211                original_cached_result,
1212                cached_runs: Some(_),
1213                ..
1214            }
1215            | FieldSource::Parsed {
1216                original_cached_result,
1217                ..
1218            } => Some(original_cached_result),
1219            _ => None,
1220        };
1221        if original_cached_result != Some(&self.cached_result) {
1222            oxml_core::xml::reject_non_xml_characters("field result", &self.cached_result)?;
1223        }
1224        Ok(())
1225    }
1226
1227    fn parsed(
1228        instruction: FieldInstruction,
1229        cached_result: String,
1230        cached_segments: Vec<CachedDisplaySegment>,
1231        dirty: Option<bool>,
1232        form: FieldForm,
1233        raw_xml: Vec<u8>,
1234        word_prefixes: Vec<String>,
1235    ) -> Self {
1236        let locked = field_source_lock(&raw_xml, form, &word_prefixes)
1237            .ok()
1238            .flatten();
1239        let source_id = NEXT_FIELD_SOURCE_ID.fetch_add(1, Ordering::Relaxed);
1240        let original_instruction = instruction.clone();
1241        let original_cached_result = cached_result.clone();
1242        let (legacy_form, legacy_form_parse_error) = match parse_legacy_form_data(
1243            &raw_xml,
1244            &word_prefixes,
1245            &instruction.name,
1246            &cached_result,
1247        ) {
1248            Ok(legacy_form) => (legacy_form, false),
1249            Err(_) => (None, true),
1250        };
1251        Self {
1252            instruction,
1253            cached_result,
1254            dirty,
1255            locked,
1256            legacy_form: legacy_form.clone(),
1257            legacy_form_parse_error,
1258            nested_order: Vec::new(),
1259            cached_fields: Vec::new(),
1260            simple_cached_runs: None,
1261            typed_cached_runs: None,
1262            typed_cached_comment_ranges: Vec::new(),
1263            parsed_cached_comment_ranges: Vec::new(),
1264            parsed_cached_runs: None,
1265            span: None,
1266            source: FieldSource::Parsed {
1267                source_id,
1268                owner_id: source_id,
1269                form,
1270                raw_xml,
1271                original_instruction,
1272                original_cached_result,
1273                cached_segments,
1274                original_dirty: dirty,
1275                original_locked: locked,
1276                original_legacy_form: legacy_form,
1277                word_prefixes,
1278            },
1279        }
1280    }
1281
1282    /// Inspect stored owners without invoking recursive display selection.
1283    pub(crate) fn resource_owner_children(&self) -> Vec<crate::document::WordOwner<'_>> {
1284        use crate::document::WordOwner;
1285        let mut children = Vec::new();
1286        for runs in [
1287            &self.simple_cached_runs,
1288            &self.typed_cached_runs,
1289            &self.parsed_cached_runs,
1290        ]
1291        .into_iter()
1292        .flatten()
1293        {
1294            children.extend(runs.iter().map(WordOwner::Run));
1295        }
1296        for (_, field) in &self.cached_fields {
1297            children.push(WordOwner::Field(field));
1298        }
1299        for instruction in [&self.instruction, self.original_instruction()] {
1300            for argument in instruction.arguments.iter().chain(
1301                instruction
1302                    .switches
1303                    .iter()
1304                    .filter_map(|s| s.argument.as_ref()),
1305            ) {
1306                if let FieldArgument::Nested(field) = argument {
1307                    children.push(WordOwner::Field(field));
1308                }
1309            }
1310        }
1311        if let FieldSource::New {
1312            cached_runs: Some(runs),
1313            ..
1314        } = &self.source
1315        {
1316            children.extend(runs.iter().map(WordOwner::Run));
1317        }
1318        if let Some(span) = &self.span {
1319            children.extend(span.runs.iter().flatten().map(WordOwner::Run));
1320        }
1321        children
1322    }
1323
1324    /// Return nested fields in their original instruction order.
1325    #[doc(hidden)]
1326    pub fn nested_fields_in_source_order(&self) -> Vec<&Field> {
1327        let original = self.original_instruction();
1328        let structured_unchanged = instruction_structure_eq(&self.instruction, original);
1329        if structured_unchanged && self.instruction.raw != original.raw {
1330            return Vec::new();
1331        }
1332        self.nested_fields_from_instruction(&self.instruction, structured_unchanged)
1333    }
1334
1335    fn parsed_cached_child_runs(&self) -> Option<&[CT_R]> {
1336        self.parsed_cached_runs.as_deref().filter(|runs| {
1337            self.cached_fields.is_empty()
1338                && runs
1339                    .iter()
1340                    .flat_map(|run| &run.content)
1341                    .any(|content| matches!(content, RunContent::Field(_)))
1342        })
1343    }
1344
1345    /// Direct nested fields in the ordered cached result, without flattening it.
1346    #[doc(hidden)]
1347    pub fn cached_fields_in_source_order(&self) -> Vec<&Field> {
1348        if let Some(runs) = self
1349            .typed_cache()
1350            .or(self.simple_cached_runs.as_deref())
1351            .or(self.parsed_cached_child_runs())
1352        {
1353            return runs
1354                .iter()
1355                .flat_map(|run| &run.content)
1356                .filter_map(|content| match content {
1357                    RunContent::Field(field) => Some(field),
1358                    _ => None,
1359                })
1360                .collect();
1361        }
1362        match &self.source {
1363            FieldSource::New {
1364                cached_runs: Some(runs),
1365                ..
1366            } => runs
1367                .iter()
1368                .flat_map(|run| &run.content)
1369                .filter_map(|content| match content {
1370                    RunContent::Field(field) => Some(field),
1371                    _ => None,
1372                })
1373                .collect(),
1374            _ => self.cached_fields.iter().map(|(_, field)| field).collect(),
1375        }
1376    }
1377
1378    /// Direct nested operands followed by nested cached-result fields.
1379    #[doc(hidden)]
1380    pub fn all_nested_fields_in_source_order(&self) -> Vec<&Field> {
1381        let mut fields = self.nested_fields_in_source_order();
1382        fields.extend(self.cached_fields_in_source_order());
1383        fields
1384    }
1385
1386    /// Mutate a direct cached-result field by its physical encounter index.
1387    /// Preserve a parsed child's identity. Checked serialization rejects a new
1388    /// replacement without the original simple-cache source span.
1389    #[doc(hidden)]
1390    pub fn cached_field_mut(&mut self, index: usize) -> Option<&mut Field> {
1391        let parsed_children = self.parsed_cached_child_runs().is_some();
1392        let runs = self
1393            .typed_cached_runs
1394            .as_mut()
1395            .or(self.simple_cached_runs.as_mut())
1396            .or(if parsed_children {
1397                self.parsed_cached_runs.as_mut()
1398            } else {
1399                None
1400            })
1401            .or(match &mut self.source {
1402                FieldSource::New { cached_runs, .. } => cached_runs.as_mut(),
1403                _ => None,
1404            });
1405        if let Some(runs) = runs {
1406            return runs
1407                .iter_mut()
1408                .flat_map(|run| &mut run.content)
1409                .filter_map(|content| {
1410                    if let RunContent::Field(field) = content {
1411                        Some(field)
1412                    } else {
1413                        None
1414                    }
1415                })
1416                .nth(index);
1417        }
1418        self.cached_fields.get_mut(index).map(|(_, field)| field)
1419    }
1420
1421    /// Refresh the display projection after nested cached fields were edited.
1422    #[doc(hidden)]
1423    pub fn refresh_cached_field_projection(&mut self) {
1424        if let Some(value) = self.nested_cached_projection() {
1425            self.cached_result = value;
1426        }
1427    }
1428
1429    fn nested_cached_projection(&self) -> Option<String> {
1430        if let Some(runs) = self
1431            .typed_cache()
1432            .or(self.simple_cached_runs.as_deref())
1433            .or(self.parsed_cached_child_runs())
1434        {
1435            return Some(runs.iter().map(cached_run_display).collect());
1436        }
1437        match &self.source {
1438            FieldSource::New {
1439                cached_runs: Some(runs),
1440                ..
1441            } => Some(runs.iter().map(cached_run_display).collect()),
1442            FieldSource::Parsed {
1443                original_cached_result,
1444                ..
1445            } if !self.cached_fields.is_empty() => {
1446                let mut value = original_cached_result.clone();
1447                for (range, field) in self.cached_fields.iter().rev() {
1448                    if !value.is_char_boundary(range.start)
1449                        || !value.is_char_boundary(range.end)
1450                        || range.end > value.len()
1451                    {
1452                        return None;
1453                    }
1454                    value.replace_range(range.clone(), &field.cached_result);
1455                }
1456                Some(value)
1457            }
1458            _ => None,
1459        }
1460    }
1461
1462    /// Reject a malformed legacy-form owner retained by this field.
1463    #[doc(hidden)]
1464    pub fn validate_legacy_form_owner(&self) -> Result<()> {
1465        if self.legacy_form_parse_error {
1466            return Err(OxmlError::InvalidValue(
1467                "malformed legacy form owner".to_owned(),
1468            ));
1469        }
1470        Ok(())
1471    }
1472
1473    /// Replace the selected legacy form while retaining original nested-field order.
1474    #[doc(hidden)]
1475    pub fn set_nth_legacy_form_value_in_source_order(
1476        &mut self,
1477        remaining: &mut usize,
1478        value: &LegacyFormFieldValue,
1479    ) -> Result<bool> {
1480        self.validate_legacy_form_owner()?;
1481        if self.legacy_form.is_some() {
1482            if *remaining == 0 {
1483                self.set_legacy_form_value(value.clone())?;
1484                return Ok(true);
1485            }
1486            *remaining -= 1;
1487        }
1488
1489        let original = self.original_instruction().clone();
1490        let structured_unchanged = instruction_structure_eq(&self.instruction, &original);
1491        if structured_unchanged && self.instruction.raw != original.raw {
1492            return Ok(false);
1493        }
1494        let ordered = if structured_unchanged {
1495            self.nested_order.clone()
1496        } else {
1497            Vec::new()
1498        };
1499        for position in &ordered {
1500            let nested = match *position {
1501                NestedFieldPosition::Argument(index) => self.instruction.arguments.get_mut(index),
1502                NestedFieldPosition::Switch(index) => self
1503                    .instruction
1504                    .switches
1505                    .get_mut(index)
1506                    .and_then(|field_switch| field_switch.argument.as_mut()),
1507            };
1508            if let Some(FieldArgument::Nested(field)) = nested
1509                && field.set_nth_legacy_form_value_in_source_order(remaining, value)?
1510            {
1511                return Ok(true);
1512            }
1513        }
1514        for (index, argument) in self.instruction.arguments.iter_mut().enumerate() {
1515            if ordered.contains(&NestedFieldPosition::Argument(index)) {
1516                continue;
1517            }
1518            if let FieldArgument::Nested(field) = argument
1519                && field.set_nth_legacy_form_value_in_source_order(remaining, value)?
1520            {
1521                return Ok(true);
1522            }
1523        }
1524        for (index, field_switch) in self.instruction.switches.iter_mut().enumerate() {
1525            if ordered.contains(&NestedFieldPosition::Switch(index)) {
1526                continue;
1527            }
1528            if let Some(FieldArgument::Nested(field)) = &mut field_switch.argument
1529                && field.set_nth_legacy_form_value_in_source_order(remaining, value)?
1530            {
1531                return Ok(true);
1532            }
1533        }
1534        Ok(false)
1535    }
1536
1537    /// Return the instruction selected by the public raw-versus-structured edit rules.
1538    #[doc(hidden)]
1539    pub fn effective_instruction(&self) -> FieldInstruction {
1540        instruction_for_write(self)
1541    }
1542
1543    /// Return the effective instruction spelling without cloning nested field trees.
1544    #[doc(hidden)]
1545    pub fn effective_instruction_text(&self) -> String {
1546        let original = self.original_instruction();
1547        let structured_changed = !instruction_structure_eq(&self.instruction, original);
1548        if self.instruction.raw != original.raw && !structured_changed {
1549            self.instruction.raw.trim().to_owned()
1550        } else if structured_changed {
1551            canonical_instruction_text(&self.instruction)
1552        } else {
1553            self.instruction.raw.clone()
1554        }
1555    }
1556
1557    /// Return nested fields from an effective instruction in evaluation order.
1558    #[doc(hidden)]
1559    pub fn effective_nested_fields_in_source_order<'a>(
1560        &self,
1561        instruction: &'a FieldInstruction,
1562    ) -> Vec<&'a Field> {
1563        self.nested_fields_from_instruction(
1564            instruction,
1565            instruction_structure_eq(&self.instruction, self.original_instruction()),
1566        )
1567    }
1568
1569    fn original_instruction(&self) -> &FieldInstruction {
1570        match &self.source {
1571            FieldSource::New {
1572                original_instruction,
1573                ..
1574            }
1575            | FieldSource::Parsed {
1576                original_instruction,
1577                ..
1578            } => original_instruction,
1579        }
1580    }
1581
1582    fn nested_fields_from_instruction<'a>(
1583        &self,
1584        instruction: &'a FieldInstruction,
1585        preserve_source_order: bool,
1586    ) -> Vec<&'a Field> {
1587        let mut fields = Vec::new();
1588        if preserve_source_order {
1589            for position in &self.nested_order {
1590                let argument = match *position {
1591                    NestedFieldPosition::Argument(index) => instruction.arguments.get(index),
1592                    NestedFieldPosition::Switch(index) => instruction
1593                        .switches
1594                        .get(index)
1595                        .and_then(|switch| switch.argument.as_ref()),
1596                };
1597                if let Some(FieldArgument::Nested(field)) = argument {
1598                    fields.push(field.as_ref());
1599                }
1600            }
1601        }
1602        for argument in instruction.arguments.iter().chain(
1603            instruction
1604                .switches
1605                .iter()
1606                .filter_map(|switch| switch.argument.as_ref()),
1607        ) {
1608            let FieldArgument::Nested(field) = argument else {
1609                continue;
1610            };
1611            if !fields
1612                .iter()
1613                .any(|ordered| std::ptr::eq(*ordered, field.as_ref()))
1614            {
1615                fields.push(field.as_ref());
1616            }
1617        }
1618        fields
1619    }
1620
1621    fn is_unchanged(&self) -> bool {
1622        if self.typed_cached_runs.is_some() {
1623            return false;
1624        }
1625        match &self.source {
1626            FieldSource::New { .. } => false,
1627            FieldSource::Parsed {
1628                original_instruction,
1629                original_cached_result,
1630                original_dirty,
1631                original_locked,
1632                original_legacy_form,
1633                ..
1634            } => {
1635                self.instruction == *original_instruction
1636                    && instruction_source_identity_eq(&self.instruction, original_instruction)
1637                    && self.cached_result == *original_cached_result
1638                    && self.dirty == *original_dirty
1639                    && self.locked == *original_locked
1640                    && self.legacy_form == *original_legacy_form
1641                    && self
1642                        .cached_fields_in_source_order()
1643                        .iter()
1644                        .all(|field| field.is_unchanged())
1645            }
1646        }
1647    }
1648
1649    fn is_parsed_complex(&self) -> bool {
1650        matches!(
1651            self.source,
1652            FieldSource::Parsed {
1653                form: FieldForm::Complex,
1654                ..
1655            }
1656        )
1657    }
1658
1659    /// Whether this field writes a complex `w:fldChar` sequence.
1660    #[doc(hidden)]
1661    pub fn is_complex(&self) -> bool {
1662        self.form() == FieldForm::Complex
1663    }
1664
1665    /// Return the text this field contributes to [`CT_R::text`].
1666    #[doc(hidden)]
1667    pub fn projected_text(&self) -> Option<&str> {
1668        (self.is_parsed_complex()
1669            || matches!(
1670                self.source,
1671                FieldSource::New {
1672                    form: FieldForm::Complex,
1673                    ..
1674                }
1675            ))
1676        .then_some(self.cached_result.as_str())
1677    }
1678
1679    /// Whether the retained field source carries semantic attributes outside
1680    /// the modeled instruction, type, and dirty-state projection.
1681    pub fn has_unmodeled_semantic_attributes(&self) -> bool {
1682        let FieldSource::Parsed {
1683            form,
1684            raw_xml,
1685            word_prefixes,
1686            ..
1687        } = &self.source
1688        else {
1689            return false;
1690        };
1691
1692        field_source_has_unmodeled_semantic_attributes(raw_xml, *form, word_prefixes)
1693            .unwrap_or(true)
1694    }
1695
1696    /// Return the stored display split by its original result-run formatting.
1697    #[doc(hidden)]
1698    pub fn cached_display_segments(&self) -> Vec<(&str, Option<&CT_RPr>)> {
1699        self.cached_display_field_segments()
1700            .into_iter()
1701            .map(|(text, properties, _)| (text, properties))
1702            .collect()
1703    }
1704
1705    /// Return the stored display split by its original result-run formatting.
1706    #[doc(hidden)]
1707    pub fn cached_display_field_segments(&self) -> Vec<(&str, Option<&CT_RPr>, &Field)> {
1708        let (original_cached_result, cached_segments) = match &self.source {
1709            FieldSource::Parsed {
1710                original_cached_result,
1711                cached_segments,
1712                ..
1713            }
1714            | FieldSource::New {
1715                original_cached_result,
1716                cached_segments,
1717                ..
1718            } => (original_cached_result, cached_segments),
1719        };
1720        if self.nested_cached_projection().as_deref() == Some(self.cached_result.as_str()) {
1721            let runs = self
1722                .typed_cache()
1723                .or(self.simple_cached_runs.as_deref())
1724                .or(self.parsed_cached_child_runs())
1725                .or(match &self.source {
1726                    FieldSource::New {
1727                        cached_runs: Some(runs),
1728                        ..
1729                    } => Some(runs.as_slice()),
1730                    _ => None,
1731                });
1732            if let Some(runs) = runs {
1733                return runs
1734                    .iter()
1735                    .flat_map(|run| {
1736                        run.content.iter().flat_map(|content| match content {
1737                            RunContent::Field(child) => child.cached_display_field_segments(),
1738                            RunContent::Break(BreakType::Page) => {
1739                                vec![("\u{000c}", run.properties.as_ref(), self)]
1740                            }
1741                            RunContent::Break(BreakType::Column) => {
1742                                vec![("\u{000b}", run.properties.as_ref(), self)]
1743                            }
1744                            _ => vec![(CT_R::content_text(content), run.properties.as_ref(), self)],
1745                        })
1746                    })
1747                    .collect();
1748            }
1749            if !self.cached_fields.is_empty() {
1750                let mut segments = Vec::new();
1751                let mut start = 0;
1752                for (range, child) in &self.cached_fields {
1753                    segments.extend(cached_display_range(
1754                        cached_segments,
1755                        start..range.start,
1756                        self,
1757                    ));
1758                    segments.extend(child.cached_display_field_segments());
1759                    start = range.end;
1760                }
1761                segments.extend(cached_display_range(
1762                    cached_segments,
1763                    start..original_cached_result.len(),
1764                    self,
1765                ));
1766                return segments;
1767            }
1768        }
1769        if !cached_segments.is_empty() {
1770            if self.cached_result == *original_cached_result {
1771                return cached_segments
1772                    .iter()
1773                    .map(|segment| (segment.text.as_str(), segment.properties.as_ref(), self))
1774                    .collect();
1775            }
1776            return vec![(
1777                self.cached_result.as_str(),
1778                cached_segments
1779                    .first()
1780                    .and_then(|segment| segment.properties.as_ref()),
1781                self,
1782            )];
1783        }
1784        vec![(self.cached_result.as_str(), None, self)]
1785    }
1786
1787    /// Whether this cached display owner is protected by any enclosing field.
1788    /// An owner outside this field's cached tree is conservatively locked.
1789    #[doc(hidden)]
1790    pub fn cached_display_owner_is_locked(&self, owner: &Field) -> bool {
1791        fn find(field: &Field, owner: &Field, inherited: bool) -> Option<bool> {
1792            let locked = inherited || field.locked() == Some(true);
1793            if std::ptr::eq(field, owner) {
1794                return Some(locked);
1795            }
1796            field
1797                .cached_fields_in_source_order()
1798                .into_iter()
1799                .find_map(|child| find(child, owner, locked))
1800        }
1801        find(self, owner, false).unwrap_or(true)
1802    }
1803
1804    /// Return the parsed source fragment and its cache-aware replacement.
1805    #[doc(hidden)]
1806    pub fn source_replacement(&self) -> Result<Option<(&[u8], Vec<u8>)>> {
1807        let FieldSource::Parsed { raw_xml, .. } = &self.source else {
1808            return Ok(None);
1809        };
1810        let mut writer = Writer::new(Vec::new());
1811        write_field(&mut writer, self, None)?;
1812        Ok(Some((raw_xml, writer.into_inner())))
1813    }
1814
1815    /// Return what this field writes on its own when it was read from a run
1816    /// that also held content outside it.
1817    #[doc(hidden)]
1818    pub fn detached_source(&self) -> Result<Option<Vec<u8>>> {
1819        if self.span.is_none() {
1820            return Ok(None);
1821        }
1822        let mut writer = Writer::new(Vec::new());
1823        write_run_field(&mut writer, self, None, None)?;
1824        Ok(Some(writer.into_inner()))
1825    }
1826
1827    /// Return the retained physical XML owner identity for a parsed field.
1828    #[doc(hidden)]
1829    pub fn source_owner_id(&self) -> Option<u64> {
1830        match self.source {
1831            FieldSource::Parsed { owner_id, .. } => Some(owner_id),
1832            FieldSource::New { .. } => None,
1833        }
1834    }
1835
1836    /// Change the typed value of a legacy form field and its cached display.
1837    #[doc(hidden)]
1838    pub fn set_legacy_form_value(&mut self, value: LegacyFormFieldValue) -> Result<()> {
1839        let form = self
1840            .legacy_form
1841            .as_mut()
1842            .ok_or_else(|| OxmlError::MissingElement("w:ffData".to_owned()))?;
1843        let display = match (&form.kind, &value) {
1844            (LegacyFormFieldKind::TextInput, LegacyFormFieldValue::Text(value)) => {
1845                if form
1846                    .max_length
1847                    .is_some_and(|maximum| value.chars().count() > maximum)
1848                {
1849                    return Err(OxmlError::InvalidValue(
1850                        "legacy text form value exceeds w:maxLength".to_owned(),
1851                    ));
1852                }
1853                value.clone()
1854            }
1855            (LegacyFormFieldKind::CheckBox, LegacyFormFieldValue::Checked(checked)) => {
1856                if *checked { "☒" } else { "☐" }.to_owned()
1857            }
1858            (LegacyFormFieldKind::DropDownList, LegacyFormFieldValue::SelectedIndex(index)) => {
1859                let choice = form.choices.get(*index).ok_or_else(|| {
1860                    OxmlError::InvalidValue("legacy drop-down selection is out of range".to_owned())
1861                })?;
1862                choice.clone()
1863            }
1864            _ => {
1865                return Err(OxmlError::InvalidValue(
1866                    "legacy form value kind does not match the field kind".to_owned(),
1867                ));
1868            }
1869        };
1870        form.value = value;
1871        self.cached_result = display;
1872        Ok(())
1873    }
1874}
1875
1876impl PartialEq for Field {
1877    fn eq(&self, other: &Self) -> bool {
1878        self.instruction == other.instruction
1879            && self.cached_result == other.cached_result
1880            && self.dirty == other.dirty
1881            && self.locked == other.locked
1882            && self.legacy_form == other.legacy_form
1883            && self.legacy_form_parse_error == other.legacy_form_parse_error
1884            && self.cached_result_runs() == other.cached_result_runs()
1885            && self.cached_result_comment_ranges() == other.cached_result_comment_ranges()
1886    }
1887}
1888
1889/// The shared grammar for simple and complex field instructions.
1890#[derive(Debug, Clone, PartialEq)]
1891pub struct FieldInstruction {
1892    pub raw: String,
1893    pub name: String,
1894    pub arguments: Vec<FieldArgument>,
1895    pub switches: Vec<FieldSwitch>,
1896}
1897
1898impl FieldInstruction {
1899    /// Whether the preserved instruction closes every unescaped quoted operand.
1900    #[doc(hidden)]
1901    pub fn quotes_are_balanced(&self) -> bool {
1902        let mut characters = self.raw.chars().peekable();
1903        let mut quoted = false;
1904        while let Some(character) = characters.next() {
1905            if quoted
1906                && character == '\\'
1907                && characters
1908                    .peek()
1909                    .is_some_and(|next| matches!(next, '"' | '\\'))
1910            {
1911                characters.next();
1912            } else if character == '"' {
1913                quoted = !quoted;
1914            }
1915        }
1916        !quoted
1917    }
1918
1919    /// Build one canonical instruction from operands that cannot inject tokens.
1920    pub fn new(
1921        name: &str,
1922        arguments: Vec<FieldArgument>,
1923        switches: Vec<FieldSwitch>,
1924    ) -> Result<Self> {
1925        if !valid_field_token(name) || name.starts_with('\\') {
1926            return Err(OxmlError::InvalidValue("invalid field name".into()));
1927        }
1928        for argument in arguments.iter().chain(
1929            switches
1930                .iter()
1931                .filter_map(|switch| switch.argument.as_ref()),
1932        ) {
1933            match argument {
1934                FieldArgument::Text(value) => {
1935                    oxml_core::xml::reject_non_xml_characters("field argument", value)?
1936                }
1937                FieldArgument::Nested(field) => field.validate_for_attachment()?,
1938            }
1939        }
1940        let name = name.to_uppercase();
1941        let mut switches = switches;
1942        for switch in &mut switches {
1943            if !(valid_field_token(&switch.name) || switch.name == "!")
1944                || switch.name.contains('\\')
1945            {
1946                return Err(OxmlError::InvalidValue("invalid field switch name".into()));
1947            }
1948            switch.name = switch.name.to_ascii_lowercase();
1949            if switch.argument.is_some() && switch_is_known_flag(&name, &switch.name) {
1950                return Err(OxmlError::InvalidValue(format!(
1951                    "field switch {} does not take an argument",
1952                    switch.name
1953                )));
1954            }
1955        }
1956        let mut instruction = Self {
1957            raw: String::new(),
1958            name,
1959            arguments,
1960            switches,
1961        };
1962        instruction.raw = canonical_instruction_text(&instruction);
1963        Ok(instruction)
1964    }
1965}
1966
1967fn valid_field_token(value: &str) -> bool {
1968    !value.is_empty()
1969        && value
1970            .chars()
1971            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '=' | '*' | '#' | '@'))
1972}
1973
1974fn validate_raw_field_instruction(value: &str) -> Result<()> {
1975    oxml_core::xml::reject_non_xml_characters("field instruction", value)?;
1976    let mut quoted = false;
1977    let mut characters = value.chars().peekable();
1978    while let Some(character) = characters.next() {
1979        if quoted
1980            && character == '\\'
1981            && characters
1982                .peek()
1983                .is_some_and(|next| matches!(next, '"' | '\\'))
1984        {
1985            characters.next();
1986        } else if character == '"' {
1987            quoted = !quoted;
1988        }
1989    }
1990    if quoted {
1991        return Err(OxmlError::InvalidValue(
1992            "unbalanced field instruction quotes".into(),
1993        ));
1994    }
1995    let tokens = lex_field_text(value);
1996    let Some(InstructionToken {
1997        argument: FieldArgument::Text(name),
1998        quoted: false,
1999        ..
2000    }) = tokens.first()
2001    else {
2002        return Err(OxmlError::InvalidValue(
2003            "field instruction must start with a field name".into(),
2004        ));
2005    };
2006    if !valid_field_token(name) {
2007        return Err(OxmlError::InvalidValue("invalid field name".into()));
2008    }
2009    for token in &tokens[1..] {
2010        if let FieldArgument::Text(value) = &token.argument
2011            && !token.quoted
2012            && value.starts_with('\\')
2013            && !(valid_field_token(&value[1..]) || value == "\\!")
2014        {
2015            return Err(OxmlError::InvalidValue("invalid field switch token".into()));
2016        }
2017    }
2018    Ok(())
2019}
2020
2021fn cached_display_range<'a>(
2022    segments: &'a [CachedDisplaySegment],
2023    range: std::ops::Range<usize>,
2024    owner: &'a Field,
2025) -> Vec<(&'a str, Option<&'a CT_RPr>, &'a Field)> {
2026    let mut offset = 0;
2027    let mut result = Vec::new();
2028    for segment in segments {
2029        let end = offset + segment.text.len();
2030        let start = range.start.max(offset);
2031        let stop = range.end.min(end);
2032        if start < stop
2033            && let Some(text) = segment.text.get(start - offset..stop - offset)
2034        {
2035            result.push((text, segment.properties.as_ref(), owner));
2036        }
2037        offset = end;
2038    }
2039    result
2040}
2041
2042fn cached_run_display(run: &CT_R) -> String {
2043    run.content
2044        .iter()
2045        .map(|content| match content {
2046            RunContent::Field(field) => field.cached_result.clone(),
2047            RunContent::Break(BreakType::Page) => "\u{000c}".into(),
2048            RunContent::Break(BreakType::Column) => "\u{000b}".into(),
2049            _ => CT_R::content_text(content).to_owned(),
2050        })
2051        .collect()
2052}
2053
2054const FIELD_CACHE_VALIDATION_SCOPE: &[u8] = concat!(
2055    "<w:p xmlns:w=\"http://schemas.openxmlformats.org/wordprocessingml/2006/main\" ",
2056    "xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" ",
2057    "xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\" ",
2058    "xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\" ",
2059    "xmlns:pic=\"http://schemas.openxmlformats.org/drawingml/2006/picture\" ",
2060    "xmlns:v=\"urn:schemas-microsoft-com:vml\" ",
2061    "xmlns:o=\"urn:schemas-microsoft-com:office:office\" ",
2062    "xmlns:mc=\"http://schemas.openxmlformats.org/markup-compatibility/2006\" ",
2063    "xmlns:w14=\"http://schemas.microsoft.com/office/word/2010/wordml\" ",
2064    "xmlns:wps=\"http://schemas.microsoft.com/office/word/2010/wordprocessingShape\" ",
2065    "xmlns:m=\"http://schemas.openxmlformats.org/officeDocument/2006/math\">",
2066)
2067.as_bytes();
2068
2069fn validate_cached_field_runs(runs: &[CT_R]) -> Result<()> {
2070    for run in runs {
2071        for content in &run.content {
2072            match content {
2073                RunContent::Text(text) | RunContent::DeletedText(text) => {
2074                    oxml_core::xml::reject_non_xml_characters("field result", &text.text)?
2075                }
2076                RunContent::Field(field) => field.validate_for_attachment()?,
2077                _ => {}
2078            }
2079        }
2080        let mut localized = Vec::new();
2081        let (raw_root, raw_bindings) = run_xml_root(run, None, &mut localized)?;
2082        for (index, raw) in run.extra_xml.iter().enumerate() {
2083            if run
2084                .extra_xml_positions
2085                .get(index)
2086                .is_some_and(|position| CT_R::raw_child_is_root_attributes(*position))
2087            {
2088                continue;
2089            }
2090            validate_raw_cached_field_content(&raw_root, &raw_bindings, &localized, raw)?;
2091        }
2092        let mut writer = Writer::new(Vec::new());
2093        writer
2094            .get_mut()
2095            .extend_from_slice(FIELD_CACHE_VALIDATION_SCOPE);
2096        run.to_xml(&mut writer)?;
2097        writer.get_mut().extend_from_slice(b"</w:p>");
2098        oxml_core::xml::validate_strict_xml_1_0(&writer.into_inner())
2099            .map_err(|error| OxmlError::InvalidValue(format!("invalid cached XML: {error:?}")))?;
2100    }
2101    Ok(())
2102}
2103
2104fn validate_raw_cached_field_content(
2105    root: &BytesStart<'_>,
2106    raw_bindings: &[(String, String)],
2107    localized: &[(String, String)],
2108    raw: &[u8],
2109) -> Result<()> {
2110    let mut writer = Writer::new(FIELD_CACHE_VALIDATION_SCOPE.to_vec());
2111    writer.write_event(Event::Start(root.clone()))?;
2112    write_run_raw_source(&mut writer, raw, None, raw_bindings, localized)?;
2113    writer.get_mut().extend_from_slice(b"</w:r></w:p>");
2114    let scoped = writer.into_inner();
2115    oxml_core::xml::validate_strict_xml_1_0(&scoped)
2116        .map_err(|error| OxmlError::InvalidValue(format!("invalid cached XML: {error:?}")))?;
2117    let mut reader = NsReader::from_reader(scoped.as_slice());
2118    loop {
2119        let (namespace, event) = reader.read_resolved_event()?;
2120        match event {
2121            Event::Start(element) | Event::Empty(element)
2122                if namespace
2123                    == ResolveResult::Bound(Namespace(crate::namespace::W_NS.as_bytes()))
2124                    && matches!(
2125                        element.local_name().as_ref(),
2126                        b"fldChar"
2127                            | b"fldSimple"
2128                            | b"instrText"
2129                            | b"delInstrText"
2130                            | b"commentRangeStart"
2131                            | b"commentRangeEnd"
2132                    ) =>
2133            {
2134                return Err(OxmlError::InvalidValue(
2135                    "raw Word field delimiters are not cached display content".into(),
2136                ));
2137            }
2138            Event::Eof => return Ok(()),
2139            _ => {}
2140        }
2141    }
2142}
2143
2144#[derive(Debug, Clone, PartialEq)]
2145pub enum FieldArgument {
2146    Text(String),
2147    Nested(Box<Field>),
2148}
2149
2150#[derive(Debug, Clone, PartialEq)]
2151pub struct FieldSwitch {
2152    pub name: String,
2153    pub argument: Option<FieldArgument>,
2154}
2155
2156#[derive(Debug, Clone)]
2157enum FieldSource {
2158    New {
2159        original_instruction: FieldInstruction,
2160        form: FieldForm,
2161        cached_runs: Option<Vec<CT_R>>,
2162        original_cached_result: String,
2163        cached_segments: Vec<CachedDisplaySegment>,
2164    },
2165    Parsed {
2166        source_id: u64,
2167        owner_id: u64,
2168        form: FieldForm,
2169        raw_xml: Vec<u8>,
2170        original_instruction: FieldInstruction,
2171        original_cached_result: String,
2172        cached_segments: Vec<CachedDisplaySegment>,
2173        original_dirty: Option<bool>,
2174        original_locked: Option<bool>,
2175        original_legacy_form: Option<LegacyFormFieldData>,
2176        word_prefixes: Vec<String>,
2177    },
2178}
2179
2180#[derive(Debug, Clone)]
2181struct CachedDisplaySegment {
2182    run_index: usize,
2183    text: String,
2184    properties: Option<CT_RPr>,
2185}
2186
2187/// The representation used to serialize a Word field.
2188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2189pub enum FieldForm {
2190    /// One `w:fldSimple` element.
2191    Simple,
2192    /// Ordered begin, instruction, separate, result and end runs.
2193    Complex,
2194}
2195
2196/// Content that can appear inside a run.
2197#[derive(Debug, Clone, PartialEq)]
2198pub enum RunContent {
2199    Text(CT_Text),
2200    /// Deleted text projected from `w:delText` inside a deletion wrapper.
2201    DeletedText(CT_Text),
2202    Tab,
2203    Break(BreakType),
2204    Drawing(CT_Drawing),
2205    /// A simple or complex Word field.
2206    Field(Field),
2207    /// A footnote reference (`<w:footnoteReference w:id="..."/>`).
2208    FootnoteRef {
2209        id: i32,
2210        /// Custom mark displayed by this reference instead of a numeric label.
2211        custom_mark: Option<String>,
2212    },
2213    /// An endnote reference (`<w:endnoteReference w:id="..."/>`).
2214    EndnoteRef {
2215        id: i32,
2216        /// Custom mark displayed by this reference instead of a numeric label.
2217        custom_mark: Option<String>,
2218    },
2219    /// A comment reference (`<w:commentReference w:id="..."/>`).
2220    CommentReference {
2221        id: i32,
2222        /// Number of raw run children that precede this reference.
2223        raw_before: usize,
2224    },
2225    /// A symbol character from a specific font (`<w:sym w:font="..." w:char="..."/>`).
2226    Symbol {
2227        /// The font the code point is looked up in.
2228        font: String,
2229        /// The code point, written back as four upper-case hex digits.
2230        char_code: u16,
2231    },
2232    /// One of the Word special characters that carries no text of its own.
2233    SpecialCharacter(SpecialCharacter),
2234}
2235
2236/// A Word special character run child.
2237///
2238/// The four share one placement contract, so they share one `RunContent`
2239/// variant rather than taking one each across the ten files that match on it.
2240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2241pub enum SpecialCharacter {
2242    /// `<w:cr/>`, a carriage return that breaks the line.
2243    CarriageReturn,
2244    /// `<w:noBreakHyphen/>`, a hyphen that is not a break opportunity.
2245    NoBreakHyphen,
2246    /// `<w:softHyphen/>`, a hyphen that renders only at a line break.
2247    SoftHyphen,
2248    /// `<w:ptab/>`, an absolutely positioned tab.
2249    PositionalTab {
2250        alignment: ST_PTabAlignment,
2251        relative_to: ST_PTabRelativeTo,
2252        leader: ST_PTabLeader,
2253    },
2254}
2255
2256/// `ST_PTabAlignment` — how content aligns against a positional tab.
2257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2258#[allow(non_camel_case_types)]
2259pub enum ST_PTabAlignment {
2260    Left,
2261    Center,
2262    Right,
2263}
2264
2265/// `ST_PTabRelativeTo` — what a positional tab position is measured from.
2266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2267#[allow(non_camel_case_types)]
2268pub enum ST_PTabRelativeTo {
2269    Margin,
2270    Indent,
2271}
2272
2273/// `ST_PTabLeader` — the leader drawn across a positional tab gap.
2274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2275#[allow(non_camel_case_types)]
2276pub enum ST_PTabLeader {
2277    None,
2278    Dot,
2279    Hyphen,
2280    Underscore,
2281    MiddleDot,
2282}
2283
2284impl ST_PTabAlignment {
2285    fn from_str(value: &str) -> Option<Self> {
2286        match value {
2287            "left" => Some(ST_PTabAlignment::Left),
2288            "center" => Some(ST_PTabAlignment::Center),
2289            "right" => Some(ST_PTabAlignment::Right),
2290            _ => None,
2291        }
2292    }
2293
2294    fn as_str(self) -> &'static str {
2295        match self {
2296            ST_PTabAlignment::Left => "left",
2297            ST_PTabAlignment::Center => "center",
2298            ST_PTabAlignment::Right => "right",
2299        }
2300    }
2301}
2302
2303impl ST_PTabRelativeTo {
2304    fn from_str(value: &str) -> Option<Self> {
2305        match value {
2306            "margin" => Some(ST_PTabRelativeTo::Margin),
2307            "indent" => Some(ST_PTabRelativeTo::Indent),
2308            _ => None,
2309        }
2310    }
2311
2312    fn as_str(self) -> &'static str {
2313        match self {
2314            ST_PTabRelativeTo::Margin => "margin",
2315            ST_PTabRelativeTo::Indent => "indent",
2316        }
2317    }
2318}
2319
2320impl ST_PTabLeader {
2321    fn from_str(value: &str) -> Option<Self> {
2322        match value {
2323            "none" => Some(ST_PTabLeader::None),
2324            "dot" => Some(ST_PTabLeader::Dot),
2325            "hyphen" => Some(ST_PTabLeader::Hyphen),
2326            "underscore" => Some(ST_PTabLeader::Underscore),
2327            "middleDot" => Some(ST_PTabLeader::MiddleDot),
2328            _ => None,
2329        }
2330    }
2331
2332    fn as_str(self) -> &'static str {
2333        match self {
2334            ST_PTabLeader::None => "none",
2335            ST_PTabLeader::Dot => "dot",
2336            ST_PTabLeader::Hyphen => "hyphen",
2337            ST_PTabLeader::Underscore => "underscore",
2338            ST_PTabLeader::MiddleDot => "middleDot",
2339        }
2340    }
2341}
2342
2343/// Read `<w:sym/>` as typed content.
2344///
2345/// A symbol whose `w:char` is not four hex digits, or that carries an
2346/// attribute outside the modeled pair, stays raw so nothing is lost.
2347fn parse_symbol(e: &BytesStart<'_>, prefixes: &[String]) -> Result<Option<RunContent>> {
2348    let mut font = None;
2349    let mut char_code = None;
2350    for attribute in e.attributes() {
2351        let attribute = attribute?;
2352        let key = attribute.key.as_ref();
2353        let value = std::str::from_utf8(&attribute.value)?;
2354        if is_word_attribute(key, b"font", prefixes) {
2355            font = Some(value.to_owned());
2356        } else if is_word_attribute(key, b"char", prefixes) {
2357            if value.len() != 4 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
2358                return Ok(None);
2359            }
2360            char_code = u16::from_str_radix(value, 16).ok();
2361        } else if !key.starts_with(b"xmlns") {
2362            return Ok(None);
2363        }
2364    }
2365    Ok(match (font, char_code) {
2366        (Some(font), Some(char_code)) => Some(RunContent::Symbol { font, char_code }),
2367        _ => None,
2368    })
2369}
2370
2371/// Read `<w:ptab/>` as typed content.
2372///
2373/// All three attributes are required by the schema, so a positional tab
2374/// missing one, or carrying a token outside the inventory, stays raw.
2375fn parse_positional_tab(e: &BytesStart<'_>, prefixes: &[String]) -> Result<Option<RunContent>> {
2376    let mut alignment = None;
2377    let mut relative_to = None;
2378    let mut leader = None;
2379    for attribute in e.attributes() {
2380        let attribute = attribute?;
2381        let key = attribute.key.as_ref();
2382        let value = std::str::from_utf8(&attribute.value)?;
2383        if is_word_attribute(key, b"alignment", prefixes) {
2384            alignment = ST_PTabAlignment::from_str(value);
2385        } else if is_word_attribute(key, b"relativeTo", prefixes) {
2386            relative_to = ST_PTabRelativeTo::from_str(value);
2387        } else if is_word_attribute(key, b"leader", prefixes) {
2388            leader = ST_PTabLeader::from_str(value);
2389        } else if !key.starts_with(b"xmlns") {
2390            return Ok(None);
2391        }
2392    }
2393    Ok(match (alignment, relative_to, leader) {
2394        (Some(alignment), Some(relative_to), Some(leader)) => Some(RunContent::SpecialCharacter(
2395            SpecialCharacter::PositionalTab {
2396                alignment,
2397                relative_to,
2398                leader,
2399            },
2400        )),
2401        _ => None,
2402    })
2403}
2404
2405/// Read `<w:cr/>`, `<w:noBreakHyphen/>` and `<w:softHyphen/>` as typed content.
2406///
2407/// These are `CT_Empty`, so an element carrying any attribute other than a
2408/// namespace declaration stays raw.
2409fn parse_empty_special_character(
2410    e: &BytesStart<'_>,
2411    prefixes: &[String],
2412) -> Result<Option<RunContent>> {
2413    let name = e.name();
2414    let character = if is_word_element(name.as_ref(), b"cr", prefixes) {
2415        SpecialCharacter::CarriageReturn
2416    } else if is_word_element(name.as_ref(), b"noBreakHyphen", prefixes) {
2417        SpecialCharacter::NoBreakHyphen
2418    } else if is_word_element(name.as_ref(), b"softHyphen", prefixes) {
2419        SpecialCharacter::SoftHyphen
2420    } else {
2421        return Ok(None);
2422    };
2423    for attribute in e.attributes() {
2424        if !attribute?.key.as_ref().starts_with(b"xmlns") {
2425            return Ok(None);
2426        }
2427    }
2428    Ok(Some(RunContent::SpecialCharacter(character)))
2429}
2430
2431/// Read the run children F-265 typed, or `None` to keep the element raw.
2432fn parse_typed_special_run_child(
2433    e: &BytesStart<'_>,
2434    prefixes: &[String],
2435) -> Result<Option<RunContent>> {
2436    if is_word_element(e.name().as_ref(), b"sym", prefixes) {
2437        parse_symbol(e, prefixes)
2438    } else if is_word_element(e.name().as_ref(), b"ptab", prefixes) {
2439        parse_positional_tab(e, prefixes)
2440    } else {
2441        parse_empty_special_character(e, prefixes)
2442    }
2443}
2444
2445fn write_special_character<W: std::io::Write>(
2446    writer: &mut Writer<W>,
2447    character: SpecialCharacter,
2448) -> Result<()> {
2449    match character {
2450        SpecialCharacter::CarriageReturn => {
2451            writer.write_event(Event::Empty(BytesStart::new("w:cr")))?;
2452        }
2453        SpecialCharacter::NoBreakHyphen => {
2454            writer.write_event(Event::Empty(BytesStart::new("w:noBreakHyphen")))?;
2455        }
2456        SpecialCharacter::SoftHyphen => {
2457            writer.write_event(Event::Empty(BytesStart::new("w:softHyphen")))?;
2458        }
2459        SpecialCharacter::PositionalTab {
2460            alignment,
2461            relative_to,
2462            leader,
2463        } => {
2464            let mut e = BytesStart::new("w:ptab");
2465            e.push_attribute(("w:alignment", alignment.as_str()));
2466            e.push_attribute(("w:relativeTo", relative_to.as_str()));
2467            e.push_attribute(("w:leader", leader.as_str()));
2468            writer.write_event(Event::Empty(e))?;
2469        }
2470    }
2471    Ok(())
2472}
2473
2474/// A typed comment range boundary at a run insertion point.
2475#[derive(Debug, Clone, PartialEq, Eq)]
2476pub enum CommentRangeMarker {
2477    Start {
2478        id: i32,
2479        run_index: usize,
2480        /// Number of raw children at this run boundary that precede the marker.
2481        raw_before: usize,
2482        /// Whether the source marker contained a child element or visible text.
2483        has_child_content: bool,
2484    },
2485    End {
2486        id: i32,
2487        run_index: usize,
2488        /// Number of raw children at this run boundary that precede the marker.
2489        raw_before: usize,
2490        /// Whether the source marker contained a child element or visible text.
2491        has_child_content: bool,
2492    },
2493}
2494
2495/// Read projection of a bookmark marker retained at a run boundary.
2496#[derive(Clone)]
2497pub struct BookmarkMarker {
2498    start: bool,
2499    id: Option<i32>,
2500    name: Option<String>,
2501    run_index: usize,
2502    raw_before: usize,
2503    projected_run_index: usize,
2504    tracked_run_index: usize,
2505    word_prefixes: Vec<String>,
2506    has_child_content: bool,
2507}
2508
2509impl PartialEq for BookmarkMarker {
2510    fn eq(&self, other: &Self) -> bool {
2511        self.start == other.start
2512            && self.id == other.id
2513            && self.name == other.name
2514            && self.run_index == other.run_index
2515            && self.raw_before == other.raw_before
2516            && self.projected_run_index == other.projected_run_index
2517            && self.tracked_run_index == other.tracked_run_index
2518            && self.has_child_content == other.has_child_content
2519    }
2520}
2521
2522impl Eq for BookmarkMarker {}
2523
2524impl std::fmt::Debug for BookmarkMarker {
2525    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2526        formatter
2527            .debug_struct("BookmarkMarker")
2528            .field("start", &self.start)
2529            .field("id", &self.id)
2530            .field("name", &self.name)
2531            .field("run_index", &self.run_index)
2532            .field("raw_before", &self.raw_before)
2533            .field("projected_run_index", &self.projected_run_index)
2534            .field("tracked_run_index", &self.tracked_run_index)
2535            .field("has_child_content", &self.has_child_content)
2536            .finish()
2537    }
2538}
2539
2540impl BookmarkMarker {
2541    pub fn is_start(&self) -> bool {
2542        self.start
2543    }
2544
2545    pub fn id(&self) -> Option<i32> {
2546        self.id
2547    }
2548
2549    pub fn name(&self) -> Option<&str> {
2550        self.name.as_deref()
2551    }
2552
2553    pub fn run_index(&self) -> usize {
2554        self.run_index
2555    }
2556
2557    /// Number of preserved raw children before this marker at its run boundary.
2558    #[doc(hidden)]
2559    pub fn raw_before(&self) -> usize {
2560        self.raw_before
2561    }
2562
2563    /// Whether the source marker contained a child element or visible text.
2564    pub fn has_child_content(&self) -> bool {
2565        self.has_child_content
2566    }
2567
2568    /// Accepted-view run boundary represented by this marker.
2569    #[doc(hidden)]
2570    pub fn projected_run_index(&self) -> usize {
2571        self.projected_run_index
2572    }
2573
2574    /// Tracked-view run boundary represented by this marker.
2575    #[doc(hidden)]
2576    pub fn tracked_run_index(&self) -> usize {
2577        self.tracked_run_index
2578    }
2579}
2580
2581impl CommentRangeMarker {
2582    fn run_index(&self) -> usize {
2583        match self {
2584            Self::Start { run_index, .. } | Self::End { run_index, .. } => *run_index,
2585        }
2586    }
2587
2588    fn raw_before(&self) -> usize {
2589        match self {
2590            Self::Start { raw_before, .. } | Self::End { raw_before, .. } => *raw_before,
2591        }
2592    }
2593}
2594
2595/// Types of breaks.
2596#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2597pub enum BreakType {
2598    Line,
2599    Page,
2600    Column,
2601}
2602
2603/// `CT_R` — A run of text with uniform formatting.
2604#[derive(Debug, Clone, PartialEq)]
2605#[allow(non_snake_case)]
2606pub struct CT_R {
2607    pub properties: Option<CT_RPr>,
2608    pub content: Vec<RunContent>,
2609    /// Unknown child elements captured as raw XML.
2610    pub extra_xml: Vec<Vec<u8>>,
2611    /// Encoded raw-child positions among properties and typed run content.
2612    #[doc(hidden)]
2613    pub extra_xml_positions: Vec<usize>,
2614    /// Drawings read out of an `mc:AlternateContent` block, for layout only.
2615    ///
2616    /// Never serialised. The verbatim copy in `extra_xml` is what gets
2617    /// written, so emitting these as well would duplicate the element.
2618    pub alt_drawings: Vec<CT_Drawing>,
2619}
2620
2621const RAW_LEGACY_HORIZONTAL_RULE_FLAG: usize = 1usize << (usize::BITS - 1);
2622const RAW_ALTERNATE_CONTENT_DRAWING_FLAG: usize = 1usize << (usize::BITS - 2);
2623const RAW_ROOT_ATTRIBUTES_FLAG: usize = 1usize << (usize::BITS - 3);
2624const RAW_COMMENT_REFERENCE_FLAG: usize = 1usize << (usize::BITS - 4);
2625const RAW_RUN_LOCAL_BINDINGS_FLAG: usize = 1usize << (usize::BITS - 5);
2626const RAW_CHILD_FLAGS: usize = RAW_LEGACY_HORIZONTAL_RULE_FLAG
2627    | RAW_ALTERNATE_CONTENT_DRAWING_FLAG
2628    | RAW_ROOT_ATTRIBUTES_FLAG
2629    | RAW_COMMENT_REFERENCE_FLAG
2630    | RAW_RUN_LOCAL_BINDINGS_FLAG;
2631const RAW_CHILD_POSITION_MASK: usize = !RAW_CHILD_FLAGS;
2632const VML_NAMESPACE: &[u8] = b"urn:schemas-microsoft-com:vml";
2633const OFFICE_NAMESPACE: &[u8] = b"urn:schemas-microsoft-com:office:office";
2634
2635fn resolved_name_matches(
2636    namespace: &ResolveResult<'_>,
2637    local_name: &[u8],
2638    expected_namespace: &[u8],
2639    expected_local_name: &[u8],
2640) -> bool {
2641    local_name == expected_local_name
2642        && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if *uri == expected_namespace)
2643}
2644
2645fn rect_has_enabled_horizontal_rule(reader: &NsReader<&[u8]>, element: &BytesStart<'_>) -> bool {
2646    let mut enabled = None;
2647    for attribute in element.attributes() {
2648        let Ok(attribute) = attribute else {
2649            return false;
2650        };
2651        let (namespace, local_name) = reader.resolver().resolve_attribute(attribute.key);
2652        if resolved_name_matches(&namespace, local_name.as_ref(), OFFICE_NAMESPACE, b"hr") {
2653            if enabled.is_some() {
2654                return false;
2655            }
2656            let Ok(value) =
2657                attribute.decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
2658            else {
2659                return false;
2660            };
2661            enabled = Some(matches!(value.as_bytes(), b"t" | b"true"));
2662        }
2663    }
2664    enabled == Some(true)
2665}
2666
2667fn is_legacy_horizontal_rule(raw_xml: &[u8], inherited_namespaces: &[(String, String)]) -> bool {
2668    let scoped_xml = if inherited_namespaces.is_empty() {
2669        None
2670    } else {
2671        let mut wrapper = BytesStart::new("rdocx-scope");
2672        let names = inherited_namespaces
2673            .iter()
2674            .map(|(prefix, _)| {
2675                if prefix.is_empty() {
2676                    "xmlns".to_owned()
2677                } else {
2678                    format!("xmlns:{prefix}")
2679                }
2680            })
2681            .collect::<Vec<_>>();
2682        for ((_, namespace), name) in inherited_namespaces.iter().zip(&names) {
2683            wrapper.push_attribute((name.as_str(), namespace.as_str()));
2684        }
2685        let mut writer = Writer::new(Vec::new());
2686        if writer.write_event(Event::Start(wrapper)).is_err() {
2687            return false;
2688        }
2689        writer.get_mut().extend_from_slice(raw_xml);
2690        if writer
2691            .write_event(Event::End(BytesEnd::new("rdocx-scope")))
2692            .is_err()
2693        {
2694            return false;
2695        }
2696        Some(writer.into_inner())
2697    };
2698    let mut reader = NsReader::from_reader(scoped_xml.as_deref().unwrap_or(raw_xml));
2699    reader.config_mut().trim_text(false);
2700    let mut buffer = Vec::new();
2701    let mut state = u8::from(scoped_xml.is_some()) * 4;
2702    let mut found_rect = false;
2703
2704    loop {
2705        let Ok((namespace, event)) = reader.read_resolved_event_into(&mut buffer) else {
2706            return false;
2707        };
2708        match event {
2709            Event::Start(element) if state == 4 && element.name().as_ref() == b"rdocx-scope" => {
2710                state = 0;
2711            }
2712            Event::Start(element) if state == 0 => {
2713                if !resolved_name_matches(
2714                    &namespace,
2715                    element.local_name().as_ref(),
2716                    crate::namespace::W_NS.as_bytes(),
2717                    b"pict",
2718                ) {
2719                    return false;
2720                }
2721                state = 1;
2722            }
2723            Event::Start(element) if state == 1 && !found_rect => {
2724                if !resolved_name_matches(
2725                    &namespace,
2726                    element.local_name().as_ref(),
2727                    VML_NAMESPACE,
2728                    b"rect",
2729                ) || !rect_has_enabled_horizontal_rule(&reader, &element)
2730                {
2731                    return false;
2732                }
2733                found_rect = true;
2734                state = 2;
2735            }
2736            Event::Empty(element) if state == 1 && !found_rect => {
2737                if !resolved_name_matches(
2738                    &namespace,
2739                    element.local_name().as_ref(),
2740                    VML_NAMESPACE,
2741                    b"rect",
2742                ) || !rect_has_enabled_horizontal_rule(&reader, &element)
2743                {
2744                    return false;
2745                }
2746                found_rect = true;
2747            }
2748            Event::End(element) if state == 2 => {
2749                if !resolved_name_matches(
2750                    &namespace,
2751                    element.local_name().as_ref(),
2752                    VML_NAMESPACE,
2753                    b"rect",
2754                ) {
2755                    return false;
2756                }
2757                state = 1;
2758            }
2759            Event::End(element) if state == 1 && found_rect => {
2760                if !resolved_name_matches(
2761                    &namespace,
2762                    element.local_name().as_ref(),
2763                    crate::namespace::W_NS.as_bytes(),
2764                    b"pict",
2765                ) {
2766                    return false;
2767                }
2768                state = 3;
2769            }
2770            Event::End(element)
2771                if state == 3
2772                    && scoped_xml.is_some()
2773                    && element.name().as_ref() == b"rdocx-scope" =>
2774            {
2775                state = 4;
2776            }
2777            Event::Text(text) if text.as_ref().iter().all(u8::is_ascii_whitespace) => {}
2778            Event::Eof => {
2779                let finished = if scoped_xml.is_some() {
2780                    state == 4
2781                } else {
2782                    state == 3
2783                };
2784                return finished && found_rect;
2785            }
2786            _ => return false,
2787        }
2788        buffer.clear();
2789    }
2790}
2791
2792#[allow(non_snake_case)]
2793impl CT_R {
2794    pub fn new(text: &str) -> Self {
2795        CT_R {
2796            properties: None,
2797            content: vec![RunContent::Text(CT_Text::new(text))],
2798            extra_xml: Vec::new(),
2799            extra_xml_positions: Vec::new(),
2800            alt_drawings: Vec::new(),
2801        }
2802    }
2803
2804    pub(crate) fn from_empty_root(root: &BytesStart<'_>, word_prefixes: &[String]) -> Result<Self> {
2805        let mut run = Self {
2806            properties: None,
2807            content: Vec::new(),
2808            extra_xml: Vec::new(),
2809            extra_xml_positions: Vec::new(),
2810            alt_drawings: Vec::new(),
2811        };
2812        if let Some(record) = capture_root_attribute_record(root, word_prefixes)? {
2813            run.extra_xml.push(record);
2814            run.extra_xml_positions.push(RAW_ROOT_ATTRIBUTES_FLAG);
2815        }
2816        Ok(run)
2817    }
2818
2819    /// Decode a raw-child boundary stored in `extra_xml_positions`.
2820    #[doc(hidden)]
2821    pub fn raw_child_position(encoded: usize) -> usize {
2822        encoded & RAW_CHILD_POSITION_MASK
2823    }
2824
2825    /// Report whether a raw child was parsed as a legacy horizontal rule.
2826    #[doc(hidden)]
2827    pub fn raw_child_is_legacy_horizontal_rule(encoded: usize) -> bool {
2828        encoded & RAW_LEGACY_HORIZONTAL_RULE_FLAG != 0
2829    }
2830
2831    /// Report whether a raw carrier retains attributes from the run root.
2832    #[doc(hidden)]
2833    pub fn raw_child_is_root_attributes(encoded: usize) -> bool {
2834        encoded & RAW_ROOT_ATTRIBUTES_FLAG != 0
2835    }
2836
2837    /// Report whether this raw slot has one selected drawing projection.
2838    #[doc(hidden)]
2839    pub fn raw_child_has_alternate_content_drawing(encoded: usize) -> bool {
2840        encoded & RAW_ALTERNATE_CONTENT_DRAWING_FLAG != 0
2841    }
2842
2843    /// Replace a raw-child boundary without changing its parsed classification.
2844    #[doc(hidden)]
2845    pub fn set_raw_child_position(encoded: &mut usize, position: usize) {
2846        debug_assert_eq!(position & RAW_CHILD_FLAGS, 0);
2847        *encoded = (position & RAW_CHILD_POSITION_MASK) | (*encoded & RAW_CHILD_FLAGS);
2848    }
2849
2850    fn encode_raw_child_position(
2851        position: usize,
2852        legacy_horizontal_rule: bool,
2853        alternate_content_drawing: bool,
2854    ) -> usize {
2855        debug_assert_eq!(position & RAW_CHILD_FLAGS, 0);
2856        position
2857            | if legacy_horizontal_rule {
2858                RAW_LEGACY_HORIZONTAL_RULE_FLAG
2859            } else {
2860                0
2861            }
2862            | if alternate_content_drawing {
2863                RAW_ALTERNATE_CONTENT_DRAWING_FLAG
2864            } else {
2865                0
2866            }
2867    }
2868
2869    /// Get the combined text of all text content in this run.
2870    pub fn text(&self) -> String {
2871        self.content.iter().map(Self::content_text).collect()
2872    }
2873
2874    /// The text one content item contributes to [`Self::text`].
2875    fn content_text(content: &RunContent) -> &str {
2876        match content {
2877            RunContent::Text(t) | RunContent::DeletedText(t) => &t.text,
2878            RunContent::Tab => "\t",
2879            RunContent::Break(_) => "\n",
2880            RunContent::Drawing(_) => "", // Drawings have no text content
2881            RunContent::Field(field) => field.projected_text().unwrap_or(""),
2882            RunContent::FootnoteRef { custom_mark, .. }
2883            | RunContent::EndnoteRef { custom_mark, .. } => custom_mark.as_deref().unwrap_or(""),
2884            RunContent::CommentReference { .. } => "",
2885            // A symbol is font-encoded rather than Unicode, and the special
2886            // characters carry no text, so neither contributes to extraction.
2887            RunContent::Symbol { .. } => "",
2888            RunContent::SpecialCharacter(SpecialCharacter::CarriageReturn) => "\n",
2889            RunContent::SpecialCharacter(_) => "",
2890        }
2891    }
2892
2893    /// The literal text that contributes to a public split offset.
2894    fn literal_text(content: &RunContent) -> &str {
2895        match content {
2896            RunContent::Text(text) | RunContent::DeletedText(text) => &text.text,
2897            RunContent::Tab
2898            | RunContent::Break(_)
2899            | RunContent::Drawing(_)
2900            | RunContent::Field(_)
2901            | RunContent::FootnoteRef { .. }
2902            | RunContent::EndnoteRef { .. }
2903            | RunContent::CommentReference { .. }
2904            | RunContent::Symbol { .. }
2905            | RunContent::SpecialCharacter(_) => "",
2906        }
2907    }
2908
2909    fn literal_len(&self) -> usize {
2910        self.content
2911            .iter()
2912            .map(Self::literal_text)
2913            .map(str::chars)
2914            .map(Iterator::count)
2915            .sum()
2916    }
2917
2918    /// Split this run at a Unicode scalar offset of its literal text.
2919    ///
2920    /// This run keeps the content before `offset`, and the returned run, with
2921    /// cloned properties, receives the rest. Text-free content and raw children
2922    /// at the split point stay in this run. Nothing changes on error.
2923    fn split_off_at(&mut self, offset: usize) -> std::result::Result<CT_R, RunSplitError> {
2924        let len = self.literal_len();
2925        if offset == 0 || offset >= len {
2926            return Err(RunSplitError::OffsetOutOfRange { offset, len });
2927        }
2928        // The first item whose text reaches past the offset, and how many of
2929        // its characters come before the offset.
2930        let mut end = 0;
2931        let (index, within, width) = self
2932            .content
2933            .iter()
2934            .enumerate()
2935            .find_map(|(index, content)| {
2936                let start = end;
2937                let width = Self::literal_text(content).chars().count();
2938                end += width;
2939                (width > 0 && end >= offset).then_some((index, offset - start, width))
2940            })
2941            .expect("an offset inside the run text falls inside one content item");
2942        let tail_content = if within == width {
2943            self.content.split_off(index + 1)
2944        } else {
2945            let rest = match &mut self.content[index] {
2946                RunContent::Text(text) => RunContent::Text(text.split_off_at(within)),
2947                RunContent::DeletedText(text) => RunContent::DeletedText(text.split_off_at(within)),
2948                _ => unreachable!("an interior literal offset falls inside literal text"),
2949            };
2950            let mut tail = self.content.split_off(index + 1);
2951            tail.insert(0, rest);
2952            tail
2953        };
2954        let removed_prefix_content = index + usize::from(within == width);
2955
2956        let mut tail = CT_R {
2957            properties: self.properties.clone(),
2958            content: tail_content,
2959            extra_xml: Vec::new(),
2960            extra_xml_positions: Vec::new(),
2961            alt_drawings: Vec::new(),
2962        };
2963        // A raw child position counts the modeled children before it, so a
2964        // child after the first `index` content items moves to the tail.
2965        let head_boundary = usize::from(self.properties.is_some()) + index;
2966        if self.extra_xml_positions.len() == self.extra_xml.len() {
2967            let raw_children = std::mem::take(&mut self.extra_xml);
2968            let positions = std::mem::take(&mut self.extra_xml_positions);
2969            let mut alternate_drawings = std::mem::take(&mut self.alt_drawings).into_iter();
2970            for (raw, mut encoded) in raw_children.into_iter().zip(positions) {
2971                let position = Self::raw_child_position(encoded);
2972                let alternate_drawing = Self::raw_child_has_alternate_content_drawing(encoded)
2973                    .then(|| alternate_drawings.next())
2974                    .flatten();
2975                if encoded & RAW_RUN_LOCAL_BINDINGS_FLAG != 0
2976                    || (Self::raw_child_is_root_attributes(encoded)
2977                        && (raw.starts_with(b"<rdocxRootAttributes xmlns:")
2978                            || raw.starts_with(b"<rdocxRootAttributes xmlns=")))
2979                {
2980                    // Namespace-only records are constructed with declarations
2981                    // first. Real root-attribute records put their payload first.
2982                    // Scope follows both pieces without duplicating owner attrs.
2983                    self.extra_xml.push(raw.clone());
2984                    self.extra_xml_positions.push(encoded);
2985                    tail.extra_xml.push(raw);
2986                    tail.extra_xml_positions.push(encoded);
2987                    continue;
2988                }
2989                if position <= head_boundary {
2990                    self.extra_xml.push(raw);
2991                    self.extra_xml_positions.push(encoded);
2992                    self.alt_drawings.extend(alternate_drawing);
2993                } else {
2994                    Self::set_raw_child_position(&mut encoded, position - removed_prefix_content);
2995                    tail.extra_xml.push(raw);
2996                    tail.extra_xml_positions.push(encoded);
2997                    tail.alt_drawings.extend(alternate_drawing);
2998                }
2999            }
3000            self.alt_drawings.extend(alternate_drawings);
3001        }
3002        let head_raw_children = self.extra_xml.len();
3003        for content in &mut tail.content {
3004            if let RunContent::CommentReference { raw_before, .. } = content {
3005                *raw_before = raw_before.saturating_sub(head_raw_children);
3006            }
3007        }
3008        Ok(tail)
3009    }
3010
3011    /// Replace typed run content while retaining every raw child boundary.
3012    #[doc(hidden)]
3013    pub fn replace_content(&mut self, content: Vec<RunContent>) {
3014        self.retain_comment_reference_sources(None);
3015        let property_boundary = usize::from(self.properties.is_some());
3016        let replacement_end = property_boundary + content.len();
3017        for position in &mut self.extra_xml_positions {
3018            if Self::raw_child_position(*position) > property_boundary {
3019                Self::set_raw_child_position(position, replacement_end);
3020            }
3021        }
3022        self.content = content;
3023    }
3024
3025    /// Append typed content without moving raw children after existing content.
3026    #[doc(hidden)]
3027    pub fn append_content(&mut self, content: RunContent) {
3028        self.content.push(content);
3029    }
3030
3031    /// Materialize run properties in their required first-child position.
3032    #[doc(hidden)]
3033    pub fn ensure_properties(&mut self) -> &mut CT_RPr {
3034        if self.properties.is_none() {
3035            for position in &mut self.extra_xml_positions {
3036                let shifted = Self::raw_child_position(*position) + 1;
3037                Self::set_raw_child_position(position, shifted);
3038            }
3039            self.properties = Some(CT_RPr::default());
3040        }
3041        self.properties.as_mut().expect("properties were inserted")
3042    }
3043
3044    /// Remap raw boundaries after selected typed content children are removed.
3045    #[doc(hidden)]
3046    pub fn remap_removed_content(&mut self, removed: &[bool]) {
3047        let property_boundary = usize::from(self.properties.is_some());
3048        self.retain_comment_reference_sources(Some(removed));
3049        for position in &mut self.extra_xml_positions {
3050            let decoded = Self::raw_child_position(*position);
3051            let content_boundary = decoded.saturating_sub(property_boundary);
3052            let remapped = decoded.saturating_sub(
3053                removed
3054                    .iter()
3055                    .take(content_boundary.min(removed.len()))
3056                    .filter(|remove| **remove)
3057                    .count(),
3058            );
3059            Self::set_raw_child_position(position, remapped);
3060        }
3061    }
3062
3063    fn retain_comment_reference_sources(&mut self, removed: Option<&[bool]>) {
3064        if self.extra_xml.len() != self.extra_xml_positions.len() {
3065            return;
3066        }
3067        let property_boundary = usize::from(self.properties.is_some());
3068        let keep = self
3069            .extra_xml_positions
3070            .iter()
3071            .map(|encoded| {
3072                *encoded & RAW_COMMENT_REFERENCE_FLAG == 0
3073                    || removed.is_some_and(|removed| {
3074                        !removed
3075                            .get(
3076                                Self::raw_child_position(*encoded)
3077                                    .saturating_sub(property_boundary),
3078                            )
3079                            .copied()
3080                            .unwrap_or(true)
3081                    })
3082            })
3083            .collect::<Vec<_>>();
3084        let mut index = 0;
3085        self.extra_xml.retain(|_| {
3086            let retain = keep[index];
3087            index += 1;
3088            retain
3089        });
3090        let mut index = 0;
3091        self.extra_xml_positions.retain(|_| {
3092            let retain = keep[index];
3093            index += 1;
3094            retain
3095        });
3096    }
3097
3098    /// Remove selected comment-reference content and retain surrounding raw XML.
3099    #[doc(hidden)]
3100    pub fn remove_comment_references(&mut self, ids: &[i32]) -> bool {
3101        let removed = self
3102            .content
3103            .iter()
3104            .map(|content| {
3105                matches!(content, RunContent::CommentReference { id, .. } if ids.contains(id))
3106            })
3107            .collect::<Vec<_>>();
3108        let removed_any = removed.iter().any(|remove| *remove);
3109        if removed_any {
3110            self.remap_removed_content(&removed);
3111            self.content = self
3112                .content
3113                .drain(..)
3114                .zip(removed)
3115                .filter_map(|(content, remove)| (!remove).then_some(content))
3116                .collect();
3117        }
3118        removed_any
3119    }
3120
3121    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
3122        crate::document::admit_word_reader(reader, &["w".to_owned()], b"r", None)?;
3123        Self::from_xml_with_prefixes(
3124            reader,
3125            &["w".to_owned(), format!("\0mc\0{}", crate::namespace::MC_NS)],
3126        )
3127    }
3128
3129    /// Parse one namespace-complete regular run for checked owner replacement.
3130    #[doc(hidden)]
3131    pub fn from_xml_fragment_checked(xml: &[u8]) -> Result<Self> {
3132        crate::document::admit_word_xml(xml, &[], None)?;
3133        validate_word_owner_fragment(xml, b"r", true)?;
3134        let mut reader = Reader::from_reader(xml);
3135        reader.config_mut().trim_text(false);
3136        loop {
3137            match reader.read_event()? {
3138                Event::Start(root) => {
3139                    let prefixes = word_prefixes_at(&root, &[])?;
3140                    return Self::from_xml_with_prefixes_and_root(
3141                        &mut reader,
3142                        &prefixes,
3143                        Some(&root),
3144                    );
3145                }
3146                Event::Empty(root) => {
3147                    let prefixes = word_prefixes_at(&root, &[])?;
3148                    return Self::from_empty_root(&root, &prefixes);
3149                }
3150                Event::Eof => return Err(OxmlError::MissingElement("w:r root".into())),
3151                _ => {}
3152            }
3153        }
3154    }
3155
3156    pub(crate) fn from_xml_with_prefixes(
3157        reader: &mut Reader<&[u8]>,
3158        word_prefixes: &[String],
3159    ) -> Result<Self> {
3160        Self::from_xml_with_prefixes_and_root(reader, word_prefixes, None)
3161    }
3162
3163    pub(crate) fn from_xml_with_prefixes_and_root(
3164        reader: &mut Reader<&[u8]>,
3165        word_prefixes: &[String],
3166        root: Option<&BytesStart<'_>>,
3167    ) -> Result<Self> {
3168        let mut properties = None;
3169        let mut content = Vec::new();
3170        let mut extra_xml = Vec::new();
3171        let mut extra_xml_positions = Vec::new();
3172        let mut alt_drawings = Vec::new();
3173        let mut modeled_children = 0usize;
3174        let mut buf = Vec::new();
3175        loop {
3176            match reader.read_event_into(&mut buf) {
3177                Ok(Event::Start(ref e)) => {
3178                    let name = e.name();
3179                    let prefixes = word_prefixes_at(e, word_prefixes)?;
3180                    if is_word_element(name.as_ref(), b"rPr", &prefixes) {
3181                        let raw = capture_element(reader, e)?;
3182                        properties = Some(crate::numbering::parse_scoped_rpr(&raw, word_prefixes)?);
3183                        modeled_children += 1;
3184                    } else if is_word_element(name.as_ref(), b"t", &prefixes) {
3185                        let preserve = e.attributes().any(|a| {
3186                            a.ok()
3187                                .map(|a| {
3188                                    a.key.as_ref() == b"xml:space"
3189                                        && a.value.as_ref() == b"preserve"
3190                                })
3191                                .unwrap_or(false)
3192                        });
3193                        // `read_text` returns the raw markup span, so entity
3194                        // references in it still need resolving.
3195                        let encoded = reader.read_text(name)?;
3196                        encoded
3197                            .decode()
3198                            .map_err(|error| OxmlError::InvalidValue(error.to_string()))?;
3199                        let text = crate::xml_text::decode_escaped(&encoded);
3200                        match content.last_mut() {
3201                            Some(RunContent::FootnoteRef {
3202                                custom_mark: Some(mark),
3203                                ..
3204                            })
3205                            | Some(RunContent::EndnoteRef {
3206                                custom_mark: Some(mark),
3207                                ..
3208                            }) if mark.is_empty() => *mark = text,
3209                            _ => content.push(RunContent::Text(CT_Text {
3210                                text,
3211                                preserve_space: preserve,
3212                            })),
3213                        }
3214                        modeled_children += 1;
3215                    } else if is_word_element(name.as_ref(), b"delText", &prefixes) {
3216                        let preserve = e.attributes().any(|a| {
3217                            a.ok().is_some_and(|a| {
3218                                a.key.as_ref() == b"xml:space" && a.value.as_ref() == b"preserve"
3219                            })
3220                        });
3221                        let encoded = reader.read_text(name)?;
3222                        encoded
3223                            .decode()
3224                            .map_err(|error| OxmlError::InvalidValue(error.to_string()))?;
3225                        let text = crate::xml_text::decode_escaped(&encoded);
3226                        content.push(RunContent::DeletedText(CT_Text {
3227                            text,
3228                            preserve_space: preserve,
3229                        }));
3230                        modeled_children += 1;
3231                    } else if is_word_element(name.as_ref(), b"drawing", &prefixes) {
3232                        content.push(RunContent::Drawing(CT_Drawing::from_xml_with_prefixes(
3233                            reader,
3234                            &prefixes,
3235                            word_prefixes,
3236                        )?));
3237                        modeled_children += 1;
3238                    } else if is_word_element(name.as_ref(), b"commentReference", &prefixes) {
3239                        let id = required_word_i32_attribute(e, b"id", &prefixes)?;
3240                        let raw = capture_element(reader, e)?;
3241                        let mut source = Reader::from_reader(raw.as_slice());
3242                        source.read_event()?;
3243                        let has_payload = !matches!(source.read_event()?, Event::End(_));
3244                        if has_payload || comment_reference_has_payload_attributes(e, &prefixes) {
3245                            extra_xml.push(close_comment_reference_source(&raw, &prefixes)?);
3246                            extra_xml_positions.push(modeled_children | RAW_COMMENT_REFERENCE_FLAG);
3247                        }
3248                        content.push(RunContent::CommentReference {
3249                            id,
3250                            raw_before: extra_xml.len(),
3251                        });
3252                        modeled_children += 1;
3253                    } else if is_element_in_namespace(
3254                        name.as_ref(),
3255                        b"AlternateContent",
3256                        crate::namespace::MC_NS,
3257                        &prefixes,
3258                    ) {
3259                        // Keep the block verbatim so the VML fallback survives
3260                        // a write, and separately read the DrawingML out of it
3261                        // so layout can see the shape. alt_drawings is never
3262                        // serialised, the raw copy below is what gets written.
3263                        let raw = capture_element(reader, e)?;
3264                        let drawing =
3265                            crate::drawing::parse_alternate_content_admitted(&raw, &prefixes);
3266                        let has_drawing = drawing.is_some();
3267                        if let Some(drawing) = drawing {
3268                            alt_drawings.push(drawing);
3269                        }
3270                        extra_xml.push(raw);
3271                        extra_xml_positions.push(Self::encode_raw_child_position(
3272                            modeled_children,
3273                            false,
3274                            has_drawing,
3275                        ));
3276                    } else {
3277                        // Capture unknown child elements as raw XML
3278                        let is_word_pict = is_word_element(name.as_ref(), b"pict", &prefixes);
3279                        let raw = capture_element(reader, e)?;
3280                        let legacy_horizontal_rule = is_word_pict
3281                            && is_legacy_horizontal_rule(&raw, &namespace_bindings(word_prefixes));
3282                        extra_xml.push(raw);
3283                        extra_xml_positions.push(Self::encode_raw_child_position(
3284                            modeled_children,
3285                            legacy_horizontal_rule,
3286                            false,
3287                        ));
3288                    }
3289                }
3290                Ok(Event::Empty(ref e)) => {
3291                    let name = e.name();
3292                    let prefixes = word_prefixes_at(e, word_prefixes)?;
3293                    if is_word_element(name.as_ref(), b"tab", &prefixes) {
3294                        content.push(RunContent::Tab);
3295                        modeled_children += 1;
3296                    } else if is_word_element(name.as_ref(), b"br", &prefixes) {
3297                        let break_type = optional_word_attribute(e, b"type", &prefixes)
3298                            .map(|value| match value.as_bytes() {
3299                                b"page" => BreakType::Page,
3300                                b"column" => BreakType::Column,
3301                                _ => BreakType::Line,
3302                            })
3303                            .unwrap_or(BreakType::Line);
3304                        content.push(RunContent::Break(break_type));
3305                        modeled_children += 1;
3306                    } else if is_word_element(name.as_ref(), b"footnoteReference", &prefixes) {
3307                        let id = optional_word_attribute(e, b"id", &prefixes)
3308                            .and_then(|value| value.parse::<i32>().ok())
3309                            .unwrap_or(0);
3310                        let custom_mark =
3311                            optional_word_attribute(e, b"customMarkFollows", &prefixes)
3312                                .filter(|value| value == "1" || value == "true")
3313                                .map(|_| String::new());
3314                        content.push(RunContent::FootnoteRef { id, custom_mark });
3315                        modeled_children += 1;
3316                    } else if is_word_element(name.as_ref(), b"endnoteReference", &prefixes) {
3317                        let id = optional_word_attribute(e, b"id", &prefixes)
3318                            .and_then(|value| value.parse::<i32>().ok())
3319                            .unwrap_or(0);
3320                        let custom_mark =
3321                            optional_word_attribute(e, b"customMarkFollows", &prefixes)
3322                                .filter(|value| value == "1" || value == "true")
3323                                .map(|_| String::new());
3324                        content.push(RunContent::EndnoteRef { id, custom_mark });
3325                        modeled_children += 1;
3326                    } else if is_word_element(name.as_ref(), b"commentReference", &prefixes) {
3327                        let id = required_word_i32_attribute(e, b"id", &prefixes)?;
3328                        if comment_reference_has_payload_attributes(e, &prefixes) {
3329                            extra_xml.push(close_comment_reference_source(
3330                                &capture_empty_element(e)?,
3331                                &prefixes,
3332                            )?);
3333                            extra_xml_positions.push(modeled_children | RAW_COMMENT_REFERENCE_FLAG);
3334                        }
3335                        content.push(RunContent::CommentReference {
3336                            id,
3337                            raw_before: extra_xml.len(),
3338                        });
3339                        modeled_children += 1;
3340                    } else if let Some(typed) = parse_typed_special_run_child(e, &prefixes)? {
3341                        content.push(typed);
3342                        modeled_children += 1;
3343                    } else if !is_word_element(name.as_ref(), b"rPr", &prefixes) {
3344                        // Capture unknown empty child elements (e.g.
3345                        // w:commentReference) as raw XML, mirroring the
3346                        // Event::Start fallback above.
3347                        //
3348                        // A self-closing <w:rPr/> is deliberately skipped.
3349                        // extra_xml is re-emitted after the run content, but
3350                        // CT_R requires w:rPr to be the first child, so
3351                        // capturing it here would move it past <w:t> and
3352                        // produce schema-invalid output. An empty rPr carries
3353                        // no formatting, so dropping it loses nothing.
3354                        let raw = capture_empty_element(e)?;
3355                        extra_xml.push(raw);
3356                        extra_xml_positions.push(modeled_children);
3357                    }
3358                }
3359                Ok(event @ (Event::Comment(_) | Event::PI(_))) => {
3360                    extra_xml.push(capture_standalone_event(event.into_owned())?);
3361                    extra_xml_positions.push(modeled_children);
3362                }
3363                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"r") => {
3364                    break;
3365                }
3366                Ok(Event::Eof) => break,
3367                Err(e) => return Err(e.into()),
3368                _ => {}
3369            }
3370            buf.clear();
3371        }
3372
3373        let retained_attributes = root
3374            .map(|root| capture_root_attribute_record(root, word_prefixes))
3375            .transpose()?
3376            .flatten();
3377        let mut retained_bindings = Vec::new();
3378        // Canonical w is supplied by the generated run itself. Recording it
3379        // would introduce a synthetic raw slot and break source fixed points.
3380        let external_bindings = namespace_bindings(word_prefixes)
3381            .into_iter()
3382            .filter(|(prefix, uri)| prefix != "w" || uri != crate::namespace::W_NS)
3383            .collect::<Vec<_>>();
3384        for raw in &extra_xml {
3385            for binding in external_bindings_used_by_raw(raw, &external_bindings)? {
3386                if !retained_bindings.contains(&binding) {
3387                    retained_bindings.push(binding);
3388                }
3389            }
3390        }
3391        let mut local_bindings = Vec::new();
3392        if let Some(root) = root {
3393            let declarations = raw_namespace_declarations(root)?;
3394            for drawing in content.iter().filter_map(|item| match item {
3395                RunContent::Drawing(drawing) => Some(drawing),
3396                _ => None,
3397            }) {
3398                for raw in drawing
3399                    .inline
3400                    .as_ref()
3401                    .and_then(|inline| inline.raw_xml.as_deref())
3402                    .into_iter()
3403                    .chain(
3404                        drawing
3405                            .anchor
3406                            .as_ref()
3407                            .and_then(|anchor| anchor.raw_xml.as_deref()),
3408                    )
3409                {
3410                    for (prefix, _) in external_bindings_used_by_raw(raw, &declarations)? {
3411                        if prefix == "w"
3412                            && declarations.iter().any(|(bound, uri)| {
3413                                bound == &prefix && uri == crate::namespace::W_NS
3414                            })
3415                        {
3416                            continue;
3417                        }
3418                        if !local_bindings.contains(&prefix) {
3419                            local_bindings.push(prefix);
3420                        }
3421                    }
3422                }
3423            }
3424        }
3425        if let Some(record) = retained_attributes {
3426            extra_xml.push(record);
3427            extra_xml_positions.push(RAW_ROOT_ATTRIBUTES_FLAG);
3428        }
3429        if !retained_bindings.is_empty() {
3430            let mut record = BytesStart::new(std::str::from_utf8(ROOT_ATTRIBUTES_ELEMENT)?);
3431            for (prefix, uri) in retained_bindings {
3432                let name = if prefix.is_empty() {
3433                    "xmlns".to_owned()
3434                } else {
3435                    format!("xmlns:{prefix}")
3436                };
3437                record.push_attribute((name.as_str(), uri.as_str()));
3438            }
3439            let mut writer = Writer::new(Vec::new());
3440            writer.write_event(Event::Empty(record))?;
3441            extra_xml.push(writer.into_inner());
3442            extra_xml_positions.push(RAW_ROOT_ATTRIBUTES_FLAG);
3443        }
3444        if !local_bindings.is_empty() {
3445            let mut record = BytesStart::new(std::str::from_utf8(ROOT_ATTRIBUTES_ELEMENT)?);
3446            if let Some(root) = root {
3447                for attribute in root.attributes() {
3448                    let attribute = attribute?;
3449                    let name = attribute.key.as_ref();
3450                    let prefix = if name == b"xmlns" {
3451                        Some("")
3452                    } else {
3453                        name.strip_prefix(b"xmlns:")
3454                            .map(std::str::from_utf8)
3455                            .transpose()?
3456                    };
3457                    if prefix
3458                        .is_some_and(|prefix| local_bindings.iter().any(|local| local == prefix))
3459                    {
3460                        // Preserve the actual declaration's lexical value, including references.
3461                        record.push_attribute(attribute);
3462                    }
3463                }
3464            }
3465            let mut writer = Writer::new(Vec::new());
3466            writer.write_event(Event::Empty(record))?;
3467            extra_xml.push(writer.into_inner());
3468            extra_xml_positions.push(RAW_ROOT_ATTRIBUTES_FLAG | RAW_RUN_LOCAL_BINDINGS_FLAG);
3469        }
3470
3471        Ok(CT_R {
3472            properties,
3473            content,
3474            extra_xml,
3475            extra_xml_positions,
3476            alt_drawings,
3477        })
3478    }
3479
3480    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
3481        self.to_xml_with_word_override(writer, None)
3482    }
3483
3484    pub(crate) fn to_xml_with_word_override<W: std::io::Write>(
3485        &self,
3486        writer: &mut Writer<W>,
3487        foreign_word_namespace: Option<&str>,
3488    ) -> Result<()> {
3489        crate::document::admit_word_model(crate::document::WordOwner::Run(self))?;
3490        let mut localized = Vec::new();
3491        let (run, raw_bindings) = run_xml_root(self, foreign_word_namespace, &mut localized)?;
3492        writer.write_event(Event::Start(run))?;
3493
3494        let ordered_raw = self.extra_xml_positions.len() == self.extra_xml.len();
3495        let mut typed_boundary = 0usize;
3496        if ordered_raw {
3497            write_run_raw_boundary(
3498                writer,
3499                self,
3500                typed_boundary,
3501                foreign_word_namespace,
3502                &raw_bindings,
3503                &localized,
3504            )?;
3505        }
3506        if let Some(ref props) = self.properties {
3507            props.to_xml_with_word_override(writer, foreign_word_namespace)?;
3508            typed_boundary += 1;
3509            if ordered_raw {
3510                write_run_raw_boundary(
3511                    writer,
3512                    self,
3513                    typed_boundary,
3514                    foreign_word_namespace,
3515                    &raw_bindings,
3516                    &localized,
3517                )?;
3518            }
3519        }
3520
3521        let mut raw_written = 0;
3522        for item in &self.content {
3523            match item {
3524                RunContent::Text(t) => {
3525                    let mut e = BytesStart::new("w:t");
3526                    if t.preserve_space {
3527                        e.push_attribute(("xml:space", "preserve"));
3528                    }
3529                    writer.write_event(Event::Start(e))?;
3530                    writer.write_event(Event::Text(BytesText::new(&t.text)))?;
3531                    writer.write_event(Event::End(BytesEnd::new("w:t")))?;
3532                }
3533                RunContent::DeletedText(t) => {
3534                    let mut e = BytesStart::new("w:delText");
3535                    if t.preserve_space {
3536                        e.push_attribute(("xml:space", "preserve"));
3537                    }
3538                    writer.write_event(Event::Start(e))?;
3539                    writer.write_event(Event::Text(BytesText::new(&t.text)))?;
3540                    writer.write_event(Event::End(BytesEnd::new("w:delText")))?;
3541                }
3542                RunContent::Tab => {
3543                    writer.write_event(Event::Empty(BytesStart::new("w:tab")))?;
3544                }
3545                RunContent::Break(bt) => {
3546                    let mut e = BytesStart::new("w:br");
3547                    match bt {
3548                        BreakType::Page => e.push_attribute(("w:type", "page")),
3549                        BreakType::Column => e.push_attribute(("w:type", "column")),
3550                        BreakType::Line => {}
3551                    }
3552                    writer.write_event(Event::Empty(e))?;
3553                }
3554                RunContent::Drawing(d) => {
3555                    d.to_xml(writer)?;
3556                }
3557                RunContent::Field(_) => {
3558                    // Field runs are serialized at the paragraph level as <w:fldSimple>
3559                }
3560                RunContent::FootnoteRef { id, custom_mark } => {
3561                    let mut buf = itoa::Buffer::new();
3562                    let mut e = BytesStart::new("w:footnoteReference");
3563                    e.push_attribute(("w:id", buf.format(*id)));
3564                    if custom_mark.is_some() {
3565                        e.push_attribute(("w:customMarkFollows", "1"));
3566                    }
3567                    writer.write_event(Event::Empty(e))?;
3568                    if let Some(mark) = custom_mark {
3569                        let mut text = BytesStart::new("w:t");
3570                        text.push_attribute(("xml:space", "preserve"));
3571                        writer.write_event(Event::Start(text))?;
3572                        writer.write_event(Event::Text(quick_xml::events::BytesText::new(mark)))?;
3573                        writer.write_event(Event::End(BytesEnd::new("w:t")))?;
3574                    }
3575                }
3576                RunContent::EndnoteRef { id, custom_mark } => {
3577                    let mut buf = itoa::Buffer::new();
3578                    let mut e = BytesStart::new("w:endnoteReference");
3579                    e.push_attribute(("w:id", buf.format(*id)));
3580                    if custom_mark.is_some() {
3581                        e.push_attribute(("w:customMarkFollows", "1"));
3582                    }
3583                    writer.write_event(Event::Empty(e))?;
3584                    if let Some(mark) = custom_mark {
3585                        let mut text = BytesStart::new("w:t");
3586                        text.push_attribute(("xml:space", "preserve"));
3587                        writer.write_event(Event::Start(text))?;
3588                        writer.write_event(Event::Text(quick_xml::events::BytesText::new(mark)))?;
3589                        writer.write_event(Event::End(BytesEnd::new("w:t")))?;
3590                    }
3591                }
3592                RunContent::Symbol { font, char_code } => {
3593                    let mut e = BytesStart::new("w:sym");
3594                    e.push_attribute(("w:font", font.as_str()));
3595                    e.push_attribute(("w:char", format!("{char_code:04X}").as_str()));
3596                    writer.write_event(Event::Empty(e))?;
3597                }
3598                RunContent::SpecialCharacter(character) => {
3599                    write_special_character(writer, *character)?;
3600                }
3601                RunContent::CommentReference { id, raw_before } => {
3602                    if !ordered_raw {
3603                        for (index, raw) in self
3604                            .extra_xml
3605                            .iter()
3606                            .enumerate()
3607                            .take((*raw_before).min(self.extra_xml.len()))
3608                            .skip(raw_written)
3609                        {
3610                            if !self
3611                                .extra_xml_positions
3612                                .get(index)
3613                                .is_some_and(|position| *position & RAW_COMMENT_REFERENCE_FLAG != 0)
3614                            {
3615                                write_run_raw_source(
3616                                    writer,
3617                                    raw,
3618                                    foreign_word_namespace,
3619                                    &raw_bindings,
3620                                    &localized,
3621                                )?;
3622                            }
3623                            raw_written += 1;
3624                        }
3625                    }
3626                    if let Some(raw) = self
3627                        .extra_xml_positions
3628                        .iter()
3629                        .zip(&self.extra_xml)
3630                        .find_map(|(position, raw)| {
3631                            (*position & RAW_COMMENT_REFERENCE_FLAG != 0
3632                                && Self::raw_child_position(*position) == typed_boundary
3633                                && retained_comment_reference_id(raw) == Some(*id))
3634                            .then_some(raw)
3635                        })
3636                    {
3637                        write_run_raw_source(
3638                            writer,
3639                            raw,
3640                            foreign_word_namespace,
3641                            &raw_bindings,
3642                            &localized,
3643                        )?;
3644                        typed_boundary += 1;
3645                        if ordered_raw {
3646                            write_run_raw_boundary(
3647                                writer,
3648                                self,
3649                                typed_boundary,
3650                                foreign_word_namespace,
3651                                &raw_bindings,
3652                                &localized,
3653                            )?;
3654                        }
3655                        continue;
3656                    }
3657                    let mut buf = itoa::Buffer::new();
3658                    let mut e = BytesStart::new("w:commentReference");
3659                    e.push_attribute(("w:id", buf.format(*id)));
3660                    writer.write_event(Event::Empty(e))?;
3661                }
3662            }
3663            typed_boundary += 1;
3664            if ordered_raw {
3665                write_run_raw_boundary(
3666                    writer,
3667                    self,
3668                    typed_boundary,
3669                    foreign_word_namespace,
3670                    &raw_bindings,
3671                    &localized,
3672                )?;
3673            }
3674        }
3675
3676        // Write captured unknown child elements
3677        if !ordered_raw {
3678            for (index, raw) in self.extra_xml.iter().enumerate().skip(raw_written) {
3679                if is_root_attribute_record(raw)
3680                    || self
3681                        .extra_xml_positions
3682                        .get(index)
3683                        .is_some_and(|position| *position & RAW_COMMENT_REFERENCE_FLAG != 0)
3684                {
3685                    continue;
3686                }
3687                write_run_raw_source(
3688                    writer,
3689                    raw,
3690                    foreign_word_namespace,
3691                    &raw_bindings,
3692                    &localized,
3693                )?;
3694            }
3695        }
3696
3697        writer.write_event(Event::End(BytesEnd::new("w:r")))?;
3698        Ok(())
3699    }
3700}
3701
3702fn comment_reference_has_payload_attributes(element: &BytesStart<'_>, prefixes: &[String]) -> bool {
3703    let name = element.name();
3704    let own_binding = qualified_name_prefix(name.as_ref())
3705        .map_or_else(|| "xmlns".to_owned(), |prefix| format!("xmlns:{prefix}"));
3706    element.attributes().any(|attribute| {
3707        attribute.ok().is_none_or(|attribute| {
3708            if attribute_in_namespace(
3709                attribute.key.as_ref(),
3710                b"id",
3711                crate::namespace::W_NS,
3712                prefixes,
3713            ) {
3714                return false;
3715            }
3716            // The element's own Word alias declaration is structural, not opaque
3717            // payload. Every other declaration or attribute retains its source.
3718            !(attribute.key.as_ref() == own_binding.as_bytes()
3719                && attribute
3720                    .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())
3721                    .is_ok_and(|value| value == crate::namespace::W_NS))
3722        })
3723    })
3724}
3725
3726fn close_comment_reference_source(raw: &[u8], prefixes: &[String]) -> Result<Vec<u8>> {
3727    let mut bindings = prefixes
3728        .iter()
3729        .filter_map(|binding| {
3730            binding
3731                .strip_prefix('\0')
3732                .and_then(|binding| binding.split_once('\0'))
3733                .map(|(prefix, uri)| (prefix.to_owned(), uri.to_owned()))
3734        })
3735        .collect::<Vec<_>>();
3736    for prefix in prefixes.iter().filter(|prefix| !prefix.starts_with('\0')) {
3737        if !bindings.iter().any(|(bound, _)| bound == prefix) {
3738            bindings.push((prefix.clone(), crate::namespace::W_NS.to_owned()));
3739        }
3740    }
3741    raw_with_external_bindings(raw, &bindings)
3742}
3743
3744fn retained_comment_reference_id(raw: &[u8]) -> Option<i32> {
3745    let mut reader = quick_xml::reader::NsReader::from_reader(raw);
3746    let (ns, event) = reader.read_resolved_event().ok()?;
3747    if !matches!(ns, ResolveResult::Bound(namespace) if namespace.as_ref() == crate::namespace::W_NS.as_bytes())
3748    {
3749        return None;
3750    }
3751    let element = match event {
3752        Event::Start(element) | Event::Empty(element) => element,
3753        _ => return None,
3754    };
3755    if element.local_name().as_ref() != b"commentReference" {
3756        return None;
3757    }
3758    element.attributes().find_map(|attr| {
3759        let attr = attr.ok()?;
3760        let (ns, local) = reader.resolver().resolve_attribute(attr.key);
3761        (matches!(ns, ResolveResult::Bound(namespace) if namespace.as_ref() == crate::namespace::W_NS.as_bytes())
3762            && local.as_ref() == b"id").then(|| attr.decoded_and_normalized_value(XmlVersion::Implicit1_0, reader.decoder()).ok()?.parse().ok()).flatten()
3763    })
3764}
3765
3766fn run_xml_root(
3767    run: &CT_R,
3768    foreign_word_namespace: Option<&str>,
3769    localized: &mut Vec<(String, String)>,
3770) -> Result<(BytesStart<'static>, Vec<(String, String)>)> {
3771    localized.clear();
3772    let mut root = BytesStart::new("w:r");
3773    if foreign_word_namespace.is_some() {
3774        root.push_attribute(("xmlns:w", crate::namespace::W_NS));
3775    }
3776    let mut bindings = Vec::new();
3777    let mut lexical = Vec::new();
3778    let mut local = HashSet::new();
3779    let mut attributes = Vec::new();
3780    for (position, raw) in run.extra_xml_positions.iter().zip(&run.extra_xml) {
3781        #[cfg(test)]
3782        RUN_METADATA_VISITS.with(|count| count.set(count.get() + 1));
3783        if CT_R::raw_child_is_root_attributes(*position) {
3784            let mut reader = Reader::from_reader(raw.as_slice());
3785            let Event::Empty(record) = reader.read_event()? else {
3786                return Err(OxmlError::InvalidValue(
3787                    "invalid run root attributes".into(),
3788                ));
3789            };
3790            let declarations = raw_namespace_declarations(&record)?;
3791            let source_local = *position & RAW_RUN_LOCAL_BINDINGS_FLAG != 0;
3792            for binding in declarations {
3793                if source_local
3794                    && bindings
3795                        .iter()
3796                        .any(|(prefix, uri)| prefix == &binding.0 && uri != &binding.1)
3797                {
3798                    return Err(OxmlError::InvalidValue(
3799                        "inconsistent source-local run namespace binding".into(),
3800                    ));
3801                }
3802                if !bindings.contains(&binding) {
3803                    bindings.push(binding);
3804                }
3805            }
3806            for attribute in record.attributes() {
3807                let attribute = attribute?;
3808                let name = attribute.key.as_ref();
3809                let prefix = if name == b"xmlns" {
3810                    Some("")
3811                } else {
3812                    name.strip_prefix(b"xmlns:")
3813                        .map(std::str::from_utf8)
3814                        .transpose()?
3815                };
3816                if let Some(prefix) = prefix {
3817                    let value = std::str::from_utf8(&attribute.value)?.to_owned();
3818                    if let Some((_, spelling)) =
3819                        lexical.iter_mut().find(|(existing, _)| existing == prefix)
3820                    {
3821                        if source_local {
3822                            *spelling = value;
3823                        }
3824                    } else {
3825                        lexical.push((prefix.to_owned(), value));
3826                    }
3827                    if source_local {
3828                        local.insert(prefix.to_owned());
3829                    }
3830                }
3831            }
3832            let prefixes = word_prefixes_at(&record, &[])?;
3833            if let Some(record) = capture_root_attribute_record(&record, &prefixes)? {
3834                attributes.push(record);
3835            }
3836        }
3837    }
3838    let mut wp_writer = false;
3839    let mut r_writer = false;
3840    for drawing in run.content.iter().filter_map(|item| match item {
3841        RunContent::Drawing(drawing) => Some(drawing),
3842        _ => None,
3843    }) {
3844        if let Some(inline) = &drawing.inline
3845            && inline.raw_xml.is_none()
3846        {
3847            wp_writer = true;
3848            r_writer |= inline.chart_rel_id.is_none();
3849        }
3850        if let Some(anchor) = &drawing.anchor
3851            && anchor.raw_xml.is_none()
3852        {
3853            wp_writer = true;
3854            r_writer |= anchor.chart_rel_id.is_none();
3855        }
3856    }
3857    for (prefix, value) in &lexical {
3858        let generated = match prefix.as_str() {
3859            "w" => Some(crate::namespace::W_NS),
3860            "wp" if wp_writer => Some(WP_NS),
3861            "r" if r_writer => Some(R_NS),
3862            _ => None,
3863        };
3864        if let Some(uri) = generated
3865            && bindings
3866                .iter()
3867                .any(|(bound, actual)| bound == prefix && actual != uri)
3868        {
3869            localized.push((prefix.clone(), value.clone()));
3870            if prefix != "w" {
3871                root.push_attribute((format!("xmlns:{prefix}").as_str(), uri));
3872            }
3873        }
3874    }
3875    // Root aliases protect only the actual proven canonical target scopes above.
3876    for record in attributes {
3877        push_root_attribute_record(&mut root, &record, None)?;
3878    }
3879    let guarantees = ROOT_BINDINGS.with(Cell::get);
3880    for (prefix, value) in &lexical {
3881        if prefix == "w" || localized.iter().any(|(bound, _)| bound == prefix) {
3882            continue;
3883        }
3884        let guaranteed = !local.contains(prefix)
3885            && match prefix.as_str() {
3886                "r" => guarantees & ROOT_R_BINDING != 0 && value == R_NS,
3887                "mc" => guarantees & ROOT_MC_BINDING != 0 && value == MC_NS,
3888                "wp" => guarantees & ROOT_WP_BINDING != 0 && value == WP_NS,
3889                _ => false,
3890            };
3891        if guaranteed {
3892            continue;
3893        }
3894        let name = if prefix.is_empty() {
3895            "xmlns".to_owned()
3896        } else {
3897            format!("xmlns:{prefix}")
3898        };
3899        if !root.attributes().any(|attribute| {
3900            attribute.is_ok_and(|attribute| attribute.key.as_ref() == name.as_bytes())
3901        }) {
3902            root.push_attribute(quick_xml::events::attributes::Attribute {
3903                key: quick_xml::name::QName(name.as_bytes()),
3904                value: Cow::Borrowed(value.as_bytes()),
3905            });
3906        }
3907    }
3908    Ok((root, bindings))
3909}
3910
3911fn write_run_raw_source<W: std::io::Write>(
3912    writer: &mut Writer<W>,
3913    raw: &[u8],
3914    foreign_word_namespace: Option<&str>,
3915    bindings: &[(String, String)],
3916    localized: &[(String, String)],
3917) -> Result<()> {
3918    // QName values need every actual source fact during discovery, including
3919    // the attribute namespace itself. Only conflicting generated prefixes
3920    // need a declaration on the opaque subtree.
3921    let used = external_bindings_used_by_raw(raw, bindings)?;
3922    let needed = localized
3923        .iter()
3924        .filter(|(prefix, _)| used.iter().any(|(used, _)| used == prefix))
3925        .cloned()
3926        .collect::<Vec<_>>();
3927    // These are lexical source values, not newly authored namespace URIs.
3928    let closed = if needed.is_empty() {
3929        raw.to_vec()
3930    } else {
3931        raw_with_root_bindings(raw, &needed, true)?
3932    };
3933    write_raw_with_word_override(writer, &closed, foreign_word_namespace)
3934}
3935
3936fn write_run_raw_boundary<W: std::io::Write>(
3937    writer: &mut Writer<W>,
3938    run: &CT_R,
3939    boundary: usize,
3940    foreign_word_namespace: Option<&str>,
3941    raw_bindings: &[(String, String)],
3942    localized: &[(String, String)],
3943) -> Result<()> {
3944    for (position, raw) in run.extra_xml_positions.iter().zip(&run.extra_xml) {
3945        if !CT_R::raw_child_is_root_attributes(*position)
3946            && *position & RAW_COMMENT_REFERENCE_FLAG == 0
3947            && CT_R::raw_child_position(*position) == boundary
3948        {
3949            write_run_raw_source(writer, raw, foreign_word_namespace, raw_bindings, localized)?;
3950        }
3951    }
3952    Ok(())
3953}
3954
3955/// A hyperlink span that wraps a range of runs.
3956#[derive(Debug, Clone, PartialEq)]
3957pub struct HyperlinkSpan {
3958    /// The relationship ID for the hyperlink target.
3959    pub rel_id: Option<String>,
3960    /// Optional anchor within the document (for internal links).
3961    pub anchor: Option<String>,
3962    /// Optional user-facing hover text.
3963    pub tooltip: Option<String>,
3964    /// Optional location in the hyperlink target document.
3965    pub doc_location: Option<String>,
3966    /// Index of the first run in the hyperlink (inclusive).
3967    pub run_start: usize,
3968    /// Index of the last run in the hyperlink (exclusive).
3969    pub run_end: usize,
3970    /// Unmodeled owner attributes retained when the hyperlink came from XML.
3971    #[doc(hidden)]
3972    pub extra_attributes: Vec<(String, String)>,
3973    /// Raw children at `(relative run boundary, typed revisions before, XML)`.
3974    #[doc(hidden)]
3975    pub extra_xml: Vec<(usize, usize, Vec<u8>)>,
3976    /// Parent raw slot for a revision-only hyperlink preserved as one subtree.
3977    #[doc(hidden)]
3978    pub preserved_raw_before: Option<usize>,
3979}
3980
3981const HYPERLINK_REVISION_FLAG: usize = 1usize << (usize::BITS - 1);
3982
3983struct ParsedHyperlinkChildren {
3984    runs: Vec<CT_R>,
3985    run_sources: Vec<Option<Vec<u8>>>,
3986    revisions: Vec<(usize, CT_Revision)>,
3987    extra_xml: Vec<(usize, usize, Vec<u8>)>,
3988    bookmark_markers: Vec<BookmarkMarker>,
3989    projected_run_count: usize,
3990    tracked_run_count: usize,
3991}
3992
3993#[derive(Default)]
3994struct AcceptedBookmarkProjection {
3995    markers: Vec<BookmarkMarker>,
3996    projected_run_count: usize,
3997    tracked_run_count: usize,
3998}
3999
4000/// A hyperlink represented by a complex field sequence rather than `w:hyperlink`.
4001#[derive(Debug, Clone, PartialEq, Eq)]
4002pub struct ComplexFieldHyperlink {
4003    pub run_start: usize,
4004    pub run_end: usize,
4005    pub target: String,
4006}
4007
4008type ParsedHyperlinkAttributes = (
4009    Option<String>,
4010    Option<String>,
4011    Option<String>,
4012    Option<String>,
4013    Vec<(String, String)>,
4014);
4015
4016pub(crate) fn hyperlink_revision_slot(hyperlink_index: usize) -> usize {
4017    HYPERLINK_REVISION_FLAG | hyperlink_index
4018}
4019
4020#[doc(hidden)]
4021pub fn hyperlink_revision_index(slot: usize) -> Option<usize> {
4022    (slot & HYPERLINK_REVISION_FLAG != 0).then_some(slot & !HYPERLINK_REVISION_FLAG)
4023}
4024
4025fn field_run(field: Field, properties: Option<CT_RPr>) -> CT_R {
4026    CT_R {
4027        properties,
4028        content: vec![RunContent::Field(field)],
4029        extra_xml: Vec::new(),
4030        extra_xml_positions: Vec::new(),
4031        alt_drawings: Vec::new(),
4032    }
4033}
4034
4035pub(crate) fn parse_run_raw(raw: &[u8], word_prefixes: &[String]) -> Result<CT_R> {
4036    let mut reader = Reader::from_reader(raw);
4037    reader.config_mut().trim_text(false);
4038    let mut buffer = Vec::new();
4039    loop {
4040        match reader.read_event_into(&mut buffer)? {
4041            Event::Start(start) if matches_local_name(start.name().as_ref(), b"r") => {
4042                let prefixes = word_prefixes_at(&start, word_prefixes)?;
4043                return CT_R::from_xml_with_prefixes_and_root(&mut reader, &prefixes, Some(&start));
4044            }
4045            Event::Eof => {
4046                return Err(OxmlError::MissingElement("w:r".to_owned()));
4047            }
4048            _ => {}
4049        }
4050        buffer.clear();
4051    }
4052}
4053
4054fn parse_simple_field(raw: &[u8], word_prefixes: &[String]) -> Result<Option<Field>> {
4055    let mut reader = Reader::from_reader(raw);
4056    reader.config_mut().trim_text(false);
4057    let mut buffer = Vec::new();
4058    let (instruction, dirty, prefixes) = loop {
4059        match reader.read_event_into(&mut buffer)? {
4060            Event::Start(start) if matches_local_name(start.name().as_ref(), b"fldSimple") => {
4061                let prefixes = word_prefixes_at(&start, word_prefixes)?;
4062                if !is_word_element(start.name().as_ref(), b"fldSimple", &prefixes) {
4063                    return Ok(None);
4064                }
4065                let Some(instruction) = optional_word_attribute(&start, b"instr", &prefixes) else {
4066                    return Ok(None);
4067                };
4068                let dirty = optional_word_attribute(&start, b"dirty", &prefixes)
4069                    .and_then(|value| parse_field_bool(&value));
4070                break (instruction, dirty, prefixes);
4071            }
4072            Event::Eof => return Ok(None),
4073            _ => {}
4074        }
4075        buffer.clear();
4076    };
4077
4078    let result_start = reader.buffer_position() as usize;
4079    let mut result_end = result_start;
4080    let mut cached_result = String::new();
4081    let mut cached_segments = Vec::new();
4082    let mut result_run_index = 0usize;
4083    let mut result_runs = Vec::new();
4084    loop {
4085        buffer.clear();
4086        let before = reader.buffer_position() as usize;
4087        match reader.read_event_into(&mut buffer)? {
4088            Event::Start(start) => {
4089                let child_prefixes = word_prefixes_at(&start, &prefixes)?;
4090                if is_word_element(start.name().as_ref(), b"r", &child_prefixes) {
4091                    let run_raw = capture_element(&mut reader, &start)?;
4092                    let run = parse_run_raw(&run_raw, &child_prefixes)?;
4093                    if let Some(text) = simple_result_run_display(&run_raw, &child_prefixes)? {
4094                        cached_result.push_str(&text);
4095                        cached_segments.push(CachedDisplaySegment {
4096                            run_index: result_run_index,
4097                            text,
4098                            properties: run.properties.clone(),
4099                        });
4100                    }
4101                    result_runs.push(run);
4102                    result_run_index += 1;
4103                } else {
4104                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
4105                }
4106            }
4107            Event::End(end) if matches_local_name(end.name().as_ref(), b"fldSimple") => {
4108                result_end = before;
4109                break;
4110            }
4111            Event::Eof => break,
4112            _ => {}
4113        }
4114    }
4115
4116    let instruction = parse_field_instruction(&instruction);
4117    if instruction.name.is_empty() {
4118        return Ok(None);
4119    }
4120    let mut field = Field::parsed(
4121        instruction,
4122        cached_result,
4123        cached_segments,
4124        dirty,
4125        FieldForm::Simple,
4126        raw.to_vec(),
4127        prefixes.clone(),
4128    );
4129    if cached_runs_need_typed_projection(&result_runs)
4130        && result_runs
4131            .iter()
4132            .map(cached_run_display)
4133            .collect::<String>()
4134            == field.cached_result
4135    {
4136        field.simple_cached_runs = Some(result_runs);
4137    }
4138    let result_xml = &raw[result_start..result_end];
4139    // The typed pass still decides namespace and structure. This cheap absence
4140    // test avoids reparsing ordinary text-only caches.
4141    let possible_nested = result_xml.windows(7).any(|bytes| bytes == b"fldChar")
4142        || result_xml.windows(9).any(|bytes| bytes == b"fldSimple")
4143        || source_has_note_reference_name(result_xml);
4144    if possible_nested {
4145        // Reuse a Word prefix from the effective producer scope. Introducing
4146        // a canonical w declaration here would reclassify foreign w children.
4147        let prefix = prefixes
4148            .iter()
4149            .find(|prefix| !prefix.starts_with('\0'))
4150            .ok_or_else(|| OxmlError::InvalidValue("simple field has no Word prefix".to_owned()))?;
4151        let name = if prefix.is_empty() {
4152            "p".to_owned()
4153        } else {
4154            format!("{prefix}:p")
4155        };
4156        let mut cache_xml = format!("<{name}>").into_bytes();
4157        cache_xml.extend_from_slice(&raw[result_start..result_end]);
4158        cache_xml.extend_from_slice(format!("</{name}>").as_bytes());
4159        let mut cache_reader = Reader::from_reader(cache_xml.as_slice());
4160        let mut cache_buffer = Vec::new();
4161        let Event::Start(cache_root) = cache_reader.read_event_into(&mut cache_buffer)? else {
4162            unreachable!("synthetic cache root");
4163        };
4164        let cache_prefixes = word_prefixes_at(&cache_root, &prefixes)?;
4165        let paragraph = CT_P::from_xml_with_prefixes_and_root(
4166            &mut cache_reader,
4167            &cache_prefixes,
4168            Some(&cache_root),
4169        )?;
4170        let runs = paragraph.runs().into_iter().cloned().collect::<Vec<_>>();
4171        if cached_runs_need_typed_projection(&runs)
4172            || runs.iter().flat_map(|run| &run.content).any(|content| {
4173                matches!(
4174                    content,
4175                    RunContent::Field(_)
4176                        | RunContent::FootnoteRef { .. }
4177                        | RunContent::EndnoteRef { .. }
4178                        | RunContent::CommentReference { .. }
4179                )
4180            })
4181        {
4182            field.cached_result = runs.iter().map(cached_run_display).collect();
4183            if let FieldSource::Parsed {
4184                original_cached_result,
4185                ..
4186            } = &mut field.source
4187            {
4188                *original_cached_result = field.cached_result.clone();
4189            }
4190            field.parsed_cached_comment_ranges = paragraph.comment_ranges.clone();
4191            field.simple_cached_runs = Some(runs);
4192        }
4193    }
4194    Ok(Some(field))
4195}
4196
4197/// Whether a preserved simple field is admitted by the complete typed parser.
4198#[doc(hidden)]
4199pub fn story_simple_field_is_typed(raw: &[u8], word_prefixes: &[String]) -> bool {
4200    parse_simple_field(raw, word_prefixes)
4201        .ok()
4202        .flatten()
4203        .is_some()
4204}
4205
4206fn simple_result_run_display(raw: &[u8], word_prefixes: &[String]) -> Result<Option<String>> {
4207    let mut reader = Reader::from_reader(raw);
4208    reader.config_mut().trim_text(false);
4209    let mut buffer = Vec::new();
4210    let run_prefixes = loop {
4211        match reader.read_event_into(&mut buffer)? {
4212            Event::Start(start) if matches_local_name(start.name().as_ref(), b"r") => {
4213                break word_prefixes_at(&start, word_prefixes)?;
4214            }
4215            Event::Eof => return Ok(None),
4216            _ => {}
4217        }
4218        buffer.clear();
4219    };
4220    let mut display = None::<String>;
4221    loop {
4222        buffer.clear();
4223        match reader.read_event_into(&mut buffer)? {
4224            Event::Start(start) => {
4225                let prefixes = word_prefixes_at(&start, &run_prefixes)?;
4226                if is_word_element(start.name().as_ref(), b"t", &prefixes)
4227                    || is_word_element(start.name().as_ref(), b"delText", &prefixes)
4228                {
4229                    let text = reader
4230                        .read_text(start.name())
4231                        .map(|text| crate::xml_text::decode_escaped(&text))
4232                        .unwrap_or_default();
4233                    display.get_or_insert_default().push_str(&text);
4234                } else if is_word_element(start.name().as_ref(), b"tab", &prefixes) {
4235                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
4236                    display.get_or_insert_default().push('\t');
4237                } else if is_word_element(start.name().as_ref(), b"br", &prefixes) {
4238                    let marker = match field_break_type(&start, &prefixes) {
4239                        BreakType::Line => '\n',
4240                        BreakType::Page => '\u{000c}',
4241                        BreakType::Column => '\u{000b}',
4242                    };
4243                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
4244                    display.get_or_insert_default().push(marker);
4245                } else {
4246                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
4247                }
4248            }
4249            Event::Empty(element) => {
4250                let prefixes = word_prefixes_at(&element, &run_prefixes)?;
4251                if is_word_element(element.name().as_ref(), b"t", &prefixes)
4252                    || is_word_element(element.name().as_ref(), b"delText", &prefixes)
4253                {
4254                    display.get_or_insert_default();
4255                } else if is_word_element(element.name().as_ref(), b"tab", &prefixes) {
4256                    display.get_or_insert_default().push('\t');
4257                } else if is_word_element(element.name().as_ref(), b"br", &prefixes) {
4258                    let marker = match field_break_type(&element, &prefixes) {
4259                        BreakType::Line => '\n',
4260                        BreakType::Page => '\u{000c}',
4261                        BreakType::Column => '\u{000b}',
4262                    };
4263                    display.get_or_insert_default().push(marker);
4264                } else if matches!(
4265                    parse_typed_special_run_child(&element, &prefixes)?,
4266                    Some(RunContent::SpecialCharacter(
4267                        SpecialCharacter::CarriageReturn
4268                    ))
4269                ) {
4270                    display.get_or_insert_default().push('\n');
4271                }
4272            }
4273            Event::End(end) if matches_local_name(end.name().as_ref(), b"r") => {
4274                return Ok(display);
4275            }
4276            Event::Eof => return Ok(display),
4277            _ => {}
4278        }
4279    }
4280}
4281
4282fn parse_field_bool(value: &str) -> Option<bool> {
4283    match value {
4284        "1" | "true" | "on" => Some(true),
4285        "0" | "false" | "off" => Some(false),
4286        _ => None,
4287    }
4288}
4289
4290#[derive(Default)]
4291struct LegacyFormProjection {
4292    ff_data_last_slot: Option<usize>,
4293    kind_last_slot: Option<usize>,
4294    name: Option<String>,
4295    name_seen: bool,
4296    enabled: Option<bool>,
4297    enabled_seen: bool,
4298    calculate_on_exit: Option<bool>,
4299    calculate_on_exit_seen: bool,
4300    entry_macro_seen: bool,
4301    exit_macro_seen: bool,
4302    help_text_seen: bool,
4303    status_text_seen: bool,
4304    kind: Option<LegacyFormFieldKind>,
4305    text_type_seen: bool,
4306    text_format_seen: bool,
4307    checkbox_size_seen: bool,
4308    default_text: Option<String>,
4309    default_text_seen: bool,
4310    checked: Option<bool>,
4311    checked_seen: bool,
4312    check_default: Option<bool>,
4313    check_default_seen: bool,
4314    selected_index: Option<usize>,
4315    selected_index_seen: bool,
4316    selected_default: Option<usize>,
4317    selected_default_seen: bool,
4318    choices: Vec<String>,
4319    max_length: Option<usize>,
4320    max_length_seen: bool,
4321}
4322
4323fn parse_legacy_form_data(
4324    raw: &[u8],
4325    word_prefixes: &[String],
4326    instruction_name: &str,
4327    cached_result: &str,
4328) -> Result<Option<LegacyFormFieldData>> {
4329    let mut reader = Reader::from_reader(raw);
4330    reader.config_mut().trim_text(false);
4331    let mut buffer = Vec::new();
4332    let mut stack = Vec::<Option<String>>::new();
4333    let mut prefixes = word_prefixes.to_vec();
4334    let mut prefix_scopes = Vec::new();
4335    let mut begin_depth = None;
4336    let mut ff_data_depth = None;
4337    let mut ff_data_count = 0usize;
4338    let mut kind_depth = None;
4339    let mut leaf_depth = None;
4340    let mut projection = LegacyFormProjection::default();
4341
4342    loop {
4343        match reader.read_event_into(&mut buffer)? {
4344            Event::Start(element) => {
4345                if leaf_depth.is_some() {
4346                    return Err(OxmlError::InvalidValue(
4347                        "legacy form leaf property contains an element".to_owned(),
4348                    ));
4349                }
4350                let local_prefixes = word_prefixes_at(&element, &prefixes)?;
4351                let word = is_word_element(
4352                    element.name().as_ref(),
4353                    local_name(element.name().as_ref()),
4354                    &local_prefixes,
4355                );
4356                let local = word.then(|| {
4357                    String::from_utf8_lossy(local_name(element.name().as_ref())).into_owned()
4358                });
4359                stack.push(local.clone());
4360                prefix_scopes.push(std::mem::replace(&mut prefixes, local_prefixes));
4361                let depth = stack.len() - 1;
4362                if local.as_deref() == Some("fldChar")
4363                    && begin_depth.is_none()
4364                    && optional_word_attribute(&element, b"fldCharType", &prefixes).as_deref()
4365                        == Some("begin")
4366                {
4367                    begin_depth = Some(depth);
4368                } else if local.as_deref() == Some("ffData") && begin_depth == depth.checked_sub(1)
4369                {
4370                    ff_data_count += 1;
4371                    if ff_data_count > 1 {
4372                        return Err(OxmlError::InvalidValue(
4373                            "duplicate direct w:ffData owner".to_owned(),
4374                        ));
4375                    }
4376                    ff_data_depth = Some(depth);
4377                } else if let Some(ff_depth) = ff_data_depth {
4378                    leaf_depth = project_legacy_form_element(
4379                        &element,
4380                        local.as_deref(),
4381                        depth,
4382                        ff_depth,
4383                        &prefixes,
4384                        &mut kind_depth,
4385                        &mut projection,
4386                    )?
4387                    .then_some(depth);
4388                }
4389            }
4390            Event::Empty(element) => {
4391                if leaf_depth.is_some() {
4392                    return Err(OxmlError::InvalidValue(
4393                        "legacy form leaf property contains an element".to_owned(),
4394                    ));
4395                }
4396                let local_prefixes = word_prefixes_at(&element, &prefixes)?;
4397                let word = is_word_element(
4398                    element.name().as_ref(),
4399                    local_name(element.name().as_ref()),
4400                    &local_prefixes,
4401                );
4402                let local = word.then(|| {
4403                    String::from_utf8_lossy(local_name(element.name().as_ref())).into_owned()
4404                });
4405                let depth = stack.len();
4406                if local.as_deref() == Some("fldChar")
4407                    && begin_depth.is_none()
4408                    && optional_word_attribute(&element, b"fldCharType", &local_prefixes).as_deref()
4409                        == Some("begin")
4410                {
4411                    begin_depth = Some(depth);
4412                } else if local.as_deref() == Some("ffData") && begin_depth == depth.checked_sub(1)
4413                {
4414                    ff_data_count += 1;
4415                    if ff_data_count > 1 {
4416                        return Err(OxmlError::InvalidValue(
4417                            "duplicate direct w:ffData owner".to_owned(),
4418                        ));
4419                    }
4420                } else if let Some(ff_depth) = ff_data_depth {
4421                    project_legacy_form_element(
4422                        &element,
4423                        local.as_deref(),
4424                        depth,
4425                        ff_depth,
4426                        &local_prefixes,
4427                        &mut kind_depth,
4428                        &mut projection,
4429                    )?;
4430                    if depth == ff_depth + 1
4431                        && matches!(local.as_deref(), Some("checkBox" | "ddList" | "textInput"))
4432                    {
4433                        kind_depth = None;
4434                    }
4435                }
4436            }
4437            Event::End(_) => {
4438                let depth = stack.len().saturating_sub(1);
4439                if leaf_depth == Some(depth) {
4440                    leaf_depth = None;
4441                }
4442                if kind_depth == Some(depth) {
4443                    kind_depth = None;
4444                }
4445                if ff_data_depth == Some(depth) {
4446                    ff_data_depth = None;
4447                }
4448                let closes_begin = begin_depth == Some(depth);
4449                stack.pop();
4450                prefixes = prefix_scopes
4451                    .pop()
4452                    .unwrap_or_else(|| word_prefixes.to_vec());
4453                if closes_begin {
4454                    break;
4455                }
4456            }
4457            Event::Text(text)
4458                if (leaf_depth.is_some()
4459                    || is_legacy_form_element_only_container(
4460                        stack.len(),
4461                        ff_data_depth,
4462                        kind_depth,
4463                    ))
4464                    && text.iter().any(|byte| !byte.is_ascii_whitespace()) =>
4465            {
4466                return Err(OxmlError::InvalidValue(
4467                    "legacy form element-only content contains character data".to_owned(),
4468                ));
4469            }
4470            Event::CData(text)
4471                if leaf_depth.is_some()
4472                    || (is_legacy_form_element_only_container(
4473                        stack.len(),
4474                        ff_data_depth,
4475                        kind_depth,
4476                    ) && text.iter().any(|byte| !byte.is_ascii_whitespace())) =>
4477            {
4478                return Err(OxmlError::InvalidValue(
4479                    "legacy form element-only content contains character data".to_owned(),
4480                ));
4481            }
4482            Event::GeneralRef(reference) => {
4483                let in_element_only_container =
4484                    is_legacy_form_element_only_container(stack.len(), ff_data_depth, kind_depth);
4485                let non_whitespace = match reference.resolve_char_ref() {
4486                    Ok(Some(character)) => !character.is_ascii_whitespace(),
4487                    Ok(None) | Err(_) => true,
4488                };
4489                if leaf_depth.is_some() || (in_element_only_container && non_whitespace) {
4490                    return Err(OxmlError::InvalidValue(
4491                        "legacy form element-only content contains character data".to_owned(),
4492                    ));
4493                }
4494            }
4495            Event::Eof => break,
4496            _ => {}
4497        }
4498        buffer.clear();
4499    }
4500
4501    let Some(kind) = projection.kind else {
4502        if ff_data_count == 0 {
4503            return Ok(None);
4504        }
4505        return Err(OxmlError::MissingElement("w:ffData form kind".to_owned()));
4506    };
4507    let matches_instruction = matches!(
4508        (kind, instruction_name),
4509        (LegacyFormFieldKind::TextInput, "FORMTEXT")
4510            | (LegacyFormFieldKind::CheckBox, "FORMCHECKBOX")
4511            | (LegacyFormFieldKind::DropDownList, "FORMDROPDOWN")
4512    );
4513    if !matches_instruction {
4514        return Err(OxmlError::InvalidValue(
4515            "w:ffData kind contradicts its field instruction".to_owned(),
4516        ));
4517    }
4518    if kind == LegacyFormFieldKind::CheckBox && !projection.checkbox_size_seen {
4519        return Err(OxmlError::MissingElement(
4520            "w:checkBox requires w:size or w:sizeAuto".to_owned(),
4521        ));
4522    }
4523    let value = match kind {
4524        LegacyFormFieldKind::TextInput => LegacyFormFieldValue::Text(
4525            projection
4526                .default_text
4527                .unwrap_or_else(|| cached_result.to_owned()),
4528        ),
4529        LegacyFormFieldKind::CheckBox => LegacyFormFieldValue::Checked(
4530            projection
4531                .checked
4532                .or(projection.check_default)
4533                .unwrap_or(false),
4534        ),
4535        LegacyFormFieldKind::DropDownList => {
4536            let index = projection
4537                .selected_index
4538                .or(projection.selected_default)
4539                .unwrap_or(0);
4540            if projection.choices.get(index).is_none() {
4541                return Err(OxmlError::InvalidValue(
4542                    "w:ddList selection is outside its entry list".to_owned(),
4543                ));
4544            }
4545            LegacyFormFieldValue::SelectedIndex(index)
4546        }
4547    };
4548    Ok(Some(LegacyFormFieldData {
4549        name: projection.name,
4550        enabled: projection.enabled.unwrap_or(true),
4551        calculate_on_exit: projection.calculate_on_exit.unwrap_or(true),
4552        kind,
4553        value,
4554        choices: projection.choices,
4555        max_length: projection.max_length,
4556    }))
4557}
4558
4559fn project_legacy_form_element(
4560    element: &BytesStart<'_>,
4561    local: Option<&str>,
4562    depth: usize,
4563    ff_depth: usize,
4564    word_prefixes: &[String],
4565    kind_depth: &mut Option<usize>,
4566    projection: &mut LegacyFormProjection,
4567) -> Result<bool> {
4568    if let Some(local) = local
4569        && is_known_legacy_form_vocabulary(local)
4570    {
4571        let valid_position = if depth == ff_depth + 1 {
4572            legacy_form_ff_data_slot(Some(local)).is_some()
4573        } else if kind_depth.is_some_and(|kind| depth == kind + 1) {
4574            projection
4575                .kind
4576                .is_some_and(|kind| legacy_form_kind_child_slot(kind, local).is_some())
4577        } else {
4578            false
4579        };
4580        if !valid_position {
4581            return Err(OxmlError::InvalidValue(format!(
4582                "w:{local} is at the wrong legacy form container level"
4583            )));
4584        }
4585    }
4586    let is_leaf = (depth == ff_depth + 1
4587        && matches!(
4588            local,
4589            Some(
4590                "name"
4591                    | "enabled"
4592                    | "calcOnExit"
4593                    | "entryMacro"
4594                    | "exitMacro"
4595                    | "helpText"
4596                    | "statusText"
4597            )
4598        ))
4599        || (kind_depth.is_some_and(|kind| depth == kind + 1)
4600            && matches!(
4601                local,
4602                Some(
4603                    "type"
4604                        | "default"
4605                        | "maxLength"
4606                        | "format"
4607                        | "size"
4608                        | "sizeAuto"
4609                        | "checked"
4610                        | "result"
4611                        | "listEntry"
4612                )
4613            ));
4614    if depth == ff_depth + 1 {
4615        if let Some(slot) = legacy_form_ff_data_slot(local) {
4616            validate_legacy_form_child_order(&mut projection.ff_data_last_slot, slot, "w:ffData")?;
4617        }
4618        match local {
4619            Some("name") => {
4620                mark_legacy_form_singleton(&mut projection.name_seen, "w:name")?;
4621                projection.name = Some(required_bounded_legacy_form_value(
4622                    element,
4623                    word_prefixes,
4624                    "w:name",
4625                    20,
4626                )?);
4627            }
4628            Some("enabled") => {
4629                mark_legacy_form_singleton(&mut projection.enabled_seen, "w:enabled")?;
4630                projection.enabled = Some(legacy_form_boolean_value(
4631                    element,
4632                    word_prefixes,
4633                    "w:enabled",
4634                )?);
4635            }
4636            Some("calcOnExit") => {
4637                mark_legacy_form_singleton(&mut projection.calculate_on_exit_seen, "w:calcOnExit")?;
4638                projection.calculate_on_exit = Some(legacy_form_boolean_value(
4639                    element,
4640                    word_prefixes,
4641                    "w:calcOnExit",
4642                )?);
4643            }
4644            Some("entryMacro") => {
4645                mark_legacy_form_singleton(&mut projection.entry_macro_seen, "w:entryMacro")?;
4646                required_bounded_legacy_form_value(element, word_prefixes, "w:entryMacro", 33)?;
4647            }
4648            Some("exitMacro") => {
4649                mark_legacy_form_singleton(&mut projection.exit_macro_seen, "w:exitMacro")?;
4650                required_bounded_legacy_form_value(element, word_prefixes, "w:exitMacro", 33)?;
4651            }
4652            Some("helpText") => {
4653                mark_legacy_form_singleton(&mut projection.help_text_seen, "w:helpText")?;
4654                validate_legacy_info_text(element, word_prefixes, "w:helpText", 255)?;
4655            }
4656            Some("statusText") => {
4657                mark_legacy_form_singleton(&mut projection.status_text_seen, "w:statusText")?;
4658                validate_legacy_info_text(element, word_prefixes, "w:statusText", 140)?;
4659            }
4660            Some("textInput") => set_legacy_form_kind(
4661                projection,
4662                LegacyFormFieldKind::TextInput,
4663                depth,
4664                kind_depth,
4665            )?,
4666            Some("checkBox") => {
4667                set_legacy_form_kind(projection, LegacyFormFieldKind::CheckBox, depth, kind_depth)?
4668            }
4669            Some("ddList") => set_legacy_form_kind(
4670                projection,
4671                LegacyFormFieldKind::DropDownList,
4672                depth,
4673                kind_depth,
4674            )?,
4675            _ => {}
4676        }
4677    } else if kind_depth.is_some_and(|kind| depth == kind + 1) {
4678        if let (Some(kind), Some(local)) = (projection.kind, local) {
4679            if let Some(slot) = legacy_form_kind_child_slot(kind, local) {
4680                validate_legacy_form_child_order(
4681                    &mut projection.kind_last_slot,
4682                    slot,
4683                    "legacy form kind",
4684                )?;
4685            } else if is_known_legacy_form_kind_child(local) {
4686                return Err(OxmlError::InvalidValue(format!(
4687                    "w:{local} is not valid for this legacy form kind"
4688                )));
4689            }
4690        }
4691        match (projection.kind, local) {
4692            (Some(LegacyFormFieldKind::TextInput), Some("type")) => {
4693                mark_legacy_form_singleton(&mut projection.text_type_seen, "w:textInput/w:type")?;
4694                let value =
4695                    required_legacy_form_value(element, word_prefixes, "w:textInput/w:type")?;
4696                if !matches!(
4697                    value.as_str(),
4698                    "regular" | "number" | "date" | "currentTime" | "currentDate" | "calculated"
4699                ) {
4700                    return Err(OxmlError::InvalidValue(
4701                        "invalid w:textInput/w:type token".to_owned(),
4702                    ));
4703                }
4704            }
4705            (Some(LegacyFormFieldKind::TextInput), Some("default")) => {
4706                mark_legacy_form_singleton(
4707                    &mut projection.default_text_seen,
4708                    "w:textInput/w:default",
4709                )?;
4710                projection.default_text = Some(required_bounded_legacy_form_value(
4711                    element,
4712                    word_prefixes,
4713                    "w:textInput/w:default",
4714                    255,
4715                )?);
4716            }
4717            (Some(LegacyFormFieldKind::TextInput), Some("maxLength")) => {
4718                mark_legacy_form_singleton(
4719                    &mut projection.max_length_seen,
4720                    "w:textInput/w:maxLength",
4721                )?;
4722                let value =
4723                    legacy_form_usize_value(element, word_prefixes, "w:textInput/w:maxLength")?;
4724                if !(1..=32_767).contains(&value) {
4725                    return Err(OxmlError::InvalidValue(
4726                        "w:textInput/w:maxLength must be between 1 and 32767".to_owned(),
4727                    ));
4728                }
4729                projection.max_length = Some(value);
4730            }
4731            (Some(LegacyFormFieldKind::TextInput), Some("format")) => {
4732                mark_legacy_form_singleton(
4733                    &mut projection.text_format_seen,
4734                    "w:textInput/w:format",
4735                )?;
4736                required_bounded_legacy_form_value(
4737                    element,
4738                    word_prefixes,
4739                    "w:textInput/w:format",
4740                    64,
4741                )?;
4742            }
4743            (Some(LegacyFormFieldKind::CheckBox), Some("size")) => {
4744                mark_legacy_form_singleton(
4745                    &mut projection.checkbox_size_seen,
4746                    "w:checkBox size choice",
4747                )?;
4748                legacy_form_hps_measure_value(element, word_prefixes, "w:checkBox/w:size")?;
4749            }
4750            (Some(LegacyFormFieldKind::CheckBox), Some("sizeAuto")) => {
4751                mark_legacy_form_singleton(
4752                    &mut projection.checkbox_size_seen,
4753                    "w:checkBox size choice",
4754                )?;
4755                legacy_form_boolean_value(element, word_prefixes, "w:checkBox/w:sizeAuto")?;
4756            }
4757            (Some(LegacyFormFieldKind::CheckBox), Some("checked")) => {
4758                mark_legacy_form_singleton(&mut projection.checked_seen, "w:checkBox/w:checked")?;
4759                projection.checked = Some(legacy_form_boolean_value(
4760                    element,
4761                    word_prefixes,
4762                    "w:checkBox/w:checked",
4763                )?);
4764            }
4765            (Some(LegacyFormFieldKind::CheckBox), Some("default")) => {
4766                mark_legacy_form_singleton(
4767                    &mut projection.check_default_seen,
4768                    "w:checkBox/w:default",
4769                )?;
4770                projection.check_default = Some(legacy_form_boolean_value(
4771                    element,
4772                    word_prefixes,
4773                    "w:checkBox/w:default",
4774                )?);
4775            }
4776            (Some(LegacyFormFieldKind::DropDownList), Some("result")) => {
4777                mark_legacy_form_singleton(
4778                    &mut projection.selected_index_seen,
4779                    "w:ddList/w:result",
4780                )?;
4781                projection.selected_index = Some(legacy_form_usize_value(
4782                    element,
4783                    word_prefixes,
4784                    "w:ddList/w:result",
4785                )?);
4786            }
4787            (Some(LegacyFormFieldKind::DropDownList), Some("default")) => {
4788                mark_legacy_form_singleton(
4789                    &mut projection.selected_default_seen,
4790                    "w:ddList/w:default",
4791                )?;
4792                let value = legacy_form_usize_value(element, word_prefixes, "w:ddList/w:default")?;
4793                if value > 24 {
4794                    return Err(OxmlError::InvalidValue(
4795                        "w:ddList/w:default must be between 0 and 24".to_owned(),
4796                    ));
4797                }
4798                projection.selected_default = Some(value);
4799            }
4800            (Some(LegacyFormFieldKind::DropDownList), Some("listEntry")) => {
4801                if projection.choices.len() == 25 {
4802                    return Err(OxmlError::InvalidValue(
4803                        "w:ddList contains more than 25 list entries".to_owned(),
4804                    ));
4805                }
4806                projection.choices.push(required_bounded_legacy_form_value(
4807                    element,
4808                    word_prefixes,
4809                    "w:ddList/w:listEntry",
4810                    255,
4811                )?);
4812            }
4813            _ => {}
4814        }
4815    }
4816    Ok(is_leaf)
4817}
4818
4819fn legacy_form_ff_data_slot(local: Option<&str>) -> Option<usize> {
4820    match local? {
4821        "name" => Some(0),
4822        "enabled" => Some(1),
4823        "calcOnExit" => Some(2),
4824        "entryMacro" => Some(3),
4825        "exitMacro" => Some(4),
4826        "helpText" => Some(5),
4827        "statusText" => Some(6),
4828        "checkBox" | "ddList" | "textInput" => Some(7),
4829        _ => None,
4830    }
4831}
4832
4833fn legacy_form_kind_child_slot(kind: LegacyFormFieldKind, local: &str) -> Option<usize> {
4834    match kind {
4835        LegacyFormFieldKind::TextInput => match local {
4836            "type" => Some(0),
4837            "default" => Some(1),
4838            "maxLength" => Some(2),
4839            "format" => Some(3),
4840            _ => None,
4841        },
4842        LegacyFormFieldKind::CheckBox => match local {
4843            "size" | "sizeAuto" => Some(0),
4844            "default" => Some(1),
4845            "checked" => Some(2),
4846            _ => None,
4847        },
4848        LegacyFormFieldKind::DropDownList => match local {
4849            "result" => Some(0),
4850            "default" => Some(1),
4851            "listEntry" => Some(2),
4852            _ => None,
4853        },
4854    }
4855}
4856
4857fn is_known_legacy_form_kind_child(local: &str) -> bool {
4858    matches!(
4859        local,
4860        "type"
4861            | "default"
4862            | "maxLength"
4863            | "format"
4864            | "size"
4865            | "sizeAuto"
4866            | "checked"
4867            | "result"
4868            | "listEntry"
4869    )
4870}
4871
4872fn is_known_legacy_form_vocabulary(local: &str) -> bool {
4873    local == "ffData"
4874        || legacy_form_ff_data_slot(Some(local)).is_some()
4875        || is_known_legacy_form_kind_child(local)
4876}
4877
4878fn is_legacy_form_element_only_container(
4879    stack_len: usize,
4880    ff_data_depth: Option<usize>,
4881    kind_depth: Option<usize>,
4882) -> bool {
4883    ff_data_depth.is_some_and(|depth| stack_len == depth + 1)
4884        || kind_depth.is_some_and(|depth| stack_len == depth + 1)
4885}
4886
4887fn validate_legacy_form_child_order(
4888    last_slot: &mut Option<usize>,
4889    slot: usize,
4890    owner: &str,
4891) -> Result<()> {
4892    if last_slot.is_some_and(|last| slot < last) {
4893        return Err(OxmlError::InvalidValue(format!(
4894            "out-of-order modeled child in {owner}"
4895        )));
4896    }
4897    *last_slot = Some(slot);
4898    Ok(())
4899}
4900
4901fn mark_legacy_form_singleton(seen: &mut bool, name: &str) -> Result<()> {
4902    if std::mem::replace(seen, true) {
4903        return Err(OxmlError::InvalidValue(format!(
4904            "duplicate legacy form singleton {name}"
4905        )));
4906    }
4907    Ok(())
4908}
4909
4910fn required_legacy_form_value(
4911    element: &BytesStart<'_>,
4912    word_prefixes: &[String],
4913    name: &str,
4914) -> Result<String> {
4915    legacy_form_value_attribute(element, word_prefixes)?
4916        .ok_or_else(|| OxmlError::MissingElement(format!("{name} w:val attribute")))
4917}
4918
4919fn required_bounded_legacy_form_value(
4920    element: &BytesStart<'_>,
4921    word_prefixes: &[String],
4922    name: &str,
4923    max_characters: usize,
4924) -> Result<String> {
4925    let value = required_legacy_form_value(element, word_prefixes, name)?;
4926    if value.chars().count() > max_characters {
4927        return Err(OxmlError::InvalidValue(format!(
4928            "{name} exceeds {max_characters} characters"
4929        )));
4930    }
4931    Ok(value)
4932}
4933
4934fn legacy_form_boolean_value(
4935    element: &BytesStart<'_>,
4936    word_prefixes: &[String],
4937    name: &str,
4938) -> Result<bool> {
4939    let Some(value) = legacy_form_value_attribute(element, word_prefixes)? else {
4940        return Ok(true);
4941    };
4942    parse_field_bool(&value)
4943        .ok_or_else(|| OxmlError::InvalidValue(format!("invalid {name} boolean token")))
4944}
4945
4946fn legacy_form_value_attribute(
4947    element: &BytesStart<'_>,
4948    word_prefixes: &[String],
4949) -> Result<Option<String>> {
4950    legacy_form_word_attribute(element, word_prefixes, b"val", "w:val")
4951}
4952
4953fn legacy_form_word_attribute(
4954    element: &BytesStart<'_>,
4955    word_prefixes: &[String],
4956    wanted: &[u8],
4957    name: &str,
4958) -> Result<Option<String>> {
4959    let mut value = None;
4960    for attribute in element.attributes() {
4961        let attribute = attribute?;
4962        let key = attribute.key.as_ref();
4963        let Some(separator) = key.iter().position(|byte| *byte == b':') else {
4964            continue;
4965        };
4966        if key.get(separator + 1..) == Some(wanted)
4967            && word_prefixes
4968                .iter()
4969                .any(|prefix| prefix.as_bytes() == &key[..separator])
4970        {
4971            if value.is_some() {
4972                return Err(OxmlError::InvalidValue(format!(
4973                    "duplicate legacy form {name} attribute"
4974                )));
4975            }
4976            let raw = std::str::from_utf8(&attribute.value)?;
4977            value = Some(
4978                quick_xml::escape::unescape(raw)
4979                    .map_err(|error| {
4980                        OxmlError::InvalidValue(format!(
4981                            "invalid legacy form {name} attribute: {error}"
4982                        ))
4983                    })?
4984                    .into_owned(),
4985            );
4986        }
4987    }
4988    Ok(value)
4989}
4990
4991fn validate_legacy_info_text(
4992    element: &BytesStart<'_>,
4993    word_prefixes: &[String],
4994    name: &str,
4995    max_characters: usize,
4996) -> Result<()> {
4997    if legacy_form_word_attribute(element, word_prefixes, b"type", "w:type")?
4998        .is_some_and(|value| !matches!(value.as_str(), "text" | "autoText"))
4999    {
5000        return Err(OxmlError::InvalidValue(format!(
5001            "invalid {name} w:type token"
5002        )));
5003    }
5004    if legacy_form_word_attribute(element, word_prefixes, b"val", "w:val")?
5005        .is_some_and(|value| value.chars().count() > max_characters)
5006    {
5007        return Err(OxmlError::InvalidValue(format!(
5008            "{name} w:val exceeds {max_characters} characters"
5009        )));
5010    }
5011    Ok(())
5012}
5013
5014fn legacy_form_usize_value(
5015    element: &BytesStart<'_>,
5016    word_prefixes: &[String],
5017    name: &str,
5018) -> Result<usize> {
5019    required_legacy_form_value(element, word_prefixes, name)?
5020        .parse()
5021        .map_err(|_| OxmlError::InvalidValue(format!("invalid {name} numeric token")))
5022}
5023
5024fn legacy_form_hps_measure_value(
5025    element: &BytesStart<'_>,
5026    word_prefixes: &[String],
5027    name: &str,
5028) -> Result<()> {
5029    let value = required_legacy_form_value(element, word_prefixes, name)?;
5030    if value.parse::<u64>().is_ok() {
5031        return Ok(());
5032    }
5033    let Some(unit) = ["mm", "cm", "in", "pt", "pc", "pi"]
5034        .into_iter()
5035        .find(|unit| value.ends_with(unit))
5036    else {
5037        return Err(OxmlError::InvalidValue(format!(
5038            "invalid {name} measure token"
5039        )));
5040    };
5041    let number = &value[..value.len() - unit.len()];
5042    let valid_number = number.split_once('.').map_or_else(
5043        || !number.is_empty() && number.bytes().all(|byte| byte.is_ascii_digit()),
5044        |(integer, fraction)| {
5045            !integer.is_empty()
5046                && !fraction.is_empty()
5047                && integer.bytes().all(|byte| byte.is_ascii_digit())
5048                && fraction.bytes().all(|byte| byte.is_ascii_digit())
5049        },
5050    );
5051    if !valid_number {
5052        return Err(OxmlError::InvalidValue(format!(
5053            "invalid {name} measure token"
5054        )));
5055    }
5056    Ok(())
5057}
5058
5059fn set_legacy_form_kind(
5060    projection: &mut LegacyFormProjection,
5061    kind: LegacyFormFieldKind,
5062    depth: usize,
5063    kind_depth: &mut Option<usize>,
5064) -> Result<()> {
5065    if projection.kind.replace(kind).is_some() {
5066        return Err(OxmlError::InvalidValue(
5067            "w:ffData contains more than one form kind".to_owned(),
5068        ));
5069    }
5070    *kind_depth = Some(depth);
5071    Ok(())
5072}
5073
5074fn local_name(name: &[u8]) -> &[u8] {
5075    name.rsplit(|byte| *byte == b':').next().unwrap_or(name)
5076}
5077
5078#[derive(Debug)]
5079enum ComplexFieldEvent {
5080    Begin(Option<bool>),
5081    Separate(Option<bool>),
5082    End(Option<bool>),
5083    Instruction(String),
5084    Result(RunContent),
5085}
5086
5087fn complex_field_events(
5088    raw: &[u8],
5089    word_prefixes: &[String],
5090    mut field_depth: usize,
5091) -> Result<Vec<ComplexFieldEvent>> {
5092    let mut reader = Reader::from_reader(raw);
5093    reader.config_mut().trim_text(false);
5094    let mut events = Vec::new();
5095    let mut buffer = Vec::new();
5096    let run_prefixes = loop {
5097        match reader.read_event_into(&mut buffer)? {
5098            Event::Start(start) if matches_local_name(start.name().as_ref(), b"r") => {
5099                break word_prefixes_at(&start, word_prefixes)?;
5100            }
5101            Event::Eof => return Ok(events),
5102            _ => {}
5103        }
5104        buffer.clear();
5105    };
5106    loop {
5107        buffer.clear();
5108        match reader.read_event_into(&mut buffer)? {
5109            Event::Start(start) => {
5110                let prefixes = word_prefixes_at(&start, &run_prefixes)?;
5111                if is_word_element(start.name().as_ref(), b"fldChar", &prefixes) {
5112                    let before = events.len();
5113                    push_field_char_event(&mut events, &start, &prefixes);
5114                    match events.get(before) {
5115                        Some(ComplexFieldEvent::Begin(_)) => field_depth += 1,
5116                        Some(ComplexFieldEvent::End(_)) => {
5117                            field_depth = field_depth.saturating_sub(1)
5118                        }
5119                        _ => {}
5120                    }
5121                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
5122                } else if is_word_element(start.name().as_ref(), b"instrText", &prefixes) {
5123                    events.push(ComplexFieldEvent::Instruction(
5124                        crate::xml_text::read_element_text(&mut reader, start.name()),
5125                    ));
5126                } else if is_word_element(start.name().as_ref(), b"t", &prefixes) {
5127                    events.push(ComplexFieldEvent::Result(RunContent::Text(CT_Text {
5128                        preserve_space: start.attributes().any(|attribute| {
5129                            attribute.ok().is_some_and(|attribute| {
5130                                attribute.key.as_ref() == b"xml:space"
5131                                    && attribute.value.as_ref() == b"preserve"
5132                            })
5133                        }),
5134                        text: crate::xml_text::read_element_text(&mut reader, start.name()),
5135                    })));
5136                } else if is_word_element(start.name().as_ref(), b"tab", &prefixes) {
5137                    events.push(ComplexFieldEvent::Result(RunContent::Tab));
5138                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
5139                } else if is_word_element(start.name().as_ref(), b"br", &prefixes) {
5140                    events.push(ComplexFieldEvent::Result(RunContent::Break(
5141                        field_break_type(&start, &prefixes),
5142                    )));
5143                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
5144                } else if is_word_element(start.name().as_ref(), b"drawing", &prefixes) {
5145                    if field_depth == 0 {
5146                        // The typed run already owns this drawing. Without an
5147                        // active field, result events are discarded. Rebuilding
5148                        // a nested text body here doubles recursive parse work.
5149                        reader.read_to_end_into(start.name(), &mut Vec::new())?;
5150                    } else {
5151                        events.push(ComplexFieldEvent::Result(RunContent::Drawing(
5152                            CT_Drawing::from_xml_with_prefixes(
5153                                &mut reader,
5154                                &prefixes,
5155                                &run_prefixes,
5156                            )?,
5157                        )));
5158                    }
5159                } else {
5160                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
5161                }
5162            }
5163            Event::Empty(start) => {
5164                let prefixes = word_prefixes_at(&start, &run_prefixes)?;
5165                if is_word_element(start.name().as_ref(), b"fldChar", &prefixes) {
5166                    let before = events.len();
5167                    push_field_char_event(&mut events, &start, &prefixes);
5168                    match events.get(before) {
5169                        Some(ComplexFieldEvent::Begin(_)) => field_depth += 1,
5170                        Some(ComplexFieldEvent::End(_)) => {
5171                            field_depth = field_depth.saturating_sub(1)
5172                        }
5173                        _ => {}
5174                    }
5175                } else if is_word_element(start.name().as_ref(), b"tab", &prefixes) {
5176                    events.push(ComplexFieldEvent::Result(RunContent::Tab));
5177                } else if is_word_element(start.name().as_ref(), b"br", &prefixes) {
5178                    events.push(ComplexFieldEvent::Result(RunContent::Break(
5179                        field_break_type(&start, &prefixes),
5180                    )));
5181                } else if let Some(content) = parse_typed_special_run_child(&start, &prefixes)? {
5182                    events.push(ComplexFieldEvent::Result(content));
5183                }
5184            }
5185            Event::End(end) if matches_local_name(end.name().as_ref(), b"r") => break,
5186            Event::Eof => break,
5187            _ => {}
5188        }
5189    }
5190    Ok(events)
5191}
5192
5193fn field_break_type(element: &BytesStart<'_>, word_prefixes: &[String]) -> BreakType {
5194    optional_word_attribute(element, b"type", word_prefixes)
5195        .map(|value| match value.as_str() {
5196            "page" => BreakType::Page,
5197            "column" => BreakType::Column,
5198            _ => BreakType::Line,
5199        })
5200        .unwrap_or(BreakType::Line)
5201}
5202
5203fn push_field_char_event(
5204    events: &mut Vec<ComplexFieldEvent>,
5205    element: &BytesStart<'_>,
5206    word_prefixes: &[String],
5207) {
5208    match optional_word_attribute(element, b"fldCharType", word_prefixes).as_deref() {
5209        Some("begin") => events.push(ComplexFieldEvent::Begin(
5210            optional_word_attribute(element, b"dirty", word_prefixes)
5211                .and_then(|value| parse_field_bool(&value)),
5212        )),
5213        Some("separate") => events.push(ComplexFieldEvent::Separate(
5214            optional_word_attribute(element, b"dirty", word_prefixes)
5215                .and_then(|value| parse_field_bool(&value)),
5216        )),
5217        Some("end") => events.push(ComplexFieldEvent::End(
5218            optional_word_attribute(element, b"dirty", word_prefixes)
5219                .and_then(|value| parse_field_bool(&value)),
5220        )),
5221        _ => {}
5222    }
5223}
5224
5225struct ComplexFieldBuilder {
5226    start_run: usize,
5227    separate_run: Option<usize>,
5228    dirty: Option<bool>,
5229    instruction: Vec<InstructionPart>,
5230    cached_result: String,
5231    cached_segments: Vec<CachedDisplaySegment>,
5232    cached_child_ranges: Vec<std::ops::Range<usize>>,
5233    cached_runs: Vec<CT_R>,
5234    valid: bool,
5235}
5236
5237struct ComplexFieldProjection<'a> {
5238    runs: &'a mut Vec<CT_R>,
5239    run_sources: &'a mut Vec<Option<Vec<u8>>>,
5240    extra_xml: &'a mut Vec<(usize, Vec<u8>)>,
5241    comment_ranges: &'a mut Vec<CommentRangeMarker>,
5242    comment_sources: &'a [(CommentRangeMarker, Vec<u8>)],
5243    bookmark_markers: &'a mut [BookmarkMarker],
5244    content_controls: &'a mut [(usize, usize, usize, CT_Sdt)],
5245    revisions: &'a mut [(usize, usize, CT_Revision)],
5246    hyperlinks: &'a mut [HyperlinkSpan],
5247    rubies: &'a mut [CT_Ruby],
5248    word_prefixes: &'a [String],
5249}
5250
5251fn project_complex_fields(projection: ComplexFieldProjection<'_>) -> Result<()> {
5252    let ComplexFieldProjection {
5253        runs,
5254        run_sources,
5255        extra_xml,
5256        comment_ranges,
5257        comment_sources,
5258        bookmark_markers,
5259        content_controls,
5260        revisions,
5261        hyperlinks,
5262        rubies,
5263        word_prefixes,
5264    } = projection;
5265    if runs.len() != run_sources.len() {
5266        return Ok(());
5267    }
5268    let mut stack = Vec::<ComplexFieldBuilder>::new();
5269    let mut completed = Vec::<(usize, usize, Field, Option<CT_RPr>)>::new();
5270    for run_index in 0..runs.len() {
5271        let Some(raw) = run_sources[run_index].as_deref() else {
5272            continue;
5273        };
5274        if let Some(child) = runs[run_index]
5275            .content
5276            .iter()
5277            .find_map(|content| match content {
5278                RunContent::Field(field) if field.form() == FieldForm::Simple => Some(field),
5279                _ => None,
5280            })
5281        {
5282            for parent in &mut stack {
5283                if parent.separate_run.is_some() {
5284                    parent.cached_result.push_str(&child.cached_result);
5285                    for (text, properties) in child.cached_display_segments() {
5286                        push_cached_display_segment(
5287                            &mut parent.cached_segments,
5288                            run_index,
5289                            text,
5290                            properties,
5291                        );
5292                    }
5293                }
5294            }
5295            if let Some(parent) = stack.last_mut()
5296                && parent.separate_run.is_some()
5297            {
5298                let end = parent.cached_result.len();
5299                parent
5300                    .cached_child_ranges
5301                    .push(end - child.cached_result.len()..end);
5302                parent
5303                    .cached_runs
5304                    .push(field_run(child.clone(), runs[run_index].properties.clone()));
5305            }
5306            continue;
5307        }
5308
5309        for event in complex_field_events(raw, word_prefixes, stack.len())? {
5310            match event {
5311                ComplexFieldEvent::Begin(dirty) => stack.push(ComplexFieldBuilder {
5312                    start_run: run_index,
5313                    separate_run: None,
5314                    dirty,
5315                    instruction: Vec::new(),
5316                    cached_result: String::new(),
5317                    cached_segments: Vec::new(),
5318                    cached_child_ranges: Vec::new(),
5319                    cached_runs: Vec::new(),
5320                    valid: true,
5321                }),
5322                ComplexFieldEvent::Instruction(text) => {
5323                    if let Some(field) = stack.last_mut()
5324                        && field.separate_run.is_none()
5325                    {
5326                        field.instruction.push(InstructionPart::Text(text));
5327                    }
5328                }
5329                ComplexFieldEvent::Result(content) => {
5330                    let display = match &content {
5331                        RunContent::Break(BreakType::Page) => "\u{000c}",
5332                        RunContent::Break(BreakType::Column) => "\u{000b}",
5333                        _ => CT_R::content_text(&content),
5334                    };
5335                    for field in &mut stack {
5336                        if field.separate_run.is_some() {
5337                            field.cached_result.push_str(display);
5338                            push_cached_display_segment(
5339                                &mut field.cached_segments,
5340                                run_index,
5341                                display,
5342                                runs[run_index].properties.as_ref(),
5343                            );
5344                        }
5345                    }
5346                    if let Some(field) = stack.last_mut()
5347                        && field.separate_run.is_some()
5348                    {
5349                        field.cached_runs.push(CT_R {
5350                            properties: runs[run_index].properties.clone(),
5351                            content: vec![content],
5352                            extra_xml: Vec::new(),
5353                            extra_xml_positions: Vec::new(),
5354                            alt_drawings: Vec::new(),
5355                        });
5356                    }
5357                }
5358                ComplexFieldEvent::Separate(dirty) => {
5359                    if let Some(field) = stack.last_mut() {
5360                        merge_field_dirty(&mut field.dirty, dirty);
5361                        if field.separate_run.replace(run_index).is_some() {
5362                            field.valid = false;
5363                        }
5364                    }
5365                }
5366                ComplexFieldEvent::End(dirty) => {
5367                    let Some(mut field) = stack.pop() else {
5368                        continue;
5369                    };
5370                    merge_field_dirty(&mut field.dirty, dirty);
5371                    let (instruction, nested_order) =
5372                        parse_field_instruction_parts_with_order(field.instruction);
5373                    let source = complex_field_source(
5374                        field.start_run,
5375                        run_index,
5376                        run_sources,
5377                        extra_xml,
5378                        hyperlinks,
5379                        comment_sources,
5380                    );
5381                    let mut parsed = Field::parsed(
5382                        instruction,
5383                        field.cached_result,
5384                        field.cached_segments,
5385                        field.dirty,
5386                        FieldForm::Complex,
5387                        source,
5388                        word_prefixes.to_vec(),
5389                    );
5390                    parsed.nested_order = nested_order;
5391                    if field.separate_run.is_some()
5392                        && let FieldSource::Parsed { raw_xml, .. } = &parsed.source
5393                        && source_has_note_reference_name(raw_xml)
5394                        && let Ok((cache_runs, cache_ranges)) =
5395                            parsed_complex_cache_runs(raw_xml, word_prefixes)
5396                        && cache_runs
5397                            .iter()
5398                            .flat_map(|run| &run.content)
5399                            .any(|content| {
5400                                matches!(
5401                                    content,
5402                                    RunContent::FootnoteRef { .. }
5403                                        | RunContent::EndnoteRef { .. }
5404                                        | RunContent::CommentReference { .. }
5405                                )
5406                            })
5407                    {
5408                        parsed.parsed_cached_runs = Some(cache_runs);
5409                        parsed.parsed_cached_comment_ranges = cache_ranges;
5410                    }
5411                    if parsed.parsed_cached_runs.is_none()
5412                        && cached_runs_need_typed_projection(&field.cached_runs)
5413                        && field
5414                            .cached_runs
5415                            .iter()
5416                            .map(cached_run_display)
5417                            .collect::<String>()
5418                            == parsed.cached_result
5419                    {
5420                        parsed.parsed_cached_runs = Some(field.cached_runs);
5421                    } else {
5422                        parsed.cached_fields = field
5423                            .cached_child_ranges
5424                            .into_iter()
5425                            .zip(
5426                                field
5427                                    .cached_runs
5428                                    .into_iter()
5429                                    .flat_map(|run| run.content)
5430                                    .filter_map(|content| {
5431                                        if let RunContent::Field(child) = content {
5432                                            Some(child)
5433                                        } else {
5434                                            None
5435                                        }
5436                                    }),
5437                            )
5438                            .collect();
5439                    }
5440                    let valid = field.valid
5441                        && !parsed.instruction.name.is_empty()
5442                        && run_sources[field.start_run..=run_index]
5443                            .iter()
5444                            .all(Option::is_some);
5445                    if let Some(parent) = stack.last_mut() {
5446                        if !valid {
5447                            parent.valid = false;
5448                        } else if parent.separate_run.is_none() {
5449                            parent.instruction.push(InstructionPart::Nested(parsed));
5450                        } else {
5451                            let end = parent.cached_result.len();
5452                            let start = end.saturating_sub(parsed.cached_result.len());
5453                            parent.cached_child_ranges.push(start..end);
5454                            parent.cached_runs.push(field_run(parsed, None));
5455                        }
5456                    } else if valid {
5457                        completed.push((field.start_run, run_index, parsed, None));
5458                    }
5459                }
5460            }
5461        }
5462    }
5463
5464    let mut grouped = Vec::<(usize, usize, Vec<(Field, Option<CT_RPr>)>)>::new();
5465    for (start, end, field, properties) in completed {
5466        // Fields that share a physical run form one span, so the splice of
5467        // one never removes the run that holds the next.
5468        if let Some((_, group_end, fields)) = grouped.last_mut()
5469            && start <= *group_end
5470        {
5471            *group_end = end.max(*group_end);
5472            fields.push((field, properties));
5473        } else {
5474            grouped.push((start, end, vec![(field, properties)]));
5475        }
5476    }
5477    for (start, end, mut fields) in grouped.into_iter().rev() {
5478        let mut owned_comment_ids = std::collections::BTreeSet::new();
5479        let mut cache_owners = fields.iter().map(|(field, _)| field).collect::<Vec<_>>();
5480        while let Some(field) = cache_owners.pop() {
5481            cache_owners.extend(field.all_nested_fields_in_source_order());
5482            let mut ranges = field.parsed_cached_comment_ranges.clone();
5483            for marker in &mut ranges {
5484                match marker {
5485                    CommentRangeMarker::Start { raw_before, .. }
5486                    | CommentRangeMarker::End { raw_before, .. } => *raw_before = 0,
5487                }
5488            }
5489            if !ranges.is_empty()
5490                && field
5491                    .cached_result_runs()
5492                    .is_some_and(|runs| validate_cached_comment_ranges(runs, &ranges).is_ok())
5493            {
5494                for marker in ranges {
5495                    match marker {
5496                        CommentRangeMarker::Start { id, .. }
5497                        | CommentRangeMarker::End { id, .. } => {
5498                            owned_comment_ids.insert(id);
5499                        }
5500                    }
5501                }
5502            }
5503        }
5504        let unrelated_comments = comment_ranges
5505            .iter()
5506            .filter(|marker| match marker {
5507                CommentRangeMarker::Start { id, .. } | CommentRangeMarker::End { id, .. } => {
5508                    !owned_comment_ids.contains(id)
5509                }
5510            })
5511            .cloned()
5512            .collect::<Vec<_>>();
5513        if has_typed_boundary_inside(
5514            start,
5515            end,
5516            &unrelated_comments,
5517            bookmark_markers,
5518            content_controls,
5519            revisions,
5520            hyperlinks,
5521        ) {
5522            continue;
5523        }
5524        let raw_xml = complex_field_source(
5525            start,
5526            end,
5527            run_sources,
5528            extra_xml,
5529            hyperlinks,
5530            comment_sources,
5531        );
5532        let owner_id = fields.iter().find_map(|(field, _)| match field.source {
5533            FieldSource::Parsed { source_id, .. } => Some(source_id),
5534            FieldSource::New { .. } => None,
5535        });
5536        for (field, _) in &mut fields {
5537            if let FieldSource::Parsed {
5538                raw_xml: source,
5539                owner_id: field_owner_id,
5540                ..
5541            } = &mut field.source
5542            {
5543                *source = raw_xml.clone();
5544                if let Some(owner_id) = owner_id {
5545                    *field_owner_id = owner_id;
5546                }
5547            }
5548        }
5549        comment_ranges.retain(|marker| match marker {
5550            CommentRangeMarker::Start { id, .. } | CommentRangeMarker::End { id, .. } => {
5551                !owned_comment_ids.contains(id)
5552                    || !(marker.run_index() > start && marker.run_index() <= end)
5553            }
5554        });
5555        extra_xml.retain(|(at, _)| !(*at > start && *at <= end));
5556        for hyperlink in hyperlinks.iter_mut() {
5557            let hyperlink_start = hyperlink.run_start;
5558            hyperlink.extra_xml.retain(|(boundary, _, _)| {
5559                let absolute = hyperlink_start + *boundary;
5560                !(absolute > start && absolute <= end)
5561            });
5562        }
5563
5564        let replacement = field_span_runs(&raw_xml, fields, word_prefixes);
5565        let replacement_count = replacement.len();
5566        runs.splice(start..=end, replacement);
5567        run_sources.splice(start..=end, std::iter::repeat_n(None, replacement_count));
5568        remap_complex_field_boundaries(
5569            start,
5570            end,
5571            replacement_count,
5572            ComplexFieldBoundariesMut {
5573                extra_xml,
5574                comment_ranges,
5575                bookmark_markers,
5576                content_controls,
5577                revisions,
5578                hyperlinks,
5579                rubies,
5580            },
5581        );
5582    }
5583    Ok(())
5584}
5585
5586fn merge_field_dirty(current: &mut Option<bool>, next: Option<bool>) {
5587    if next == Some(true) || current.is_none() {
5588        *current = next;
5589    }
5590}
5591
5592fn push_cached_display_segment(
5593    segments: &mut Vec<CachedDisplaySegment>,
5594    run_index: usize,
5595    text: &str,
5596    properties: Option<&CT_RPr>,
5597) {
5598    if let Some(segment) = segments.last_mut()
5599        && segment.run_index == run_index
5600    {
5601        segment.text.push_str(text);
5602        return;
5603    }
5604    segments.push(CachedDisplaySegment {
5605        run_index,
5606        text: text.to_owned(),
5607        properties: properties.cloned(),
5608    });
5609}
5610
5611fn complex_field_source(
5612    start: usize,
5613    end: usize,
5614    run_sources: &[Option<Vec<u8>>],
5615    extra_xml: &[(usize, Vec<u8>)],
5616    hyperlinks: &[HyperlinkSpan],
5617    comment_sources: &[(CommentRangeMarker, Vec<u8>)],
5618) -> Vec<u8> {
5619    let mut source = Vec::new();
5620    for (run_index, raw_source) in run_sources.iter().enumerate().take(end + 1).skip(start) {
5621        if let Some(raw) = raw_source.as_deref() {
5622            source.extend_from_slice(raw);
5623        }
5624        if run_index < end {
5625            let extras = extra_xml
5626                .iter()
5627                .filter(|(at, _)| *at == run_index + 1)
5628                .collect::<Vec<_>>();
5629            for slot in 0..=extras.len() {
5630                for (marker, raw) in comment_sources.iter().filter(|(marker, _)| {
5631                    marker.run_index() == run_index + 1 && marker.raw_before() == slot
5632                }) {
5633                    let _ = marker;
5634                    source.extend_from_slice(raw);
5635                }
5636                if let Some((_, raw)) = extras.get(slot) {
5637                    source.extend_from_slice(raw);
5638                }
5639            }
5640            for hyperlink in hyperlinks
5641                .iter()
5642                .filter(|hyperlink| hyperlink.run_start <= start && hyperlink.run_end > end)
5643            {
5644                let relative = run_index + 1 - hyperlink.run_start;
5645                for (_, _, raw) in hyperlink
5646                    .extra_xml
5647                    .iter()
5648                    .filter(|(boundary, _, _)| *boundary == relative)
5649                {
5650                    source.extend_from_slice(raw);
5651                }
5652            }
5653        }
5654    }
5655    source
5656}
5657
5658/// One part of a physical field span: a whole field with its bytes, or the
5659/// run read from content outside any field.
5660enum FieldSpanPart {
5661    Field(Vec<u8>),
5662    Outside(CT_R),
5663}
5664
5665/// Split the exact bytes of a complex field span into its fields and the run
5666/// content outside them, in order.
5667///
5668/// Each part is written as its own runs, so a part taken from a shared run
5669/// repeats that run's start tag and `w:rPr`. Returns `None` for a shape this
5670/// does not model, which leaves the span as it was read.
5671fn field_span_parts(raw: &[u8], word_prefixes: &[String]) -> Result<Option<Vec<FieldSpanPart>>> {
5672    use std::ops::Range;
5673
5674    // A run's start tag, `w:rPr` and end tag.
5675    let mut runs = Vec::<(Range<usize>, Option<Range<usize>>, Range<usize>)>::new();
5676    // Each part: whether it is a field, and its children (run index, bytes).
5677    let mut parts = Vec::<(bool, Vec<(Option<usize>, Range<usize>)>)>::new();
5678    let mut current: Option<usize> = None;
5679    let mut depth = 0usize;
5680    let mut reader = Reader::from_reader(raw);
5681    reader.config_mut().trim_text(false);
5682    let mut buffer = Vec::new();
5683    loop {
5684        let before = reader.buffer_position() as usize;
5685        let event = reader.read_event_into(&mut buffer)?;
5686        let (run_start, run_prefixes) = match event {
5687            Event::Start(start) => {
5688                let prefixes = word_prefixes_at(&start, word_prefixes)?;
5689                if is_word_element(start.name().as_ref(), b"r", &prefixes) {
5690                    (before..reader.buffer_position() as usize, prefixes)
5691                } else {
5692                    reader.read_to_end_into(start.name(), &mut Vec::new())?;
5693                    let range = before..reader.buffer_position() as usize;
5694                    let Some(part) = current.filter(|_| depth > 0) else {
5695                        return Ok(None);
5696                    };
5697                    parts[part].1.push((None, range));
5698                    buffer.clear();
5699                    continue;
5700                }
5701            }
5702            Event::Eof => break,
5703            _ => {
5704                let range = before..reader.buffer_position() as usize;
5705                let Some(part) = current.filter(|_| depth > 0) else {
5706                    return Ok(None);
5707                };
5708                parts[part].1.push((None, range));
5709                buffer.clear();
5710                continue;
5711            }
5712        };
5713        let run = runs.len();
5714        runs.push((run_start, None, 0..0));
5715        let mut children = 0usize;
5716        loop {
5717            buffer.clear();
5718            let child_start = reader.buffer_position() as usize;
5719            let kind = match reader.read_event_into(&mut buffer)? {
5720                Event::Start(child) => {
5721                    let prefixes = word_prefixes_at(&child, &run_prefixes)?;
5722                    let kind = field_span_child_kind(&child, &prefixes);
5723                    reader.read_to_end_into(child.name(), &mut Vec::new())?;
5724                    kind
5725                }
5726                Event::Empty(child) => {
5727                    let prefixes = word_prefixes_at(&child, &run_prefixes)?;
5728                    field_span_child_kind(&child, &prefixes)
5729                }
5730                Event::End(_) => {
5731                    let end = reader.buffer_position() as usize;
5732                    if children == 0 {
5733                        let Some(part) = current.filter(|_| depth > 0) else {
5734                            return Ok(None);
5735                        };
5736                        parts[part].1.push((Some(run), child_start..child_start));
5737                    }
5738                    runs[run].2 = child_start..end;
5739                    break;
5740                }
5741                Event::Eof => return Ok(None),
5742                _ => FieldSpanChild::Other,
5743            };
5744            let range = child_start..reader.buffer_position() as usize;
5745            if kind == FieldSpanChild::Properties {
5746                runs[run].1 = Some(range);
5747                continue;
5748            }
5749            children += 1;
5750            let inside = match kind {
5751                FieldSpanChild::Begin => {
5752                    if depth == 0 {
5753                        parts.push((true, Vec::new()));
5754                        current = Some(parts.len() - 1);
5755                    }
5756                    depth += 1;
5757                    true
5758                }
5759                FieldSpanChild::End => {
5760                    if depth == 0 {
5761                        return Ok(None);
5762                    }
5763                    depth -= 1;
5764                    true
5765                }
5766                FieldSpanChild::FieldMarker => {
5767                    if depth == 0 {
5768                        return Ok(None);
5769                    }
5770                    true
5771                }
5772                FieldSpanChild::Other | FieldSpanChild::Properties => depth > 0,
5773            };
5774            if !inside && current.is_none_or(|part| parts[part].0) {
5775                parts.push((false, Vec::new()));
5776                current = Some(parts.len() - 1);
5777            }
5778            let part = current.expect("a part is open");
5779            parts[part].1.push((Some(run), range));
5780            if kind == FieldSpanChild::End && depth == 0 {
5781                current = None;
5782            }
5783        }
5784        buffer.clear();
5785    }
5786    if depth != 0 {
5787        return Ok(None);
5788    }
5789
5790    let materialize = |children: &[(Option<usize>, Range<usize>)]| {
5791        let mut bytes = Vec::new();
5792        let mut open = None;
5793        for (run, range) in children {
5794            if *run != open {
5795                if let Some(open) = open {
5796                    bytes.extend_from_slice(&raw[runs[open].2.clone()]);
5797                }
5798                if let Some(run) = *run {
5799                    bytes.extend_from_slice(&raw[runs[run].0.clone()]);
5800                    if let Some(properties) = runs[run].1.clone() {
5801                        bytes.extend_from_slice(&raw[properties]);
5802                    }
5803                }
5804                open = *run;
5805            }
5806            bytes.extend_from_slice(&raw[range.clone()]);
5807        }
5808        if let Some(open) = open {
5809            bytes.extend_from_slice(&raw[runs[open].2.clone()]);
5810        }
5811        bytes
5812    };
5813
5814    // Content outside every field that no reader sees, such as whitespace or
5815    // an unknown element, stays with its neighbouring field as before.
5816    let mut outside = Vec::with_capacity(parts.len());
5817    let mut index = 0;
5818    while index < parts.len() {
5819        if parts[index].0 {
5820            outside.push(None);
5821            index += 1;
5822            continue;
5823        }
5824        if let Ok(run) = parse_run_raw(&materialize(&parts[index].1), word_prefixes)
5825            && !run.content.is_empty()
5826        {
5827            outside.push(Some(run));
5828            index += 1;
5829            continue;
5830        }
5831        let (_, children) = parts.remove(index);
5832        if let Some(next) = parts.get_mut(index) {
5833            next.1.splice(0..0, children);
5834        } else if let Some(previous) = index.checked_sub(1).and_then(|at| parts.get_mut(at)) {
5835            previous.1.extend(children);
5836        }
5837    }
5838    Ok(Some(
5839        parts
5840            .iter()
5841            .zip(outside)
5842            .map(|((_, children), run)| match run {
5843                Some(run) => FieldSpanPart::Outside(run),
5844                None => FieldSpanPart::Field(materialize(children)),
5845            })
5846            .collect(),
5847    ))
5848}
5849
5850#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5851enum FieldSpanChild {
5852    Properties,
5853    Begin,
5854    End,
5855    /// A separator or instruction text, which only ever sits inside a field.
5856    FieldMarker,
5857    Other,
5858}
5859
5860fn field_span_child_kind(element: &BytesStart<'_>, word_prefixes: &[String]) -> FieldSpanChild {
5861    let name = element.name();
5862    if is_word_element(name.as_ref(), b"rPr", word_prefixes) {
5863        return FieldSpanChild::Properties;
5864    }
5865    if is_word_element(name.as_ref(), b"instrText", word_prefixes) {
5866        return FieldSpanChild::FieldMarker;
5867    }
5868    if !is_word_element(name.as_ref(), b"fldChar", word_prefixes) {
5869        return FieldSpanChild::Other;
5870    }
5871    match optional_word_attribute(element, b"fldCharType", word_prefixes).as_deref() {
5872        Some("begin") => FieldSpanChild::Begin,
5873        Some("end") => FieldSpanChild::End,
5874        _ => FieldSpanChild::FieldMarker,
5875    }
5876}
5877
5878/// Whether one field's span starts with its begin marker and ends with its
5879/// end marker, the shape Word writes, so no run content sits outside it.
5880///
5881/// Only the first and last run children are read. Any other shape answers
5882/// `false` and takes the full split.
5883fn field_span_is_bare(raw: &[u8], word_prefixes: &[String]) -> bool {
5884    let marker = |element: &BytesStart<'_>, prefixes: &[String], kind: &str| {
5885        is_word_element(element.name().as_ref(), b"fldChar", prefixes)
5886            && optional_word_attribute(element, b"fldCharType", prefixes).as_deref() == Some(kind)
5887    };
5888    let mut reader = Reader::from_reader(raw);
5889    reader.config_mut().trim_text(true);
5890    let mut buffer = Vec::new();
5891    let mut run_prefixes = None;
5892    let begins = loop {
5893        let Ok(event) = reader.read_event_into(&mut buffer) else {
5894            return false;
5895        };
5896        match event {
5897            Event::Start(element) => {
5898                let Ok(prefixes) = word_prefixes_at(&element, word_prefixes) else {
5899                    return false;
5900                };
5901                if run_prefixes.is_none() {
5902                    if !is_word_element(element.name().as_ref(), b"r", &prefixes) {
5903                        return false;
5904                    }
5905                    run_prefixes = Some(prefixes);
5906                } else if is_word_element(element.name().as_ref(), b"rPr", &prefixes) {
5907                    if reader
5908                        .read_to_end_into(element.name(), &mut Vec::new())
5909                        .is_err()
5910                    {
5911                        return false;
5912                    }
5913                } else {
5914                    return false;
5915                }
5916            }
5917            Event::Empty(element) => {
5918                let Some(inherited) = run_prefixes.as_deref() else {
5919                    return false;
5920                };
5921                let Ok(prefixes) = word_prefixes_at(&element, inherited) else {
5922                    return false;
5923                };
5924                break marker(&element, &prefixes, "begin");
5925            }
5926            _ => return false,
5927        }
5928        buffer.clear();
5929    };
5930    if !begins {
5931        return false;
5932    }
5933    // The last run child is the empty element before the closing run tag.
5934    let body = raw.trim_ascii_end();
5935    let Some(close) = body.iter().rposition(|byte| *byte == b'<') else {
5936        return false;
5937    };
5938    let Some(last) = body[..close].iter().rposition(|byte| *byte == b'<') else {
5939        return false;
5940    };
5941    let mut reader = Reader::from_reader(&body[last..close]);
5942    let mut buffer = Vec::new();
5943    let Some(prefixes) = run_prefixes else {
5944        return false;
5945    };
5946    matches!(
5947        reader.read_event_into(&mut buffer),
5948        Ok(Event::Empty(element))
5949            if word_prefixes_at(&element, &prefixes)
5950                .is_ok_and(|prefixes| marker(&element, &prefixes, "end"))
5951    ) && body[last..close].trim_ascii_end().ends_with(b"/>")
5952}
5953
5954/// Turn a projected field span into model runs, splitting out the run
5955/// content that sits outside its fields.
5956///
5957/// Without such content the span stays one run per field, as before.
5958fn field_span_runs(
5959    raw: &[u8],
5960    fields: Vec<(Field, Option<CT_RPr>)>,
5961    word_prefixes: &[String],
5962) -> Vec<CT_R> {
5963    let mut fields = fields;
5964    if source_has_note_reference_name(raw)
5965        && let Ok(Some(parts)) = field_span_parts(raw, word_prefixes)
5966    {
5967        for ((field, _), source) in
5968            fields
5969                .iter_mut()
5970                .zip(parts.into_iter().filter_map(|part| match part {
5971                    FieldSpanPart::Field(source) => Some(source),
5972                    FieldSpanPart::Outside(_) => None,
5973                }))
5974        {
5975            if let Ok((runs, ranges)) = parsed_complex_cache_runs(&source, word_prefixes)
5976                && runs.iter().flat_map(|run| &run.content).any(|content| {
5977                    matches!(
5978                        content,
5979                        RunContent::FootnoteRef { .. }
5980                            | RunContent::EndnoteRef { .. }
5981                            | RunContent::CommentReference { .. }
5982                    )
5983                })
5984            {
5985                field.parsed_cached_runs = Some(runs);
5986                field.parsed_cached_comment_ranges = ranges;
5987            }
5988        }
5989    }
5990    let one_run_per_field = |fields: Vec<(Field, Option<CT_RPr>)>| {
5991        fields
5992            .into_iter()
5993            .map(|(field, properties)| field_run(field, properties))
5994            .collect()
5995    };
5996    if fields.len() == 1 && field_span_is_bare(raw, word_prefixes) {
5997        return one_run_per_field(fields);
5998    }
5999    let parts = match field_span_parts(raw, word_prefixes) {
6000        Ok(Some(parts))
6001            if (fields.len() > 1
6002                || parts
6003                    .iter()
6004                    .any(|part| matches!(part, FieldSpanPart::Outside(_))))
6005                && parts
6006                    .iter()
6007                    .filter(|part| matches!(part, FieldSpanPart::Field(_)))
6008                    .count()
6009                    == fields.len() =>
6010        {
6011            parts
6012        }
6013        _ => return one_run_per_field(fields),
6014    };
6015    let shape = parts
6016        .into_iter()
6017        .map(|part| match part {
6018            FieldSpanPart::Field(_) => None,
6019            FieldSpanPart::Outside(run) => Some(run),
6020        })
6021        .collect::<Vec<_>>();
6022    let mut fields = fields.into_iter().enumerate();
6023    shape
6024        .iter()
6025        .enumerate()
6026        .map(|(position, run)| match run {
6027            Some(run) => run.clone(),
6028            None => {
6029                let (field_index, (mut field, properties)) =
6030                    fields.next().expect("one field per field part");
6031                field.span = Some(Box::new(FieldRunSpan {
6032                    runs: shape.clone(),
6033                    position,
6034                    field_index,
6035                }));
6036                field_run(field, properties)
6037            }
6038        })
6039        .collect()
6040}
6041
6042fn has_typed_boundary_inside(
6043    start: usize,
6044    end: usize,
6045    comment_ranges: &[CommentRangeMarker],
6046    bookmark_markers: &[BookmarkMarker],
6047    content_controls: &[(usize, usize, usize, CT_Sdt)],
6048    revisions: &[(usize, usize, CT_Revision)],
6049    hyperlinks: &[HyperlinkSpan],
6050) -> bool {
6051    let inside = |at: usize| at > start && at <= end;
6052    comment_ranges
6053        .iter()
6054        .any(|marker| inside(marker.run_index()))
6055        || bookmark_markers
6056            .iter()
6057            .any(|marker| inside(marker.run_index))
6058        || content_controls.iter().any(|(at, _, _, _)| inside(*at))
6059        || revisions.iter().any(|(at, _, _)| inside(*at))
6060        || hyperlinks.iter().any(|hyperlink| {
6061            let overlaps = hyperlink.run_start <= end && hyperlink.run_end > start;
6062            let contains_field = hyperlink.run_start <= start && hyperlink.run_end > end;
6063            overlaps && !contains_field
6064        })
6065}
6066
6067struct ComplexFieldBoundariesMut<'a> {
6068    extra_xml: &'a mut [(usize, Vec<u8>)],
6069    comment_ranges: &'a mut [CommentRangeMarker],
6070    bookmark_markers: &'a mut [BookmarkMarker],
6071    content_controls: &'a mut [(usize, usize, usize, CT_Sdt)],
6072    revisions: &'a mut [(usize, usize, CT_Revision)],
6073    hyperlinks: &'a mut [HyperlinkSpan],
6074    rubies: &'a mut [CT_Ruby],
6075}
6076
6077fn remap_complex_field_boundaries(
6078    start: usize,
6079    end: usize,
6080    replacement_count: usize,
6081    boundaries: ComplexFieldBoundariesMut<'_>,
6082) {
6083    let ComplexFieldBoundariesMut {
6084        extra_xml,
6085        comment_ranges,
6086        bookmark_markers,
6087        content_controls,
6088        revisions,
6089        hyperlinks,
6090        rubies,
6091    } = boundaries;
6092    let source_count = end - start + 1;
6093    let remap = |at: &mut usize| {
6094        if *at > end {
6095            if replacement_count >= source_count {
6096                *at += replacement_count - source_count;
6097            } else {
6098                *at -= source_count - replacement_count;
6099            }
6100        } else if *at > start {
6101            *at = start + replacement_count;
6102        }
6103    };
6104    for (at, _) in extra_xml {
6105        if *at != ROOT_ATTRIBUTES_POSITION {
6106            remap(at);
6107        }
6108    }
6109    for marker in comment_ranges {
6110        match marker {
6111            CommentRangeMarker::Start { run_index, .. }
6112            | CommentRangeMarker::End { run_index, .. } => remap(run_index),
6113        }
6114    }
6115    for marker in bookmark_markers {
6116        if marker.run_index > end {
6117            if replacement_count >= source_count {
6118                let inserted = replacement_count - source_count;
6119                marker.projected_run_index += inserted;
6120                marker.tracked_run_index += inserted;
6121            } else {
6122                let removed = source_count - replacement_count;
6123                marker.projected_run_index = marker.projected_run_index.saturating_sub(removed);
6124                marker.tracked_run_index = marker.tracked_run_index.saturating_sub(removed);
6125            }
6126        }
6127        remap(&mut marker.run_index);
6128    }
6129    for (at, _, _, _) in content_controls {
6130        remap(at);
6131    }
6132    for (at, _, _) in revisions {
6133        remap(at);
6134    }
6135    for ruby in rubies {
6136        remap(&mut ruby.base_start);
6137        remap(&mut ruby.base_end);
6138    }
6139    for hyperlink in hyperlinks {
6140        let old_start = hyperlink.run_start;
6141        let old_boundaries = hyperlink
6142            .extra_xml
6143            .iter()
6144            .map(|(boundary, _, _)| old_start + *boundary)
6145            .collect::<Vec<_>>();
6146        remap(&mut hyperlink.run_start);
6147        remap(&mut hyperlink.run_end);
6148        for ((boundary, _, _), mut absolute) in hyperlink.extra_xml.iter_mut().zip(old_boundaries) {
6149            remap(&mut absolute);
6150            *boundary = absolute.saturating_sub(hyperlink.run_start);
6151        }
6152    }
6153}
6154
6155/// Why [`CT_P::split_run`] could not split a run.
6156#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
6157pub enum RunSplitError {
6158    #[error("run index {run_index} is out of range for a paragraph with {run_count} runs")]
6159    RunOutOfRange { run_index: usize, run_count: usize },
6160    #[error("split offset {offset} exceeds a run's literal text length of {len} characters")]
6161    OffsetOutOfRange { offset: usize, len: usize },
6162    #[error("bookmark markers could not be projected after the split")]
6163    BookmarkProjection,
6164}
6165
6166/// The range kind that [`CT_P::anchor_accepted_range`] writes.
6167#[doc(hidden)]
6168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6169pub enum RangeAnchor<'a> {
6170    /// Comment range markers, with the reference run right after the end.
6171    Comment(i32),
6172    /// Bookmark markers.
6173    Bookmark { id: i32, name: &'a str },
6174    /// Permission range with one selected editor or editor group.
6175    Permission {
6176        id: i32,
6177        editor: Option<&'a str>,
6178        group: Option<&'a str>,
6179    },
6180    /// Spelling or grammar proofing range, which has no numeric identifier.
6181    Proofing { kind: &'a str },
6182}
6183
6184/// Why [`CT_P::anchor_accepted_range`] could not place a range exactly.
6185#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
6186pub enum RangeAnchorError {
6187    #[error("run boundary {boundary} exceeds the paragraph run count {run_count}")]
6188    OutOfRange { boundary: usize, run_count: usize },
6189    #[error("run range {start}..{end} ends before it starts")]
6190    Reversed { start: usize, end: usize },
6191    #[error("run range {start}..{end} crosses the edge of an inline content control")]
6192    CrossesControl { start: usize, end: usize },
6193    #[error(
6194        "run boundary {boundary} falls inside an inline content control, where a range that continues into another paragraph cannot start or end"
6195    )]
6196    InsideControl { boundary: usize },
6197    #[error("run boundary {boundary} falls inside a tracked insertion or move")]
6198    InsideRevision { boundary: usize },
6199    #[error("run boundary {boundary} sits next to a tracked change inside a hyperlink")]
6200    HyperlinkRevision { boundary: usize },
6201    #[error("range markers could not be written into the paragraph")]
6202    Write,
6203}
6204
6205/// One physical place for a range marker.
6206#[derive(Debug, Clone, PartialEq, Eq)]
6207enum MarkerSite {
6208    /// A position among the paragraph-level children at a direct run boundary.
6209    Paragraph { boundary: usize, position: usize },
6210    /// A `w:sdtContent` child index in the inline control that `controls`
6211    /// reaches: a paragraph control index, then nested content indexes.
6212    Control { controls: Vec<usize>, index: usize },
6213}
6214
6215/// A direct run boundary and a position among its paragraph-level children.
6216type ChildPosition = (usize, usize);
6217
6218/// One paragraph-level child at a direct run boundary, in serialization order.
6219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6220enum BoundaryItem {
6221    Control(usize),
6222    Marker(usize),
6223    Raw(usize),
6224}
6225
6226/// `CT_P` — A paragraph element containing runs and properties.
6227#[derive(Debug, Clone, PartialEq)]
6228#[allow(non_snake_case)]
6229pub struct CT_P {
6230    pub properties: Option<CT_PPr>,
6231    pub runs: Vec<CT_R>,
6232    /// Hyperlink spans referencing ranges of runs.
6233    pub hyperlinks: Vec<HyperlinkSpan>,
6234    /// Typed comment range boundaries at run insertion points.
6235    pub comment_ranges: Vec<CommentRangeMarker>,
6236    /// Typed projections of preserved bookmark markers.
6237    pub bookmark_markers: Vec<BookmarkMarker>,
6238    /// Unknown child elements captured as raw XML with their insertion position (run index).
6239    pub extra_xml: Vec<(usize, Vec<u8>)>,
6240    /// Typed run controls at
6241    /// `(run index, raw children before, comment markers before, control)`.
6242    pub content_controls: Vec<(usize, usize, usize, CT_Sdt)>,
6243    /// Read projections of revision wrappers retained at paragraph or hyperlink boundaries.
6244    pub revisions: Vec<(usize, usize, CT_Revision)>,
6245    /// Typed OfficeMath projections keyed by `(run boundary, raw child slot)`.
6246    pub equations: Vec<(usize, usize, OfficeMath)>,
6247    /// Ruby phonetic guides over half-open spans of [`Self::runs`].
6248    ///
6249    /// The base runs are ordinary paragraph runs, so text extraction, search
6250    /// and redaction see them without knowing about ruby. The phonetic runs
6251    /// stay inside the annotation, which is what keeps them out of every
6252    /// text projection.
6253    pub rubies: Vec<CT_Ruby>,
6254}
6255
6256#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
6257enum AcceptedOwnerOrder {
6258    BeforeRaw,
6259    Raw(usize),
6260    AfterRaw,
6261}
6262
6263/// One recursive step to a visible accepted-view run.
6264#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6265pub enum AcceptedRunPathSegment {
6266    /// A direct run index in the current paragraph or content control.
6267    Run(usize),
6268    /// A content-control index in the current paragraph or content control.
6269    ContentControl(usize),
6270    /// A revision index in the current paragraph or content control.
6271    Revision(usize),
6272}
6273
6274/// A checked recursive address for one visible accepted-view run.
6275#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6276pub struct AcceptedRunPath {
6277    pub(crate) segments: Vec<AcceptedRunPathSegment>,
6278}
6279
6280impl AcceptedRunPath {
6281    /// Return the recursive source steps for this run.
6282    pub fn segments(&self) -> &[AcceptedRunPathSegment] {
6283        &self.segments
6284    }
6285}
6286
6287/// A content control or a revision wrapper at a run boundary of a paragraph.
6288#[derive(Clone, Copy)]
6289pub(crate) enum BoundaryOwner {
6290    /// An index into [`CT_P::content_controls`].
6291    ContentControl(usize),
6292    /// An index into [`CT_P::revisions`].
6293    Revision(usize),
6294    /// An index into [`CT_P::extra_xml`] of a smart tag or an inline custom
6295    /// XML element, see [`run_wrapper_paragraph`].
6296    Wrapper(usize),
6297}
6298
6299/// The content controls, revision wrappers, smart tags and inline custom
6300/// XML elements at run `boundary` of `paragraph`, in document order.
6301pub(crate) fn boundary_owners(paragraph: &CT_P, boundary: usize) -> Vec<BoundaryOwner> {
6302    let mut owners = paragraph
6303        .content_controls
6304        .iter()
6305        .enumerate()
6306        .filter(|(_, (at, _, _, _))| *at == boundary)
6307        .map(|(index, (_, raw_before, _, _))| {
6308            (
6309                AcceptedOwnerOrder::Raw(*raw_before),
6310                BoundaryOwner::ContentControl(index),
6311            )
6312        })
6313        .chain(
6314            paragraph
6315                .revisions
6316                .iter()
6317                .enumerate()
6318                .filter(|(_, (at, _, _))| *at == boundary)
6319                .map(|(index, (_, slot, _))| {
6320                    let order = if let Some(hyperlink_index) = hyperlink_revision_index(*slot) {
6321                        if let Some(raw_before) = paragraph
6322                            .hyperlinks
6323                            .get(hyperlink_index)
6324                            .and_then(|hyperlink| hyperlink.preserved_raw_before)
6325                        {
6326                            AcceptedOwnerOrder::Raw(raw_before)
6327                        } else if paragraph
6328                            .hyperlinks
6329                            .get(hyperlink_index)
6330                            .is_some_and(|hyperlink| boundary == hyperlink.run_end)
6331                        {
6332                            AcceptedOwnerOrder::BeforeRaw
6333                        } else {
6334                            AcceptedOwnerOrder::AfterRaw
6335                        }
6336                    } else {
6337                        AcceptedOwnerOrder::Raw(*slot)
6338                    };
6339                    (order, BoundaryOwner::Revision(index))
6340                }),
6341        )
6342        .chain(
6343            paragraph
6344                .extra_xml
6345                .iter()
6346                .enumerate()
6347                .filter(|(_, (at, _))| *at == boundary)
6348                .enumerate()
6349                .filter(|(_, (_, (_, raw)))| is_run_wrapper(raw))
6350                .map(|(slot, (index, _))| {
6351                    (AcceptedOwnerOrder::Raw(slot), BoundaryOwner::Wrapper(index))
6352                }),
6353        )
6354        .collect::<Vec<_>>();
6355    // A control goes before the raw child of the same slot.
6356    owners
6357        .sort_by_key(|(order, owner)| (*order, !matches!(owner, BoundaryOwner::ContentControl(_))));
6358    owners.into_iter().map(|(_, owner)| owner).collect()
6359}
6360
6361/// Whether `raw` is a smart tag or an inline custom XML element with
6362/// content, read with the conventional `w` prefix and those it declares.
6363fn is_run_wrapper(raw: &[u8]) -> bool {
6364    let mut reader = Reader::from_reader(raw);
6365    let mut buffer = Vec::new();
6366    let Ok(Event::Start(start)) = reader.read_event_into(&mut buffer) else {
6367        return false;
6368    };
6369    let Ok(prefixes) = word_prefixes_at(&start, &["w".to_owned()]) else {
6370        return false;
6371    };
6372    is_word_element(start.name().as_ref(), b"smartTag", &prefixes)
6373        || is_word_element(start.name().as_ref(), b"customXml", &prefixes)
6374}
6375
6376/// The content of a smart tag, an inline custom XML element or a simple
6377/// field, parsed as a paragraph from its preserved source `raw` in the scope
6378/// of the Word prefixes `inherited`. Its runs are the text a reader sees
6379/// inside the wrapper. None for any other element, and for a wrapper whose
6380/// content [`with_run_wrapper_content`] could not write back, because `w`
6381/// does not name WordprocessingML there.
6382pub(crate) fn run_wrapper_paragraph(raw: &[u8], inherited: &[String]) -> Option<CT_P> {
6383    let mut reader = Reader::from_reader(raw);
6384    let mut buffer = Vec::new();
6385    let Ok(Event::Start(start)) = reader.read_event_into(&mut buffer) else {
6386        return None;
6387    };
6388    let prefixes = word_prefixes_at(&start, inherited).ok()?;
6389    if !["smartTag", "customXml", "fldSimple"]
6390        .iter()
6391        .any(|local| is_word_element(start.name().as_ref(), local.as_bytes(), &prefixes))
6392        || !prefixes.iter().any(|prefix| prefix == "w")
6393    {
6394        return None;
6395    }
6396    let (content_start, content_end) = run_wrapper_content_bounds(raw)?;
6397    let mut paragraph_xml = b"<w:p>".to_vec();
6398    paragraph_xml.extend_from_slice(&raw[content_start..content_end]);
6399    paragraph_xml.extend_from_slice(b"</w:p>");
6400    let mut paragraph_reader = Reader::from_reader(paragraph_xml.as_slice());
6401    let mut paragraph_buffer = Vec::new();
6402    let Ok(Event::Start(paragraph_start)) = paragraph_reader.read_event_into(&mut paragraph_buffer)
6403    else {
6404        return None;
6405    };
6406    CT_P::from_xml_with_prefixes_and_root(&mut paragraph_reader, &prefixes, Some(&paragraph_start))
6407        .ok()
6408}
6409
6410/// The source `raw` of a wrapper that [`run_wrapper_paragraph`] read, with
6411/// `paragraph` written back as its content between its own start and end
6412/// tags.
6413pub(crate) fn with_run_wrapper_content(raw: &[u8], paragraph: &CT_P) -> Result<Vec<u8>> {
6414    let missing = || OxmlError::MissingElement("wrapper content".to_owned());
6415    let mut writer = Writer::new(Vec::new());
6416    paragraph.to_xml(&mut writer)?;
6417    let paragraph_xml = writer.into_inner();
6418    // A paragraph without any content is written as `<w:p/>`.
6419    let content = match paragraph_xml.iter().position(|byte| *byte == b'>') {
6420        Some(end) if paragraph_xml[..end].ends_with(b"/") => &[][..],
6421        Some(end) => paragraph_xml[end + 1..]
6422            .strip_suffix(b"</w:p>")
6423            .ok_or_else(missing)?,
6424        None => return Err(missing()),
6425    };
6426    let (content_start, content_end) = run_wrapper_content_bounds(raw).ok_or_else(missing)?;
6427    let mut updated = raw[..content_start].to_vec();
6428    updated.extend_from_slice(content);
6429    updated.extend_from_slice(&raw[content_end..]);
6430    Ok(updated)
6431}
6432
6433/// The byte range of the content of the element `raw`, between the end of
6434/// its start tag and the start of its end tag.
6435fn run_wrapper_content_bounds(raw: &[u8]) -> Option<(usize, usize)> {
6436    let mut reader = Reader::from_reader(raw);
6437    let mut buffer = Vec::new();
6438    let Ok(Event::Start(_)) = reader.read_event_into(&mut buffer) else {
6439        return None;
6440    };
6441    let content_start = reader.buffer_position() as usize;
6442    let content_end = raw.iter().rposition(|byte| *byte == b'<')?;
6443    (content_start <= content_end).then_some((content_start, content_end))
6444}
6445
6446/// The preserved source of a run that holds only an unchanged simple field,
6447/// with the Word prefixes it was read with.
6448pub(crate) fn simple_field_source(run: &CT_R) -> Option<(&[u8], &[String])> {
6449    let [RunContent::Field(field)] = run.content.as_slice() else {
6450        return None;
6451    };
6452    match &field.source {
6453        FieldSource::Parsed {
6454            form: FieldForm::Simple,
6455            raw_xml,
6456            word_prefixes,
6457            ..
6458        } if field.is_unchanged() => Some((raw_xml, word_prefixes)),
6459        _ => None,
6460    }
6461}
6462
6463/// Read the simple field of `run` again from the source `raw`, which
6464/// [`with_run_wrapper_content`] wrote. Returns false, and changes nothing,
6465/// when `run` holds no unchanged simple field or `raw` is not one.
6466pub(crate) fn set_simple_field_source(run: &mut CT_R, raw: &[u8]) -> Result<bool> {
6467    let Some((_, prefixes)) = simple_field_source(run) else {
6468        return Ok(false);
6469    };
6470    let Some(field) = parse_simple_field(raw, prefixes)? else {
6471        return Ok(false);
6472    };
6473    run.content = vec![RunContent::Field(field)];
6474    Ok(true)
6475}
6476
6477/// Append the text of the accepted view of `paragraph` to `output`, see
6478/// [`CT_P::accepted_text`].
6479fn append_accepted_text(paragraph: &CT_P, output: &mut String) {
6480    let wrapper_text = |raw: &[u8], prefixes: &[String], output: &mut String| {
6481        if let Some(content) = run_wrapper_paragraph(raw, prefixes) {
6482            append_accepted_text(&content, output);
6483        }
6484    };
6485    for boundary in 0..=paragraph.runs.len() {
6486        for owner in boundary_owners(paragraph, boundary) {
6487            let mut runs = Vec::new();
6488            match owner {
6489                BoundaryOwner::ContentControl(index) => {
6490                    append_accepted_control_runs(&paragraph.content_controls[index].3, &mut runs);
6491                }
6492                BoundaryOwner::Revision(index) => {
6493                    append_accepted_revision_runs(&paragraph.revisions[index].2, &mut runs);
6494                }
6495                BoundaryOwner::Wrapper(index) => {
6496                    wrapper_text(&paragraph.extra_xml[index].1, &["w".to_owned()], output);
6497                }
6498            }
6499            output.extend(runs.iter().map(|run| run.text()));
6500        }
6501        if let Some(run) = paragraph.runs.get(boundary) {
6502            match simple_field_source(run) {
6503                Some((raw, prefixes)) => wrapper_text(raw, prefixes, output),
6504                None => output.push_str(&run.text()),
6505            }
6506        }
6507    }
6508}
6509
6510struct CommentTextProjection {
6511    text: String,
6512    open: bool,
6513    start: Option<usize>,
6514    end: Option<usize>,
6515    start_offset: Option<usize>,
6516    end_offset: Option<usize>,
6517    markers: Vec<std::ops::Range<usize>>,
6518}
6519
6520fn comment_projection_paragraph(
6521    xml: &[u8],
6522    runs: &[std::ops::Range<usize>],
6523    fields: &[std::ops::Range<usize>],
6524    window: std::ops::Range<usize>,
6525) -> Result<CT_P> {
6526    let kept = |span: &std::ops::Range<usize>| window.start <= span.start && span.end <= window.end;
6527    let mut removed = runs
6528        .iter()
6529        .filter(|span| !kept(span))
6530        .cloned()
6531        .collect::<Vec<_>>();
6532    removed.extend(
6533        fields
6534            .iter()
6535            .filter(|field| {
6536                !kept(field)
6537                    && !runs
6538                        .iter()
6539                        .any(|run| field.start <= run.start && run.end <= field.end && kept(run))
6540            })
6541            .cloned(),
6542    );
6543    removed.sort_by_key(|span| (span.start, std::cmp::Reverse(span.end)));
6544    let mut outer = Vec::<std::ops::Range<usize>>::new();
6545    for span in removed {
6546        if outer
6547            .last()
6548            .is_some_and(|previous| span.end <= previous.end)
6549        {
6550            continue;
6551        }
6552        outer.push(span);
6553    }
6554    let mut projected = xml.to_vec();
6555    for span in outer.into_iter().rev() {
6556        projected.drain(span);
6557    }
6558    CT_P::from_xml_fragment(&projected)
6559}
6560
6561fn project_comment_text(xml: &[u8], id: i32, incoming: bool) -> Result<CommentTextProjection> {
6562    let invalid = |reason: &str| OxmlError::InvalidValue(format!("comment {id}: {reason}"));
6563    let mut reader = NsReader::from_reader(xml);
6564    reader.config_mut().trim_text(false);
6565    let mut stack = Vec::<(Vec<u8>, bool, usize, Option<usize>)>::new();
6566    let mut runs = Vec::new();
6567    let mut fields = Vec::new();
6568    let mut markers = Vec::<(bool, std::ops::Range<usize>)>::new();
6569    let mut buffer = Vec::new();
6570    loop {
6571        let before = reader.buffer_position() as usize;
6572        let (namespace, event) = reader.read_resolved_event_into(&mut buffer)?;
6573        let word = matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == crate::namespace::W_NS.as_bytes()
6574            || std::str::from_utf8(uri).ok().and_then(|uri| quick_xml::escape::unescape(uri).ok()).is_some_and(|uri| uri == crate::namespace::W_NS));
6575        drop(namespace);
6576        let after = reader.buffer_position() as usize;
6577        match event {
6578            Event::Start(ref element) | Event::Empty(ref element) => {
6579                let local = element.local_name().as_ref().to_vec();
6580                let mut marker = None;
6581                if word && matches!(local.as_slice(), b"commentRangeStart" | b"commentRangeEnd") {
6582                    let mut selected_id = None;
6583                    for attribute in element.attributes() {
6584                        let attribute = attribute?;
6585                        let (namespace, name) = reader.resolver().resolve_attribute(attribute.key);
6586                        if name.as_ref() == b"id"
6587                            && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == crate::namespace::W_NS.as_bytes())
6588                        {
6589                            if selected_id.is_some() {
6590                                return Err(invalid("duplicate identity attribute"));
6591                            }
6592                            selected_id = Some(
6593                                attribute
6594                                    .decoded_and_normalized_value(
6595                                        XmlVersion::Implicit1_0,
6596                                        reader.decoder(),
6597                                    )?
6598                                    .parse::<i32>()
6599                                    .map_err(|_| invalid("invalid marker id"))?,
6600                            );
6601                        }
6602                    }
6603                    if selected_id == Some(id)
6604                        && !stack
6605                            .iter()
6606                            .any(|(name, word, _, _)| *word && name == b"txbxContent")
6607                    {
6608                        if stack.iter().any(|(name, word, _, _)| {
6609                            *word
6610                                && matches!(name.as_slice(), b"del" | b"moveFrom" | b"txbxContent")
6611                        }) {
6612                            return Err(invalid(
6613                                "selected source is outside the accepted paragraph view",
6614                            ));
6615                        }
6616                        if stack.iter().any(|(name, word, _, _)| *word && name == b"r") {
6617                            return Err(invalid(
6618                                "existing accepted run axis cannot faithfully represent a mid-run source boundary",
6619                            ));
6620                        }
6621                        if stack.iter().any(|(name, word, _, _)| {
6622                            !*word
6623                                || !matches!(
6624                                    name.as_slice(),
6625                                    b"p" | b"sdt"
6626                                        | b"sdtContent"
6627                                        | b"hyperlink"
6628                                        | b"ins"
6629                                        | b"moveTo"
6630                                        | b"smartTag"
6631                                        | b"customXml"
6632                                        | b"fldSimple"
6633                                )
6634                        }) {
6635                            return Err(invalid(
6636                                "selected source has an unsupported opaque owner outside the accepted paragraph view",
6637                            ));
6638                        }
6639                        markers.push((local == b"commentRangeStart", before..after));
6640                        marker = Some(markers.len() - 1);
6641                    }
6642                }
6643                if matches!(event, Event::Start(_)) {
6644                    stack.push((local, word, before, marker));
6645                } else if word && local == b"r" {
6646                    runs.push(before..after);
6647                } else if word && local == b"fldSimple" {
6648                    fields.push(before..after);
6649                }
6650            }
6651            Event::End(_) => {
6652                if let Some((name, word, start, marker)) = stack.pop() {
6653                    if let Some(index) = marker {
6654                        if !xml[markers[index].1.end..before]
6655                            .iter()
6656                            .all(u8::is_ascii_whitespace)
6657                        {
6658                            return Err(invalid(
6659                                "selected range marker has unsupported child content",
6660                            ));
6661                        }
6662                        markers[index].1.end = after;
6663                    }
6664                    if word && name == b"r" {
6665                        runs.push(start..after);
6666                    }
6667                    if word && name == b"fldSimple" {
6668                        fields.push(start..after);
6669                    }
6670                }
6671            }
6672            Event::Eof => break,
6673            _ => {}
6674        }
6675        buffer.clear();
6676    }
6677    let mut open = incoming;
6678    let mut start_span = None;
6679    let mut end_span = None;
6680    for (start, span) in &markers {
6681        if *start {
6682            if open || start_span.is_some() {
6683                return Err(invalid("duplicate or reversed selected start"));
6684            }
6685            start_span = Some(span.clone());
6686            open = true;
6687        } else {
6688            if !open || end_span.is_some() {
6689                return Err(invalid("unmatched or reversed selected end"));
6690            }
6691            end_span = Some(span.clone());
6692            open = false;
6693        }
6694    }
6695    let prefix = |at: usize| -> Result<(usize, usize)> {
6696        let p = comment_projection_paragraph(xml, &runs, &fields, 0..at)?;
6697        Ok((p.accepted_run_paths().len(), p.accepted_text().len()))
6698    };
6699    let start = start_span
6700        .as_ref()
6701        .map(|span| prefix(span.start))
6702        .transpose()?;
6703    let end = end_span
6704        .as_ref()
6705        .map(|span| prefix(span.start))
6706        .transpose()?;
6707    let text = if incoming || start_span.is_some() {
6708        comment_projection_paragraph(
6709            xml,
6710            &runs,
6711            &fields,
6712            start_span.as_ref().map_or(0, |span| span.end)
6713                ..end_span.as_ref().map_or(xml.len(), |span| span.start),
6714        )?
6715        .accepted_text()
6716    } else {
6717        String::new()
6718    };
6719    Ok(CommentTextProjection {
6720        text,
6721        open,
6722        start: start.map(|value| value.0),
6723        end: end.map(|value| value.0),
6724        start_offset: start.map(|value| value.1),
6725        end_offset: end.map(|value| value.1),
6726        markers: markers.into_iter().map(|(_, span)| span).collect(),
6727    })
6728}
6729
6730fn accepted_paragraph_run_paths(paragraph: &CT_P) -> Vec<AcceptedRunPath> {
6731    let mut output = Vec::new();
6732    let mut prefix = Vec::new();
6733    append_accepted_paragraph_run_paths(paragraph, &mut prefix, &mut output);
6734    output
6735}
6736
6737pub(crate) fn append_accepted_paragraph_run_paths(
6738    paragraph: &CT_P,
6739    prefix: &mut Vec<AcceptedRunPathSegment>,
6740    output: &mut Vec<AcceptedRunPath>,
6741) {
6742    for boundary in 0..=paragraph.runs.len() {
6743        for owner in boundary_owners(paragraph, boundary) {
6744            match owner {
6745                BoundaryOwner::ContentControl(index) => {
6746                    prefix.push(AcceptedRunPathSegment::ContentControl(index));
6747                    paragraph.content_controls[index]
6748                        .3
6749                        .append_accepted_run_paths(prefix, output);
6750                }
6751                BoundaryOwner::Revision(index) => {
6752                    prefix.push(AcceptedRunPathSegment::Revision(index));
6753                    paragraph.revisions[index]
6754                        .2
6755                        .append_accepted_run_paths(prefix, output);
6756                }
6757                BoundaryOwner::Wrapper(_) => continue,
6758            }
6759            prefix.pop();
6760        }
6761        if paragraph.runs.get(boundary).is_some() {
6762            prefix.push(AcceptedRunPathSegment::Run(boundary));
6763            output.push(AcceptedRunPath {
6764                segments: prefix.clone(),
6765            });
6766            prefix.pop();
6767        }
6768    }
6769}
6770
6771fn accepted_paragraph_runs(paragraph: &CT_P) -> Vec<&CT_R> {
6772    let mut output = Vec::new();
6773    for boundary in 0..=paragraph.runs.len() {
6774        for owner in boundary_owners(paragraph, boundary) {
6775            match owner {
6776                BoundaryOwner::ContentControl(index) => {
6777                    append_accepted_control_runs(&paragraph.content_controls[index].3, &mut output);
6778                }
6779                BoundaryOwner::Revision(index) => {
6780                    append_accepted_revision_runs(&paragraph.revisions[index].2, &mut output);
6781                }
6782                BoundaryOwner::Wrapper(_) => {}
6783            }
6784        }
6785        if let Some(run) = paragraph.runs.get(boundary) {
6786            output.push(run);
6787        }
6788    }
6789    output
6790}
6791
6792fn append_accepted_control_runs<'a>(control: &'a CT_Sdt, output: &mut Vec<&'a CT_R>) {
6793    for boundary in 0..=control.content.len() {
6794        for (_, revision) in control.revisions().iter().filter(|(at, _)| *at == boundary) {
6795            append_accepted_revision_runs(revision, output);
6796        }
6797        let Some(content) = control.content.get(boundary) else {
6798            continue;
6799        };
6800        match content {
6801            SdtContent::Run(run) => output.push(run),
6802            SdtContent::ContentControl(control) => append_accepted_control_runs(control, output),
6803            SdtContent::Paragraph(paragraph) => output.extend(accepted_paragraph_runs(paragraph)),
6804            SdtContent::Table(table) => append_accepted_table_runs(table, output),
6805            SdtContent::Row(row) => append_accepted_row_runs(row, output),
6806            SdtContent::Cell(cell) => append_accepted_cell_runs(cell, output),
6807            SdtContent::RawXml(_) => {}
6808        }
6809    }
6810}
6811
6812fn append_accepted_table_runs<'a>(table: &'a CT_Tbl, output: &mut Vec<&'a CT_R>) {
6813    for boundary in 0..=table.rows.len() {
6814        for (_, _, control) in table
6815            .content_controls
6816            .iter()
6817            .filter(|(at, _, _)| *at == boundary)
6818        {
6819            append_accepted_control_runs(control, output);
6820        }
6821        if let Some(row) = table.rows.get(boundary) {
6822            append_accepted_row_runs(row, output);
6823        }
6824    }
6825}
6826
6827fn append_accepted_row_runs<'a>(row: &'a CT_Row, output: &mut Vec<&'a CT_R>) {
6828    for boundary in 0..=row.cells.len() {
6829        for (_, _, control) in row
6830            .content_controls
6831            .iter()
6832            .filter(|(at, _, _)| *at == boundary)
6833        {
6834            append_accepted_control_runs(control, output);
6835        }
6836        if let Some(cell) = row.cells.get(boundary) {
6837            append_accepted_cell_runs(cell, output);
6838        }
6839    }
6840}
6841
6842fn append_accepted_cell_runs<'a>(cell: &'a CT_Tc, output: &mut Vec<&'a CT_R>) {
6843    for content in &cell.content {
6844        match content {
6845            CellContent::Paragraph(paragraph) => output.extend(accepted_paragraph_runs(paragraph)),
6846            CellContent::Table(table) => append_accepted_table_runs(table, output),
6847            CellContent::ContentControl(control) => append_accepted_control_runs(control, output),
6848        }
6849    }
6850}
6851
6852fn tracked_paragraph_runs(paragraph: &CT_P) -> Vec<&CT_R> {
6853    let mut output = Vec::new();
6854    for boundary in 0..=paragraph.runs.len() {
6855        for owner in boundary_owners(paragraph, boundary) {
6856            match owner {
6857                BoundaryOwner::ContentControl(index) => {
6858                    append_tracked_control_runs(&paragraph.content_controls[index].3, &mut output);
6859                }
6860                BoundaryOwner::Revision(index) => {
6861                    append_tracked_revision_runs(&paragraph.revisions[index].2, &mut output);
6862                }
6863                BoundaryOwner::Wrapper(_) => {}
6864            }
6865        }
6866        if let Some(run) = paragraph.runs.get(boundary) {
6867            output.push(run);
6868        }
6869    }
6870    output
6871}
6872
6873fn append_tracked_control_runs<'a>(control: &'a CT_Sdt, output: &mut Vec<&'a CT_R>) {
6874    for boundary in 0..=control.content.len() {
6875        for (_, revision) in control.revisions().iter().filter(|(at, _)| *at == boundary) {
6876            append_tracked_revision_runs(revision, output);
6877        }
6878        let Some(content) = control.content.get(boundary) else {
6879            continue;
6880        };
6881        match content {
6882            SdtContent::Run(run) => output.push(run),
6883            SdtContent::ContentControl(control) => append_tracked_control_runs(control, output),
6884            SdtContent::Paragraph(paragraph) => output.extend(tracked_paragraph_runs(paragraph)),
6885            SdtContent::Table(table) => append_tracked_table_runs(table, output),
6886            SdtContent::Row(row) => append_tracked_row_runs(row, output),
6887            SdtContent::Cell(cell) => append_tracked_cell_runs(cell, output),
6888            SdtContent::RawXml(_) => {}
6889        }
6890    }
6891}
6892
6893fn append_tracked_revision_runs<'a>(revision: &'a CT_Revision, output: &mut Vec<&'a CT_R>) {
6894    if let Some(paragraph) = revision.content_paragraph() {
6895        output.extend(tracked_paragraph_runs(paragraph));
6896        return;
6897    }
6898    let crate::revision::RevisionContent::Runs(runs) = revision.content() else {
6899        return;
6900    };
6901    for boundary in 0..=runs.len() {
6902        for (_, nested) in revision
6903            .nested_revisions()
6904            .iter()
6905            .filter(|(at, _)| *at == boundary)
6906        {
6907            append_tracked_revision_runs(nested, output);
6908        }
6909        if let Some(run) = runs.get(boundary) {
6910            output.push(run);
6911        }
6912    }
6913}
6914
6915fn append_tracked_table_runs<'a>(table: &'a CT_Tbl, output: &mut Vec<&'a CT_R>) {
6916    for boundary in 0..=table.rows.len() {
6917        for (_, _, control) in table
6918            .content_controls
6919            .iter()
6920            .filter(|(at, _, _)| *at == boundary)
6921        {
6922            append_tracked_control_runs(control, output);
6923        }
6924        if let Some(row) = table.rows.get(boundary) {
6925            append_tracked_row_runs(row, output);
6926        }
6927    }
6928}
6929
6930fn append_tracked_row_runs<'a>(row: &'a CT_Row, output: &mut Vec<&'a CT_R>) {
6931    for boundary in 0..=row.cells.len() {
6932        for (_, _, control) in row
6933            .content_controls
6934            .iter()
6935            .filter(|(at, _, _)| *at == boundary)
6936        {
6937            append_tracked_control_runs(control, output);
6938        }
6939        if let Some(cell) = row.cells.get(boundary) {
6940            append_tracked_cell_runs(cell, output);
6941        }
6942    }
6943}
6944
6945fn append_tracked_cell_runs<'a>(cell: &'a CT_Tc, output: &mut Vec<&'a CT_R>) {
6946    for content in &cell.content {
6947        match content {
6948            CellContent::Paragraph(paragraph) => output.extend(tracked_paragraph_runs(paragraph)),
6949            CellContent::Table(table) => append_tracked_table_runs(table, output),
6950            CellContent::ContentControl(control) => append_tracked_control_runs(control, output),
6951        }
6952    }
6953}
6954
6955// Markdown's accepted projection carries physical scope boundaries separately
6956// from the typed runs. Default accepted-view readers keep their existing walk.
6957fn append_accepted_inline_paragraph(
6958    paragraph: &CT_P,
6959    runs: &mut Vec<CT_R>,
6960    boundaries: &mut Vec<usize>,
6961) {
6962    let (view, inner) = paragraph.accepted_view_projection(true);
6963    let offset = runs.len();
6964    let view = view.into_owned();
6965    if !view.runs.is_empty() {
6966        boundaries.push(offset);
6967        boundaries.extend(inner.into_iter().map(|at| offset + at));
6968        boundaries.push(offset + view.runs.len());
6969        runs.extend(view.runs);
6970    }
6971}
6972
6973fn append_accepted_inline_revision(
6974    revision: &CT_Revision,
6975    runs: &mut Vec<CT_R>,
6976    boundaries: &mut Vec<usize>,
6977) {
6978    if matches!(
6979        revision.kind(),
6980        RevisionKind::Insertion | RevisionKind::MoveTo
6981    ) && let Some(paragraph) = revision.content_paragraph()
6982    {
6983        append_accepted_inline_paragraph(paragraph, runs, boundaries);
6984    }
6985}
6986
6987fn append_accepted_inline_control(
6988    control: &CT_Sdt,
6989    runs: &mut Vec<CT_R>,
6990    boundaries: &mut Vec<usize>,
6991) {
6992    let start = runs.len();
6993    let boundary_start = boundaries.len();
6994    boundaries.push(start);
6995    for boundary in 0..=control.content.len() {
6996        for (_, revision) in control.revisions().iter().filter(|(at, _)| *at == boundary) {
6997            append_accepted_inline_revision(revision, runs, boundaries);
6998        }
6999        let Some(content) = control.content.get(boundary) else {
7000            continue;
7001        };
7002        match content {
7003            SdtContent::Run(run) => runs.push(run.clone()),
7004            SdtContent::ContentControl(nested) => {
7005                append_accepted_inline_control(nested, runs, boundaries)
7006            }
7007            SdtContent::Paragraph(paragraph) => {
7008                append_accepted_inline_paragraph(paragraph, runs, boundaries)
7009            }
7010            // Block carriers are emitted separately by Markdown. Retain the
7011            // accepted typed projection if encountered in a synthetic inline model.
7012            SdtContent::Table(table) => {
7013                let mut owned = Vec::new();
7014                append_accepted_table_runs(table, &mut owned);
7015                boundaries.push(runs.len());
7016                runs.extend(owned.into_iter().cloned());
7017                boundaries.push(runs.len());
7018            }
7019            SdtContent::Row(row) => {
7020                let mut owned = Vec::new();
7021                append_accepted_row_runs(row, &mut owned);
7022                boundaries.push(runs.len());
7023                runs.extend(owned.into_iter().cloned());
7024                boundaries.push(runs.len());
7025            }
7026            SdtContent::Cell(cell) => {
7027                let mut owned = Vec::new();
7028                append_accepted_cell_runs(cell, &mut owned);
7029                boundaries.push(runs.len());
7030                runs.extend(owned.into_iter().cloned());
7031                boundaries.push(runs.len());
7032            }
7033            SdtContent::RawXml(_) => {}
7034        }
7035    }
7036    if runs.len() == start {
7037        boundaries.truncate(boundary_start);
7038    } else {
7039        boundaries.push(runs.len());
7040    }
7041}
7042
7043fn append_accepted_revision_runs<'a>(revision: &'a CT_Revision, output: &mut Vec<&'a CT_R>) {
7044    if !matches!(
7045        revision.kind(),
7046        RevisionKind::Insertion | RevisionKind::MoveTo
7047    ) {
7048        return;
7049    }
7050    if let Some(paragraph) = revision.content_paragraph() {
7051        output.extend(accepted_paragraph_runs(paragraph));
7052    }
7053}
7054
7055#[allow(non_snake_case)]
7056impl CT_P {
7057    pub fn new() -> Self {
7058        CT_P {
7059            properties: None,
7060            runs: Vec::new(),
7061            hyperlinks: Vec::new(),
7062            comment_ranges: Vec::new(),
7063            bookmark_markers: Vec::new(),
7064            extra_xml: Vec::new(),
7065            content_controls: Vec::new(),
7066            revisions: Vec::new(),
7067            equations: Vec::new(),
7068            rubies: Vec::new(),
7069        }
7070    }
7071
7072    /// Report whether one raw paragraph carrier retains root attributes.
7073    #[doc(hidden)]
7074    pub fn raw_is_root_attributes(position: usize, raw: &[u8]) -> bool {
7075        position == ROOT_ATTRIBUTES_POSITION && is_root_attribute_record(raw)
7076    }
7077
7078    pub(crate) fn from_empty_root(root: &BytesStart<'_>, word_prefixes: &[String]) -> Result<Self> {
7079        let mut paragraph = Self::new();
7080        if let Some(record) = capture_root_attribute_record(root, word_prefixes)? {
7081            paragraph.extra_xml.push((ROOT_ATTRIBUTES_POSITION, record));
7082        }
7083        Ok(paragraph)
7084    }
7085
7086    /// Get the combined text of all runs in this paragraph.
7087    pub fn text(&self) -> String {
7088        self.runs().iter().map(|run| run.text()).collect()
7089    }
7090
7091    /// Return valid complex `HYPERLINK` fields projected into synthetic runs.
7092    pub fn complex_field_hyperlinks(&self) -> Vec<ComplexFieldHyperlink> {
7093        self.runs
7094            .iter()
7095            .enumerate()
7096            .filter_map(|(run_index, run)| {
7097                let [RunContent::Field(field)] = run.content.as_slice() else {
7098                    return None;
7099                };
7100                if !field.is_parsed_complex()
7101                    || field.dirty == Some(true)
7102                    || field.cached_result.is_empty()
7103                    || field.instruction.name != "HYPERLINK"
7104                {
7105                    return None;
7106                }
7107                let target = field.instruction.arguments.iter().find_map(|argument| {
7108                    let FieldArgument::Text(target) = argument else {
7109                        return None;
7110                    };
7111                    (!target.is_empty()).then(|| target.clone())
7112                })?;
7113                Some(ComplexFieldHyperlink {
7114                    run_start: run_index,
7115                    run_end: run_index + 1,
7116                    target,
7117                })
7118            })
7119            .collect()
7120    }
7121
7122    /// Return direct and content-control-wrapped runs in document order.
7123    pub fn runs(&self) -> Vec<&CT_R> {
7124        let mut runs = Vec::new();
7125        self.collect_runs(&mut runs);
7126        runs
7127    }
7128
7129    /// Borrow typed source runs in physical order, including revision owners.
7130    /// Deleted runs reserve identity but selection remains the caller's job.
7131    /// Opaque wrapper XML does not become a typed source.
7132    #[doc(hidden)]
7133    pub fn source_runs(&self) -> Vec<&CT_R> {
7134        tracked_paragraph_runs(self)
7135    }
7136
7137    /// Return the text of the accepted view, as `Paragraph::text` reads it:
7138    /// the text of [`Self::accepted_bookmark_runs`], and that of the runs
7139    /// inside smart tags, inline custom XML elements and simple fields, in
7140    /// document order, nested ones included. The runs of a wrapper inside a
7141    /// content control, a revision or a hyperlink are not read.
7142    #[doc(hidden)]
7143    pub fn accepted_text(&self) -> String {
7144        let mut text = String::new();
7145        append_accepted_text(self, &mut text);
7146        text
7147    }
7148
7149    /// Project one selected comment span through the existing accepted text and run axis.
7150    /// Returns display, continuation state and optional start/end run boundaries.
7151    /// Original source is untouched. Unrepresentable source endpoints are errors.
7152    #[doc(hidden)]
7153    pub fn accepted_comment_projection(
7154        &self,
7155        id: i32,
7156        open_at_start: bool,
7157    ) -> Result<(String, bool, Option<usize>, Option<usize>)> {
7158        if self.comment_ranges.iter().any(|marker| match marker {
7159            CommentRangeMarker::Start {
7160                id: marker_id,
7161                has_child_content,
7162                ..
7163            }
7164            | CommentRangeMarker::End {
7165                id: marker_id,
7166                has_child_content,
7167                ..
7168            } => *marker_id == id && *has_child_content,
7169        }) {
7170            return Err(OxmlError::InvalidValue(format!(
7171                "comment {id}: selected range marker has unsupported child content"
7172            )));
7173        }
7174        let mut raw = Vec::new();
7175        self.to_xml(&mut Writer::new(&mut raw))?;
7176        Self::accepted_comment_source_projection(&raw, id, open_at_start)
7177    }
7178
7179    /// Project a comment from its namespace-closed original paragraph source.
7180    /// Retain its exact inherited bindings through the private fidelity probe.
7181    /// The accepted text, endpoint axis and unsupported-source checks are the
7182    /// same as [`Self::accepted_comment_projection`].
7183    #[doc(hidden)]
7184    #[allow(clippy::type_complexity)] // Concrete text, open state and two accepted endpoints.
7185    pub fn accepted_comment_source_projection(
7186        xml: &[u8],
7187        id: i32,
7188        open_at_start: bool,
7189    ) -> Result<(String, bool, Option<usize>, Option<usize>)> {
7190        let raw = raw_with_external_bindings(
7191            xml,
7192            &[("w".to_owned(), crate::namespace::W_NS.to_owned())],
7193        )?;
7194        let mut root_reader = Reader::from_reader(raw.as_slice());
7195        let bindings = loop {
7196            match root_reader.read_event()? {
7197                Event::Start(element) | Event::Empty(element) => {
7198                    break raw_namespace_declarations(&element)?;
7199                }
7200                Event::Eof => {
7201                    return Err(OxmlError::MissingElement("comment paragraph root".into()));
7202                }
7203                _ => {}
7204            }
7205        };
7206        let selected = project_comment_text(&raw, id, open_at_start)?;
7207        if !selected.markers.is_empty() {
7208            // Remove only this selected pair before asking the existing checked
7209            // writer to place the same endpoints. No temporary identity is allocated.
7210            let mut unmarked = raw;
7211            for span in selected.markers.iter().rev() {
7212                unmarked.drain(span.clone());
7213            }
7214            let mut probe = CT_P::from_xml_fragment(&unmarked)?;
7215            probe.anchor_accepted_range(selected.start, selected.end, RangeAnchor::Comment(id))
7216                .map_err(|error| OxmlError::InvalidValue(format!("comment {id}: existing accepted run axis cannot faithfully represent selected source: {error}")))?;
7217            let mut raw = Vec::new();
7218            probe.to_xml(&mut Writer::new(&mut raw))?;
7219            let raw = raw_with_external_bindings(&raw, &bindings)?;
7220            let raw = raw_with_external_bindings(
7221                &raw,
7222                &[("w".to_owned(), crate::namespace::W_NS.to_owned())],
7223            )?;
7224            let placed = project_comment_text(&raw, id, open_at_start)?;
7225            if selected.text != placed.text
7226                || selected.start_offset != placed.start_offset
7227                || selected.end_offset != placed.end_offset
7228            {
7229                return Err(OxmlError::InvalidValue(format!(
7230                    "comment {id}: existing accepted run axis cannot faithfully represent selected source"
7231                )));
7232            }
7233        }
7234        Ok((selected.text, selected.open, selected.start, selected.end))
7235    }
7236
7237    /// Return the accepted view as a paragraph of plain runs, for the
7238    /// exporters: the runs whose text [`Self::accepted_text`] reads, in
7239    /// document order. Those of content controls, tracked insertions and
7240    /// moves in, smart tags, inline custom XML elements and simple fields
7241    /// are included, deleted and moved-away runs are left out, and the
7242    /// hyperlink spans cover the same runs as in the source. Content
7243    /// controls, revisions, wrappers and markers are not kept. Borrowed when
7244    /// the paragraph holds none of them, since its runs are then the view.
7245    #[doc(hidden)]
7246    pub fn accepted_view(&self) -> Cow<'_, CT_P> {
7247        self.accepted_view_projection(false).0
7248    }
7249
7250    /// Read accepted content while retaining physical simple-field boundaries.
7251    /// Markdown uses these boundaries to keep independent formatting spans.
7252    #[doc(hidden)]
7253    pub fn accepted_view_with_field_owners(&self) -> (Cow<'_, CT_P>, Vec<usize>) {
7254        self.accepted_view_projection(true)
7255    }
7256
7257    fn accepted_view_projection(&self, preserve_field_owners: bool) -> (Cow<'_, CT_P>, Vec<usize>) {
7258        if self.content_controls.is_empty()
7259            && self.revisions.is_empty()
7260            && !self.extra_xml.iter().any(|(_, raw)| is_run_wrapper(raw))
7261            && !self
7262                .runs
7263                .iter()
7264                .any(|run| simple_field_source(run).is_some())
7265        {
7266            return (Cow::Borrowed(self), Vec::new());
7267        }
7268        let mut runs = Vec::new();
7269        let mut inline_boundaries = Vec::new();
7270        // The index into `self.hyperlinks` of the hyperlink each run is in.
7271        let mut links = Vec::new();
7272        for boundary in 0..=self.runs.len() {
7273            for owner in boundary_owners(self, boundary) {
7274                let mut owned = Vec::new();
7275                let link = match owner {
7276                    BoundaryOwner::ContentControl(index) => {
7277                        if preserve_field_owners {
7278                            let offset = runs.len();
7279                            append_accepted_inline_control(
7280                                &self.content_controls[index].3,
7281                                &mut runs,
7282                                &mut inline_boundaries,
7283                            );
7284                            links.extend(std::iter::repeat_n(None, runs.len() - offset));
7285                            continue;
7286                        }
7287                        append_accepted_control_runs(&self.content_controls[index].3, &mut owned);
7288                        None
7289                    }
7290                    BoundaryOwner::Revision(index) => {
7291                        let (_, slot, revision) = &self.revisions[index];
7292                        if preserve_field_owners {
7293                            let offset = runs.len();
7294                            append_accepted_inline_revision(
7295                                revision,
7296                                &mut runs,
7297                                &mut inline_boundaries,
7298                            );
7299                            links.extend(std::iter::repeat_n(
7300                                hyperlink_revision_index(*slot),
7301                                runs.len() - offset,
7302                            ));
7303                            continue;
7304                        }
7305                        append_accepted_revision_runs(revision, &mut owned);
7306                        hyperlink_revision_index(*slot)
7307                    }
7308                    BoundaryOwner::Wrapper(index) => {
7309                        if let Some(content) =
7310                            run_wrapper_paragraph(&self.extra_xml[index].1, &["w".to_owned()])
7311                        {
7312                            let (view, boundaries) =
7313                                content.accepted_view_projection(preserve_field_owners);
7314                            let offset = runs.len();
7315                            let view = view.into_owned();
7316                            if preserve_field_owners && !view.runs.is_empty() {
7317                                inline_boundaries.push(offset);
7318                                inline_boundaries
7319                                    .extend(boundaries.into_iter().map(|at| offset + at));
7320                                inline_boundaries.push(offset + view.runs.len());
7321                            }
7322                            links.extend(std::iter::repeat_n(None, view.runs.len()));
7323                            runs.extend(view.runs);
7324                        }
7325                        continue;
7326                    }
7327                };
7328                if preserve_field_owners && !owned.is_empty() {
7329                    inline_boundaries.push(runs.len());
7330                    inline_boundaries.push(runs.len() + owned.len());
7331                }
7332                links.extend(std::iter::repeat_n(link, owned.len()));
7333                runs.extend(owned.into_iter().cloned());
7334            }
7335            if let Some(run) = self.runs.get(boundary) {
7336                let link = self.hyperlinks.iter().position(|hyperlink| {
7337                    hyperlink.run_start <= boundary && boundary < hyperlink.run_end
7338                });
7339                // A simple field reads as the runs of its result, as in
7340                // `Self::accepted_text`, so a revision inside it is resolved.
7341                let field = simple_field_source(run)
7342                    .and_then(|(raw, prefixes)| run_wrapper_paragraph(raw, prefixes));
7343                match field {
7344                    Some(content) => {
7345                        let (view, boundaries) =
7346                            content.accepted_view_projection(preserve_field_owners);
7347                        let offset = runs.len();
7348                        let view = view.into_owned();
7349                        if preserve_field_owners && !view.runs.is_empty() {
7350                            inline_boundaries.push(offset);
7351                            inline_boundaries.extend(boundaries.into_iter().map(|at| offset + at));
7352                            inline_boundaries.push(offset + view.runs.len());
7353                        }
7354                        links.extend(std::iter::repeat_n(link, view.runs.len()));
7355                        runs.extend(view.runs);
7356                    }
7357                    None => {
7358                        runs.push(run.clone());
7359                        links.push(link);
7360                    }
7361                }
7362            }
7363        }
7364        let mut hyperlinks = Vec::new();
7365        let mut start = 0;
7366        while start < runs.len() {
7367            let mut end = start + 1;
7368            while end < runs.len() && links[end] == links[start] {
7369                end += 1;
7370            }
7371            if let Some(link) = links[start] {
7372                hyperlinks.push(HyperlinkSpan {
7373                    run_start: start,
7374                    run_end: end,
7375                    ..self.hyperlinks[link].clone()
7376                });
7377            }
7378            start = end;
7379        }
7380        (
7381            Cow::Owned(CT_P {
7382                properties: self.properties.clone(),
7383                runs,
7384                hyperlinks,
7385                ..CT_P::new()
7386            }),
7387            inline_boundaries,
7388        )
7389    }
7390
7391    /// Return the content of a preserved smart tag or inline custom XML
7392    /// element, which [`Self::accepted_view`] reads in place. None for any
7393    /// other raw paragraph child.
7394    #[doc(hidden)]
7395    pub fn raw_run_wrapper_content(raw: &[u8]) -> Option<CT_P> {
7396        if !is_run_wrapper(raw) {
7397            return None;
7398        }
7399        run_wrapper_paragraph(raw, &["w".to_owned()])
7400    }
7401
7402    /// Return accepted-view runs in the same order as bookmark projections.
7403    #[doc(hidden)]
7404    pub fn accepted_bookmark_runs(&self) -> Vec<&CT_R> {
7405        accepted_paragraph_runs(self)
7406    }
7407
7408    /// Return recursive addresses for every accepted-view run.
7409    #[doc(hidden)]
7410    pub fn accepted_run_paths(&self) -> Vec<AcceptedRunPath> {
7411        accepted_paragraph_run_paths(self)
7412    }
7413
7414    /// Resolve one recursive accepted-view run address.
7415    #[doc(hidden)]
7416    pub fn accepted_run(&self, path: &AcceptedRunPath) -> Option<&CT_R> {
7417        self.accepted_run_segments(path.segments())
7418    }
7419
7420    pub(crate) fn accepted_run_segments(&self, path: &[AcceptedRunPathSegment]) -> Option<&CT_R> {
7421        let (first, rest) = path.split_first()?;
7422        match *first {
7423            AcceptedRunPathSegment::Run(index) if rest.is_empty() => self.runs.get(index),
7424            AcceptedRunPathSegment::ContentControl(index) => self
7425                .content_controls
7426                .get(index)?
7427                .3
7428                .accepted_run_segments(rest),
7429            AcceptedRunPathSegment::Revision(index) => {
7430                self.revisions.get(index)?.2.accepted_run_segments(rest)
7431            }
7432            AcceptedRunPathSegment::Run(_) => None,
7433        }
7434    }
7435
7436    /// Replace one accepted-view run while retaining its recursive owner.
7437    #[doc(hidden)]
7438    pub fn replace_accepted_run(
7439        &mut self,
7440        path: &AcceptedRunPath,
7441        replacement: CT_R,
7442    ) -> Result<bool> {
7443        self.replace_accepted_run_segments(path.segments(), replacement)
7444    }
7445
7446    pub(crate) fn replace_accepted_run_segments(
7447        &mut self,
7448        path: &[AcceptedRunPathSegment],
7449        replacement: CT_R,
7450    ) -> Result<bool> {
7451        let Some((first, rest)) = path.split_first() else {
7452            return Ok(false);
7453        };
7454        match *first {
7455            AcceptedRunPathSegment::Run(index) if rest.is_empty() => {
7456                let Some(run) = self.runs.get_mut(index) else {
7457                    return Ok(false);
7458                };
7459                *run = replacement;
7460                Ok(true)
7461            }
7462            AcceptedRunPathSegment::ContentControl(index) => {
7463                let Some((_, _, _, control)) = self.content_controls.get_mut(index) else {
7464                    return Ok(false);
7465                };
7466                control.replace_accepted_run_segments(rest, replacement)
7467            }
7468            AcceptedRunPathSegment::Revision(index) => {
7469                let Some((_, _, revision)) = self.revisions.get_mut(index) else {
7470                    return Ok(false);
7471                };
7472                revision.replace_accepted_run_segments(rest, replacement)
7473            }
7474            AcceptedRunPathSegment::Run(_) => Ok(false),
7475        }
7476    }
7477
7478    /// Split one accepted-view run and return the accepted boundary.
7479    #[doc(hidden)]
7480    pub fn split_accepted_run(
7481        &mut self,
7482        path: &AcceptedRunPath,
7483        accepted_index: usize,
7484        offset: usize,
7485    ) -> Result<usize> {
7486        let run = self
7487            .accepted_run(path)
7488            .ok_or_else(|| OxmlError::InvalidValue("accepted run path is stale".to_owned()))?;
7489        let literal_len = run.literal_len();
7490        if offset > literal_len {
7491            return Err(OxmlError::InvalidValue(format!(
7492                "split offset {offset} exceeds a run's literal text length of {literal_len} characters"
7493            )));
7494        }
7495        if offset == 0 {
7496            return Ok(accepted_index);
7497        }
7498        if offset == literal_len {
7499            return Ok(accepted_index + 1);
7500        }
7501        if self.split_accepted_run_segments(path.segments(), offset)? {
7502            Ok(accepted_index + 1)
7503        } else {
7504            Err(OxmlError::InvalidValue(
7505                "accepted run path is stale".to_owned(),
7506            ))
7507        }
7508    }
7509
7510    /// Return the literal text of the accepted-view runs, the text that
7511    /// split offsets count. Tabs, breaks and other non-text content have no
7512    /// width.
7513    #[doc(hidden)]
7514    pub fn accepted_literal_text(&self) -> String {
7515        accepted_paragraph_runs(self)
7516            .into_iter()
7517            .flat_map(|run| run.content.iter().map(CT_R::literal_text))
7518            .collect()
7519    }
7520
7521    /// Return the text of every `t` element, in any namespace, between the
7522    /// range markers of comment `id`, or `None` when this paragraph does not
7523    /// hold both markers.
7524    ///
7525    /// Unlike [`Self::accepted_literal_text`], this includes the text of
7526    /// preserved children that the accepted view leaves out, such as a
7527    /// `w:fldSimple` result, so it is the text the commented range shows.
7528    #[doc(hidden)]
7529    pub fn comment_range_text(&self, id: i32) -> Option<String> {
7530        let mut xml = Vec::new();
7531        self.to_xml(&mut Writer::new(&mut xml)).ok()?;
7532        let is_marker = |element: &BytesStart<'_>, local: &[u8]| {
7533            matches_local_name(element.name().as_ref(), local)
7534                && element
7535                    .attributes()
7536                    .flatten()
7537                    .find(|attribute| matches_local_name(attribute.key.as_ref(), b"id"))
7538                    .and_then(|attribute| std::str::from_utf8(&attribute.value).ok()?.parse().ok())
7539                    == Some(id)
7540        };
7541        let mut reader = Reader::from_reader(xml.as_slice());
7542        let mut buffer = Vec::new();
7543        let mut text = None::<String>;
7544        loop {
7545            match reader.read_event_into(&mut buffer).ok()? {
7546                Event::Start(element) | Event::Empty(element)
7547                    if is_marker(&element, b"commentRangeStart") =>
7548                {
7549                    text = Some(String::new());
7550                }
7551                Event::Start(element) | Event::Empty(element)
7552                    if is_marker(&element, b"commentRangeEnd") =>
7553                {
7554                    return text;
7555                }
7556                Event::Start(element) if matches_local_name(element.name().as_ref(), b"t") => {
7557                    let content = crate::xml_text::read_element_text(&mut reader, element.name());
7558                    if let Some(text) = text.as_mut() {
7559                        text.push_str(&content);
7560                    }
7561                }
7562                Event::Eof => return None,
7563                _ => {}
7564            }
7565            buffer.clear();
7566        }
7567    }
7568
7569    /// Read the literal range safeguard from its namespace-qualified source.
7570    /// Only qualified Word markers, identity attributes and `t` elements count.
7571    /// Tabs and breaks have zero width, while field-result text remains visible.
7572    /// This does not change the richer accepted anchor projection.
7573    #[doc(hidden)]
7574    pub fn comment_range_text_from_source(xml: &[u8], id: i32) -> Result<Option<String>> {
7575        // Validate the complete source separately. Text extraction below skips
7576        // `t` subtrees and ends at the selected marker, neither of which may
7577        // hide a missing namespace declaration elsewhere in the candidate.
7578        let mut validation = NsReader::from_reader(xml);
7579        loop {
7580            let (namespace, event) = validation.read_resolved_event()?;
7581            if let ResolveResult::Unknown(prefix) = namespace {
7582                return Err(OxmlError::InvalidValue(format!(
7583                    "comment literal source has unresolved element namespace prefix {:?}",
7584                    String::from_utf8_lossy(&prefix)
7585                )));
7586            }
7587            match event {
7588                Event::Start(element) | Event::Empty(element) => {
7589                    for attribute in element.attributes() {
7590                        let attribute = attribute?;
7591                        attribute.decoded_and_normalized_value(
7592                            XmlVersion::Implicit1_0,
7593                            validation.decoder(),
7594                        )?;
7595                        if !namespace_declaration(attribute.key.as_ref())
7596                            && let (ResolveResult::Unknown(prefix), _) =
7597                                validation.resolver().resolve_attribute(attribute.key)
7598                        {
7599                            return Err(OxmlError::InvalidValue(format!(
7600                                "comment literal source has unresolved attribute namespace prefix {:?}",
7601                                String::from_utf8_lossy(&prefix)
7602                            )));
7603                        }
7604                    }
7605                }
7606                Event::Eof => break,
7607                _ => {}
7608            }
7609        }
7610        let mut reader = NsReader::from_reader(xml);
7611        let mut text = None::<String>;
7612        loop {
7613            let (namespace, event) = reader.read_resolved_event()?;
7614            let word = matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == crate::namespace::W_NS.as_bytes()
7615            || std::str::from_utf8(uri).ok().and_then(|uri| quick_xml::escape::unescape(uri).ok()).is_some_and(|uri| uri == crate::namespace::W_NS));
7616            drop(namespace);
7617            match event {
7618                Event::Start(element) | Event::Empty(element)
7619                    if word
7620                        && matches!(
7621                            element.local_name().as_ref(),
7622                            b"commentRangeStart" | b"commentRangeEnd"
7623                        ) =>
7624                {
7625                    let mut marker_id = None;
7626                    for attribute in element.attributes() {
7627                        let attribute = attribute?;
7628                        let (namespace, local) = reader.resolver().resolve_attribute(attribute.key);
7629                        if local.as_ref() == b"id"
7630                            && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == crate::namespace::W_NS.as_bytes())
7631                        {
7632                            marker_id = Some(
7633                                attribute
7634                                    .decoded_and_normalized_value(
7635                                        XmlVersion::Implicit1_0,
7636                                        reader.decoder(),
7637                                    )?
7638                                    .parse::<i32>()
7639                                    .map_err(|error| OxmlError::InvalidValue(error.to_string()))?,
7640                            );
7641                        }
7642                    }
7643                    if marker_id == Some(id) {
7644                        if element.local_name().as_ref() == b"commentRangeEnd" {
7645                            return Ok(text);
7646                        }
7647                        text = Some(String::new());
7648                    }
7649                }
7650                Event::Start(element) if word && element.local_name().as_ref() == b"t" => {
7651                    let encoded = reader.read_text(element.name())?;
7652                    if let Some(text) = text.as_mut() {
7653                        text.push_str(&crate::xml_text::decode_escaped(&encoded));
7654                    }
7655                }
7656                Event::Eof => return Ok(None),
7657                _ => {}
7658            }
7659        }
7660    }
7661
7662    /// Split accepted-view runs so that the non-empty literal text span
7663    /// `[start, end)`, in Unicode scalar values of
7664    /// [`Self::accepted_literal_text`], covers whole runs, and return the run
7665    /// boundaries around it.
7666    #[doc(hidden)]
7667    pub fn split_accepted_literal_span(
7668        &mut self,
7669        start: usize,
7670        end: usize,
7671    ) -> Result<(usize, usize)> {
7672        let lengths = accepted_paragraph_runs(self)
7673            .into_iter()
7674            .map(CT_R::literal_len)
7675            .collect::<Vec<_>>();
7676        // The run holding one literal character and its offset in that run.
7677        let locate = |character: usize| {
7678            let mut run_start = 0;
7679            lengths.iter().enumerate().find_map(|(index, len)| {
7680                let found = (character < run_start + len).then_some((index, character - run_start));
7681                run_start += len;
7682                found
7683            })
7684        };
7685        let outside = || {
7686            OxmlError::InvalidValue(format!(
7687                "literal span {start}..{end} is not inside the paragraph text"
7688            ))
7689        };
7690        let ((start_run, start_offset), (end_run, end_offset)) = match end.checked_sub(1) {
7691            Some(last) if start < end => (
7692                locate(start).ok_or_else(outside)?,
7693                locate(last).ok_or_else(outside)?,
7694            ),
7695            _ => return Err(outside()),
7696        };
7697        // The end goes first, so the start run keeps its index.
7698        let path = self.accepted_run_paths()[end_run].clone();
7699        let end_boundary = self.split_accepted_run(&path, end_run, end_offset + 1)?;
7700        let path = self.accepted_run_paths()[start_run].clone();
7701        let start_boundary = self.split_accepted_run(&path, start_run, start_offset)?;
7702        // A split inside the start run adds one run before the end boundary.
7703        Ok((start_boundary, end_boundary + usize::from(start_offset > 0)))
7704    }
7705
7706    pub(crate) fn split_accepted_run_segments(
7707        &mut self,
7708        path: &[AcceptedRunPathSegment],
7709        offset: usize,
7710    ) -> Result<bool> {
7711        let Some((first, rest)) = path.split_first() else {
7712            return Ok(false);
7713        };
7714        match *first {
7715            AcceptedRunPathSegment::Run(index) if rest.is_empty() => self
7716                .split_run(index, offset)
7717                .map(|_| true)
7718                .map_err(|error| OxmlError::InvalidValue(error.to_string())),
7719            AcceptedRunPathSegment::ContentControl(index) => {
7720                let Some((_, _, _, control)) = self.content_controls.get_mut(index) else {
7721                    return Ok(false);
7722                };
7723                control.split_accepted_run_segments(rest, offset)
7724            }
7725            AcceptedRunPathSegment::Revision(index) => {
7726                let Some((_, _, revision)) = self.revisions.get_mut(index) else {
7727                    return Ok(false);
7728                };
7729                revision.split_accepted_run_segments(rest, offset)
7730            }
7731            AcceptedRunPathSegment::Run(_) => Ok(false),
7732        }
7733    }
7734
7735    /// Remove one accepted-view run.
7736    ///
7737    /// The comment, bookmark and permission markers and the other preserved
7738    /// children around the run stay where they are. A run inside a
7739    /// hyperlink, an inline content control or a tracked insertion is removed
7740    /// inside it. A hyperlink or a tracked insertion left with nothing in it
7741    /// goes too, while a content control stays with its properties, as Word
7742    /// keeps an emptied control to show its placeholder. A run holding a
7743    /// comment, footnote or endnote reference, part of a complex field whose
7744    /// other parts are in other runs, or part of a tracked move destination
7745    /// is refused, since the note, the field or the move would lose its
7746    /// balance. On
7747    /// error the paragraph is unchanged.
7748    #[doc(hidden)]
7749    pub fn remove_accepted_run(&mut self, path: &AcceptedRunPath) -> Result<()> {
7750        let stale = || OxmlError::InvalidValue("accepted run path is stale".to_owned());
7751        let run = self.accepted_run(path).ok_or_else(stale)?;
7752        if let Some(id) = run.content.iter().find_map(|content| match content {
7753            RunContent::CommentReference { id, .. } => Some(*id),
7754            _ => None,
7755        }) {
7756            return Err(OxmlError::InvalidValue(format!(
7757                "it holds the reference of comment {id}, which removing the comment removes"
7758            )));
7759        }
7760        if let Some((kind, id)) = run.content.iter().find_map(|content| match content {
7761            RunContent::FootnoteRef { id, .. } => Some(("footnote", *id)),
7762            RunContent::EndnoteRef { id, .. } => Some(("endnote", *id)),
7763            _ => None,
7764        }) {
7765            return Err(OxmlError::InvalidValue(format!(
7766                "it holds the reference of {kind} {id}, which would be left without one"
7767            )));
7768        }
7769        if run
7770            .extra_xml
7771            .iter()
7772            .any(|raw| raw_is_complex_field_part(raw))
7773        {
7774            return Err(OxmlError::InvalidValue(
7775                "it holds part of a complex field whose other parts are in other runs".to_owned(),
7776            ));
7777        }
7778        let mut paragraph = self.clone();
7779        if !paragraph.remove_accepted_run_segments(path.segments())? {
7780            return Err(stale());
7781        }
7782        // Accepted run indexes after the run moved down by one.
7783        let _ = paragraph.refresh_bookmark_projection();
7784        *self = paragraph;
7785        Ok(())
7786    }
7787
7788    /// Remove the accepted-view run at `path`, and a tracked insertion left
7789    /// with nothing in it. Returns false for a stale path.
7790    pub(crate) fn remove_accepted_run_segments(
7791        &mut self,
7792        path: &[AcceptedRunPathSegment],
7793    ) -> Result<bool> {
7794        let Some((first, rest)) = path.split_first() else {
7795            return Ok(false);
7796        };
7797        match *first {
7798            AcceptedRunPathSegment::Run(index) if rest.is_empty() => {
7799                if index >= self.runs.len() {
7800                    return Ok(false);
7801                }
7802                let mut removed = vec![false; self.runs.len()];
7803                removed[index] = true;
7804                self.remove_runs(&removed);
7805                Ok(true)
7806            }
7807            AcceptedRunPathSegment::ContentControl(index) => {
7808                let Some((_, _, _, control)) = self.content_controls.get_mut(index) else {
7809                    return Ok(false);
7810                };
7811                control.remove_accepted_run_segments(rest)
7812            }
7813            AcceptedRunPathSegment::Revision(index) => {
7814                let Some((_, _, revision)) = self.revisions.get_mut(index) else {
7815                    return Ok(false);
7816                };
7817                match revision.remove_accepted_run_segments(rest)? {
7818                    None => Ok(false),
7819                    Some(emptied) => {
7820                        if emptied {
7821                            self.remove_revision(index);
7822                        }
7823                        Ok(true)
7824                    }
7825                }
7826            }
7827            AcceptedRunPathSegment::Run(_) => Ok(false),
7828        }
7829    }
7830
7831    /// Whether the paragraph has no child at all, attributes aside.
7832    pub(crate) fn has_no_children(&self) -> bool {
7833        self.runs.is_empty()
7834            && self.hyperlinks.is_empty()
7835            && self.comment_ranges.is_empty()
7836            && self.bookmark_markers.is_empty()
7837            && self.content_controls.is_empty()
7838            && self.revisions.is_empty()
7839            && self.equations.is_empty()
7840            && self.rubies.is_empty()
7841            && self
7842                .extra_xml
7843                .iter()
7844                .all(|(position, raw)| Self::raw_is_root_attributes(*position, raw))
7845    }
7846
7847    /// Remove the revision wrapper at `index` with the placeholder that holds
7848    /// its place among the paragraph children, or its place inside a
7849    /// hyperlink, and the hyperlink when nothing else is left in it.
7850    fn remove_revision(&mut self, index: usize) {
7851        let (boundary, slot, _) = self.revisions.remove(index);
7852        let Some(hyperlink_index) = hyperlink_revision_index(slot) else {
7853            self.remove_boundary_raw(boundary, slot);
7854            return;
7855        };
7856        // The children of a hyperlink count the revisions written before them.
7857        let position = self.revisions[..index]
7858            .iter()
7859            .filter(|(at, other, _)| *at == boundary && *other == slot)
7860            .count();
7861        let Some(hyperlink) = self.hyperlinks.get_mut(hyperlink_index) else {
7862            return;
7863        };
7864        let relative = boundary.saturating_sub(hyperlink.run_start);
7865        for (at, revisions_before, _) in &mut hyperlink.extra_xml {
7866            if *at == relative && *revisions_before > position {
7867                *revisions_before -= 1;
7868            }
7869        }
7870        let emptied = hyperlink.run_start == hyperlink.run_end
7871            && hyperlink.extra_xml.is_empty()
7872            && !self.revisions.iter().any(|(_, other, _)| *other == slot);
7873        if !emptied {
7874            return;
7875        }
7876        let hyperlink = self.hyperlinks.remove(hyperlink_index);
7877        for (_, other, _) in &mut self.revisions {
7878            if let Some(later) = hyperlink_revision_index(*other)
7879                && later > hyperlink_index
7880            {
7881                *other = hyperlink_revision_slot(later - 1);
7882            }
7883        }
7884        if let Some(raw_before) = hyperlink.preserved_raw_before {
7885            self.remove_boundary_raw(hyperlink.run_start, raw_before);
7886        }
7887    }
7888
7889    /// Remove the preserved child at `slot` among those at run `boundary`,
7890    /// and move every projection that counts the children before it. The
7891    /// children on both sides of it then share one slot, where those that
7892    /// came before it stay first.
7893    fn remove_boundary_raw(&mut self, boundary: usize, slot: usize) {
7894        let Some(position) = self
7895            .extra_xml
7896            .iter()
7897            .enumerate()
7898            .filter(|(_, (at, _))| *at == boundary)
7899            .nth(slot)
7900            .map(|(position, _)| position)
7901        else {
7902            return;
7903        };
7904        self.extra_xml.remove(position);
7905        self.comment_ranges
7906            .sort_by_key(|marker| (marker.run_index(), marker.raw_before()));
7907        let merged_markers = self
7908            .comment_ranges
7909            .iter()
7910            .filter(|marker| marker.run_index() == boundary && marker.raw_before() == slot)
7911            .count();
7912        let shift = |before: &mut usize| {
7913            if *before > slot {
7914                *before -= 1;
7915            }
7916        };
7917        for marker in &mut self.comment_ranges {
7918            match marker {
7919                CommentRangeMarker::Start {
7920                    run_index,
7921                    raw_before,
7922                    ..
7923                }
7924                | CommentRangeMarker::End {
7925                    run_index,
7926                    raw_before,
7927                    ..
7928                } if *run_index == boundary => shift(raw_before),
7929                _ => {}
7930            }
7931        }
7932        for (at, raw_before, markers_before, _) in &mut self.content_controls {
7933            if *at == boundary {
7934                if *raw_before == slot + 1 {
7935                    *markers_before += merged_markers;
7936                }
7937                shift(raw_before);
7938            }
7939        }
7940        for marker in &mut self.bookmark_markers {
7941            if marker.run_index == boundary {
7942                shift(&mut marker.raw_before);
7943            }
7944        }
7945        for (at, other, _) in &mut self.revisions {
7946            if *at == boundary && hyperlink_revision_index(*other).is_none() {
7947                shift(other);
7948            }
7949        }
7950        for (at, other, _) in &mut self.equations {
7951            if *at == boundary {
7952                shift(other);
7953            }
7954        }
7955        for hyperlink in &mut self.hyperlinks {
7956            if hyperlink.run_start == boundary
7957                && let Some(raw_before) = hyperlink.preserved_raw_before.as_mut()
7958            {
7959                shift(raw_before);
7960            }
7961        }
7962    }
7963
7964    /// Add a run with the given text.
7965    pub fn add_run(&mut self, text: &str) -> &mut CT_R {
7966        self.runs.push(CT_R::new(text));
7967        self.runs.last_mut().unwrap()
7968    }
7969
7970    /// Insert a direct run while keeping every paragraph boundary projection aligned.
7971    #[doc(hidden)]
7972    pub fn insert_unwrapped_run(&mut self, run_index: usize, run: CT_R) -> bool {
7973        if run_index > self.runs.len() {
7974            return false;
7975        }
7976        self.shift_run_boundaries_from(run_index);
7977
7978        let mut hyperlinks = Vec::with_capacity(self.hyperlinks.len() + 1);
7979        let mut hyperlink_map = Vec::with_capacity(self.hyperlinks.len());
7980        for mut hyperlink in self.hyperlinks.drain(..) {
7981            if hyperlink.run_start >= run_index {
7982                hyperlink.run_start += 1;
7983                hyperlink.run_end += 1;
7984                hyperlink_map.push((hyperlinks.len(), None, false));
7985                hyperlinks.push(hyperlink);
7986            } else if hyperlink.run_end > run_index {
7987                let split_at = run_index - hyperlink.run_start;
7988                let suffix = HyperlinkSpan {
7989                    rel_id: hyperlink.rel_id.clone(),
7990                    anchor: hyperlink.anchor.clone(),
7991                    tooltip: hyperlink.tooltip.clone(),
7992                    doc_location: hyperlink.doc_location.clone(),
7993                    run_start: run_index + 1,
7994                    run_end: hyperlink.run_end + 1,
7995                    extra_attributes: hyperlink.extra_attributes.clone(),
7996                    extra_xml: hyperlink
7997                        .extra_xml
7998                        .iter()
7999                        .filter(|(boundary, _, _)| *boundary >= split_at)
8000                        .map(|(boundary, before, raw)| (boundary - split_at, *before, raw.clone()))
8001                        .collect(),
8002                    preserved_raw_before: None,
8003                };
8004                hyperlink
8005                    .extra_xml
8006                    .retain(|(boundary, _, _)| *boundary < split_at);
8007                hyperlink.run_end = run_index;
8008                let prefix_index = hyperlinks.len();
8009                hyperlinks.push(hyperlink);
8010                let suffix_index = hyperlinks.len();
8011                hyperlinks.push(suffix);
8012                hyperlink_map.push((prefix_index, Some(suffix_index), false));
8013            } else {
8014                hyperlink_map.push((hyperlinks.len(), None, hyperlink.run_end == run_index));
8015                hyperlinks.push(hyperlink);
8016            }
8017        }
8018        self.hyperlinks = hyperlinks;
8019        for (at, slot, _) in &mut self.revisions {
8020            let Some(old_index) = hyperlink_revision_index(*slot) else {
8021                continue;
8022            };
8023            let Some((prefix, suffix, ended_at_insertion)) = hyperlink_map.get(old_index) else {
8024                continue;
8025            };
8026            if *ended_at_insertion && *at == run_index + 1 {
8027                *at = run_index;
8028            }
8029            let new_index = suffix.filter(|_| *at > run_index).unwrap_or(*prefix);
8030            *slot = hyperlink_revision_slot(new_index);
8031        }
8032        self.shift_ruby_spans_for_insert(run_index);
8033        self.runs.insert(run_index, run);
8034        self.refresh_bookmark_projection()
8035    }
8036
8037    /// Split the direct run at `run_index` at a Unicode scalar offset of its
8038    /// literal text.
8039    ///
8040    /// The run keeps the text before `offset`. A new direct run at
8041    /// `run_index + 1` receives the rest with cloned properties, inside the
8042    /// same hyperlink. Zero selects the boundary before the run and the literal
8043    /// text length selects the boundary after it without changing the paragraph.
8044    /// Tabs, breaks, fields, drawings, references, and raw children are
8045    /// zero-width for this operation.
8046    /// Text-free content and raw children at the split point stay in the first
8047    /// run, and boundary markers after the original run stay after the second.
8048    /// Only [`RunSplitError::BookmarkProjection`] leaves the paragraph changed.
8049    pub fn split_run(
8050        &mut self,
8051        run_index: usize,
8052        offset: usize,
8053    ) -> std::result::Result<usize, RunSplitError> {
8054        let run_count = self.runs.len();
8055        let literal_len = self
8056            .runs
8057            .get(run_index)
8058            .ok_or(RunSplitError::RunOutOfRange {
8059                run_index,
8060                run_count,
8061            })?
8062            .literal_len();
8063        if offset > literal_len {
8064            return Err(RunSplitError::OffsetOutOfRange {
8065                offset,
8066                len: literal_len,
8067            });
8068        }
8069        if offset == 0 {
8070            return Ok(run_index);
8071        }
8072        if offset == literal_len {
8073            return Ok(run_index + 1);
8074        }
8075        let tail = self.runs[run_index].split_off_at(offset)?;
8076        let inserted = run_index + 1;
8077        self.shift_run_boundaries_from(inserted);
8078        for hyperlink in &mut self.hyperlinks {
8079            if hyperlink.run_start >= inserted {
8080                hyperlink.run_start += 1;
8081                hyperlink.run_end += 1;
8082            } else if hyperlink.run_end >= inserted {
8083                // The hyperlink contains the split run, so it grows by one run.
8084                let boundary_after_run = inserted - hyperlink.run_start;
8085                for (boundary, _, _) in &mut hyperlink.extra_xml {
8086                    if *boundary >= boundary_after_run {
8087                        *boundary += 1;
8088                    }
8089                }
8090                hyperlink.run_end += 1;
8091            }
8092        }
8093        self.shift_ruby_spans_for_insert(inserted);
8094        self.runs.insert(inserted, tail);
8095        if self.refresh_bookmark_projection() {
8096            Ok(inserted)
8097        } else {
8098            Err(RunSplitError::BookmarkProjection)
8099        }
8100    }
8101
8102    /// Move every run-boundary projection at or after `run_index` one run later.
8103    ///
8104    /// Hyperlink spans are left to the caller, which decides whether the new
8105    /// run joins or splits them.
8106    /// Move every ruby span across one run inserted at `inserted`.
8107    ///
8108    /// A span that starts at or after the insertion point moves whole, and a
8109    /// span that already contains the point grows by one run, which is the
8110    /// rule `w:hyperlink` spans follow beside this call.
8111    fn shift_ruby_spans_for_insert(&mut self, inserted: usize) {
8112        for ruby in &mut self.rubies {
8113            if ruby.base_start >= inserted {
8114                ruby.base_start += 1;
8115                ruby.base_end += 1;
8116            } else if ruby.base_end >= inserted {
8117                ruby.base_end += 1;
8118            }
8119        }
8120    }
8121
8122    fn shift_run_boundaries_from(&mut self, run_index: usize) {
8123        for marker in &mut self.comment_ranges {
8124            match marker {
8125                CommentRangeMarker::Start { run_index: at, .. }
8126                | CommentRangeMarker::End { run_index: at, .. }
8127                    if *at >= run_index =>
8128                {
8129                    *at += 1;
8130                }
8131                CommentRangeMarker::Start { .. } | CommentRangeMarker::End { .. } => {}
8132            }
8133        }
8134        for marker in &mut self.bookmark_markers {
8135            if marker.run_index >= run_index {
8136                marker.run_index += 1;
8137            }
8138        }
8139        for (at, _, _, _) in &mut self.content_controls {
8140            if *at >= run_index {
8141                *at += 1;
8142            }
8143        }
8144        for (at, _, _) in &mut self.revisions {
8145            if *at >= run_index {
8146                *at += 1;
8147            }
8148        }
8149        for (position, _) in &mut self.extra_xml {
8150            if *position != ROOT_ATTRIBUTES_POSITION && *position >= run_index {
8151                *position += 1;
8152            }
8153        }
8154        for (position, _, _) in &mut self.equations {
8155            if *position >= run_index {
8156                *position += 1;
8157            }
8158        }
8159    }
8160
8161    /// Remove selected comment anchors and remap every collapsed run boundary.
8162    #[doc(hidden)]
8163    pub fn remove_comment_anchors(&mut self, ids: &[i32]) {
8164        for (run_index, raw_before, markers_before, _) in &mut self.content_controls {
8165            let preceding_removed =
8166                self.comment_ranges
8167                    .iter()
8168                    .filter(|marker| {
8169                        marker.run_index() == *run_index && marker.raw_before() == *raw_before
8170                    })
8171                    .take(*markers_before)
8172                    .filter(|marker| match marker {
8173                        CommentRangeMarker::Start { id, .. }
8174                        | CommentRangeMarker::End { id, .. } => ids.contains(id),
8175                    })
8176                    .count();
8177            *markers_before = markers_before.saturating_sub(preceding_removed);
8178        }
8179        self.comment_ranges.retain(|marker| match marker {
8180            CommentRangeMarker::Start { id, .. } | CommentRangeMarker::End { id, .. } => {
8181                !ids.contains(id)
8182            }
8183        });
8184
8185        let mut removed = Vec::with_capacity(self.runs.len());
8186        for run in &mut self.runs {
8187            let removed_reference = run.remove_comment_references(ids);
8188            removed.push(
8189                removed_reference
8190                    && run.content.is_empty()
8191                    && run.extra_xml.is_empty()
8192                    && run.alt_drawings.is_empty(),
8193            );
8194        }
8195        if removed.iter().all(|remove| !remove) {
8196            return;
8197        }
8198        self.remove_runs(&removed);
8199    }
8200
8201    /// Remove the direct runs flagged in `removed` and move every run-boundary
8202    /// projection onto the boundary that remains. Raw children, comment
8203    /// markers, bookmarks and controls of the boundaries that collapse into
8204    /// one keep their order, and a hyperlink or a ruby annotation left
8205    /// without runs is dropped. A hyperlink that still holds a marker or a
8206    /// tracked change stays where it was, after the children at its start.
8207    pub(crate) fn remove_runs(&mut self, removed: &[bool]) {
8208        let removed_run_addresses = self
8209            .runs
8210            .iter()
8211            .zip(removed)
8212            .filter_map(|(run, remove)| remove.then_some(std::ptr::from_ref(run)))
8213            .collect::<Vec<_>>();
8214        let removed_projected_indices = accepted_paragraph_runs(self)
8215            .into_iter()
8216            .enumerate()
8217            .filter_map(|(index, run)| {
8218                removed_run_addresses
8219                    .contains(&std::ptr::from_ref(run))
8220                    .then_some(index)
8221            })
8222            .collect::<Vec<_>>();
8223
8224        let old_run_count = self.runs.len();
8225        // A hyperlink keeping no run is written in place of a raw child at its
8226        // boundary. Its placeholder goes after the children at its start, so
8227        // the children at its end stay after it.
8228        let collapsed = (0..self.hyperlinks.len())
8229            .filter(|&index| {
8230                let hyperlink = &self.hyperlinks[index];
8231                hyperlink.preserved_raw_before.is_none()
8232                    && hyperlink.run_start < hyperlink.run_end
8233                    && hyperlink.run_end <= old_run_count
8234                    && removed[hyperlink.run_start..hyperlink.run_end]
8235                        .iter()
8236                        .all(|remove| *remove)
8237                    && (!hyperlink.extra_xml.is_empty()
8238                        || self
8239                            .revisions
8240                            .iter()
8241                            .any(|(_, slot, _)| hyperlink_revision_index(*slot) == Some(index)))
8242            })
8243            .collect::<Vec<_>>();
8244        for index in &collapsed {
8245            let start = self.hyperlinks[*index].run_start;
8246            let slot = self
8247                .extra_xml
8248                .iter()
8249                .filter(|(position, _)| *position == start)
8250                .count();
8251            self.extra_xml.push((start, Vec::new()));
8252            self.hyperlinks[*index].preserved_raw_before = Some(slot);
8253        }
8254        let boundary_map = (0..=old_run_count)
8255            .map(|boundary| {
8256                boundary
8257                    - removed
8258                        .iter()
8259                        .take(boundary)
8260                        .filter(|remove| **remove)
8261                        .count()
8262            })
8263            .collect::<Vec<_>>();
8264        let mut raw_counts = vec![0usize; old_run_count + 1];
8265        for (position, raw) in &self.extra_xml {
8266            if Self::raw_is_root_attributes(*position, raw) {
8267                continue;
8268            }
8269            raw_counts[(*position).min(old_run_count)] += 1;
8270        }
8271        let mut raw_prefixes = vec![0usize; old_run_count + 1];
8272        for boundary in 1..=old_run_count {
8273            if boundary_map[boundary] == boundary_map[boundary - 1] {
8274                raw_prefixes[boundary] = raw_prefixes[boundary - 1] + raw_counts[boundary - 1];
8275            }
8276        }
8277
8278        self.extra_xml.sort_by_key(|(position, _)| *position);
8279        self.comment_ranges
8280            .sort_by_key(|marker| (marker.run_index(), marker.raw_before()));
8281        self.bookmark_markers.sort_by_key(|marker| marker.run_index);
8282        self.content_controls
8283            .sort_by_key(|(at, raw_before, markers_before, _)| (*at, *raw_before, *markers_before));
8284
8285        for control_index in 0..self.content_controls.len() {
8286            let (run_index, raw_before, markers_before) = {
8287                let control = &self.content_controls[control_index];
8288                (control.0, control.1, control.2)
8289            };
8290            let old_boundary = run_index.min(old_run_count);
8291            let old_raw_slot = raw_before.min(raw_counts[old_boundary]);
8292            let new_boundary = boundary_map[old_boundary];
8293            let new_raw_slot = raw_prefixes[old_boundary] + old_raw_slot;
8294            let preceding_markers = self
8295                .comment_ranges
8296                .iter()
8297                .filter(|marker| {
8298                    let marker_boundary = marker.run_index().min(old_run_count);
8299                    marker_boundary < old_boundary
8300                        && boundary_map[marker_boundary] == new_boundary
8301                        && raw_prefixes[marker_boundary]
8302                            + marker.raw_before().min(raw_counts[marker_boundary])
8303                            == new_raw_slot
8304                })
8305                .count();
8306            let local_marker_count = self
8307                .comment_ranges
8308                .iter()
8309                .filter(|marker| {
8310                    marker.run_index() == old_boundary
8311                        && marker.raw_before().min(raw_counts[old_boundary]) == old_raw_slot
8312                })
8313                .count();
8314            let control = &mut self.content_controls[control_index];
8315            control.0 = new_boundary;
8316            control.1 = new_raw_slot;
8317            control.2 = preceding_markers + markers_before.min(local_marker_count);
8318        }
8319        for marker in &mut self.comment_ranges {
8320            match marker {
8321                CommentRangeMarker::Start {
8322                    run_index,
8323                    raw_before,
8324                    ..
8325                }
8326                | CommentRangeMarker::End {
8327                    run_index,
8328                    raw_before,
8329                    ..
8330                } => {
8331                    let old_boundary = (*run_index).min(old_run_count);
8332                    *run_index = boundary_map[old_boundary];
8333                    *raw_before =
8334                        raw_prefixes[old_boundary] + (*raw_before).min(raw_counts[old_boundary]);
8335                }
8336            }
8337        }
8338        for marker in &mut self.bookmark_markers {
8339            let old_boundary = marker.run_index.min(old_run_count);
8340            marker.run_index = boundary_map[old_boundary];
8341            marker.raw_before =
8342                raw_prefixes[old_boundary] + marker.raw_before.min(raw_counts[old_boundary]);
8343            marker.projected_run_index -= removed_projected_indices
8344                .iter()
8345                .filter(|index| **index < marker.projected_run_index)
8346                .count();
8347        }
8348        let hyperlink_revision_counts = self
8349            .hyperlinks
8350            .iter()
8351            .enumerate()
8352            .map(|(hyperlink_index, hyperlink)| {
8353                let start = hyperlink.run_start.min(old_run_count);
8354                let end = hyperlink.run_end.min(old_run_count);
8355                (start..=end)
8356                    .map(|boundary| {
8357                        self.revisions
8358                            .iter()
8359                            .filter(|(at, slot, _)| {
8360                                *at == boundary
8361                                    && hyperlink_revision_index(*slot) == Some(hyperlink_index)
8362                            })
8363                            .count()
8364                    })
8365                    .collect::<Vec<_>>()
8366            })
8367            .collect::<Vec<_>>();
8368        for (run_index, raw_before, _) in &mut self.revisions {
8369            let old_boundary = (*run_index).min(old_run_count);
8370            *run_index = boundary_map[old_boundary];
8371            if hyperlink_revision_index(*raw_before).is_none() {
8372                *raw_before =
8373                    raw_prefixes[old_boundary] + (*raw_before).min(raw_counts[old_boundary]);
8374            }
8375        }
8376        for (position, raw) in &mut self.extra_xml {
8377            if Self::raw_is_root_attributes(*position, raw) {
8378                continue;
8379            }
8380            *position = boundary_map[(*position).min(old_run_count)];
8381        }
8382        for (position, raw_before, _) in &mut self.equations {
8383            let old_boundary = (*position).min(old_run_count);
8384            *position = boundary_map[old_boundary];
8385            *raw_before = raw_prefixes[old_boundary] + (*raw_before).min(raw_counts[old_boundary]);
8386        }
8387        for ruby in &mut self.rubies {
8388            ruby.base_start = boundary_map[ruby.base_start.min(old_run_count)];
8389            ruby.base_end = boundary_map[ruby.base_end.min(old_run_count)];
8390        }
8391        // An annotation over no base run is not written, so drop it.
8392        self.rubies.retain(|ruby| ruby.base_start < ruby.base_end);
8393        let old_hyperlinks = std::mem::take(&mut self.hyperlinks);
8394        let mut hyperlink_map = vec![None; old_hyperlinks.len()];
8395        for (old_index, mut hyperlink) in old_hyperlinks.into_iter().enumerate() {
8396            let old_start = hyperlink.run_start.min(old_run_count);
8397            let old_end = hyperlink.run_end.min(old_run_count);
8398            if let Some(raw_before) = hyperlink.preserved_raw_before {
8399                hyperlink.preserved_raw_before =
8400                    Some(raw_prefixes[old_start] + raw_before.min(raw_counts[old_start]));
8401            }
8402            let revision_counts = &hyperlink_revision_counts[old_index];
8403            let new_start = boundary_map[old_start];
8404            let new_end = boundary_map[old_end];
8405            for (boundary, revisions_before, _) in &mut hyperlink.extra_xml {
8406                let old_relative = (*boundary).min(old_end - old_start);
8407                let old_boundary = old_start + old_relative;
8408                let new_boundary = boundary_map[old_boundary];
8409                let collapsed_before = (0..old_relative)
8410                    .filter(|relative| boundary_map[old_start + relative] == new_boundary)
8411                    .map(|relative| revision_counts[relative])
8412                    .sum::<usize>();
8413                *boundary = new_boundary.saturating_sub(new_start);
8414                *revisions_before =
8415                    collapsed_before + (*revisions_before).min(revision_counts[old_relative]);
8416            }
8417            let owns_revision = self
8418                .revisions
8419                .iter()
8420                .any(|(_, slot, _)| hyperlink_revision_index(*slot) == Some(old_index));
8421            hyperlink.run_start = new_start;
8422            hyperlink.run_end = new_end;
8423            if new_start < new_end || owns_revision || !hyperlink.extra_xml.is_empty() {
8424                hyperlink_map[old_index] = Some(self.hyperlinks.len());
8425                self.hyperlinks.push(hyperlink);
8426            }
8427        }
8428        for (_, slot, _) in &mut self.revisions {
8429            let Some(old_index) = hyperlink_revision_index(*slot) else {
8430                continue;
8431            };
8432            if let Some(new_index) = hyperlink_map.get(old_index).copied().flatten() {
8433                *slot = hyperlink_revision_slot(new_index);
8434            }
8435        }
8436        self.runs = self
8437            .runs
8438            .drain(..)
8439            .zip(removed)
8440            .filter_map(|(run, remove)| (!*remove).then_some(run))
8441            .collect();
8442        for old_index in collapsed {
8443            let Some(index) = hyperlink_map[old_index] else {
8444                continue;
8445            };
8446            let hyperlink = &self.hyperlinks[index];
8447            let (boundary, slot) = (
8448                hyperlink.run_start,
8449                hyperlink.preserved_raw_before.unwrap_or_default(),
8450            );
8451            let mut raw = Vec::new();
8452            let mut writer = Writer::new(&mut raw);
8453            let written = write_hyperlink_start(&mut writer, hyperlink)
8454                .and_then(|()| {
8455                    write_hyperlink_boundary(
8456                        &mut writer,
8457                        &self.revisions,
8458                        index,
8459                        hyperlink,
8460                        boundary,
8461                    )
8462                })
8463                .and_then(|()| {
8464                    writer
8465                        .write_event(Event::End(BytesEnd::new(hyperlink_qname(hyperlink))))
8466                        .map_err(Into::into)
8467                });
8468            if written.is_ok()
8469                && let Some((_, placeholder)) = self
8470                    .extra_xml
8471                    .iter_mut()
8472                    .filter(|(position, _)| *position == boundary)
8473                    .nth(slot)
8474            {
8475                *placeholder = raw;
8476            }
8477        }
8478        let _ = self.refresh_bookmark_projection();
8479    }
8480
8481    /// Insert a canonical bookmark start marker at a direct-run boundary.
8482    pub fn insert_bookmark_start(&mut self, run_index: usize, id: i32, name: &str) -> bool {
8483        if run_index > self.runs.len() {
8484            return false;
8485        }
8486        let mut value = itoa::Buffer::new();
8487        let mut element = BytesStart::new("w:bookmarkStart");
8488        element.push_attribute(("w:id", value.format(id)));
8489        element.push_attribute(("w:name", name));
8490        let mut raw = Vec::new();
8491        if Writer::new(&mut raw)
8492            .write_event(Event::Empty(element))
8493            .is_err()
8494        {
8495            return false;
8496        }
8497        self.extra_xml.push((run_index, raw));
8498        let projected = self.refresh_bookmark_projection();
8499        if !projected {
8500            self.extra_xml.pop();
8501        }
8502        projected
8503    }
8504
8505    /// Insert a canonical bookmark end marker at a direct-run boundary.
8506    pub fn insert_bookmark_end(&mut self, run_index: usize, id: i32) -> bool {
8507        if run_index > self.runs.len() {
8508            return false;
8509        }
8510        let mut value = itoa::Buffer::new();
8511        let mut element = BytesStart::new("w:bookmarkEnd");
8512        element.push_attribute(("w:id", value.format(id)));
8513        let mut raw = Vec::new();
8514        if Writer::new(&mut raw)
8515            .write_event(Event::Empty(element))
8516            .is_err()
8517        {
8518            return false;
8519        }
8520        self.extra_xml.push((run_index, raw));
8521        let projected = self.refresh_bookmark_projection();
8522        if !projected {
8523            self.extra_xml.pop();
8524        }
8525        projected
8526    }
8527
8528    /// Write range markers at accepted-view run boundaries, the run index
8529    /// space of [`Self::accepted_run_paths`].
8530    ///
8531    /// The markers go inside `w:sdtContent` when the range starts or ends
8532    /// between two runs of an inline content control, and around the whole
8533    /// control when the range covers it. A missing side means the range
8534    /// continues into another paragraph, so that side must not fall inside a
8535    /// control. A comment reference run follows the comment end marker. A
8536    /// range that cannot be written exactly is refused and the paragraph is
8537    /// unchanged.
8538    #[doc(hidden)]
8539    pub fn anchor_accepted_range(
8540        &mut self,
8541        start: Option<usize>,
8542        end: Option<usize>,
8543        anchor: RangeAnchor<'_>,
8544    ) -> std::result::Result<(), RangeAnchorError> {
8545        let (start_site, end_site) = self.accepted_range_sites(start, end)?;
8546        let mut paragraph = self.clone();
8547        // The end goes first. A start site never follows it, so the end and
8548        // its reference run leave the start site where it was.
8549        if let Some(site) = end_site {
8550            paragraph.insert_range_marker(site, anchor, false)?;
8551        }
8552        if let Some(site) = start_site {
8553            paragraph.insert_range_marker(site, anchor, true)?;
8554        }
8555        if !paragraph.refresh_bookmark_projection() {
8556            return Err(RangeAnchorError::Write);
8557        }
8558        *self = paragraph;
8559        Ok(())
8560    }
8561
8562    /// Insert one run at a checked accepted-view boundary atomically.
8563    #[doc(hidden)]
8564    pub fn insert_accepted_run(
8565        &mut self,
8566        boundary: usize,
8567        run: CT_R,
8568    ) -> std::result::Result<(), RangeAnchorError> {
8569        let (site, _) = self.accepted_range_sites(Some(boundary), Some(boundary))?;
8570        let mut paragraph = self.clone();
8571        let inserted = match site.ok_or(RangeAnchorError::Write)? {
8572            MarkerSite::Control { controls, index } => paragraph
8573                .control_at_mut(&controls)
8574                .is_some_and(|control| control.insert_content(index, vec![SdtContent::Run(run)])),
8575            MarkerSite::Paragraph { boundary, position } => {
8576                paragraph.insert_run_after_boundary_items(boundary, position, run)
8577            }
8578        };
8579        if !inserted || !paragraph.refresh_bookmark_projection() {
8580            return Err(RangeAnchorError::Write);
8581        }
8582        *self = paragraph;
8583        Ok(())
8584    }
8585
8586    /// Resolve where the start and end markers of a range go.
8587    fn accepted_range_sites(
8588        &self,
8589        start: Option<usize>,
8590        end: Option<usize>,
8591    ) -> std::result::Result<(Option<MarkerSite>, Option<MarkerSite>), RangeAnchorError> {
8592        let paths = self.accepted_run_paths();
8593        let run_count = paths.len();
8594        for boundary in [start, end].into_iter().flatten() {
8595            if boundary > run_count {
8596                return Err(RangeAnchorError::OutOfRange {
8597                    boundary,
8598                    run_count,
8599                });
8600            }
8601        }
8602        // The recursive owner of a run is its path without the final run step.
8603        let owner = |index: usize| {
8604            let segments = paths[index].segments();
8605            &segments[..segments.len() - 1]
8606        };
8607        // The deepest owner holding the runs on both sides of a boundary.
8608        let shared = |boundary: usize| match boundary.checked_sub(1) {
8609            Some(left) if boundary < run_count => {
8610                &owner(left)[..common_prefix_len(owner(left), owner(boundary))]
8611            }
8612            _ => &[] as &[AcceptedRunPathSegment],
8613        };
8614        let (owner_path, blamed) = match (start, end) {
8615            (Some(start), Some(end)) if start > end => {
8616                return Err(RangeAnchorError::Reversed { start, end });
8617            }
8618            (Some(start), Some(end)) if start == end => (shared(start), start),
8619            (Some(start), Some(end)) => {
8620                // The outermost owner that holds the first and the last run
8621                // of the range and reaches both boundaries.
8622                let (start_depth, end_depth) = (shared(start).len(), shared(end).len());
8623                let depth = start_depth.max(end_depth);
8624                let first = owner(start);
8625                if depth > common_prefix_len(first, owner(end - 1)) {
8626                    return Err(RangeAnchorError::CrossesControl { start, end });
8627                }
8628                let blamed = if start_depth >= end_depth { start } else { end };
8629                (&first[..depth], blamed)
8630            }
8631            (Some(boundary), None) | (None, Some(boundary)) => {
8632                let shared = shared(boundary);
8633                if shared
8634                    .iter()
8635                    .any(|segment| matches!(segment, AcceptedRunPathSegment::Revision(_)))
8636                {
8637                    return Err(RangeAnchorError::InsideRevision { boundary });
8638                } else if !shared.is_empty() {
8639                    return Err(RangeAnchorError::InsideControl { boundary });
8640                }
8641                (shared, boundary)
8642            }
8643            (None, None) => return Ok((None, None)),
8644        };
8645        let mut controls = Vec::with_capacity(owner_path.len());
8646        for segment in owner_path {
8647            match *segment {
8648                AcceptedRunPathSegment::ContentControl(index) => controls.push(index),
8649                AcceptedRunPathSegment::Revision(_) | AcceptedRunPathSegment::Run(_) => {
8650                    return Err(RangeAnchorError::InsideRevision { boundary: blamed });
8651                }
8652            }
8653        }
8654
8655        let end_site = end
8656            .map(|boundary| self.marker_site(&paths, &controls, boundary, false))
8657            .transpose()?;
8658        let start_site = match (start, end) {
8659            (Some(start), Some(end)) if start == end => end_site.clone(),
8660            _ => start
8661                .map(|boundary| self.marker_site(&paths, &controls, boundary, true))
8662                .transpose()?,
8663        };
8664        Ok((start_site, end_site))
8665    }
8666
8667    /// Place a marker just before the run after `boundary` for a start, or
8668    /// just after the run before it for an end, inside the owner `controls`.
8669    fn marker_site(
8670        &self,
8671        paths: &[AcceptedRunPath],
8672        controls: &[usize],
8673        boundary: usize,
8674        start: bool,
8675    ) -> std::result::Result<MarkerSite, RangeAnchorError> {
8676        if !controls.is_empty() {
8677            let path = if start {
8678                paths.get(boundary)
8679            } else {
8680                boundary.checked_sub(1).map(|index| &paths[index])
8681            }
8682            .ok_or(RangeAnchorError::Write)?;
8683            let control = self.control_at(controls).ok_or(RangeAnchorError::Write)?;
8684            let index = match path.segments()[controls.len()] {
8685                AcceptedRunPathSegment::Run(index)
8686                | AcceptedRunPathSegment::ContentControl(index) => index,
8687                AcceptedRunPathSegment::Revision(index) => {
8688                    control
8689                        .revisions()
8690                        .get(index)
8691                        .ok_or(RangeAnchorError::Write)?
8692                        .0
8693                }
8694            };
8695            return Ok(MarkerSite::Control {
8696                controls: controls.to_vec(),
8697                index: index + usize::from(!start),
8698            });
8699        }
8700        // A paragraph-level marker must fall between the top-level owners of
8701        // both neighbouring runs, which a revision inside a hyperlink can
8702        // prevent.
8703        let before_right = paths
8704            .get(boundary)
8705            .map(|path| self.paragraph_owner_positions(path).0);
8706        let after_left = boundary
8707            .checked_sub(1)
8708            .map(|index| self.paragraph_owner_positions(&paths[index]).1);
8709        let (Some(before_right), Some(after_left)) = (
8710            before_right.unwrap_or(Some((
8711                self.runs.len(),
8712                self.boundary_items(self.runs.len()).len(),
8713            ))),
8714            after_left.unwrap_or(Some((0, 0))),
8715        ) else {
8716            return Err(RangeAnchorError::HyperlinkRevision { boundary });
8717        };
8718        let (boundary, position) = if start { before_right } else { after_left };
8719        Ok(MarkerSite::Paragraph { boundary, position })
8720    }
8721
8722    /// Positions just before and just after the paragraph-level owner of one
8723    /// accepted run, or `None` where no paragraph-level child can go.
8724    fn paragraph_owner_positions(
8725        &self,
8726        path: &AcceptedRunPath,
8727    ) -> (Option<ChildPosition>, Option<ChildPosition>) {
8728        let around = |boundary: usize, item: BoundaryItem| {
8729            let items = self.boundary_items(boundary);
8730            match items.iter().position(|candidate| *candidate == item) {
8731                Some(position) => (Some((boundary, position)), Some((boundary, position + 1))),
8732                None => (None, None),
8733            }
8734        };
8735        let raw_at = |boundary: usize, slot: usize| {
8736            self.extra_xml
8737                .iter()
8738                .enumerate()
8739                .filter(|(_, (position, _))| *position == boundary)
8740                .nth(slot)
8741                .map(|(index, _)| BoundaryItem::Raw(index))
8742        };
8743        match path.segments()[0] {
8744            AcceptedRunPathSegment::Run(index) => (
8745                Some((index, self.boundary_items(index).len())),
8746                Some((index + 1, 0)),
8747            ),
8748            AcceptedRunPathSegment::ContentControl(index) => self
8749                .content_controls
8750                .get(index)
8751                .map_or((None, None), |(boundary, _, _, _)| {
8752                    around(*boundary, BoundaryItem::Control(index))
8753                }),
8754            AcceptedRunPathSegment::Revision(index) => {
8755                let Some((boundary, slot, _)) = self.revisions.get(index) else {
8756                    return (None, None);
8757                };
8758                let boundary = *boundary;
8759                let Some(hyperlink) = hyperlink_revision_index(*slot) else {
8760                    return raw_at(boundary, *slot)
8761                        .map_or((None, None), |item| around(boundary, item));
8762                };
8763                match self.hyperlinks.get(hyperlink) {
8764                    Some(hyperlink) if hyperlink.preserved_raw_before.is_some() => {
8765                        raw_at(boundary, hyperlink.preserved_raw_before.unwrap_or_default())
8766                            .map_or((None, None), |item| around(boundary, item))
8767                    }
8768                    // A closing hyperlink writes its revision before the
8769                    // paragraph children at its end boundary, and an open
8770                    // one writes it after them.
8771                    Some(hyperlink)
8772                        if hyperlink.run_start < hyperlink.run_end
8773                            && boundary == hyperlink.run_end =>
8774                    {
8775                        (None, Some((boundary, 0)))
8776                    }
8777                    Some(hyperlink) if hyperlink.run_start < hyperlink.run_end => {
8778                        (Some((boundary, self.boundary_items(boundary).len())), None)
8779                    }
8780                    _ => (None, None),
8781                }
8782            }
8783        }
8784    }
8785
8786    /// Reach the inline control that `controls` names.
8787    fn control_at(&self, controls: &[usize]) -> Option<&CT_Sdt> {
8788        let (first, rest) = controls.split_first()?;
8789        let mut control = &self.content_controls.get(*first)?.3;
8790        for index in rest {
8791            let SdtContent::ContentControl(nested) = control.content.get(*index)? else {
8792                return None;
8793            };
8794            control = nested;
8795        }
8796        Some(control)
8797    }
8798
8799    fn control_at_mut(&mut self, controls: &[usize]) -> Option<&mut CT_Sdt> {
8800        let (first, rest) = controls.split_first()?;
8801        let mut control = &mut self.content_controls.get_mut(*first)?.3;
8802        for index in rest {
8803            let SdtContent::ContentControl(nested) = control.content.get_mut(*index)? else {
8804                return None;
8805            };
8806            control = nested;
8807        }
8808        Some(control)
8809    }
8810
8811    fn insert_range_marker(
8812        &mut self,
8813        site: MarkerSite,
8814        anchor: RangeAnchor<'_>,
8815        start: bool,
8816    ) -> std::result::Result<(), RangeAnchorError> {
8817        let marker_xml = range_marker_xml(anchor, start).ok_or(RangeAnchorError::Write)?;
8818        let reference = match anchor {
8819            RangeAnchor::Comment(id) if !start => Some(CT_R {
8820                properties: None,
8821                content: vec![RunContent::CommentReference { id, raw_before: 0 }],
8822                extra_xml: Vec::new(),
8823                extra_xml_positions: Vec::new(),
8824                alt_drawings: Vec::new(),
8825            }),
8826            RangeAnchor::Comment(_)
8827            | RangeAnchor::Bookmark { .. }
8828            | RangeAnchor::Permission { .. }
8829            | RangeAnchor::Proofing { .. } => None,
8830        };
8831        let inserted = match site {
8832            MarkerSite::Control { controls, index } => {
8833                let mut children = vec![SdtContent::RawXml(marker_xml)];
8834                children.extend(reference.map(SdtContent::Run));
8835                self.control_at_mut(&controls)
8836                    .is_some_and(|control| control.insert_content(index, children))
8837            }
8838            MarkerSite::Paragraph { boundary, position } => {
8839                let mut items = self.boundary_items(boundary);
8840                let item = match anchor {
8841                    RangeAnchor::Comment(id) => {
8842                        self.comment_ranges.push(if start {
8843                            CommentRangeMarker::Start {
8844                                id,
8845                                run_index: boundary,
8846                                raw_before: 0,
8847                                has_child_content: false,
8848                            }
8849                        } else {
8850                            CommentRangeMarker::End {
8851                                id,
8852                                run_index: boundary,
8853                                raw_before: 0,
8854                                has_child_content: false,
8855                            }
8856                        });
8857                        BoundaryItem::Marker(self.comment_ranges.len() - 1)
8858                    }
8859                    RangeAnchor::Bookmark { .. }
8860                    | RangeAnchor::Permission { .. }
8861                    | RangeAnchor::Proofing { .. } => {
8862                        self.extra_xml.push((boundary, marker_xml));
8863                        BoundaryItem::Raw(self.extra_xml.len() - 1)
8864                    }
8865                };
8866                items.insert(position.min(items.len()), item);
8867                self.place_boundary_items(&[(boundary, items)]);
8868                match reference {
8869                    Some(run) => self.insert_run_after_boundary_items(boundary, position + 1, run),
8870                    None => true,
8871                }
8872            }
8873        };
8874        if inserted {
8875            Ok(())
8876        } else {
8877            Err(RangeAnchorError::Write)
8878        }
8879    }
8880
8881    /// List the paragraph-level children at one direct run boundary in the
8882    /// order that serialization writes them.
8883    fn boundary_items(&self, boundary: usize) -> Vec<BoundaryItem> {
8884        let raws = self
8885            .extra_xml
8886            .iter()
8887            .enumerate()
8888            .filter(|(_, (position, _))| *position == boundary)
8889            .map(|(index, _)| index)
8890            .collect::<Vec<_>>();
8891        let mut items = Vec::new();
8892        for raw_slot in 0..=raws.len() {
8893            let markers = self
8894                .comment_ranges
8895                .iter()
8896                .enumerate()
8897                .filter(|(_, marker)| {
8898                    marker.run_index() == boundary
8899                        && marker.raw_before().min(raws.len()) == raw_slot
8900                })
8901                .map(|(index, _)| index)
8902                .collect::<Vec<_>>();
8903            for marker_slot in 0..=markers.len() {
8904                items.extend(
8905                    self.content_controls
8906                        .iter()
8907                        .enumerate()
8908                        .filter(|(_, (at, raw_before, markers_before, _))| {
8909                            *at == boundary
8910                                && (*raw_before).min(raws.len()) == raw_slot
8911                                && (*markers_before).min(markers.len()) == marker_slot
8912                        })
8913                        .map(|(index, _)| BoundaryItem::Control(index)),
8914                );
8915                if let Some(marker) = markers.get(marker_slot) {
8916                    items.push(BoundaryItem::Marker(*marker));
8917                }
8918            }
8919            if let Some(raw) = raws.get(raw_slot) {
8920                items.push(BoundaryItem::Raw(*raw));
8921            }
8922        }
8923        items
8924    }
8925
8926    /// Give each listed direct run boundary exactly its listed children in
8927    /// order, and move every projection that names a moved raw child.
8928    fn place_boundary_items(&mut self, boundaries: &[(usize, Vec<BoundaryItem>)]) {
8929        let mut raw_moves = Vec::new();
8930        let mut placed_raws = Vec::new();
8931        let mut placed_markers = Vec::new();
8932        for (boundary, items) in boundaries {
8933            let (mut raws, mut markers) = (0, 0);
8934            for item in items {
8935                match *item {
8936                    BoundaryItem::Control(index) => {
8937                        let control = &mut self.content_controls[index];
8938                        (control.0, control.1, control.2) = (*boundary, raws, markers);
8939                    }
8940                    BoundaryItem::Marker(index) => {
8941                        match &mut self.comment_ranges[index] {
8942                            CommentRangeMarker::Start {
8943                                run_index,
8944                                raw_before,
8945                                ..
8946                            }
8947                            | CommentRangeMarker::End {
8948                                run_index,
8949                                raw_before,
8950                                ..
8951                            } => (*run_index, *raw_before) = (*boundary, raws),
8952                        }
8953                        markers += 1;
8954                        placed_markers.push(index);
8955                    }
8956                    BoundaryItem::Raw(index) => {
8957                        let old_boundary = self.extra_xml[index].0;
8958                        let old_slot = self.extra_xml[..index]
8959                            .iter()
8960                            .filter(|(position, _)| *position == old_boundary)
8961                            .count();
8962                        raw_moves.push(((old_boundary, old_slot), (*boundary, raws)));
8963                        placed_raws.push(index);
8964                        raws += 1;
8965                        markers = 0;
8966                    }
8967                }
8968            }
8969        }
8970        // Serialization reads the raw children and markers of one boundary in
8971        // vector order, so the placed ones move to the end in list order.
8972        let mut extra_xml = std::mem::take(&mut self.extra_xml)
8973            .into_iter()
8974            .map(Some)
8975            .collect::<Vec<_>>();
8976        let placed = placed_raws
8977            .iter()
8978            .zip(&raw_moves)
8979            .filter_map(|(index, (_, (boundary, _)))| {
8980                extra_xml[*index].take().map(|(_, raw)| (*boundary, raw))
8981            })
8982            .collect::<Vec<_>>();
8983        self.extra_xml = extra_xml.into_iter().flatten().chain(placed).collect();
8984        let mut comment_ranges = std::mem::take(&mut self.comment_ranges)
8985            .into_iter()
8986            .map(Some)
8987            .collect::<Vec<_>>();
8988        let placed = placed_markers
8989            .iter()
8990            .filter_map(|index| comment_ranges[*index].take())
8991            .collect::<Vec<_>>();
8992        self.comment_ranges = comment_ranges.into_iter().flatten().chain(placed).collect();
8993
8994        let moved = |at: usize, slot: usize| {
8995            raw_moves
8996                .iter()
8997                .find(|(old, _)| *old == (at, slot))
8998                .map(|(_, new)| *new)
8999        };
9000        let mut moved_hyperlinks = Vec::new();
9001        for (hyperlink_index, hyperlink) in self.hyperlinks.iter_mut().enumerate() {
9002            if hyperlink.run_start == hyperlink.run_end
9003                && let Some(slot) = hyperlink.preserved_raw_before
9004                && let Some((boundary, slot)) = moved(hyperlink.run_start, slot)
9005            {
9006                moved_hyperlinks.push((hyperlink_index, hyperlink.run_start, boundary));
9007                (hyperlink.run_start, hyperlink.run_end) = (boundary, boundary);
9008                hyperlink.preserved_raw_before = Some(slot);
9009            }
9010        }
9011        for (at, slot, _) in &mut self.revisions {
9012            if let Some(hyperlink) = hyperlink_revision_index(*slot) {
9013                if let Some((_, _, boundary)) = moved_hyperlinks
9014                    .iter()
9015                    .find(|(index, old, _)| *index == hyperlink && *old == *at)
9016                {
9017                    *at = *boundary;
9018                }
9019            } else if let Some(new) = moved(*at, *slot) {
9020                (*at, *slot) = new;
9021            }
9022        }
9023        for (at, slot, _) in &mut self.equations {
9024            if let Some(new) = moved(*at, *slot) {
9025                (*at, *slot) = new;
9026            }
9027        }
9028    }
9029
9030    /// Insert a direct run at `boundary` after the first `kept` children
9031    /// there, so the remaining children follow the new run.
9032    fn insert_run_after_boundary_items(&mut self, boundary: usize, kept: usize, run: CT_R) -> bool {
9033        let items = self.boundary_items(boundary);
9034        // The insertion moves every child of the boundary after the new run.
9035        if !self.insert_unwrapped_run(boundary, run) {
9036            return false;
9037        }
9038        let (before, after) = items.split_at(kept.min(items.len()));
9039        if !before.is_empty() {
9040            self.place_boundary_items(&[
9041                (boundary, before.to_vec()),
9042                (boundary + 1, after.to_vec()),
9043            ]);
9044        }
9045        true
9046    }
9047
9048    /// Remap facade-authored bookmark marker ids without reserializing any
9049    /// unrelated preserved child XML.
9050    #[doc(hidden)]
9051    pub fn remap_authored_bookmark_ids(
9052        &mut self,
9053        remap: &std::collections::HashMap<i32, i32>,
9054    ) -> bool {
9055        for (_, raw) in &mut self.extra_xml {
9056            remap_authored_bookmark_marker(raw, remap);
9057        }
9058        // Markers anchored inside an inline control are its content children.
9059        for (_, _, _, control) in &mut self.content_controls {
9060            control.remap_authored_bookmark_ids(remap);
9061        }
9062        self.refresh_bookmark_projection()
9063    }
9064
9065    fn refresh_bookmark_projection(&mut self) -> bool {
9066        let mut word_prefixes = vec![
9067            "w".to_owned(),
9068            format!("\0r\0{R_NS}"),
9069            format!("\0mc\0{}", crate::namespace::MC_NS),
9070        ];
9071        // Inline controls keep the source prefix of their preserved runs,
9072        // which the isolated paragraph no longer declares.
9073        for prefix in self
9074            .bookmark_markers
9075            .iter()
9076            .flat_map(|marker| marker.word_prefixes.iter())
9077            .chain(
9078                self.content_controls
9079                    .iter()
9080                    .flat_map(|(_, _, _, control)| control.word_prefixes()),
9081            )
9082        {
9083            if !word_prefixes.contains(prefix) {
9084                word_prefixes.push(prefix.clone());
9085            }
9086        }
9087        let mut raw = Vec::new();
9088        if self.to_xml(&mut Writer::new(&mut raw)).is_err() {
9089            return false;
9090        }
9091        let mut reader = Reader::from_reader(raw.as_slice());
9092        reader.config_mut().trim_text(false);
9093        let mut buffer = Vec::new();
9094        loop {
9095            match reader.read_event_into(&mut buffer) {
9096                Ok(Event::Start(start)) if matches_local_name(start.name().as_ref(), b"p") => {
9097                    let Ok(parsed) = CT_P::from_xml_with_prefixes(&mut reader, &word_prefixes)
9098                    else {
9099                        return false;
9100                    };
9101                    self.bookmark_markers = parsed.bookmark_markers;
9102                    return true;
9103                }
9104                Ok(Event::Eof) | Err(_) => return false,
9105                Ok(_) => {}
9106            }
9107            buffer.clear();
9108        }
9109    }
9110
9111    pub fn from_xml(reader: &mut Reader<&[u8]>) -> Result<Self> {
9112        crate::document::admit_word_reader(reader, &["w".to_owned()], b"p", None)?;
9113        Self::from_xml_with_prefixes(
9114            reader,
9115            &[
9116                "w".to_string(),
9117                format!("\0r\0{R_NS}"),
9118                format!("\0mc\0{}", crate::namespace::MC_NS),
9119            ],
9120        )
9121    }
9122
9123    /// Parse a standalone paragraph fragment with declarations on its root.
9124    #[doc(hidden)]
9125    pub fn from_xml_fragment(xml: &[u8]) -> Result<Self> {
9126        crate::document::admit_word_xml(xml, &["w".to_owned()], None)?;
9127        Self::from_xml_fragment_admitted(xml)
9128    }
9129
9130    fn from_xml_fragment_admitted(xml: &[u8]) -> Result<Self> {
9131        validate_word_owner_fragment(xml, b"p", false)?;
9132        let mut reader = Reader::from_reader(xml);
9133        reader.config_mut().trim_text(false);
9134        let mut buffer = Vec::new();
9135        loop {
9136            match reader.read_event_into(&mut buffer)? {
9137                Event::Start(element) => {
9138                    let prefixes = word_prefixes_at(&element, &[])?;
9139                    if !is_word_element(element.name().as_ref(), b"p", &prefixes) {
9140                        return Err(OxmlError::MissingElement("w:p root".to_owned()));
9141                    }
9142                    return Self::from_xml_with_prefixes_and_root(
9143                        &mut reader,
9144                        &prefixes,
9145                        Some(&element),
9146                    );
9147                }
9148                Event::Empty(element) => {
9149                    let prefixes = word_prefixes_at(&element, &[])?;
9150                    if is_word_element(element.name().as_ref(), b"p", &prefixes) {
9151                        return Self::from_empty_root(&element, &prefixes);
9152                    }
9153                    return Err(OxmlError::MissingElement("w:p root".to_owned()));
9154                }
9155                Event::Eof => return Err(OxmlError::MissingElement("w:p root".to_owned())),
9156                _ => {}
9157            }
9158            buffer.clear();
9159        }
9160    }
9161
9162    /// Parse one namespace-complete paragraph for checked owner replacement.
9163    #[doc(hidden)]
9164    pub fn from_xml_fragment_checked(xml: &[u8]) -> Result<Self> {
9165        crate::document::admit_word_xml(xml, &[], None)?;
9166        validate_word_owner_fragment(xml, b"p", true)?;
9167        let mut paragraph = Self::from_xml_fragment_admitted(xml)?;
9168        // Checked replacement regenerates the paragraph root. Move only its
9169        // disappearing declarations onto preserved drawing owners. Ordinary
9170        // source snapshots must retain their original physical byte identity.
9171        let mut reader = Reader::from_reader(xml);
9172        let root = loop {
9173            match reader.read_event()? {
9174                Event::Start(root) | Event::Empty(root) => break root,
9175                Event::Eof => return Err(OxmlError::MissingElement("w:p root".into())),
9176                _ => {}
9177            }
9178        };
9179        let prefixes = word_prefixes_at(&root, &[])?;
9180        let bindings = bindings_outside_retained_root(
9181            &raw_namespace_declarations(&root)?,
9182            Some(&root),
9183            &prefixes,
9184        )?;
9185        for run in &mut paragraph.runs {
9186            let (retained_root, _) = run_xml_root(run, None, &mut Vec::new())?;
9187            let retained_bindings = raw_namespace_declarations(&retained_root)?;
9188            let bindings = bindings
9189                .iter()
9190                .filter(|binding| !retained_bindings.contains(binding))
9191                .cloned()
9192                .collect::<Vec<_>>();
9193            for drawing in run.content.iter_mut().filter_map(|content| match content {
9194                RunContent::Drawing(drawing) => Some(drawing),
9195                _ => None,
9196            }) {
9197                for raw in drawing
9198                    .inline
9199                    .iter_mut()
9200                    .filter_map(|inline| inline.raw_xml.as_mut())
9201                    .chain(
9202                        drawing
9203                            .anchor
9204                            .iter_mut()
9205                            .filter_map(|anchor| anchor.raw_xml.as_mut()),
9206                    )
9207                {
9208                    *raw = raw_with_external_bindings(raw, &bindings)?;
9209                }
9210            }
9211        }
9212        Ok(paragraph)
9213    }
9214
9215    /// Return exact source bytes for complex fields admitted by the typed paragraph parser.
9216    #[doc(hidden)]
9217    pub fn story_complex_field_sources(
9218        xml: &[u8],
9219        inherited_word_prefixes: &[String],
9220    ) -> Result<Vec<Vec<u8>>> {
9221        let mut reader = Reader::from_reader(xml);
9222        reader.config_mut().trim_text(false);
9223        let mut buffer = Vec::new();
9224        let paragraph = loop {
9225            match reader.read_event_into(&mut buffer)? {
9226                Event::Start(element) => {
9227                    let prefixes = word_prefixes_at(&element, inherited_word_prefixes)?;
9228                    if !is_word_element(element.name().as_ref(), b"p", &prefixes) {
9229                        return Err(OxmlError::MissingElement("w:p root".to_owned()));
9230                    }
9231                    break Self::from_xml_with_prefixes_and_root(
9232                        &mut reader,
9233                        &prefixes,
9234                        Some(&element),
9235                    )?;
9236                }
9237                Event::Empty(element) => {
9238                    let prefixes = word_prefixes_at(&element, inherited_word_prefixes)?;
9239                    if is_word_element(element.name().as_ref(), b"p", &prefixes) {
9240                        break Self::from_empty_root(&element, &prefixes)?;
9241                    }
9242                    return Err(OxmlError::MissingElement("w:p root".to_owned()));
9243                }
9244                Event::Eof => return Err(OxmlError::MissingElement("w:p root".to_owned())),
9245                _ => {}
9246            }
9247            buffer.clear();
9248        };
9249        let mut sources = Vec::new();
9250        collect_story_complex_field_sources(&paragraph, &mut sources);
9251        Ok(sources)
9252    }
9253
9254    pub(crate) fn from_xml_with_prefixes(
9255        reader: &mut Reader<&[u8]>,
9256        word_prefixes: &[String],
9257    ) -> Result<Self> {
9258        Self::from_xml_with_prefixes_and_root(reader, word_prefixes, None)
9259    }
9260
9261    pub(crate) fn from_xml_with_prefixes_and_root(
9262        reader: &mut Reader<&[u8]>,
9263        word_prefixes: &[String],
9264        root: Option<&BytesStart<'_>>,
9265    ) -> Result<Self> {
9266        reader.config_mut().trim_text(false);
9267        let mut properties = None;
9268        let mut runs = Vec::new();
9269        let mut run_sources = Vec::new();
9270        let mut comment_sources = Vec::new();
9271        let mut hyperlinks = Vec::new();
9272        let mut comment_ranges = Vec::new();
9273        let mut bookmark_markers = Vec::new();
9274        let mut extra_xml = Vec::new();
9275        let mut content_controls = Vec::new();
9276        let mut revisions = Vec::new();
9277        let mut rubies = Vec::new();
9278        let mut projected_run_count = 0usize;
9279        let mut tracked_run_count = 0usize;
9280        let mut buf = Vec::new();
9281        // Only the legacy consumed paragraph API lacks an actual Word scope.
9282        // Actual starts and source-backed scopes must not promote unbound XML.
9283        let legacy_consumed_owner = root.is_none()
9284            && !namespace_bindings(word_prefixes)
9285                .iter()
9286                .any(|(_, namespace)| namespace == crate::namespace::W_NS);
9287
9288        loop {
9289            match reader.read_event_into(&mut buf) {
9290                Ok(Event::Start(ref e)) => {
9291                    let name = e.name();
9292                    let prefixes = word_prefixes_at(e, word_prefixes)?;
9293                    if is_word_element(name.as_ref(), b"pPr", &prefixes) {
9294                        let raw = capture_element(reader, e)?;
9295                        properties = Some(parse_scoped_ppr(&raw, word_prefixes)?);
9296                    } else if is_word_element(name.as_ref(), b"r", &prefixes) {
9297                        let raw = capture_element(reader, e)?;
9298                        runs.push(parse_run_raw(&raw, &prefixes)?);
9299                        run_sources.push(Some(raw));
9300                        projected_run_count += 1;
9301                        tracked_run_count += 1;
9302                    } else if legacy_consumed_owner
9303                        && matches_local_name(name.as_ref(), b"r")
9304                        && !element_prefix_has_binding(name.as_ref(), &prefixes)
9305                    {
9306                        let prefixes = prefixes_with_assumed_word_owner(name.as_ref(), &prefixes)?;
9307                        let raw = capture_element(reader, e)?;
9308                        runs.push(parse_run_raw(&raw, &prefixes)?);
9309                        run_sources.push(Some(raw));
9310                        projected_run_count += 1;
9311                        tracked_run_count += 1;
9312                    } else if is_word_element(name.as_ref(), b"sdt", &prefixes) {
9313                        let raw = capture_element(reader, e)?;
9314                        if let Some(sdt) = CT_Sdt::from_inline_raw(&raw, &prefixes) {
9315                            let raw_before = raw_xml_count_at(&extra_xml, runs.len());
9316                            let markers_before = comment_ranges
9317                                .iter()
9318                                .filter(|marker: &&CommentRangeMarker| {
9319                                    marker.run_index() == runs.len()
9320                                        && marker.raw_before() == raw_before
9321                                })
9322                                .count();
9323                            let projection = accepted_control_bookmark_projection(&sdt);
9324                            append_nested_bookmark_projection(
9325                                &mut bookmark_markers,
9326                                &mut projected_run_count,
9327                                &mut tracked_run_count,
9328                                projection,
9329                                runs.len(),
9330                                raw_before,
9331                            );
9332                            content_controls.push((runs.len(), raw_before, markers_before, sdt));
9333                        } else {
9334                            extra_xml.push((runs.len(), raw));
9335                        }
9336                    } else if is_word_element(name.as_ref(), b"hyperlink", &prefixes) {
9337                        let (rel_id, anchor, tooltip, doc_location, extra_attributes) =
9338                            parse_hyperlink_attributes(e, &prefixes)?;
9339                        let raw = capture_element(reader, e)?;
9340                        let parsed = parse_hyperlink_children(&raw, &prefixes)?;
9341                        let run_start = runs.len();
9342                        if parsed.runs.is_empty() && parsed.revisions.is_empty() {
9343                            // A hyperlink with no run stays raw, and its
9344                            // bookmarks still count where it sits.
9345                            append_nested_bookmark_projection(
9346                                &mut bookmark_markers,
9347                                &mut projected_run_count,
9348                                &mut tracked_run_count,
9349                                AcceptedBookmarkProjection {
9350                                    markers: parsed.bookmark_markers,
9351                                    projected_run_count: 0,
9352                                    tracked_run_count: 0,
9353                                },
9354                                run_start,
9355                                raw_xml_count_at(&extra_xml, run_start),
9356                            );
9357                            extra_xml.push((run_start, raw));
9358                        } else {
9359                            append_nested_bookmark_projection(
9360                                &mut bookmark_markers,
9361                                &mut projected_run_count,
9362                                &mut tracked_run_count,
9363                                AcceptedBookmarkProjection {
9364                                    markers: parsed.bookmark_markers.clone(),
9365                                    projected_run_count: parsed.projected_run_count,
9366                                    tracked_run_count: parsed.tracked_run_count,
9367                                },
9368                                run_start,
9369                                0,
9370                            );
9371                            let hyperlink_index = hyperlinks.len();
9372                            let run_end = run_start + parsed.runs.len();
9373                            let preserved_raw_before = parsed
9374                                .runs
9375                                .is_empty()
9376                                .then(|| raw_xml_count_at(&extra_xml, run_start));
9377                            runs.extend(parsed.runs);
9378                            run_sources.extend(parsed.run_sources);
9379                            hyperlinks.push(HyperlinkSpan {
9380                                rel_id,
9381                                anchor,
9382                                tooltip,
9383                                doc_location,
9384                                run_start,
9385                                run_end,
9386                                extra_attributes,
9387                                extra_xml: parsed.extra_xml,
9388                                preserved_raw_before,
9389                            });
9390                            revisions.extend(parsed.revisions.into_iter().map(|(at, revision)| {
9391                                (
9392                                    run_start + at,
9393                                    hyperlink_revision_slot(hyperlink_index),
9394                                    revision,
9395                                )
9396                            }));
9397                            if preserved_raw_before.is_some() {
9398                                extra_xml.push((run_start, raw));
9399                            }
9400                        }
9401                    } else if is_word_element(name.as_ref(), b"ruby", &prefixes) {
9402                        let raw = capture_element(reader, e)?;
9403                        if let Some((mut ruby, base_runs)) = CT_Ruby::from_raw(&raw, &prefixes)? {
9404                            ruby.base_start = runs.len();
9405                            ruby.base_end = runs.len() + base_runs.len();
9406                            projected_run_count += base_runs.len();
9407                            tracked_run_count += base_runs.len();
9408                            run_sources.extend(base_runs.iter().map(|_| None));
9409                            runs.extend(base_runs);
9410                            rubies.push(ruby);
9411                        } else {
9412                            extra_xml.push((runs.len(), raw));
9413                        }
9414                    } else if is_word_element(name.as_ref(), b"fldSimple", &prefixes) {
9415                        let raw = capture_element(reader, e)?;
9416                        if let Some(field) = parse_simple_field(&raw, &prefixes)? {
9417                            runs.push(field_run(field, None));
9418                            run_sources.push(Some(raw));
9419                            projected_run_count += 1;
9420                            tracked_run_count += 1;
9421                        } else {
9422                            extra_xml.push((runs.len(), raw));
9423                        }
9424                    } else if is_word_element(name.as_ref(), b"commentRangeStart", &prefixes)
9425                        || is_word_element(name.as_ref(), b"commentRangeEnd", &prefixes)
9426                    {
9427                        let id = required_word_i32_attribute(e, b"id", &prefixes)?;
9428                        let raw = capture_element(reader, e)?;
9429                        push_comment_marker(
9430                            &mut comment_ranges,
9431                            &extra_xml,
9432                            runs.len(),
9433                            id,
9434                            is_word_element(name.as_ref(), b"commentRangeStart", &prefixes),
9435                            raw_element_has_child_content(&raw),
9436                        );
9437                        comment_sources
9438                            .push((comment_ranges.last().expect("captured marker").clone(), raw));
9439                    } else if is_word_element(name.as_ref(), b"bookmarkStart", &prefixes)
9440                        || is_word_element(name.as_ref(), b"bookmarkEnd", &prefixes)
9441                    {
9442                        let start = is_word_element(name.as_ref(), b"bookmarkStart", &prefixes);
9443                        let id = optional_word_attribute(e, b"id", &prefixes)
9444                            .and_then(|value| value.parse().ok());
9445                        let bookmark_name = optional_word_attribute(e, b"name", &prefixes);
9446                        let raw = capture_element(reader, e)?;
9447                        bookmark_markers.push(BookmarkMarker {
9448                            start,
9449                            id,
9450                            name: bookmark_name,
9451                            run_index: runs.len(),
9452                            raw_before: raw_xml_count_at(&extra_xml, runs.len()),
9453                            projected_run_index: projected_run_count,
9454                            tracked_run_index: tracked_run_count,
9455                            word_prefixes: prefixes.clone(),
9456                            has_child_content: raw_element_has_child_content(&raw),
9457                        });
9458                        extra_xml.push((runs.len(), raw));
9459                    } else if is_word_element(name.as_ref(), b"ins", &prefixes)
9460                        || is_word_element(name.as_ref(), b"del", &prefixes)
9461                        || is_word_element(name.as_ref(), b"moveFrom", &prefixes)
9462                        || is_word_element(name.as_ref(), b"moveTo", &prefixes)
9463                    {
9464                        let raw_before = raw_xml_count_at(&extra_xml, runs.len());
9465                        let raw = capture_element(reader, e)?;
9466                        if let Some(revision) =
9467                            CT_Revision::from_raw_content(raw.clone(), &prefixes)
9468                        {
9469                            let projection = accepted_revision_bookmark_projection(&revision);
9470                            append_nested_bookmark_projection(
9471                                &mut bookmark_markers,
9472                                &mut projected_run_count,
9473                                &mut tracked_run_count,
9474                                projection,
9475                                runs.len(),
9476                                raw_before,
9477                            );
9478                            revisions.push((runs.len(), raw_before, revision));
9479                        }
9480                        extra_xml.push((runs.len(), raw));
9481                    } else {
9482                        // Capture unknown elements (bookmarks, comments, etc.) as raw XML
9483                        extra_xml.push((runs.len(), capture_element(reader, e)?));
9484                    }
9485                }
9486                Ok(Event::Empty(ref e)) => {
9487                    let name = e.name();
9488                    let prefixes = word_prefixes_at(e, word_prefixes)?;
9489                    if is_word_element(name.as_ref(), b"pPr", &prefixes)
9490                        && e.attributes().next().is_none()
9491                    {
9492                        properties.get_or_insert_default();
9493                    } else if is_word_element(name.as_ref(), b"commentRangeStart", &prefixes)
9494                        || is_word_element(name.as_ref(), b"commentRangeEnd", &prefixes)
9495                    {
9496                        let id = required_word_i32_attribute(e, b"id", &prefixes)?;
9497                        push_comment_marker(
9498                            &mut comment_ranges,
9499                            &extra_xml,
9500                            runs.len(),
9501                            id,
9502                            is_word_element(name.as_ref(), b"commentRangeStart", &prefixes),
9503                            false,
9504                        );
9505                        comment_sources.push((
9506                            comment_ranges.last().expect("captured marker").clone(),
9507                            capture_empty_element(e)?,
9508                        ));
9509                    } else if is_word_element(name.as_ref(), b"bookmarkStart", &prefixes)
9510                        || is_word_element(name.as_ref(), b"bookmarkEnd", &prefixes)
9511                    {
9512                        let start = is_word_element(name.as_ref(), b"bookmarkStart", &prefixes);
9513                        let id = optional_word_attribute(e, b"id", &prefixes)
9514                            .and_then(|value| value.parse().ok());
9515                        let bookmark_name = optional_word_attribute(e, b"name", &prefixes);
9516                        bookmark_markers.push(BookmarkMarker {
9517                            start,
9518                            id,
9519                            name: bookmark_name,
9520                            run_index: runs.len(),
9521                            raw_before: raw_xml_count_at(&extra_xml, runs.len()),
9522                            projected_run_index: projected_run_count,
9523                            tracked_run_index: tracked_run_count,
9524                            word_prefixes: prefixes.clone(),
9525                            has_child_content: false,
9526                        });
9527                        extra_xml.push((runs.len(), capture_empty_element(e)?));
9528                    } else if is_word_element(name.as_ref(), b"fldSimple", &prefixes) {
9529                        let raw = capture_empty_element(e)?;
9530                        let field = optional_word_attribute(e, b"instr", &prefixes).and_then(
9531                            |instruction| {
9532                                let instruction = parse_field_instruction(&instruction);
9533                                (!instruction.name.is_empty()).then(|| {
9534                                    let dirty = optional_word_attribute(e, b"dirty", &prefixes)
9535                                        .and_then(|value| parse_field_bool(&value));
9536                                    Field::parsed(
9537                                        instruction,
9538                                        String::new(),
9539                                        Vec::new(),
9540                                        dirty,
9541                                        FieldForm::Simple,
9542                                        raw.clone(),
9543                                        prefixes.clone(),
9544                                    )
9545                                })
9546                            },
9547                        );
9548                        if let Some(field) = field {
9549                            runs.push(field_run(field, None));
9550                            run_sources.push(None);
9551                            projected_run_count += 1;
9552                            tracked_run_count += 1;
9553                        } else {
9554                            extra_xml.push((runs.len(), raw));
9555                        }
9556                    } else if !matches_local_name(name.as_ref(), b"p") {
9557                        let raw_before = raw_xml_count_at(&extra_xml, runs.len());
9558                        let raw = capture_empty_element(e)?;
9559                        if (is_word_element(name.as_ref(), b"ins", &prefixes)
9560                            || is_word_element(name.as_ref(), b"del", &prefixes)
9561                            || is_word_element(name.as_ref(), b"moveFrom", &prefixes)
9562                            || is_word_element(name.as_ref(), b"moveTo", &prefixes))
9563                            && let Some(revision) =
9564                                CT_Revision::from_raw_content(raw.clone(), &prefixes)
9565                        {
9566                            let projection = accepted_revision_bookmark_projection(&revision);
9567                            append_nested_bookmark_projection(
9568                                &mut bookmark_markers,
9569                                &mut projected_run_count,
9570                                &mut tracked_run_count,
9571                                projection,
9572                                runs.len(),
9573                                raw_before,
9574                            );
9575                            revisions.push((runs.len(), raw_before, revision));
9576                        }
9577                        extra_xml.push((runs.len(), raw));
9578                    }
9579                }
9580                Ok(Event::End(ref e)) if matches_local_name(e.name().as_ref(), b"p") => {
9581                    break;
9582                }
9583                Ok(Event::Text(ref text)) if is_xml_whitespace(text.as_ref()) => {
9584                    extra_xml.push((runs.len(), text.as_ref().to_vec()));
9585                }
9586                Ok(Event::Comment(ref comment)) => {
9587                    extra_xml.push((
9588                        runs.len(),
9589                        capture_standalone_event(Event::Comment(comment.to_owned().into_owned()))?,
9590                    ));
9591                }
9592                Ok(Event::PI(ref instruction)) => {
9593                    extra_xml.push((
9594                        runs.len(),
9595                        capture_standalone_event(Event::PI(instruction.to_owned().into_owned()))?,
9596                    ));
9597                }
9598                Ok(Event::Eof) => break,
9599                Err(e) => return Err(e.into()),
9600                _ => {}
9601            }
9602            buf.clear();
9603        }
9604
9605        project_complex_fields(ComplexFieldProjection {
9606            runs: &mut runs,
9607            run_sources: &mut run_sources,
9608            extra_xml: &mut extra_xml,
9609            comment_ranges: &mut comment_ranges,
9610            comment_sources: &comment_sources,
9611            bookmark_markers: &mut bookmark_markers,
9612            content_controls: &mut content_controls,
9613            revisions: &mut revisions,
9614            hyperlinks: &mut hyperlinks,
9615            rubies: &mut rubies,
9616            word_prefixes,
9617        })?;
9618        extra_xml.retain(|(_, raw)| !is_xml_whitespace(raw));
9619        for hyperlink in &mut hyperlinks {
9620            hyperlink
9621                .extra_xml
9622                .retain(|(_, _, raw)| !is_xml_whitespace(raw));
9623        }
9624
9625        let inherited_bindings = namespace_bindings(word_prefixes);
9626        let mut equations = Vec::new();
9627        for run_index in 0..=runs.len() {
9628            for (raw_before, raw) in extra_xml
9629                .iter()
9630                .filter(|(position, _)| *position == run_index)
9631                .map(|(_, raw)| raw)
9632                .enumerate()
9633            {
9634                if let Some(equation) = OfficeMath::from_raw(raw, &inherited_bindings)? {
9635                    equations.push((run_index, raw_before, equation));
9636                }
9637            }
9638        }
9639
9640        if let Some(record) = root
9641            .map(|root| capture_root_attribute_record(root, word_prefixes))
9642            .transpose()?
9643            .flatten()
9644        {
9645            extra_xml.push((ROOT_ATTRIBUTES_POSITION, record));
9646        }
9647
9648        Ok(CT_P {
9649            properties,
9650            runs,
9651            hyperlinks,
9652            comment_ranges,
9653            bookmark_markers,
9654            extra_xml,
9655            content_controls,
9656            revisions,
9657            equations,
9658            rubies,
9659        })
9660    }
9661
9662    /// Validate borrowed owners before cloning or serializing this paragraph.
9663    #[doc(hidden)]
9664    pub fn validate_resource_limits(&self) -> Result<()> {
9665        crate::document::admit_word_model(crate::document::WordOwner::Paragraph(self))
9666    }
9667
9668    pub fn to_xml<W: std::io::Write>(&self, writer: &mut Writer<W>) -> Result<()> {
9669        self.to_xml_with_para_id(writer, None)
9670    }
9671
9672    pub(crate) fn to_xml_with_para_id<W: std::io::Write>(
9673        &self,
9674        writer: &mut Writer<W>,
9675        para_id: Option<&str>,
9676    ) -> Result<()> {
9677        crate::document::admit_word_model(crate::document::WordOwner::Paragraph(self))?;
9678        if para_id.is_none()
9679            && self.properties.is_none()
9680            && self.runs.is_empty()
9681            && self.hyperlinks.is_empty()
9682            && self.comment_ranges.is_empty()
9683            && self.bookmark_markers.is_empty()
9684            && self.extra_xml.is_empty()
9685            && self.content_controls.is_empty()
9686            && self.revisions.is_empty()
9687            && self.equations.is_empty()
9688        {
9689            writer.write_event(Event::Empty(BytesStart::new("w:p")))?;
9690            return Ok(());
9691        }
9692
9693        let mut start = BytesStart::new("w:p");
9694        for (position, raw) in &self.extra_xml {
9695            if Self::raw_is_root_attributes(*position, raw) {
9696                push_root_attribute_record(&mut start, raw, para_id.map(|_| (W14_NS, "paraId")))?;
9697            }
9698        }
9699        if let Some(para_id) = para_id {
9700            start.push_attribute(("w14:paraId", para_id));
9701        }
9702        writer.write_event(Event::Start(start))?;
9703
9704        if let Some(ref props) = self.properties {
9705            props.to_xml(writer)?;
9706        }
9707
9708        // Build a set of run indices that are inside hyperlinks
9709        let mut hyperlink_runs: std::collections::HashMap<usize, usize> =
9710            std::collections::HashMap::new();
9711        for (hl_idx, hl) in self.hyperlinks.iter().enumerate() {
9712            for run_idx in hl.run_start..hl.run_end {
9713                hyperlink_runs.insert(run_idx, hl_idx);
9714            }
9715        }
9716
9717        let mut current_hyperlink: Option<usize> = None;
9718        let mut current_ruby: Option<usize> = None;
9719        let mut written_field_owner = None;
9720        let mut written_span_end = 0;
9721        for (run_idx, run) in self.runs.iter().enumerate() {
9722            if run_idx < written_span_end {
9723                continue;
9724            }
9725            let in_hl = hyperlink_runs.get(&run_idx).copied();
9726
9727            // A ruby annotation wraps its base runs, so it closes before any
9728            // paragraph boundary content that sits between two runs.
9729            if let Some(ruby_index) = current_ruby
9730                && self.rubies[ruby_index].base_end == run_idx
9731            {
9732                self.rubies[ruby_index].write_end(writer)?;
9733                current_ruby = None;
9734            }
9735
9736            // Paragraph boundary content is a sibling of the hyperlink.
9737            if current_hyperlink.is_some() && current_hyperlink != in_hl {
9738                let hyperlink_index = current_hyperlink.expect("open hyperlink exists");
9739                write_hyperlink_boundary(
9740                    writer,
9741                    &self.revisions,
9742                    hyperlink_index,
9743                    &self.hyperlinks[hyperlink_index],
9744                    run_idx,
9745                )?;
9746                writer.write_event(Event::End(BytesEnd::new(hyperlink_qname(
9747                    &self.hyperlinks[hyperlink_index],
9748                ))))?;
9749                current_hyperlink = None;
9750            }
9751
9752            write_paragraph_boundary(
9753                writer,
9754                ParagraphBoundary {
9755                    extra_xml: &self.extra_xml,
9756                    content_controls: &self.content_controls,
9757                    markers: &self.comment_ranges,
9758                    hyperlinks: &self.hyperlinks,
9759                    revisions: &self.revisions,
9760                    equations: &self.equations,
9761                },
9762                run_idx,
9763            )?;
9764
9765            write_empty_hyperlinks(writer, &self.hyperlinks, &self.revisions, run_idx)?;
9766
9767            // Open hyperlink if entering one
9768            if let Some(hl_idx) = in_hl
9769                && current_hyperlink != in_hl
9770            {
9771                let hl = &self.hyperlinks[hl_idx];
9772                write_hyperlink_start(writer, hl)?;
9773                current_hyperlink = in_hl;
9774            }
9775
9776            if let Some(hyperlink_index) = current_hyperlink {
9777                write_hyperlink_boundary(
9778                    writer,
9779                    &self.revisions,
9780                    hyperlink_index,
9781                    &self.hyperlinks[hyperlink_index],
9782                    run_idx,
9783                )?;
9784            }
9785
9786            let foreign_word_namespace = current_hyperlink
9787                .and_then(|index| shadowed_word_namespace(&self.hyperlinks[index]));
9788            if let Some((span_end, raw, true)) = self.field_span_at(run_idx) {
9789                write_raw_with_word_override(writer, raw, foreign_word_namespace)?;
9790                written_field_owner = None;
9791                written_span_end = span_end;
9792                continue;
9793            }
9794
9795            // Fields are paragraph children even though the facade stores them
9796            // in synthetic runs for the existing inline traversal contract.
9797            if run.content.len() == 1
9798                && let RunContent::Field(field) = &run.content[0]
9799            {
9800                if field.span.is_some() {
9801                    write_run_field(
9802                        writer,
9803                        field,
9804                        foreign_word_namespace,
9805                        run.properties.as_ref(),
9806                    )?;
9807                    written_field_owner = None;
9808                    continue;
9809                }
9810                let owner_id = field.source_owner_id();
9811                if owner_id.is_some() && owner_id == written_field_owner {
9812                    continue;
9813                }
9814                let field = if let Some(owner_id) = owner_id {
9815                    let owner_fields = self.runs[run_idx..]
9816                        .iter()
9817                        .map_while(|run| match run.content.as_slice() {
9818                            [RunContent::Field(field)]
9819                                if field.source_owner_id() == Some(owner_id) =>
9820                            {
9821                                Some(field)
9822                            }
9823                            _ => None,
9824                        })
9825                        .collect::<Vec<_>>();
9826                    let owner_changed = owner_fields.iter().any(|field| !field.is_unchanged());
9827                    if owner_fields.len() > 1 && owner_changed {
9828                        return Err(OxmlError::InvalidValue(
9829                            "a field sharing one physical run was changed".to_owned(),
9830                        ));
9831                    }
9832                    written_field_owner = Some(owner_id);
9833                    field
9834                } else {
9835                    written_field_owner = None;
9836                    field
9837                };
9838                write_field_with_result_properties(
9839                    writer,
9840                    field,
9841                    current_hyperlink
9842                        .and_then(|index| shadowed_word_namespace(&self.hyperlinks[index])),
9843                    run.properties.as_ref(),
9844                )?;
9845                continue;
9846            }
9847
9848            if run
9849                .content
9850                .iter()
9851                .any(|content| matches!(content, RunContent::Field(_)))
9852            {
9853                for field in run.content.iter().filter_map(|content| match content {
9854                    RunContent::Field(field) => Some(field),
9855                    _ => None,
9856                }) {
9857                    let Some(owner_id) = field.source_owner_id() else {
9858                        continue;
9859                    };
9860                    let owner_fields = self
9861                        .runs
9862                        .iter()
9863                        .flat_map(|candidate| &candidate.content)
9864                        .filter(|content| {
9865                            matches!(content, RunContent::Field(candidate) if candidate.source_owner_id() == Some(owner_id))
9866                        })
9867                        .count();
9868                    if owner_fields > 1 {
9869                        return Err(OxmlError::InvalidValue(
9870                            "a field sharing one physical run was changed".to_owned(),
9871                        ));
9872                    }
9873                }
9874                written_field_owner = None;
9875                write_mixed_field_run(
9876                    writer,
9877                    run,
9878                    current_hyperlink
9879                        .and_then(|index| shadowed_word_namespace(&self.hyperlinks[index])),
9880                )?;
9881                continue;
9882            }
9883
9884            written_field_owner = None;
9885
9886            if current_ruby.is_none()
9887                && let Some(ruby_index) = self
9888                    .rubies
9889                    .iter()
9890                    .position(|ruby| ruby.base_start == run_idx && ruby.base_end > run_idx)
9891            {
9892                self.rubies[ruby_index].write_start(writer)?;
9893                current_ruby = Some(ruby_index);
9894            }
9895
9896            if let Some(hyperlink_index) = current_hyperlink {
9897                run.to_xml_with_word_override(
9898                    writer,
9899                    shadowed_word_namespace(&self.hyperlinks[hyperlink_index]),
9900                )?;
9901            } else {
9902                run.to_xml(writer)?;
9903            }
9904        }
9905
9906        if let Some(ruby_index) = current_ruby {
9907            self.rubies[ruby_index].write_end(writer)?;
9908        }
9909
9910        // Close any remaining open hyperlink
9911        if let Some(hyperlink_index) = current_hyperlink {
9912            write_hyperlink_boundary(
9913                writer,
9914                &self.revisions,
9915                hyperlink_index,
9916                &self.hyperlinks[hyperlink_index],
9917                self.runs.len(),
9918            )?;
9919            writer.write_event(Event::End(BytesEnd::new(hyperlink_qname(
9920                &self.hyperlinks[hyperlink_index],
9921            ))))?;
9922        }
9923
9924        write_paragraph_boundary(
9925            writer,
9926            ParagraphBoundary {
9927                extra_xml: &self.extra_xml,
9928                content_controls: &self.content_controls,
9929                markers: &self.comment_ranges,
9930                hyperlinks: &self.hyperlinks,
9931                revisions: &self.revisions,
9932                equations: &self.equations,
9933            },
9934            self.runs.len(),
9935        )?;
9936        write_empty_hyperlinks(writer, &self.hyperlinks, &self.revisions, self.runs.len())?;
9937
9938        writer.write_event(Event::End(BytesEnd::new("w:p")))?;
9939        Ok(())
9940    }
9941
9942    /// Each untouched field span's source bytes, with the bytes that write its
9943    /// split runs as separate physical runs.
9944    #[doc(hidden)]
9945    pub fn detached_field_spans(&self) -> Result<Vec<(&[u8], Vec<u8>)>> {
9946        let mut spans = Vec::new();
9947        let mut run_idx = 0;
9948        while run_idx < self.runs.len() {
9949            let Some((end, raw, true)) = self.field_span_at(run_idx) else {
9950                run_idx += 1;
9951                continue;
9952            };
9953            spans.push((raw, self.detached_field_span(run_idx..end)?));
9954            run_idx = end;
9955        }
9956        Ok(spans)
9957    }
9958
9959    /// Append the field source replacements that start at run `run_idx`, and
9960    /// return the number of runs they cover.
9961    ///
9962    /// A field read from a run it shared with text is replaced together with
9963    /// that text, as one physical span, so every field of the span can change.
9964    #[doc(hidden)]
9965    pub fn field_source_replacements_at(
9966        &self,
9967        run_idx: usize,
9968        output: &mut Vec<(Vec<u8>, Vec<u8>)>,
9969    ) -> Result<usize> {
9970        if let Some((end, raw, untouched)) = self.field_span_at(run_idx) {
9971            let replacement = if untouched {
9972                raw.to_vec()
9973            } else {
9974                self.detached_field_span(run_idx..end)?
9975            };
9976            output.push((raw.to_vec(), replacement));
9977            return Ok(end - run_idx);
9978        }
9979        for content in self.runs.get(run_idx).map_or(&[][..], |run| &run.content) {
9980            let RunContent::Field(field) = content else {
9981                continue;
9982            };
9983            let (source, replacement) = field.source_replacement()?.ok_or_else(|| {
9984                OxmlError::MissingElement("the source of a parsed field".to_owned())
9985            })?;
9986            output.push((source.to_vec(), replacement));
9987        }
9988        Ok(1)
9989    }
9990
9991    /// Write the runs of one field span as separate physical runs.
9992    fn detached_field_span(&self, runs: std::ops::Range<usize>) -> Result<Vec<u8>> {
9993        let mut writer = Writer::new(Vec::new());
9994        for run in &self.runs[runs] {
9995            match run.content.as_slice() {
9996                [RunContent::Field(field)] => {
9997                    write_run_field(&mut writer, field, None, run.properties.as_ref())?
9998                }
9999                _ => run.to_xml(&mut writer)?,
10000            }
10001        }
10002        Ok(writer.into_inner())
10003    }
10004
10005    /// The field span whose runs start at `run_idx`, while they are still the
10006    /// runs the reader split it into: its end, source bytes, and whether its
10007    /// fields are unchanged too.
10008    ///
10009    /// The reader splits a run shared by a field and text into sibling runs.
10010    /// While those runs, their fields and the boundaries between them are as
10011    /// read, the span is written back as its original bytes.
10012    fn field_span_at(&self, run_idx: usize) -> Option<(usize, &[u8], bool)> {
10013        for lead in 0..=1 {
10014            let [RunContent::Field(field)] = self.runs.get(run_idx + lead)?.content.as_slice()
10015            else {
10016                continue;
10017            };
10018            let Some(span) = field.span.as_deref() else {
10019                continue;
10020            };
10021            let FieldSource::Parsed {
10022                raw_xml, owner_id, ..
10023            } = &field.source
10024            else {
10025                continue;
10026            };
10027            if span.position != lead || span.field_index != 0 {
10028                continue;
10029            }
10030            let end = run_idx + span.runs.len();
10031            let runs = self.runs.get(run_idx..end)?;
10032            let in_place = runs.iter().zip(&span.runs).all(|(run, shape)| match shape {
10033                Some(shape) => run == shape,
10034                None => matches!(
10035                    run.content.as_slice(),
10036                    [RunContent::Field(candidate)]
10037                        if candidate.source_owner_id() == Some(*owner_id)
10038                ),
10039            });
10040            let last = end - 1;
10041            let inside = |at: usize| at > run_idx && at <= last;
10042            let bounded = has_typed_boundary_inside(
10043                run_idx,
10044                last,
10045                &self.comment_ranges,
10046                &self.bookmark_markers,
10047                &self.content_controls,
10048                &self.revisions,
10049                &self.hyperlinks,
10050            ) || self.extra_xml.iter().any(|(at, _)| inside(*at))
10051                || self.equations.iter().any(|(at, _, _)| inside(*at))
10052                || self
10053                    .rubies
10054                    .iter()
10055                    .any(|ruby| ruby.base_start < end && ruby.base_end > run_idx)
10056                || self.hyperlinks.iter().any(|hyperlink| {
10057                    hyperlink
10058                        .extra_xml
10059                        .iter()
10060                        .any(|(boundary, _, _)| inside(hyperlink.run_start + *boundary))
10061                });
10062            if !in_place || bounded {
10063                return None;
10064            }
10065            let unchanged = runs.iter().all(|run| match run.content.as_slice() {
10066                [RunContent::Field(field)] => field.is_unchanged(),
10067                _ => true,
10068            });
10069            return Some((end, raw_xml.as_slice(), unchanged));
10070        }
10071        None
10072    }
10073
10074    pub(crate) fn collect_runs<'a>(&'a self, runs: &mut Vec<&'a CT_R>) {
10075        for index in 0..=self.runs.len() {
10076            for (_, _, _, sdt) in self
10077                .content_controls
10078                .iter()
10079                .filter(|(at, _, _, _)| *at == index)
10080            {
10081                sdt.collect_runs(runs);
10082            }
10083            if let Some(run) = self.runs.get(index) {
10084                runs.push(run);
10085            }
10086        }
10087    }
10088
10089    pub(crate) fn collect_controls<'a>(&'a self, controls: &mut Vec<&'a CT_Sdt>) {
10090        for (_, _, _, sdt) in &self.content_controls {
10091            controls.push(sdt);
10092            sdt.collect_controls(SdtOwner::Inline, controls);
10093        }
10094    }
10095}
10096
10097/// Write one field, leaving out the run content the reader split off it.
10098///
10099/// A field read from a run it shared with text or another field keeps that
10100/// whole span as its source. Once the span is edited the text is written by
10101/// its own runs, so this field writes only its own part of the source, with
10102/// its own cache and dirty updates.
10103fn write_run_field<W: std::io::Write>(
10104    writer: &mut Writer<W>,
10105    field: &Field,
10106    foreign_word_namespace: Option<&str>,
10107    result_properties: Option<&CT_RPr>,
10108) -> Result<()> {
10109    let Some(span) = &field.span else {
10110        return write_field_with_result_properties(
10111            writer,
10112            field,
10113            foreign_word_namespace,
10114            result_properties,
10115        );
10116    };
10117    let mut own = field.clone();
10118    own.span = None;
10119    if let FieldSource::Parsed {
10120        raw_xml,
10121        word_prefixes,
10122        ..
10123    } = &mut own.source
10124    {
10125        *raw_xml = field_span_parts(raw_xml, word_prefixes)?
10126            .and_then(|parts| {
10127                parts
10128                    .into_iter()
10129                    .filter_map(|part| match part {
10130                        FieldSpanPart::Field(bytes) => Some(bytes),
10131                        FieldSpanPart::Outside(_) => None,
10132                    })
10133                    .nth(span.field_index)
10134            })
10135            .ok_or_else(|| {
10136                OxmlError::InvalidValue(
10137                    "a field could not be separated from its physical run".to_owned(),
10138                )
10139            })?;
10140    }
10141    write_field_with_result_properties(writer, &own, foreign_word_namespace, result_properties)
10142}
10143
10144fn write_mixed_field_run<W: std::io::Write>(
10145    writer: &mut Writer<W>,
10146    run: &CT_R,
10147    foreign_word_namespace: Option<&str>,
10148) -> Result<()> {
10149    let mut segment_start = 0usize;
10150    for (field_index, content) in run.content.iter().enumerate() {
10151        let RunContent::Field(field) = content else {
10152            continue;
10153        };
10154        write_run_content_segment(
10155            writer,
10156            run,
10157            segment_start,
10158            field_index,
10159            segment_start == 0,
10160            false,
10161            foreign_word_namespace,
10162        )?;
10163        write_run_field(
10164            writer,
10165            field,
10166            foreign_word_namespace,
10167            run.properties.as_ref(),
10168        )?;
10169        segment_start = field_index + 1;
10170    }
10171    write_run_content_segment(
10172        writer,
10173        run,
10174        segment_start,
10175        run.content.len(),
10176        segment_start == 0,
10177        true,
10178        foreign_word_namespace,
10179    )
10180}
10181
10182fn write_run_content_segment<W: std::io::Write>(
10183    writer: &mut Writer<W>,
10184    run: &CT_R,
10185    start: usize,
10186    end: usize,
10187    first: bool,
10188    last: bool,
10189    foreign_word_namespace: Option<&str>,
10190) -> Result<()> {
10191    let property_boundary = usize::from(run.properties.is_some());
10192    let lower = if first { 0 } else { property_boundary + start };
10193    let upper = property_boundary + end;
10194    let mut extra_xml = Vec::new();
10195    let mut extra_xml_positions = Vec::new();
10196    if run.extra_xml_positions.len() == run.extra_xml.len() {
10197        for (position, raw) in run.extra_xml_positions.iter().zip(&run.extra_xml) {
10198            let boundary = CT_R::raw_child_position(*position);
10199            if boundary < lower
10200                || boundary > upper
10201                || (*position & RAW_COMMENT_REFERENCE_FLAG != 0 && boundary == upper)
10202            {
10203                continue;
10204            }
10205            let mut mapped = *position;
10206            let mapped_boundary = if boundary < property_boundary {
10207                boundary
10208            } else {
10209                boundary.saturating_sub(start)
10210            };
10211            CT_R::set_raw_child_position(&mut mapped, mapped_boundary);
10212            extra_xml.push(raw.clone());
10213            extra_xml_positions.push(mapped);
10214        }
10215    } else if last {
10216        extra_xml = run.extra_xml.clone();
10217    }
10218    if start == end && extra_xml.is_empty() {
10219        return Ok(());
10220    }
10221    CT_R {
10222        properties: run.properties.clone(),
10223        content: run.content[start..end].to_vec(),
10224        extra_xml,
10225        extra_xml_positions,
10226        alt_drawings: Vec::new(),
10227    }
10228    .to_xml_with_word_override(writer, foreign_word_namespace)
10229}
10230
10231fn collect_story_complex_field_sources(paragraph: &CT_P, sources: &mut Vec<Vec<u8>>) {
10232    for run in paragraph.runs() {
10233        for content in &run.content {
10234            let RunContent::Field(field) = content else {
10235                continue;
10236            };
10237            collect_story_complex_field_source(field, sources);
10238        }
10239    }
10240    for (_, _, revision) in &paragraph.revisions {
10241        collect_story_revision_complex_field_sources(revision, sources);
10242    }
10243}
10244
10245fn collect_story_complex_field_source(field: &Field, sources: &mut Vec<Vec<u8>>) {
10246    if let FieldSource::Parsed {
10247        form: FieldForm::Complex,
10248        raw_xml,
10249        ..
10250    } = &field.source
10251    {
10252        sources.push(raw_xml.clone());
10253    }
10254    for nested in field.nested_fields_in_source_order() {
10255        collect_story_complex_field_source(nested, sources);
10256    }
10257}
10258
10259fn collect_story_revision_complex_field_sources(
10260    revision: &CT_Revision,
10261    sources: &mut Vec<Vec<u8>>,
10262) {
10263    if let Some(paragraph) = revision.content_paragraph() {
10264        collect_story_complex_field_sources(paragraph, sources);
10265    }
10266    for (_, nested) in revision.nested_revisions() {
10267        collect_story_revision_complex_field_sources(nested, sources);
10268    }
10269}
10270
10271fn is_xml_whitespace(value: &[u8]) -> bool {
10272    !value.is_empty()
10273        && value
10274            .iter()
10275            .all(|byte| matches!(*byte, b' ' | b'\t' | b'\r' | b'\n'))
10276}
10277
10278fn capture_standalone_event(event: Event<'static>) -> Result<Vec<u8>> {
10279    let mut writer = Writer::new(Vec::new());
10280    writer.write_event(event)?;
10281    Ok(writer.into_inner())
10282}
10283
10284fn write_field<W: std::io::Write>(
10285    writer: &mut Writer<W>,
10286    field: &Field,
10287    foreign_word_namespace: Option<&str>,
10288) -> Result<()> {
10289    write_field_with_result_properties(writer, field, foreign_word_namespace, None)
10290}
10291
10292fn write_field_with_result_properties<W: std::io::Write>(
10293    writer: &mut Writer<W>,
10294    field: &Field,
10295    foreign_word_namespace: Option<&str>,
10296    result_properties: Option<&CT_RPr>,
10297) -> Result<()> {
10298    if field.is_unchanged()
10299        && let FieldSource::Parsed { raw_xml, .. } = &field.source
10300    {
10301        return write_raw_with_word_override(writer, raw_xml, foreign_word_namespace);
10302    }
10303
10304    if let FieldSource::Parsed {
10305        form,
10306        raw_xml,
10307        original_instruction,
10308        original_cached_result,
10309        original_dirty,
10310        original_locked,
10311        original_legacy_form,
10312        word_prefixes,
10313        ..
10314    } = &field.source
10315        && instruction_structure_eq(&field.instruction, original_instruction)
10316        && instruction_source_identity_eq(&field.instruction, original_instruction)
10317        && field.instruction.raw == original_instruction.raw
10318    {
10319        let cached_changed = field.cached_result != *original_cached_result
10320            && field.nested_cached_projection().as_ref() != Some(&field.cached_result);
10321        let legacy_form_changed = field.legacy_form != *original_legacy_form;
10322        let updated = match form {
10323            FieldForm::Simple => {
10324                update_simple_field_source(field, raw_xml, word_prefixes, cached_changed)?
10325            }
10326            FieldForm::Complex => {
10327                let mut updated = update_nested_field_sources(field, raw_xml, word_prefixes)?;
10328                if legacy_form_changed {
10329                    let form = field.legacy_form.as_ref().ok_or_else(|| {
10330                        OxmlError::MissingElement("typed legacy form data".to_owned())
10331                    })?;
10332                    updated = rewrite_legacy_form_source(&updated, word_prefixes, form)?;
10333                }
10334                if cached_changed || field.dirty != *original_dirty {
10335                    update_complex_field_source(field, &updated, word_prefixes, cached_changed)?
10336                } else {
10337                    updated
10338                }
10339            }
10340        };
10341        let updated = if let Some(runs) = field.typed_cache() {
10342            replace_typed_field_cache(
10343                &updated,
10344                *form,
10345                word_prefixes,
10346                runs,
10347                &field.typed_cached_comment_ranges,
10348            )?
10349        } else {
10350            updated
10351        };
10352        let updated = if field.locked != *original_locked {
10353            rewrite_field_lock(&updated, *form, word_prefixes, field.locked)?
10354        } else {
10355            updated
10356        };
10357        return write_raw_with_word_override(writer, &updated, foreign_word_namespace);
10358    }
10359
10360    let mut field = field.clone();
10361    field.instruction = instruction_for_write(&field);
10362
10363    let form = match &field.source {
10364        FieldSource::Parsed { form, .. } => *form,
10365        FieldSource::New { form, .. } => *form,
10366    };
10367    let form = if instruction_contains_nested(&field.instruction) {
10368        FieldForm::Complex
10369    } else {
10370        form
10371    };
10372    match form {
10373        FieldForm::Simple => {
10374            write_simple_field(writer, &field, foreign_word_namespace, result_properties)
10375        }
10376        FieldForm::Complex => {
10377            write_complex_field(writer, &field, foreign_word_namespace, result_properties)
10378        }
10379    }
10380}
10381
10382fn rewrite_legacy_form_source(
10383    raw: &[u8],
10384    word_prefixes: &[String],
10385    form: &LegacyFormFieldData,
10386) -> Result<Vec<u8>> {
10387    let (kind_name, value_name, value) = match (&form.kind, &form.value) {
10388        (LegacyFormFieldKind::TextInput, LegacyFormFieldValue::Text(value)) => {
10389            ("textInput", "default", value.clone())
10390        }
10391        (LegacyFormFieldKind::CheckBox, LegacyFormFieldValue::Checked(value)) => (
10392            "checkBox",
10393            "checked",
10394            if *value { "1" } else { "0" }.to_owned(),
10395        ),
10396        (LegacyFormFieldKind::DropDownList, LegacyFormFieldValue::SelectedIndex(value)) => {
10397            ("ddList", "result", value.to_string())
10398        }
10399        _ => {
10400            return Err(OxmlError::InvalidValue(
10401                "legacy form value kind does not match the field kind".to_owned(),
10402            ));
10403        }
10404    };
10405    let mut reader = Reader::from_reader(raw);
10406    reader.config_mut().trim_text(false);
10407    let mut writer = Writer::new(Vec::with_capacity(raw.len() + value.len()));
10408    let mut buffer = Vec::new();
10409    let mut prefixes = word_prefixes.to_vec();
10410    let mut prefix_scopes = Vec::new();
10411    let mut stack = Vec::<Option<String>>::new();
10412    let mut begin_depth = None;
10413    let mut begin_seen = false;
10414    let mut ff_depth = None;
10415    let mut kind_depth = None;
10416    let mut kind_prefix = "w".to_owned();
10417    let mut wrote_value = false;
10418
10419    loop {
10420        match reader.read_event_into(&mut buffer)? {
10421            Event::Start(element) => {
10422                let local_prefixes = word_prefixes_at(&element, &prefixes)?;
10423                let local = word_local_name(&element, &local_prefixes);
10424                let depth = stack.len();
10425                if kind_depth.is_some_and(|kind| depth == kind + 1)
10426                    && !wrote_value
10427                    && form.kind == LegacyFormFieldKind::TextInput
10428                    && matches!(local.as_deref(), Some("maxLength" | "format"))
10429                {
10430                    write_legacy_form_value_element(
10431                        writer.get_mut(),
10432                        &kind_prefix,
10433                        value_name,
10434                        &value,
10435                    );
10436                    wrote_value = true;
10437                }
10438                if kind_depth.is_some_and(|kind| depth == kind + 1)
10439                    && !wrote_value
10440                    && form.kind == LegacyFormFieldKind::DropDownList
10441                    && local.as_deref() != Some(value_name)
10442                {
10443                    write_legacy_form_value_element(
10444                        writer.get_mut(),
10445                        &kind_prefix,
10446                        value_name,
10447                        &value,
10448                    );
10449                    wrote_value = true;
10450                }
10451                if kind_depth.is_some_and(|kind| depth == kind + 1)
10452                    && local.as_deref() == Some(value_name)
10453                {
10454                    write_patched_value_event(
10455                        writer.get_mut(),
10456                        &element,
10457                        &local_prefixes,
10458                        &value,
10459                        false,
10460                    )?;
10461                    wrote_value = true;
10462                } else {
10463                    writer.write_event(Event::Start(element.clone()))?;
10464                }
10465                stack.push(local.clone());
10466                prefix_scopes.push(std::mem::replace(&mut prefixes, local_prefixes));
10467                if local.as_deref() == Some("fldChar")
10468                    && !begin_seen
10469                    && optional_word_attribute(&element, b"fldCharType", &prefixes).as_deref()
10470                        == Some("begin")
10471                {
10472                    begin_depth = Some(depth);
10473                    begin_seen = true;
10474                } else if local.as_deref() == Some("ffData") && begin_depth == depth.checked_sub(1)
10475                {
10476                    ff_depth = Some(depth);
10477                } else if ff_depth.is_some_and(|ff| depth == ff + 1)
10478                    && local.as_deref() == Some(kind_name)
10479                {
10480                    kind_depth = Some(depth);
10481                    kind_prefix = qname_prefix(element.name().as_ref())
10482                        .unwrap_or("w")
10483                        .to_owned();
10484                }
10485            }
10486            Event::Empty(element) => {
10487                let local_prefixes = word_prefixes_at(&element, &prefixes)?;
10488                let local = word_local_name(&element, &local_prefixes);
10489                let depth = stack.len();
10490                if ff_depth.is_some_and(|ff| depth == ff + 1) && local.as_deref() == Some(kind_name)
10491                {
10492                    kind_prefix = qname_prefix(element.name().as_ref())
10493                        .unwrap_or("w")
10494                        .to_owned();
10495                    writer.write_event(Event::Start(element.clone()))?;
10496                    write_legacy_form_value_element(
10497                        writer.get_mut(),
10498                        &kind_prefix,
10499                        value_name,
10500                        &value,
10501                    );
10502                    writer.write_event(Event::End(element.to_end()))?;
10503                    wrote_value = true;
10504                    buffer.clear();
10505                    continue;
10506                }
10507                if kind_depth.is_some_and(|kind| depth == kind + 1)
10508                    && !wrote_value
10509                    && form.kind == LegacyFormFieldKind::TextInput
10510                    && matches!(local.as_deref(), Some("maxLength" | "format"))
10511                {
10512                    write_legacy_form_value_element(
10513                        writer.get_mut(),
10514                        &kind_prefix,
10515                        value_name,
10516                        &value,
10517                    );
10518                    wrote_value = true;
10519                }
10520                if kind_depth.is_some_and(|kind| depth == kind + 1)
10521                    && !wrote_value
10522                    && form.kind == LegacyFormFieldKind::DropDownList
10523                    && local.as_deref() != Some(value_name)
10524                {
10525                    write_legacy_form_value_element(
10526                        writer.get_mut(),
10527                        &kind_prefix,
10528                        value_name,
10529                        &value,
10530                    );
10531                    wrote_value = true;
10532                }
10533                if kind_depth.is_some_and(|kind| depth == kind + 1)
10534                    && local.as_deref() == Some(value_name)
10535                {
10536                    write_patched_value_event(
10537                        writer.get_mut(),
10538                        &element,
10539                        &local_prefixes,
10540                        &value,
10541                        true,
10542                    )?;
10543                    wrote_value = true;
10544                } else {
10545                    writer.write_event(Event::Empty(element))?;
10546                }
10547            }
10548            Event::End(element) => {
10549                let depth = stack.len().saturating_sub(1);
10550                if kind_depth == Some(depth) {
10551                    if !wrote_value {
10552                        write_legacy_form_value_element(
10553                            writer.get_mut(),
10554                            &kind_prefix,
10555                            value_name,
10556                            &value,
10557                        );
10558                        wrote_value = true;
10559                    }
10560                    kind_depth = None;
10561                }
10562                if ff_depth == Some(depth) {
10563                    ff_depth = None;
10564                }
10565                if begin_depth == Some(depth) {
10566                    begin_depth = None;
10567                }
10568                writer.write_event(Event::End(element))?;
10569                stack.pop();
10570                prefixes = prefix_scopes
10571                    .pop()
10572                    .unwrap_or_else(|| word_prefixes.to_vec());
10573            }
10574            Event::Eof => break,
10575            event => writer.write_event(event)?,
10576        }
10577        buffer.clear();
10578    }
10579    if !wrote_value {
10580        return Err(OxmlError::MissingElement(format!(
10581            "legacy form {kind_name} value"
10582        )));
10583    }
10584    Ok(writer.into_inner())
10585}
10586
10587fn word_local_name(element: &BytesStart<'_>, word_prefixes: &[String]) -> Option<String> {
10588    let name = element.name();
10589    let local = local_name(name.as_ref());
10590    is_word_element(name.as_ref(), local, word_prefixes)
10591        .then(|| String::from_utf8_lossy(local).into_owned())
10592}
10593
10594fn qname_prefix(name: &[u8]) -> Option<&str> {
10595    let separator = name.iter().position(|byte| *byte == b':')?;
10596    std::str::from_utf8(&name[..separator]).ok()
10597}
10598
10599fn write_legacy_form_value_element(output: &mut Vec<u8>, prefix: &str, name: &str, value: &str) {
10600    let value = quick_xml::escape::escape(value);
10601    output.extend_from_slice(
10602        format!(
10603            "<{prefix}:{name} xmlns:{prefix}=\"{}\" {prefix}:val=\"{value}\"/>",
10604            crate::namespace::W_NS,
10605        )
10606        .as_bytes(),
10607    );
10608}
10609
10610fn write_patched_value_event(
10611    output: &mut Vec<u8>,
10612    element: &BytesStart<'_>,
10613    word_prefixes: &[String],
10614    value: &str,
10615    empty: bool,
10616) -> Result<()> {
10617    let mut tag_writer = Writer::new(Vec::new());
10618    tag_writer.write_event(if empty {
10619        Event::Empty(element.clone())
10620    } else {
10621        Event::Start(element.clone())
10622    })?;
10623    let mut tag = tag_writer.into_inner();
10624    let escaped = quick_xml::escape::escape(value);
10625    for attribute in element.attributes() {
10626        let attribute = attribute?;
10627        if is_word_attribute(attribute.key.as_ref(), b"val", word_prefixes) {
10628            let key = attribute.key.as_ref();
10629            if let Some(range) = lexical_attribute_value_range(&tag, key) {
10630                tag.splice(range, escaped.as_bytes().iter().copied());
10631                output.extend_from_slice(&tag);
10632                return Ok(());
10633            }
10634        }
10635    }
10636    let name = element.name();
10637    let preferred = qname_prefix(name.as_ref()).unwrap_or("w");
10638    let (prefix, declare) = usable_qualified_word_prefix(word_prefixes, &tag, preferred);
10639    if declare {
10640        let insert_at = if empty { tag.len() - 2 } else { tag.len() - 1 };
10641        tag.splice(
10642            insert_at..insert_at,
10643            format!(r#" xmlns:{prefix}="{}""#, crate::namespace::W_NS).bytes(),
10644        );
10645    }
10646    let insert_at = if empty { tag.len() - 2 } else { tag.len() - 1 };
10647    tag.splice(
10648        insert_at..insert_at,
10649        format!(" {prefix}:val=\"{escaped}\"").bytes(),
10650    );
10651    output.extend_from_slice(&tag);
10652    Ok(())
10653}
10654
10655fn usable_qualified_word_prefix(
10656    word_prefixes: &[String],
10657    tag: &[u8],
10658    preferred: &str,
10659) -> (String, bool) {
10660    if !preferred.is_empty() && word_prefixes.iter().any(|prefix| prefix == preferred) {
10661        return (preferred.to_owned(), false);
10662    }
10663    if let Some(prefix) = word_prefixes
10664        .iter()
10665        .find(|prefix| !prefix.is_empty() && !prefix.starts_with('\0'))
10666    {
10667        return (prefix.clone(), false);
10668    }
10669    for index in 0usize.. {
10670        let candidate = if index == 0 {
10671            "w".to_owned()
10672        } else {
10673            format!("w{index}")
10674        };
10675        if !tag
10676            .windows(candidate.len() + 6)
10677            .any(|window| window.starts_with(b"xmlns:") && window[6..] == *candidate.as_bytes())
10678        {
10679            return (candidate, true);
10680        }
10681    }
10682    unreachable!()
10683}
10684
10685fn lexical_attribute_value_range(tag: &[u8], wanted: &[u8]) -> Option<std::ops::Range<usize>> {
10686    let mut cursor = tag.iter().position(|byte| byte.is_ascii_whitespace())?;
10687    while cursor < tag.len() {
10688        while tag.get(cursor).is_some_and(u8::is_ascii_whitespace) {
10689            cursor += 1;
10690        }
10691        if matches!(tag.get(cursor), Some(b'>' | b'/')) {
10692            return None;
10693        }
10694        let name_start = cursor;
10695        while tag
10696            .get(cursor)
10697            .is_some_and(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'=' | b'>' | b'/'))
10698        {
10699            cursor += 1;
10700        }
10701        let name = &tag[name_start..cursor];
10702        while tag.get(cursor).is_some_and(u8::is_ascii_whitespace) {
10703            cursor += 1;
10704        }
10705        if tag.get(cursor) != Some(&b'=') {
10706            return None;
10707        }
10708        cursor += 1;
10709        while tag.get(cursor).is_some_and(u8::is_ascii_whitespace) {
10710            cursor += 1;
10711        }
10712        let quote = *tag.get(cursor)?;
10713        if !matches!(quote, b'\"' | b'\'') {
10714            return None;
10715        }
10716        cursor += 1;
10717        let value_start = cursor;
10718        while tag.get(cursor) != Some(&quote) {
10719            cursor += 1;
10720            if cursor >= tag.len() {
10721                return None;
10722            }
10723        }
10724        let value_end = cursor;
10725        cursor += 1;
10726        if name == wanted {
10727            return Some(value_start..value_end);
10728        }
10729    }
10730    None
10731}
10732
10733fn update_nested_field_sources(
10734    field: &Field,
10735    raw: &[u8],
10736    word_prefixes: &[String],
10737) -> Result<Vec<u8>> {
10738    let scan = scan_complex_source(raw, word_prefixes)?;
10739    let nested_fields = field.nested_fields_in_source_order();
10740    if scan.nested.len() != nested_fields.len() {
10741        return Err(OxmlError::MissingElement(
10742            "nested field spans in owning complex field".to_owned(),
10743        ));
10744    }
10745    let mut edits = Vec::new();
10746    for (nested, span) in nested_fields.into_iter().zip(scan.nested) {
10747        if nested.is_unchanged() {
10748            continue;
10749        }
10750        let replacement =
10751            rewrite_isolated_nested_field(nested, raw, &span, &scan.runs, word_prefixes)?;
10752        edits.push((span.start, span.end, replacement));
10753    }
10754
10755    let children = field.cached_fields_in_source_order();
10756    let complex_children = children
10757        .iter()
10758        .copied()
10759        .filter(|child| child.form() == FieldForm::Complex)
10760        .collect::<Vec<_>>();
10761    let simple_children = children
10762        .iter()
10763        .copied()
10764        .filter(|child| child.form() == FieldForm::Simple)
10765        .collect::<Vec<_>>();
10766    if field.typed_cache().is_none()
10767        && (scan.result_nested.len() != complex_children.len()
10768            || scan.result_simple.len() != simple_children.len())
10769    {
10770        return Err(OxmlError::MissingElement(
10771            "nested cached field spans in owning complex field".to_owned(),
10772        ));
10773    }
10774    for (nested, span) in complex_children.into_iter().zip(scan.result_nested) {
10775        if field.parsed_cached_child_runs().is_some() {
10776            let expected = &raw[scan.runs[span.start_run].start..scan.runs[span.end_run].end];
10777            if !matches!(&nested.source, FieldSource::Parsed { raw_xml, .. } if raw_xml.as_slice() == expected)
10778            {
10779                return Err(OxmlError::InvalidValue(
10780                    "replacement cached field has no original complex-cache source identity"
10781                        .to_owned(),
10782                ));
10783            }
10784        }
10785        if nested.is_unchanged() {
10786            continue;
10787        }
10788        let replacement =
10789            rewrite_isolated_nested_field(nested, raw, &span, &scan.runs, word_prefixes)?;
10790        edits.push((span.start, span.end, replacement));
10791    }
10792    for (child, (start, end)) in simple_children.into_iter().zip(scan.result_simple) {
10793        if field.parsed_cached_child_runs().is_some()
10794            && !matches!(&child.source, FieldSource::Parsed { raw_xml, .. } if raw_xml.as_slice() == &raw[start..end])
10795        {
10796            return Err(OxmlError::InvalidValue(
10797                "replacement cached field has no original simple-cache source identity".to_owned(),
10798            ));
10799        }
10800        if child.is_unchanged() {
10801            continue;
10802        }
10803        let mut writer = Writer::new(Vec::new());
10804        write_field(&mut writer, child, None)?;
10805        edits.push((start, end, writer.into_inner()));
10806    }
10807    edits.sort_by_key(|(start, _, _)| *start);
10808    let mut updated = raw.to_vec();
10809    for (start, end, replacement) in edits.into_iter().rev() {
10810        updated.splice(start..end, replacement);
10811    }
10812    Ok(updated)
10813}
10814
10815#[derive(Debug)]
10816struct ComplexSourceScan {
10817    runs: Vec<RunSourceSpan>,
10818    nested: Vec<NestedComplexSpan>,
10819    result_nested: Vec<NestedComplexSpan>,
10820    result_simple: Vec<(usize, usize)>,
10821}
10822
10823#[derive(Debug)]
10824struct RunSourceSpan {
10825    start: usize,
10826    content_start: usize,
10827    content_end: usize,
10828    end: usize,
10829    field_content: Vec<(usize, usize)>,
10830}
10831
10832#[derive(Debug)]
10833struct NestedComplexSpan {
10834    start: usize,
10835    end: usize,
10836    start_run: usize,
10837    end_run: usize,
10838}
10839
10840fn is_word_source_name(name: &[u8], word_prefixes: &[String]) -> bool {
10841    let Some(separator) = name.iter().position(|byte| *byte == b':') else {
10842        return word_prefixes.iter().any(String::is_empty);
10843    };
10844    word_prefixes
10845        .iter()
10846        .any(|prefix| prefix.as_bytes() == &name[..separator])
10847}
10848
10849fn scan_complex_source(raw: &[u8], word_prefixes: &[String]) -> Result<ComplexSourceScan> {
10850    let mut reader = Reader::from_reader(raw);
10851    reader.config_mut().trim_text(false);
10852    let mut buffer = Vec::new();
10853    let mut runs = Vec::new();
10854    let mut open_fields = Vec::<OpenComplexSourceField>::new();
10855    let mut nested = Vec::new();
10856    let mut result_nested = Vec::new();
10857    let mut result_simple = Vec::new();
10858    loop {
10859        let run_start = reader.buffer_position() as usize;
10860        match reader.read_event_into(&mut buffer)? {
10861            Event::Start(element) => {
10862                let run_prefixes = word_prefixes_at(&element, word_prefixes)?;
10863                if is_word_element(element.name().as_ref(), b"fldSimple", &run_prefixes) {
10864                    reader.read_to_end_into(element.name(), &mut Vec::new())?;
10865                    if open_fields.len() == 1 && open_fields[0].separated {
10866                        result_simple.push((run_start, reader.buffer_position() as usize));
10867                    }
10868                    buffer.clear();
10869                    continue;
10870                }
10871                if !is_word_element(element.name().as_ref(), b"r", &run_prefixes) {
10872                    reader.read_to_end_into(element.name(), &mut Vec::new())?;
10873                    buffer.clear();
10874                    continue;
10875                }
10876                let run_index = runs.len();
10877                let content_start = reader.buffer_position() as usize;
10878                let mut field_content = Vec::new();
10879                loop {
10880                    buffer.clear();
10881                    let child_start = reader.buffer_position() as usize;
10882                    match reader.read_event_into(&mut buffer)? {
10883                        Event::Start(child) => {
10884                            let prefixes = word_prefixes_at(&child, &run_prefixes)?;
10885                            let is_field_content =
10886                                !is_word_element(child.name().as_ref(), b"rPr", &prefixes)
10887                                    && is_word_source_name(child.name().as_ref(), &prefixes);
10888                            if is_word_element(child.name().as_ref(), b"fldChar", &prefixes) {
10889                                let kind =
10890                                    optional_word_attribute(&child, b"fldCharType", &prefixes);
10891                                reader.read_to_end_into(child.name(), &mut Vec::new())?;
10892                                record_complex_source_marker(
10893                                    kind.as_deref(),
10894                                    child_start,
10895                                    reader.buffer_position() as usize,
10896                                    run_index,
10897                                    &mut open_fields,
10898                                    &mut nested,
10899                                    &mut result_nested,
10900                                );
10901                            } else {
10902                                reader.read_to_end_into(child.name(), &mut Vec::new())?;
10903                            }
10904                            if is_field_content {
10905                                field_content
10906                                    .push((child_start, reader.buffer_position() as usize));
10907                            }
10908                        }
10909                        Event::Empty(child) => {
10910                            let prefixes = word_prefixes_at(&child, &run_prefixes)?;
10911                            let is_field_content =
10912                                !is_word_element(child.name().as_ref(), b"rPr", &prefixes)
10913                                    && is_word_source_name(child.name().as_ref(), &prefixes);
10914                            if is_word_element(child.name().as_ref(), b"fldChar", &prefixes) {
10915                                let kind =
10916                                    optional_word_attribute(&child, b"fldCharType", &prefixes);
10917                                record_complex_source_marker(
10918                                    kind.as_deref(),
10919                                    child_start,
10920                                    reader.buffer_position() as usize,
10921                                    run_index,
10922                                    &mut open_fields,
10923                                    &mut nested,
10924                                    &mut result_nested,
10925                                );
10926                            }
10927                            if is_field_content {
10928                                field_content
10929                                    .push((child_start, reader.buffer_position() as usize));
10930                            }
10931                        }
10932                        Event::End(end) if matches_local_name(end.name().as_ref(), b"r") => {
10933                            runs.push(RunSourceSpan {
10934                                start: run_start,
10935                                content_start,
10936                                content_end: child_start,
10937                                end: reader.buffer_position() as usize,
10938                                field_content,
10939                            });
10940                            break;
10941                        }
10942                        Event::Eof => {
10943                            return Err(OxmlError::MissingElement(
10944                                "end of field source run".to_owned(),
10945                            ));
10946                        }
10947                        _ => {}
10948                    }
10949                }
10950            }
10951            Event::Eof => {
10952                return Ok(ComplexSourceScan {
10953                    runs,
10954                    nested,
10955                    result_nested,
10956                    result_simple,
10957                });
10958            }
10959            _ => {}
10960        }
10961        buffer.clear();
10962    }
10963}
10964
10965fn field_source_has_unmodeled_semantic_attributes(
10966    raw: &[u8],
10967    form: FieldForm,
10968    word_prefixes: &[String],
10969) -> Result<bool> {
10970    let mut reader = Reader::from_reader(raw);
10971    reader.config_mut().trim_text(false);
10972    let mut buffer = Vec::new();
10973
10974    loop {
10975        match reader.read_event_into(&mut buffer)? {
10976            Event::Start(element) | Event::Empty(element) => {
10977                let prefixes = word_prefixes_at(&element, word_prefixes)?;
10978                let allowed = match form {
10979                    FieldForm::Simple
10980                        if is_word_element(element.name().as_ref(), b"fldSimple", &prefixes) =>
10981                    {
10982                        &[b"instr".as_slice(), b"dirty".as_slice()][..]
10983                    }
10984                    FieldForm::Complex
10985                        if is_word_element(element.name().as_ref(), b"fldChar", &prefixes) =>
10986                    {
10987                        &[b"fldCharType".as_slice(), b"dirty".as_slice()][..]
10988                    }
10989                    _ => {
10990                        buffer.clear();
10991                        continue;
10992                    }
10993                };
10994
10995                for attribute in element.attributes() {
10996                    let attribute = attribute?;
10997                    let key = attribute.key.as_ref();
10998                    if key == b"xmlns" || key.starts_with(b"xmlns:") {
10999                        continue;
11000                    }
11001                    if allowed
11002                        .iter()
11003                        .any(|local| is_word_attribute(key, local, &prefixes))
11004                    {
11005                        continue;
11006                    }
11007                    return Ok(true);
11008                }
11009
11010                if matches!(form, FieldForm::Simple) {
11011                    return Ok(false);
11012                }
11013            }
11014            Event::Eof => return Ok(false),
11015            _ => {}
11016        }
11017        buffer.clear();
11018    }
11019}
11020
11021fn record_complex_source_marker(
11022    kind: Option<&str>,
11023    start: usize,
11024    end: usize,
11025    run_index: usize,
11026    open_fields: &mut Vec<OpenComplexSourceField>,
11027    nested: &mut Vec<NestedComplexSpan>,
11028    result_nested: &mut Vec<NestedComplexSpan>,
11029) {
11030    match kind {
11031        Some("begin") => open_fields.push(OpenComplexSourceField {
11032            start,
11033            start_run: run_index,
11034            separated: false,
11035        }),
11036        Some("separate") => {
11037            if let Some(field) = open_fields.last_mut() {
11038                field.separated = true;
11039            }
11040        }
11041        Some("end") => {
11042            let Some(field) = open_fields.pop() else {
11043                return;
11044            };
11045            if open_fields.len() == 1 {
11046                let target = if open_fields[0].separated {
11047                    result_nested
11048                } else {
11049                    nested
11050                };
11051                target.push(NestedComplexSpan {
11052                    start: field.start,
11053                    end,
11054                    start_run: field.start_run,
11055                    end_run: run_index,
11056                });
11057            }
11058        }
11059        _ => {}
11060    }
11061}
11062
11063struct OpenComplexSourceField {
11064    start: usize,
11065    start_run: usize,
11066    separated: bool,
11067}
11068
11069fn rewrite_isolated_nested_field(
11070    field: &Field,
11071    raw: &[u8],
11072    field_span: &NestedComplexSpan,
11073    runs: &[RunSourceSpan],
11074    word_prefixes: &[String],
11075) -> Result<Vec<u8>> {
11076    let start_run = &runs[field_span.start_run];
11077    let end_run = &runs[field_span.end_run];
11078    let mut isolated = raw[start_run.start..start_run.content_start].to_vec();
11079    if field_span.start_run == field_span.end_run {
11080        isolated.extend_from_slice(&raw[field_span.start..field_span.end]);
11081    } else {
11082        isolated.extend_from_slice(&raw[field_span.start..start_run.end]);
11083        isolated.extend_from_slice(&raw[start_run.end..end_run.content_start]);
11084        isolated.extend_from_slice(&raw[end_run.content_start..field_span.end]);
11085    }
11086    isolated.extend_from_slice(&raw[end_run.content_end..end_run.end]);
11087
11088    let FieldSource::Parsed {
11089        original_instruction,
11090        original_cached_result,
11091        original_dirty,
11092        original_legacy_form,
11093        ..
11094    } = &field.source
11095    else {
11096        return Err(OxmlError::MissingElement(
11097            "parsed nested field source".to_owned(),
11098        ));
11099    };
11100    let raw_only_instruction_changed = field.instruction.raw != original_instruction.raw
11101        && instruction_structure_eq(&field.instruction, original_instruction);
11102    let mut updated = if raw_only_instruction_changed {
11103        remove_nested_field_sources(&isolated, word_prefixes)?
11104    } else {
11105        update_nested_field_sources(field, &isolated, word_prefixes)?
11106    };
11107    if raw_only_instruction_changed {
11108        updated = update_complex_instruction_source(field, &updated, word_prefixes)?;
11109    }
11110    if field.legacy_form != *original_legacy_form {
11111        let form = field
11112            .legacy_form
11113            .as_ref()
11114            .ok_or_else(|| OxmlError::MissingElement("typed legacy form data".to_owned()))?;
11115        updated = rewrite_legacy_form_source(&updated, word_prefixes, form)?;
11116    }
11117    let cached_changed = field.cached_result != *original_cached_result
11118        && field.nested_cached_projection().as_ref() != Some(&field.cached_result);
11119    if cached_changed || field.dirty != *original_dirty {
11120        updated = update_complex_field_source(field, &updated, word_prefixes, cached_changed)?;
11121    }
11122    if let Some(runs) = field.typed_cache() {
11123        updated = replace_typed_field_cache(
11124            &updated,
11125            FieldForm::Complex,
11126            word_prefixes,
11127            runs,
11128            &field.typed_cached_comment_ranges,
11129        )?;
11130    }
11131    let updated_scan = scan_complex_source(&updated, word_prefixes)?;
11132    let (Some(first_run), Some(last_run)) = (updated_scan.runs.first(), updated_scan.runs.last())
11133    else {
11134        return Err(OxmlError::MissingElement(
11135            "updated nested field runs".to_owned(),
11136        ));
11137    };
11138    Ok(updated[first_run.content_start..last_run.content_end].to_vec())
11139}
11140
11141fn remove_nested_field_sources(raw: &[u8], word_prefixes: &[String]) -> Result<Vec<u8>> {
11142    let scan = scan_complex_source(raw, word_prefixes)?;
11143    let mut removals = Vec::new();
11144    for span in &scan.nested {
11145        for run in &scan.runs[span.start_run..=span.end_run] {
11146            removals.extend(
11147                run.field_content
11148                    .iter()
11149                    .copied()
11150                    .filter(|(start, end)| *start >= span.start && *end <= span.end),
11151            );
11152        }
11153    }
11154    removals.sort_unstable();
11155    removals.dedup();
11156    let mut updated = raw.to_vec();
11157    for (start, end) in removals.into_iter().rev() {
11158        updated.drain(start..end);
11159    }
11160    Ok(updated)
11161}
11162
11163struct FieldRewriteContext {
11164    prefixes: Vec<String>,
11165    word_run: bool,
11166    canonical_end: Option<&'static str>,
11167}
11168
11169fn find_cached_field_source(
11170    raw: &[u8],
11171    source: &[u8],
11172    from: usize,
11173    inherited: &[String],
11174) -> Result<Option<usize>> {
11175    let mut reader = Reader::from_reader(raw);
11176    let mut buffer = Vec::new();
11177    let mut scopes = Vec::<Vec<String>>::new();
11178    loop {
11179        let start = reader.buffer_position() as usize;
11180        match reader.read_event_into(&mut buffer)? {
11181            Event::Start(element) => {
11182                let prefixes =
11183                    word_prefixes_at(&element, scopes.last().map_or(inherited, Vec::as_slice))?;
11184                let field_source = is_word_element(element.name().as_ref(), b"r", &prefixes)
11185                    || is_word_element(element.name().as_ref(), b"fldSimple", &prefixes);
11186                if !scopes.is_empty()
11187                    && field_source
11188                    && start >= from
11189                    && raw.get(start..start.saturating_add(source.len())) == Some(source)
11190                {
11191                    return Ok(Some(start));
11192                }
11193                let wrapper = [
11194                    b"fldSimple".as_slice(),
11195                    b"hyperlink",
11196                    b"sdt",
11197                    b"sdtContent",
11198                    b"ins",
11199                    b"del",
11200                    b"moveFrom",
11201                    b"moveTo",
11202                    b"smartTag",
11203                    b"customXml",
11204                ]
11205                .iter()
11206                .any(|local| is_word_element(element.name().as_ref(), local, &prefixes));
11207                if wrapper {
11208                    scopes.push(prefixes);
11209                } else {
11210                    reader.read_to_end_into(element.name(), &mut Vec::new())?;
11211                }
11212            }
11213            Event::Empty(element) => {
11214                let prefixes =
11215                    word_prefixes_at(&element, scopes.last().map_or(inherited, Vec::as_slice))?;
11216                if !scopes.is_empty()
11217                    && is_word_element(element.name().as_ref(), b"fldSimple", &prefixes)
11218                    && start >= from
11219                    && raw.get(start..start.saturating_add(source.len())) == Some(source)
11220                {
11221                    return Ok(Some(start));
11222                }
11223            }
11224            Event::End(_) => {
11225                scopes.pop();
11226            }
11227            Event::Eof => return Ok(None),
11228            _ => {}
11229        }
11230        buffer.clear();
11231    }
11232}
11233
11234fn update_simple_field_source(
11235    field: &Field,
11236    raw: &[u8],
11237    word_prefixes: &[String],
11238    cached_changed: bool,
11239) -> Result<Vec<u8>> {
11240    let nested_source;
11241    let raw = if let Some(runs) = &field.simple_cached_runs {
11242        if field
11243            .cached_fields_in_source_order()
11244            .iter()
11245            .any(|child| matches!(child.source, FieldSource::New { .. }))
11246        {
11247            return Err(OxmlError::MissingElement(
11248                "replacement cached field has no original simple-cache source identity".to_owned(),
11249            ));
11250        }
11251        let mut paragraph = CT_P::new();
11252        paragraph.runs = runs.clone();
11253        let mut replacements = Vec::new();
11254        let mut boundary = 0;
11255        while boundary < paragraph.runs.len() {
11256            boundary += paragraph.field_source_replacements_at(boundary, &mut replacements)?;
11257        }
11258        let mut edits = Vec::new();
11259        let mut cursor = 0;
11260        for (source, replacement) in replacements {
11261            let start = find_cached_field_source(raw, &source, cursor, word_prefixes)?.ok_or_else(
11262                || OxmlError::MissingElement("nested simple-cache field source".to_owned()),
11263            )?;
11264            cursor = start + source.len();
11265            edits.push((start..cursor, replacement));
11266        }
11267        let mut updated = raw.to_vec();
11268        for (range, replacement) in edits.into_iter().rev() {
11269            updated.splice(range, replacement);
11270        }
11271        nested_source = updated;
11272        nested_source.as_slice()
11273    } else {
11274        raw
11275    };
11276    let mut reader = Reader::from_reader(raw);
11277    reader.config_mut().trim_text(false);
11278    let mut output = Vec::new();
11279    let mut writer = Writer::new(&mut output);
11280    let mut contexts = Vec::<FieldRewriteContext>::new();
11281    let mut buffer = Vec::new();
11282    let mut wrote_result = false;
11283
11284    loop {
11285        match reader.read_event_into(&mut buffer)? {
11286            Event::Start(element) => {
11287                let inherited = contexts
11288                    .last()
11289                    .map(|context| context.prefixes.as_slice())
11290                    .unwrap_or(word_prefixes);
11291                let prefixes = word_prefixes_at(&element, inherited)?;
11292                let depth = contexts.len();
11293                let parent_is_word_run = contexts.last().is_some_and(|context| context.word_run);
11294                if depth == 0 && is_word_element(element.name().as_ref(), b"fldSimple", &prefixes) {
11295                    let foreign_word_binding =
11296                        namespace_bindings(&prefixes)
11297                            .into_iter()
11298                            .any(|(prefix, namespace)| {
11299                                prefix == "w" && namespace != crate::namespace::W_NS
11300                            });
11301                    if foreign_word_binding {
11302                        // Keep the alias and original bindings on the cache owner.
11303                        // A canonical w declaration would change opaque descendants.
11304                        let mut replacement = element.clone().into_owned();
11305                        replacement.clear_attributes();
11306                        for attribute in element.attributes() {
11307                            let attribute = attribute?;
11308                            if !is_field_word_attribute(attribute.key.as_ref(), b"dirty", &prefixes)
11309                            {
11310                                replacement.push_attribute(attribute);
11311                            }
11312                        }
11313                        if let Some(dirty) = field.dirty {
11314                            let name = element.name();
11315                            let prefix = std::str::from_utf8(name.as_ref())?
11316                                .split_once(':')
11317                                .map(|(prefix, _)| prefix)
11318                                .or_else(|| {
11319                                    prefixes
11320                                        .iter()
11321                                        .find(|prefix| {
11322                                            !prefix.is_empty() && !prefix.starts_with('\0')
11323                                        })
11324                                        .map(String::as_str)
11325                                })
11326                                .ok_or_else(|| {
11327                                    OxmlError::InvalidValue(
11328                                        "field attribute requires a Word prefix".to_owned(),
11329                                    )
11330                                })?;
11331                            let attribute = format!("{prefix}:dirty");
11332                            replacement.push_attribute((
11333                                attribute.as_str(),
11334                                if dirty { "1" } else { "0" },
11335                            ));
11336                        }
11337                        writer.write_event(Event::Start(replacement))?;
11338                    } else {
11339                        write_rewritten_field_element(
11340                            &mut writer,
11341                            &element,
11342                            "w:fldSimple",
11343                            &prefixes,
11344                            &[("instr", field.instruction.raw.as_str())],
11345                            field.dirty,
11346                            true,
11347                            false,
11348                        )?;
11349                    }
11350                    contexts.push(FieldRewriteContext {
11351                        prefixes,
11352                        word_run: false,
11353                        canonical_end: (!foreign_word_binding).then_some("w:fldSimple"),
11354                    });
11355                } else if depth == 2
11356                    && parent_is_word_run
11357                    && (is_word_element(element.name().as_ref(), b"t", &prefixes)
11358                        || is_word_element(element.name().as_ref(), b"delText", &prefixes))
11359                {
11360                    if cached_changed {
11361                        write_updated_text_element(
11362                            &mut reader,
11363                            &mut writer,
11364                            &element,
11365                            (!wrote_result).then_some(field.cached_result.as_str()),
11366                            !prefixes.iter().any(|prefix| prefix == "w"),
11367                        )?;
11368                        wrote_result = true;
11369                    } else {
11370                        writer.write_event(Event::Start(element.into_owned()))?;
11371                        contexts.push(FieldRewriteContext {
11372                            prefixes,
11373                            word_run: false,
11374                            canonical_end: None,
11375                        });
11376                    }
11377                } else if cached_changed
11378                    && depth == 2
11379                    && parent_is_word_run
11380                    && (is_word_element(element.name().as_ref(), b"tab", &prefixes)
11381                        || is_word_element(element.name().as_ref(), b"br", &prefixes))
11382                {
11383                    reader.read_to_end_into(element.name(), &mut Vec::new())?;
11384                    if !wrote_result {
11385                        write_field_result_content_with_binding(
11386                            &mut writer,
11387                            &field.cached_result,
11388                            !prefixes.iter().any(|prefix| prefix == "w"),
11389                        )?;
11390                        wrote_result = true;
11391                    }
11392                } else {
11393                    let word_run =
11394                        depth == 1 && is_word_element(element.name().as_ref(), b"r", &prefixes);
11395                    writer.write_event(Event::Start(element.into_owned()))?;
11396                    contexts.push(FieldRewriteContext {
11397                        prefixes,
11398                        word_run,
11399                        canonical_end: None,
11400                    });
11401                }
11402            }
11403            Event::Empty(element) => {
11404                let inherited = contexts
11405                    .last()
11406                    .map(|context| context.prefixes.as_slice())
11407                    .unwrap_or(word_prefixes);
11408                let prefixes = word_prefixes_at(&element, inherited)?;
11409                let depth = contexts.len();
11410                if depth == 2
11411                    && contexts.last().is_some_and(|context| context.word_run)
11412                    && (is_word_element(element.name().as_ref(), b"t", &prefixes)
11413                        || is_word_element(element.name().as_ref(), b"delText", &prefixes))
11414                {
11415                    if cached_changed {
11416                        write_updated_empty_text_element(
11417                            &mut writer,
11418                            &element,
11419                            (!wrote_result).then_some(field.cached_result.as_str()),
11420                            !prefixes.iter().any(|prefix| prefix == "w"),
11421                        )?;
11422                        wrote_result = true;
11423                    } else {
11424                        writer.write_event(Event::Empty(element.into_owned()))?;
11425                    }
11426                } else if cached_changed
11427                    && depth == 2
11428                    && contexts.last().is_some_and(|context| context.word_run)
11429                    && (is_word_element(element.name().as_ref(), b"tab", &prefixes)
11430                        || is_word_element(element.name().as_ref(), b"br", &prefixes))
11431                {
11432                    if !wrote_result {
11433                        write_field_result_content_with_binding(
11434                            &mut writer,
11435                            &field.cached_result,
11436                            !prefixes.iter().any(|prefix| prefix == "w"),
11437                        )?;
11438                        wrote_result = true;
11439                    }
11440                } else {
11441                    writer.write_event(Event::Empty(element.into_owned()))?;
11442                }
11443            }
11444            Event::End(element) => {
11445                let Some(context) = contexts.pop() else {
11446                    writer.write_event(Event::End(element.into_owned()))?;
11447                    buffer.clear();
11448                    continue;
11449                };
11450                if cached_changed && contexts.is_empty() && !wrote_result {
11451                    let foreign_word_namespace = namespace_bindings(&context.prefixes)
11452                        .into_iter()
11453                        .find(|(prefix, namespace)| {
11454                            prefix == "w" && namespace != crate::namespace::W_NS
11455                        })
11456                        .map(|(_, namespace)| namespace);
11457                    write_field_result_run(
11458                        &mut writer,
11459                        field,
11460                        foreign_word_namespace.as_deref(),
11461                        None,
11462                    )?;
11463                    wrote_result = true;
11464                }
11465                if let Some(name) = context.canonical_end {
11466                    writer.write_event(Event::End(BytesEnd::new(name)))?;
11467                } else {
11468                    writer.write_event(Event::End(element.into_owned()))?;
11469                }
11470            }
11471            Event::Eof => break,
11472            event => writer.write_event(event.into_owned())?,
11473        }
11474        buffer.clear();
11475    }
11476    Ok(output)
11477}
11478
11479fn update_complex_instruction_source(
11480    field: &Field,
11481    raw: &[u8],
11482    word_prefixes: &[String],
11483) -> Result<Vec<u8>> {
11484    let effective = instruction_for_write(field);
11485    let instruction = format!(" {} ", canonical_instruction_text(&effective));
11486    let mut reader = Reader::from_reader(raw);
11487    reader.config_mut().trim_text(false);
11488    let mut output = Vec::new();
11489    let mut writer = Writer::new(&mut output);
11490    let mut contexts = Vec::<FieldRewriteContext>::new();
11491    let mut buffer = Vec::new();
11492    let mut field_depth = 0usize;
11493    let mut outer_result = false;
11494    let mut wrote_instruction = false;
11495    let canonical_word_binding = !word_prefixes.iter().any(|prefix| prefix == "w");
11496
11497    loop {
11498        match reader.read_event_into(&mut buffer)? {
11499            Event::Start(element) => {
11500                let inherited = contexts
11501                    .last()
11502                    .map(|context| context.prefixes.as_slice())
11503                    .unwrap_or(word_prefixes);
11504                let prefixes = word_prefixes_at(&element, inherited)?;
11505                let direct_run_child =
11506                    contexts.len() == 1 && contexts.last().is_some_and(|context| context.word_run);
11507                if direct_run_child
11508                    && is_word_element(element.name().as_ref(), b"fldChar", &prefixes)
11509                {
11510                    let kind = optional_word_attribute(&element, b"fldCharType", &prefixes);
11511                    update_complex_phase(kind.as_deref(), &mut field_depth, &mut outer_result);
11512                }
11513                if direct_run_child
11514                    && field_depth == 1
11515                    && !outer_result
11516                    && is_word_element(element.name().as_ref(), b"instrText", &prefixes)
11517                {
11518                    reader.read_to_end_into(element.name(), &mut Vec::new())?;
11519                    write_updated_instruction_text(
11520                        &mut writer,
11521                        &element,
11522                        (!wrote_instruction).then_some(instruction.as_str()),
11523                        canonical_word_binding,
11524                    )?;
11525                    wrote_instruction = true;
11526                } else {
11527                    let word_run = contexts.is_empty()
11528                        && is_word_element(element.name().as_ref(), b"r", &prefixes);
11529                    writer.write_event(Event::Start(element.into_owned()))?;
11530                    contexts.push(FieldRewriteContext {
11531                        prefixes,
11532                        word_run,
11533                        canonical_end: None,
11534                    });
11535                }
11536            }
11537            Event::Empty(element) => {
11538                let inherited = contexts
11539                    .last()
11540                    .map(|context| context.prefixes.as_slice())
11541                    .unwrap_or(word_prefixes);
11542                let prefixes = word_prefixes_at(&element, inherited)?;
11543                let direct_run_child =
11544                    contexts.len() == 1 && contexts.last().is_some_and(|context| context.word_run);
11545                if direct_run_child
11546                    && is_word_element(element.name().as_ref(), b"fldChar", &prefixes)
11547                {
11548                    let kind = optional_word_attribute(&element, b"fldCharType", &prefixes);
11549                    update_complex_phase(kind.as_deref(), &mut field_depth, &mut outer_result);
11550                    writer.write_event(Event::Empty(element.into_owned()))?;
11551                } else if direct_run_child
11552                    && field_depth == 1
11553                    && !outer_result
11554                    && is_word_element(element.name().as_ref(), b"instrText", &prefixes)
11555                {
11556                    write_updated_instruction_text(
11557                        &mut writer,
11558                        &element,
11559                        (!wrote_instruction).then_some(instruction.as_str()),
11560                        canonical_word_binding,
11561                    )?;
11562                    wrote_instruction = true;
11563                } else {
11564                    writer.write_event(Event::Empty(element.into_owned()))?;
11565                }
11566            }
11567            Event::End(element) => {
11568                contexts.pop();
11569                writer.write_event(Event::End(element.into_owned()))?;
11570            }
11571            Event::Eof => break,
11572            event => writer.write_event(event.into_owned())?,
11573        }
11574        buffer.clear();
11575    }
11576    if !wrote_instruction {
11577        return Err(OxmlError::MissingElement(
11578            "nested complex field instruction text".to_owned(),
11579        ));
11580    }
11581    Ok(output)
11582}
11583
11584fn write_updated_instruction_text<W: std::io::Write>(
11585    writer: &mut Writer<W>,
11586    source: &BytesStart<'_>,
11587    value: Option<&str>,
11588    canonical_word_binding: bool,
11589) -> Result<()> {
11590    let value = value.unwrap_or_default();
11591    let mut element = BytesStart::new("w:instrText");
11592    for attribute in source.attributes() {
11593        let attribute = attribute?;
11594        if attribute.key.as_ref() != b"xml:space"
11595            && !(canonical_word_binding && attribute.key.as_ref() == b"xmlns:w")
11596        {
11597            element.push_attribute(attribute);
11598        }
11599    }
11600    if canonical_word_binding {
11601        element.push_attribute(("xmlns:w", crate::namespace::W_NS));
11602    }
11603    if needs_space_preservation(value) {
11604        element.push_attribute(("xml:space", "preserve"));
11605    }
11606    writer.write_event(Event::Start(element))?;
11607    writer.write_event(Event::Text(BytesText::new(value)))?;
11608    writer.write_event(Event::End(BytesEnd::new("w:instrText")))?;
11609    Ok(())
11610}
11611
11612/// Replace only the outer result, using its actual namespace and run boundaries.
11613/// A result sharing a control run is split into sibling runs with the original
11614/// control wrapper and properties retained on both preserved fragments.
11615enum CachedFieldSource {
11616    EmptySimple(BytesStart<'static>),
11617    Simple(std::ops::Range<usize>),
11618    Complex {
11619        result: std::ops::Range<usize>,
11620        first_open: std::ops::Range<usize>,
11621        first_close: std::ops::Range<usize>,
11622        last_open: std::ops::Range<usize>,
11623        last_close: std::ops::Range<usize>,
11624    },
11625}
11626
11627fn cached_field_source(
11628    raw: &[u8],
11629    form: FieldForm,
11630    word_prefixes: &[String],
11631) -> Result<CachedFieldSource> {
11632    let mut reader = Reader::from_reader(raw);
11633    reader.config_mut().trim_text(false);
11634    let mut contexts = Vec::<FieldRewriteContext>::new();
11635    let mut buffer = Vec::new();
11636    let mut physical_runs = Vec::<(usize, usize, usize, usize, usize)>::new();
11637    let mut current_run = None;
11638    let mut separate = None;
11639    let mut end = None;
11640    let mut depth = 0usize;
11641    let mut simple_open = None;
11642    let mut simple_close = None;
11643    loop {
11644        let start = reader.buffer_position() as usize;
11645        let event = reader.read_event_into(&mut buffer)?;
11646        let event_end = reader.buffer_position() as usize;
11647        match event {
11648            Event::Start(element) | Event::Empty(element) => {
11649                let empty = raw.get(event_end.saturating_sub(2)..event_end) == Some(b"/>");
11650                let inherited = contexts
11651                    .last()
11652                    .map(|context| context.prefixes.as_slice())
11653                    .unwrap_or(word_prefixes);
11654                let prefixes = word_prefixes_at(&element, inherited)?;
11655                let word_run = contexts.is_empty()
11656                    && is_word_element(element.name().as_ref(), b"r", &prefixes);
11657                if word_run {
11658                    current_run = Some(physical_runs.len());
11659                    physical_runs.push((start, event_end, event_end, event_end, event_end));
11660                }
11661                if contexts.is_empty()
11662                    && is_word_element(element.name().as_ref(), b"fldSimple", &prefixes)
11663                {
11664                    if empty {
11665                        return Ok(CachedFieldSource::EmptySimple(element.into_owned()));
11666                    }
11667                    simple_open = Some(event_end);
11668                }
11669                if contexts.len() == 1
11670                    && contexts.last().is_some_and(|context| context.word_run)
11671                    && is_word_element(element.name().as_ref(), b"fldChar", &prefixes)
11672                {
11673                    let kind = optional_word_attribute(&element, b"fldCharType", &prefixes);
11674                    let marker_end = if empty {
11675                        event_end
11676                    } else {
11677                        reader.read_to_end_into(element.name(), &mut Vec::new())?;
11678                        reader.buffer_position() as usize
11679                    };
11680                    match kind.as_deref() {
11681                        Some("begin") => depth += 1,
11682                        Some("separate") if depth == 1 => {
11683                            separate = current_run.map(|run| (run, marker_end))
11684                        }
11685                        Some("end") => {
11686                            if depth == 1 {
11687                                end = current_run.map(|run| (run, start));
11688                            }
11689                            depth = depth.saturating_sub(1);
11690                        }
11691                        _ => {}
11692                    }
11693                    buffer.clear();
11694                    continue;
11695                }
11696                if empty {
11697                    if contexts.len() == 1
11698                        && contexts.last().is_some_and(|context| context.word_run)
11699                        && is_word_element(element.name().as_ref(), b"rPr", &prefixes)
11700                        && let Some(run) = current_run
11701                    {
11702                        physical_runs[run].2 = event_end;
11703                    }
11704                } else {
11705                    contexts.push(FieldRewriteContext {
11706                        prefixes,
11707                        word_run,
11708                        canonical_end: None,
11709                    });
11710                }
11711            }
11712            Event::End(element) => {
11713                let context = contexts.pop().ok_or_else(|| {
11714                    OxmlError::InvalidValue("unbalanced typed field source".into())
11715                })?;
11716                if contexts.len() == 1
11717                    && contexts.last().is_some_and(|context| context.word_run)
11718                    && is_word_element(element.name().as_ref(), b"rPr", &context.prefixes)
11719                    && let Some(run) = current_run
11720                {
11721                    physical_runs[run].2 = event_end;
11722                }
11723                if context.word_run {
11724                    let run = current_run.take().ok_or_else(|| {
11725                        OxmlError::MissingElement("typed cache physical run".into())
11726                    })?;
11727                    physical_runs[run].3 = start;
11728                    physical_runs[run].4 = event_end;
11729                }
11730                if contexts.is_empty()
11731                    && is_word_element(element.name().as_ref(), b"fldSimple", &context.prefixes)
11732                {
11733                    simple_close = Some(start);
11734                }
11735            }
11736            Event::Eof => break,
11737            _ => {}
11738        }
11739        buffer.clear();
11740    }
11741    match form {
11742        FieldForm::Simple => {
11743            let (start, end) = simple_open
11744                .zip(simple_close)
11745                .ok_or_else(|| OxmlError::MissingElement("simple typed cache owner".into()))?;
11746            Ok(CachedFieldSource::Simple(start..end))
11747        }
11748        FieldForm::Complex => {
11749            let (first, start) = separate
11750                .ok_or_else(|| OxmlError::MissingElement("complex typed cache separator".into()))?;
11751            let (last, end) =
11752                end.ok_or_else(|| OxmlError::MissingElement("complex typed cache end".into()))?;
11753            if start > end || first > last {
11754                return Err(OxmlError::InvalidValue("inverted typed cache span".into()));
11755            }
11756            let first = physical_runs[first];
11757            let last = physical_runs[last];
11758            Ok(CachedFieldSource::Complex {
11759                result: start..end,
11760                first_open: first.0..first.2,
11761                first_close: first.3..first.4,
11762                last_open: last.0..last.2,
11763                last_close: last.3..last.4,
11764            })
11765        }
11766    }
11767}
11768
11769fn validate_cached_comment_ranges(runs: &[CT_R], ranges: &[CommentRangeMarker]) -> Result<()> {
11770    let mut references = std::collections::BTreeMap::<i32, usize>::new();
11771    for (index, run) in runs.iter().enumerate() {
11772        for content in &run.content {
11773            if let RunContent::CommentReference { id, .. } = content
11774                && references.insert(*id, index).is_some()
11775            {
11776                return Err(OxmlError::InvalidValue(
11777                    "cached annotation reference IDs must be unique".into(),
11778                ));
11779            }
11780        }
11781    }
11782    let mut open = std::collections::BTreeMap::<i32, usize>::new();
11783    let mut closed = std::collections::BTreeSet::new();
11784    let mut last_position = 0;
11785    for marker in ranges {
11786        let (id, position, raw_before, has_child_content, start) = match marker {
11787            CommentRangeMarker::Start {
11788                id,
11789                run_index,
11790                raw_before,
11791                has_child_content,
11792            } => (*id, *run_index, *raw_before, *has_child_content, true),
11793            CommentRangeMarker::End {
11794                id,
11795                run_index,
11796                raw_before,
11797                has_child_content,
11798            } => (*id, *run_index, *raw_before, *has_child_content, false),
11799        };
11800        if position > runs.len()
11801            || position < last_position
11802            || raw_before != 0
11803            || has_child_content
11804            || !references.contains_key(&id)
11805        {
11806            return Err(OxmlError::InvalidValue(
11807                "cached annotation marker has no checked run boundary or reference".into(),
11808            ));
11809        }
11810        last_position = position;
11811        if start {
11812            if closed.contains(&id) || open.insert(id, position).is_some() {
11813                return Err(OxmlError::InvalidValue(
11814                    "cached annotation range has duplicate start".into(),
11815                ));
11816            }
11817        } else {
11818            if open.remove(&id).is_none() || !closed.insert(id) || position > references[&id] {
11819                return Err(OxmlError::InvalidValue(
11820                    "cached annotation range has no paired reference boundary".into(),
11821                ));
11822            }
11823        }
11824    }
11825    if !open.is_empty() || closed.len() != references.len() {
11826        return Err(OxmlError::InvalidValue(
11827            "cached annotation ranges and typed references disagree".into(),
11828        ));
11829    }
11830    Ok(())
11831}
11832
11833fn write_typed_cached_runs<W: std::io::Write>(
11834    writer: &mut Writer<W>,
11835    runs: &[CT_R],
11836    ranges: &[CommentRangeMarker],
11837    foreign_word_namespace: Option<&str>,
11838) -> Result<()> {
11839    for index in 0..=runs.len() {
11840        for marker in ranges {
11841            let (id, position, name) = match marker {
11842                CommentRangeMarker::Start { id, run_index, .. } => {
11843                    (*id, *run_index, "w:commentRangeStart")
11844                }
11845                CommentRangeMarker::End { id, run_index, .. } => {
11846                    (*id, *run_index, "w:commentRangeEnd")
11847                }
11848            };
11849            if position != index {
11850                continue;
11851            }
11852            let mut element = BytesStart::new(name);
11853            element.push_attribute(("w:id", id.to_string().as_str()));
11854            if foreign_word_namespace.is_some() {
11855                element.push_attribute(("xmlns:w", crate::namespace::W_NS));
11856            }
11857            writer.write_event(Event::Empty(element))?;
11858        }
11859        if let Some(run) = runs.get(index) {
11860            write_mixed_field_run(writer, run, foreign_word_namespace)?;
11861        }
11862    }
11863    Ok(())
11864}
11865
11866fn replace_typed_field_cache(
11867    raw: &[u8],
11868    form: FieldForm,
11869    word_prefixes: &[String],
11870    runs: &[CT_R],
11871    ranges: &[CommentRangeMarker],
11872) -> Result<Vec<u8>> {
11873    let mut writer = Writer::new(Vec::new());
11874    write_typed_cached_runs(&mut writer, runs, ranges, Some("typed-cache"))?;
11875    let payload = writer.into_inner();
11876    let mut output = Vec::new();
11877    match cached_field_source(raw, form, word_prefixes)? {
11878        CachedFieldSource::EmptySimple(element) => {
11879            let mut root = Writer::new(Vec::new());
11880            root.write_event(Event::Start(element.clone()))?;
11881            output.extend(root.into_inner());
11882            output.extend_from_slice(&payload);
11883            output.extend_from_slice(b"</");
11884            output.extend_from_slice(element.name().as_ref());
11885            output.extend_from_slice(b">");
11886        }
11887        CachedFieldSource::Simple(result) => {
11888            output.extend_from_slice(&raw[..result.start]);
11889            output.extend_from_slice(&payload);
11890            output.extend_from_slice(&raw[result.end..]);
11891        }
11892        CachedFieldSource::Complex {
11893            result,
11894            first_close,
11895            last_open,
11896            ..
11897        } => {
11898            output.extend_from_slice(&raw[..result.start]);
11899            output.extend_from_slice(&raw[first_close]);
11900            output.extend_from_slice(&payload);
11901            output.extend_from_slice(&raw[last_open]);
11902            output.extend_from_slice(&raw[result.end..]);
11903        }
11904    }
11905    Ok(output)
11906}
11907
11908fn cached_runs_need_typed_projection(runs: &[CT_R]) -> bool {
11909    let mut pending = vec![runs];
11910    while let Some(runs) = pending.pop() {
11911        for content in runs.iter().flat_map(|run| &run.content) {
11912            match content {
11913                RunContent::Tab
11914                | RunContent::Break(_)
11915                | RunContent::Symbol { .. }
11916                | RunContent::Drawing(_)
11917                | RunContent::DeletedText(_) => return true,
11918                RunContent::SpecialCharacter(character)
11919                    if !matches!(character, SpecialCharacter::SoftHyphen) =>
11920                {
11921                    return true;
11922                }
11923                RunContent::Field(child) => {
11924                    if let Some(runs) = child.cached_result_runs() {
11925                        pending.push(runs);
11926                    }
11927                }
11928                _ => {}
11929            }
11930        }
11931    }
11932    false
11933}
11934
11935fn source_has_note_reference_name(raw: &[u8]) -> bool {
11936    [
11937        b"footnoteReference".as_slice(),
11938        b"endnoteReference".as_slice(),
11939        b"commentReference".as_slice(),
11940    ]
11941    .iter()
11942    .any(|name| raw.windows(name.len()).any(|window| window == *name))
11943}
11944
11945fn parsed_complex_cache_runs(
11946    raw: &[u8],
11947    word_prefixes: &[String],
11948) -> Result<(Vec<CT_R>, Vec<CommentRangeMarker>)> {
11949    let CachedFieldSource::Complex {
11950        result,
11951        first_open,
11952        last_close,
11953        ..
11954    } = cached_field_source(raw, FieldForm::Complex, word_prefixes)?
11955    else {
11956        unreachable!("complex field cache");
11957    };
11958    let prefix = word_prefixes
11959        .iter()
11960        .find(|prefix| !prefix.starts_with('\0'))
11961        .ok_or_else(|| OxmlError::InvalidValue("complex field has no Word prefix".to_owned()))?;
11962    let name = if prefix.is_empty() {
11963        "p".to_owned()
11964    } else {
11965        format!("{prefix}:p")
11966    };
11967    let mut cache = format!("<{name}>").into_bytes();
11968    cache.extend_from_slice(&raw[first_open]);
11969    cache.extend_from_slice(&raw[result]);
11970    cache.extend_from_slice(&raw[last_close]);
11971    cache.extend_from_slice(format!("</{name}>").as_bytes());
11972    let mut reader = Reader::from_reader(cache.as_slice());
11973    let mut buffer = Vec::new();
11974    let Event::Start(root) = reader.read_event_into(&mut buffer)? else {
11975        unreachable!("cache root");
11976    };
11977    let prefixes = word_prefixes_at(&root, word_prefixes)?;
11978    let paragraph = CT_P::from_xml_with_prefixes_and_root(&mut reader, &prefixes, Some(&root))?;
11979    Ok((
11980        paragraph.runs().into_iter().cloned().collect(),
11981        paragraph.comment_ranges,
11982    ))
11983}
11984
11985fn update_complex_field_source(
11986    field: &Field,
11987    raw: &[u8],
11988    word_prefixes: &[String],
11989    cached_changed: bool,
11990) -> Result<Vec<u8>> {
11991    let mut reader = Reader::from_reader(raw);
11992    reader.config_mut().trim_text(false);
11993    let mut output = Vec::new();
11994    let mut writer = Writer::new(&mut output);
11995    let mut contexts = Vec::<FieldRewriteContext>::new();
11996    let mut buffer = Vec::new();
11997    let mut field_depth = 0usize;
11998    let mut outer_result = false;
11999    let mut wrote_result = false;
12000    let canonical_word_binding = !word_prefixes.iter().any(|prefix| prefix == "w");
12001
12002    loop {
12003        match reader.read_event_into(&mut buffer)? {
12004            Event::Start(element) => {
12005                let inherited = contexts
12006                    .last()
12007                    .map(|context| context.prefixes.as_slice())
12008                    .unwrap_or(word_prefixes);
12009                let prefixes = word_prefixes_at(&element, inherited)?;
12010                let depth = contexts.len();
12011                let direct_run_child =
12012                    depth == 1 && contexts.last().is_some_and(|context| context.word_run);
12013                if direct_run_child
12014                    && is_word_element(element.name().as_ref(), b"fldChar", &prefixes)
12015                {
12016                    let kind = optional_word_attribute(&element, b"fldCharType", &prefixes);
12017                    let depth_before = field_depth;
12018                    let outer_marker = matches!(kind.as_deref(), Some("begin")) && field_depth == 0
12019                        || matches!(kind.as_deref(), Some("separate" | "end")) && field_depth == 1;
12020                    if outer_marker
12021                        && kind.as_deref() == Some("end")
12022                        && outer_result
12023                        && !wrote_result
12024                        && cached_changed
12025                    {
12026                        write_field_result_content_with_binding(
12027                            &mut writer,
12028                            &field.cached_result,
12029                            canonical_word_binding,
12030                        )?;
12031                        wrote_result = true;
12032                    }
12033                    if cached_changed
12034                        && outer_result
12035                        && depth_before == 1
12036                        && kind.as_deref() == Some("begin")
12037                        && !wrote_result
12038                    {
12039                        write_field_result_content_with_binding(
12040                            &mut writer,
12041                            &field.cached_result,
12042                            canonical_word_binding,
12043                        )?;
12044                        wrote_result = true;
12045                    }
12046                    update_complex_phase(kind.as_deref(), &mut field_depth, &mut outer_result);
12047                    if outer_marker {
12048                        let kind = kind.as_deref().unwrap_or_default();
12049                        write_rewritten_field_element(
12050                            &mut writer,
12051                            &element,
12052                            "w:fldChar",
12053                            &prefixes,
12054                            &[("fldCharType", kind)],
12055                            (kind == "begin").then_some(field.dirty).flatten(),
12056                            canonical_word_binding,
12057                            false,
12058                        )?;
12059                        contexts.push(FieldRewriteContext {
12060                            prefixes,
12061                            word_run: false,
12062                            canonical_end: Some("w:fldChar"),
12063                        });
12064                        buffer.clear();
12065                        continue;
12066                    }
12067                    if cached_changed
12068                        && wrote_result
12069                        && outer_result
12070                        && (depth_before > 1
12071                            || depth_before == 1 && kind.as_deref() == Some("begin"))
12072                    {
12073                        reader.read_to_end_into(element.name(), &mut Vec::new())?;
12074                        buffer.clear();
12075                        continue;
12076                    }
12077                }
12078                if direct_run_child
12079                    && outer_result
12080                    && field_depth == 1
12081                    && is_word_element(element.name().as_ref(), b"t", &prefixes)
12082                {
12083                    if cached_changed {
12084                        reader.read_to_end_into(element.name(), &mut Vec::new())?;
12085                        if !wrote_result {
12086                            write_field_result_content_with_binding(
12087                                &mut writer,
12088                                &field.cached_result,
12089                                canonical_word_binding,
12090                            )?;
12091                            wrote_result = true;
12092                        }
12093                    } else {
12094                        writer.write_event(Event::Start(element.into_owned()))?;
12095                        contexts.push(FieldRewriteContext {
12096                            prefixes,
12097                            word_run: false,
12098                            canonical_end: None,
12099                        });
12100                    }
12101                } else if direct_run_child
12102                    && cached_changed
12103                    && ((outer_result
12104                        && field_depth == 1
12105                        && (is_word_element(element.name().as_ref(), b"tab", &prefixes)
12106                            || is_word_element(element.name().as_ref(), b"br", &prefixes)))
12107                        || outer_result
12108                            && field_depth > 1
12109                            && (is_word_element(element.name().as_ref(), b"instrText", &prefixes)
12110                                || is_word_element(element.name().as_ref(), b"t", &prefixes)
12111                                || is_word_element(element.name().as_ref(), b"tab", &prefixes)
12112                                || is_word_element(element.name().as_ref(), b"br", &prefixes)))
12113                {
12114                    reader.read_to_end_into(element.name(), &mut Vec::new())?;
12115                    if outer_result && field_depth == 1 && !wrote_result {
12116                        write_field_result_content_with_binding(
12117                            &mut writer,
12118                            &field.cached_result,
12119                            canonical_word_binding,
12120                        )?;
12121                        wrote_result = true;
12122                    }
12123                } else {
12124                    let word_run =
12125                        depth == 0 && is_word_element(element.name().as_ref(), b"r", &prefixes);
12126                    writer.write_event(Event::Start(element.into_owned()))?;
12127                    contexts.push(FieldRewriteContext {
12128                        prefixes,
12129                        word_run,
12130                        canonical_end: None,
12131                    });
12132                }
12133            }
12134            Event::Empty(element) => {
12135                let inherited = contexts
12136                    .last()
12137                    .map(|context| context.prefixes.as_slice())
12138                    .unwrap_or(word_prefixes);
12139                let prefixes = word_prefixes_at(&element, inherited)?;
12140                let direct_run_child =
12141                    contexts.len() == 1 && contexts.last().is_some_and(|context| context.word_run);
12142                if direct_run_child
12143                    && is_word_element(element.name().as_ref(), b"fldChar", &prefixes)
12144                {
12145                    let kind = optional_word_attribute(&element, b"fldCharType", &prefixes);
12146                    let depth_before = field_depth;
12147                    let outer_marker = matches!(kind.as_deref(), Some("begin")) && field_depth == 0
12148                        || matches!(kind.as_deref(), Some("separate" | "end")) && field_depth == 1;
12149                    if outer_marker
12150                        && kind.as_deref() == Some("end")
12151                        && outer_result
12152                        && !wrote_result
12153                        && cached_changed
12154                    {
12155                        write_field_result_content_with_binding(
12156                            &mut writer,
12157                            &field.cached_result,
12158                            canonical_word_binding,
12159                        )?;
12160                        wrote_result = true;
12161                    }
12162                    if cached_changed
12163                        && outer_result
12164                        && depth_before == 1
12165                        && kind.as_deref() == Some("begin")
12166                        && !wrote_result
12167                    {
12168                        write_field_result_content_with_binding(
12169                            &mut writer,
12170                            &field.cached_result,
12171                            canonical_word_binding,
12172                        )?;
12173                        wrote_result = true;
12174                    }
12175                    update_complex_phase(kind.as_deref(), &mut field_depth, &mut outer_result);
12176                    if outer_marker {
12177                        let kind = kind.as_deref().unwrap_or_default();
12178                        write_rewritten_field_element(
12179                            &mut writer,
12180                            &element,
12181                            "w:fldChar",
12182                            &prefixes,
12183                            &[("fldCharType", kind)],
12184                            (kind == "begin").then_some(field.dirty).flatten(),
12185                            canonical_word_binding,
12186                            true,
12187                        )?;
12188                        buffer.clear();
12189                        continue;
12190                    }
12191                    if cached_changed
12192                        && wrote_result
12193                        && outer_result
12194                        && (depth_before > 1
12195                            || depth_before == 1 && kind.as_deref() == Some("begin"))
12196                    {
12197                        buffer.clear();
12198                        continue;
12199                    }
12200                }
12201                if direct_run_child
12202                    && outer_result
12203                    && field_depth == 1
12204                    && is_word_element(element.name().as_ref(), b"t", &prefixes)
12205                {
12206                    if cached_changed {
12207                        if !wrote_result {
12208                            write_field_result_content_with_binding(
12209                                &mut writer,
12210                                &field.cached_result,
12211                                canonical_word_binding,
12212                            )?;
12213                            wrote_result = true;
12214                        }
12215                    } else {
12216                        writer.write_event(Event::Empty(element.into_owned()))?;
12217                    }
12218                } else if direct_run_child
12219                    && cached_changed
12220                    && ((outer_result
12221                        && field_depth == 1
12222                        && (is_word_element(element.name().as_ref(), b"tab", &prefixes)
12223                            || is_word_element(element.name().as_ref(), b"br", &prefixes)))
12224                        || outer_result
12225                            && field_depth > 1
12226                            && (is_word_element(element.name().as_ref(), b"instrText", &prefixes)
12227                                || is_word_element(element.name().as_ref(), b"t", &prefixes)
12228                                || is_word_element(element.name().as_ref(), b"tab", &prefixes)
12229                                || is_word_element(element.name().as_ref(), b"br", &prefixes)))
12230                {
12231                    if outer_result && field_depth == 1 && !wrote_result {
12232                        write_field_result_content_with_binding(
12233                            &mut writer,
12234                            &field.cached_result,
12235                            canonical_word_binding,
12236                        )?;
12237                        wrote_result = true;
12238                    }
12239                } else {
12240                    writer.write_event(Event::Empty(element.into_owned()))?;
12241                }
12242            }
12243            Event::End(element) => {
12244                if let Some(name) = contexts.pop().and_then(|context| context.canonical_end) {
12245                    writer.write_event(Event::End(BytesEnd::new(name)))?;
12246                } else {
12247                    writer.write_event(Event::End(element.into_owned()))?;
12248                }
12249            }
12250            Event::Eof => break,
12251            event => writer.write_event(event.into_owned())?,
12252        }
12253        buffer.clear();
12254    }
12255    Ok(output)
12256}
12257
12258fn update_complex_phase(kind: Option<&str>, field_depth: &mut usize, outer_result: &mut bool) {
12259    match kind {
12260        Some("begin") => *field_depth += 1,
12261        Some("separate") if *field_depth == 1 => *outer_result = true,
12262        Some("end") if *field_depth == 1 => {
12263            *outer_result = false;
12264            *field_depth = 0;
12265        }
12266        Some("end") if *field_depth > 1 => *field_depth -= 1,
12267        _ => {}
12268    }
12269}
12270
12271#[allow(clippy::too_many_arguments)]
12272fn write_rewritten_field_element<W: std::io::Write>(
12273    writer: &mut Writer<W>,
12274    source: &BytesStart<'_>,
12275    name: &'static str,
12276    word_prefixes: &[String],
12277    replacements: &[(&str, &str)],
12278    dirty: Option<bool>,
12279    canonical_word_binding: bool,
12280    empty: bool,
12281) -> Result<()> {
12282    let mut element = BytesStart::new(name);
12283    for attribute in source.attributes() {
12284        let attribute = attribute?;
12285        let key = attribute.key.as_ref();
12286        if canonical_word_binding && key == b"xmlns:w" {
12287            continue;
12288        }
12289        if replacements
12290            .iter()
12291            .any(|(local, _)| is_field_word_attribute(key, local.as_bytes(), word_prefixes))
12292            || is_field_word_attribute(key, b"dirty", word_prefixes)
12293        {
12294            continue;
12295        }
12296        element.push_attribute(attribute);
12297    }
12298    if canonical_word_binding {
12299        element.push_attribute(("xmlns:w", crate::namespace::W_NS));
12300    }
12301    for (local, value) in replacements {
12302        let name = match *local {
12303            "instr" => "w:instr",
12304            "fldCharType" => "w:fldCharType",
12305            _ => {
12306                return Err(OxmlError::MissingElement(format!(
12307                    "field attribute {local}"
12308                )));
12309            }
12310        };
12311        element.push_attribute((name, *value));
12312    }
12313    push_dirty_attribute(&mut element, dirty);
12314    writer.write_event(if empty {
12315        Event::Empty(element)
12316    } else {
12317        Event::Start(element)
12318    })?;
12319    Ok(())
12320}
12321
12322fn is_field_word_attribute(key: &[u8], local: &[u8], word_prefixes: &[String]) -> bool {
12323    let Some(separator) = key.iter().position(|byte| *byte == b':') else {
12324        return false;
12325    };
12326    key.get(separator + 1..) == Some(local)
12327        && word_prefixes
12328            .iter()
12329            .any(|prefix| prefix.as_bytes() == &key[..separator])
12330}
12331
12332fn write_updated_text_element<W: std::io::Write>(
12333    reader: &mut Reader<&[u8]>,
12334    writer: &mut Writer<W>,
12335    source: &BytesStart<'_>,
12336    value: Option<&str>,
12337    canonical_word_binding: bool,
12338) -> Result<()> {
12339    reader.read_to_end_into(source.name(), &mut Vec::new())?;
12340    if let Some(value) = value {
12341        write_field_result_content_inner(writer, value, Some(source), canonical_word_binding)?;
12342    } else {
12343        write_field_text_with_source(writer, "", Some(source), canonical_word_binding)?;
12344    }
12345    Ok(())
12346}
12347
12348fn write_updated_empty_text_element<W: std::io::Write>(
12349    writer: &mut Writer<W>,
12350    source: &BytesStart<'_>,
12351    value: Option<&str>,
12352    canonical_word_binding: bool,
12353) -> Result<()> {
12354    if let Some(value) = value {
12355        write_field_result_content_inner(writer, value, Some(source), canonical_word_binding)?;
12356    } else {
12357        write_field_text_with_source(writer, "", Some(source), canonical_word_binding)?;
12358    }
12359    Ok(())
12360}
12361
12362fn write_field_result_content<W: std::io::Write>(
12363    writer: &mut Writer<W>,
12364    value: &str,
12365) -> Result<()> {
12366    write_field_result_content_with_binding(writer, value, false)
12367}
12368
12369fn write_field_result_content_with_binding<W: std::io::Write>(
12370    writer: &mut Writer<W>,
12371    value: &str,
12372    canonical_word_binding: bool,
12373) -> Result<()> {
12374    write_field_result_content_inner(writer, value, None, canonical_word_binding)
12375}
12376
12377fn write_field_result_content_inner<W: std::io::Write>(
12378    writer: &mut Writer<W>,
12379    value: &str,
12380    source: Option<&BytesStart<'_>>,
12381    canonical_word_binding: bool,
12382) -> Result<()> {
12383    let mut start = 0usize;
12384    let mut used_source = false;
12385    for (index, character) in value
12386        .char_indices()
12387        .chain(std::iter::once((value.len(), '\0')))
12388    {
12389        let element = match character {
12390            '\t' => Some(("w:tab", None)),
12391            '\n' => Some(("w:br", None)),
12392            '\u{000c}' => Some(("w:br", Some("page"))),
12393            '\u{000b}' => Some(("w:br", Some("column"))),
12394            '\0' if index == value.len() => None,
12395            _ => continue,
12396        };
12397        if start < index || value.is_empty() {
12398            write_field_text_with_source(
12399                writer,
12400                &value[start..index],
12401                (!used_source).then_some(source).flatten(),
12402                canonical_word_binding,
12403            )?;
12404            used_source = true;
12405        }
12406        if let Some((name, break_type)) = element {
12407            if !used_source && source.is_some() {
12408                write_field_text_with_source(writer, "", source, canonical_word_binding)?;
12409                used_source = true;
12410            }
12411            let mut element = BytesStart::new(name);
12412            if canonical_word_binding {
12413                element.push_attribute(("xmlns:w", crate::namespace::W_NS));
12414            }
12415            if let Some(break_type) = break_type {
12416                element.push_attribute(("w:type", break_type));
12417            }
12418            writer.write_event(Event::Empty(element))?;
12419            start = index + character.len_utf8();
12420        }
12421    }
12422    Ok(())
12423}
12424
12425fn write_field_text_with_source<W: std::io::Write>(
12426    writer: &mut Writer<W>,
12427    value: &str,
12428    source: Option<&BytesStart<'_>>,
12429    canonical_word_binding: bool,
12430) -> Result<()> {
12431    // Keep the source Word alias when canonical w would shadow foreign
12432    // attribute bindings. Generated text without a source gets its own scope.
12433    let name = if canonical_word_binding {
12434        source
12435            .map(|source| {
12436                let name = source.name();
12437                let name = std::str::from_utf8(name.as_ref())?;
12438                Ok::<_, OxmlError>(
12439                    name.rsplit_once(':')
12440                        .map_or_else(|| "t".to_owned(), |(prefix, _)| format!("{prefix}:t")),
12441                )
12442            })
12443            .transpose()?
12444            .unwrap_or_else(|| "w:t".to_owned())
12445    } else {
12446        "w:t".to_owned()
12447    };
12448    let mut element = BytesStart::new(name.as_str());
12449    if let Some(source) = source {
12450        for attribute in source.attributes() {
12451            let attribute = attribute?;
12452            if attribute.key.as_ref() != b"xml:space" {
12453                element.push_attribute(attribute);
12454            }
12455        }
12456    }
12457    if canonical_word_binding && source.is_none() {
12458        element.push_attribute(("xmlns:w", crate::namespace::W_NS));
12459    }
12460    if needs_space_preservation(value) {
12461        element.push_attribute(("xml:space", "preserve"));
12462    }
12463    writer.write_event(Event::Start(element))?;
12464    writer.write_event(Event::Text(BytesText::new(value)))?;
12465    writer.write_event(Event::End(BytesEnd::new(name.as_str())))?;
12466    Ok(())
12467}
12468
12469fn needs_space_preservation(value: &str) -> bool {
12470    value.chars().next().is_some_and(char::is_whitespace)
12471        || value.chars().next_back().is_some_and(char::is_whitespace)
12472}
12473
12474fn instruction_for_write(field: &Field) -> FieldInstruction {
12475    let original = match &field.source {
12476        FieldSource::New {
12477            original_instruction,
12478            ..
12479        }
12480        | FieldSource::Parsed {
12481            original_instruction,
12482            ..
12483        } => original_instruction,
12484    };
12485    let raw_changed = field.instruction.raw != original.raw;
12486    let structured_changed = !instruction_structure_eq(&field.instruction, original);
12487
12488    if raw_changed && !structured_changed {
12489        return parse_field_instruction(&field.instruction.raw);
12490    }
12491    if structured_changed {
12492        let mut instruction = field.instruction.clone();
12493        instruction.raw = canonical_instruction_text(&instruction);
12494        return instruction;
12495    }
12496    field.instruction.clone()
12497}
12498
12499fn instruction_structure_eq(left: &FieldInstruction, right: &FieldInstruction) -> bool {
12500    left.name == right.name
12501        && left.arguments.len() == right.arguments.len()
12502        && left
12503            .arguments
12504            .iter()
12505            .zip(&right.arguments)
12506            .all(|(left, right)| match (left, right) {
12507                (FieldArgument::Text(left), FieldArgument::Text(right)) => left == right,
12508                (FieldArgument::Nested(left), FieldArgument::Nested(right)) => {
12509                    instruction_structure_eq(&left.instruction, &right.instruction)
12510                }
12511                _ => false,
12512            })
12513        && left.switches.len() == right.switches.len()
12514        && left
12515            .switches
12516            .iter()
12517            .zip(&right.switches)
12518            .all(|(left, right)| {
12519                left.name == right.name
12520                    && match (&left.argument, &right.argument) {
12521                        (None, None) => true,
12522                        (Some(FieldArgument::Text(left)), Some(FieldArgument::Text(right))) => {
12523                            left == right
12524                        }
12525                        (Some(FieldArgument::Nested(left)), Some(FieldArgument::Nested(right))) => {
12526                            instruction_structure_eq(&left.instruction, &right.instruction)
12527                        }
12528                        _ => false,
12529                    }
12530            })
12531}
12532
12533fn instruction_source_identity_eq(left: &FieldInstruction, right: &FieldInstruction) -> bool {
12534    let argument_identities_match =
12535        left.arguments
12536            .iter()
12537            .zip(&right.arguments)
12538            .all(|(left, right)| match (left, right) {
12539                (FieldArgument::Nested(left), FieldArgument::Nested(right)) => {
12540                    field_source_identity_eq(left, right)
12541                }
12542                (FieldArgument::Text(_), FieldArgument::Text(_)) => true,
12543                _ => false,
12544            });
12545    let switch_identities_match = left
12546        .switches
12547        .iter()
12548        .zip(&right.switches)
12549        .all(|(left, right)| match (&left.argument, &right.argument) {
12550            (Some(FieldArgument::Nested(left)), Some(FieldArgument::Nested(right))) => {
12551                field_source_identity_eq(left, right)
12552            }
12553            (None, None) | (Some(FieldArgument::Text(_)), Some(FieldArgument::Text(_))) => true,
12554            _ => false,
12555        });
12556    left.arguments.len() == right.arguments.len()
12557        && left.switches.len() == right.switches.len()
12558        && argument_identities_match
12559        && switch_identities_match
12560}
12561
12562fn field_source_identity_eq(left: &Field, right: &Field) -> bool {
12563    match (&left.source, &right.source) {
12564        (
12565            FieldSource::Parsed {
12566                source_id: left_id, ..
12567            },
12568            FieldSource::Parsed {
12569                source_id: right_id,
12570                ..
12571            },
12572        ) => {
12573            left_id == right_id
12574                && instruction_source_identity_eq(&left.instruction, &right.instruction)
12575        }
12576        _ => false,
12577    }
12578}
12579
12580fn canonical_instruction_text(instruction: &FieldInstruction) -> String {
12581    let mut text = instruction.name.clone();
12582    for argument in &instruction.arguments {
12583        if let FieldArgument::Text(value) = argument {
12584            push_canonical_field_token(&mut text, value, false);
12585        }
12586    }
12587    for switch in &instruction.switches {
12588        text.push(' ');
12589        text.push('\\');
12590        text.push_str(&switch.name);
12591        if let Some(FieldArgument::Text(value)) = &switch.argument {
12592            push_canonical_field_token(
12593                &mut text,
12594                value,
12595                !switch_takes_argument(&instruction.name, &switch.name),
12596            );
12597        }
12598    }
12599    text
12600}
12601
12602fn instruction_contains_nested(instruction: &FieldInstruction) -> bool {
12603    instruction
12604        .arguments
12605        .iter()
12606        .any(|argument| matches!(argument, FieldArgument::Nested(_)))
12607        || instruction
12608            .switches
12609            .iter()
12610            .any(|switch| matches!(switch.argument, Some(FieldArgument::Nested(_))))
12611}
12612
12613fn write_simple_field<W: std::io::Write>(
12614    writer: &mut Writer<W>,
12615    field: &Field,
12616    foreign_word_namespace: Option<&str>,
12617    result_properties: Option<&CT_RPr>,
12618) -> Result<()> {
12619    let mut element = BytesStart::new("w:fldSimple");
12620    if foreign_word_namespace.is_some() {
12621        element.push_attribute(("xmlns:w", crate::namespace::W_NS));
12622    }
12623    element.push_attribute(("w:instr", field.instruction.raw.as_str()));
12624    push_dirty_attribute(&mut element, field.dirty);
12625    push_lock_attribute(&mut element, field.locked);
12626    writer.write_event(Event::Start(element))?;
12627    write_field_result_run(writer, field, foreign_word_namespace, result_properties)?;
12628    writer.write_event(Event::End(BytesEnd::new("w:fldSimple")))?;
12629    Ok(())
12630}
12631
12632fn write_complex_field<W: std::io::Write>(
12633    writer: &mut Writer<W>,
12634    field: &Field,
12635    foreign_word_namespace: Option<&str>,
12636    result_properties: Option<&CT_RPr>,
12637) -> Result<()> {
12638    write_field_char_run(
12639        writer,
12640        "begin",
12641        field.dirty,
12642        field.locked,
12643        foreign_word_namespace,
12644    )?;
12645    let mut text = format!(" {}", field.instruction.name);
12646    for argument in &field.instruction.arguments {
12647        match argument {
12648            FieldArgument::Text(value) => push_canonical_field_token(&mut text, value, false),
12649            FieldArgument::Nested(field) => {
12650                text.push(' ');
12651                write_instruction_run(writer, &text, foreign_word_namespace)?;
12652                text.clear();
12653                write_nested_instruction_field(writer, field, foreign_word_namespace)?;
12654            }
12655        }
12656    }
12657    for switch in &field.instruction.switches {
12658        text.push(' ');
12659        text.push('\\');
12660        text.push_str(&switch.name);
12661        if let Some(argument) = &switch.argument {
12662            match argument {
12663                FieldArgument::Text(value) => push_canonical_field_token(
12664                    &mut text,
12665                    value,
12666                    !switch_takes_argument(&field.instruction.name, &switch.name),
12667                ),
12668                FieldArgument::Nested(field) => {
12669                    text.push(' ');
12670                    write_instruction_run(writer, &text, foreign_word_namespace)?;
12671                    text.clear();
12672                    write_nested_instruction_field(writer, field, foreign_word_namespace)?;
12673                }
12674            }
12675        }
12676    }
12677    text.push(' ');
12678    if !text.trim().is_empty() {
12679        write_instruction_run(writer, &text, foreign_word_namespace)?;
12680    }
12681    write_field_char_run(writer, "separate", None, None, foreign_word_namespace)?;
12682    write_field_result_run(writer, field, foreign_word_namespace, result_properties)?;
12683    write_field_char_run(writer, "end", None, None, foreign_word_namespace)?;
12684    Ok(())
12685}
12686
12687fn write_nested_instruction_field<W: std::io::Write>(
12688    writer: &mut Writer<W>,
12689    field: &Field,
12690    foreign_word_namespace: Option<&str>,
12691) -> Result<()> {
12692    let mut field = field.clone();
12693    field.instruction = instruction_for_write(&field);
12694    write_complex_field(writer, &field, foreign_word_namespace, None)
12695}
12696
12697fn push_canonical_field_token(output: &mut String, value: &str, force_quotes: bool) {
12698    output.push(' ');
12699    if force_quotes
12700        || value.is_empty()
12701        || value.starts_with('\\')
12702        || value.chars().any(char::is_whitespace)
12703        || value.contains('"')
12704    {
12705        output.push('"');
12706        for character in value.chars() {
12707            if matches!(character, '"' | '\\') {
12708                output.push('\\');
12709            }
12710            output.push(character);
12711        }
12712        output.push('"');
12713    } else {
12714        output.push_str(value);
12715    }
12716}
12717
12718fn write_instruction_run<W: std::io::Write>(
12719    writer: &mut Writer<W>,
12720    instruction: &str,
12721    foreign_word_namespace: Option<&str>,
12722) -> Result<()> {
12723    write_word_run_start(writer, foreign_word_namespace)?;
12724    let mut element = BytesStart::new("w:instrText");
12725    element.push_attribute(("xml:space", "preserve"));
12726    writer.write_event(Event::Start(element))?;
12727    writer.write_event(Event::Text(BytesText::new(instruction)))?;
12728    writer.write_event(Event::End(BytesEnd::new("w:instrText")))?;
12729    writer.write_event(Event::End(BytesEnd::new("w:r")))?;
12730    Ok(())
12731}
12732
12733fn write_field_char_run<W: std::io::Write>(
12734    writer: &mut Writer<W>,
12735    kind: &str,
12736    dirty: Option<bool>,
12737    locked: Option<bool>,
12738    foreign_word_namespace: Option<&str>,
12739) -> Result<()> {
12740    write_word_run_start(writer, foreign_word_namespace)?;
12741    let mut element = BytesStart::new("w:fldChar");
12742    element.push_attribute(("w:fldCharType", kind));
12743    push_dirty_attribute(&mut element, dirty);
12744    push_lock_attribute(&mut element, locked);
12745    writer.write_event(Event::Empty(element))?;
12746    writer.write_event(Event::End(BytesEnd::new("w:r")))?;
12747    Ok(())
12748}
12749
12750fn write_field_result_run<W: std::io::Write>(
12751    writer: &mut Writer<W>,
12752    field: &Field,
12753    foreign_word_namespace: Option<&str>,
12754    properties: Option<&CT_RPr>,
12755) -> Result<()> {
12756    if let Some(runs) = field.typed_cache() {
12757        return write_typed_cached_runs(
12758            writer,
12759            runs,
12760            &field.typed_cached_comment_ranges,
12761            foreign_word_namespace,
12762        );
12763    }
12764    if let FieldSource::New {
12765        cached_runs: Some(runs),
12766        original_cached_result,
12767        ..
12768    } = &field.source
12769        && (field.cached_result == *original_cached_result
12770            || field.nested_cached_projection().as_ref() == Some(&field.cached_result))
12771    {
12772        for run in runs {
12773            if run
12774                .content
12775                .iter()
12776                .any(|content| matches!(content, RunContent::Field(_)))
12777            {
12778                write_mixed_field_run(writer, run, foreign_word_namespace)?;
12779            } else {
12780                run.to_xml_with_word_override(writer, foreign_word_namespace)?;
12781            }
12782        }
12783        return Ok(());
12784    }
12785    write_word_run_start(writer, foreign_word_namespace)?;
12786    let properties = properties.or_else(|| match &field.source {
12787        FieldSource::New {
12788            cached_runs: Some(runs),
12789            ..
12790        } => runs.first().and_then(|run| run.properties.as_ref()),
12791        _ => None,
12792    });
12793    if let Some(properties) = properties {
12794        properties.to_xml_with_word_override(writer, foreign_word_namespace)?;
12795    }
12796    let display = if field.cached_result.is_empty()
12797        && matches!(field.instruction.name.as_str(), "PAGE" | "NUMPAGES")
12798    {
12799        "1"
12800    } else {
12801        &field.cached_result
12802    };
12803    write_field_result_content(writer, display)?;
12804    writer.write_event(Event::End(BytesEnd::new("w:r")))?;
12805    Ok(())
12806}
12807
12808fn write_word_run_start<W: std::io::Write>(
12809    writer: &mut Writer<W>,
12810    foreign_word_namespace: Option<&str>,
12811) -> Result<()> {
12812    let mut run = BytesStart::new("w:r");
12813    if foreign_word_namespace.is_some() {
12814        run.push_attribute(("xmlns:w", crate::namespace::W_NS));
12815    }
12816    writer.write_event(Event::Start(run))?;
12817    Ok(())
12818}
12819
12820fn push_lock_attribute(element: &mut BytesStart<'_>, locked: Option<bool>) {
12821    if let Some(locked) = locked {
12822        element.push_attribute(("w:fldLock", if locked { "1" } else { "0" }));
12823    }
12824}
12825
12826struct FieldLockOwner<'a> {
12827    start: usize,
12828    end: usize,
12829    element: BytesStart<'a>,
12830    prefixes: Vec<String>,
12831    empty: bool,
12832}
12833
12834fn field_lock_owner<'a>(
12835    raw: &'a [u8],
12836    form: FieldForm,
12837    word_prefixes: &[String],
12838) -> Result<Option<FieldLockOwner<'a>>> {
12839    let mut reader = Reader::from_reader(raw);
12840    let mut contexts = Vec::<(Vec<String>, bool)>::new();
12841    loop {
12842        let start = reader.buffer_position() as usize;
12843        let event = reader.read_event()?;
12844        match event {
12845            Event::Start(ref element) | Event::Empty(ref element) => {
12846                let prefixes = word_prefixes_at(
12847                    element,
12848                    contexts
12849                        .last()
12850                        .map(|context| context.0.as_slice())
12851                        .unwrap_or(word_prefixes),
12852                )?;
12853                let owner = match form {
12854                    FieldForm::Simple => {
12855                        contexts.is_empty()
12856                            && is_word_element(element.name().as_ref(), b"fldSimple", &prefixes)
12857                    }
12858                    FieldForm::Complex => {
12859                        contexts.len() == 1
12860                            && contexts[0].1
12861                            && is_word_element(element.name().as_ref(), b"fldChar", &prefixes)
12862                            && optional_word_attribute(element, b"fldCharType", &prefixes)
12863                                .as_deref()
12864                                == Some("begin")
12865                    }
12866                };
12867                if owner {
12868                    return Ok(Some(FieldLockOwner {
12869                        start,
12870                        end: reader.buffer_position() as usize,
12871                        element: element.clone(),
12872                        prefixes,
12873                        empty: matches!(event, Event::Empty(_)),
12874                    }));
12875                }
12876                if matches!(event, Event::Start(_)) {
12877                    let word_run = contexts.is_empty()
12878                        && is_word_element(element.name().as_ref(), b"r", &prefixes);
12879                    contexts.push((prefixes, word_run));
12880                }
12881            }
12882            Event::End(_) => {
12883                contexts.pop();
12884            }
12885            Event::Eof => return Ok(None),
12886            _ => {}
12887        }
12888    }
12889}
12890
12891fn field_source_lock(
12892    raw: &[u8],
12893    form: FieldForm,
12894    word_prefixes: &[String],
12895) -> Result<Option<bool>> {
12896    Ok(
12897        field_lock_owner(raw, form, word_prefixes)?.and_then(|owner| {
12898            optional_word_attribute(&owner.element, b"fldLock", &owner.prefixes)
12899                .and_then(|value| parse_field_bool(&value))
12900        }),
12901    )
12902}
12903
12904fn rewrite_field_lock(
12905    raw: &[u8],
12906    form: FieldForm,
12907    word_prefixes: &[String],
12908    locked: Option<bool>,
12909) -> Result<Vec<u8>> {
12910    let owner = field_lock_owner(raw, form, word_prefixes)?
12911        .ok_or_else(|| OxmlError::MissingElement("field lock owner".into()))?;
12912    let owner_name = std::str::from_utf8(owner.element.name().as_ref())?.to_owned();
12913    let mut replacement = BytesStart::new(owner_name.as_str());
12914    for attribute in owner.element.attributes() {
12915        let attribute = attribute?;
12916        if !is_field_word_attribute(attribute.key.as_ref(), b"fldLock", &owner.prefixes) {
12917            replacement.push_attribute(attribute);
12918        }
12919    }
12920    if let Some(value) = locked {
12921        let prefix = owner_name
12922            .split_once(':')
12923            .map(|(prefix, _)| prefix)
12924            .unwrap_or("w");
12925        if !owner_name.contains(':') {
12926            replacement.push_attribute(("xmlns:w", crate::namespace::W_NS));
12927        }
12928        let name = format!("{prefix}:fldLock");
12929        replacement.push_attribute((name.as_str(), if value { "1" } else { "0" }));
12930    }
12931    let mut writer = Writer::new(raw[..owner.start].to_vec());
12932    writer.write_event(if owner.empty {
12933        Event::Empty(replacement)
12934    } else {
12935        Event::Start(replacement)
12936    })?;
12937    writer.get_mut().extend_from_slice(&raw[owner.end..]);
12938    Ok(writer.into_inner())
12939}
12940
12941fn push_dirty_attribute(element: &mut BytesStart<'_>, dirty: Option<bool>) {
12942    if let Some(dirty) = dirty {
12943        element.push_attribute(("w:dirty", if dirty { "1" } else { "0" }));
12944    }
12945}
12946
12947fn push_comment_marker(
12948    markers: &mut Vec<CommentRangeMarker>,
12949    extra_xml: &[(usize, Vec<u8>)],
12950    run_index: usize,
12951    id: i32,
12952    start: bool,
12953    has_child_content: bool,
12954) {
12955    let raw_before = raw_xml_count_at(extra_xml, run_index);
12956    let marker = if start {
12957        CommentRangeMarker::Start {
12958            id,
12959            run_index,
12960            raw_before,
12961            has_child_content,
12962        }
12963    } else {
12964        CommentRangeMarker::End {
12965            id,
12966            run_index,
12967            raw_before,
12968            has_child_content,
12969        }
12970    };
12971    markers.push(marker);
12972}
12973
12974fn raw_element_has_child_content(raw: &[u8]) -> bool {
12975    let mut reader = Reader::from_reader(raw);
12976    reader.config_mut().trim_text(false);
12977    let mut buffer = Vec::new();
12978    let mut depth = 0usize;
12979    loop {
12980        match reader.read_event_into(&mut buffer) {
12981            Ok(Event::Start(_)) => {
12982                if depth > 0 {
12983                    return true;
12984                }
12985                depth += 1;
12986            }
12987            Ok(Event::Empty(_)) if depth > 0 => return true,
12988            Ok(Event::Text(text))
12989                if depth > 0 && !text.as_ref().iter().all(u8::is_ascii_whitespace) =>
12990            {
12991                return true;
12992            }
12993            Ok(Event::CData(text))
12994                if depth > 0 && !text.as_ref().iter().all(u8::is_ascii_whitespace) =>
12995            {
12996                return true;
12997            }
12998            Ok(Event::GeneralRef(reference)) if depth > 0 => match reference.resolve_char_ref() {
12999                Ok(Some(character)) if !character.is_ascii_whitespace() => return true,
13000                Ok(Some(_)) => {}
13001                Ok(None) => return true,
13002                Err(_) => return false,
13003            },
13004            Ok(Event::End(_)) => depth = depth.saturating_sub(1),
13005            Ok(Event::Eof) | Err(_) => return false,
13006            _ => {}
13007        }
13008        buffer.clear();
13009    }
13010}
13011
13012fn raw_xml_count_at(extra_xml: &[(usize, Vec<u8>)], run_index: usize) -> usize {
13013    extra_xml
13014        .iter()
13015        .filter(|(position, raw)| *position == run_index && !is_xml_whitespace(raw))
13016        .count()
13017}
13018
13019fn common_prefix_len(left: &[AcceptedRunPathSegment], right: &[AcceptedRunPathSegment]) -> usize {
13020    left.iter()
13021        .zip(right)
13022        .take_while(|(left, right)| left == right)
13023        .count()
13024}
13025
13026/// Serialize one canonical comment or bookmark range marker.
13027fn range_marker_xml(anchor: RangeAnchor<'_>, start: bool) -> Option<Vec<u8>> {
13028    let tag = match anchor {
13029        RangeAnchor::Comment(_) if start => "w:commentRangeStart",
13030        RangeAnchor::Comment(_) => "w:commentRangeEnd",
13031        RangeAnchor::Bookmark { .. } if start => "w:bookmarkStart",
13032        RangeAnchor::Bookmark { .. } => "w:bookmarkEnd",
13033        RangeAnchor::Permission { .. } if start => "w:permStart",
13034        RangeAnchor::Permission { .. } => "w:permEnd",
13035        RangeAnchor::Proofing { .. } => "w:proofErr",
13036    };
13037    let mut value = itoa::Buffer::new();
13038    let mut element = BytesStart::new(tag);
13039    match anchor {
13040        RangeAnchor::Comment(id)
13041        | RangeAnchor::Bookmark { id, .. }
13042        | RangeAnchor::Permission { id, .. } => {
13043            element.push_attribute(("w:id", value.format(id)));
13044        }
13045        RangeAnchor::Proofing { kind } => {
13046            let value = match (kind, start) {
13047                ("spell", true) => "spellStart",
13048                ("spell", false) => "spellEnd",
13049                ("gram", true) => "gramStart",
13050                ("gram", false) => "gramEnd",
13051                _ => return None,
13052            };
13053            element.push_attribute(("w:type", value));
13054        }
13055    }
13056    if let RangeAnchor::Bookmark { name, .. } = anchor
13057        && start
13058    {
13059        element.push_attribute(("w:name", name));
13060    }
13061    if let RangeAnchor::Permission { editor, group, .. } = anchor
13062        && start
13063    {
13064        if let Some(editor) = editor {
13065            element.push_attribute(("w:ed", editor));
13066        }
13067        if let Some(group) = group {
13068            element.push_attribute(("w:edGrp", group));
13069        }
13070    }
13071    let mut raw = Vec::new();
13072    Writer::new(&mut raw)
13073        .write_event(Event::Empty(element))
13074        .ok()?;
13075    Some(raw)
13076}
13077
13078/// Rewrite the id of one facade-authored bookmark marker through `remap`,
13079/// leaving any other raw child unchanged.
13080pub(crate) fn remap_authored_bookmark_marker(
13081    raw: &mut Vec<u8>,
13082    remap: &std::collections::HashMap<i32, i32>,
13083) {
13084    let Ok(text) = std::str::from_utf8(raw) else {
13085        return;
13086    };
13087    if !(text.starts_with("<w:bookmarkStart ") || text.starts_with("<w:bookmarkEnd ")) {
13088        return;
13089    }
13090    let Some(attribute) = text.find("w:id=\"") else {
13091        return;
13092    };
13093    let value_start = attribute + "w:id=\"".len();
13094    let Some(value_end) = text[value_start..].find('"').map(|end| value_start + end) else {
13095        return;
13096    };
13097    let Ok(old) = text[value_start..value_end].parse::<i32>() else {
13098        return;
13099    };
13100    let Some(updated) = remap.get(&old) else {
13101        return;
13102    };
13103    let mut replaced = Vec::with_capacity(raw.len());
13104    replaced.extend_from_slice(&raw[..value_start]);
13105    replaced.extend_from_slice(updated.to_string().as_bytes());
13106    replaced.extend_from_slice(&raw[value_end..]);
13107    *raw = replaced;
13108}
13109
13110/// Whether a preserved run child is a field character or field code, which
13111/// a run keeps as raw XML when the rest of its complex field lies in other
13112/// runs.
13113fn raw_is_complex_field_part(raw: &[u8]) -> bool {
13114    let mut reader = Reader::from_reader(raw);
13115    let mut buffer = Vec::new();
13116    loop {
13117        match reader.read_event_into(&mut buffer) {
13118            Ok(Event::Start(element) | Event::Empty(element)) => {
13119                return [b"fldChar".as_slice(), b"instrText", b"delInstrText"]
13120                    .iter()
13121                    .any(|local| matches_local_name(element.name().as_ref(), local));
13122            }
13123            Ok(Event::Eof) | Err(_) => return false,
13124            _ => {}
13125        }
13126        buffer.clear();
13127    }
13128}
13129
13130/// Return the id of a preserved comment range marker, or `None` for any
13131/// other raw child.
13132pub(crate) fn raw_comment_marker_id(raw: &[u8], word_prefixes: &[String]) -> Option<i32> {
13133    let mut reader = Reader::from_reader(raw);
13134    let mut buffer = Vec::new();
13135    loop {
13136        match reader.read_event_into(&mut buffer).ok()? {
13137            Event::Start(element) | Event::Empty(element) => {
13138                let prefixes = word_prefixes_at(&element, word_prefixes).ok()?;
13139                let name = element.name();
13140                if !is_word_element(name.as_ref(), b"commentRangeStart", &prefixes)
13141                    && !is_word_element(name.as_ref(), b"commentRangeEnd", &prefixes)
13142                {
13143                    return None;
13144                }
13145                return required_word_i32_attribute(&element, b"id", &prefixes).ok();
13146            }
13147            Event::Eof => return None,
13148            _ => {}
13149        }
13150        buffer.clear();
13151    }
13152}
13153
13154fn parse_hyperlink_children(
13155    raw: &[u8],
13156    word_prefixes: &[String],
13157) -> Result<ParsedHyperlinkChildren> {
13158    let mut reader = Reader::from_reader(raw);
13159    reader.config_mut().trim_text(false);
13160    let mut runs = Vec::new();
13161    let mut run_sources = Vec::new();
13162    let mut revisions = Vec::new();
13163    let mut extra_xml = Vec::new();
13164    let mut revisions_at_boundary = 0usize;
13165    let mut buffer = Vec::new();
13166    let mut inside = false;
13167    loop {
13168        match reader.read_event_into(&mut buffer)? {
13169            Event::Start(element) if !inside => {
13170                inside = true;
13171                let _ = word_prefixes_at(&element, word_prefixes)?;
13172            }
13173            Event::Start(element) => {
13174                let prefixes = word_prefixes_at(&element, word_prefixes)?;
13175                if is_word_element(element.name().as_ref(), b"r", &prefixes) {
13176                    let raw = capture_element(&mut reader, &element)?;
13177                    runs.push(parse_run_raw(&raw, &prefixes)?);
13178                    run_sources.push(Some(raw));
13179                    revisions_at_boundary = 0;
13180                } else if is_content_revision_element(element.name().as_ref(), &prefixes) {
13181                    let raw = capture_element(&mut reader, &element)?;
13182                    if let Some(revision) = CT_Revision::from_raw(raw.clone(), &prefixes) {
13183                        revisions.push((runs.len(), revision));
13184                        revisions_at_boundary += 1;
13185                    } else {
13186                        extra_xml.push((runs.len(), revisions_at_boundary, raw));
13187                    }
13188                } else {
13189                    extra_xml.push((
13190                        runs.len(),
13191                        revisions_at_boundary,
13192                        capture_element(&mut reader, &element)?,
13193                    ));
13194                }
13195            }
13196            Event::Empty(element) => {
13197                let prefixes = word_prefixes_at(&element, word_prefixes)?;
13198                let raw = capture_empty_element(&element)?;
13199                if is_content_revision_element(element.name().as_ref(), &prefixes) {
13200                    if let Some(revision) = CT_Revision::from_raw(raw.clone(), &prefixes) {
13201                        revisions.push((runs.len(), revision));
13202                        revisions_at_boundary += 1;
13203                    } else {
13204                        extra_xml.push((runs.len(), revisions_at_boundary, raw));
13205                    }
13206                } else {
13207                    extra_xml.push((runs.len(), revisions_at_boundary, raw));
13208                }
13209            }
13210            Event::End(element)
13211                if inside && matches_local_name(element.name().as_ref(), b"hyperlink") =>
13212            {
13213                break;
13214            }
13215            Event::Text(text) if is_xml_whitespace(text.as_ref()) => {
13216                extra_xml.push((runs.len(), revisions_at_boundary, text.as_ref().to_vec()));
13217            }
13218            Event::Comment(comment) => {
13219                extra_xml.push((
13220                    runs.len(),
13221                    revisions_at_boundary,
13222                    capture_standalone_event(Event::Comment(comment.into_owned()))?,
13223                ));
13224            }
13225            Event::PI(instruction) => {
13226                extra_xml.push((
13227                    runs.len(),
13228                    revisions_at_boundary,
13229                    capture_standalone_event(Event::PI(instruction.into_owned()))?,
13230                ));
13231            }
13232            Event::Eof => break,
13233            _ => {}
13234        }
13235        buffer.clear();
13236    }
13237    let projection =
13238        accepted_hyperlink_bookmark_projection(&runs, &revisions, &extra_xml, word_prefixes);
13239    Ok(ParsedHyperlinkChildren {
13240        runs,
13241        run_sources,
13242        revisions,
13243        extra_xml,
13244        bookmark_markers: projection.markers,
13245        projected_run_count: projection.projected_run_count,
13246        tracked_run_count: projection.tracked_run_count,
13247    })
13248}
13249
13250fn bookmark_marker_projection_from_raw(
13251    raw: &[u8],
13252    word_prefixes: &[String],
13253    run_index: usize,
13254    raw_before: usize,
13255    projected_run_index: usize,
13256    tracked_run_index: usize,
13257) -> Option<BookmarkMarker> {
13258    let mut reader = Reader::from_reader(raw);
13259    reader.config_mut().trim_text(false);
13260    let mut buffer = Vec::new();
13261    loop {
13262        let event = reader.read_event_into(&mut buffer).ok()?;
13263        let (element, has_child_content) = match event {
13264            Event::Start(element) => (element, raw_element_has_child_content(raw)),
13265            Event::Empty(element) => (element, false),
13266            Event::Text(text) if is_xml_whitespace(text.as_ref()) => {
13267                buffer.clear();
13268                continue;
13269            }
13270            Event::Eof => return None,
13271            _ => return None,
13272        };
13273        let prefixes = word_prefixes_at(&element, word_prefixes).ok()?;
13274        let start = is_word_element(element.name().as_ref(), b"bookmarkStart", &prefixes);
13275        if !start && !is_word_element(element.name().as_ref(), b"bookmarkEnd", &prefixes) {
13276            return None;
13277        }
13278        return Some(BookmarkMarker {
13279            start,
13280            id: optional_word_attribute(&element, b"id", &prefixes)
13281                .and_then(|value| value.parse().ok()),
13282            name: optional_word_attribute(&element, b"name", &prefixes),
13283            run_index,
13284            raw_before,
13285            projected_run_index,
13286            tracked_run_index,
13287            word_prefixes: prefixes,
13288            has_child_content,
13289        });
13290    }
13291}
13292
13293fn append_projection(
13294    output: &mut AcceptedBookmarkProjection,
13295    projection: AcceptedBookmarkProjection,
13296    direct_run_offset: usize,
13297    raw_before: usize,
13298) {
13299    let projected_offset = output.projected_run_count;
13300    let tracked_offset = output.tracked_run_count;
13301    output
13302        .markers
13303        .extend(projection.markers.into_iter().map(|mut marker| {
13304            marker.run_index += direct_run_offset;
13305            marker.raw_before = raw_before;
13306            marker.projected_run_index += projected_offset;
13307            marker.tracked_run_index += tracked_offset;
13308            marker
13309        }));
13310    output.projected_run_count += projection.projected_run_count;
13311    output.tracked_run_count += projection.tracked_run_count;
13312}
13313
13314fn append_nested_bookmark_projection(
13315    markers: &mut Vec<BookmarkMarker>,
13316    projected_run_count: &mut usize,
13317    tracked_run_count: &mut usize,
13318    projection: AcceptedBookmarkProjection,
13319    direct_run_offset: usize,
13320    raw_before: usize,
13321) {
13322    let projected_offset = *projected_run_count;
13323    let tracked_offset = *tracked_run_count;
13324    markers.extend(projection.markers.into_iter().map(|mut marker| {
13325        marker.run_index += direct_run_offset;
13326        marker.raw_before = raw_before;
13327        marker.projected_run_index += projected_offset;
13328        marker.tracked_run_index += tracked_offset;
13329        marker
13330    }));
13331    *projected_run_count += projection.projected_run_count;
13332    *tracked_run_count += projection.tracked_run_count;
13333}
13334
13335fn accepted_paragraph_bookmark_projection(paragraph: &CT_P) -> AcceptedBookmarkProjection {
13336    let mut projection = AcceptedBookmarkProjection {
13337        markers: paragraph.bookmark_markers.clone(),
13338        projected_run_count: accepted_paragraph_runs(paragraph).len(),
13339        tracked_run_count: tracked_paragraph_runs(paragraph).len(),
13340    };
13341    projection
13342        .markers
13343        .sort_by_key(BookmarkMarker::projected_run_index);
13344    projection
13345}
13346
13347fn accepted_revision_bookmark_projection(revision: &CT_Revision) -> AcceptedBookmarkProjection {
13348    if matches!(
13349        revision.kind(),
13350        RevisionKind::Insertion | RevisionKind::MoveTo
13351    ) && let Some(paragraph) = revision.content_paragraph()
13352    {
13353        return accepted_paragraph_bookmark_projection(paragraph);
13354    }
13355    let mut runs = Vec::new();
13356    append_tracked_revision_runs(revision, &mut runs);
13357    AcceptedBookmarkProjection {
13358        markers: Vec::new(),
13359        projected_run_count: 0,
13360        tracked_run_count: runs.len(),
13361    }
13362}
13363
13364fn accepted_control_bookmark_projection(control: &CT_Sdt) -> AcceptedBookmarkProjection {
13365    let mut output = AcceptedBookmarkProjection::default();
13366    for boundary in 0..=control.content.len() {
13367        for (_, revision) in control.revisions().iter().filter(|(at, _)| *at == boundary) {
13368            let projection = accepted_revision_bookmark_projection(revision);
13369            append_projection(&mut output, projection, 0, 0);
13370        }
13371        let Some(content) = control.content.get(boundary) else {
13372            continue;
13373        };
13374        match content {
13375            SdtContent::Run(_) => {
13376                output.projected_run_count += 1;
13377                output.tracked_run_count += 1;
13378            }
13379            SdtContent::RawXml(raw) => {
13380                if let Some(marker) = bookmark_marker_projection_from_raw(
13381                    raw,
13382                    control.word_prefixes(),
13383                    0,
13384                    0,
13385                    output.projected_run_count,
13386                    output.tracked_run_count,
13387                ) {
13388                    output.markers.push(marker);
13389                }
13390            }
13391            SdtContent::ContentControl(control) => {
13392                let projection = accepted_control_bookmark_projection(control);
13393                append_projection(&mut output, projection, 0, 0);
13394            }
13395            SdtContent::Paragraph(paragraph) => {
13396                let projection = accepted_paragraph_bookmark_projection(paragraph);
13397                append_projection(&mut output, projection, 0, 0);
13398            }
13399            SdtContent::Table(table) => {
13400                append_control_table_bookmark_projection(&mut output, table)
13401            }
13402            SdtContent::Row(row) => append_control_row_bookmark_projection(&mut output, row),
13403            SdtContent::Cell(cell) => append_control_cell_bookmark_projection(&mut output, cell),
13404        }
13405    }
13406    output
13407}
13408
13409fn append_control_table_bookmark_projection(
13410    output: &mut AcceptedBookmarkProjection,
13411    table: &CT_Tbl,
13412) {
13413    for boundary in 0..=table.rows.len() {
13414        for (_, _, control) in table
13415            .content_controls
13416            .iter()
13417            .filter(|(at, _, _)| *at == boundary)
13418        {
13419            let projection = accepted_control_bookmark_projection(control);
13420            append_projection(output, projection, 0, 0);
13421        }
13422        if let Some(row) = table.rows.get(boundary) {
13423            append_control_row_bookmark_projection(output, row);
13424        }
13425    }
13426}
13427
13428fn append_control_row_bookmark_projection(output: &mut AcceptedBookmarkProjection, row: &CT_Row) {
13429    for boundary in 0..=row.cells.len() {
13430        for (_, _, control) in row
13431            .content_controls
13432            .iter()
13433            .filter(|(at, _, _)| *at == boundary)
13434        {
13435            let projection = accepted_control_bookmark_projection(control);
13436            append_projection(output, projection, 0, 0);
13437        }
13438        if let Some(cell) = row.cells.get(boundary) {
13439            append_control_cell_bookmark_projection(output, cell);
13440        }
13441    }
13442}
13443
13444fn append_control_cell_bookmark_projection(output: &mut AcceptedBookmarkProjection, cell: &CT_Tc) {
13445    for content in &cell.content {
13446        let projection = match content {
13447            CellContent::Paragraph(paragraph) => accepted_paragraph_bookmark_projection(paragraph),
13448            CellContent::Table(table) => {
13449                append_control_table_bookmark_projection(output, table);
13450                continue;
13451            }
13452            CellContent::ContentControl(control) => accepted_control_bookmark_projection(control),
13453        };
13454        append_projection(output, projection, 0, 0);
13455    }
13456}
13457
13458fn accepted_hyperlink_bookmark_projection(
13459    runs: &[CT_R],
13460    revisions: &[(usize, CT_Revision)],
13461    extra_xml: &[(usize, usize, Vec<u8>)],
13462    word_prefixes: &[String],
13463) -> AcceptedBookmarkProjection {
13464    let mut output = AcceptedBookmarkProjection::default();
13465    for boundary in 0..=runs.len() {
13466        let boundary_revisions = revisions
13467            .iter()
13468            .filter(|(at, _)| *at == boundary)
13469            .map(|(_, revision)| revision)
13470            .collect::<Vec<_>>();
13471        for revision_before in 0..=boundary_revisions.len() {
13472            for (_, _, raw) in extra_xml
13473                .iter()
13474                .filter(|(at, before, _)| *at == boundary && *before == revision_before)
13475            {
13476                if let Some(marker) = bookmark_marker_projection_from_raw(
13477                    raw,
13478                    word_prefixes,
13479                    boundary,
13480                    0,
13481                    output.projected_run_count,
13482                    output.tracked_run_count,
13483                ) {
13484                    output.markers.push(marker);
13485                }
13486            }
13487            if let Some(revision) = boundary_revisions.get(revision_before) {
13488                let projection = accepted_revision_bookmark_projection(revision);
13489                append_projection(&mut output, projection, boundary, 0);
13490            }
13491        }
13492        if boundary < runs.len() {
13493            output.projected_run_count += 1;
13494            output.tracked_run_count += 1;
13495        }
13496    }
13497    output
13498}
13499
13500fn parse_hyperlink_attributes(
13501    element: &BytesStart<'_>,
13502    scope: &[String],
13503) -> Result<ParsedHyperlinkAttributes> {
13504    let mut rel_id = None;
13505    let mut anchor = None;
13506    let mut tooltip = None;
13507    let mut doc_location = None;
13508    let mut extra = Vec::new();
13509    for attribute in element.attributes() {
13510        let attribute = attribute?;
13511        let name = attribute.key.as_ref();
13512        let value = attribute
13513            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
13514            .into_owned();
13515        if attribute_in_namespace(name, b"id", R_NS, scope) {
13516            rel_id = Some(value);
13517        } else if attribute_in_namespace(name, b"anchor", crate::namespace::W_NS, scope) {
13518            anchor = Some(value);
13519        } else if attribute_in_namespace(name, b"tooltip", crate::namespace::W_NS, scope) {
13520            tooltip = Some(value);
13521        } else if attribute_in_namespace(name, b"docLocation", crate::namespace::W_NS, scope) {
13522            doc_location = Some(value);
13523        } else {
13524            extra.push((std::str::from_utf8(name)?.to_owned(), value));
13525        }
13526    }
13527    Ok((rel_id, anchor, tooltip, doc_location, extra))
13528}
13529
13530fn attribute_in_namespace(name: &[u8], local: &[u8], namespace: &str, scope: &[String]) -> bool {
13531    let Some(separator) = name.iter().position(|byte| *byte == b':') else {
13532        return false;
13533    };
13534    if name.get(separator + 1..) != Some(local) {
13535        return false;
13536    }
13537    let prefix = &name[..separator];
13538    let bound_namespace = scope.iter().find_map(|binding| {
13539        binding
13540            .strip_prefix('\0')
13541            .and_then(|binding| binding.split_once('\0'))
13542            .filter(|(candidate, _)| candidate.as_bytes() == prefix)
13543            .map(|(_, value)| value)
13544    });
13545    match bound_namespace {
13546        Some(value) => value == namespace,
13547        None if namespace == crate::namespace::W_NS => {
13548            scope.iter().any(|candidate| candidate.as_bytes() == prefix)
13549        }
13550        None => false,
13551    }
13552}
13553
13554fn is_element_in_namespace(name: &[u8], local: &[u8], namespace: &str, scope: &[String]) -> bool {
13555    let (prefix, actual_local) = name
13556        .iter()
13557        .position(|byte| *byte == b':')
13558        .map_or((&b""[..], name), |separator| {
13559            (&name[..separator], &name[separator + 1..])
13560        });
13561    actual_local == local
13562        && scope.iter().any(|binding| {
13563            binding
13564                .strip_prefix('\0')
13565                .and_then(|binding| binding.split_once('\0'))
13566                .is_some_and(|(candidate, value)| {
13567                    candidate.as_bytes() == prefix && value == namespace
13568                })
13569        })
13570}
13571
13572fn element_prefix_has_binding(name: &[u8], scope: &[String]) -> bool {
13573    let prefix = name
13574        .iter()
13575        .position(|byte| *byte == b':')
13576        .map_or(&b""[..], |separator| &name[..separator]);
13577    scope.iter().any(|binding| {
13578        binding
13579            .strip_prefix('\0')
13580            .and_then(|binding| binding.split_once('\0'))
13581            .is_some_and(|(candidate, _)| candidate.as_bytes() == prefix)
13582    })
13583}
13584
13585fn prefixes_with_assumed_word_owner(name: &[u8], scope: &[String]) -> Result<Vec<String>> {
13586    let prefix = name
13587        .iter()
13588        .position(|byte| *byte == b':')
13589        .map_or(&b""[..], |separator| &name[..separator]);
13590    let mut prefixes = scope.to_vec();
13591    prefixes.push(std::str::from_utf8(prefix)?.to_owned());
13592    Ok(prefixes)
13593}
13594
13595fn is_content_revision_element(name: &[u8], word_prefixes: &[String]) -> bool {
13596    is_word_element(name, b"ins", word_prefixes)
13597        || is_word_element(name, b"del", word_prefixes)
13598        || is_word_element(name, b"moveFrom", word_prefixes)
13599        || is_word_element(name, b"moveTo", word_prefixes)
13600}
13601
13602struct ParagraphBoundary<'a> {
13603    extra_xml: &'a [(usize, Vec<u8>)],
13604    content_controls: &'a [(usize, usize, usize, CT_Sdt)],
13605    markers: &'a [CommentRangeMarker],
13606    hyperlinks: &'a [HyperlinkSpan],
13607    revisions: &'a [(usize, usize, CT_Revision)],
13608    equations: &'a [(usize, usize, OfficeMath)],
13609}
13610
13611fn write_paragraph_boundary<W: std::io::Write>(
13612    writer: &mut Writer<W>,
13613    boundary: ParagraphBoundary<'_>,
13614    run_index: usize,
13615) -> Result<()> {
13616    let extras = boundary
13617        .extra_xml
13618        .iter()
13619        .filter(|(position, _)| *position == run_index)
13620        .map(|(_, raw)| raw)
13621        .collect::<Vec<_>>();
13622    for raw_index in 0..=extras.len() {
13623        let boundary_markers = boundary
13624            .markers
13625            .iter()
13626            .filter(|marker| {
13627                marker.run_index() == run_index
13628                    && marker.raw_before().min(extras.len()) == raw_index
13629            })
13630            .collect::<Vec<_>>();
13631        for marker_index in 0..=boundary_markers.len() {
13632            for (_, _, _, sdt) in
13633                boundary
13634                    .content_controls
13635                    .iter()
13636                    .filter(|(at, raw_before, markers_before, _)| {
13637                        *at == run_index
13638                            && (*raw_before).min(extras.len()) == raw_index
13639                            && (*markers_before).min(boundary_markers.len()) == marker_index
13640                    })
13641            {
13642                sdt.to_xml(writer)?;
13643            }
13644            if let Some(marker) = boundary_markers.get(marker_index) {
13645                let (tag, id) = match marker {
13646                    CommentRangeMarker::Start { id, .. } => ("w:commentRangeStart", *id),
13647                    CommentRangeMarker::End { id, .. } => ("w:commentRangeEnd", *id),
13648                };
13649                let mut value = itoa::Buffer::new();
13650                let mut element = BytesStart::new(tag);
13651                element.push_attribute(("w:id", value.format(id)));
13652                writer.write_event(Event::Empty(element))?;
13653            }
13654        }
13655        if let Some(raw) = extras.get(raw_index) {
13656            if let Some((_, _, equation)) = boundary
13657                .equations
13658                .iter()
13659                .find(|(at, slot, _)| *at == run_index && *slot == raw_index)
13660            {
13661                equation.write_xml(writer)?;
13662            } else if let Some((_, _, revision)) =
13663                boundary.revisions.iter().find(|(at, slot, _)| {
13664                    *at == run_index
13665                        && hyperlink_revision_index(*slot).is_none()
13666                        && *slot == raw_index
13667                })
13668            {
13669                revision.write_xml(writer)?;
13670            } else if let Some((hyperlink_index, hyperlink)) = boundary
13671                .hyperlinks
13672                .iter()
13673                .enumerate()
13674                .find(|(_, hyperlink)| {
13675                    hyperlink.run_start == run_index
13676                        && hyperlink.run_end == run_index
13677                        && hyperlink.preserved_raw_before == Some(raw_index)
13678                })
13679            {
13680                let mut replacement = Vec::new();
13681                let mut replacement_writer = Writer::new(&mut replacement);
13682                write_hyperlink_start(&mut replacement_writer, hyperlink)?;
13683                write_hyperlink_boundary(
13684                    &mut replacement_writer,
13685                    boundary.revisions,
13686                    hyperlink_index,
13687                    hyperlink,
13688                    run_index,
13689                )?;
13690                replacement_writer
13691                    .write_event(Event::End(BytesEnd::new(hyperlink_qname(hyperlink))))?;
13692                writer.get_mut().write_all(&replacement)?;
13693            } else {
13694                writer.get_mut().write_all(raw)?;
13695            }
13696        }
13697    }
13698    Ok(())
13699}
13700
13701fn write_hyperlink_boundary<W: std::io::Write>(
13702    writer: &mut Writer<W>,
13703    revisions: &[(usize, usize, CT_Revision)],
13704    hyperlink_index: usize,
13705    hyperlink: &HyperlinkSpan,
13706    run_index: usize,
13707) -> Result<()> {
13708    let boundary_revisions = revisions
13709        .iter()
13710        .filter(|(at, slot, _)| {
13711            *at == run_index && hyperlink_revision_index(*slot) == Some(hyperlink_index)
13712        })
13713        .map(|(_, _, revision)| revision)
13714        .collect::<Vec<_>>();
13715    let relative_boundary = run_index.saturating_sub(hyperlink.run_start);
13716    for revision_index in 0..=boundary_revisions.len() {
13717        for (_, _, raw) in hyperlink.extra_xml.iter().filter(|(boundary, before, _)| {
13718            *boundary == relative_boundary
13719                && (*before).min(boundary_revisions.len()) == revision_index
13720        }) {
13721            writer.get_mut().write_all(raw)?;
13722        }
13723        if let Some(revision) = boundary_revisions.get(revision_index) {
13724            revision.write_xml(writer)?;
13725        }
13726    }
13727    Ok(())
13728}
13729
13730fn write_hyperlink_start<W: std::io::Write>(
13731    writer: &mut Writer<W>,
13732    hyperlink: &HyperlinkSpan,
13733) -> Result<()> {
13734    let (word_prefix, declare_word) =
13735        safe_hyperlink_prefix(hyperlink, "w", "rdocxW", crate::namespace::W_NS);
13736    let (relationship_prefix, declare_relationship) =
13737        safe_hyperlink_prefix(hyperlink, "r", "rdocxR", R_NS);
13738    let mut element = BytesStart::new(format!("{word_prefix}:hyperlink"));
13739    let word_declaration = format!("xmlns:{word_prefix}");
13740    let relationship_declaration = format!("xmlns:{relationship_prefix}");
13741    let relationship_id = format!("{relationship_prefix}:id");
13742    let anchor_name = format!("{word_prefix}:anchor");
13743    if declare_word {
13744        element.push_attribute((word_declaration.as_str(), crate::namespace::W_NS));
13745    }
13746    if hyperlink.rel_id.is_some() && declare_relationship {
13747        element.push_attribute((relationship_declaration.as_str(), R_NS));
13748    }
13749    if let Some(rel_id) = &hyperlink.rel_id {
13750        element.push_attribute((relationship_id.as_str(), rel_id.as_str()));
13751    }
13752    if let Some(anchor) = &hyperlink.anchor {
13753        element.push_attribute((anchor_name.as_str(), anchor.as_str()));
13754    }
13755    if let Some(tooltip) = &hyperlink.tooltip {
13756        let tooltip_name = format!("{word_prefix}:tooltip");
13757        element.push_attribute((tooltip_name.as_str(), tooltip.as_str()));
13758    }
13759    if let Some(doc_location) = &hyperlink.doc_location {
13760        let doc_location_name = format!("{word_prefix}:docLocation");
13761        element.push_attribute((doc_location_name.as_str(), doc_location.as_str()));
13762    }
13763    for (name, value) in &hyperlink.extra_attributes {
13764        element.push_attribute((name.as_str(), value.as_str()));
13765    }
13766    writer.write_event(Event::Start(element))?;
13767    Ok(())
13768}
13769
13770fn hyperlink_qname(hyperlink: &HyperlinkSpan) -> String {
13771    let (prefix, _) = safe_hyperlink_prefix(hyperlink, "w", "rdocxW", crate::namespace::W_NS);
13772    format!("{prefix}:hyperlink")
13773}
13774
13775fn safe_hyperlink_prefix(
13776    hyperlink: &HyperlinkSpan,
13777    preferred: &str,
13778    fallback: &str,
13779    namespace: &str,
13780) -> (String, bool) {
13781    let preferred_declaration = format!("xmlns:{preferred}");
13782    match hyperlink
13783        .extra_attributes
13784        .iter()
13785        .find(|(name, _)| name == &preferred_declaration)
13786    {
13787        None => return (preferred.to_owned(), false),
13788        Some((_, value)) if value == namespace => return (preferred.to_owned(), false),
13789        Some(_) => {}
13790    }
13791    for suffix in 0usize.. {
13792        let candidate = if suffix == 0 {
13793            fallback.to_owned()
13794        } else {
13795            format!("{fallback}{suffix}")
13796        };
13797        let declaration = format!("xmlns:{candidate}");
13798        if !hyperlink
13799            .extra_attributes
13800            .iter()
13801            .any(|(name, _)| name == &declaration)
13802        {
13803            return (candidate, true);
13804        }
13805    }
13806    unreachable!("the finite attribute set cannot occupy every prefix")
13807}
13808
13809fn shadowed_word_namespace(hyperlink: &HyperlinkSpan) -> Option<&str> {
13810    hyperlink
13811        .extra_attributes
13812        .iter()
13813        .find(|(name, value)| name == "xmlns:w" && value != crate::namespace::W_NS)
13814        .map(|(_, value)| value.as_str())
13815}
13816
13817pub(crate) fn write_raw_with_word_override<W: std::io::Write>(
13818    writer: &mut Writer<W>,
13819    raw: &[u8],
13820    foreign_word_namespace: Option<&str>,
13821) -> Result<()> {
13822    let Some(namespace) = foreign_word_namespace else {
13823        writer.get_mut().write_all(raw)?;
13824        return Ok(());
13825    };
13826    if !raw_uses_external_word_binding(raw) {
13827        writer.get_mut().write_all(raw)?;
13828        return Ok(());
13829    }
13830    writer
13831        .get_mut()
13832        .write_all(&raw_with_root_word_binding(raw, namespace)?)?;
13833    Ok(())
13834}
13835
13836// Only declarations the writer actually retains on this physical root can
13837// replace a synthetic child binding. Namespace-only roots retain no record.
13838pub(crate) fn bindings_outside_retained_root(
13839    bindings: &[(String, String)],
13840    root: Option<&BytesStart<'_>>,
13841    word_prefixes: &[String],
13842) -> Result<Vec<(String, String)>> {
13843    let record = root
13844        .map(|root| capture_root_attribute_record(root, word_prefixes))
13845        .transpose()?
13846        .flatten();
13847    let declared = match record {
13848        Some(record) => {
13849            let mut reader = Reader::from_reader(record.as_slice());
13850            match reader.read_event()? {
13851                Event::Empty(root) => raw_namespace_declarations(&root)?,
13852                _ => {
13853                    return Err(OxmlError::InvalidValue(
13854                        "retained root binding record is malformed".into(),
13855                    ));
13856                }
13857            }
13858        }
13859        None => Vec::new(),
13860    };
13861    Ok(bindings
13862        .iter()
13863        .filter(|binding| {
13864            !declared.contains(binding) || (binding.0 == "w" && binding.1 != crate::namespace::W_NS)
13865        })
13866        .cloned()
13867        .collect())
13868}
13869
13870pub(crate) fn raw_with_external_bindings(
13871    raw: &[u8],
13872    external_bindings: &[(String, String)],
13873) -> Result<Vec<u8>> {
13874    let bindings = external_bindings_used_by_raw(raw, external_bindings)?;
13875    if bindings.is_empty() {
13876        return Ok(raw.to_vec());
13877    }
13878    raw_with_root_bindings(raw, &bindings, false)
13879}
13880
13881fn external_bindings_used_by_raw(
13882    raw: &[u8],
13883    external_bindings: &[(String, String)],
13884) -> Result<Vec<(String, String)>> {
13885    if external_bindings.is_empty() {
13886        return Ok(Vec::new());
13887    }
13888    let mut reader = Reader::from_reader(raw);
13889    reader.config_mut().trim_text(false);
13890    let mut scopes = Vec::<Vec<(String, String)>>::new();
13891    let mut required = Vec::<String>::new();
13892    let mut buffer = Vec::new();
13893    loop {
13894        match reader.read_event_into(&mut buffer)? {
13895            Event::Start(element) => {
13896                let declarations = raw_namespace_declarations(&element)?;
13897                record_external_bindings_used(
13898                    &element,
13899                    &scopes,
13900                    &declarations,
13901                    external_bindings,
13902                    &mut required,
13903                )?;
13904                scopes.push(declarations);
13905            }
13906            Event::Empty(element) => {
13907                let declarations = raw_namespace_declarations(&element)?;
13908                record_external_bindings_used(
13909                    &element,
13910                    &scopes,
13911                    &declarations,
13912                    external_bindings,
13913                    &mut required,
13914                )?;
13915            }
13916            Event::End(_) => {
13917                scopes.pop();
13918            }
13919            Event::Eof => break,
13920            _ => {}
13921        }
13922        buffer.clear();
13923    }
13924
13925    Ok(external_bindings
13926        .iter()
13927        .filter(|(prefix, _)| required.contains(prefix))
13928        .cloned()
13929        .collect())
13930}
13931
13932fn raw_namespace_declarations(element: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
13933    let mut declarations = Vec::new();
13934    for attribute in element.attributes() {
13935        let attribute = attribute?;
13936        let name = attribute.key.as_ref();
13937        let prefix = if name == b"xmlns" {
13938            ""
13939        } else if let Some(prefix) = name.strip_prefix(b"xmlns:") {
13940            std::str::from_utf8(prefix)?
13941        } else {
13942            continue;
13943        };
13944        let value = attribute
13945            .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
13946            .into_owned();
13947        declarations.push((prefix.to_owned(), value));
13948    }
13949    Ok(declarations)
13950}
13951
13952fn record_external_bindings_used(
13953    element: &BytesStart<'_>,
13954    scopes: &[Vec<(String, String)>],
13955    declarations: &[(String, String)],
13956    external_bindings: &[(String, String)],
13957    required: &mut Vec<String>,
13958) -> Result<()> {
13959    let element_name = element.name();
13960    let element_prefix = qualified_name_prefix(element_name.as_ref()).unwrap_or("");
13961    record_external_prefix_used(
13962        element_prefix,
13963        scopes,
13964        declarations,
13965        external_bindings,
13966        required,
13967    );
13968    for attribute in element.attributes().filter_map(|attribute| attribute.ok()) {
13969        let name = attribute.key.as_ref();
13970        if name == b"xmlns" || name.starts_with(b"xmlns:") {
13971            continue;
13972        }
13973        if let Some(prefix) = qualified_name_prefix(name) {
13974            record_external_prefix_used(prefix, scopes, declarations, external_bindings, required);
13975            let namespace = declarations
13976                .iter()
13977                .rev()
13978                .chain(scopes.iter().rev().flat_map(|scope| scope.iter().rev()))
13979                .chain(external_bindings.iter())
13980                .find(|(candidate, _)| candidate == prefix)
13981                .map(|(_, namespace)| namespace.as_str());
13982            let local = name.rsplit(|byte| *byte == b':').next().unwrap_or(name);
13983            let namespace_value = namespace == Some(crate::namespace::MC_NS)
13984                && matches!(
13985                    local,
13986                    b"Ignorable"
13987                        | b"MustUnderstand"
13988                        | b"ProcessContent"
13989                        | b"PreserveAttributes"
13990                        | b"PreserveElements"
13991                )
13992                || namespace == Some("http://www.w3.org/2001/XMLSchema-instance")
13993                    && local == b"type";
13994            if !namespace_value {
13995                continue;
13996            }
13997            let value = attribute
13998                .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?;
13999            for prefix in root_attribute_value_prefixes(namespace, local, &value) {
14000                record_external_prefix_used(
14001                    prefix,
14002                    scopes,
14003                    declarations,
14004                    external_bindings,
14005                    required,
14006                );
14007            }
14008        }
14009    }
14010    Ok(())
14011}
14012
14013fn record_external_prefix_used(
14014    prefix: &str,
14015    scopes: &[Vec<(String, String)>],
14016    declarations: &[(String, String)],
14017    external_bindings: &[(String, String)],
14018    required: &mut Vec<String>,
14019) {
14020    let internally_bound = declarations
14021        .iter()
14022        .rev()
14023        .any(|(candidate, _)| candidate == prefix)
14024        || scopes
14025            .iter()
14026            .rev()
14027            .any(|scope| scope.iter().rev().any(|(candidate, _)| candidate == prefix));
14028    if !internally_bound
14029        && external_bindings
14030            .iter()
14031            .any(|(candidate, _)| candidate == prefix)
14032        && !required.iter().any(|candidate| candidate == prefix)
14033    {
14034        required.push(prefix.to_owned());
14035    }
14036}
14037
14038fn qualified_name_prefix(name: &[u8]) -> Option<&str> {
14039    let separator = name.iter().position(|byte| *byte == b':')?;
14040    std::str::from_utf8(&name[..separator]).ok()
14041}
14042
14043fn raw_with_root_bindings(
14044    raw: &[u8],
14045    bindings: &[(String, String)],
14046    source_values: bool,
14047) -> Result<Vec<u8>> {
14048    let mut reader = Reader::from_reader(raw);
14049    reader.config_mut().trim_text(false);
14050    let mut buffer = Vec::new();
14051    loop {
14052        let start = reader.buffer_position() as usize;
14053        match reader.read_event_into(&mut buffer)? {
14054            Event::Start(element) => {
14055                let end = reader.buffer_position() as usize;
14056                let mut element = element.into_owned();
14057                let names = bindings
14058                    .iter()
14059                    .map(|(prefix, _)| {
14060                        if prefix.is_empty() {
14061                            "xmlns".to_owned()
14062                        } else {
14063                            format!("xmlns:{prefix}")
14064                        }
14065                    })
14066                    .collect::<Vec<_>>();
14067                for ((_, namespace), name) in bindings.iter().zip(&names) {
14068                    if source_values {
14069                        element.push_attribute(quick_xml::events::attributes::Attribute {
14070                            key: quick_xml::name::QName(name.as_bytes()),
14071                            value: Cow::Borrowed(namespace.as_bytes()),
14072                        });
14073                    } else {
14074                        element.push_attribute((name.as_str(), namespace.as_str()));
14075                    }
14076                }
14077                let mut output = raw[..start].to_vec();
14078                Writer::new(&mut output).write_event(Event::Start(element))?;
14079                output.extend_from_slice(&raw[end..]);
14080                return Ok(output);
14081            }
14082            Event::Empty(element) => {
14083                let end = reader.buffer_position() as usize;
14084                let mut element = element.into_owned();
14085                let names = bindings
14086                    .iter()
14087                    .map(|(prefix, _)| {
14088                        if prefix.is_empty() {
14089                            "xmlns".to_owned()
14090                        } else {
14091                            format!("xmlns:{prefix}")
14092                        }
14093                    })
14094                    .collect::<Vec<_>>();
14095                for ((_, namespace), name) in bindings.iter().zip(&names) {
14096                    if source_values {
14097                        element.push_attribute(quick_xml::events::attributes::Attribute {
14098                            key: quick_xml::name::QName(name.as_bytes()),
14099                            value: Cow::Borrowed(namespace.as_bytes()),
14100                        });
14101                    } else {
14102                        element.push_attribute((name.as_str(), namespace.as_str()));
14103                    }
14104                }
14105                let mut output = raw[..start].to_vec();
14106                Writer::new(&mut output).write_event(Event::Empty(element))?;
14107                output.extend_from_slice(&raw[end..]);
14108                return Ok(output);
14109            }
14110            Event::Eof => return Ok(raw.to_vec()),
14111            _ => {}
14112        }
14113        buffer.clear();
14114    }
14115}
14116
14117fn raw_uses_external_word_binding(raw: &[u8]) -> bool {
14118    let mut reader = Reader::from_reader(raw);
14119    reader.config_mut().trim_text(false);
14120    let mut external_binding = vec![true];
14121    let mut saw_root = false;
14122    let mut buffer = Vec::new();
14123    loop {
14124        match reader.read_event_into(&mut buffer) {
14125            Ok(Event::Start(element)) => {
14126                let (declares_word, uses_word) = raw_word_binding_use(&element);
14127                if !saw_root {
14128                    saw_root = true;
14129                    if declares_word {
14130                        return false;
14131                    }
14132                }
14133                let uses_external =
14134                    external_binding.last().copied().unwrap_or(true) && !declares_word;
14135                if uses_external && uses_word {
14136                    return true;
14137                }
14138                external_binding.push(uses_external);
14139            }
14140            Ok(Event::Empty(element)) => {
14141                let (declares_word, uses_word) = raw_word_binding_use(&element);
14142                if !saw_root {
14143                    saw_root = true;
14144                    if declares_word {
14145                        return false;
14146                    }
14147                }
14148                let uses_external =
14149                    external_binding.last().copied().unwrap_or(true) && !declares_word;
14150                if uses_external && uses_word {
14151                    return true;
14152                }
14153            }
14154            Ok(Event::End(_)) => {
14155                if external_binding.len() > 1 {
14156                    external_binding.pop();
14157                }
14158            }
14159            Ok(Event::Eof) => return false,
14160            Err(_) => return false,
14161            _ => {}
14162        }
14163        buffer.clear();
14164    }
14165}
14166
14167fn raw_word_binding_use(element: &BytesStart<'_>) -> (bool, bool) {
14168    let declares_word = element
14169        .attributes()
14170        .filter_map(|attribute| attribute.ok())
14171        .any(|attribute| attribute.key.as_ref() == b"xmlns:w");
14172    let uses_word = qualified_name_uses_prefix(element.name().as_ref(), b"w")
14173        || element
14174            .attributes()
14175            .filter_map(|attribute| attribute.ok())
14176            .any(|attribute| qualified_name_uses_prefix(attribute.key.as_ref(), b"w"));
14177    (declares_word, uses_word)
14178}
14179
14180fn qualified_name_uses_prefix(name: &[u8], prefix: &[u8]) -> bool {
14181    name.iter()
14182        .position(|byte| *byte == b':')
14183        .is_some_and(|separator| &name[..separator] == prefix)
14184}
14185
14186fn raw_with_root_word_binding(raw: &[u8], namespace: &str) -> Result<Vec<u8>> {
14187    let mut reader = Reader::from_reader(raw);
14188    reader.config_mut().trim_text(false);
14189    let mut buffer = Vec::new();
14190    loop {
14191        let start = reader.buffer_position() as usize;
14192        match reader.read_event_into(&mut buffer)? {
14193            Event::Start(element) => {
14194                let end = reader.buffer_position() as usize;
14195                let mut element = element.into_owned();
14196                element.push_attribute(("xmlns:w", namespace));
14197                let mut output = raw[..start].to_vec();
14198                Writer::new(&mut output).write_event(Event::Start(element))?;
14199                output.extend_from_slice(&raw[end..]);
14200                return Ok(output);
14201            }
14202            Event::Empty(element) => {
14203                let end = reader.buffer_position() as usize;
14204                let mut element = element.into_owned();
14205                element.push_attribute(("xmlns:w", namespace));
14206                let mut output = raw[..start].to_vec();
14207                Writer::new(&mut output).write_event(Event::Empty(element))?;
14208                output.extend_from_slice(&raw[end..]);
14209                return Ok(output);
14210            }
14211            Event::Eof => return Ok(raw.to_vec()),
14212            _ => {}
14213        }
14214        buffer.clear();
14215    }
14216}
14217
14218fn write_empty_hyperlinks<W: std::io::Write>(
14219    writer: &mut Writer<W>,
14220    hyperlinks: &[HyperlinkSpan],
14221    revisions: &[(usize, usize, CT_Revision)],
14222    run_index: usize,
14223) -> Result<()> {
14224    for (hyperlink_index, hyperlink) in hyperlinks.iter().enumerate().filter(|(_, hyperlink)| {
14225        hyperlink.run_start == run_index
14226            && hyperlink.run_end == run_index
14227            && hyperlink.preserved_raw_before.is_none()
14228    }) {
14229        write_hyperlink_start(writer, hyperlink)?;
14230        write_hyperlink_boundary(writer, revisions, hyperlink_index, hyperlink, run_index)?;
14231        writer.write_event(Event::End(BytesEnd::new(hyperlink_qname(hyperlink))))?;
14232    }
14233    Ok(())
14234}
14235
14236fn required_word_i32_attribute(
14237    element: &BytesStart<'_>,
14238    local: &[u8],
14239    word_prefixes: &[String],
14240) -> Result<i32> {
14241    for attribute in element.attributes() {
14242        let attribute = attribute?;
14243        let key = attribute.key.as_ref();
14244        let Some(separator) = key.iter().position(|byte| *byte == b':') else {
14245            continue;
14246        };
14247        if key.get(separator + 1..) == Some(local)
14248            && word_prefixes
14249                .iter()
14250                .any(|prefix| prefix.as_bytes() == &key[..separator])
14251        {
14252            return Ok(attribute
14253                .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
14254                .parse()?);
14255        }
14256    }
14257    Err(OxmlError::MissingElement(format!(
14258        "{} attribute",
14259        String::from_utf8_lossy(local)
14260    )))
14261}
14262
14263pub(crate) fn optional_word_attribute(
14264    element: &BytesStart<'_>,
14265    local: &[u8],
14266    word_prefixes: &[String],
14267) -> Option<String> {
14268    for attribute in element.attributes().flatten() {
14269        let key = attribute.key.as_ref();
14270        let Some(separator) = key.iter().position(|byte| *byte == b':') else {
14271            continue;
14272        };
14273        if key.get(separator + 1..) == Some(local)
14274            && word_prefixes
14275                .iter()
14276                .any(|prefix| prefix.as_bytes() == &key[..separator])
14277        {
14278            let value = std::str::from_utf8(&attribute.value).ok()?;
14279            return Some(
14280                quick_xml::escape::unescape(value)
14281                    .map(|value| value.into_owned())
14282                    .unwrap_or_else(|_| value.to_owned()),
14283            );
14284        }
14285    }
14286    None
14287}
14288
14289/// Parse a field instruction string into the shared recursive grammar.
14290fn parse_field_instruction(instr: &str) -> FieldInstruction {
14291    parse_field_instruction_parts(vec![InstructionPart::Text(instr.to_owned())])
14292}
14293
14294/// Whether the shared field lexer and parser admit a nonempty first token.
14295#[doc(hidden)]
14296pub fn story_field_instruction_has_name(instruction: &str) -> bool {
14297    !parse_field_instruction(instruction).name.is_empty()
14298}
14299
14300#[derive(Debug)]
14301enum InstructionPart {
14302    Text(String),
14303    Nested(Field),
14304}
14305
14306struct InstructionToken {
14307    argument: FieldArgument,
14308    quoted: bool,
14309    source_span: Option<std::ops::Range<usize>>,
14310    well_formed: bool,
14311}
14312
14313fn parse_field_instruction_parts(parts: Vec<InstructionPart>) -> FieldInstruction {
14314    parse_field_instruction_parts_with_order(parts).0
14315}
14316
14317fn parse_field_instruction_parts_with_order(
14318    parts: Vec<InstructionPart>,
14319) -> (FieldInstruction, Vec<NestedFieldPosition>) {
14320    let mut raw = String::new();
14321    let mut tokens = Vec::new();
14322    let mut text = String::new();
14323    for part in parts {
14324        match part {
14325            InstructionPart::Text(value) => {
14326                raw.push_str(&value);
14327                text.push_str(&value);
14328            }
14329            InstructionPart::Nested(field) => {
14330                tokens.extend(lex_field_text(&text));
14331                text.clear();
14332                tokens.push(InstructionToken {
14333                    argument: FieldArgument::Nested(Box::new(field)),
14334                    quoted: false,
14335                    source_span: None,
14336                    well_formed: true,
14337                });
14338            }
14339        }
14340    }
14341    tokens.extend(lex_field_text(&text));
14342
14343    let mut tokens = tokens.into_iter();
14344    let name = match tokens.next().map(|token| token.argument) {
14345        Some(FieldArgument::Text(value)) => value.to_uppercase(),
14346        Some(FieldArgument::Nested(_)) | None => String::new(),
14347    };
14348    let remaining = tokens.collect::<Vec<_>>();
14349    let mut arguments = Vec::new();
14350    let mut switches = Vec::new();
14351    let mut nested_order = Vec::new();
14352    let mut index = 0usize;
14353    while index < remaining.len() {
14354        let InstructionToken {
14355            argument: FieldArgument::Text(value),
14356            quoted,
14357            ..
14358        } = &remaining[index]
14359        else {
14360            if matches!(&remaining[index].argument, FieldArgument::Nested(_)) {
14361                nested_order.push(NestedFieldPosition::Argument(arguments.len()));
14362            }
14363            arguments.push(remaining[index].argument.clone());
14364            index += 1;
14365            continue;
14366        };
14367        let Some(switch_name) = (!quoted)
14368            .then(|| value.strip_prefix('\\'))
14369            .flatten()
14370            .filter(|name| !name.is_empty())
14371        else {
14372            if matches!(&remaining[index].argument, FieldArgument::Nested(_)) {
14373                nested_order.push(NestedFieldPosition::Argument(arguments.len()));
14374            }
14375            arguments.push(remaining[index].argument.clone());
14376            index += 1;
14377            continue;
14378        };
14379        let takes_argument = switch_takes_argument(&name, switch_name)
14380            || !switch_is_known_flag(&name, switch_name)
14381                && remaining.get(index + 1).is_some_and(|token| {
14382                    token.quoted || matches!(token.argument, FieldArgument::Nested(_))
14383                });
14384        let argument = if takes_argument && remaining.get(index + 1).is_some_and(|next| {
14385            next.quoted
14386                || !matches!(&next.argument, FieldArgument::Text(text) if text.starts_with('\\'))
14387        }) {
14388            index += 1;
14389            Some(remaining[index].argument.clone())
14390        } else {
14391            None
14392        };
14393        if matches!(&argument, Some(FieldArgument::Nested(_))) {
14394            nested_order.push(NestedFieldPosition::Switch(switches.len()));
14395        }
14396        switches.push(FieldSwitch {
14397            name: switch_name.to_ascii_lowercase(),
14398            argument,
14399        });
14400        index += 1;
14401    }
14402
14403    (
14404        FieldInstruction {
14405            raw: raw.trim().to_owned(),
14406            name,
14407            arguments,
14408            switches,
14409        },
14410        nested_order,
14411    )
14412}
14413
14414fn switch_is_known_flag(field_name: &str, switch_name: &str) -> bool {
14415    let flags: &[&str] = match field_name {
14416        "REF" => &["h", "n", "t", "w", "p"],
14417        "PAGEREF" => &["h", "p"],
14418        "SEQ" => &["n", "c", "h"],
14419        "STYLEREF" => &["l", "n", "t", "w", "p"],
14420        "TOC" => &["h", "z", "u", "w", "x"],
14421        "TC" => &["n"],
14422        "TOA" => &["b", "h", "p"],
14423        "INDEX" => &["r"],
14424        "XE" | "TA" => &["b", "i"],
14425        "CITATION" => &["n", "y", "t"],
14426        "INCLUDETEXT" => &["!"],
14427        "FILENAME" => &["p"],
14428        "MERGEFIELD" => &["m", "v"],
14429        _ => &[],
14430    };
14431    flags.contains(&switch_name.to_ascii_lowercase().as_str())
14432}
14433
14434fn switch_takes_argument(field_name: &str, switch_name: &str) -> bool {
14435    let switch_name = switch_name.to_ascii_lowercase();
14436    if field_name == "INDEX" && switch_name == "r" {
14437        return false;
14438    }
14439    matches!(
14440        switch_name.as_str(),
14441        "*" | "#" | "@" | "r" | "s" | "d" | "b" | "f"
14442    ) || field_name == "INCLUDETEXT" && switch_name == "c"
14443        || field_name == "TOC" && matches!(switch_name.as_str(), "o" | "n" | "t" | "p")
14444        || field_name == "TC" && switch_name == "l"
14445        || field_name == "CITATION"
14446            && matches!(switch_name.as_str(), "l" | "m" | "p" | "v" | "f" | "s")
14447        || field_name == "BIBLIOGRAPHY" && matches!(switch_name.as_str(), "l" | "m" | "f")
14448        || field_name == "INDEX"
14449            && matches!(
14450                switch_name.as_str(),
14451                "z" | "f" | "h" | "e" | "l" | "g" | "k" | "s"
14452            )
14453        || field_name == "XE" && matches!(switch_name.as_str(), "f" | "r" | "t")
14454        || field_name == "TA" && matches!(switch_name.as_str(), "l" | "s" | "c" | "r")
14455        || field_name == "TOA" && matches!(switch_name.as_str(), "c" | "e" | "l" | "g")
14456        || field_name == "TOC" && matches!(switch_name.as_str(), "c" | "a")
14457        || matches!(field_name, "DISPLAYBARCODE" | "MERGEBARCODE")
14458            && matches!(switch_name.as_str(), "h" | "q" | "p" | "c")
14459}
14460
14461fn lex_field_text(input: &str) -> Vec<InstructionToken> {
14462    let mut tokens = Vec::new();
14463    let mut current = String::new();
14464    let mut characters = input.char_indices().peekable();
14465    let mut quoted = false;
14466    let mut token_was_quoted = false;
14467    let mut token_start = None;
14468    let mut quote_closed = false;
14469    let mut well_formed = true;
14470    while let Some((offset, character)) = characters.next() {
14471        if !quoted && character.is_whitespace() {
14472            if !current.is_empty() || token_was_quoted {
14473                tokens.push(InstructionToken {
14474                    argument: FieldArgument::Text(std::mem::take(&mut current)),
14475                    quoted: token_was_quoted,
14476                    source_span: token_start.map(|start| start..offset),
14477                    well_formed,
14478                });
14479            }
14480            token_start = None;
14481            token_was_quoted = false;
14482            quote_closed = false;
14483            well_formed = true;
14484            continue;
14485        }
14486        token_start.get_or_insert(offset);
14487        if quoted {
14488            match character {
14489                '"' => {
14490                    quoted = false;
14491                    quote_closed = true;
14492                }
14493                '\\' if characters
14494                    .peek()
14495                    .is_some_and(|(_, next)| matches!(next, '"' | '\\')) =>
14496                {
14497                    current.push(characters.next().expect("peeked character exists").1);
14498                }
14499                _ => current.push(character),
14500            }
14501        } else {
14502            if quote_closed {
14503                well_formed = false;
14504            }
14505            match character {
14506                '"' => {
14507                    if !current.is_empty() {
14508                        well_formed = false;
14509                    }
14510                    quoted = true;
14511                    token_was_quoted = true;
14512                }
14513                _ => current.push(character),
14514            }
14515        }
14516    }
14517    if !current.is_empty() || token_was_quoted {
14518        tokens.push(InstructionToken {
14519            argument: FieldArgument::Text(current),
14520            quoted: token_was_quoted,
14521            source_span: token_start.map(|start| start..input.len()),
14522            well_formed: well_formed && !quoted,
14523        });
14524    }
14525    tokens
14526}
14527
14528/// Replace only bibliography l, f and m token spans, retaining other raw spelling.
14529///
14530/// This checked edit uses the shared field lexer. Ambiguous source boundaries
14531/// return an error before a caller can publish any instruction changes.
14532#[doc(hidden)]
14533pub fn bibliography_option_switch_edits(
14534    raw: &str,
14535    replacement: &[FieldSwitch],
14536) -> Result<Vec<(std::ops::Range<usize>, String)>> {
14537    let tokens = lex_field_text(raw);
14538    if tokens.iter().any(|token| !token.well_formed) {
14539        return Err(OxmlError::InvalidValue(
14540            "ambiguous bibliography token boundary".into(),
14541        ));
14542    }
14543    let instruction = parse_field_instruction(raw);
14544    if instruction.name != "BIBLIOGRAPHY" || !instruction.arguments.is_empty() {
14545        return Err(OxmlError::InvalidValue(
14546            "invalid bibliography instruction shape".into(),
14547        ));
14548    }
14549    if replacement.iter().any(|switch| {
14550        !matches!(switch.name.as_str(), "l" | "f" | "m")
14551            || !matches!(switch.argument, Some(FieldArgument::Text(_)))
14552    }) {
14553        return Err(OxmlError::InvalidValue(
14554            "invalid owned bibliography replacement switch".into(),
14555        ));
14556    }
14557    let authored = FieldInstruction::new("BIBLIOGRAPHY", Vec::new(), replacement.to_vec())?;
14558    let mut ranges = Vec::new();
14559    let mut index = 1;
14560    while index < tokens.len() {
14561        let FieldArgument::Text(value) = &tokens[index].argument else {
14562            return Err(OxmlError::InvalidValue("nested bibliography token".into()));
14563        };
14564        let Some(name) = (!tokens[index].quoted)
14565            .then(|| value.strip_prefix('\\'))
14566            .flatten()
14567        else {
14568            return Err(OxmlError::InvalidValue(
14569                "unowned bibliography argument".into(),
14570            ));
14571        };
14572        let name = name.to_ascii_lowercase();
14573        let takes_argument = switch_takes_argument("BIBLIOGRAPHY", &name)
14574            || !switch_is_known_flag("BIBLIOGRAPHY", &name)
14575                && tokens.get(index + 1).is_some_and(|token| token.quoted);
14576        let has_argument = takes_argument && tokens.get(index + 1).is_some_and(|token| {
14577            token.quoted
14578                || !matches!(&token.argument, FieldArgument::Text(value) if value.starts_with('\\'))
14579        });
14580        let start = tokens[index]
14581            .source_span
14582            .as_ref()
14583            .ok_or_else(|| OxmlError::InvalidValue("missing bibliography token span".into()))?
14584            .start;
14585        let end_index = index + usize::from(has_argument);
14586        if matches!(name.as_str(), "l" | "m") && !has_argument {
14587            return Err(OxmlError::InvalidValue(
14588                "missing bibliography option operand".into(),
14589            ));
14590        }
14591        if matches!(name.as_str(), "l" | "f" | "m") {
14592            let end = tokens[end_index]
14593                .source_span
14594                .as_ref()
14595                .ok_or_else(|| OxmlError::InvalidValue("missing bibliography operand span".into()))?
14596                .end;
14597            ranges.push(start..end);
14598        }
14599        index = end_index + 1;
14600    }
14601    let owned = instruction
14602        .switches
14603        .iter()
14604        .filter(|switch| matches!(switch.name.as_str(), "l" | "f" | "m"))
14605        .cloned()
14606        .collect::<Vec<_>>();
14607    if owned == replacement {
14608        return Ok(Vec::new());
14609    }
14610    let mut edits = ranges
14611        .into_iter()
14612        .map(|range| (range, String::new()))
14613        .collect::<Vec<_>>();
14614    let appended = &authored.raw["BIBLIOGRAPHY".len()..];
14615    if !appended.is_empty() {
14616        edits.push((raw.len()..raw.len(), appended.to_owned()));
14617    }
14618    Ok(edits)
14619}
14620
14621impl Default for CT_P {
14622    fn default() -> Self {
14623        Self::new()
14624    }
14625}
14626
14627#[cfg(test)]
14628mod tests {
14629    #[test]
14630    fn fx211_local_drawing_scope_survives_foreign_header_reopen_and_split() {
14631        let word = crate::namespace::W_NS;
14632        let raw = r#"<wp:opaque r:tag="producer"/>"#;
14633        let source = format!(
14634            r#"<w:hdr xmlns:w="{word}" xmlns:wp="urn:wp-producer"><w:p><w:r xmlns:r="urn:r-producer" r:keep="root-producer"><w:t>AB</w:t>{raw}</w:r></w:p></w:hdr>"#
14635        );
14636        let mut header = crate::header_footer::CT_HdrFtr::from_xml(source.as_bytes()).unwrap();
14637        header.paragraphs[0].runs[0]
14638            .content
14639            .push(RunContent::Drawing(CT_Drawing::inline(
14640                crate::drawing::CT_Inline::new("rId1", 914_400, 914_400),
14641            )));
14642        let mut bytes = header.to_xml_header().unwrap();
14643        for iteration in 0..3 {
14644            let mut reader = NsReader::from_reader(bytes.as_slice());
14645            let mut pictures = 0;
14646            let mut opaque = 0;
14647            loop {
14648                let (namespace, event) = reader.read_resolved_event().unwrap();
14649                if let Event::Start(element) | Event::Empty(element) = &event {
14650                    if element.local_name().as_ref() == b"inline" {
14651                        assert_eq!(namespace, ResolveResult::Bound(Namespace(WP_NS.as_bytes())));
14652                        pictures += 1;
14653                    }
14654                    if element.local_name().as_ref() == b"opaque" {
14655                        assert_eq!(
14656                            namespace,
14657                            ResolveResult::Bound(Namespace(b"urn:wp-producer"))
14658                        );
14659                        opaque += 1;
14660                        let attribute = element
14661                            .attributes()
14662                            .find_map(|attribute| {
14663                                attribute
14664                                    .ok()
14665                                    .filter(|attribute| attribute.key.as_ref() == b"r:tag")
14666                            })
14667                            .unwrap();
14668                        let (binding, _) = reader.resolver().resolve_attribute(attribute.key);
14669                        assert_eq!(binding, ResolveResult::Bound(Namespace(b"urn:r-producer")));
14670                    }
14671                    if element.local_name().as_ref() == b"blip" {
14672                        let attribute = element
14673                            .attributes()
14674                            .find_map(|attribute| {
14675                                attribute
14676                                    .ok()
14677                                    .filter(|attribute| attribute.key.as_ref() == b"r:embed")
14678                            })
14679                            .unwrap();
14680                        let (binding, _) = reader.resolver().resolve_attribute(attribute.key);
14681                        assert_eq!(binding, ResolveResult::Bound(Namespace(R_NS.as_bytes())));
14682                        assert_eq!(attribute.value.as_ref(), b"rId1");
14683                    }
14684                }
14685                if matches!(event, Event::Eof) {
14686                    break;
14687                }
14688            }
14689            assert_eq!((pictures, opaque), (1, 1));
14690            let mut reopened = crate::header_footer::CT_HdrFtr::from_xml(&bytes).unwrap();
14691            let run = &reopened.paragraphs[0].runs[0];
14692            assert!(
14693                run.extra_xml_positions
14694                    .iter()
14695                    .any(|position| *position & RAW_RUN_LOCAL_BINDINGS_FLAG != 0)
14696            );
14697            let next = reopened.to_xml_header().unwrap();
14698            if iteration > 0 {
14699                assert_eq!(next, bytes);
14700            }
14701            if iteration == 2 {
14702                let mut head = run.clone();
14703                let tail = head.split_off_at(1).unwrap();
14704                for part in [&head, &tail] {
14705                    assert!(
14706                        part.extra_xml_positions
14707                            .iter()
14708                            .any(|position| *position & RAW_RUN_LOCAL_BINDINGS_FLAG != 0)
14709                    );
14710                }
14711                reopened.paragraphs[0].runs = vec![head, tail];
14712                let split = reopened.to_xml_header().unwrap();
14713                let split = crate::header_footer::CT_HdrFtr::from_xml(&split).unwrap();
14714                assert_eq!(split.paragraphs[0].text(), "AB");
14715                let drawing = split.paragraphs[0].runs[1]
14716                    .content
14717                    .iter()
14718                    .find_map(|item| match item {
14719                        RunContent::Drawing(drawing) => Some(drawing),
14720                        _ => None,
14721                    })
14722                    .unwrap();
14723                assert_eq!(drawing.inline.as_ref().unwrap().embed_id, "rId1");
14724                assert_eq!(
14725                    split.paragraphs[0].runs[1]
14726                        .extra_xml
14727                        .iter()
14728                        .filter(|raw| String::from_utf8_lossy(raw).contains("root-producer"))
14729                        .count(),
14730                    0
14731                );
14732            }
14733            bytes = next;
14734        }
14735    }
14736
14737    #[test]
14738    fn fx211_local_vs_inherited_namespace_records_preserve_positions_and_lexical_scope() {
14739        let word = crate::namespace::W_NS;
14740        let raw = r#"<wp:inline><wp:docPr id="1"/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="urn:producer"><x:opaque/></a:graphicData></a:graphic></wp:inline>"#;
14741        for (local, mixed) in [(false, false), (true, false), (true, true)] {
14742            let declarations = format!(r#"xmlns:wp="{WP_NS}" xmlns:x="urn:lexical&#x2F;producer""#);
14743            let source = if local {
14744                format!(
14745                    r#"<w:r xmlns:w="{word}" {declarations}>{}<w:drawing>{raw}</w:drawing></w:r>"#,
14746                    if mixed { "<x:opaque/>" } else { "" }
14747                )
14748            } else {
14749                format!(
14750                    r#"<w:r xmlns:w="{word}"><w:drawing {declarations}>{raw}</w:drawing></w:r>"#
14751                )
14752            };
14753            let run = CT_R::from_xml_fragment_checked(source.as_bytes()).unwrap();
14754            assert_eq!(
14755                run.extra_xml_positions
14756                    .iter()
14757                    .any(|position| *position & RAW_RUN_LOCAL_BINDINGS_FLAG != 0),
14758                local
14759            );
14760            let retained = run
14761                .content
14762                .iter()
14763                .find_map(|item| match item {
14764                    RunContent::Drawing(drawing) => drawing.inline.as_ref()?.raw_xml.as_deref(),
14765                    _ => None,
14766                })
14767                .unwrap();
14768            let original_payload = raw.split_once('>').unwrap().1.as_bytes();
14769            let retained_payload = retained.splitn(2, |byte| *byte == b'>').nth(1).unwrap();
14770            assert_eq!(retained_payload, original_payload);
14771            if local {
14772                assert_eq!(retained, raw.as_bytes());
14773            } else {
14774                let retained_root = retained.split(|byte| *byte == b'>').next().unwrap();
14775                assert!(
14776                    String::from_utf8_lossy(retained_root)
14777                        .contains("xmlns:x=\"urn:lexical/producer\"")
14778                );
14779            }
14780            let mut output = Vec::new();
14781            {
14782                let _scope = root_binding_scope(ROOT_WP_BINDING);
14783                run.to_xml(&mut Writer::new(&mut output)).unwrap();
14784            }
14785            if local {
14786                assert!(
14787                    String::from_utf8_lossy(&output)
14788                        .contains("xmlns:x=\"urn:lexical&#x2F;producer\"")
14789                );
14790            }
14791            assert!(
14792                output
14793                    .windows(original_payload.len())
14794                    .any(|bytes| bytes == original_payload)
14795            );
14796            let paragraph = format!(
14797                r#"<w:p xmlns:w="{word}" xmlns:wp="{WP_NS}">{}</w:p>"#,
14798                String::from_utf8_lossy(&output)
14799            );
14800            let reopened = CT_P::from_xml_fragment_checked(paragraph.as_bytes()).unwrap();
14801            let mut fixed = Vec::new();
14802            {
14803                let _scope = root_binding_scope(ROOT_WP_BINDING);
14804                reopened.runs[0]
14805                    .to_xml(&mut Writer::new(&mut fixed))
14806                    .unwrap();
14807            }
14808            assert_eq!(fixed, output);
14809            let wrapper = format!(
14810                r#"<root xmlns:w="{word}" xmlns:wp="{WP_NS}">{}</root>"#,
14811                String::from_utf8(output).unwrap()
14812            );
14813            let mut reader = NsReader::from_str(&wrapper);
14814            let mut observed = 0;
14815            loop {
14816                let (namespace, event) = reader.read_resolved_event().unwrap();
14817                if let Event::Empty(element) = &event
14818                    && element.local_name().as_ref() == b"opaque"
14819                {
14820                    assert_eq!(
14821                        namespace,
14822                        ResolveResult::Bound(Namespace(if local {
14823                            b"urn:lexical&#x2F;producer"
14824                        } else {
14825                            // Existing lost-wrapper closure normalizes URI values.
14826                            b"urn:lexical/producer"
14827                        }))
14828                    );
14829                    observed += 1;
14830                }
14831                if matches!(event, Event::Eof) {
14832                    break;
14833                }
14834            }
14835            assert_eq!(observed, if mixed { 2 } else { 1 });
14836        }
14837        // Vec allocation bounds leave room for every reserved position bit on
14838        // both native usize and WASM32. Real modeled positions cannot truncate.
14839        assert!(
14840            (isize::MAX as usize / std::mem::size_of::<RunContent>()) + 1
14841                < RAW_RUN_LOCAL_BINDINGS_FLAG
14842        );
14843        for position in [0, 1, 1_000_000] {
14844            let mut encoded = RAW_ROOT_ATTRIBUTES_FLAG | RAW_RUN_LOCAL_BINDINGS_FLAG;
14845            CT_R::set_raw_child_position(&mut encoded, position);
14846            assert_eq!(CT_R::raw_child_position(encoded), position);
14847            assert!(CT_R::raw_child_is_root_attributes(encoded));
14848            assert_ne!(encoded & RAW_RUN_LOCAL_BINDINGS_FLAG, 0);
14849        }
14850    }
14851
14852    #[test]
14853    fn fx211_local_canonical_uri_spelling_uses_decoded_writer_authority() {
14854        let word = crate::namespace::W_NS;
14855        let wp = WP_NS.replace("wordprocessingDrawing", "wordprocessing&#x44;rawing");
14856        let relationship = R_NS.replace("relationships", "relationship&#x73;");
14857        let source = format!(
14858            r#"<w:r xmlns:w="{word}" xmlns:wp="{wp}" xmlns:r="{relationship}"><w:drawing><wp:inline><wp:docPr id="1"/><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="urn:producer"><a:blip r:embed="rIdOld"/></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>"#
14859        );
14860        let mut run = CT_R::from_xml_fragment_checked(source.as_bytes()).unwrap();
14861        run.content.push(RunContent::Drawing(CT_Drawing::inline(
14862            crate::drawing::CT_Inline::new("rIdTyped", 914_400, 914_400),
14863        )));
14864        let mut localized = Vec::new();
14865        let (root, proof) = run_xml_root(&run, None, &mut localized).unwrap();
14866        assert!(localized.is_empty());
14867        assert!(proof.contains(&("wp".into(), WP_NS.into())));
14868        assert!(proof.contains(&("r".into(), R_NS.into())));
14869        let declarations = raw_namespace_declarations(&root).unwrap();
14870        assert!(declarations.contains(&("wp".into(), WP_NS.into())));
14871        assert!(declarations.contains(&("r".into(), R_NS.into())));
14872        let mut output = Vec::new();
14873        run.to_xml(&mut Writer::new(&mut output)).unwrap();
14874        let output = String::from_utf8(output).unwrap();
14875        assert!(output.contains(&format!("xmlns:wp=\"{wp}\"")));
14876        assert!(output.contains(&format!("xmlns:r=\"{relationship}\"")));
14877        assert!(output.contains("r:embed=\"rIdTyped\""));
14878        assert!(output.contains("r:embed=\"rIdOld\""));
14879    }
14880
14881    #[test]
14882    fn fx211_run_raw_wp_relationship_collisions_preserve_typed_picture_authority() {
14883        let word = crate::namespace::W_NS;
14884        let raw = r#"<wp:opaque r:link="producer"/><r:opaque wp:flag="producer"/>"#;
14885        for attributes in [
14886            "",
14887            " wp:keep=\"drawing-owner\"",
14888            " r:keep=\"relationship-owner\"",
14889            " wp:keep=\"drawing-owner\" r:keep=\"relationship-owner\" xmlns:rdocxProducer=\"urn:occupied\"",
14890        ] {
14891            let source = format!(
14892                r#"<w:r xmlns:w="{word}" xmlns:wp="urn:wp-producer" xmlns:r="urn:r-producer"{attributes}>{raw}</w:r>"#
14893            );
14894            let mut run = CT_R::from_xml_fragment_checked(source.as_bytes()).unwrap();
14895            assert_eq!(run.extra_xml[0], b"<wp:opaque r:link=\"producer\"/>");
14896            assert_eq!(run.extra_xml[1], b"<r:opaque wp:flag=\"producer\"/>");
14897            run.content.push(RunContent::Drawing(CT_Drawing::inline(
14898                crate::drawing::CT_Inline::new("rId1", 914_400, 914_400),
14899            )));
14900            let original_raw = run.extra_xml[..2].to_vec();
14901            let mut output = Vec::new();
14902            run.to_xml(&mut Writer::new(&mut output)).unwrap();
14903            assert_eq!(run.extra_xml[..2], original_raw);
14904            let closed = raw_with_external_bindings(
14905                &output,
14906                &[
14907                    ("w".into(), word.into()),
14908                    ("wp".into(), WP_NS.into()),
14909                    ("r".into(), R_NS.into()),
14910                ],
14911            )
14912            .unwrap();
14913            let reopened = CT_R::from_xml_fragment_checked(&closed).unwrap();
14914            assert_eq!(reopened.content.len(), 1);
14915            let RunContent::Drawing(drawing) = &reopened.content[0] else {
14916                panic!("actual generated picture missing");
14917            };
14918            assert_eq!(drawing.inline.as_ref().unwrap().embed_id, "rId1");
14919            let mut reader = NsReader::from_reader(closed.as_slice());
14920            let mut opaque = 0;
14921            let mut inline = 0;
14922            let mut relationship = 0;
14923            let mut root_attributes = 0;
14924            loop {
14925                let (namespace, event) = reader.read_resolved_event().unwrap();
14926                if let Event::Start(element) | Event::Empty(element) = &event {
14927                    match element.local_name().as_ref() {
14928                        b"r" => {
14929                            assert_eq!(namespace, ResolveResult::Bound(Namespace(word.as_bytes())));
14930                            for attribute in element.attributes() {
14931                                let attribute = attribute.unwrap();
14932                                let (binding, local) =
14933                                    reader.resolver().resolve_attribute(attribute.key);
14934                                if local.as_ref() == b"keep" {
14935                                    let uri = if attribute.value.as_ref() == b"drawing-owner" {
14936                                        b"urn:wp-producer".as_slice()
14937                                    } else {
14938                                        b"urn:r-producer".as_slice()
14939                                    };
14940                                    assert_eq!(binding, ResolveResult::Bound(Namespace(uri)));
14941                                    root_attributes += 1;
14942                                }
14943                            }
14944                        }
14945                        b"opaque" => {
14946                            let expected = if element.name().as_ref().starts_with(b"wp:") {
14947                                b"urn:wp-producer".as_slice()
14948                            } else {
14949                                b"urn:r-producer".as_slice()
14950                            };
14951                            assert_eq!(namespace, ResolveResult::Bound(Namespace(expected)));
14952                            opaque += 1;
14953                        }
14954                        b"inline" | b"extent" | b"docPr" => {
14955                            assert_eq!(
14956                                namespace,
14957                                ResolveResult::Bound(Namespace(WP_NS.as_bytes()))
14958                            );
14959                            if element.local_name().as_ref() == b"inline" {
14960                                inline += 1;
14961                            }
14962                        }
14963                        b"blip" => {
14964                            for attribute in element.attributes() {
14965                                let attribute = attribute.unwrap();
14966                                let (binding, local) =
14967                                    reader.resolver().resolve_attribute(attribute.key);
14968                                if local.as_ref() == b"embed" {
14969                                    assert_eq!(
14970                                        binding,
14971                                        ResolveResult::Bound(Namespace(R_NS.as_bytes()))
14972                                    );
14973                                    assert_eq!(attribute.value.as_ref(), b"rId1");
14974                                    relationship += 1;
14975                                }
14976                            }
14977                        }
14978                        _ => {}
14979                    }
14980                }
14981                if matches!(event, Event::Eof) {
14982                    break;
14983                }
14984            }
14985            assert_eq!((opaque, inline, relationship), (2, 1, 1));
14986            assert_eq!(root_attributes, attributes.matches(":keep=").count());
14987            let mut second = Vec::new();
14988            reopened.to_xml(&mut Writer::new(&mut second)).unwrap();
14989            let closed_second = raw_with_external_bindings(
14990                &second,
14991                &[
14992                    ("w".into(), word.into()),
14993                    ("wp".into(), WP_NS.into()),
14994                    ("r".into(), R_NS.into()),
14995                ],
14996            )
14997            .unwrap();
14998            let again = CT_R::from_xml_fragment_checked(&closed_second).unwrap();
14999            let RunContent::Drawing(drawing) = &again.content[0] else {
15000                panic!("reopened picture missing");
15001            };
15002            assert_eq!(drawing.inline.as_ref().unwrap().embed_id, "rId1");
15003            assert_eq!(reopened.extra_xml, again.extra_xml);
15004        }
15005        for (prefix, uri) in [("wp", "urn:wp-producer"), ("r", "urn:r-producer")] {
15006            let source = format!(
15007                r#"<w:r xmlns:w="{word}" xmlns:{prefix}="{uri}" xmlns:mc="{MC_NS}" {prefix}:keep="source" mc:Ignorable="{prefix}"/>"#
15008            );
15009            let mut run = CT_R::from_xml_fragment_checked(source.as_bytes()).unwrap();
15010            let mut raw_only = Vec::new();
15011            run.to_xml(&mut Writer::new(&mut raw_only)).unwrap();
15012            assert!(
15013                String::from_utf8_lossy(&raw_only).contains(&format!("mc:Ignorable=\"{prefix}\""))
15014            );
15015            run.content.push(RunContent::Drawing(CT_Drawing::inline(
15016                crate::drawing::CT_Inline::new("rId1", 914_400, 914_400),
15017            )));
15018            let mut output = Vec::new();
15019            assert!(
15020                run.to_xml(&mut Writer::new(&mut output))
15021                    .unwrap_err()
15022                    .to_string()
15023                    .contains("namespace-valued")
15024            );
15025            assert!(output.is_empty());
15026        }
15027    }
15028
15029    #[test]
15030    fn fx211_run_raw_bindings_keep_fixed_points_and_single_metadata_scan() {
15031        let word = crate::namespace::W_NS;
15032        let xsi = "http://www.w3.org/2001/XMLSchema-instance";
15033        let raw = r#"<x:payload xsi:type="y:Future"><child/></x:payload>"#;
15034        let source = format!(
15035            r#"<w:r xmlns:w="{word}" xmlns:x="urn:payload" xmlns:y="urn:qname" xmlns:xsi="{xsi}" xmlns="urn:default" xmlns:unused="urn:unused">{raw}</w:r>"#
15036        );
15037        let run = CT_R::from_xml_fragment_checked(source.as_bytes()).unwrap();
15038        assert_eq!(run.extra_xml[0], raw.as_bytes());
15039        let mut output = Vec::new();
15040        run.to_xml(&mut Writer::new(&mut output)).unwrap();
15041        assert!(String::from_utf8_lossy(&output).contains(raw));
15042        assert!(!String::from_utf8_lossy(&output).contains("urn:unused"));
15043        let closed = raw_with_external_bindings(&output, &[("w".into(), word.into())]).unwrap();
15044        let reopened = CT_R::from_xml_fragment_checked(&closed).unwrap();
15045        assert_eq!(run, reopened);
15046        let mut second = Vec::new();
15047        reopened.to_xml(&mut Writer::new(&mut second)).unwrap();
15048        assert_eq!(output, second);
15049        let mut reader = NsReader::from_reader(closed.as_slice());
15050        loop {
15051            let (namespace, event) = reader.read_resolved_event().unwrap();
15052            if let Event::Start(element) | Event::Empty(element) = &event {
15053                if element.local_name().as_ref() == b"child" {
15054                    assert_eq!(namespace, ResolveResult::Bound(Namespace(b"urn:default")));
15055                }
15056                if element.local_name().as_ref() == b"payload" {
15057                    assert_eq!(namespace, ResolveResult::Bound(Namespace(b"urn:payload")));
15058                    let (binding, _) = reader
15059                        .resolver()
15060                        .resolve_element(quick_xml::name::QName(b"y:Future"));
15061                    assert_eq!(binding, ResolveResult::Bound(Namespace(b"urn:qname")));
15062                }
15063            }
15064            if matches!(event, Event::Eof) {
15065                break;
15066            }
15067        }
15068        for count in [512, 1024] {
15069            let raw = "<x:opaque x:literal='keep'/>";
15070            let source = format!(
15071                r#"<w:r xmlns:w="{word}" xmlns:x="urn:producer">{}</w:r>"#,
15072                raw.repeat(count)
15073            );
15074            let run = CT_R::from_xml_fragment_checked(source.as_bytes()).unwrap();
15075            RUN_METADATA_VISITS.with(|visits| visits.set(0));
15076            let mut output = Vec::new();
15077            run.to_xml(&mut Writer::new(&mut output)).unwrap();
15078            assert_eq!(RUN_METADATA_VISITS.with(Cell::get), count + 1);
15079            assert_eq!(String::from_utf8_lossy(&output).matches(raw).count(), count);
15080            assert_eq!(
15081                run.extra_xml
15082                    .iter()
15083                    .filter(|raw| !is_root_attribute_record(raw))
15084                    .count(),
15085                count
15086            );
15087        }
15088        let raw = b"<w:separator/>";
15089        let source = format!(
15090            r#"<w:r xmlns:w="{word}">{}</w:r>"#,
15091            std::str::from_utf8(raw).unwrap()
15092        );
15093        let run = CT_R::from_xml_fragment_checked(source.as_bytes()).unwrap();
15094        assert_eq!(run.extra_xml, [raw.to_vec()]);
15095        let mut output = Vec::new();
15096        run.to_xml(&mut Writer::new(&mut output)).unwrap();
15097        assert!(String::from_utf8_lossy(&output).contains(std::str::from_utf8(raw).unwrap()));
15098        let closed = raw_with_external_bindings(&output, &[("w".into(), word.into())]).unwrap();
15099        assert_eq!(run, CT_R::from_xml_fragment_checked(&closed).unwrap());
15100    }
15101
15102    #[test]
15103    fn fx211_run_raw_foreign_graphic_binding_does_not_rebind_typed_drawing() {
15104        let word = crate::namespace::W_NS;
15105        let raw = r#"<a:opaque a:flag="producer"/>"#;
15106        let source = format!(r#"<w:r xmlns:w="{word}" xmlns:a="urn:producer">{raw}</w:r>"#);
15107        let mut run = CT_R::from_xml_fragment_checked(source.as_bytes()).unwrap();
15108        run.content.push(RunContent::Drawing(CT_Drawing::inline(
15109            crate::drawing::CT_Inline::new("rId1", 914_400, 914_400),
15110        )));
15111        let mut output = Vec::new();
15112        run.to_xml(&mut Writer::new(&mut output)).unwrap();
15113        assert!(String::from_utf8_lossy(&output).contains(raw));
15114        let wrapper = format!(
15115            r#"<root xmlns:w="{word}" xmlns:wp="{WP_NS}" xmlns:r="{R_NS}">{}</root>"#,
15116            String::from_utf8(output).unwrap()
15117        );
15118        let mut reader = NsReader::from_str(&wrapper);
15119        let mut opaque = 0;
15120        let mut graphic = 0;
15121        loop {
15122            let (namespace, event) = reader.read_resolved_event().unwrap();
15123            if let Event::Start(element) | Event::Empty(element) = &event {
15124                if element.local_name().as_ref() == b"opaque" {
15125                    assert_eq!(namespace, ResolveResult::Bound(Namespace(b"urn:producer")));
15126                    opaque += 1;
15127                }
15128                if element.local_name().as_ref() == b"graphic" {
15129                    assert_eq!(
15130                        namespace,
15131                        ResolveResult::Bound(Namespace(
15132                            b"http://schemas.openxmlformats.org/drawingml/2006/main"
15133                        ))
15134                    );
15135                    graphic += 1;
15136                }
15137            }
15138            if matches!(event, Event::Eof) {
15139                break;
15140            }
15141        }
15142        assert_eq!((opaque, graphic), (1, 1));
15143    }
15144
15145    #[test]
15146    fn fx211_run_raw_bindings_preserve_storage_emission_and_cached_field_authority() {
15147        let word = crate::namespace::W_NS;
15148        let xsi = "http://www.w3.org/2001/XMLSchema-instance";
15149        for attribute in ["", " q:rsidR=\"ABCDEF12\""] {
15150            let raw =
15151                r#"<w:fldChar w:fldCharType="begin"><x:payload xsi:type="w:Future"/></w:fldChar>"#;
15152            let source = format!(
15153                r#"<q:r xmlns:q="{word}" xmlns:w="urn:producer" xmlns:x="urn:payload" xmlns:xsi="{xsi}" xmlns:unused="urn:unused"{attribute}>{raw}</q:r>"#
15154            );
15155            let run = CT_R::from_xml_fragment_checked(source.as_bytes()).unwrap();
15156            assert_eq!(run.extra_xml[0], raw.as_bytes());
15157            let mut localized = Vec::new();
15158            let (root, bindings) = run_xml_root(&run, None, &mut localized).unwrap();
15159            validate_raw_cached_field_content(&root, &bindings, &localized, &run.extra_xml[0])
15160                .unwrap();
15161            let mut output = Vec::new();
15162            run.to_xml(&mut Writer::new(&mut output)).unwrap();
15163            assert_eq!(run.extra_xml[0], raw.as_bytes());
15164            let wrapped = format!(
15165                "<root xmlns:w=\"{word}\">{}</root>",
15166                String::from_utf8(output).unwrap()
15167            );
15168            assert!(!wrapped.contains("urn:unused"));
15169            assert!(!wrapped.contains("rdocxProducer"));
15170            let mut reader = NsReader::from_str(&wrapped);
15171            loop {
15172                let (namespace, event) = reader.read_resolved_event().unwrap();
15173                if let Event::Start(element) | Event::Empty(element) = &event {
15174                    if element.local_name().as_ref() == b"r" {
15175                        assert_eq!(namespace, ResolveResult::Bound(Namespace(word.as_bytes())));
15176                    }
15177                    if element.local_name().as_ref() == b"fldChar" {
15178                        assert_eq!(namespace, ResolveResult::Bound(Namespace(b"urn:producer")));
15179                    }
15180                    if element.local_name().as_ref() == b"payload" {
15181                        let (binding, _) = reader
15182                            .resolver()
15183                            .resolve_element(quick_xml::name::QName(b"w:Future"));
15184                        assert_eq!(binding, ResolveResult::Bound(Namespace(b"urn:producer")));
15185                        let attr = element.attributes().next().unwrap().unwrap();
15186                        let (binding, _) = reader.resolver().resolve_attribute(attr.key);
15187                        assert_eq!(binding, ResolveResult::Bound(Namespace(xsi.as_bytes())));
15188                    }
15189                }
15190                if matches!(event, Event::Eof) {
15191                    break;
15192                }
15193            }
15194        }
15195        let raw = r#"<w:opaque xmlns:w="urn:inner"><child xmlns="urn:default" xsi:type="w:Future"/></w:opaque>"#;
15196        let source = format!(
15197            r#"<q:r xmlns:q="{word}" xmlns:w="urn:outer" xmlns:xsi="{xsi}" xmlns:unused="urn:unused">{raw}</q:r>"#
15198        );
15199        let run = CT_R::from_xml_fragment_checked(source.as_bytes()).unwrap();
15200        assert_eq!(run.extra_xml[0], raw.as_bytes());
15201        let mut output = Vec::new();
15202        run.to_xml(&mut Writer::new(&mut output)).unwrap();
15203        let output = String::from_utf8(output).unwrap();
15204        assert!(output.contains("xmlns:w=\"urn:inner\""));
15205        assert!(output.contains("xmlns=\"urn:default\""));
15206        assert!(!output.contains("urn:outer"));
15207        assert!(!output.contains("urn:unused"));
15208        assert!(output.contains(&format!("xmlns:xsi=\"{xsi}\"")));
15209    }
15210
15211    #[test]
15212    fn fx211_consumed_reader_owner_preserves_sibling_cursor_and_refuses_atomically() {
15213        let first = r#"<w:p><w:r><w:t>FIRST</w:t></w:r></w:p>"#;
15214        let deep = format!(
15215            "{}<w:r><w:t>LAST</w:t></w:r>{}",
15216            "<w:sdt><w:sdtContent>".repeat(257),
15217            "</w:sdtContent></w:sdt>".repeat(257)
15218        );
15219        let input = format!("{first}<w:p>{deep}</w:p>");
15220        let mut reader = Reader::from_str(&input);
15221        assert!(matches!(reader.read_event().unwrap(), Event::Start(_)));
15222        assert_eq!(CT_P::from_xml(&mut reader).unwrap().text(), "FIRST");
15223        assert_eq!(reader.buffer_position() as usize, first.len());
15224        assert!(matches!(reader.read_event().unwrap(), Event::Start(_)));
15225        let position = reader.buffer_position();
15226        let remaining = reader.get_ref().to_vec();
15227        assert!(
15228            CT_P::from_xml(&mut reader)
15229                .unwrap_err()
15230                .to_string()
15231                .contains("256")
15232        );
15233        assert_eq!(reader.buffer_position(), position);
15234        assert_eq!(*reader.get_ref(), remaining.as_slice());
15235        let mut malformed = Reader::from_str("<w:p><w:r><w:t>bad</w:t></w:r></w:r>");
15236        malformed.read_event().unwrap();
15237        let position = malformed.buffer_position();
15238        assert!(CT_P::from_xml(&mut malformed).is_err());
15239        assert_eq!(malformed.buffer_position(), position);
15240    }
15241
15242    #[test]
15243    fn fx211_nested_cached_field_drawing_spanning_runs_preserves_owner_source() {
15244        const W_NS: &str = crate::namespace::W_NS;
15245        let drawing = concat!(
15246            r#"<w:drawing><wp:anchor><wp:extent cx="10" cy="20"/><wp:docPr id="7"/>"#,
15247            r#"<a:graphic><a:graphicData><wps:wsp><wps:txbx><w:txbxContent>"#,
15248            r#"<w:p><x:keep xmlns:x="urn:producer" token="exact"/><w:r><w:t>shape leaf</w:t></w:r></w:p>"#,
15249            r#"</w:txbxContent></wps:txbx></wps:wsp></a:graphicData></a:graphic></wp:anchor></w:drawing>"#,
15250        );
15251        let source = format!(
15252            r#"<w:p xmlns:w="{W_NS}" xmlns:wp="{WP_NS}" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"><w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText>UNKNOWN outer</w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>prefix </w:t></w:r><w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText>UNKNOWN child</w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r>{drawing}</w:r><w:r><w:fldChar w:fldCharType="end"/></w:r><w:r><w:t> suffix</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r><w:r><w:fldChar w:fldCharType="unknown"/></w:r></w:p>"#
15253        );
15254        let paragraph = CT_P::from_xml_fragment_checked(source.as_bytes()).unwrap();
15255        let outer = parsed_field(&paragraph, 0);
15256        assert_eq!(outer.cached_result, "prefix  suffix");
15257        let children = outer.cached_fields_in_source_order();
15258        assert_eq!(children.len(), 1);
15259        let drawing_model = children[0]
15260            .cached_result_runs()
15261            .unwrap()
15262            .iter()
15263            .flat_map(|run| &run.content)
15264            .find_map(|content| match content {
15265                RunContent::Drawing(drawing) => Some(drawing),
15266                _ => None,
15267            })
15268            .expect("active nested cached drawing remains typed");
15269        let body = drawing_model
15270            .anchor
15271            .as_ref()
15272            .unwrap()
15273            .shape
15274            .as_ref()
15275            .unwrap()
15276            .text_body
15277            .as_ref()
15278            .unwrap();
15279        assert_eq!(body.paragraphs().next().unwrap().text(), "shape leaf");
15280        let saved = serialized_paragraph(&paragraph);
15281        assert_eq!(
15282            saved.matches(drawing).count(),
15283            1,
15284            "exact owned drawing source appears once"
15285        );
15286        let closed = raw_with_external_bindings(
15287            saved.as_bytes(),
15288            &[
15289                ("w".into(), W_NS.into()),
15290                ("wp".into(), WP_NS.into()),
15291                (
15292                    "a".into(),
15293                    "http://schemas.openxmlformats.org/drawingml/2006/main".into(),
15294                ),
15295                (
15296                    "wps".into(),
15297                    "http://schemas.microsoft.com/office/word/2010/wordprocessingShape".into(),
15298                ),
15299            ],
15300        )
15301        .unwrap();
15302        let reopened = CT_P::from_xml_fragment_checked(&closed).unwrap();
15303        assert_eq!(serialized_paragraph(&reopened), saved);
15304        assert_eq!(parsed_field(&reopened, 0).cached_result, "prefix  suffix");
15305        assert_eq!(
15306            parsed_field(&reopened, 0)
15307                .cached_fields_in_source_order()
15308                .len(),
15309            1
15310        );
15311    }
15312
15313    #[test]
15314    fn fx211_checked_property_invariants_refuse_lossy_admission() {
15315        let uri = crate::namespace::W_NS;
15316        let cases = [
15317            (
15318                "p",
15319                "<w:pPr><w:jc w:val=\"center\"/><w:pStyle w:val=\"Normal\"/></w:pPr>",
15320            ),
15321            (
15322                "p",
15323                "<w:pPr><w:jc w:val=\"left\"/><w:jc w:val=\"right\"/></w:pPr>",
15324            ),
15325            ("p", "<w:pPr><w:keepNext w:val=\"sometimes\"/></w:pPr>"),
15326            ("r", "<w:rPr><w:u w:val=\"invented\"/></w:rPr>"),
15327            ("r", "<w:rPr><w:sz/></w:rPr>"),
15328            ("r", "<w:rPr><w:b w:val=\"sometimes\"/></w:rPr>"),
15329            ("r", "<w:rPr><w:b><w:sz w:val=\"24\"/></w:b></w:rPr>"),
15330            ("p", "<w:pPr><w:keepNext>unexpected</w:keepNext></w:pPr>"),
15331            ("r", "<w:rPr><w:color w:val=\"GGGGGG\"/></w:rPr>"),
15332        ];
15333        let mut accepted = Vec::new();
15334        for (root, inner) in cases {
15335            let xml = format!("<w:{root} xmlns:w=\"{uri}\">{inner}</w:{root}>");
15336            let refused = if root == "p" {
15337                CT_P::from_xml_fragment_checked(xml.as_bytes()).is_err()
15338            } else {
15339                CT_R::from_xml_fragment_checked(xml.as_bytes()).is_err()
15340            };
15341            if !refused {
15342                accepted.push(inner);
15343            }
15344        }
15345        assert!(
15346            accepted.is_empty(),
15347            "lossy checked property acceptance: {accepted:?}"
15348        );
15349        for inner in [
15350            "<w:rPr><w:b/><w:b w:val=\"false\"/></w:rPr>",
15351            "<w:rPr><w:sz w:val=\"24\"/></w:rPr>",
15352        ] {
15353            let xml = format!("<w:r xmlns:w=\"{uri}\">{inner}</w:r>");
15354            CT_R::from_xml_fragment_checked(xml.as_bytes()).unwrap();
15355        }
15356        let xml = format!(
15357            "<w:p xmlns:w=\"{uri}\"><w:pPr><w:ind w:firstLine=\"120\" w:hanging=\"120\"/><w:outlineLvl w:val=\"10\"/></w:pPr></w:p>"
15358        );
15359        CT_P::from_xml_fragment_checked(xml.as_bytes()).unwrap();
15360        let opaque = "<x:payload xmlns:x=\"urn:producer\"><w:b w:val=\"sometimes\"/></x:payload>";
15361        let xml =
15362            format!("<w:r xmlns:w=\"{uri}\"><w:rPr>{opaque}</w:rPr><w:t>retained</w:t></w:r>");
15363        let run = CT_R::from_xml_fragment_checked(xml.as_bytes()).unwrap();
15364        let mut saved = Vec::new();
15365        run.to_xml(&mut Writer::new(&mut saved)).unwrap();
15366        assert!(String::from_utf8(saved).unwrap().contains(opaque));
15367    }
15368
15369    #[test]
15370    fn fx211_checked_namespace_and_opaque_word_routes_match_native_owners() {
15371        let uri = crate::namespace::W_NS;
15372        let mut failures = Vec::new();
15373        let encoded_uri = uri.replace("wordprocessingml", "wordprocessingm&#108;");
15374        let encoded = format!(
15375            r#"<a:p xmlns:a="{encoded_uri}"><a:r><a:t>decoded namespace</a:t></a:r></a:p>"#
15376        );
15377        if CT_P::from_xml_fragment_checked(encoded.as_bytes()).is_err() {
15378            failures.push("encoded qualified root");
15379        }
15380        let opaque = r#"<w:producer><w:r><w:t>opaque</w:t><w:rPr/></w:r></w:producer>"#;
15381        let xml = format!(r#"<w:p xmlns:w="{uri}"><w:r><w:t>visible</w:t></w:r>{opaque}</w:p>"#);
15382        match CT_P::from_xml_fragment_checked(xml.as_bytes()) {
15383            Ok(paragraph) => {
15384                assert_eq!(paragraph.text(), "visible");
15385                let mut saved = Vec::new();
15386                paragraph.to_xml(&mut Writer::new(&mut saved)).unwrap();
15387                assert!(String::from_utf8(saved).unwrap().contains(opaque));
15388            }
15389            Err(_) => failures.push("unknown Word wrapper is opaque"),
15390        }
15391        assert!(failures.is_empty(), "qualified owner gaps: {failures:?}");
15392    }
15393
15394    #[test]
15395    fn fx211_checked_text_owners_admit_aliases_and_default_namespaces() {
15396        let uri = crate::namespace::W_NS;
15397        for prefix in ["w", "alias", ""] {
15398            let tag = |name: &str| {
15399                if prefix.is_empty() {
15400                    name.to_owned()
15401                } else {
15402                    format!("{prefix}:{name}")
15403                }
15404            };
15405            let declaration = if prefix.is_empty() {
15406                format!("xmlns=\"{uri}\"")
15407            } else {
15408                format!("xmlns:{prefix}=\"{uri}\"")
15409            };
15410            let p = tag("p");
15411            let r = tag("r");
15412            let t = tag("t");
15413            let xml = format!("<{p} {declaration}><{r}><{t}>A &amp; Ω</{t}></{r}></{p}>");
15414            assert_eq!(
15415                CT_P::from_xml_fragment_checked(xml.as_bytes())
15416                    .unwrap()
15417                    .text(),
15418                "A & Ω"
15419            );
15420            let xml = format!("<{r} {declaration}><{t}>A &amp; Ω</{t}></{r}>");
15421            assert_eq!(
15422                CT_R::from_xml_fragment_checked(xml.as_bytes())
15423                    .unwrap()
15424                    .text(),
15425                "A & Ω"
15426            );
15427        }
15428    }
15429
15430    #[test]
15431    fn fx211_checked_composite_profiles_preserve_valid_models_and_opaque_values() {
15432        let uri = crate::namespace::W_NS;
15433        let opaque = r#"<x:spacing xmlns:x="urn:producer" x:lineRule="invalid"/>"#;
15434        let xml = format!(
15435            r#"<w:p xmlns:w="{uri}"><w:pPr><w:numPr><w:ilvl w:val="2"/><w:numId w:val="7"/></w:numPr><w:spacing w:line="240" w:lineRule="exact" w:beforeAutospacing="true" w:afterAutospacing="off"/>{opaque}</w:pPr><w:r><w:t>value</w:t></w:r></w:p>"#
15436        );
15437        let paragraph = CT_P::from_xml_fragment_checked(xml.as_bytes()).unwrap();
15438        let properties = paragraph.properties.as_ref().unwrap();
15439        assert_eq!(properties.line_rule.as_deref(), Some("exact"));
15440        assert_eq!(properties.before_autospacing, Some(true));
15441        assert_eq!(properties.after_autospacing, Some(false));
15442        let mut bytes = Vec::new();
15443        paragraph.to_xml(&mut Writer::new(&mut bytes)).unwrap();
15444        assert!(
15445            bytes
15446                .windows(opaque.len())
15447                .any(|window| window == opaque.as_bytes())
15448        );
15449        let standalone = String::from_utf8(bytes).unwrap().replacen(
15450            "<w:p",
15451            &format!(r#"<w:p xmlns:w="{uri}""#),
15452            1,
15453        );
15454        assert_eq!(
15455            CT_P::from_xml_fragment_checked(standalone.as_bytes())
15456                .unwrap()
15457                .text(),
15458            "value"
15459        );
15460        let xml = format!(
15461            r#"<w:tbl xmlns:w="{uri}"><w:tblPr><w:tblW w:w="1440" w:type="dxa"/><w:jc w:val="center"/><w:tblLayout w:type="fixed"/></w:tblPr><w:tblGrid><w:gridCol w:w="1440"/></w:tblGrid><w:tr><w:tc><w:tcPr><w:tcW w:w="1440" w:type="dxa"/><w:gridSpan w:val="1"/><w:vAlign w:val="center"/></w:tcPr><w:p/></w:tc></w:tr></w:tbl>"#
15462        );
15463        let table = CT_Tbl::from_xml_fragment_checked(xml.as_bytes()).unwrap();
15464        assert_eq!(
15465            table.properties.as_ref().unwrap().layout.as_deref(),
15466            Some("fixed")
15467        );
15468        assert_eq!(table.rows.len(), 1);
15469        assert_eq!(table.rows[0].cells.len(), 1);
15470        let mut bytes = Vec::new();
15471        table.to_xml(&mut Writer::new(&mut bytes)).unwrap();
15472        let standalone = String::from_utf8(bytes).unwrap().replacen(
15473            "<w:tbl",
15474            &format!(r#"<w:tbl xmlns:w="{uri}""#),
15475            1,
15476        );
15477        let reopened = CT_Tbl::from_xml_fragment_checked(standalone.as_bytes()).unwrap();
15478        assert_eq!(reopened.properties, table.properties);
15479        assert_eq!(reopened.rows.len(), table.rows.len());
15480    }
15481
15482    #[test]
15483    fn fx211_checked_composite_profiles_refuse_silent_native_defaults() {
15484        let uri = crate::namespace::W_NS;
15485        let mut failures = Vec::new();
15486        for properties in [
15487            r#"<w:spacing w:lineRule="invalid"/>"#,
15488            r#"<w:spacing w:beforeAutospacing="invalid"/>"#,
15489            r#"<w:numPr><w:ilvl w:val="invalid"/><w:numId w:val="1"/></w:numPr>"#,
15490            r#"<w:numPr><w:ilvl/><w:numId w:val="1"/></w:numPr>"#,
15491            r#"<w:numPr><w:ilvl w:val="0"/><w:ilvl w:val="1"/><w:numId w:val="1"/></w:numPr>"#,
15492            r#"<w:numPr><w:numId w:val="1"/><w:ilvl w:val="0"/></w:numPr>"#,
15493        ] {
15494            let xml = format!(
15495                r#"<w:p xmlns:w="{uri}"><w:pPr>{properties}</w:pPr><w:r><w:t>value</w:t></w:r></w:p>"#
15496            );
15497            if CT_P::from_xml_fragment_checked(xml.as_bytes()).is_ok() {
15498                failures.push(format!("paragraph silent default accepted: {properties}"));
15499            }
15500        }
15501        for (parent, properties) in [
15502            ("tblPr", r#"<w:jc w:val="invalid"/>"#),
15503            ("tblPr", r#"<w:tblW w:w="1440" w:type="invalid"/>"#),
15504            ("tblPr", r#"<w:tblLayout w:type="invalid"/>"#),
15505            ("tcPr", r#"<w:gridSpan/>"#),
15506            ("tcPr", r#"<w:hMerge w:val="invalid"/>"#),
15507            ("tcPr", r#"<w:vAlign w:val="invalid"/>"#),
15508        ] {
15509            let xml = if parent == "tblPr" {
15510                format!(
15511                    r#"<w:tbl xmlns:w="{uri}"><w:tblPr>{properties}</w:tblPr><w:tblGrid><w:gridCol w:w="1440"/></w:tblGrid><w:tr><w:tc><w:p/></w:tc></w:tr></w:tbl>"#
15512                )
15513            } else {
15514                format!(
15515                    r#"<w:tbl xmlns:w="{uri}"><w:tblGrid><w:gridCol w:w="1440"/></w:tblGrid><w:tr><w:tc><w:tcPr>{properties}</w:tcPr><w:p/></w:tc></w:tr></w:tbl>"#
15516                )
15517            };
15518            if CT_Tbl::from_xml_fragment_checked(xml.as_bytes()).is_ok() {
15519                failures.push(format!("table silent default accepted: {properties}"));
15520            }
15521        }
15522        assert!(failures.is_empty(), "{failures:?}");
15523    }
15524
15525    #[test]
15526    fn fx211_checked_text_owners_refuse_types_lexical_and_child_order_errors() {
15527        let uri = crate::namespace::W_NS;
15528        let invalid = [
15529            "<w:p xmlns:w=\"urn:foreign\"/>".to_owned(),
15530            "<p/>".to_owned(),
15531            format!("<w:r xmlns:w=\"{uri}\"/>"),
15532            format!("<w:p xmlns:w=\"{uri}\"><w:r/><w:pPr/></w:p>"),
15533            format!("<w:p xmlns:w=\"{uri}\"><w:pPr/><w:pPr/></w:p>"),
15534            format!("<w:p xmlns:w=\"{uri}\"><w:r><w:t>T</w:t><w:rPr/></w:r></w:p>"),
15535            format!("<w:p xmlns:w=\"{uri}\"><w:tbl/></w:p>"),
15536            format!("<w:p xmlns:w=\"{uri}\">unexpected text</w:p>"),
15537            format!("<w:p xmlns:w=\"{uri}\"><w:r><w:t>&unknown;</w:t></w:r></w:p>"),
15538            format!("<w:p xmlns:w=\"{uri}\"><w:r><w:t>\u{1}</w:t></w:r></w:p>"),
15539            format!("<w:p xmlns:w=\"{uri}\"/>trailing"),
15540        ];
15541        for xml in invalid {
15542            assert!(
15543                CT_P::from_xml_fragment_checked(xml.as_bytes()).is_err(),
15544                "{xml}"
15545            );
15546        }
15547        for name in ["fld", "br", "p", "tbl"] {
15548            let xml = format!("<w:{name} xmlns:w=\"{uri}\"/>");
15549            assert!(
15550                CT_R::from_xml_fragment_checked(xml.as_bytes()).is_err(),
15551                "{xml}"
15552            );
15553        }
15554        assert!(
15555            CT_R::from_xml_fragment_checked(
15556                format!("<w:r xmlns:w=\"{uri}\"><w:r/></w:r>").as_bytes()
15557            )
15558            .is_err()
15559        );
15560        assert!(
15561            CT_R::from_xml_fragment_checked(
15562                format!("<w:r xmlns:w=\"{uri}\"><w:rPr/><w:rPr/></w:r>").as_bytes()
15563            )
15564            .is_err()
15565        );
15566    }
15567
15568    #[test]
15569    fn fx211_checked_text_owners_preserve_unmodelled_payload() {
15570        let raw = r#"<x:payload xmlns:x="urn:producer" x:flag='keep'><x:item value="Ω"/><w:r><w:t>opaque</w:t><w:rPr/></w:r></x:payload>"#;
15571        let xml = format!(
15572            r#"<w:p xmlns:w="{}"><w:r><w:t>before</w:t>{raw}</w:r>{raw}</w:p>"#,
15573            crate::namespace::W_NS
15574        );
15575        let paragraph = CT_P::from_xml_fragment_checked(xml.as_bytes()).unwrap();
15576        let mut output = Vec::new();
15577        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
15578        let output = String::from_utf8(output).unwrap();
15579        assert_eq!(output.matches(raw).count(), 2);
15580        let closed = raw_with_external_bindings(
15581            output.as_bytes(),
15582            &[("w".to_owned(), crate::namespace::W_NS.to_owned())],
15583        )
15584        .unwrap();
15585        assert_eq!(
15586            CT_P::from_xml_fragment_checked(&closed).unwrap().text(),
15587            "before"
15588        );
15589    }
15590
15591    #[test]
15592    fn fx211_paragraph_fragment_refuses_second_root_before_publication() {
15593        let xml = br#"<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:r><w:t>first</w:t></w:r></w:p><w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>"#;
15594        assert!(
15595            CT_P::from_xml_fragment(xml).is_err(),
15596            "a standalone paragraph must not silently discard the second root"
15597        );
15598    }
15599
15600    #[test]
15601    fn fx211_paragraph_fragment_refuses_document_type() {
15602        let xml = br#"<!DOCTYPE p><w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:r><w:t>kept</w:t></w:r></w:p>"#;
15603        assert!(
15604            CT_P::from_xml_fragment(xml).is_err(),
15605            "replacement must refuse DTD before typed construction"
15606        );
15607    }
15608
15609    #[test]
15610    fn comment_literal_source_validates_namespaces_outside_selected_range() {
15611        let range = r#"<w:commentRangeStart w:id="4"/><w:r><w:t>A</w:t><w:tab/><w:t>B</w:t></w:r><w:commentRangeEnd w:id="4"/>"#;
15612        for invalid in [
15613            "<q:bad/>",
15614            "<w:r q:bad=\"value\"/>",
15615            "<w:r q:bad=\"&invalid;\"/>",
15616        ] {
15617            for (before, inside, after) in [(invalid, "", ""), ("", invalid, ""), ("", "", invalid)]
15618            {
15619                let selected = range.replace("<w:tab/>", &format!("<w:tab/>{inside}"));
15620                let xml = format!(
15621                    r#"<w:p xmlns:w="{}">{before}{selected}{after}</w:p>"#,
15622                    crate::namespace::W_NS
15623                );
15624                assert!(
15625                    super::CT_P::comment_range_text_from_source(xml.as_bytes(), 4).is_err(),
15626                    "{xml}"
15627                );
15628            }
15629        }
15630        let xml = format!(
15631            r#"<w:p xmlns:w="{}" xmlns:q="urn:foreign"><q:box q:keep="value"/>{range}<q:box xmlns:q="urn:other"><q:t>IGNORED</q:t></q:box></w:p>"#,
15632            crate::namespace::W_NS
15633        );
15634        assert_eq!(
15635            super::CT_P::comment_range_text_from_source(xml.as_bytes(), 4)
15636                .unwrap()
15637                .as_deref(),
15638            Some("AB")
15639        );
15640    }
15641    use super::*;
15642
15643    fn parse_paragraph(xml: &str) -> CT_P {
15644        let full = format!("<w:p>{xml}</w:p>");
15645        let mut reader = Reader::from_str(&full);
15646        reader.config_mut().trim_text(true);
15647        let mut buf = Vec::new();
15648        loop {
15649            match reader.read_event_into(&mut buf) {
15650                Ok(Event::Start(ref e)) if matches_local_name(e.name().as_ref(), b"p") => break,
15651                _ => {}
15652            }
15653            buf.clear();
15654        }
15655        CT_P::from_xml(&mut reader).unwrap()
15656    }
15657
15658    #[test]
15659    fn bibliography_owned_token_edits_preserve_unowned_source_spelling() {
15660        let raw = "  bIbLiOgRaPhY\t\\L 1033  \\M \"a \\\"b\\\" \\\\ c\"\n\\Q \"keep \\\"exact\\\"\"  \\* MERGEFORMAT \\f 1036 \\m bare  ";
15661        let mut output = raw.to_owned();
15662        let edits = bibliography_option_switch_edits(
15663            raw,
15664            &[
15665                FieldSwitch {
15666                    name: "l".into(),
15667                    argument: Some(FieldArgument::Text("1031".into())),
15668                },
15669                FieldSwitch {
15670                    name: "m".into(),
15671                    argument: Some(FieldArgument::Text("new source".into())),
15672                },
15673            ],
15674        )
15675        .unwrap();
15676        for (range, replacement) in edits.into_iter().rev() {
15677            output.replace_range(range, &replacement);
15678        }
15679        assert_eq!(
15680            output,
15681            "  bIbLiOgRaPhY\t  \n\\Q \"keep \\\"exact\\\"\"  \\* MERGEFORMAT     \\l 1031 \\m \"new source\""
15682        );
15683        let parsed = parse_field_instruction(&output);
15684        assert!(parsed.arguments.is_empty());
15685        assert_eq!(
15686            parsed.switches.last().unwrap().argument,
15687            Some(FieldArgument::Text("new source".into()))
15688        );
15689        let raw = "BIBLIOGRAPHY \\f \\* MERGEFORMAT";
15690        let mut output = raw.to_owned();
15691        for (range, replacement) in bibliography_option_switch_edits(raw, &[])
15692            .unwrap()
15693            .into_iter()
15694            .rev()
15695        {
15696            output.replace_range(range, &replacement);
15697        }
15698        assert_eq!(output, "BIBLIOGRAPHY  \\* MERGEFORMAT");
15699    }
15700
15701    #[test]
15702    fn bibliography_owned_token_edits_reject_ambiguous_boundaries() {
15703        for raw in [
15704            r#"BIBLIOGRAPHY \l "1033"adjacent"#,
15705            r#"BIBLIOGRAPHY \m "unterminated"#,
15706            r#"BIBLIOGRAPHY \m \l 1033"#,
15707            r#"BIBLIOGRAPHY \Q bare \l 1033"#,
15708            r#"BIBLIOGRAPHY unexpected"#,
15709        ] {
15710            assert!(bibliography_option_switch_edits(raw, &[]).is_err(), "{raw}");
15711        }
15712    }
15713
15714    #[test]
15715    fn citation_and_bibliography_bare_operands_keep_opcode_specific_association() {
15716        let instruction = parse_field_instruction(
15717            r#"CITATION first \l 1033 \p 17 \v 3 \n \m second \f "before " \y \s " after" \t \l 1036"#,
15718        );
15719        assert_eq!(instruction.arguments, [FieldArgument::Text("first".into())]);
15720        assert_eq!(
15721            instruction
15722                .switches
15723                .iter()
15724                .map(|switch| switch.name.as_str())
15725                .collect::<Vec<_>>(),
15726            ["l", "p", "v", "n", "m", "f", "y", "s", "t", "l"]
15727        );
15728        assert!(
15729            instruction
15730                .switches
15731                .iter()
15732                .filter(|switch| matches!(switch.name.as_str(), "n" | "y" | "t"))
15733                .all(|switch| switch.argument.is_none())
15734        );
15735        let bibliography =
15736            parse_field_instruction(r#"BIBLIOGRAPHY \l 1033 \m first \m "second tag" \f"#);
15737        assert!(bibliography.arguments.is_empty());
15738        assert_eq!(bibliography.switches.len(), 4);
15739        assert!(bibliography.switches[3].argument.is_none());
15740        let unrelated = parse_field_instruction(r#"UNKNOWN first \l 1033"#);
15741        assert_eq!(unrelated.arguments.len(), 2);
15742        assert!(unrelated.switches[0].argument.is_none());
15743    }
15744
15745    #[test]
15746    fn parse_simple_paragraph() {
15747        let p = parse_paragraph(r#"<w:r><w:t>Hello World</w:t></w:r>"#);
15748        assert_eq!(p.text(), "Hello World");
15749        assert_eq!(p.runs.len(), 1);
15750    }
15751
15752    #[test]
15753    fn paragraph_root_attributes_reject_alias_duplicates_and_authored_id_wins() {
15754        let duplicate = br#"<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:a="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:b="http://schemas.microsoft.com/office/word/2010/wordml" a:paraId="11111111" b:paraId="22222222"/>"#;
15755        let error = CT_P::from_xml_fragment(duplicate).unwrap_err();
15756        assert!(
15757            error
15758                .to_string()
15759                .contains("duplicate expanded root attribute")
15760        );
15761
15762        let source = br#"<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:i="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:x="urn:producer" i:paraId="11111111" i:textId="22222222" x:keep="yes"/>"#;
15763        let paragraph = CT_P::from_xml_fragment(source).unwrap();
15764        let mut output = Vec::new();
15765        paragraph
15766            .to_xml_with_para_id(&mut Writer::new(&mut output), Some("AAAAAAAA"))
15767            .unwrap();
15768        let output = String::from_utf8(output).unwrap();
15769        assert_eq!(output.matches(":paraId=").count(), 1, "{output}");
15770        assert!(output.contains(r#"w14:paraId="AAAAAAAA""#), "{output}");
15771        assert!(output.contains(r#"i:textId="22222222""#), "{output}");
15772        assert!(output.contains(r#"x:keep="yes""#), "{output}");
15773    }
15774
15775    #[test]
15776    fn a_retained_root_attribute_keeps_the_declaration_its_own_prefix_needs() {
15777        let source = br#"<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:i="http://schemas.microsoft.com/office/word/2010/wordml" i:paraId="11111111"/>"#;
15778        let paragraph = CT_P::from_xml_fragment(source).unwrap();
15779        let mut output = Vec::new();
15780        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
15781        let output = String::from_utf8(output).unwrap();
15782        assert!(output.contains(r#"i:paraId="11111111""#), "{output}");
15783        assert!(
15784            output.contains(&format!(r#"xmlns:i="{W14_NS}""#)),
15785            "{output}"
15786        );
15787    }
15788
15789    #[test]
15790    fn a_reopened_paragraph_identity_does_not_rebind_the_prefix_its_part_root_owns() {
15791        // A paragraph written with a bare `w14:paraId` and reopened used to
15792        // come back carrying its own `xmlns:w14`, so saving the reopened
15793        // document produced different bytes from the save it was read from.
15794        let w_ns = crate::namespace::W_NS;
15795        let source =
15796            format!(r#"<w:p xmlns:w="{w_ns}" xmlns:w14="{W14_NS}" w14:paraId="00000001"/>"#);
15797        let paragraph = CT_P::from_xml_fragment(source.as_bytes()).unwrap();
15798        let mut output = Vec::new();
15799        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
15800        let output = String::from_utf8(output).unwrap();
15801        assert!(output.contains(r#"w14:paraId="00000001""#), "{output}");
15802        assert!(
15803            !output.contains("xmlns:w14"),
15804            "the paragraph rebound a prefix its part root already owns: {output}"
15805        );
15806    }
15807
15808    #[test]
15809    fn dropping_w14_identities_keeps_every_other_retained_attribute() {
15810        // An alias prefix goes with the identities it bound, and a record
15811        // left with nothing else goes away.
15812        let w_ns = crate::namespace::W_NS;
15813        let source = format!(
15814            r#"<w:p xmlns:w="{w_ns}" xmlns:i="{W14_NS}" xmlns:x="urn:producer" i:paraId="11111111" w:rsidR="00A1B2C3" i:textId="22222222" x:keep="yes"/>"#
15815        );
15816        let mut paragraph = CT_P::from_xml_fragment(source.as_bytes()).unwrap();
15817        drop_w14_paragraph_identities(&mut paragraph.extra_xml).unwrap();
15818        let mut output = Vec::new();
15819        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
15820        let output = String::from_utf8(output).unwrap();
15821        assert!(
15822            output.starts_with(r#"<w:p w:rsidR="00A1B2C3" x:keep="yes""#),
15823            "{output}"
15824        );
15825        assert!(output.contains(r#"xmlns:x="urn:producer""#), "{output}");
15826        assert!(!output.contains("Id="), "{output}");
15827        assert!(!output.contains("xmlns:i="), "{output}");
15828
15829        let source = format!(
15830            r#"<w:p xmlns:w="{w_ns}" xmlns:w14="{W14_NS}" w14:paraId="11111111" w14:textId="22222222"/>"#
15831        );
15832        let mut paragraph = CT_P::from_xml_fragment(source.as_bytes()).unwrap();
15833        drop_w14_paragraph_identities(&mut paragraph.extra_xml).unwrap();
15834        assert!(paragraph.extra_xml.is_empty(), "{:?}", paragraph.extra_xml);
15835    }
15836
15837    #[test]
15838    fn a_part_root_declares_w14_only_when_its_content_needs_it() {
15839        let w_ns = crate::namespace::W_NS;
15840        let part = |root: &str, body: &str| {
15841            format!(
15842                r#"<?xml version="1.0"?><w:document xmlns:w="{w_ns}"{root}><w:body>{body}</w:body></w:document>"#
15843            )
15844            .into_bytes()
15845        };
15846        let identity = r#"<w:p w14:paraId="00000001"/>"#;
15847        let mut unbound = part("", identity);
15848        declare_w14_on_part_root(&mut unbound).unwrap();
15849        assert_eq!(
15850            unbound,
15851            part(&format!(r#" xmlns:w14="{W14_NS}""#), identity)
15852        );
15853        crate::document::CT_Document::from_xml(&unbound).unwrap();
15854
15855        for unchanged in [
15856            part(&format!(r#" xmlns:w14="{W14_NS}""#), identity),
15857            part(r#" xmlns:w14="urn:other""#, identity),
15858            part("", r#"<w:p><w:r><w:t>w14</w:t></w:r></w:p>"#),
15859        ] {
15860            let mut xml = unchanged.clone();
15861            declare_w14_on_part_root(&mut xml).unwrap();
15862            assert_eq!(xml, unchanged);
15863        }
15864    }
15865
15866    #[test]
15867    fn a_root_with_only_namespace_declarations_records_nothing() {
15868        let start = BytesStart::from_content(
15869            r#"w:sectPr xmlns:sa="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:sx="urn:section""#,
15870            "w:sectPr".len(),
15871        );
15872        let record =
15873            capture_root_attribute_record(&start, &["w".to_owned()]).expect("capture succeeds");
15874        assert!(record.is_none(), "{record:?}");
15875    }
15876
15877    #[test]
15878    fn a_run_identity_resolves_the_word_prefix_the_default_scope_names() {
15879        // `CT_P::from_xml` names `w` as the Word prefix without binding it,
15880        // which is the scope a paragraph cut out of its part can be parsed in.
15881        // A run identity under it used to fail the paragraph as unbound.
15882        let paragraph = parse_paragraph(
15883            r#"<w:r w:rsidR="00A1B2C3" w:rsidRPr="00D4E5F6"><w:t>entry</w:t></w:r>"#,
15884        );
15885        assert_eq!(paragraph.text(), "entry");
15886        let mut output = Vec::new();
15887        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
15888        let output = String::from_utf8(output).unwrap();
15889        assert!(
15890            output.contains(r#"<w:r w:rsidR="00A1B2C3" w:rsidRPr="00D4E5F6""#),
15891            "{output}"
15892        );
15893    }
15894
15895    #[test]
15896    fn the_default_word_prefix_records_what_its_explicit_binding_records() {
15897        // The fallback must produce the record a part that binds `w` produces,
15898        // must never override an explicit binding, and must not reach a
15899        // prefix the scope does not name as Word.
15900        let start = BytesStart::from_content(r#"w:r w:rsidR="00A1B2C3""#, "w:r".len());
15901        let default_scope = capture_root_attribute_record(&start, &["w".to_owned()]).unwrap();
15902        let bound = capture_root_attribute_record(
15903            &start,
15904            &[format!("\0w\0{}", crate::namespace::W_NS), "w".to_owned()],
15905        )
15906        .unwrap();
15907        assert!(default_scope.is_some());
15908        assert_eq!(default_scope, bound);
15909
15910        let rebound =
15911            capture_root_attribute_record(&start, &["w".to_owned(), "\0w\0urn:other".to_owned()])
15912                .unwrap()
15913                .unwrap();
15914        let rebound = String::from_utf8(rebound).unwrap();
15915        assert!(rebound.contains(r#"xmlns:w="urn:other""#), "{rebound}");
15916
15917        let foreign = BytesStart::from_content(r#"w:r x:id="1""#, "w:r".len());
15918        let error = capture_root_attribute_record(&foreign, &["w".to_owned()]).unwrap_err();
15919        assert!(
15920            error.to_string().contains("prefix `x` is unbound"),
15921            "{error}"
15922        );
15923    }
15924
15925    #[test]
15926    fn a_retained_record_declares_only_a_binding_the_written_element_lacks() {
15927        // The written element's own `w:` name needs the canonical `w` binding
15928        // in scope, so repeating it on every element with a `w:rsid*`
15929        // attribute only grew the part. An alias prefix and a foreign prefix
15930        // still need their declaration.
15931        let w_ns = crate::namespace::W_NS;
15932        let written = |attributes: &str, prefixes: &[String]| {
15933            let source = BytesStart::from_content(format!("w:p {attributes}"), "w:p".len());
15934            let record = capture_root_attribute_record(&source, prefixes)
15935                .unwrap()
15936                .unwrap();
15937            let mut start = BytesStart::new("w:p");
15938            push_root_attribute_record(&mut start, &record, None).unwrap();
15939            let mut output = Vec::new();
15940            Writer::new(&mut output)
15941                .write_event(Event::Empty(start))
15942                .unwrap();
15943            String::from_utf8(output).unwrap()
15944        };
15945        let word = format!("\0w\0{w_ns}");
15946
15947        assert_eq!(
15948            written(r#"w:rsidR="00A1B2C3""#, std::slice::from_ref(&word)),
15949            r#"<w:p w:rsidR="00A1B2C3"/>"#
15950        );
15951        assert_eq!(
15952            written(
15953                r#"q:rsidR="00A1B2C3""#,
15954                &[word.clone(), format!("\0q\0{w_ns}")]
15955            ),
15956            format!(r#"<w:p q:rsidR="00A1B2C3" xmlns:q="{w_ns}"/>"#)
15957        );
15958        assert_eq!(
15959            written(
15960                r#"w:rsidR="00A1B2C3" x:keep="yes""#,
15961                &[word.clone(), "\0x\0urn:producer".to_owned()]
15962            ),
15963            r#"<w:p w:rsidR="00A1B2C3" x:keep="yes" xmlns:x="urn:producer"/>"#
15964        );
15965    }
15966
15967    #[test]
15968    fn a_retained_element_does_not_rebind_a_prefix_its_part_root_declares() {
15969        let r_ns = crate::namespace::R_NS;
15970        let mc_ns = "http://schemas.openxmlformats.org/markup-compatibility/2006";
15971        let wp_ns = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
15972        let xml = format!(
15973            r#"<w:document xmlns:w="{}" xmlns:r="{r_ns}" xmlns:mc="{mc_ns}" xmlns:wp="{wp_ns}"><w:body><w:p w:rsidR="00A1B2C3" r:stamp="relationship" mc:stamp="compatibility" wp:stamp="drawing" xmlns:x="urn:producer" x:stamp="paragraph"><w:r w:rsidRPr="00D4E5F6" xmlns:y="urn:new-binding" y:stamp="run"><w:t>Kept</w:t></w:r><x:opaque xmlns:x="urn:producer" x:flag="exact"><x:inside/></x:opaque></w:p><w:sectPr/></w:body></w:document>"#,
15974            crate::namespace::W_NS
15975        );
15976        let document = crate::document::CT_Document::from_xml(xml.as_bytes()).unwrap();
15977        let saved = String::from_utf8(document.to_xml().unwrap()).unwrap();
15978        assert_eq!(saved.matches("xmlns:w=").count(), 1, "{saved}");
15979        for prefix in ["r", "mc", "wp"] {
15980            assert_eq!(
15981                saved.matches(&format!("xmlns:{prefix}=")).count(),
15982                1,
15983                "{saved}"
15984            );
15985        }
15986        for attribute in [
15987            r#"w:rsidR="00A1B2C3""#,
15988            r#"w:rsidRPr="00D4E5F6""#,
15989            r#"r:stamp="relationship""#,
15990            r#"mc:stamp="compatibility""#,
15991            r#"wp:stamp="drawing""#,
15992            r#"x:stamp="paragraph""#,
15993            r#"y:stamp="run""#,
15994            r#"xmlns:x="urn:producer""#,
15995            r#"xmlns:y="urn:new-binding""#,
15996        ] {
15997            assert!(saved.contains(attribute), "missing {attribute}: {saved}");
15998        }
15999        let raw = r#"<x:opaque xmlns:x="urn:producer" x:flag="exact"><x:inside/></x:opaque>"#;
16000        assert!(saved.contains(raw), "raw child changed: {saved}");
16001        let reopened = crate::document::CT_Document::from_xml(saved.as_bytes()).unwrap();
16002        assert_eq!(reopened.to_xml().unwrap(), saved.as_bytes());
16003    }
16004
16005    #[test]
16006    fn a_standalone_or_comment_paragraph_keeps_its_local_relationship_binding() {
16007        let source = format!(
16008            r#"<w:p xmlns:w="{}" xmlns:r="{R_NS}" r:stamp="kept"/>"#,
16009            crate::namespace::W_NS
16010        );
16011        let paragraph = CT_P::from_xml_fragment(source.as_bytes()).unwrap();
16012        let mut output = Vec::new();
16013        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
16014        let output = String::from_utf8(output).unwrap();
16015        assert!(output.contains(&format!(r#"xmlns:r="{R_NS}""#)), "{output}");
16016
16017        let comments = format!(
16018            r#"<w:comments xmlns:w="{}"><w:comment w:id="1" w:author="Ada"><w:p xmlns:r="{R_NS}" r:stamp="kept"/></w:comment></w:comments>"#,
16019            crate::namespace::W_NS
16020        );
16021        let comments = crate::comments::CT_Comments::from_xml(comments.as_bytes()).unwrap();
16022        let output = String::from_utf8(comments.to_xml().unwrap()).unwrap();
16023        assert!(output.contains(&format!(r#"xmlns:r="{R_NS}""#)), "{output}");
16024        assert!(output.contains(r#"r:stamp="kept""#), "{output}");
16025    }
16026
16027    #[test]
16028    fn header_footer_and_note_roots_own_their_canonical_bindings() {
16029        let wp_ns = WP_NS;
16030        for root in ["hdr", "ftr"] {
16031            let source = format!(
16032                r#"<w:{root} xmlns:w="{}" xmlns:r="{R_NS}" xmlns:wp="{wp_ns}"><w:p r:stamp="link" wp:stamp="drawing"/></w:{root}>"#,
16033                crate::namespace::W_NS
16034            );
16035            let part = crate::header_footer::CT_HdrFtr::from_xml(source.as_bytes()).unwrap();
16036            let output = if root == "hdr" {
16037                part.to_xml_header().unwrap()
16038            } else {
16039                part.to_xml_footer().unwrap()
16040            };
16041            let output = String::from_utf8(output).unwrap();
16042            assert_eq!(output.matches("xmlns:r=").count(), 1, "{output}");
16043            assert_eq!(output.matches("xmlns:wp=").count(), 1, "{output}");
16044            assert!(output.contains(r#"r:stamp="link""#), "{output}");
16045            assert!(output.contains(r#"wp:stamp="drawing""#), "{output}");
16046        }
16047
16048        for (root, note) in [("footnotes", "footnote"), ("endnotes", "endnote")] {
16049            let source = format!(
16050                r#"<w:{root} xmlns:w="{}" xmlns:r="{R_NS}"><w:{note} w:id="2"><w:p r:stamp="link"/></w:{note}></w:{root}>"#,
16051                crate::namespace::W_NS
16052            );
16053            let part = crate::footnotes::CT_Footnotes::from_xml(source.as_bytes()).unwrap();
16054            let output = if root == "footnotes" {
16055                part.to_xml_footnotes().unwrap()
16056            } else {
16057                part.to_xml_endnotes().unwrap()
16058            };
16059            let output = String::from_utf8(output).unwrap();
16060            assert_eq!(output.matches("xmlns:r=").count(), 1, "{output}");
16061            assert!(output.contains(r#"r:stamp="link""#), "{output}");
16062        }
16063    }
16064
16065    #[test]
16066    fn a_root_prefix_shadow_with_another_uri_remains_local() {
16067        let source = format!(
16068            r#"<w:document xmlns:w="{}" xmlns:r="{R_NS}"><w:body><w:p xmlns:r="urn:producer" r:stamp="foreign"><w:r><w:t>Kept</w:t></w:r></w:p></w:body></w:document>"#,
16069            crate::namespace::W_NS
16070        );
16071        let document = crate::document::CT_Document::from_xml(source.as_bytes()).unwrap();
16072        let output = String::from_utf8(document.to_xml().unwrap()).unwrap();
16073        assert!(output.contains(r#"xmlns:r="urn:producer""#), "{output}");
16074        assert!(output.contains(r#"r:stamp="foreign""#), "{output}");
16075    }
16076
16077    #[test]
16078    fn a_noncanonical_wp_root_does_not_claim_the_canonical_binding() {
16079        let source = format!(
16080            r#"<w:document xmlns:w="{}" xmlns:wp="urn:producer"><w:body><w:p xmlns:wp="{WP_NS}" wp:stamp="drawing"/></w:body></w:document>"#,
16081            crate::namespace::W_NS
16082        );
16083        let document = crate::document::CT_Document::from_xml(source.as_bytes()).unwrap();
16084        let output = String::from_utf8(document.to_xml().unwrap()).unwrap();
16085        assert!(output.contains(r#"xmlns:wp="urn:producer""#), "{output}");
16086        assert!(
16087            output.contains(&format!(r#"xmlns:wp="{WP_NS}""#)),
16088            "{output}"
16089        );
16090        assert!(output.contains(r#"wp:stamp="drawing""#), "{output}");
16091    }
16092
16093    #[test]
16094    fn nested_root_binding_scopes_restore_on_unwind() {
16095        let before = ROOT_BINDINGS.with(Cell::get);
16096        let result = std::panic::catch_unwind(|| {
16097            let _outer = root_binding_scope(ROOT_R_BINDING);
16098            assert_eq!(ROOT_BINDINGS.with(Cell::get), ROOT_R_BINDING);
16099            let _inner = root_binding_scope(ROOT_MC_BINDING);
16100            assert_eq!(ROOT_BINDINGS.with(Cell::get), ROOT_MC_BINDING);
16101            panic!("scope restoration probe");
16102        });
16103        assert!(result.is_err());
16104        assert_eq!(ROOT_BINDINGS.with(Cell::get), before);
16105    }
16106
16107    #[test]
16108    fn complex_hyperlink_field_exposes_its_target_and_cached_text() {
16109        let p = parse_paragraph(concat!(
16110            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
16111            r#"<w:r><w:instrText xml:space="preserve"> HYPERLINK &quot;https://example.test/path&quot; </w:instrText></w:r>"#,
16112            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
16113            r#"<w:r><w:t>Example link</w:t></w:r>"#,
16114            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
16115        ));
16116
16117        assert_eq!(p.text(), "Example link");
16118        assert_eq!(
16119            p.complex_field_hyperlinks(),
16120            vec![ComplexFieldHyperlink {
16121                run_start: 0,
16122                run_end: 1,
16123                target: "https://example.test/path".to_owned(),
16124            }]
16125        );
16126    }
16127
16128    #[test]
16129    fn form_text_field_imports_its_cached_plain_text() {
16130        let p = parse_paragraph(concat!(
16131            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
16132            r#"<w:r><w:instrText> FORMTEXT </w:instrText></w:r>"#,
16133            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
16134            r#"<w:r><w:t>Entered value</w:t></w:r>"#,
16135            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
16136        ));
16137
16138        assert_eq!(p.text(), "Entered value");
16139        assert!(p.complex_field_hyperlinks().is_empty());
16140    }
16141
16142    #[test]
16143    fn unsafe_complex_fields_are_not_exposed_as_links() {
16144        let cases = [
16145            concat!(
16146                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
16147                r#"<w:r><w:instrText> HYPERLINK &quot;https://example.test&quot; </w:instrText></w:r>"#,
16148            ),
16149            concat!(
16150                r#"<w:r><w:fldChar w:fldCharType="begin" w:dirty="true"/></w:r>"#,
16151                r#"<w:r><w:instrText> HYPERLINK &quot;https://example.test&quot; </w:instrText></w:r>"#,
16152                r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
16153                r#"<w:r><w:t>Cached</w:t></w:r>"#,
16154                r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
16155            ),
16156            concat!(
16157                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
16158                r#"<w:r><w:instrText> HYPERLINK </w:instrText></w:r>"#,
16159                r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
16160                r#"<w:r><w:t>Cached</w:t></w:r>"#,
16161                r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
16162            ),
16163            concat!(
16164                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
16165                r#"<w:r><w:instrText> HYPERLINK &quot;https://example.test&quot; </w:instrText></w:r>"#,
16166                r#"<w:r><w:fldChar w:fldCharType="separate" w:dirty="true"/></w:r>"#,
16167                r#"<w:r><w:t>Cached</w:t></w:r>"#,
16168                r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
16169            ),
16170            concat!(
16171                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
16172                r#"<w:r><w:instrText> HYPERLINK &quot;https://example.test&quot; </w:instrText></w:r>"#,
16173                r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
16174                r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
16175            ),
16176        ];
16177
16178        for case in cases {
16179            let paragraph = parse_paragraph(case);
16180            assert!(paragraph.complex_field_hyperlinks().is_empty(), "{case}");
16181        }
16182    }
16183
16184    #[test]
16185    fn empty_paragraph_properties_model_only_attribute_free_form() {
16186        let plain = parse_paragraph(r#"<w:pPr/><w:r><w:t>Text</w:t></w:r>"#);
16187        assert!(plain.properties.is_some());
16188        assert!(plain.extra_xml.is_empty());
16189
16190        let attributed =
16191            parse_paragraph(r#"<w:pPr w:rsidR="00112233"/><w:r><w:t>Text</w:t></w:r>"#);
16192        assert!(attributed.properties.is_none());
16193        assert_eq!(attributed.extra_xml.len(), 1);
16194        assert!(String::from_utf8_lossy(&attributed.extra_xml[0].1).contains("00112233"));
16195    }
16196
16197    #[test]
16198    fn parse_paragraph_with_properties() {
16199        let p = parse_paragraph(
16200            r#"<w:pPr><w:jc w:val="center"/></w:pPr><w:r><w:t>Centered</w:t></w:r>"#,
16201        );
16202        assert_eq!(p.text(), "Centered");
16203        assert!(p.properties.is_some());
16204        assert_eq!(
16205            p.properties.as_ref().unwrap().jc,
16206            Some(crate::shared::ST_Jc::Center)
16207        );
16208    }
16209
16210    #[test]
16211    fn consumed_reader_admission_preserves_qname_cursor_and_siblings() {
16212        let xml = format!(
16213            r#"<outer><q:p xmlns:q="{0}" xmlns:w="{0}"><w:r><w:t>first</w:t></w:r></q:p><p xmlns="{0}" xmlns:w="{0}"><r><t>second</t></r></p></outer>"#,
16214            crate::namespace::W_NS
16215        );
16216        let mut reader = Reader::from_str(&xml);
16217        assert!(matches!(reader.read_event().unwrap(), Event::Start(_)));
16218        assert!(matches!(reader.read_event().unwrap(), Event::Start(_)));
16219        let position = reader.buffer_position();
16220        crate::document::admit_word_reader(&reader, &["w".into()], b"p", None).unwrap();
16221        assert_eq!(reader.buffer_position(), position);
16222        assert_eq!(CT_P::from_xml(&mut reader).unwrap().text(), "first");
16223        let Event::Start(start) = reader.read_event().unwrap() else {
16224            panic!("second owner")
16225        };
16226        assert_eq!(start.name().as_ref(), b"p");
16227        assert_eq!(CT_P::from_xml(&mut reader).unwrap().text(), "second");
16228        assert!(matches!(reader.read_event().unwrap(), Event::End(_)));
16229
16230        let mut malformed = Reader::from_str("<q:p><w:r/></w:p>");
16231        malformed.read_event().unwrap();
16232        let position = malformed.buffer_position();
16233        assert!(CT_P::from_xml(&mut malformed).is_err());
16234        assert_eq!(malformed.buffer_position(), position);
16235
16236        let mut run = Reader::from_str("<q:r><w:t>run</w:t></q:r><tail/>");
16237        run.read_event().unwrap();
16238        assert_eq!(CT_R::from_xml(&mut run).unwrap().text(), "run");
16239        let Event::Empty(tail) = run.read_event().unwrap() else {
16240            panic!("run sibling")
16241        };
16242        assert_eq!(tail.name().as_ref(), b"tail");
16243    }
16244
16245    #[test]
16246    fn consumed_paragraph_unbound_run_fallback_has_resource_admission() {
16247        let drawing = |depth: usize| {
16248            let mut body = "<w:p><w:r><w:t>leaf</w:t></w:r></w:p>".to_owned();
16249            for _ in 0..depth {
16250                body = format!(
16251                    "<w:p><w:r><w:drawing xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\" xmlns:x=\"urn:producer\"><wp:anchor><wp:extent cx=\"10\" cy=\"20\"/><wp:docPr id=\"1\" name=\"bounded\"/><x:wrapper><w:txbxContent>{body}</w:txbxContent></x:wrapper></wp:anchor></w:drawing></w:r></w:p>"
16252                );
16253            }
16254            body.strip_prefix("<w:p><w:r>")
16255                .unwrap()
16256                .strip_suffix("</w:r></w:p>")
16257                .unwrap()
16258                .to_owned()
16259        };
16260        let shallow = format!("<q:p><q:r>{}</q:r></q:p><tail/>", drawing(1));
16261        let mut reader = Reader::from_str(&shallow);
16262        reader.read_event().unwrap();
16263        let paragraph = CT_P::from_xml(&mut reader).unwrap();
16264        let RunContent::Drawing(actual_drawing) = &paragraph.runs[0].content[0] else {
16265            panic!("actual legacy fallback drawing")
16266        };
16267        let shape = actual_drawing
16268            .anchor
16269            .as_ref()
16270            .unwrap()
16271            .shape
16272            .as_ref()
16273            .unwrap();
16274        let body = shape.text_body.as_ref().unwrap();
16275        let crate::document::BodyContent::Paragraph(p) = &body.content[0] else {
16276            panic!("actual typed text body")
16277        };
16278        assert_eq!(p.text(), "leaf");
16279        assert!(matches!(reader.read_event().unwrap(), Event::Empty(_)));
16280        for prefix in ["q:", ""] {
16281            let source = format!(
16282                "<p><{prefix}r>{}</{prefix}r></p>",
16283                drawing(1).replace("w:drawing", &format!("{prefix}drawing"))
16284            );
16285            let mut reader = Reader::from_str(&source);
16286            reader.read_event().unwrap();
16287            let paragraph = CT_P::from_xml(&mut reader).unwrap();
16288            assert!(
16289                matches!(paragraph.runs[0].content[0], RunContent::Drawing(_)),
16290                "actual {prefix} fallback"
16291            );
16292        }
16293        // Deep input is checked by admission only, never constructed in the
16294        // prechange baseline. The native shallow case proves this real route.
16295        for (depth, accepted) in [(16, true), (17, false)] {
16296            let xml = format!("<q:p><q:r>{}</q:r></q:p>", drawing(depth));
16297            let mut reader = Reader::from_str(&xml);
16298            reader.read_event().unwrap();
16299            let before = reader.buffer_position();
16300            let admitted = crate::document::admit_word_reader(&reader, &["w".into()], b"p", None);
16301            assert_eq!(
16302                admitted.is_ok(),
16303                accepted,
16304                "legacy depth {depth}: {admitted:?}"
16305            );
16306            assert_eq!(reader.buffer_position(), before);
16307        }
16308        let opaque = format!(
16309            "<q:p><q:r xmlns:q=\"urn:foreign\">{}</q:r></q:p>",
16310            drawing(17)
16311        );
16312        let mut reader = Reader::from_str(&opaque);
16313        reader.read_event().unwrap();
16314        crate::document::admit_word_reader(&reader, &["w".into()], b"p", None).unwrap();
16315    }
16316
16317    #[test]
16318    fn full_document_admission_accounts_for_native_unbound_run_fallback() {
16319        let wrap = |depth: usize| {
16320            let mut body = "<w:p><w:r><w:t>leaf</w:t></w:r></w:p>".to_owned();
16321            for _ in 0..depth {
16322                body = format!(
16323                    "<w:p><w:r><w:drawing xmlns:wp=\"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing\" xmlns:x=\"urn:producer\"><wp:anchor><wp:extent cx=\"10\" cy=\"20\"/><wp:docPr id=\"1\" name=\"bounded\"/><x:wrapper><w:txbxContent>{body}</w:txbxContent></x:wrapper></wp:anchor></w:drawing></w:r></w:p>"
16324                );
16325            }
16326            let content = body
16327                .strip_prefix("<w:p><w:r>")
16328                .unwrap()
16329                .strip_suffix("</w:r></w:p>")
16330                .unwrap();
16331            format!(
16332                "<w:document xmlns:w=\"{}\"><w:body><w:p><q:r>{content}</q:r></w:p></w:body></w:document>",
16333                crate::namespace::W_NS
16334            )
16335        };
16336        for depth in [1, 17] {
16337            let xml = wrap(depth);
16338            let document = crate::document::CT_Document::from_xml(xml.as_bytes()).unwrap();
16339            let crate::document::BodyContent::Paragraph(paragraph) = &document.body.content[0]
16340            else {
16341                panic!("typed paragraph")
16342            };
16343            assert!(
16344                paragraph.runs.is_empty(),
16345                "unbound run is opaque at full depth {depth}"
16346            );
16347            let raw =
16348                &xml[xml.find("<q:r>").unwrap()..xml.find("</q:r>").unwrap() + "</q:r>".len()];
16349            assert!(
16350                paragraph
16351                    .extra_xml
16352                    .iter()
16353                    .any(|(_, bytes)| bytes == raw.as_bytes())
16354            );
16355            let saved = document.to_xml().unwrap();
16356            assert!(
16357                saved
16358                    .windows(raw.len())
16359                    .any(|bytes| bytes == raw.as_bytes())
16360            );
16361            let reopened = crate::document::CT_Document::from_xml(&saved).unwrap();
16362            let crate::document::BodyContent::Paragraph(p) = &reopened.body.content[0] else {
16363                panic!("reopened paragraph")
16364            };
16365            assert!(p.runs.is_empty());
16366            assert!(p.extra_xml.iter().any(|(_, bytes)| bytes == raw.as_bytes()));
16367            crate::document::admit_word_xml(xml.as_bytes(), &["w".into()], None).unwrap();
16368        }
16369        for declaration in [
16370            format!("xmlns:q=\"{}\"", crate::namespace::W_NS),
16371            "xmlns:q=\"urn:foreign\"".into(),
16372        ] {
16373            let xml = wrap(1).replace("<w:document ", &format!("<w:document {declaration} "));
16374            let document = crate::document::CT_Document::from_xml(xml.as_bytes()).unwrap();
16375            let crate::document::BodyContent::Paragraph(p) = &document.body.content[0] else {
16376                panic!("namespace paragraph")
16377            };
16378            if declaration.contains(crate::namespace::W_NS) {
16379                assert!(matches!(p.runs[0].content[0], RunContent::Drawing(_)));
16380                let deep = wrap(17).replace("<w:document ", &format!("<w:document {declaration} "));
16381                assert!(
16382                    crate::document::admit_word_xml(deep.as_bytes(), &["w".into()], None).is_err()
16383                );
16384            } else {
16385                assert!(p.runs.is_empty());
16386            }
16387        }
16388    }
16389
16390    #[test]
16391    fn consumed_reader_admission_retains_resource_refusal_and_opaque_scope() {
16392        for (depth, accepted) in [(256, true), (257, false)] {
16393            let xml = format!(
16394                "<q:p>{}<w:r><w:t>text</w:t></w:r>{}</q:p>",
16395                "<w:sdt><w:sdtContent>".repeat(depth),
16396                "</w:sdtContent></w:sdt>".repeat(depth)
16397            );
16398            let mut reader = Reader::from_str(&xml);
16399            reader.read_event().unwrap();
16400            let position = reader.buffer_position();
16401            assert_eq!(
16402                crate::document::admit_word_reader(&reader, &["w".into()], b"p", None).is_ok(),
16403                accepted
16404            );
16405            assert_eq!(reader.buffer_position(), position);
16406        }
16407        let xml = format!(
16408            "<foreign:p><foreign:payload>{}{}</foreign:payload></foreign:p>",
16409            "<w:sdt><w:sdtContent>".repeat(257),
16410            "</w:sdtContent></w:sdt>".repeat(257)
16411        );
16412        let mut reader = Reader::from_str(&xml);
16413        reader.read_event().unwrap();
16414        crate::document::admit_word_reader(&reader, &["w".into()], b"p", None).unwrap();
16415    }
16416
16417    #[test]
16418    fn direct_paragraph_parser_accepts_explicit_property_binding() {
16419        let xml = format!(
16420            r#"<outer xmlns:ext="urn:producer"><q:p xmlns:q="{}"><ext:pPr><ext:jc ext:val="right"/></ext:pPr><q:pPr xmlns:q="{}"><ext:jc ext:val="right"/><q:jc q:val="center"/></q:pPr><q:r><q:t>Direct</q:t></q:r></q:p></outer>"#,
16421            crate::namespace::W_NS,
16422            crate::namespace::W_NS
16423        );
16424        let mut reader = Reader::from_str(&xml);
16425        let mut buf = Vec::new();
16426        let parsed = loop {
16427            match reader.read_event_into(&mut buf) {
16428                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"p" => {
16429                    break CT_P::from_xml(&mut reader).unwrap();
16430                }
16431                Ok(Event::Eof) => panic!("missing paragraph"),
16432                event => {
16433                    event.unwrap();
16434                }
16435            }
16436            buf.clear();
16437        };
16438        assert_eq!(parsed.text(), "Direct");
16439        assert_eq!(
16440            parsed.properties.as_ref().unwrap().jc,
16441            Some(crate::shared::ST_Jc::Center)
16442        );
16443    }
16444
16445    #[test]
16446    fn direct_paragraph_parser_does_not_invent_foreign_word_identity() {
16447        let xml = r#"<outer><ext:p xmlns:ext="urn:producer"><ext:pPr xmlns:ext="urn:producer"><ext:jc ext:val="right"/></ext:pPr><ext:r><ext:t>Foreign</ext:t></ext:r></ext:p></outer>"#;
16448        let mut reader = Reader::from_str(xml);
16449        let mut buf = Vec::new();
16450        let parsed = loop {
16451            match reader.read_event_into(&mut buf) {
16452                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"p" => {
16453                    break CT_P::from_xml(&mut reader).unwrap();
16454                }
16455                Ok(Event::Eof) => panic!("missing paragraph"),
16456                event => {
16457                    event.unwrap();
16458                }
16459            }
16460            buf.clear();
16461        };
16462        assert!(parsed.properties.is_none());
16463    }
16464
16465    #[test]
16466    fn direct_paragraph_parser_accepts_default_word_namespace() {
16467        let xml = format!(
16468            r#"<outer xmlns:ext="urn:producer"><p xmlns="{0}" xmlns:w="{0}"><ext:pPr><ext:jc ext:val="right"/></ext:pPr><pPr xmlns="{0}" xmlns:w="{0}"><ext:jc ext:val="right"/><jc w:val="center"/></pPr><r><t>Direct</t></r></p></outer>"#,
16469            crate::namespace::W_NS
16470        );
16471        let mut reader = Reader::from_str(&xml);
16472        let mut buf = Vec::new();
16473        let parsed = loop {
16474            match reader.read_event_into(&mut buf) {
16475                Ok(Event::Start(ref element)) if element.local_name().as_ref() == b"p" => {
16476                    break CT_P::from_xml(&mut reader).unwrap();
16477                }
16478                Ok(Event::Eof) => panic!("missing paragraph"),
16479                event => {
16480                    event.unwrap();
16481                }
16482            }
16483            buf.clear();
16484        };
16485        assert_eq!(parsed.text(), "Direct");
16486        assert_eq!(
16487            parsed.properties.as_ref().unwrap().jc,
16488            Some(crate::shared::ST_Jc::Center)
16489        );
16490    }
16491
16492    #[test]
16493    fn parse_run_with_formatting() {
16494        let p = parse_paragraph(r#"<w:r><w:rPr><w:b/><w:i/></w:rPr><w:t>Bold Italic</w:t></w:r>"#);
16495        let run = &p.runs[0];
16496        let rpr = run.properties.as_ref().unwrap();
16497        assert_eq!(rpr.bold, Some(true));
16498        assert_eq!(rpr.italic, Some(true));
16499    }
16500
16501    #[test]
16502    fn parse_multiple_runs() {
16503        let p = parse_paragraph(r#"<w:r><w:t>Hello </w:t></w:r><w:r><w:t>World</w:t></w:r>"#);
16504        assert_eq!(p.runs.len(), 2);
16505        assert_eq!(p.text(), "Hello World");
16506    }
16507
16508    #[test]
16509    fn parse_hyperlink() {
16510        let p = parse_paragraph(
16511            r#"<w:hyperlink r:id="rId5"><w:r><w:t>Click here</w:t></w:r></w:hyperlink>"#,
16512        );
16513        assert_eq!(p.runs.len(), 1);
16514        assert_eq!(p.text(), "Click here");
16515        assert_eq!(p.hyperlinks.len(), 1);
16516        assert_eq!(p.hyperlinks[0].rel_id, Some("rId5".to_string()));
16517        assert_eq!(p.hyperlinks[0].run_start, 0);
16518        assert_eq!(p.hyperlinks[0].run_end, 1);
16519    }
16520
16521    #[test]
16522    fn parse_hyperlink_with_anchor() {
16523        let p = parse_paragraph(
16524            r#"<w:hyperlink w:anchor="section1" w:tooltip="Jump" w:docLocation="target"><w:r><w:t>Go to section</w:t></w:r></w:hyperlink>"#,
16525        );
16526        assert_eq!(p.hyperlinks.len(), 1);
16527        assert_eq!(p.hyperlinks[0].anchor, Some("section1".to_string()));
16528        assert_eq!(p.hyperlinks[0].tooltip.as_deref(), Some("Jump"));
16529        assert_eq!(p.hyperlinks[0].doc_location.as_deref(), Some("target"));
16530        assert!(p.hyperlinks[0].rel_id.is_none());
16531    }
16532
16533    #[test]
16534    fn parse_hyperlink_uses_local_namespace_aliases_without_reporting_declarations() {
16535        let p = parse_paragraph(concat!(
16536            r#"<q:hyperlink xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main" "#,
16537            r#"q:tooltip="Jump" q:docLocation="target" q:history="1">"#,
16538            r#"<q:r><q:t>Go</q:t></q:r></q:hyperlink>"#,
16539        ));
16540        assert_eq!(p.hyperlinks[0].tooltip.as_deref(), Some("Jump"));
16541        assert_eq!(p.hyperlinks[0].doc_location.as_deref(), Some("target"));
16542        assert!(
16543            p.hyperlinks[0]
16544                .extra_attributes
16545                .contains(&("q:history".to_owned(), "1".to_owned()))
16546        );
16547        assert!(
16548            p.hyperlinks[0]
16549                .extra_attributes
16550                .iter()
16551                .any(|(name, _)| name == "xmlns:q")
16552        );
16553    }
16554
16555    #[test]
16556    fn parse_hyperlink_multiple_runs() {
16557        let p = parse_paragraph(
16558            r#"<w:r><w:t>Before </w:t></w:r><w:hyperlink r:id="rId6"><w:r><w:t>link </w:t></w:r><w:r><w:rPr><w:b/></w:rPr><w:t>text</w:t></w:r></w:hyperlink><w:r><w:t> after</w:t></w:r>"#,
16559        );
16560        assert_eq!(p.runs.len(), 4);
16561        assert_eq!(p.text(), "Before link text after");
16562        assert_eq!(p.hyperlinks.len(), 1);
16563        assert_eq!(p.hyperlinks[0].run_start, 1);
16564        assert_eq!(p.hyperlinks[0].run_end, 3);
16565    }
16566
16567    #[test]
16568    fn round_trip_hyperlink() {
16569        let mut p = CT_P::new();
16570        p.add_run("Before ");
16571        p.add_run("link text");
16572        p.add_run(" after");
16573        p.hyperlinks.push(HyperlinkSpan {
16574            rel_id: Some("rId7".to_string()),
16575            anchor: None,
16576            tooltip: None,
16577            doc_location: None,
16578            run_start: 1,
16579            run_end: 2,
16580            extra_attributes: Vec::new(),
16581            extra_xml: Vec::new(),
16582            preserved_raw_before: None,
16583        });
16584
16585        let mut output = Vec::new();
16586        let mut writer = Writer::new(&mut output);
16587        p.to_xml(&mut writer).unwrap();
16588        let xml = String::from_utf8(output).unwrap();
16589
16590        let parsed = parse_paragraph(
16591            xml.strip_prefix("<w:p>")
16592                .unwrap()
16593                .strip_suffix("</w:p>")
16594                .unwrap(),
16595        );
16596        assert_eq!(parsed.text(), "Before link text after");
16597        assert_eq!(parsed.hyperlinks.len(), 1);
16598        assert_eq!(parsed.hyperlinks[0].rel_id, Some("rId7".to_string()));
16599        assert_eq!(parsed.hyperlinks[0].run_start, 1);
16600        assert_eq!(parsed.hyperlinks[0].run_end, 2);
16601    }
16602
16603    #[test]
16604    fn comment_reference_is_typed_and_round_trips() {
16605        let p = parse_paragraph(
16606            r#"<w:r><w:t>flagged</w:t></w:r><w:r><w:commentReference w:id="1"/></w:r>"#,
16607        );
16608        assert_eq!(p.runs.len(), 2);
16609        assert!(matches!(
16610            p.runs[1].content.as_slice(),
16611            [RunContent::CommentReference { id: 1, .. }]
16612        ));
16613
16614        let mut output = Vec::new();
16615        let mut writer = Writer::new(&mut output);
16616        p.to_xml(&mut writer).unwrap();
16617        let xml = String::from_utf8(output).unwrap();
16618        assert!(
16619            xml.contains(r#"<w:commentReference w:id="1"/>"#),
16620            "comment reference must survive round-trip: {xml}"
16621        );
16622        assert!(xml.contains("flagged"));
16623    }
16624
16625    #[test]
16626    fn alternate_content_is_preserved_once_and_parsed_for_layout() {
16627        // A shape as Word writes it: DrawingML in mc:Choice, VML in the
16628        // fallback. The block has to come back out verbatim, exactly once,
16629        // while still being visible to layout.
16630        let src = concat!(
16631            r#"<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"><mc:AlternateContent><mc:Choice Requires="wps">"#,
16632            r#"<w:drawing><wp:anchor behindDoc="0">"#,
16633            r#"<wp:positionH relativeFrom="column"><wp:posOffset>914400</wp:posOffset></wp:positionH>"#,
16634            r#"<wp:positionV relativeFrom="paragraph"><wp:posOffset>0</wp:posOffset></wp:positionV>"#,
16635            r#"<wp:extent cx="914400" cy="457200"/>"#,
16636            r#"<wp:docPr id="1" name="Shape"/>"#,
16637            r#"<a:graphic><a:graphicData><wps:wsp><wps:spPr>"#,
16638            r#"<a:prstGeom prst="rect"/><a:solidFill><a:srgbClr val="729FCF"/></a:solidFill>"#,
16639            r#"<a:ln><a:solidFill><a:srgbClr val="000000"/></a:solidFill></a:ln>"#,
16640            r#"</wps:spPr><wps:txbx><w:txbxContent>"#,
16641            r#"<w:p><w:r><w:t>boxed</w:t></w:r></w:p>"#,
16642            r#"</w:txbxContent></wps:txbx></wps:wsp></a:graphicData></a:graphic>"#,
16643            r#"</wp:anchor></w:drawing></mc:Choice>"#,
16644            r#"<mc:Fallback><w:pict><v:rect/></w:pict></mc:Fallback>"#,
16645            r#"</mc:AlternateContent></w:r>"#,
16646        );
16647        let p = parse_paragraph(src);
16648        assert_eq!(p.runs.len(), 1);
16649        let run = &p.runs[0];
16650
16651        // Visible to layout.
16652        assert_eq!(
16653            run.alt_drawings.len(),
16654            1,
16655            "the drawing must reach the model"
16656        );
16657        let anchor = run.alt_drawings[0]
16658            .anchor
16659            .as_ref()
16660            .expect("should be an anchored drawing");
16661        let shape = anchor.shape.as_ref().expect("should carry shape content");
16662        assert_eq!(shape.preset.as_deref(), Some("rect"));
16663        assert_eq!(
16664            shape.solid_fill.as_deref(),
16665            Some("729FCF"),
16666            "the fill colour must win over the outline colour"
16667        );
16668        assert!(shape.text.is_empty(), "parsed text body is authoritative");
16669        let body = shape.text_body.as_ref().expect("typed shape text body");
16670        assert_eq!(body.content.len(), 1);
16671        let crate::document::BodyContent::Paragraph(paragraph) = &body.content[0] else {
16672            panic!("typed shape paragraph");
16673        };
16674        assert_eq!(paragraph.text(), "boxed");
16675
16676        // The fragment API supplies canonical w, but explicit foreign rebinding
16677        // must override that fallback at either the owner or paragraph boundary.
16678        for foreign in [
16679            src.replace(
16680                "<w:txbxContent>",
16681                "<w:txbxContent xmlns:w=\"urn:f279:foreign\">",
16682            ),
16683            src.replace(
16684                "<w:p><w:r><w:t>boxed",
16685                "<w:p xmlns:w=\"urn:f279:foreign\"><w:r><w:t>boxed",
16686            ),
16687        ] {
16688            let foreign_paragraph = parse_paragraph(&foreign);
16689            let shape = foreign_paragraph.runs[0].alt_drawings[0]
16690                .anchor
16691                .as_ref()
16692                .unwrap()
16693                .shape
16694                .as_ref()
16695                .unwrap();
16696            assert!(
16697                shape.text.is_empty(),
16698                "explicit foreign Word prefix is not admitted"
16699            );
16700            assert!(shape.text_body.as_ref().is_none_or(|body| {
16701                body.content
16702                    .iter()
16703                    .all(|content| !matches!(content, crate::document::BodyContent::Paragraph(_)))
16704            }));
16705            let mut preserved = Vec::new();
16706            foreign_paragraph
16707                .to_xml(&mut Writer::new(&mut preserved))
16708                .unwrap();
16709            assert!(
16710                String::from_utf8(preserved)
16711                    .unwrap()
16712                    .contains("urn:f279:foreign")
16713            );
16714        }
16715
16716        // Preserved verbatim, exactly once.
16717        let mut output = Vec::new();
16718        let mut writer = Writer::new(&mut output);
16719        p.to_xml(&mut writer).unwrap();
16720        let xml = String::from_utf8(output).unwrap();
16721        assert_eq!(
16722            xml.matches("<mc:AlternateContent").count(),
16723            1,
16724            "the block must not be duplicated: {xml}"
16725        );
16726        assert_eq!(
16727            xml.matches("<mc:Fallback").count(),
16728            1,
16729            "the VML fallback must survive"
16730        );
16731        assert!(xml.contains(r#"<a:prstGeom prst="rect"/>"#));
16732    }
16733
16734    #[test]
16735    fn vml_only_alternate_content_is_preserved_without_a_layout_projection() {
16736        let p = parse_paragraph(concat!(
16737            r#"<w:r><mc:AlternateContent><mc:Choice Requires="vml">"#,
16738            r#"<w:pict><v:shape id="legacy"/></w:pict>"#,
16739            r#"</mc:Choice><mc:Fallback><w:pict><v:shape id="fallback"/></w:pict>"#,
16740            r#"</mc:Fallback></mc:AlternateContent></w:r>"#,
16741        ));
16742
16743        assert!(p.runs[0].alt_drawings.is_empty());
16744        let mut output = Vec::new();
16745        p.to_xml(&mut Writer::new(&mut output))
16746            .expect("paragraph writes");
16747        let xml = String::from_utf8(output).expect("XML is UTF-8");
16748        assert_eq!(xml.matches("<mc:AlternateContent").count(), 1, "{xml}");
16749        assert!(xml.contains(r#"<v:shape id="legacy"/>"#), "{xml}");
16750    }
16751
16752    #[test]
16753    fn empty_run_properties_are_not_moved_after_content() {
16754        // extra_xml is written after the run content, and CT_R requires
16755        // w:rPr first, so a self-closing <w:rPr/> must not be captured.
16756        // Capturing it would emit <w:t> before <w:rPr/> and break the schema.
16757        let p = parse_paragraph(r#"<w:r><w:rPr/><w:t>x</w:t></w:r>"#);
16758        assert_eq!(p.runs.len(), 1);
16759        assert!(
16760            p.runs[0].extra_xml.is_empty(),
16761            "an empty w:rPr must not be captured as extra_xml"
16762        );
16763
16764        let mut output = Vec::new();
16765        let mut writer = Writer::new(&mut output);
16766        p.to_xml(&mut writer).unwrap();
16767        let xml = String::from_utf8(output).unwrap();
16768        assert!(
16769            !xml.contains(r#"<w:t>x</w:t><w:rPr/>"#),
16770            "w:rPr must never follow run content: {xml}"
16771        );
16772    }
16773
16774    #[test]
16775    fn parse_fld_simple_page() {
16776        let p = parse_paragraph(
16777            r#"<w:fldSimple w:instr=" PAGE "><w:r><w:t>1</w:t></w:r></w:fldSimple>"#,
16778        );
16779        assert_eq!(p.runs.len(), 1);
16780        assert_eq!(p.runs[0].content.len(), 1);
16781        assert_eq!(parsed_field(&p, 0).instruction.name, "PAGE");
16782    }
16783
16784    #[test]
16785    fn parse_empty_fld_simple_preserves_the_field() {
16786        let paragraph = parse_paragraph(r#"<w:fldSimple w:instr=" PAGE "/>"#);
16787        let field = parsed_field(&paragraph, 0);
16788
16789        assert_eq!(field.instruction.name, "PAGE");
16790        assert!(field.cached_result.is_empty());
16791    }
16792
16793    #[test]
16794    fn parse_fld_simple_numpages() {
16795        let p = parse_paragraph(
16796            r#"<w:fldSimple w:instr=" NUMPAGES \* MERGEFORMAT "><w:r><w:t>5</w:t></w:r></w:fldSimple>"#,
16797        );
16798        assert_eq!(p.runs.len(), 1);
16799        assert_eq!(parsed_field(&p, 0).instruction.name, "NUMPAGES");
16800    }
16801
16802    fn parsed_field(paragraph: &CT_P, run_index: usize) -> &Field {
16803        match &paragraph.runs[run_index].content[0] {
16804            RunContent::Field(field) => field,
16805            other => panic!("expected field, got {other:?}"),
16806        }
16807    }
16808
16809    fn serialized_paragraph(paragraph: &CT_P) -> String {
16810        let mut output = Vec::new();
16811        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
16812        String::from_utf8(output).unwrap()
16813    }
16814
16815    #[test]
16816    fn field_instruction_corpus_parses_every_simple_complex_split_and_nested_form() {
16817        let corpus = [
16818            (r"PAGE \* MERGEFORMAT", "PAGE"),
16819            ("NUMPAGES \\# \"0\"", "NUMPAGES"),
16820            ("REF \"destination\" \\h", "REF"),
16821            (r"PAGEREF destination \p", "PAGEREF"),
16822            (r"SEQ Figure \r 1", "SEQ"),
16823            ("DOCPROPERTY \"Last Saved By\" \\* Upper", "DOCPROPERTY"),
16824            ("DOCVARIABLE \"Customer Name\"", "DOCVARIABLE"),
16825            ("STYLEREF \"Heading 1\" \\l", "STYLEREF"),
16826            ("INCLUDETEXT \"chapter one.docx\" bookmark", "INCLUDETEXT"),
16827            ("DATE \\@ \"MMMM d, yyyy\"", "DATE"),
16828            ("TIME \\@ \"HH:mm\"", "TIME"),
16829            (r"FILENAME \p", "FILENAME"),
16830            (r"AUTHOR \* Caps", "AUTHOR"),
16831            ("MERGEFIELD \"First Name\" \\* MERGEFORMAT", "MERGEFIELD"),
16832            ("IF \"10\" >= \"2\" \"yes\" \"no\"", "IF"),
16833        ];
16834
16835        for (instruction, expected_name) in corpus {
16836            let escaped = instruction.replace('&', "&amp;").replace('"', "&quot;");
16837            let paragraph = parse_paragraph(&format!(
16838                r#"<w:fldSimple w:instr="{escaped}"><w:r><w:t>cached</w:t></w:r></w:fldSimple>"#,
16839            ));
16840            let field = parsed_field(&paragraph, 0);
16841            assert_eq!(field.instruction.raw, instruction, "{instruction}");
16842            assert_eq!(field.instruction.name, expected_name, "{instruction}");
16843            assert_eq!(field.cached_result, "cached", "{instruction}");
16844            let expected_switch = match expected_name {
16845                "PAGE" | "DOCPROPERTY" | "AUTHOR" | "MERGEFIELD" => Some("*"),
16846                "NUMPAGES" => Some("#"),
16847                "REF" => Some("h"),
16848                "PAGEREF" | "FILENAME" => Some("p"),
16849                "SEQ" => Some("r"),
16850                "STYLEREF" => Some("l"),
16851                "DATE" | "TIME" => Some("@"),
16852                _ => None,
16853            };
16854            if let Some(expected_switch) = expected_switch {
16855                assert!(
16856                    field
16857                        .instruction
16858                        .switches
16859                        .iter()
16860                        .any(|switch| switch.name == expected_switch),
16861                    "{instruction}"
16862                );
16863            }
16864            if expected_name == "IF" {
16865                assert_eq!(
16866                    field.instruction.arguments,
16867                    ["10", ">=", "2", "yes", "no"]
16868                        .map(|value| FieldArgument::Text(value.to_owned()))
16869                );
16870            }
16871        }
16872
16873        let escaped = parse_paragraph(
16874            r#"<w:fldSimple w:instr="MERGEFIELD &quot;Customer \&quot;Code\&quot;&quot; \* MERGEFORMAT"><w:r><w:t>value</w:t></w:r></w:fldSimple>"#,
16875        );
16876        let escaped = parsed_field(&escaped, 0);
16877        assert!(matches!(
16878            escaped.instruction.arguments.first(),
16879            Some(FieldArgument::Text(value)) if value == "Customer \"Code\""
16880        ));
16881        assert!(escaped
16882            .instruction
16883            .switches
16884            .iter()
16885            .any(|switch| switch.name == "*"
16886                && matches!(&switch.argument, Some(FieldArgument::Text(value)) if value == "MERGEFORMAT")));
16887
16888        let complex = parse_paragraph(concat!(
16889            r#"<w:r><w:fldChar w:fldCharType="begin" w:dirty="1"/></w:r>"#,
16890            r#"<w:r><w:instrText xml:space="preserve"> IF 1 = </w:instrText></w:r>"#,
16891            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
16892            r#"<w:r><w:instrText>REF destination</w:instrText></w:r>"#,
16893            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
16894            r#"<w:r><w:t>inside</w:t></w:r>"#,
16895            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
16896            r#"<w:r><w:instrText xml:space="preserve"> &quot;yes&quot; &quot;no&quot; </w:instrText></w:r>"#,
16897            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
16898            r#"<w:r><w:t>yes</w:t></w:r>"#,
16899            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
16900        ));
16901        let complex = parsed_field(&complex, 0);
16902        assert_eq!(complex.instruction.name, "IF");
16903        assert_eq!(complex.cached_result, "yes");
16904        assert_eq!(complex.dirty, Some(true));
16905        assert!(
16906            complex
16907                .instruction
16908                .arguments
16909                .iter()
16910                .any(|argument| matches!(argument, FieldArgument::Nested(field)
16911                if field.instruction.name == "REF" && field.cached_result == "inside"))
16912        );
16913    }
16914
16915    #[test]
16916    fn same_run_complex_markers_are_safe_and_keep_the_cached_result() {
16917        let paragraph = parse_paragraph(concat!(
16918            r#"<w:r><w:fldChar w:fldCharType="begin"/>"#,
16919            r#"<w:instrText>DATE</w:instrText>"#,
16920            r#"<w:fldChar w:fldCharType="separate"/>"#,
16921            r#"<w:t>17 August 2026</w:t>"#,
16922            r#"<w:fldChar w:fldCharType="end"/></w:r>"#,
16923        ));
16924
16925        let field = parsed_field(&paragraph, 0);
16926        assert_eq!(field.instruction.name, "DATE");
16927        assert_eq!(field.cached_result, "17 August 2026");
16928    }
16929
16930    #[test]
16931    fn same_run_sibling_complex_fields_keep_both_typed_sources() {
16932        let xml = concat!(
16933            r#"<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:r>"#,
16934            r#"<w:fldChar w:fldCharType="begin"/><w:instrText>DATE</w:instrText><w:fldChar w:fldCharType="separate"/><w:t>first</w:t><w:fldChar w:fldCharType="end"/>"#,
16935            r#"<w:fldChar w:fldCharType="begin"/><w:instrText>PAGE</w:instrText><w:fldChar w:fldCharType="separate"/><w:t>second</w:t><w:fldChar w:fldCharType="end"/>"#,
16936            r#"</w:r><w:bookmarkStart w:id="1" w:name="after"/></w:p>"#,
16937        );
16938        let paragraph = CT_P::from_xml_fragment(xml.as_bytes()).unwrap();
16939        let fields = paragraph
16940            .runs()
16941            .into_iter()
16942            .flat_map(|run| run.content.iter())
16943            .filter_map(|content| match content {
16944                RunContent::Field(field) => Some(field),
16945                _ => None,
16946            })
16947            .collect::<Vec<_>>();
16948        assert_eq!(fields.len(), 2);
16949        assert_eq!(fields[0].instruction.name, "DATE");
16950        assert_eq!(fields[0].cached_result, "first");
16951        assert_eq!(fields[1].instruction.name, "PAGE");
16952        assert_eq!(fields[1].cached_result, "second");
16953        assert_eq!(fields[0].source_owner_id(), fields[1].source_owner_id());
16954        let serialized = serialized_paragraph(&paragraph);
16955        assert_eq!(serialized.matches("<w:r>").count(), 1, "{serialized}");
16956        assert!(
16957            serialized.find("PAGE").unwrap() < serialized.find("bookmarkStart").unwrap(),
16958            "{serialized}"
16959        );
16960        assert_eq!(
16961            CT_P::story_complex_field_sources(xml.as_bytes(), &[])
16962                .unwrap()
16963                .len(),
16964            2
16965        );
16966
16967        let mut changed = paragraph;
16968        let RunContent::Field(field) = &mut changed.runs[1].content[0] else {
16969            panic!("expected second sibling field")
16970        };
16971        field.cached_result = "changed".to_owned();
16972        let output = serialized_paragraph(&changed);
16973        assert_eq!(output.matches("<w:r>").count(), 2, "{output}");
16974        assert_eq!(output.matches("first").count(), 1, "{output}");
16975        assert_eq!(output.matches("changed").count(), 1, "{output}");
16976        assert!(!output.contains("second"), "{output}");
16977        assert!(
16978            output.find("PAGE").unwrap() < output.find("bookmarkStart").unwrap(),
16979            "{output}"
16980        );
16981    }
16982
16983    #[test]
16984    fn field_discovery_uses_direct_run_children_and_restores_namespace_scope() {
16985        let word_namespace = crate::namespace::W_NS;
16986        let nested_marker = parse_paragraph(concat!(
16987            r#"<w:r><x:extension xmlns:x="urn:producer"><w:fldChar w:fldCharType="begin"/></x:extension></w:r>"#,
16988            r#"<w:r><w:instrText>DATE</w:instrText></w:r>"#,
16989            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
16990            r#"<w:r><w:t>literal</w:t></w:r>"#,
16991            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
16992        ));
16993        assert!(!nested_marker.runs.iter().any(|run| {
16994            run.content
16995                .iter()
16996                .any(|content| matches!(content, RunContent::Field(_)))
16997        }));
16998        assert_eq!(nested_marker.text(), "literal");
16999
17000        let shadowed = parse_paragraph(&format!(
17001            r#"<q:r xmlns:q="{}"><q:fldChar q:fldCharType="begin"/><x:extension xmlns:x="urn:producer" xmlns:q="urn:not-word"/><q:instrText>DATE</q:instrText><q:fldChar q:fldCharType="separate"/><q:t>cached</q:t><q:fldChar q:fldCharType="end"/></q:r>"#,
17002            word_namespace,
17003        ));
17004        let field = parsed_field(&shadowed, 0);
17005        assert_eq!(field.instruction.name, "DATE");
17006        assert_eq!(field.cached_result, "cached");
17007    }
17008
17009    #[test]
17010    fn preserved_instruction_quote_balance_honors_escaped_quotes_and_backslashes() {
17011        for instruction in [
17012            r#"SEQ Figure \* "ARABIC""#,
17013            r#"INCLUDETEXT "\\server\share\file.docx" """#,
17014            r#"DOCPROPERTY "a\"b""#,
17015        ] {
17016            assert!(
17017                Field::new(instruction, "stored")
17018                    .effective_instruction()
17019                    .quotes_are_balanced(),
17020                "{instruction}"
17021            );
17022        }
17023        for instruction in [r#"SEQ Figure \* "ARABIC"#, r#"DOCPROPERTY "a\"b"#] {
17024            assert!(
17025                !Field::new(instruction, "stored")
17026                    .effective_instruction()
17027                    .quotes_are_balanced(),
17028                "{instruction}"
17029            );
17030        }
17031    }
17032
17033    #[test]
17034    fn quoted_backslash_and_empty_operands_remain_arguments() {
17035        let paragraph = parse_paragraph(
17036            r#"<w:fldSimple w:instr="INCLUDETEXT &quot;\\\\server\\share\\file.docx&quot; &quot;&quot;"><w:r><w:t>cached</w:t></w:r></w:fldSimple>"#,
17037        );
17038        let field = parsed_field(&paragraph, 0);
17039        assert_eq!(
17040            field.instruction.arguments,
17041            [r#"\\server\share\file.docx"#, ""].map(|value| FieldArgument::Text(value.to_owned()))
17042        );
17043        assert!(field.instruction.switches.is_empty());
17044    }
17045
17046    #[test]
17047    fn mergefield_and_includetext_switches_own_their_arguments() {
17048        let merge = parse_paragraph(
17049            r#"<w:fldSimple w:instr="MERGEFIELD Name \b &quot;before text&quot; \f &quot;after text&quot;"><w:r><w:t>cached</w:t></w:r></w:fldSimple>"#,
17050        );
17051        let merge = parsed_field(&merge, 0);
17052        assert_eq!(
17053            merge.instruction.arguments,
17054            vec![FieldArgument::Text("Name".to_owned())]
17055        );
17056        assert_eq!(
17057            merge.instruction.switches,
17058            vec![
17059                FieldSwitch {
17060                    name: "b".to_owned(),
17061                    argument: Some(FieldArgument::Text("before text".to_owned())),
17062                },
17063                FieldSwitch {
17064                    name: "f".to_owned(),
17065                    argument: Some(FieldArgument::Text("after text".to_owned())),
17066                },
17067            ]
17068        );
17069
17070        let include = parse_paragraph(
17071            r#"<w:fldSimple w:instr="INCLUDETEXT &quot;chapter.docx&quot; \c &quot;MSWord8&quot;"><w:r><w:t>cached</w:t></w:r></w:fldSimple>"#,
17072        );
17073        let include = parsed_field(&include, 0);
17074        assert_eq!(
17075            include.instruction.arguments,
17076            vec![FieldArgument::Text("chapter.docx".to_owned())]
17077        );
17078        assert_eq!(
17079            include.instruction.switches,
17080            vec![FieldSwitch {
17081                name: "c".to_owned(),
17082                argument: Some(FieldArgument::Text("MSWord8".to_owned())),
17083            }]
17084        );
17085    }
17086
17087    #[test]
17088    fn extended_field_switches_use_the_shared_recursive_grammar() {
17089        let toc = parse_field_instruction_parts(vec![
17090            InstructionPart::Text("TOC \\b ".to_owned()),
17091            InstructionPart::Nested(Field::new("MERGEFIELD Scope", "nested")),
17092        ]);
17093        assert!(toc.arguments.is_empty());
17094        assert!(matches!(
17095            toc.switches.as_slice(),
17096            [FieldSwitch {
17097                name,
17098                argument: Some(FieldArgument::Nested(_)),
17099            }] if name == "b"
17100        ));
17101
17102        let tc = parse_field_instruction(r#"TC "A\"B" \l 2"#);
17103        assert_eq!(tc.arguments, vec![FieldArgument::Text("A\"B".to_owned())]);
17104        assert_eq!(
17105            tc.switches,
17106            vec![FieldSwitch {
17107                name: "l".to_owned(),
17108                argument: Some(FieldArgument::Text("2".to_owned())),
17109            }]
17110        );
17111
17112        let barcode = parse_field_instruction(r"DISPLAYBARCODE value QR \q 3 \p STD");
17113        assert_eq!(barcode.arguments.len(), 2);
17114        assert!(
17115            barcode
17116                .switches
17117                .iter()
17118                .all(|field_switch| field_switch.argument.is_some())
17119        );
17120
17121        let source = concat!(
17122            r#"<q:fldSimple xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main" q:instr="TOC \p &quot;:&quot;" data-token="kept">"#,
17123            r#"<q:r><q:t>cached</q:t><x:opaque xmlns:x="urn:producer"/></q:r>"#,
17124            r#"</q:fldSimple>"#,
17125        );
17126        let paragraph = parse_paragraph(source);
17127        let field = parsed_field(&paragraph, 0);
17128        assert!(matches!(
17129            field.instruction.switches.as_slice(),
17130            [FieldSwitch {
17131                name,
17132                argument: Some(FieldArgument::Text(separator)),
17133            }] if name == "p" && separator == ":"
17134        ));
17135        assert_eq!(
17136            serialized_paragraph(&paragraph),
17137            format!("<w:p>{source}</w:p>")
17138        );
17139    }
17140
17141    #[test]
17142    fn malformed_simple_fields_remain_opaque_and_byte_identical() {
17143        for source in [
17144            r#"<w:fldSimple><w:r><w:t>cached</w:t></w:r></w:fldSimple>"#,
17145            r#"<w:fldSimple w:instr=""><w:r><w:t>cached</w:t></w:r></w:fldSimple>"#,
17146        ] {
17147            let paragraph = parse_paragraph(source);
17148            assert!(!paragraph.runs.iter().any(|run| {
17149                run.content
17150                    .iter()
17151                    .any(|content| matches!(content, RunContent::Field(_)))
17152            }));
17153            assert_eq!(
17154                serialized_paragraph(&paragraph),
17155                format!("<w:p>{source}</w:p>")
17156            );
17157        }
17158    }
17159
17160    #[test]
17161    fn field_mutation_preserves_source_runs_formatting_and_unmodelled_xml() {
17162        let word_namespace = crate::namespace::W_NS;
17163        let mut simple = parse_paragraph(&format!(
17164            r#"<q:fldSimple xmlns:q="{word_namespace}" xmlns:x="urn:producer" q:instr="DATE" q:dirty="1"><q:r data="one"><q:rPr><q:b/></q:rPr><q:t>old</q:t><x:custom><x:item/></x:custom></q:r><q:r data="two"><q:rPr><q:i/></q:rPr><q:t> result</q:t></q:r></q:fldSimple>"#,
17165        ));
17166        let RunContent::Field(field) = &mut simple.runs[0].content[0] else {
17167            panic!("expected simple field")
17168        };
17169        field.cached_result = "new result".to_owned();
17170        field.dirty = Some(false);
17171        let output = serialized_paragraph(&simple);
17172        assert!(
17173            output.contains(r#"<x:custom><x:item/></x:custom>"#),
17174            "{output}"
17175        );
17176        assert!(output.contains(r#"<q:rPr><q:b/></q:rPr>"#), "{output}");
17177        assert!(
17178            output.contains(r#"<q:r data="two"><q:rPr><q:i/></q:rPr>"#),
17179            "{output}"
17180        );
17181        assert_eq!(output.matches("<q:r ").count(), 2, "{output}");
17182        assert!(output.contains("new result"), "{output}");
17183        assert!(output.contains(r#"w:dirty="0""#), "{output}");
17184
17185        let mut complex = parse_paragraph(concat!(
17186            r#"<w:r data="begin"><w:fldChar w:fldCharType="begin" w:dirty="1"/><w:producerBegin/></w:r>"#,
17187            r#"<w:r data="instruction"><w:rPr><w:b/></w:rPr><w:instrText>DATE</w:instrText><w:producerInstruction/></w:r>"#,
17188            r#"<w:proofErr w:type="spellStart"/>"#,
17189            r#"<w:r data="separator"><w:fldChar w:fldCharType="separate"/></w:r>"#,
17190            r#"<w:r data="result-one"><w:rPr><w:i/></w:rPr><w:t>old</w:t><w:producerResult/></w:r>"#,
17191            r#"<w:r data="result-two"><w:rPr><w:b/></w:rPr><w:t> result</w:t></w:r>"#,
17192            r#"<w:r data="end"><w:fldChar w:fldCharType="end"/></w:r>"#,
17193        ));
17194        let RunContent::Field(field) = &mut complex.runs[0].content[0] else {
17195            panic!("expected complex field")
17196        };
17197        field.cached_result = "new result".to_owned();
17198        field.dirty = Some(false);
17199        let output = serialized_paragraph(&complex);
17200        for preserved in [
17201            r#"<w:producerBegin/>"#,
17202            r#"<w:producerInstruction/>"#,
17203            r#"<w:proofErr w:type="spellStart"/>"#,
17204            r#"<w:rPr><w:i/></w:rPr>"#,
17205            r#"<w:producerResult/>"#,
17206            r#"<w:r data="result-two"><w:rPr><w:b/></w:rPr>"#,
17207        ] {
17208            assert!(output.contains(preserved), "missing {preserved}: {output}");
17209        }
17210        assert_eq!(output.matches("<w:r ").count(), 6, "{output}");
17211        assert!(output.contains("new result"), "{output}");
17212        assert!(output.contains(r#"w:dirty="0""#), "{output}");
17213    }
17214
17215    #[test]
17216    fn nested_cache_mutation_preserves_operand_order_and_producer_xml() {
17217        let word_namespace = crate::namespace::W_NS;
17218        let mut paragraph = parse_paragraph(&format!(
17219            concat!(
17220                r#"<q:r xmlns:q="{0}" xmlns:x="urn:producer" data="outer-begin"><q:fldChar q:fldCharType="begin"><x:outerBegin/></q:fldChar></q:r>"#,
17221                r#"<q:r xmlns:q="{0}" xmlns:x="urn:producer" data="outer-instruction"><q:rPr><q:b/></q:rPr><q:instrText xml:space="preserve">IF </q:instrText><x:outerInstruction/></q:r>"#,
17222                r#"<q:r xmlns:q="{0}" xmlns:x="urn:producer" data="nested-begin"><q:fldChar q:fldCharType="begin" q:dirty="on"><x:nestedBegin/></q:fldChar></q:r>"#,
17223                r#"<q:r xmlns:q="{0}" xmlns:x="urn:producer" data="nested-instruction"><q:rPr><q:i/></q:rPr><q:instrText>REF destination</q:instrText><x:nestedInstruction/></q:r>"#,
17224                r#"<q:r xmlns:q="{0}" data="nested-separator"><q:fldChar q:fldCharType="separate"/></q:r>"#,
17225                r#"<q:r xmlns:q="{0}" xmlns:x="urn:producer" data="nested-result"><q:rPr><q:u/></q:rPr><q:t>old</q:t><x:nestedResult/></q:r>"#,
17226                r#"<q:r xmlns:q="{0}" data="nested-end"><q:fldChar q:fldCharType="end"/></q:r>"#,
17227                r#"<q:r xmlns:q="{0}" xmlns:x="urn:producer" data="outer-tail"><q:instrText xml:space="preserve"> = &quot;x&quot; &quot;yes&quot; &quot;no&quot;</q:instrText><x:outerTail/></q:r>"#,
17228                r#"<q:r xmlns:q="{0}" data="outer-separator"><q:fldChar q:fldCharType="separate"/></q:r>"#,
17229                r#"<q:r xmlns:q="{0}" data="outer-result"><q:t>yes</q:t></q:r>"#,
17230                r#"<q:r xmlns:q="{0}" data="outer-end"><q:fldChar q:fldCharType="end"/></q:r>"#,
17231            ),
17232            word_namespace,
17233        ));
17234        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17235            panic!("expected outer field")
17236        };
17237        let Some(FieldArgument::Nested(nested)) = field.instruction.arguments.first_mut() else {
17238            panic!("expected first positional operand to be nested")
17239        };
17240        nested.cached_result = "x".to_owned();
17241        nested.dirty = Some(false);
17242
17243        let output = serialized_paragraph(&paragraph);
17244        for preserved in [
17245            r#"data="outer-begin""#,
17246            r#"<x:outerBegin/>"#,
17247            r#"<q:rPr><q:b/></q:rPr>"#,
17248            r#"<x:outerInstruction/>"#,
17249            r#"data="nested-instruction""#,
17250            r#"<q:rPr><q:i/></q:rPr>"#,
17251            r#"<x:nestedInstruction/>"#,
17252            r#"<q:rPr><q:u/></q:rPr>"#,
17253            r#"<x:nestedResult/>"#,
17254            r#"<x:outerTail/>"#,
17255        ] {
17256            assert!(output.contains(preserved), "missing {preserved}: {output}");
17257        }
17258        assert!(output.contains(r#"w:dirty="0""#), "{output}");
17259        assert!(output.contains(">x</w:t>"), "{output}");
17260        let nested_begin = output.find(r#"data="nested-begin""#).unwrap();
17261        let comparison = output.find(" = &quot;x&quot;").unwrap();
17262        assert!(nested_begin < comparison, "{output}");
17263
17264        let reparsed = parse_paragraph(
17265            output
17266                .strip_prefix("<w:p>")
17267                .and_then(|value| value.strip_suffix("</w:p>"))
17268                .unwrap(),
17269        );
17270        let field = parsed_field(&reparsed, 0);
17271        assert!(matches!(
17272            field.instruction.arguments.as_slice(),
17273            [FieldArgument::Nested(nested), FieldArgument::Text(operator), FieldArgument::Text(right), FieldArgument::Text(yes), FieldArgument::Text(no)]
17274                if nested.cached_result == "x"
17275                    && operator == "="
17276                    && right == "x"
17277                    && yes == "yes"
17278                    && no == "no"
17279        ));
17280    }
17281
17282    #[test]
17283    fn same_instruction_nested_replacements_use_canonical_source_identity() {
17284        let outer_source = concat!(
17285            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17286            r#"<w:r><w:instrText xml:space="preserve">IF </w:instrText></w:r>"#,
17287            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17288            r#"<w:r><w:instrText>REF destination</w:instrText></w:r>"#,
17289            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17290            r#"<w:r><w:t>old nested</w:t></w:r>"#,
17291            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17292            r#"<w:r><w:instrText xml:space="preserve"> = &quot;x&quot; &quot;yes&quot; &quot;no&quot;</w:instrText></w:r>"#,
17293            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17294            r#"<w:r><w:t>old outer</w:t></w:r>"#,
17295            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17296        );
17297        let word_namespace = crate::namespace::W_NS;
17298        let foreign = parse_paragraph(&format!(
17299            r#"<q:fldSimple xmlns:q="{word_namespace}" xmlns:x="urn:foreign" q:instr="REF destination"><q:r><q:t>foreign replacement</q:t><x:foreign/></q:r></q:fldSimple>"#,
17300        ));
17301        let replacements = [
17302            Field::new("REF destination", "new replacement"),
17303            parsed_field(&foreign, 0).clone(),
17304        ];
17305
17306        for (replacement, expected) in replacements
17307            .into_iter()
17308            .zip(["new replacement", "foreign replacement"])
17309        {
17310            let mut paragraph = parse_paragraph(outer_source);
17311            let RunContent::Field(outer) = &mut paragraph.runs[0].content[0] else {
17312                panic!("expected outer field")
17313            };
17314            let Some(FieldArgument::Nested(nested)) = outer.instruction.arguments.first_mut()
17315            else {
17316                panic!("expected nested positional field")
17317            };
17318            **nested = replacement;
17319
17320            let output = serialized_paragraph(&paragraph);
17321            let reparsed = parse_paragraph(
17322                output
17323                    .strip_prefix("<w:p>")
17324                    .and_then(|value| value.strip_suffix("</w:p>"))
17325                    .unwrap(),
17326            );
17327            let field = parsed_field(&reparsed, 0);
17328            let Some(FieldArgument::Nested(nested)) = field.instruction.arguments.first() else {
17329                panic!("replacement field was discarded: {output}")
17330            };
17331            assert_eq!(nested.cached_result, expected, "{output}");
17332        }
17333    }
17334
17335    #[test]
17336    fn nested_updates_skip_opaque_lookalikes_and_map_identical_siblings() {
17337        let word_namespace = crate::namespace::W_NS;
17338        let nested_source = format!(
17339            concat!(
17340                r#"<q:r xmlns:q="{0}"><q:fldChar q:fldCharType="begin"/></q:r>"#,
17341                r#"<q:r xmlns:q="{0}"><q:instrText>REF destination</q:instrText></q:r>"#,
17342                r#"<q:r xmlns:q="{0}"><q:fldChar q:fldCharType="separate"/></q:r>"#,
17343                r#"<q:r xmlns:q="{0}"><q:t>stored nested</q:t></q:r>"#,
17344                r#"<q:r xmlns:q="{0}"><q:fldChar q:fldCharType="end"/></q:r>"#,
17345            ),
17346            word_namespace,
17347        );
17348        let source = format!(
17349            concat!(
17350                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17351                r#"<w:r xmlns:x="urn:producer"><w:instrText xml:space="preserve">IF </w:instrText><x:opaque>{0}</x:opaque></w:r>"#,
17352                "{0}",
17353                r#"<w:r><w:instrText xml:space="preserve"> </w:instrText></w:r>"#,
17354                "{0}",
17355                r#"<w:r><w:instrText xml:space="preserve"> = &quot;x&quot; &quot;yes&quot; &quot;no&quot;</w:instrText></w:r>"#,
17356                r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17357                r#"<w:r><w:t>stored outer</w:t></w:r>"#,
17358                r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17359            ),
17360            nested_source,
17361        );
17362        let mut paragraph = parse_paragraph(&source);
17363        let RunContent::Field(outer) = &mut paragraph.runs[0].content[0] else {
17364            panic!("expected outer field")
17365        };
17366        let mut nested =
17367            outer
17368                .instruction
17369                .arguments
17370                .iter_mut()
17371                .filter_map(|argument| match argument {
17372                    FieldArgument::Nested(field) => Some(field.as_mut()),
17373                    FieldArgument::Text(_) => None,
17374                });
17375        nested.next().unwrap().cached_result = "first typed".to_owned();
17376        nested.next().unwrap().cached_result = "second typed".to_owned();
17377        assert!(nested.next().is_none());
17378
17379        let output = serialized_paragraph(&paragraph);
17380        assert!(
17381            output.contains(&format!("<x:opaque>{nested_source}</x:opaque>")),
17382            "{output}"
17383        );
17384        assert_eq!(output.matches("stored nested").count(), 1, "{output}");
17385        assert_eq!(output.matches("first typed").count(), 1, "{output}");
17386        assert_eq!(output.matches("second typed").count(), 1, "{output}");
17387        assert!(
17388            output.find("stored nested").unwrap() < output.find("first typed").unwrap()
17389                && output.find("first typed").unwrap() < output.find("second typed").unwrap(),
17390            "{output}"
17391        );
17392    }
17393
17394    #[test]
17395    fn same_run_nested_siblings_have_distinct_update_spans() {
17396        let word_namespace = crate::namespace::W_NS;
17397        let nested_source = concat!(
17398            r#"<q:fldChar q:fldCharType="begin"/>"#,
17399            r#"<q:instrText>MERGEFIELD Same</q:instrText>"#,
17400            r#"<q:fldChar q:fldCharType="separate"/>"#,
17401            r#"<q:t>stored sibling</q:t>"#,
17402            r#"<q:fldChar q:fldCharType="end"/>"#,
17403        );
17404        let source = format!(
17405            concat!(
17406                r#"<q:r xmlns:q="{0}" xmlns:x="urn:producer"><x:before/>"#,
17407                r#"<q:fldChar q:fldCharType="begin"/><q:instrText xml:space="preserve">IF </q:instrText>"#,
17408                "{1}",
17409                r#"<q:instrText xml:space="preserve"> </q:instrText>"#,
17410                "{1}",
17411                r#"<q:instrText xml:space="preserve"> = &quot;x&quot; &quot;yes&quot; &quot;no&quot;</q:instrText>"#,
17412                r#"<q:fldChar q:fldCharType="separate"/><q:t>stored outer</q:t>"#,
17413                r#"<q:fldChar q:fldCharType="end"/><x:after/></q:r>"#,
17414            ),
17415            word_namespace, nested_source,
17416        );
17417        let mut paragraph = parse_paragraph(&source);
17418        let RunContent::Field(outer) = &mut paragraph.runs[0].content[0] else {
17419            panic!("expected outer field")
17420        };
17421        let mut nested =
17422            outer
17423                .instruction
17424                .arguments
17425                .iter_mut()
17426                .filter_map(|argument| match argument {
17427                    FieldArgument::Nested(field) => Some(field.as_mut()),
17428                    FieldArgument::Text(_) => None,
17429                });
17430        nested.next().unwrap().cached_result = "first same run".to_owned();
17431        nested.next().unwrap().cached_result = "second same run".to_owned();
17432        assert!(nested.next().is_none());
17433
17434        let output = serialized_paragraph(&paragraph);
17435        assert_eq!(output.matches("first same run").count(), 1, "{output}");
17436        assert_eq!(output.matches("second same run").count(), 1, "{output}");
17437        assert!(!output.contains("stored sibling"), "{output}");
17438        assert!(output.contains("<x:before/>"), "{output}");
17439        assert!(output.contains("<x:after/>"), "{output}");
17440        assert!(
17441            output.find("first same run").unwrap() < output.find("second same run").unwrap(),
17442            "{output}"
17443        );
17444    }
17445
17446    #[test]
17447    fn shared_boundary_run_nested_siblings_have_non_overlapping_spans() {
17448        let word_namespace = crate::namespace::W_NS;
17449        let source = format!(
17450            concat!(
17451                r#"<q:r xmlns:q="{0}"><q:fldChar q:fldCharType="begin"/><q:instrText xml:space="preserve">IF </q:instrText></q:r>"#,
17452                r#"<q:r xmlns:q="{0}"><q:fldChar q:fldCharType="begin"/><q:instrText>MERGEFIELD First</q:instrText></q:r>"#,
17453                r#"<q:r xmlns:q="{0}" xmlns:x="urn:producer"><q:fldChar q:fldCharType="separate"/><q:t>stored first</q:t><q:fldChar q:fldCharType="end"/><x:between/><q:instrText xml:space="preserve"> = </q:instrText><q:fldChar q:fldCharType="begin"/></q:r>"#,
17454                r#"<q:r xmlns:q="{0}"><q:instrText>MERGEFIELD Second</q:instrText><q:fldChar q:fldCharType="separate"/><q:t>stored second</q:t><q:fldChar q:fldCharType="end"/><q:instrText xml:space="preserve"> &quot;yes&quot; &quot;no&quot;</q:instrText></q:r>"#,
17455                r#"<q:r xmlns:q="{0}"><q:fldChar q:fldCharType="separate"/><q:t>stored outer</q:t><q:fldChar q:fldCharType="end"/></q:r>"#,
17456            ),
17457            word_namespace,
17458        );
17459        let mut paragraph = parse_paragraph(&source);
17460        let RunContent::Field(outer) = &mut paragraph.runs[0].content[0] else {
17461            panic!("expected outer field")
17462        };
17463        let mut nested =
17464            outer
17465                .instruction
17466                .arguments
17467                .iter_mut()
17468                .filter_map(|argument| match argument {
17469                    FieldArgument::Nested(field) => Some(field.as_mut()),
17470                    FieldArgument::Text(_) => None,
17471                });
17472        nested.next().unwrap().cached_result = "updated first".to_owned();
17473        nested.next().unwrap().cached_result = "updated second".to_owned();
17474        assert!(nested.next().is_none());
17475
17476        let output = serialized_paragraph(&paragraph);
17477        assert_eq!(output.matches("updated first").count(), 1, "{output}");
17478        assert_eq!(output.matches("updated second").count(), 1, "{output}");
17479        assert!(!output.contains("stored first"), "{output}");
17480        assert!(!output.contains("stored second"), "{output}");
17481        assert!(output.contains("<x:between/>"), "{output}");
17482        assert!(
17483            output.find("updated first").unwrap() < output.find("<x:between/>").unwrap()
17484                && output.find("<x:between/>").unwrap() < output.find("updated second").unwrap(),
17485            "{output}"
17486        );
17487    }
17488
17489    #[test]
17490    fn isolated_same_run_nested_field_applies_its_raw_instruction_edit() {
17491        let word_namespace = crate::namespace::W_NS;
17492        let source = format!(
17493            concat!(
17494                r#"<q:r xmlns:q="{0}" xmlns:x="urn:producer"><q:fldChar q:fldCharType="begin"/><q:instrText xml:space="preserve">IF </q:instrText>"#,
17495                r#"<q:fldChar q:fldCharType="begin" q:dirty="on"/><q:instrText>MERGEFIELD Old</q:instrText><x:nestedInstruction/><q:fldChar q:fldCharType="separate"/><q:t>stored nested</q:t><q:fldChar q:fldCharType="end"/>"#,
17496                r#"<q:instrText xml:space="preserve"> = &quot;new value&quot; &quot;yes&quot; &quot;no&quot;</q:instrText><q:fldChar q:fldCharType="separate"/><q:t>stored outer</q:t><q:fldChar q:fldCharType="end"/></q:r>"#,
17497            ),
17498            word_namespace,
17499        );
17500        let mut paragraph = parse_paragraph(&source);
17501        let RunContent::Field(outer) = &mut paragraph.runs[0].content[0] else {
17502            panic!("expected outer field")
17503        };
17504        let Some(FieldArgument::Nested(nested)) = outer.instruction.arguments.first_mut() else {
17505            panic!("expected nested field")
17506        };
17507        nested.instruction.raw = "MERGEFIELD New".to_owned();
17508        nested.cached_result = "new value".to_owned();
17509        nested.dirty = Some(false);
17510
17511        let output = serialized_paragraph(&paragraph);
17512        assert!(output.contains("MERGEFIELD New"), "{output}");
17513        assert!(!output.contains("MERGEFIELD Old"), "{output}");
17514        assert!(output.contains("new value"), "{output}");
17515        assert!(output.contains(r#"w:dirty="0""#), "{output}");
17516        assert!(output.contains("<x:nestedInstruction/>"), "{output}");
17517
17518        let reparsed = parse_paragraph(
17519            output
17520                .strip_prefix("<w:p>")
17521                .and_then(|value| value.strip_suffix("</w:p>"))
17522                .unwrap(),
17523        );
17524        let outer = parsed_field(&reparsed, 0);
17525        let Some(FieldArgument::Nested(nested)) = outer.instruction.arguments.first() else {
17526            panic!("expected reopened nested field")
17527        };
17528        assert_eq!(nested.instruction.name, "MERGEFIELD");
17529        assert!(matches!(
17530            nested.instruction.arguments.first(),
17531            Some(FieldArgument::Text(value)) if value == "New"
17532        ));
17533        assert_eq!(nested.cached_result, "new value");
17534        assert_eq!(nested.dirty, Some(false));
17535    }
17536
17537    #[test]
17538    fn complex_source_retains_comments_and_processing_instructions() {
17539        let source = concat!(
17540            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17541            "<!-- before instruction -->",
17542            r#"<w:r><w:instrText>MERGEFIELD Producer</w:instrText></w:r>"#,
17543            "<?producer before-separator?>",
17544            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17545            "<!-- before result -->",
17546            r#"<w:r><w:t>stored producer</w:t></w:r>"#,
17547            "<?producer before-end?>",
17548            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17549        );
17550        let mut paragraph = parse_paragraph(source);
17551        let field = parsed_field(&paragraph, 0);
17552        let (captured, _) = field.source_replacement().unwrap().unwrap();
17553        assert_eq!(captured, source.as_bytes());
17554
17555        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17556            panic!("expected field")
17557        };
17558        field.cached_result = "updated producer".to_owned();
17559        field.dirty = Some(false);
17560        let output = serialized_paragraph(&paragraph);
17561        for preserved in [
17562            "<!-- before instruction -->",
17563            "<?producer before-separator?>",
17564            "<!-- before result -->",
17565            "<?producer before-end?>",
17566        ] {
17567            assert!(output.contains(preserved), "missing {preserved}: {output}");
17568        }
17569        assert!(output.contains("updated producer"), "{output}");
17570    }
17571
17572    #[test]
17573    fn pretty_printed_complex_source_retains_inter_run_whitespace() {
17574        let source = concat!(
17575            "\n    ",
17576            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17577            "\n    ",
17578            r#"<w:r><w:instrText>MERGEFIELD Pretty</w:instrText></w:r>"#,
17579            "\n    ",
17580            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17581            "\n    ",
17582            r#"<w:r><w:t>stored pretty</w:t></w:r>"#,
17583            "\n    ",
17584            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17585            "\n  ",
17586        );
17587        let paragraph = parse_paragraph(source);
17588        let field = parsed_field(&paragraph, 0);
17589        let (captured, _) = field.source_replacement().unwrap().unwrap();
17590        assert!(
17591            captured
17592                .windows(b"</w:r>\n    <w:r>".len())
17593                .any(|window| window == b"</w:r>\n    <w:r>"),
17594            "{}",
17595            String::from_utf8_lossy(captured)
17596        );
17597    }
17598
17599    #[test]
17600    fn simple_and_complex_dirty_flags_preserve_aliases_and_unmodelled_content() {
17601        let word_namespace = crate::namespace::W_NS;
17602        let mut simple = parse_paragraph(&format!(
17603            r#"<q:fldSimple xmlns:q="{word_namespace}" xmlns:x="urn:producer" q:instr="REF destination" q:dirty="on" x:token="simple"><x:before/><q:r><q:t>old</q:t><x:inside/></q:r><x:after/></q:fldSimple>"#,
17604        ));
17605        let RunContent::Field(field) = &mut simple.runs[0].content[0] else {
17606            panic!("expected simple field")
17607        };
17608        assert_eq!(field.dirty, Some(true));
17609        field.cached_result = "new".to_owned();
17610        field.dirty = Some(false);
17611        let output = serialized_paragraph(&simple);
17612        assert!(output.contains(r#"<w:fldSimple xmlns:q="#,), "{output}");
17613        assert!(output.contains(r#"w:dirty="0""#), "{output}");
17614        assert!(output.contains(r#"x:token="simple""#), "{output}");
17615        assert!(output.contains("<x:before/>"), "{output}");
17616        assert!(output.contains("<x:inside/>"), "{output}");
17617        assert!(output.contains("<x:after/>"), "{output}");
17618
17619        let mut complex = parse_paragraph(&format!(
17620            concat!(
17621                r#"<q:r xmlns:q="{}" xmlns:x="urn:producer"><q:fldChar q:fldCharType="begin" q:dirty="off"><x:begin/></q:fldChar></q:r>"#,
17622                r#"<q:r xmlns:q="{}" xmlns:x="urn:producer"><q:instrText>DATE</q:instrText><x:instruction/></q:r>"#,
17623                r#"<x:between xmlns:x="urn:producer"/>"#,
17624                r#"<q:r xmlns:q="{}"><q:fldChar q:fldCharType="separate" q:dirty="off"/></q:r>"#,
17625                r#"<q:r xmlns:q="{}" xmlns:x="urn:producer"><q:t>old</q:t><x:result/></q:r>"#,
17626                r#"<q:r xmlns:q="{}"><q:fldChar q:fldCharType="end" q:dirty="off"/></q:r>"#,
17627            ),
17628            word_namespace, word_namespace, word_namespace, word_namespace, word_namespace,
17629        ));
17630        let RunContent::Field(field) = &mut complex.runs[0].content[0] else {
17631            panic!("expected complex field")
17632        };
17633        assert_eq!(field.dirty, Some(false));
17634        field.cached_result = "new".to_owned();
17635        field.dirty = Some(true);
17636        let output = serialized_paragraph(&complex);
17637        assert!(
17638            output.contains(r#"<w:fldChar w:fldCharType="begin" w:dirty="1">"#),
17639            "{output}"
17640        );
17641        assert_eq!(output.matches(r#"w:dirty="1""#).count(), 1, "{output}");
17642        assert!(output.contains("<x:begin/>"), "{output}");
17643        assert!(output.contains("<x:instruction/>"), "{output}");
17644        assert!(output.contains("<x:between"), "{output}");
17645        assert!(output.contains("<x:result/>"), "{output}");
17646    }
17647
17648    #[test]
17649    fn complex_field_projection_keeps_result_formatting_and_controls() {
17650        let paragraph = parse_paragraph(concat!(
17651            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17652            r#"<w:r><w:instrText>DATE</w:instrText></w:r>"#,
17653            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17654            r#"<w:r><w:rPr><w:b/></w:rPr><w:t>one</w:t><w:tab/><w:t>two</w:t><w:br/><w:t>three</w:t><w:br w:type="page"/><w:t>four</w:t><w:br w:type="column"/></w:r>"#,
17655            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17656        ));
17657
17658        let field = parsed_field(&paragraph, 0);
17659        assert_eq!(field.cached_result, "one\ttwo\nthree\u{000c}four\u{000b}");
17660        let segments = field.cached_display_segments();
17661        assert_eq!(segments.len(), 1);
17662        assert_eq!(segments[0].1.unwrap().bold, Some(true));
17663    }
17664
17665    #[test]
17666    fn complex_field_projection_keeps_each_result_runs_formatting() {
17667        let paragraph = parse_paragraph(concat!(
17668            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17669            r#"<w:r><w:instrText>DATE</w:instrText></w:r>"#,
17670            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17671            r#"<w:r><w:rPr><w:b/></w:rPr><w:t>bold</w:t></w:r>"#,
17672            r#"<w:r><w:rPr><w:i/></w:rPr><w:t>italic</w:t></w:r>"#,
17673            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17674        ));
17675
17676        let segments = parsed_field(&paragraph, 0).cached_display_segments();
17677        assert_eq!(segments.len(), 2);
17678        assert_eq!(segments[0].0, "bold");
17679        assert_eq!(segments[0].1.unwrap().bold, Some(true));
17680        assert_eq!(segments[1].0, "italic");
17681        assert_eq!(segments[1].1.unwrap().italic, Some(true));
17682    }
17683
17684    #[test]
17685    fn edited_complex_cache_keeps_the_first_result_runs_formatting() {
17686        let mut paragraph = parse_paragraph(concat!(
17687            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17688            r#"<w:r><w:instrText>DATE</w:instrText></w:r>"#,
17689            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17690            r#"<w:r><w:rPr><w:b/><w:i/></w:rPr><w:t>old</w:t></w:r>"#,
17691            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17692        ));
17693        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17694            panic!("expected field")
17695        };
17696        field.cached_result = "fresh".to_owned();
17697
17698        let segments = field.cached_display_segments();
17699        assert_eq!(segments.len(), 1);
17700        assert_eq!(segments[0].0, "fresh");
17701        assert_eq!(segments[0].1.unwrap().bold, Some(true));
17702        assert_eq!(segments[0].1.unwrap().italic, Some(true));
17703    }
17704
17705    #[test]
17706    fn complex_field_mutation_clears_non_begin_dirty_and_adds_missing_result_text() {
17707        let mut paragraph = parse_paragraph(concat!(
17708            r#"<w:r><w:fldChar w:fldCharType="begin" w:dirty="1"/></w:r>"#,
17709            r#"<w:r><w:instrText>DATE</w:instrText></w:r>"#,
17710            r#"<w:r><w:fldChar w:fldCharType="separate" w:dirty="1"/></w:r>"#,
17711            r#"<w:r><w:producerResult/></w:r>"#,
17712            r#"<w:r><w:fldChar w:fldCharType="end" w:dirty="1"/></w:r>"#,
17713        ));
17714        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17715            panic!("expected field")
17716        };
17717        field.cached_result = "fresh".to_owned();
17718        field.dirty = Some(false);
17719
17720        let output = serialized_paragraph(&paragraph);
17721        assert_eq!(output.matches("w:dirty=").count(), 1, "{output}");
17722        assert!(output.contains(r#"w:dirty="0""#), "{output}");
17723        assert!(output.contains("<w:t>fresh</w:t>"), "{output}");
17724        assert!(output.contains("<w:producerResult/>"), "{output}");
17725
17726        let reparsed = parse_paragraph(
17727            output
17728                .strip_prefix("<w:p>")
17729                .and_then(|value| value.strip_suffix("</w:p>"))
17730                .unwrap(),
17731        );
17732        let field = parsed_field(&reparsed, 0);
17733        assert_eq!(field.cached_result, "fresh");
17734        assert_eq!(field.dirty, Some(false));
17735    }
17736
17737    #[test]
17738    fn dirty_only_complex_rewrite_does_not_duplicate_result_controls() {
17739        let mut paragraph = parse_paragraph(concat!(
17740            r#"<w:r><w:fldChar w:fldCharType="begin" w:dirty="1"/></w:r>"#,
17741            r#"<w:r><w:instrText>DATE</w:instrText></w:r>"#,
17742            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17743            r#"<w:r><w:t>one</w:t><w:tab/><w:t>two</w:t><w:br/><w:t>three</w:t><w:br w:type="page"/><w:t>four</w:t><w:br w:type="column"/></w:r>"#,
17744            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17745        ));
17746        let expected = parsed_field(&paragraph, 0).cached_result.clone();
17747        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17748            panic!("expected field")
17749        };
17750        field.dirty = Some(false);
17751
17752        let output = serialized_paragraph(&paragraph);
17753        let reparsed = parse_paragraph(
17754            output
17755                .strip_prefix("<w:p>")
17756                .and_then(|value| value.strip_suffix("</w:p>"))
17757                .unwrap(),
17758        );
17759        assert_eq!(parsed_field(&reparsed, 0).cached_result, expected);
17760    }
17761
17762    #[test]
17763    fn simple_field_cache_mutation_replaces_old_result_controls() {
17764        let mut paragraph = parse_paragraph(
17765            r#"<w:fldSimple w:instr="DATE"><w:r><w:t>one</w:t><w:tab/><w:t>two</w:t><w:br/><w:t>three</w:t><w:br w:type="page"/><w:t>four</w:t><w:br w:type="column"/></w:r></w:fldSimple>"#,
17766        );
17767        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17768            panic!("expected field")
17769        };
17770        field.cached_result = "fresh".to_owned();
17771
17772        let output = serialized_paragraph(&paragraph);
17773        let reparsed = parse_paragraph(
17774            output
17775                .strip_prefix("<w:p>")
17776                .and_then(|value| value.strip_suffix("</w:p>"))
17777                .unwrap(),
17778        );
17779        assert_eq!(parsed_field(&reparsed, 0).cached_result, "fresh");
17780        assert!(!output.contains("<w:tab"), "{output}");
17781        assert!(!output.contains("<w:br"), "{output}");
17782    }
17783
17784    #[test]
17785    fn simple_control_only_cache_mutation_keeps_result_run_formatting() {
17786        let mut paragraph = parse_paragraph(
17787            r#"<w:fldSimple w:instr="DATE"><w:r><w:rPr><w:b/><w:i/></w:rPr><w:tab/></w:r></w:fldSimple>"#,
17788        );
17789        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17790            panic!("expected field")
17791        };
17792        field.cached_result = "fresh".to_owned();
17793
17794        let output = serialized_paragraph(&paragraph);
17795        let reparsed = parse_paragraph(
17796            output
17797                .strip_prefix("<w:p>")
17798                .and_then(|value| value.strip_suffix("</w:p>"))
17799                .unwrap(),
17800        );
17801        let field = parsed_field(&reparsed, 0);
17802        assert_eq!(field.cached_result, "fresh");
17803        let segments = field.cached_display_segments();
17804        assert_eq!(segments.len(), 1);
17805        assert_eq!(segments[0].1.unwrap().bold, Some(true));
17806        assert_eq!(segments[0].1.unwrap().italic, Some(true));
17807        assert_eq!(output.matches("<w:r>").count(), 1, "{output}");
17808    }
17809
17810    #[test]
17811    fn simple_cache_mutation_skips_empty_leading_result_runs_for_formatting() {
17812        let mut paragraph = parse_paragraph(concat!(
17813            r#"<w:fldSimple w:instr="DATE">"#,
17814            r#"<w:r><w:rPr><w:b/></w:rPr></w:r>"#,
17815            r#"<w:r><w:rPr><w:i/></w:rPr><w:t>old</w:t></w:r>"#,
17816            r#"</w:fldSimple>"#,
17817        ));
17818        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17819            panic!("expected field")
17820        };
17821        field.cached_result = "fresh".to_owned();
17822        let in_memory = field.cached_display_segments();
17823        assert_eq!(in_memory[0].1.unwrap().bold, None);
17824        assert_eq!(in_memory[0].1.unwrap().italic, Some(true));
17825
17826        let output = serialized_paragraph(&paragraph);
17827        let reparsed = parse_paragraph(
17828            output
17829                .strip_prefix("<w:p>")
17830                .and_then(|value| value.strip_suffix("</w:p>"))
17831                .unwrap(),
17832        );
17833        let reopened = parsed_field(&reparsed, 0).cached_display_segments();
17834        assert_eq!(reopened[0].0, "fresh");
17835        assert_eq!(reopened[0].1.unwrap().bold, None);
17836        assert_eq!(reopened[0].1.unwrap().italic, Some(true));
17837    }
17838
17839    #[test]
17840    fn simple_cache_mutation_uses_an_empty_text_runs_formatting() {
17841        let mut paragraph = parse_paragraph(concat!(
17842            r#"<w:fldSimple w:instr="DATE">"#,
17843            r#"<w:r><w:rPr><w:b/></w:rPr><w:t/></w:r>"#,
17844            r#"<w:r><w:rPr><w:i/></w:rPr><w:t>old</w:t></w:r>"#,
17845            r#"</w:fldSimple>"#,
17846        ));
17847        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17848            panic!("expected field")
17849        };
17850        field.cached_result = "fresh".to_owned();
17851        let in_memory = field.cached_display_segments();
17852        assert_eq!(in_memory[0].1.unwrap().bold, Some(true));
17853        assert_eq!(in_memory[0].1.unwrap().italic, None);
17854
17855        let output = serialized_paragraph(&paragraph);
17856        let reparsed = parse_paragraph(
17857            output
17858                .strip_prefix("<w:p>")
17859                .and_then(|value| value.strip_suffix("</w:p>"))
17860                .unwrap(),
17861        );
17862        let reopened = parsed_field(&reparsed, 0).cached_display_segments();
17863        assert_eq!(reopened[0].0, "fresh");
17864        assert_eq!(reopened[0].1.unwrap().bold, Some(true));
17865        assert_eq!(reopened[0].1.unwrap().italic, None);
17866    }
17867
17868    #[test]
17869    fn expanded_simple_result_controls_contribute_to_the_cached_display() {
17870        let paragraph = parse_paragraph(
17871            r#"<w:fldSimple w:instr="DATE"><w:r><w:t>one</w:t><w:tab></w:tab><w:t>two</w:t><w:br></w:br><w:t>three</w:t><w:br w:type="page"></w:br><w:t>four</w:t><w:br w:type="column"></w:br></w:r></w:fldSimple>"#,
17872        );
17873        assert_eq!(
17874            parsed_field(&paragraph, 0).cached_result,
17875            "one\ttwo\nthree\u{000c}four\u{000b}"
17876        );
17877    }
17878
17879    #[test]
17880    fn cache_mutation_replaces_a_nested_only_complex_result() {
17881        let mut paragraph = parse_paragraph(concat!(
17882            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17883            r#"<w:r><w:instrText>IF 1 = 1 yes no</w:instrText></w:r>"#,
17884            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17885            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17886            r#"<w:r><w:instrText>REF destination</w:instrText></w:r>"#,
17887            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17888            r#"<w:r><w:t>old nested result</w:t></w:r>"#,
17889            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17890            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17891        ));
17892        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17893            panic!("expected field")
17894        };
17895        field.cached_result = "new result".to_owned();
17896
17897        let output = serialized_paragraph(&paragraph);
17898        assert!(!output.contains("old nested result"), "{output}");
17899        let reparsed = parse_paragraph(
17900            output
17901                .strip_prefix("<w:p>")
17902                .and_then(|value| value.strip_suffix("</w:p>"))
17903                .unwrap(),
17904        );
17905        assert_eq!(parsed_field(&reparsed, 0).cached_result, "new result");
17906    }
17907
17908    #[test]
17909    fn canonical_instruction_preserves_empty_and_backslash_leading_operands() {
17910        let mut paragraph = parse_paragraph(
17911            r#"<w:fldSimple w:instr="INCLUDETEXT old"><w:r><w:t>cached</w:t></w:r></w:fldSimple>"#,
17912        );
17913        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17914            panic!("expected field")
17915        };
17916        field.instruction.arguments = vec![
17917            FieldArgument::Text(String::new()),
17918            FieldArgument::Text(r#"\\server\share\file.docx"#.to_owned()),
17919        ];
17920
17921        let output = serialized_paragraph(&paragraph);
17922        let reparsed = parse_paragraph(
17923            output
17924                .strip_prefix("<w:p>")
17925                .and_then(|value| value.strip_suffix("</w:p>"))
17926                .unwrap(),
17927        );
17928        assert_eq!(
17929            parsed_field(&reparsed, 0).instruction.arguments,
17930            vec![
17931                FieldArgument::Text(String::new()),
17932                FieldArgument::Text(r#"\\server\share\file.docx"#.to_owned()),
17933            ]
17934        );
17935    }
17936
17937    #[test]
17938    fn complex_field_inside_explicit_hyperlink_is_projected() {
17939        let paragraph = parse_paragraph(concat!(
17940            r#"<w:hyperlink w:anchor="destination">"#,
17941            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17942            r#"<w:r><w:instrText>REF destination</w:instrText></w:r>"#,
17943            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17944            r#"<w:r><w:t>cached</w:t></w:r>"#,
17945            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17946            r#"</w:hyperlink>"#,
17947        ));
17948
17949        assert_eq!(paragraph.runs.len(), 1);
17950        assert_eq!(parsed_field(&paragraph, 0).instruction.name, "REF");
17951        assert_eq!(paragraph.hyperlinks[0].run_start, 0);
17952        assert_eq!(paragraph.hyperlinks[0].run_end, 1);
17953    }
17954
17955    #[test]
17956    fn hyperlink_local_raw_children_stay_inside_a_projected_field_boundary() {
17957        let paragraph = parse_paragraph(concat!(
17958            r#"<w:hyperlink w:anchor="destination">"#,
17959            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17960            r#"<x:producer xmlns:x="urn:producer"/>"#,
17961            r#"<w:r><w:instrText>REF destination</w:instrText></w:r>"#,
17962            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17963            r#"<w:r><w:t>cached</w:t></w:r>"#,
17964            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17965            r#"</w:hyperlink>"#,
17966        ));
17967
17968        assert_eq!(paragraph.runs.len(), 1);
17969        let output = serialized_paragraph(&paragraph);
17970        assert_eq!(output.matches("<x:producer").count(), 1, "{output}");
17971        assert!(
17972            output.find("fldCharType=\"begin\"").unwrap() < output.find("<x:producer").unwrap(),
17973            "{output}"
17974        );
17975        assert!(
17976            output.find("<x:producer").unwrap() < output.find("<w:instrText").unwrap(),
17977            "{output}"
17978        );
17979    }
17980
17981    #[test]
17982    fn hyperlink_complex_source_retains_inter_run_trivia() {
17983        let mut paragraph = parse_paragraph(concat!(
17984            r#"<w:hyperlink w:anchor="destination">"#,
17985            "\n  ",
17986            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
17987            "\n  <!-- hyperlink-before-instruction -->",
17988            r#"<w:r><w:instrText>REF destination</w:instrText></w:r>"#,
17989            "<?producer hyperlink-before-separator?>",
17990            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
17991            "\n  <!-- hyperlink-before-result -->",
17992            r#"<w:r><w:t>cached</w:t></w:r>"#,
17993            "<?producer hyperlink-before-end?>",
17994            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
17995            "\n",
17996            r#"</w:hyperlink>"#,
17997        ));
17998        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
17999            panic!("expected hyperlink field")
18000        };
18001        let (captured, _) = field.source_replacement().unwrap().unwrap();
18002        for preserved in [
18003            "\n  <!-- hyperlink-before-instruction -->",
18004            "<?producer hyperlink-before-separator?>",
18005            "\n  <!-- hyperlink-before-result -->",
18006            "<?producer hyperlink-before-end?>",
18007        ] {
18008            assert!(
18009                String::from_utf8_lossy(captured).contains(preserved),
18010                "missing {preserved}: {}",
18011                String::from_utf8_lossy(captured)
18012            );
18013        }
18014
18015        field.cached_result = "updated hyperlink".to_owned();
18016        field.dirty = Some(false);
18017        let output = serialized_paragraph(&paragraph);
18018        for preserved in [
18019            "<!-- hyperlink-before-instruction -->",
18020            "<?producer hyperlink-before-separator?>",
18021            "<!-- hyperlink-before-result -->",
18022            "<?producer hyperlink-before-end?>",
18023        ] {
18024            assert!(output.contains(preserved), "missing {preserved}: {output}");
18025        }
18026        assert!(output.contains("updated hyperlink"), "{output}");
18027        assert!(output.contains("<w:hyperlink"), "{output}");
18028    }
18029
18030    #[test]
18031    fn cache_mutation_marks_significant_result_whitespace() {
18032        for source in [
18033            r#"<w:fldSimple w:instr="DATE"><w:r><w:t>old</w:t></w:r></w:fldSimple>"#,
18034            concat!(
18035                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
18036                r#"<w:r><w:instrText>DATE</w:instrText></w:r>"#,
18037                r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18038                r#"<w:r><w:t>old</w:t></w:r>"#,
18039                r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
18040            ),
18041        ] {
18042            let mut paragraph = parse_paragraph(source);
18043            let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
18044                panic!("expected field")
18045            };
18046            field.cached_result = " value ".to_owned();
18047            let output = serialized_paragraph(&paragraph);
18048            assert!(
18049                output.contains(r#"<w:t xml:space="preserve"> value </w:t>"#),
18050                "{output}"
18051            );
18052        }
18053    }
18054
18055    #[test]
18056    fn nested_structured_edit_converts_a_simple_source_to_complex_form() {
18057        let mut paragraph = parse_paragraph(
18058            r#"<w:fldSimple w:instr="IF 1 = 1 yes no"><w:r><w:t>yes</w:t></w:r></w:fldSimple>"#,
18059        );
18060        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
18061            panic!("expected field")
18062        };
18063        field.instruction.arguments = vec![
18064            FieldArgument::Nested(Box::new(Field::new("REF destination", "cached"))),
18065            FieldArgument::Text("=".to_owned()),
18066            FieldArgument::Text("cached".to_owned()),
18067            FieldArgument::Text("yes".to_owned()),
18068            FieldArgument::Text("no".to_owned()),
18069        ];
18070
18071        let output = serialized_paragraph(&paragraph);
18072        assert!(!output.starts_with("<w:p><w:fldSimple"), "{output}");
18073        assert!(output.contains("REF destination"), "{output}");
18074        let reparsed = parse_paragraph(
18075            output
18076                .strip_prefix("<w:p>")
18077                .and_then(|value| value.strip_suffix("</w:p>"))
18078                .unwrap(),
18079        );
18080        assert!(matches!(
18081            parsed_field(&reparsed, 0).instruction.arguments.first(),
18082            Some(FieldArgument::Nested(nested)) if nested.instruction.name == "REF"
18083        ));
18084    }
18085
18086    #[test]
18087    fn malformed_nested_result_field_invalidates_the_outer_field() {
18088        let source = concat!(
18089            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
18090            r#"<w:r><w:instrText>IF 1 = 1 yes no</w:instrText></w:r>"#,
18091            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18092            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
18093            r#"<w:r><w:instrText>REF destination</w:instrText></w:r>"#,
18094            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18095            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18096            r#"<w:r><w:t>nested</w:t></w:r>"#,
18097            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
18098            r#"<w:r><w:t>outer</w:t></w:r>"#,
18099            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
18100        );
18101        let paragraph = parse_paragraph(source);
18102
18103        assert!(!paragraph.runs.iter().any(|run| {
18104            run.content
18105                .iter()
18106                .any(|content| matches!(content, RunContent::Field(_)))
18107        }));
18108        assert!(serialized_paragraph(&paragraph).contains(source));
18109    }
18110
18111    #[test]
18112    fn public_instruction_edits_serialize_consistently_for_both_forms() {
18113        let sources = [
18114            r#"<w:fldSimple w:instr="DATE"><w:r><w:t>cached</w:t></w:r></w:fldSimple>"#,
18115            concat!(
18116                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
18117                r#"<w:r><w:instrText>DATE</w:instrText></w:r>"#,
18118                r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18119                r#"<w:r><w:t>cached</w:t></w:r>"#,
18120                r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
18121            ),
18122        ];
18123
18124        for source in sources {
18125            let mut raw_edit = parse_paragraph(source);
18126            let RunContent::Field(field) = &mut raw_edit.runs[0].content[0] else {
18127                panic!("expected field")
18128            };
18129            field.instruction.raw = r"TIME \@ HH:mm".to_owned();
18130            let output = serialized_paragraph(&raw_edit);
18131            assert!(output.contains("TIME"), "raw edit absent: {output}");
18132            assert!(
18133                !output.contains(">DATE<") && !output.contains("&quot;DATE&quot;"),
18134                "{output}"
18135            );
18136
18137            let mut structured_edit = parse_paragraph(source);
18138            let RunContent::Field(field) = &mut structured_edit.runs[0].content[0] else {
18139                panic!("expected field")
18140            };
18141            field.instruction.name = "TIME".to_owned();
18142            field.instruction.arguments.clear();
18143            field.instruction.switches = vec![FieldSwitch {
18144                name: "@".to_owned(),
18145                argument: Some(FieldArgument::Text("HH:mm".to_owned())),
18146            }];
18147            let output = serialized_paragraph(&structured_edit);
18148            assert!(output.contains("TIME"), "structured edit absent: {output}");
18149            assert!(
18150                !output.contains(">DATE<") && !output.contains("&quot;DATE&quot;"),
18151                "{output}"
18152            );
18153        }
18154    }
18155
18156    #[test]
18157    fn malformed_complex_fields_remain_untyped_and_preserved() {
18158        for source in [
18159            concat!(
18160                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
18161                r#"<w:r><w:instrText>REF destination</w:instrText></w:r>"#,
18162                r#"<w:r><w:t>literal</w:t></w:r>"#,
18163            ),
18164            concat!(
18165                r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18166                r#"<w:r><w:t>literal</w:t></w:r>"#,
18167                r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
18168            ),
18169        ] {
18170            let paragraph = parse_paragraph(source);
18171            assert!(!paragraph.runs.iter().any(|run| {
18172                run.content
18173                    .iter()
18174                    .any(|content| matches!(content, RunContent::Field(_)))
18175            }));
18176            assert_eq!(paragraph.text(), "literal");
18177
18178            let mut output = Vec::new();
18179            paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
18180            let output = String::from_utf8(output).unwrap();
18181            assert!(output.contains(source), "{output}");
18182        }
18183    }
18184
18185    #[test]
18186    fn unchanged_complex_fields_keep_source_runs_and_unmodelled_neighbours() {
18187        let source = concat!(
18188            r#"<w:customBefore data="left"/>"#,
18189            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
18190            r#"<w:r><w:rPr><w:b/></w:rPr><w:instrText xml:space="preserve"> DATE </w:instrText><w:producer/></w:r>"#,
18191            r#"<w:proofErr w:type="spellStart"/>"#,
18192            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18193            r#"<w:r><w:rPr><w:i/></w:rPr><w:t>17 August 2026</w:t></w:r>"#,
18194            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
18195            r#"<w:customAfter data="right"/>"#,
18196        );
18197        let mut paragraph = parse_paragraph(source);
18198        assert_eq!(paragraph.runs.len(), 1);
18199        assert_eq!(parsed_field(&paragraph, 0).instruction.name, "DATE");
18200
18201        let mut output = Vec::new();
18202        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
18203        assert_eq!(
18204            String::from_utf8(output).unwrap(),
18205            format!("<w:p>{source}</w:p>")
18206        );
18207
18208        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
18209            panic!("expected complex field")
18210        };
18211        field.cached_result = "18 August 2026".to_owned();
18212        field.dirty = Some(false);
18213        let mut output = Vec::new();
18214        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
18215        let output = String::from_utf8(output).unwrap();
18216        let begin = output.find(r#"w:fldCharType="begin""#).unwrap();
18217        let instruction = output.find("<w:instrText").unwrap();
18218        let separate = output.find(r#"w:fldCharType="separate""#).unwrap();
18219        let result = output.find("<w:t>18 August 2026</w:t>").unwrap();
18220        let end = output.find(r#"w:fldCharType="end""#).unwrap();
18221        assert!(begin < instruction && instruction < separate && separate < result && result < end);
18222
18223        let word_namespace = crate::namespace::W_NS;
18224        let mut aliased = parse_paragraph(&format!(
18225            r#"<q:fldSimple xmlns:q="{word_namespace}" q:instr="REF destination" q:dirty="true"><q:r><q:t>old</q:t><q:custom/></q:r></q:fldSimple>"#,
18226        ));
18227        let RunContent::Field(field) = &mut aliased.runs[0].content[0] else {
18228            panic!("expected aliased field")
18229        };
18230        field.cached_result = "new".to_owned();
18231        field.dirty = Some(false);
18232
18233        let mut output = Vec::new();
18234        aliased.to_xml(&mut Writer::new(&mut output)).unwrap();
18235        let output = String::from_utf8(output).unwrap();
18236        assert!(output.contains("<w:fldSimple"), "{output}");
18237        assert!(output.contains(r#"w:instr="REF destination""#), "{output}");
18238        assert!(output.contains(r#"w:dirty="0""#), "{output}");
18239        assert!(output.contains("<w:t>new</w:t>"), "{output}");
18240        assert!(!output.contains("<q:fldSimple"), "{output}");
18241    }
18242
18243    #[test]
18244    fn ref_and_pageref_instructions_keep_targets_and_switches() {
18245        let ref_field = parse_paragraph(
18246            r#"<w:fldSimple w:instr=" REF destination \h \* MERGEFORMAT "><w:r><w:t>cached text</w:t></w:r></w:fldSimple>"#,
18247        );
18248        let page_ref = parse_paragraph(
18249            r#"<w:fldSimple w:instr=" PAGEREF destination \p "><w:r><w:t>7</w:t></w:r></w:fldSimple>"#,
18250        );
18251
18252        let ref_field_model = parsed_field(&ref_field, 0);
18253        assert_eq!(ref_field_model.instruction.name, "REF");
18254        assert!(matches!(
18255            ref_field_model.instruction.arguments.first(),
18256            Some(FieldArgument::Text(bookmark)) if bookmark == "destination"
18257        ));
18258        assert_eq!(
18259            ref_field_model.instruction.raw,
18260            r"REF destination \h \* MERGEFORMAT"
18261        );
18262        assert_eq!(ref_field_model.cached_result, "cached text");
18263        let page_ref_model = parsed_field(&page_ref, 0);
18264        assert_eq!(page_ref_model.instruction.name, "PAGEREF");
18265        assert!(matches!(
18266            page_ref_model.instruction.arguments.first(),
18267            Some(FieldArgument::Text(bookmark)) if bookmark == "destination"
18268        ));
18269        assert_eq!(page_ref_model.instruction.raw, r"PAGEREF destination \p");
18270        assert_eq!(page_ref_model.cached_result, "7");
18271
18272        let mut output = Vec::new();
18273        ref_field.to_xml(&mut Writer::new(&mut output)).unwrap();
18274        let output = String::from_utf8(output).unwrap();
18275        assert!(output.contains(r#"w:instr=" REF destination \h \* MERGEFORMAT ""#));
18276        assert!(output.contains("<w:t>cached text</w:t>"));
18277    }
18278
18279    #[test]
18280    fn empty_cross_reference_displays_remain_empty() {
18281        let paragraph = parse_paragraph(
18282            r#"<w:fldSimple w:instr=" REF destination "><w:r><w:t></w:t></w:r></w:fldSimple><w:fldSimple w:instr=" PAGEREF destination "><w:r><w:t></w:t></w:r></w:fldSimple>"#,
18283        );
18284
18285        let mut output = Vec::new();
18286        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
18287        let output = String::from_utf8(output).unwrap();
18288        assert_eq!(output.matches("<w:t></w:t>").count(), 2, "{output}");
18289        assert!(!output.contains("<w:t>1</w:t>"), "{output}");
18290    }
18291
18292    #[test]
18293    fn field_parser_uses_expanded_names_and_accepts_word_aliases() {
18294        let word_namespace = crate::namespace::W_NS;
18295        let paragraph = parse_paragraph(&format!(
18296            r#"<x:fldSimple xmlns:x="urn:producer" x:instr="REF foreign"><x:r><x:t>foreign</x:t></x:r></x:fldSimple><q:fldSimple xmlns:q="{word_namespace}" q:instr="REF destination"><q:r><q:t>cached</q:t></q:r></q:fldSimple>"#,
18297        ));
18298
18299        assert_eq!(paragraph.runs.len(), 1);
18300        let field = parsed_field(&paragraph, 0);
18301        assert_eq!(field.instruction.name, "REF");
18302        assert!(matches!(
18303            field.instruction.arguments.first(),
18304            Some(FieldArgument::Text(bookmark)) if bookmark == "destination"
18305        ));
18306        assert_eq!(field.cached_result, "cached");
18307        let mut output = Vec::new();
18308        paragraph.to_xml(&mut Writer::new(&mut output)).unwrap();
18309        let output = String::from_utf8(output).unwrap();
18310        assert!(output.contains("<x:fldSimple"), "{output}");
18311        assert!(output.contains("x:instr=\"REF foreign\""), "{output}");
18312        assert!(output.contains("<q:fldSimple"), "{output}");
18313    }
18314
18315    #[test]
18316    fn bookmark_markers_keep_range_order_and_unmodelled_neighbours() {
18317        let p = parse_paragraph(
18318            r#"<w:customBefore/><w:bookmarkStart w:id="4" w:name="destination"/><w:r><w:t>inside</w:t></w:r><w:bookmarkEnd w:id="4"/><w:customAfter/>"#,
18319        );
18320
18321        assert_eq!(p.bookmark_markers.len(), 2);
18322        assert!(p.bookmark_markers[0].is_start());
18323        assert_eq!(p.bookmark_markers[0].id(), Some(4));
18324        assert_eq!(p.bookmark_markers[0].name(), Some("destination"));
18325        assert_eq!(p.bookmark_markers[0].run_index(), 0);
18326        assert!(!p.bookmark_markers[1].is_start());
18327        assert_eq!(p.bookmark_markers[1].run_index(), 1);
18328
18329        let mut output = Vec::new();
18330        p.to_xml(&mut Writer::new(&mut output)).unwrap();
18331        let output = String::from_utf8(output).unwrap();
18332        let before = output.find("<w:customBefore/>").unwrap();
18333        let start = output.find("<w:bookmarkStart").unwrap();
18334        let run = output.find("<w:r>").unwrap();
18335        let end = output.find("<w:bookmarkEnd").unwrap();
18336        let after = output.find("<w:customAfter/>").unwrap();
18337        assert!(before < start && start < run && run < end && end < after);
18338    }
18339
18340    #[test]
18341    fn accepted_inline_owners_project_bookmarks_once_in_exact_order() {
18342        let paragraph = parse_paragraph(concat!(
18343            r#"<w:bookmarkStart w:id="1" w:name="direct"/>"#,
18344            r#"<w:hyperlink w:anchor="target"><w:r><w:t>h</w:t></w:r><w:bookmarkStart w:id="2" w:name="hyper"/></w:hyperlink>"#,
18345            r#"<w:ins w:id="3" w:author="Ada"><w:r><w:t>i</w:t></w:r><w:bookmarkStart w:id="4" w:name="revision"/></w:ins>"#,
18346            r#"<w:sdt><w:sdtContent><w:r><w:t>c</w:t></w:r><w:bookmarkStart w:id="5" w:name="control"/></w:sdtContent></w:sdt>"#,
18347            r#"<x:opaque xmlns:x="urn:opaque"><w:bookmarkStart w:id="6" w:name="opaque"/></x:opaque>"#,
18348            r#"<w:sdtContent><w:bookmarkStart w:id="7" w:name="malformed"/></w:sdtContent>"#,
18349            r#"<w:bookmarkEnd w:id="1"/>"#,
18350        ));
18351
18352        assert_eq!(
18353            paragraph
18354                .bookmark_markers
18355                .iter()
18356                .filter_map(BookmarkMarker::name)
18357                .collect::<Vec<_>>(),
18358            ["direct", "hyper", "revision", "control"]
18359        );
18360        assert_eq!(
18361            paragraph
18362                .bookmark_markers
18363                .iter()
18364                .map(BookmarkMarker::projected_run_index)
18365                .collect::<Vec<_>>(),
18366            [0, 1, 2, 3, 3]
18367        );
18368        assert_eq!(paragraph.accepted_bookmark_runs().len(), 3);
18369        let output = serialized_paragraph(&paragraph);
18370        for name in [
18371            "direct",
18372            "hyper",
18373            "revision",
18374            "control",
18375            "opaque",
18376            "malformed",
18377        ] {
18378            assert_eq!(
18379                output.matches(&format!(r#"w:name="{name}""#)).count(),
18380                1,
18381                "{output}"
18382            );
18383        }
18384    }
18385
18386    #[test]
18387    fn nested_control_local_word_alias_projects_bookmarks_after_reopen() {
18388        let paragraph = parse_paragraph(concat!(
18389            r#"<w:sdt><w:sdtContent>"#,
18390            r#"<q:sdt xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><q:sdtContent>"#,
18391            r#"<q:bookmarkStart q:id="8" q:name="nested"/>"#,
18392            r#"<q:r><q:t>inside</q:t></q:r>"#,
18393            r#"<q:bookmarkEnd q:id="8"/>"#,
18394            r#"</q:sdtContent></q:sdt>"#,
18395            r#"</w:sdtContent></w:sdt>"#,
18396        ));
18397
18398        assert_eq!(
18399            paragraph
18400                .bookmark_markers
18401                .iter()
18402                .filter_map(BookmarkMarker::name)
18403                .collect::<Vec<_>>(),
18404            ["nested"]
18405        );
18406        assert_eq!(
18407            paragraph
18408                .bookmark_markers
18409                .iter()
18410                .map(BookmarkMarker::projected_run_index)
18411                .collect::<Vec<_>>(),
18412            [0, 1]
18413        );
18414        let serialized = serialized_paragraph(&paragraph);
18415        assert!(
18416            serialized.contains(
18417                r#"xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main""#
18418            )
18419        );
18420        let reopened = parse_paragraph(
18421            serialized
18422                .strip_prefix("<w:p>")
18423                .and_then(|xml| xml.strip_suffix("</w:p>"))
18424                .expect("serialized paragraph wrapper"),
18425        );
18426        assert_eq!(reopened.bookmark_markers.len(), 2);
18427        assert_eq!(reopened.accepted_bookmark_runs()[0].text(), "inside");
18428    }
18429
18430    #[test]
18431    fn complex_field_collapse_remaps_accepted_bookmark_boundaries() {
18432        let paragraph = parse_paragraph(concat!(
18433            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
18434            r#"<w:r><w:instrText>AUTHOR</w:instrText></w:r>"#,
18435            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18436            r#"<w:r><w:t>cached</w:t></w:r>"#,
18437            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
18438            r#"<w:bookmarkStart w:id="9" w:name="afterField"/>"#,
18439            r#"<w:r><w:t>target</w:t></w:r>"#,
18440            r#"<w:bookmarkEnd w:id="9"/>"#,
18441        ));
18442
18443        assert_eq!(paragraph.runs.len(), 2);
18444        assert_eq!(
18445            paragraph
18446                .bookmark_markers
18447                .iter()
18448                .map(BookmarkMarker::projected_run_index)
18449                .collect::<Vec<_>>(),
18450            [1, 2]
18451        );
18452        assert_eq!(paragraph.accepted_bookmark_runs().len(), 2);
18453    }
18454
18455    #[test]
18456    fn parse_fld_simple_mixed_with_text() {
18457        let p = parse_paragraph(
18458            r#"<w:r><w:t>Page </w:t></w:r><w:fldSimple w:instr=" PAGE "><w:r><w:t>1</w:t></w:r></w:fldSimple><w:r><w:t> of </w:t></w:r><w:fldSimple w:instr=" NUMPAGES "><w:r><w:t>5</w:t></w:r></w:fldSimple>"#,
18459        );
18460        assert_eq!(p.runs.len(), 4);
18461        assert_eq!(p.text(), "Page  of ");
18462        assert_eq!(parsed_field(&p, 1).instruction.name, "PAGE");
18463        assert_eq!(parsed_field(&p, 3).instruction.name, "NUMPAGES");
18464    }
18465
18466    #[test]
18467    fn round_trip_fld_simple() {
18468        let mut p = CT_P::new();
18469        p.add_run("Page ");
18470        p.runs.push(CT_R {
18471            properties: None,
18472            content: vec![RunContent::Field(Field::new(" PAGE ", "1"))],
18473            extra_xml: Vec::new(),
18474            extra_xml_positions: Vec::new(),
18475            alt_drawings: Vec::new(),
18476        });
18477
18478        let mut output = Vec::new();
18479        let mut writer = Writer::new(&mut output);
18480        p.to_xml(&mut writer).unwrap();
18481        let xml = String::from_utf8(output).unwrap();
18482
18483        let parsed = parse_paragraph(
18484            xml.strip_prefix("<w:p>")
18485                .unwrap()
18486                .strip_suffix("</w:p>")
18487                .unwrap(),
18488        );
18489        assert_eq!(parsed.runs.len(), 2);
18490        assert_eq!(parsed_field(&parsed, 1).instruction.name, "PAGE");
18491    }
18492
18493    #[test]
18494    fn round_trip_paragraph() {
18495        let mut p = CT_P::new();
18496        p.add_run("Hello ");
18497        let run = p.add_run("World");
18498        run.properties = Some(CT_RPr {
18499            bold: Some(true),
18500            ..Default::default()
18501        });
18502
18503        let mut output = Vec::new();
18504        let mut writer = Writer::new(&mut output);
18505        p.to_xml(&mut writer).unwrap();
18506        let xml = String::from_utf8(output).unwrap();
18507
18508        let parsed = parse_paragraph(
18509            xml.strip_prefix("<w:p>")
18510                .unwrap()
18511                .strip_suffix("</w:p>")
18512                .unwrap(),
18513        );
18514        assert_eq!(parsed.text(), "Hello World");
18515        assert_eq!(parsed.runs.len(), 2);
18516        assert_eq!(parsed.runs[1].properties.as_ref().unwrap().bold, Some(true));
18517    }
18518
18519    #[test]
18520    fn parse_footnote_reference() {
18521        let p = parse_paragraph(
18522            r#"<w:r><w:t>Some text</w:t></w:r><w:r><w:footnoteReference w:id="1"/></w:r>"#,
18523        );
18524        assert_eq!(p.runs.len(), 2);
18525        assert_eq!(p.runs[0].text(), "Some text");
18526        assert_eq!(p.runs[1].content.len(), 1);
18527        assert!(matches!(
18528            p.runs[1].content[0],
18529            RunContent::FootnoteRef { id: 1, .. }
18530        ));
18531    }
18532
18533    #[test]
18534    fn parse_endnote_reference() {
18535        let p = parse_paragraph(r#"<w:r><w:endnoteReference w:id="3"/></w:r>"#);
18536        assert_eq!(p.runs.len(), 1);
18537        assert!(matches!(
18538            p.runs[0].content[0],
18539            RunContent::EndnoteRef { id: 3, .. }
18540        ));
18541    }
18542
18543    #[test]
18544    fn round_trip_footnote_reference() {
18545        let mut p = CT_P::new();
18546        p.add_run("Text before");
18547        p.runs.push(CT_R {
18548            properties: None,
18549            content: vec![RunContent::FootnoteRef {
18550                id: 2,
18551                custom_mark: None,
18552            }],
18553            extra_xml: Vec::new(),
18554            extra_xml_positions: Vec::new(),
18555            alt_drawings: Vec::new(),
18556        });
18557        p.add_run(" text after");
18558
18559        let mut output = Vec::new();
18560        let mut writer = Writer::new(&mut output);
18561        p.to_xml(&mut writer).unwrap();
18562        let xml = String::from_utf8(output).unwrap();
18563
18564        let parsed = parse_paragraph(
18565            xml.strip_prefix("<w:p>")
18566                .unwrap()
18567                .strip_suffix("</w:p>")
18568                .unwrap(),
18569        );
18570        assert_eq!(parsed.runs.len(), 3);
18571        assert!(matches!(
18572            parsed.runs[1].content[0],
18573            RunContent::FootnoteRef { id: 2, .. }
18574        ));
18575    }
18576
18577    #[test]
18578    fn comment_anchors_are_typed_without_moving_neighbouring_xml() {
18579        let word_namespace = crate::namespace::W_NS;
18580        let p = parse_paragraph(&format!(
18581            r#"<w:r><w:t>before</w:t></w:r><ext:marker xmlns:ext="urn:producer"/><q:commentRangeStart xmlns:q="{word_namespace}" q:id="7"/><w:r><w:t>inside</w:t></w:r><q:commentRangeEnd xmlns:q="{word_namespace}" q:id="7"/><w:r><ext:runBefore xmlns:ext="urn:producer"/><q:commentReference xmlns:q="{word_namespace}" q:id="7"/><ext:runAfter xmlns:ext="urn:producer"/></w:r><ext:tail xmlns:ext="urn:producer"/>"#
18582        ));
18583
18584        assert_eq!(p.comment_ranges.len(), 2);
18585        assert!(matches!(
18586            p.comment_ranges[0],
18587            CommentRangeMarker::Start {
18588                id: 7,
18589                run_index: 1,
18590                ..
18591            }
18592        ));
18593        assert!(matches!(
18594            p.comment_ranges[1],
18595            CommentRangeMarker::End {
18596                id: 7,
18597                run_index: 2,
18598                ..
18599            }
18600        ));
18601        assert!(matches!(
18602            p.runs[2].content[0],
18603            RunContent::CommentReference { id: 7, .. }
18604        ));
18605
18606        let mut output = Vec::new();
18607        p.to_xml(&mut Writer::new(&mut output)).unwrap();
18608        let output = String::from_utf8(output).unwrap();
18609        assert!(output.contains(
18610            r#"<ext:marker xmlns:ext="urn:producer"/><w:commentRangeStart w:id="7"/><w:r><w:t>inside</w:t></w:r><w:commentRangeEnd w:id="7"/><w:r><ext:runBefore xmlns:ext="urn:producer"/><w:commentReference w:id="7"/><ext:runAfter xmlns:ext="urn:producer"/></w:r><ext:tail xmlns:ext="urn:producer"/>"#
18611        ));
18612    }
18613
18614    #[test]
18615    fn encoded_comment_ids_decode_for_ranges_and_references() {
18616        let paragraph = parse_paragraph(concat!(
18617            r#"<w:commentRangeStart w:id="&#55;"/>"#,
18618            r#"<w:r><w:t>commented</w:t></w:r>"#,
18619            r#"<w:commentRangeEnd w:id="&#x37;"/>"#,
18620            r#"<w:r><w:commentReference w:id="&#x37;"/></w:r>"#,
18621        ));
18622
18623        assert!(matches!(
18624            paragraph.comment_ranges.as_slice(),
18625            [
18626                CommentRangeMarker::Start { id: 7, .. },
18627                CommentRangeMarker::End { id: 7, .. }
18628            ]
18629        ));
18630        assert!(matches!(
18631            paragraph.runs[1].content.as_slice(),
18632            [RunContent::CommentReference { id: 7, .. }]
18633        ));
18634    }
18635
18636    #[test]
18637    fn edited_nested_field_order_matches_canonical_serialization() {
18638        let mut paragraph = parse_paragraph(concat!(
18639            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
18640            r#"<w:r><w:instrText xml:space="preserve">MERGEFIELD \b </w:instrText></w:r>"#,
18641            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
18642            r#"<w:r><w:instrText>AUTHOR</w:instrText></w:r>"#,
18643            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18644            r#"<w:r><w:t>stored author</w:t></w:r>"#,
18645            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
18646            r#"<w:r><w:instrText xml:space="preserve"> </w:instrText></w:r>"#,
18647            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
18648            r#"<w:r><w:instrText>MERGEFIELD Name</w:instrText></w:r>"#,
18649            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18650            r#"<w:r><w:t>stored name</w:t></w:r>"#,
18651            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
18652            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r>"#,
18653            r#"<w:r><w:t>stored outer</w:t></w:r>"#,
18654            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
18655        ));
18656        assert_eq!(
18657            parsed_field(&paragraph, 0)
18658                .nested_fields_in_source_order()
18659                .iter()
18660                .map(|field| field.instruction.name.as_str())
18661                .collect::<Vec<_>>(),
18662            ["AUTHOR", "MERGEFIELD"]
18663        );
18664
18665        let mut raw_edited = paragraph.clone();
18666        let RunContent::Field(field) = &mut raw_edited.runs[0].content[0] else {
18667            unreachable!()
18668        };
18669        field.instruction.raw = "AUTHOR".to_owned();
18670        assert!(field.nested_fields_in_source_order().is_empty());
18671        assert_eq!(field.effective_instruction().name, "AUTHOR");
18672        let output = serialized_paragraph(&raw_edited);
18673        let reparsed = parse_paragraph(
18674            output
18675                .strip_prefix("<w:p>")
18676                .and_then(|value| value.strip_suffix("</w:p>"))
18677                .unwrap(),
18678        );
18679        assert_eq!(parsed_field(&reparsed, 0).instruction.name, "AUTHOR");
18680        assert!(
18681            parsed_field(&reparsed, 0)
18682                .nested_fields_in_source_order()
18683                .is_empty()
18684        );
18685
18686        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
18687            unreachable!()
18688        };
18689        field.instruction.arguments.insert(
18690            0,
18691            FieldArgument::Nested(Box::new(Field::new("MERGEFIELD First", "stored first"))),
18692        );
18693        let expected = ["MERGEFIELD First", "MERGEFIELD Name", "AUTHOR"];
18694        assert_eq!(
18695            field
18696                .nested_fields_in_source_order()
18697                .iter()
18698                .map(|field| field.instruction.raw.as_str())
18699                .collect::<Vec<_>>(),
18700            expected
18701        );
18702
18703        let output = serialized_paragraph(&paragraph);
18704        let reparsed = parse_paragraph(
18705            output
18706                .strip_prefix("<w:p>")
18707                .and_then(|value| value.strip_suffix("</w:p>"))
18708                .unwrap(),
18709        );
18710        assert_eq!(
18711            parsed_field(&reparsed, 0)
18712                .nested_fields_in_source_order()
18713                .iter()
18714                .map(|field| field.instruction.raw.as_str())
18715                .collect::<Vec<_>>(),
18716            expected
18717        );
18718    }
18719
18720    #[test]
18721    fn effective_instruction_text_matches_raw_structured_and_nested_edits() {
18722        let paragraph = parse_paragraph(
18723            r#"<w:fldSimple w:instr="  MERGEFIELD Name \* MERGEFORMAT  "><w:r><w:t>stored</w:t></w:r></w:fldSimple>"#,
18724        );
18725        let original_bytes = serialized_paragraph(&paragraph);
18726        let original = parsed_field(&paragraph, 0);
18727        assert_eq!(
18728            original.effective_instruction_text(),
18729            original.effective_instruction().raw
18730        );
18731        assert_eq!(serialized_paragraph(&paragraph), original_bytes);
18732        let mut raw = original.clone();
18733        raw.instruction.raw = "  AUTHOR  ".into();
18734        assert_eq!(
18735            raw.effective_instruction_text(),
18736            raw.effective_instruction().raw
18737        );
18738        assert_eq!(raw.effective_instruction_text(), "AUTHOR");
18739        let mut structured = original.clone();
18740        structured.instruction.name = "REF".into();
18741        structured.instruction.arguments = vec![FieldArgument::Text("Other Target".into())];
18742        assert_eq!(
18743            structured.effective_instruction_text(),
18744            structured.effective_instruction().raw
18745        );
18746        let mut nested = Field::new("IF", "stored");
18747        nested.instruction.arguments = vec![FieldArgument::Nested(Box::new(Field::new(
18748            "SEQ Figure",
18749            "old",
18750        )))];
18751        assert_eq!(
18752            nested.effective_instruction_text(),
18753            nested.effective_instruction().raw
18754        );
18755        let FieldArgument::Nested(child) = &mut nested.instruction.arguments[0] else {
18756            unreachable!()
18757        };
18758        child.instruction.name = "REF".into();
18759        child.instruction.arguments = vec![FieldArgument::Text("Target".into())];
18760        assert_eq!(
18761            nested.effective_instruction_text(),
18762            nested.effective_instruction().raw
18763        );
18764    }
18765
18766    #[test]
18767    fn malformed_comment_anchor_id_is_rejected() {
18768        let full = format!(
18769            r#"<w:p xmlns:w="{}"><w:commentRangeStart w:id="not-a-number"/></w:p>"#,
18770            crate::namespace::W_NS
18771        );
18772        let mut reader = Reader::from_str(&full);
18773        let mut buf = Vec::new();
18774        loop {
18775            match reader.read_event_into(&mut buf) {
18776                Ok(Event::Start(ref element))
18777                    if matches_local_name(element.name().as_ref(), b"p") =>
18778                {
18779                    break;
18780                }
18781                Ok(Event::Eof) => panic!("missing paragraph"),
18782                event => {
18783                    event.unwrap();
18784                }
18785            }
18786            buf.clear();
18787        }
18788        assert!(CT_P::from_xml(&mut reader).is_err());
18789    }
18790
18791    #[test]
18792    fn undecodable_ordinary_and_deleted_text_are_rejected() {
18793        for child in [
18794            b"<w:t>before\xff</w:t>".as_slice(),
18795            b"<w:delText>before\xff</w:delText>".as_slice(),
18796        ] {
18797            let mut xml = br#"<w:p xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:r>"#.to_vec();
18798            xml.extend_from_slice(child);
18799            xml.extend_from_slice(b"</w:r></w:p>");
18800            let mut reader = Reader::from_reader(xml.as_slice());
18801            let mut buffer = Vec::new();
18802            assert!(matches!(
18803                reader.read_event_into(&mut buffer),
18804                Ok(Event::Start(_))
18805            ));
18806            assert!(matches!(
18807                CT_P::from_xml(&mut reader),
18808                Err(OxmlError::InvalidValue(_))
18809            ));
18810        }
18811
18812        let valid = parse_paragraph(
18813            "<w:r><w:t>one &amp; two</w:t><w:delText>three &lt; four</w:delText></w:r>",
18814        );
18815        assert_eq!(valid.runs[0].text(), "one & twothree < four");
18816    }
18817
18818    #[test]
18819    fn collapsed_run_boundaries_rebase_equation_raw_slots() {
18820        let math_namespace = crate::namespace::M_NS;
18821        let mut paragraph = parse_paragraph(&format!(
18822            r#"<m:oMath xmlns:m="{math_namespace}"><m:r><m:t>first</m:t></m:r></m:oMath><w:r><w:commentReference w:id="7"/></w:r><m:oMath xmlns:m="{math_namespace}"><m:r><m:t>second</m:t></m:r></m:oMath><w:r><w:t>kept</w:t></w:r>"#
18823        ));
18824        paragraph.remove_comment_anchors(&[7]);
18825        assert_eq!(paragraph.equations.len(), 2);
18826        assert_eq!(paragraph.equations[0].0, 0);
18827        assert_eq!(paragraph.equations[0].1, 0);
18828        assert_eq!(paragraph.equations[1].0, 0);
18829        assert_eq!(paragraph.equations[1].1, 1);
18830        let OfficeMath::Inline(second) = &mut paragraph.equations[1].2 else {
18831            panic!("inline equation")
18832        };
18833        let crate::math::MathExpression::Run(run) = &mut second.expressions[0] else {
18834            panic!("math run")
18835        };
18836        run.text = "changed".to_owned();
18837
18838        let output = serialized_paragraph(&paragraph);
18839        assert!(output.find("first").unwrap() < output.find("changed").unwrap());
18840        assert!(!output.contains("second"));
18841    }
18842
18843    #[test]
18844    fn nested_math_text_extension_remains_raw_through_paragraph_reopen() {
18845        let math_namespace = crate::namespace::M_NS;
18846        let relationships_namespace = crate::namespace::R_NS;
18847        let compatibility_namespace = crate::namespace::MC_NS;
18848        let equation = format!(
18849            r#"<m:oMath xmlns:m="{math_namespace}" xmlns:r="{relationships_namespace}" xmlns:mc="{compatibility_namespace}"><m:r><m:t>x<x:nested xmlns:x="urn:test"/></m:t></m:r></m:oMath>"#
18850        );
18851        let source =
18852            format!(r#"<w:r><w:t>before</w:t></w:r>{equation}<w:r><w:t>after</w:t></w:r>"#);
18853        let paragraph = parse_paragraph(&source);
18854        assert_eq!(paragraph.text(), "beforeafter");
18855        assert_eq!(paragraph.equations.len(), 1);
18856        assert!(paragraph.equations[0].2.has_unsupported_content());
18857
18858        let first = serialized_paragraph(&paragraph);
18859        assert!(first.contains(&equation), "{first}");
18860        let reopened = parse_paragraph(
18861            first
18862                .strip_prefix("<w:p>")
18863                .and_then(|xml| xml.strip_suffix("</w:p>"))
18864                .expect("serialized paragraph wrapper"),
18865        );
18866        assert_eq!(reopened.text(), "beforeafter");
18867        assert_eq!(reopened.equations.len(), 1);
18868        assert!(reopened.equations[0].2.has_unsupported_content());
18869        let second = serialized_paragraph(&reopened);
18870        assert!(second.contains(&equation), "{second}");
18871    }
18872
18873    #[test]
18874    fn self_closing_simple_fields_count_in_bookmark_run_projections() {
18875        for field in [
18876            r#"<w:fldSimple w:instr=" PAGE "/>"#,
18877            r#"<w:fldSimple w:instr=" PAGE "><w:r><w:t>1</w:t></w:r></w:fldSimple>"#,
18878        ] {
18879            let paragraph = parse_paragraph(&format!(
18880                r#"{field}<w:bookmarkStart w:id="1" w:name="after"/><w:r><w:t>text</w:t></w:r><w:bookmarkEnd w:id="1"/>"#
18881            ));
18882
18883            assert_eq!(paragraph.runs.len(), 2, "{field}");
18884            let marker = &paragraph.bookmark_markers[0];
18885            assert_eq!(marker.run_index(), 1, "{field}");
18886            assert_eq!(marker.projected_run_index(), 1, "{field}");
18887            assert_eq!(marker.tracked_run_index(), 1, "{field}");
18888            assert_eq!(
18889                paragraph.bookmark_markers[1].projected_run_index(),
18890                2,
18891                "{field}"
18892            );
18893        }
18894    }
18895
18896    #[test]
18897    fn mutation_refresh_retains_ancestor_only_bookmark_aliases() {
18898        let source = format!(
18899            r#"<q:p xmlns:q="{word}"><q:bookmarkStart q:id="7" q:name="target"/><q:r><q:t>target</q:t></q:r><q:bookmarkEnd q:id="7"/></q:p>"#,
18900            word = crate::namespace::W_NS,
18901        );
18902        let mut reader = Reader::from_str(&source);
18903        let mut buffer = Vec::new();
18904        let start = loop {
18905            match reader.read_event_into(&mut buffer).unwrap() {
18906                Event::Start(start) => break start.into_owned(),
18907                Event::Eof => panic!("missing paragraph"),
18908                _ => buffer.clear(),
18909            }
18910        };
18911        let prefixes = word_prefixes_at(&start, &["w".to_owned()]).unwrap();
18912        let mut paragraph = CT_P::from_xml_with_prefixes(&mut reader, &prefixes).unwrap();
18913
18914        assert_eq!(paragraph.bookmark_markers.len(), 2);
18915        assert!(paragraph.insert_unwrapped_run(1, CT_R::new("inserted")));
18916        assert_eq!(paragraph.bookmark_markers.len(), 2);
18917        assert_eq!(paragraph.bookmark_markers[0].projected_run_index(), 0);
18918        assert_eq!(paragraph.bookmark_markers[1].projected_run_index(), 2);
18919        let output = serialized_paragraph(&paragraph);
18920        assert!(output.contains(r#"<q:bookmarkStart q:id="7" q:name="target"/>"#));
18921        assert!(output.contains(r#"<q:bookmarkEnd q:id="7"/>"#));
18922    }
18923
18924    #[test]
18925    fn inline_control_keeps_block_children_opaque_to_typed_projections() {
18926        let paragraph = parse_paragraph(concat!(
18927            r#"<w:sdt><w:sdtContent>"#,
18928            r#"<w:p><w:bookmarkStart w:id="1" w:name="paragraph"/><w:r><w:t>paragraph</w:t></w:r><w:bookmarkEnd w:id="1"/></w:p>"#,
18929            r#"<w:tbl><w:tr><w:tc><w:p><w:r><w:t>table</w:t></w:r></w:p></w:tc></w:tr></w:tbl>"#,
18930            r#"<w:tr><w:tc><w:p><w:r><w:t>row</w:t></w:r></w:p></w:tc></w:tr>"#,
18931            r#"<w:tc><w:p><w:r><w:t>cell</w:t></w:r></w:p></w:tc>"#,
18932            r#"<w:bookmarkStart w:id="2" w:name="inline"/><w:r><w:t>inline</w:t></w:r><w:bookmarkEnd w:id="2"/>"#,
18933            r#"</w:sdtContent></w:sdt>"#,
18934        ));
18935
18936        assert_eq!(accepted_paragraph_runs(&paragraph).len(), 1);
18937        assert_eq!(tracked_paragraph_runs(&paragraph).len(), 1);
18938        assert_eq!(accepted_paragraph_runs(&paragraph)[0].text(), "inline");
18939        assert_eq!(
18940            paragraph
18941                .bookmark_markers
18942                .iter()
18943                .filter_map(BookmarkMarker::name)
18944                .collect::<Vec<_>>(),
18945            ["inline"]
18946        );
18947        let output = serialized_paragraph(&paragraph);
18948        for retained in ["paragraph", "table", "row", "cell", "inline"] {
18949            assert!(output.contains(retained), "{output}");
18950        }
18951    }
18952
18953    #[test]
18954    fn legacy_form_data_accepts_aliases_and_rewrites_only_the_typed_value() {
18955        let mut paragraph = parse_paragraph(&format!(
18956            r#"<q:r xmlns:q="{word}" xmlns:x="urn:test"><q:fldChar q:fldCharType="begin"><q:ffData><q:name q:val="Choice"/><x:raw keep="yes"/><q:ddList><q:result q:val="0"/><q:default q:val="0"/><q:listEntry q:val="one"/><q:listEntry q:val="two"/></q:ddList></q:ffData></q:fldChar></q:r><q:r xmlns:q="{word}"><q:instrText> FORMDROPDOWN </q:instrText></q:r><q:r xmlns:q="{word}"><q:fldChar q:fldCharType="separate"/></q:r><q:r xmlns:q="{word}"><q:t>one</q:t></q:r><q:r xmlns:q="{word}"><q:fldChar q:fldCharType="end"/></q:r>"#,
18957            word = crate::namespace::W_NS,
18958        ));
18959        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
18960            panic!("legacy field")
18961        };
18962        let form = field.legacy_form.as_ref().expect("typed form data");
18963        assert_eq!(form.kind, LegacyFormFieldKind::DropDownList);
18964        assert_eq!(form.value, LegacyFormFieldValue::SelectedIndex(0));
18965        assert_eq!(form.choices, ["one", "two"]);
18966        field
18967            .set_legacy_form_value(LegacyFormFieldValue::SelectedIndex(1))
18968            .unwrap();
18969        assert_eq!(field.cached_result, "two");
18970        let output = serialized_paragraph(&paragraph);
18971        assert!(output.contains(r#"<x:raw keep="yes"/>"#));
18972        assert!(output.contains(r#"q:result q:val="1""#));
18973        assert_eq!(output.matches("q:result").count(), 1);
18974    }
18975
18976    #[test]
18977    fn sibling_namespace_shadow_does_not_hide_a_later_legacy_form_kind() {
18978        let paragraph = parse_paragraph(&format!(
18979            r#"<q:r xmlns:q="{word}" xmlns:x="urn:test"><q:fldChar q:fldCharType="begin"><q:ffData><x:raw xmlns:q="urn:producer"/><q:ddList><q:result q:val="0"/><q:listEntry q:val="one"/></q:ddList></q:ffData></q:fldChar></q:r><q:r xmlns:q="{word}"><q:instrText> FORMDROPDOWN </q:instrText></q:r><q:r xmlns:q="{word}"><q:fldChar q:fldCharType="end"/></q:r>"#,
18980            word = crate::namespace::W_NS,
18981        ));
18982        let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
18983            panic!("legacy field")
18984        };
18985        assert_eq!(
18986            field.legacy_form.as_ref().map(|form| form.kind),
18987            Some(LegacyFormFieldKind::DropDownList)
18988        );
18989    }
18990
18991    #[test]
18992    fn legacy_form_value_patch_targets_the_expanded_value_attribute_only() {
18993        let mut paragraph = parse_paragraph(&format!(
18994            r#"<q:r xmlns:q="{word}" xmlns:x="urn:test"><q:fldChar q:fldCharType="begin"><q:ffData><q:ddList><q:result x:a="q:val" x:b="keep" q:val="0"/><q:listEntry q:val="one"/><q:listEntry q:val="two"/></q:ddList></q:ffData></q:fldChar></q:r><q:r xmlns:q="{word}"><q:instrText> FORMDROPDOWN </q:instrText></q:r><q:r xmlns:q="{word}"><q:fldChar q:fldCharType="end"/></q:r>"#,
18995            word = crate::namespace::W_NS,
18996        ));
18997        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
18998            panic!("legacy field")
18999        };
19000        field
19001            .set_legacy_form_value(LegacyFormFieldValue::SelectedIndex(1))
19002            .unwrap();
19003        let output = serialized_paragraph(&paragraph);
19004        assert!(output.contains(r#"x:a="q:val" x:b="keep" q:val="1""#));
19005    }
19006
19007    #[test]
19008    fn default_namespace_missing_form_value_insertion_declares_its_prefix() {
19009        let mut paragraph = parse_paragraph(&format!(
19010            r#"<r xmlns="{word}" xmlns:q="{word}"><fldChar q:fldCharType="begin"><ffData><checkBox><sizeAuto/><default q:val="0"/></checkBox></ffData></fldChar></r><r xmlns="{word}"><instrText> FORMCHECKBOX </instrText></r><r xmlns="{word}" xmlns:q="{word}"><fldChar q:fldCharType="end"/></r>"#,
19011            word = crate::namespace::W_NS,
19012        ));
19013        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
19014            panic!("legacy field")
19015        };
19016        field
19017            .set_legacy_form_value(LegacyFormFieldValue::Checked(true))
19018            .unwrap();
19019        let output = serialized_paragraph(&paragraph);
19020        assert!(output.contains(&format!(
19021            r#"<w:checked xmlns:w="{}" w:val="1"/>"#,
19022            crate::namespace::W_NS
19023        )));
19024        let scoped = output.replacen(
19025            "<w:p>",
19026            &format!(r#"<w:p xmlns="{0}" xmlns:q="{0}">"#, crate::namespace::W_NS),
19027            1,
19028        );
19029        let mut reader = Reader::from_str(&scoped);
19030        let mut buffer = Vec::new();
19031        let Event::Start(start) = reader.read_event_into(&mut buffer).unwrap() else {
19032            panic!("reparsed paragraph start")
19033        };
19034        let prefixes = word_prefixes_at(&start, &["w".to_owned()]).unwrap();
19035        let reparsed = CT_P::from_xml_with_prefixes(&mut reader, &prefixes).unwrap();
19036        let RunContent::Field(field) = &reparsed.runs[0].content[0] else {
19037            panic!("reparsed legacy field")
19038        };
19039        assert_eq!(
19040            field.legacy_form.as_ref().map(|form| form.value.clone()),
19041            Some(LegacyFormFieldValue::Checked(true))
19042        );
19043    }
19044
19045    #[test]
19046    fn default_namespace_valueless_form_element_gets_a_declared_value_prefix() {
19047        let xml = format!(
19048            r#"<p xmlns="{word}" xmlns:q="{word}"><r><fldChar q:fldCharType="begin"><ffData><checkBox><sizeAuto/><checked/></checkBox></ffData></fldChar></r><r><instrText> FORMCHECKBOX </instrText></r><r><fldChar q:fldCharType="end"/></r></p>"#,
19049            word = crate::namespace::W_NS,
19050        );
19051        let mut reader = Reader::from_str(&xml);
19052        let mut buffer = Vec::new();
19053        let Event::Start(start) = reader.read_event_into(&mut buffer).unwrap() else {
19054            panic!("paragraph start")
19055        };
19056        let prefixes = word_prefixes_at(&start, &[]).unwrap();
19057        let mut paragraph = CT_P::from_xml_with_prefixes(&mut reader, &prefixes).unwrap();
19058        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
19059            panic!("legacy field")
19060        };
19061        field
19062            .set_legacy_form_value(LegacyFormFieldValue::Checked(false))
19063            .unwrap();
19064        let output = serialized_paragraph(&paragraph);
19065        assert!(output.contains(r#"<checked q:val="0"/>"#), "{output}");
19066        let scoped = output.replacen(
19067            "<w:p>",
19068            &format!(r#"<w:p xmlns="{0}" xmlns:q="{0}">"#, crate::namespace::W_NS),
19069            1,
19070        );
19071        let mut reader = Reader::from_str(&scoped);
19072        let mut buffer = Vec::new();
19073        let Event::Start(start) = reader.read_event_into(&mut buffer).unwrap() else {
19074            panic!("reparsed paragraph start")
19075        };
19076        let prefixes = word_prefixes_at(&start, &["w".to_owned()]).unwrap();
19077        let reparsed = CT_P::from_xml_with_prefixes(&mut reader, &prefixes).unwrap();
19078        let RunContent::Field(field) = &reparsed.runs[0].content[0] else {
19079            panic!("reparsed legacy field")
19080        };
19081        assert_eq!(
19082            field.legacy_form.as_ref().map(|form| form.value.clone()),
19083            Some(LegacyFormFieldValue::Checked(false))
19084        );
19085    }
19086
19087    #[test]
19088    fn malformed_or_duplicate_legacy_form_singletons_fail_closed() {
19089        let cases = [
19090            (
19091                "duplicate-name",
19092                r#"<w:name w:val="one"/><w:name w:val="two"/><w:textInput><w:default w:val="old"/></w:textInput>"#,
19093                "FORMTEXT",
19094            ),
19095            (
19096                "duplicate-enabled",
19097                r#"<w:enabled/><w:enabled w:val="0"/><w:textInput><w:default w:val="old"/></w:textInput>"#,
19098                "FORMTEXT",
19099            ),
19100            (
19101                "duplicate-calculate",
19102                r#"<w:calcOnExit/><w:calcOnExit w:val="0"/><w:textInput><w:default w:val="old"/></w:textInput>"#,
19103                "FORMTEXT",
19104            ),
19105            (
19106                "duplicate-text-default",
19107                r#"<w:textInput><w:default w:val="one"/><w:default w:val="two"/></w:textInput>"#,
19108                "FORMTEXT",
19109            ),
19110            (
19111                "duplicate-max-length",
19112                r#"<w:textInput><w:maxLength w:val="1"/><w:maxLength w:val="2"/></w:textInput>"#,
19113                "FORMTEXT",
19114            ),
19115            (
19116                "duplicate-checked",
19117                r#"<w:checkBox><w:checked/><w:checked w:val="0"/></w:checkBox>"#,
19118                "FORMCHECKBOX",
19119            ),
19120            (
19121                "duplicate-check-default",
19122                r#"<w:checkBox><w:default/><w:default w:val="0"/></w:checkBox>"#,
19123                "FORMCHECKBOX",
19124            ),
19125            (
19126                "duplicate-result",
19127                r#"<w:ddList><w:result w:val="0"/><w:result w:val="1"/><w:listEntry w:val="one"/><w:listEntry w:val="two"/></w:ddList>"#,
19128                "FORMDROPDOWN",
19129            ),
19130            (
19131                "duplicate-list-default",
19132                r#"<w:ddList><w:default w:val="0"/><w:default w:val="1"/><w:listEntry w:val="one"/><w:listEntry w:val="two"/></w:ddList>"#,
19133                "FORMDROPDOWN",
19134            ),
19135            (
19136                "invalid-enabled-token",
19137                r#"<w:enabled w:val="maybe"/><w:textInput><w:default w:val="old"/></w:textInput>"#,
19138                "FORMTEXT",
19139            ),
19140            (
19141                "invalid-calculate-token",
19142                r#"<w:calcOnExit w:val="2"/><w:textInput><w:default w:val="old"/></w:textInput>"#,
19143                "FORMTEXT",
19144            ),
19145            (
19146                "invalid-checked-token",
19147                r#"<w:checkBox><w:checked w:val="maybe"/></w:checkBox>"#,
19148                "FORMCHECKBOX",
19149            ),
19150            (
19151                "invalid-check-default-token",
19152                r#"<w:checkBox><w:default w:val="maybe"/></w:checkBox>"#,
19153                "FORMCHECKBOX",
19154            ),
19155            (
19156                "invalid-max-length",
19157                r#"<w:textInput><w:maxLength w:val="many"/></w:textInput>"#,
19158                "FORMTEXT",
19159            ),
19160            (
19161                "invalid-result",
19162                r#"<w:ddList><w:result w:val="first"/><w:listEntry w:val="one"/></w:ddList>"#,
19163                "FORMDROPDOWN",
19164            ),
19165            (
19166                "invalid-list-default",
19167                r#"<w:ddList><w:default w:val="first"/><w:listEntry w:val="one"/></w:ddList>"#,
19168                "FORMDROPDOWN",
19169            ),
19170            (
19171                "missing-name-value",
19172                r#"<w:name/><w:textInput><w:default w:val="old"/></w:textInput>"#,
19173                "FORMTEXT",
19174            ),
19175            (
19176                "missing-text-default-value",
19177                r#"<w:textInput><w:default/></w:textInput>"#,
19178                "FORMTEXT",
19179            ),
19180            (
19181                "missing-max-length-value",
19182                r#"<w:textInput><w:maxLength/></w:textInput>"#,
19183                "FORMTEXT",
19184            ),
19185            (
19186                "missing-result-value",
19187                r#"<w:ddList><w:result/><w:listEntry w:val="one"/></w:ddList>"#,
19188                "FORMDROPDOWN",
19189            ),
19190            (
19191                "missing-list-entry-value",
19192                r#"<w:ddList><w:listEntry/></w:ddList>"#,
19193                "FORMDROPDOWN",
19194            ),
19195        ];
19196        for (case, ff_data, instruction) in cases {
19197            let paragraph = parse_paragraph(&format!(
19198                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19199            ));
19200            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19201                panic!("{case}: legacy field")
19202            };
19203            assert!(field.validate_legacy_form_owner().is_err(), "{case}");
19204        }
19205    }
19206
19207    #[test]
19208    fn legacy_form_ffdata_and_kind_children_require_schema_order() {
19209        let cases = [
19210            (
19211                "common-after-kind",
19212                r#"<w:checkBox><w:checked/></w:checkBox><w:name w:val="late"/>"#,
19213                "FORMCHECKBOX",
19214            ),
19215            (
19216                "checkbox-default-after-checked",
19217                r#"<w:checkBox><w:checked/><w:default/></w:checkBox>"#,
19218                "FORMCHECKBOX",
19219            ),
19220            (
19221                "text-default-after-max-length",
19222                r#"<w:textInput><w:maxLength w:val="12"/><w:default w:val="late"/></w:textInput>"#,
19223                "FORMTEXT",
19224            ),
19225            (
19226                "dropdown-result-after-entry",
19227                r#"<w:ddList><w:listEntry w:val="one"/><w:result w:val="0"/></w:ddList>"#,
19228                "FORMDROPDOWN",
19229            ),
19230        ];
19231        for (case, ff_data, instruction) in cases {
19232            let paragraph = parse_paragraph(&format!(
19233                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19234            ));
19235            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19236                panic!("{case}: legacy field")
19237            };
19238            assert!(field.validate_legacy_form_owner().is_err(), "{case}");
19239        }
19240    }
19241
19242    #[test]
19243    fn missing_contradictory_and_out_of_range_legacy_form_kinds_fail_closed() {
19244        let cases = [
19245            ("missing-kind", r#"<w:name w:val="missing"/>"#, "FORMTEXT"),
19246            (
19247                "contradictory-kind",
19248                r#"<w:checkBox><w:checked/></w:checkBox>"#,
19249                "FORMTEXT",
19250            ),
19251            (
19252                "out-of-range-selection",
19253                r#"<w:ddList><w:result w:val="2"/><w:listEntry w:val="one"/></w:ddList>"#,
19254                "FORMDROPDOWN",
19255            ),
19256        ];
19257        for (case, ff_data, instruction) in cases {
19258            let paragraph = parse_paragraph(&format!(
19259                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19260            ));
19261            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19262                panic!("{case}: legacy field")
19263            };
19264            assert!(field.validate_legacy_form_owner().is_err(), "{case}");
19265        }
19266    }
19267
19268    #[test]
19269    fn known_form_descendants_under_unmodeled_wrappers_are_rejected() {
19270        let paragraph = parse_paragraph(concat!(
19271            r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>"#,
19272            r#"<w:checkBox><w:sizeAuto/></w:checkBox><w:unsupported><w:checked/></w:unsupported>"#,
19273            r#"</w:ffData></w:fldChar></w:r>"#,
19274            r#"<w:r><w:instrText> FORMCHECKBOX </w:instrText></w:r>"#,
19275            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
19276        ));
19277        let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19278            panic!("legacy field")
19279        };
19280        assert!(field.validate_legacy_form_owner().is_err());
19281    }
19282
19283    #[test]
19284    fn known_legacy_form_singletons_and_checkbox_size_choice_are_exclusive() {
19285        let cases = [
19286            (
19287                r#"<w:entryMacro/><w:entryMacro/><w:textInput/>"#,
19288                "FORMTEXT",
19289            ),
19290            (r#"<w:exitMacro/><w:exitMacro/><w:textInput/>"#, "FORMTEXT"),
19291            (r#"<w:helpText/><w:helpText/><w:textInput/>"#, "FORMTEXT"),
19292            (
19293                r#"<w:statusText/><w:statusText/><w:textInput/>"#,
19294                "FORMTEXT",
19295            ),
19296            (
19297                r#"<w:textInput><w:type/><w:type/></w:textInput>"#,
19298                "FORMTEXT",
19299            ),
19300            (
19301                r#"<w:textInput><w:format/><w:format/></w:textInput>"#,
19302                "FORMTEXT",
19303            ),
19304            (
19305                r#"<w:checkBox><w:size/><w:size/></w:checkBox>"#,
19306                "FORMCHECKBOX",
19307            ),
19308            (
19309                r#"<w:checkBox><w:sizeAuto/><w:sizeAuto/></w:checkBox>"#,
19310                "FORMCHECKBOX",
19311            ),
19312            (
19313                r#"<w:checkBox><w:size/><w:sizeAuto/></w:checkBox>"#,
19314                "FORMCHECKBOX",
19315            ),
19316        ];
19317        for (ff_data, instruction) in cases {
19318            let paragraph = parse_paragraph(&format!(
19319                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19320            ));
19321            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19322                panic!("legacy field")
19323            };
19324            assert!(field.validate_legacy_form_owner().is_err(), "{ff_data}");
19325        }
19326
19327        for ff_data in [
19328            r#"&#x20;<w:textInput/>"#,
19329            r#"<w:textInput><![CDATA[ ]]></w:textInput>"#,
19330        ] {
19331            let paragraph = parse_paragraph(&format!(
19332                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> FORMTEXT </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19333            ));
19334            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19335                panic!("legacy field")
19336            };
19337            assert!(field.validate_legacy_form_owner().is_ok(), "{ff_data}");
19338        }
19339    }
19340
19341    #[test]
19342    fn typed_legacy_form_values_reject_duplicate_or_unescape_failing_attributes() {
19343        for ff_data in [
19344            r#"<w:name xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main" w:val="one" q:val="two"/><w:textInput/>"#,
19345            r#"<w:textInput><w:default w:val="&undefined;"/></w:textInput>"#,
19346        ] {
19347            let paragraph = parse_paragraph(&format!(
19348                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> FORMTEXT </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19349            ));
19350            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19351                panic!("legacy field")
19352            };
19353            assert!(field.validate_legacy_form_owner().is_err(), "{ff_data}");
19354        }
19355        let malformed = BytesStart::from_content("w:name w:val", 6);
19356        assert!(legacy_form_value_attribute(&malformed, &["w".to_owned()]).is_err());
19357    }
19358
19359    #[test]
19360    fn known_legacy_form_leaf_properties_require_values_tokens_and_empty_content() {
19361        let cases = [
19362            (r#"<w:entryMacro/><w:textInput/>"#, "FORMTEXT"),
19363            (r#"<w:exitMacro/><w:textInput/>"#, "FORMTEXT"),
19364            (r#"<w:textInput><w:type/></w:textInput>"#, "FORMTEXT"),
19365            (
19366                r#"<w:textInput><w:type w:val="wrong"/></w:textInput>"#,
19367                "FORMTEXT",
19368            ),
19369            (r#"<w:textInput><w:format/></w:textInput>"#, "FORMTEXT"),
19370            (r#"<w:checkBox><w:size/></w:checkBox>"#, "FORMCHECKBOX"),
19371            (
19372                r#"<w:checkBox><w:size w:val="large"/></w:checkBox>"#,
19373                "FORMCHECKBOX",
19374            ),
19375            (
19376                r#"<w:checkBox><w:sizeAuto w:val="maybe"/></w:checkBox>"#,
19377                "FORMCHECKBOX",
19378            ),
19379            (
19380                r#"<w:checkBox><w:size w:val="20"><w:checked/></w:size></w:checkBox>"#,
19381                "FORMCHECKBOX",
19382            ),
19383        ];
19384        for (ff_data, instruction) in cases {
19385            let paragraph = parse_paragraph(&format!(
19386                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19387            ));
19388            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19389                panic!("legacy field")
19390            };
19391            assert!(field.validate_legacy_form_owner().is_err(), "{ff_data}");
19392        }
19393        for size in [
19394            r#"<w:size w:val="20"/>"#,
19395            r#"<w:size w:val="10.5pt"/>"#,
19396            r#"<w:sizeAuto/>"#,
19397        ] {
19398            let paragraph = parse_paragraph(&format!(
19399                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData><w:checkBox>{size}</w:checkBox></w:ffData></w:fldChar></w:r><w:r><w:instrText> FORMCHECKBOX </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19400            ));
19401            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19402                panic!("legacy field")
19403            };
19404            assert!(field.validate_legacy_form_owner().is_ok(), "{size}");
19405        }
19406    }
19407
19408    #[test]
19409    fn legacy_help_and_status_text_types_accept_only_schema_tokens() {
19410        for leaf in [
19411            r#"<w:helpText w:type="bogus"/>"#,
19412            r#"<w:statusText w:type="bogus"/>"#,
19413        ] {
19414            let paragraph = parse_paragraph(&format!(
19415                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{leaf}<w:textInput/></w:ffData></w:fldChar></w:r><w:r><w:instrText> FORMTEXT </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19416            ));
19417            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19418                panic!("legacy field")
19419            };
19420            assert!(field.validate_legacy_form_owner().is_err(), "{leaf}");
19421        }
19422        for leaf in [
19423            r#"<w:helpText w:type="text"/>"#,
19424            r#"<w:statusText w:type="autoText"/>"#,
19425            r#"<w:helpText/>"#,
19426        ] {
19427            let paragraph = parse_paragraph(&format!(
19428                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{leaf}<w:textInput/></w:ffData></w:fldChar></w:r><w:r><w:instrText> FORMTEXT </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19429            ));
19430            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19431                panic!("legacy field")
19432            };
19433            assert!(field.validate_legacy_form_owner().is_ok(), "{leaf}");
19434        }
19435    }
19436
19437    #[test]
19438    fn legacy_form_schema_facets_bound_strings_ranges_and_dropdown_cardinality() {
19439        let too_long_name = "n".repeat(21);
19440        let too_long_macro = "m".repeat(34);
19441        let too_long_string = "s".repeat(256);
19442        let too_many_entries = (0..26)
19443            .map(|index| format!(r#"<w:listEntry w:val="{index}"/>"#))
19444            .collect::<String>();
19445        let cases = [
19446            (
19447                format!(r#"<w:name w:val="{too_long_name}"/><w:textInput/>"#),
19448                "FORMTEXT",
19449            ),
19450            (
19451                format!(r#"<w:entryMacro w:val="{too_long_macro}"/><w:textInput/>"#),
19452                "FORMTEXT",
19453            ),
19454            (
19455                format!(r#"<w:textInput><w:default w:val="{too_long_string}"/></w:textInput>"#),
19456                "FORMTEXT",
19457            ),
19458            (
19459                format!(r#"<w:textInput><w:format w:val="{too_long_string}"/></w:textInput>"#),
19460                "FORMTEXT",
19461            ),
19462            (
19463                r#"<w:textInput><w:maxLength w:val="0"/></w:textInput>"#.to_owned(),
19464                "FORMTEXT",
19465            ),
19466            (
19467                r#"<w:textInput><w:maxLength w:val="32768"/></w:textInput>"#.to_owned(),
19468                "FORMTEXT",
19469            ),
19470            (
19471                format!(r#"<w:ddList><w:listEntry w:val="{too_long_string}"/></w:ddList>"#),
19472                "FORMDROPDOWN",
19473            ),
19474            (
19475                format!(r#"<w:ddList>{too_many_entries}</w:ddList>"#),
19476                "FORMDROPDOWN",
19477            ),
19478        ];
19479        for (ff_data, instruction) in cases {
19480            let paragraph = parse_paragraph(&format!(
19481                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19482            ));
19483            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19484                panic!("legacy field")
19485            };
19486            assert!(field.validate_legacy_form_owner().is_err(), "{ff_data}");
19487        }
19488        let boundary_entries = (0..25)
19489            .map(|index| format!(r#"<w:listEntry w:val="{index}"/>"#))
19490            .collect::<String>();
19491        for (ff_data, instruction) in [
19492            (
19493                format!(
19494                    r#"<w:name w:val="{}"/><w:entryMacro w:val="{}"/><w:textInput><w:default w:val="{}"/><w:maxLength w:val="32767"/><w:format w:val="{}"/></w:textInput>"#,
19495                    "n".repeat(20),
19496                    "m".repeat(33),
19497                    "s".repeat(255),
19498                    "f".repeat(64),
19499                ),
19500                "FORMTEXT",
19501            ),
19502            (
19503                format!(r#"<w:ddList>{boundary_entries}</w:ddList>"#),
19504                "FORMDROPDOWN",
19505            ),
19506        ] {
19507            let paragraph = parse_paragraph(&format!(
19508                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19509            ));
19510            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19511                panic!("legacy field")
19512            };
19513            assert!(field.validate_legacy_form_owner().is_ok(), "{ff_data}");
19514        }
19515    }
19516
19517    #[test]
19518    fn legacy_form_name_and_format_use_schema_maxima() {
19519        for (ff_data, instruction) in [
19520            (
19521                format!(r#"<w:name w:val="{}"/><w:textInput/>"#, "n".repeat(21)),
19522                "FORMTEXT",
19523            ),
19524            (
19525                format!(
19526                    r#"<w:textInput><w:format w:val="{}"/></w:textInput>"#,
19527                    "f".repeat(65)
19528                ),
19529                "FORMTEXT",
19530            ),
19531        ] {
19532            let paragraph = parse_paragraph(&format!(
19533                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19534            ));
19535            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19536                panic!("legacy field")
19537            };
19538            assert!(field.validate_legacy_form_owner().is_err(), "{ff_data}");
19539        }
19540
19541        let boundary = format!(
19542            r#"<w:name w:val="{}"/><w:textInput><w:format w:val="{}"/></w:textInput>"#,
19543            "n".repeat(20),
19544            "f".repeat(64)
19545        );
19546        let paragraph = parse_paragraph(&format!(
19547            r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{boundary}</w:ffData></w:fldChar></w:r><w:r><w:instrText> FORMTEXT </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19548        ));
19549        let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19550            panic!("legacy field")
19551        };
19552        assert!(field.validate_legacy_form_owner().is_ok());
19553    }
19554
19555    #[test]
19556    fn legacy_help_and_status_values_obey_schema_maxima() {
19557        for (leaf, max) in [("helpText", 255), ("statusText", 140)] {
19558            for (length, valid) in [(max, true), (max + 1, false)] {
19559                let value = "v".repeat(length);
19560                let paragraph = parse_paragraph(&format!(
19561                    r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData><w:{leaf} w:type="text" w:val="{value}"/><w:textInput/></w:ffData></w:fldChar></w:r><w:r><w:instrText> FORMTEXT </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19562                ));
19563                let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19564                    panic!("legacy field")
19565                };
19566                assert_eq!(
19567                    field.validate_legacy_form_owner().is_ok(),
19568                    valid,
19569                    "{leaf} {length}"
19570                );
19571            }
19572        }
19573    }
19574
19575    #[test]
19576    fn dropdown_default_index_is_validated_even_when_result_is_present() {
19577        let entries = (0..25)
19578            .map(|index| format!(r#"<w:listEntry w:val="{index}"/>"#))
19579            .collect::<String>();
19580        let paragraph = parse_paragraph(&format!(
19581            r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData><w:ddList><w:result w:val="0"/><w:default w:val="25"/>{entries}</w:ddList></w:ffData></w:fldChar></w:r><w:r><w:instrText> FORMDROPDOWN </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19582        ));
19583        let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19584            panic!("legacy field")
19585        };
19586        assert!(field.validate_legacy_form_owner().is_err());
19587    }
19588
19589    #[test]
19590    fn checkbox_requires_exactly_one_size_choice() {
19591        for ff_data in [
19592            r#"<w:checkBox/>"#,
19593            r#"<w:checkBox><w:default/></w:checkBox>"#,
19594        ] {
19595            let paragraph = parse_paragraph(&format!(
19596                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> FORMCHECKBOX </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19597            ));
19598            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19599                panic!("legacy field")
19600            };
19601            assert!(field.validate_legacy_form_owner().is_err(), "{ff_data}");
19602        }
19603    }
19604
19605    #[test]
19606    fn cross_kind_known_legacy_form_children_are_rejected() {
19607        for (ff_data, instruction) in [
19608            (r#"<w:textInput><w:checked/></w:textInput>"#, "FORMTEXT"),
19609            (
19610                r#"<w:checkBox><w:sizeAuto/><w:listEntry w:val="foreign"/></w:checkBox>"#,
19611                "FORMCHECKBOX",
19612            ),
19613            (
19614                r#"<w:ddList><w:format w:val="foreign"/><w:listEntry w:val="one"/></w:ddList>"#,
19615                "FORMDROPDOWN",
19616            ),
19617        ] {
19618            let paragraph = parse_paragraph(&format!(
19619                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19620            ));
19621            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19622                panic!("legacy field")
19623            };
19624            assert!(field.validate_legacy_form_owner().is_err(), "{ff_data}");
19625        }
19626    }
19627
19628    #[test]
19629    fn legacy_form_element_only_containers_reject_character_data() {
19630        for (ff_data, instruction) in [
19631            (r#"text<w:textInput/>"#, "FORMTEXT"),
19632            (r#"<w:textInput>text</w:textInput>"#, "FORMTEXT"),
19633            (
19634                r#"<w:checkBox><w:sizeAuto/><![CDATA[text]]></w:checkBox>"#,
19635                "FORMCHECKBOX",
19636            ),
19637            (
19638                r#"<w:ddList>&amp;<w:listEntry w:val="one"/></w:ddList>"#,
19639                "FORMDROPDOWN",
19640            ),
19641        ] {
19642            let paragraph = parse_paragraph(&format!(
19643                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19644            ));
19645            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19646                panic!("legacy field")
19647            };
19648            assert!(field.validate_legacy_form_owner().is_err(), "{ff_data}");
19649        }
19650    }
19651
19652    #[test]
19653    fn legacy_form_rewriter_targets_only_direct_ffdata_owner() {
19654        let mut paragraph = parse_paragraph(concat!(
19655            r#"<w:r xmlns:x="urn:producer"><w:fldChar w:fldCharType="begin">"#,
19656            r#"<x:producer><w:ffData><w:textInput><w:default w:val="decoy"/></w:textInput></w:ffData></x:producer>"#,
19657            r#"<w:ffData><w:textInput><w:default w:val="old"/></w:textInput></w:ffData>"#,
19658            r#"</w:fldChar></w:r><w:r><w:instrText> FORMTEXT </w:instrText></w:r>"#,
19659            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
19660        ));
19661        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
19662            panic!("legacy field")
19663        };
19664        field
19665            .set_legacy_form_value(LegacyFormFieldValue::Text("new".to_owned()))
19666            .unwrap();
19667        let output = serialized_paragraph(&paragraph);
19668        assert!(
19669            output.contains(
19670                r#"<x:producer><w:ffData><w:textInput><w:default w:val="decoy"/></w:textInput></w:ffData></x:producer>"#
19671            ),
19672            "{output}"
19673        );
19674        assert!(
19675            output.contains(
19676                r#"<w:ffData><w:textInput><w:default w:val="new"/></w:textInput></w:ffData>"#
19677            ),
19678            "{output}"
19679        );
19680    }
19681
19682    #[test]
19683    fn known_legacy_form_vocabulary_at_wrong_container_levels_is_rejected() {
19684        for (ff_data, instruction) in [
19685            (r#"<w:checked/><w:textInput/>"#, "FORMTEXT"),
19686            (
19687                r#"<w:textInput><w:name w:val="nested"/></w:textInput>"#,
19688                "FORMTEXT",
19689            ),
19690            (
19691                r#"<w:textInput><w:checkBox><w:sizeAuto/></w:checkBox></w:textInput>"#,
19692                "FORMTEXT",
19693            ),
19694            (r#"<w:textInput><w:ffData/></w:textInput>"#, "FORMTEXT"),
19695        ] {
19696            let paragraph = parse_paragraph(&format!(
19697                r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData>{ff_data}</w:ffData></w:fldChar></w:r><w:r><w:instrText> {instruction} </w:instrText></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
19698            ));
19699            let RunContent::Field(field) = &paragraph.runs[0].content[0] else {
19700                panic!("legacy field")
19701            };
19702            assert!(field.validate_legacy_form_owner().is_err(), "{ff_data}");
19703        }
19704    }
19705
19706    #[test]
19707    fn empty_text_input_form_can_be_expanded_for_value_mutation() {
19708        let mut paragraph = parse_paragraph(concat!(
19709            r#"<w:r><w:fldChar w:fldCharType="begin"><w:ffData><w:textInput/></w:ffData></w:fldChar></w:r>"#,
19710            r#"<w:r><w:instrText> FORMTEXT </w:instrText></w:r>"#,
19711            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>old</w:t></w:r>"#,
19712            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
19713        ));
19714        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
19715            panic!("legacy field")
19716        };
19717        assert_eq!(
19718            field.legacy_form.as_ref().unwrap().value,
19719            LegacyFormFieldValue::Text("old".to_owned())
19720        );
19721        field
19722            .set_legacy_form_value(LegacyFormFieldValue::Text("new".to_owned()))
19723            .unwrap();
19724        let output = serialized_paragraph(&paragraph);
19725        assert!(
19726            output.contains(
19727                r#"<w:ffData><w:textInput><w:default xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" w:val="new"/></w:textInput></w:ffData>"#
19728            ),
19729            "{output}"
19730        );
19731    }
19732
19733    #[test]
19734    fn split_run_keeps_properties_space_and_later_markers_in_order() {
19735        let mut paragraph = parse_paragraph(concat!(
19736            r#"<w:bookmarkStart w:id="1" w:name="mark"/>"#,
19737            r#"<w:r><w:rPr><w:b/></w:rPr><w:t>Hello world</w:t></w:r>"#,
19738            r#"<w:bookmarkEnd w:id="1"/><w:r><w:t>!</w:t></w:r>"#,
19739        ));
19740        paragraph.split_run(0, 6).unwrap();
19741
19742        let texts = paragraph.runs.iter().map(CT_R::text).collect::<Vec<_>>();
19743        assert_eq!(texts, ["Hello ", "world", "!"]);
19744        assert_eq!(paragraph.runs[0].properties, paragraph.runs[1].properties);
19745        let output = serialized_paragraph(&paragraph);
19746        assert!(
19747            output.contains(r#"<w:t xml:space="preserve">Hello </w:t>"#),
19748            "{output}"
19749        );
19750        let world = output.find(">world<").unwrap();
19751        let bookmark_end = output.find("bookmarkEnd").unwrap();
19752        let exclamation = output.find(">!<").unwrap();
19753        assert!(
19754            world < bookmark_end && bookmark_end < exclamation,
19755            "{output}"
19756        );
19757    }
19758
19759    #[test]
19760    fn split_run_keeps_both_parts_in_the_hyperlink_with_raw_children_in_order() {
19761        let mut paragraph = parse_paragraph(concat!(
19762            r#"<w:hyperlink w:anchor="target"><w:r><w:t>ab</w:t><x:mark/><w:t>cd</w:t><x:end/></w:r></w:hyperlink>"#,
19763            r#"<w:r><w:t>after</w:t></w:r>"#,
19764        ));
19765        paragraph.split_run(0, 3).unwrap();
19766
19767        let texts = paragraph.runs.iter().map(CT_R::text).collect::<Vec<_>>();
19768        assert_eq!(texts, ["abc", "d", "after"]);
19769        let hyperlink = &paragraph.hyperlinks[0];
19770        assert_eq!((hyperlink.run_start, hyperlink.run_end), (0, 2));
19771        let output = serialized_paragraph(&paragraph);
19772        assert_eq!(output.matches("<w:hyperlink").count(), 1, "{output}");
19773        assert!(
19774            output.contains(concat!(
19775                r#"<w:r><w:t>ab</w:t><x:mark/><w:t>c</w:t></w:r>"#,
19776                r#"<w:r><w:t>d</w:t><x:end/></w:r></w:hyperlink>"#,
19777            )),
19778            "{output}"
19779        );
19780    }
19781
19782    #[test]
19783    fn split_run_rejects_missing_runs_edge_offsets_and_field_results_unchanged() {
19784        let mut paragraph = parse_paragraph(concat!(
19785            r#"<w:r><w:t>Page </w:t></w:r>"#,
19786            r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r>"#,
19787            r#"<w:r><w:instrText> PAGE </w:instrText></w:r>"#,
19788            r#"<w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>12</w:t></w:r>"#,
19789            r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
19790        ));
19791        let before = serialized_paragraph(&paragraph);
19792        let field_run = paragraph
19793            .runs
19794            .iter()
19795            .position(|run| {
19796                run.content
19797                    .iter()
19798                    .any(|content| matches!(content, RunContent::Field(_)))
19799            })
19800            .unwrap();
19801        assert_eq!(paragraph.runs[field_run].text(), "12");
19802        assert_eq!(paragraph.split_run(field_run, 0), Ok(field_run));
19803        assert_eq!(
19804            paragraph.split_run(field_run, 1),
19805            Err(RunSplitError::OffsetOutOfRange { offset: 1, len: 0 })
19806        );
19807        assert_eq!(paragraph.split_run(0, 0), Ok(0));
19808        assert_eq!(paragraph.split_run(0, 5), Ok(1));
19809        assert_eq!(
19810            paragraph.split_run(0, 6),
19811            Err(RunSplitError::OffsetOutOfRange { offset: 6, len: 5 })
19812        );
19813        let run_count = paragraph.runs.len();
19814        assert_eq!(
19815            paragraph.split_run(run_count, 1),
19816            Err(RunSplitError::RunOutOfRange {
19817                run_index: run_count,
19818                run_count
19819            })
19820        );
19821        assert_eq!(serialized_paragraph(&paragraph), before);
19822    }
19823
19824    #[test]
19825    fn symbols_and_special_characters_reopen_in_source_order() {
19826        let paragraph = parse_paragraph(concat!(
19827            r#"<w:r><w:t>a</w:t><w:sym w:font="Wingdings" w:char="F0FC"/><w:cr/>"#,
19828            r#"<w:noBreakHyphen/><w:tab/><w:softHyphen/>"#,
19829            r#"<w:ptab w:alignment="right" w:relativeTo="margin" w:leader="dot"/>"#,
19830            r#"<w:lastRenderedPageBreak/><w:br/><w:t>b</w:t></w:r>"#,
19831        ));
19832        let content = &paragraph.runs[0].content;
19833        assert_eq!(
19834            content[1],
19835            RunContent::Symbol {
19836                font: "Wingdings".to_owned(),
19837                char_code: 0xF0FC,
19838            }
19839        );
19840        assert_eq!(
19841            content[2],
19842            RunContent::SpecialCharacter(SpecialCharacter::CarriageReturn)
19843        );
19844        assert_eq!(
19845            content[3],
19846            RunContent::SpecialCharacter(SpecialCharacter::NoBreakHyphen)
19847        );
19848        assert_eq!(content[4], RunContent::Tab);
19849        assert_eq!(
19850            content[5],
19851            RunContent::SpecialCharacter(SpecialCharacter::SoftHyphen)
19852        );
19853        assert_eq!(
19854            content[6],
19855            RunContent::SpecialCharacter(SpecialCharacter::PositionalTab {
19856                alignment: ST_PTabAlignment::Right,
19857                relative_to: ST_PTabRelativeTo::Margin,
19858                leader: ST_PTabLeader::Dot,
19859            })
19860        );
19861
19862        let output = serialized_paragraph(&paragraph);
19863        let ordered = [
19864            "<w:t>a</w:t>",
19865            r#"<w:sym w:font="Wingdings" w:char="F0FC"/>"#,
19866            "<w:cr/>",
19867            "<w:noBreakHyphen/>",
19868            "<w:tab/>",
19869            "<w:softHyphen/>",
19870            r#"<w:ptab w:alignment="right" w:relativeTo="margin" w:leader="dot"/>"#,
19871            "<w:lastRenderedPageBreak/>",
19872            "<w:br/>",
19873            "<w:t>b</w:t>",
19874        ]
19875        .map(|needle| {
19876            output
19877                .find(needle)
19878                .unwrap_or_else(|| panic!("{needle} in {output}"))
19879        });
19880        assert!(ordered.windows(2).all(|pair| pair[0] < pair[1]), "{output}");
19881    }
19882
19883    #[test]
19884    fn a_special_character_outside_the_modeled_shape_stays_raw() {
19885        let paragraph = parse_paragraph(concat!(
19886            r#"<w:r><w:sym w:font="Wingdings" w:char="zzzz"/>"#,
19887            r#"<w:sym w:font="Wingdings"/>"#,
19888            r#"<w:ptab w:alignment="right" w:leader="dot"/>"#,
19889            r#"<w:cr w:producerFlag="1"/></w:r>"#,
19890        ));
19891        assert!(paragraph.runs[0].content.is_empty());
19892        assert_eq!(paragraph.runs[0].extra_xml.len(), 4);
19893
19894        let output = serialized_paragraph(&paragraph);
19895        for retained in [
19896            r#"w:char="zzzz""#,
19897            r#"<w:sym w:font="Wingdings"/>"#,
19898            r#"<w:ptab w:alignment="right" w:leader="dot"/>"#,
19899            r#"<w:cr w:producerFlag="1"/>"#,
19900        ] {
19901            assert!(
19902                output.contains(retained),
19903                "{retained} missing from {output}"
19904            );
19905        }
19906    }
19907
19908    #[test]
19909    fn split_run_partitions_zero_width_mixed_content_without_reordering() {
19910        let mut paragraph = parse_paragraph(concat!(
19911            r#"<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape"><w:t>ab</w:t><w:tab/><w:br/><w:fldChar w:fldCharType="begin"/><w:instrText> PAGE </w:instrText>"#,
19912            r#"<mc:AlternateContent><mc:Choice Requires="wps">"#,
19913            r#"<w:drawing><wp:anchor behindDoc="0">"#,
19914            r#"<wp:positionH relativeFrom="column"><wp:posOffset>0</wp:posOffset></wp:positionH>"#,
19915            r#"<wp:positionV relativeFrom="paragraph"><wp:posOffset>0</wp:posOffset></wp:positionV>"#,
19916            r#"<wp:extent cx="914400" cy="457200"/><wp:docPr id="1" name="Shape"/>"#,
19917            r#"<a:graphic><a:graphicData><wps:wsp><wps:spPr><a:prstGeom prst="rect"/></wps:spPr></wps:wsp></a:graphicData></a:graphic>"#,
19918            r#"</wp:anchor></w:drawing></mc:Choice>"#,
19919            r#"<mc:Fallback><w:pict><v:rect/></w:pict></mc:Fallback></mc:AlternateContent>"#,
19920            r#"<w:sym w:font="Wingdings" w:char="F0FC"/><x:raw/><w:t>cd</w:t></w:r>"#,
19921        ));
19922        assert_eq!(paragraph.runs.len(), 1);
19923        assert_eq!(paragraph.runs[0].alt_drawings.len(), 1);
19924
19925        assert_eq!(paragraph.split_run(0, 2), Ok(1));
19926        assert_eq!(paragraph.runs.len(), 2);
19927        assert_eq!(paragraph.runs[0].text(), "ab");
19928        assert_eq!(paragraph.runs[1].text(), "\t\ncd");
19929        assert!(paragraph.runs[0].alt_drawings.is_empty());
19930        assert_eq!(paragraph.runs[1].alt_drawings.len(), 1);
19931
19932        let output = serialized_paragraph(&paragraph);
19933        assert_eq!(
19934            output.matches("<mc:AlternateContent").count(),
19935            1,
19936            "{output}"
19937        );
19938        let ordered = [
19939            "<w:t>ab</w:t>",
19940            "<w:tab/>",
19941            "<w:br/>",
19942            "fldCharType=\"begin\"",
19943            "<w:instrText> PAGE </w:instrText>",
19944            "<mc:AlternateContent",
19945            "<w:sym",
19946            "<x:raw/>",
19947            "<w:t>cd</w:t>",
19948        ]
19949        .map(|needle| output.find(needle).unwrap());
19950        assert!(ordered.windows(2).all(|pair| pair[0] < pair[1]), "{output}");
19951    }
19952    #[test]
19953    fn authored_simple_and_complex_fields_reopen_with_identical_semantics() {
19954        for form in [FieldForm::Simple, FieldForm::Complex] {
19955            let instruction = FieldInstruction::new(
19956                "REF",
19957                vec![FieldArgument::Text("target name".into())],
19958                vec![],
19959            )
19960            .unwrap();
19961            let field =
19962                Field::from_instruction(instruction, form, vec![CT_R::new("cached")]).unwrap();
19963            let mut paragraph = CT_P::new();
19964            paragraph.runs.push(field_run(field, None));
19965            let output = serialized_paragraph(&paragraph);
19966            let reopened = CT_P::from_xml_fragment(
19967                output
19968                    .replacen(
19969                        "<w:p>",
19970                        &format!("<w:p xmlns:w=\"{}\">", crate::namespace::W_NS),
19971                        1,
19972                    )
19973                    .as_bytes(),
19974            )
19975            .unwrap();
19976            let field = parsed_field(&reopened, 0);
19977            assert_eq!(field.form(), form);
19978            assert_eq!(
19979                field.instruction.arguments,
19980                vec![FieldArgument::Text("target name".into())]
19981            );
19982            assert_eq!(field.cached_result, "cached");
19983        }
19984    }
19985
19986    #[test]
19987    fn authored_nested_instruction_fields_preserve_operand_order() {
19988        let inner = Field::from_raw(
19989            "MERGEFIELD name",
19990            FieldForm::Complex,
19991            vec![CT_R::new("Ada")],
19992        )
19993        .unwrap();
19994        let instruction = FieldInstruction::new(
19995            "IF",
19996            vec![
19997                FieldArgument::Nested(Box::new(inner)),
19998                FieldArgument::Text("=".into()),
19999                FieldArgument::Text("Ada".into()),
20000                FieldArgument::Text("yes".into()),
20001                FieldArgument::Text("no".into()),
20002            ],
20003            vec![],
20004        )
20005        .unwrap();
20006        assert!(Field::from_instruction(instruction.clone(), FieldForm::Simple, vec![]).is_err());
20007        let field =
20008            Field::from_instruction(instruction, FieldForm::Complex, vec![CT_R::new("yes")])
20009                .unwrap();
20010        let mut paragraph = CT_P::new();
20011        paragraph.runs.push(field_run(field, None));
20012        let output = serialized_paragraph(&paragraph);
20013        assert_eq!(output.matches("fldCharType=\"begin\"").count(), 2);
20014        let reopened = CT_P::from_xml_fragment(
20015            output
20016                .replacen(
20017                    "<w:p>",
20018                    &format!("<w:p xmlns:w=\"{}\">", crate::namespace::W_NS),
20019                    1,
20020                )
20021                .as_bytes(),
20022        )
20023        .unwrap();
20024        let field = parsed_field(&reopened, 0);
20025        assert_eq!(
20026            field.nested_fields_in_source_order()[0].instruction.name,
20027            "MERGEFIELD"
20028        );
20029        assert_eq!(field.cached_result, "yes");
20030    }
20031
20032    #[test]
20033    fn field_lock_and_dirty_preserve_absent_false_and_true() {
20034        for form in [FieldForm::Simple, FieldForm::Complex] {
20035            for value in [None, Some(false), Some(true)] {
20036                let mut field = Field::from_raw("UNKNOWN", form, vec![CT_R::new("cache")]).unwrap();
20037                field.set_locked(value);
20038                field.dirty = value;
20039                let mut paragraph = CT_P::new();
20040                paragraph.runs.push(field_run(field, None));
20041                let output = serialized_paragraph(&paragraph);
20042                let reopened = CT_P::from_xml_fragment(
20043                    output
20044                        .replacen(
20045                            "<w:p>",
20046                            &format!("<w:p xmlns:w=\"{}\">", crate::namespace::W_NS),
20047                            1,
20048                        )
20049                        .as_bytes(),
20050                )
20051                .unwrap();
20052                let field = parsed_field(&reopened, 0);
20053                assert_eq!(field.locked(), value);
20054                assert_eq!(field.dirty, value);
20055            }
20056        }
20057    }
20058
20059    #[test]
20060    fn typed_field_arguments_cannot_inject_instruction_tokens() {
20061        for value in [
20062            "",
20063            "\\h",
20064            "two words",
20065            "quoted\"name",
20066            "path\\name",
20067            "x\" \\h \"y",
20068        ] {
20069            let instruction =
20070                FieldInstruction::new("REF", vec![FieldArgument::Text(value.into())], vec![])
20071                    .unwrap();
20072            let field = Field::from_instruction(instruction, FieldForm::Simple, vec![]).unwrap();
20073            let mut paragraph = CT_P::new();
20074            paragraph.runs.push(field_run(field, None));
20075            let reopened = CT_P::from_xml_fragment(
20076                serialized_paragraph(&paragraph)
20077                    .replacen(
20078                        "<w:p>",
20079                        &format!("<w:p xmlns:w=\"{}\">", crate::namespace::W_NS),
20080                        1,
20081                    )
20082                    .as_bytes(),
20083            )
20084            .unwrap();
20085            let field = parsed_field(&reopened, 0);
20086            assert_eq!(
20087                field.instruction.arguments,
20088                vec![FieldArgument::Text(value.into())]
20089            );
20090            assert!(field.instruction.switches.is_empty());
20091        }
20092        for raw in ["", " PAGE \"unterminated", "\\h", "PA\0GE"] {
20093            assert!(
20094                Field::from_raw(raw, FieldForm::Simple, vec![]).is_err(),
20095                "{raw:?}"
20096            );
20097        }
20098    }
20099
20100    #[test]
20101    fn visibility_parsed_nontext_field_results_keep_owned_order_and_current_cache() {
20102        let mut failures = Vec::new();
20103        for (name, result, expected_cache) in [
20104            ("tab", "<w:tab/>", "\t"),
20105            ("break", "<w:br/>", "\n"),
20106            ("symbol", r#"<w:sym w:font="Wingdings" w:char="F0FC"/>"#, ""),
20107            ("cr", "<w:cr/>", "\n"),
20108            ("no-break-hyphen", "<w:noBreakHyphen/>", ""),
20109            (
20110                "positional-tab",
20111                r#"<w:ptab w:alignment="left" w:relativeTo="margin" w:leader="none"/>"#,
20112                "",
20113            ),
20114        ] {
20115            for form in [FieldForm::Simple, FieldForm::Complex] {
20116                let source = match form {
20117                    FieldForm::Simple => format!(
20118                        r#"<w:fldSimple w:instr=" PAGE "><w:r>{result}</w:r></w:fldSimple>"#
20119                    ),
20120                    FieldForm::Complex => format!(
20121                        r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText> PAGE </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r>{result}</w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
20122                    ),
20123                };
20124                let paragraph = parse_paragraph(&source);
20125                let field = parsed_field(&paragraph, 0);
20126                assert_eq!(field.cached_result, expected_cache, "{name} {form:?}");
20127                let output = serialized_paragraph(&paragraph);
20128                assert_eq!(output, format!("<w:p>{source}</w:p>"));
20129                let reopened = parse_paragraph(&source);
20130                assert_eq!(paragraph, reopened);
20131                let Some(runs) = field.cached_result_runs() else {
20132                    failures.push(format!("{name} {form:?}: typed result absent"));
20133                    continue;
20134                };
20135                let content = runs.iter().flat_map(|run| &run.content).collect::<Vec<_>>();
20136                assert_eq!(content.len(), 1, "{name} {form:?}: {content:?}");
20137                assert!(match name {
20138                    "tab" => matches!(content[0], RunContent::Tab),
20139                    "break" => matches!(content[0], RunContent::Break(BreakType::Line)),
20140                    "symbol" => matches!(content[0], RunContent::Symbol { .. }),
20141                    "cr" => matches!(
20142                        content[0],
20143                        RunContent::SpecialCharacter(SpecialCharacter::CarriageReturn)
20144                    ),
20145                    "no-break-hyphen" => matches!(
20146                        content[0],
20147                        RunContent::SpecialCharacter(SpecialCharacter::NoBreakHyphen)
20148                    ),
20149                    "positional-tab" => matches!(
20150                        content[0],
20151                        RunContent::SpecialCharacter(SpecialCharacter::PositionalTab { .. })
20152                    ),
20153                    _ => unreachable!(),
20154                });
20155                let mut changed = field.clone();
20156                changed.cached_result = "changed".into();
20157                assert!(changed.cached_result_runs().is_none());
20158                changed.cached_result = " ".into();
20159                assert!(changed.cached_result_runs().is_none());
20160                assert_eq!(field.cached_result, expected_cache);
20161            }
20162        }
20163        assert!(failures.is_empty(), "{}", failures.join("\n"));
20164    }
20165
20166    #[test]
20167    fn visibility_deleted_simple_and_nested_results_keep_public_cache_and_owned_graph() {
20168        for sibling in ["", "<w:r><w:t>accepted</w:t></w:r>"] {
20169            let child = format!(
20170                r#"<w:fldSimple w:instr=" PAGE "><w:r><w:delText>gone</w:delText></w:r>{sibling}</w:fldSimple>"#
20171            );
20172            for form in [None, Some(FieldForm::Simple), Some(FieldForm::Complex)] {
20173                let source = match form {
20174                    None => child.clone(),
20175                    Some(FieldForm::Simple) => {
20176                        format!(r#"<w:fldSimple w:instr=" UNKNOWN ">{child}</w:fldSimple>"#)
20177                    }
20178                    Some(FieldForm::Complex) => format!(
20179                        r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText> UNKNOWN </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r>{child}<w:r><w:fldChar w:fldCharType="end"/></w:r>"#
20180                    ),
20181                };
20182                let paragraph = parse_paragraph(&source);
20183                let field = parsed_field(&paragraph, 0);
20184                assert_eq!(
20185                    field.cached_result,
20186                    if sibling.is_empty() {
20187                        "gone"
20188                    } else {
20189                        "goneaccepted"
20190                    }
20191                );
20192                let mut pending = vec![field];
20193                let mut deleted = 0;
20194                let mut accepted = 0;
20195                while let Some(field) = pending.pop() {
20196                    assert!(
20197                        field.cached_fields.is_empty(),
20198                        "retained accepted graph owns each child once"
20199                    );
20200                    for content in field
20201                        .cached_result_runs()
20202                        .expect("consistent accepted result graph")
20203                        .iter()
20204                        .flat_map(|run| &run.content)
20205                    {
20206                        match content {
20207                            RunContent::DeletedText(text) => {
20208                                assert_eq!(text.text, "gone");
20209                                deleted += 1;
20210                            }
20211                            RunContent::Text(text) => {
20212                                assert_eq!(text.text, "accepted");
20213                                accepted += 1;
20214                            }
20215                            RunContent::Field(child) => pending.push(child),
20216                            _ => panic!("unexpected result content"),
20217                        }
20218                    }
20219                }
20220                assert_eq!(deleted, 1);
20221                assert_eq!(accepted, usize::from(!sibling.is_empty()));
20222                assert_eq!(
20223                    serialized_paragraph(&paragraph),
20224                    format!("<w:p>{source}</w:p>")
20225                );
20226                assert_eq!(paragraph, parse_paragraph(&source));
20227                let mut changed = field.clone();
20228                changed.cached_result = "current".into();
20229                assert!(changed.cached_result_runs().is_none());
20230                changed.cached_result = field.cached_result.clone();
20231                assert!(changed.cached_result_runs().is_some());
20232            }
20233        }
20234        for source in [
20235            r#"<w:fldSimple w:instr=" PAGE "><w:r><w:softHyphen/></w:r></w:fldSimple>"#,
20236            r#"<w:r><w:fldChar w:fldCharType="begin"/><w:instrText> PAGE </w:instrText><w:fldChar w:fldCharType="separate"/><w:softHyphen/><w:fldChar w:fldCharType="end"/></w:r>"#,
20237        ] {
20238            let paragraph = parse_paragraph(source);
20239            let field = parsed_field(&paragraph, 0);
20240            assert_eq!(field.cached_result, "");
20241            assert!(field.cached_result_runs().is_none());
20242            assert_eq!(
20243                serialized_paragraph(&paragraph),
20244                format!("<w:p>{source}</w:p>")
20245            );
20246        }
20247    }
20248
20249    #[test]
20250    fn visibility_parsed_nontext_field_drawing_results_have_actual_typed_payload() {
20251        let drawing = CT_Drawing::inline(crate::drawing::CT_Inline::new("rId1", 914_400, 914_400));
20252        let mut writer = Writer::new(Vec::new());
20253        drawing.to_xml(&mut writer).unwrap();
20254        // The writer uses the main document's inherited DrawingML declarations.
20255        // Close that actual producer scope for this standalone paragraph fixture.
20256        let result = String::from_utf8(writer.into_inner()).unwrap().replacen(
20257            "<w:drawing>",
20258            &format!(
20259                r#"<w:drawing xmlns:wp="{}" xmlns:a="{}" xmlns:pic="{}" xmlns:r="{}">"#,
20260                crate::drawing::drawing_ns::WP,
20261                crate::drawing::drawing_ns::A,
20262                crate::drawing::drawing_ns::PIC,
20263                crate::drawing::drawing_ns::R,
20264            ),
20265            1,
20266        );
20267        let positive = parse_paragraph(&format!("<w:r>{result}</w:r>"));
20268        assert!(
20269            matches!(positive.runs[0].content[0], RunContent::Drawing(ref value) if value.inline.as_ref().is_some_and(|inline| inline.embed_id == "rId1"))
20270        );
20271        let mut failures = Vec::new();
20272        for form in [FieldForm::Simple, FieldForm::Complex] {
20273            let source = match form {
20274                FieldForm::Simple => {
20275                    format!(r#"<w:fldSimple w:instr=" PAGE "><w:r>{result}</w:r></w:fldSimple>"#)
20276                }
20277                FieldForm::Complex => format!(
20278                    r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText> PAGE </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r>{result}</w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
20279                ),
20280            };
20281            let paragraph = parse_paragraph(&source);
20282            let field = parsed_field(&paragraph, 0);
20283            assert_eq!(field.cached_result, "");
20284            assert_eq!(
20285                serialized_paragraph(&paragraph),
20286                format!("<w:p>{source}</w:p>")
20287            );
20288            let Some(runs) = field.cached_result_runs() else {
20289                failures.push(format!("{form:?}: actual drawing result absent"));
20290                continue;
20291            };
20292            assert!(runs.iter().flat_map(|run| &run.content).any(|item| matches!(item, RunContent::Drawing(value) if value.inline.as_ref().is_some_and(|inline| inline.embed_id == "rId1"))));
20293            let mut changed = field.clone();
20294            changed.cached_result = " ".into();
20295            assert!(changed.cached_result_runs().is_none());
20296        }
20297        assert!(failures.is_empty(), "{}", failures.join("\n"));
20298    }
20299
20300    #[test]
20301    fn visibility_parsed_nontext_field_results_respect_producer_namespaces() {
20302        let mut failures = Vec::new();
20303        for prefix in ["q:", ""] {
20304            for simple in [true, false] {
20305                let result = format!("<{prefix}r><w:tab/><{prefix}tab/></{prefix}r>");
20306                let source = if simple {
20307                    format!(r#"<{prefix}fldSimple a:instr=" PAGE ">{result}</{prefix}fldSimple>"#)
20308                } else {
20309                    format!(
20310                        r#"<{prefix}r><{prefix}fldChar a:fldCharType="begin"/></{prefix}r><{prefix}r><{prefix}instrText> PAGE </{prefix}instrText></{prefix}r><{prefix}r><{prefix}fldChar a:fldCharType="separate"/></{prefix}r>{result}<{prefix}r><{prefix}fldChar a:fldCharType="end"/></{prefix}r>"#
20311                    )
20312                };
20313                let binding = if prefix.is_empty() {
20314                    "xmlns"
20315                } else {
20316                    "xmlns:q"
20317                };
20318                let xml = format!(
20319                    r#"<{prefix}p {binding}="{}" xmlns:w="urn:foreign" xmlns:a="{}">{source}</{prefix}p>"#,
20320                    crate::namespace::W_NS,
20321                    crate::namespace::W_NS
20322                );
20323                let paragraph = CT_P::from_xml_fragment(xml.as_bytes()).unwrap();
20324                let field = parsed_field(&paragraph, 0);
20325                assert_eq!(field.cached_result, "\t", "{prefix:?} simple={simple}");
20326                let before = serialized_paragraph(&paragraph);
20327                let FieldSource::Parsed {
20328                    raw_xml,
20329                    word_prefixes,
20330                    ..
20331                } = &field.source
20332                else {
20333                    panic!("parsed field owner");
20334                };
20335                assert!(
20336                    word_prefixes
20337                        .iter()
20338                        .any(|value| value == prefix.trim_end_matches(':'))
20339                );
20340                assert!(!word_prefixes.iter().any(|value| value == "w"));
20341                assert!(String::from_utf8_lossy(raw_xml).contains("<w:tab/>"));
20342                let (original, replacement) = field.source_replacement().unwrap().unwrap();
20343                assert_eq!(original, replacement);
20344                if !simple {
20345                    let (projected, _) = parsed_complex_cache_runs(raw_xml, word_prefixes).unwrap();
20346                    let tabs = projected
20347                        .iter()
20348                        .flat_map(|run| &run.content)
20349                        .filter(|item| matches!(item, RunContent::Tab))
20350                        .count();
20351                    if tabs != 1 {
20352                        failures.push(format!("{prefix:?} complex helper admitted {tabs} tabs"));
20353                    }
20354                }
20355                let Some(runs) = field.cached_result_runs() else {
20356                    failures.push(format!("{prefix:?} simple={simple}: typed result absent"));
20357                    continue;
20358                };
20359                assert_eq!(
20360                    runs.iter()
20361                        .flat_map(|run| &run.content)
20362                        .filter(|item| matches!(item, RunContent::Tab))
20363                        .count(),
20364                    1
20365                );
20366                assert_eq!(serialized_paragraph(&paragraph), before);
20367            }
20368        }
20369        assert!(failures.is_empty(), "{}", failures.join("\n"));
20370    }
20371
20372    #[test]
20373    fn visibility_parsed_nontext_field_results_keep_nested_and_shared_owners() {
20374        let mut failures = Vec::new();
20375        for (name, source, expected_cache) in [
20376            (
20377                "nested simple",
20378                r#"<w:fldSimple w:instr=" REF Outer "><w:fldSimple w:instr=" PAGE "><w:r><w:tab/></w:r></w:fldSimple></w:fldSimple>"#,
20379                "\t",
20380            ),
20381            (
20382                "nested complex",
20383                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText> REF Outer </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText> PAGE </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:tab/></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
20384                "\t",
20385            ),
20386            (
20387                "shared physical run",
20388                r#"<w:r><w:t>PRE</w:t><w:fldChar w:fldCharType="begin"/><w:instrText> PAGE </w:instrText><w:fldChar w:fldCharType="separate"/><w:tab/><w:fldChar w:fldCharType="end"/><w:t>POST</w:t></w:r>"#,
20389                "\t",
20390            ),
20391            (
20392                "spanning result runs",
20393                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText> PAGE </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:tab/></w:r><w:r><w:br/></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#,
20394                "\t\n",
20395            ),
20396        ] {
20397            let paragraph = parse_paragraph(source);
20398            let field = paragraph
20399                .runs()
20400                .into_iter()
20401                .flat_map(|run| &run.content)
20402                .find_map(|item| match item {
20403                    RunContent::Field(field) => Some(field),
20404                    _ => None,
20405                })
20406                .expect("complete parsed owner");
20407            assert_eq!(field.cached_result, expected_cache, "{name}");
20408            assert_eq!(
20409                serialized_paragraph(&paragraph),
20410                format!("<w:p>{source}</w:p>")
20411            );
20412            let mut work = vec![field];
20413            let mut controls = Vec::new();
20414            while let Some(current) = work.pop() {
20415                if let Some(runs) = current.cached_result_runs() {
20416                    for item in runs.iter().flat_map(|run| &run.content) {
20417                        match item {
20418                            RunContent::Field(child) => work.push(child),
20419                            RunContent::Tab => controls.push("tab"),
20420                            RunContent::Break(BreakType::Line) => controls.push("break"),
20421                            _ => {}
20422                        }
20423                    }
20424                }
20425            }
20426            let expected = if name == "spanning result runs" {
20427                vec!["tab", "break"]
20428            } else {
20429                vec!["tab"]
20430            };
20431            if controls != expected {
20432                failures.push(format!("{name}: {controls:?}"));
20433            }
20434        }
20435        let incomplete = parse_paragraph(
20436            r#"<w:r><w:fldChar w:fldCharType="begin"/><w:instrText> PAGE </w:instrText><w:fldChar w:fldCharType="separate"/><w:tab/></w:r>"#,
20437        );
20438        assert!(
20439            !incomplete
20440                .runs()
20441                .into_iter()
20442                .flat_map(|run| &run.content)
20443                .any(|item| matches!(item, RunContent::Field(_)))
20444        );
20445        assert!(failures.is_empty(), "{}", failures.join("\n"));
20446    }
20447
20448    #[test]
20449    fn visibility_nontext_nested_cache_mutation_keeps_graph_and_source_identity() {
20450        for child_form in [FieldForm::Simple, FieldForm::Complex] {
20451            let child = match child_form {
20452                FieldForm::Simple => {
20453                    r#"<w:fldSimple w:instr=" PAGE "><w:r><w:tab/></w:r></w:fldSimple>"#
20454                }
20455                FieldForm::Complex => {
20456                    r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText> PAGE </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:tab/></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
20457                }
20458            };
20459            let source = format!(
20460                r#"<w:r producer="keep"><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText> UNKNOWN </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>PRE</w:t></w:r>{child}<w:r><w:t>POST</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
20461            );
20462            let mut paragraph = parse_paragraph(&source);
20463            assert_eq!(
20464                serialized_paragraph(&paragraph),
20465                format!("<w:p>{source}</w:p>")
20466            );
20467            let original = paragraph.clone();
20468            let RunContent::Field(outer) = &mut paragraph.runs[0].content[0] else {
20469                panic!("outer")
20470            };
20471            assert!(outer.cached_fields.is_empty());
20472            assert!(outer.parsed_cached_child_runs().is_some());
20473            let child_owner = outer.cached_fields_in_source_order()[0]
20474                .source_owner_id()
20475                .unwrap();
20476            outer.cached_field_mut(0).unwrap().cached_result = "changed".into();
20477            assert_eq!(outer.cached_result, "PRE\tPOST");
20478            assert!(
20479                outer.cached_result_runs().is_none(),
20480                "stale display must not expose stored graph"
20481            );
20482            assert_eq!(
20483                outer.cached_fields_in_source_order()[0].cached_result,
20484                "changed"
20485            );
20486            assert_eq!(
20487                outer.cached_field_mut(0).unwrap().source_owner_id(),
20488                Some(child_owner)
20489            );
20490            outer.refresh_cached_field_projection();
20491            assert_eq!(outer.cached_result, "PREchangedPOST");
20492            assert!(outer.cached_result_runs().is_some());
20493            let (_, replacement) = outer.source_replacement().unwrap().unwrap();
20494            assert!(
20495                std::str::from_utf8(&replacement)
20496                    .unwrap()
20497                    .contains(r#"producer="keep""#)
20498            );
20499            assert_ne!(paragraph, original);
20500            let edited = serialized_paragraph(&paragraph);
20501            let reopened = parse_paragraph(&edited.replace("<w:p>", "").replace("</w:p>", ""));
20502            let outer = parsed_field(&reopened, 0);
20503            assert_eq!(outer.cached_result, "PREchangedPOST");
20504            assert_eq!(
20505                outer.cached_fields_in_source_order()[0].cached_result,
20506                "changed"
20507            );
20508            assert_eq!(outer.cached_fields_in_source_order()[0].form(), child_form);
20509            assert_eq!(serialized_paragraph(&reopened), edited);
20510
20511            let mut rejected = parse_paragraph(&source);
20512            let RunContent::Field(outer) = &mut rejected.runs[0].content[0] else {
20513                panic!("outer")
20514            };
20515            *outer.cached_field_mut(0).unwrap() =
20516                Field::from_raw("PAGE", child_form, vec![CT_R::new("replacement")]).unwrap();
20517            outer.refresh_cached_field_projection();
20518            assert!(
20519                outer
20520                    .source_replacement()
20521                    .unwrap_err()
20522                    .to_string()
20523                    .contains("source identity")
20524            );
20525        }
20526    }
20527
20528    #[test]
20529    fn visibility_nontext_nested_graph_growth_and_clone_have_one_owned_child_axis() {
20530        fn owned_shape(field: &Field) -> (usize, usize, usize) {
20531            let mut pending = vec![field];
20532            let mut fields = 0;
20533            let mut source_bytes = 0;
20534            let mut clone_payload_bytes = 0;
20535            while let Some(field) = pending.pop() {
20536                fields += 1;
20537                clone_payload_bytes += std::mem::size_of::<Field>()
20538                    + field.cached_result.len()
20539                    + field.instruction.raw.len();
20540                if let FieldSource::Parsed {
20541                    raw_xml,
20542                    original_cached_result,
20543                    original_instruction,
20544                    cached_segments,
20545                    word_prefixes,
20546                    ..
20547                } = &field.source
20548                {
20549                    source_bytes += raw_xml.len();
20550                    clone_payload_bytes += raw_xml.len()
20551                        + original_cached_result.len()
20552                        + original_instruction.raw.len()
20553                        + cached_segments.len() * std::mem::size_of::<CachedDisplaySegment>()
20554                        + cached_segments
20555                            .iter()
20556                            .map(|segment| segment.text.len())
20557                            .sum::<usize>()
20558                        + word_prefixes
20559                            .iter()
20560                            .map(|prefix| std::mem::size_of::<String>() + prefix.len())
20561                            .sum::<usize>();
20562                }
20563                pending.extend(field.cached_fields.iter().map(|(_, child)| child));
20564                for runs in [
20565                    &field.parsed_cached_runs,
20566                    &field.simple_cached_runs,
20567                    &field.typed_cached_runs,
20568                ]
20569                .into_iter()
20570                .flatten()
20571                {
20572                    clone_payload_bytes += runs.len() * std::mem::size_of::<CT_R>();
20573                    for run in runs {
20574                        assert!(
20575                            run.properties.is_none(),
20576                            "growth control isolates cache ownership"
20577                        );
20578                        clone_payload_bytes +=
20579                            run.content.len() * std::mem::size_of::<RunContent>();
20580                        clone_payload_bytes += run
20581                            .content
20582                            .iter()
20583                            .map(|content| match content {
20584                                RunContent::Text(text) | RunContent::DeletedText(text) => {
20585                                    text.text.len()
20586                                }
20587                                _ => 0,
20588                            })
20589                            .sum::<usize>();
20590                    }
20591                    pending.extend(runs.iter().flat_map(|run| &run.content).filter_map(
20592                        |content| {
20593                            if let RunContent::Field(child) = content {
20594                                Some(child)
20595                            } else {
20596                                None
20597                            }
20598                        },
20599                    ));
20600                }
20601            }
20602            (fields, source_bytes, clone_payload_bytes)
20603        }
20604        for depth in [1, 2, 4, 8, 16, 32] {
20605            let begin = r#"<w:r><w:fldChar w:fldCharType="begin"/><w:instrText> UNKNOWN </w:instrText><w:fldChar w:fldCharType="separate"/></w:r>"#;
20606            let end = r#"<w:r><w:fldChar w:fldCharType="end"/></w:r>"#;
20607            let source = format!(
20608                "{}<w:r><w:tab/></w:r>{}",
20609                begin.repeat(depth),
20610                end.repeat(depth)
20611            );
20612            let paragraph = parse_paragraph(&source);
20613            let field = parsed_field(&paragraph, 0);
20614            let shape = owned_shape(field);
20615            assert_eq!(shape.0, depth, "each cached owner retained exactly once");
20616            // Existing source fragments include every descendant's XML. The
20617            // retained graph adds one child axis, never an exponential clone.
20618            assert!(shape.1 <= depth * source.len());
20619            assert!(shape.2 <= depth * (source.len() + 4_096));
20620            let cloned = field.clone();
20621            assert_eq!(owned_shape(&cloned), shape);
20622            assert_eq!(&cloned, field);
20623            assert_eq!(
20624                serialized_paragraph(&paragraph),
20625                format!("<w:p>{source}</w:p>")
20626            );
20627            println!(
20628                "depth={depth} owned_fields={} retained_source_bytes={} clone_payload_bytes={} cloned_fields={}",
20629                shape.0,
20630                shape.1,
20631                shape.2,
20632                owned_shape(&cloned).0
20633            );
20634        }
20635        let ordinary = parse_paragraph(
20636            r#"<w:r><w:fldChar w:fldCharType="begin"/><w:instrText> PAGE </w:instrText><w:fldChar w:fldCharType="separate"/><w:t>ordinary</w:t><w:fldChar w:fldCharType="end"/></w:r>"#,
20637        );
20638        assert!(parsed_field(&ordinary, 0).cached_result_runs().is_none());
20639        assert_eq!(ordinary, ordinary.clone());
20640    }
20641
20642    #[test]
20643    fn visibility_nontext_graph_preserves_mixed_annotation_owner_boundaries() {
20644        for marker in ["footnoteReference", "endnoteReference", "commentReference"] {
20645            let source = format!(
20646                r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText> UNKNOWN </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:{marker} w:id="7"/></w:r><w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText> PAGE </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:tab/></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
20647            );
20648            let paragraph = parse_paragraph(&source);
20649            let field = parsed_field(&paragraph, 0);
20650            assert_eq!(field.cached_fields_in_source_order().len(), 1);
20651            assert!(
20652                !field.cached_fields.is_empty(),
20653                "legacy annotation range axis stays authoritative"
20654            );
20655            assert!(field.parsed_cached_child_runs().is_none());
20656            let runs = field.cached_result_runs().unwrap();
20657            assert!(runs.iter().flat_map(|run| &run.content).any(|content| {
20658                matches!(
20659                    (marker, content),
20660                    ("footnoteReference", RunContent::FootnoteRef { id: 7, .. })
20661                        | ("endnoteReference", RunContent::EndnoteRef { id: 7, .. })
20662                        | (
20663                            "commentReference",
20664                            RunContent::CommentReference { id: 7, .. }
20665                        )
20666                )
20667            }));
20668            assert_eq!(
20669                serialized_paragraph(&paragraph),
20670                format!("<w:p>{source}</w:p>")
20671            );
20672            let reopened = parse_paragraph(&source);
20673            assert_eq!(paragraph, reopened);
20674        }
20675    }
20676
20677    #[test]
20678    fn field_cache_runs_keep_controls_and_properties_in_order() {
20679        let mut first = CT_R::new("first");
20680        first.properties = Some(CT_RPr {
20681            bold: Some(true),
20682            ..Default::default()
20683        });
20684        first.content.push(RunContent::Tab);
20685        first.content.push(RunContent::Break(BreakType::Line));
20686        let second = CT_R::new("second");
20687        for form in [FieldForm::Simple, FieldForm::Complex] {
20688            let field =
20689                Field::from_raw("UNKNOWN", form, vec![first.clone(), second.clone()]).unwrap();
20690            assert_eq!(field.cached_result, "first\t\nsecond");
20691            let mut paragraph = CT_P::new();
20692            paragraph.runs.push(field_run(field, None));
20693            let output = serialized_paragraph(&paragraph);
20694            assert!(output.contains("<w:b"), "{output}");
20695            assert!(output.find("first").unwrap() < output.find("<w:tab").unwrap());
20696            assert!(output.find("<w:br").unwrap() < output.find("second").unwrap());
20697            let reopened = CT_P::from_xml_fragment(
20698                output
20699                    .replacen(
20700                        "<w:p>",
20701                        &format!("<w:p xmlns:w=\"{}\">", crate::namespace::W_NS),
20702                        1,
20703                    )
20704                    .as_bytes(),
20705            )
20706            .unwrap();
20707            assert_eq!(parsed_field(&reopened, 0).cached_result, "first\t\nsecond");
20708        }
20709    }
20710
20711    #[test]
20712    fn typed_cache_replaces_simple_and_complex_results_without_losing_controls() {
20713        for xml in [
20714            r#"<w:fldSimple w:instr=" REF Target \f " w:dirty="1" xmlns:x="urn:producer" x:flag="kept"><w:r><w:t>OLD</w:t></w:r></w:fldSimple>"#,
20715            r#"<w:r xmlns:x="urn:producer"><w:rPr><w:b/></w:rPr><w:fldChar w:fldCharType="begin" x:flag="kept"/></w:r><w:r><w:instrText xml:space="preserve"> REF Target \f </w:instrText></w:r><w:r><w:rPr><w:i/></w:rPr><w:fldChar w:fldCharType="separate"/><w:t>OLD</w:t><w:fldChar w:fldCharType="end"/></w:r>"#,
20716        ] {
20717            let mut paragraph = parse_paragraph(xml);
20718            let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
20719                panic!("field");
20720            };
20721            let instruction = field.instruction.clone();
20722            let mut reference = CT_R::new("");
20723            reference.content = vec![RunContent::FootnoteRef {
20724                id: 37,
20725                custom_mark: None,
20726            }];
20727            reference.properties = Some(CT_RPr {
20728                bold: Some(true),
20729                ..Default::default()
20730            });
20731            field.set_cached_runs(vec![reference]).unwrap();
20732            assert_eq!(field.cached_result, "");
20733            assert_eq!(field.instruction, instruction);
20734            let output = serialized_paragraph(&paragraph);
20735            assert!(output.contains("x:flag=\"kept\""), "{output}");
20736            assert!(output.contains("footnoteReference w:id=\"37\""), "{output}");
20737            assert!(!output.contains("OLD"), "{output}");
20738            let reopened = parse_paragraph(
20739                output
20740                    .strip_prefix("<w:p>")
20741                    .unwrap()
20742                    .strip_suffix("</w:p>")
20743                    .unwrap(),
20744            );
20745            assert_eq!(parsed_field(&reopened, 0).cached_result, "");
20746            assert_eq!(serialized_paragraph(&reopened), output);
20747        }
20748    }
20749
20750    #[test]
20751    fn typed_cache_shared_run_keeps_prefix_suffix_and_nested_result() {
20752        let mut paragraph = parse_paragraph(
20753            r#"<w:r xmlns:x="urn:producer" x:keep="yes"><w:rPr><w:b/></w:rPr><w:t>PRE</w:t><w:fldChar w:fldCharType="begin"/><w:instrText> REF Target \f </w:instrText><w:fldChar w:fldCharType="separate"/><w:t>OLD</w:t><w:fldChar w:fldCharType="end"/><w:t>POST</w:t></w:r>"#,
20754        );
20755        let field = paragraph
20756            .runs
20757            .iter_mut()
20758            .flat_map(|run| &mut run.content)
20759            .find_map(|content| match content {
20760                RunContent::Field(field) => Some(field),
20761                _ => None,
20762            })
20763            .unwrap();
20764        let child =
20765            Field::from_raw("UNKNOWN", FieldForm::Complex, vec![CT_R::new("RICH")]).unwrap();
20766        field
20767            .set_cached_runs(vec![field_run(
20768                child,
20769                Some(CT_RPr {
20770                    italic: Some(true),
20771                    ..Default::default()
20772                }),
20773            )])
20774            .unwrap();
20775        let output = serialized_paragraph(&paragraph);
20776        assert!(
20777            output.contains("PRE") && output.contains("POST") && output.contains("RICH"),
20778            "{output}"
20779        );
20780        assert!(!output.contains("OLD"), "{output}");
20781        let reopened = parse_paragraph(
20782            output
20783                .strip_prefix("<w:p>")
20784                .unwrap()
20785                .strip_suffix("</w:p>")
20786                .unwrap(),
20787        );
20788        assert_eq!(reopened.text(), "PRERICHPOST");
20789        assert_eq!(serialized_paragraph(&reopened), output);
20790    }
20791
20792    #[test]
20793    fn typed_cache_nested_child_preserves_parent_and_namespace_bindings() {
20794        for outer in [FieldForm::Simple, FieldForm::Complex] {
20795            let child =
20796                Field::from_raw(r"REF Target \f", FieldForm::Complex, vec![CT_R::new("OLD")])
20797                    .unwrap();
20798            let root = Field::from_raw("UNKNOWN", outer, vec![field_run(child, None)]).unwrap();
20799            let mut paragraph = CT_P::new();
20800            paragraph.runs.push(field_run(root, None));
20801            let source = serialized_paragraph(&paragraph);
20802            let mut parsed = parse_paragraph(
20803                source
20804                    .strip_prefix("<w:p>")
20805                    .unwrap()
20806                    .strip_suffix("</w:p>")
20807                    .unwrap(),
20808            );
20809            let RunContent::Field(parent) = &mut parsed.runs[0].content[0] else {
20810                panic!("parent");
20811            };
20812            let child = parent.cached_field_mut(0).unwrap();
20813            let mut reference = CT_R::new("kept ");
20814            reference.content.push(RunContent::EndnoteRef {
20815                id: 41,
20816                custom_mark: None,
20817            });
20818            child.set_cached_runs(vec![reference]).unwrap();
20819            parent.refresh_cached_field_projection();
20820            let output = serialized_paragraph(&parsed);
20821            assert!(output.contains("endnoteReference w:id=\"41\""), "{output}");
20822            assert!(!output.contains("OLD"), "{output}");
20823            assert!(output.contains("UNKNOWN"), "{output}");
20824            let reopened = parse_paragraph(
20825                output
20826                    .strip_prefix("<w:p>")
20827                    .unwrap()
20828                    .strip_suffix("</w:p>")
20829                    .unwrap(),
20830            );
20831            assert_eq!(parsed_field(&reopened, 0).cached_result, "kept ");
20832        }
20833        let mut parsed = CT_P::from_xml_fragment(br#"<q:p xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w="urn:foreign"><q:fldSimple xmlns:q="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w="urn:foreign" q:instr=" REF Target \f " xmlns:x="urn:producer" x:keep="yes"><q:r><q:t>OLD</q:t></q:r></q:fldSimple></q:p>"#).unwrap();
20834        let RunContent::Field(field) = &mut parsed.runs[0].content[0] else {
20835            panic!("alias field");
20836        };
20837        let mut reference = CT_R::new("");
20838        reference.content = vec![RunContent::CommentReference {
20839            id: 43,
20840            raw_before: 0,
20841        }];
20842        field.set_cached_runs(vec![reference]).unwrap();
20843        let output = serialized_paragraph(&parsed);
20844        assert!(output.contains("xmlns:w=\"urn:foreign\""), "{output}");
20845        assert!(output.contains("x:keep=\"yes\""), "{output}");
20846        assert!(output.contains("commentReference w:id=\"43\""), "{output}");
20847        assert!(!output.contains("OLD"), "{output}");
20848    }
20849
20850    #[test]
20851    fn typed_cache_parse_keeps_empty_typed_results_and_original_shared_owner_bytes() {
20852        let source = r#"<w:r><w:rPr><w:b/></w:rPr><w:t>PRE</w:t><w:fldChar w:fldCharType="begin"/><w:instrText> REF A \f </w:instrText><w:fldChar w:fldCharType="separate"/><w:footnoteReference w:id="17"/><w:fldChar w:fldCharType="end"/><w:t>MID</w:t><w:fldChar w:fldCharType="begin"/><w:instrText> REF B \f </w:instrText><w:fldChar w:fldCharType="separate"/><w:endnoteReference w:id="19"/><w:fldChar w:fldCharType="end"/><w:t>POST</w:t></w:r>"#;
20853        let paragraph = parse_paragraph(source);
20854        assert_eq!(
20855            serialized_paragraph(&paragraph),
20856            format!("<w:p>{source}</w:p>")
20857        );
20858        let fields = paragraph
20859            .runs()
20860            .into_iter()
20861            .flat_map(|run| &run.content)
20862            .filter_map(|content| match content {
20863                RunContent::Field(field) => Some(field),
20864                _ => None,
20865            })
20866            .collect::<Vec<_>>();
20867        assert_eq!(fields.len(), 2);
20868        assert_eq!(fields[0].cached_result, "");
20869        assert_eq!(fields[1].cached_result, "");
20870        assert!(
20871            fields[0]
20872                .cached_result_runs()
20873                .unwrap()
20874                .iter()
20875                .flat_map(|run| &run.content)
20876                .any(|content| matches!(content, RunContent::FootnoteRef { id: 17, .. }))
20877        );
20878        assert!(
20879            !fields[0]
20880                .cached_result_runs()
20881                .unwrap()
20882                .iter()
20883                .flat_map(|run| &run.content)
20884                .any(|content| matches!(content, RunContent::EndnoteRef { .. }))
20885        );
20886        assert!(
20887            fields[1]
20888                .cached_result_runs()
20889                .unwrap()
20890                .iter()
20891                .flat_map(|run| &run.content)
20892                .any(|content| matches!(content, RunContent::EndnoteRef { id: 19, .. }))
20893        );
20894        assert!(
20895            !fields[1]
20896                .cached_result_runs()
20897                .unwrap()
20898                .iter()
20899                .flat_map(|run| &run.content)
20900                .any(|content| matches!(content, RunContent::FootnoteRef { .. }))
20901        );
20902    }
20903
20904    #[test]
20905    fn typed_cache_rejection_preserves_source_and_cached_state() {
20906        let mut field = Field::new(r"REF Target \f", "OLD");
20907        let before = format!("{field:?}");
20908        assert!(field.set_cached_runs(vec![CT_R::new("invalid\0")]).is_err());
20909        assert_eq!(field.cached_result, "OLD");
20910        assert_eq!(format!("{field:?}"), before);
20911        field.set_locked(Some(true));
20912        assert!(field.set_cached_runs(vec![CT_R::new("fresh")]).is_err());
20913        assert_eq!(field.cached_result, "OLD");
20914    }
20915
20916    #[test]
20917    fn field_property_edits_preserve_unmodelled_xml_verbatim() {
20918        for xml in [
20919            r#"<a:fldSimple xmlns:a="http://schemas.openxmlformats.org/wordprocessingml/2006/main" a:instr="UNKNOWN" a:fldLock="1" xmlns:x="urn:producer"><x:keep note='yes'> exact </x:keep><a:r><a:t>cache</a:t></a:r></a:fldSimple>"#,
20920            r#"<a:r xmlns:a="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:x="urn:producer"><a:fldChar a:fldCharType="begin" a:fldLock="1"><x:keep note='yes'> exact </x:keep></a:fldChar></a:r><a:r xmlns:a="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><a:instrText> UNKNOWN </a:instrText></a:r><a:r xmlns:a="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><a:fldChar a:fldCharType="separate"/><a:t>cache</a:t><a:fldChar a:fldCharType="end"/></a:r>"#,
20921        ] {
20922            let mut paragraph = parse_paragraph(xml);
20923            let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
20924                panic!("field not typed");
20925            };
20926            assert_eq!(field.locked(), Some(true));
20927            field.set_locked(Some(false));
20928            let output = serialized_paragraph(&paragraph);
20929            assert!(
20930                output.contains("<x:keep note='yes'> exact </x:keep>"),
20931                "{output}"
20932            );
20933            let reopened = CT_P::from_xml_fragment(
20934                output
20935                    .replacen(
20936                        "<w:p>",
20937                        &format!("<w:p xmlns:w=\"{}\">", crate::namespace::W_NS),
20938                        1,
20939                    )
20940                    .as_bytes(),
20941            )
20942            .unwrap();
20943            assert_eq!(parsed_field(&reopened, 0).locked(), Some(false));
20944        }
20945    }
20946    #[test]
20947    fn unknown_typed_switch_operands_reopen_without_changing_known_flags() {
20948        for form in [FieldForm::Simple, FieldForm::Complex] {
20949            let instruction = FieldInstruction::new(
20950                "PRODUCER",
20951                vec![],
20952                vec![FieldSwitch {
20953                    name: "q".into(),
20954                    argument: Some(FieldArgument::Text("operand".into())),
20955                }],
20956            )
20957            .unwrap();
20958            let field =
20959                Field::from_instruction(instruction, form, vec![CT_R::new("cache")]).unwrap();
20960            let mut paragraph = CT_P::new();
20961            paragraph.runs.push(field_run(field, None));
20962            let reopened = parse_paragraph(
20963                &serialized_paragraph(&paragraph)
20964                    .replace("<w:p>", "")
20965                    .replace("</w:p>", ""),
20966            );
20967            assert_eq!(
20968                parsed_field(&reopened, 0).instruction.switches[0].argument,
20969                Some(FieldArgument::Text("operand".into()))
20970            );
20971            assert!(parsed_field(&reopened, 0).instruction.arguments.is_empty());
20972        }
20973        let nested = Field::from_raw(
20974            "MERGEFIELD name",
20975            FieldForm::Complex,
20976            vec![CT_R::new("Ada")],
20977        )
20978        .unwrap();
20979        let instruction = FieldInstruction::new(
20980            "PRODUCER",
20981            vec![],
20982            vec![FieldSwitch {
20983                name: "q".into(),
20984                argument: Some(FieldArgument::Nested(Box::new(nested))),
20985            }],
20986        )
20987        .unwrap();
20988        let field =
20989            Field::from_instruction(instruction, FieldForm::Complex, vec![CT_R::new("cache")])
20990                .unwrap();
20991        let mut paragraph = CT_P::new();
20992        paragraph.runs.push(field_run(field, None));
20993        let output = serialized_paragraph(&paragraph);
20994        let reopened = parse_paragraph(&output.replace("<w:p>", "").replace("</w:p>", ""));
20995        let field = parsed_field(&reopened, 0);
20996        assert!(field.instruction.arguments.is_empty());
20997        assert!(
20998            matches!(&field.instruction.switches[0].argument, Some(FieldArgument::Nested(nested)) if nested.cached_result == "Ada")
20999        );
21000        let raw = r#"<w:fldSimple w:instr='REF \h "target name"'><w:r><w:t>cache</w:t></w:r></w:fldSimple>"#;
21001        let original = parse_paragraph(raw);
21002        let instruction = &parsed_field(&original, 0).instruction;
21003        assert_eq!(
21004            instruction.arguments,
21005            vec![FieldArgument::Text("target name".into())]
21006        );
21007        assert_eq!(instruction.switches[0].argument, None);
21008        assert!(serialized_paragraph(&original).contains(raw));
21009        assert!(
21010            FieldInstruction::new(
21011                "REF",
21012                vec![],
21013                vec![FieldSwitch {
21014                    name: "h".into(),
21015                    argument: Some(FieldArgument::Text("target name".into()))
21016                }]
21017            )
21018            .is_err()
21019        );
21020        let producer_raw = r#"<w:fldSimple w:instr='PRODUCER \q "value"'><w:r><w:t>cache</w:t></w:r></w:fldSimple>"#;
21021        assert!(serialized_paragraph(&parse_paragraph(producer_raw)).contains(producer_raw));
21022    }
21023
21024    #[test]
21025    fn generated_table_operands_parse_bare_and_quoted_without_flag_collisions() {
21026        for raw in [
21027            r#"INDEX \z 1033 \f a \h A \e " | " \l "; " \g " to " \k " => " \s Chapter \r"#,
21028            r#"INDEX \z "1033" \f "a" \h "A" \e " | " \l "; " \g " to " \k " => " \s "Chapter" \r"#,
21029        ] {
21030            let field = Field::from_raw(raw, FieldForm::Complex, vec![]).unwrap();
21031            assert!(field.instruction.arguments.is_empty());
21032            for (name, operand) in [
21033                ("z", "1033"),
21034                ("f", "a"),
21035                ("h", "A"),
21036                ("e", " | "),
21037                ("l", "; "),
21038                ("g", " to "),
21039                ("k", " => "),
21040                ("s", "Chapter"),
21041            ] {
21042                assert_eq!(
21043                    field
21044                        .instruction
21045                        .switches
21046                        .iter()
21047                        .find(|switch| switch.name == name)
21048                        .unwrap()
21049                        .argument,
21050                    Some(FieldArgument::Text(operand.into()))
21051                );
21052            }
21053            assert_eq!(field.instruction.switches.last().unwrap().argument, None);
21054            assert_eq!(field.effective_instruction_text(), raw);
21055        }
21056        for (raw, operands, flags) in [
21057            (
21058                r#"XE "Alpha:Beta" \f a \r Range \t "See Alpha" \b \i"#,
21059                vec![("f", "a"), ("r", "Range"), ("t", "See Alpha")],
21060                vec!["b", "i"],
21061            ),
21062            (
21063                r#"TA \l "Long citation" \s Short \c 16 \r Range \b \i"#,
21064                vec![
21065                    ("l", "Long citation"),
21066                    ("s", "Short"),
21067                    ("c", "16"),
21068                    ("r", "Range"),
21069                ],
21070                vec!["b", "i"],
21071            ),
21072            (
21073                r#"TOA \c 1 \e " | " \l "; " \g " to " \h \p"#,
21074                vec![("c", "1"), ("e", " | "), ("l", "; "), ("g", " to ")],
21075                vec!["h", "p"],
21076            ),
21077            (r#"TOC \c Figure \h"#, vec![("c", "Figure")], vec!["h"]),
21078            (
21079                r#"TOC \a "Custom label" \h"#,
21080                vec![("a", "Custom label")],
21081                vec!["h"],
21082            ),
21083        ] {
21084            let field = Field::from_raw(raw, FieldForm::Simple, vec![]).unwrap();
21085            for (name, operand) in operands {
21086                assert_eq!(
21087                    field
21088                        .instruction
21089                        .switches
21090                        .iter()
21091                        .find(|switch| switch.name == name)
21092                        .unwrap()
21093                        .argument,
21094                    Some(FieldArgument::Text(operand.into()))
21095                );
21096            }
21097            for name in flags {
21098                assert_eq!(
21099                    field
21100                        .instruction
21101                        .switches
21102                        .iter()
21103                        .find(|switch| switch.name == name)
21104                        .unwrap()
21105                        .argument,
21106                    None
21107                );
21108            }
21109        }
21110        let reference = parse_field_instruction(r#"REF \h "target name""#);
21111        assert_eq!(
21112            reference.arguments,
21113            vec![FieldArgument::Text("target name".into())]
21114        );
21115        assert_eq!(reference.switches[0].argument, None);
21116        let sequence = parse_field_instruction(r#"SEQ Chapter \r 7"#);
21117        assert_eq!(
21118            sequence.switches[0].argument,
21119            Some(FieldArgument::Text("7".into()))
21120        );
21121        let malformed = parse_field_instruction(r#"TOA \c "1"#);
21122        assert!(!malformed.quotes_are_balanced());
21123    }
21124
21125    #[test]
21126    fn equal_text_nested_replacement_writes_its_new_cache_properties() {
21127        for depth in [1, 2] {
21128            for switch_operand in [false, true] {
21129                let instruction_prefix = if switch_operand {
21130                    "PRODUCER \\q "
21131                } else {
21132                    "PRODUCER "
21133                };
21134                let source = format!(
21135                    r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText>{instruction_prefix}</w:instrText></w:r><w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText>MERGEFIELD name</w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>Ada</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>cache</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
21136                );
21137                let source = if depth == 2 {
21138                    format!(
21139                        r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText>OUTER </w:instrText></w:r>{source}<w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:t>outer cache</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#
21140                    )
21141                } else {
21142                    source
21143                };
21144                let mut paragraph = parse_paragraph(&source);
21145                let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
21146                    panic!("outer field");
21147                };
21148                let field = if depth == 2 {
21149                    let FieldArgument::Nested(middle) = &mut field.instruction.arguments[0] else {
21150                        panic!("middle field");
21151                    };
21152                    middle.as_mut()
21153                } else {
21154                    field
21155                };
21156                let mut cached = CT_R::new("Ada");
21157                cached.properties = Some(CT_RPr {
21158                    bold: Some(true),
21159                    ..Default::default()
21160                });
21161                let nested =
21162                    Field::from_raw("MERGEFIELD name", FieldForm::Complex, vec![cached]).unwrap();
21163                if switch_operand {
21164                    field.instruction.switches[0].argument =
21165                        Some(FieldArgument::Nested(Box::new(nested)));
21166                } else {
21167                    field.instruction.arguments[0] = FieldArgument::Nested(Box::new(nested));
21168                }
21169                let output = serialized_paragraph(&paragraph);
21170                assert!(output.contains("<w:b"), "{output}");
21171                let reopened = parse_paragraph(&output.replace("<w:p>", "").replace("</w:p>", ""));
21172                let mut nested = parsed_field(&reopened, 0).nested_fields_in_source_order()[0];
21173                if depth == 2 {
21174                    nested = nested.nested_fields_in_source_order()[0];
21175                }
21176                assert_eq!(nested.cached_result, "Ada");
21177                assert_eq!(
21178                    nested.cached_display_segments()[0].1.unwrap().bold,
21179                    Some(true)
21180                );
21181            }
21182        }
21183    }
21184
21185    #[test]
21186    fn cached_opaque_namespaces_preserve_foreign_lookalikes_and_inherited_word_names() {
21187        for raw in [
21188            br#"<p:fldChar xmlns:p="urn:producer" note='exact'/>"#.as_slice(),
21189            b"<w:lastRenderedPageBreak/>",
21190        ] {
21191            let mut cached = CT_R::new("cache");
21192            cached.extra_xml.push(raw.to_vec());
21193            let field = Field::from_raw("UNKNOWN", FieldForm::Simple, vec![cached]).unwrap();
21194            let mut paragraph = CT_P::new();
21195            paragraph.runs.push(field_run(field, None));
21196            let output = serialized_paragraph(&paragraph);
21197            assert!(output.contains(std::str::from_utf8(raw).unwrap()));
21198            let reopened = parse_paragraph(&output.replace("<w:p>", "").replace("</w:p>", ""));
21199            assert!(serialized_paragraph(&reopened).contains(std::str::from_utf8(raw).unwrap()));
21200        }
21201        for raw in [br#"<a:fldChar xmlns:a="http://schemas.openxmlformats.org/wordprocessingml/2006/main" a:fldCharType="begin"/>"#.as_slice(), b"<w:fldChar w:fldCharType=\"begin\"/>", b"<w:unknown>"] {
21202            let mut cached = CT_R::new("cache");
21203            cached.extra_xml.push(raw.to_vec());
21204            assert!(Field::from_raw("UNKNOWN", FieldForm::Simple, vec![cached]).is_err());
21205        }
21206    }
21207    #[test]
21208    fn nested_cached_fields_preserve_both_wire_forms() {
21209        for outer_form in [FieldForm::Simple, FieldForm::Complex] {
21210            for child_form in [FieldForm::Simple, FieldForm::Complex] {
21211                let mut result = CT_R::new("OLD");
21212                result.properties = Some(CT_RPr {
21213                    italic: Some(true),
21214                    ..CT_RPr::default()
21215                });
21216                let child = Field::from_raw("PAGE", child_form, vec![result]).unwrap();
21217                let nested = field_run(child, None);
21218                let outer = Field::from_raw(
21219                    "UNKNOWN",
21220                    outer_form,
21221                    vec![CT_R::new("prefix "), nested, CT_R::new(" suffix")],
21222                )
21223                .unwrap();
21224                let mut authored = CT_P::new();
21225                authored.runs.push(field_run(outer, None));
21226                let original = serialized_paragraph(&authored);
21227                let mut reopened =
21228                    parse_paragraph(&original.replace("<w:p>", "").replace("</w:p>", ""));
21229                assert_eq!(serialized_paragraph(&reopened), original);
21230                let RunContent::Field(outer) = &mut reopened.runs[0].content[0] else {
21231                    panic!("outer");
21232                };
21233                assert_eq!(outer.form(), outer_form);
21234                assert_eq!(
21235                    outer.cached_result, "prefix OLD suffix",
21236                    "{outer_form:?}/{child_form:?} {original}"
21237                );
21238                assert_eq!(
21239                    outer.cached_fields_in_source_order().len(),
21240                    1,
21241                    "{outer_form:?}/{child_form:?}"
21242                );
21243                let display = outer.cached_display_segments();
21244                assert_eq!(
21245                    display.iter().map(|(text, _)| *text).collect::<String>(),
21246                    "prefix OLD suffix"
21247                );
21248                assert!(display.iter().any(|(text, properties)| *text == "OLD"
21249                    && properties.is_some_and(|properties| properties.italic == Some(true))));
21250                let child = outer.cached_field_mut(0).unwrap();
21251                assert_eq!(child.form(), child_form);
21252                child.cached_result = "2".into();
21253                outer.refresh_cached_field_projection();
21254                let display = outer.cached_display_segments();
21255                assert_eq!(
21256                    display.iter().map(|(text, _)| *text).collect::<String>(),
21257                    "prefix 2 suffix"
21258                );
21259                assert!(display.iter().any(|(text, properties)| *text == "2"
21260                    && properties.is_some_and(|properties| properties.italic == Some(true))));
21261                let edited = serialized_paragraph(&reopened);
21262                assert!(edited.contains("<w:i"));
21263                let final_model =
21264                    parse_paragraph(&edited.replace("<w:p>", "").replace("</w:p>", ""));
21265                let field = parsed_field(&final_model, 0);
21266                assert_eq!(field.cached_result, "prefix 2 suffix");
21267                let children = field.cached_fields_in_source_order();
21268                assert_eq!(children.len(), 1);
21269                assert_eq!(children[0].form(), child_form);
21270                assert_eq!(children[0].cached_result, "2");
21271                assert_eq!(
21272                    children[0].cached_display_segments()[0].1.unwrap().italic,
21273                    Some(true)
21274                );
21275            }
21276        }
21277    }
21278
21279    #[test]
21280    fn cached_result_fields_retain_typed_identity_and_producer_bytes() {
21281        let raw = r#"<w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText>UNKNOWN</w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r data="cache"><w:rPr><w:b/></w:rPr><w:t>prefix </w:t></w:r><w:r><w:fldChar w:fldCharType="begin"/></w:r><w:r><w:instrText>PAGE</w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/></w:r><w:r><w:rPr><w:i/></w:rPr><w:t>OLD</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r><w:r><w:t> suffix</w:t></w:r><w:r><w:fldChar w:fldCharType="end"/></w:r>"#;
21282        let mut paragraph = parse_paragraph(raw);
21283        assert!(serialized_paragraph(&paragraph).contains(raw));
21284        let RunContent::Field(field) = &mut paragraph.runs[0].content[0] else {
21285            panic!("outer");
21286        };
21287        assert_eq!(field.cached_fields_in_source_order().len(), 1);
21288        let child = field.cached_field_mut(0).unwrap();
21289        child.cached_result = "2".into();
21290        child.dirty = Some(false);
21291        field.refresh_cached_field_projection();
21292        assert_eq!(field.cached_result, "prefix 2 suffix");
21293        let output = serialized_paragraph(&paragraph);
21294        assert!(
21295            output.contains(r#"<w:r data="cache"><w:rPr><w:b/></w:rPr><w:t>prefix </w:t></w:r>"#)
21296        );
21297        assert_eq!(output.matches("fldCharType=\"begin\"").count(), 2);
21298        let reopened = parse_paragraph(&output.replace("<w:p>", "").replace("</w:p>", ""));
21299        let field = parsed_field(&reopened, 0);
21300        assert_eq!(field.cached_result, "prefix 2 suffix");
21301        assert_eq!(field.cached_fields_in_source_order()[0].cached_result, "2");
21302        assert_eq!(
21303            field.cached_fields_in_source_order()[0].cached_display_segments()[0]
21304                .1
21305                .unwrap()
21306                .italic,
21307            Some(true)
21308        );
21309    }
21310    #[test]
21311    fn replacement_child_cannot_silently_discard_a_parsed_simple_cache() {
21312        let raw = br#"<w:fldSimple xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" w:instr="UNKNOWN"><w:fldSimple w:instr="PAGE"><w:r><w:t>OLD</w:t></w:r></w:fldSimple></w:fldSimple>"#;
21313        let mut field = parse_simple_field(raw, &[]).unwrap().unwrap();
21314        *field.cached_field_mut(0).unwrap() = Field::new("PAGE", "replacement");
21315        let mut writer = Writer::new(Vec::new());
21316        let error = write_field(&mut writer, &field, None).unwrap_err();
21317        assert!(error.to_string().contains("source identity"));
21318    }
21319    #[test]
21320    fn typed_annotation_cache_reopens_as_field_with_sibling_range_markers() {
21321        for xml in [
21322            r#"<w:fldSimple w:instr=" REF Target \f " producer="keep"><w:r><w:t>OLD</w:t></w:r></w:fldSimple>"#,
21323            r#"<w:r><w:fldChar w:fldCharType="begin" producer="keep"/></w:r><w:r><w:instrText xml:space="preserve"> REF Target \f </w:instrText></w:r><w:r><w:fldChar w:fldCharType="separate"/><w:t>OLD</w:t><w:fldChar w:fldCharType="end"/></w:r>"#,
21324            r#"<w:r producer="keep"><w:rPr><w:b/></w:rPr><w:t>PRE</w:t><w:fldChar w:fldCharType="begin"/><w:instrText xml:space="preserve"> REF Target \f </w:instrText><w:fldChar w:fldCharType="separate"/><w:t>OLD</w:t><w:fldChar w:fldCharType="end"/><w:t>POST</w:t></w:r>"#,
21325        ] {
21326            let mut paragraph = parse_paragraph(xml);
21327            let field = paragraph
21328                .runs
21329                .iter_mut()
21330                .flat_map(|run| &mut run.content)
21331                .find_map(|content| {
21332                    if let RunContent::Field(field) = content {
21333                        Some(field)
21334                    } else {
21335                        None
21336                    }
21337                })
21338                .unwrap();
21339            field
21340                .set_cached_runs_with_comment_ranges(Vec::new(), Vec::new())
21341                .unwrap();
21342            assert_eq!(
21343                field.cached_result, "",
21344                "empty marker payload keeps the ordinary typed result contract"
21345            );
21346            let mut reference = CT_R::new("");
21347            reference.content = vec![RunContent::CommentReference {
21348                id: 17,
21349                raw_before: 0,
21350            }];
21351            let ranges = vec![
21352                CommentRangeMarker::Start {
21353                    id: 17,
21354                    run_index: 0,
21355                    raw_before: 0,
21356                    has_child_content: false,
21357                },
21358                CommentRangeMarker::End {
21359                    id: 17,
21360                    run_index: 1,
21361                    raw_before: 0,
21362                    has_child_content: false,
21363                },
21364            ];
21365            field
21366                .set_cached_runs_with_comment_ranges(vec![CT_R::new("LITERAL"), reference], ranges)
21367                .unwrap();
21368            let output = serialized_paragraph(&paragraph);
21369            assert!(output.contains("producer=\"keep\""));
21370            assert!(!output.contains("OLD"));
21371            let reopened = parse_paragraph(
21372                output
21373                    .strip_prefix("<w:p>")
21374                    .unwrap()
21375                    .strip_suffix("</w:p>")
21376                    .unwrap(),
21377            );
21378            let field = reopened
21379                .runs
21380                .iter()
21381                .flat_map(|run| &run.content)
21382                .find_map(|content| {
21383                    if let RunContent::Field(field) = content {
21384                        Some(field)
21385                    } else {
21386                        None
21387                    }
21388                })
21389                .expect("complex annotation cache remains typed");
21390            assert_eq!(field.cached_result, "LITERAL");
21391            assert_eq!(field.effective_instruction().raw, r"REF Target \f");
21392            assert_eq!(field.cached_result_comment_ranges().len(), 2);
21393            assert!(
21394                field
21395                    .cached_result_runs()
21396                    .unwrap()
21397                    .iter()
21398                    .flat_map(|run| &run.content)
21399                    .any(|content| matches!(content, RunContent::CommentReference { id: 17, .. }))
21400            );
21401            assert!(
21402                reopened.comment_ranges.is_empty(),
21403                "cache markers are owned by the field, never moved outside it"
21404            );
21405            assert_eq!(serialized_paragraph(&reopened), output);
21406            let mut reader = NsReader::from_reader(output.as_bytes());
21407            let mut stack = Vec::<Vec<u8>>::new();
21408            let mut buffer = Vec::new();
21409            loop {
21410                match reader.read_event_into(&mut buffer).unwrap() {
21411                    Event::Start(element) => stack.push(element.local_name().as_ref().to_vec()),
21412                    Event::End(_) => {
21413                        stack.pop();
21414                    }
21415                    Event::Empty(element)
21416                        if matches!(
21417                            element.local_name().as_ref(),
21418                            b"commentRangeStart" | b"commentRangeEnd"
21419                        ) =>
21420                    {
21421                        assert_ne!(stack.last().map(Vec::as_slice), Some(b"r".as_slice()))
21422                    }
21423                    Event::Eof => break,
21424                    _ => {}
21425                }
21426                buffer.clear();
21427            }
21428        }
21429    }
21430
21431    #[test]
21432    fn typed_annotation_marker_failures_leave_field_unchanged() {
21433        let mut reference = CT_R::new("");
21434        reference.content = vec![RunContent::CommentReference {
21435            id: 17,
21436            raw_before: 0,
21437        }];
21438        for ranges in [
21439            vec![CommentRangeMarker::Start {
21440                id: 17,
21441                run_index: 0,
21442                raw_before: 0,
21443                has_child_content: false,
21444            }],
21445            vec![
21446                CommentRangeMarker::Start {
21447                    id: 18,
21448                    run_index: 0,
21449                    raw_before: 0,
21450                    has_child_content: false,
21451                },
21452                CommentRangeMarker::End {
21453                    id: 18,
21454                    run_index: 1,
21455                    raw_before: 0,
21456                    has_child_content: false,
21457                },
21458            ],
21459            vec![
21460                CommentRangeMarker::Start {
21461                    id: 17,
21462                    run_index: 0,
21463                    raw_before: 1,
21464                    has_child_content: false,
21465                },
21466                CommentRangeMarker::End {
21467                    id: 17,
21468                    run_index: 1,
21469                    raw_before: 0,
21470                    has_child_content: false,
21471                },
21472            ],
21473            vec![
21474                CommentRangeMarker::Start {
21475                    id: 17,
21476                    run_index: 0,
21477                    raw_before: 0,
21478                    has_child_content: true,
21479                },
21480                CommentRangeMarker::End {
21481                    id: 17,
21482                    run_index: 1,
21483                    raw_before: 0,
21484                    has_child_content: false,
21485                },
21486            ],
21487            vec![
21488                CommentRangeMarker::Start {
21489                    id: 17,
21490                    run_index: 3,
21491                    raw_before: 0,
21492                    has_child_content: false,
21493                },
21494                CommentRangeMarker::End {
21495                    id: 17,
21496                    run_index: 3,
21497                    raw_before: 0,
21498                    has_child_content: false,
21499                },
21500            ],
21501        ] {
21502            let mut field = Field::new(r"REF Target \f", "OLD");
21503            let before = format!("{field:?}");
21504            assert!(
21505                field
21506                    .set_cached_runs_with_comment_ranges(
21507                        vec![CT_R::new("literal"), reference.clone()],
21508                        ranges
21509                    )
21510                    .is_err()
21511            );
21512            assert_eq!(format!("{field:?}"), before);
21513        }
21514        let mut field = Field::new(r"REF Target \f", "OLD");
21515        let before = format!("{field:?}");
21516        let mut raw = CT_R::new("literal");
21517        raw.extra_xml
21518            .push(br#"<w:commentRangeStart w:id="17"/>"#.to_vec());
21519        assert!(
21520            field
21521                .set_cached_runs_with_comment_ranges(vec![raw, reference], vec![])
21522                .is_err()
21523        );
21524        assert_eq!(format!("{field:?}"), before);
21525    }
21526
21527    #[test]
21528    fn typed_annotation_nested_cache_keeps_raw_markers_and_parent_controls() {
21529        for outer in [FieldForm::Simple, FieldForm::Complex] {
21530            for inner in [FieldForm::Simple, FieldForm::Complex] {
21531                let child =
21532                    Field::from_raw(r"REF Target \f", inner, vec![CT_R::new("OLD")]).unwrap();
21533                let parent = Field::from_raw(
21534                    "UNKNOWN",
21535                    outer,
21536                    vec![CT_R::new("kept "), field_run(child, None)],
21537                )
21538                .unwrap();
21539                let mut paragraph = CT_P::new();
21540                paragraph.runs.push(field_run(parent, None));
21541                let original = serialized_paragraph(&paragraph);
21542                let mut paragraph = parse_paragraph(
21543                    original
21544                        .strip_prefix("<w:p>")
21545                        .unwrap()
21546                        .strip_suffix("</w:p>")
21547                        .unwrap(),
21548                );
21549                let RunContent::Field(parent) = &mut paragraph.runs[0].content[0] else {
21550                    panic!("parent");
21551                };
21552                let child = parent.cached_field_mut(0).unwrap();
21553                let mut reference = CT_R::new("");
21554                reference.content = vec![RunContent::CommentReference {
21555                    id: 29,
21556                    raw_before: 0,
21557                }];
21558                child
21559                    .set_cached_runs_with_comment_ranges(
21560                        vec![CT_R::new("ANNOTATED"), reference],
21561                        vec![
21562                            CommentRangeMarker::Start {
21563                                id: 29,
21564                                run_index: 0,
21565                                raw_before: 0,
21566                                has_child_content: false,
21567                            },
21568                            CommentRangeMarker::End {
21569                                id: 29,
21570                                run_index: 1,
21571                                raw_before: 0,
21572                                has_child_content: false,
21573                            },
21574                        ],
21575                    )
21576                    .unwrap();
21577                parent.refresh_cached_field_projection();
21578                let output = serialized_paragraph(&paragraph);
21579                assert!(!output.contains("OLD"));
21580                assert!(output.contains("UNKNOWN"));
21581                let output = output.replace(
21582                    "<w:commentRangeStart ",
21583                    "<w:commentRangeStart producer='verbatim' ",
21584                );
21585                let reopened = parse_paragraph(
21586                    output
21587                        .strip_prefix("<w:p>")
21588                        .unwrap()
21589                        .strip_suffix("</w:p>")
21590                        .unwrap(),
21591                );
21592                let parent = parsed_field(&reopened, 0);
21593                assert_eq!(parent.cached_result, "kept ANNOTATED");
21594                let child = parent.cached_fields_in_source_order()[0];
21595                assert_eq!(child.cached_result_comment_ranges().len(), 2);
21596                assert_eq!(child.cached_result, "ANNOTATED");
21597                assert!(reopened.comment_ranges.is_empty());
21598                assert_eq!(serialized_paragraph(&reopened), output);
21599            }
21600        }
21601    }
21602
21603    #[test]
21604    fn comment_reference_carriers_preserve_source_and_follow_typed_ownership() {
21605        for paired in [false, true] {
21606            let reference = if paired {
21607                r#"<q:commentReference q:id="7" x:id="foreign" xmlns:w="urn:foreign"><x:owned/><!--owned--><?owned exact?></q:commentReference>"#
21608            } else {
21609                r#"<q:commentReference q:id="7" x:id="foreign" xmlns:w="urn:foreign"/>"#
21610            };
21611            let raw = format!(
21612                r#"<w:p xmlns:w="{}" xmlns:q="{}" xmlns:x="urn:opaque"><w:r x:run="kept"><!--before--><?before exact?><w:t>AB</w:t>{reference}<!--between--><w:t>CD</w:t><q:commentReference q:id="8" x:id="second"/><?after exact?></w:r></w:p>"#,
21613                crate::namespace::W_NS,
21614                crate::namespace::W_NS
21615            );
21616            let paragraph = CT_P::from_xml_fragment(raw.as_bytes()).unwrap();
21617            assert_eq!(paragraph.accepted_text(), "ABCD");
21618            assert_eq!(paragraph.accepted_run_paths().len(), 1);
21619            let mut run = paragraph.runs[0].clone();
21620            let serialize = |run: &CT_R| {
21621                let mut writer = Writer::new(Vec::new());
21622                run.to_xml(&mut writer).unwrap();
21623                String::from_utf8(writer.into_inner()).unwrap()
21624            };
21625            let output = serialize(&run);
21626            assert_eq!(output.matches("x:id=\"foreign\"").count(), 1, "{output}");
21627            assert_eq!(output.matches("x:id=\"second\"").count(), 1, "{output}");
21628            assert!(output.find("<!--before-->").unwrap() < output.find("<w:t>AB").unwrap());
21629            assert!(
21630                output.find("<!--between-->").unwrap() > output.find("x:id=\"foreign\"").unwrap()
21631            );
21632            assert!(output.contains("<?after exact?>"));
21633            if paired {
21634                assert!(output.contains("<x:owned/><!--owned--><?owned exact?>"));
21635            }
21636            run.ensure_properties().bold = Some(true);
21637            let output = serialize(&run);
21638            assert!(output.find("<w:rPr>").unwrap() < output.find("x:id=\"foreign\"").unwrap());
21639            let mut changed = run.clone();
21640            let RunContent::CommentReference { id, .. } = &mut changed.content[1] else {
21641                panic!("reference");
21642            };
21643            *id = 9;
21644            let changed = serialize(&changed);
21645            assert!(!changed.contains("x:id=\"foreign\""), "{changed}");
21646            assert!(changed.contains("w:id=\"9\""));
21647            let mut removed = run.clone();
21648            assert!(removed.remove_comment_references(&[7]));
21649            let removed = serialize(&removed);
21650            assert!(
21651                !removed.contains("foreign") && !removed.contains("<x:owned"),
21652                "{removed}"
21653            );
21654            assert!(removed.contains("<!--between-->") && removed.contains("x:id=\"second\""));
21655            let mut replaced = run.clone();
21656            replaced.replace_content(vec![RunContent::Text(CT_Text::new("NEW"))]);
21657            let replaced = serialize(&replaced);
21658            assert!(!replaced.contains("commentReference"), "{replaced}");
21659            assert!(replaced.contains("<!--before-->") && replaced.contains("<?after exact?>"));
21660            let mut head = run.clone();
21661            let tail = head.split_off_at(2).unwrap();
21662            assert!(!serialize(&head).contains("commentReference"));
21663            assert_eq!(serialize(&tail).matches("commentReference ").count(), 2);
21664            let mut segmented = Writer::new(Vec::new());
21665            write_run_content_segment(&mut segmented, &run, 0, 1, true, false, None).unwrap();
21666            write_run_content_segment(&mut segmented, &run, 1, 3, false, false, None).unwrap();
21667            write_run_content_segment(&mut segmented, &run, 3, 4, false, true, None).unwrap();
21668            let segmented = String::from_utf8(segmented.into_inner()).unwrap();
21669            assert_eq!(
21670                segmented.matches("x:id=\"foreign\"").count(),
21671                1,
21672                "{segmented}"
21673            );
21674            assert_eq!(
21675                segmented.matches("x:id=\"second\"").count(),
21676                1,
21677                "{segmented}"
21678            );
21679            let mut unordered = run.clone();
21680            unordered.extra_xml_positions.clear();
21681            // Without private boundary provenance, raw references are independent sources.
21682            let unordered = serialize(&unordered);
21683            assert_eq!(
21684                unordered.matches("commentReference ").count(),
21685                4,
21686                "{unordered}"
21687            );
21688            assert!(unordered.contains("x:id=\"foreign\""));
21689            assert!(unordered.contains("x:id=\"second\""));
21690        }
21691    }
21692
21693    #[test]
21694    fn unordered_raw_comment_references_remain_independent_sources() {
21695        let raw = format!(
21696            r#"<w:commentReference xmlns:w="{}" w:id="7" xmlns:x="urn:x" x:keep="opaque"/>"#,
21697            crate::namespace::W_NS
21698        );
21699        let mut failures = Vec::new();
21700        for typed in [None, Some(7), Some(8)] {
21701            let run = CT_R {
21702                properties: None,
21703                content: typed
21704                    .map(|id| RunContent::CommentReference { id, raw_before: 1 })
21705                    .into_iter()
21706                    .collect(),
21707                extra_xml: vec![raw.as_bytes().to_vec()],
21708                extra_xml_positions: Vec::new(),
21709                alt_drawings: Vec::new(),
21710            };
21711            let mut writer = Writer::new(Vec::new());
21712            run.to_xml(&mut writer).unwrap();
21713            let output = String::from_utf8(writer.into_inner()).unwrap();
21714            if !output.contains(&raw)
21715                || output.matches("commentReference ").count() != 1 + usize::from(typed.is_some())
21716            {
21717                failures.push(format!("typed={typed:?}: {output}"));
21718            }
21719        }
21720        assert!(
21721            failures.is_empty(),
21722            "independent raw references were lost: {failures:?}"
21723        );
21724    }
21725    #[test]
21726    fn fx232_picture_payload_keeps_paragraph_ancestor_namespace_after_reopen() {
21727        const SVG: &str = "http://schemas.microsoft.com/office/drawing/2016/SVG/main";
21728        for owner in ["inline", "anchor"] {
21729            for declaration_owner in ["paragraph", "run", "drawing", "payload"] {
21730                let declaration = format!(r#" xmlns:asvg="{SVG}""#);
21731                let payload = format!(
21732                    r#"<a:extLst><a:ext uri='producer'><asvg:svgBlip{} r:embed='rId2'/><foreign:opaque xmlns:foreign='urn:foreign' literal='keep &amp; exact'/><asvg:scope xmlns:asvg='urn:shadow'><asvg:svgBlip literal='foreign same local'/></asvg:scope></a:ext></a:extLst>"#,
21733                    if declaration_owner == "payload" {
21734                        declaration.as_str()
21735                    } else {
21736                        ""
21737                    }
21738                );
21739                let source = format!(
21740                    r#"<w:p xmlns:w="{}" xmlns:wp="{WP_NS}" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="{R_NS}"{}><w:r{}><w:drawing{}><wp:{owner}><wp:extent cx="100" cy="100"/><wp:docPr id="1"/><a:graphic><a:graphicData uri="urn:picture"><a:blip r:embed="rId1">{payload}</a:blip></a:graphicData></a:graphic></wp:{owner}></w:drawing></w:r></w:p>"#,
21741                    crate::namespace::W_NS,
21742                    if declaration_owner == "paragraph" {
21743                        declaration.as_str()
21744                    } else {
21745                        ""
21746                    },
21747                    if declaration_owner == "run" {
21748                        declaration.as_str()
21749                    } else {
21750                        ""
21751                    },
21752                    if declaration_owner == "drawing" {
21753                        declaration.as_str()
21754                    } else {
21755                        ""
21756                    }
21757                );
21758                let paragraph = CT_P::from_xml_fragment_checked(source.as_bytes()).unwrap();
21759                let mut writer = Writer::new(Vec::new());
21760                paragraph.to_xml(&mut writer).unwrap();
21761                let output = writer.into_inner();
21762                assert!(
21763                    output
21764                        .windows(payload.len())
21765                        .any(|window| window == payload.as_bytes()),
21766                    "interior opaque payload changed"
21767                );
21768                let closed = raw_with_external_bindings(
21769                    &output,
21770                    &[
21771                        ("w".into(), crate::namespace::W_NS.into()),
21772                        ("r".into(), R_NS.into()),
21773                        ("wp".into(), WP_NS.into()),
21774                    ],
21775                )
21776                .unwrap();
21777                let mut qualified = NsReader::from_reader(closed.as_slice());
21778                let mut count = 0;
21779                loop {
21780                    let (namespace, event) = qualified.read_resolved_event().unwrap();
21781                    match event {
21782                        Event::Start(ref element) | Event::Empty(ref element) => {
21783                            assert!(
21784                                !matches!(namespace, ResolveResult::Unknown(_)),
21785                                "{owner}/{declaration_owner}: {}",
21786                                String::from_utf8_lossy(&closed)
21787                            );
21788                            if element.local_name().as_ref() == b"svgBlip" {
21789                                match namespace {
21790                                    ResolveResult::Bound(uri) if uri.as_ref() == SVG.as_bytes() => {
21791                                        count += 1
21792                                    }
21793                                    ResolveResult::Bound(uri) => {
21794                                        assert_eq!(uri.as_ref(), b"urn:shadow")
21795                                    }
21796                                    _ => panic!("bound SVG or foreign shadow"),
21797                                }
21798                            }
21799                        }
21800                        Event::Eof => break,
21801                        _ => {}
21802                    }
21803                }
21804                assert_eq!(count, 1);
21805                let reopened = CT_P::from_xml_fragment_checked(&closed).unwrap();
21806                let mut again = Writer::new(Vec::new());
21807                reopened.to_xml(&mut again).unwrap();
21808                assert_eq!(output, again.into_inner(), "namespace closure is stable");
21809            }
21810        }
21811    }
21812}