1use 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
34pub(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
375pub(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
413fn 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 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 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 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 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#[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
759fn 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 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#[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 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#[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 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#[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 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 span: Option<Box<FieldRunSpan>>,
923}
924
925#[derive(Debug, Clone)]
931struct FieldRunSpan {
932 runs: Vec<Option<CT_R>>,
934 position: usize,
936 field_index: usize,
938}
939
940#[derive(Debug, Clone, Copy, PartialEq, Eq)]
942pub enum LegacyFormFieldKind {
943 TextInput,
944 CheckBox,
945 DropDownList,
946}
947
948#[derive(Debug, Clone, PartialEq, Eq)]
950pub enum LegacyFormFieldValue {
951 Text(String),
952 Checked(bool),
953 SelectedIndex(usize),
954}
955
956#[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 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 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 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 #[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(¶graph)?;
1083 *self = candidate;
1084 Ok(())
1085 }
1086
1087 #[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(¶graph)?;
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 #[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 #[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 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 pub fn locked(&self) -> Option<bool> {
1181 self.locked
1182 }
1183
1184 pub fn set_locked(&mut self, value: Option<bool>) {
1186 self.locked = value;
1187 }
1188
1189 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[doc(hidden)]
1539 pub fn effective_instruction(&self) -> FieldInstruction {
1540 instruction_for_write(self)
1541 }
1542
1543 #[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 #[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 #[doc(hidden)]
1661 pub fn is_complex(&self) -> bool {
1662 self.form() == FieldForm::Complex
1663 }
1664
1665 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[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 #[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2189pub enum FieldForm {
2190 Simple,
2192 Complex,
2194}
2195
2196#[derive(Debug, Clone, PartialEq)]
2198pub enum RunContent {
2199 Text(CT_Text),
2200 DeletedText(CT_Text),
2202 Tab,
2203 Break(BreakType),
2204 Drawing(CT_Drawing),
2205 Field(Field),
2207 FootnoteRef {
2209 id: i32,
2210 custom_mark: Option<String>,
2212 },
2213 EndnoteRef {
2215 id: i32,
2216 custom_mark: Option<String>,
2218 },
2219 CommentReference {
2221 id: i32,
2222 raw_before: usize,
2224 },
2225 Symbol {
2227 font: String,
2229 char_code: u16,
2231 },
2232 SpecialCharacter(SpecialCharacter),
2234}
2235
2236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2241pub enum SpecialCharacter {
2242 CarriageReturn,
2244 NoBreakHyphen,
2246 SoftHyphen,
2248 PositionalTab {
2250 alignment: ST_PTabAlignment,
2251 relative_to: ST_PTabRelativeTo,
2252 leader: ST_PTabLeader,
2253 },
2254}
2255
2256#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2267#[allow(non_camel_case_types)]
2268pub enum ST_PTabRelativeTo {
2269 Margin,
2270 Indent,
2271}
2272
2273#[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
2343fn 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
2371fn 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
2405fn 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
2431fn 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#[derive(Debug, Clone, PartialEq, Eq)]
2476pub enum CommentRangeMarker {
2477 Start {
2478 id: i32,
2479 run_index: usize,
2480 raw_before: usize,
2482 has_child_content: bool,
2484 },
2485 End {
2486 id: i32,
2487 run_index: usize,
2488 raw_before: usize,
2490 has_child_content: bool,
2492 },
2493}
2494
2495#[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 #[doc(hidden)]
2559 pub fn raw_before(&self) -> usize {
2560 self.raw_before
2561 }
2562
2563 pub fn has_child_content(&self) -> bool {
2565 self.has_child_content
2566 }
2567
2568 #[doc(hidden)]
2570 pub fn projected_run_index(&self) -> usize {
2571 self.projected_run_index
2572 }
2573
2574 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2597pub enum BreakType {
2598 Line,
2599 Page,
2600 Column,
2601}
2602
2603#[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 pub extra_xml: Vec<Vec<u8>>,
2611 #[doc(hidden)]
2613 pub extra_xml_positions: Vec<usize>,
2614 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 #[doc(hidden)]
2821 pub fn raw_child_position(encoded: usize) -> usize {
2822 encoded & RAW_CHILD_POSITION_MASK
2823 }
2824
2825 #[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 #[doc(hidden)]
2833 pub fn raw_child_is_root_attributes(encoded: usize) -> bool {
2834 encoded & RAW_ROOT_ATTRIBUTES_FLAG != 0
2835 }
2836
2837 #[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 #[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 pub fn text(&self) -> String {
2871 self.content.iter().map(Self::content_text).collect()
2872 }
2873
2874 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(_) => "", 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 RunContent::Symbol { .. } => "",
2888 RunContent::SpecialCharacter(SpecialCharacter::CarriageReturn) => "\n",
2889 RunContent::SpecialCharacter(_) => "",
2890 }
2891 }
2892
2893 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 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 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 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 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 #[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 #[doc(hidden)]
3027 pub fn append_content(&mut self, content: RunContent) {
3028 self.content.push(content);
3029 }
3030
3031 #[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 #[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 #[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 #[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 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 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 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 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 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 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 }
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 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 !(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 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 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 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#[derive(Debug, Clone, PartialEq)]
3957pub struct HyperlinkSpan {
3958 pub rel_id: Option<String>,
3960 pub anchor: Option<String>,
3962 pub tooltip: Option<String>,
3964 pub doc_location: Option<String>,
3966 pub run_start: usize,
3968 pub run_end: usize,
3970 #[doc(hidden)]
3972 pub extra_attributes: Vec<(String, String)>,
3973 #[doc(hidden)]
3975 pub extra_xml: Vec<(usize, usize, Vec<u8>)>,
3976 #[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#[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 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 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#[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 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 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
5658enum FieldSpanPart {
5661 Field(Vec<u8>),
5662 Outside(CT_R),
5663}
5664
5665fn field_span_parts(raw: &[u8], word_prefixes: &[String]) -> Result<Option<Vec<FieldSpanPart>>> {
5672 use std::ops::Range;
5673
5674 let mut runs = Vec::<(Range<usize>, Option<Range<usize>>, Range<usize>)>::new();
5676 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 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 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
5878fn 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 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
5954fn 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#[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#[doc(hidden)]
6168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6169pub enum RangeAnchor<'a> {
6170 Comment(i32),
6172 Bookmark { id: i32, name: &'a str },
6174 Permission {
6176 id: i32,
6177 editor: Option<&'a str>,
6178 group: Option<&'a str>,
6179 },
6180 Proofing { kind: &'a str },
6182}
6183
6184#[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#[derive(Debug, Clone, PartialEq, Eq)]
6207enum MarkerSite {
6208 Paragraph { boundary: usize, position: usize },
6210 Control { controls: Vec<usize>, index: usize },
6213}
6214
6215type ChildPosition = (usize, usize);
6217
6218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6220enum BoundaryItem {
6221 Control(usize),
6222 Marker(usize),
6223 Raw(usize),
6224}
6225
6226#[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 pub hyperlinks: Vec<HyperlinkSpan>,
6234 pub comment_ranges: Vec<CommentRangeMarker>,
6236 pub bookmark_markers: Vec<BookmarkMarker>,
6238 pub extra_xml: Vec<(usize, Vec<u8>)>,
6240 pub content_controls: Vec<(usize, usize, usize, CT_Sdt)>,
6243 pub revisions: Vec<(usize, usize, CT_Revision)>,
6245 pub equations: Vec<(usize, usize, OfficeMath)>,
6247 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6265pub enum AcceptedRunPathSegment {
6266 Run(usize),
6268 ContentControl(usize),
6270 Revision(usize),
6272}
6273
6274#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6276pub struct AcceptedRunPath {
6277 pub(crate) segments: Vec<AcceptedRunPathSegment>,
6278}
6279
6280impl AcceptedRunPath {
6281 pub fn segments(&self) -> &[AcceptedRunPathSegment] {
6283 &self.segments
6284 }
6285}
6286
6287#[derive(Clone, Copy)]
6289pub(crate) enum BoundaryOwner {
6290 ContentControl(usize),
6292 Revision(usize),
6294 Wrapper(usize),
6297}
6298
6299pub(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 owners
6357 .sort_by_key(|(order, owner)| (*order, !matches!(owner, BoundaryOwner::ContentControl(_))));
6358 owners.into_iter().map(|(_, owner)| owner).collect()
6359}
6360
6361fn 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
6376pub(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(¶graph_start))
6407 .ok()
6408}
6409
6410pub(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 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
6433fn 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
6446pub(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
6463pub(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
6477fn 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(¶graph.content_controls[index].3, &mut runs);
6491 }
6492 BoundaryOwner::Revision(index) => {
6493 append_accepted_revision_runs(¶graph.revisions[index].2, &mut runs);
6494 }
6495 BoundaryOwner::Wrapper(index) => {
6496 wrapper_text(¶graph.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(¶graph.content_controls[index].3, &mut output);
6778 }
6779 BoundaryOwner::Revision(index) => {
6780 append_accepted_revision_runs(¶graph.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(¶graph.content_controls[index].3, &mut output);
6859 }
6860 BoundaryOwner::Revision(index) => {
6861 append_tracked_revision_runs(¶graph.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
6955fn 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 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 #[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 pub fn text(&self) -> String {
7088 self.runs().iter().map(|run| run.text()).collect()
7089 }
7090
7091 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 pub fn runs(&self) -> Vec<&CT_R> {
7124 let mut runs = Vec::new();
7125 self.collect_runs(&mut runs);
7126 runs
7127 }
7128
7129 #[doc(hidden)]
7133 pub fn source_runs(&self) -> Vec<&CT_R> {
7134 tracked_paragraph_runs(self)
7135 }
7136
7137 #[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 #[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 #[doc(hidden)]
7184 #[allow(clippy::type_complexity)] 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 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 #[doc(hidden)]
7246 pub fn accepted_view(&self) -> Cow<'_, CT_P> {
7247 self.accepted_view_projection(false).0
7248 }
7249
7250 #[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 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 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 #[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 #[doc(hidden)]
7404 pub fn accepted_bookmark_runs(&self) -> Vec<&CT_R> {
7405 accepted_paragraph_runs(self)
7406 }
7407
7408 #[doc(hidden)]
7410 pub fn accepted_run_paths(&self) -> Vec<AcceptedRunPath> {
7411 accepted_paragraph_run_paths(self)
7412 }
7413
7414 #[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 #[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 #[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 #[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 #[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 #[doc(hidden)]
7574 pub fn comment_range_text_from_source(xml: &[u8], id: i32) -> Result<Option<String>> {
7575 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 #[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 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 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 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 #[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 let _ = paragraph.refresh_bookmark_projection();
7784 *self = paragraph;
7785 Ok(())
7786 }
7787
7788 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 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 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 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 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 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 #[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 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 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 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 #[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 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 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 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 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 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 #[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 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 #[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 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 let owner = |index: usize| {
8604 let segments = paths[index].segments();
8605 &segments[..segments.len() - 1]
8606 };
8607 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 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 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 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 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 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 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 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 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 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 fn insert_run_after_boundary_items(&mut self, boundary: usize, kept: usize, run: CT_R) -> bool {
9033 let items = self.boundary_items(boundary);
9034 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 #[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 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 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 #[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 #[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 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 #[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(¶graph, &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 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 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 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 #[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 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 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 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 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 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 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 #[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 #[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 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 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
10097fn 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 ¶graph.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("e) {
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 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
11612enum 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 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
13026fn 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
13078pub(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
13110fn 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
13130pub(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
13836pub(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
14289fn parse_field_instruction(instr: &str) -> FieldInstruction {
14291 parse_field_instruction_parts(vec![InstructionPart::Text(instr.to_owned())])
14292}
14293
14294#[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#[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/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/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/producer"
14824 } else {
14825 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 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", "wordprocessingDrawing");
14856 let relationship = R_NS.replace("relationships", "relationships");
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(¶graph, 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(¶graph);
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", "wordprocessingml");
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 & Ω</{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 & Ω</{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 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 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 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 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 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 "https://example.test/path" </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 "https://example.test" </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 "https://example.test" </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 "https://example.test" </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 "https://example.test" </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) = ¶graph.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 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 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 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 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 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 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(¶graph, 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 ¶graph.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('&', "&").replace('"', """);
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(¶graph, 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 "Customer \"Code\"" \* 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"> "yes" "no" </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(¶graph, 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(¶graph);
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 "\\\\server\\share\\file.docx" """><w:r><w:t>cached</w:t></w:r></w:fldSimple>"#,
17037 );
17038 let field = parsed_field(¶graph, 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 "before text" \f "after text""><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 "chapter.docx" \c "MSWord8""><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 ":"" 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(¶graph, 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(¶graph),
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(¶graph),
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"> = "x" "yes" "no"</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(¶graph);
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(" = "x"").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"> = "x" "yes" "no"</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(¶graph);
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"> = "x" "yes" "no"</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(¶graph);
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"> = "x" "yes" "no"</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(¶graph);
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"> "yes" "no"</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(¶graph);
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"> = "new value" "yes" "no"</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(¶graph);
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(¶graph, 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(¶graph);
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(¶graph, 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(¶graph, 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(¶graph, 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(¶graph);
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(¶graph, 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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph, 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(¶graph);
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(¶graph);
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(¶graph, 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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph).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(""DATE""),
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(""DATE""),
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(¶graph, 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(¶graph, 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(¶graph);
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(¶graph);
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="7"/>"#,
18618 r#"<w:r><w:t>commented</w:t></w:r>"#,
18619 r#"<w:commentRangeEnd w:id="7"/>"#,
18620 r#"<w:r><w:commentReference w:id="7"/></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(¶graph, 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(¶graph);
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(¶graph);
18726 let original = parsed_field(¶graph, 0);
18727 assert_eq!(
18728 original.effective_instruction_text(),
18729 original.effective_instruction().raw
18730 );
18731 assert_eq!(serialized_paragraph(¶graph), 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 & two</w:t><w:delText>three < 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(¶graph);
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(¶graph);
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 = ¶graph.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(¶graph);
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(¶graph).len(), 1);
18937 assert_eq!(tracked_paragraph_runs(¶graph).len(), 1);
18938 assert_eq!(accepted_paragraph_runs(¶graph)[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(¶graph);
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(¶graph);
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) = ¶graph.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(¶graph);
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(¶graph);
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(¶graph);
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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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#" <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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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) = ¶graph.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>&<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) = ¶graph.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(¶graph);
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) = ¶graph.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(¶graph);
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(¶graph);
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 = ¶graph.hyperlinks[0];
19770 assert_eq!((hyperlink.run_start, hyperlink.run_end), (0, 2));
19771 let output = serialized_paragraph(¶graph);
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(¶graph);
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(¶graph), 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 = ¶graph.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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph)
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(¶graph, 0);
20126 assert_eq!(field.cached_result, expected_cache, "{name} {form:?}");
20127 let output = serialized_paragraph(¶graph);
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(¶graph, 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(¶graph),
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(¶graph, 0);
20240 assert_eq!(field.cached_result, "");
20241 assert!(field.cached_result_runs().is_none());
20242 assert_eq!(
20243 serialized_paragraph(¶graph),
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 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(¶graph, 0);
20283 assert_eq!(field.cached_result, "");
20284 assert_eq!(
20285 serialized_paragraph(¶graph),
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(¶graph, 0);
20325 assert_eq!(field.cached_result, "\t", "{prefix:?} simple={simple}");
20326 let before = serialized_paragraph(¶graph);
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(¶graph), 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(¶graph),
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(¶graph),
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(¶graph);
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(¶graph, 0);
20614 let shape = owned_shape(field);
20615 assert_eq!(shape.0, depth, "each cached owner retained exactly once");
20616 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(¶graph),
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(¶graph, 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(¶graph),
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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph),
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(¶graph);
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(¶graph)
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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph).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(¶graph);
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(¶graph);
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(¶graph);
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(¶graph);
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 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 & 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}