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) {
3960 let old_run_count = self.runs.len();
3961 let retained_run = self.runs.iter().find_map(|run| match run {
3962 TextRun::Run(run) => Some(run.clone()),
3963 TextRun::Break(_) | TextRun::Field(_) => None,
3964 });
3965 let mut raw_children = OrderedRawChildren::default();
3966 for boundary in 0..=2 + old_run_count {
3967 let new_boundary = if boundary <= 1 {
3968 boundary
3969 } else if boundary <= 1 + old_run_count {
3970 2
3971 } else {
3972 3
3973 };
3974 for child in self.raw_children.at(boundary) {
3975 raw_children.push(new_boundary, child.to_vec());
3976 }
3977 }
3978 self.raw_children = raw_children;
3979 let mut run = retained_run.unwrap_or_else(|| CT_RegularTextRun {
3980 properties: self
3981 .end_properties
3982 .clone()
3983 .map(CT_TextCharacterProperties::without_hyperlinks),
3984 ..CT_RegularTextRun::new(text)
3985 });
3986 run.set_text(text);
3987 self.runs = vec![TextRun::Run(run)];
3988 self.split_line_breaks(text);
3989 }
3990
3991 fn split_line_breaks(&mut self, text: &str) {
3995 let [TextRun::Run(first)] = self.runs.as_mut_slice() else {
3996 return;
3997 };
3998 if !text.contains('\u{b}') {
3999 return;
4000 }
4001 let mut lines = text.split('\u{b}');
4002 first.set_text(lines.next().unwrap_or_default());
4003 let properties = first.properties.clone();
4004 for line in lines {
4005 self.raw_children.shift_boundaries_from(2);
4006 self.runs.push(TextRun::Break(CT_TextLineBreak {
4007 properties: properties.clone(),
4008 ..CT_TextLineBreak::default()
4009 }));
4010 if !line.is_empty() {
4011 self.raw_children.shift_boundaries_from(2);
4012 self.runs.push(TextRun::Run(CT_RegularTextRun {
4013 properties: properties.clone(),
4014 ..CT_RegularTextRun::new(line)
4015 }));
4016 }
4017 }
4018 }
4019
4020 pub fn add_run(&mut self, text: &str) -> &mut CT_RegularTextRun {
4022 self.raw_children.shift_boundaries_from(2 + self.runs.len());
4023 self.runs.push(TextRun::Run(CT_RegularTextRun::new(text)));
4024 let Some(TextRun::Run(run)) = self.runs.last_mut() else {
4025 unreachable!("the appended text choice is a regular run")
4026 };
4027 run
4028 }
4029
4030 pub fn from_xml(xml: &[u8]) -> Result<Self> {
4032 Self::from_xml_with_namespaces(xml, &TextNamespaceScope::default())
4033 }
4034
4035 pub(crate) fn from_xml_with_namespaces(
4036 xml: &[u8],
4037 inherited: &TextNamespaceScope,
4038 ) -> Result<Self> {
4039 parse_complete(
4040 xml,
4041 b"p",
4042 |reader, start| Self::from_element_with_namespaces(reader, start, inherited),
4043 |start| {
4044 Ok(Self {
4045 source_root: text_root_spellings_scoped(
4046 start,
4047 inherited,
4048 &capture_empty_element(start)?,
4049 )?,
4050 source_tag: std::str::from_utf8(start.name().as_ref())
4051 .map_err(OxmlError::from)?
4052 .to_owned(),
4053 ..Self::default()
4054 })
4055 },
4056 )
4057 }
4058
4059 fn from_element_with_namespaces(
4060 reader: &mut Reader<&[u8]>,
4061 start: &BytesStart<'_>,
4062 inherited: &TextNamespaceScope,
4063 ) -> Result<Self> {
4064 let namespaces = text_fragment_scope(start, inherited)?;
4065 let mut paragraph = Self {
4066 source_root: text_root_spellings_scoped(
4067 start,
4068 inherited,
4069 &capture_element(&mut reader.clone(), start)?,
4070 )?,
4071 source_tag: std::str::from_utf8(start.name().as_ref())
4072 .map_err(OxmlError::from)?
4073 .to_owned(),
4074 ..Self::default()
4075 };
4076 let mut boundary = 0;
4077 let mut buffer = Vec::new();
4078 loop {
4079 match reader
4080 .read_event_into(&mut buffer)
4081 .map_err(OxmlError::from)?
4082 {
4083 Event::Start(element)
4088 if text_owned_name(&element, &namespaces)? == b"pPr"
4089 && paragraph.properties.is_none() =>
4090 {
4091 paragraph.properties =
4092 Some(CT_TextParagraphProperties::from_element_with_namespaces(
4093 reader,
4094 &element,
4095 &namespaces,
4096 )?);
4097 boundary = boundary.max(1);
4098 }
4099 Event::Empty(element)
4100 if text_owned_name(&element, &namespaces)? == b"pPr"
4101 && paragraph.properties.is_none() =>
4102 {
4103 paragraph.properties =
4104 Some(CT_TextParagraphProperties::from_start_with_namespaces(
4105 &element,
4106 &namespaces,
4107 )?);
4108 boundary = boundary.max(1);
4109 }
4110 Event::Start(element) if is_text_run(&text_owned_name(&element, &namespaces)?) => {
4111 let name = local_name(element.name().as_ref()).to_vec();
4112 let raw = capture_element(reader, &element)?;
4113 paragraph.runs.push(TextRun::from_xml_with_namespaces(
4114 &raw,
4115 &name,
4116 &namespaces,
4117 )?);
4118 boundary = boundary.max(1 + paragraph.runs.len());
4119 }
4120 Event::Empty(element) if is_text_run(&text_owned_name(&element, &namespaces)?) => {
4121 let name = local_name(element.name().as_ref()).to_vec();
4122 let raw = capture_empty_element(&element)?;
4123 paragraph.runs.push(TextRun::from_xml_with_namespaces(
4124 &raw,
4125 &name,
4126 &namespaces,
4127 )?);
4128 boundary = boundary.max(1 + paragraph.runs.len());
4129 }
4130 Event::Start(element)
4131 if text_owned_name(&element, &namespaces)? == b"endParaRPr" =>
4132 {
4133 if paragraph.end_properties.is_some() {
4134 return Err(duplicate("endParaRPr"));
4135 }
4136 paragraph.end_properties =
4137 Some(CT_TextCharacterProperties::from_element_with_namespaces(
4138 reader,
4139 &element,
4140 &namespaces,
4141 )?);
4142 boundary = boundary.max(2 + paragraph.runs.len());
4143 }
4144 Event::Empty(element)
4145 if text_owned_name(&element, &namespaces)? == b"endParaRPr" =>
4146 {
4147 if paragraph.end_properties.is_some() {
4148 return Err(duplicate("endParaRPr"));
4149 }
4150 paragraph.end_properties =
4151 Some(CT_TextCharacterProperties::from_start_with_namespaces(
4152 &element,
4153 &namespaces,
4154 )?);
4155 boundary = boundary.max(2 + paragraph.runs.len());
4156 }
4157 Event::Start(element) => paragraph
4158 .raw_children
4159 .push(boundary, capture_element(reader, &element)?),
4160 Event::Empty(element) => paragraph
4161 .raw_children
4162 .push(boundary, capture_empty_element(&element)?),
4163 Event::End(element) if matches_local_name(element.name().as_ref(), b"p") => {
4164 return Ok(paragraph);
4165 }
4166 Event::Eof => return Err(missing_end("p")),
4167 event @ (Event::Comment(_) | Event::PI(_) | Event::Text(_) | Event::CData(_)) => {
4168 paragraph
4169 .raw_children
4170 .push(boundary, text_capture_opaque_event(event)?);
4171 }
4172 _ => {}
4173 }
4174 buffer.clear();
4175 }
4176 }
4177
4178 pub fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
4180 let owned_tag = text_source_tag(
4181 "a:p",
4182 &self.source_tag,
4183 &text_namespace_scope(&text_source_start("a:p", &self.source_root), &[])?,
4184 );
4185 let tag = owned_tag.as_str();
4186 if self.properties.is_none()
4187 && self.runs.is_empty()
4188 && self.end_properties.is_none()
4189 && self.raw_children.is_empty()
4190 {
4191 return write_empty(writer, text_source_start(tag, &self.source_root));
4192 }
4193 write_start(writer, text_source_start(tag, &self.source_root))?;
4194 emit_raw(writer, self.raw_children.at(0))?;
4195 if let Some(properties) = &self.properties {
4196 {
4197 let mut child = Writer::new(Vec::new());
4198 properties.write_xml(&mut child, "a:pPr")?;
4199 writer
4200 .get_mut()
4201 .write_all(&text_close_generated_child(
4202 &child.into_inner(),
4203 &text_namespace_scope(&text_source_start(tag, &self.source_root), &[])?,
4204 )?)
4205 .map_err(OxmlError::from)?;
4206 }
4207 }
4208 emit_raw(writer, self.raw_children.at(1))?;
4209 for (index, run) in self.runs.iter().enumerate() {
4210 run.write_xml(writer)?;
4211 emit_raw(writer, self.raw_children.at(2 + index))?;
4212 }
4213 if let Some(properties) = &self.end_properties {
4214 {
4215 let mut child = Writer::new(Vec::new());
4216 properties.write_xml(&mut child, "a:endParaRPr")?;
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(2 + self.runs.len()))?;
4227 write_end(writer, tag)
4228 }
4229
4230 pub fn to_xml(&self) -> Result<Vec<u8>> {
4232 let mut writer = Writer::new(Vec::new());
4233 self.write_xml(&mut writer)?;
4234 Ok(writer.into_inner())
4235 }
4236
4237 pub fn raw_children(&self) -> &OrderedRawChildren {
4238 &self.raw_children
4239 }
4240}
4241
4242fn is_text_run(name: &[u8]) -> bool {
4243 matches!(local_name(name), b"r" | b"br" | b"fld")
4244}
4245
4246fn parse_complete<T>(
4247 xml: &[u8],
4248 expected: &[u8],
4249 parse_start: impl FnOnce(&mut Reader<&[u8]>, &BytesStart<'_>) -> Result<T>,
4250 parse_empty: impl FnOnce(&BytesStart<'_>) -> Result<T>,
4251) -> Result<T> {
4252 let mut reader = Reader::from_reader(xml);
4253 let mut buffer = Vec::new();
4254 loop {
4255 match reader
4256 .read_event_into(&mut buffer)
4257 .map_err(OxmlError::from)?
4258 {
4259 Event::Start(element) if matches_local_name(element.name().as_ref(), expected) => {
4260 text_validate_root(&element)?;
4261 let parsed = parse_start(&mut reader, &element)?;
4262 text_reject_trailing(&mut reader)?;
4263 return Ok(parsed);
4264 }
4265 Event::Empty(element) if matches_local_name(element.name().as_ref(), expected) => {
4266 text_validate_root(&element)?;
4267 let parsed = parse_empty(&element)?;
4268 text_reject_trailing(&mut reader)?;
4269 return Ok(parsed);
4270 }
4271 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
4272 Event::Eof => {
4273 return Err(TextError::Xml(OxmlError::MissingElement(
4274 String::from_utf8_lossy(expected).into_owned(),
4275 )));
4276 }
4277 _ => {}
4278 }
4279 buffer.clear();
4280 }
4281}
4282
4283pub(crate) fn text_reject_trailing(reader: &mut Reader<&[u8]>) -> Result<()> {
4284 loop {
4285 match reader.read_event().map_err(OxmlError::from)? {
4286 Event::Eof => return Ok(()),
4287 Event::Comment(_) | Event::PI(_) => {}
4288 Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
4289 _ => {
4290 return Err(TextError::UnexpectedElement(
4291 "trailing XML content".to_owned(),
4292 ));
4293 }
4294 }
4295 }
4296}
4297
4298fn root_local_name(xml: &[u8]) -> Result<Vec<u8>> {
4299 let mut reader = Reader::from_reader(xml);
4300 let mut buffer = Vec::new();
4301 loop {
4302 match reader
4303 .read_event_into(&mut buffer)
4304 .map_err(OxmlError::from)?
4305 {
4306 Event::Start(element) | Event::Empty(element) => {
4307 return Ok(local_name(element.name().as_ref()).to_vec());
4308 }
4309 Event::Eof => {
4310 return Err(TextError::Xml(OxmlError::MissingElement(
4311 "DrawingML text properties".to_owned(),
4312 )));
4313 }
4314 _ => {}
4315 }
4316 buffer.clear();
4317 }
4318}
4319
4320fn ensure_empty(reader: &mut Reader<&[u8]>, expected: &[u8]) -> Result<()> {
4321 let mut buffer = Vec::new();
4322 loop {
4323 match reader
4324 .read_event_into(&mut buffer)
4325 .map_err(OxmlError::from)?
4326 {
4327 Event::End(element) if matches_local_name(element.name().as_ref(), expected) => {
4328 return Ok(());
4329 }
4330 Event::Text(text) if text.iter().all(u8::is_ascii_whitespace) => {}
4331 Event::Comment(_) => {}
4332 Event::Start(element) | Event::Empty(element) => return Err(unexpected(&element)),
4333 Event::Eof => return Err(missing_end(&String::from_utf8_lossy(expected))),
4334 _ => {
4335 return Err(TextError::UnexpectedElement(
4336 String::from_utf8_lossy(expected).into_owned(),
4337 ));
4338 }
4339 }
4340 buffer.clear();
4341 }
4342}
4343
4344fn text_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<String>> {
4345 for attribute in start.attributes() {
4346 let attribute = attribute.map_err(OxmlError::from)?;
4347 if attribute.key.as_ref() == name {
4348 let value = attribute
4349 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
4350 .map_err(OxmlError::from)?;
4351 return Ok(Some(value.into_owned()));
4352 }
4353 }
4354 Ok(None)
4355}
4356
4357fn capture_raw_attributes(
4358 start: &BytesStart<'_>,
4359 modelled: &[&[u8]],
4360) -> Result<Vec<(String, String)>> {
4361 let mut raw = Vec::new();
4362 for attribute in start.attributes() {
4363 let attribute = attribute.map_err(OxmlError::from)?;
4364 if modelled.iter().any(|name| attribute.key.as_ref() == *name) {
4365 continue;
4366 }
4367 let name = std::str::from_utf8(attribute.key.as_ref())
4368 .map_err(OxmlError::from)?
4369 .to_owned();
4370 let value = attribute
4371 .decoded_and_normalized_value(XmlVersion::Implicit1_0, start.decoder())
4372 .map_err(OxmlError::from)?
4373 .into_owned();
4374 raw.push((name, value));
4375 }
4376 Ok(raw)
4377}
4378
4379fn push_raw_attributes(start: &mut BytesStart<'_>, attributes: &[(String, String)]) {
4380 for (name, value) in attributes {
4381 start.push_attribute((name.as_str(), value.as_str()));
4382 }
4383}
4384
4385fn required_attr(start: &BytesStart<'_>, name: &[u8]) -> Result<String> {
4386 text_attr(start, name)?
4387 .filter(|value| !value.is_empty())
4388 .ok_or_else(|| {
4389 missing_attribute(
4390 &String::from_utf8_lossy(local_name(start.name().as_ref())),
4391 &String::from_utf8_lossy(name),
4392 )
4393 })
4394}
4395
4396fn parse_optional_i32(
4397 start: &BytesStart<'_>,
4398 name: &[u8],
4399 min: i32,
4400 max: i32,
4401) -> Result<Option<i32>> {
4402 text_attr(start, name)?
4403 .map(|value| {
4404 parse_i32_value(
4405 &String::from_utf8_lossy(local_name(start.name().as_ref())),
4406 &String::from_utf8_lossy(name),
4407 &value,
4408 min,
4409 max,
4410 )
4411 })
4412 .transpose()
4413}
4414
4415fn parse_i32_attr(start: &BytesStart<'_>, name: &[u8], min: i32, max: i32) -> Result<i32> {
4416 let value = required_attr(start, name)?;
4417 parse_i32_value(
4418 &String::from_utf8_lossy(local_name(start.name().as_ref())),
4419 &String::from_utf8_lossy(name),
4420 &value,
4421 min,
4422 max,
4423 )
4424}
4425
4426fn parse_i32_value(element: &str, attribute: &str, value: &str, min: i32, max: i32) -> Result<i32> {
4427 let parsed = value
4428 .parse::<i32>()
4429 .map_err(|_| invalid_attribute(element, attribute, value))?;
4430 if (min..=max).contains(&parsed) {
4431 Ok(parsed)
4432 } else {
4433 Err(invalid_attribute(element, attribute, value))
4434 }
4435}
4436
4437fn parse_optional_bool(start: &BytesStart<'_>, name: &[u8]) -> Result<Option<bool>> {
4438 text_attr(start, name)?
4439 .map(|value| match value.as_str() {
4440 "true" | "1" => Ok(true),
4441 "false" | "0" => Ok(false),
4442 _ => Err(invalid_attribute(
4443 &String::from_utf8_lossy(local_name(start.name().as_ref())),
4444 &String::from_utf8_lossy(name),
4445 &value,
4446 )),
4447 })
4448 .transpose()
4449}
4450
4451fn parse_optional_enum<T>(
4452 start: &BytesStart<'_>,
4453 name: &[u8],
4454 parse: impl FnOnce(&str) -> Option<T>,
4455) -> Result<Option<T>> {
4456 let Some(value) = text_attr(start, name)? else {
4457 return Ok(None);
4458 };
4459 parse(&value).map(Some).ok_or_else(|| {
4460 invalid_attribute(
4461 &String::from_utf8_lossy(local_name(start.name().as_ref())),
4462 &String::from_utf8_lossy(name),
4463 &value,
4464 )
4465 })
4466}
4467
4468fn validate_spacing_percent(value: &str) -> Result<()> {
4469 if is_percentage_string(value) {
4470 return Ok(());
4471 }
4472 parse_i32_value("spcPct", "val", value, 0, MAX_TRANSITIONAL_SPACING_PERCENT)?;
4473 Ok(())
4474}
4475
4476fn validate_baseline(value: &str) -> Result<()> {
4477 if is_percentage_string(value) {
4478 return Ok(());
4479 }
4480 parse_i32_value("rPr", "baseline", value, -100_000, 100_000)?;
4481 Ok(())
4482}
4483
4484fn is_percentage_string(value: &str) -> bool {
4485 value.strip_suffix('%').is_some_and(is_signed_decimal)
4486}
4487
4488fn is_universal_measure(value: &str) -> bool {
4489 ["mm", "cm", "in", "pt", "pc", "pi"]
4490 .iter()
4491 .find_map(|unit| value.strip_suffix(unit))
4492 .is_some_and(is_signed_decimal)
4493}
4494
4495fn is_signed_decimal(value: &str) -> bool {
4496 let unsigned = value.strip_prefix('-').unwrap_or(value);
4497 let mut parts = unsigned.split('.');
4498 let Some(integer) = parts.next() else {
4499 return false;
4500 };
4501 if integer.is_empty() || !integer.bytes().all(|byte| byte.is_ascii_digit()) {
4502 return false;
4503 }
4504 if let Some(fraction) = parts.next()
4505 && (fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit()))
4506 {
4507 return false;
4508 }
4509 parts.next().is_none()
4510}
4511
4512fn is_guid(value: &str) -> bool {
4513 if value.len() != 38 || !value.starts_with('{') || !value.ends_with('}') {
4514 return false;
4515 }
4516 value.bytes().enumerate().all(|(index, byte)| match index {
4517 0 => byte == b'{',
4518 37 => byte == b'}',
4519 9 | 14 | 19 | 24 => byte == b'-',
4520 _ => byte.is_ascii_hexdigit(),
4521 })
4522}
4523
4524fn push_optional_attr<'a>(start: &mut BytesStart<'a>, name: &'a str, value: Option<&'a str>) {
4525 if let Some(value) = value {
4526 start.push_attribute((name, value));
4527 }
4528}
4529
4530fn push_optional_bool(start: &mut BytesStart<'_>, name: &'static str, value: Option<bool>) {
4531 if let Some(value) = value {
4532 start.push_attribute((name, if value { "1" } else { "0" }));
4533 }
4534}
4535
4536fn push_owned_attr(start: &mut BytesStart<'_>, name: &'static str, value: String) {
4537 start.push_attribute((name, value.as_str()));
4538}
4539
4540fn emit_raw<'a, W: Write>(
4541 writer: &mut Writer<W>,
4542 children: impl Iterator<Item = &'a [u8]>,
4543) -> Result<()> {
4544 for child in children {
4545 writer.get_mut().write_all(child).map_err(OxmlError::from)?;
4546 }
4547 Ok(())
4548}
4549
4550fn write_start<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
4551 writer
4552 .write_event(Event::Start(start))
4553 .map_err(OxmlError::from)?;
4554 Ok(())
4555}
4556
4557fn write_empty<W: Write>(writer: &mut Writer<W>, start: BytesStart<'_>) -> Result<()> {
4558 writer
4559 .write_event(Event::Empty(start))
4560 .map_err(OxmlError::from)?;
4561 Ok(())
4562}
4563
4564fn write_end<W: Write>(writer: &mut Writer<W>, tag: &str) -> Result<()> {
4565 writer
4566 .write_event(Event::End(BytesEnd::new(tag)))
4567 .map_err(OxmlError::from)?;
4568 Ok(())
4569}
4570
4571fn unexpected(element: &BytesStart<'_>) -> TextError {
4572 TextError::UnexpectedElement(String::from_utf8_lossy(element.name().as_ref()).into_owned())
4573}
4574
4575fn duplicate(element: &str) -> TextError {
4576 TextError::DuplicateElement(element.to_owned())
4577}
4578
4579fn missing_attribute(element: &str, attribute: &str) -> TextError {
4580 TextError::MissingAttribute {
4581 element: element.to_owned(),
4582 attribute: attribute.to_owned(),
4583 }
4584}
4585
4586fn invalid_attribute(element: &str, attribute: &str, value: &str) -> TextError {
4587 TextError::InvalidAttribute {
4588 element: element.to_owned(),
4589 attribute: attribute.to_owned(),
4590 value: value.to_owned(),
4591 }
4592}
4593
4594#[cfg(test)]
4595mod tests {
4596 use quick_xml::Writer;
4597 use std::panic;
4598
4599 use super::{
4600 CT_TextCharacterProperties, CT_TextParagraph, CT_TextParagraphProperties, TextAlignment,
4601 TextHyperlink, TextRun, TextSpace, TextSpacing,
4602 };
4603 use crate::color::ColorChoice;
4604 use crate::text::CT_TextBody;
4605 use crate::text::{
4606 TextAutoNumberScheme, TextBullet, TextBulletCharacter, TextBulletChoice,
4607 TextBulletSizeValue,
4608 };
4609
4610 #[test]
4611 fn fx205_language_edits_preserve_other_attributes_and_font_source() {
4612 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>"#;
4613 let mut properties = CT_TextCharacterProperties::from_xml(source).unwrap();
4614 let original = properties.clone();
4615 properties.set_language(Some("ja-JP"));
4616 assert_eq!(properties.language(), Some("ja-JP"));
4617 let mut writer = Writer::new(Vec::new());
4618 properties.write_xml(&mut writer, "a:rPr").unwrap();
4619 let saved = writer.into_inner();
4620 let xml = String::from_utf8(saved.clone()).unwrap();
4621 assert!(xml.contains("e:lang=\"unchanged\""));
4622 assert!(xml.contains("<e:payload keep=\"yes\"/>"));
4623 assert!(xml.contains("panose=\"020B0604020202020204\""));
4624 let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
4625 assert_eq!(reopened.language(), Some("ja-JP"));
4626 assert_eq!(reopened.east_asian, properties.east_asian);
4627 let mut writer = Writer::new(Vec::new());
4628 reopened.write_xml(&mut writer, "a:rPr").unwrap();
4629 assert_eq!(writer.into_inner(), saved);
4630 properties.set_language(Some("en-US"));
4631 assert_eq!(properties, original);
4632 properties.set_language(None);
4633 assert_eq!(properties.language(), None);
4634 assert!(super::TextFont::new("Arial\u{0}").is_err());
4635 }
4636
4637 #[test]
4638 fn fx206_picture_bullet_list_equality_preserves_foreign_and_opaque_owners() {
4639 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>"#;
4640 let body = CT_TextBody::from_xml(source).unwrap();
4641 let saved = body.to_xml().unwrap();
4642 let reopened = CT_TextBody::from_xml(&saved).unwrap();
4643 assert_eq!(reopened, body);
4644 assert_eq!(reopened.to_xml().unwrap(), saved);
4645 let changed_owner = String::from_utf8_lossy(source).replace("rId3", "rId4");
4646 assert_ne!(
4647 body,
4648 CT_TextBody::from_xml(changed_owner.as_bytes()).unwrap()
4649 );
4650 assert!(String::from_utf8_lossy(&saved).contains("r:embed=\"rId3\""));
4651
4652 let plain = crate::text::CT_TextListStyle::from_xml(
4653 br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>"#,
4654 )
4655 .unwrap();
4656 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();
4657 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();
4658 let opaque = crate::text::CT_TextListStyle::from_xml(br#"<a:lstStyle xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" keep="opaque"/>"#).unwrap();
4659 assert_eq!(plain, canonical);
4660 assert_ne!(plain, foreign);
4661 assert_ne!(plain, opaque);
4662 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();
4663 assert_ne!(plain, opaque_child);
4664 assert!(
4665 String::from_utf8_lossy(&foreign.to_xml().unwrap()).contains("xmlns:r=\"urn:foreign\"")
4666 );
4667 assert!(String::from_utf8_lossy(&opaque.to_xml().unwrap()).contains("keep=\"opaque\""));
4668
4669 let first = CT_TextParagraphProperties::default();
4670 let mut transport = first.clone();
4671 transport
4672 .raw_attributes
4673 .push(("xmlns:r".into(), super::TEXT_REL_NS.into()));
4674 assert_eq!(first, transport);
4675 transport.raw_attributes[0].1 = "urn:foreign".into();
4676 assert_ne!(first, transport);
4677 transport.raw_attributes.clear();
4678 transport
4679 .raw_attributes
4680 .push(("keep".into(), "opaque".into()));
4681 assert_ne!(first, transport);
4682 transport.raw_attributes.clear();
4683 transport.level = Some(1);
4684 assert_ne!(first, transport);
4685 }
4686
4687 #[test]
4688 fn fx206_hyperlink_equality_ignores_only_canonical_transport_declarations() {
4689 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();
4690 let bytes = body.to_xml().unwrap();
4691 assert_eq!(CT_TextBody::from_xml(&bytes).unwrap(), body);
4692 let mut first = TextHyperlink::default();
4693 let mut canonical = first.clone();
4694 canonical.raw_attributes = vec![
4695 ("xmlns:a".into(), super::A_NS.into()),
4696 ("xmlns:r".into(), super::TEXT_REL_NS.into()),
4697 ];
4698 assert_eq!(first, canonical);
4699 first
4700 .raw_attributes
4701 .push(("xmlns:r".into(), "urn:foreign".into()));
4702 assert_ne!(first, canonical);
4703 canonical
4704 .raw_attributes
4705 .push(("foreign".into(), "kept".into()));
4706 assert_ne!(TextHyperlink::default(), canonical);
4707 }
4708
4709 #[test]
4710 fn drawingml_rtl_attribute_becomes_typed_without_reordering_unknown_content() {
4711 let parsed = CT_TextParagraphProperties::from_xml(
4712 br#"<q:pPr xmlns:q="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:x="urn:test" defTabSz="457200" rtl="1" x:keep="yes"/>"#,
4713 )
4714 .unwrap();
4715
4716 assert_eq!(parsed.right_to_left, Some(true));
4717 let serialized = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
4718 assert!(serialized.contains("rtl=\"1\""));
4719 assert!(serialized.contains("x:keep=\"yes\""));
4720 assert!(
4721 serialized.find("defTabSz").unwrap() < serialized.find("rtl").unwrap(),
4722 "{serialized}"
4723 );
4724
4725 let mut changed = parsed;
4726 changed.right_to_left = Some(false);
4727 let serialized = String::from_utf8(changed.to_xml().unwrap()).unwrap();
4728 assert!(serialized.contains("rtl=\"0\""), "{serialized}");
4729 assert!(!serialized.contains("rtl=\"1\""), "{serialized}");
4730 }
4731
4732 #[test]
4733 fn leading_and_trailing_text_whitespace_survives_via_xml_space_preserve() {
4734 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>"#;
4735 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
4736 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>"#);
4737
4738 let TextRun::Run(first) = ¶graph.runs[0] else {
4739 panic!("expected regular run");
4740 };
4741 assert_eq!(first.text.value, " leading");
4742 assert_eq!(first.text.space, TextSpace::Preserve);
4743
4744 let body = CT_TextBody::from_xml(
4745 br#"<q:txBody><q:bodyPr/><q:p><q:r><q:t> body </q:t></q:r></q:p></q:txBody>"#,
4746 )
4747 .unwrap();
4748 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>"#);
4749
4750 let hostile = CT_TextParagraph::from_xml(
4751 br#"<q:p><q:r><q:t x:space="preserve">plain</q:t></q:r></q:p>"#,
4752 )
4753 .unwrap();
4754 let TextRun::Run(run) = &hostile.runs[0] else {
4755 panic!("expected regular run");
4756 };
4757 assert_eq!(run.text.space, TextSpace::Default);
4758 assert_eq!(
4759 hostile.to_xml().unwrap(),
4760 br#"<a:p><a:r><a:t x:space="preserve">plain</a:t></a:r></a:p>"#
4761 );
4762 }
4763
4764 #[test]
4765 fn canonical_text_state_matches_whitespace_and_empty_hyperlinks_from_real_decks() {
4766 let paragraph = CT_TextParagraph::from_xml(
4767 br#"<q:p><q:r><q:rPr><q:hlinkClick r:id=""/></q:rPr><q:t>trailing </q:t></q:r></q:p>"#,
4768 )
4769 .unwrap();
4770 let TextRun::Run(run) = ¶graph.runs[0] else {
4771 panic!("expected regular run");
4772 };
4773 assert_eq!(run.text.space, TextSpace::Preserve);
4774 assert_eq!(
4775 run.properties
4776 .as_ref()
4777 .and_then(|properties| properties.hyperlink_click.as_ref())
4778 .and_then(|hyperlink| hyperlink.relationship_id.as_deref()),
4779 Some("")
4780 );
4781 let written = paragraph.to_xml().unwrap();
4782 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
4783 }
4784
4785 #[test]
4786 fn text_written_into_an_empty_paragraph_takes_its_end_formatting() {
4787 let mut paragraph = CT_TextParagraph::from_xml(
4788 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>"#,
4789 )
4790 .unwrap();
4791 paragraph.set_text("Risk");
4792 assert_eq!(
4793 String::from_utf8(paragraph.to_xml().unwrap()).unwrap(),
4794 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>"#
4795 );
4796
4797 let saved = paragraph.to_xml().unwrap();
4798 assert_eq!(
4799 CT_TextParagraph::from_xml(&saved)
4800 .unwrap()
4801 .to_xml()
4802 .unwrap(),
4803 saved
4804 );
4805
4806 let mut formatted = CT_TextParagraph::from_xml(
4807 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>"#,
4808 )
4809 .unwrap();
4810 formatted.set_text("new");
4811 assert_eq!(
4812 String::from_utf8(formatted.to_xml().unwrap()).unwrap(),
4813 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>"#
4814 );
4815
4816 let mut bare = CT_TextParagraph::default();
4817 bare.set_text("plain");
4818 assert_eq!(
4819 bare.to_xml().unwrap(),
4820 br#"<a:p><a:r><a:t>plain</a:t></a:r></a:p>"#
4821 );
4822 }
4823
4824 #[test]
4825 fn paragraph_runs_fields_and_breaks_round_trip_structurally() {
4826 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>"#;
4827 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
4828 assert_eq!(paragraph.runs.len(), 3);
4829 let written = paragraph.to_xml().unwrap();
4830 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>"#);
4831 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
4832
4833 let hostile = CT_TextCharacterProperties::from_xml(
4834 br#"<q:rPr><q:hlinkClick x:id="not-a-relationship"/></q:rPr>"#,
4835 )
4836 .unwrap();
4837 assert!(
4838 hostile
4839 .hyperlink_click
4840 .as_ref()
4841 .unwrap()
4842 .relationship_id
4843 .is_none()
4844 );
4845 let mut writer = quick_xml::Writer::new(Vec::new());
4846 hostile.write_xml(&mut writer, "a:rPr").unwrap();
4847 assert_eq!(
4848 writer.into_inner(),
4849 br#"<a:rPr><a:hlinkClick x:id="not-a-relationship"/></a:rPr>"#
4850 );
4851 }
4852
4853 #[test]
4854 fn a_later_paragraph_properties_element_is_preserved_where_it_stands() {
4855 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>"#;
4856 let mut paragraph = CT_TextParagraph::from_xml(xml).unwrap();
4857 let properties = paragraph.properties.as_ref().unwrap();
4858 assert_eq!(properties.alignment, Some(TextAlignment::Left));
4859 assert_eq!(properties.line_spacing, None);
4860 let texts = paragraph
4861 .runs
4862 .iter()
4863 .map(|run| match run {
4864 TextRun::Run(run) => run.text.value.as_str(),
4865 TextRun::Break(_) | TextRun::Field(_) => panic!("expected regular runs"),
4866 })
4867 .collect::<Vec<_>>();
4868 assert_eq!(texts, ["One.", "Two."]);
4869 assert_eq!(paragraph.to_xml().unwrap(), xml);
4870 assert_eq!(CT_TextParagraph::from_xml(xml).unwrap(), paragraph);
4871
4872 let TextRun::Run(first) = &mut paragraph.runs[0] else {
4873 panic!("expected regular run");
4874 };
4875 first.set_text("Uno.");
4876 paragraph.properties_mut().level = Some(1);
4877 paragraph.add_run("Three.");
4878 assert_eq!(
4879 paragraph.to_xml().unwrap(),
4880 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>"#
4881 );
4882
4883 paragraph.set_text("Whole.");
4884 assert_eq!(
4885 paragraph.to_xml().unwrap(),
4886 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>"#
4887 );
4888
4889 let late = CT_TextParagraph::from_xml(
4890 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>"#,
4891 )
4892 .unwrap();
4893 assert_eq!(
4894 late.properties.as_ref().unwrap().alignment,
4895 Some(TextAlignment::Left)
4896 );
4897 assert_eq!(
4898 late.to_xml().unwrap(),
4899 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>"#
4900 );
4901 }
4902
4903 #[test]
4904 fn fx206_foreign_highlight_does_not_collide_with_owned_highlight() {
4905 let foreign =
4906 br#"<x:highlight xmlns:x="urn:producer"><x:payload code="kept"/></x:highlight>"#;
4907 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>"#;
4908 let properties = CT_TextCharacterProperties::from_xml(xml)
4909 .expect("foreign extension and owned highlight are different expanded names");
4910 let mut writer = quick_xml::Writer::new(Vec::new());
4911 properties.write_xml(&mut writer, "a:rPr").unwrap();
4912 let written = writer.into_inner();
4913 assert!(written.windows(foreign.len()).any(|part| part == foreign));
4914 assert_eq!(
4915 String::from_utf8_lossy(&written)
4916 .matches("<a:highlight>")
4917 .count(),
4918 1
4919 );
4920 assert!(CT_TextCharacterProperties::from_xml(&written).is_ok());
4921 }
4922
4923 #[test]
4924 fn fx206_typed_highlight_qualified_colour_preserves_opaque_and_source_order() {
4925 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>"#;
4926 let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
4927 let colour = properties.highlight.as_ref().unwrap().clone();
4928 assert_eq!(
4929 colour.transforms().len(),
4930 1,
4931 "foreign alpha is not an owned colour transform"
4932 );
4933 assert!(!colour.raw_children().is_empty());
4934 let mut writer = Writer::new(Vec::new());
4935 properties.write_xml(&mut writer, "a:rPr").unwrap();
4936 assert_eq!(writer.into_inner(), xml);
4937 let before = properties.clone();
4938 assert!(properties.set_highlight(None).is_err());
4939 assert_eq!(properties, before, "opaque clear refuses before mutation");
4940 let mut replacement = colour.clone();
4941 if let ColorChoice::Srgb { value, .. } = &mut replacement {
4942 *value = crate::color::RgbColor::new(0, 255, 255);
4943 } else {
4944 panic!("fixture selected exact srgb colour");
4945 }
4946 properties.set_highlight(Some(replacement)).unwrap();
4947 let mut writer = Writer::new(Vec::new());
4948 properties.write_xml(&mut writer, "a:rPr").unwrap();
4949 let updated = writer.into_inner();
4950 let text = std::str::from_utf8(&updated).unwrap();
4951 assert!(text.contains("<x:alpha val=\"12345\"/>"));
4952 assert!(text.contains("<y:keep token=\"a&b\"/>"));
4953 assert!(text.contains("xmlns:y=\"urn:colour\""));
4954 assert!(text.find("<x:beforeColour/>").unwrap() < text.find("val=\"00FFFF\"").unwrap());
4955 assert!(text.find("<x:afterColour/>").unwrap() < text.find("<x:after/>").unwrap());
4956 let reopened = CT_TextCharacterProperties::from_xml(&updated).unwrap();
4957 assert_eq!(reopened.highlight, properties.highlight);
4958 let mut again = Writer::new(Vec::new());
4959 reopened.write_xml(&mut again, "a:rPr").unwrap();
4960 assert_eq!(again.into_inner(), updated);
4961 }
4962
4963 #[test]
4964 fn fx206_highlight_colour_opaque_attributes_refuse_edit_before_publication() {
4965 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>"#;
4966 let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
4967 let before = properties.clone();
4968 assert!(
4969 properties
4970 .set_highlight(Some(ColorChoice::srgb(crate::color::RgbColor::new(
4971 0, 255, 255
4972 ))))
4973 .is_err()
4974 );
4975 assert_eq!(properties, before);
4976 properties.highlight = Some(ColorChoice::srgb(crate::color::RgbColor::new(0, 255, 255)));
4978 let mut writer = Writer::new(Vec::new());
4979 assert!(properties.write_xml(&mut writer, "a:rPr").is_err());
4980 assert!(writer.into_inner().is_empty());
4981 }
4982
4983 #[test]
4984 fn fx206_highlight_transform_opaque_attributes_refuse_destructive_edits() {
4985 for transform in [
4986 r#"<q:alpha val="50000" x:keep="A"/>"#,
4987 r#"<q:alpha val="50000" x:keep="A"> </q:alpha>"#,
4988 ] {
4989 let xml = format!(
4990 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>"#
4991 );
4992 let mut properties = CT_TextCharacterProperties::from_xml(xml.as_bytes()).unwrap();
4993 assert_eq!(properties.highlight.as_ref().unwrap().transforms().len(), 1);
4994 let before = properties.clone();
4995 let mut writer = Writer::new(Vec::new());
4996 properties.write_xml(&mut writer, "a:rPr").unwrap();
4997 assert_eq!(writer.into_inner(), xml.as_bytes());
4998 let mut changed = properties.highlight.clone().unwrap();
4999 if let ColorChoice::Srgb { value, .. } = &mut changed {
5000 *value = crate::color::RgbColor::new(0, 255, 255);
5001 } else {
5002 panic!("fixture must select RGB");
5003 }
5004 for supplied in [Some(changed), None] {
5005 assert!(properties.set_highlight(supplied.clone()).is_err());
5006 assert_eq!(properties, before);
5007 let mut direct = properties.clone();
5008 direct.highlight = supplied;
5009 let mut rejected = Writer::new(Vec::new());
5010 assert!(direct.write_xml(&mut rejected, "a:rPr").is_err());
5011 assert!(rejected.into_inner().is_empty());
5012 }
5013 let mut writer = Writer::new(Vec::new());
5015 properties.write_xml(&mut writer, "a:rPr").unwrap();
5016 let bytes = writer.into_inner();
5017 assert_eq!(bytes, xml.as_bytes());
5018 let reopened = CT_TextCharacterProperties::from_xml(&bytes).unwrap();
5019 assert_eq!(reopened.highlight, before.highlight);
5020 let mut again = Writer::new(Vec::new());
5021 reopened.write_xml(&mut again, "a:rPr").unwrap();
5022 assert_eq!(again.into_inner(), bytes);
5023 }
5024 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>"#;
5026 let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
5027 let mut changed = properties.highlight.clone().unwrap();
5028 assert!(changed.transforms().is_empty());
5029 if let ColorChoice::Srgb { value, .. } = &mut changed {
5030 *value = crate::color::RgbColor::new(0, 255, 255);
5031 }
5032 properties.set_highlight(Some(changed)).unwrap();
5033 let mut writer = Writer::new(Vec::new());
5034 properties.write_xml(&mut writer, "a:rPr").unwrap();
5035 let updated = writer.into_inner();
5036 assert!(
5037 std::str::from_utf8(&updated)
5038 .unwrap()
5039 .contains(r#"<x:alpha val="50000" x:keep="A"/>"#)
5040 );
5041 let reopened = CT_TextCharacterProperties::from_xml(&updated).unwrap();
5042 assert_eq!(reopened.highlight, properties.highlight);
5043 }
5044
5045 #[test]
5046 fn fx206_duplicate_owned_highlight_is_refused_control() {
5047 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>"#;
5048 assert!(CT_TextCharacterProperties::from_xml(xml).is_err());
5049 }
5050
5051 #[test]
5052 fn fx206_known_small_cap_is_owned_while_unknown_cap_remains_raw() {
5053 let small =
5054 CT_TextCharacterProperties::from_xml(br#"<a:rPr cap="small" lang="en-US"/>"#).unwrap();
5055 let unknown =
5056 CT_TextCharacterProperties::from_xml(br#"<a:rPr cap="producer" lang="en-US"/>"#)
5057 .unwrap();
5058 assert_eq!(
5059 small.all_caps, None,
5060 "legacy all-caps getter for small stays unset"
5061 );
5062 assert!(
5063 unknown
5064 .raw_attributes
5065 .iter()
5066 .any(|(name, value)| name == "cap" && value == "producer")
5067 );
5068 assert!(
5069 !small.raw_attributes.iter().any(|(name, _)| name == "cap"),
5070 "schema small-cap mode must have typed ownership, not an opaque attribute"
5071 );
5072 let mut writer = quick_xml::Writer::new(Vec::new());
5073 small.write_xml(&mut writer, "a:rPr").unwrap();
5074 assert_eq!(writer.into_inner(), br#"<a:rPr cap="small" lang="en-US"/>"#);
5075 }
5076
5077 #[test]
5078 fn fx206_conflicting_caps_refuse_before_serialized_output() {
5079 let properties = CT_TextCharacterProperties {
5080 all_caps: Some(true),
5081 small_caps: Some(true),
5082 ..Default::default()
5083 };
5084 let mut writer = quick_xml::Writer::new(Vec::new());
5085 assert!(properties.write_xml(&mut writer, "a:rPr").is_err());
5086 assert!(writer.into_inner().is_empty());
5087 }
5088
5089 #[test]
5090 fn fx206_checked_cap_updates_select_explicit_modes_and_preserve_other_on_none() {
5091 for initial in ["all", "small", "none"] {
5092 for update_all in [true, false] {
5093 for value in [None, Some(false), Some(true)] {
5094 let input = format!(r#"<a:rPr cap="{initial}"/>"#);
5095 let mut properties =
5096 CT_TextCharacterProperties::from_xml(input.as_bytes()).unwrap();
5097 if update_all {
5098 properties.set_all_caps(value);
5099 } else {
5100 properties.set_small_caps(value);
5101 }
5102 let expected = match value {
5103 Some(false) => Some("none"),
5104 Some(true) if update_all => Some("all"),
5105 Some(true) => Some("small"),
5106 None if update_all && initial == "small" => Some("small"),
5107 None if !update_all && initial == "all" => Some("all"),
5108 None if initial == "none" => Some("none"),
5109 None => None,
5110 };
5111 let mut writer = quick_xml::Writer::new(Vec::new());
5112 properties.write_xml(&mut writer, "a:rPr").unwrap();
5113 let written = writer.into_inner();
5114 let expected_xml = expected.map_or_else(
5115 || "<a:rPr/>".to_owned(),
5116 |mode| format!(r#"<a:rPr cap="{mode}"/>"#),
5117 );
5118 assert_eq!(
5119 written,
5120 expected_xml.as_bytes(),
5121 "initial={initial}, update_all={update_all}, value={value:?}"
5122 );
5123 let reparsed = CT_TextCharacterProperties::from_xml(&written).unwrap();
5124 let mut reopened_writer = quick_xml::Writer::new(Vec::new());
5125 reparsed.write_xml(&mut reopened_writer, "a:rPr").unwrap();
5126 assert_eq!(reopened_writer.into_inner(), written);
5127 let (requested, other) = if update_all {
5128 (properties.all_caps, properties.small_caps)
5129 } else {
5130 (properties.small_caps, properties.all_caps)
5131 };
5132 assert_eq!(requested, value);
5133 if value.is_some() {
5134 assert_eq!(other, None, "explicit mode must clear the other flag");
5135 }
5136 assert!(
5137 !(properties.all_caps == Some(true) && properties.small_caps == Some(true))
5138 );
5139 }
5140 }
5141 }
5142 }
5143
5144 #[test]
5145 fn fx206_ancestor_highlight_scope_survives_typed_edit_and_foreign_shadow() {
5146 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>"#;
5147 let mut run = super::CT_RegularTextRun::from_xml(source).unwrap();
5148 let colour = run
5149 .properties
5150 .as_ref()
5151 .unwrap()
5152 .highlight
5153 .as_ref()
5154 .expect("ancestor-owned highlight must be typed, not opaque")
5155 .clone();
5156 let ColorChoice::Srgb { value, .. } = &colour else {
5157 panic!("source owner must resolve srgb");
5158 };
5159 assert_eq!(*value, crate::color::RgbColor::new(255, 0, 0));
5160 let save = |run: &super::CT_RegularTextRun| {
5161 let mut writer = quick_xml::Writer::new(Vec::new());
5162 run.write_xml(&mut writer).unwrap();
5163 writer.into_inner()
5164 };
5165 let first = save(&run);
5166 assert_eq!(
5167 save(&super::CT_RegularTextRun::from_xml(&first).unwrap()),
5168 first
5169 );
5170 let mut replacement = colour.clone();
5171 let ColorChoice::Srgb { value, .. } = &mut replacement else {
5172 unreachable!();
5173 };
5174 *value = crate::color::RgbColor::new(0, 255, 255);
5175 run.properties
5176 .as_mut()
5177 .unwrap()
5178 .set_highlight(Some(replacement))
5179 .unwrap();
5180 let changed = save(&run);
5181 for raw in [
5182 b"<q:highlight xmlns:q='urn:foreign'><f:payload code='A'/></q:highlight>"
5183 .as_slice(),
5184 b"<f:extension code='B'/>".as_slice(),
5185 ] {
5186 assert!(changed.windows(raw.len()).any(|part| part == raw));
5187 }
5188 let reopened = super::CT_RegularTextRun::from_xml(&changed).unwrap();
5189 let ColorChoice::Srgb { value, .. } = reopened
5190 .properties
5191 .as_ref()
5192 .unwrap()
5193 .highlight
5194 .as_ref()
5195 .unwrap()
5196 else {
5197 panic!("saved owner must remain srgb");
5198 };
5199 assert_eq!(*value, crate::color::RgbColor::new(0, 255, 255));
5200 assert_eq!(save(&reopened), changed);
5201 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>"#;
5202 assert!(super::CT_RegularTextRun::from_xml(duplicate).is_err());
5203 }
5204
5205 #[test]
5206 fn fx206_highlight_admits_alias_and_default_namespace_without_foreign_collision() {
5207 for xml in [
5208 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(),
5209 br#"<rPr xmlns="http://schemas.openxmlformats.org/drawingml/2006/main"><highlight><srgbClr val="FFFF00"/></highlight><highlight xmlns="urn:foreign"/></rPr>"#.as_slice(),
5210 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(),
5211 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(),
5212 ] {
5213 let properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
5214 let mut writer = quick_xml::Writer::new(Vec::new());
5215 properties.write_xml(&mut writer, "a:rPr").unwrap();
5216 let written = writer.into_inner();
5217 assert!(String::from_utf8_lossy(&written).contains("urn:foreign"));
5218 assert!(String::from_utf8_lossy(&written).contains("FFFF00"));
5219 assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), properties);
5220 }
5221 }
5222
5223 #[test]
5224 fn character_all_and_small_caps_round_trip_as_distinct_modes() {
5225 let all = CT_TextCharacterProperties::from_xml(
5226 br#"<q:rPr cap="all" sz="1800" x:producer="kept"/>"#,
5227 )
5228 .unwrap();
5229 assert_eq!(all.all_caps, Some(true));
5230 let mut writer = quick_xml::Writer::new(Vec::new());
5231 all.write_xml(&mut writer, "a:rPr").unwrap();
5232 let written = writer.into_inner();
5233 assert_eq!(
5234 written,
5235 br#"<a:rPr sz="1800" cap="all" x:producer="kept"/>"#
5236 );
5237 assert_eq!(CT_TextCharacterProperties::from_xml(&written).unwrap(), all);
5238
5239 let none = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="none"/>"#).unwrap();
5240 assert_eq!(none.all_caps, Some(false));
5241
5242 let small = CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="small"/>"#).unwrap();
5243 assert_eq!(small.all_caps, None);
5244 assert_eq!(small.small_caps, Some(true));
5245 let mut writer = quick_xml::Writer::new(Vec::new());
5246 small.write_xml(&mut writer, "a:rPr").unwrap();
5247 assert_eq!(writer.into_inner(), br#"<a:rPr cap="small"/>"#);
5248 }
5249
5250 #[test]
5251 fn setting_all_caps_over_preserved_unknown_caps_writes_one_cap_attribute() {
5252 let mut properties =
5253 CT_TextCharacterProperties::from_xml(br#"<q:rPr cap="producer" lang="en-US"/>"#)
5254 .unwrap();
5255 properties.all_caps = Some(true);
5256 let mut writer = quick_xml::Writer::new(Vec::new());
5257 properties.write_xml(&mut writer, "a:rPr").unwrap();
5258 let written = writer.into_inner();
5259 assert_eq!(written, br#"<a:rPr cap="all" lang="en-US"/>"#);
5260 assert_eq!(
5261 CT_TextCharacterProperties::from_xml(&written)
5262 .unwrap()
5263 .all_caps,
5264 Some(true)
5265 );
5266
5267 properties.all_caps = None;
5268 let mut writer = quick_xml::Writer::new(Vec::new());
5269 properties.write_xml(&mut writer, "a:rPr").unwrap();
5270 assert_eq!(
5271 writer.into_inner(),
5272 br#"<a:rPr cap="producer" lang="en-US"/>"#
5273 );
5274 }
5275
5276 #[test]
5277 fn spacing_percentages_above_two_lines_parse_up_to_the_schema_maximum() {
5278 let xml = br#"<q:pPr><q:lnSpc><q:spcPct val="250000"/></q:lnSpc><q:spcBef><q:spcPct val="13200000"/></q:spcBef></q:pPr>"#;
5279 let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
5280 assert_eq!(
5281 properties.line_spacing,
5282 Some(TextSpacing::Percent("250000".to_owned()))
5283 );
5284 assert_eq!(
5285 properties.space_before,
5286 Some(TextSpacing::Percent("13200000".to_owned()))
5287 );
5288 assert_eq!(
5289 properties.to_xml().unwrap(),
5290 br#"<a:pPr><a:lnSpc><a:spcPct val="250000"/></a:lnSpc><a:spcBef><a:spcPct val="13200000"/></a:spcBef></a:pPr>"#
5291 );
5292 }
5293
5294 #[test]
5295 fn setting_a_bullet_part_removes_the_preserved_alternative_it_replaces() {
5296 let mut properties = CT_TextParagraphProperties::from_xml(
5297 br#"<q:pPr><q:buClrTx/><q:buSzTx/><q:buFontTx/><q:buBlip><q:blip x:embed="rId9"/></q:buBlip><q:tabLst/></q:pPr>"#,
5298 )
5299 .unwrap();
5300 assert!(properties.has_picture_bullet());
5301
5302 properties.set_bullet(Some(TextBullet {
5303 choice: Some(TextBulletChoice::Character(
5304 TextBulletCharacter::new("*").unwrap(),
5305 )),
5306 ..TextBullet::default()
5307 }));
5308 assert!(!properties.has_picture_bullet());
5309 assert_eq!(
5310 properties.to_xml().unwrap(),
5311 br#"<a:pPr><q:buClrTx/><q:buSzTx/><q:buFontTx/><a:buChar char="*"/><q:tabLst/></a:pPr>"#
5312 );
5313
5314 properties.set_bullet(None);
5315 assert_eq!(
5316 properties.to_xml().unwrap(),
5317 br#"<a:pPr><q:tabLst/></a:pPr>"#
5318 );
5319 }
5320
5321 #[test]
5322 fn paragraph_and_run_properties_use_drawingml_units_and_schema_order() {
5323 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();
5324 let written = {
5325 let mut writer = quick_xml::Writer::new(Vec::new());
5326 paragraph_properties
5327 .write_xml(&mut writer, "a:pPr")
5328 .unwrap();
5329 writer.into_inner()
5330 };
5331 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>"#);
5332
5333 let run_properties = CT_TextCharacterProperties::from_xml(
5334 br#"<q:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#,
5335 )
5336 .unwrap();
5337 let mut writer = quick_xml::Writer::new(Vec::new());
5338 run_properties.write_xml(&mut writer, "a:rPr").unwrap();
5339 assert_eq!(
5340 writer.into_inner(),
5341 br#"<a:rPr sz="100" spc="1.25pt" baseline="-100000"/>"#
5342 );
5343
5344 let mut inserted_spacing =
5345 CT_TextParagraphProperties::from_xml(br#"<q:pPr><q:buChar char="*"/></q:pPr>"#)
5346 .unwrap();
5347 inserted_spacing.line_spacing = Some(TextSpacing::Percent("100000".to_owned()));
5348 let mut writer = quick_xml::Writer::new(Vec::new());
5349 inserted_spacing.write_xml(&mut writer, "a:pPr").unwrap();
5350 assert_eq!(
5351 writer.into_inner(),
5352 br#"<a:pPr><a:lnSpc><a:spcPct val="100000"/></a:lnSpc><a:buChar char="*"/></a:pPr>"#
5353 );
5354 }
5355
5356 #[test]
5357 fn every_modelled_bullet_form_round_trips_in_schema_order() {
5358 let cases: &[(&[u8], &[u8])] = &[
5359 (
5360 br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/></q:buClr><q:buSzPct val="125000"/><q:buFont typeface="Wingdings"/><q:buChar char="*"/></q:pPr>"#,
5361 br#"<a:pPr><a:buClr><a:srgbClr val="AABBCC"/></a:buClr><a:buSzPct val="125000"/><a:buFont typeface="Wingdings"/><a:buChar char="*"/></a:pPr>"#,
5362 ),
5363 (
5364 br#"<q:pPr><q:buSzPts val="1800"/><q:buAutoNum type="arabicPeriod" startAt="3"/></q:pPr>"#,
5365 br#"<a:pPr><a:buSzPts val="1800"/><a:buAutoNum type="arabicPeriod" startAt="3"/></a:pPr>"#,
5366 ),
5367 (br#"<q:pPr><q:buNone/></q:pPr>"#, br#"<a:pPr><a:buNone/></a:pPr>"#),
5368 ];
5369
5370 for (xml, expected) in cases {
5371 let properties = CT_TextParagraphProperties::from_xml(xml).unwrap();
5372 let mut writer = quick_xml::Writer::new(Vec::new());
5373 properties.write_xml(&mut writer, "a:pPr").unwrap();
5374 let written = writer.into_inner();
5375 assert_eq!(written, *expected);
5376 assert_eq!(
5377 CT_TextParagraphProperties::from_xml(&written).unwrap(),
5378 properties
5379 );
5380 }
5381
5382 let character = CT_TextParagraphProperties::from_xml(cases[0].0).unwrap();
5383 let bullet = character.bullet.as_ref().unwrap();
5384 assert!(matches!(
5385 bullet.choice,
5386 Some(TextBulletChoice::Character(_))
5387 ));
5388 assert_eq!(bullet.font.as_ref().unwrap().typeface, "Wingdings");
5389 assert!(matches!(
5390 bullet.size.as_ref().unwrap().value,
5391 TextBulletSizeValue::Percent(ref value) if value == "125000"
5392 ));
5393 assert!(matches!(
5394 bullet.color.as_ref().unwrap().color,
5395 ColorChoice::Srgb { .. }
5396 ));
5397
5398 let automatic = CT_TextParagraphProperties::from_xml(cases[1].0).unwrap();
5399 let Some(TextBulletChoice::AutoNumber(numbering)) =
5400 automatic.bullet.as_ref().unwrap().choice.as_ref()
5401 else {
5402 panic!("expected automatic-number bullet");
5403 };
5404 assert_eq!(numbering.scheme, TextAutoNumberScheme::ArabicPeriod);
5405 assert_eq!(numbering.start_at, Some(3));
5406 assert!(matches!(
5407 automatic
5408 .bullet
5409 .as_ref()
5410 .unwrap()
5411 .size
5412 .as_ref()
5413 .unwrap()
5414 .value,
5415 TextBulletSizeValue::Points(1800)
5416 ));
5417
5418 let none = CT_TextParagraphProperties::from_xml(cases[2].0).unwrap();
5419 assert!(matches!(
5420 none.bullet.as_ref().unwrap().choice,
5421 Some(TextBulletChoice::None(_))
5422 ));
5423 }
5424
5425 #[test]
5426 fn bullet_font_size_and_colour_keep_their_schema_positions() {
5427 let properties = CT_TextParagraphProperties::from_xml(
5428 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>"#,
5429 )
5430 .unwrap();
5431 let mut writer = quick_xml::Writer::new(Vec::new());
5432 properties.write_xml(&mut writer, "a:pPr").unwrap();
5433 assert_eq!(
5434 writer.into_inner(),
5435 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>"#
5436 );
5437 }
5438
5439 #[test]
5440 fn unknown_bullet_children_round_trip_byte_for_byte() {
5441 let properties = CT_TextParagraphProperties::from_xml(
5442 br#"<q:pPr><q:buClrTx x:mode="stay"><x:nested>one & two</x:nested><!--note--></q:buClrTx><q:buChar char="*"/><x:after/></q:pPr>"#,
5443 )
5444 .unwrap();
5445 let mut writer = quick_xml::Writer::new(Vec::new());
5446 properties.write_xml(&mut writer, "a:pPr").unwrap();
5447 assert_eq!(
5448 writer.into_inner(),
5449 br#"<a:pPr><q:buClrTx x:mode="stay"><x:nested>one & two</x:nested><!--note--></q:buClrTx><a:buChar char="*"/><x:after/></a:pPr>"#
5450 );
5451 }
5452
5453 #[test]
5454 fn malformed_bullet_values_return_errors_without_panicking() {
5455 let cases: &[&[u8]] = &[
5456 br#"<q:pPr><q:buChar/></q:pPr>"#,
5457 br#"<q:pPr><q:buChar char=""/></q:pPr>"#,
5458 br#"<q:pPr><q:buSzPct val="24999"/></q:pPr>"#,
5459 br#"<q:pPr><q:buSzPct val="400001"/></q:pPr>"#,
5460 br#"<q:pPr><q:buSzPts val="99"/></q:pPr>"#,
5461 br#"<q:pPr><q:buSzPts val="400001"/></q:pPr>"#,
5462 br#"<q:pPr><q:buAutoNum type="unknown"/></q:pPr>"#,
5463 br#"<q:pPr><q:buAutoNum/></q:pPr>"#,
5464 br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="0"/></q:pPr>"#,
5465 br#"<q:pPr><q:buAutoNum type="arabicPeriod" startAt="32768"/></q:pPr>"#,
5466 br#"<q:pPr><q:buFont/></q:pPr>"#,
5467 br#"<q:pPr><q:buClr/></q:pPr>"#,
5468 br#"<q:pPr><q:buClr><q:srgbClr x:val="AABBCC"/></q:buClr></q:pPr>"#,
5469 br#"<q:pPr><q:buClr><q:srgbClr val="GGGGGG"/></q:buClr></q:pPr>"#,
5470 br#"<q:pPr><q:buClr><q:srgbClr val="AABBCC"/><q:schemeClr val="accent1"/></q:buClr></q:pPr>"#,
5471 br#"<q:pPr><q:buChar char="*"/><q:buNone/></q:pPr>"#,
5472 br#"<q:pPr><q:buSzPct val="100000"/><q:buSzPts val="1200"/></q:pPr>"#,
5473 ];
5474
5475 for xml in cases {
5476 let result = panic::catch_unwind(|| CT_TextParagraphProperties::from_xml(xml));
5477 assert!(result.is_ok(), "bullet parser panicked");
5478 assert!(result.unwrap().is_err(), "malformed bullet parsed");
5479 }
5480 }
5481
5482 #[test]
5483 fn malformed_text_content_returns_errors_without_panicking() {
5484 let cases: &[&[u8]] = &[
5485 br#"<q:r/>"#,
5486 br#"<q:r><q:t><x:nested/></q:t></q:r>"#,
5487 br#"<q:r><q:t xml:space="keep">x</q:t></q:r>"#,
5488 br#"<q:r><q:t>x</q:t><q:t>y</q:t></q:r>"#,
5489 br#"<q:pPr lvl="9"/>"#,
5490 br#"<q:pPr marL="-1"/>"#,
5491 br#"<q:pPr indent="51206401"/>"#,
5492 br#"<q:pPr algn="middle"/>"#,
5493 br#"<q:pPr><q:lnSpc/></q:pPr>"#,
5494 br#"<q:pPr><q:lnSpc><q:spcPts val="158401"/></q:lnSpc></q:pPr>"#,
5495 br#"<q:pPr><q:lnSpc><q:spcPct val="13200001"/></q:lnSpc></q:pPr>"#,
5496 br#"<q:rPr sz="99"/>"#,
5497 br#"<q:rPr b="yes"/>"#,
5498 br#"<q:rPr u="triple"/>"#,
5499 br#"<q:rPr spc="400001"/>"#,
5500 r#"<q:rPr spc="éx"/>"#.as_bytes(),
5501 br#"<q:rPr baseline="100001"/>"#,
5502 br#"<q:rPr><q:latin/></q:rPr>"#,
5503 br#"<q:fld type="slidenum"/>"#,
5504 br#"<q:fld id="not-a-guid"/>"#,
5505 br#"<q:p><q:endParaRPr/><q:endParaRPr/></q:p>"#,
5506 br#"<q:notPPr/>"#,
5507 br#"<q:notRPr/>"#,
5508 ];
5509 for xml in cases {
5510 let result = panic::catch_unwind(|| match super::root_local_name(xml).as_deref() {
5511 Ok(b"r") => super::CT_RegularTextRun::from_xml(xml).map(|_| ()),
5512 Ok(b"pPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
5513 Ok(b"rPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
5514 Ok(b"fld") => super::CT_TextField::from_xml(xml).map(|_| ()),
5515 Ok(b"p") => CT_TextParagraph::from_xml(xml).map(|_| ()),
5516 Ok(b"notPPr") => CT_TextParagraphProperties::from_xml(xml).map(|_| ()),
5517 Ok(b"notRPr") => CT_TextCharacterProperties::from_xml(xml).map(|_| ()),
5518 _ => unreachable!(),
5519 });
5520 assert!(result.is_ok(), "text parser panicked");
5521 assert!(result.unwrap().is_err(), "malformed text parsed");
5522 }
5523 }
5524
5525 #[test]
5526 fn characters_xml_cannot_carry_are_stored_escaped_and_never_decoded() {
5527 let mut paragraph = CT_TextParagraph::default();
5528 paragraph.add_run("a\u{1}b\u{b}c\u{1f}d\u{fffe}e\u{0}");
5529 let TextRun::Run(run) = ¶graph.runs[0] else {
5530 panic!("expected a run");
5531 };
5532 assert_eq!(
5533 run.text.value, "a_x0001_b_x000B_c_x001F_d_xFFFE_e_x0000_",
5534 "the setter stores the escaped form, as python-pptx does"
5535 );
5536 let written = paragraph.to_xml().unwrap();
5537 assert_eq!(
5538 written,
5539 br#"<a:p><a:r><a:t>a_x0001_b_x000B_c_x001F_d_xFFFE_e_x0000_</a:t></a:r></a:p>"#
5540 );
5541 let declared = [
5542 br#"<a:p xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main""#.as_slice(),
5543 &written[4..],
5544 ]
5545 .concat();
5546 oxml_core::xml::validate_strict_xml_1_0(&declared)
5547 .expect("the written paragraph is XML 1.0");
5548 assert_eq!(CT_TextParagraph::from_xml(&written).unwrap(), paragraph);
5549
5550 let xml = br#"<a:p><a:r><a:t>C_x0041_D bell_x0007_ _x000b_ _x0001</a:t></a:r></a:p>"#;
5553 let paragraph = CT_TextParagraph::from_xml(xml).unwrap();
5554 let TextRun::Run(run) = ¶graph.runs[0] else {
5555 panic!("expected a run");
5556 };
5557 assert_eq!(run.text.value, "C_x0041_D bell_x0007_ _x000b_ _x0001");
5558 assert_eq!(paragraph.to_xml().unwrap(), xml);
5559 }
5560
5561 #[test]
5562 fn a_vertical_tab_set_as_paragraph_text_becomes_a_line_break() {
5563 let mut paragraph = CT_TextParagraph::from_xml(
5564 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>"#,
5565 )
5566 .unwrap();
5567 paragraph.set_text("one\u{b}two\u{1}\u{b}");
5568 assert_eq!(
5569 String::from_utf8(paragraph.to_xml().unwrap()).unwrap(),
5570 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>"#
5571 );
5572
5573 let saved = paragraph.to_xml().unwrap();
5574 assert_eq!(
5575 CT_TextParagraph::from_xml(&saved)
5576 .unwrap()
5577 .to_xml()
5578 .unwrap(),
5579 saved
5580 );
5581
5582 let mut plain = CT_TextParagraph::default();
5583 plain.set_text("\u{b}");
5584 assert_eq!(
5585 plain.to_xml().unwrap(),
5586 br#"<a:p><a:r><a:t/></a:r><a:br/></a:p>"#
5587 );
5588 }
5589 fn fx206_link_saved(properties: &CT_TextCharacterProperties) -> Vec<u8> {
5590 let mut writer = Writer::new(Vec::new());
5591 properties.write_xml(&mut writer, "a:rPr").unwrap();
5592 writer.into_inner()
5593 }
5594
5595 #[test]
5596 fn fx206_run_link_foreign_owner_and_qualified_relationship_alias_base() {
5597 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();
5598 assert_eq!(
5599 positive
5600 .hyperlink_click
5601 .as_ref()
5602 .unwrap()
5603 .relationship_id
5604 .as_deref(),
5605 Some("rId1")
5606 );
5607 assert!(!fx206_link_saved(&positive).is_empty());
5608 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>"#;
5609 let result = CT_TextCharacterProperties::from_xml(input);
5610 assert!(
5611 result.is_ok(),
5612 "foreign hlinkClick must not consume the owned schema slot: {result:?}"
5613 );
5614 let mut properties = result.unwrap();
5615 assert_eq!(
5616 properties
5617 .hyperlink_click
5618 .as_ref()
5619 .unwrap()
5620 .relationship_id
5621 .as_deref(),
5622 Some("owned")
5623 );
5624 let unchanged = fx206_link_saved(&properties);
5625 assert!(
5626 std::str::from_utf8(&unchanged)
5627 .unwrap()
5628 .contains(r#"<f:hlinkClick rel:id="foreign"><f:opaque/></f:hlinkClick>"#)
5629 );
5630 assert!(
5631 std::str::from_utf8(&unchanged)
5632 .unwrap()
5633 .contains("f:keep='A'")
5634 );
5635 properties.hyperlink_click.as_mut().unwrap().relationship_id = Some("next".to_owned());
5636 let changed = fx206_link_saved(&properties);
5637 let output = std::str::from_utf8(&changed).unwrap();
5638 assert!(output.contains(r#"<f:hlinkClick rel:id="foreign"><f:opaque/></f:hlinkClick>"#));
5639 assert!(output.contains("f:keep='A'"));
5640 assert!(output.contains("<f:payload/>"));
5641 let reopened = CT_TextCharacterProperties::from_xml(&changed).unwrap();
5642 assert_eq!(
5643 reopened
5644 .hyperlink_click
5645 .as_ref()
5646 .unwrap()
5647 .relationship_id
5648 .as_deref(),
5649 Some("next")
5650 );
5651 assert_eq!(fx206_link_saved(&reopened), changed);
5652 }
5653
5654 #[test]
5655 fn fx206_run_link_alias_and_local_rebound_id_are_distinct_base() {
5656 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();
5657 assert_eq!(
5658 positive
5659 .hyperlink_click
5660 .as_ref()
5661 .unwrap()
5662 .relationship_id
5663 .as_deref(),
5664 Some("canonical"),
5665 "independent existing canonical relationship positive setup"
5666 );
5667 assert!(!fx206_link_saved(&positive).is_empty());
5668 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();
5669 assert!(
5670 alias.hyperlink_click.is_some(),
5671 "owned click exists before deciding alias assertion"
5672 );
5673 assert_eq!(
5674 alias
5675 .hyperlink_click
5676 .as_ref()
5677 .unwrap()
5678 .relationship_id
5679 .as_deref(),
5680 Some("owned"),
5681 "deciding relationship-URI alias interpretation"
5682 );
5683 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();
5684 assert_eq!(
5685 foreign.hyperlink_click.as_ref().unwrap().relationship_id,
5686 None
5687 );
5688 assert!(
5689 std::str::from_utf8(&fx206_link_saved(&foreign))
5690 .unwrap()
5691 .contains(r#"r:id="opaque""#)
5692 );
5693 }
5694
5695 #[test]
5696 fn fx206_link_uri_owned_duplicates_refuse_but_foreign_shadow_preserves() {
5697 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""#;
5698 for children in [
5699 r#"<a:hlinkClick r:id="one" rel:id="two"/>"#,
5700 r#"<a:hlinkClick r:id="one"/><a:hlinkClick rel:id="two"/>"#,
5701 ] {
5702 let xml = format!("<a:rPr {namespaces}>{children}</a:rPr>");
5703 assert!(CT_TextCharacterProperties::from_xml(xml.as_bytes()).is_err());
5704 }
5705 let xml = format!(
5706 r#"<a:rPr {namespaces}><!-- lexical --><f:hlinkClick rel:id='opaque'><a:hlinkClick r:id='shadow'/></f:hlinkClick><a:hlinkClick r:id='owned'/></a:rPr>"#
5707 );
5708 let parsed = CT_TextCharacterProperties::from_xml(xml.as_bytes()).unwrap();
5709 assert_eq!(
5710 parsed
5711 .hyperlink_click
5712 .as_ref()
5713 .unwrap()
5714 .relationship_id
5715 .as_deref(),
5716 Some("owned")
5717 );
5718 let saved = fx206_link_saved(&parsed);
5719 let text = std::str::from_utf8(&saved).unwrap();
5720 assert!(text.contains("<!-- lexical -->"));
5721 assert!(text.contains(
5722 "<f:hlinkClick rel:id='opaque'><a:hlinkClick r:id='shadow'/></f:hlinkClick>"
5723 ));
5724 assert_eq!(
5725 fx206_link_saved(&CT_TextCharacterProperties::from_xml(&saved).unwrap()),
5726 saved
5727 );
5728 }
5729
5730 #[test]
5731 fn fx206_owned_alias_root_retains_foreign_a_and_closes_new_click_descendant() {
5732 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>"#;
5733 assert!(CT_TextCharacterProperties::from_xml(br#"<q:rPr xmlns:q="urn:foreign" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"/>"#).is_err());
5734 assert!(
5735 CT_TextCharacterProperties::from_xml(br#"<a:rPr xmlns:a="urn:foreign"/>"#).is_err()
5736 );
5737 let mut properties = CT_TextCharacterProperties::from_xml(xml).unwrap();
5738 assert_eq!(
5739 properties
5740 .hyperlink_click
5741 .as_ref()
5742 .unwrap()
5743 .relationship_id
5744 .as_deref(),
5745 Some("owned")
5746 );
5747 properties.hyperlink_click.as_mut().unwrap().relationship_id =
5748 Some("retargeted".to_owned());
5749 properties.hyperlink_mouse_over = Some(TextHyperlink {
5750 relationship_id: Some("hover".to_owned()),
5751 ..TextHyperlink::default()
5752 });
5753 let saved = fx206_link_saved(&properties);
5754 let text = std::str::from_utf8(&saved).unwrap();
5755 assert!(text.starts_with("<q:rPr"));
5756 for token in [
5757 "xmlns:a='urn:foreign'",
5758 "f:keep='A'",
5759 "<q:latin typeface='Arial' f:font='B'/>",
5760 "<a:hlinkClick rel:id='foreign'><a:payload f:keep='C'/></a:hlinkClick>",
5761 "tooltip='D'",
5762 "<a:payload f:keep='E'/>",
5763 ] {
5764 assert!(text.contains(token), "{token}");
5765 }
5766 assert!(text.contains("<a:hlinkMouseOver"));
5767 assert!(text.contains("xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\""));
5768 let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
5769 assert_eq!(
5770 reopened
5771 .hyperlink_click
5772 .as_ref()
5773 .unwrap()
5774 .relationship_id
5775 .as_deref(),
5776 Some("retargeted")
5777 );
5778 assert_eq!(
5779 reopened
5780 .hyperlink_mouse_over
5781 .as_ref()
5782 .unwrap()
5783 .relationship_id
5784 .as_deref(),
5785 Some("hover")
5786 );
5787 assert_eq!(fx206_link_saved(&reopened), saved);
5788 let mut provenance_only = properties.clone();
5789 provenance_only.source_tag.clear();
5790 provenance_only.source_namespaces.clear();
5791 assert_eq!(provenance_only, properties);
5792 let mut foreign_change = properties.clone();
5793 let attribute = foreign_change
5794 .raw_attributes
5795 .iter_mut()
5796 .find(|(key, _)| key == "f:keep")
5797 .unwrap();
5798 attribute.1 = "different".to_owned();
5799 assert_ne!(foreign_change, properties);
5800 }
5801
5802 #[test]
5803 fn fx206_hyperlink_opaque_unqualified_id_survives_retarget_and_clear() {
5804 for owned in ["", " rel:id='before'"] {
5805 let input = format!(
5806 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>"#
5807 );
5808 let mut properties = CT_TextCharacterProperties::from_xml(input.as_bytes()).unwrap();
5809 assert_eq!(
5810 properties
5811 .hyperlink_click
5812 .as_ref()
5813 .unwrap()
5814 .relationship_id
5815 .as_deref(),
5816 if owned.is_empty() {
5817 None
5818 } else {
5819 Some("before")
5820 }
5821 );
5822 for target in [Some("after"), None] {
5823 properties.hyperlink_click.as_mut().unwrap().relationship_id =
5824 target.map(str::to_owned);
5825 let saved = fx206_link_saved(&properties);
5826 let xml = std::str::from_utf8(&saved).unwrap();
5827 for token in [
5828 "id='A'",
5829 "f:keep='B'",
5830 "<f:opaque/>",
5831 "xmlns:r=\"urn:foreign\"",
5832 ] {
5833 assert!(xml.contains(token), "lost opaque token {token}: {xml}");
5834 }
5835 assert_eq!(xml.matches("id='A'").count(), 1);
5836 let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
5837 assert_eq!(
5838 reopened
5839 .hyperlink_click
5840 .as_ref()
5841 .unwrap()
5842 .relationship_id
5843 .as_deref(),
5844 target
5845 );
5846 assert_eq!(fx206_link_saved(&reopened), saved);
5847 properties = reopened;
5848 }
5849 }
5850 }
5851
5852 #[test]
5853 fn fx206_new_hyperlink_closes_relationship_uri_without_requalifying_raw_payload() {
5854 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>"#;
5855 for raw_namespace in [false, true] {
5856 let mut properties = CT_TextCharacterProperties::from_xml(input).unwrap();
5857 assert!(properties.hyperlink_click.is_none());
5858 let mut click = TextHyperlink {
5859 relationship_id: Some("target".to_owned()),
5860 ..TextHyperlink::default()
5861 };
5862 if raw_namespace {
5863 click
5864 .raw_attributes
5865 .push(("xmlns:r".to_owned(), "urn:opaque-click".to_owned()));
5866 click
5867 .raw_attributes
5868 .push(("r:opaque".to_owned(), "preserved".to_owned()));
5869 }
5870 properties.hyperlink_click = Some(click);
5871 let saved = fx206_link_saved(&properties);
5872 let xml = std::str::from_utf8(&saved).unwrap();
5873 assert!(xml.contains("xmlns:r=\"urn:foreign-r\""));
5874 assert!(xml.contains("<r:opaque id='A'/>"));
5875 if raw_namespace {
5876 assert!(xml.contains("xmlns:r=\"urn:opaque-click\""));
5877 assert!(xml.contains("r:opaque=\"preserved\""));
5878 assert!(xml.contains("xmlns:rLink1=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\""));
5879 } else {
5880 assert!(xml.contains("xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\""));
5881 }
5882 let reopened = CT_TextCharacterProperties::from_xml(&saved).unwrap();
5883 assert_eq!(
5884 reopened
5885 .hyperlink_click
5886 .as_ref()
5887 .unwrap()
5888 .relationship_id
5889 .as_deref(),
5890 Some("target")
5891 );
5892 assert_eq!(fx206_link_saved(&reopened), saved);
5893 }
5894 }
5895 #[test]
5896 fn fx206_lookup_fallbacks_do_not_replace_physical_namespace_provenance() {
5897 let fallback =
5898 CT_TextParagraph::from_xml(br#"<q:p><q:r><q:rPr b="1"/><q:t>text</q:t></q:r></q:p>"#)
5899 .unwrap();
5900 assert_eq!(
5901 fallback.to_xml().unwrap(),
5902 br#"<a:p><a:r><a:rPr b="1"/><a:t>text</a:t></a:r></a:p>"#
5903 );
5904 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>"#;
5905 let physical = CT_TextParagraph::from_xml(input).unwrap();
5906 let saved = physical.to_xml().unwrap();
5907 let xml = std::str::from_utf8(&saved).unwrap();
5908 assert!(xml.starts_with("<q:p "));
5909 assert!(xml.contains("xmlns:a='urn:foreign'"));
5910 assert!(xml.contains("<a:opaque a:keep='A'/>"));
5911 assert!(xml.contains("xmlns:q=\"http://schemas.openxmlformats.org/drawingml/2006/main\""));
5912 assert_eq!(
5913 CT_TextParagraph::from_xml(&saved)
5914 .unwrap()
5915 .to_xml()
5916 .unwrap(),
5917 saved
5918 );
5919 }
5920 #[test]
5921 fn fx206_relocated_run_retains_opaque_inherited_namespace_closure() {
5922 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>"#;
5923 let parsed = super::CT_TextParagraph::from_xml(input).unwrap();
5924 let relocated = super::CT_TextParagraph {
5925 runs: parsed.runs,
5926 ..Default::default()
5927 };
5928 let saved = relocated.to_xml().unwrap();
5929 let xml = std::str::from_utf8(&saved).unwrap();
5930 assert!(xml.contains("<x:opaque x:keep='A'/>"));
5931 assert!(xml.contains("xmlns:x=\"urn:foreign\""));
5932 assert!(
5933 matches!(&relocated.runs[..], [super::TextRun::Run(run)] if run.text.value == "text")
5934 );
5935 assert_eq!(
5936 super::CT_TextParagraph::from_xml(&saved)
5937 .unwrap()
5938 .to_xml()
5939 .unwrap(),
5940 saved
5941 );
5942 }
5943 #[test]
5944 fn fx206_paragraph_attribute_order_is_semantic_but_raw_save_stays_exact() {
5945 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>"#;
5946 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>"#;
5947 let left = CT_TextParagraphProperties::from_xml(left_source).unwrap();
5948 let right = CT_TextParagraphProperties::from_xml(right_source).unwrap();
5949 assert_eq!(left, right);
5950 for (properties, source) in [
5951 (&left, left_source.as_slice()),
5952 (&right, right_source.as_slice()),
5953 ] {
5954 let mut writer = Writer::new(Vec::new());
5955 properties.write_xml(&mut writer, "a:pPr").unwrap();
5956 assert_eq!(writer.into_inner(), source);
5957 }
5958 let changed_value = std::str::from_utf8(right_source)
5960 .unwrap()
5961 .replace("457200", "457201");
5962 assert_ne!(
5963 left,
5964 CT_TextParagraphProperties::from_xml(changed_value.as_bytes()).unwrap()
5965 );
5966 let changed_body = std::str::from_utf8(right_source)
5967 .unwrap()
5968 .replace("<f:opaque/>", "<f:other/>");
5969 assert_ne!(
5970 left,
5971 CT_TextParagraphProperties::from_xml(changed_body.as_bytes()).unwrap()
5972 );
5973 }
5974}
5975
5976fn text_close_inherited_property(raw: &[u8], inherited: &[(String, String)]) -> Result<Vec<u8>> {
5979 let mut reader = Reader::from_reader(raw);
5980 let event = reader.read_event().map_err(OxmlError::from)?;
5981 let empty = matches!(&event, Event::Empty(_));
5982 let start = match event {
5983 Event::Start(start) | Event::Empty(start) => start,
5984 _ => return Ok(raw.to_vec()),
5985 };
5986 let mut content = std::str::from_utf8(start.as_ref())
5987 .map_err(OxmlError::from)?
5988 .to_owned();
5989 let declared = start
5990 .attributes()
5991 .map(|attribute| attribute.map(|attribute| attribute.key.as_ref().to_vec()))
5992 .collect::<std::result::Result<Vec<_>, _>>()
5993 .map_err(OxmlError::from)?;
5994 for (prefix, uri) in inherited {
5995 let key = if prefix.is_empty() {
5996 "xmlns".to_owned()
5997 } else {
5998 format!("xmlns:{prefix}")
5999 };
6000 if !declared.iter().any(|old| old == key.as_bytes()) {
6001 content.push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(uri)));
6002 }
6003 }
6004 if !inherited.iter().any(|(prefix, _)| prefix.is_empty())
6005 && !declared.iter().any(|key| key == b"xmlns")
6006 {
6007 content.push_str(" xmlns=\"\"");
6008 }
6009 let start = BytesStart::from_content(content, start.name().as_ref().len());
6010 let mut writer = Writer::new(Vec::new());
6011 if empty {
6012 write_empty(&mut writer, start)?;
6013 } else {
6014 write_start(&mut writer, start)?;
6015 }
6016 let mut closed = writer.into_inner();
6017 closed.extend_from_slice(&raw[reader.buffer_position() as usize..]);
6018 Ok(closed)
6019}
6020
6021pub(crate) fn text_inherit_property_xml(
6024 local: &[u8],
6025 local_scope: &[(String, String)],
6026 inherited: &[u8],
6027 inherited_scope: &[(String, String)],
6028) -> Result<Vec<u8>> {
6029 let read = |xml: &[u8], scope: &[(String, String)]| -> Result<_> {
6030 let mut reader = Reader::from_reader(xml);
6031 let start = match reader.read_event().map_err(OxmlError::from)? {
6032 Event::Start(start) | Event::Empty(start) => start.into_owned(),
6033 _ => {
6034 return Err(invalid_attribute(
6035 "inherit text properties",
6036 "root",
6037 "missing owner",
6038 ));
6039 }
6040 };
6041 let empty = xml[reader.buffer_position() as usize - 2] == b'/';
6042 let scope = text_fragment_scope(&start, &TextNamespaceScope::inherited(scope))?;
6043 let root_name = local_name(start.name().as_ref()).to_vec();
6044 let mut children = Vec::new();
6045 let mut boundary = 0;
6046 if !empty {
6047 loop {
6048 let event = reader.read_event().map_err(OxmlError::from)?;
6049 let raw = match event {
6050 Event::Start(child) | Event::Empty(child) => {
6051 let child_empty = xml[reader.buffer_position() as usize - 2] == b'/';
6052 let name = text_owned_name(&child, &scope)?;
6053 let slot = if is_character_property_tag(&root_name) {
6054 character_property_slot(&name)
6055 } else if is_paragraph_property_tag(&root_name) {
6056 paragraph_property_slot(&name)
6057 } else if name == b"defPPr" {
6058 Some(1)
6059 } else if root_name != b"extLst" && name == b"extLst" {
6060 Some(11)
6061 } else if root_name != b"extLst" {
6062 name.strip_prefix(b"lvl")
6063 .and_then(|tail| tail.strip_suffix(b"pPr"))
6064 .and_then(|level| {
6065 std::str::from_utf8(level).ok()?.parse::<usize>().ok()
6066 })
6067 .filter(|level| (1..=9).contains(level))
6068 .map(|level| level + 1)
6069 } else {
6070 None
6071 };
6072 if let Some(slot) = slot {
6073 boundary = boundary.max(slot);
6074 }
6075 let raw = if child_empty {
6076 capture_empty_element(&child)?
6077 } else {
6078 capture_element(&mut reader, &child)?
6079 };
6080 children.push((boundary, slot, raw));
6081 continue;
6082 }
6083 Event::End(_) => break,
6084 Event::Eof => return Err(missing_end("inherited property owner")),
6085 event @ (Event::Comment(_)
6086 | Event::PI(_)
6087 | Event::Text(_)
6088 | Event::CData(_)) => text_capture_opaque_event(event)?,
6089 _ => continue,
6090 };
6091 children.push((boundary, None, raw));
6092 }
6093 }
6094 Ok((start, scope, children))
6095 };
6096 let (mut start, local_scope, mut children) = read(local, local_scope)?;
6097 let (parent, inherited_scope, inherited_children) = read(inherited, inherited_scope)?;
6098 let root_name = local_name(start.name().as_ref()).to_vec();
6099 let attribute_identity = |key: &[u8], scope: &TextNamespaceScope| -> Result<_> {
6100 let key = std::str::from_utf8(key).map_err(OxmlError::from)?;
6101 let (prefix, name) = key.split_once(':').unwrap_or(("", key));
6102 let uri = if prefix.is_empty() {
6103 None
6104 } else if prefix == "xml" {
6105 Some("http://www.w3.org/XML/1998/namespace".to_owned())
6106 } else {
6107 scope
6108 .physical
6109 .iter()
6110 .find(|(p, _)| p == prefix)
6111 .map(|(_, uri)| uri.clone())
6112 };
6113 if !prefix.is_empty() && uri.is_none() {
6114 return Err(invalid_attribute(
6115 "inherit text properties",
6116 "namespace",
6117 "unbound attribute prefix",
6118 ));
6119 }
6120 Ok((uri, name.to_owned()))
6121 };
6122 let known_attribute = |key: &[u8]| {
6123 if is_character_property_tag(&root_name) {
6124 matches!(
6125 key,
6126 b"sz"
6127 | b"b"
6128 | b"i"
6129 | b"cap"
6130 | b"u"
6131 | b"strike"
6132 | b"spc"
6133 | b"baseline"
6134 | b"lang"
6135 | b"altLang"
6136 | b"kern"
6137 | b"kumimoji"
6138 | b"normalizeH"
6139 | b"noProof"
6140 | b"dirty"
6141 | b"err"
6142 | b"smtClean"
6143 | b"smtId"
6144 | b"bmk"
6145 )
6146 } else if is_paragraph_property_tag(&root_name) {
6147 matches!(
6148 key,
6149 b"marL"
6150 | b"marR"
6151 | b"lvl"
6152 | b"indent"
6153 | b"algn"
6154 | b"rtl"
6155 | b"defTabSz"
6156 | b"eaLnBrk"
6157 | b"latinLnBrk"
6158 | b"hangingPunct"
6159 | b"fontAlgn"
6160 )
6161 } else {
6162 false
6163 }
6164 };
6165 let namespaces = |scope: &TextNamespaceScope| {
6166 scope
6167 .physical
6168 .iter()
6169 .filter(|(prefix, uri)| !prefix.is_empty() || !uri.is_empty())
6170 .cloned()
6171 .collect::<std::collections::BTreeMap<_, _>>()
6172 };
6173 let local_tokens = text_attribute_tokens(&start)?;
6174 let parent_tokens = text_attribute_tokens(&parent)?;
6175 let mut additions = String::new();
6176 for attribute in parent.attributes() {
6177 let attribute = attribute.map_err(OxmlError::from)?;
6178 let key = attribute.key.as_ref();
6179 if key == b"xmlns" || key.starts_with(b"xmlns:") {
6180 continue;
6181 }
6182 let identity = attribute_identity(key, &inherited_scope)?;
6183 let mut existing = None;
6184 for old in start.attributes() {
6185 let old = old.map_err(OxmlError::from)?;
6186 if old.key.as_ref() != b"xmlns"
6187 && !old.key.as_ref().starts_with(b"xmlns:")
6188 && attribute_identity(old.key.as_ref(), &local_scope)? == identity
6189 {
6190 existing = Some(old);
6191 break;
6192 }
6193 }
6194 if let Some(old) = existing {
6195 if !known_attribute(key)
6196 && (old
6197 .decoded_and_normalized_value(
6198 quick_xml::XmlVersion::Implicit1_0,
6199 start.decoder(),
6200 )
6201 .map_err(OxmlError::from)?
6202 != attribute
6203 .decoded_and_normalized_value(
6204 quick_xml::XmlVersion::Implicit1_0,
6205 parent.decoder(),
6206 )
6207 .map_err(OxmlError::from)?
6208 || namespaces(&local_scope) != namespaces(&inherited_scope))
6209 {
6210 return Err(invalid_attribute(
6211 "inherit text properties",
6212 "metadata",
6213 "conflicting opaque attribute authority",
6214 ));
6215 }
6216 continue;
6217 }
6218 if !known_attribute(key)
6219 && (!children.is_empty()
6220 || local_tokens
6221 .iter()
6222 .any(|(key, _)| !key.starts_with("xmlns") && !known_attribute(key.as_bytes())))
6223 && namespaces(&local_scope) != namespaces(&inherited_scope)
6224 {
6225 return Err(invalid_attribute(
6226 "inherit text properties",
6227 "metadata",
6228 "unrepresentable opaque namespace contexts",
6229 ));
6230 }
6231 if !known_attribute(key) {
6232 for (prefix, uri) in &inherited_scope.physical {
6233 if let Some((_, old)) = local_scope.physical.iter().find(|(old, _)| old == prefix) {
6234 if old != uri {
6235 return Err(invalid_attribute(
6236 "inherit text properties",
6237 "namespace",
6238 "conflicting inherited root binding",
6239 ));
6240 }
6241 } else {
6242 let key = if prefix.is_empty() {
6243 "xmlns".to_owned()
6244 } else {
6245 format!("xmlns:{prefix}")
6246 };
6247 if !additions.contains(&format!(" {key}=")) {
6248 additions
6249 .push_str(&format!(" {key}=\"{}\"", quick_xml::escape::escape(uri)));
6250 }
6251 }
6252 }
6253 }
6254 let (_, token) = parent_tokens
6255 .iter()
6256 .find(|(old, _)| old.as_bytes() == key)
6257 .ok_or_else(|| {
6258 invalid_attribute(
6259 "inherit text properties",
6260 "attribute",
6261 "missing source token",
6262 )
6263 })?;
6264 additions.push_str(token);
6265 }
6266 for (boundary, slot, raw) in inherited_children {
6267 if let Some(slot) = slot
6268 && let Some(index) = children.iter().position(|(_, old, _)| *old == Some(slot))
6269 {
6270 let name = raw_local_name(&raw);
6271 if matches!(name, b"defRPr" | b"defPPr" | b"extLst") || name.starts_with(b"lvl") {
6272 children[index].2 = text_inherit_property_xml(
6273 &children[index].2,
6274 &local_scope.physical,
6275 &raw,
6276 &inherited_scope.physical,
6277 )?;
6278 }
6279 continue;
6280 }
6281 let raw = text_close_inherited_property(&raw, &inherited_scope.physical)?;
6282 if children.iter().any(|(_, _, old)| old == &raw) {
6283 continue;
6284 }
6285 let position = children
6286 .iter()
6287 .position(|(old_boundary, _, _)| *old_boundary > boundary)
6288 .unwrap_or(children.len());
6289 children.insert(position, (boundary, slot, raw));
6290 }
6291 if !additions.is_empty() {
6292 let name_len = start.name().as_ref().len();
6293 let content = format!(
6294 "{}{additions}",
6295 std::str::from_utf8(start.as_ref()).map_err(OxmlError::from)?
6296 );
6297 start = BytesStart::from_content(content, name_len);
6298 }
6299 let name = std::str::from_utf8(start.name().as_ref())
6300 .map_err(OxmlError::from)?
6301 .to_owned();
6302 let mut writer = Writer::new(Vec::new());
6303 if children.is_empty() {
6304 write_empty(&mut writer, start)?;
6305 } else {
6306 write_start(&mut writer, start)?;
6307 for (_, _, raw) in children {
6308 writer.get_mut().write_all(&raw).map_err(OxmlError::from)?;
6309 }
6310 write_end(&mut writer, &name)?;
6311 }
6312 Ok(writer.into_inner())
6313}
6314
6315#[cfg(test)]
6316#[test]
6317fn fx216_inherited_scalar_retains_local_opaque_namespace_authority() {
6318 let local = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><opaque/></a:rPr>"#;
6319 let inherited = br#"<a:rPr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns="urn:parent" b="1"/>"#;
6320 let local = CT_TextCharacterProperties::from_xml(local).unwrap();
6321 let inherited = CT_TextCharacterProperties::from_xml(inherited).unwrap();
6322 let merged = local.with_inherited_defaults(&inherited).unwrap();
6323 let mut writer = Writer::new(Vec::new());
6324 merged.write_xml(&mut writer, "a:rPr").unwrap();
6325 let xml = writer.into_inner();
6326 let mut reader = quick_xml::NsReader::from_reader(xml.as_slice());
6327 let mut seen = false;
6328 loop {
6329 let (namespace, event) = reader.read_resolved_event().unwrap();
6330 match event {
6331 Event::Empty(element) if element.local_name().as_ref() == b"opaque" => {
6332 assert!(matches!(namespace, quick_xml::name::ResolveResult::Unbound));
6333 seen = true;
6334 }
6335 Event::Eof => break,
6336 _ => {}
6337 }
6338 }
6339 assert!(seen);
6340 assert_eq!(merged.bold, Some(true));
6341}
6342
6343#[cfg(test)]
6344#[test]
6345fn fx216_inherited_scalars_keep_false_zero_and_harmless_aliases() {
6346 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();
6347 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();
6348 let merged = local.with_inherited_defaults(&inherited).unwrap();
6349 assert_eq!(merged.bold, Some(false));
6350 assert_eq!(merged.italic, Some(false));
6351 assert_eq!(merged.spacing, Some(TextPointValue::Centipoints(0)));
6352 assert_eq!(merged.baseline.as_deref(), Some("0"));
6353 assert_eq!(merged.font_size, Some(2400));
6354 let mut writer = Writer::new(Vec::new());
6355 merged.write_xml(&mut writer, "a:rPr").unwrap();
6356 let xml = writer.into_inner();
6357 assert!(!String::from_utf8_lossy(&xml).contains("urn:unused"));
6358 assert_eq!(CT_TextCharacterProperties::from_xml(&xml).unwrap(), merged);
6359 assert_eq!(local.bold, Some(false));
6360 assert_eq!(inherited.bold, Some(true));
6361}