Skip to main content

rdocx_oxml/
math.rs

1//! Typed support for the Transitional OfficeMath vocabulary.
2
3use 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/// One inline OfficeMath equation.
36#[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    /// Whether this equation retains content outside the typed OfficeMath subset.
83    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/// One display OfficeMath paragraph.
110#[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    /// Whether this display equation retains content outside the typed subset.
175    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/// Inline or display OfficeMath at a paragraph boundary.
220#[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    /// Whether this value retains content outside the typed OfficeMath subset.
243    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/// One expression in the normalized OfficeMath tree.
273#[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    /// Whether this expression or one of its arguments retains unsupported content.
292    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/// A recursively nested OfficeMath argument.
415#[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    /// Whether this argument retains content outside the typed OfficeMath subset.
434    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/// OfficeMath run formatting needed by authoring and layout.
474#[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/// Mathematical glyph style.
486#[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/// Script category for a math run.
516#[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/// Text-bearing OfficeMath run.
552#[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/// Fraction bar form.
633#[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/// Base and one script argument, used for subscript and superscript forms.
742#[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/// Document-wide defaults from `w:settings/m:mathPr`.
1749#[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    /// Whether the settings subtree retains content outside the typed subset.
1773    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}