Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
OfficeMathML
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
3 / 3
20
100.00% covered (success)
100.00%
1 / 1
 read
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 parseNode
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getElement
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
1 / 1
15
1<?php
2
3namespace PhpOffice\Math\Reader;
4
5use DOMDocument;
6use DOMNode;
7use DOMXPath;
8use PhpOffice\Math\Element;
9use PhpOffice\Math\Exception\InvalidInputException;
10use PhpOffice\Math\Exception\NotImplementedException;
11use PhpOffice\Math\Math;
12
13class OfficeMathML implements ReaderInterface
14{
15    /** @var DOMDocument */
16    protected $dom;
17
18    /** @var Math */
19    protected $math;
20
21    /** @var DOMXPath */
22    protected $xpath;
23
24    /** @var string[] */
25    protected $operators = ['+', '-', '/', '∗'];
26
27    public function read(string $content): ?Math
28    {
29        $nsMath = 'xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"';
30        $nsWord = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"';
31
32        $content = str_replace(
33            $nsMath,
34            $nsMath . ' ' . $nsWord,
35            $content
36        );
37
38        $this->dom = new DOMDocument();
39        $this->dom->loadXML($content);
40
41        $this->math = new Math();
42        $this->parseNode(null, $this->math);
43
44        return $this->math;
45    }
46
47    /**
48     * @see https://devblogs.microsoft.com/math-in-office/officemath/
49     * @see https://learn.microsoft.com/fr-fr/archive/blogs/murrays/mathml-and-ecma-math-omml
50     *
51     * @param Math|Element\AbstractGroupElement $parent
52     */
53    protected function parseNode(?DOMNode $nodeRowElement, $parent): void
54    {
55        $this->xpath = new DOMXPath($this->dom);
56        foreach ($this->xpath->query('*', $nodeRowElement) ?: [] as $nodeElement) {
57            $element = $this->getElement($nodeElement);
58            $parent->add($element);
59
60            if ($element instanceof Element\AbstractGroupElement) {
61                $this->parseNode($nodeElement, $element);
62            }
63        }
64    }
65
66    protected function getElement(DOMNode $nodeElement): Element\AbstractElement
67    {
68        switch ($nodeElement->nodeName) {
69            case 'm:f':
70                // Numerator
71                $nodeNumerator = $this->xpath->query('m:num/m:r/m:t', $nodeElement);
72                if ($nodeNumerator && $nodeNumerator->length == 1) {
73                    $value = $nodeNumerator->item(0)->nodeValue;
74                    if (is_numeric($value)) {
75                        $numerator = new Element\Numeric(floatval($value));
76                    } else {
77                        $numerator = new Element\Identifier($value);
78                    }
79                } else {
80                    throw new InvalidInputException(sprintf(
81                        '%s : The tag `%s` has no numerator defined',
82                        __METHOD__,
83                        $nodeElement->nodeName
84                    ));
85                }
86                // Denominator
87                $nodeDenominator = $this->xpath->query('m:den/m:r/m:t', $nodeElement);
88                if ($nodeDenominator && $nodeDenominator->length == 1) {
89                    $value = $nodeDenominator->item(0)->nodeValue;
90                    if (is_numeric($value)) {
91                        $denominator = new Element\Numeric(floatval($value));
92                    } else {
93                        $denominator = new Element\Identifier($value);
94                    }
95                } else {
96                    throw new InvalidInputException(sprintf(
97                        '%s : The tag `%s` has no denominator defined',
98                        __METHOD__,
99                        $nodeElement->nodeName
100                    ));
101                }
102
103                return new Element\Fraction($numerator, $denominator);
104            case 'm:r':
105                $nodeText = $this->xpath->query('m:t', $nodeElement);
106                if ($nodeText && $nodeText->length == 1) {
107                    $value = trim($nodeText->item(0)->nodeValue);
108                    if (in_array($value, $this->operators)) {
109                        return new Element\Operator($value);
110                    }
111                    if (is_numeric($value)) {
112                        return new Element\Numeric(floatval($value));
113                    }
114
115                    return new Element\Identifier($value);
116                }
117
118                throw new InvalidInputException(sprintf(
119                    '%s : The tag `%s` has no tag `m:t` defined',
120                    __METHOD__,
121                    $nodeElement->nodeName
122                ));
123            case 'm:oMath':
124                return new Element\Row();
125            default:
126                throw new NotImplementedException(sprintf(
127                    '%s : The tag `%s` is not implemented',
128                    __METHOD__,
129                    $nodeElement->nodeName
130                ));
131        }
132    }
133}