first commit

This commit is contained in:
2025-11-20 14:44:35 +08:00
commit 91f70ed69c
839 changed files with 207385 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
<?php
namespace PhpOffice\Math\Reader;
use DOMDocument;
use DOMElement;
use DOMNode;
use DOMXPath;
use PhpOffice\Math\Element;
use PhpOffice\Math\Exception\InvalidInputException;
use PhpOffice\Math\Exception\NotImplementedException;
use PhpOffice\Math\Math;
use PhpOffice\Math\Reader\Security\XmlScanner;
class MathML implements ReaderInterface
{
/** @var Math */
private $math;
/** @var DOMDocument */
private $dom;
/** @var DOMXPath */
private $xpath;
/** @var XmlScanner */
private $xmlScanner;
public function __construct()
{
$this->xmlScanner = XmlScanner::getInstance();
}
public function read(string $content): ?Math
{
$content = $this->xmlScanner->scan($content);
$content = str_replace(
[
'&InvisibleTimes;',
],
[
'<mchar name="InvisibleTimes"/>',
],
$content
);
$this->dom = new DOMDocument();
$this->dom->loadXML($content);
$this->math = new Math();
$this->parseNode(null, $this->math);
return $this->math;
}
/**
* @param Math|Element\AbstractGroupElement $parent
*/
protected function parseNode(?DOMNode $nodeRowElement, $parent): void
{
$this->xpath = new DOMXPath($this->dom);
foreach ($this->xpath->query('*', $nodeRowElement) ?: [] as $nodeElement) {
if ($parent instanceof Element\Semantics
&& $nodeElement instanceof DOMElement
&& $nodeElement->nodeName == 'annotation') {
$parent->addAnnotation(
$nodeElement->getAttribute('encoding'),
trim($nodeElement->nodeValue)
);
continue;
}
$parent->add($this->getElement($nodeElement));
}
}
protected function getElement(DOMNode $nodeElement): Element\AbstractElement
{
$nodeValue = trim($nodeElement->nodeValue);
switch ($nodeElement->nodeName) {
case 'mfrac':
$nodeList = $this->xpath->query('*', $nodeElement);
if ($nodeList && $nodeList->length == 2) {
return new Element\Fraction(
$this->getElement($nodeList->item(0)),
$this->getElement($nodeList->item(1))
);
}
throw new InvalidInputException(sprintf(
'%s : The tag `%s` has not two subelements',
__METHOD__,
$nodeElement->nodeName
));
case 'mi':
return new Element\Identifier($nodeValue);
case 'mn':
return new Element\Numeric(floatval($nodeValue));
case 'mo':
if (empty($nodeValue)) {
$nodeList = $this->xpath->query('*', $nodeElement);
if (
$nodeList
&& $nodeList->length == 1
&& $nodeList->item(0)->nodeName == 'mchar'
&& $nodeList->item(0) instanceof DOMElement
&& $nodeList->item(0)->hasAttribute('name')
) {
$nodeValue = $nodeList->item(0)->getAttribute('name');
}
}
return new Element\Operator($nodeValue);
case 'mrow':
$mrow = new Element\Row();
$this->parseNode($nodeElement, $mrow);
return $mrow;
case 'msup':
$nodeList = $this->xpath->query('*', $nodeElement);
if ($nodeList && $nodeList->length == 2) {
return new Element\Superscript(
$this->getElement($nodeList->item(0)),
$this->getElement($nodeList->item(1))
);
}
throw new InvalidInputException(sprintf(
'%s : The tag `%s` has not two subelements',
__METHOD__,
$nodeElement->nodeName
));
case 'semantics':
$semantics = new Element\Semantics();
$this->parseNode($nodeElement, $semantics);
return $semantics;
default:
throw new NotImplementedException(sprintf(
'%s : The tag `%s` is not implemented',
__METHOD__,
$nodeElement->nodeName
));
}
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
namespace PhpOffice\Math\Reader;
use DOMDocument;
use DOMNode;
use DOMXPath;
use PhpOffice\Math\Element;
use PhpOffice\Math\Exception\InvalidInputException;
use PhpOffice\Math\Exception\NotImplementedException;
use PhpOffice\Math\Math;
class OfficeMathML implements ReaderInterface
{
/** @var DOMDocument */
protected $dom;
/** @var Math */
protected $math;
/** @var DOMXPath */
protected $xpath;
/** @var string[] */
protected $operators = ['+', '-', '/', '∗'];
public function read(string $content): ?Math
{
$nsMath = 'xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"';
$nsWord = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"';
$content = str_replace(
$nsMath,
$nsMath . ' ' . $nsWord,
$content
);
$this->dom = new DOMDocument();
$this->dom->loadXML($content);
$this->math = new Math();
$this->parseNode(null, $this->math);
return $this->math;
}
/**
* @see https://devblogs.microsoft.com/math-in-office/officemath/
* @see https://learn.microsoft.com/fr-fr/archive/blogs/murrays/mathml-and-ecma-math-omml
*
* @param Math|Element\AbstractGroupElement $parent
*/
protected function parseNode(?DOMNode $nodeRowElement, $parent): void
{
$this->xpath = new DOMXPath($this->dom);
foreach ($this->xpath->query('*', $nodeRowElement) ?: [] as $nodeElement) {
$element = $this->getElement($nodeElement);
$parent->add($element);
if ($element instanceof Element\AbstractGroupElement) {
$this->parseNode($nodeElement, $element);
}
}
}
protected function getElement(DOMNode $nodeElement): Element\AbstractElement
{
switch ($nodeElement->nodeName) {
case 'm:f':
// Numerator
$nodeNumerator = $this->xpath->query('m:num/m:r/m:t', $nodeElement);
if ($nodeNumerator && $nodeNumerator->length == 1) {
$value = $nodeNumerator->item(0)->nodeValue;
if (is_numeric($value)) {
$numerator = new Element\Numeric(floatval($value));
} else {
$numerator = new Element\Identifier($value);
}
} else {
throw new InvalidInputException(sprintf(
'%s : The tag `%s` has no numerator defined',
__METHOD__,
$nodeElement->nodeName
));
}
// Denominator
$nodeDenominator = $this->xpath->query('m:den/m:r/m:t', $nodeElement);
if ($nodeDenominator && $nodeDenominator->length == 1) {
$value = $nodeDenominator->item(0)->nodeValue;
if (is_numeric($value)) {
$denominator = new Element\Numeric(floatval($value));
} else {
$denominator = new Element\Identifier($value);
}
} else {
throw new InvalidInputException(sprintf(
'%s : The tag `%s` has no denominator defined',
__METHOD__,
$nodeElement->nodeName
));
}
return new Element\Fraction($numerator, $denominator);
case 'm:r':
$nodeText = $this->xpath->query('m:t', $nodeElement);
if ($nodeText && $nodeText->length == 1) {
$value = trim($nodeText->item(0)->nodeValue);
if (in_array($value, $this->operators)) {
return new Element\Operator($value);
}
if (is_numeric($value)) {
return new Element\Numeric(floatval($value));
}
return new Element\Identifier($value);
}
throw new InvalidInputException(sprintf(
'%s : The tag `%s` has no tag `m:t` defined',
__METHOD__,
$nodeElement->nodeName
));
case 'm:oMath':
return new Element\Row();
default:
throw new NotImplementedException(sprintf(
'%s : The tag `%s` is not implemented',
__METHOD__,
$nodeElement->nodeName
));
}
}
}
@@ -0,0 +1,10 @@
<?php
namespace PhpOffice\Math\Reader;
use PhpOffice\Math\Math;
interface ReaderInterface
{
public function read(string $content): ?Math;
}
@@ -0,0 +1,77 @@
<?php
namespace PhpOffice\Math\Reader\Security;
use PhpOffice\Math\Exception\SecurityException;
class XmlScanner
{
public static function getInstance(): self
{
return new self();
}
/**
* Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
*/
public function scan(string $xml): string
{
// Don't rely purely on libxml_disable_entity_loader()
$searchDoctype = static::mb_str_split('<!DOCTYPE', 1, 'UTF-8');
$patternDoctype = '/\0*' . implode('\0*', is_array($searchDoctype) ? $searchDoctype : []) . '\0*/';
$searchDoctypeMath = static::mb_str_split('<!DOCTYPE math', 1, 'UTF-8');
$patternDoctypeMath = '/\0*' . implode('\0*', is_array($searchDoctypeMath) ? $searchDoctypeMath : []) . '\0*/';
if (preg_match($patternDoctype, $xml) && !preg_match($patternDoctypeMath, $xml)) {
throw new SecurityException('Detected use of ENTITY in XML, loading aborted to prevent XXE/XEE attacks');
}
return $xml;
}
/**
* @param string $string
* @param int<1, max> $split_length
* @param string|null $encoding
*
* @return array<string>|bool|null
*/
public static function mb_str_split(string $string, int $split_length = 1, ?string $encoding = null)
{
if (extension_loaded('mbstring')) {
if (function_exists('mb_str_split')) {
return mb_str_split($string, $split_length, $encoding);
}
}
// @phpstan-ignore-next-line
if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) {
trigger_error('mb_str_split() expects parameter 1 to be string, ' . \gettype($string) . ' given', \E_USER_WARNING);
return null;
}
// @phpstan-ignore-next-line
if (1 > $split_length = (int) $split_length) {
trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING);
return false;
}
if (null === $encoding) {
$encoding = mb_internal_encoding();
}
if ('UTF-8' === $encoding || \in_array(strtoupper($encoding), ['UTF-8', 'UTF8'], true)) {
return preg_split("/(.{{$split_length}})/u", $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
}
$result = [];
$length = mb_strlen($string, $encoding);
for ($i = 0; $i < $length; $i += $split_length) {
$result[] = mb_substr($string, $i, $split_length, $encoding);
}
return $result;
}
}