1use std::borrow::Cow;
2use std::io::Write;
3
4use oxml_core::OxmlError;
5use oxml_core::raw_xml::{capture_element, capture_empty_element};
6use oxml_core::xml::{local_name, matches_local_name};
7use oxml_core::xml_text::{decode_plain, resolve_entity};
8use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
9use quick_xml::name::{Namespace, ResolveResult};
10use quick_xml::{NsReader, Reader, Writer, XmlVersion};
11
12use crate::color::{ColorChoice, ColorTransform};
13use crate::fill::Fill;
14use crate::namespace::A_NS;
15use crate::order::OrderedRawChildren;
16
17use super::body::{Result, TextError, missing_end};
18use super::bullet::TextBullet;
19
20const TEXT_REL_NS: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
21
22pub(crate) fn text_validate_root(start: &BytesStart<'_>) -> Result<()> {
23 let name = start.name();
24 let raw = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
25 let prefix = raw.split_once(':').map_or("", |(prefix, _)| prefix);
26 let scope = text_namespace_scope(start, &[])?;
27 if scope.iter().any(|(key, uri)| key == prefix && uri != A_NS) {
28 return Err(invalid_attribute(
29 "DrawingML text",
30 "namespace",
31 "foreign root",
32 ));
33 }
34 Ok(())
35}
36
37pub(crate) fn text_source_tag(tag: &str, original: &str, physical: &[(String, String)]) -> String {
38 let requested_prefix = tag.split_once(':').map_or("", |(prefix, _)| prefix);
39 let expected = match requested_prefix {
40 "a" => Some(A_NS),
41 "p" if matches!(
42 tag.split_once(':').map_or(tag, |(_, local)| local),
43 "defaultTextStyle"
44 | "titleStyle"
45 | "bodyStyle"
46 | "otherStyle"
47 | "notesStyle"
48 | "txBody"
49 ) =>
50 {
51 Some("http://schemas.openxmlformats.org/presentationml/2006/main")
52 }
53 "dgm" if tag == "dgm:t" => Some("http://schemas.openxmlformats.org/drawingml/2006/diagram"),
54 _ => None,
55 };
56 let prefix = original.split_once(':').map_or("", |(prefix, _)| prefix);
57 let owned = expected.is_some_and(|expected| {
58 physical
59 .iter()
60 .any(|(name, uri)| name == prefix && uri == expected)
61 });
62 if original.is_empty() || !owned {
63 return tag.to_owned();
64 }
65 let local = tag.split_once(':').map_or(tag, |(_, local)| local);
68 original.split_once(':').map_or_else(
69 || local.to_owned(),
70 |(prefix, _)| format!("{prefix}:{local}"),
71 )
72}
73
74pub(crate) fn text_wrapper_start(
78 tag: &str,
79 original: &str,
80 spellings: &[String],
81) -> Result<BytesStart<'static>> {
82 let physical = text_namespace_scope(&text_source_start(tag, spellings), &[])?;
83 let owned = text_source_tag(tag, original, &physical);
84 let mut start = text_source_start(&owned, spellings);
85 if original.is_empty() || owned != tag {
86 return Ok(start);
87 }
88 let Some((prefix, local)) = tag.split_once(':') else {
89 return Ok(start);
90 };
91 let expected = match prefix {
92 "a" => A_NS,
93 "p" => "http://schemas.openxmlformats.org/presentationml/2006/main",
94 "dgm" => "http://schemas.openxmlformats.org/drawingml/2006/diagram",
95 _ => return Ok(start),
96 };
97 let original_prefix = original.split_once(':').map_or("", |(prefix, _)| prefix);
98 if !physical.iter().any(|(name, _)| name == original_prefix) {
99 return Ok(start);
102 }
103 if physical
104 .iter()
105 .any(|(name, uri)| name == prefix && uri == expected)
106 {
107 return Ok(start);
108 }
109 let mut destination = prefix.to_owned();
110 let mut suffix = 1;
111 while physical.iter().any(|(name, _)| name == &destination) {
112 destination = format!("{prefix}{suffix}");
113 suffix += 1;
114 }
115 start = text_source_start(&format!("{destination}:{local}"), spellings);
116 start.push_attribute((format!("xmlns:{destination}").as_str(), expected));
117 Ok(start)
118}
119
120fn text_has_physical_owner(tag: &str, physical: &[(String, String)]) -> bool {
121 let prefix = tag.split_once(':').map_or("", |(prefix, _)| prefix);
122 physical
123 .iter()
124 .any(|(name, uri)| name == prefix && uri == A_NS)
125}
126
127pub(crate) fn text_close_generated_child(
130 raw: &[u8],
131 inherited: &[(String, String)],
132) -> Result<Vec<u8>> {
133 if !inherited
134 .iter()
135 .any(|(prefix, uri)| prefix == "a" && uri != A_NS)
136 {
137 return Ok(raw.to_vec());
138 }
139 let mut reader = Reader::from_reader(raw);
140 let event = reader.read_event().map_err(OxmlError::from)?;
141 let (start, empty) = match event {
142 Event::Start(start) => (start, false),
143 Event::Empty(start) => (start, true),
144 _ => return Ok(raw.to_vec()),
145 };
146 if !start.name().as_ref().starts_with(b"a:")
147 || start
148 .attributes()
149 .any(|attribute| attribute.is_ok_and(|attribute| attribute.key.as_ref() == b"xmlns:a"))
150 {
151 return Ok(raw.to_vec());
152 }
153 let name = std::str::from_utf8(start.name().as_ref())
154 .map_err(OxmlError::from)?
155 .to_owned();
156 let content = format!(
157 "{} xmlns:a=\"{A_NS}\"",
158 std::str::from_utf8(start.as_ref()).map_err(OxmlError::from)?
159 );
160 let replacement = BytesStart::from_content(content, name.len());
161 let mut writer = Writer::new(Vec::new());
162 if empty {
163 write_empty(&mut writer, replacement)?;
164 } else {
165 write_start(&mut writer, replacement)?;
166 writer
167 .get_mut()
168 .write_all(&raw[reader.buffer_position() as usize..])
169 .map_err(OxmlError::from)?;
170 }
171 Ok(writer.into_inner())
172}
173
174pub(crate) fn text_namespace_scope(
175 start: &BytesStart<'_>,
176 inherited: &[(String, String)],
177) -> Result<Vec<(String, String)>> {
178 let mut scope = inherited.to_vec();
179 for attribute in start.attributes() {
180 let attribute = attribute.map_err(OxmlError::from)?;
181 let key = attribute.key.as_ref();
182 let prefix = if key == b"xmlns" {
183 Some("")
184 } else if let Some(prefix) = key.strip_prefix(b"xmlns:") {
185 Some(std::str::from_utf8(prefix).map_err(OxmlError::from)?)
186 } else {
187 None
188 };
189 if let Some(prefix) = prefix {
190 let value = attribute
191 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
192 .map_err(OxmlError::from)?
193 .into_owned();
194 if let Some((_, existing)) = scope.iter_mut().find(|(name, _)| name == prefix) {
195 *existing = value;
196 } else {
197 scope.push((prefix.to_owned(), value));
198 }
199 }
200 }
201 Ok(scope)
202}
203
204#[derive(Clone, Debug, Default)]
207pub(crate) struct TextNamespaceScope {
208 pub(crate) physical: Vec<(String, String)>,
209 lookup: Vec<(String, String)>,
210 pub(crate) fixed_list_style_children: bool,
211}
212
213impl TextNamespaceScope {
214 pub(crate) fn inherited(bindings: &[(String, String)]) -> Self {
215 Self {
216 physical: bindings.to_vec(),
217 lookup: bindings.to_vec(),
218 fixed_list_style_children: false,
219 }
220 }
221}
222
223pub(crate) fn text_fragment_scope(
224 start: &BytesStart<'_>,
225 inherited: &TextNamespaceScope,
226) -> Result<TextNamespaceScope> {
227 let physical = text_namespace_scope(start, &inherited.physical)?;
228 let mut lookup = inherited.lookup.clone();
229 if lookup.is_empty() {
230 lookup.push(("a".to_owned(), A_NS.to_owned()));
231 lookup.push(("r".to_owned(), TEXT_REL_NS.to_owned()));
232 let name = start.name();
233 let root = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
234 let prefix = root.split_once(':').map_or("", |(prefix, _)| prefix);
235 if !lookup.iter().any(|(existing, _)| existing == prefix) {
236 lookup.push((prefix.to_owned(), A_NS.to_owned()));
237 }
238 }
239 let lookup = text_namespace_scope(start, &lookup)?;
240 Ok(TextNamespaceScope {
241 physical,
242 lookup,
243 fixed_list_style_children: inherited.fixed_list_style_children,
244 })
245}
246
247pub(crate) fn text_owned_name(
248 start: &BytesStart<'_>,
249 inherited: &TextNamespaceScope,
250) -> Result<Vec<u8>> {
251 let scope = text_namespace_scope(start, &inherited.lookup)?;
252 let name = start.name();
253 let raw = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
254 let (prefix, local) = raw.split_once(':').unwrap_or(("", raw));
255 let owned = scope
256 .iter()
257 .any(|(name, uri)| name == prefix && uri == A_NS);
258 if owned
261 && inherited.fixed_list_style_children
262 && (is_paragraph_property_tag(local.as_bytes())
263 || is_character_property_tag(local.as_bytes())
264 || matches!(local, "lnSpc" | "spcBef" | "spcAft"))
265 {
266 crate::namespace::reject_conflicting_a_prefix(start)?;
267 }
268 Ok(if owned {
269 local.as_bytes().to_vec()
270 } else {
271 Vec::new()
272 })
273}
274
275fn text_attribute_tokens(start: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
278 for attribute in start.attributes() {
279 attribute.map_err(OxmlError::from)?;
280 }
281 let raw: &[u8] = start.as_ref();
282 let mut index = start.name().as_ref().len();
283 let mut output = Vec::new();
284 while index < raw.len() {
285 let token_start = index;
286 while index < raw.len() && raw[index].is_ascii_whitespace() {
287 index += 1;
288 }
289 if index == raw.len() {
290 break;
291 }
292 let name_start = index;
293 while index < raw.len() && !raw[index].is_ascii_whitespace() && raw[index] != b'=' {
294 index += 1;
295 }
296 let name = std::str::from_utf8(&raw[name_start..index])
297 .map_err(OxmlError::from)?
298 .to_owned();
299 while index < raw.len() && raw[index].is_ascii_whitespace() {
300 index += 1;
301 }
302 if raw.get(index) != Some(&b'=') {
303 return Err(invalid_attribute("text source", "token", "missing equals"));
304 }
305 index += 1;
306 while index < raw.len() && raw[index].is_ascii_whitespace() {
307 index += 1;
308 }
309 let quote = *raw
310 .get(index)
311 .ok_or_else(|| invalid_attribute("text source", "token", "missing quote"))?;
312 if quote != b'\'' && quote != b'"' {
313 return Err(invalid_attribute("text source", "token", "invalid quote"));
314 }
315 index += 1;
316 while index < raw.len() && raw[index] != quote {
317 index += 1;
318 }
319 if index == raw.len() {
320 return Err(invalid_attribute(
321 "text source",
322 "token",
323 "unterminated quote",
324 ));
325 }
326 index += 1;
327 output.push((
328 name,
329 std::str::from_utf8(&raw[token_start..index])
330 .map_err(OxmlError::from)?
331 .to_owned(),
332 ));
333 }
334 Ok(output)
335}
336
337pub(crate) fn text_root_spellings(start: &BytesStart<'_>) -> Result<Vec<String>> {
338 Ok(text_attribute_tokens(start)?
339 .into_iter()
340 .map(|(_, token)| token)
341 .collect())
342}
343
344pub(crate) fn text_root_spellings_scoped(
345 start: &BytesStart<'_>,
346 inherited: &TextNamespaceScope,
347 raw: &[u8],
348) -> Result<Vec<String>> {
349 let mut tokens = text_attribute_tokens(start)?;
350 for (prefix, uri) in text_owner_closure(raw, &text_fragment_scope(start, inherited)?.physical)?
351 {
352 let key = if prefix.is_empty() {
353 "xmlns".to_owned()
354 } else {
355 format!("xmlns:{prefix}")
356 };
357 if !tokens.iter().any(|(name, _)| name == &key) {
358 tokens.push((
359 key.clone(),
360 format!(" {key}=\"{}\"", quick_xml::escape::escape(&uri)),
361 ));
362 }
363 }
364 Ok(tokens.into_iter().map(|(_, token)| token).collect())
365}
366
367fn text_field_root_spellings(
368 start: &BytesStart<'_>,
369 inherited: &TextNamespaceScope,
370) -> Result<Vec<String>> {
371 let mut tokens = text_attribute_tokens(start)?;
372 for (prefix, uri) in text_fragment_scope(start, inherited)?.physical {
373 let key = if prefix.is_empty() {
374 "xmlns".to_owned()
375 } else {
376 format!("xmlns:{prefix}")
377 };
378 if !tokens.iter().any(|(name, _)| name == &key) {
379 tokens.push((
380 key.clone(),
381 format!(" {key}=\"{}\"", quick_xml::escape::escape(&uri)),
382 ));
383 }
384 }
385 Ok(tokens
386 .into_iter()
387 .filter(|(key, _)| key != "id" && key != "type")
388 .map(|(_, token)| token)
389 .collect())
390}
391
392pub(crate) fn text_source_start(tag: &str, spellings: &[String]) -> BytesStart<'static> {
393 BytesStart::from_content(format!("{tag}{}", spellings.concat()), tag.len())
394}
395
396pub(crate) fn text_capture_opaque_event(event: Event<'_>) -> Result<Vec<u8>> {
397 let mut writer = Writer::new(Vec::new());
398 writer.write_event(event).map_err(OxmlError::from)?;
399 Ok(writer.into_inner())
400}
401
402fn text_referenced_prefixes(raw: &[u8]) -> Result<Vec<String>> {
403 let mut reader = Reader::from_reader(raw);
404 let mut prefixes = Vec::new();
405 loop {
406 match reader.read_event().map_err(OxmlError::from)? {
407 Event::Start(start) | Event::Empty(start) => {
408 let name = start.name();
409 let name = std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?;
410 let prefix = name.split_once(':').map_or("", |(prefix, _)| prefix);
411 if !prefixes.iter().any(|p| p == prefix) {
412 prefixes.push(prefix.to_owned());
413 }
414 for attribute in start.attributes() {
415 let attribute = attribute.map_err(OxmlError::from)?;
416 let key =
417 std::str::from_utf8(attribute.key.as_ref()).map_err(OxmlError::from)?;
418 if let Some((prefix, _)) = key.split_once(':')
419 && prefix != "xmlns"
420 && !prefixes.iter().any(|p| p == prefix)
421 {
422 prefixes.push(prefix.to_owned());
423 }
424 }
425 }
426 Event::Eof => return Ok(prefixes),
427 _ => {}
428 }
429 }
430}
431
432fn text_owner_closure(raw: &[u8], physical: &[(String, String)]) -> Result<Vec<(String, String)>> {
433 let mut reader = Reader::from_reader(raw);
434 let start = match reader.read_event().map_err(OxmlError::from)? {
435 Event::Start(start) | Event::Empty(start) => start,
436 _ => return Ok(Vec::new()),
437 };
438 let mut referenced = text_referenced_prefixes(raw)?;
439 if start.name().as_ref().starts_with(b"a:")
443 && !start
444 .attributes()
445 .any(|attribute| attribute.is_ok_and(|attribute| attribute.key.as_ref() == b"xmlns:a"))
446 {
447 referenced.retain(|prefix| prefix != "a");
448 }
449 Ok(physical
450 .iter()
451 .filter(|(prefix, _)| referenced.contains(prefix))
452 .cloned()
453 .collect())
454}
455
456pub(crate) fn text_close_source_owner(
457 raw: &[u8],
458 inherited: &[(String, String)],
459 tag: &str,
460) -> Result<Vec<u8>> {
461 let mut reader = Reader::from_reader(raw);
462 let event = reader.read_event().map_err(OxmlError::from)?;
463 let (start, empty) = match event {
464 Event::Start(start) => (start, false),
465 Event::Empty(start) => (start, true),
466 _ => return Err(invalid_attribute("text source", "root", "missing owner")),
467 };
468 text_validate_root(&start)?;
469 let scope = text_namespace_scope(&start, inherited)?;
470 let closure = text_owner_closure(raw, &scope)?;
471 let mut referenced = closure
472 .iter()
473 .map(|(prefix, _)| prefix.clone())
474 .collect::<Vec<_>>();
475 if !start.name().as_ref().starts_with(b"a:") {
476 referenced.push(
477 tag.split_once(':')
478 .map_or("", |(prefix, _)| prefix)
479 .to_owned(),
480 );
481 }
482 let tokens = text_attribute_tokens(&start)?;
483 let mut body = format!(
484 "{tag}{}",
485 tokens
486 .iter()
487 .map(|(_, token)| token.as_str())
488 .collect::<String>()
489 );
490 for (prefix, uri) in &scope {
491 if !referenced.contains(prefix) {
492 continue;
493 }
494 let key = if prefix.is_empty() {
495 "xmlns".to_owned()
496 } else {
497 format!("xmlns:{prefix}")
498 };
499 if !tokens.iter().any(|(name, _)| name == &key) {
500 body.push_str(&format!(
501 " {key}=\"{}\"",
502 quick_xml::escape::escape(uri.as_str())
503 ));
504 }
505 }
506 let original_name = std::str::from_utf8(start.name().as_ref())
507 .map_err(OxmlError::from)?
508 .to_owned();
509 let owned_tag = text_source_tag(tag, &original_name, &scope);
510 let tag = owned_tag.as_str();
511 let suffix = body.strip_prefix(tag).map(str::to_owned);
512 let original_requested = body.find(char::is_whitespace).unwrap_or(body.len());
513 let body = format!(
514 "{tag}{}",
515 suffix.unwrap_or_else(|| body[original_requested..].to_owned())
516 );
517 let replacement = BytesStart::from_content(body, tag.len());
518 let mut writer = Writer::new(Vec::new());
519 if empty {
520 write_empty(&mut writer, replacement)?;
521 } else {
522 write_start(&mut writer, replacement)?;
523 let end = raw
524 .iter()
525 .rposition(|byte| *byte == b'<')
526 .ok_or_else(|| missing_end("text source"))?;
527 let begin = reader.buffer_position() as usize;
528 if end < begin {
529 return Err(missing_end("text source"));
530 }
531 writer
532 .get_mut()
533 .write_all(&raw[begin..end])
534 .map_err(OxmlError::from)?;
535 write_end(&mut writer, tag)?;
536 }
537 Ok(writer.into_inner())
538}
539
540const MAX_TEXT_MARGIN: i32 = 51_206_400;
541const MAX_TEXT_POINT: i32 = 400_000;
542const MAX_TEXT_SPACING_POINT: i32 = 158_400;
543const MAX_TRANSITIONAL_SPACING_PERCENT: i32 = 13_200_000;
544
545#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
547pub enum TextSpace {
548 #[default]
549 Default,
550 Preserve,
551}
552
553#[derive(Clone, Debug, Default)]
561pub struct TextValue {
562 pub value: String,
563 pub space: TextSpace,
564 raw_attributes: Vec<(String, String)>,
565 source_xml: Option<Vec<u8>>,
566 source_value: String,
567 source_space: TextSpace,
568 source_namespaces: Vec<(String, String)>,
569 source_attributes: Vec<(String, String)>,
570 source_tag: String,
571}
572impl PartialEq for TextValue {
573 fn eq(&self, other: &Self) -> bool {
574 self.value == other.value
575 && self.space == other.space
576 && self.raw_attributes == other.raw_attributes
577 }
578}
579impl Eq for TextValue {}
580
581impl TextValue {
582 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
583 if !matches_local_name(start.name().as_ref(), b"t") {
584 return Err(unexpected(start));
585 }
586 let space_value = text_attr(start, b"xml:space")?;
587 let space = match space_value.as_deref() {
588 None | Some("default") => TextSpace::Default,
589 Some("preserve") => TextSpace::Preserve,
590 Some(value) => return Err(invalid_attribute("t", "xml:space", value)),
591 };
592 Ok(Self {
593 value: String::new(),
594 space,
595 raw_attributes: capture_raw_attributes(start, &[b"xml:space"])?,
596 source_xml: Some(capture_empty_element(start)?),
597 source_value: String::new(),
598 source_space: space,
599 source_namespaces: text_fragment_scope(start, &TextNamespaceScope::default())?.physical,
600 source_attributes: text_attribute_tokens(start)?,
601 source_tag: std::str::from_utf8(start.name().as_ref())
602 .map_err(OxmlError::from)?
603 .to_owned(),
604 })
605 }
606
607 fn from_start_with_namespaces(
608 start: &BytesStart<'_>,
609 inherited: &TextNamespaceScope,
610 ) -> Result<Self> {
611 let mut value = Self::from_start(start)?;
612 value.source_namespaces = text_fragment_scope(start, inherited)?.physical;
613 Ok(value)
614 }
615
616 fn from_element_with_namespaces(
617 reader: &mut Reader<&[u8]>,
618 start: &BytesStart<'_>,
619 inherited: &TextNamespaceScope,
620 ) -> Result<Self> {
621 let raw = capture_element(reader, start)?;
622 let mut parsed = parse_complete(&raw, b"t", Self::from_element_model, Self::from_start)?;
623 parsed.source_namespaces = text_fragment_scope(start, inherited)?.physical;
624 parsed.source_value = parsed.value.clone();
625 parsed.source_space = parsed.space;
626 let had_explicit_space = text_attr(start, b"xml:space")?.is_some();
628 if parsed.space != TextSpace::Preserve || had_explicit_space {
629 parsed.source_xml = Some(raw);
630 } else {
631 parsed.source_xml = None;
632 }
633 Ok(parsed)
634 }
635
636 fn from_element_model(reader: &mut Reader<&[u8]>, start: &BytesStart<'_>) -> Result<Self> {
637 let mut text = Self::from_start(start)?;
638 let mut buffer = Vec::new();
639 loop {
640 match reader
641 .read_event_into(&mut buffer)
642 .map_err(OxmlError::from)?
643 {
644 Event::Text(value) => text.value.push_str(&decode_plain(&value)),
645 Event::CData(value) => {
646 let value = std::str::from_utf8(value.as_ref()).map_err(OxmlError::from)?;
647 text.value.push_str(value);
648 }
649 Event::GeneralRef(value) => text.value.push_str(&resolve_entity(&value)),
650 Event::End(element) if matches_local_name(element.name().as_ref(), b"t") => {
651 if text.space == TextSpace::Default
652 && (text.value.chars().next().is_some_and(char::is_whitespace)
653 || text
654 .value
655 .chars()
656 .next_back()
657 .is_some_and(char::is_whitespace))
658 {
659 text.space = TextSpace::Preserve;
660 }
661 return Ok(text);
662 }
663 Event::Start(element) | Event::Empty(element) => {
664 return Err(unexpected(&element));
665 }
666 Event::Eof => return Err(missing_end("t")),
667 _ => {}
668 }
669 buffer.clear();
670 }
671 }
672
673 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
674 if text_has_physical_owner(&self.source_tag, &self.source_namespaces)
675 && self.value == self.source_value
676 && self.space == self.source_space
677 && let Some(raw) = &self.source_xml
678 {
679 writer
680 .get_mut()
681 .write_all(&text_close_source_owner(
682 raw,
683 &self.source_namespaces,
684 "a:t",
685 )?)
686 .map_err(OxmlError::from)?;
687 return Ok(());
688 }
689 let preserve = self.space == TextSpace::Preserve
690 || self.value.chars().next().is_some_and(char::is_whitespace)
691 || self
692 .value
693 .chars()
694 .next_back()
695 .is_some_and(char::is_whitespace);
696 let owned_tag = text_source_tag("a:t", &self.source_tag, &self.source_namespaces);
697 let tag = owned_tag.as_str();
698 let mut start = BytesStart::new(tag);
699 if preserve {
700 start.push_attribute(("xml:space", "preserve"));
701 }
702 if self.source_attributes.is_empty() {
703 push_raw_attributes(&mut start, &self.raw_attributes);
704 } else {
705 let unknown: String = self
706 .source_attributes
707 .iter()
708 .filter(|(key, _)| key != "xml:space")
709 .map(|(_, token)| token.as_str())
710 .collect();
711 let generated = std::str::from_utf8(start.as_ref())
712 .map_err(OxmlError::from)?
713 .to_owned();
714 start = BytesStart::from_content(format!("{generated}{unknown}"), tag.len());
715 }
716 let existing_keys: Vec<String> = start
717 .attributes()
718 .map(|attribute| {
719 attribute.map_err(OxmlError::from).and_then(|attribute| {
720 std::str::from_utf8(attribute.key.as_ref())
721 .map(str::to_owned)
722 .map_err(OxmlError::from)
723 })
724 })
725 .collect::<std::result::Result<_, OxmlError>>()?;
726 let source = self
727 .source_xml
728 .clone()
729 .unwrap_or(capture_empty_element(&start)?);
730 for (prefix, uri) in text_owner_closure(&source, &self.source_namespaces)? {
731 let key = if prefix.is_empty() {
732 "xmlns".to_owned()
733 } else {
734 format!("xmlns:{prefix}")
735 };
736 if !existing_keys.contains(&key) {
737 start.push_attribute((key.as_str(), uri.as_str()));
738 }
739 }
740 if self.value.is_empty() {
741 return write_empty(writer, start);
742 }
743 write_start(writer, start)?;
744 writer
745 .write_event(Event::Text(BytesText::new(&self.value)))
746 .map_err(OxmlError::from)?;
747 write_end(writer, tag)
748 }
749}
750
751pub fn escape_invalid_xml_characters(text: &str) -> Cow<'_, str> {
754 let is_invalid = |character: char| !oxml_core::xml::is_xml_1_0_character(character);
755 if !text.chars().any(is_invalid) {
756 return Cow::Borrowed(text);
757 }
758 let mut escaped = String::with_capacity(text.len() + 6);
759 for character in text.chars() {
760 if is_invalid(character) {
761 escaped.push_str(&format!("_x{:04X}_", u32::from(character)));
762 } else {
763 escaped.push(character);
764 }
765 }
766 Cow::Owned(escaped)
767}
768
769#[derive(Clone, Copy, Debug, Eq, PartialEq)]
771pub enum TextAlignment {
772 Left,
773 Center,
774 Right,
775 Justified,
776 JustifiedLow,
777 Distributed,
778 ThaiDistributed,
779}
780
781impl TextAlignment {
782 fn parse(value: &str) -> Option<Self> {
783 match value {
784 "l" => Some(Self::Left),
785 "ctr" => Some(Self::Center),
786 "r" => Some(Self::Right),
787 "just" => Some(Self::Justified),
788 "justLow" => Some(Self::JustifiedLow),
789 "dist" => Some(Self::Distributed),
790 "thaiDist" => Some(Self::ThaiDistributed),
791 _ => None,
792 }
793 }
794
795 const fn as_str(self) -> &'static str {
796 match self {
797 Self::Left => "l",
798 Self::Center => "ctr",
799 Self::Right => "r",
800 Self::Justified => "just",
801 Self::JustifiedLow => "justLow",
802 Self::Distributed => "dist",
803 Self::ThaiDistributed => "thaiDist",
804 }
805 }
806}
807
808#[derive(Clone, Debug, Eq, PartialEq)]
810pub enum TextSpacing {
811 Percent(String),
812 Points(i32),
813}
814
815impl TextSpacing {
816 fn from_xml(xml: &[u8], wrapper: &[u8]) -> Result<Self> {
817 parse_complete(
818 xml,
819 wrapper,
820 |reader, start| Self::from_element(reader, start, wrapper),
821 |_| {
822 Err(TextError::UnexpectedElement(
823 String::from_utf8_lossy(wrapper).into_owned(),
824 ))
825 },
826 )
827 }
828
829 fn from_element(
830 reader: &mut Reader<&[u8]>,
831 start: &BytesStart<'_>,
832 wrapper: &[u8],
833 ) -> Result<Self> {
834 text_validate_root(start)?;
835 let mut spacing = None;
836 let mut buffer = Vec::new();
837 loop {
838 match reader
839 .read_event_into(&mut buffer)
840 .map_err(OxmlError::from)?
841 {
842 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"spcPct") => {
843 text_validate_root(&element)?;
844 if spacing.is_some() {
845 return Err(duplicate("text spacing choice"));
846 }
847 let value = required_attr(&element, b"val")?;
848 validate_spacing_percent(&value)?;
849 spacing = Some(Self::Percent(value));
850 }
851 Event::Empty(element) if matches_local_name(element.name().as_ref(), b"spcPts") => {
852 text_validate_root(&element)?;
853 if spacing.is_some() {
854 return Err(duplicate("text spacing choice"));
855 }
856 let value = parse_i32_attr(&element, b"val", 0, MAX_TEXT_SPACING_POINT)?;
857 spacing = Some(Self::Points(value));
858 }
859 Event::Start(element)
860 if matches_local_name(element.name().as_ref(), b"spcPct")
861 || matches_local_name(element.name().as_ref(), b"spcPts") =>
862 {
863 text_validate_root(&element)?;
864 if spacing.is_some() {
865 return Err(duplicate("text spacing choice"));
866 }
867 let name = local_name(element.name().as_ref()).to_vec();
868 let value = if name == b"spcPct" {
869 let value = required_attr(&element, b"val")?;
870 validate_spacing_percent(&value)?;
871 Self::Percent(value)
872 } else {
873 Self::Points(parse_i32_attr(&element, b"val", 0, MAX_TEXT_SPACING_POINT)?)
874 };
875 ensure_empty(reader, &name)?;
876 spacing = Some(value);
877 }
878 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
879 Event::End(element) if matches_local_name(element.name().as_ref(), wrapper) => {
880 return spacing.ok_or_else(|| {
881 TextError::UnexpectedElement(String::from_utf8_lossy(wrapper).into_owned())
882 });
883 }
884 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(wrapper))),
885 _ => {}
886 }
887 buffer.clear();
888 }
889 }
890
891 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, wrapper: &str) -> Result<()> {
892 write_start(writer, BytesStart::new(wrapper))?;
893 match self {
894 Self::Percent(value) => {
895 validate_spacing_percent(value)?;
896 let mut start = BytesStart::new("a:spcPct");
897 start.push_attribute(("val", value.as_str()));
898 write_empty(writer, start)?;
899 }
900 Self::Points(value) if (0..=MAX_TEXT_SPACING_POINT).contains(value) => {
901 let value = value.to_string();
902 let mut start = BytesStart::new("a:spcPts");
903 start.push_attribute(("val", value.as_str()));
904 write_empty(writer, start)?;
905 }
906 Self::Points(value) => {
907 return Err(invalid_attribute("spcPts", "val", &value.to_string()));
908 }
909 }
910 write_end(writer, wrapper)
911 }
912}
913
914#[derive(Clone, Debug, Eq, PartialEq)]
916pub enum TextPointValue {
917 Centipoints(i32),
918 UniversalMeasure(String),
919}
920
921impl TextPointValue {
922 fn parse(value: String) -> Result<Self> {
923 if let Ok(point) = value.parse::<i32>() {
924 if (-MAX_TEXT_POINT..=MAX_TEXT_POINT).contains(&point) {
925 return Ok(Self::Centipoints(point));
926 }
927 return Err(invalid_attribute("rPr", "spc", &value));
928 }
929 if is_universal_measure(&value) {
930 Ok(Self::UniversalMeasure(value))
931 } else {
932 Err(invalid_attribute("rPr", "spc", &value))
933 }
934 }
935
936 fn as_xml(&self) -> Result<String> {
937 match self {
938 Self::Centipoints(value) if (-MAX_TEXT_POINT..=MAX_TEXT_POINT).contains(value) => {
939 Ok(value.to_string())
940 }
941 Self::Centipoints(value) => Err(invalid_attribute("rPr", "spc", &value.to_string())),
942 Self::UniversalMeasure(value) if is_universal_measure(value) => Ok(value.clone()),
943 Self::UniversalMeasure(value) => Err(invalid_attribute("rPr", "spc", value)),
944 }
945 }
946}
947
948#[derive(Clone, Copy, Debug, Eq, PartialEq)]
950pub enum TextUnderline {
951 None,
952 Words,
953 Single,
954 Double,
955 Heavy,
956 Dotted,
957 DottedHeavy,
958 Dash,
959 DashHeavy,
960 DashLong,
961 DashLongHeavy,
962 DotDash,
963 DotDashHeavy,
964 DotDotDash,
965 DotDotDashHeavy,
966 Wavy,
967 WavyHeavy,
968 WavyDouble,
969}
970
971impl TextUnderline {
972 fn parse(value: &str) -> Option<Self> {
973 Some(match value {
974 "none" => Self::None,
975 "words" => Self::Words,
976 "sng" => Self::Single,
977 "dbl" => Self::Double,
978 "heavy" => Self::Heavy,
979 "dotted" => Self::Dotted,
980 "dottedHeavy" => Self::DottedHeavy,
981 "dash" => Self::Dash,
982 "dashHeavy" => Self::DashHeavy,
983 "dashLong" => Self::DashLong,
984 "dashLongHeavy" => Self::DashLongHeavy,
985 "dotDash" => Self::DotDash,
986 "dotDashHeavy" => Self::DotDashHeavy,
987 "dotDotDash" => Self::DotDotDash,
988 "dotDotDashHeavy" => Self::DotDotDashHeavy,
989 "wavy" => Self::Wavy,
990 "wavyHeavy" => Self::WavyHeavy,
991 "wavyDbl" => Self::WavyDouble,
992 _ => return None,
993 })
994 }
995
996 pub const fn as_str(self) -> &'static str {
998 match self {
999 Self::None => "none",
1000 Self::Words => "words",
1001 Self::Single => "sng",
1002 Self::Double => "dbl",
1003 Self::Heavy => "heavy",
1004 Self::Dotted => "dotted",
1005 Self::DottedHeavy => "dottedHeavy",
1006 Self::Dash => "dash",
1007 Self::DashHeavy => "dashHeavy",
1008 Self::DashLong => "dashLong",
1009 Self::DashLongHeavy => "dashLongHeavy",
1010 Self::DotDash => "dotDash",
1011 Self::DotDashHeavy => "dotDashHeavy",
1012 Self::DotDotDash => "dotDotDash",
1013 Self::DotDotDashHeavy => "dotDotDashHeavy",
1014 Self::Wavy => "wavy",
1015 Self::WavyHeavy => "wavyHeavy",
1016 Self::WavyDouble => "wavyDbl",
1017 }
1018 }
1019}
1020
1021#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1023pub enum TextStrike {
1024 None,
1025 Single,
1026 Double,
1027}
1028
1029impl TextStrike {
1030 fn parse(value: &str) -> Option<Self> {
1031 match value {
1032 "noStrike" => Some(Self::None),
1033 "sngStrike" => Some(Self::Single),
1034 "dblStrike" => Some(Self::Double),
1035 _ => None,
1036 }
1037 }
1038
1039 const fn as_str(self) -> &'static str {
1040 match self {
1041 Self::None => "noStrike",
1042 Self::Single => "sngStrike",
1043 Self::Double => "dblStrike",
1044 }
1045 }
1046}
1047
1048#[derive(Clone, Debug, Eq, PartialEq)]
1050pub struct TextFont {
1051 pub typeface: String,
1052 raw_attributes: Vec<(String, String)>,
1053}
1054
1055impl TextFont {
1056 pub fn new(typeface: impl Into<String>) -> Result<Self> {
1057 let typeface = typeface.into();
1058 oxml_core::xml::reject_non_xml_characters("font typeface", &typeface)?;
1059 if typeface.is_empty() {
1060 return Err(invalid_attribute("font", "typeface", ""));
1061 }
1062 Ok(Self {
1063 typeface,
1064 raw_attributes: Vec::new(),
1065 })
1066 }
1067
1068 pub(crate) fn from_xml(xml: &[u8], expected: &[u8]) -> Result<Self> {
1069 parse_complete(
1070 xml,
1071 expected,
1072 |reader, start| {
1073 let font = Self::from_start(start)?;
1074 ensure_empty(reader, expected)?;
1075 Ok(font)
1076 },
1077 Self::from_start,
1078 )
1079 }
1080
1081 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
1082 text_validate_root(start)?;
1083 Ok(Self {
1084 typeface: required_attr(start, b"typeface")?,
1085 raw_attributes: capture_raw_attributes(start, &[b"typeface"])?,
1086 })
1087 }
1088
1089 pub(crate) fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
1090 if self.typeface.is_empty() {
1091 return Err(invalid_attribute(tag, "typeface", ""));
1092 }
1093 let mut start = BytesStart::new(tag);
1094 start.push_attribute(("typeface", self.typeface.as_str()));
1095 push_raw_attributes(&mut start, &self.raw_attributes);
1096 write_empty(writer, start)
1097 }
1098}
1099
1100#[derive(Clone, Debug, Default)]
1102pub struct TextHyperlink {
1103 pub relationship_id: Option<String>,
1104 pub invalid_url: Option<String>,
1105 pub action: Option<String>,
1106 pub target_frame: Option<String>,
1107 pub tooltip: Option<String>,
1108 pub history: Option<bool>,
1109 pub highlight_click: Option<bool>,
1110 pub end_sound: Option<bool>,
1111 raw_attributes: Vec<(String, String)>,
1112 raw_children: OrderedRawChildren,
1113 source_xml: Option<Vec<u8>>,
1114 source_namespaces: Vec<(String, String)>,
1115 source_values: Vec<(String, Option<String>)>,
1116 source_id_key: Option<String>,
1117}
1118impl PartialEq for TextHyperlink {
1119 fn eq(&self, other: &Self) -> bool {
1120 self.relationship_id == other.relationship_id
1121 && self.invalid_url == other.invalid_url
1122 && self.action == other.action
1123 && self.target_frame == other.target_frame
1124 && self.tooltip == other.tooltip
1125 && self.history == other.history
1126 && self.highlight_click == other.highlight_click
1127 && self.end_sound == other.end_sound
1128 && self
1129 .raw_attributes
1130 .iter()
1131 .filter(|(name, value)| {
1132 !((name == "xmlns:a" && value == A_NS)
1133 || (name == "xmlns:r" && value == TEXT_REL_NS))
1134 })
1135 .eq(other.raw_attributes.iter().filter(|(name, value)| {
1136 !((name == "xmlns:a" && value == A_NS)
1137 || (name == "xmlns:r" && value == TEXT_REL_NS))
1138 }))
1139 && self.raw_children == other.raw_children
1140 }
1141}
1142impl Eq for TextHyperlink {}
1143
1144impl TextHyperlink {
1145 fn from_xml_with_namespaces(
1146 xml: &[u8],
1147 expected: &[u8],
1148 inherited: &TextNamespaceScope,
1149 ) -> Result<Self> {
1150 let mut result = parse_complete(
1151 xml,
1152 expected,
1153 |reader, start| Self::from_element_with_namespaces(reader, start, expected, inherited),
1154 |start| Self::from_start_with_namespaces(start, inherited),
1155 )?;
1156 result.source_xml = Some(xml.to_vec());
1157 result.source_values = result.attribute_values();
1158 Ok(result)
1159 }
1160
1161 fn from_start_with_namespaces(
1162 start: &BytesStart<'_>,
1163 inherited: &TextNamespaceScope,
1164 ) -> Result<Self> {
1165 text_validate_root(start)?;
1166 let namespaces = text_fragment_scope(start, inherited)?;
1167 if text_owned_name(start, &namespaces)?.is_empty() {
1168 return Err(invalid_attribute("hyperlink", "namespace", "foreign owner"));
1169 }
1170 let mut relationship_id = None;
1171 let mut source_id_key = None;
1172 for attribute in start.attributes() {
1173 let attribute = attribute.map_err(OxmlError::from)?;
1174 let key = std::str::from_utf8(attribute.key.as_ref()).map_err(OxmlError::from)?;
1175 if let Some((prefix, "id")) = key.split_once(':')
1176 && namespaces
1177 .lookup
1178 .iter()
1179 .any(|(name, uri)| name == prefix && uri == TEXT_REL_NS)
1180 {
1181 if source_id_key.is_some() {
1182 return Err(duplicate("hyperlink relationship id"));
1183 }
1184 relationship_id = Some(
1185 attribute
1186 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
1187 .map_err(OxmlError::from)?
1188 .into_owned(),
1189 );
1190 source_id_key = Some(key.to_owned());
1191 }
1192 }
1193 let mut raw_attributes = capture_raw_attributes(
1194 start,
1195 &[
1196 b"invalidUrl",
1197 b"action",
1198 b"tgtFrame",
1199 b"tooltip",
1200 b"history",
1201 b"highlightClick",
1202 b"endSnd",
1203 ],
1204 )?;
1205 raw_attributes.retain(|(key, _)| Some(key) != source_id_key.as_ref());
1206 Ok(Self {
1207 relationship_id,
1208 invalid_url: text_attr(start, b"invalidUrl")?,
1209 action: text_attr(start, b"action")?,
1210 target_frame: text_attr(start, b"tgtFrame")?,
1211 tooltip: text_attr(start, b"tooltip")?,
1212 history: parse_optional_bool(start, b"history")?,
1213 highlight_click: parse_optional_bool(start, b"highlightClick")?,
1214 end_sound: parse_optional_bool(start, b"endSnd")?,
1215 raw_attributes,
1216 raw_children: OrderedRawChildren::default(),
1217 source_xml: None,
1218 source_namespaces: namespaces.physical,
1219 source_values: Vec::new(),
1220 source_id_key,
1221 })
1222 }
1223
1224 fn from_element_with_namespaces(
1225 reader: &mut Reader<&[u8]>,
1226 start: &BytesStart<'_>,
1227 expected: &[u8],
1228 inherited: &TextNamespaceScope,
1229 ) -> Result<Self> {
1230 let mut hyperlink = Self::from_start_with_namespaces(start, inherited)?;
1231 let namespaces = text_fragment_scope(start, inherited)?;
1232 let mut boundary = 0;
1233 let mut seen = [false; 2];
1234 let mut buffer = Vec::new();
1235 loop {
1236 match reader
1237 .read_event_into(&mut buffer)
1238 .map_err(OxmlError::from)?
1239 {
1240 Event::Start(element) => {
1241 let name = text_owned_name(&element, &namespaces)?;
1242 let raw = capture_element(reader, &element)?;
1243 capture_hyperlink_raw(&mut hyperlink, &name, raw, &mut boundary, &mut seen)?;
1244 }
1245 Event::Empty(element) => {
1246 let name = text_owned_name(&element, &namespaces)?;
1247 let raw = capture_empty_element(&element)?;
1248 capture_hyperlink_raw(&mut hyperlink, &name, raw, &mut boundary, &mut seen)?;
1249 }
1250 Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
1251 return Ok(hyperlink);
1252 }
1253 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
1254 _ => {}
1255 }
1256 buffer.clear();
1257 }
1258 }
1259
1260 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
1261 if self.source_namespaces.iter().any(|(_, uri)| uri == A_NS)
1262 && let Some(source) = &self.source_xml
1263 {
1264 return self.write_source_xml(writer, tag, source);
1265 }
1266 let mut start = BytesStart::new(tag);
1267 if let Some(id) = self.relationship_id.as_deref() {
1270 let occupied = |prefix: &str| {
1271 self.raw_attributes.iter().any(|(key, value)| {
1272 key.starts_with(&format!("{prefix}:"))
1273 || (key == &format!("xmlns:{prefix}") && value != TEXT_REL_NS)
1274 })
1275 };
1276 let prefix = (0..=self.raw_attributes.len() + 1)
1277 .map(|index| {
1278 if index == 0 {
1279 "r".to_owned()
1280 } else {
1281 format!("rLink{index}")
1282 }
1283 })
1284 .find(|prefix| !occupied(prefix))
1285 .ok_or_else(|| {
1286 invalid_attribute("hyperlink", "namespace", "no relationship prefix")
1287 })?;
1288 let declaration = format!("xmlns:{prefix}");
1289 if self.source_xml.is_none()
1290 && !self
1291 .raw_attributes
1292 .iter()
1293 .any(|(key, value)| key == &declaration && value == TEXT_REL_NS)
1294 {
1295 start.push_attribute((declaration.as_str(), TEXT_REL_NS));
1296 }
1297 let key = format!("{prefix}:id");
1298 start.push_attribute((key.as_str(), id));
1299 }
1300 push_optional_attr(&mut start, "invalidUrl", self.invalid_url.as_deref());
1301 push_optional_attr(&mut start, "action", self.action.as_deref());
1302 push_optional_attr(&mut start, "tgtFrame", self.target_frame.as_deref());
1303 push_optional_attr(&mut start, "tooltip", self.tooltip.as_deref());
1304 push_optional_bool(&mut start, "history", self.history);
1305 push_optional_bool(&mut start, "highlightClick", self.highlight_click);
1306 push_optional_bool(&mut start, "endSnd", self.end_sound);
1307 push_raw_attributes(&mut start, &self.raw_attributes);
1308 if self.raw_children.is_empty() {
1309 return write_empty(writer, start);
1310 }
1311 write_start(writer, start)?;
1312 for boundary in 0..=2 {
1313 emit_raw(writer, self.raw_children.at(boundary))?;
1314 }
1315 write_end(writer, tag)
1316 }
1317
1318 fn attribute_values(&self) -> Vec<(String, Option<String>)> {
1319 vec![
1320 ("id".to_owned(), self.relationship_id.clone()),
1321 ("invalidUrl".to_owned(), self.invalid_url.clone()),
1322 ("action".to_owned(), self.action.clone()),
1323 ("tgtFrame".to_owned(), self.target_frame.clone()),
1324 ("tooltip".to_owned(), self.tooltip.clone()),
1325 (
1326 "history".to_owned(),
1327 self.history.map(|v| if v { "1" } else { "0" }.to_owned()),
1328 ),
1329 (
1330 "highlightClick".to_owned(),
1331 self.highlight_click
1332 .map(|v| if v { "1" } else { "0" }.to_owned()),
1333 ),
1334 (
1335 "endSnd".to_owned(),
1336 self.end_sound.map(|v| if v { "1" } else { "0" }.to_owned()),
1337 ),
1338 ]
1339 }
1340
1341 fn write_source_xml<W: Write>(
1342 &self,
1343 writer: &mut Writer<W>,
1344 tag: &str,
1345 source: &[u8],
1346 ) -> Result<()> {
1347 let mut reader = Reader::from_reader(source);
1348 let mut buffer = Vec::new();
1349 let (start, empty) = match reader
1350 .read_event_into(&mut buffer)
1351 .map_err(OxmlError::from)?
1352 {
1353 Event::Start(start) => (start, false),
1354 Event::Empty(start) => (start, true),
1355 _ => return Err(invalid_attribute("hyperlink", "source", "missing owner")),
1356 };
1357 let head_end = reader.buffer_position() as usize;
1358 let tokens = text_attribute_tokens(&start)?;
1359 let current = self.attribute_values();
1360 let changed: Vec<_> = current
1361 .iter()
1362 .filter(|(name, value)| {
1363 self.source_values
1364 .iter()
1365 .find(|(old, _)| old == name)
1366 .map(|(_, original)| original)
1367 != Some(value)
1368 })
1369 .collect();
1370 let original_name = std::str::from_utf8(start.name().as_ref())
1371 .map_err(OxmlError::from)?
1372 .to_owned();
1373 let owned_tag = text_source_tag(tag, &original_name, &self.source_namespaces);
1374 let tag = owned_tag.as_str();
1375 let mut body = tag.to_owned();
1376 let mut emitted = Vec::new();
1377 let mut id_key = self.source_id_key.clone();
1378 let mut namespace_additions = Vec::new();
1379 let mut referenced = text_referenced_prefixes(source)?;
1380 referenced.push(
1381 tag.split_once(':')
1382 .map_or("", |(prefix, _)| prefix)
1383 .to_owned(),
1384 );
1385 if self.relationship_id.is_some() && id_key.is_none() {
1386 let prefix = self
1387 .source_namespaces
1388 .iter()
1389 .find(|(prefix, uri)| !prefix.is_empty() && uri == TEXT_REL_NS)
1390 .map(|(prefix, _)| prefix.clone())
1391 .or_else(|| {
1392 (0..=self.source_namespaces.len())
1393 .map(|index| {
1394 if index == 0 {
1395 "r".to_owned()
1396 } else {
1397 format!("rLink{index}")
1398 }
1399 })
1400 .find(|prefix| {
1401 !self
1402 .source_namespaces
1403 .iter()
1404 .any(|(name, uri)| name == prefix && uri != TEXT_REL_NS)
1405 })
1406 })
1407 .ok_or_else(|| {
1408 invalid_attribute("hyperlink", "namespace", "no relationship prefix")
1409 })?;
1410 if !self
1411 .source_namespaces
1412 .iter()
1413 .any(|(name, uri)| name == &prefix && uri == TEXT_REL_NS)
1414 {
1415 namespace_additions.push((prefix.clone(), TEXT_REL_NS.to_owned()));
1416 }
1417 referenced.push(prefix.clone());
1418 id_key = Some(format!("{prefix}:id"));
1419 }
1420 for (key, token) in &tokens {
1421 let owned = if self.source_id_key.as_ref() == Some(key) {
1424 Some("id")
1425 } else if key == "id" {
1426 None
1427 } else {
1428 Some(key.as_str())
1429 };
1430 if let Some((name, value)) = changed
1431 .iter()
1432 .find(|(name, _)| Some(name.as_str()) == owned)
1433 {
1434 emitted.push(name.as_str());
1435 if let Some(value) = value {
1436 let key = if owned == Some("id") {
1437 id_key.as_deref().unwrap_or("r:id")
1438 } else {
1439 key.as_str()
1440 };
1441 body.push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(value)));
1442 }
1443 } else {
1444 body.push_str(token);
1445 }
1446 }
1447 for (name, value) in &changed {
1448 if emitted.contains(&name.as_str()) {
1449 continue;
1450 }
1451 if let Some(value) = value {
1452 let key = if name == "id" {
1453 id_key.as_deref().unwrap_or("r:id")
1454 } else {
1455 name.as_str()
1456 };
1457 body.push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(value)));
1458 }
1459 }
1460 for (prefix, uri) in self
1461 .source_namespaces
1462 .iter()
1463 .chain(namespace_additions.iter())
1464 {
1465 if !referenced.contains(prefix) {
1466 continue;
1467 }
1468 let key = if prefix.is_empty() {
1469 "xmlns".to_owned()
1470 } else {
1471 format!("xmlns:{prefix}")
1472 };
1473 if tokens.iter().any(|(name, _)| name == &key) {
1474 continue;
1475 }
1476 body.push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(uri)));
1477 }
1478 let replacement = BytesStart::from_content(body, tag.len());
1479 if empty {
1480 return write_empty(writer, replacement);
1481 }
1482 write_start(writer, replacement)?;
1483 let end_start = source
1484 .iter()
1485 .rposition(|byte| *byte == b'<')
1486 .ok_or_else(|| missing_end("hyperlink"))?;
1487 if end_start < head_end {
1488 return Err(missing_end("hyperlink"));
1489 }
1490 writer
1491 .get_mut()
1492 .write_all(&source[head_end..end_start])
1493 .map_err(OxmlError::from)?;
1494 write_end(writer, tag)
1495 }
1496}
1497
1498fn capture_hyperlink_raw(
1499 hyperlink: &mut TextHyperlink,
1500 name: &[u8],
1501 raw: Vec<u8>,
1502 boundary: &mut usize,
1503 seen: &mut [bool; 2],
1504) -> Result<()> {
1505 let slot = match name {
1506 b"snd" => Some(0),
1507 b"extLst" => Some(1),
1508 _ => None,
1509 };
1510 if let Some(slot) = slot {
1511 if seen[slot] {
1512 return Err(duplicate(&String::from_utf8_lossy(name)));
1513 }
1514 seen[slot] = true;
1515 hyperlink.raw_children.push((*boundary).max(slot), raw);
1516 *boundary = (*boundary).max(slot + 1);
1517 } else {
1518 hyperlink.raw_children.push(*boundary, raw);
1519 }
1520 Ok(())
1521}
1522
1523#[allow(non_camel_case_types)]
1525#[derive(Clone, Debug, Default)]
1526pub struct CT_TextCharacterProperties {
1527 pub font_size: Option<i32>,
1528 pub bold: Option<bool>,
1529 pub italic: Option<bool>,
1530 pub all_caps: Option<bool>,
1531 pub small_caps: Option<bool>,
1532 pub highlight: Option<ColorChoice>,
1533 highlight_source: Option<(Vec<u8>, std::ops::Range<usize>, ColorChoice)>,
1534 pub underline: Option<TextUnderline>,
1535 pub strike: Option<TextStrike>,
1536 pub spacing: Option<TextPointValue>,
1537 pub baseline: Option<String>,
1538 pub fill: Option<Fill>,
1539 pub latin: Option<TextFont>,
1540 pub east_asian: Option<TextFont>,
1541 pub complex_script: Option<TextFont>,
1542 pub symbol: Option<TextFont>,
1543 pub hyperlink_click: Option<TextHyperlink>,
1544 pub hyperlink_mouse_over: Option<TextHyperlink>,
1545 raw_attributes: Vec<(String, String)>,
1546 source_attributes: Vec<(String, String)>,
1547 source_tag: String,
1548 source_namespaces: Vec<(String, String)>,
1549 source_closure: Vec<(String, String)>,
1550 source_fill: Option<(Vec<u8>, Fill)>,
1551 source_latin: Option<(Vec<u8>, TextFont)>,
1552 source_east_asian: Option<(Vec<u8>, TextFont)>,
1553 source_complex_script: Option<(Vec<u8>, TextFont)>,
1554 source_symbol: Option<(Vec<u8>, TextFont)>,
1555 source_cap_modeled: bool,
1556 raw_children: OrderedRawChildren,
1557}
1558impl PartialEq for CT_TextCharacterProperties {
1559 fn eq(&self, other: &Self) -> bool {
1560 self.font_size == other.font_size
1561 && self.bold == other.bold
1562 && self.italic == other.italic
1563 && self.all_caps == other.all_caps
1564 && self.small_caps == other.small_caps
1565 && self.highlight == other.highlight
1566 && self.highlight_source == other.highlight_source
1567 && self.underline == other.underline
1568 && self.strike == other.strike
1569 && self.spacing == other.spacing
1570 && self.baseline == other.baseline
1571 && self.fill == other.fill
1572 && self.latin == other.latin
1573 && self.east_asian == other.east_asian
1574 && self.complex_script == other.complex_script
1575 && self.symbol == other.symbol
1576 && self.hyperlink_click == other.hyperlink_click
1577 && self.hyperlink_mouse_over == other.hyperlink_mouse_over
1578 && self
1579 .raw_attributes
1580 .iter()
1581 .filter(|(name, value)| {
1582 !((name == "xmlns:a" && value == A_NS)
1583 || (name == "xmlns:r" && value == TEXT_REL_NS))
1584 })
1585 .eq(other.raw_attributes.iter().filter(|(name, value)| {
1586 !((name == "xmlns:a" && value == A_NS)
1587 || (name == "xmlns:r" && value == TEXT_REL_NS))
1588 }))
1589 && self.raw_children == other.raw_children
1590 }
1591}
1592impl Eq for CT_TextCharacterProperties {}
1593
1594impl CT_TextCharacterProperties {
1595 pub fn with_inherited_defaults(&self, defaults: &Self) -> Result<Self> {
1597 let mut local = Writer::new(Vec::new());
1598 self.write_xml(&mut local, "a:rPr")?;
1599 let mut inherited = Writer::new(Vec::new());
1600 defaults.write_xml(&mut inherited, "a:rPr")?;
1601 let xml = text_inherit_property_xml(
1602 &local.into_inner(),
1603 &self.source_namespaces,
1604 &inherited.into_inner(),
1605 &defaults.source_namespaces,
1606 )?;
1607 Self::from_xml_with_namespaces(
1608 &xml,
1609 &TextNamespaceScope::inherited(&self.source_namespaces),
1610 )
1611 }
1612
1613 fn highlight_scope_attributes(&self) -> Vec<(String, String)> {
1614 let mut attributes = self.raw_attributes.clone();
1615 for (prefix, uri) in &self.source_namespaces {
1616 let key = if prefix.is_empty() {
1617 "xmlns".to_owned()
1618 } else {
1619 format!("xmlns:{prefix}")
1620 };
1621 if !attributes.iter().any(|(name, _)| name == &key) {
1622 attributes.push((key, uri.clone()));
1623 }
1624 }
1625 attributes
1626 }
1627
1628 pub fn language(&self) -> Option<&str> {
1630 self.raw_attributes
1631 .iter()
1632 .find(|(name, _)| name == "lang")
1633 .map(|(_, value)| value.as_str())
1634 }
1635
1636 pub fn set_language(&mut self, language: Option<&str>) {
1639 if self.language() == language {
1640 return;
1641 }
1642 let position = self
1643 .raw_attributes
1644 .iter()
1645 .position(|(name, _)| name == "lang");
1646 match (position, language) {
1647 (Some(index), Some(value)) => self.raw_attributes[index].1 = value.to_owned(),
1648 (Some(index), None) => {
1649 self.raw_attributes.remove(index);
1650 }
1651 (None, Some(value)) => self
1652 .raw_attributes
1653 .push(("lang".to_owned(), value.to_owned())),
1654 (None, None) => {}
1655 }
1656 if !self.source_attributes.is_empty() {
1657 let position = self
1658 .source_attributes
1659 .iter()
1660 .position(|(name, _)| name == "lang");
1661 let token =
1662 language.map(|value| format!(" lang=\"{}\"", quick_xml::escape::escape(value)));
1663 match (position, token) {
1664 (Some(index), Some(token)) => self.source_attributes[index].1 = token,
1665 (Some(index), None) => {
1666 self.source_attributes.remove(index);
1667 }
1668 (None, Some(token)) => self.source_attributes.push(("lang".to_owned(), token)),
1669 (None, None) => {}
1670 }
1671 }
1672 }
1673
1674 pub fn from_xml(xml: &[u8]) -> Result<Self> {
1676 let expected = root_local_name(xml)?;
1677 if !is_character_property_tag(&expected) {
1678 return Err(TextError::UnexpectedElement(
1679 String::from_utf8_lossy(&expected).into_owned(),
1680 ));
1681 }
1682 Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
1683 }
1684
1685 pub(crate) fn from_xml_with_namespaces(
1686 xml: &[u8],
1687 inherited: &TextNamespaceScope,
1688 ) -> Result<Self> {
1689 let expected = root_local_name(xml)?;
1690 parse_complete(
1691 xml,
1692 &expected,
1693 |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
1694 |start| Self::from_start_with_namespaces(start, inherited),
1695 )
1696 }
1697
1698 fn from_start_with_namespaces(
1699 start: &BytesStart<'_>,
1700 inherited: &TextNamespaceScope,
1701 ) -> Result<Self> {
1702 let mut parsed = Self::from_start(start)?;
1703 parsed.source_namespaces = text_fragment_scope(start, inherited)?.physical;
1704 parsed.source_closure =
1705 text_owner_closure(&capture_empty_element(start)?, &parsed.source_namespaces)?;
1706 Ok(parsed)
1707 }
1708
1709 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
1710 text_validate_root(start)?;
1711 let element = String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned();
1712 let font_size = text_attr(start, b"sz")?
1713 .map(|value| parse_i32_value(&element, "sz", &value, 100, MAX_TEXT_POINT))
1714 .transpose()?;
1715 let spacing = text_attr(start, b"spc")?
1716 .map(TextPointValue::parse)
1717 .transpose()?;
1718 let baseline = text_attr(start, b"baseline")?;
1719 if let Some(value) = baseline.as_deref() {
1720 validate_baseline(value)?;
1721 }
1722 let all_caps = match text_attr(start, b"cap")?.as_deref() {
1723 Some("all") => Some(true),
1724 Some("none") => Some(false),
1725 _ => None,
1726 };
1727 let small_caps = match text_attr(start, b"cap")?.as_deref() {
1728 Some("small") => Some(true),
1729 Some("none") => Some(false),
1730 _ => None,
1731 };
1732 let mut raw_attributes = capture_raw_attributes(
1733 start,
1734 &[b"sz", b"b", b"i", b"u", b"strike", b"spc", b"baseline"],
1735 )?;
1736 if all_caps.is_some() || small_caps.is_some() {
1737 raw_attributes.retain(|(name, _)| name != "cap");
1738 }
1739 Ok(Self {
1740 font_size,
1741 bold: parse_optional_bool(start, b"b")?,
1742 italic: parse_optional_bool(start, b"i")?,
1743 all_caps,
1744 small_caps,
1745 underline: parse_optional_enum(start, b"u", TextUnderline::parse)?,
1746 strike: parse_optional_enum(start, b"strike", TextStrike::parse)?,
1747 spacing,
1748 baseline,
1749 source_attributes: text_attribute_tokens(start)?,
1750 source_tag: std::str::from_utf8(start.name().as_ref())
1751 .map_err(OxmlError::from)?
1752 .to_owned(),
1753 source_namespaces: text_fragment_scope(start, &TextNamespaceScope::default())?.physical,
1754 source_closure: text_owner_closure(
1755 &capture_empty_element(start)?,
1756 &text_fragment_scope(start, &TextNamespaceScope::default())?.physical,
1757 )?,
1758 source_cap_modeled: matches!(
1759 text_attr(start, b"cap")?.as_deref(),
1760 Some("all" | "small" | "none")
1761 ),
1762 raw_attributes,
1763 ..Self::default()
1764 })
1765 }
1766
1767 fn from_element_with_namespaces(
1768 reader: &mut Reader<&[u8]>,
1769 start: &BytesStart<'_>,
1770 inherited: &TextNamespaceScope,
1771 ) -> Result<Self> {
1772 let namespaces = text_fragment_scope(start, inherited)?;
1773 let expected = local_name(start.name().as_ref()).to_vec();
1774 let mut properties = Self::from_start(start)?;
1775 properties.source_namespaces = namespaces.physical.clone();
1776 properties.source_closure = text_owner_closure(
1777 &capture_element(&mut reader.clone(), start)?,
1778 &properties.source_namespaces,
1779 )?;
1780 let mut boundary = 0;
1781 let mut seen = [false; 14];
1782 let mut buffer = Vec::new();
1783 loop {
1784 match reader
1785 .read_event_into(&mut buffer)
1786 .map_err(OxmlError::from)?
1787 {
1788 Event::Start(element) => {
1789 let name = text_owned_name(&element, &namespaces)?;
1790 let raw = capture_element(reader, &element)?;
1791 properties.capture_child(&name, raw, &mut boundary, &mut seen, &namespaces)?;
1792 }
1793 Event::Empty(element) => {
1794 let name = text_owned_name(&element, &namespaces)?;
1795 let raw = capture_empty_element(&element)?;
1796 properties.capture_child(&name, raw, &mut boundary, &mut seen, &namespaces)?;
1797 }
1798 Event::End(element) if matches_local_name(element.name().as_ref(), &expected) => {
1799 return Ok(properties);
1800 }
1801 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(&expected))),
1802 event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
1803 properties
1804 .raw_children
1805 .push(boundary, text_capture_opaque_event(event)?);
1806 }
1807 _ => {}
1808 }
1809 buffer.clear();
1810 }
1811 }
1812
1813 fn capture_child(
1814 &mut self,
1815 name: &[u8],
1816 raw: Vec<u8>,
1817 boundary: &mut usize,
1818 seen: &mut [bool; 14],
1819 namespaces: &TextNamespaceScope,
1820 ) -> Result<()> {
1821 let mut source_reader = Reader::from_reader(raw.as_slice());
1822 let physical_owner = match source_reader.read_event().map_err(OxmlError::from)? {
1823 Event::Start(start) | Event::Empty(start) => {
1824 let name = start.name();
1825 text_has_physical_owner(
1826 std::str::from_utf8(name.as_ref()).map_err(OxmlError::from)?,
1827 &text_namespace_scope(&start, &namespaces.physical)?,
1828 )
1829 }
1830 _ => false,
1831 };
1832 let name = if name == b"highlight"
1834 && !character_highlight_is_drawingml(&raw, &self.highlight_scope_attributes())?
1835 {
1836 b"".as_slice()
1837 } else {
1838 name
1839 };
1840 let slot = character_property_slot(name);
1841 if let Some(slot) = slot {
1842 if seen[slot - 1] {
1843 return Err(duplicate(&String::from_utf8_lossy(name)));
1844 }
1845 seen[slot - 1] = true;
1846 }
1847 match name {
1848 name if is_fill(name) => {
1849 let value = Fill::from_xml(&raw)?;
1850 self.source_fill = physical_owner.then(|| (raw, value.clone()));
1851 self.fill = Some(value);
1852 }
1853 b"highlight" => {
1854 let (colour, range) =
1855 character_highlight_colour(&raw, &self.highlight_scope_attributes())?;
1856 self.highlight = Some(colour.clone());
1857 self.highlight_source = Some((raw, range, colour));
1858 }
1859 b"latin" => {
1860 let value = TextFont::from_xml(&raw, b"latin")?;
1861 self.source_latin = physical_owner.then(|| (raw, value.clone()));
1862 self.latin = Some(value);
1863 }
1864 b"ea" => {
1865 let value = TextFont::from_xml(&raw, b"ea")?;
1866 self.source_east_asian = physical_owner.then(|| (raw, value.clone()));
1867 self.east_asian = Some(value);
1868 }
1869 b"cs" => {
1870 let value = TextFont::from_xml(&raw, b"cs")?;
1871 self.source_complex_script = physical_owner.then(|| (raw, value.clone()));
1872 self.complex_script = Some(value);
1873 }
1874 b"sym" => {
1875 let value = TextFont::from_xml(&raw, b"sym")?;
1876 self.source_symbol = physical_owner.then(|| (raw, value.clone()));
1877 self.symbol = Some(value);
1878 }
1879 b"hlinkClick" => {
1880 self.hyperlink_click = Some(TextHyperlink::from_xml_with_namespaces(
1881 &raw,
1882 b"hlinkClick",
1883 namespaces,
1884 )?)
1885 }
1886 b"hlinkMouseOver" => {
1887 self.hyperlink_mouse_over = Some(TextHyperlink::from_xml_with_namespaces(
1888 &raw,
1889 b"hlinkMouseOver",
1890 namespaces,
1891 )?)
1892 }
1893 _ => self.raw_children.push(
1894 slot.map_or(*boundary, |slot| (*boundary).max(slot - 1)),
1895 raw,
1896 ),
1897 }
1898 if let Some(slot) = slot {
1899 *boundary = (*boundary).max(slot);
1900 }
1901 Ok(())
1902 }
1903
1904 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
1906 let owned_tag = text_source_tag(tag, &self.source_tag, &self.source_namespaces);
1907 let tag = owned_tag.as_str();
1908 if self.all_caps == Some(true) && self.small_caps == Some(true) {
1909 return Err(invalid_attribute(tag, "cap", "conflicting all and small"));
1910 }
1911 self.write_highlight(&mut Writer::new(Vec::new()))?;
1912 let mut start = BytesStart::new(tag);
1913 if let Some(value) = self.font_size {
1914 parse_i32_value(tag, "sz", &value.to_string(), 100, MAX_TEXT_POINT)?;
1915 push_owned_attr(&mut start, "sz", value.to_string());
1916 }
1917 push_optional_bool(&mut start, "b", self.bold);
1918 push_optional_bool(&mut start, "i", self.italic);
1919 let caps = if self.all_caps == Some(true) {
1920 Some("all")
1921 } else if self.small_caps == Some(true) {
1922 Some("small")
1923 } else if self.all_caps == Some(false) || self.small_caps == Some(false) {
1924 Some("none")
1925 } else {
1926 None
1927 };
1928 if let Some(value) = caps {
1929 start.push_attribute(("cap", value));
1930 }
1931 if let Some(value) = self.underline {
1932 start.push_attribute(("u", value.as_str()));
1933 }
1934 if let Some(value) = self.strike {
1935 start.push_attribute(("strike", value.as_str()));
1936 }
1937 if let Some(value) = &self.spacing {
1938 push_owned_attr(&mut start, "spc", value.as_xml()?);
1939 }
1940 if let Some(value) = self.baseline.as_deref() {
1941 validate_baseline(value)?;
1942 start.push_attribute(("baseline", value));
1943 }
1944 if self.source_attributes.is_empty() {
1945 push_raw_attributes(&mut start, &self.raw_attributes);
1946 } else {
1947 let modeled = ["sz", "b", "i", "u", "strike", "spc", "baseline"];
1948 let preserved: Vec<String> = self
1949 .source_attributes
1950 .iter()
1951 .filter(|(key, _)| {
1952 !modeled.contains(&key.as_str())
1953 && !(key == "cap"
1954 && (self.source_cap_modeled
1955 || self.all_caps.is_some()
1956 || self.small_caps.is_some()))
1957 })
1958 .map(|(_, token)| token.clone())
1959 .collect();
1960 let generated = std::str::from_utf8(start.as_ref())
1961 .map_err(OxmlError::from)?
1962 .to_owned();
1963 start =
1964 BytesStart::from_content(format!("{generated}{}", preserved.concat()), tag.len());
1965 }
1966
1967 let existing_keys: Vec<String> = start
1968 .attributes()
1969 .map(|attribute| {
1970 attribute.map_err(OxmlError::from).and_then(|attribute| {
1971 std::str::from_utf8(attribute.key.as_ref())
1972 .map(str::to_owned)
1973 .map_err(OxmlError::from)
1974 })
1975 })
1976 .collect::<std::result::Result<_, OxmlError>>()?;
1977 for (prefix, uri) in &self.source_closure {
1978 let key = if prefix.is_empty() {
1979 "xmlns".to_owned()
1980 } else {
1981 format!("xmlns:{prefix}")
1982 };
1983 if !existing_keys.contains(&key) {
1984 start.push_attribute((key.as_str(), uri.as_str()));
1985 }
1986 }
1987 let has_modelled_children = self.highlight.is_some()
1988 || self.highlight_source.is_some()
1989 || self.fill.is_some()
1990 || self.latin.is_some()
1991 || self.east_asian.is_some()
1992 || self.complex_script.is_some()
1993 || self.symbol.is_some()
1994 || self.hyperlink_click.is_some()
1995 || self.hyperlink_mouse_over.is_some();
1996 if !has_modelled_children && self.raw_children.is_empty() {
1997 return write_empty(writer, start);
1998 }
1999 write_start(writer, start)?;
2000 emit_raw(writer, self.raw_children.at(0))?;
2001 emit_raw(writer, self.raw_children.at(1))?;
2002 if let Some(fill) = &self.fill {
2003 let mut child = Writer::new(Vec::new());
2004 {
2005 let writer = &mut child;
2006 if let Some((raw, original)) = &self.source_fill {
2007 if fill == original {
2008 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2009 } else {
2010 fill.write_xml(writer)?;
2011 }
2012 } else {
2013 fill.write_xml(writer)?;
2014 }
2015 }
2016 writer
2017 .get_mut()
2018 .write_all(&text_close_generated_child(
2019 &child.into_inner(),
2020 &self.source_namespaces,
2021 )?)
2022 .map_err(OxmlError::from)?;
2023 }
2024 for boundary in 2..=6 {
2025 if boundary == 4 {
2026 let mut child = Writer::new(Vec::new());
2027 self.write_highlight(&mut child)?;
2028 writer
2029 .get_mut()
2030 .write_all(&text_close_generated_child(
2031 &child.into_inner(),
2032 &self.source_namespaces,
2033 )?)
2034 .map_err(OxmlError::from)?;
2035 }
2036 emit_raw(writer, self.raw_children.at(boundary))?;
2037 }
2038 if let Some(font) = &self.latin {
2039 let mut child = Writer::new(Vec::new());
2040 {
2041 let writer = &mut child;
2042 if let Some((raw, original)) = &self.source_latin {
2043 if font == original {
2044 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2045 } else {
2046 font.write_xml(writer, "a:latin")?;
2047 }
2048 } else {
2049 font.write_xml(writer, "a:latin")?;
2050 }
2051 }
2052 writer
2053 .get_mut()
2054 .write_all(&text_close_generated_child(
2055 &child.into_inner(),
2056 &self.source_namespaces,
2057 )?)
2058 .map_err(OxmlError::from)?;
2059 }
2060 emit_raw(writer, self.raw_children.at(7))?;
2061 if let Some(font) = &self.east_asian {
2062 let mut child = Writer::new(Vec::new());
2063 {
2064 let writer = &mut child;
2065 if let Some((raw, original)) = &self.source_east_asian {
2066 if font == original {
2067 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2068 } else {
2069 font.write_xml(writer, "a:ea")?;
2070 }
2071 } else {
2072 font.write_xml(writer, "a:ea")?;
2073 }
2074 }
2075 writer
2076 .get_mut()
2077 .write_all(&text_close_generated_child(
2078 &child.into_inner(),
2079 &self.source_namespaces,
2080 )?)
2081 .map_err(OxmlError::from)?;
2082 }
2083 emit_raw(writer, self.raw_children.at(8))?;
2084 if let Some(font) = &self.complex_script {
2085 let mut child = Writer::new(Vec::new());
2086 {
2087 let writer = &mut child;
2088 if let Some((raw, original)) = &self.source_complex_script {
2089 if font == original {
2090 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2091 } else {
2092 font.write_xml(writer, "a:cs")?;
2093 }
2094 } else {
2095 font.write_xml(writer, "a:cs")?;
2096 }
2097 }
2098 writer
2099 .get_mut()
2100 .write_all(&text_close_generated_child(
2101 &child.into_inner(),
2102 &self.source_namespaces,
2103 )?)
2104 .map_err(OxmlError::from)?;
2105 }
2106 emit_raw(writer, self.raw_children.at(9))?;
2107 if let Some(font) = &self.symbol {
2108 let mut child = Writer::new(Vec::new());
2109 {
2110 let writer = &mut child;
2111 if let Some((raw, original)) = &self.source_symbol {
2112 if font == original {
2113 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2114 } else {
2115 font.write_xml(writer, "a:sym")?;
2116 }
2117 } else {
2118 font.write_xml(writer, "a:sym")?;
2119 }
2120 }
2121 writer
2122 .get_mut()
2123 .write_all(&text_close_generated_child(
2124 &child.into_inner(),
2125 &self.source_namespaces,
2126 )?)
2127 .map_err(OxmlError::from)?;
2128 }
2129 emit_raw(writer, self.raw_children.at(10))?;
2130 if let Some(hyperlink) = &self.hyperlink_click {
2131 let mut child = Writer::new(Vec::new());
2132 {
2133 let writer = &mut child;
2134 hyperlink.write_xml(writer, "a:hlinkClick")?;
2135 }
2136 writer
2137 .get_mut()
2138 .write_all(&text_close_generated_child(
2139 &child.into_inner(),
2140 &self.source_namespaces,
2141 )?)
2142 .map_err(OxmlError::from)?;
2143 }
2144 emit_raw(writer, self.raw_children.at(11))?;
2145 if let Some(hyperlink) = &self.hyperlink_mouse_over {
2146 let mut child = Writer::new(Vec::new());
2147 {
2148 let writer = &mut child;
2149 hyperlink.write_xml(writer, "a:hlinkMouseOver")?;
2150 }
2151 writer
2152 .get_mut()
2153 .write_all(&text_close_generated_child(
2154 &child.into_inner(),
2155 &self.source_namespaces,
2156 )?)
2157 .map_err(OxmlError::from)?;
2158 }
2159 for boundary in 12..=14 {
2160 emit_raw(writer, self.raw_children.at(boundary))?;
2161 }
2162 write_end(writer, tag)
2163 }
2164
2165 pub fn set_highlight(&mut self, value: Option<ColorChoice>) -> Result<()> {
2167 let mut candidate = self.clone();
2168 candidate.highlight = value;
2169 candidate.write_highlight(&mut Writer::new(Vec::new()))?;
2170 *self = candidate;
2171 Ok(())
2172 }
2173
2174 fn write_highlight<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
2175 if let Some((raw, _, original)) = &self.highlight_source
2176 && self.highlight.as_ref() != Some(original)
2177 && highlight_has_opaque_transform_attributes(raw, &self.highlight_scope_attributes())?
2178 {
2179 return Err(invalid_attribute(
2180 "highlight",
2181 "transform",
2182 "opaque transform attributes cannot be replaced or cleared",
2183 ));
2184 }
2185 match (&self.highlight_source, &self.highlight) {
2186 (Some((raw, _, original)), Some(value)) if original == value => {
2187 writer.get_mut().write_all(raw).map_err(OxmlError::from)?;
2188 }
2189 (Some((raw, range, original)), Some(value)) => {
2190 if !original.raw_children().is_empty()
2191 && original.raw_children() != value.raw_children()
2192 {
2193 return Err(invalid_attribute(
2194 "highlight",
2195 "colour",
2196 "opaque colour children cannot be replaced",
2197 ));
2198 }
2199 let mut reader = Reader::from_reader(&raw[range.clone()]);
2200 let event = reader.read_event().map_err(OxmlError::from)?;
2201 let start = match event {
2202 Event::Start(start) | Event::Empty(start) => start,
2203 _ => return Err(missing_end("highlight colour")),
2204 };
2205 if start.attributes().with_checks(false).any(|attribute| {
2206 attribute.map_or(true, |attribute| {
2207 let name = attribute.key.as_ref();
2208 !matches!(name, b"val" | b"lastClr" | b"xmlns")
2209 && !name.starts_with(b"xmlns:")
2210 })
2211 }) {
2212 return Err(invalid_attribute(
2213 "highlight",
2214 "colour",
2215 "opaque colour attributes cannot be replaced",
2216 ));
2217 }
2218 writer
2219 .get_mut()
2220 .write_all(&raw[..range.start])
2221 .map_err(OxmlError::from)?;
2222 let namespaces = start
2223 .attributes()
2224 .with_checks(false)
2225 .filter_map(|attribute| attribute.ok())
2226 .filter(|attribute| {
2227 attribute.key.as_ref() == b"xmlns"
2228 || attribute.key.as_ref().starts_with(b"xmlns:")
2229 })
2230 .map(|attribute| {
2231 Ok((
2232 String::from_utf8_lossy(attribute.key.as_ref()).into_owned(),
2233 attribute
2234 .normalized_value(XmlVersion::Implicit1_0)
2235 .map_err(OxmlError::from)?
2236 .into_owned(),
2237 ))
2238 })
2239 .collect::<Result<Vec<_>>>()?;
2240 if namespaces
2241 .iter()
2242 .any(|(name, uri)| name == "xmlns:a" && uri != A_NS)
2243 {
2244 return Err(invalid_attribute(
2245 "highlight",
2246 "colour",
2247 "rebound canonical namespace cannot be replaced",
2248 ));
2249 }
2250 write_namespace_closed_highlight_colour(writer, value, &namespaces)?;
2251 writer
2252 .get_mut()
2253 .write_all(&raw[range.end..])
2254 .map_err(OxmlError::from)?;
2255 }
2256 (Some((raw, range, original)), None) => {
2257 if !original.raw_children().is_empty() {
2258 return Err(invalid_attribute(
2259 "highlight",
2260 "colour",
2261 "opaque colour children cannot be cleared",
2262 ));
2263 }
2264 let mut colour_reader = Reader::from_reader(&raw[range.clone()]);
2265 let colour_event = colour_reader.read_event().map_err(OxmlError::from)?;
2266 let colour_start = match colour_event {
2267 Event::Start(start) | Event::Empty(start) => start,
2268 _ => return Err(missing_end("highlight colour")),
2269 };
2270 if colour_start
2271 .attributes()
2272 .with_checks(false)
2273 .any(|attribute| {
2274 attribute.map_or(true, |attribute| {
2275 let name = attribute.key.as_ref();
2276 !matches!(name, b"val" | b"lastClr" | b"xmlns")
2277 && !name.starts_with(b"xmlns:")
2278 })
2279 })
2280 {
2281 return Err(invalid_attribute(
2282 "highlight",
2283 "colour",
2284 "opaque colour attributes cannot be cleared",
2285 ));
2286 }
2287 let mut reader = Reader::from_reader(raw.as_slice());
2289 let first = reader.read_event().map_err(OxmlError::from)?;
2290 let Event::Start(start) = first else {
2291 return Err(invalid_attribute("highlight", "source", "not a wrapper"));
2292 };
2293 if start.attributes().with_checks(false).any(|item| {
2294 item.map_or(true, |attribute| {
2295 let name = attribute.key.as_ref();
2296 name != b"xmlns" && !name.starts_with(b"xmlns:")
2297 })
2298 }) {
2299 return Err(invalid_attribute(
2300 "highlight",
2301 "source",
2302 "opaque wrapper attributes cannot be cleared",
2303 ));
2304 }
2305 let prefix_end = reader.buffer_position() as usize;
2306 let suffix_start = raw[range.end..]
2307 .iter()
2308 .position(|byte| *byte == b'<')
2309 .map(|offset| range.end + offset)
2310 .ok_or_else(|| missing_end("highlight"))?;
2311 if !raw[prefix_end..range.start]
2312 .iter()
2313 .all(u8::is_ascii_whitespace)
2314 || !raw[range.end..suffix_start]
2315 .iter()
2316 .all(u8::is_ascii_whitespace)
2317 || !raw[suffix_start..].starts_with(b"</")
2318 {
2319 return Err(invalid_attribute(
2320 "highlight",
2321 "source",
2322 "opaque wrapper children cannot be cleared",
2323 ));
2324 }
2325 }
2326 (None, Some(value)) => {
2327 let mut start = BytesStart::new("a:highlight");
2328 start.push_attribute(("xmlns:a", A_NS));
2329 write_start(writer, start)?;
2330 value.to_xml(writer)?;
2331 write_end(writer, "a:highlight")?;
2332 }
2333 (None, None) => {}
2334 }
2335 Ok(())
2336 }
2337
2338 pub fn set_all_caps(&mut self, value: Option<bool>) {
2340 self.all_caps = value;
2341 if value.is_some() {
2342 self.small_caps = None;
2343 }
2344 }
2345
2346 pub fn set_small_caps(&mut self, value: Option<bool>) {
2348 self.small_caps = value;
2349 if value.is_some() {
2350 self.all_caps = None;
2351 }
2352 }
2353
2354 pub fn raw_children(&self) -> &OrderedRawChildren {
2355 &self.raw_children
2356 }
2357
2358 pub(crate) fn without_hyperlinks(mut self) -> Self {
2361 self.hyperlink_click = None;
2362 self.hyperlink_mouse_over = None;
2363 self
2364 }
2365}
2366
2367fn write_namespace_closed_highlight_colour<W: Write>(
2368 writer: &mut Writer<W>,
2369 colour: &ColorChoice,
2370 namespaces: &[(String, String)],
2371) -> Result<()> {
2372 let mut canonical = Writer::new(Vec::new());
2373 colour.to_xml(&mut canonical)?;
2374 let bytes = canonical.into_inner();
2375 let mut reader = Reader::from_reader(bytes.as_slice());
2376 match reader.read_event().map_err(OxmlError::from)? {
2377 Event::Start(start) => {
2378 let mut start = start.into_owned();
2379 start.push_attribute(("xmlns:a", A_NS));
2380 for (name, value) in namespaces {
2381 if name != "xmlns:a" {
2382 start.push_attribute((name.as_str(), value.as_str()));
2383 }
2384 }
2385 write_start(writer, start)?;
2386 }
2387 Event::Empty(start) => {
2388 let mut start = start.into_owned();
2389 start.push_attribute(("xmlns:a", A_NS));
2390 for (name, value) in namespaces {
2391 if name != "xmlns:a" {
2392 start.push_attribute((name.as_str(), value.as_str()));
2393 }
2394 }
2395 write_empty(writer, start)?;
2396 }
2397 _ => return Err(invalid_attribute("highlight", "colour", "missing colour")),
2398 }
2399 writer
2400 .get_mut()
2401 .write_all(&bytes[reader.buffer_position() as usize..])
2402 .map_err(OxmlError::from)?;
2403 Ok(())
2404}
2405
2406fn highlight_has_opaque_transform_attributes(
2410 raw: &[u8],
2411 attributes: &[(String, String)],
2412) -> Result<bool> {
2413 let mut scope = BytesStart::new("scope");
2414 if !attributes.iter().any(|(name, _)| name == "xmlns:a") {
2415 scope.push_attribute(("xmlns:a", A_NS));
2416 }
2417 for (name, value) in attributes {
2418 if name == "xmlns" || name.starts_with("xmlns:") {
2419 scope.push_attribute((name.as_str(), value.as_str()));
2420 }
2421 }
2422 let mut writer = Writer::new(Vec::new());
2423 write_start(&mut writer, scope)?;
2424 writer.get_mut().extend_from_slice(raw);
2425 write_end(&mut writer, "scope")?;
2426 let xml = writer.into_inner();
2427 let mut reader = NsReader::from_reader(xml.as_slice());
2428 let mut depth = 0usize;
2429 let mut buffer = Vec::new();
2430 let mut started_transform = None;
2431 loop {
2432 let before = reader.buffer_position() as usize;
2433 let (namespace, event) = reader
2434 .read_resolved_event_into(&mut buffer)
2435 .map_err(OxmlError::from)?;
2436 match event {
2437 Event::Start(start) => {
2438 if depth == 3
2439 && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes())
2440 && ColorTransform::from_xml(&start)?.is_some()
2441 && start.attributes().with_checks(false).any(|attribute| {
2442 attribute.map_or(true, |attribute| {
2443 let name = attribute.key.as_ref();
2444 name != b"val" && name != b"xmlns" && !name.starts_with(b"xmlns:")
2445 })
2446 })
2447 {
2448 started_transform = Some(reader.buffer_position() as usize);
2449 }
2450 depth += 1;
2451 }
2452 Event::Empty(start) => {
2453 if depth == 3
2454 && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes())
2455 && ColorTransform::from_xml(&start)?.is_some()
2456 && start.attributes().with_checks(false).any(|attribute| {
2457 attribute.map_or(true, |attribute| {
2458 let name = attribute.key.as_ref();
2459 name != b"val" && name != b"xmlns" && !name.starts_with(b"xmlns:")
2460 })
2461 })
2462 {
2463 return Ok(true);
2464 }
2465 }
2466 Event::End(_) => {
2467 if depth == 4
2468 && let Some(content_start) = started_transform.take()
2469 && xml[content_start..before]
2470 .iter()
2471 .all(u8::is_ascii_whitespace)
2472 {
2473 return Ok(true);
2474 }
2475 depth = depth
2476 .checked_sub(1)
2477 .ok_or_else(|| missing_end("highlight"))?;
2478 }
2479 Event::Eof => return Ok(false),
2480 _ => {}
2481 }
2482 buffer.clear();
2483 }
2484}
2485
2486fn character_highlight_colour(
2487 raw: &[u8],
2488 attributes: &[(String, String)],
2489) -> Result<(ColorChoice, std::ops::Range<usize>)> {
2490 let mut scope = BytesStart::new("scope");
2491 if !attributes.iter().any(|(name, _)| name == "xmlns:a") {
2492 scope.push_attribute(("xmlns:a", A_NS));
2493 }
2494 for (name, value) in attributes {
2495 if name == "xmlns" || name.starts_with("xmlns:") {
2496 scope.push_attribute((name.as_str(), value.as_str()));
2497 }
2498 }
2499 let mut writer = Writer::new(Vec::new());
2500 write_start(&mut writer, scope)?;
2501 let prefix = writer.get_ref().len();
2502 writer.get_mut().extend_from_slice(raw);
2503 write_end(&mut writer, "scope")?;
2504 let xml = writer.into_inner();
2505 let mut reader = NsReader::from_reader(xml.as_slice());
2506 let mut depth = 0usize;
2507 let mut selected = None;
2508 let mut colour_start = None;
2509 let mut buffer = Vec::new();
2510 loop {
2511 let before = reader.buffer_position() as usize;
2512 let (namespace, event) = reader
2513 .read_resolved_event_into(&mut buffer)
2514 .map_err(OxmlError::from)?;
2515 match event {
2516 Event::Start(start) => {
2517 if depth == 2
2518 && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes())
2519 && matches!(
2520 local_name(start.name().as_ref()),
2521 b"scrgbClr"
2522 | b"srgbClr"
2523 | b"hslClr"
2524 | b"sysClr"
2525 | b"schemeClr"
2526 | b"prstClr"
2527 )
2528 {
2529 if selected.is_some() || colour_start.is_some() {
2530 return Err(duplicate("highlight colour"));
2531 }
2532 colour_start = Some(before);
2533 }
2534 depth += 1;
2535 }
2536 Event::Empty(start)
2537 if depth == 2
2538 && matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes())
2539 && matches!(
2540 local_name(start.name().as_ref()),
2541 b"scrgbClr"
2542 | b"srgbClr"
2543 | b"hslClr"
2544 | b"sysClr"
2545 | b"schemeClr"
2546 | b"prstClr"
2547 ) =>
2548 {
2549 if selected.is_some() {
2550 return Err(duplicate("highlight colour"));
2551 }
2552 selected = Some(before..reader.buffer_position() as usize);
2553 }
2554 Event::End(_) => {
2555 if depth == 3
2556 && let Some(start) = colour_start.take()
2557 {
2558 selected = Some(start..reader.buffer_position() as usize);
2559 }
2560 depth = depth
2561 .checked_sub(1)
2562 .ok_or_else(|| missing_end("highlight"))?;
2563 if depth == 0 {
2564 break;
2565 }
2566 }
2567 Event::Eof => return Err(missing_end("highlight")),
2568 _ => {}
2569 }
2570 buffer.clear();
2571 }
2572 let range = selected
2573 .ok_or_else(|| invalid_attribute("highlight", "colour", "missing qualified colour"))?;
2574 let colour = qualified_highlight_colour(&xml, range.clone())?;
2575 Ok((colour, range.start - prefix..range.end - prefix))
2576}
2577
2578fn qualified_highlight_colour(xml: &[u8], range: std::ops::Range<usize>) -> Result<ColorChoice> {
2579 let mut reader = NsReader::from_reader(xml);
2580 let mut buffer = Vec::new();
2581 let mut selected_start = None;
2582 let mut root_depth = None;
2583 let mut depth = 0usize;
2584 let mut transforms = Vec::new();
2585 let mut raw_children = OrderedRawChildren::default();
2586 let mut child_start = None;
2587 loop {
2588 let before = reader.buffer_position() as usize;
2589 let (namespace, event) = reader
2590 .read_resolved_event_into(&mut buffer)
2591 .map_err(OxmlError::from)?;
2592 match event {
2593 Event::Start(start) => {
2594 if before == range.start {
2595 selected_start = Some(start.into_owned());
2596 root_depth = Some(depth);
2597 } else if root_depth.is_some_and(|root| depth == root + 1) {
2598 let transform = if matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes())
2599 {
2600 ColorTransform::from_xml(&start)?
2601 } else {
2602 None
2603 };
2604 child_start = Some((before, reader.buffer_position() as usize, transform));
2605 }
2606 depth += 1;
2607 }
2608 Event::Empty(start) if before == range.start => {
2609 return Ok(ColorChoice::from_parts(&start, transforms, raw_children)?);
2610 }
2611 Event::Empty(start) if root_depth.is_some_and(|root| depth == root + 1) => {
2612 let qualified = matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes());
2613 let transform = if qualified {
2614 ColorTransform::from_xml(&start)?
2615 } else {
2616 None
2617 };
2618 if let Some(transform) = transform {
2619 transforms.push(transform);
2620 } else {
2621 raw_children.push(
2622 transforms.len(),
2623 xml[before..reader.buffer_position() as usize].to_vec(),
2624 );
2625 }
2626 }
2627 Event::End(_) => {
2628 depth = depth
2629 .checked_sub(1)
2630 .ok_or_else(|| missing_end("highlight colour"))?;
2631 if root_depth.is_some_and(|root| depth == root + 1) {
2632 let (start, content_start, transform) = child_start
2633 .take()
2634 .ok_or_else(|| missing_end("highlight colour child"))?;
2635 if let Some(transform) = transform.filter(|_| {
2636 xml[content_start..before]
2637 .iter()
2638 .all(u8::is_ascii_whitespace)
2639 }) {
2640 transforms.push(transform);
2641 } else {
2642 raw_children.push(
2643 transforms.len(),
2644 xml[start..reader.buffer_position() as usize].to_vec(),
2645 );
2646 }
2647 }
2648 if root_depth == Some(depth) {
2649 return Ok(ColorChoice::from_parts(
2650 &selected_start.ok_or_else(|| missing_end("highlight colour"))?,
2651 transforms,
2652 raw_children,
2653 )?);
2654 }
2655 }
2656 Event::Eof => return Err(missing_end("highlight colour")),
2657 _ if root_depth.is_some_and(|root| depth == root + 1) => {
2658 raw_children.push(
2659 transforms.len(),
2660 xml[before..reader.buffer_position() as usize].to_vec(),
2661 );
2662 }
2663 _ => {}
2664 }
2665 buffer.clear();
2666 }
2667}
2668
2669fn character_highlight_is_drawingml(raw: &[u8], attributes: &[(String, String)]) -> Result<bool> {
2670 let mut scope = BytesStart::new("scope");
2671 if !attributes.iter().any(|(name, _)| name == "xmlns:a") {
2672 scope.push_attribute(("xmlns:a", A_NS));
2673 }
2674 for (name, value) in attributes {
2675 if name == "xmlns" || name.starts_with("xmlns:") {
2676 scope.push_attribute((name.as_str(), value.as_str()));
2677 }
2678 }
2679 let mut writer = Writer::new(Vec::new());
2680 write_start(&mut writer, scope)?;
2681 writer.get_mut().extend_from_slice(raw);
2682 write_end(&mut writer, "scope")?;
2683 let xml = writer.into_inner();
2684 let mut reader = NsReader::from_reader(xml.as_slice());
2685 let mut buffer = Vec::new();
2686 let mut in_scope = false;
2687 loop {
2688 match reader
2689 .read_resolved_event_into(&mut buffer)
2690 .map_err(OxmlError::from)?
2691 {
2692 (_, Event::Start(_)) if !in_scope => in_scope = true,
2693 (namespace, Event::Start(_) | Event::Empty(_)) => {
2694 return Ok(
2695 matches!(namespace, ResolveResult::Bound(Namespace(uri)) if uri == A_NS.as_bytes()),
2696 );
2697 }
2698 (_, Event::Eof) => return Err(missing_end("highlight")),
2699 _ => {}
2700 }
2701 buffer.clear();
2702 }
2703}
2704
2705fn character_property_slot(name: &[u8]) -> Option<usize> {
2706 match name {
2707 b"ln" => Some(1),
2708 name if is_fill(name) => Some(2),
2709 b"effectLst" | b"effectDag" => Some(3),
2710 b"highlight" => Some(4),
2711 b"uLnTx" | b"uLn" => Some(5),
2712 b"uFillTx" | b"uFill" => Some(6),
2713 b"latin" => Some(7),
2714 b"ea" => Some(8),
2715 b"cs" => Some(9),
2716 b"sym" => Some(10),
2717 b"hlinkClick" => Some(11),
2718 b"hlinkMouseOver" => Some(12),
2719 b"rtl" => Some(13),
2720 b"extLst" => Some(14),
2721 _ => None,
2722 }
2723}
2724
2725fn is_character_property_tag(name: &[u8]) -> bool {
2726 matches!(name, b"rPr" | b"defRPr" | b"endParaRPr")
2727}
2728
2729fn is_fill(name: &[u8]) -> bool {
2730 matches!(
2731 name,
2732 b"noFill" | b"solidFill" | b"gradFill" | b"pattFill" | b"blipFill"
2733 )
2734}
2735
2736#[allow(non_camel_case_types)]
2738#[derive(Clone, Debug, Default)]
2739pub struct CT_TextParagraphProperties {
2740 pub left_margin: Option<i32>,
2741 pub right_margin: Option<i32>,
2742 pub level: Option<u8>,
2743 pub indent: Option<i32>,
2744 pub alignment: Option<TextAlignment>,
2745 pub right_to_left: Option<bool>,
2746 pub line_spacing: Option<TextSpacing>,
2747 pub space_before: Option<TextSpacing>,
2748 pub space_after: Option<TextSpacing>,
2749 pub bullet: Option<TextBullet>,
2750 pub default_run_properties: Option<CT_TextCharacterProperties>,
2751 raw_attributes: Vec<(String, String)>,
2752 source_attributes: Vec<(String, String)>,
2753 source_tag: String,
2754 source_xml: Option<Vec<u8>>,
2755 source_projection: Vec<u8>,
2756 source_namespaces: Vec<(String, String)>,
2757 raw_children: OrderedRawChildren,
2758}
2759impl PartialEq for CT_TextParagraphProperties {
2760 fn eq(&self, other: &Self) -> bool {
2761 self.left_margin == other.left_margin
2762 && self.right_margin == other.right_margin
2763 && self.level == other.level
2764 && self.indent == other.indent
2765 && self.alignment == other.alignment
2766 && self.right_to_left == other.right_to_left
2767 && self.line_spacing == other.line_spacing
2768 && self.space_before == other.space_before
2769 && self.space_after == other.space_after
2770 && self.bullet == other.bullet
2771 && self.default_run_properties == other.default_run_properties
2772 && {
2773 let mut left = self.raw_attributes.clone();
2774 let mut right = other.raw_attributes.clone();
2775 let inherited_binding =
2776 |key: &str, value: &str, namespaces: &[(String, String)]| {
2777 let prefix = if key == "xmlns" {
2778 Some("")
2779 } else {
2780 key.strip_prefix("xmlns:")
2781 };
2782 prefix.is_some_and(|prefix| {
2783 namespaces
2784 .iter()
2785 .any(|(name, uri)| name == prefix && uri == value)
2786 })
2787 };
2788 left.retain(|(key, value)| {
2789 !(key == "xmlns:r" && value == TEXT_REL_NS)
2790 && !inherited_binding(key, value, &other.source_namespaces)
2791 });
2792 right.retain(|(key, value)| {
2793 !(key == "xmlns:r" && value == TEXT_REL_NS)
2794 && !inherited_binding(key, value, &self.source_namespaces)
2795 });
2796 left.sort();
2797 right.sort();
2798 left == right
2799 }
2800 && matches!(
2801 (
2802 text_owner_closure(self.source_xml.as_deref().unwrap_or_default(), &self.source_namespaces),
2803 text_owner_closure(other.source_xml.as_deref().unwrap_or_default(), &other.source_namespaces),
2804 ),
2805 (Ok(left), Ok(right)) if left == right
2806 )
2807 && self.raw_children == other.raw_children
2808 }
2809}
2810impl Eq for CT_TextParagraphProperties {}
2811
2812impl CT_TextParagraphProperties {
2813 pub fn with_inherited_defaults(&self, defaults: &Self) -> Result<Self> {
2815 let xml = text_inherit_property_xml(
2816 &self.to_xml()?,
2817 &self.source_namespaces,
2818 &defaults.to_xml()?,
2819 &defaults.source_namespaces,
2820 )?;
2821 Self::from_xml_with_namespaces(
2822 &xml,
2823 &TextNamespaceScope::inherited(&self.source_namespaces),
2824 )
2825 }
2826
2827 pub fn from_xml(xml: &[u8]) -> Result<Self> {
2829 let expected = root_local_name(xml)?;
2830 if !is_paragraph_property_tag(&expected) {
2831 return Err(TextError::UnexpectedElement(
2832 String::from_utf8_lossy(&expected).into_owned(),
2833 ));
2834 }
2835 Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
2836 }
2837
2838 pub(crate) fn from_xml_with_namespaces(
2839 xml: &[u8],
2840 inherited: &TextNamespaceScope,
2841 ) -> Result<Self> {
2842 let expected = root_local_name(xml)?;
2843 let mut parsed = parse_complete(
2844 xml,
2845 &expected,
2846 |reader, start| Self::from_element_model(reader, start, inherited),
2847 |start| Self::from_start_with_namespaces(start, inherited),
2848 )?;
2849 let mut writer = Writer::new(Vec::new());
2850 parsed.write_xml(&mut writer, "a:pPr")?;
2851 parsed.source_projection = writer.into_inner();
2852 parsed.source_xml = Some(xml.to_vec());
2853 Ok(parsed)
2854 }
2855
2856 fn from_start_with_namespaces(
2857 start: &BytesStart<'_>,
2858 inherited: &TextNamespaceScope,
2859 ) -> Result<Self> {
2860 let mut parsed = Self::from_start(start)?;
2861 parsed.source_namespaces = text_fragment_scope(start, inherited)?.physical;
2862 Ok(parsed)
2863 }
2864
2865 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
2866 text_validate_root(start)?;
2867 let element = String::from_utf8_lossy(local_name(start.name().as_ref())).into_owned();
2868 let level = text_attr(start, b"lvl")?
2869 .map(|value| parse_i32_value(&element, "lvl", &value, 0, 8).map(|v| v as u8))
2870 .transpose()?;
2871 Ok(Self {
2872 left_margin: parse_optional_i32(start, b"marL", 0, MAX_TEXT_MARGIN)?,
2873 right_margin: parse_optional_i32(start, b"marR", 0, MAX_TEXT_MARGIN)?,
2874 level,
2875 indent: parse_optional_i32(start, b"indent", -MAX_TEXT_MARGIN, MAX_TEXT_MARGIN)?,
2876 alignment: parse_optional_enum(start, b"algn", TextAlignment::parse)?,
2877 right_to_left: parse_optional_bool(start, b"rtl")?,
2878 source_attributes: text_attribute_tokens(start)?,
2879 source_tag: std::str::from_utf8(start.name().as_ref())
2880 .map_err(OxmlError::from)?
2881 .to_owned(),
2882 raw_attributes: capture_raw_attributes(
2883 start,
2884 &[b"marL", b"marR", b"lvl", b"indent", b"algn"],
2885 )?,
2886 ..Self::default()
2887 })
2888 }
2889
2890 fn from_element_with_namespaces(
2891 reader: &mut Reader<&[u8]>,
2892 start: &BytesStart<'_>,
2893 inherited: &TextNamespaceScope,
2894 ) -> Result<Self> {
2895 let raw = capture_element(reader, start)?;
2896 Self::from_xml_with_namespaces(&raw, inherited)
2897 }
2898
2899 fn from_element_model(
2900 reader: &mut Reader<&[u8]>,
2901 start: &BytesStart<'_>,
2902 inherited: &TextNamespaceScope,
2903 ) -> Result<Self> {
2904 let namespaces = text_fragment_scope(start, inherited)?;
2905 let expected = local_name(start.name().as_ref()).to_vec();
2906 let mut properties = Self::from_start(start)?;
2907 properties.source_namespaces = namespaces.physical.clone();
2908 let mut boundary = 0;
2909 let mut seen = [false; 10];
2910 let mut buffer = Vec::new();
2911 loop {
2912 match reader
2913 .read_event_into(&mut buffer)
2914 .map_err(OxmlError::from)?
2915 {
2916 Event::Start(element) => {
2917 let name = text_owned_name(&element, &namespaces)?;
2918 let raw = capture_element(reader, &element)?;
2919 properties.capture_child(&name, raw, &mut boundary, &mut seen, &namespaces)?;
2920 }
2921 Event::Empty(element) => {
2922 let name = text_owned_name(&element, &namespaces)?;
2923 let raw = capture_empty_element(&element)?;
2924 properties.capture_child(&name, raw, &mut boundary, &mut seen, &namespaces)?;
2925 }
2926 Event::End(element) if matches_local_name(element.name().as_ref(), &expected) => {
2927 return Ok(properties);
2928 }
2929 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(&expected))),
2930 event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
2931 properties
2932 .raw_children
2933 .push(boundary, text_capture_opaque_event(event)?);
2934 }
2935 _ => {}
2936 }
2937 buffer.clear();
2938 }
2939 }
2940
2941 fn capture_child(
2942 &mut self,
2943 name: &[u8],
2944 raw: Vec<u8>,
2945 boundary: &mut usize,
2946 seen: &mut [bool; 10],
2947 namespaces: &TextNamespaceScope,
2948 ) -> Result<()> {
2949 if namespaces.fixed_list_style_children && matches!(name, b"lnSpc" | b"spcBef" | b"spcAft")
2950 {
2951 let mut spacing = Reader::from_reader(raw.as_slice());
2952 loop {
2953 match spacing.read_event().map_err(OxmlError::from)? {
2954 Event::Start(start) | Event::Empty(start) => {
2955 crate::namespace::reject_conflicting_a_prefix(&start)?;
2956 }
2957 Event::Eof => break,
2958 _ => {}
2959 }
2960 }
2961 }
2962 let slot = paragraph_property_slot(name);
2963 if let Some(slot) = slot {
2964 if seen[slot - 1] {
2965 return Err(duplicate(&String::from_utf8_lossy(name)));
2966 }
2967 seen[slot - 1] = true;
2968 }
2969 match name {
2970 b"lnSpc" => self.line_spacing = Some(TextSpacing::from_xml(&raw, b"lnSpc")?),
2971 b"spcBef" => self.space_before = Some(TextSpacing::from_xml(&raw, b"spcBef")?),
2972 b"spcAft" => self.space_after = Some(TextSpacing::from_xml(&raw, b"spcAft")?),
2973 b"defRPr" => {
2974 self.default_run_properties = Some(
2975 CT_TextCharacterProperties::from_xml_with_namespaces(&raw, namespaces)?,
2976 )
2977 }
2978 _ => {
2979 let mut bullet = self.bullet.take().unwrap_or_default();
2980 if bullet.capture_component(name, &raw)? {
2981 self.bullet = Some(bullet);
2982 } else {
2983 if !bullet.is_empty() {
2984 self.bullet = Some(bullet);
2985 }
2986 self.raw_children.push(
2987 slot.map_or(*boundary, |slot| (*boundary).max(slot - 1)),
2988 raw,
2989 );
2990 }
2991 }
2992 }
2993 if let Some(slot) = slot {
2994 *boundary = (*boundary).max(slot);
2995 }
2996 Ok(())
2997 }
2998
2999 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
3001 let owned_tag = text_source_tag(tag, &self.source_tag, &self.source_namespaces);
3002 let tag = owned_tag.as_str();
3003 if text_has_physical_owner(&self.source_tag, &self.source_namespaces)
3004 && let Some(raw) = &self.source_xml
3005 {
3006 let mut candidate = self.clone();
3007 candidate.source_xml = None;
3008 let mut projection = Writer::new(Vec::new());
3009 candidate.write_xml(&mut projection, "a:pPr")?;
3010 if projection.into_inner() == self.source_projection {
3011 writer
3012 .get_mut()
3013 .write_all(&text_close_source_owner(raw, &self.source_namespaces, tag)?)
3014 .map_err(OxmlError::from)?;
3015 return Ok(());
3016 }
3017 }
3018 let mut start = BytesStart::new(tag);
3019 if let Some(value) = self.left_margin {
3020 parse_i32_value(tag, "marL", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
3021 push_owned_attr(&mut start, "marL", value.to_string());
3022 }
3023 if let Some(value) = self.right_margin {
3024 parse_i32_value(tag, "marR", &value.to_string(), 0, MAX_TEXT_MARGIN)?;
3025 push_owned_attr(&mut start, "marR", value.to_string());
3026 }
3027 if let Some(value) = self.level {
3028 if value > 8 {
3029 return Err(invalid_attribute(tag, "lvl", &value.to_string()));
3030 }
3031 push_owned_attr(&mut start, "lvl", value.to_string());
3032 }
3033 if let Some(value) = self.indent {
3034 parse_i32_value(
3035 tag,
3036 "indent",
3037 &value.to_string(),
3038 -MAX_TEXT_MARGIN,
3039 MAX_TEXT_MARGIN,
3040 )?;
3041 push_owned_attr(&mut start, "indent", value.to_string());
3042 }
3043 if let Some(value) = self.alignment {
3044 start.push_attribute(("algn", value.as_str()));
3045 }
3046 push_optional_bool(&mut start, "rtl", self.right_to_left);
3047 if self.source_attributes.is_empty() {
3048 for (name, value) in &self.raw_attributes {
3049 if name != "rtl" {
3050 start.push_attribute((name.as_str(), value.as_str()));
3051 }
3052 }
3053 } else {
3054 let modeled = ["marL", "marR", "lvl", "indent", "algn", "rtl"];
3055 let preserved: Vec<String> = self
3056 .source_attributes
3057 .iter()
3058 .filter(|(key, _)| !modeled.contains(&key.as_str()))
3059 .map(|(_, token)| token.clone())
3060 .collect();
3061 let generated = std::str::from_utf8(start.as_ref())
3062 .map_err(OxmlError::from)?
3063 .to_owned();
3064 start =
3065 BytesStart::from_content(format!("{generated}{}", preserved.concat()), tag.len());
3066 }
3067
3068 let existing_keys: Vec<String> = start
3069 .attributes()
3070 .map(|attribute| {
3071 attribute.map_err(OxmlError::from).and_then(|attribute| {
3072 std::str::from_utf8(attribute.key.as_ref())
3073 .map(str::to_owned)
3074 .map_err(OxmlError::from)
3075 })
3076 })
3077 .collect::<std::result::Result<_, OxmlError>>()?;
3078 let source = self
3079 .source_xml
3080 .clone()
3081 .unwrap_or(capture_empty_element(&start)?);
3082 for (prefix, uri) in text_owner_closure(&source, &self.source_namespaces)? {
3083 let key = if prefix.is_empty() {
3084 "xmlns".to_owned()
3085 } else {
3086 format!("xmlns:{prefix}")
3087 };
3088 if !existing_keys.contains(&key) {
3089 start.push_attribute((key.as_str(), uri.as_str()));
3090 }
3091 }
3092
3093 let modelled = self.line_spacing.is_some()
3094 || self.space_before.is_some()
3095 || self.space_after.is_some()
3096 || self.bullet.is_some()
3097 || self.default_run_properties.is_some();
3098 if !modelled && self.raw_children.is_empty() {
3099 return write_empty(writer, start);
3100 }
3101 write_start(writer, start)?;
3102 emit_raw(writer, self.raw_children.at(0))?;
3103 if let Some(spacing) = &self.line_spacing {
3104 {
3105 let mut child = Writer::new(Vec::new());
3106 spacing.write_xml(&mut child, "a:lnSpc")?;
3107 writer
3108 .get_mut()
3109 .write_all(&text_close_generated_child(
3110 &child.into_inner(),
3111 &self.source_namespaces,
3112 )?)
3113 .map_err(OxmlError::from)?;
3114 }
3115 }
3116 emit_raw(writer, self.raw_children.at(1))?;
3117 if let Some(spacing) = &self.space_before {
3118 {
3119 let mut child = Writer::new(Vec::new());
3120 spacing.write_xml(&mut child, "a:spcBef")?;
3121 writer
3122 .get_mut()
3123 .write_all(&text_close_generated_child(
3124 &child.into_inner(),
3125 &self.source_namespaces,
3126 )?)
3127 .map_err(OxmlError::from)?;
3128 }
3129 }
3130 emit_raw(writer, self.raw_children.at(2))?;
3131 if let Some(spacing) = &self.space_after {
3132 {
3133 let mut child = Writer::new(Vec::new());
3134 spacing.write_xml(&mut child, "a:spcAft")?;
3135 writer
3136 .get_mut()
3137 .write_all(&text_close_generated_child(
3138 &child.into_inner(),
3139 &self.source_namespaces,
3140 )?)
3141 .map_err(OxmlError::from)?;
3142 }
3143 }
3144 emit_raw(writer, self.raw_children.at(3))?;
3145 if let Some(bullet) = &self.bullet {
3146 {
3147 let mut child = Writer::new(Vec::new());
3148 bullet.write_color(&mut child)?;
3149 writer
3150 .get_mut()
3151 .write_all(&text_close_generated_child(
3152 &child.into_inner(),
3153 &self.source_namespaces,
3154 )?)
3155 .map_err(OxmlError::from)?;
3156 }
3157 }
3158 emit_raw(writer, self.raw_children.at(4))?;
3159 if let Some(bullet) = &self.bullet {
3160 {
3161 let mut child = Writer::new(Vec::new());
3162 bullet.write_size(&mut child)?;
3163 writer
3164 .get_mut()
3165 .write_all(&text_close_generated_child(
3166 &child.into_inner(),
3167 &self.source_namespaces,
3168 )?)
3169 .map_err(OxmlError::from)?;
3170 }
3171 }
3172 emit_raw(writer, self.raw_children.at(5))?;
3173 if let Some(bullet) = &self.bullet {
3174 {
3175 let mut child = Writer::new(Vec::new());
3176 bullet.write_font(&mut child)?;
3177 writer
3178 .get_mut()
3179 .write_all(&text_close_generated_child(
3180 &child.into_inner(),
3181 &self.source_namespaces,
3182 )?)
3183 .map_err(OxmlError::from)?;
3184 }
3185 }
3186 emit_raw(writer, self.raw_children.at(6))?;
3187 if let Some(bullet) = &self.bullet {
3188 {
3189 let mut child = Writer::new(Vec::new());
3190 bullet.write_choice(&mut child)?;
3191 writer
3192 .get_mut()
3193 .write_all(&text_close_generated_child(
3194 &child.into_inner(),
3195 &self.source_namespaces,
3196 )?)
3197 .map_err(OxmlError::from)?;
3198 }
3199 }
3200 emit_raw(writer, self.raw_children.at(7))?;
3201 emit_raw(writer, self.raw_children.at(8))?;
3202 if let Some(properties) = &self.default_run_properties {
3203 {
3204 let mut child = Writer::new(Vec::new());
3205 properties.write_xml(&mut child, "a:defRPr")?;
3206 writer
3207 .get_mut()
3208 .write_all(&text_close_generated_child(
3209 &child.into_inner(),
3210 &self.source_namespaces,
3211 )?)
3212 .map_err(OxmlError::from)?;
3213 }
3214 }
3215 emit_raw(writer, self.raw_children.at(9))?;
3216 emit_raw(writer, self.raw_children.at(10))?;
3217 write_end(writer, tag)
3218 }
3219
3220 pub fn raw_children(&self) -> &OrderedRawChildren {
3221 &self.raw_children
3222 }
3223
3224 pub fn has_picture_bullet(&self) -> bool {
3226 (0..=10).any(|boundary| {
3227 self.raw_children
3228 .at(boundary)
3229 .any(|raw| raw_local_name(raw) == b"buBlip")
3230 })
3231 }
3232
3233 pub fn set_bullet(&mut self, bullet: Option<TextBullet>) {
3239 let replaces = |name: &[u8]| {
3240 let Some(bullet) = bullet.as_ref() else {
3241 return matches!(name, b"buClrTx" | b"buSzTx" | b"buFontTx" | b"buBlip");
3242 };
3243 match name {
3244 b"buClrTx" => bullet.color.is_some(),
3245 b"buSzTx" => bullet.size.is_some(),
3246 b"buFontTx" => bullet.font.is_some(),
3247 b"buBlip" => bullet.choice.is_some(),
3248 _ => false,
3249 }
3250 };
3251 self.raw_children
3252 .retain(|raw| !replaces(raw_local_name(raw)));
3253 self.bullet = bullet;
3254 }
3255
3256 pub fn to_xml(&self) -> Result<Vec<u8>> {
3258 let mut writer = Writer::new(Vec::new());
3259 self.write_xml(&mut writer, "a:pPr")?;
3260 Ok(writer.into_inner())
3261 }
3262}
3263
3264fn paragraph_property_slot(name: &[u8]) -> Option<usize> {
3265 match name {
3266 b"lnSpc" => Some(1),
3267 b"spcBef" => Some(2),
3268 b"spcAft" => Some(3),
3269 b"buClrTx" | b"buClr" => Some(4),
3270 b"buSzTx" | b"buSzPct" | b"buSzPts" => Some(5),
3271 b"buFontTx" | b"buFont" => Some(6),
3272 b"buNone" | b"buAutoNum" | b"buChar" | b"buBlip" => Some(7),
3273 b"tabLst" => Some(8),
3274 b"defRPr" => Some(9),
3275 b"extLst" => Some(10),
3276 _ => None,
3277 }
3278}
3279
3280fn raw_local_name(raw: &[u8]) -> &[u8] {
3282 let tag = raw.strip_prefix(b"<").unwrap_or(raw);
3283 let end = tag
3284 .iter()
3285 .position(|byte| byte.is_ascii_whitespace() || matches!(byte, b'/' | b'>'))
3286 .unwrap_or(tag.len());
3287 local_name(&tag[..end])
3288}
3289
3290fn is_paragraph_property_tag(name: &[u8]) -> bool {
3291 matches!(
3292 name,
3293 b"pPr"
3294 | b"defPPr"
3295 | b"lvl1pPr"
3296 | b"lvl2pPr"
3297 | b"lvl3pPr"
3298 | b"lvl4pPr"
3299 | b"lvl5pPr"
3300 | b"lvl6pPr"
3301 | b"lvl7pPr"
3302 | b"lvl8pPr"
3303 | b"lvl9pPr"
3304 )
3305}
3306
3307#[allow(non_camel_case_types)]
3309#[derive(Clone, Debug)]
3310pub struct CT_RegularTextRun {
3311 pub properties: Option<CT_TextCharacterProperties>,
3312 pub text: TextValue,
3313 raw_children: OrderedRawChildren,
3314 source_root: Vec<String>,
3315 source_tag: String,
3316}
3317impl PartialEq for CT_RegularTextRun {
3318 fn eq(&self, other: &Self) -> bool {
3319 self.properties == other.properties
3320 && self.text == other.text
3321 && self.raw_children == other.raw_children
3322 }
3323}
3324impl Eq for CT_RegularTextRun {}
3325
3326impl CT_RegularTextRun {
3327 pub fn new(text: impl Into<String>) -> Self {
3329 Self {
3330 properties: None,
3331 text: TextValue {
3332 value: escape_invalid_xml_characters(&text.into()).into_owned(),
3333 ..TextValue::default()
3334 },
3335 raw_children: OrderedRawChildren::default(),
3336 source_root: Vec::new(),
3337 source_tag: String::new(),
3338 }
3339 }
3340
3341 pub fn set_text(&mut self, text: &str) {
3346 self.text.value = escape_invalid_xml_characters(text).into_owned();
3347 }
3348
3349 pub fn to_xml(&self) -> Result<Vec<u8>> {
3351 let mut writer = Writer::new(Vec::new());
3352 self.write_xml(&mut writer)?;
3353 text_close_generated_child(&writer.into_inner(), &[])
3354 }
3355
3356 pub fn from_xml(xml: &[u8]) -> Result<Self> {
3358 Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
3359 }
3360
3361 pub(crate) fn from_xml_with_namespaces(
3362 xml: &[u8],
3363 inherited: &TextNamespaceScope,
3364 ) -> Result<Self> {
3365 parse_complete(
3366 xml,
3367 b"r",
3368 |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
3369 |_| Err(TextError::UnexpectedElement("r".to_owned())),
3370 )
3371 }
3372
3373 fn from_element_with_namespaces(
3374 reader: &mut Reader<&[u8]>,
3375 start: &BytesStart<'_>,
3376 inherited: &TextNamespaceScope,
3377 ) -> Result<Self> {
3378 let source_root = text_root_spellings_scoped(
3379 start,
3380 inherited,
3381 &capture_element(&mut reader.clone(), start)?,
3382 )?;
3383 let namespaces = text_fragment_scope(start, inherited)?;
3384 let mut properties = None;
3385 let mut text = None;
3386 let mut raw_children = OrderedRawChildren::default();
3387 let mut boundary = 0;
3388 let mut buffer = Vec::new();
3389 loop {
3390 match reader
3391 .read_event_into(&mut buffer)
3392 .map_err(OxmlError::from)?
3393 {
3394 Event::Start(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3395 if properties.is_some() {
3396 return Err(duplicate("rPr"));
3397 }
3398 properties = Some(CT_TextCharacterProperties::from_element_with_namespaces(
3399 reader,
3400 &element,
3401 &namespaces,
3402 )?);
3403 boundary = boundary.max(1);
3404 }
3405 Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3406 if properties.is_some() {
3407 return Err(duplicate("rPr"));
3408 }
3409 properties = Some(CT_TextCharacterProperties::from_start_with_namespaces(
3410 &element,
3411 &namespaces,
3412 )?);
3413 boundary = boundary.max(1);
3414 }
3415 Event::Start(element) if text_owned_name(&element, &namespaces)? == b"t" => {
3416 if text.is_some() {
3417 return Err(duplicate("t"));
3418 }
3419 text = Some(TextValue::from_element_with_namespaces(
3420 reader,
3421 &element,
3422 &namespaces,
3423 )?);
3424 boundary = boundary.max(2);
3425 }
3426 Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"t" => {
3427 if text.is_some() {
3428 return Err(duplicate("t"));
3429 }
3430 text = Some(TextValue::from_start_with_namespaces(
3431 &element,
3432 &namespaces,
3433 )?);
3434 boundary = boundary.max(2);
3435 }
3436 Event::Start(element) => {
3437 raw_children.push(boundary, capture_element(reader, &element)?)
3438 }
3439 Event::Empty(element) => {
3440 raw_children.push(boundary, capture_empty_element(&element)?)
3441 }
3442 Event::End(element) if matches_local_name(element.name().as_ref(), b"r") => break,
3443 Event::Eof => return Err(missing_end("r")),
3444 event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
3445 raw_children.push(boundary, text_capture_opaque_event(event)?);
3446 }
3447 _ => {}
3448 }
3449 buffer.clear();
3450 }
3451 Ok(Self {
3452 properties,
3453 text: text.ok_or_else(|| {
3454 TextError::Xml(OxmlError::MissingElement("DrawingML run text".to_owned()))
3455 })?,
3456 raw_children,
3457 source_root,
3458 source_tag: std::str::from_utf8(start.name().as_ref())
3459 .map_err(OxmlError::from)?
3460 .to_owned(),
3461 })
3462 }
3463
3464 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
3465 let owned_tag = text_source_tag(
3466 "a:r",
3467 &self.source_tag,
3468 &text_namespace_scope(&text_source_start("a:r", &self.source_root), &[])?,
3469 );
3470 let tag = owned_tag.as_str();
3471 write_start(writer, text_source_start(tag, &self.source_root))?;
3472 emit_raw(writer, self.raw_children.at(0))?;
3473 if let Some(properties) = &self.properties {
3474 {
3475 let mut child = Writer::new(Vec::new());
3476 properties.write_xml(&mut child, "a:rPr")?;
3477 writer
3478 .get_mut()
3479 .write_all(&text_close_generated_child(
3480 &child.into_inner(),
3481 &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
3482 )?)
3483 .map_err(OxmlError::from)?;
3484 }
3485 }
3486 emit_raw(writer, self.raw_children.at(1))?;
3487 self.text.write_xml(writer)?;
3488 emit_raw(writer, self.raw_children.at(2))?;
3489 write_end(writer, tag)
3490 }
3491}
3492
3493#[allow(non_camel_case_types)]
3495#[derive(Clone, Debug, Default)]
3496pub struct CT_TextLineBreak {
3497 pub properties: Option<CT_TextCharacterProperties>,
3498 raw_children: OrderedRawChildren,
3499 source_root: Vec<String>,
3500 source_tag: String,
3501}
3502impl PartialEq for CT_TextLineBreak {
3503 fn eq(&self, other: &Self) -> bool {
3504 self.properties == other.properties && self.raw_children == other.raw_children
3505 }
3506}
3507impl Eq for CT_TextLineBreak {}
3508
3509impl CT_TextLineBreak {
3510 pub fn from_xml(xml: &[u8]) -> Result<Self> {
3512 Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
3513 }
3514
3515 pub(crate) fn from_xml_with_namespaces(
3516 xml: &[u8],
3517 inherited: &TextNamespaceScope,
3518 ) -> Result<Self> {
3519 parse_complete(
3520 xml,
3521 b"br",
3522 |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
3523 |start| {
3524 Ok(Self {
3525 source_root: text_root_spellings_scoped(
3526 start,
3527 inherited,
3528 &capture_empty_element(start)?,
3529 )?,
3530 source_tag: std::str::from_utf8(start.name().as_ref())
3531 .map_err(OxmlError::from)?
3532 .to_owned(),
3533 ..Self::default()
3534 })
3535 },
3536 )
3537 }
3538
3539 fn from_element_with_namespaces(
3540 reader: &mut Reader<&[u8]>,
3541 start: &BytesStart<'_>,
3542 inherited: &TextNamespaceScope,
3543 ) -> Result<Self> {
3544 let namespaces = text_fragment_scope(start, inherited)?;
3545 let mut line_break = Self {
3546 source_root: text_root_spellings_scoped(
3547 start,
3548 inherited,
3549 &capture_element(&mut reader.clone(), start)?,
3550 )?,
3551 source_tag: std::str::from_utf8(start.name().as_ref())
3552 .map_err(OxmlError::from)?
3553 .to_owned(),
3554 ..Self::default()
3555 };
3556 let mut boundary = 0;
3557 let mut buffer = Vec::new();
3558 loop {
3559 match reader
3560 .read_event_into(&mut buffer)
3561 .map_err(OxmlError::from)?
3562 {
3563 Event::Start(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3564 if line_break.properties.is_some() {
3565 return Err(duplicate("rPr"));
3566 }
3567 line_break.properties =
3568 Some(CT_TextCharacterProperties::from_element_with_namespaces(
3569 reader,
3570 &element,
3571 &namespaces,
3572 )?);
3573 boundary = 1;
3574 }
3575 Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3576 if line_break.properties.is_some() {
3577 return Err(duplicate("rPr"));
3578 }
3579 line_break.properties =
3580 Some(CT_TextCharacterProperties::from_start_with_namespaces(
3581 &element,
3582 &namespaces,
3583 )?);
3584 boundary = 1;
3585 }
3586 Event::Start(element) => line_break
3587 .raw_children
3588 .push(boundary, capture_element(reader, &element)?),
3589 Event::Empty(element) => line_break
3590 .raw_children
3591 .push(boundary, capture_empty_element(&element)?),
3592 Event::End(element) if matches_local_name(element.name().as_ref(), b"br") => {
3593 return Ok(line_break);
3594 }
3595 Event::Eof => return Err(missing_end("br")),
3596 event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
3597 line_break
3598 .raw_children
3599 .push(boundary, text_capture_opaque_event(event)?);
3600 }
3601 _ => {}
3602 }
3603 buffer.clear();
3604 }
3605 }
3606
3607 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
3608 let owned_tag = text_source_tag(
3609 "a:br",
3610 &self.source_tag,
3611 &text_namespace_scope(&text_source_start("a:br", &self.source_root), &[])?,
3612 );
3613 let tag = owned_tag.as_str();
3614 if self.properties.is_none() && self.raw_children.is_empty() {
3615 return write_empty(writer, text_source_start(tag, &self.source_root));
3616 }
3617 write_start(writer, text_source_start(tag, &self.source_root))?;
3618 emit_raw(writer, self.raw_children.at(0))?;
3619 if let Some(properties) = &self.properties {
3620 {
3621 let mut child = Writer::new(Vec::new());
3622 properties.write_xml(&mut child, "a:rPr")?;
3623 writer
3624 .get_mut()
3625 .write_all(&text_close_generated_child(
3626 &child.into_inner(),
3627 &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
3628 )?)
3629 .map_err(OxmlError::from)?;
3630 }
3631 }
3632 emit_raw(writer, self.raw_children.at(1))?;
3633 write_end(writer, tag)
3634 }
3635}
3636
3637#[allow(non_camel_case_types)]
3639#[derive(Clone, Debug)]
3640pub struct CT_TextField {
3641 pub id: String,
3642 pub field_type: Option<String>,
3643 pub run_properties: Option<CT_TextCharacterProperties>,
3644 pub paragraph_properties: Option<CT_TextParagraphProperties>,
3645 pub text: Option<TextValue>,
3646 raw_children: OrderedRawChildren,
3647 source_root: Vec<String>,
3648 source_tag: String,
3649}
3650impl PartialEq for CT_TextField {
3651 fn eq(&self, other: &Self) -> bool {
3652 self.id == other.id
3653 && self.field_type == other.field_type
3654 && self.run_properties == other.run_properties
3655 && self.paragraph_properties == other.paragraph_properties
3656 && self.text == other.text
3657 && self.raw_children == other.raw_children
3658 }
3659}
3660impl Eq for CT_TextField {}
3661
3662impl CT_TextField {
3663 pub fn from_xml(xml: &[u8]) -> Result<Self> {
3665 Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
3666 }
3667
3668 pub(crate) fn from_xml_with_namespaces(
3669 xml: &[u8],
3670 inherited: &TextNamespaceScope,
3671 ) -> Result<Self> {
3672 parse_complete(
3673 xml,
3674 b"fld",
3675 |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
3676 |start| {
3677 let mut field = Self::from_empty(start)?;
3678 field.source_root = text_field_root_spellings(start, inherited)?;
3679 Ok(field)
3680 },
3681 )
3682 }
3683
3684 fn from_start(start: &BytesStart<'_>) -> Result<Self> {
3685 let id = required_attr(start, b"id")?;
3686 if !is_guid(&id) {
3687 return Err(invalid_attribute("fld", "id", &id));
3688 }
3689 Ok(Self {
3690 id,
3691 field_type: text_attr(start, b"type")?,
3692 run_properties: None,
3693 paragraph_properties: None,
3694 text: None,
3695 raw_children: OrderedRawChildren::default(),
3696 source_root: text_attribute_tokens(start)?
3697 .into_iter()
3698 .filter(|(key, _)| key != "id" && key != "type")
3699 .map(|(_, token)| token)
3700 .collect(),
3701 source_tag: std::str::from_utf8(start.name().as_ref())
3702 .map_err(OxmlError::from)?
3703 .to_owned(),
3704 })
3705 }
3706
3707 fn from_empty(start: &BytesStart<'_>) -> Result<Self> {
3708 Self::from_start(start)
3709 }
3710
3711 fn from_element_with_namespaces(
3712 reader: &mut Reader<&[u8]>,
3713 start: &BytesStart<'_>,
3714 inherited: &TextNamespaceScope,
3715 ) -> Result<Self> {
3716 let namespaces = text_fragment_scope(start, inherited)?;
3717 let mut field = Self::from_start(start)?;
3718 field.source_root = text_field_root_spellings(start, inherited)?;
3719 let mut boundary = 0;
3720 let mut buffer = Vec::new();
3721 loop {
3722 match reader
3723 .read_event_into(&mut buffer)
3724 .map_err(OxmlError::from)?
3725 {
3726 Event::Start(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3727 if field.run_properties.is_some() {
3728 return Err(duplicate("rPr"));
3729 }
3730 field.run_properties =
3731 Some(CT_TextCharacterProperties::from_element_with_namespaces(
3732 reader,
3733 &element,
3734 &namespaces,
3735 )?);
3736 boundary = boundary.max(1);
3737 }
3738 Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"rPr" => {
3739 if field.run_properties.is_some() {
3740 return Err(duplicate("rPr"));
3741 }
3742 field.run_properties =
3743 Some(CT_TextCharacterProperties::from_start_with_namespaces(
3744 &element,
3745 &namespaces,
3746 )?);
3747 boundary = boundary.max(1);
3748 }
3749 Event::Start(element) if text_owned_name(&element, &namespaces)? == b"pPr" => {
3750 if field.paragraph_properties.is_some() {
3751 return Err(duplicate("pPr"));
3752 }
3753 field.paragraph_properties =
3754 Some(CT_TextParagraphProperties::from_element_with_namespaces(
3755 reader,
3756 &element,
3757 &namespaces,
3758 )?);
3759 boundary = boundary.max(2);
3760 }
3761 Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"pPr" => {
3762 if field.paragraph_properties.is_some() {
3763 return Err(duplicate("pPr"));
3764 }
3765 field.paragraph_properties =
3766 Some(CT_TextParagraphProperties::from_start_with_namespaces(
3767 &element,
3768 &namespaces,
3769 )?);
3770 boundary = boundary.max(2);
3771 }
3772 Event::Start(element) if text_owned_name(&element, &namespaces)? == b"t" => {
3773 if field.text.is_some() {
3774 return Err(duplicate("t"));
3775 }
3776 field.text = Some(TextValue::from_element_with_namespaces(
3777 reader,
3778 &element,
3779 &namespaces,
3780 )?);
3781 boundary = boundary.max(3);
3782 }
3783 Event::Empty(element) if text_owned_name(&element, &namespaces)? == b"t" => {
3784 if field.text.is_some() {
3785 return Err(duplicate("t"));
3786 }
3787 field.text = Some(TextValue::from_start_with_namespaces(
3788 &element,
3789 &namespaces,
3790 )?);
3791 boundary = boundary.max(3);
3792 }
3793 Event::Start(element) => field
3794 .raw_children
3795 .push(boundary, capture_element(reader, &element)?),
3796 Event::Empty(element) => field
3797 .raw_children
3798 .push(boundary, capture_empty_element(&element)?),
3799 Event::End(element) if matches_local_name(element.name().as_ref(), b"fld") => {
3800 return Ok(field);
3801 }
3802 Event::Eof => return Err(missing_end("fld")),
3803 event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
3804 field
3805 .raw_children
3806 .push(boundary, text_capture_opaque_event(event)?);
3807 }
3808 _ => {}
3809 }
3810 buffer.clear();
3811 }
3812 }
3813
3814 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
3815 let owned_tag = text_source_tag(
3816 "a:fld",
3817 &self.source_tag,
3818 &text_namespace_scope(&text_source_start("a:fld", &self.source_root), &[])?,
3819 );
3820 let tag = owned_tag.as_str();
3821 if !is_guid(&self.id) {
3822 return Err(invalid_attribute("fld", "id", &self.id));
3823 }
3824 let mut start = text_source_start(tag, &self.source_root);
3825 start.push_attribute(("id", self.id.as_str()));
3826 push_optional_attr(&mut start, "type", self.field_type.as_deref());
3827 let modelled = self.run_properties.is_some()
3828 || self.paragraph_properties.is_some()
3829 || self.text.is_some();
3830 if !modelled && self.raw_children.is_empty() {
3831 return write_empty(writer, start);
3832 }
3833 write_start(writer, start)?;
3834 emit_raw(writer, self.raw_children.at(0))?;
3835 if let Some(properties) = &self.run_properties {
3836 {
3837 let mut child = Writer::new(Vec::new());
3838 properties.write_xml(&mut child, "a:rPr")?;
3839 writer
3840 .get_mut()
3841 .write_all(&text_close_generated_child(
3842 &child.into_inner(),
3843 &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
3844 )?)
3845 .map_err(OxmlError::from)?;
3846 }
3847 }
3848 emit_raw(writer, self.raw_children.at(1))?;
3849 if let Some(properties) = &self.paragraph_properties {
3850 {
3851 let mut child = Writer::new(Vec::new());
3852 properties.write_xml(&mut child, "a:pPr")?;
3853 writer
3854 .get_mut()
3855 .write_all(&text_close_generated_child(
3856 &child.into_inner(),
3857 &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
3858 )?)
3859 .map_err(OxmlError::from)?;
3860 }
3861 }
3862 emit_raw(writer, self.raw_children.at(2))?;
3863 if let Some(text) = &self.text {
3864 text.write_xml(writer)?;
3865 }
3866 emit_raw(writer, self.raw_children.at(3))?;
3867 write_end(writer, tag)
3868 }
3869}
3870
3871#[derive(Clone, Debug, Eq, PartialEq)]
3873pub enum TextRun {
3874 Run(CT_RegularTextRun),
3875 Break(CT_TextLineBreak),
3876 Field(Box<CT_TextField>),
3877}
3878
3879impl TextRun {
3880 fn from_xml_with_namespaces(
3881 xml: &[u8],
3882 name: &[u8],
3883 namespaces: &TextNamespaceScope,
3884 ) -> Result<Self> {
3885 match name {
3886 b"r" => Ok(Self::Run(CT_RegularTextRun::from_xml_with_namespaces(
3887 xml, namespaces,
3888 )?)),
3889 b"br" => Ok(Self::Break(CT_TextLineBreak::from_xml_with_namespaces(
3890 xml, namespaces,
3891 )?)),
3892 b"fld" => Ok(Self::Field(Box::new(
3893 CT_TextField::from_xml_with_namespaces(xml, namespaces)?,
3894 ))),
3895 _ => Err(TextError::UnexpectedElement(
3896 String::from_utf8_lossy(name).into_owned(),
3897 )),
3898 }
3899 }
3900
3901 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
3902 match self {
3903 Self::Run(run) => run.write_xml(writer),
3904 Self::Break(line_break) => line_break.write_xml(writer),
3905 Self::Field(field) => field.write_xml(writer),
3906 }
3907 }
3908}
3909
3910#[allow(non_camel_case_types)]
3912#[derive(Clone, Debug, Default)]
3913pub struct CT_TextParagraph {
3914 pub properties: Option<CT_TextParagraphProperties>,
3915 pub runs: Vec<TextRun>,
3916 pub end_properties: Option<CT_TextCharacterProperties>,
3917 raw_children: OrderedRawChildren,
3918 source_root: Vec<String>,
3919 source_tag: String,
3920}
3921impl PartialEq for CT_TextParagraph {
3922 fn eq(&self, other: &Self) -> bool {
3923 self.properties == other.properties
3924 && self.runs == other.runs
3925 && self.end_properties == other.end_properties
3926 && self.raw_children == other.raw_children
3927 }
3928}
3929impl Eq for CT_TextParagraph {}
3930
3931impl CT_TextParagraph {
3932 pub fn properties_mut(&mut self) -> &mut CT_TextParagraphProperties {
3934 if self.properties.is_none() {
3935 let mut raw_children = OrderedRawChildren::default();
3936 for boundary in 0..=2 + self.runs.len() {
3937 let new_boundary = if boundary == 0 { 1 } else { boundary };
3938 for child in self.raw_children.at(boundary) {
3939 raw_children.push(new_boundary, child.to_vec());
3940 }
3941 }
3942 self.raw_children = raw_children;
3943 self.properties = Some(CT_TextParagraphProperties::default());
3944 }
3945 self.properties
3946 .as_mut()
3947 .expect("paragraph properties were inserted")
3948 }
3949
3950 pub fn set_text(&mut self, text: &str) {
3961 let old_run_count = self.runs.len();
3962 if text.is_empty() {
3963 let mut raw_children = OrderedRawChildren::default();
3964 for boundary in 0..=2 + old_run_count {
3965 if boundary <= 1 {
3966 for child in self.raw_children.at(boundary) {
3967 raw_children.push(boundary, child.to_vec());
3968 }
3969 } else if boundary > 1 + old_run_count {
3970 for child in self.raw_children.at(boundary) {
3971 raw_children.push(2, child.to_vec());
3972 }
3973 }
3974 }
3975 self.raw_children = raw_children;
3976 self.runs.clear();
3977 return;
3978 }
3979 let retained_run = self.runs.iter().find_map(|run| match run {
3980 TextRun::Run(run) => Some(run.clone()),
3981 TextRun::Break(_) | TextRun::Field(_) => None,
3982 });
3983 let mut raw_children = OrderedRawChildren::default();
3984 for boundary in 0..=2 + old_run_count {
3985 let new_boundary = if boundary <= 1 {
3986 boundary
3987 } else if boundary <= 1 + old_run_count {
3988 2
3989 } else {
3990 3
3991 };
3992 for child in self.raw_children.at(boundary) {
3993 raw_children.push(new_boundary, child.to_vec());
3994 }
3995 }
3996 self.raw_children = raw_children;
3997 let mut run = retained_run.unwrap_or_else(|| CT_RegularTextRun {
3998 properties: self
3999 .end_properties
4000 .clone()
4001 .map(CT_TextCharacterProperties::without_hyperlinks),
4002 ..CT_RegularTextRun::new(text)
4003 });
4004 run.set_text(text);
4005 self.runs = vec![TextRun::Run(run)];
4006 self.split_line_breaks(text);
4007 }
4008
4009 fn split_line_breaks(&mut self, text: &str) {
4013 let [TextRun::Run(first)] = self.runs.as_mut_slice() else {
4014 return;
4015 };
4016 if !text.contains('\u{b}') {
4017 return;
4018 }
4019 let mut lines = text.split('\u{b}');
4020 first.set_text(lines.next().unwrap_or_default());
4021 let properties = first.properties.clone();
4022 for line in lines {
4023 self.raw_children.shift_boundaries_from(2);
4024 self.runs.push(TextRun::Break(CT_TextLineBreak {
4025 properties: properties.clone(),
4026 ..CT_TextLineBreak::default()
4027 }));
4028 if !line.is_empty() {
4029 self.raw_children.shift_boundaries_from(2);
4030 self.runs.push(TextRun::Run(CT_RegularTextRun {
4031 properties: properties.clone(),
4032 ..CT_RegularTextRun::new(line)
4033 }));
4034 }
4035 }
4036 }
4037
4038 pub fn add_run(&mut self, text: &str) -> &mut CT_RegularTextRun {
4040 self.raw_children.shift_boundaries_from(2 + self.runs.len());
4041 self.runs.push(TextRun::Run(CT_RegularTextRun::new(text)));
4042 let Some(TextRun::Run(run)) = self.runs.last_mut() else {
4043 unreachable!("the appended text choice is a regular run")
4044 };
4045 run
4046 }
4047
4048 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4050 Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
4051 }
4052
4053 pub(crate) fn from_xml_with_namespaces(
4054 xml: &[u8],
4055 inherited: &TextNamespaceScope,
4056 ) -> Result<Self> {
4057 parse_complete(
4058 xml,
4059 b"p",
4060 |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
4061 |start| {
4062 Ok(Self {
4063 source_root: text_root_spellings_scoped(
4064 start,
4065 inherited,
4066 &capture_empty_element(start)?,
4067 )?,
4068 source_tag: std::str::from_utf8(start.name().as_ref())
4069 .map_err(OxmlError::from)?
4070 .to_owned(),
4071 ..Self::default()
4072 })
4073 },
4074 )
4075 }
4076
4077 fn from_element_with_namespaces(
4078 reader: &mut Reader<&[u8]>,
4079 start: &BytesStart<'_>,
4080 inherited: &TextNamespaceScope,
4081 ) -> Result<Self> {
4082 let namespaces = text_fragment_scope(start, inherited)?;
4083 let mut paragraph = Self {
4084 source_root: text_root_spellings_scoped(
4085 start,
4086 inherited,
4087 &capture_element(&mut reader.clone(), start)?,
4088 )?,
4089 source_tag: std::str::from_utf8(start.name().as_ref())
4090 .map_err(OxmlError::from)?
4091 .to_owned(),
4092 ..Self::default()
4093 };
4094 let mut boundary = 0;
4095 let mut buffer = Vec::new();
4096 loop {
4097 match reader
4098 .read_event_into(&mut buffer)
4099 .map_err(OxmlError::from)?
4100 {
4101 Event::Start(element)
4106 if text_owned_name(&element, &namespaces)? == b"pPr"
4107 && paragraph.properties.is_none() =>
4108 {
4109 paragraph.properties =
4110 Some(CT_TextParagraphProperties::from_element_with_namespaces(
4111 reader,
4112 &element,
4113 &namespaces,
4114 )?);
4115 boundary = boundary.max(1);
4116 }
4117 Event::Empty(element)
4118 if text_owned_name(&element, &namespaces)? == b"pPr"
4119 && paragraph.properties.is_none() =>
4120 {
4121 paragraph.properties =
4122 Some(CT_TextParagraphProperties::from_start_with_namespaces(
4123 &element,
4124 &namespaces,
4125 )?);
4126 boundary = boundary.max(1);
4127 }
4128 Event::Start(element) if is_text_run(&text_owned_name(&element, &namespaces)?) => {
4129 let name = local_name(element.name().as_ref()).to_vec();
4130 let raw = capture_element(reader, &element)?;
4131 paragraph.runs.push(TextRun::from_xml_with_namespaces(
4132 &raw,
4133 &name,
4134 &namespaces,
4135 )?);
4136 boundary = boundary.max(1 + paragraph.runs.len());
4137 }
4138 Event::Empty(element) if is_text_run(&text_owned_name(&element, &namespaces)?) => {
4139 let name = local_name(element.name().as_ref()).to_vec();
4140 let raw = capture_empty_element(&element)?;
4141 paragraph.runs.push(TextRun::from_xml_with_namespaces(
4142 &raw,
4143 &name,
4144 &namespaces,
4145 )?);
4146 boundary = boundary.max(1 + paragraph.runs.len());
4147 }
4148 Event::Start(element)
4149 if text_owned_name(&element, &namespaces)? == b"endParaRPr" =>
4150 {
4151 if paragraph.end_properties.is_some() {
4152 return Err(duplicate("endParaRPr"));
4153 }
4154 paragraph.end_properties =
4155 Some(CT_TextCharacterProperties::from_element_with_namespaces(
4156 reader,
4157 &element,
4158 &namespaces,
4159 )?);
4160 boundary = boundary.max(2 + paragraph.runs.len());
4161 }
4162 Event::Empty(element)
4163 if text_owned_name(&element, &namespaces)? == b"endParaRPr" =>
4164 {
4165 if paragraph.end_properties.is_some() {
4166 return Err(duplicate("endParaRPr"));
4167 }
4168 paragraph.end_properties =
4169 Some(CT_TextCharacterProperties::from_start_with_namespaces(
4170 &element,
4171 &namespaces,
4172 )?);
4173 boundary = boundary.max(2 + paragraph.runs.len());
4174 }
4175 Event::Start(element) => paragraph
4176 .raw_children
4177 .push(boundary, capture_element(reader, &element)?),
4178 Event::Empty(element) => paragraph
4179 .raw_children
4180 .push(boundary, capture_empty_element(&element)?),
4181 Event::End(element) if matches_local_name(element.name().as_ref(), b"p") => {
4182 return Ok(paragraph);
4183 }
4184 Event::Eof => return Err(missing_end("p")),
4185 event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
4186 paragraph
4187 .raw_children
4188 .push(boundary, text_capture_opaque_event(event)?);
4189 }
4190 _ => {}
4191 }
4192 buffer.clear();
4193 }
4194 }
4195
4196 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
4198 let owned_tag = text_source_tag(
4199 "a:p",
4200 &self.source_tag,
4201 &text_namespace_scope(&text_source_start("a:p", &self.source_root), &[])?,
4202 );
4203 let tag = owned_tag.as_str();
4204 if self.properties.is_none()
4205 && self.runs.is_empty()
4206 && self.end_properties.is_none()
4207 && self.raw_children.is_empty()
4208 {
4209 return write_empty(writer, text_source_start(tag, &self.source_root));
4210 }
4211 write_start(writer, text_source_start(tag, &self.source_root))?;
4212 emit_raw(writer, self.raw_children.at(0))?;
4213 if let Some(properties) = &self.properties {
4214 {
4215 let mut child = Writer::new(Vec::new());
4216 properties.write_xml(&mut child, "a:pPr")?;
4217 writer
4218 .get_mut()
4219 .write_all(&text_close_generated_child(
4220 &child.into_inner(),
4221 &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
4222 )?)
4223 .map_err(OxmlError::from)?;
4224 }
4225 }
4226 emit_raw(writer, self.raw_children.at(1))?;
4227 for (index, run) in self.runs.iter().enumerate() {
4228 run.write_xml(writer)?;
4229 emit_raw(writer, self.raw_children.at(2 + index))?;
4230 }
4231 if let Some(properties) = &self.end_properties {
4232 {
4233 let mut child = Writer::new(Vec::new());
4234 properties.write_xml(&mut child, "a:endParaRPr")?;
4235 writer
4236 .get_mut()
4237 .write_all(&text_close_generated_child(
4238 &child.into_inner(),
4239 &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
4240 )?)
4241 .map_err(OxmlError::from)?;
4242 }
4243 }
4244 emit_raw(writer, self.raw_children.at(2 + self.runs.len()))?;
4245 write_end(writer, tag)
4246 }
4247
4248 pub fn to_xml(&self) -> Result<Vec<u8>> {
4250 let mut writer = Writer::new(Vec::new());
4251 self.write_xml(&mut writer)?;
4252 Ok(writer.into_inner())
4253 }
4254
4255 pub fn raw_children(&self) -> &OrderedRawChildren {
4256 &self.raw_children
4257 }
4258}
4259
4260fn is_text_run(name: &[u8]) -> bool {
4261 matches!(local_name(name), b"r" | b"br" | b"fld")
4262}
4263
4264fn parse_complete<T>(
4265 xml: &[u8],
4266 expected: &[u8],
4267 parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
4268 parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
4269) -> Result<T> {
4270 let mut reader = Reader::from_reader(xml);
4271 let mut buffer = Vec::new();
4272 loop {
4273 match reader
4274 .read_event_into(&mut buffer)
4275 .map_err(OxmlError::from)?
4276 {
4277 Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
4278 text_validate_root(&element)?;
4279 let parsed = parse_start(&mut reader, &element)?;
4280 text_reject_trailing(&mut reader)?;
4281 return Ok(parsed);
4282 }
4283 Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
4284 text_validate_root(&element)?;
4285 let parsed = parse_empty(&element)?;
4286 text_reject_trailing(&mut reader)?;
4287 return Ok(parsed);
4288 }
4289 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
4290 Event::Eof => {
4291 return Err(TextError::Xml(OxmlError::MissingElement(
4292 String::from_utf8_lossy(expected).into_owned(),
4293 )));
4294 }
4295 _ => {}
4296 }
4297 buffer.clear();
4298 }
4299}
4300
4301pub(crate) fn text_reject_trailing(reader: &mut Reader<&[u8]>) -> Result<()> {
4302 loop {
4303 match reader.read_event().map_err(OxmlError::from)? {
4304 Event::Eof => return Ok(()),
4305 Event::Comment(_) | Event::PI(_) => {}
4306 Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
4307 _ => {
4308 return Err(TextError::UnexpectedElement(
4309 "trailing XML content".to_owned(),
4310 ));
4311 }
4312 }
4313 }
4314}
4315
4316fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
4317 let mut reader = Reader::from_reader(xml);
4318 let mut buffer = Vec::new();
4319 loop {
4320 match reader
4321 .read_event_into(&mut buffer)
4322 .map_err(OxmlError::from)?
4323 {
4324 Event::Start(element) | Event::Empty(element) => {
4325 return Ok(local_name(element.name().as_ref()).to_vec());
4326 }
4327 Event::Eof => {
4328 return Err(TextError::Xml(OxmlError::MissingElement(
4329 "DrawingML text properties".to_owned(),
4330 )));
4331 }
4332 _ => {}
4333 }
4334 buffer.clear();
4335 }
4336}
4337
4338fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
4339 let mut buffer = Vec::new();
4340 loop {
4341 match reader
4342 .read_event_into(&mut buffer)
4343 .map_err(OxmlError::from)?
4344 {
4345 Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
4346 return Ok(());
4347 }
4348 Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
4349 Event::Comment(_) => {}
4350 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
4351 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
4352 _ => {
4353 return Err(TextError::UnexpectedElement(
4354 String::from_utf8_lossy(expected).into_owned(),
4355 ));
4356 }
4357 }
4358 buffer.clear();
4359 }
4360}
4361
4362fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
4363 for attribute in start.attributes() {
4364 let attribute = attribute.map_err(OxmlError::from)?;
4365 if attribute.key.as_ref() == name {
4366 let value = attribute
4367 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
4368 .map_err(OxmlError::from)?;
4369 return Ok(Some(value.into_owned()));
4370 }
4371 }
4372 Ok(None)
4373}
4374
4375fn capture_raw_attributes(
4376 start: &BytesStart<'_>,
4377 modelled: &[&[u8]],
4378) -> Result<Vec<(String, String)>> {
4379 let mut raw = Vec::new();
4380 for attribute in start.attributes() {
4381 let attribute = attribute.map_err(OxmlError::from)?;
4382 if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
4383 continue;
4384 }
4385 let name = std::str::from_utf8(attribute.key.as_ref())
4386 .map_err(OxmlError::from)?
4387 .to_owned();
4388 let value = attribute
4389 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
4390 .map_err(OxmlError::from)?
4391 .into_owned();
4392 raw.push((name, value));
4393 }
4394 Ok(raw)
4395}
4396
4397fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
4398 for (name, value) in attributes {
4399 start.push_attribute((name.as_str(), value.as_str()));
4400 }
4401}
4402
4403fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
4404 text_attr(start, name)?
4405 .filter(|value| !value.is_empty())
4406 .ok_or_else(|| {
4407 missing_attribute(
4408 &String::from_utf8_lossy(local_name(start.name().as_ref())),
4409 &String::from_utf8_lossy(name),
4410 )
4411 })
4412}
4413
4414fn parse_optional_i32(
4415 start: &BytesStart<'_>,
4416 name: &[u8],
4417 min: i32,
4418 max: i32,
4419) -> Result<Option<i32>> {
4420 text_attr(start, name)?
4421 .map(|value| {
4422 parse_i32_value(
4423 &String::from_utf8_lossy(local_name(start.name().as_ref())),
4424 &String::from_utf8_lossy(name),
4425 &value,
4426 min,
4427 max,
4428 )
4429 })
4430 .transpose()
4431}
4432
4433fn parse_i32_attr(start: &BytesStart<'_>, name: &[u8], min: i32, max: i32) -> Result<i32> {
4434 let value = required_attr(start, name)?;
4435 parse_i32_value(
4436 &String::from_utf8_lossy(local_name(start.name().as_ref())),
4437 &String::from_utf8_lossy(name),
4438 &value,
4439 min,
4440 max,
4441 )
4442}
4443
4444fn parse_i32_value(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
4445 let parsed = value
4446 .parse::<i32>()
4447 .map_err(|_| invalid_attribute(element, attribute, value))?;
4448 if (min..=max).contains(&parsed) {
4449 Ok(parsed)
4450 } else {
4451 Err(invalid_attribute(element, attribute, value))
4452 }
4453}
4454
4455fn parse_optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
4456 text_attr(start, name)?
4457 .map(|value| match value.as_str() {
4458 "true" | "1" => Ok(true),
4459 "false" | "0" => Ok(false),
4460 _ => Err(invalid_attribute(
4461 &String::from_utf8_lossy(local_name(start.name().as_ref())),
4462 &String::from_utf8_lossy(name),
4463 &value,
4464 )),
4465 })
4466 .transpose()
4467}
4468
4469fn parse_optional_enum<T>(
4470 start: &BytesStart<'_>,
4471 name: &[u8],
4472 parse: impl FnOnce(&str) -> Option<T>,
4473) -> Result<Option<T>> {
4474 let Some(value) = text_attr(start, name)? else {
4475 return Ok(None);
4476 };
4477 parse(&value).map(Some).ok_or_else(|| {
4478 invalid_attribute(
4479 &String::from_utf8_lossy(local_name(start.name().as_ref())),
4480 &String::from_utf8_lossy(name),
4481 &value,
4482 )
4483 })
4484}
4485
4486fn validate_spacing_percent(value: &str) -> Result<()> {
4487 if is_percentage_string(value) {
4488 return Ok(());
4489 }
4490 parse_i32_value("spcPct", "val", value, 0, MAX_TRANSITIONAL_SPACING_PERCENT)?;
4491 Ok(())
4492}
4493
4494fn validate_baseline(value: &str) -> Result<()> {
4495 if is_percentage_string(value) {
4496 return Ok(());
4497 }
4498 parse_i32_value("rPr", "baseline", value, -100_000, 100_000)?;
4499 Ok(())
4500}
4501
4502fn is_percentage_string(value: &str) -> bool {
4503 value.strip_suffix('%').is_some_and(is_signed_decimal)
4504}
4505
4506fn is_universal_measure(value: &str) -> bool {
4507 ["mm", "cm", "in", "pt", "pc", "pi"]
4508 .iter()
4509 .find_map(|unit| value.strip_suffix(unit))
4510 .is_some_and(is_signed_decimal)
4511}
4512
4513fn is_signed_decimal(value: &str) -> bool {
4514 let unsigned = value.strip_prefix('-').unwrap_or(value);
4515 let mut parts = unsigned.split('.');
4516 let Some(integer) = parts.next() else {
4517 return false;
4518 };
4519 if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
4520 return false;
4521 }
4522 if let Some(fraction) = parts.next()
4523 && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
4524 {
4525 return false;
4526 }
4527 parts.next().is_none()
4528}
4529
4530fn is_guid(value: &str) -> bool {
4531 if value.len() != 38 || !value.starts_with('{') || !value.ends_with('}') {
4532 return false;
4533 }
4534 value.bytes().enumerate().all(|(index, byte)| match index {
4535 0 => byte == b'{',
4536 37 => byte == b'}',
4537 9 | 14 | 19 | 24 => byte == b'-',
4538 _ => byte.is_ascii_hexdigit(),
4539 })
4540}
4541
4542fn push_optional_attr<'a>(start: &mut BytesStart<'a>, name: &'a str, value: Option<&'a str>) {
4543 if let Some(value) = value {
4544 start.push_attribute((name, value));
4545 }
4546}
4547
4548fn push_optional_bool(start: &mut BytesStart<'_>, name: &'static str, value: Option<bool>) {
4549 if let Some(value) = value {
4550 start.push_attribute((name, if value { "1" } else { "0" }));
4551 }
4552}
4553
4554fn push_owned_attr(start: &mut BytesStart<'_>, name: &'static str, value: String) {
4555 start.push_attribute((name, value.as_str()));
4556}
4557
4558fn emit_raw<'a, W: Write>(
4559 writer: &mut Writer<W>,
4560 children: impl Iterator<Item = &'a [u8]>,
4561) -> Result<()> {
4562 for child in children {
4563 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
4564 }
4565 Ok(())
4566}
4567
4568fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
4569 writer
4570 .write_event(Event::Start(start))
4571 .map_err(OxmlError::from)?;
4572 Ok(())
4573}
4574
4575fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
4576 writer
4577 .write_event(Event::Empty(start))
4578 .map_err(OxmlError::from)?;
4579 Ok(())
4580}
4581
4582fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
4583 writer
4584 .write_event(Event::End(BytesEnd::new(tag)))
4585 .map_err(OxmlError::from)?;
4586 Ok(())
4587}
4588
4589fn unexpected(element: &BytesStart<'_>) -> TextError {
4590 TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
4591}
4592
4593fn duplicate(element: &str) -> TextError {
4594 TextError::DuplicateElement(element.to_owned())
4595}
4596
4597fn missing_attribute(element: &str, attribute: &str) -> TextError {
4598 TextError::MissingAttribute {
4599 element: element.to_owned(),
4600 attribute: attribute.to_owned(),
4601 }
4602}
4603
4604fn invalid_attribute(element: &str, attribute: &str, value: &str) -> TextError {
4605 TextError::InvalidAttribute {
4606 element: element.to_owned(),
4607 attribute: attribute.to_owned(),
4608 value: value.to_owned(),
4609 }
4610}
4611
4612#[cfg(test)]
4613mod tests {
4614 use quick_xml::Writer;
4615 use std::panic;
4616
4617 use super::{
4618 CT_TextCharacterProperties, CT_TextParagraph, CT_TextParagraphProperties, TextAlignment,
4619 TextHyperlink, TextRun, TextSpace, TextSpacing,
4620 };
4621 use crate::color::ColorChoice;
4622 use crate::text::CT_TextBody;
4623 use crate::text::{
4624 TextAutoNumberScheme, TextBullet, TextBulletCharacter, TextBulletChoice,
4625 TextBulletSizeValue,
4626 };
4627
4628 #[test]
4629 fn fx205_language_edits_preserve_other_attributes_and_font_source() {
4630 let source = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:e="urn:opaque" lang="en-US" e:lang="unchanged" dirty="0"><a:ea typeface="Arial" panose="020B0604020202020204"/><e:payload keep="yes"/></a:rPr>"#;
4631 let mut properties = CT_TextCharacterProperties::from_xml(source).unwrap();
4632 let original = properties.clone();
4633 properties.set_language(Some("ja-JP"));
4634 assert_eq!(properties.language(), Some("ja-JP"));
4635 let mut writer = Writer::new(Vec::new());
4636 properties.write_xml(&mut writer, "a:rPr").unwrap();
4637 let saved = writer.into_inner();
4638 let xml = String::from_utf8(saved.clone()).unwrap();
4639 assert!(xml.contains("e:lang=\"unchanged\""));
4640 assert!(xml.contains("<e:payload keep=\"yes\"/>"));
4641 assert!(xml.contains("panose=\"020B0604020202020204\""));
4642 let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
4643 assert_eq!(reopened.language(), Some("ja-JP"));
4644 assert_eq!(reopened.east_asian, properties.east_asian);
4645 let mut writer = Writer::new(Vec::new());
4646 reopened.write_xml(&mut writer, "a:rPr").unwrap();
4647 assert_eq!(writer.into_inner(), saved);
4648 properties.set_language(Some("en-US"));
4649 assert_eq!(properties, original);
4650 properties.set_language(None);
4651 assert_eq!(properties.language(), None);
4652 assert!(super::TextFont::new("Arial\u{0}").is_err());
4653 }
4654
4655 #[test]
4656 fn fx206_picture_bullet_list_equality_preserves_foreign_and_opaque_owners() {
4657 let source = br#"<a:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:bodyPr/><a:lstStyle><a:lvl1pPr><a:defRPr><a:solidFill><a:srgbClr val="5D5F64"/></a:solidFill></a:defRPr></a:lvl1pPr><a:lvl2pPr><a:buFontTx/><a:buBlip><a:blip r:embed="rId3"/></a:buBlip><a:defRPr><a:solidFill><a:srgbClr val="F89728"/></a:solidFill></a:defRPr></a:lvl2pPr></a:lstStyle><a:p/></a:txBody>"#;
4658 let body = CT_TextBody::from_xml(source).unwrap();
4659 let saved = body.to_xml().unwrap();
4660 let reopened = CT_TextBody::from_xml(&saved).unwrap();
4661 assert_eq!(reopened, body);
4662 assert_eq!(reopened.to_xml().unwrap(), saved);
4663 let changed_owner = String::from_utf8_lossy(source).replace("rId3", "rId4");
4664 assert_ne!(
4665 body,
4666 CT_TextBody::from_xml(changed_owner.as_bytes()).unwrap()
4667 );
4668 assert!(String::from_utf8_lossy(&saved).contains("r:embed=\"rId3\""));
4669
4670 let plain = crate::text::CT_TextListStyle::from_xml(
4671 br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>"#,
4672 )
4673 .unwrap();
4674 let canonical = crate::text::CT_TextListStyle::from_xml(br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/>"#).unwrap();
4675 let foreign = crate::text::CT_TextListStyle::from_xml(br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="urn:foreign"/>"#).unwrap();
4676 let opaque = crate::text::CT_TextListStyle::from_xml(br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" keep="opaque"/>"#).unwrap();
4677 assert_eq!(plain, canonical);
4678 assert_ne!(plain, foreign);
4679 assert_ne!(plain, opaque);
4680 let opaque_child = crate::text::CT_TextListStyle::from_xml(br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:extLst/></a:lstStyle>"#).unwrap();
4681 assert_ne!(plain, opaque_child);
4682 assert!(
4683 String::from_utf8_lossy(&foreign.to_xml().unwrap()).contains("xmlns:r=\"urn:foreign\"")
4684 );
4685 assert!(String::from_utf8_lossy(&opaque.to_xml().unwrap()).contains("keep=\"opaque\""));
4686
4687 let first = CT_TextParagraphProperties::default();
4688 let mut transport = first.clone();
4689 transport
4690 .raw_attributes
4691 .push(("xmlns:r".into(), super::TEXT_REL_NS.into()));
4692 assert_eq!(first, transport);
4693 transport.raw_attributes[0].1 = "urn:foreign".into();
4694 assert_ne!(first, transport);
4695 transport.raw_attributes.clear();
4696 transport
4697 .raw_attributes
4698 .push(("keep".into(), "opaque".into()));
4699 assert_ne!(first, transport);
4700 transport.raw_attributes.clear();
4701 transport.level = Some(1);
4702 assert_ne!(first, transport);
4703 }
4704
4705 #[test]
4706 fn fx206_hyperlink_equality_ignores_only_canonical_transport_declarations() {
4707 let body = CT_TextBody::from_xml(br#"<a:txBody xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:bodyPr/><a:lstStyle/><a:p><a:r><a:rPr><a:hlinkClick r:id=""/></a:rPr><a:t>link</a:t></a:r></a:p></a:txBody>"#).unwrap();
4708 let bytes = body.to_xml().unwrap();
4709 assert_eq!(CT_TextBody::from_xml(&bytes).unwrap(), body);
4710 let mut first = TextHyperlink::default();
4711 let mut canonical = first.clone();
4712 canonical.raw_attributes = vec![
4713 ("xmlns:a".into(), super::A_NS.into()),
4714 ("xmlns:r".into(), super::TEXT_REL_NS.into()),
4715 ];
4716 assert_eq!(first, canonical);
4717 first
4718 .raw_attributes
4719 .push(("xmlns:r".into(), "urn:foreign".into()));
4720 assert_ne!(first, canonical);
4721 canonical
4722 .raw_attributes
4723 .push(("foreign".into(), "kept".into()));
4724 assert_ne!(TextHyperlink::default(), canonical);
4725 }
4726
4727 #[test]
4728 fn drawingml_rtl_attribute_becomes_typed_without_reordering_unknown_content() {
4729 let parsed = CT_TextParagraphProperties::from_xml(
4730 br#"<q:pPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:test" defTabSz="457200" rtl="1" x:keep="yes"/>"#,
4731 )
4732 .unwrap();
4733
4734 assert_eq!(parsed.right_to_left, Some(true));
4735 let serialized = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
4736 assert!(serialized.contains("rtl=\"1\""));
4737 assert!(serialized.contains("x:keep=\"yes\""));
4738 assert!(
4739 serialized.find("defTabSz").unwrap() < serialized.find("rtl").unwrap(),
4740 "{serialized}"
4741 );
4742
4743 let mut changed = parsed;
4744 changed.right_to_left = Some(false);
4745 let serialized = String::from_utf8(changed.to_xml().unwrap()).unwrap();
4746 assert!(serialized.contains("rtl=\"0\""), "{serialized}");
4747 assert!(!serialized.contains("rtl=\"1\""), "{serialized}");
4748 }
4749
4750 #[test]
4751 fn leading_and_trailing_text_whitespace_survives_via_xml_space_preserve() {
4752 let xml = br#"<q:p><q:r><q:t> leading</q:t></q:r><q:r><q:t>trailing </q:t></q:r><q:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}"><q:t xml:space="preserve">middle</q:t></q:fld></q:p>"#;
4753 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
4754 assert_eq!(paragraph.to_xml().unwrap(), br#"<a:p><a:r><a:t xml:space="preserve"> leading</a:t></a:r><a:r><a:t xml:space="preserve">trailing </a:t></a:r><a:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}"><a:t xml:space="preserve">middle</a:t></a:fld></a:p>"#);
4755
4756 let TextRun::Run(first) = ¶graph.runs[0] else {
4757 panic!("expected regular run");
4758 };
4759 assert_eq!(first.text.value, " leading");
4760 assert_eq!(first.text.space, TextSpace::Preserve);
4761
4762 let body = CT_TextBody::from_xml(
4763 br#"<q:txBody><q:bodyPr/><q:p><q:r><q:t> body </q:t></q:r></q:p></q:txBody>"#,
4764 )
4765 .unwrap();
4766 assert_eq!(body.to_xml().unwrap(), br#"<a:txBody><a:bodyPr/><a:p><a:r><a:t xml:space="preserve"> body </a:t></a:r></a:p></a:txBody>"#);
4767
4768 let hostile = CT_TextParagraph::from_xml(
4769 br#"<q:p><q:r><q:t x:space="preserve">plain</q:t></q:r></q:p>"#,
4770 )
4771 .unwrap();
4772 let TextRun::Run(run) = &hostile.runs[0] else {
4773 panic!("expected regular run");
4774 };
4775 assert_eq!(run.text.space, TextSpace::Default);
4776 assert_eq!(
4777 hostile.to_xml().unwrap(),
4778 br#"<a:p><a:r><a:t x:space="preserve">plain</a:t></a:r></a:p>"#
4779 );
4780 }
4781
4782 #[test]
4783 fn canonical_text_state_matches_whitespace_and_empty_hyperlinks_from_real_decks() {
4784 let paragraph = CT_TextParagraph::from_xml(
4785 br#"<q:p><q:r><q:rPr><q:hlinkClick r:id=""/></q:rPr><q:t>trailing </q:t></q:r></q:p>"#,
4786 )
4787 .unwrap();
4788 let TextRun::Run(run) = ¶graph.runs[0] else {
4789 panic!("expected regular run");
4790 };
4791 assert_eq!(run.text.space, TextSpace::Preserve);
4792 assert_eq!(
4793 run.properties
4794 .as_ref()
4795 .and_then(|properties| properties.hyperlink_click.as_ref())
4796 .and_then(|hyperlink| hyperlink.relationship_id.as_deref()),
4797 Some("")
4798 );
4799 let written = paragraph.to_xml().unwrap();
4800 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
4801 }
4802
4803 #[test]
4804 fn text_written_into_an_empty_paragraph_takes_its_end_formatting() {
4805 let mut paragraph = CT_TextParagraph::from_xml(
4806 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:pPr algn="r"/><a:endParaRPr sz="1500" b="1"><a:hlinkClick r:id="rId9"/></a:endParaRPr></a:p>"#,
4807 )
4808 .unwrap();
4809 paragraph.set_text("Risk");
4810 assert_eq!(
4811 String::from_utf8(paragraph.to_xml().unwrap()).unwrap(),
4812 r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:pPr algn="r"/><a:r><a:rPr sz="1500" b="1" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/><a:t>Risk</a:t></a:r><a:endParaRPr sz="1500" b="1" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:hlinkClick r:id="rId9" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/></a:endParaRPr></a:p>"#
4813 );
4814
4815 let saved = paragraph.to_xml().unwrap();
4816 assert_eq!(
4817 CT_TextParagraph::from_xml(&saved)
4818 .unwrap()
4819 .to_xml()
4820 .unwrap(),
4821 saved
4822 );
4823
4824 let mut formatted = CT_TextParagraph::from_xml(
4825 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr i="1"/><a:t>old</a:t></a:r><a:endParaRPr sz="1500"/></a:p>"#,
4826 )
4827 .unwrap();
4828 formatted.set_text("new");
4829 assert_eq!(
4830 String::from_utf8(formatted.to_xml().unwrap()).unwrap(),
4831 r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:r><a:rPr i="1"/><a:t>new</a:t></a:r><a:endParaRPr sz="1500"/></a:p>"#
4832 );
4833
4834 let mut bare = CT_TextParagraph::default();
4835 bare.set_text("plain");
4836 assert_eq!(
4837 bare.to_xml().unwrap(),
4838 br#"<a:p><a:r><a:t>plain</a:t></a:r></a:p>"#
4839 );
4840 }
4841
4842 #[test]
4843 fn paragraph_runs_fields_and_breaks_round_trip_structurally() {
4844 let xml = br#"<q:p><x:before/><q:pPr algn="ctr"/><x:afterPPr/><q:r><q:rPr b="1"><q:hlinkClick x:id="not-a-relationship" r:id="rId7" action="ppaction://hlinksldjump" tooltip="go & stay" history="0" highlightClick="1" endSnd="0"><x:sound/></q:hlinkClick></q:rPr><q:t>one</q:t></q:r><x:between/><q:br><q:rPr i="true"/></q:br><q:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}" type="slidenum"><q:rPr sz="1200"/><q:pPr lvl="1"/><q:t>2</q:t></q:fld><q:endParaRPr sz="1400"/><x:after/></q:p>"#;
4845 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
4846 assert_eq!(paragraph.runs.len(), 3);
4847 let written = paragraph.to_xml().unwrap();
4848 assert_eq!(written, br#"<a:p><x:before/><a:pPr algn="ctr"/><x:afterPPr/><a:r><a:rPr b="1"><a:hlinkClick r:id="rId7" action="ppaction://hlinksldjump" tooltip="go & stay" history="0" highlightClick="1" endSnd="0" x:id="not-a-relationship"><x:sound/></a:hlinkClick></a:rPr><a:t>one</a:t></a:r><x:between/><a:br><a:rPr i="1"/></a:br><a:fld id="{00112233-4455-6677-8899-AABBCCDDEEFF}" type="slidenum"><a:rPr sz="1200"/><a:pPr lvl="1"/><a:t>2</a:t></a:fld><a:endParaRPr sz="1400"/><x:after/></a:p>"#);
4849 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
4850
4851 let hostile = CT_TextCharacterProperties::from_xml(
4852 br#"<q:rPr><q:hlinkClick x:id="not-a-relationship"/></q:rPr>"#,
4853 )
4854 .unwrap();
4855 assert!(
4856 hostile
4857 .hyperlink_click
4858 .as_ref()
4859 .unwrap()
4860 .relationship_id
4861 .is_none()
4862 );
4863 let mut writer = quick_xml::Writer::new(Vec::new());
4864 hostile.write_xml(&mut writer, "a:rPr").unwrap();
4865 assert_eq!(
4866 writer.into_inner(),
4867 br#"<a:rPr><a:hlinkClick x:id="not-a-relationship"/></a:rPr>"#
4868 );
4869 }
4870
4871 #[test]
4872 fn a_later_paragraph_properties_element_is_preserved_where_it_stands() {
4873 let xml = br#"<a:p><a:pPr algn="l"/><a:r><a:t>One.</a:t></a:r><a:pPr algn="r"><a:lnSpc><a:spcPct val="90000"/></a:lnSpc></a:pPr><a:r><a:t>Two.</a:t></a:r><a:pPr/><a:endParaRPr lang="en-US"/></a:p>"#;
4874 let mut paragraph = CT_TextParagraph::from_xml(xml).unwrap();
4875 let properties = paragraph.properties.as_ref().unwrap();
4876 assert_eq!(properties.alignment, Some(TextAlignment::Left));
4877 assert_eq!(properties.line_spacing, None);
4878 let texts = paragraph
4879 .runs
4880 .iter()
4881 .map(|run| match run {
4882 TextRun::Run(run) => run.text.value.as_str(),
4883 TextRun::Break(_) | TextRun::Field(_) => panic!("expected regular runs"),
4884 })
4885 .collect::<Vec<_>>();
4886 assert_eq!(texts, ["One.", "Two."]);
4887 assert_eq!(paragraph.to_xml().unwrap(), xml);
4888 assert_eq!(CT_TextParagraph::from_xml(xml).unwrap(), paragraph);
4889
4890 let TextRun::Run(first) = &mut paragraph.runs[0] else {
4891 panic!("expected regular run");
4892 };
4893 first.set_text("Uno.");
4894 paragraph.properties_mut().level = Some(1);
4895 paragraph.add_run("Three.");
4896 assert_eq!(
4897 paragraph.to_xml().unwrap(),
4898 br#"<a:p><a:pPr lvl="1" algn="l"/><a:r><a:t>Uno.</a:t></a:r><a:pPr algn="r"><a:lnSpc><a:spcPct val="90000"/></a:lnSpc></a:pPr><a:r><a:t>Two.</a:t></a:r><a:pPr/><a:r><a:t>Three.</a:t></a:r><a:endParaRPr lang="en-US"/></a:p>"#
4899 );
4900
4901 paragraph.set_text("Whole.");
4902 assert_eq!(
4903 paragraph.to_xml().unwrap(),
4904 br#"<a:p><a:pPr lvl="1" algn="l"/><a:r><a:t>Whole.</a:t></a:r><a:pPr algn="r"><a:lnSpc><a:spcPct val="90000"/></a:lnSpc></a:pPr><a:pPr/><a:endParaRPr lang="en-US"/></a:p>"#
4905 );
4906
4907 let late = CT_TextParagraph::from_xml(
4908 br#"<a:p><a:r><a:t>One.</a:t></a:r><a:pPr algn="l"/><a:r><a:t>Two.</a:t></a:r><a:pPr algn="r"/></a:p>"#,
4909 )
4910 .unwrap();
4911 assert_eq!(
4912 late.properties.as_ref().unwrap().alignment,
4913 Some(TextAlignment::Left)
4914 );
4915 assert_eq!(
4916 late.to_xml().unwrap(),
4917 br#"<a:p><a:pPr algn="l"/><a:r><a:t>One.</a:t></a:r><a:r><a:t>Two.</a:t></a:r><a:pPr algn="r"/></a:p>"#
4918 );
4919 }
4920
4921 #[test]
4922 fn fx206_foreign_highlight_does_not_collide_with_owned_highlight() {
4923 let foreign =
4924 br#"<x:highlight xmlns:x="urn:producer"><x:payload code="kept"/></x:highlight>"#;
4925 let xml = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><x:highlight xmlns:x="urn:producer"><x:payload code="kept"/></x:highlight><a:highlight><a:srgbClr val="FFFF00"/></a:highlight><a:latin typeface="Arial"/></a:rPr>"#;
4926 let properties = CT_TextCharacterProperties::from_xml(xml)
4927 .expect("foreign extension and owned highlight are different expanded names");
4928 let mut writer = quick_xml::Writer::new(Vec::new());
4929 properties.write_xml(&mut writer, "a:rPr").unwrap();
4930 let written = writer.into_inner();
4931 assert!(written.windows(foreign.len()).any(|part| part == foreign));
4932 assert_eq!(
4933 String::from_utf8_lossy(&written)
4934 .matches("<a:highlight>")
4935 .count(),
4936 1
4937 );
4938 assert!(CT_TextCharacterProperties::from_xml(&written).is_ok());
4939 }
4940
4941 #[test]
4942 fn fx206_typed_highlight_qualified_colour_preserves_opaque_and_source_order() {
4943 let xml = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><x:before/><q:highlight x:owner="keep"><x:beforeColour/><q:srgbClr val="FFFF00" xmlns:y="urn:colour"><x:alpha val="12345"/><q:alpha val="50000"></q:alpha><y:keep token="a&b"/></q:srgbClr><x:afterColour/></q:highlight><x:after/><a:latin typeface="Carlito"/></a:rPr>"#;
4944 let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
4945 let colour = properties.highlight.as_ref().unwrap().clone();
4946 assert_eq!(
4947 colour.transforms().len(),
4948 1,
4949 "foreign alpha is not an owned colour transform"
4950 );
4951 assert!(!colour.raw_children().is_empty());
4952 let mut writer = Writer::new(Vec::new());
4953 properties.write_xml(&mut writer, "a:rPr").unwrap();
4954 assert_eq!(writer.into_inner(), xml);
4955 let before = properties.clone();
4956 assert!(properties.set_highlight(None).is_err());
4957 assert_eq!(properties, before, "opaque clear refuses before mutation");
4958 let mut replacement = colour.clone();
4959 if let ColorChoice::Srgb { value, .. } = &mut replacement {
4960 *value = crate::color::RgbColor::new(0, 255, 255);
4961 } else {
4962 panic!("fixture selected exact srgb colour");
4963 }
4964 properties.set_highlight(Some(replacement)).unwrap();
4965 let mut writer = Writer::new(Vec::new());
4966 properties.write_xml(&mut writer, "a:rPr").unwrap();
4967 let updated = writer.into_inner();
4968 let text = std::str::from_utf8(&updated).unwrap();
4969 assert!(text.contains("<x:alpha val=\"12345\"/>"));
4970 assert!(text.contains("<y:keep token=\"a&b\"/>"));
4971 assert!(text.contains("xmlns:y=\"urn:colour\""));
4972 assert!(text.find("<x:beforeColour/>").unwrap() < text.find("val=\"00FFFF\"").unwrap());
4973 assert!(text.find("<x:afterColour/>").unwrap() < text.find("<x:after/>").unwrap());
4974 let reopened = CT_TextCharacterProperties::from_xml(&updated).unwrap();
4975 assert_eq!(reopened.highlight, properties.highlight);
4976 let mut again = Writer::new(Vec::new());
4977 reopened.write_xml(&mut again, "a:rPr").unwrap();
4978 assert_eq!(again.into_inner(), updated);
4979 }
4980
4981 #[test]
4982 fn fx206_highlight_colour_opaque_attributes_refuse_edit_before_publication() {
4983 let xml = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><a:highlight><a:srgbClr val="FFFF00" x:token="raw&value"/></a:highlight></a:rPr>"#;
4984 let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
4985 let before = properties.clone();
4986 assert!(
4987 properties
4988 .set_highlight(Some(ColorChoice::srgb(crate::color::RgbColor::new(
4989 0, 255, 255
4990 ))))
4991 .is_err()
4992 );
4993 assert_eq!(properties, before);
4994 properties.highlight = Some(ColorChoice::srgb(crate::color::RgbColor::new(0, 255, 255)));
4996 let mut writer = Writer::new(Vec::new());
4997 assert!(properties.write_xml(&mut writer, "a:rPr").is_err());
4998 assert!(writer.into_inner().is_empty());
4999 }
5000
5001 #[test]
5002 fn fx206_highlight_transform_opaque_attributes_refuse_destructive_edits() {
5003 for transform in [
5004 r#"<q:alpha val="50000" x:keep="A"/>"#,
5005 r#"<q:alpha val="50000" x:keep="A"> </q:alpha>"#,
5006 ] {
5007 let xml = format!(
5008 r#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><q:highlight><q:srgbClr val="FFFF00">{transform}</q:srgbClr></q:highlight></a:rPr>"#
5009 );
5010 let mut properties = CT_TextCharacterProperties::from_xml(xml.as_bytes()).unwrap();
5011 assert_eq!(properties.highlight.as_ref().unwrap().transforms().len(), 1);
5012 let before = properties.clone();
5013 let mut writer = Writer::new(Vec::new());
5014 properties.write_xml(&mut writer, "a:rPr").unwrap();
5015 assert_eq!(writer.into_inner(), xml.as_bytes());
5016 let mut changed = properties.highlight.clone().unwrap();
5017 if let ColorChoice::Srgb { value, .. } = &mut changed {
5018 *value = crate::color::RgbColor::new(0, 255, 255);
5019 } else {
5020 panic!("fixture must select RGB");
5021 }
5022 for supplied in [Some(changed), None] {
5023 assert!(properties.set_highlight(supplied.clone()).is_err());
5024 assert_eq!(properties, before);
5025 let mut direct = properties.clone();
5026 direct.highlight = supplied;
5027 let mut rejected = Writer::new(Vec::new());
5028 assert!(direct.write_xml(&mut rejected, "a:rPr").is_err());
5029 assert!(rejected.into_inner().is_empty());
5030 }
5031 let mut writer = Writer::new(Vec::new());
5033 properties.write_xml(&mut writer, "a:rPr").unwrap();
5034 let bytes = writer.into_inner();
5035 assert_eq!(bytes, xml.as_bytes());
5036 let reopened = CT_TextCharacterProperties::from_xml(&bytes).unwrap();
5037 assert_eq!(reopened.highlight, before.highlight);
5038 let mut again = Writer::new(Vec::new());
5039 reopened.write_xml(&mut again, "a:rPr").unwrap();
5040 assert_eq!(again.into_inner(), bytes);
5041 }
5042 let xml = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:producer"><a:highlight><a:srgbClr val="FFFF00"><x:alpha val="50000" x:keep="A"/></a:srgbClr></a:highlight></a:rPr>"#;
5044 let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
5045 let mut changed = properties.highlight.clone().unwrap();
5046 assert!(changed.transforms().is_empty());
5047 if let ColorChoice::Srgb { value, .. } = &mut changed {
5048 *value = crate::color::RgbColor::new(0, 255, 255);
5049 }
5050 properties.set_highlight(Some(changed)).unwrap();
5051 let mut writer = Writer::new(Vec::new());
5052 properties.write_xml(&mut writer, "a:rPr").unwrap();
5053 let updated = writer.into_inner();
5054 assert!(
5055 std::str::from_utf8(&updated)
5056 .unwrap()
5057 .contains(r#"<x:alpha val="50000" x:keep="A"/>"#)
5058 );
5059 let reopened = CT_TextCharacterProperties::from_xml(&updated).unwrap();
5060 assert_eq!(reopened.highlight, properties.highlight);
5061 }
5062
5063 #[test]
5064 fn fx206_duplicate_owned_highlight_is_refused_control() {
5065 let xml = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:highlight><a:srgbClr val="FFFF00"/></a:highlight><a:highlight><a:srgbClr val="00FFFF"/></a:highlight></a:rPr>"#;
5066 assert!(CT_TextCharacterProperties::from_xml(xml).is_err());
5067 }
5068
5069 #[test]
5070 fn fx206_known_small_cap_is_owned_while_unknown_cap_remains_raw() {
5071 let small =
5072 CT_TextCharacterProperties::from_xml(br#"<a:rPr cap="small" lang="en-US"/>"#).unwrap();
5073 let unknown =
5074 CT_TextCharacterProperties::from_xml(br#"<a:rPr cap="producer" lang="en-US"/>"#)
5075 .unwrap();
5076 assert_eq!(
5077 small.all_caps, None,
5078 "legacy all-caps getter for small stays unset"
5079 );
5080 assert!(
5081 unknown
5082 .raw_attributes
5083 .iter()
5084 .any(|(name, value)| name == "cap" && value == "producer")
5085 );
5086 assert!(
5087 !small.raw_attributes.iter().any(|(name, _)| name == "cap"),
5088 "schema small-cap mode must have typed ownership, not an opaque attribute"
5089 );
5090 let mut writer = quick_xml::Writer::new(Vec::new());
5091 small.write_xml(&mut writer, "a:rPr").unwrap();
5092 assert_eq!(writer.into_inner(), br#"<a:rPr cap="small" lang="en-US"/>"#);
5093 }
5094
5095 #[test]
5096 fn fx206_conflicting_caps_refuse_before_serialized_output() {
5097 let properties = CT_TextCharacterProperties {
5098 all_caps: Some(true),
5099 small_caps: Some(true),
5100 ..Default::default()
5101 };
5102 let mut writer = quick_xml::Writer::new(Vec::new());
5103 assert!(properties.write_xml(&mut writer, "a:rPr").is_err());
5104 assert!(writer.into_inner().is_empty());
5105 }
5106
5107 #[test]
5108 fn fx206_checked_cap_updates_select_explicit_modes_and_preserve_other_on_none() {
5109 for initial in ["all", "small", "none"] {
5110 for update_all in [true, false] {
5111 for value in [None, Some(false), Some(true)] {
5112 let input = format!(r#"<a:rPr cap="{initial}"/>"#);
5113 let mut properties =
5114 CT_TextCharacterProperties::from_xml(input.as_bytes()).unwrap();
5115 if update_all {
5116 properties.set_all_caps(value);
5117 } else {
5118 properties.set_small_caps(value);
5119 }
5120 let expected = match value {
5121 Some(false) => Some("none"),
5122 Some(true) if update_all => Some("all"),
5123 Some(true) => Some("small"),
5124 None if update_all && initial == "small" => Some("small"),
5125 None if !update_all && initial == "all" => Some("all"),
5126 None if initial == "none" => Some("none"),
5127 None => None,
5128 };
5129 let mut writer = quick_xml::Writer::new(Vec::new());
5130 properties.write_xml(&mut writer, "a:rPr").unwrap();
5131 let written = writer.into_inner();
5132 let expected_xml = expected.map_or_else(
5133 || "<a:rPr/>".to_owned(),
5134 |mode| format!(r#"<a:rPr cap="{mode}"/>"#),
5135 );
5136 assert_eq!(
5137 written,
5138 expected_xml.as_bytes(),
5139 "initial={initial}, update_all={update_all}, value={value:?}"
5140 );
5141 let reparsed = CT_TextCharacterProperties::from_xml(&written).unwrap();
5142 let mut reopened_writer = quick_xml::Writer::new(Vec::new());
5143 reparsed.write_xml(&mut reopened_writer, "a:rPr").unwrap();
5144 assert_eq!(reopened_writer.into_inner(), written);
5145 let (requested, other) = if update_all {
5146 (properties.all_caps, properties.small_caps)
5147 } else {
5148 (properties.small_caps, properties.all_caps)
5149 };
5150 assert_eq!(requested, value);
5151 if value.is_some() {
5152 assert_eq!(other, None, "explicit mode must clear the other flag");
5153 }
5154 assert!(
5155 !(properties.all_caps == Some(true) && properties.small_caps == Some(true))
5156 );
5157 }
5158 }
5159 }
5160 }
5161
5162 #[test]
5163 fn fx206_ancestor_highlight_scope_survives_typed_edit_and_foreign_shadow() {
5164 let source = br#"<a:r xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:f="urn:foreign"><a:rPr><q:highlight xmlns:q='urn:foreign'><f:payload code='A'/></q:highlight><q:highlight><q:srgbClr val='FF0000'><f:extension code='B'/></q:srgbClr></q:highlight></a:rPr><a:t>x</a:t></a:r>"#;
5165 let mut run = super::CT_RegularTextRun::from_xml(source).unwrap();
5166 let colour = run
5167 .properties
5168 .as_ref()
5169 .unwrap()
5170 .highlight
5171 .as_ref()
5172 .expect("ancestor-owned highlight must be typed, not opaque")
5173 .clone();
5174 let ColorChoice::Srgb { value, .. } = &colour else {
5175 panic!("source owner must resolve srgb");
5176 };
5177 assert_eq!(*value, crate::color::RgbColor::new(255, 0, 0));
5178 let save = |run: &super::CT_RegularTextRun| {
5179 let mut writer = quick_xml::Writer::new(Vec::new());
5180 run.write_xml(&mut writer).unwrap();
5181 writer.into_inner()
5182 };
5183 let first = save(&run);
5184 assert_eq!(
5185 save(&super::CT_RegularTextRun::from_xml(&first).unwrap()),
5186 first
5187 );
5188 let mut replacement = colour.clone();
5189 let ColorChoice::Srgb { value, .. } = &mut replacement else {
5190 unreachable!();
5191 };
5192 *value = crate::color::RgbColor::new(0, 255, 255);
5193 run.properties
5194 .as_mut()
5195 .unwrap()
5196 .set_highlight(Some(replacement))
5197 .unwrap();
5198 let changed = save(&run);
5199 for raw in [
5200 b"<q:highlight xmlns:q='urn:foreign'><f:payload code='A'/></q:highlight>"
5201 .as_slice(),
5202 b"<f:extension code='B'/>".as_slice(),
5203 ] {
5204 assert!(changed.windows(raw.len()).any(|part| part == raw));
5205 }
5206 let reopened = super::CT_RegularTextRun::from_xml(&changed).unwrap();
5207 let ColorChoice::Srgb { value, .. } = reopened
5208 .properties
5209 .as_ref()
5210 .unwrap()
5211 .highlight
5212 .as_ref()
5213 .unwrap()
5214 else {
5215 panic!("saved owner must remain srgb");
5216 };
5217 assert_eq!(*value, crate::color::RgbColor::new(0, 255, 255));
5218 assert_eq!(save(&reopened), changed);
5219 let duplicate = br#"<a:r xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main"><a:rPr><q:highlight><q:srgbClr val="FF0000"/></q:highlight><q:highlight><q:srgbClr val="00FF00"/></q:highlight></a:rPr><a:t>x</a:t></a:r>"#;
5220 assert!(super::CT_RegularTextRun::from_xml(duplicate).is_err());
5221 }
5222
5223 #[test]
5224 fn fx206_highlight_admits_alias_and_default_namespace_without_foreign_collision() {
5225 for xml in [
5226 br#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main"><q:highlight><q:srgbClr val="FFFF00"/></q:highlight><x:highlight xmlns:x="urn:foreign"/></q:rPr>"#.as_slice(),
5227 br#"<rPr xmlns="http://schemas.openxmlformats.org/drawingml/2006/main"><highlight><srgbClr val="FFFF00"/></highlight><highlight xmlns="urn:foreign"/></rPr>"#.as_slice(),
5228 br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><q:highlight xmlns:q="urn:foreign"/><q:highlight xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main"><q:srgbClr val="FFFF00"/></q:highlight></a:rPr>"#.as_slice(),
5229 br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:highlight xmlns:a="urn:foreign"/><a:highlight><a:srgbClr val="FFFF00"/></a:highlight></a:rPr>"#.as_slice(),
5230 ] {
5231 let properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
5232 let mut writer = quick_xml::Writer::new(Vec::new());
5233 properties.write_xml(&mut writer, "a:rPr").unwrap();
5234 let written = writer.into_inner();
5235 assert!(String::from_utf8_lossy(&written).contains("urn:foreign"));
5236 assert!(String::from_utf8_lossy(&written).contains("FFFF00"));
5237 assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), properties);
5238 }
5239 }
5240
5241 #[test]
5242 fn character_all_and_small_caps_round_trip_as_distinct_modes() {
5243 let all = CT_TextCharacterProperties::from_xml(
5244 br#"<q:rPr cap="all" sz="1800" x:producer="kept"/>"#,
5245 )
5246 .unwrap();
5247 assert_eq!(all.all_caps, Some(true));
5248 let mut writer = quick_xml::Writer::new(Vec::new());
5249 all.write_xml(&mut writer, "a:rPr").unwrap();
5250 let written = writer.into_inner();
5251 assert_eq!(
5252 written,
5253 br#"<a:rPr sz="1800" cap="all" x:producer="kept"/>"#
5254 );
5255 assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), all);
5256
5257 let none = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="none"/>"#).unwrap();
5258 assert_eq!(none.all_caps, Some(false));
5259
5260 let small = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="small"/>"#).unwrap();
5261 assert_eq!(small.all_caps, None);
5262 assert_eq!(small.small_caps, Some(true));
5263 let mut writer = quick_xml::Writer::new(Vec::new());
5264 small.write_xml(&mut writer, "a:rPr").unwrap();
5265 assert_eq!(writer.into_inner(), br#"<a:rPr cap="small"/>"#);
5266 }
5267
5268 #[test]
5269 fn setting_all_caps_over_preserved_unknown_caps_writes_one_cap_attribute() {
5270 let mut properties =
5271 CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="producer" lang="en-US"/>"#)
5272 .unwrap();
5273 properties.all_caps = Some(true);
5274 let mut writer = quick_xml::Writer::new(Vec::new());
5275 properties.write_xml(&mut writer, "a:rPr").unwrap();
5276 let written = writer.into_inner();
5277 assert_eq!(written, br#"<a:rPr cap="all" lang="en-US"/>"#);
5278 assert_eq!(
5279 CT_TextCharacterProperties::from_xml(&written)
5280 .unwrap()
5281 .all_caps,
5282 Some(true)
5283 );
5284
5285 properties.all_caps = None;
5286 let mut writer = quick_xml::Writer::new(Vec::new());
5287 properties.write_xml(&mut writer, "a:rPr").unwrap();
5288 assert_eq!(
5289 writer.into_inner(),
5290 br#"<a:rPr cap="producer" lang="en-US"/>"#
5291 );
5292 }
5293
5294 #[test]
5295 fn spacing_percentages_above_two_lines_parse_up_to_the_schema_maximum() {
5296 let xml = br#"<q:pPr><q:lnSpc><q:spcPct val="250000"/></q:lnSpc><q:spcBef><q:spcPct val="13200000"/></q:spcBef></q:pPr>"#;
5297 let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
5298 assert_eq!(
5299 properties.line_spacing,
5300 Some(TextSpacing::Percent("250000".to_owned()))
5301 );
5302 assert_eq!(
5303 properties.space_before,
5304 Some(TextSpacing::Percent("13200000".to_owned()))
5305 );
5306 assert_eq!(
5307 properties.to_xml().unwrap(),
5308 br#"<a:pPr><a:lnSpc><a:spcPct val="250000"/></a:lnSpc><a:spcBef><a:spcPct val="13200000"/></a:spcBef></a:pPr>"#
5309 );
5310 }
5311
5312 #[test]
5313 fn setting_a_bullet_part_removes_the_preserved_alternative_it_replaces() {
5314 let mut properties = CT_TextParagraphProperties::from_xml(
5315 br#"<q:pPr><q:buClrTx/><q:buSzTx/><q:buFontTx/><q:buBlip><q:blip x:embed="rId9"/></q:buBlip><q:tabLst/></q:pPr>"#,
5316 )
5317 .unwrap();
5318 assert!(properties.has_picture_bullet());
5319
5320 properties.set_bullet(Some(TextBullet {
5321 choice: Some(TextBulletChoice::Character(
5322 TextBulletCharacter::new("*").unwrap(),
5323 )),
5324 ..TextBullet::default()
5325 }));
5326 assert!(!properties.has_picture_bullet());
5327 assert_eq!(
5328 properties.to_xml().unwrap(),
5329 br#"<a:pPr><q:buClrTx/><q:buSzTx/><q:buFontTx/><a:buChar char="*"/><q:tabLst/></a:pPr>"#
5330 );
5331
5332 properties.set_bullet(None);
5333 assert_eq!(
5334 properties.to_xml().unwrap(),
5335 br#"<a:pPr><q:tabLst/></a:pPr>"#
5336 );
5337 }
5338
5339 #[test]
5340 fn paragraph_and_run_properties_use_drawingml_units_and_schema_order() {
5341 let paragraph_properties = CT_TextParagraphProperties::from_xml(br#"<q:pPr marL="0" marR="51206400" lvl="8" indent="-51206400" algn="thaiDist" rtl="1" defTabSz="914400"><x:before/><q:lnSpc><q:spcPct val="100000"/></q:lnSpc><q:spcBef><q:spcPts val="158400"/></q:spcBef><q:spcAft><q:spcPct val="125.5%"/></q:spcAft><q:buChar char="*"/><x:afterBullet/><q:defRPr sz="400000" b="true" i="0" u="wavyDbl" strike="dblStrike" spc="-400000" baseline="25%" xmlns:b="urn:test" lang="en-US" kern="1200"><q:solidFill><q:srgbClr val="AABBCC"/></q:solidFill><q:latin typeface="Aptos" pitchFamily="34" charset="0"/><q:ea typeface="Yu Gothic"/><q:cs typeface="Arial"/><q:sym typeface="Symbol"/><q:hlinkMouseOver r:id="rId8" tgtFrame="_blank" invalidUrl="bad"/></q:defRPr><x:last/></q:pPr>"#).unwrap();
5342 let written = {
5343 let mut writer = quick_xml::Writer::new(Vec::new());
5344 paragraph_properties
5345 .write_xml(&mut writer, "a:pPr")
5346 .unwrap();
5347 writer.into_inner()
5348 };
5349 assert_eq!(written, br#"<a:pPr marL="0" marR="51206400" lvl="8" indent="-51206400" algn="thaiDist" rtl="1" defTabSz="914400"><x:before/><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:spcBef><a:spcPts val="158400"/></a:spcBef><a:spcAft><a:spcPct val="125.5%"/></a:spcAft><a:buChar char="*"/><x:afterBullet/><a:defRPr sz="400000" b="1" i="0" u="wavyDbl" strike="dblStrike" spc="-400000" baseline="25%" xmlns:b="urn:test" lang="en-US" kern="1200"><a:solidFill><a:srgbClr val="AABBCC"/></a:solidFill><a:latin typeface="Aptos" pitchFamily="34" charset="0"/><a:ea typeface="Yu Gothic"/><a:cs typeface="Arial"/><a:sym typeface="Symbol"/><a:hlinkMouseOver r:id="rId8" invalidUrl="bad" tgtFrame="_blank"/></a:defRPr><x:last/></a:pPr>"#);
5350
5351 let run_properties = CT_TextCharacterProperties::from_xml(
5352 br#"<q:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#,
5353 )
5354 .unwrap();
5355 let mut writer = quick_xml::Writer::new(Vec::new());
5356 run_properties.write_xml(&mut writer, "a:rPr").unwrap();
5357 assert_eq!(
5358 writer.into_inner(),
5359 br#"<a:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#
5360 );
5361
5362 let mut inserted_spacing =
5363 CT_TextParagraphProperties::from_xml(br#"<q:pPr><q:buChar char="*"/></q:pPr>"#)
5364 .unwrap();
5365 inserted_spacing.line_spacing = Some(TextSpacing::Percent("100000".to_owned()));
5366 let mut writer = quick_xml::Writer::new(Vec::new());
5367 inserted_spacing.write_xml(&mut writer, "a:pPr").unwrap();
5368 assert_eq!(
5369 writer.into_inner(),
5370 br#"<a:pPr><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:buChar char="*"/></a:pPr>"#
5371 );
5372 }
5373
5374 #[test]
5375 fn every_modelled_bullet_form_round_trips_in_schema_order() {
5376 let cases: &[(&[u8], &[u8])] = &[
5377 (
5378 br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/></q:buClr><q:buSzPct val="125000"/><q:buFont typeface="Wingdings"/><q:buChar char="*"/></q:pPr>"#,
5379 br#"<a:pPr><a:buClr><a:srgbClr val="AABBCC"/></a:buClr><a:buSzPct val="125000"/><a:buFont typeface="Wingdings"/><a:buChar char="*"/></a:pPr>"#,
5380 ),
5381 (
5382 br#"<q:pPr><q:buSzPts val="1800"/><q:buAutoNum type="arabicPeriod" startAt="3"/></q:pPr>"#,
5383 br#"<a:pPr><a:buSzPts val="1800"/><a:buAutoNum type="arabicPeriod" startAt="3"/></a:pPr>"#,
5384 ),
5385 (br#"<q:pPr><q:buNone/></q:pPr>"#, br#"<a:pPr><a:buNone/></a:pPr>"#),
5386 ];
5387
5388 for (xml, expected) in cases {
5389 let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
5390 let mut writer = quick_xml::Writer::new(Vec::new());
5391 properties.write_xml(&mut writer, "a:pPr").unwrap();
5392 let written = writer.into_inner();
5393 assert_eq!(written, *expected);
5394 assert_eq!(
5395 CT_TextParagraphProperties::from_xml(&written).unwrap(),
5396 properties
5397 );
5398 }
5399
5400 let character = CT_TextParagraphProperties::from_xml(cases[0].0).unwrap();
5401 let bullet = character.bullet.as_ref().unwrap();
5402 assert!(matches!(
5403 bullet.choice,
5404 Some(TextBulletChoice::Character(_))
5405 ));
5406 assert_eq!(bullet.font.as_ref().unwrap().typeface, "Wingdings");
5407 assert!(matches!(
5408 bullet.size.as_ref().unwrap().value,
5409 TextBulletSizeValue::Percent(ref value) if value == "125000"
5410 ));
5411 assert!(matches!(
5412 bullet.color.as_ref().unwrap().color,
5413 ColorChoice::Srgb { .. }
5414 ));
5415
5416 let automatic = CT_TextParagraphProperties::from_xml(cases[1].0).unwrap();
5417 let Some(TextBulletChoice::AutoNumber(numbering)) =
5418 automatic.bullet.as_ref().unwrap().choice.as_ref()
5419 else {
5420 panic!("expected automatic-number bullet");
5421 };
5422 assert_eq!(numbering.scheme, TextAutoNumberScheme::ArabicPeriod);
5423 assert_eq!(numbering.start_at, Some(3));
5424 assert!(matches!(
5425 automatic
5426 .bullet
5427 .as_ref()
5428 .unwrap()
5429 .size
5430 .as_ref()
5431 .unwrap()
5432 .value,
5433 TextBulletSizeValue::Points(1800)
5434 ));
5435
5436 let none = CT_TextParagraphProperties::from_xml(cases[2].0).unwrap();
5437 assert!(matches!(
5438 none.bullet.as_ref().unwrap().choice,
5439 Some(TextBulletChoice::None(_))
5440 ));
5441 }
5442
5443 #[test]
5444 fn bullet_font_size_and_colour_keep_their_schema_positions() {
5445 let properties = CT_TextParagraphProperties::from_xml(
5446 br#"<q:pPr><q:buFont typeface="Aptos"/><q:buChar char="*"/><q:buClr><q:schemeClr val="accent1"/></q:buClr><q:buSzPct val="80%"/><q:defRPr b="1"/></q:pPr>"#,
5447 )
5448 .unwrap();
5449 let mut writer = quick_xml::Writer::new(Vec::new());
5450 properties.write_xml(&mut writer, "a:pPr").unwrap();
5451 assert_eq!(
5452 writer.into_inner(),
5453 br#"<a:pPr><a:buClr><a:schemeClr val="accent1"/></a:buClr><a:buSzPct val="80%"/><a:buFont typeface="Aptos"/><a:buChar char="*"/><a:defRPr b="1"/></a:pPr>"#
5454 );
5455 }
5456
5457 #[test]
5458 fn unknown_bullet_children_round_trip_byte_for_byte() {
5459 let properties = CT_TextParagraphProperties::from_xml(
5460 br#"<q:pPr><q:buClrTx x:mode="stay"><x:nested>one & two</x:nested><!--note--></q:buClrTx><q:buChar char="*"/><x:after/></q:pPr>"#,
5461 )
5462 .unwrap();
5463 let mut writer = quick_xml::Writer::new(Vec::new());
5464 properties.write_xml(&mut writer, "a:pPr").unwrap();
5465 assert_eq!(
5466 writer.into_inner(),
5467 br#"<a:pPr><q:buClrTx x:mode="stay"><x:nested>one & two</x:nested><!--note--></q:buClrTx><a:buChar char="*"/><x:after/></a:pPr>"#
5468 );
5469 }
5470
5471 #[test]
5472 fn malformed_bullet_values_return_errors_without_panicking() {
5473 let cases: &[&[u8]] = &[
5474 br#"<q:pPr><q:buChar/></q:pPr>"#,
5475 br#"<q:pPr><q:buChar char=""/></q:pPr>"#,
5476 br#"<q:pPr><q:buSzPct val="24999"/></q:pPr>"#,
5477 br#"<q:pPr><q:buSzPct val="400001"/></q:pPr>"#,
5478 br#"<q:pPr><q:buSzPts val="99"/></q:pPr>"#,
5479 br#"<q:pPr><q:buSzPts val="400001"/></q:pPr>"#,
5480 br#"<q:pPr><q:buAutoNum type="unknown"/></q:pPr>"#,
5481 br#"<q:pPr><q:buAutoNum/></q:pPr>"#,
5482 br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="0"/></q:pPr>"#,
5483 br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="32768"/></q:pPr>"#,
5484 br#"<q:pPr><q:buFont/></q:pPr>"#,
5485 br#"<q:pPr><q:buClr/></q:pPr>"#,
5486 br#"<q:pPr><q:buClr><q:srgbClr x:val="AABBCC"/></q:buClr></q:pPr>"#,
5487 br#"<q:pPr><q:buClr><q:srgbClr val="GGGGGG"/></q:buClr></q:pPr>"#,
5488 br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/><q:schemeClr val="accent1"/></q:buClr></q:pPr>"#,
5489 br#"<q:pPr><q:buChar char="*"/><q:buNone/></q:pPr>"#,
5490 br#"<q:pPr><q:buSzPct val="100000"/><q:buSzPts val="1200"/></q:pPr>"#,
5491 ];
5492
5493 for xml in cases {
5494 let result = panic::catch_unwind(|| CT_TextParagraphProperties::from_xml(xml));
5495 assert!(result.is_ok(), "bullet parser panicked");
5496 assert!(result.unwrap().is_err(), "malformed bullet parsed");
5497 }
5498 }
5499
5500 #[test]
5501 fn malformed_text_content_returns_errors_without_panicking() {
5502 let cases: &[&[u8]] = &[
5503 br#"<q:r/>"#,
5504 br#"<q:r><q:t><x:nested/></q:t></q:r>"#,
5505 br#"<q:r><q:t xml:space="keep">x</q:t></q:r>"#,
5506 br#"<q:r><q:t>x</q:t><q:t>y</q:t></q:r>"#,
5507 br#"<q:pPr lvl="9"/>"#,
5508 br#"<q:pPr marL="-1"/>"#,
5509 br#"<q:pPr indent="51206401"/>"#,
5510 br#"<q:pPr algn="middle"/>"#,
5511 br#"<q:pPr><q:lnSpc/></q:pPr>"#,
5512 br#"<q:pPr><q:lnSpc><q:spcPts val="158401"/></q:lnSpc></q:pPr>"#,
5513 br#"<q:pPr><q:lnSpc><q:spcPct val="13200001"/></q:lnSpc></q:pPr>"#,
5514 br#"<q:rPr sz="99"/>"#,
5515 br#"<q:rPr b="yes"/>"#,
5516 br#"<q:rPr u="triple"/>"#,
5517 br#"<q:rPr spc="400001"/>"#,
5518 r#"<q:rPr spc="éx"/>"#.as_bytes(),
5519 br#"<q:rPr baseline="100001"/>"#,
5520 br#"<q:rPr><q:latin/></q:rPr>"#,
5521 br#"<q:fld type="slidenum"/>"#,
5522 br#"<q:fld id="not-a-guid"/>"#,
5523 br#"<q:p><q:endParaRPr/><q:endParaRPr/></q:p>"#,
5524 br#"<q:notPPr/>"#,
5525 br#"<q:notRPr/>"#,
5526 ];
5527 for xml in cases {
5528 let result = panic::catch_unwind(|| match super::root_local_name(xml).as_deref() {
5529 Ok(b"r") => super::CT_RegularTextRun::from_xml(xml).map(|_| ()),
5530 Ok(b"pPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
5531 Ok(b"rPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
5532 Ok(b"fld") => super::CT_TextField::from_xml(xml).map(|_| ()),
5533 Ok(b"p") => CT_TextParagraph::from_xml(xml).map(|_| ()),
5534 Ok(b"notPPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
5535 Ok(b"notRPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
5536 _ => unreachable!(),
5537 });
5538 assert!(result.is_ok(), "text parser panicked");
5539 assert!(result.unwrap().is_err(), "malformed text parsed");
5540 }
5541 }
5542
5543 #[test]
5544 fn characters_xml_cannot_carry_are_stored_escaped_and_never_decoded() {
5545 let mut paragraph = CT_TextParagraph::default();
5546 paragraph.add_run("a\u{1}b\u{b}c\u{1f}d\u{fffe}e\u{0}");
5547 let TextRun::Run(run) = ¶graph.runs[0] else {
5548 panic!("expected a run");
5549 };
5550 assert_eq!(
5551 run.text.value, "a_x0001_b_x000B_c_x001F_d_xFFFE_e_x0000_",
5552 "the setter stores the escaped form, as python-pptx does"
5553 );
5554 let written = paragraph.to_xml().unwrap();
5555 assert_eq!(
5556 written,
5557 br#"<a:p><a:r><a:t>a_x0001_b_x000B_c_x001F_d_xFFFE_e_x0000_</a:t></a:r></a:p>"#
5558 );
5559 let declared = [
5560 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main""#.as_slice(),
5561 &written[4..],
5562 ]
5563 .concat();
5564 oxml_core::xml::validate_strict_xml_1_0(&declared)
5565 .expect("the written paragraph is XML 1.0");
5566 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
5567
5568 let xml = br#"<a:p><a:r><a:t>C_x0041_D bell_x0007_ _x000b_ _x0001</a:t></a:r></a:p>"#;
5571 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
5572 let TextRun::Run(run) = ¶graph.runs[0] else {
5573 panic!("expected a run");
5574 };
5575 assert_eq!(run.text.value, "C_x0041_D bell_x0007_ _x000b_ _x0001");
5576 assert_eq!(paragraph.to_xml().unwrap(), xml);
5577 }
5578
5579 #[test]
5580 fn a_vertical_tab_set_as_paragraph_text_becomes_a_line_break() {
5581 let mut paragraph = CT_TextParagraph::from_xml(
5582 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:x"><a:r><a:rPr b="1"/><a:t>old</a:t></a:r><x:after/><a:endParaRPr sz="1200"/><x:end/></a:p>"#,
5583 )
5584 .unwrap();
5585 paragraph.set_text("one\u{b}two\u{1}\u{b}");
5586 assert_eq!(
5587 String::from_utf8(paragraph.to_xml().unwrap()).unwrap(),
5588 r#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:x"><a:r><a:rPr b="1"/><a:t>one</a:t></a:r><a:br><a:rPr b="1"/></a:br><a:r><a:rPr b="1"/><a:t>two_x0001_</a:t></a:r><a:br><a:rPr b="1"/></a:br><x:after/><a:endParaRPr sz="1200"/><x:end/></a:p>"#
5589 );
5590
5591 let saved = paragraph.to_xml().unwrap();
5592 assert_eq!(
5593 CT_TextParagraph::from_xml(&saved)
5594 .unwrap()
5595 .to_xml()
5596 .unwrap(),
5597 saved
5598 );
5599
5600 let mut plain = CT_TextParagraph::default();
5601 plain.set_text("\u{b}");
5602 assert_eq!(
5603 plain.to_xml().unwrap(),
5604 br#"<a:p><a:r><a:t/></a:r><a:br/></a:p>"#
5605 );
5606 }
5607 fn fx206_link_saved(properties: &CT_TextCharacterProperties) -> Vec<u8> {
5608 let mut writer = Writer::new(Vec::new());
5609 properties.write_xml(&mut writer, "a:rPr").unwrap();
5610 writer.into_inner()
5611 }
5612
5613 #[test]
5614 fn fx206_run_link_foreign_owner_and_qualified_relationship_alias_base() {
5615 let positive = CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:hlinkClick r:id="rId1"/></a:rPr>"#).unwrap();
5616 assert_eq!(
5617 positive
5618 .hyperlink_click
5619 .as_ref()
5620 .unwrap()
5621 .relationship_id
5622 .as_deref(),
5623 Some("rId1")
5624 );
5625 assert!(!fx206_link_saved(&positive).is_empty());
5626 let input = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:f="urn:foreign"><f:hlinkClick rel:id="foreign"><f:opaque/></f:hlinkClick><a:hlinkClick rel:id="owned" action="ppaction://hlinksldjump" f:keep='A'><f:payload/></a:hlinkClick></a:rPr>"#;
5627 let result = CT_TextCharacterProperties::from_xml(input);
5628 assert!(
5629 result.is_ok(),
5630 "foreign hlinkClick must not consume the owned schema slot: {result:?}"
5631 );
5632 let mut properties = result.unwrap();
5633 assert_eq!(
5634 properties
5635 .hyperlink_click
5636 .as_ref()
5637 .unwrap()
5638 .relationship_id
5639 .as_deref(),
5640 Some("owned")
5641 );
5642 let unchanged = fx206_link_saved(&properties);
5643 assert!(
5644 std::str::from_utf8(&unchanged)
5645 .unwrap()
5646 .contains(r#"<f:hlinkClick rel:id="foreign"><f:opaque/></f:hlinkClick>"#)
5647 );
5648 assert!(
5649 std::str::from_utf8(&unchanged)
5650 .unwrap()
5651 .contains("f:keep='A'")
5652 );
5653 properties.hyperlink_click.as_mut().unwrap().relationship_id = Some("next".to_owned());
5654 let changed = fx206_link_saved(&properties);
5655 let output = std::str::from_utf8(&changed).unwrap();
5656 assert!(output.contains(r#"<f:hlinkClick rel:id="foreign"><f:opaque/></f:hlinkClick>"#));
5657 assert!(output.contains("f:keep='A'"));
5658 assert!(output.contains("<f:payload/>"));
5659 let reopened = CT_TextCharacterProperties::from_xml(&changed).unwrap();
5660 assert_eq!(
5661 reopened
5662 .hyperlink_click
5663 .as_ref()
5664 .unwrap()
5665 .relationship_id
5666 .as_deref(),
5667 Some("next")
5668 );
5669 assert_eq!(fx206_link_saved(&reopened), changed);
5670 }
5671
5672 #[test]
5673 fn fx206_run_link_alias_and_local_rebound_id_are_distinct_base() {
5674 let positive = CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:hlinkClick r:id="canonical"/></a:rPr>"#).unwrap();
5675 assert_eq!(
5676 positive
5677 .hyperlink_click
5678 .as_ref()
5679 .unwrap()
5680 .relationship_id
5681 .as_deref(),
5682 Some("canonical"),
5683 "independent existing canonical relationship positive setup"
5684 );
5685 assert!(!fx206_link_saved(&positive).is_empty());
5686 let alias = CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:hlinkClick rel:id="owned"/></a:rPr>"#).unwrap();
5687 assert!(
5688 alias.hyperlink_click.is_some(),
5689 "owned click exists before deciding alias assertion"
5690 );
5691 assert_eq!(
5692 alias
5693 .hyperlink_click
5694 .as_ref()
5695 .unwrap()
5696 .relationship_id
5697 .as_deref(),
5698 Some("owned"),
5699 "deciding relationship-URI alias interpretation"
5700 );
5701 let foreign = CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><a:hlinkClick xmlns:r="urn:foreign" r:id="opaque"/></a:rPr>"#).unwrap();
5702 assert_eq!(
5703 foreign.hyperlink_click.as_ref().unwrap().relationship_id,
5704 None
5705 );
5706 assert!(
5707 std::str::from_utf8(&fx206_link_saved(&foreign))
5708 .unwrap()
5709 .contains(r#"r:id="opaque""#)
5710 );
5711 }
5712
5713 #[test]
5714 fn fx206_link_uri_owned_duplicates_refuse_but_foreign_shadow_preserves() {
5715 let namespaces = r#"xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:f="urn:foreign""#;
5716 for children in [
5717 r#"<a:hlinkClick r:id="one" rel:id="two"/>"#,
5718 r#"<a:hlinkClick r:id="one"/><a:hlinkClick rel:id="two"/>"#,
5719 ] {
5720 let xml = format!("<a:rPr {namespaces}>{children}</a:rPr>");
5721 assert!(CT_TextCharacterProperties::from_xml(xml.as_bytes()).is_err());
5722 }
5723 let xml = format!(
5724 r#"<a:rPr {namespaces}><!-- lexical --><f:hlinkClick rel:id='opaque'><a:hlinkClick r:id='shadow'/></f:hlinkClick><a:hlinkClick r:id='owned'/></a:rPr>"#
5725 );
5726 let parsed = CT_TextCharacterProperties::from_xml(xml.as_bytes()).unwrap();
5727 assert_eq!(
5728 parsed
5729 .hyperlink_click
5730 .as_ref()
5731 .unwrap()
5732 .relationship_id
5733 .as_deref(),
5734 Some("owned")
5735 );
5736 let saved = fx206_link_saved(&parsed);
5737 let text = std::str::from_utf8(&saved).unwrap();
5738 assert!(text.contains("<!-- lexical -->"));
5739 assert!(text.contains(
5740 "<f:hlinkClick rel:id='opaque'><a:hlinkClick r:id='shadow'/></f:hlinkClick>"
5741 ));
5742 assert_eq!(
5743 fx206_link_saved(&CT_TextCharacterProperties::from_xml(&saved).unwrap()),
5744 saved
5745 );
5746 }
5747
5748 #[test]
5749 fn fx206_owned_alias_root_retains_foreign_a_and_closes_new_click_descendant() {
5750 let xml = br#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:a='urn:foreign' xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:f="urn:payload" f:keep='A'><q:latin typeface='Arial' f:font='B'/><a:hlinkClick rel:id='foreign'><a:payload f:keep='C'/></a:hlinkClick><q:hlinkClick rel:id='owned' tooltip='D'><a:payload f:keep='E'/></q:hlinkClick></q:rPr>"#;
5751 assert!(CT_TextCharacterProperties::from_xml(br#"<q:rPr xmlns:q="urn:foreign" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>"#).is_err());
5752 assert!(
5753 CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="urn:foreign"/>"#).is_err()
5754 );
5755 let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
5756 assert_eq!(
5757 properties
5758 .hyperlink_click
5759 .as_ref()
5760 .unwrap()
5761 .relationship_id
5762 .as_deref(),
5763 Some("owned")
5764 );
5765 properties.hyperlink_click.as_mut().unwrap().relationship_id =
5766 Some("retargeted".to_owned());
5767 properties.hyperlink_mouse_over = Some(TextHyperlink {
5768 relationship_id: Some("hover".to_owned()),
5769 ..TextHyperlink::default()
5770 });
5771 let saved = fx206_link_saved(&properties);
5772 let text = std::str::from_utf8(&saved).unwrap();
5773 assert!(text.starts_with("<q:rPr"));
5774 for token in [
5775 "xmlns:a='urn:foreign'",
5776 "f:keep='A'",
5777 "<q:latin typeface='Arial' f:font='B'/>",
5778 "<a:hlinkClick rel:id='foreign'><a:payload f:keep='C'/></a:hlinkClick>",
5779 "tooltip='D'",
5780 "<a:payload f:keep='E'/>",
5781 ] {
5782 assert!(text.contains(token), "{token}");
5783 }
5784 assert!(text.contains("<a:hlinkMouseOver"));
5785 assert!(text.contains("xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\""));
5786 let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
5787 assert_eq!(
5788 reopened
5789 .hyperlink_click
5790 .as_ref()
5791 .unwrap()
5792 .relationship_id
5793 .as_deref(),
5794 Some("retargeted")
5795 );
5796 assert_eq!(
5797 reopened
5798 .hyperlink_mouse_over
5799 .as_ref()
5800 .unwrap()
5801 .relationship_id
5802 .as_deref(),
5803 Some("hover")
5804 );
5805 assert_eq!(fx206_link_saved(&reopened), saved);
5806 let mut provenance_only = properties.clone();
5807 provenance_only.source_tag.clear();
5808 provenance_only.source_namespaces.clear();
5809 assert_eq!(provenance_only, properties);
5810 let mut foreign_change = properties.clone();
5811 let attribute = foreign_change
5812 .raw_attributes
5813 .iter_mut()
5814 .find(|(key, _)| key == "f:keep")
5815 .unwrap();
5816 attribute.1 = "different".to_owned();
5817 assert_ne!(foreign_change, properties);
5818 }
5819
5820 #[test]
5821 fn fx206_hyperlink_opaque_unqualified_id_survives_retarget_and_clear() {
5822 for owned in ["", " rel:id='before'"] {
5823 let input = format!(
5824 r#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="urn:foreign" xmlns:rel="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:f="urn:payload"><q:hlinkClick id='A' {owned} action='ppaction://hlinksldjump' f:keep='B'><f:opaque/></q:hlinkClick></q:rPr>"#
5825 );
5826 let mut properties = CT_TextCharacterProperties::from_xml(input.as_bytes()).unwrap();
5827 assert_eq!(
5828 properties
5829 .hyperlink_click
5830 .as_ref()
5831 .unwrap()
5832 .relationship_id
5833 .as_deref(),
5834 if owned.is_empty() {
5835 None
5836 } else {
5837 Some("before")
5838 }
5839 );
5840 for target in [Some("after"), None] {
5841 properties.hyperlink_click.as_mut().unwrap().relationship_id =
5842 target.map(str::to_owned);
5843 let saved = fx206_link_saved(&properties);
5844 let xml = std::str::from_utf8(&saved).unwrap();
5845 for token in [
5846 "id='A'",
5847 "f:keep='B'",
5848 "<f:opaque/>",
5849 "xmlns:r=\"urn:foreign\"",
5850 ] {
5851 assert!(xml.contains(token), "lost opaque token {token}: {xml}");
5852 }
5853 assert_eq!(xml.matches("id='A'").count(), 1);
5854 let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
5855 assert_eq!(
5856 reopened
5857 .hyperlink_click
5858 .as_ref()
5859 .unwrap()
5860 .relationship_id
5861 .as_deref(),
5862 target
5863 );
5864 assert_eq!(fx206_link_saved(&reopened), saved);
5865 properties = reopened;
5866 }
5867 }
5868 }
5869
5870 #[test]
5871 fn fx206_new_hyperlink_closes_relationship_uri_without_requalifying_raw_payload() {
5872 let input = br#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:a="urn:foreign-a" xmlns:r="urn:foreign-r"><r:opaque id='A'/></q:rPr>"#;
5873 for raw_namespace in [false, true] {
5874 let mut properties = CT_TextCharacterProperties::from_xml(input).unwrap();
5875 assert!(properties.hyperlink_click.is_none());
5876 let mut click = TextHyperlink {
5877 relationship_id: Some("target".to_owned()),
5878 ..TextHyperlink::default()
5879 };
5880 if raw_namespace {
5881 click
5882 .raw_attributes
5883 .push(("xmlns:r".to_owned(), "urn:opaque-click".to_owned()));
5884 click
5885 .raw_attributes
5886 .push(("r:opaque".to_owned(), "preserved".to_owned()));
5887 }
5888 properties.hyperlink_click = Some(click);
5889 let saved = fx206_link_saved(&properties);
5890 let xml = std::str::from_utf8(&saved).unwrap();
5891 assert!(xml.contains("xmlns:r=\"urn:foreign-r\""));
5892 assert!(xml.contains("<r:opaque id='A'/>"));
5893 if raw_namespace {
5894 assert!(xml.contains("xmlns:r=\"urn:opaque-click\""));
5895 assert!(xml.contains("r:opaque=\"preserved\""));
5896 assert!(xml.contains("xmlns:rLink1=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\""));
5897 } else {
5898 assert!(xml.contains("xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\""));
5899 }
5900 let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
5901 assert_eq!(
5902 reopened
5903 .hyperlink_click
5904 .as_ref()
5905 .unwrap()
5906 .relationship_id
5907 .as_deref(),
5908 Some("target")
5909 );
5910 assert_eq!(fx206_link_saved(&reopened), saved);
5911 }
5912 }
5913 #[test]
5914 fn fx206_lookup_fallbacks_do_not_replace_physical_namespace_provenance() {
5915 let fallback =
5916 CT_TextParagraph::from_xml(br#"<q:p><q:r><q:rPr b="1"/><q:t>text</q:t></q:r></q:p>"#)
5917 .unwrap();
5918 assert_eq!(
5919 fallback.to_xml().unwrap(),
5920 br#"<a:p><a:r><a:rPr b="1"/><a:t>text</a:t></a:r></a:p>"#
5921 );
5922 let input = br#"<q:p xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:a='urn:foreign'><a:opaque a:keep='A'/><q:r><q:rPr b="1"/><q:t>text</q:t></q:r></q:p>"#;
5923 let physical = CT_TextParagraph::from_xml(input).unwrap();
5924 let saved = physical.to_xml().unwrap();
5925 let xml = std::str::from_utf8(&saved).unwrap();
5926 assert!(xml.starts_with("<q:p "));
5927 assert!(xml.contains("xmlns:a='urn:foreign'"));
5928 assert!(xml.contains("<a:opaque a:keep='A'/>"));
5929 assert!(xml.contains("xmlns:q=\"http://schemas.openxmlformats.org/drawingml/2006/main\""));
5930 assert_eq!(
5931 CT_TextParagraph::from_xml(&saved)
5932 .unwrap()
5933 .to_xml()
5934 .unwrap(),
5935 saved
5936 );
5937 }
5938 #[test]
5939 fn fx206_relocated_run_retains_opaque_inherited_namespace_closure() {
5940 let input = br#"<q:p xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x='urn:foreign'><q:r><x:opaque x:keep='A'/><q:t>text</q:t></q:r></q:p>"#;
5941 let parsed = super::CT_TextParagraph::from_xml(input).unwrap();
5942 let relocated = super::CT_TextParagraph {
5943 runs: parsed.runs,
5944 ..Default::default()
5945 };
5946 let saved = relocated.to_xml().unwrap();
5947 let xml = std::str::from_utf8(&saved).unwrap();
5948 assert!(xml.contains("<x:opaque x:keep='A'/>"));
5949 assert!(xml.contains("xmlns:x=\"urn:foreign\""));
5950 assert!(
5951 matches!(&relocated.runs[..], [super::TextRun::Run(run)] if run.text.value == "text")
5952 );
5953 assert_eq!(
5954 super::CT_TextParagraph::from_xml(&saved)
5955 .unwrap()
5956 .to_xml()
5957 .unwrap(),
5958 saved
5959 );
5960 }
5961 #[test]
5962 fn fx206_paragraph_attribute_order_is_semantic_but_raw_save_stays_exact() {
5963 let left_source = br#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:f="urn:foreign" defTabSz="457200" rtl="0" f:keep='A'><f:opaque/></a:pPr>"#;
5964 let right_source = br#"<a:pPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:f="urn:foreign" rtl="0" defTabSz="457200" f:keep='A'><f:opaque/></a:pPr>"#;
5965 let left = CT_TextParagraphProperties::from_xml(left_source).unwrap();
5966 let right = CT_TextParagraphProperties::from_xml(right_source).unwrap();
5967 assert_eq!(left, right);
5968 for (properties, source) in [
5969 (&left, left_source.as_slice()),
5970 (&right, right_source.as_slice()),
5971 ] {
5972 let mut writer = Writer::new(Vec::new());
5973 properties.write_xml(&mut writer, "a:pPr").unwrap();
5974 assert_eq!(writer.into_inner(), source);
5975 }
5976 let changed_value = std::str::from_utf8(right_source)
5978 .unwrap()
5979 .replace("457200", "457201");
5980 assert_ne!(
5981 left,
5982 CT_TextParagraphProperties::from_xml(changed_value.as_bytes()).unwrap()
5983 );
5984 let changed_body = std::str::from_utf8(right_source)
5985 .unwrap()
5986 .replace("<f:opaque/>", "<f:other/>");
5987 assert_ne!(
5988 left,
5989 CT_TextParagraphProperties::from_xml(changed_body.as_bytes()).unwrap()
5990 );
5991 }
5992}
5993
5994fn text_close_inherited_property(raw: &[u8], inherited: &[(String, String)]) -> Result<Vec<u8>> {
5997 let mut reader = Reader::from_reader(raw);
5998 let event = reader.read_event().map_err(OxmlError::from)?;
5999 let empty = matches!(&event, Event::Empty(_));
6000 let start = match event {
6001 Event::Start(start) | Event::Empty(start) => start,
6002 _ => return Ok(raw.to_vec()),
6003 };
6004 let mut content = std::str::from_utf8(start.as_ref())
6005 .map_err(OxmlError::from)?
6006 .to_owned();
6007 let declared = start
6008 .attributes()
6009 .map(|attribute| attribute.map(|attribute| attribute.key.as_ref().to_vec()))
6010 .collect::<std::result::Result<Vec<_>, _>>()
6011 .map_err(OxmlError::from)?;
6012 for (prefix, uri) in inherited {
6013 let key = if prefix.is_empty() {
6014 "xmlns".to_owned()
6015 } else {
6016 format!("xmlns:{prefix}")
6017 };
6018 if !declared.iter().any(|old| old == key.as_bytes()) {
6019 content.push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(uri)));
6020 }
6021 }
6022 if !inherited.iter().any(|(prefix, _)| prefix.is_empty())
6023 && !declared.iter().any(|key| key == b"xmlns")
6024 {
6025 content.push_str(" xmlns=\"\"");
6026 }
6027 let start = BytesStart::from_content(content, start.name().as_ref().len());
6028 let mut writer = Writer::new(Vec::new());
6029 if empty {
6030 write_empty(&mut writer, start)?;
6031 } else {
6032 write_start(&mut writer, start)?;
6033 }
6034 let mut closed = writer.into_inner();
6035 closed.extend_from_slice(&raw[reader.buffer_position() as usize..]);
6036 Ok(closed)
6037}
6038
6039pub(crate) fn text_inherit_property_xml(
6042 local: &[u8],
6043 local_scope: &[(String, String)],
6044 inherited: &[u8],
6045 inherited_scope: &[(String, String)],
6046) -> Result<Vec<u8>> {
6047 let read = |xml: &[u8], scope: &[(String, String)]| -> Result<_> {
6048 let mut reader = Reader::from_reader(xml);
6049 let start = match reader.read_event().map_err(OxmlError::from)? {
6050 Event::Start(start) | Event::Empty(start) => start.into_owned(),
6051 _ => {
6052 return Err(invalid_attribute(
6053 "inherit text properties",
6054 "root",
6055 "missing owner",
6056 ));
6057 }
6058 };
6059 let empty = xml[reader.buffer_position() as usize - 2] == b'/';
6060 let scope = text_fragment_scope(&start, &TextNamespaceScope::inherited(scope))?;
6061 let root_name = local_name(start.name().as_ref()).to_vec();
6062 let mut children = Vec::new();
6063 let mut boundary = 0;
6064 if !empty {
6065 loop {
6066 let event = reader.read_event().map_err(OxmlError::from)?;
6067 let raw = match event {
6068 Event::Start(child) | Event::Empty(child) => {
6069 let child_empty = xml[reader.buffer_position() as usize - 2] == b'/';
6070 let name = text_owned_name(&child, &scope)?;
6071 let slot = if is_character_property_tag(&root_name) {
6072 character_property_slot(&name)
6073 } else if is_paragraph_property_tag(&root_name) {
6074 paragraph_property_slot(&name)
6075 } else if name == b"defPPr" {
6076 Some(1)
6077 } else if root_name != b"extLst" && name == b"extLst" {
6078 Some(11)
6079 } else if root_name != b"extLst" {
6080 name.strip_prefix(b"lvl")
6081 .and_then(|tail| tail.strip_suffix(b"pPr"))
6082 .and_then(|level| {
6083 std::str::from_utf8(level).ok()?.parse::<usize>().ok()
6084 })
6085 .filter(|level| (1..=9).contains(level))
6086 .map(|level| level + 1)
6087 } else {
6088 None
6089 };
6090 if let Some(slot) = slot {
6091 boundary = boundary.max(slot);
6092 }
6093 let raw = if child_empty {
6094 capture_empty_element(&child)?
6095 } else {
6096 capture_element(&mut reader, &child)?
6097 };
6098 children.push((boundary, slot, raw));
6099 continue;
6100 }
6101 Event::End(_) => break,
6102 Event::Eof => return Err(missing_end("inherited property owner")),
6103 event @ (Event::Comment(_)
6104 | Event::PI(_)
6105 | Event::Text(_)
6106 | Event::CData(_)) => text_capture_opaque_event(event)?,
6107 _ => continue,
6108 };
6109 children.push((boundary, None, raw));
6110 }
6111 }
6112 Ok((start, scope, children))
6113 };
6114 let (mut start, local_scope, mut children) = read(local, local_scope)?;
6115 let (parent, inherited_scope, inherited_children) = read(inherited, inherited_scope)?;
6116 let root_name = local_name(start.name().as_ref()).to_vec();
6117 let attribute_identity = |key: &[u8], scope: &TextNamespaceScope| -> Result<_> {
6118 let key = std::str::from_utf8(key).map_err(OxmlError::from)?;
6119 let (prefix, name) = key.split_once(':').unwrap_or(("", key));
6120 let uri = if prefix.is_empty() {
6121 None
6122 } else if prefix == "xml" {
6123 Some("http://www.w3.org/XML/1998/namespace".to_owned())
6124 } else {
6125 scope
6126 .physical
6127 .iter()
6128 .find(|(p, _)| p == prefix)
6129 .map(|(_, uri)| uri.clone())
6130 };
6131 if !prefix.is_empty() && uri.is_none() {
6132 return Err(invalid_attribute(
6133 "inherit text properties",
6134 "namespace",
6135 "unbound attribute prefix",
6136 ));
6137 }
6138 Ok((uri, name.to_owned()))
6139 };
6140 let known_attribute = |key: &[u8]| {
6141 if is_character_property_tag(&root_name) {
6142 matches!(
6143 key,
6144 b"sz"
6145 | b"b"
6146 | b"i"
6147 | b"cap"
6148 | b"u"
6149 | b"strike"
6150 | b"spc"
6151 | b"baseline"
6152 | b"lang"
6153 | b"altLang"
6154 | b"kern"
6155 | b"kumimoji"
6156 | b"normalizeH"
6157 | b"noProof"
6158 | b"dirty"
6159 | b"err"
6160 | b"smtClean"
6161 | b"smtId"
6162 | b"bmk"
6163 )
6164 } else if is_paragraph_property_tag(&root_name) {
6165 matches!(
6166 key,
6167 b"marL"
6168 | b"marR"
6169 | b"lvl"
6170 | b"indent"
6171 | b"algn"
6172 | b"rtl"
6173 | b"defTabSz"
6174 | b"eaLnBrk"
6175 | b"latinLnBrk"
6176 | b"hangingPunct"
6177 | b"fontAlgn"
6178 )
6179 } else {
6180 false
6181 }
6182 };
6183 let namespaces = |scope: &TextNamespaceScope| {
6184 scope
6185 .physical
6186 .iter()
6187 .filter(|(prefix, uri)| !prefix.is_empty() || !uri.is_empty())
6188 .cloned()
6189 .collect::<std::collections::BTreeMap<_, _>>()
6190 };
6191 let local_tokens = text_attribute_tokens(&start)?;
6192 let parent_tokens = text_attribute_tokens(&parent)?;
6193 let mut additions = String::new();
6194 for attribute in parent.attributes() {
6195 let attribute = attribute.map_err(OxmlError::from)?;
6196 let key = attribute.key.as_ref();
6197 if key == b"xmlns" || key.starts_with(b"xmlns:") {
6198 continue;
6199 }
6200 let identity = attribute_identity(key, &inherited_scope)?;
6201 let mut existing = None;
6202 for old in start.attributes() {
6203 let old = old.map_err(OxmlError::from)?;
6204 if old.key.as_ref() != b"xmlns"
6205 && !old.key.as_ref().starts_with(b"xmlns:")
6206 && attribute_identity(old.key.as_ref(), &local_scope)? == identity
6207 {
6208 existing = Some(old);
6209 break;
6210 }
6211 }
6212 if let Some(old) = existing {
6213 if !known_attribute(key)
6214 && (old
6215 .decoded_and_normalized_value(
6216 quick_xml::XmlVersion::Implicit1_0,
6217 start.decoder(),
6218 )
6219 .map_err(OxmlError::from)?
6220 != attribute
6221 .decoded_and_normalized_value(
6222 quick_xml::XmlVersion::Implicit1_0,
6223 parent.decoder(),
6224 )
6225 .map_err(OxmlError::from)?
6226 || namespaces(&local_scope) != namespaces(&inherited_scope))
6227 {
6228 return Err(invalid_attribute(
6229 "inherit text properties",
6230 "metadata",
6231 "conflicting opaque attribute authority",
6232 ));
6233 }
6234 continue;
6235 }
6236 if !known_attribute(key)
6237 && (!children.is_empty()
6238 || local_tokens
6239 .iter()
6240 .any(|(key, _)| !key.starts_with("xmlns") && !known_attribute(key.as_bytes())))
6241 && namespaces(&local_scope) != namespaces(&inherited_scope)
6242 {
6243 return Err(invalid_attribute(
6244 "inherit text properties",
6245 "metadata",
6246 "unrepresentable opaque namespace contexts",
6247 ));
6248 }
6249 if !known_attribute(key) {
6250 for (prefix, uri) in &inherited_scope.physical {
6251 if let Some((_, old)) = local_scope.physical.iter().find(|(old, _)| old == prefix) {
6252 if old != uri {
6253 return Err(invalid_attribute(
6254 "inherit text properties",
6255 "namespace",
6256 "conflicting inherited root binding",
6257 ));
6258 }
6259 } else {
6260 let key = if prefix.is_empty() {
6261 "xmlns".to_owned()
6262 } else {
6263 format!("xmlns:{prefix}")
6264 };
6265 if !additions.contains(&format!(" {key}=")) {
6266 additions
6267 .push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(uri)));
6268 }
6269 }
6270 }
6271 }
6272 let (_, token) = parent_tokens
6273 .iter()
6274 .find(|(old, _)| old.as_bytes() == key)
6275 .ok_or_else(|| {
6276 invalid_attribute(
6277 "inherit text properties",
6278 "attribute",
6279 "missing source token",
6280 )
6281 })?;
6282 additions.push_str(token);
6283 }
6284 for (boundary, slot, raw) in inherited_children {
6285 if let Some(slot) = slot
6286 && let Some(index) = children.iter().position(|(_, old, _)| *old == Some(slot))
6287 {
6288 let name = raw_local_name(&raw);
6289 if matches!(name, b"defRPr" | b"defPPr" | b"extLst") || name.starts_with(b"lvl") {
6290 children[index].2 = text_inherit_property_xml(
6291 &children[index].2,
6292 &local_scope.physical,
6293 &raw,
6294 &inherited_scope.physical,
6295 )?;
6296 }
6297 continue;
6298 }
6299 let raw = text_close_inherited_property(&raw, &inherited_scope.physical)?;
6300 if children.iter().any(|(_, _, old)| old == &raw) {
6301 continue;
6302 }
6303 let position = children
6304 .iter()
6305 .position(|(old_boundary, _, _)| *old_boundary > boundary)
6306 .unwrap_or(children.len());
6307 children.insert(position, (boundary, slot, raw));
6308 }
6309 if !additions.is_empty() {
6310 let name_len = start.name().as_ref().len();
6311 let content = format!(
6312 "{}{additions}",
6313 std::str::from_utf8(start.as_ref()).map_err(OxmlError::from)?
6314 );
6315 start = BytesStart::from_content(content, name_len);
6316 }
6317 let name = std::str::from_utf8(start.name().as_ref())
6318 .map_err(OxmlError::from)?
6319 .to_owned();
6320 let mut writer = Writer::new(Vec::new());
6321 if children.is_empty() {
6322 write_empty(&mut writer, start)?;
6323 } else {
6324 write_start(&mut writer, start)?;
6325 for (_, _, raw) in children {
6326 writer.get_mut().write_all(&raw).map_err(OxmlError::from)?;
6327 }
6328 write_end(&mut writer, &name)?;
6329 }
6330 Ok(writer.into_inner())
6331}
6332
6333#[cfg(test)]
6334#[test]
6335fn fx216_inherited_scalar_retains_local_opaque_namespace_authority() {
6336 let local = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><opaque/></a:rPr>"#;
6337 let inherited = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns="urn:parent" b="1"/>"#;
6338 let local = CT_TextCharacterProperties::from_xml(local).unwrap();
6339 let inherited = CT_TextCharacterProperties::from_xml(inherited).unwrap();
6340 let merged = local.with_inherited_defaults(&inherited).unwrap();
6341 let mut writer = Writer::new(Vec::new());
6342 merged.write_xml(&mut writer, "a:rPr").unwrap();
6343 let xml = writer.into_inner();
6344 let mut reader = quick_xml::NsReader::from_reader(xml.as_slice());
6345 let mut seen = false;
6346 loop {
6347 let (namespace, event) = reader.read_resolved_event().unwrap();
6348 match event {
6349 Event::Empty(element) if element.local_name().as_ref() == b"opaque" => {
6350 assert!(matches!(namespace, quick_xml::name::ResolveResult::Unbound));
6351 seen = true;
6352 }
6353 Event::Eof => break,
6354 _ => {}
6355 }
6356 }
6357 assert!(seen);
6358 assert_eq!(merged.bold, Some(true));
6359}
6360
6361#[cfg(test)]
6362#[test]
6363fn fx216_inherited_scalars_keep_false_zero_and_harmless_aliases() {
6364 let local = CT_TextCharacterProperties::from_xml(br#"<q:rPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" b="0" i="false" spc="0" baseline="0"/>"#).unwrap();
6365 let inherited = CT_TextCharacterProperties::from_xml(br#"<a:defRPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns="urn:unused" b="1" i="1" spc="200" baseline="5000" sz="2400"/>"#).unwrap();
6366 let merged = local.with_inherited_defaults(&inherited).unwrap();
6367 assert_eq!(merged.bold, Some(false));
6368 assert_eq!(merged.italic, Some(false));
6369 assert_eq!(merged.spacing, Some(TextPointValue::Centipoints(0)));
6370 assert_eq!(merged.baseline.as_deref(), Some("0"));
6371 assert_eq!(merged.font_size, Some(2400));
6372 let mut writer = Writer::new(Vec::new());
6373 merged.write_xml(&mut writer, "a:rPr").unwrap();
6374 let xml = writer.into_inner();
6375 assert!(!String::from_utf8_lossy(&xml).contains("urn:unused"));
6376 assert_eq!(CT_TextCharacterProperties::from_xml(&xml).unwrap(), merged);
6377 assert_eq!(local.bold, Some(false));
6378 assert_eq!(inherited.bold, Some(true));
6379}