1use std::io::Write;
4
5use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
6use quick_xml::{Reader, Writer, XmlVersion};
7
8use crate::error::{OxmlError, Result};
9use crate::namespace::M_NS;
10use crate::raw_xml::capture_element;
11
12#[derive(Debug, Clone, PartialEq, Eq, Default)]
13struct Preservation {
14 attributes: Vec<(String, String)>,
15 raw_children: Vec<(usize, Vec<u8>)>,
16 modeled_children: Vec<PreservedChild>,
17 inherited_bindings: Vec<(String, String)>,
18 property_raw_children: Vec<(usize, Vec<u8>)>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22struct PreservedChild {
23 name: String,
24 raw: Vec<u8>,
25 bindings: Vec<(String, String)>,
26}
27
28#[derive(Debug)]
29struct ParsedElement {
30 preservation: Preservation,
31 bindings: Vec<(String, String)>,
32 children: Vec<Vec<u8>>,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq, Default)]
37pub struct CT_OMath {
38 pub expressions: Vec<MathExpression>,
39 preservation: Preservation,
40}
41
42impl CT_OMath {
43 pub fn new(expressions: Vec<MathExpression>) -> Self {
44 Self {
45 expressions,
46 preservation: Preservation::default(),
47 }
48 }
49
50 pub fn from_xml(xml: &[u8]) -> Result<Self> {
51 Self::from_raw(xml, &[])
52 }
53
54 pub(crate) fn from_raw(xml: &[u8], inherited: &[(String, String)]) -> Result<Self> {
55 validate_xml_depth(xml)?;
56 require_safe_fixed_math_prefix(xml, inherited)?;
57 let mut parsed = parse_element(xml, inherited)?;
58 require_root(xml, &parsed.bindings, b"oMath")?;
59 let mut expressions = Vec::new();
60 for child in parsed.children {
61 if let Some(expression) = MathExpression::from_raw(&child, &parsed.bindings)? {
62 expressions.push(expression);
63 } else {
64 parsed
65 .preservation
66 .raw_children
67 .push((expressions.len(), child));
68 }
69 }
70 Ok(Self {
71 expressions,
72 preservation: parsed.preservation,
73 })
74 }
75
76 pub fn to_xml(&self) -> Result<Vec<u8>> {
77 let mut writer = Writer::new(Vec::new());
78 self.write_xml(&mut writer, true)?;
79 Ok(writer.into_inner())
80 }
81
82 pub fn has_unsupported_content(&self) -> bool {
84 preservation_has_unsupported_content(&self.preservation)
85 || self
86 .expressions
87 .iter()
88 .any(MathExpression::has_unsupported_content)
89 }
90
91 pub(crate) fn write_xml<W: Write>(
92 &self,
93 writer: &mut Writer<W>,
94 declare_math: bool,
95 ) -> Result<()> {
96 let root = math_start("oMath", &self.preservation, declare_math);
97 writer.write_event(Event::Start(root.borrow()))?;
98 write_raw_slot(writer, &self.preservation, 0)?;
99 for (index, expression) in self.expressions.iter().enumerate() {
100 expression.write_xml(writer)?;
101 write_raw_slot(writer, &self.preservation, index + 1)?;
102 }
103 write_raw_tail(writer, &self.preservation, self.expressions.len() + 1)?;
104 writer.write_event(Event::End(BytesEnd::new("m:oMath")))?;
105 Ok(())
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Default)]
111pub struct CT_OMathPara {
112 pub properties: MathParagraphProperties,
113 pub equations: Vec<CT_OMath>,
114 preservation: Preservation,
115}
116
117impl CT_OMathPara {
118 pub fn new(equations: Vec<CT_OMath>) -> Self {
119 Self {
120 equations,
121 ..Self::default()
122 }
123 }
124
125 pub fn from_xml(xml: &[u8]) -> Result<Self> {
126 Self::from_raw(xml, &[])
127 }
128
129 pub(crate) fn from_raw(xml: &[u8], inherited: &[(String, String)]) -> Result<Self> {
130 validate_xml_depth(xml)?;
131 require_safe_fixed_math_prefix(xml, inherited)?;
132 if !valid_officemath_paragraph_shape(xml, inherited)? {
133 return Err(OxmlError::InvalidValue(
134 "invalid OfficeMath paragraph child sequence".to_owned(),
135 ));
136 }
137 let mut parsed = parse_element(xml, inherited)?;
138 require_root(xml, &parsed.bindings, b"oMathPara")?;
139 let mut properties = MathParagraphProperties::default();
140 let mut equations = Vec::new();
141 let mut modeled = 0usize;
142 for child in parsed.children {
143 match math_local_name(&child, &parsed.bindings)?.as_deref() {
144 Some("oMathParaPr") if modeled == 0 => {
145 properties = MathParagraphProperties::from_raw(&child, &parsed.bindings)?;
146 preserve_modeled_child(
147 &mut parsed.preservation,
148 "oMathParaPr",
149 &child,
150 &parsed.bindings,
151 );
152 modeled += 1;
153 }
154 Some("oMath") => {
155 equations.push(CT_OMath::from_raw(&child, &parsed.bindings)?);
156 modeled += 1;
157 }
158 _ => parsed.preservation.raw_children.push((modeled, child)),
159 }
160 }
161 Ok(Self {
162 properties,
163 equations,
164 preservation: parsed.preservation,
165 })
166 }
167
168 pub fn to_xml(&self) -> Result<Vec<u8>> {
169 let mut writer = Writer::new(Vec::new());
170 self.write_xml(&mut writer, true)?;
171 Ok(writer.into_inner())
172 }
173
174 pub fn has_unsupported_content(&self) -> bool {
176 preservation_has_unsupported_content(&self.preservation)
177 || property_preservation_has_unsupported_content(
178 &self.properties.preservation,
179 "oMathParaPr",
180 )
181 || self.equations.iter().any(CT_OMath::has_unsupported_content)
182 }
183
184 pub(crate) fn write_xml<W: Write>(
185 &self,
186 writer: &mut Writer<W>,
187 declare_math: bool,
188 ) -> Result<()> {
189 if self.equations.is_empty() {
190 return Err(OxmlError::InvalidValue(
191 "OfficeMath display requires at least one equation".to_owned(),
192 ));
193 }
194 let root = math_start("oMathPara", &self.preservation, declare_math);
195 writer.write_event(Event::Start(root.borrow()))?;
196 let had_properties = has_preserved_modeled_child(&self.preservation, "oMathParaPr");
197 let has_properties = !self.properties.is_empty() || had_properties;
198 if has_properties && !had_properties {
199 self.properties.write_xml(writer)?;
200 }
201 let mut modeled = 0usize;
202 write_raw_slot(writer, &self.preservation, modeled)?;
203 if has_properties && had_properties {
204 self.properties.write_xml(writer)?;
205 modeled += 1;
206 write_raw_slot(writer, &self.preservation, modeled)?;
207 }
208 for equation in &self.equations {
209 equation.write_xml(writer, false)?;
210 modeled += 1;
211 write_raw_slot(writer, &self.preservation, modeled)?;
212 }
213 write_raw_tail(writer, &self.preservation, modeled + 1)?;
214 writer.write_event(Event::End(BytesEnd::new("m:oMathPara")))?;
215 Ok(())
216 }
217}
218
219#[derive(Debug, Clone, PartialEq, Eq)]
221pub enum OfficeMath {
222 Inline(CT_OMath),
223 Display(CT_OMathPara),
224}
225
226impl OfficeMath {
227 pub fn inline(expressions: Vec<MathExpression>) -> Self {
228 Self::Inline(CT_OMath::new(expressions))
229 }
230
231 pub fn display(equations: Vec<CT_OMath>) -> Self {
232 Self::Display(CT_OMathPara::new(equations))
233 }
234
235 pub fn to_xml(&self) -> Result<Vec<u8>> {
236 match self {
237 Self::Inline(value) => value.to_xml(),
238 Self::Display(value) => value.to_xml(),
239 }
240 }
241
242 pub fn has_unsupported_content(&self) -> bool {
244 match self {
245 Self::Inline(value) => value.has_unsupported_content(),
246 Self::Display(value) => value.has_unsupported_content(),
247 }
248 }
249
250 pub(crate) fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Option<Self>> {
251 if !fixed_math_prefix_is_safe(raw, inherited)? {
252 return Ok(None);
253 }
254 Ok(match math_local_name(raw, inherited)?.as_deref() {
255 Some("oMath") => Some(Self::Inline(CT_OMath::from_raw(raw, inherited)?)),
256 Some("oMathPara") if valid_officemath_paragraph_shape(raw, inherited)? => {
257 Some(Self::Display(CT_OMathPara::from_raw(raw, inherited)?))
258 }
259 Some("oMathPara") => None,
260 _ => None,
261 })
262 }
263
264 pub(crate) fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
265 match self {
266 Self::Inline(value) => value.write_xml(writer, true),
267 Self::Display(value) => value.write_xml(writer, true),
268 }
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
274pub enum MathExpression {
275 Run(MathRun),
276 Fraction(MathFraction),
277 Subscript(MathScript),
278 Superscript(MathScript),
279 SubSuperscript(MathSubSuperscript),
280 PreSubSuperscript(MathPreSubSuperscript),
281 Radical(MathRadical),
282 Matrix(MathMatrix),
283 LowerLimit(MathLimit),
284 UpperLimit(MathLimit),
285 Nary(MathNary),
286 Delimiter(MathDelimiter),
287 Accent(MathAccent),
288}
289
290impl MathExpression {
291 pub fn has_unsupported_content(&self) -> bool {
293 match self {
294 Self::Run(value) => {
295 preservation_has_unsupported_content(&value.preservation)
296 || property_preservation_has_unsupported_content(
297 &value.properties.preservation,
298 "rPr",
299 )
300 || math_text_has_unsupported_content(&value.preservation)
301 }
302 Self::Fraction(value) => {
303 preservation_has_unsupported_content(&value.preservation)
304 || property_container_has_unsupported_content(&value.preservation, "fPr")
305 || value.numerator.has_unsupported_content()
306 || value.denominator.has_unsupported_content()
307 }
308 Self::Subscript(value) => script_has_unsupported_content(value, "sSubPr"),
309 Self::Superscript(value) => script_has_unsupported_content(value, "sSupPr"),
310 Self::SubSuperscript(value) => three_argument_script_has_unsupported_content(
311 &value.preservation,
312 "sSubSupPr",
313 &value.base,
314 &value.subscript,
315 &value.superscript,
316 ),
317 Self::PreSubSuperscript(value) => three_argument_script_has_unsupported_content(
318 &value.preservation,
319 "sPrePr",
320 &value.base,
321 &value.subscript,
322 &value.superscript,
323 ),
324 Self::Radical(value) => {
325 preservation_has_unsupported_content(&value.preservation)
326 || property_container_has_unsupported_content(&value.preservation, "radPr")
327 || value.degree.has_unsupported_content()
328 || value.base.has_unsupported_content()
329 }
330 Self::Matrix(value) => {
331 preservation_has_unsupported_content(&value.preservation)
332 || property_container_has_unsupported_content(&value.preservation, "mPr")
333 || value.rows.iter().any(|row| {
334 preservation_has_unsupported_content(&row.preservation)
335 || row.cells.iter().any(MathArgument::has_unsupported_content)
336 })
337 }
338 Self::LowerLimit(value) => limit_has_unsupported_content(value, "limLowPr"),
339 Self::UpperLimit(value) => limit_has_unsupported_content(value, "limUppPr"),
340 Self::Nary(value) => {
341 preservation_has_unsupported_content(&value.preservation)
342 || property_container_has_unsupported_content(&value.preservation, "naryPr")
343 || value.base.has_unsupported_content()
344 || value.subscript.has_unsupported_content()
345 || value.superscript.has_unsupported_content()
346 }
347 Self::Delimiter(value) => {
348 preservation_has_unsupported_content(&value.preservation)
349 || property_container_has_unsupported_content(&value.preservation, "dPr")
350 || value
351 .arguments
352 .iter()
353 .any(MathArgument::has_unsupported_content)
354 }
355 Self::Accent(value) => {
356 preservation_has_unsupported_content(&value.preservation)
357 || property_container_has_unsupported_content(&value.preservation, "accPr")
358 || value.base.has_unsupported_content()
359 }
360 }
361 }
362
363 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Option<Self>> {
364 if !valid_expression_shape(raw, inherited)? {
365 return Ok(None);
366 }
367 Ok(match math_local_name(raw, inherited)?.as_deref() {
368 Some("r") => Some(Self::Run(MathRun::from_raw(raw, inherited)?)),
369 Some("f") => Some(Self::Fraction(MathFraction::from_raw(raw, inherited)?)),
370 Some("sSub") => Some(Self::Subscript(MathScript::from_raw(raw, inherited)?)),
371 Some("sSup") => Some(Self::Superscript(MathScript::from_raw(raw, inherited)?)),
372 Some("sSubSup") => Some(Self::SubSuperscript(MathSubSuperscript::from_raw(
373 raw, inherited,
374 )?)),
375 Some("sPre") => Some(Self::PreSubSuperscript(MathPreSubSuperscript::from_raw(
376 raw, inherited,
377 )?)),
378 Some("rad") => Some(Self::Radical(MathRadical::from_raw(raw, inherited)?)),
379 Some("m") => Some(Self::Matrix(MathMatrix::from_raw(raw, inherited)?)),
380 Some("limLow") => Some(Self::LowerLimit(MathLimit::from_raw(raw, inherited)?)),
381 Some("limUpp") => Some(Self::UpperLimit(MathLimit::from_raw(raw, inherited)?)),
382 Some("nary") => Some(Self::Nary(MathNary::from_raw(raw, inherited)?)),
383 Some("d") => Some(Self::Delimiter(MathDelimiter::from_raw(raw, inherited)?)),
384 Some("acc") => Some(Self::Accent(MathAccent::from_raw(raw, inherited)?)),
385 _ => None,
386 })
387 }
388
389 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
390 match self {
391 Self::Run(value) => value.write_xml(writer),
392 Self::Fraction(value) => value.write_xml(writer),
393 Self::Subscript(value) => value.write_xml(writer, "sSub"),
394 Self::Superscript(value) => value.write_xml(writer, "sSup"),
395 Self::SubSuperscript(value) => value.write_xml(writer),
396 Self::PreSubSuperscript(value) => value.write_xml(writer),
397 Self::Radical(value) => value.write_xml(writer),
398 Self::Matrix(value) => value.write_xml(writer),
399 Self::LowerLimit(value) => value.write_xml(writer, "limLow"),
400 Self::UpperLimit(value) => value.write_xml(writer, "limUpp"),
401 Self::Nary(value) => value.write_xml(writer),
402 Self::Delimiter(value) => value.write_xml(writer),
403 Self::Accent(value) => value.write_xml(writer),
404 }
405 }
406}
407
408impl From<MathRun> for MathExpression {
409 fn from(value: MathRun) -> Self {
410 Self::Run(value)
411 }
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Default)]
416pub struct MathArgument {
417 pub expressions: Vec<MathExpression>,
418 preservation: Preservation,
419}
420
421impl MathArgument {
422 pub fn new(expressions: Vec<MathExpression>) -> Self {
423 Self {
424 expressions,
425 preservation: Preservation::default(),
426 }
427 }
428
429 pub fn text(text: impl Into<String>) -> Self {
430 Self::new(vec![MathRun::new(text).into()])
431 }
432
433 pub fn has_unsupported_content(&self) -> bool {
435 preservation_has_unsupported_content(&self.preservation)
436 || self
437 .expressions
438 .iter()
439 .any(MathExpression::has_unsupported_content)
440 }
441
442 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
443 let mut parsed = parse_element(raw, inherited)?;
444 let mut expressions = Vec::new();
445 for child in parsed.children {
446 if let Some(expression) = MathExpression::from_raw(&child, &parsed.bindings)? {
447 expressions.push(expression);
448 } else {
449 parsed
450 .preservation
451 .raw_children
452 .push((expressions.len(), child));
453 }
454 }
455 Ok(Self {
456 expressions,
457 preservation: parsed.preservation,
458 })
459 }
460
461 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
462 write_container(writer, tag, &self.preservation, |writer| {
463 write_raw_slot(writer, &self.preservation, 0)?;
464 for (index, expression) in self.expressions.iter().enumerate() {
465 expression.write_xml(writer)?;
466 write_raw_slot(writer, &self.preservation, index + 1)?;
467 }
468 write_raw_tail(writer, &self.preservation, self.expressions.len() + 1)
469 })
470 }
471}
472
473#[derive(Debug, Clone, PartialEq, Eq, Default)]
475pub struct MathRunProperties {
476 pub style: Option<MathStyle>,
477 pub normal: Option<bool>,
478 pub literal: Option<bool>,
479 pub script: Option<MathScriptStyle>,
480 pub break_before: bool,
481 pub break_alignment: Option<u8>,
482 preservation: Preservation,
483}
484
485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
487pub enum MathStyle {
488 Plain,
489 Bold,
490 Italic,
491 BoldItalic,
492}
493
494impl MathStyle {
495 fn parse(value: &str) -> Option<Self> {
496 match value {
497 "p" => Some(Self::Plain),
498 "b" => Some(Self::Bold),
499 "i" => Some(Self::Italic),
500 "bi" => Some(Self::BoldItalic),
501 _ => None,
502 }
503 }
504
505 fn as_str(self) -> &'static str {
506 match self {
507 Self::Plain => "p",
508 Self::Bold => "b",
509 Self::Italic => "i",
510 Self::BoldItalic => "bi",
511 }
512 }
513}
514
515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
517pub enum MathScriptStyle {
518 Roman,
519 Script,
520 Fraktur,
521 DoubleStruck,
522 SansSerif,
523 Monospace,
524}
525
526impl MathScriptStyle {
527 fn parse(value: &str) -> Option<Self> {
528 match value {
529 "roman" => Some(Self::Roman),
530 "script" => Some(Self::Script),
531 "fraktur" => Some(Self::Fraktur),
532 "double-struck" => Some(Self::DoubleStruck),
533 "sans-serif" => Some(Self::SansSerif),
534 "monospace" => Some(Self::Monospace),
535 _ => None,
536 }
537 }
538
539 fn as_str(self) -> &'static str {
540 match self {
541 Self::Roman => "roman",
542 Self::Script => "script",
543 Self::Fraktur => "fraktur",
544 Self::DoubleStruck => "double-struck",
545 Self::SansSerif => "sans-serif",
546 Self::Monospace => "monospace",
547 }
548 }
549}
550
551#[derive(Debug, Clone, PartialEq, Eq, Default)]
553pub struct MathRun {
554 pub text: String,
555 pub properties: MathRunProperties,
556 preservation: Preservation,
557}
558
559impl MathRun {
560 pub fn new(text: impl Into<String>) -> Self {
561 Self {
562 text: text.into(),
563 ..Self::default()
564 }
565 }
566
567 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
568 let mut parsed = parse_element(raw, inherited)?;
569 let mut properties = MathRunProperties::default();
570 let mut text = String::new();
571 let mut modeled = 0usize;
572 for child in parsed.children {
573 match math_local_name(&child, &parsed.bindings)?.as_deref() {
574 Some("rPr") if modeled == 0 => {
575 properties = parse_run_properties(&child, &parsed.bindings)?;
576 preserve_modeled_child(
577 &mut parsed.preservation,
578 "rPr",
579 &child,
580 &parsed.bindings,
581 );
582 modeled += 1;
583 }
584 Some("t") => {
585 text.push_str(&element_text(&child)?);
586 preserve_modeled_child(&mut parsed.preservation, "t", &child, &parsed.bindings);
587 modeled += 1;
588 }
589 _ => parsed.preservation.raw_children.push((modeled, child)),
590 }
591 }
592 Ok(Self {
593 text,
594 properties,
595 preservation: parsed.preservation,
596 })
597 }
598
599 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
600 write_container(writer, "r", &self.preservation, |writer| {
601 let had_properties = has_preserved_modeled_child(&self.preservation, "rPr");
602 let emit_properties = !self.properties.is_empty() || had_properties;
603 if emit_properties && !had_properties {
604 write_run_properties(writer, &self.properties)?;
605 }
606 let mut modeled = 0usize;
607 write_raw_slot(writer, &self.preservation, modeled)?;
608 if emit_properties && had_properties {
609 write_run_properties(writer, &self.properties)?;
610 modeled += 1;
611 write_raw_slot(writer, &self.preservation, modeled)?;
612 }
613 write_math_text(writer, &self.text, &self.preservation)?;
614 modeled += 1;
615 write_raw_slot(writer, &self.preservation, modeled)
616 })
617 }
618}
619
620impl MathRunProperties {
621 fn is_empty(&self) -> bool {
622 self.style.is_none()
623 && self.normal.is_none()
624 && self.literal.is_none()
625 && self.script.is_none()
626 && !self.break_before
627 && self.break_alignment.is_none()
628 && self.preservation == Preservation::default()
629 }
630}
631
632#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
634pub enum FractionType {
635 #[default]
636 Bar,
637 Skewed,
638 Linear,
639 NoBar,
640}
641
642impl FractionType {
643 fn parse(value: &str) -> Option<Self> {
644 match value {
645 "bar" => Some(Self::Bar),
646 "skw" => Some(Self::Skewed),
647 "lin" => Some(Self::Linear),
648 "noBar" => Some(Self::NoBar),
649 _ => None,
650 }
651 }
652
653 fn as_str(self) -> &'static str {
654 match self {
655 Self::Bar => "bar",
656 Self::Skewed => "skw",
657 Self::Linear => "lin",
658 Self::NoBar => "noBar",
659 }
660 }
661}
662
663#[derive(Debug, Clone, PartialEq, Eq)]
664pub struct MathFraction {
665 pub fraction_type: FractionType,
666 pub numerator: MathArgument,
667 pub denominator: MathArgument,
668 preservation: Preservation,
669}
670
671impl MathFraction {
672 pub fn new(numerator: MathArgument, denominator: MathArgument) -> Self {
673 Self {
674 fraction_type: FractionType::Bar,
675 numerator,
676 denominator,
677 preservation: Preservation::default(),
678 }
679 }
680
681 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
682 let mut parsed = parse_element(raw, inherited)?;
683 let mut fraction_type = FractionType::Bar;
684 let mut numerator = MathArgument::default();
685 let mut denominator = MathArgument::default();
686 let mut modeled = 0usize;
687 for child in parsed.children {
688 match math_local_name(&child, &parsed.bindings)?.as_deref() {
689 Some("fPr") if modeled == 0 => {
690 if let Some(value) =
691 child_property_value(&child, &parsed.bindings, "fPr", "type")?
692 {
693 fraction_type = FractionType::parse(&value).unwrap_or_default();
694 }
695 preserve_modeled_child(
696 &mut parsed.preservation,
697 "fPr",
698 &child,
699 &parsed.bindings,
700 );
701 modeled += 1;
702 }
703 Some("num") => {
704 numerator = MathArgument::from_raw(&child, &parsed.bindings)?;
705 modeled += 1;
706 }
707 Some("den") => {
708 denominator = MathArgument::from_raw(&child, &parsed.bindings)?;
709 modeled += 1;
710 }
711 _ => parsed.preservation.raw_children.push((modeled, child)),
712 }
713 }
714 Ok(Self {
715 fraction_type,
716 numerator,
717 denominator,
718 preservation: parsed.preservation,
719 })
720 }
721
722 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
723 write_container(writer, "f", &self.preservation, |writer| {
724 let mut modeled = write_leading_property_container(
725 writer,
726 "fPr",
727 &[Property::text("type", self.fraction_type.as_str())],
728 &self.preservation,
729 true,
730 )?;
731 self.numerator.write_xml(writer, "num")?;
732 modeled += 1;
733 write_raw_slot(writer, &self.preservation, modeled)?;
734 self.denominator.write_xml(writer, "den")?;
735 modeled += 1;
736 write_raw_slot(writer, &self.preservation, modeled)
737 })
738 }
739}
740
741#[derive(Debug, Clone, PartialEq, Eq)]
743pub struct MathScript {
744 pub base: MathArgument,
745 pub script: MathArgument,
746 pub alignment: Option<bool>,
747 preservation: Preservation,
748}
749
750impl MathScript {
751 pub fn new(base: MathArgument, script: MathArgument) -> Self {
752 Self {
753 base,
754 script,
755 alignment: None,
756 preservation: Preservation::default(),
757 }
758 }
759
760 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
761 let mut parsed = parse_element(raw, inherited)?;
762 let root = math_local_name(raw, inherited)?.unwrap_or_default();
763 let script_tag = if root == "sSub" { "sub" } else { "sup" };
764 let property_tag = if root == "sSub" { "sSubPr" } else { "sSupPr" };
765 let mut base = MathArgument::default();
766 let mut script = MathArgument::default();
767 let mut alignment = None;
768 let mut modeled = 0usize;
769 for child in parsed.children {
770 match math_local_name(&child, &parsed.bindings)?.as_deref() {
771 Some(local) if local == property_tag && modeled == 0 => {
772 alignment = child_on_off_value(&child, &parsed.bindings, "alnScr")?;
773 preserve_modeled_child(
774 &mut parsed.preservation,
775 property_tag,
776 &child,
777 &parsed.bindings,
778 );
779 modeled += 1;
780 }
781 Some("e") => {
782 base = MathArgument::from_raw(&child, &parsed.bindings)?;
783 modeled += 1;
784 }
785 Some(local) if local == script_tag => {
786 script = MathArgument::from_raw(&child, &parsed.bindings)?;
787 modeled += 1;
788 }
789 _ => parsed.preservation.raw_children.push((modeled, child)),
790 }
791 }
792 Ok(Self {
793 base,
794 script,
795 alignment,
796 preservation: parsed.preservation,
797 })
798 }
799
800 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
801 let property_tag = if tag == "sSub" { "sSubPr" } else { "sSupPr" };
802 let script_tag = if tag == "sSub" { "sub" } else { "sup" };
803 write_container(writer, tag, &self.preservation, |writer| {
804 let properties = self
805 .alignment
806 .map(|value| Property::boolean("alnScr", value))
807 .into_iter()
808 .collect::<Vec<_>>();
809 let emit_properties = self.alignment.is_some()
810 || has_preserved_modeled_child(&self.preservation, property_tag);
811 let mut modeled = write_leading_property_container(
812 writer,
813 property_tag,
814 &properties,
815 &self.preservation,
816 emit_properties,
817 )?;
818 self.base.write_xml(writer, "e")?;
819 modeled += 1;
820 write_raw_slot(writer, &self.preservation, modeled)?;
821 self.script.write_xml(writer, script_tag)?;
822 modeled += 1;
823 write_raw_slot(writer, &self.preservation, modeled)
824 })
825 }
826}
827
828#[derive(Debug, Clone, PartialEq, Eq)]
829pub struct MathSubSuperscript {
830 pub base: MathArgument,
831 pub subscript: MathArgument,
832 pub superscript: MathArgument,
833 pub alignment: Option<bool>,
834 preservation: Preservation,
835}
836
837impl MathSubSuperscript {
838 pub fn new(base: MathArgument, subscript: MathArgument, superscript: MathArgument) -> Self {
839 Self {
840 base,
841 subscript,
842 superscript,
843 alignment: None,
844 preservation: Preservation::default(),
845 }
846 }
847
848 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
849 parse_three_argument_script(raw, inherited, false).map(|value| Self {
850 base: value.0,
851 subscript: value.1,
852 superscript: value.2,
853 alignment: value.3,
854 preservation: value.4,
855 })
856 }
857
858 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
859 write_three_argument_script(
860 writer,
861 "sSubSup",
862 "sSubSupPr",
863 &self.base,
864 &self.subscript,
865 &self.superscript,
866 self.alignment,
867 &self.preservation,
868 false,
869 )
870 }
871}
872
873#[derive(Debug, Clone, PartialEq, Eq)]
874pub struct MathPreSubSuperscript {
875 pub base: MathArgument,
876 pub subscript: MathArgument,
877 pub superscript: MathArgument,
878 pub alignment: Option<bool>,
879 preservation: Preservation,
880}
881
882impl MathPreSubSuperscript {
883 pub fn new(base: MathArgument, subscript: MathArgument, superscript: MathArgument) -> Self {
884 Self {
885 base,
886 subscript,
887 superscript,
888 alignment: None,
889 preservation: Preservation::default(),
890 }
891 }
892
893 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
894 parse_three_argument_script(raw, inherited, true).map(|value| Self {
895 base: value.0,
896 subscript: value.1,
897 superscript: value.2,
898 alignment: value.3,
899 preservation: value.4,
900 })
901 }
902
903 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
904 write_three_argument_script(
905 writer,
906 "sPre",
907 "sPrePr",
908 &self.base,
909 &self.subscript,
910 &self.superscript,
911 self.alignment,
912 &self.preservation,
913 true,
914 )
915 }
916}
917
918type ThreeArgumentScript = (
919 MathArgument,
920 MathArgument,
921 MathArgument,
922 Option<bool>,
923 Preservation,
924);
925
926fn parse_three_argument_script(
927 raw: &[u8],
928 inherited: &[(String, String)],
929 pre: bool,
930) -> Result<ThreeArgumentScript> {
931 let mut parsed = parse_element(raw, inherited)?;
932 let property_tag = if pre { "sPrePr" } else { "sSubSupPr" };
933 let mut base = MathArgument::default();
934 let mut subscript = MathArgument::default();
935 let mut superscript = MathArgument::default();
936 let mut alignment = None;
937 let mut modeled = 0usize;
938 for child in parsed.children {
939 match math_local_name(&child, &parsed.bindings)?.as_deref() {
940 Some(local) if local == property_tag && modeled == 0 => {
941 alignment = child_on_off_value(&child, &parsed.bindings, "alnScr")?;
942 preserve_modeled_child(
943 &mut parsed.preservation,
944 property_tag,
945 &child,
946 &parsed.bindings,
947 );
948 modeled += 1;
949 }
950 Some("e") => {
951 base = MathArgument::from_raw(&child, &parsed.bindings)?;
952 modeled += 1;
953 }
954 Some("sub") => {
955 subscript = MathArgument::from_raw(&child, &parsed.bindings)?;
956 modeled += 1;
957 }
958 Some("sup") => {
959 superscript = MathArgument::from_raw(&child, &parsed.bindings)?;
960 modeled += 1;
961 }
962 _ => parsed.preservation.raw_children.push((modeled, child)),
963 }
964 }
965 Ok((base, subscript, superscript, alignment, parsed.preservation))
966}
967
968#[allow(clippy::too_many_arguments)]
969fn write_three_argument_script<W: Write>(
970 writer: &mut Writer<W>,
971 root_tag: &str,
972 property_tag: &str,
973 base: &MathArgument,
974 subscript: &MathArgument,
975 superscript: &MathArgument,
976 alignment: Option<bool>,
977 preservation: &Preservation,
978 pre: bool,
979) -> Result<()> {
980 write_container(writer, root_tag, preservation, |writer| {
981 let properties = alignment
982 .map(|value| Property::boolean("alnScr", value))
983 .into_iter()
984 .collect::<Vec<_>>();
985 let emit_properties =
986 alignment.is_some() || has_preserved_modeled_child(preservation, property_tag);
987 let mut modeled = write_leading_property_container(
988 writer,
989 property_tag,
990 &properties,
991 preservation,
992 emit_properties,
993 )?;
994 if pre {
995 subscript.write_xml(writer, "sub")?;
996 modeled += 1;
997 write_raw_slot(writer, preservation, modeled)?;
998 superscript.write_xml(writer, "sup")?;
999 modeled += 1;
1000 write_raw_slot(writer, preservation, modeled)?;
1001 base.write_xml(writer, "e")?;
1002 } else {
1003 base.write_xml(writer, "e")?;
1004 modeled += 1;
1005 write_raw_slot(writer, preservation, modeled)?;
1006 subscript.write_xml(writer, "sub")?;
1007 modeled += 1;
1008 write_raw_slot(writer, preservation, modeled)?;
1009 superscript.write_xml(writer, "sup")?;
1010 }
1011 modeled += 1;
1012 write_raw_slot(writer, preservation, modeled)
1013 })
1014}
1015
1016#[derive(Debug, Clone, PartialEq, Eq)]
1017pub struct MathRadical {
1018 pub degree: MathArgument,
1019 pub base: MathArgument,
1020 pub hide_degree: bool,
1021 degree_present: bool,
1022 preservation: Preservation,
1023}
1024
1025impl MathRadical {
1026 pub fn new(base: MathArgument) -> Self {
1027 Self {
1028 degree: MathArgument::default(),
1029 base,
1030 hide_degree: true,
1031 degree_present: false,
1032 preservation: Preservation::default(),
1033 }
1034 }
1035
1036 pub fn with_degree(degree: MathArgument, base: MathArgument) -> Self {
1037 Self {
1038 degree,
1039 base,
1040 hide_degree: false,
1041 degree_present: true,
1042 preservation: Preservation::default(),
1043 }
1044 }
1045
1046 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
1047 let mut parsed = parse_element(raw, inherited)?;
1048 let mut degree = MathArgument::default();
1049 let mut base = MathArgument::default();
1050 let mut hide_degree = false;
1051 let mut degree_present = false;
1052 let mut modeled = 0usize;
1053 for child in parsed.children {
1054 match math_local_name(&child, &parsed.bindings)?.as_deref() {
1055 Some("radPr") if modeled == 0 => {
1056 hide_degree =
1057 child_on_off_value(&child, &parsed.bindings, "degHide")?.unwrap_or(false);
1058 preserve_modeled_child(
1059 &mut parsed.preservation,
1060 "radPr",
1061 &child,
1062 &parsed.bindings,
1063 );
1064 modeled += 1;
1065 }
1066 Some("deg") => {
1067 degree = MathArgument::from_raw(&child, &parsed.bindings)?;
1068 degree_present = true;
1069 modeled += 1;
1070 }
1071 Some("e") => {
1072 base = MathArgument::from_raw(&child, &parsed.bindings)?;
1073 modeled += 1;
1074 }
1075 _ => parsed.preservation.raw_children.push((modeled, child)),
1076 }
1077 }
1078 Ok(Self {
1079 degree,
1080 base,
1081 hide_degree,
1082 degree_present,
1083 preservation: parsed.preservation,
1084 })
1085 }
1086
1087 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1088 write_container(writer, "rad", &self.preservation, |writer| {
1089 let mut modeled = write_leading_property_container_before_raw(
1090 writer,
1091 "radPr",
1092 &[Property::boolean("degHide", self.hide_degree)],
1093 &self.preservation,
1094 true,
1095 )?;
1096 let emit_degree = self.degree_present
1097 || !self.degree.expressions.is_empty()
1098 || self.degree.has_unsupported_content();
1099 if self.degree_present {
1100 write_raw_slot(writer, &self.preservation, modeled)?;
1101 self.degree.write_xml(writer, "deg")?;
1102 modeled += 1;
1103 } else if emit_degree {
1104 self.degree.write_xml(writer, "deg")?;
1105 }
1106 write_raw_slot(writer, &self.preservation, modeled)?;
1107 self.base.write_xml(writer, "e")?;
1108 modeled += 1;
1109 write_raw_slot(writer, &self.preservation, modeled)
1110 })
1111 }
1112}
1113
1114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1115pub enum MathJustification {
1116 Left,
1117 Center,
1118 Right,
1119 CenterGroup,
1120}
1121
1122impl MathJustification {
1123 fn parse(value: &str) -> Option<Self> {
1124 match value {
1125 "left" => Some(Self::Left),
1126 "center" => Some(Self::Center),
1127 "right" => Some(Self::Right),
1128 "centerGroup" => Some(Self::CenterGroup),
1129 _ => None,
1130 }
1131 }
1132
1133 fn as_str(self) -> &'static str {
1134 match self {
1135 Self::Left => "left",
1136 Self::Center => "center",
1137 Self::Right => "right",
1138 Self::CenterGroup => "centerGroup",
1139 }
1140 }
1141}
1142
1143#[derive(Debug, Clone, PartialEq, Eq, Default)]
1144pub struct MathParagraphProperties {
1145 pub justification: Option<MathJustification>,
1146 preservation: Preservation,
1147}
1148
1149impl MathParagraphProperties {
1150 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
1151 Ok(Self {
1152 justification: child_property_value(raw, inherited, "oMathParaPr", "jc")?
1153 .and_then(|value| MathJustification::parse(&value)),
1154 preservation: preserve_unrecognized_property_children(raw, inherited, &["jc"])?,
1155 })
1156 }
1157
1158 fn is_empty(&self) -> bool {
1159 self.justification.is_none() && self.preservation == Preservation::default()
1160 }
1161
1162 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1163 let properties = self
1164 .justification
1165 .map(|value| Property::text("jc", value.as_str()))
1166 .into_iter()
1167 .collect::<Vec<_>>();
1168 write_property_container_with_preservation(
1169 writer,
1170 "oMathParaPr",
1171 &properties,
1172 &self.preservation,
1173 )
1174 }
1175}
1176
1177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1178pub enum MatrixBaseJustification {
1179 Top,
1180 Center,
1181 Bottom,
1182}
1183
1184impl MatrixBaseJustification {
1185 fn parse(value: &str) -> Option<Self> {
1186 match value {
1187 "top" => Some(Self::Top),
1188 "center" => Some(Self::Center),
1189 "bottom" => Some(Self::Bottom),
1190 _ => None,
1191 }
1192 }
1193
1194 fn as_str(self) -> &'static str {
1195 match self {
1196 Self::Top => "top",
1197 Self::Center => "center",
1198 Self::Bottom => "bottom",
1199 }
1200 }
1201}
1202
1203#[derive(Debug, Clone, PartialEq, Eq, Default)]
1204pub struct MathMatrixProperties {
1205 pub base_justification: Option<MatrixBaseJustification>,
1206 pub row_spacing: Option<u16>,
1207 pub column_spacing: Option<u32>,
1208}
1209
1210#[derive(Debug, Clone, PartialEq, Eq, Default)]
1211pub struct MathMatrixRow {
1212 pub cells: Vec<MathArgument>,
1213 preservation: Preservation,
1214}
1215
1216impl MathMatrixRow {
1217 pub fn new(cells: Vec<MathArgument>) -> Self {
1218 Self {
1219 cells,
1220 preservation: Preservation::default(),
1221 }
1222 }
1223}
1224
1225#[derive(Debug, Clone, PartialEq, Eq, Default)]
1226pub struct MathMatrix {
1227 pub properties: MathMatrixProperties,
1228 pub rows: Vec<MathMatrixRow>,
1229 preservation: Preservation,
1230}
1231
1232impl MathMatrix {
1233 pub fn new(rows: Vec<MathMatrixRow>) -> Self {
1234 Self {
1235 rows,
1236 ..Self::default()
1237 }
1238 }
1239
1240 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
1241 let mut parsed = parse_element(raw, inherited)?;
1242 let mut properties = MathMatrixProperties::default();
1243 let mut rows = Vec::new();
1244 let mut modeled = 0usize;
1245 for child in parsed.children {
1246 match math_local_name(&child, &parsed.bindings)?.as_deref() {
1247 Some("mPr") if modeled == 0 => {
1248 properties.base_justification =
1249 child_property_value(&child, &parsed.bindings, "mPr", "baseJc")?
1250 .and_then(|value| MatrixBaseJustification::parse(&value));
1251 properties.row_spacing =
1252 child_property_value(&child, &parsed.bindings, "mPr", "rSp")?
1253 .and_then(|value| value.parse().ok());
1254 properties.column_spacing =
1255 child_property_value(&child, &parsed.bindings, "mPr", "cSp")?
1256 .and_then(|value| value.parse().ok());
1257 preserve_modeled_child(
1258 &mut parsed.preservation,
1259 "mPr",
1260 &child,
1261 &parsed.bindings,
1262 );
1263 modeled += 1;
1264 }
1265 Some("mr") => {
1266 rows.push(parse_matrix_row(&child, &parsed.bindings)?);
1267 modeled += 1;
1268 }
1269 _ => parsed.preservation.raw_children.push((modeled, child)),
1270 }
1271 }
1272 Ok(Self {
1273 properties,
1274 rows,
1275 preservation: parsed.preservation,
1276 })
1277 }
1278
1279 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1280 if self.rows.is_empty() || self.rows.iter().any(|row| row.cells.is_empty()) {
1281 return Err(OxmlError::InvalidValue(
1282 "OfficeMath matrix requires at least one nonempty row".to_owned(),
1283 ));
1284 }
1285 write_container(writer, "m", &self.preservation, |writer| {
1286 let properties = [
1287 self.properties
1288 .base_justification
1289 .map(|value| Property::text("baseJc", value.as_str())),
1290 self.properties
1291 .row_spacing
1292 .map(|value| Property::unsigned("rSp", value.into())),
1293 self.properties
1294 .column_spacing
1295 .map(|value| Property::unsigned("cSp", value.into())),
1296 ]
1297 .into_iter()
1298 .flatten()
1299 .collect::<Vec<_>>();
1300 let emit_properties =
1301 !properties.is_empty() || has_preserved_modeled_child(&self.preservation, "mPr");
1302 let mut modeled = write_leading_property_container(
1303 writer,
1304 "mPr",
1305 &properties,
1306 &self.preservation,
1307 emit_properties,
1308 )?;
1309 for row in &self.rows {
1310 write_container(writer, "mr", &row.preservation, |writer| {
1311 write_raw_slot(writer, &row.preservation, 0)?;
1312 for (index, cell) in row.cells.iter().enumerate() {
1313 cell.write_xml(writer, "e")?;
1314 write_raw_slot(writer, &row.preservation, index + 1)?;
1315 }
1316 write_raw_tail(writer, &row.preservation, row.cells.len() + 1)
1317 })?;
1318 modeled += 1;
1319 write_raw_slot(writer, &self.preservation, modeled)?;
1320 }
1321 write_raw_tail(writer, &self.preservation, modeled + 1)
1322 })
1323 }
1324}
1325
1326fn parse_matrix_row(raw: &[u8], inherited: &[(String, String)]) -> Result<MathMatrixRow> {
1327 let mut parsed = parse_element(raw, inherited)?;
1328 let mut cells = Vec::new();
1329 for child in parsed.children {
1330 if math_local_name(&child, &parsed.bindings)?.as_deref() == Some("e") {
1331 cells.push(MathArgument::from_raw(&child, &parsed.bindings)?);
1332 } else {
1333 parsed.preservation.raw_children.push((cells.len(), child));
1334 }
1335 }
1336 Ok(MathMatrixRow {
1337 cells,
1338 preservation: parsed.preservation,
1339 })
1340}
1341
1342#[derive(Debug, Clone, PartialEq, Eq)]
1343pub struct MathLimit {
1344 pub base: MathArgument,
1345 pub limit: MathArgument,
1346 preservation: Preservation,
1347}
1348
1349impl MathLimit {
1350 pub fn new(base: MathArgument, limit: MathArgument) -> Self {
1351 Self {
1352 base,
1353 limit,
1354 preservation: Preservation::default(),
1355 }
1356 }
1357
1358 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
1359 let mut parsed = parse_element(raw, inherited)?;
1360 let mut base = MathArgument::default();
1361 let mut limit = MathArgument::default();
1362 let mut modeled = 0usize;
1363 for child in parsed.children {
1364 match math_local_name(&child, &parsed.bindings)?.as_deref() {
1365 Some(property @ ("limLowPr" | "limUppPr")) if modeled == 0 => {
1366 preserve_modeled_child(
1367 &mut parsed.preservation,
1368 property,
1369 &child,
1370 &parsed.bindings,
1371 );
1372 modeled += 1;
1373 }
1374 Some("e") => {
1375 base = MathArgument::from_raw(&child, &parsed.bindings)?;
1376 modeled += 1;
1377 }
1378 Some("lim") => {
1379 limit = MathArgument::from_raw(&child, &parsed.bindings)?;
1380 modeled += 1;
1381 }
1382 _ => parsed.preservation.raw_children.push((modeled, child)),
1383 }
1384 }
1385 Ok(Self {
1386 base,
1387 limit,
1388 preservation: parsed.preservation,
1389 })
1390 }
1391
1392 fn write_xml<W: Write>(&self, writer: &mut Writer<W>, tag: &str) -> Result<()> {
1393 write_container(writer, tag, &self.preservation, |writer| {
1394 let property_tag = if tag == "limLow" {
1395 "limLowPr"
1396 } else {
1397 "limUppPr"
1398 };
1399 let mut modeled = 0usize;
1400 write_raw_slot(writer, &self.preservation, modeled)?;
1401 if has_preserved_modeled_child(&self.preservation, property_tag) {
1402 write_property_container_from_parent(
1403 writer,
1404 property_tag,
1405 &[],
1406 &self.preservation,
1407 )?;
1408 modeled += 1;
1409 write_raw_slot(writer, &self.preservation, modeled)?;
1410 }
1411 self.base.write_xml(writer, "e")?;
1412 modeled += 1;
1413 write_raw_slot(writer, &self.preservation, modeled)?;
1414 self.limit.write_xml(writer, "lim")?;
1415 modeled += 1;
1416 write_raw_slot(writer, &self.preservation, modeled)
1417 })
1418 }
1419}
1420
1421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1422pub enum LimitLocation {
1423 SubSuperscript,
1424 UnderOver,
1425}
1426
1427impl LimitLocation {
1428 fn parse(value: &str) -> Option<Self> {
1429 match value {
1430 "subSup" => Some(Self::SubSuperscript),
1431 "undOvr" => Some(Self::UnderOver),
1432 _ => None,
1433 }
1434 }
1435 fn as_str(self) -> &'static str {
1436 match self {
1437 Self::SubSuperscript => "subSup",
1438 Self::UnderOver => "undOvr",
1439 }
1440 }
1441}
1442
1443#[derive(Debug, Clone, PartialEq, Eq)]
1444pub struct MathNary {
1445 pub character: String,
1446 pub base: MathArgument,
1447 pub subscript: MathArgument,
1448 pub superscript: MathArgument,
1449 pub hide_subscript: bool,
1450 pub hide_superscript: bool,
1451 pub grow: Option<bool>,
1452 pub limit_location: Option<LimitLocation>,
1453 subscript_present: bool,
1454 superscript_present: bool,
1455 preservation: Preservation,
1456}
1457
1458impl MathNary {
1459 pub fn new(character: impl Into<String>, base: MathArgument) -> Self {
1460 Self {
1461 character: character.into(),
1462 base,
1463 subscript: MathArgument::default(),
1464 superscript: MathArgument::default(),
1465 hide_subscript: true,
1466 hide_superscript: true,
1467 grow: None,
1468 limit_location: None,
1469 subscript_present: false,
1470 superscript_present: false,
1471 preservation: Preservation::default(),
1472 }
1473 }
1474
1475 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
1476 let mut parsed = parse_element(raw, inherited)?;
1477 let mut value = Self::new("∫", MathArgument::default());
1478 value.hide_subscript = false;
1479 value.hide_superscript = false;
1480 let mut modeled = 0usize;
1481 for child in parsed.children {
1482 match math_local_name(&child, &parsed.bindings)?.as_deref() {
1483 Some("naryPr") if modeled == 0 => {
1484 value.character =
1485 child_property_value(&child, &parsed.bindings, "naryPr", "chr")?
1486 .unwrap_or(value.character);
1487 value.hide_subscript =
1488 child_on_off_value(&child, &parsed.bindings, "subHide")?.unwrap_or(false);
1489 value.hide_superscript =
1490 child_on_off_value(&child, &parsed.bindings, "supHide")?.unwrap_or(false);
1491 value.grow = child_on_off_value(&child, &parsed.bindings, "grow")?;
1492 value.limit_location =
1493 child_property_value(&child, &parsed.bindings, "naryPr", "limLoc")?
1494 .and_then(|value| LimitLocation::parse(&value));
1495 preserve_modeled_child(
1496 &mut parsed.preservation,
1497 "naryPr",
1498 &child,
1499 &parsed.bindings,
1500 );
1501 modeled += 1;
1502 }
1503 Some("sub") => {
1504 value.subscript = MathArgument::from_raw(&child, &parsed.bindings)?;
1505 value.subscript_present = true;
1506 modeled += 1;
1507 }
1508 Some("sup") => {
1509 value.superscript = MathArgument::from_raw(&child, &parsed.bindings)?;
1510 value.superscript_present = true;
1511 modeled += 1;
1512 }
1513 Some("e") => {
1514 value.base = MathArgument::from_raw(&child, &parsed.bindings)?;
1515 modeled += 1;
1516 }
1517 _ => parsed.preservation.raw_children.push((modeled, child)),
1518 }
1519 }
1520 value.preservation = parsed.preservation;
1521 Ok(value)
1522 }
1523
1524 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1525 if self.character.chars().count() > 1 {
1526 return Err(OxmlError::InvalidValue(
1527 "OfficeMath n-ary character must contain at most one Unicode scalar".to_owned(),
1528 ));
1529 }
1530 write_container(writer, "nary", &self.preservation, |writer| {
1531 let mut properties = vec![Property::text("chr", &self.character)];
1532 if let Some(value) = self.limit_location {
1533 properties.push(Property::text("limLoc", value.as_str()));
1534 }
1535 if let Some(value) = self.grow {
1536 properties.push(Property::boolean("grow", value));
1537 }
1538 properties.push(Property::boolean("subHide", self.hide_subscript));
1539 properties.push(Property::boolean("supHide", self.hide_superscript));
1540 let mut modeled = write_leading_property_container_before_raw(
1541 writer,
1542 "naryPr",
1543 &properties,
1544 &self.preservation,
1545 true,
1546 )?;
1547 let emit_subscript = self.subscript_present
1548 || !self.subscript.expressions.is_empty()
1549 || self.subscript.has_unsupported_content();
1550 if self.subscript_present {
1551 write_raw_slot(writer, &self.preservation, modeled)?;
1552 self.subscript.write_xml(writer, "sub")?;
1553 modeled += 1;
1554 } else if emit_subscript {
1555 self.subscript.write_xml(writer, "sub")?;
1556 }
1557 let emit_superscript = self.superscript_present
1558 || !self.superscript.expressions.is_empty()
1559 || self.superscript.has_unsupported_content();
1560 if self.superscript_present {
1561 write_raw_slot(writer, &self.preservation, modeled)?;
1562 self.superscript.write_xml(writer, "sup")?;
1563 modeled += 1;
1564 } else if emit_superscript {
1565 self.superscript.write_xml(writer, "sup")?;
1566 }
1567 write_raw_slot(writer, &self.preservation, modeled)?;
1568 self.base.write_xml(writer, "e")?;
1569 modeled += 1;
1570 write_raw_slot(writer, &self.preservation, modeled)
1571 })
1572 }
1573}
1574
1575#[derive(Debug, Clone, PartialEq, Eq)]
1576pub struct MathDelimiter {
1577 pub begin_character: String,
1578 pub end_character: String,
1579 pub separator_character: String,
1580 pub grow: Option<bool>,
1581 pub arguments: Vec<MathArgument>,
1582 preservation: Preservation,
1583}
1584
1585impl MathDelimiter {
1586 pub fn new(
1587 begin: impl Into<String>,
1588 end: impl Into<String>,
1589 arguments: Vec<MathArgument>,
1590 ) -> Self {
1591 Self {
1592 begin_character: begin.into(),
1593 end_character: end.into(),
1594 separator_character: "|".to_owned(),
1595 grow: None,
1596 arguments,
1597 preservation: Preservation::default(),
1598 }
1599 }
1600
1601 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
1602 let mut parsed = parse_element(raw, inherited)?;
1603 let mut value = Self::new("(", ")", Vec::new());
1604 let mut modeled = 0usize;
1605 for child in parsed.children {
1606 match math_local_name(&child, &parsed.bindings)?.as_deref() {
1607 Some("dPr") if modeled == 0 => {
1608 value.begin_character =
1609 child_property_value(&child, &parsed.bindings, "dPr", "begChr")?
1610 .unwrap_or(value.begin_character);
1611 value.end_character =
1612 child_property_value(&child, &parsed.bindings, "dPr", "endChr")?
1613 .unwrap_or(value.end_character);
1614 value.separator_character =
1615 child_property_value(&child, &parsed.bindings, "dPr", "sepChr")?
1616 .unwrap_or(value.separator_character);
1617 value.grow = child_on_off_value(&child, &parsed.bindings, "grow")?;
1618 preserve_modeled_child(
1619 &mut parsed.preservation,
1620 "dPr",
1621 &child,
1622 &parsed.bindings,
1623 );
1624 modeled += 1;
1625 }
1626 Some("e") => {
1627 value
1628 .arguments
1629 .push(MathArgument::from_raw(&child, &parsed.bindings)?);
1630 modeled += 1;
1631 }
1632 _ => parsed.preservation.raw_children.push((modeled, child)),
1633 }
1634 }
1635 value.preservation = parsed.preservation;
1636 Ok(value)
1637 }
1638
1639 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1640 if self.arguments.is_empty()
1641 || [
1642 &self.begin_character,
1643 &self.separator_character,
1644 &self.end_character,
1645 ]
1646 .iter()
1647 .any(|value| value.chars().count() > 1)
1648 {
1649 return Err(OxmlError::InvalidValue(
1650 "OfficeMath delimiter requires arguments and scalar characters".to_owned(),
1651 ));
1652 }
1653 write_container(writer, "d", &self.preservation, |writer| {
1654 let mut properties = vec![
1655 Property::text("begChr", &self.begin_character),
1656 Property::text("sepChr", &self.separator_character),
1657 Property::text("endChr", &self.end_character),
1658 ];
1659 if let Some(value) = self.grow {
1660 properties.push(Property::boolean("grow", value));
1661 }
1662 let mut modeled = write_leading_property_container(
1663 writer,
1664 "dPr",
1665 &properties,
1666 &self.preservation,
1667 true,
1668 )?;
1669 for argument in &self.arguments {
1670 argument.write_xml(writer, "e")?;
1671 modeled += 1;
1672 write_raw_slot(writer, &self.preservation, modeled)?;
1673 }
1674 write_raw_tail(writer, &self.preservation, modeled + 1)
1675 })
1676 }
1677}
1678
1679#[derive(Debug, Clone, PartialEq, Eq)]
1680pub struct MathAccent {
1681 pub character: String,
1682 pub base: MathArgument,
1683 preservation: Preservation,
1684}
1685
1686impl MathAccent {
1687 pub fn new(character: impl Into<String>, base: MathArgument) -> Self {
1688 Self {
1689 character: character.into(),
1690 base,
1691 preservation: Preservation::default(),
1692 }
1693 }
1694
1695 fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
1696 let mut parsed = parse_element(raw, inherited)?;
1697 let mut character = "̂".to_owned();
1698 let mut base = MathArgument::default();
1699 let mut modeled = 0usize;
1700 for child in parsed.children {
1701 match math_local_name(&child, &parsed.bindings)?.as_deref() {
1702 Some("accPr") if modeled == 0 => {
1703 character = child_property_value(&child, &parsed.bindings, "accPr", "chr")?
1704 .unwrap_or(character);
1705 preserve_modeled_child(
1706 &mut parsed.preservation,
1707 "accPr",
1708 &child,
1709 &parsed.bindings,
1710 );
1711 modeled += 1;
1712 }
1713 Some("e") => {
1714 base = MathArgument::from_raw(&child, &parsed.bindings)?;
1715 modeled += 1;
1716 }
1717 _ => parsed.preservation.raw_children.push((modeled, child)),
1718 }
1719 }
1720 Ok(Self {
1721 character,
1722 base,
1723 preservation: parsed.preservation,
1724 })
1725 }
1726
1727 fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1728 if self.character.chars().count() > 1 {
1729 return Err(OxmlError::InvalidValue(
1730 "OfficeMath accent character must contain at most one Unicode scalar".to_owned(),
1731 ));
1732 }
1733 write_container(writer, "acc", &self.preservation, |writer| {
1734 let mut modeled = write_leading_property_container(
1735 writer,
1736 "accPr",
1737 &[Property::text("chr", &self.character)],
1738 &self.preservation,
1739 true,
1740 )?;
1741 self.base.write_xml(writer, "e")?;
1742 modeled += 1;
1743 write_raw_slot(writer, &self.preservation, modeled)
1744 })
1745 }
1746}
1747
1748#[derive(Debug, Clone, PartialEq, Eq, Default)]
1750pub struct MathProperties {
1751 pub math_font: Option<String>,
1752 pub justification: Option<MathJustification>,
1753 pub left_margin: Option<u32>,
1754 pub right_margin: Option<u32>,
1755 pub pre_spacing: Option<u32>,
1756 pub post_spacing: Option<u32>,
1757 pub inter_spacing: Option<u32>,
1758 pub intra_spacing: Option<u32>,
1759 pub wrap_indent: Option<u32>,
1760 pub small_fraction: Option<bool>,
1761 pub display_defaults: Option<bool>,
1762 pub integral_limit_location: Option<LimitLocation>,
1763 pub nary_limit_location: Option<LimitLocation>,
1764 preservation: Preservation,
1765}
1766
1767impl MathProperties {
1768 pub fn new() -> Self {
1769 Self::default()
1770 }
1771
1772 pub fn has_unsupported_content(&self) -> bool {
1774 property_preservation_has_unsupported_content(&self.preservation, "mathPr")
1775 }
1776
1777 pub(crate) fn from_raw(raw: &[u8], inherited: &[(String, String)]) -> Result<Self> {
1778 require_safe_fixed_math_prefix(raw, inherited)?;
1779 let mut parsed = parse_element(raw, inherited)?;
1780 require_root(raw, &parsed.bindings, b"mathPr")?;
1781 let mut value = Self::default();
1782 let mut modeled = 0usize;
1783 let mut seen = Vec::new();
1784 let mut last_rank = None;
1785 for child in parsed.children {
1786 let local = math_local_name(&child, &parsed.bindings)?;
1787 let Some(name) = local.as_deref() else {
1788 let key = property_raw_key("mathPr", None, &mut last_rank, false);
1789 parsed.preservation.property_raw_children.push((key, child));
1790 continue;
1791 };
1792 let duplicate = seen.iter().any(|seen_name| seen_name == name);
1793 if !supported_properties("mathPr").contains(&name)
1794 || duplicate
1795 || !property_leaf_is_valid("mathPr", name, &child, &parsed.bindings)?
1796 {
1797 let key = property_raw_key("mathPr", Some(name), &mut last_rank, duplicate);
1798 parsed.preservation.property_raw_children.push((key, child));
1799 continue;
1800 }
1801 property_raw_key("mathPr", Some(name), &mut last_rank, false);
1802 let parsed_value = root_value(&child, &parsed.bindings)?
1803 .or_else(|| property_value_default("mathPr", name).map(str::to_owned));
1804 let recognized = match name {
1805 "mathFont" => {
1806 value.math_font = parsed_value;
1807 true
1808 }
1809 "defJc" => {
1810 value.justification =
1811 parsed_value.as_deref().and_then(MathJustification::parse);
1812 true
1813 }
1814 "lMargin" => {
1815 value.left_margin = parse_u32(parsed_value);
1816 true
1817 }
1818 "rMargin" => {
1819 value.right_margin = parse_u32(parsed_value);
1820 true
1821 }
1822 "preSp" => {
1823 value.pre_spacing = parse_u32(parsed_value);
1824 true
1825 }
1826 "postSp" => {
1827 value.post_spacing = parse_u32(parsed_value);
1828 true
1829 }
1830 "interSp" => {
1831 value.inter_spacing = parse_u32(parsed_value);
1832 true
1833 }
1834 "intraSp" => {
1835 value.intra_spacing = parse_u32(parsed_value);
1836 true
1837 }
1838 "wrapIndent" => {
1839 value.wrap_indent = parse_u32(parsed_value);
1840 true
1841 }
1842 "smallFrac" => {
1843 value.small_fraction = parse_present_on_off(parsed_value.as_deref());
1844 true
1845 }
1846 "dispDef" => {
1847 value.display_defaults = parse_present_on_off(parsed_value.as_deref());
1848 true
1849 }
1850 "intLim" => {
1851 value.integral_limit_location =
1852 parsed_value.as_deref().and_then(LimitLocation::parse);
1853 true
1854 }
1855 "naryLim" => {
1856 value.nary_limit_location =
1857 parsed_value.as_deref().and_then(LimitLocation::parse);
1858 true
1859 }
1860 _ => false,
1861 };
1862 if recognized {
1863 seen.push(name.to_owned());
1864 preserve_modeled_child(&mut parsed.preservation, name, &child, &parsed.bindings);
1865 modeled += 1;
1866 } else {
1867 parsed.preservation.raw_children.push((modeled, child));
1868 }
1869 }
1870 value.preservation = parsed.preservation;
1871 Ok(value)
1872 }
1873
1874 pub(crate) fn write_xml<W: Write>(&self, writer: &mut Writer<W>) -> Result<()> {
1875 let mut properties = Vec::new();
1876 if let Some(value) = &self.math_font {
1877 properties.push(Property::text("mathFont", value));
1878 }
1879 if let Some(value) = self.small_fraction {
1880 properties.push(Property::boolean("smallFrac", value));
1881 }
1882 if let Some(value) = self.display_defaults {
1883 properties.push(Property::boolean("dispDef", value));
1884 }
1885 if let Some(value) = self.left_margin {
1886 properties.push(Property::unsigned("lMargin", value.into()));
1887 }
1888 if let Some(value) = self.right_margin {
1889 properties.push(Property::unsigned("rMargin", value.into()));
1890 }
1891 if let Some(value) = self.justification {
1892 properties.push(Property::text("defJc", value.as_str()));
1893 }
1894 for (name, value) in [
1895 ("preSp", self.pre_spacing),
1896 ("postSp", self.post_spacing),
1897 ("interSp", self.inter_spacing),
1898 ("intraSp", self.intra_spacing),
1899 ("wrapIndent", self.wrap_indent),
1900 ] {
1901 if let Some(value) = value {
1902 properties.push(Property::unsigned(name, value.into()));
1903 }
1904 }
1905 if let Some(value) = self.integral_limit_location {
1906 properties.push(Property::text("intLim", value.as_str()));
1907 }
1908 if let Some(value) = self.nary_limit_location {
1909 properties.push(Property::text("naryLim", value.as_str()));
1910 }
1911 write_property_container_with_preservation(
1912 writer,
1913 "mathPr",
1914 &properties,
1915 &self.preservation,
1916 )
1917 }
1918}
1919
1920pub(crate) fn is_math_element(name: &[u8], local: &[u8], scope: &[String]) -> bool {
1921 let (prefix, actual_local) = name
1922 .iter()
1923 .position(|byte| *byte == b':')
1924 .map_or((&b""[..], name), |at| (&name[..at], &name[at + 1..]));
1925 actual_local == local
1926 && scope.iter().any(|binding| {
1927 binding
1928 .strip_prefix('\0')
1929 .and_then(|binding| binding.split_once('\0'))
1930 .is_some_and(|(candidate, namespace)| {
1931 candidate.as_bytes() == prefix && namespace == M_NS
1932 })
1933 })
1934}
1935
1936fn parse_run_properties(raw: &[u8], inherited: &[(String, String)]) -> Result<MathRunProperties> {
1937 let mut parsed = parse_element(raw, inherited)?;
1938 let mut value = MathRunProperties::default();
1939 let mut modeled = 0usize;
1940 let mut seen = Vec::new();
1941 let mut last_rank = None;
1942 for child in parsed.children {
1943 let local = math_local_name(&child, &parsed.bindings)?;
1944 let Some(name) = local.as_deref() else {
1945 let key = property_raw_key("rPr", None, &mut last_rank, false);
1946 parsed.preservation.property_raw_children.push((key, child));
1947 continue;
1948 };
1949 let duplicate = seen.iter().any(|seen_name| seen_name == name);
1950 if !supported_properties("rPr").contains(&name)
1951 || duplicate
1952 || !property_leaf_is_valid("rPr", name, &child, &parsed.bindings)?
1953 {
1954 let key = property_raw_key("rPr", Some(name), &mut last_rank, duplicate);
1955 parsed.preservation.property_raw_children.push((key, child));
1956 continue;
1957 }
1958 property_raw_key("rPr", Some(name), &mut last_rank, false);
1959 let parsed_value = root_value(&child, &parsed.bindings)?
1960 .or_else(|| property_value_default("rPr", name).map(str::to_owned));
1961 let recognized = match name {
1962 "sty" => {
1963 value.style = parsed_value.as_deref().and_then(MathStyle::parse);
1964 true
1965 }
1966 "nor" => {
1967 value.normal = parse_present_on_off(parsed_value.as_deref());
1968 true
1969 }
1970 "lit" => {
1971 value.literal = parse_present_on_off(parsed_value.as_deref());
1972 true
1973 }
1974 "scr" => {
1975 value.script = parsed_value.as_deref().and_then(MathScriptStyle::parse);
1976 true
1977 }
1978 "brk" => {
1979 let alignment = root_attribute(&child, &parsed.bindings, b"alnAt")?;
1980 match alignment {
1981 Some(alignment) => match alignment.parse::<u8>() {
1982 Ok(0) | Err(_) => false,
1983 Ok(alignment) => {
1984 value.break_before = true;
1985 value.break_alignment = Some(alignment);
1986 true
1987 }
1988 },
1989 None => {
1990 value.break_before = true;
1991 true
1992 }
1993 }
1994 }
1995 _ => false,
1996 };
1997 if recognized {
1998 seen.push(name.to_owned());
1999 preserve_modeled_child(&mut parsed.preservation, name, &child, &parsed.bindings);
2000 modeled += 1;
2001 } else {
2002 parsed.preservation.raw_children.push((modeled, child));
2003 }
2004 }
2005 value.preservation = parsed.preservation;
2006 Ok(value)
2007}
2008
2009fn write_run_properties<W: Write>(writer: &mut Writer<W>, value: &MathRunProperties) -> Result<()> {
2010 let mut properties = Vec::new();
2011 if let Some(literal) = value.literal {
2012 properties.push(Property::boolean("lit", literal));
2013 }
2014 if let Some(normal) = value.normal {
2015 properties.push(Property::boolean("nor", normal));
2016 }
2017 if let Some(script) = value.script {
2018 properties.push(Property::text("scr", script.as_str()));
2019 }
2020 if let Some(style) = value.style {
2021 properties.push(Property::text("sty", style.as_str()));
2022 }
2023 if let Some(alignment) = value.break_alignment {
2024 properties.push(Property::attribute("brk", "alnAt", alignment.to_string()));
2025 } else if value.break_before {
2026 properties.push(Property::marker("brk"));
2027 }
2028 write_property_container_with_preservation(writer, "rPr", &properties, &value.preservation)
2029}
2030
2031enum PropertyValue<'a> {
2032 Text(&'a str),
2033 Owned(String),
2034}
2035struct Property<'a> {
2036 name: &'a str,
2037 attribute: Option<(&'a str, PropertyValue<'a>)>,
2038}
2039impl<'a> Property<'a> {
2040 fn text(name: &'a str, value: &'a str) -> Self {
2041 Self {
2042 name,
2043 attribute: Some(("val", PropertyValue::Text(value))),
2044 }
2045 }
2046 fn boolean(name: &'a str, value: bool) -> Self {
2047 Self {
2048 name,
2049 attribute: Some(("val", PropertyValue::Text(if value { "1" } else { "0" }))),
2050 }
2051 }
2052 fn unsigned(name: &'a str, value: u64) -> Self {
2053 Self {
2054 name,
2055 attribute: Some(("val", PropertyValue::Owned(value.to_string()))),
2056 }
2057 }
2058 fn attribute(name: &'a str, attribute: &'a str, value: String) -> Self {
2059 Self {
2060 name,
2061 attribute: Some((attribute, PropertyValue::Owned(value))),
2062 }
2063 }
2064 fn marker(name: &'a str) -> Self {
2065 Self {
2066 name,
2067 attribute: None,
2068 }
2069 }
2070 fn attribute_value(&self) -> Option<(&str, &str)> {
2071 let (name, value) = self.attribute.as_ref()?;
2072 let value = match value {
2073 PropertyValue::Text(value) => *value,
2074 PropertyValue::Owned(value) => value.as_str(),
2075 };
2076 Some((name, value))
2077 }
2078}
2079
2080fn write_property_container<W: Write>(
2081 writer: &mut Writer<W>,
2082 tag: &str,
2083 properties: &[Property<'_>],
2084) -> Result<()> {
2085 write_property_container_with_preservation(writer, tag, properties, &Preservation::default())
2086}
2087
2088fn write_math_text<W: Write>(
2089 writer: &mut Writer<W>,
2090 text_value: &str,
2091 parent: &Preservation,
2092) -> Result<()> {
2093 let mut preservation = if let Some(source) = parent
2094 .modeled_children
2095 .iter()
2096 .find(|child| child.name == "t")
2097 {
2098 parse_element(&source.raw, &source.bindings)?.preservation
2099 } else {
2100 Preservation::default()
2101 };
2102 preservation
2103 .attributes
2104 .retain(|(name, _)| name != "xml:space");
2105 let mut start = math_start("t", &preservation, false);
2106 if text_value.starts_with(char::is_whitespace) || text_value.ends_with(char::is_whitespace) {
2107 start.push_attribute(("xml:space", "preserve"));
2108 }
2109 writer.write_event(Event::Start(start.borrow()))?;
2110 writer.write_event(Event::Text(BytesText::new(text_value)))?;
2111 writer.write_event(Event::End(BytesEnd::new("m:t")))?;
2112 Ok(())
2113}
2114
2115fn preserve_modeled_child(
2116 preservation: &mut Preservation,
2117 name: &str,
2118 raw: &[u8],
2119 bindings: &[(String, String)],
2120) {
2121 preservation.modeled_children.push(PreservedChild {
2122 name: name.to_owned(),
2123 raw: raw.to_vec(),
2124 bindings: bindings.to_vec(),
2125 });
2126}
2127
2128fn preservation_has_unsupported_content(preservation: &Preservation) -> bool {
2129 preservation
2130 .attributes
2131 .iter()
2132 .any(|(name, _)| name != "xmlns" && !name.starts_with("xmlns:"))
2133 || !preservation.raw_children.is_empty()
2134 || !preservation.property_raw_children.is_empty()
2135}
2136
2137fn property_preservation_has_unsupported_content(
2138 preservation: &Preservation,
2139 container: &str,
2140) -> bool {
2141 preservation_has_unsupported_content(preservation)
2142 || preservation.modeled_children.iter().any(|child| {
2143 parse_element(&child.raw, &child.bindings).map_or(true, |parsed| {
2144 let modeled_attribute = property_modeled_attribute(container, &child.name);
2145 parsed.preservation.attributes.iter().any(|(name, _)| {
2146 if name == "xmlns" || name.starts_with("xmlns:") {
2147 return false;
2148 }
2149 expanded_attribute_name(name.as_bytes(), &parsed.bindings).is_none_or(
2150 |(namespace, local)| {
2151 namespace != M_NS || local.as_slice() != modeled_attribute
2152 },
2153 )
2154 }) || !parsed.children.is_empty()
2155 })
2156 })
2157}
2158
2159fn property_container_has_unsupported_content(preservation: &Preservation, tag: &str) -> bool {
2160 let Some(source) = preservation
2161 .modeled_children
2162 .iter()
2163 .find(|child| child.name == tag)
2164 else {
2165 return false;
2166 };
2167 preserve_property_container(
2168 &source.raw,
2169 &source.bindings,
2170 tag,
2171 supported_properties(tag),
2172 )
2173 .map_or(true, |value| {
2174 property_preservation_has_unsupported_content(&value, tag)
2175 })
2176}
2177
2178fn math_text_has_unsupported_content(preservation: &Preservation) -> bool {
2179 let Some(source) = preservation
2180 .modeled_children
2181 .iter()
2182 .find(|child| child.name == "t")
2183 else {
2184 return false;
2185 };
2186 parse_element(&source.raw, &source.bindings).map_or(true, |parsed| {
2187 parsed.preservation.attributes.iter().any(|(name, value)| {
2188 if name == "xmlns" || name.starts_with("xmlns:") {
2189 false
2190 } else if name == "xml:space" {
2191 !matches!(value.as_str(), "default" | "preserve")
2192 } else {
2193 true
2194 }
2195 })
2196 })
2197}
2198
2199fn script_has_unsupported_content(value: &MathScript, property_tag: &str) -> bool {
2200 preservation_has_unsupported_content(&value.preservation)
2201 || property_container_has_unsupported_content(&value.preservation, property_tag)
2202 || value.base.has_unsupported_content()
2203 || value.script.has_unsupported_content()
2204}
2205
2206fn three_argument_script_has_unsupported_content(
2207 preservation: &Preservation,
2208 property_tag: &str,
2209 base: &MathArgument,
2210 subscript: &MathArgument,
2211 superscript: &MathArgument,
2212) -> bool {
2213 preservation_has_unsupported_content(preservation)
2214 || property_container_has_unsupported_content(preservation, property_tag)
2215 || base.has_unsupported_content()
2216 || subscript.has_unsupported_content()
2217 || superscript.has_unsupported_content()
2218}
2219
2220fn limit_has_unsupported_content(value: &MathLimit, property_tag: &str) -> bool {
2221 preservation_has_unsupported_content(&value.preservation)
2222 || property_container_has_unsupported_content(&value.preservation, property_tag)
2223 || value.base.has_unsupported_content()
2224 || value.limit.has_unsupported_content()
2225}
2226
2227fn has_preserved_modeled_child(preservation: &Preservation, name: &str) -> bool {
2228 preservation
2229 .modeled_children
2230 .iter()
2231 .any(|child| child.name == name)
2232}
2233
2234fn write_leading_property_container<W: Write>(
2235 writer: &mut Writer<W>,
2236 tag: &str,
2237 properties: &[Property<'_>],
2238 preservation: &Preservation,
2239 emit: bool,
2240) -> Result<usize> {
2241 let modeled =
2242 write_leading_property_container_before_raw(writer, tag, properties, preservation, emit)?;
2243 write_raw_slot(writer, preservation, modeled)?;
2244 Ok(modeled)
2245}
2246
2247fn write_leading_property_container_before_raw<W: Write>(
2248 writer: &mut Writer<W>,
2249 tag: &str,
2250 properties: &[Property<'_>],
2251 preservation: &Preservation,
2252 emit: bool,
2253) -> Result<usize> {
2254 let existed_in_source = has_preserved_modeled_child(preservation, tag);
2255 if emit && existed_in_source {
2256 write_raw_slot(writer, preservation, 0)?;
2257 write_property_container_from_parent(writer, tag, properties, preservation)?;
2258 Ok(1)
2259 } else {
2260 if emit {
2261 write_property_container_from_parent(writer, tag, properties, preservation)?;
2262 }
2263 Ok(0)
2264 }
2265}
2266
2267fn write_property_container_from_parent<W: Write>(
2268 writer: &mut Writer<W>,
2269 tag: &str,
2270 properties: &[Property<'_>],
2271 parent: &Preservation,
2272) -> Result<()> {
2273 let Some(source) = parent
2274 .modeled_children
2275 .iter()
2276 .find(|child| child.name == tag)
2277 else {
2278 return write_property_container(writer, tag, properties);
2279 };
2280 let preservation = preserve_property_container(
2281 &source.raw,
2282 &source.bindings,
2283 tag,
2284 supported_properties(tag),
2285 )?;
2286 write_property_container_with_preservation(writer, tag, properties, &preservation)
2287}
2288
2289fn write_property_container_with_preservation<W: Write>(
2290 writer: &mut Writer<W>,
2291 tag: &str,
2292 properties: &[Property<'_>],
2293 preservation: &Preservation,
2294) -> Result<()> {
2295 let declare_math = tag == "mathPr";
2296 let qname = format!("m:{tag}");
2297 let start = math_start(tag, preservation, declare_math);
2298 writer.write_event(Event::Start(start.borrow()))?;
2299 let result = if preservation.property_raw_children.is_empty() {
2300 (|| {
2301 write_raw_slot(writer, preservation, 0)?;
2302 for (index, property) in properties.iter().enumerate() {
2303 write_property(writer, property, preservation)?;
2304 if index + 1 < properties.len() {
2305 write_raw_slot(writer, preservation, index + 1)?;
2306 }
2307 }
2308 write_raw_tail(writer, preservation, properties.len())
2309 })()
2310 } else {
2311 let full_order = full_property_order(tag);
2312 let mut emissions = preservation
2313 .property_raw_children
2314 .iter()
2315 .enumerate()
2316 .map(|(source_order, (key, raw))| (*key, source_order, None, Some(raw)))
2317 .collect::<Vec<_>>();
2318 emissions.extend(properties.iter().enumerate().map(|(index, property)| {
2319 let rank = full_order
2320 .iter()
2321 .position(|name| *name == property.name)
2322 .unwrap_or(full_order.len());
2323 (rank * 3 + 1, index, Some(property), None)
2324 }));
2325 emissions.sort_by_key(|(key, source_order, _, _)| (*key, *source_order));
2326 for (_, _, property, raw) in emissions {
2327 if let Some(source) = preservation
2328 .modeled_children
2329 .iter()
2330 .find(|child| property.is_some_and(|property| child.name == property.name))
2331 {
2332 write_property_leaf(
2333 writer,
2334 property.expect("source requires a property"),
2335 &source.raw,
2336 &source.bindings,
2337 )?;
2338 } else if let Some(property) = property {
2339 write_new_property(writer, property)?;
2340 } else if let Some(raw) = raw {
2341 writer.get_mut().write_all(raw)?;
2342 }
2343 }
2344 Ok(())
2345 };
2346 result?;
2347 writer.write_event(Event::End(BytesEnd::new(qname)))?;
2348 Ok(())
2349}
2350
2351fn write_property<W: Write>(
2352 writer: &mut Writer<W>,
2353 property: &Property<'_>,
2354 preservation: &Preservation,
2355) -> Result<()> {
2356 if let Some(source) = preservation
2357 .modeled_children
2358 .iter()
2359 .find(|child| child.name == property.name)
2360 {
2361 write_property_leaf(writer, property, &source.raw, &source.bindings)
2362 } else {
2363 write_new_property(writer, property)
2364 }
2365}
2366
2367fn write_new_property<W: Write>(writer: &mut Writer<W>, property: &Property<'_>) -> Result<()> {
2368 let mut child = BytesStart::new(format!("m:{}", property.name));
2369 if let Some((name, value)) = property.attribute_value() {
2370 child.push_attribute((format!("m:{name}").as_str(), value));
2371 }
2372 writer.write_event(Event::Empty(child))?;
2373 Ok(())
2374}
2375
2376fn write_property_leaf<W: Write>(
2377 writer: &mut Writer<W>,
2378 property: &Property<'_>,
2379 raw: &[u8],
2380 inherited: &[(String, String)],
2381) -> Result<()> {
2382 let parsed = parse_element(raw, inherited)?;
2383 let mut preservation = parsed.preservation;
2384 let modeled_attribute = property
2385 .attribute_value()
2386 .map(|(name, _)| name.as_bytes())
2387 .unwrap_or(b"alnAt");
2388 preservation.attributes.retain(|(name, _)| {
2389 expanded_attribute_name(name.as_bytes(), &parsed.bindings)
2390 .is_none_or(|(namespace, local)| namespace != M_NS || local != modeled_attribute)
2391 });
2392 let mut start = math_start(property.name, &preservation, false);
2393 if let Some((name, value)) = property.attribute_value() {
2394 start.push_attribute((format!("m:{name}").as_str(), value));
2395 }
2396 if parsed.children.is_empty() {
2397 writer.write_event(Event::Empty(start))?;
2398 } else {
2399 writer.write_event(Event::Start(start.borrow()))?;
2400 for child in parsed.children {
2401 writer.get_mut().write_all(&child)?;
2402 }
2403 writer.write_event(Event::End(BytesEnd::new(format!("m:{}", property.name))))?;
2404 }
2405 Ok(())
2406}
2407
2408fn write_raw_tail<W: Write>(
2409 writer: &mut Writer<W>,
2410 preservation: &Preservation,
2411 from_slot: usize,
2412) -> Result<()> {
2413 for (_, raw) in preservation
2414 .raw_children
2415 .iter()
2416 .filter(|(position, _)| *position >= from_slot)
2417 {
2418 writer.get_mut().write_all(raw)?;
2419 }
2420 Ok(())
2421}
2422
2423fn supported_properties(tag: &str) -> &'static [&'static str] {
2424 match tag {
2425 "rPr" => &["lit", "nor", "scr", "sty", "brk"],
2426 "fPr" => &["type"],
2427 "sSubPr" | "sSupPr" | "sSubSupPr" | "sPrePr" => &["alnScr"],
2428 "radPr" => &["degHide"],
2429 "mPr" => &["baseJc", "rSp", "cSp"],
2430 "limLowPr" | "limUppPr" => &[],
2431 "naryPr" => &["chr", "limLoc", "grow", "subHide", "supHide"],
2432 "dPr" => &["begChr", "sepChr", "endChr", "grow"],
2433 "accPr" => &["chr"],
2434 "oMathParaPr" => &["jc"],
2435 "mathPr" => &[
2436 "mathFont",
2437 "smallFrac",
2438 "dispDef",
2439 "lMargin",
2440 "rMargin",
2441 "defJc",
2442 "preSp",
2443 "postSp",
2444 "interSp",
2445 "intraSp",
2446 "wrapIndent",
2447 "intLim",
2448 "naryLim",
2449 ],
2450 _ => &[],
2451 }
2452}
2453
2454fn property_value_default(container: &str, property: &str) -> Option<&'static str> {
2455 match (container, property) {
2456 ("rPr", "scr") => Some("roman"),
2457 ("rPr", "sty") => Some("i"),
2458 ("fPr", "type") => Some("bar"),
2459 ("mPr", "baseJc") => Some("center"),
2460 ("mPr", "rSp" | "cSp") => Some("0"),
2461 ("naryPr" | "accPr", "chr") => Some(""),
2462 ("naryPr", "limLoc") => Some("undOvr"),
2463 ("dPr", "begChr" | "sepChr" | "endChr") => Some(""),
2464 ("oMathParaPr", "jc") => Some("centerGroup"),
2465 ("mathPr", "defJc") => Some("centerGroup"),
2466 ("mathPr", "intLim") => Some("subSup"),
2467 ("mathPr", "naryLim") => Some("undOvr"),
2468 ("mathPr", "lMargin" | "rMargin" | "wrapIndent") => Some("1440"),
2469 _ => None,
2470 }
2471}
2472
2473fn property_modeled_attribute(container: &str, property: &str) -> &'static [u8] {
2474 if container == "rPr" && property == "brk" {
2475 b"alnAt"
2476 } else {
2477 b"val"
2478 }
2479}
2480
2481fn full_property_order(tag: &str) -> &'static [&'static str] {
2482 match tag {
2483 "rPr" => &["lit", "nor", "scr", "sty", "brk", "aln"],
2484 "fPr" => &["type", "ctrlPr"],
2485 "sSubPr" | "sSupPr" | "sSubSupPr" | "sPrePr" => &["alnScr", "ctrlPr"],
2486 "radPr" => &["degHide", "ctrlPr"],
2487 "mPr" => &[
2488 "baseJc", "plcHide", "rSpRule", "cGpRule", "rSp", "cSp", "cGp", "mcs", "ctrlPr",
2489 ],
2490 "limLowPr" | "limUppPr" => &["ctrlPr"],
2491 "naryPr" => &["chr", "limLoc", "grow", "subHide", "supHide", "ctrlPr"],
2492 "dPr" => &["begChr", "sepChr", "endChr", "grow", "shp", "ctrlPr"],
2493 "accPr" => &["chr", "ctrlPr"],
2494 "oMathParaPr" => &["jc"],
2495 "mathPr" => &[
2496 "mathFont",
2497 "brkBin",
2498 "brkBinSub",
2499 "smallFrac",
2500 "dispDef",
2501 "lMargin",
2502 "rMargin",
2503 "defJc",
2504 "preSp",
2505 "postSp",
2506 "interSp",
2507 "intraSp",
2508 "wrapIndent",
2509 "wrapRight",
2510 "intLim",
2511 "naryLim",
2512 ],
2513 _ => &[],
2514 }
2515}
2516
2517fn property_raw_key(
2518 tag: &str,
2519 local: Option<&str>,
2520 last_rank: &mut Option<usize>,
2521 duplicate: bool,
2522) -> usize {
2523 if let Some(rank) = local.and_then(|local| {
2524 full_property_order(tag)
2525 .iter()
2526 .position(|name| *name == local)
2527 }) {
2528 *last_rank = Some(rank);
2529 if duplicate {
2530 rank * 3 + 2
2531 } else {
2532 rank * 3 + 1
2533 }
2534 } else {
2535 last_rank.map_or(0, |rank| rank * 3 + 2)
2536 }
2537}
2538
2539fn preserve_property_container(
2540 raw: &[u8],
2541 inherited: &[(String, String)],
2542 tag: &str,
2543 recognized: &[&str],
2544) -> Result<Preservation> {
2545 let mut parsed = parse_element(raw, inherited)?;
2546 let mut seen = Vec::new();
2547 let mut last_rank = None;
2548 for child in parsed.children {
2549 let local = math_local_name(&child, &parsed.bindings)?;
2550 if let Some(local) = local
2551 .as_deref()
2552 .filter(|local| recognized.contains(local) && !seen.iter().any(|name| name == local))
2553 {
2554 seen.push(local.to_owned());
2555 property_raw_key(tag, Some(local), &mut last_rank, false);
2556 preserve_modeled_child(&mut parsed.preservation, local, &child, &parsed.bindings);
2557 } else {
2558 let duplicate = local
2559 .as_deref()
2560 .is_some_and(|local| seen.iter().any(|name| name == local));
2561 let key = property_raw_key(tag, local.as_deref(), &mut last_rank, duplicate);
2562 parsed.preservation.property_raw_children.push((key, child));
2563 }
2564 }
2565 Ok(parsed.preservation)
2566}
2567
2568fn write_container<W: Write>(
2569 writer: &mut Writer<W>,
2570 tag: &str,
2571 preservation: &Preservation,
2572 children: impl FnOnce(&mut Writer<W>) -> Result<()>,
2573) -> Result<()> {
2574 let qname = format!("m:{tag}");
2575 let start = math_start(tag, preservation, false);
2576 writer.write_event(Event::Start(start.borrow()))?;
2577 children(writer)?;
2578 writer.write_event(Event::End(BytesEnd::new(qname)))?;
2579 Ok(())
2580}
2581
2582fn math_start(tag: &str, preservation: &Preservation, declare_math: bool) -> BytesStart<'static> {
2583 let mut start = BytesStart::new(format!("m:{tag}"));
2584 if declare_math {
2585 start.push_attribute(("xmlns:m", M_NS));
2586 for (prefix, namespace) in &preservation.inherited_bindings {
2587 let name = if prefix.is_empty() {
2588 "xmlns".to_owned()
2589 } else {
2590 format!("xmlns:{prefix}")
2591 };
2592 if prefix != "m"
2593 && prefix != "xml"
2594 && !preservation
2595 .attributes
2596 .iter()
2597 .any(|(candidate, _)| candidate == &name)
2598 {
2599 start.push_attribute((name.as_str(), namespace.as_str()));
2600 }
2601 }
2602 }
2603 for (name, value) in &preservation.attributes {
2604 if name != "xmlns:m" {
2605 start.push_attribute((name.as_str(), value.as_str()));
2606 }
2607 }
2608 start.into_owned()
2609}
2610
2611fn write_raw_slot<W: Write>(
2612 writer: &mut Writer<W>,
2613 preservation: &Preservation,
2614 slot: usize,
2615) -> Result<()> {
2616 for (_, raw) in preservation
2617 .raw_children
2618 .iter()
2619 .filter(|(position, _)| *position == slot)
2620 {
2621 writer.get_mut().write_all(raw)?;
2622 }
2623 Ok(())
2624}
2625
2626fn parse_element(raw: &[u8], inherited: &[(String, String)]) -> Result<ParsedElement> {
2627 let mut reader = Reader::from_reader(raw);
2628 reader.config_mut().trim_text(false);
2629 let mut bindings = inherited.to_vec();
2630 let mut preservation = Preservation {
2631 inherited_bindings: inherited.to_vec(),
2632 ..Preservation::default()
2633 };
2634 let mut children = Vec::new();
2635 let mut inside = false;
2636 let mut saw_root = false;
2637 let mut buffer = Vec::new();
2638 loop {
2639 match reader.read_event_into(&mut buffer)? {
2640 Event::Start(element) if !inside => {
2641 saw_root = true;
2642 merge_bindings(&mut bindings, &element)?;
2643 preservation.attributes = extra_attributes(&element)?;
2644 inside = true;
2645 }
2646 Event::Empty(element) if !inside => {
2647 saw_root = true;
2648 merge_bindings(&mut bindings, &element)?;
2649 preservation.attributes = extra_attributes(&element)?;
2650 break;
2651 }
2652 Event::Start(element) if inside => {
2653 children.push(capture_element(&mut reader, &element)?)
2654 }
2655 Event::Empty(element) if inside => children.push(capture_empty(&element)?),
2656 Event::Text(text) if inside && !text.as_ref().iter().all(u8::is_ascii_whitespace) => {
2657 children.push(capture_event(Event::Text(text.into_owned()))?)
2658 }
2659 Event::Comment(comment) if inside => {
2660 children.push(capture_event(Event::Comment(comment.into_owned()))?)
2661 }
2662 Event::PI(pi) if inside => children.push(capture_event(Event::PI(pi.into_owned()))?),
2663 Event::End(_) if inside => break,
2664 Event::Eof => break,
2665 _ => {}
2666 }
2667 buffer.clear();
2668 }
2669 if !saw_root {
2670 return Err(OxmlError::MissingElement("OfficeMath element".to_owned()));
2671 }
2672 Ok(ParsedElement {
2673 preservation,
2674 bindings,
2675 children,
2676 })
2677}
2678
2679const MAX_OFFICEMATH_XML_DEPTH: usize = 128;
2680
2681fn validate_xml_depth(raw: &[u8]) -> Result<()> {
2682 let mut reader = Reader::from_reader(raw);
2683 let mut depth = 0usize;
2684 let mut buffer = Vec::new();
2685 loop {
2686 match reader.read_event_into(&mut buffer)? {
2687 Event::Start(_) => {
2688 depth += 1;
2689 if depth > MAX_OFFICEMATH_XML_DEPTH {
2690 return Err(OxmlError::InvalidValue(format!(
2691 "OfficeMath XML nesting exceeds {MAX_OFFICEMATH_XML_DEPTH} elements"
2692 )));
2693 }
2694 }
2695 Event::End(_) => depth = depth.saturating_sub(1),
2696 Event::Eof if depth == 0 => return Ok(()),
2697 Event::Eof => {
2698 return Err(OxmlError::MissingElement(
2699 "OfficeMath closing element".to_owned(),
2700 ));
2701 }
2702 _ => {}
2703 }
2704 buffer.clear();
2705 }
2706}
2707
2708fn valid_expression_shape(raw: &[u8], inherited: &[(String, String)]) -> Result<bool> {
2709 let Some(root) = math_local_name(raw, inherited)? else {
2710 return Ok(true);
2711 };
2712 let parsed = parse_element(raw, inherited)?;
2713 let property_tag = match root.as_str() {
2714 "r" => Some("rPr"),
2715 "f" => Some("fPr"),
2716 "sSub" => Some("sSubPr"),
2717 "sSup" => Some("sSupPr"),
2718 "sSubSup" => Some("sSubSupPr"),
2719 "sPre" => Some("sPrePr"),
2720 "rad" => Some("radPr"),
2721 "m" => Some("mPr"),
2722 "limLow" => Some("limLowPr"),
2723 "limUpp" => Some("limUppPr"),
2724 "nary" => Some("naryPr"),
2725 "d" => Some("dPr"),
2726 "acc" => Some("accPr"),
2727 _ => None,
2728 };
2729 if let Some(property_tag) = property_tag {
2730 for child in &parsed.children {
2731 if math_local_name(child, &parsed.bindings)?.as_deref() == Some(property_tag)
2732 && !property_container_is_valid(child, &parsed.bindings, property_tag)?
2733 {
2734 return Ok(false);
2735 }
2736 }
2737 }
2738 if root == "r" {
2739 for child in &parsed.children {
2740 if math_local_name(child, &parsed.bindings)?.as_deref() == Some("t")
2741 && math_text_has_non_text_nodes(child)?
2742 {
2743 return Ok(false);
2744 }
2745 }
2746 }
2747 let allowed: &[&str] = match root.as_str() {
2748 "r" => &["rPr", "t"],
2749 "f" => &["fPr", "num", "den"],
2750 "sSub" => &["sSubPr", "e", "sub"],
2751 "sSup" => &["sSupPr", "e", "sup"],
2752 "sSubSup" => &["sSubSupPr", "e", "sub", "sup"],
2753 "sPre" => &["sPrePr", "sub", "sup", "e"],
2754 "rad" => &["radPr", "deg", "e"],
2755 "m" => &["mPr", "mr"],
2756 "limLow" => &["limLowPr", "e", "lim"],
2757 "limUpp" => &["limUppPr", "e", "lim"],
2758 "nary" => &["naryPr", "sub", "sup", "e"],
2759 "d" => &["dPr", "e"],
2760 "acc" => &["accPr", "e"],
2761 _ => return Ok(true),
2762 };
2763 let sequence = parsed
2764 .children
2765 .iter()
2766 .filter_map(|child| math_local_name(child, &parsed.bindings).transpose())
2767 .collect::<Result<Vec<_>>>()?
2768 .into_iter()
2769 .filter(|local| allowed.contains(&local.as_str()))
2770 .collect::<Vec<_>>();
2771 let valid = match root.as_str() {
2772 "r" => {
2773 let text_start = usize::from(sequence.first().is_some_and(|value| value == "rPr"));
2774 sequence.len() == text_start + 1 && sequence[text_start] == "t"
2775 }
2776 "f" => sequence_matches(&sequence, "fPr", &["num", "den"]),
2777 "sSub" => sequence_matches(&sequence, "sSubPr", &["e", "sub"]),
2778 "sSup" => sequence_matches(&sequence, "sSupPr", &["e", "sup"]),
2779 "sSubSup" => sequence_matches(&sequence, "sSubSupPr", &["e", "sub", "sup"]),
2780 "sPre" => sequence_matches(&sequence, "sPrePr", &["sub", "sup", "e"]),
2781 "rad" => sequence_matches_with_optional(&sequence, "radPr", &["deg"], &["e"]),
2782 "limLow" => sequence_matches(&sequence, "limLowPr", &["e", "lim"]),
2783 "limUpp" => sequence_matches(&sequence, "limUppPr", &["e", "lim"]),
2784 "nary" => sequence_matches_with_optional(&sequence, "naryPr", &["sub", "sup"], &["e"]),
2785 "acc" => sequence_matches(&sequence, "accPr", &["e"]),
2786 "d" => {
2787 let argument_start = usize::from(sequence.first().is_some_and(|value| value == "dPr"));
2788 sequence.len() > argument_start
2789 && sequence[argument_start..].iter().all(|value| value == "e")
2790 }
2791 "m" => {
2792 let row_start = usize::from(sequence.first().is_some_and(|value| value == "mPr"));
2793 let mut rows_are_valid = true;
2794 for child in &parsed.children {
2795 if math_local_name(child, &parsed.bindings)?.as_deref() == Some("mr") {
2796 rows_are_valid &= valid_matrix_row(child, &parsed.bindings)?;
2797 }
2798 }
2799 sequence.len() > row_start
2800 && sequence[row_start..].iter().all(|value| value == "mr")
2801 && rows_are_valid
2802 }
2803 _ => true,
2804 };
2805 Ok(valid)
2806}
2807
2808fn valid_officemath_paragraph_shape(raw: &[u8], inherited: &[(String, String)]) -> Result<bool> {
2809 let parsed = parse_element(raw, inherited)?;
2810 let sequence = parsed
2811 .children
2812 .iter()
2813 .filter_map(|child| math_local_name(child, &parsed.bindings).transpose())
2814 .collect::<Result<Vec<_>>>()?
2815 .into_iter()
2816 .filter(|local| local == "oMathParaPr" || local == "oMath")
2817 .collect::<Vec<_>>();
2818 let equation_start = usize::from(sequence.first().is_some_and(|value| value == "oMathParaPr"));
2819 if sequence.len() <= equation_start
2820 || !sequence[equation_start..]
2821 .iter()
2822 .all(|value| value == "oMath")
2823 {
2824 return Ok(false);
2825 }
2826 for child in &parsed.children {
2827 if math_local_name(child, &parsed.bindings)?.as_deref() == Some("oMathParaPr")
2828 && !property_container_is_valid(child, &parsed.bindings, "oMathParaPr")?
2829 {
2830 return Ok(false);
2831 }
2832 }
2833 Ok(true)
2834}
2835
2836fn property_container_is_valid(
2837 raw: &[u8],
2838 inherited: &[(String, String)],
2839 tag: &str,
2840) -> Result<bool> {
2841 let parsed = parse_element(raw, inherited)?;
2842 let supported = supported_properties(tag);
2843 let mut last_index = None;
2844 for child in &parsed.children {
2845 let Some(local) = math_local_name(child, &parsed.bindings)? else {
2846 continue;
2847 };
2848 let Some(index) = supported.iter().position(|name| *name == local) else {
2849 continue;
2850 };
2851 if last_index.is_some_and(|previous| index <= previous)
2852 || !property_leaf_is_valid(tag, &local, child, &parsed.bindings)?
2853 {
2854 return Ok(false);
2855 }
2856 last_index = Some(index);
2857 }
2858 Ok(true)
2859}
2860
2861fn property_leaf_is_valid(
2862 container: &str,
2863 property: &str,
2864 raw: &[u8],
2865 inherited: &[(String, String)],
2866) -> Result<bool> {
2867 let modeled_attribute = property_modeled_attribute(container, property);
2868 if root_has_ambiguous_modeled_attribute(raw, inherited, modeled_attribute)? {
2869 return Ok(false);
2870 }
2871 let value = root_value(raw, inherited)?
2872 .or_else(|| property_value_default(container, property).map(str::to_owned));
2873 Ok(match (container, property) {
2874 ("rPr", "lit" | "nor")
2875 | ("sSubPr" | "sSupPr" | "sSubSupPr" | "sPrePr", "alnScr")
2876 | ("radPr", "degHide")
2877 | ("naryPr", "grow" | "subHide" | "supHide")
2878 | ("dPr", "grow") => value
2879 .as_deref()
2880 .is_none_or(|value| parse_bool(value).is_some()),
2881 ("rPr", "scr") => value
2882 .as_deref()
2883 .is_some_and(|value| MathScriptStyle::parse(value).is_some()),
2884 ("rPr", "sty") => value
2885 .as_deref()
2886 .is_some_and(|value| MathStyle::parse(value).is_some()),
2887 ("rPr", "brk") => root_attribute(raw, inherited, b"alnAt")?
2888 .as_deref()
2889 .is_none_or(|value| value.parse::<u8>().is_ok_and(|value| value > 0)),
2890 ("fPr", "type") => value
2891 .as_deref()
2892 .is_some_and(|value| FractionType::parse(value).is_some()),
2893 ("mPr", "baseJc") => value
2894 .as_deref()
2895 .is_some_and(|value| MatrixBaseJustification::parse(value).is_some()),
2896 ("mPr", "rSp") => value
2897 .as_deref()
2898 .is_some_and(|value| value.parse::<u16>().is_ok()),
2899 ("mPr", "cSp") => value
2900 .as_deref()
2901 .is_some_and(|value| value.parse::<u32>().is_ok()),
2902 ("naryPr", "limLoc") => value
2903 .as_deref()
2904 .is_some_and(|value| LimitLocation::parse(value).is_some()),
2905 ("oMathParaPr", "jc") => value
2906 .as_deref()
2907 .is_some_and(|value| MathJustification::parse(value).is_some()),
2908 ("naryPr" | "accPr", "chr") | ("dPr", "begChr" | "sepChr" | "endChr") => value
2909 .as_deref()
2910 .is_none_or(|value| value.chars().count() <= 1),
2911 ("mathPr", "mathFont") => value.as_deref().is_some_and(|value| !value.is_empty()),
2912 ("mathPr", "smallFrac" | "dispDef") => value
2913 .as_deref()
2914 .is_none_or(|value| parse_bool(value).is_some()),
2915 (
2916 "mathPr",
2917 "lMargin" | "rMargin" | "preSp" | "postSp" | "interSp" | "intraSp" | "wrapIndent",
2918 ) => value
2919 .as_deref()
2920 .is_some_and(|value| value.parse::<u32>().is_ok()),
2921 ("mathPr", "defJc") => value
2922 .as_deref()
2923 .is_some_and(|value| MathJustification::parse(value).is_some()),
2924 ("mathPr", "intLim" | "naryLim") => value
2925 .as_deref()
2926 .is_some_and(|value| LimitLocation::parse(value).is_some()),
2927 _ => true,
2928 })
2929}
2930
2931fn sequence_matches(sequence: &[String], optional: &str, required: &[&str]) -> bool {
2932 let start = usize::from(sequence.first().is_some_and(|value| value == optional));
2933 sequence.len() == start + required.len()
2934 && sequence[start..]
2935 .iter()
2936 .zip(required)
2937 .all(|(actual, expected)| actual == expected)
2938}
2939
2940fn sequence_matches_with_optional(
2941 sequence: &[String],
2942 property: &str,
2943 optional: &[&str],
2944 required: &[&str],
2945) -> bool {
2946 let mut index = usize::from(sequence.first().is_some_and(|value| value == property));
2947 for child in optional {
2948 if sequence.get(index).is_some_and(|value| value == child) {
2949 index += 1;
2950 }
2951 }
2952 sequence.len() == index + required.len()
2953 && sequence[index..]
2954 .iter()
2955 .zip(required)
2956 .all(|(actual, expected)| actual == expected)
2957}
2958
2959fn valid_matrix_row(raw: &[u8], inherited: &[(String, String)]) -> Result<bool> {
2960 let parsed = parse_element(raw, inherited)?;
2961 let cells = parsed
2962 .children
2963 .iter()
2964 .filter_map(|child| math_local_name(child, &parsed.bindings).transpose())
2965 .collect::<Result<Vec<_>>>()?;
2966 Ok(!cells.is_empty() && cells.iter().all(|local| local == "e"))
2967}
2968
2969fn element_text(raw: &[u8]) -> Result<String> {
2970 let mut reader = Reader::from_reader(raw);
2971 let mut buffer = Vec::new();
2972 let mut inside = false;
2973 let mut text = String::new();
2974 loop {
2975 match reader.read_event_into(&mut buffer)? {
2976 Event::Start(_) if !inside => inside = true,
2977 Event::Empty(_) if !inside => return Ok(text),
2978 Event::Start(_) if inside => {
2979 return Err(OxmlError::UnexpectedElement(
2980 "nested element in OfficeMath text".to_owned(),
2981 ));
2982 }
2983 Event::Text(value) if inside => text.push_str(std::str::from_utf8(value.as_ref())?),
2984 Event::CData(value) if inside => text.push_str(std::str::from_utf8(value.as_ref())?),
2985 Event::GeneralRef(value) if inside => {
2986 let name = std::str::from_utf8(value.as_ref())?;
2987 let entity = format!("&{name};");
2988 let decoded = quick_xml::escape::unescape(&entity).map_err(|error| {
2989 OxmlError::InvalidValue(format!("invalid OfficeMath text entity: {error}"))
2990 })?;
2991 text.push_str(&decoded);
2992 }
2993 Event::End(_) if inside => return Ok(text),
2994 Event::Empty(_) if inside => {
2995 return Err(OxmlError::UnexpectedElement(
2996 "nested empty element in OfficeMath text".to_owned(),
2997 ));
2998 }
2999 Event::Eof => {
3000 return Err(OxmlError::MissingElement("OfficeMath text end".to_owned()));
3001 }
3002 _ => {}
3003 }
3004 buffer.clear();
3005 }
3006}
3007
3008fn math_text_has_non_text_nodes(raw: &[u8]) -> Result<bool> {
3009 let mut reader = Reader::from_reader(raw);
3010 let mut inside = false;
3011 let mut buffer = Vec::new();
3012 loop {
3013 match reader.read_event_into(&mut buffer)? {
3014 Event::Start(element) if !inside => {
3015 if math_text_has_invalid_space(&element)? {
3016 return Ok(true);
3017 }
3018 inside = true;
3019 }
3020 Event::Empty(element) if !inside => {
3021 return math_text_has_invalid_space(&element);
3022 }
3023 Event::Start(_) | Event::Empty(_) if inside => return Ok(true),
3024 Event::Comment(_) | Event::PI(_) if inside => return Ok(true),
3025 Event::End(_) if inside => return Ok(false),
3026 Event::Eof => return Ok(false),
3027 _ => {}
3028 }
3029 buffer.clear();
3030 }
3031}
3032
3033fn math_text_has_invalid_space(element: &BytesStart<'_>) -> Result<bool> {
3034 for attribute in element.attributes() {
3035 let attribute = attribute?;
3036 if attribute.key.as_ref() == b"xml:space" {
3037 let value = attribute
3038 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?;
3039 if !matches!(value.as_ref(), "default" | "preserve") {
3040 return Ok(true);
3041 }
3042 }
3043 }
3044 Ok(false)
3045}
3046
3047fn capture_empty(element: &BytesStart<'_>) -> Result<Vec<u8>> {
3048 capture_event(Event::Empty(element.to_owned().into_owned()))
3049}
3050fn capture_event(event: Event<'static>) -> Result<Vec<u8>> {
3051 let mut output = Vec::new();
3052 Writer::new(&mut output).write_event(event)?;
3053 Ok(output)
3054}
3055
3056fn merge_bindings(bindings: &mut Vec<(String, String)>, element: &BytesStart<'_>) -> Result<()> {
3057 for attribute in element.attributes() {
3058 let attribute = attribute?;
3059 let name = attribute.key.as_ref();
3060 let prefix = if name == b"xmlns" {
3061 Some("")
3062 } else {
3063 name.strip_prefix(b"xmlns:")
3064 .and_then(|value| std::str::from_utf8(value).ok())
3065 };
3066 if let Some(prefix) = prefix {
3067 let value = attribute
3068 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
3069 .into_owned();
3070 bindings.retain(|(candidate, _)| candidate != prefix);
3071 bindings.push((prefix.to_owned(), value));
3072 }
3073 }
3074 Ok(())
3075}
3076
3077pub(crate) fn fixed_math_prefix_is_safe(
3078 raw: &[u8],
3079 inherited: &[(String, String)],
3080) -> Result<bool> {
3081 let mut reader = Reader::from_reader(raw);
3082 let mut root_m_binding = inherited
3083 .iter()
3084 .rev()
3085 .find(|(prefix, _)| prefix == "m")
3086 .map(|(_, namespace)| namespace.clone());
3087 let mut saw_root = false;
3088 let mut buffer = Vec::new();
3089 loop {
3090 match reader.read_event_into(&mut buffer)? {
3091 Event::Start(element) | Event::Empty(element) => {
3092 let mut local_m_binding = None;
3093 for attribute in element.attributes() {
3094 let attribute = attribute?;
3095 if attribute.key.as_ref() == b"xmlns:m" {
3096 local_m_binding = Some(
3097 attribute
3098 .decoded_and_normalized_value(
3099 XmlVersion::Implicit1_0,
3100 element.decoder(),
3101 )?
3102 .into_owned(),
3103 );
3104 }
3105 }
3106 if !saw_root {
3107 if local_m_binding.is_some() {
3108 root_m_binding = local_m_binding;
3109 }
3110 if root_m_binding.as_deref().is_some_and(|value| value != M_NS) {
3111 return Ok(false);
3112 }
3113 saw_root = true;
3114 } else if local_m_binding
3115 .as_deref()
3116 .is_some_and(|value| value != M_NS)
3117 {
3118 return Ok(false);
3119 }
3120 }
3121 Event::Eof => return Ok(true),
3122 _ => {}
3123 }
3124 buffer.clear();
3125 }
3126}
3127
3128fn require_safe_fixed_math_prefix(raw: &[u8], inherited: &[(String, String)]) -> Result<()> {
3129 if fixed_math_prefix_is_safe(raw, inherited)? {
3130 Ok(())
3131 } else {
3132 Err(OxmlError::InvalidValue(
3133 "OfficeMath source conflicts with the canonical m prefix".to_owned(),
3134 ))
3135 }
3136}
3137
3138fn extra_attributes(element: &BytesStart<'_>) -> Result<Vec<(String, String)>> {
3139 let mut attributes = Vec::new();
3140 for attribute in element.attributes() {
3141 let attribute = attribute?;
3142 let name = std::str::from_utf8(attribute.key.as_ref())?.to_owned();
3143 let value = attribute
3144 .decoded_and_normalized_value(XmlVersion::Implicit1_0, element.decoder())?
3145 .into_owned();
3146 attributes.push((name, value));
3147 }
3148 Ok(attributes)
3149}
3150
3151fn expanded_name(name: &[u8], bindings: &[(String, String)]) -> Option<(String, Vec<u8>)> {
3152 let (prefix, local) = name
3153 .iter()
3154 .position(|byte| *byte == b':')
3155 .map_or((&b""[..], name), |at| (&name[..at], &name[at + 1..]));
3156 let prefix = std::str::from_utf8(prefix).ok()?;
3157 let namespace = bindings
3158 .iter()
3159 .rev()
3160 .find(|(candidate, _)| candidate == prefix)?
3161 .1
3162 .clone();
3163 Some((namespace, local.to_vec()))
3164}
3165
3166fn expanded_attribute_name(
3167 name: &[u8],
3168 bindings: &[(String, String)],
3169) -> Option<(String, Vec<u8>)> {
3170 if !name.contains(&b':') {
3171 return None;
3172 }
3173 expanded_name(name, bindings)
3174}
3175
3176fn root_name(raw: &[u8], inherited: &[(String, String)]) -> Result<Option<(String, Vec<u8>)>> {
3177 let mut reader = Reader::from_reader(raw);
3178 let mut bindings = inherited.to_vec();
3179 let mut buffer = Vec::new();
3180 loop {
3181 match reader.read_event_into(&mut buffer)? {
3182 Event::Start(element) | Event::Empty(element) => {
3183 merge_bindings(&mut bindings, &element)?;
3184 return Ok(expanded_name(element.name().as_ref(), &bindings));
3185 }
3186 Event::Eof => return Ok(None),
3187 _ => {}
3188 }
3189 buffer.clear();
3190 }
3191}
3192
3193fn math_local_name(raw: &[u8], inherited: &[(String, String)]) -> Result<Option<String>> {
3194 Ok(root_name(raw, inherited)?.and_then(|(namespace, local)| {
3195 (namespace == M_NS).then(|| String::from_utf8_lossy(&local).into_owned())
3196 }))
3197}
3198
3199fn require_root(raw: &[u8], inherited: &[(String, String)], expected: &[u8]) -> Result<()> {
3200 if root_name(raw, inherited)?
3201 .is_some_and(|(namespace, local)| namespace == M_NS && local == expected)
3202 {
3203 Ok(())
3204 } else {
3205 Err(OxmlError::MissingElement(format!(
3206 "OfficeMath {} root",
3207 String::from_utf8_lossy(expected)
3208 )))
3209 }
3210}
3211
3212fn root_value(raw: &[u8], inherited: &[(String, String)]) -> Result<Option<String>> {
3213 root_attribute(raw, inherited, b"val")
3214}
3215
3216fn root_has_ambiguous_modeled_attribute(
3217 raw: &[u8],
3218 inherited: &[(String, String)],
3219 expected: &[u8],
3220) -> Result<bool> {
3221 let parsed = parse_element(raw, inherited)?;
3222 let mut modeled_count = 0;
3223 for (name, _) in &parsed.preservation.attributes {
3224 if name == "xmlns" || name.starts_with("xmlns:") {
3225 continue;
3226 }
3227 let local = name
3228 .as_bytes()
3229 .rsplit(|byte| *byte == b':')
3230 .next()
3231 .unwrap_or_default();
3232 if local != expected {
3233 continue;
3234 }
3235 if expanded_attribute_name(name.as_bytes(), &parsed.bindings)
3236 .is_some_and(|(namespace, local)| namespace == M_NS && local == expected)
3237 {
3238 modeled_count += 1;
3239 } else {
3240 return Ok(true);
3241 }
3242 }
3243 Ok(modeled_count > 1)
3244}
3245
3246fn root_attribute(
3247 raw: &[u8],
3248 inherited: &[(String, String)],
3249 expected: &[u8],
3250) -> Result<Option<String>> {
3251 let mut reader = Reader::from_reader(raw);
3252 let mut bindings = inherited.to_vec();
3253 let mut buffer = Vec::new();
3254 loop {
3255 match reader.read_event_into(&mut buffer)? {
3256 Event::Start(element) | Event::Empty(element) => {
3257 merge_bindings(&mut bindings, &element)?;
3258 for attribute in element.attributes() {
3259 let attribute = attribute?;
3260 if expanded_attribute_name(attribute.key.as_ref(), &bindings)
3261 .is_some_and(|(namespace, local)| namespace == M_NS && local == expected)
3262 {
3263 return Ok(Some(
3264 attribute
3265 .decoded_and_normalized_value(
3266 XmlVersion::Implicit1_0,
3267 element.decoder(),
3268 )?
3269 .into_owned(),
3270 ));
3271 }
3272 }
3273 return Ok(None);
3274 }
3275 Event::Eof => return Ok(None),
3276 _ => {}
3277 }
3278 buffer.clear();
3279 }
3280}
3281
3282fn child_property_value(
3283 raw: &[u8],
3284 inherited: &[(String, String)],
3285 container: &str,
3286 property: &str,
3287) -> Result<Option<String>> {
3288 let parsed = parse_element(raw, inherited)?;
3289 for child in parsed.children {
3290 if math_local_name(&child, &parsed.bindings)?.as_deref() == Some(property) {
3291 return Ok(root_value(&child, &parsed.bindings)?
3292 .or_else(|| property_value_default(container, property).map(str::to_owned)));
3293 }
3294 }
3295 Ok(None)
3296}
3297
3298fn child_on_off_value(
3299 raw: &[u8],
3300 inherited: &[(String, String)],
3301 property: &str,
3302) -> Result<Option<bool>> {
3303 let parsed = parse_element(raw, inherited)?;
3304 for child in parsed.children {
3305 if math_local_name(&child, &parsed.bindings)?.as_deref() == Some(property) {
3306 return Ok(parse_present_on_off(
3307 root_value(&child, &parsed.bindings)?.as_deref(),
3308 ));
3309 }
3310 }
3311 Ok(None)
3312}
3313
3314fn preserve_unrecognized_property_children(
3315 raw: &[u8],
3316 inherited: &[(String, String)],
3317 recognized: &[&str],
3318) -> Result<Preservation> {
3319 let mut parsed = parse_element(raw, inherited)?;
3320 let mut seen = Vec::new();
3321 let mut last_rank = None;
3322 for child in parsed.children {
3323 let local = math_local_name(&child, &parsed.bindings)?;
3324 if let Some(local) = local
3325 .as_deref()
3326 .filter(|local| recognized.contains(local) && !seen.iter().any(|name| name == local))
3327 {
3328 seen.push(local.to_owned());
3329 property_raw_key("oMathParaPr", Some(local), &mut last_rank, false);
3330 preserve_modeled_child(&mut parsed.preservation, local, &child, &parsed.bindings);
3331 } else {
3332 let duplicate = local
3333 .as_deref()
3334 .is_some_and(|local| seen.iter().any(|name| name == local));
3335 let key = property_raw_key("oMathParaPr", local.as_deref(), &mut last_rank, duplicate);
3336 parsed.preservation.property_raw_children.push((key, child));
3337 }
3338 }
3339 Ok(parsed.preservation)
3340}
3341
3342fn parse_bool(value: &str) -> Option<bool> {
3343 match value {
3344 "1" | "true" | "on" => Some(true),
3345 "0" | "false" | "off" => Some(false),
3346 _ => None,
3347 }
3348}
3349fn parse_present_on_off(value: Option<&str>) -> Option<bool> {
3350 match value {
3351 Some(value) => parse_bool(value),
3352 None => Some(true),
3353 }
3354}
3355fn parse_u32(value: Option<String>) -> Option<u32> {
3356 value.and_then(|value| value.parse().ok())
3357}
3358
3359#[cfg(test)]
3360mod tests {
3361 use super::*;
3362
3363 fn complete_corpus() -> CT_OMath {
3364 let text = |value| MathArgument::text(value);
3365 CT_OMath::new(vec![
3366 MathRun::new("x").into(),
3367 MathExpression::Fraction(MathFraction::new(text("1"), text("2"))),
3368 MathExpression::Subscript(MathScript::new(text("x"), text("i"))),
3369 MathExpression::Superscript(MathScript::new(text("x"), text("2"))),
3370 MathExpression::SubSuperscript(MathSubSuperscript::new(
3371 text("x"),
3372 text("i"),
3373 text("2"),
3374 )),
3375 MathExpression::PreSubSuperscript(MathPreSubSuperscript::new(
3376 text("x"),
3377 text("i"),
3378 text("2"),
3379 )),
3380 MathExpression::Radical(MathRadical::with_degree(text("3"), text("x"))),
3381 MathExpression::Matrix(MathMatrix::new(vec![MathMatrixRow::new(vec![
3382 text("a"),
3383 text("b"),
3384 ])])),
3385 MathExpression::LowerLimit(MathLimit::new(text("lim"), text("0"))),
3386 MathExpression::UpperLimit(MathLimit::new(text("max"), text("n"))),
3387 MathExpression::Nary({
3388 let mut nary = MathNary::new("∑", text("x"));
3389 nary.subscript = text("i");
3390 nary.superscript = text("n");
3391 nary
3392 }),
3393 MathExpression::Delimiter(MathDelimiter::new("(", ")", vec![text("x")])),
3394 MathExpression::Accent(MathAccent::new("̂", text("x"))),
3395 ])
3396 }
3397
3398 #[test]
3399 fn every_supported_officemath_construct_writes_schema_order_and_reparses() {
3400 let equation = complete_corpus();
3401 let xml = equation.to_xml().unwrap();
3402 let reparsed = CT_OMath::from_xml(&xml).unwrap();
3403 assert_eq!(reparsed.expressions.len(), equation.expressions.len());
3404 let xml = String::from_utf8(xml).unwrap();
3405 assert!(xml.find("<m:fPr>").unwrap() < xml.find("<m:num>").unwrap());
3406 assert!(xml.find("<m:num>").unwrap() < xml.find("<m:den>").unwrap());
3407 let nary = &xml[xml.find("<m:nary>").unwrap()..];
3408 assert!(nary.find("<m:naryPr>").unwrap() < nary.find("<m:sub>").unwrap());
3409 }
3410
3411 #[test]
3412 fn officemath_reader_accepts_aliases_and_writer_uses_fixed_math_prefix() {
3413 let xml = format!(r#"<z:oMath xmlns:z="{M_NS}"><z:r><z:t>x</z:t></z:r></z:oMath>"#);
3414 let parsed = CT_OMath::from_xml(xml.as_bytes()).unwrap();
3415 let output = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
3416 assert!(output.starts_with(&format!(r#"<m:oMath xmlns:m="{M_NS}""#)));
3417 assert!(output.contains("<m:r><m:t>x</m:t></m:r>"));
3418 }
3419
3420 #[test]
3421 fn unsupported_officemath_siblings_survive_typed_mutation_byte_for_byte() {
3422 let xml = format!(
3423 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer" x:flag="kept"><x:before a="1"/><m:r><m:t>x</m:t></m:r><x:after> raw </x:after></m:oMath>"#
3424 );
3425 let mut parsed = CT_OMath::from_xml(xml.as_bytes()).unwrap();
3426 let MathExpression::Run(run) = &mut parsed.expressions[0] else {
3427 panic!("run")
3428 };
3429 run.text = "y".to_owned();
3430 let output = parsed.to_xml().unwrap();
3431 assert!(
3432 output
3433 .windows(b"<x:before a=\"1\"/>".len())
3434 .any(|value| value == b"<x:before a=\"1\"/>")
3435 );
3436 assert!(
3437 output
3438 .windows(b"<x:after> raw </x:after>".len())
3439 .any(|value| value == b"<x:after> raw </x:after>")
3440 );
3441 assert!(
3442 String::from_utf8(output)
3443 .unwrap()
3444 .contains("x:flag=\"kept\"")
3445 );
3446 }
3447
3448 #[test]
3449 fn inserted_property_containers_do_not_shift_raw_argument_slots() {
3450 let cases = [
3451 (
3452 format!(
3453 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:f><x:before/><m:num/><x:middle/><m:den/><x:after/></m:f></m:oMath>"#
3454 ),
3455 vec![
3456 "<m:fPr>",
3457 "<x:before/>",
3458 "<m:num>",
3459 "<x:middle/>",
3460 "<m:den>",
3461 "<x:after/>",
3462 ],
3463 ),
3464 (
3465 format!(
3466 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:rad><x:before/><m:deg/><x:middle/><m:e/><x:after/></m:rad></m:oMath>"#
3467 ),
3468 vec![
3469 "<m:radPr>",
3470 "<x:before/>",
3471 "<m:deg>",
3472 "<x:middle/>",
3473 "<m:e>",
3474 "<x:after/>",
3475 ],
3476 ),
3477 (
3478 format!(
3479 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:nary><x:before/><m:sub/><x:middle-one/><m:sup/><x:middle-two/><m:e/><x:after/></m:nary></m:oMath>"#
3480 ),
3481 vec![
3482 "<m:naryPr>",
3483 "<x:before/>",
3484 "<m:sub>",
3485 "<x:middle-one/>",
3486 "<m:sup>",
3487 "<x:middle-two/>",
3488 "<m:e>",
3489 "<x:after/>",
3490 ],
3491 ),
3492 (
3493 format!(
3494 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:d><x:before/><m:e/><x:middle/><m:e/><x:after/></m:d></m:oMath>"#
3495 ),
3496 vec![
3497 "<m:dPr>",
3498 "<x:before/>",
3499 "<m:e>",
3500 "<x:middle/>",
3501 "<m:e>",
3502 "<x:after/>",
3503 ],
3504 ),
3505 (
3506 format!(
3507 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:acc><x:before/><m:e/><x:after/></m:acc></m:oMath>"#
3508 ),
3509 vec!["<m:accPr>", "<x:before/>", "<m:e>", "<x:after/>"],
3510 ),
3511 ];
3512 for (source, expected) in cases {
3513 let parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
3514 let first = parsed.to_xml().unwrap();
3515 assert_fragments_in_order(&first, &expected);
3516 let reopened = CT_OMath::from_xml(&first).unwrap();
3517 assert_fragments_in_order(&reopened.to_xml().unwrap(), &expected);
3518 }
3519 }
3520
3521 #[test]
3522 fn unsupported_content_is_observable_without_exposing_preservation_storage() {
3523 let authored = CT_OMath::new(vec![MathRun::new("x").into()]);
3524 assert!(!authored.has_unsupported_content());
3525 assert!(!OfficeMath::Inline(authored).has_unsupported_content());
3526
3527 let source = format!(
3528 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:f><m:fPr><x:property/></m:fPr><m:num><x:argument/></m:num><m:den/></m:f></m:oMath>"#
3529 );
3530 let parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
3531 assert!(parsed.has_unsupported_content());
3532 let MathExpression::Fraction(fraction) = &parsed.expressions[0] else {
3533 panic!("fraction")
3534 };
3535 assert!(parsed.expressions[0].has_unsupported_content());
3536 assert!(fraction.numerator.has_unsupported_content());
3537 assert!(!fraction.denominator.has_unsupported_content());
3538
3539 let extended_text = format!(
3540 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:r><m:t x:keep="yes">x</m:t></m:r></m:oMath>"#
3541 );
3542 let extended_text = CT_OMath::from_xml(extended_text.as_bytes()).unwrap();
3543 assert!(extended_text.expressions[0].has_unsupported_content());
3544
3545 let spaced_text = format!(
3546 r#"<m:oMath xmlns:m="{M_NS}"><m:r><m:t xml:space="preserve"> x </m:t></m:r></m:oMath>"#
3547 );
3548 let spaced_text = CT_OMath::from_xml(spaced_text.as_bytes()).unwrap();
3549 assert!(!spaced_text.expressions[0].has_unsupported_content());
3550 }
3551
3552 #[test]
3553 fn existing_run_and_display_properties_keep_both_parent_raw_slots() {
3554 let run_source = format!(
3555 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:r><x:run-before/><m:rPr/><x:run-after-property/><m:t>x</m:t><x:run-after-text/></m:r></m:oMath>"#
3556 );
3557 let run = CT_OMath::from_xml(run_source.as_bytes()).unwrap();
3558 let first = run.to_xml().unwrap();
3559 assert_fragments_in_order(
3560 &first,
3561 &[
3562 "<x:run-before/>",
3563 "<m:rPr>",
3564 "<x:run-after-property/>",
3565 "<m:t>",
3566 "<x:run-after-text/>",
3567 ],
3568 );
3569 let reopened = CT_OMath::from_xml(&first).unwrap();
3570 assert_fragments_in_order(
3571 &reopened.to_xml().unwrap(),
3572 &[
3573 "<x:run-before/>",
3574 "<m:rPr>",
3575 "<x:run-after-property/>",
3576 "<m:t>",
3577 "<x:run-after-text/>",
3578 ],
3579 );
3580
3581 let display_source = format!(
3582 r#"<m:oMathPara xmlns:m="{M_NS}" xmlns:x="urn:producer"><x:display-before/><m:oMathParaPr/><x:display-after-property/><m:oMath><m:r><m:t>x</m:t></m:r></m:oMath><x:display-after-equation/></m:oMathPara>"#
3583 );
3584 let display = CT_OMathPara::from_xml(display_source.as_bytes()).unwrap();
3585 let first = display.to_xml().unwrap();
3586 assert_fragments_in_order(
3587 &first,
3588 &[
3589 "<x:display-before/>",
3590 "<m:oMathParaPr>",
3591 "<x:display-after-property/>",
3592 "<m:oMath>",
3593 "<x:display-after-equation/>",
3594 ],
3595 );
3596 let reopened = CT_OMathPara::from_xml(&first).unwrap();
3597 assert_fragments_in_order(
3598 &reopened.to_xml().unwrap(),
3599 &[
3600 "<x:display-before/>",
3601 "<m:oMathParaPr>",
3602 "<x:display-after-property/>",
3603 "<m:oMath>",
3604 "<x:display-after-equation/>",
3605 ],
3606 );
3607 }
3608
3609 #[test]
3610 fn comments_and_processing_instructions_inside_math_text_keep_the_run_opaque() {
3611 let source = format!(
3612 r#"<m:oMath xmlns:m="{M_NS}"><m:r><m:t>x<!--kept--></m:t></m:r><m:r><m:t>y<?kept value?></m:t></m:r></m:oMath>"#
3613 );
3614 let parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
3615 assert!(parsed.expressions.is_empty());
3616 assert!(parsed.has_unsupported_content());
3617 let first = parsed.to_xml().unwrap();
3618 let first = String::from_utf8(first).unwrap();
3619 assert!(first.contains("<m:t>x<!--kept--></m:t>"));
3620 assert!(first.contains("<m:t>y<?kept value?></m:t>"));
3621 let reopened = CT_OMath::from_xml(first.as_bytes()).unwrap();
3622 assert!(reopened.expressions.is_empty());
3623 assert!(reopened.has_unsupported_content());
3624 let second = String::from_utf8(reopened.to_xml().unwrap()).unwrap();
3625 assert!(second.contains("<m:t>x<!--kept--></m:t>"));
3626 assert!(second.contains("<m:t>y<?kept value?></m:t>"));
3627 }
3628
3629 #[test]
3630 fn unsupported_property_attributes_are_checked_against_their_actual_leaf() {
3631 let source = format!(
3632 r#"<m:oMath xmlns:m="{M_NS}"><m:f><m:fPr><m:type m:val="bar" m:alnAt="4"/></m:fPr><m:num/><m:den/></m:f><m:r><m:rPr><m:brk m:alnAt="4" m:val="1"/></m:rPr><m:t>x</m:t></m:r></m:oMath>"#
3633 );
3634 let parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
3635 assert!(parsed.expressions[0].has_unsupported_content());
3636 assert!(parsed.expressions[1].has_unsupported_content());
3637
3638 let supported = format!(
3639 r#"<m:oMath xmlns:m="{M_NS}"><m:f><m:fPr><m:type m:val="bar"/></m:fPr><m:num/><m:den/></m:f><m:r><m:rPr><m:brk m:alnAt="4"/></m:rPr><m:t>x</m:t></m:r></m:oMath>"#
3640 );
3641 let supported = CT_OMath::from_xml(supported.as_bytes()).unwrap();
3642 assert!(!supported.expressions[0].has_unsupported_content());
3643 assert!(!supported.expressions[1].has_unsupported_content());
3644 }
3645
3646 #[test]
3647 fn optional_radical_degree_and_nary_limits_remain_typed_and_omitted() {
3648 let source = format!(
3649 r#"<m:oMath xmlns:m="{M_NS}">
3650 <m:rad><m:e/></m:rad>
3651 <m:nary><m:e/></m:nary>
3652 <m:nary><m:sub/><m:e/></m:nary>
3653 <m:nary><m:sup/><m:e/></m:nary>
3654 <m:nary><m:sub/><m:sup/><m:e/></m:nary>
3655 </m:oMath>"#
3656 );
3657 let parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
3658 assert!(matches!(parsed.expressions[0], MathExpression::Radical(_)));
3659 assert!(
3660 parsed.expressions[1..]
3661 .iter()
3662 .all(|value| matches!(value, MathExpression::Nary(_)))
3663 );
3664
3665 let output = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
3666 assert!(!output.contains("<m:deg>"));
3667 assert_eq!(output.matches("<m:sub>").count(), 2);
3668 assert_eq!(output.matches("<m:sup>").count(), 2);
3669
3670 let reopened = CT_OMath::from_xml(output.as_bytes()).unwrap();
3671 assert!(matches!(
3672 reopened.expressions[0],
3673 MathExpression::Radical(_)
3674 ));
3675 assert!(
3676 reopened.expressions[1..]
3677 .iter()
3678 .all(|value| matches!(value, MathExpression::Nary(_)))
3679 );
3680 }
3681
3682 #[test]
3683 fn authored_optional_arguments_precede_preserved_slots_without_rebasing_them() {
3684 let source = format!(
3685 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer">
3686 <m:rad><x:rad-slot/><m:e/></m:rad>
3687 <m:nary><x:nary-slot/><m:e/></m:nary>
3688 </m:oMath>"#
3689 );
3690 let mut parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
3691 let MathExpression::Radical(radical) = &mut parsed.expressions[0] else {
3692 panic!("radical")
3693 };
3694 radical.degree = MathArgument::text("3");
3695 let MathExpression::Nary(nary) = &mut parsed.expressions[1] else {
3696 panic!("nary")
3697 };
3698 nary.subscript = MathArgument::text("i");
3699
3700 let output = parsed.to_xml().unwrap();
3701 assert_fragments_in_order(&output, &["<m:radPr>", "<m:deg>", "<x:rad-slot/>", "<m:e>"]);
3702 assert_fragments_in_order(
3703 &output,
3704 &["<m:naryPr>", "<m:sub>", "<x:nary-slot/>", "<m:e>"],
3705 );
3706 let reopened = CT_OMath::from_xml(&output).unwrap();
3707 let second = reopened.to_xml().unwrap();
3708 assert_fragments_in_order(&second, &["<m:radPr>", "<m:deg>", "<x:rad-slot/>", "<m:e>"]);
3709 assert_fragments_in_order(
3710 &second,
3711 &["<m:naryPr>", "<m:sub>", "<x:nary-slot/>", "<m:e>"],
3712 );
3713 }
3714
3715 #[test]
3716 fn officemath_corpus_parses_mutates_saves_and_reopens_without_losing_supported_or_raw_siblings()
3717 {
3718 let authored = String::from_utf8(complete_corpus().to_xml().unwrap()).unwrap();
3719 let source = authored
3720 .replacen(
3721 &format!(r#"<m:oMath xmlns:m="{M_NS}">"#),
3722 &format!(
3723 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><x:before keep="root"/>"#
3724 ),
3725 1,
3726 )
3727 .replacen("<m:fPr>", "<m:fPr><x:fraction keep=\"property\"/>", 1)
3728 .replacen("<m:num>", "<m:num><x:numerator keep=\"argument\"/>", 1)
3729 .replacen("</m:oMath>", "<x:after keep=\"root\"/></m:oMath>", 1);
3730 let mut parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
3731 let MathExpression::Run(run) = &mut parsed.expressions[0] else {
3732 panic!("run")
3733 };
3734 run.text = "changed".to_owned();
3735 let second = parsed.to_xml().unwrap();
3736 let serialized = String::from_utf8(second.clone()).unwrap();
3737 for raw in [
3738 r#"<x:before keep="root"/>"#,
3739 r#"<x:fraction keep="property"/>"#,
3740 r#"<x:numerator keep="argument"/>"#,
3741 r#"<x:after keep="root"/>"#,
3742 ] {
3743 assert!(serialized.contains(raw));
3744 }
3745 let reopened = CT_OMath::from_xml(&second).unwrap();
3746 assert!(matches!(reopened.expressions[0], MathExpression::Run(_)));
3747 assert!(matches!(
3748 reopened.expressions[1],
3749 MathExpression::Fraction(_)
3750 ));
3751 assert!(matches!(
3752 reopened.expressions[2],
3753 MathExpression::Subscript(_)
3754 ));
3755 assert!(matches!(
3756 reopened.expressions[3],
3757 MathExpression::Superscript(_)
3758 ));
3759 assert!(matches!(
3760 reopened.expressions[4],
3761 MathExpression::SubSuperscript(_)
3762 ));
3763 assert!(matches!(
3764 reopened.expressions[5],
3765 MathExpression::PreSubSuperscript(_)
3766 ));
3767 assert!(matches!(
3768 reopened.expressions[6],
3769 MathExpression::Radical(_)
3770 ));
3771 assert!(matches!(reopened.expressions[7], MathExpression::Matrix(_)));
3772 assert!(matches!(
3773 reopened.expressions[8],
3774 MathExpression::LowerLimit(_)
3775 ));
3776 assert!(matches!(
3777 reopened.expressions[9],
3778 MathExpression::UpperLimit(_)
3779 ));
3780 assert!(matches!(reopened.expressions[10], MathExpression::Nary(_)));
3781 assert!(matches!(
3782 reopened.expressions[11],
3783 MathExpression::Delimiter(_)
3784 ));
3785 assert!(matches!(
3786 reopened.expressions[12],
3787 MathExpression::Accent(_)
3788 ));
3789 let MathExpression::Run(run) = &reopened.expressions[0] else {
3790 panic!("run")
3791 };
3792 assert_eq!(run.text, "changed");
3793 let reopened_serialized = String::from_utf8(reopened.to_xml().unwrap()).unwrap();
3794 assert_fragments_in_order(
3795 reopened_serialized.as_bytes(),
3796 &[
3797 r#"<x:before keep="root"/>"#,
3798 "<m:r>",
3799 "<m:f>",
3800 "<m:acc>",
3801 "</m:acc>",
3802 r#"<x:after keep="root"/>"#,
3803 ],
3804 );
3805 let fraction_start = reopened_serialized.find("<m:f>").unwrap();
3806 let fraction_end = reopened_serialized[fraction_start..]
3807 .find("</m:f>")
3808 .map(|offset| fraction_start + offset + "</m:f>".len())
3809 .unwrap();
3810 let fraction = &reopened_serialized[fraction_start..fraction_end];
3811 assert_fragments_in_order(
3812 fraction.as_bytes(),
3813 &[
3814 "<m:fPr>",
3815 r#"<x:fraction keep="property"/>"#,
3816 "<m:type",
3817 "</m:fPr>",
3818 "<m:num>",
3819 r#"<x:numerator keep="argument"/>"#,
3820 "<m:r>",
3821 "</m:num>",
3822 "<m:den>",
3823 ],
3824 );
3825 }
3826
3827 #[test]
3828 fn shortening_repeated_math_children_emits_every_unreached_raw_slot() {
3829 let root_source = format!(
3830 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><x:root-zero/><m:r><m:t>A</m:t></m:r><x:root-one/><m:f><m:num><x:arg-zero/><m:r><m:t>N1</m:t></m:r><x:arg-one/><m:r><m:t>N2</m:t></m:r><x:arg-two/></m:num><m:den/></m:f><x:root-two/><m:r><m:t>Z</m:t></m:r><x:root-three/></m:oMath>"#
3831 );
3832 let mut root = CT_OMath::from_xml(root_source.as_bytes()).unwrap();
3833 root.expressions.pop();
3834 let MathExpression::Fraction(fraction) = &mut root.expressions[1] else {
3835 panic!("fraction")
3836 };
3837 fraction.numerator.expressions.pop();
3838 let first = root.to_xml().unwrap();
3839 assert_fragments_in_order(
3840 &first,
3841 &[
3842 "<x:root-zero/>",
3843 "<m:t>A</m:t>",
3844 "<x:root-one/>",
3845 "<m:f>",
3846 "<x:root-two/>",
3847 "<x:root-three/>",
3848 ],
3849 );
3850 assert_fragments_in_order(
3851 &first,
3852 &[
3853 "<m:num>",
3854 "<x:arg-zero/>",
3855 "<m:t>N1</m:t>",
3856 "<x:arg-one/>",
3857 "<x:arg-two/>",
3858 "</m:num>",
3859 ],
3860 );
3861 let reopened = CT_OMath::from_xml(&first).unwrap();
3862 let second = reopened.to_xml().unwrap();
3863 assert_fragments_in_order(
3864 &second,
3865 &[
3866 "<x:root-zero/>",
3867 "<m:t>A</m:t>",
3868 "<x:root-one/>",
3869 "<m:f>",
3870 "<x:root-two/>",
3871 "<x:root-three/>",
3872 ],
3873 );
3874 assert_fragments_in_order(
3875 &second,
3876 &[
3877 "<m:num>",
3878 "<x:arg-zero/>",
3879 "<m:t>N1</m:t>",
3880 "<x:arg-one/>",
3881 "<x:arg-two/>",
3882 "</m:num>",
3883 ],
3884 );
3885
3886 let display_source = format!(
3887 r#"<m:oMathPara xmlns:m="{M_NS}" xmlns:x="urn:producer"><x:display-zero/><m:oMath><m:r><m:t>A</m:t></m:r></m:oMath><x:display-one/><m:oMath><m:r><m:t>B</m:t></m:r></m:oMath><x:display-two/></m:oMathPara>"#
3888 );
3889 let mut display = CT_OMathPara::from_xml(display_source.as_bytes()).unwrap();
3890 display.equations.pop();
3891 let first = display.to_xml().unwrap();
3892 assert_fragments_in_order(
3893 &first,
3894 &[
3895 "<x:display-zero/>",
3896 "<m:t>A</m:t>",
3897 "<x:display-one/>",
3898 "<x:display-two/>",
3899 ],
3900 );
3901 let reopened = CT_OMathPara::from_xml(&first).unwrap();
3902 assert_fragments_in_order(
3903 &reopened.to_xml().unwrap(),
3904 &[
3905 "<x:display-zero/>",
3906 "<m:t>A</m:t>",
3907 "<x:display-one/>",
3908 "<x:display-two/>",
3909 ],
3910 );
3911
3912 let matrix_source = format!(
3913 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:m><x:matrix-zero/><m:mr><x:cell-zero/><m:e/><x:cell-one/><m:e/><x:cell-two/></m:mr><x:matrix-one/><m:mr><m:e/></m:mr><x:matrix-two/></m:m></m:oMath>"#
3914 );
3915 let mut matrix_equation = CT_OMath::from_xml(matrix_source.as_bytes()).unwrap();
3916 let MathExpression::Matrix(matrix) = &mut matrix_equation.expressions[0] else {
3917 panic!("matrix")
3918 };
3919 matrix.rows.pop();
3920 matrix.rows[0].cells.pop();
3921 let first = matrix_equation.to_xml().unwrap();
3922 assert_fragments_in_order(
3923 &first,
3924 &[
3925 "<x:matrix-zero/>",
3926 "<m:mr>",
3927 "<x:matrix-one/>",
3928 "<x:matrix-two/>",
3929 ],
3930 );
3931 assert_fragments_in_order(
3932 &first,
3933 &["<x:cell-zero/>", "<m:e>", "<x:cell-one/>", "<x:cell-two/>"],
3934 );
3935 let reopened = CT_OMath::from_xml(&first).unwrap();
3936 let second = reopened.to_xml().unwrap();
3937 assert_fragments_in_order(
3938 &second,
3939 &[
3940 "<x:matrix-zero/>",
3941 "<m:mr>",
3942 "<x:matrix-one/>",
3943 "<x:matrix-two/>",
3944 ],
3945 );
3946 assert_fragments_in_order(
3947 &second,
3948 &["<x:cell-zero/>", "<m:e>", "<x:cell-one/>", "<x:cell-two/>"],
3949 );
3950
3951 let delimiter_source = format!(
3952 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:d><x:delimiter-zero/><m:e/><x:delimiter-one/><m:e/><x:delimiter-two/></m:d></m:oMath>"#
3953 );
3954 let mut delimiter_equation = CT_OMath::from_xml(delimiter_source.as_bytes()).unwrap();
3955 let MathExpression::Delimiter(delimiter) = &mut delimiter_equation.expressions[0] else {
3956 panic!("delimiter")
3957 };
3958 delimiter.arguments.pop();
3959 let first = delimiter_equation.to_xml().unwrap();
3960 assert_fragments_in_order(
3961 &first,
3962 &[
3963 "<x:delimiter-zero/>",
3964 "<m:e>",
3965 "<x:delimiter-one/>",
3966 "<x:delimiter-two/>",
3967 ],
3968 );
3969 let reopened = CT_OMath::from_xml(&first).unwrap();
3970 assert_fragments_in_order(
3971 &reopened.to_xml().unwrap(),
3972 &[
3973 "<x:delimiter-zero/>",
3974 "<m:e>",
3975 "<x:delimiter-one/>",
3976 "<x:delimiter-two/>",
3977 ],
3978 );
3979 }
3980
3981 #[test]
3982 fn expression_vector_edits_keep_raw_slots_at_ordinal_boundaries() {
3983 let source = format!(
3984 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><x:slot-zero/><m:r><m:t>A</m:t></m:r><x:slot-one/><m:r><m:t>B</m:t></m:r><x:slot-two/></m:oMath>"#
3985 );
3986 let mut parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
3987 parsed.expressions.swap(0, 1);
3988 parsed
3989 .expressions
3990 .insert(0, MathExpression::Run(MathRun::new("C")));
3991 let first = parsed.to_xml().unwrap();
3992 assert_fragments_in_order(
3993 &first,
3994 &[
3995 "<x:slot-zero/>",
3996 "<m:t>C</m:t>",
3997 "<x:slot-one/>",
3998 "<m:t>B</m:t>",
3999 "<x:slot-two/>",
4000 "<m:t>A</m:t>",
4001 ],
4002 );
4003 let reopened = CT_OMath::from_xml(&first).unwrap();
4004 assert_fragments_in_order(
4005 &reopened.to_xml().unwrap(),
4006 &[
4007 "<x:slot-zero/>",
4008 "<m:t>C</m:t>",
4009 "<x:slot-one/>",
4010 "<m:t>B</m:t>",
4011 "<x:slot-two/>",
4012 "<m:t>A</m:t>",
4013 ],
4014 );
4015 }
4016
4017 fn assert_fragments_in_order(xml: &[u8], fragments: &[&str]) {
4018 let xml = std::str::from_utf8(xml).unwrap();
4019 let mut offset = 0usize;
4020 for fragment in fragments {
4021 let position = xml[offset..]
4022 .find(fragment)
4023 .unwrap_or_else(|| panic!("missing {fragment:?} after byte {offset} in {xml}"));
4024 offset += position + fragment.len();
4025 }
4026 }
4027
4028 #[test]
4029 fn officemath_rejects_excessive_nesting_and_invalid_text_bytes() {
4030 let mut deeply_nested = format!(r#"<m:oMath xmlns:m="{M_NS}">"#).into_bytes();
4031 for _ in 0..MAX_OFFICEMATH_XML_DEPTH {
4032 deeply_nested.extend_from_slice(b"<x:a xmlns:x=\"urn:test\">");
4033 }
4034 for _ in 0..MAX_OFFICEMATH_XML_DEPTH {
4035 deeply_nested.extend_from_slice(b"</x:a>");
4036 }
4037 deeply_nested.extend_from_slice(b"</m:oMath>");
4038 assert!(matches!(
4039 CT_OMath::from_xml(&deeply_nested),
4040 Err(OxmlError::InvalidValue(_))
4041 ));
4042
4043 let mut invalid_text = format!(r#"<m:oMath xmlns:m="{M_NS}"><m:r><m:t>"#).into_bytes();
4044 invalid_text.push(0xff);
4045 invalid_text.extend_from_slice(b"</m:t></m:r></m:oMath>");
4046 assert!(CT_OMath::from_xml(&invalid_text).is_err());
4047 }
4048
4049 #[test]
4050 fn malformed_supported_construct_remains_raw_instead_of_becoming_a_default() {
4051 let malformed = format!(
4052 r#"<m:oMath xmlns:m="{M_NS}"><m:f data="kept"><m:den><m:r><m:t>2</m:t></m:r></m:den></m:f></m:oMath>"#
4053 );
4054 let parsed = CT_OMath::from_xml(malformed.as_bytes()).unwrap();
4055 assert!(parsed.expressions.is_empty());
4056 let output = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
4057 assert!(
4058 output.contains(r#"<m:f data="kept"><m:den><m:r><m:t>2</m:t></m:r></m:den></m:f>"#)
4059 );
4060 }
4061
4062 #[test]
4063 fn modeled_property_containers_preserve_unknown_attributes_and_children() {
4064 let xml = format!(
4065 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:f><m:fPr data="container"><m:type m:val="bar" data="leaf"><x:hint/></m:type><x:extension value="kept"/></m:fPr><m:num><m:r><m:t>1</m:t></m:r></m:num><m:den><m:r><m:t>2</m:t></m:r></m:den></m:f></m:oMath>"#
4066 );
4067 let mut parsed = CT_OMath::from_xml(xml.as_bytes()).unwrap();
4068 let MathExpression::Fraction(fraction) = &mut parsed.expressions[0] else {
4069 panic!("fraction")
4070 };
4071 fraction.fraction_type = FractionType::Linear;
4072 let output = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
4073 assert!(output.contains(r#"<m:fPr data="container">"#));
4074 assert!(output.contains(r#"<m:type data="leaf" m:val="lin"><x:hint/></m:type>"#));
4075 assert!(output.contains(r#"<x:extension value="kept"/>"#));
4076 }
4077
4078 #[test]
4079 fn global_math_properties_preserve_modeled_leaf_extensions() {
4080 let xml = format!(
4081 r#"<z:mathPr xmlns:z="{M_NS}" xmlns:x="urn:producer" data="root"><z:mathFont z:val="Cambria Math" data="leaf"><x:hint/></z:mathFont><x:extension/></z:mathPr>"#
4082 );
4083 let mut properties = MathProperties::from_raw(xml.as_bytes(), &[]).unwrap();
4084 properties.math_font = Some("STIX Two Math".to_owned());
4085 let mut writer = Writer::new(Vec::new());
4086 properties.write_xml(&mut writer).unwrap();
4087 let output = String::from_utf8(writer.into_inner()).unwrap();
4088 assert!(output.starts_with(&format!(r#"<m:mathPr xmlns:m="{M_NS}""#)));
4089 assert!(output.contains("data=\"root\""));
4090 assert!(
4091 output.contains(
4092 r#"<m:mathFont data="leaf" m:val="STIX Two Math"><x:hint/></m:mathFont>"#
4093 )
4094 );
4095 assert!(output.contains("<x:extension/>"));
4096 }
4097
4098 #[test]
4099 fn property_groups_write_schema_order_and_preserve_math_text_extensions() {
4100 let xml = format!(
4101 r#"<m:oMath xmlns:m="{M_NS}" xmlns:x="urn:producer"><m:r><m:rPr><m:lit/><m:nor m:val="0"/><m:scr m:val="fraktur"/><m:sty m:val="b"/><m:brk m:alnAt="4"/></m:rPr><m:t x:keep="yes">x</m:t></m:r><m:nary><m:naryPr><m:chr m:val="∑"/><m:limLoc m:val="subSup"/><m:grow/><m:subHide m:val="0"/><m:supHide m:val="0"/></m:naryPr><m:sub/><m:sup/><m:e/></m:nary><m:d><m:dPr><m:begChr m:val="["/><m:sepChr m:val="|"/><m:endChr m:val="]"/><m:grow/></m:dPr><m:e/></m:d></m:oMath>"#
4102 );
4103 let parsed = CT_OMath::from_xml(xml.as_bytes()).unwrap();
4104 let output = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
4105 let run_properties = &output[output.find("<m:rPr>").unwrap()..];
4106 for pair in [
4107 ("lit", "nor"),
4108 ("nor", "scr"),
4109 ("scr", "sty"),
4110 ("sty", "brk"),
4111 ] {
4112 assert!(
4113 run_properties.find(&format!("<m:{}", pair.0)).unwrap()
4114 < run_properties.find(&format!("<m:{}", pair.1)).unwrap()
4115 );
4116 }
4117 assert!(output.contains(r#"<m:brk m:alnAt="4"/>"#));
4118 assert!(output.contains(r#"<m:t x:keep="yes">x</m:t>"#));
4119
4120 let nary = &output[output.find("<m:naryPr>").unwrap()..];
4121 for pair in [
4122 ("chr", "limLoc"),
4123 ("limLoc", "grow"),
4124 ("grow", "subHide"),
4125 ("subHide", "supHide"),
4126 ] {
4127 assert!(
4128 nary.find(&format!("<m:{}", pair.0)).unwrap()
4129 < nary.find(&format!("<m:{}", pair.1)).unwrap()
4130 );
4131 }
4132 let delimiter = &output[output.find("<m:dPr>").unwrap()..];
4133 assert!(delimiter.find("<m:begChr").unwrap() < delimiter.find("<m:sepChr").unwrap());
4134 assert!(delimiter.find("<m:sepChr").unwrap() < delimiter.find("<m:endChr").unwrap());
4135 assert!(delimiter.find("<m:endChr").unwrap() < delimiter.find("<m:grow").unwrap());
4136 }
4137
4138 #[test]
4139 fn matrix_base_justification_and_nary_absence_use_schema_defaults() {
4140 let xml = format!(
4141 r#"<m:oMath xmlns:m="{M_NS}"><m:m><m:mPr><m:baseJc m:val="top"/></m:mPr><m:mr><m:e/></m:mr></m:m><m:nary><m:sub/><m:sup/><m:e/></m:nary></m:oMath>"#
4142 );
4143 let parsed = CT_OMath::from_xml(xml.as_bytes()).unwrap();
4144 let MathExpression::Matrix(matrix) = &parsed.expressions[0] else {
4145 panic!("matrix")
4146 };
4147 assert_eq!(
4148 matrix.properties.base_justification,
4149 Some(MatrixBaseJustification::Top)
4150 );
4151 let MathExpression::Nary(nary) = &parsed.expressions[1] else {
4152 panic!("nary")
4153 };
4154 assert!(!nary.hide_subscript);
4155 assert!(!nary.hide_superscript);
4156 assert_eq!(nary.character, "∫");
4157 }
4158
4159 #[test]
4160 fn valueless_property_leaves_use_schema_defaults_and_reopen() {
4161 fn assert_expression_defaults(parsed: &CT_OMath) {
4162 let MathExpression::Run(run) = &parsed.expressions[0] else {
4163 panic!("run")
4164 };
4165 assert_eq!(run.properties.script, Some(MathScriptStyle::Roman));
4166 assert_eq!(run.properties.style, Some(MathStyle::Italic));
4167
4168 let MathExpression::Fraction(fraction) = &parsed.expressions[1] else {
4169 panic!("fraction")
4170 };
4171 assert_eq!(fraction.fraction_type, FractionType::Bar);
4172
4173 let MathExpression::Matrix(matrix) = &parsed.expressions[2] else {
4174 panic!("matrix")
4175 };
4176 assert_eq!(
4177 matrix.properties.base_justification,
4178 Some(MatrixBaseJustification::Center)
4179 );
4180 assert_eq!(matrix.properties.row_spacing, Some(0));
4181 assert_eq!(matrix.properties.column_spacing, Some(0));
4182
4183 let MathExpression::Nary(nary) = &parsed.expressions[3] else {
4184 panic!("n-ary")
4185 };
4186 assert_eq!(nary.character, "");
4187 assert_eq!(nary.limit_location, Some(LimitLocation::UnderOver));
4188
4189 let MathExpression::Delimiter(delimiter) = &parsed.expressions[4] else {
4190 panic!("delimiter")
4191 };
4192 assert_eq!(delimiter.begin_character, "");
4193 assert_eq!(delimiter.separator_character, "");
4194 assert_eq!(delimiter.end_character, "");
4195
4196 let MathExpression::Accent(accent) = &parsed.expressions[5] else {
4197 panic!("accent")
4198 };
4199 assert_eq!(accent.character, "");
4200 }
4201
4202 let source = format!(
4203 r#"<m:oMath xmlns:m="{M_NS}"><m:r><m:rPr><m:scr/><m:sty/></m:rPr><m:t>x</m:t></m:r><m:f><m:fPr><m:type/></m:fPr><m:num/><m:den/></m:f><m:m><m:mPr><m:baseJc/><m:rSp/><m:cSp/></m:mPr><m:mr><m:e/></m:mr></m:m><m:nary><m:naryPr><m:chr/><m:limLoc/></m:naryPr><m:e/></m:nary><m:d><m:dPr><m:begChr/><m:sepChr/><m:endChr/></m:dPr><m:e/></m:d><m:acc><m:accPr><m:chr/></m:accPr><m:e/></m:acc></m:oMath>"#
4204 );
4205 let parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
4206 assert_eq!(parsed.expressions.len(), 6);
4207 assert!(!parsed.has_unsupported_content());
4208 assert_expression_defaults(&parsed);
4209 let first = parsed.to_xml().unwrap();
4210 let first_text = String::from_utf8(first.clone()).unwrap();
4211 for expected in [
4212 r#"<m:scr m:val="roman"/>"#,
4213 r#"<m:sty m:val="i"/>"#,
4214 r#"<m:type m:val="bar"/>"#,
4215 r#"<m:baseJc m:val="center"/>"#,
4216 r#"<m:rSp m:val="0"/>"#,
4217 r#"<m:cSp m:val="0"/>"#,
4218 r#"<m:limLoc m:val="undOvr"/>"#,
4219 ] {
4220 assert!(first_text.contains(expected), "{first_text}");
4221 }
4222 assert_eq!(first_text.matches(r#"<m:chr m:val=""/>"#).count(), 2);
4223 assert!(first_text.contains(r#"<m:begChr m:val=""/>"#));
4224 assert!(first_text.contains(r#"<m:sepChr m:val=""/>"#));
4225 assert!(first_text.contains(r#"<m:endChr m:val=""/>"#));
4226 let reopened = CT_OMath::from_xml(&first).unwrap();
4227 assert_expression_defaults(&reopened);
4228 assert_eq!(reopened.to_xml().unwrap(), first);
4229
4230 let display_source = format!(
4231 r#"<m:oMathPara xmlns:m="{M_NS}"><m:oMathParaPr><m:jc/></m:oMathParaPr><m:oMath><m:r><m:t>x</m:t></m:r></m:oMath></m:oMathPara>"#
4232 );
4233 let display = CT_OMathPara::from_xml(display_source.as_bytes()).unwrap();
4234 assert_eq!(
4235 display.properties.justification,
4236 Some(MathJustification::CenterGroup)
4237 );
4238 assert!(!display.has_unsupported_content());
4239 let first = display.to_xml().unwrap();
4240 assert!(String::from_utf8_lossy(&first).contains(r#"<m:jc m:val="centerGroup"/>"#));
4241 let reopened = CT_OMathPara::from_xml(&first).unwrap();
4242 assert_eq!(
4243 reopened.properties.justification,
4244 Some(MathJustification::CenterGroup)
4245 );
4246 assert_eq!(reopened.to_xml().unwrap(), first);
4247
4248 let absent_source = format!(
4249 r#"<m:oMath xmlns:m="{M_NS}"><m:r><m:t>x</m:t></m:r><m:nary><m:e/></m:nary><m:d><m:e/></m:d></m:oMath>"#
4250 );
4251 let absent = CT_OMath::from_xml(absent_source.as_bytes()).unwrap();
4252 let MathExpression::Run(run) = &absent.expressions[0] else {
4253 panic!("run")
4254 };
4255 assert_eq!(run.properties.script, None);
4256 assert_eq!(run.properties.style, None);
4257 let MathExpression::Nary(nary) = &absent.expressions[1] else {
4258 panic!("n-ary")
4259 };
4260 assert_eq!(nary.character, "∫");
4261 assert_eq!(nary.limit_location, None);
4262 let MathExpression::Delimiter(delimiter) = &absent.expressions[2] else {
4263 panic!("delimiter")
4264 };
4265 assert_eq!(delimiter.begin_character, "(");
4266 assert_eq!(delimiter.separator_character, "|");
4267 assert_eq!(delimiter.end_character, ")");
4268 }
4269
4270 #[test]
4271 fn valueless_global_defaults_are_typed_while_required_values_remain_raw() {
4272 let source = format!(
4273 r#"<m:mathPr xmlns:m="{M_NS}"><m:mathFont/><m:lMargin/><m:rMargin/><m:defJc/><m:preSp/><m:postSp/><m:interSp/><m:intraSp/><m:wrapIndent/><m:intLim/><m:naryLim/></m:mathPr>"#
4274 );
4275 let parsed = MathProperties::from_raw(source.as_bytes(), &[]).unwrap();
4276 assert_eq!(parsed.math_font, None);
4277 assert_eq!(parsed.left_margin, Some(1440));
4278 assert_eq!(parsed.right_margin, Some(1440));
4279 assert_eq!(parsed.justification, Some(MathJustification::CenterGroup));
4280 assert_eq!(parsed.wrap_indent, Some(1440));
4281 assert_eq!(
4282 parsed.integral_limit_location,
4283 Some(LimitLocation::SubSuperscript)
4284 );
4285 assert_eq!(parsed.nary_limit_location, Some(LimitLocation::UnderOver));
4286 assert_eq!(parsed.pre_spacing, None);
4287 assert_eq!(parsed.post_spacing, None);
4288 assert_eq!(parsed.inter_spacing, None);
4289 assert_eq!(parsed.intra_spacing, None);
4290 assert!(parsed.has_unsupported_content());
4291
4292 let mut writer = Writer::new(Vec::new());
4293 parsed.write_xml(&mut writer).unwrap();
4294 let first = writer.into_inner();
4295 let first_text = String::from_utf8(first.clone()).unwrap();
4296 for raw in [
4297 "<m:mathFont/>",
4298 "<m:preSp/>",
4299 "<m:postSp/>",
4300 "<m:interSp/>",
4301 "<m:intraSp/>",
4302 ] {
4303 assert!(first_text.contains(raw), "{first_text}");
4304 }
4305 let reopened = MathProperties::from_raw(&first, &[]).unwrap();
4306 assert_eq!(reopened.left_margin, Some(1440));
4307 assert_eq!(reopened.right_margin, Some(1440));
4308 assert_eq!(reopened.wrap_indent, Some(1440));
4309 assert!(reopened.has_unsupported_content());
4310 let mut writer = Writer::new(Vec::new());
4311 reopened.write_xml(&mut writer).unwrap();
4312 assert_eq!(writer.into_inner(), first);
4313 }
4314
4315 #[test]
4316 fn invalid_math_text_space_value_keeps_the_run_opaque() {
4317 let source = format!(
4318 r#"<m:oMath xmlns:m="{M_NS}"><m:r><m:t xml:space="invalid">x</m:t></m:r></m:oMath>"#
4319 );
4320 let parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
4321 assert!(parsed.expressions.is_empty());
4322 assert!(parsed.has_unsupported_content());
4323 let first = parsed.to_xml().unwrap();
4324 assert_eq!(first, source.as_bytes());
4325 let reopened = CT_OMath::from_xml(&first).unwrap();
4326 assert!(reopened.expressions.is_empty());
4327 assert!(reopened.has_unsupported_content());
4328 assert_eq!(reopened.to_xml().unwrap(), first);
4329
4330 for valid in ["default", "preserve"] {
4331 let source = format!(
4332 r#"<m:oMath xmlns:m="{M_NS}"><m:r><m:t xml:space="{valid}">x</m:t></m:r></m:oMath>"#
4333 );
4334 let parsed = CT_OMath::from_xml(source.as_bytes()).unwrap();
4335 assert_eq!(parsed.expressions.len(), 1);
4336 assert!(!parsed.has_unsupported_content());
4337 }
4338 }
4339
4340 #[test]
4341 fn duplicate_math_text_keeps_the_run_opaque() {
4342 let xml =
4343 format!(r#"<m:oMath xmlns:m="{M_NS}"><m:r><m:t>a</m:t><m:t>b</m:t></m:r></m:oMath>"#);
4344 let parsed = CT_OMath::from_xml(xml.as_bytes()).unwrap();
4345 assert!(parsed.expressions.is_empty());
4346 let output = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
4347 assert!(output.contains(r#"<m:r><m:t>a</m:t><m:t>b</m:t></m:r>"#));
4348 }
4349
4350 #[test]
4351 fn malformed_and_duplicate_property_leaves_remain_raw() {
4352 let malformed_fraction = format!(
4353 r#"<m:oMath xmlns:m="{M_NS}"><m:f><m:fPr><m:type m:val="other"/></m:fPr><m:num/><m:den/></m:f></m:oMath>"#
4354 );
4355 let parsed = CT_OMath::from_xml(malformed_fraction.as_bytes()).unwrap();
4356 assert!(parsed.expressions.is_empty());
4357 assert!(
4358 String::from_utf8(parsed.to_xml().unwrap())
4359 .unwrap()
4360 .contains(r#"<m:type m:val="other"/>"#)
4361 );
4362
4363 let global = format!(
4364 r#"<m:mathPr xmlns:m="{M_NS}"><m:mathFont m:val="Cambria Math"/><m:mathFont m:val="duplicate"/><m:defJc m:val="sideways"/></m:mathPr>"#
4365 );
4366 let properties = MathProperties::from_raw(global.as_bytes(), &[]).unwrap();
4367 assert_eq!(properties.math_font.as_deref(), Some("Cambria Math"));
4368 assert_eq!(properties.justification, None);
4369 let mut writer = Writer::new(Vec::new());
4370 properties.write_xml(&mut writer).unwrap();
4371 let output = String::from_utf8(writer.into_inner()).unwrap();
4372 assert!(output.contains(r#"<m:mathFont m:val="duplicate"/>"#));
4373 assert!(output.contains(r#"<m:defJc m:val="sideways"/>"#));
4374
4375 let negative_margin =
4376 format!(r#"<m:mathPr xmlns:m="{M_NS}"><m:lMargin m:val="-1"/></m:mathPr>"#);
4377 let properties = MathProperties::from_raw(negative_margin.as_bytes(), &[]).unwrap();
4378 assert_eq!(properties.left_margin, None);
4379 let mut writer = Writer::new(Vec::new());
4380 properties.write_xml(&mut writer).unwrap();
4381 assert!(
4382 String::from_utf8(writer.into_inner())
4383 .unwrap()
4384 .contains(r#"<m:lMargin m:val="-1"/>"#)
4385 );
4386
4387 let negative_matrix_spacing = format!(
4388 r#"<m:oMath xmlns:m="{M_NS}"><m:m><m:mPr><m:rSp m:val="-1"/></m:mPr><m:mr><m:e/></m:mr></m:m></m:oMath>"#
4389 );
4390 let parsed = CT_OMath::from_xml(negative_matrix_spacing.as_bytes()).unwrap();
4391 assert!(parsed.expressions.is_empty());
4392 }
4393
4394 #[test]
4395 fn malformed_display_and_nested_or_truncated_text_fail_closed() {
4396 let empty_display = format!(r#"<m:oMathPara xmlns:m="{M_NS}"/>"#);
4397 assert!(CT_OMathPara::from_xml(empty_display.as_bytes()).is_err());
4398 assert!(
4399 OfficeMath::from_raw(empty_display.as_bytes(), &[])
4400 .unwrap()
4401 .is_none()
4402 );
4403
4404 let nested = format!(
4405 r#"<m:oMath xmlns:m="{M_NS}"><m:r><m:t><x:empty xmlns:x="urn:test"/></m:t></m:r></m:oMath>"#
4406 );
4407 let parsed = CT_OMath::from_xml(nested.as_bytes()).unwrap();
4408 assert!(parsed.expressions.is_empty());
4409 assert!(parsed.has_unsupported_content());
4410 let first = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
4411 assert_eq!(first, nested);
4412 let reopened = CT_OMath::from_xml(first.as_bytes()).unwrap();
4413 assert!(reopened.expressions.is_empty());
4414 assert!(reopened.has_unsupported_content());
4415 assert_eq!(
4416 String::from_utf8(reopened.to_xml().unwrap()).unwrap(),
4417 nested
4418 );
4419 let truncated = format!(r#"<m:oMath xmlns:m="{M_NS}"><m:r><m:t>x"#);
4420 assert!(CT_OMath::from_xml(truncated.as_bytes()).is_err());
4421 }
4422
4423 #[test]
4424 fn standalone_output_replays_inherited_prefixes_needed_by_raw_xml() {
4425 let raw =
4426 format!(r#"<m:oMath xmlns:m="{M_NS}"><x:before/><m:r><m:t>x</m:t></m:r></m:oMath>"#);
4427 let inherited = vec![("x".to_owned(), "urn:producer".to_owned())];
4428 let parsed = CT_OMath::from_raw(raw.as_bytes(), &inherited).unwrap();
4429 let output = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
4430 assert!(output.contains(r#"xmlns:x="urn:producer""#));
4431 assert!(output.contains("<x:before/>"));
4432 }
4433
4434 #[test]
4435 fn conflicting_m_bindings_fail_closed_and_unqualified_attributes_stay_raw() {
4436 let aliased = format!(
4437 r#"<q:oMath xmlns:q="{M_NS}" xmlns:m="urn:producer"><m:opaque/><q:r><q:t>x</q:t></q:r></q:oMath>"#
4438 );
4439 assert!(CT_OMath::from_xml(aliased.as_bytes()).is_err());
4440 assert!(
4441 OfficeMath::from_raw(aliased.as_bytes(), &[])
4442 .unwrap()
4443 .is_none()
4444 );
4445
4446 let unqualified = format!(
4447 r#"<oMath xmlns="{M_NS}" xmlns:m="{M_NS}"><f><fPr><type val="lin"/></fPr><num/><den/></f></oMath>"#
4448 );
4449 let parsed = CT_OMath::from_xml(unqualified.as_bytes()).unwrap();
4450 assert!(parsed.expressions.is_empty());
4451 assert!(
4452 String::from_utf8(parsed.to_xml().unwrap())
4453 .unwrap()
4454 .contains(r#"<type val="lin"/>"#)
4455 );
4456 }
4457
4458 #[test]
4459 fn newly_authored_properties_precede_later_preserved_schema_children() {
4460 let raw = format!(
4461 r#"<m:oMath xmlns:m="{M_NS}" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><m:f><m:fPr><m:ctrlPr><w:rPr/></m:ctrlPr></m:fPr><m:num/><m:den/></m:f></m:oMath>"#
4462 );
4463 let parsed = CT_OMath::from_xml(raw.as_bytes()).unwrap();
4464 let output = String::from_utf8(parsed.to_xml().unwrap()).unwrap();
4465 let properties = &output[output.find("<m:fPr").unwrap()..];
4466 assert!(properties.find("<m:type").unwrap() < properties.find("<m:ctrlPr").unwrap());
4467
4468 let raw = format!(
4469 r#"<m:mathPr xmlns:m="{M_NS}"><m:mathFont m:val="Cambria Math"/><m:brkBin m:val="before"/></m:mathPr>"#
4470 );
4471 let mut parsed = MathProperties::from_raw(raw.as_bytes(), &[]).unwrap();
4472 parsed.small_fraction = Some(true);
4473 let mut writer = Writer::new(Vec::new());
4474 parsed.write_xml(&mut writer).unwrap();
4475 let output = String::from_utf8(writer.into_inner()).unwrap();
4476 assert!(output.find("<m:mathFont").unwrap() < output.find("<m:brkBin").unwrap());
4477 assert!(output.find("<m:brkBin").unwrap() < output.find("<m:smallFrac").unwrap());
4478 }
4479}