├── src └── Math │ ├── Element │ ├── Row.php │ ├── AbstractElement.php │ ├── Numeric.php │ ├── Identifier.php │ ├── Operator.php │ ├── Semantics.php │ ├── AbstractGroupElement.php │ ├── Superscript.php │ └── Fraction.php │ ├── Exception │ ├── SecurityException.php │ ├── InvalidInputException.php │ ├── NotImplementedException.php │ └── MathException.php │ ├── Math.php │ ├── Reader │ ├── ReaderInterface.php │ ├── Security │ │ └── XmlScanner.php │ ├── OfficeMathML.php │ └── MathML.php │ └── Writer │ ├── WriterInterface.php │ ├── OfficeMathML.php │ └── MathML.php ├── docs ├── credits.md ├── changes │ ├── 0.4.0.md │ ├── 0.3.0.md │ ├── 0.2.0.md │ └── 0.1.0.md ├── assets │ └── mathjax.js ├── install.md ├── usage │ ├── elements │ │ ├── numeric.md │ │ ├── operator.md │ │ ├── identifier.md │ │ ├── fraction.md │ │ ├── superscript.md │ │ ├── row.md │ │ └── semantics.md │ ├── writers.md │ └── readers.md └── index.md ├── .gitignore ├── roave-bc-check.yaml ├── phpstan.neon.dist ├── .github ├── dependabot.yml └── workflows │ ├── deploy.yml │ └── php.yml ├── tests ├── Math │ ├── Element │ │ ├── NumericTest.php │ │ ├── OperatorTest.php │ │ ├── IdentifierTest.php │ │ ├── SemanticsTest.php │ │ ├── AbstractGroupElementTest.php │ │ ├── SuperscriptTest.php │ │ └── FractionTest.php │ ├── Writer │ │ ├── WriterTestCase.php │ │ ├── OfficeMathMLTest.php │ │ └── MathMLTest.php │ └── Reader │ │ ├── OfficeMathMLTest.php │ │ └── MathMLTest.php └── resources │ └── schema │ └── mathml3 │ ├── mathml3.xsd │ ├── mathml3-common.xsd │ ├── mathml3-strict-content.xsd │ └── mathml3-content.xsd ├── composer.json ├── phpunit.xml.dist ├── LICENSE ├── .php-cs-fixer.dist.php ├── mkdocs.yml └── README.md /src/Math/Element/Row.php: -------------------------------------------------------------------------------- 1 | { 15 | MathJax.typesetPromise() 16 | }) 17 | -------------------------------------------------------------------------------- /src/Math/Element/Numeric.php: -------------------------------------------------------------------------------- 1 | value = $value; 15 | } 16 | 17 | public function getValue(): float 18 | { 19 | return $this->value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Math/Element/Identifier.php: -------------------------------------------------------------------------------- 1 | value = $value; 15 | } 16 | 17 | public function getValue(): string 18 | { 19 | return $this->value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Math/Element/Operator.php: -------------------------------------------------------------------------------- 1 | value = $value; 15 | } 16 | 17 | public function getValue(): string 18 | { 19 | return $this->value; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Math/Element/NumericTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(2, $numeric->getValue()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/Math/Element/OperatorTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('+', $operator->getValue()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/resources/schema/mathml3/mathml3.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/Math/Element/IdentifierTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('x', $operator->getValue()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Requirements 4 | 5 | Mandatory: 6 | 7 | - PHP 7.1+ 8 | - PHP [DOM extension](http://php.net/manual/en/book.dom.php) 9 | - PHP [XML Parser extension](http://www.php.net/manual/en/xml.installation.php) 10 | - PHP [XMLWriter extension](http://php.net/manual/en/book.xmlwriter.php) 11 | 12 | 13 | ## Installation 14 | 15 | ### Using Composer 16 | 17 | To install via [Composer](http://getcomposer.org), add the following lines to your `composer.json`: 18 | 19 | ``` json 20 | { 21 | "require": { 22 | "phpoffice/math": "dev-master" 23 | } 24 | } 25 | ``` -------------------------------------------------------------------------------- /docs/usage/elements/numeric.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | To create a numeric, use the `PhpOffice\Math\Element\Numeric` class. 4 | 5 | ### Methods 6 | #### getValue 7 | 8 | The method has no parameter. 9 | 10 | #### setValue 11 | 12 | The method has one parameter : 13 | 14 | * `float` **$value** 15 | 16 | ## Example 17 | 18 | ### Math 19 | 20 | 3 21 | 22 | 23 | ### XML 24 | ``` xml 25 | 26 | 3 27 | 28 | ``` 29 | 30 | ### PHP 31 | 32 | ``` php 33 | add($identifier); 43 | ``` -------------------------------------------------------------------------------- /docs/usage/elements/operator.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | To create an operator, use the `PhpOffice\Math\Element\Operator` class. 4 | 5 | ### Methods 6 | #### getValue 7 | 8 | The method has no parameter. 9 | 10 | #### setValue 11 | 12 | The method has one parameter : 13 | 14 | * `string` **$value** 15 | 16 | ## Example 17 | 18 | ### Math 19 | 20 | + 21 | 22 | 23 | ### XML 24 | ``` xml 25 | 26 | + 27 | 28 | ``` 29 | 30 | ### PHP 31 | 32 | ``` php 33 | add($identifier); 43 | ``` -------------------------------------------------------------------------------- /docs/usage/elements/identifier.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | To create an identifier, use the `PhpOffice\Math\Element\Identifier` class. 4 | 5 | ### Methods 6 | #### getValue 7 | 8 | The method has no parameter. 9 | 10 | #### setValue 11 | 12 | The method has one parameter : 13 | 14 | * `string` **$value** 15 | 16 | ## Example 17 | 18 | ### Math 19 | 20 | a 21 | 22 | 23 | ### XML 24 | ``` xml 25 | 26 | a 27 | 28 | ``` 29 | 30 | ### PHP 31 | 32 | ``` php 33 | add($identifier); 43 | ``` -------------------------------------------------------------------------------- /src/Math/Element/Semantics.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | protected $annotations = []; 13 | 14 | public function addAnnotation(string $encoding, string $annotation): self 15 | { 16 | $this->annotations[$encoding] = $annotation; 17 | 18 | return $this; 19 | } 20 | 21 | public function getAnnotation(string $encoding): ?string 22 | { 23 | return $this->annotations[$encoding] ?? null; 24 | } 25 | 26 | /** 27 | * @return array 28 | */ 29 | public function getAnnotations(): array 30 | { 31 | return $this->annotations; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Math/Element/AbstractGroupElement.php: -------------------------------------------------------------------------------- 1 | elements[] = $element; 15 | 16 | return $this; 17 | } 18 | 19 | public function remove(AbstractElement $element): self 20 | { 21 | $this->elements = array_filter($this->elements, function ($child) use ($element) { 22 | return $child != $element; 23 | }); 24 | 25 | return $this; 26 | } 27 | 28 | /** 29 | * @return AbstractElement[] 30 | */ 31 | public function getElements(): array 32 | { 33 | return $this->elements; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docs/changes/0.3.0.md: -------------------------------------------------------------------------------- 1 | # 0.3.0 2 | 3 | ## Enhancements 4 | 5 | - Added support for PHP 8.4 by [@Progi1984](https://github/Progi1984) in [#18](https://github.com/PHPOffice/Math/pull/18) 6 | - MathML Writer : Write Semantics by [@codinglist](https://github/codinglist) & [@Progi1984](https://github/Progi1984) in [#19](https://github.com/PHPOffice/Math/pull/19) & [#20](https://github.com/PHPOffice/Math/pull/20) 7 | 8 | ## Bug fixes 9 | 10 | - Documentation : Added Release 0.3.0 by [@Progi1984](https://github/Progi1984) in [#17](https://github.com/PHPOffice/Math/pull/17) 11 | 12 | ## Miscellaneous 13 | 14 | - N/A 15 | 16 | ## Security fixes 17 | 18 | - Fixed XXE when processing an XML file in the MathML format by Aleksandr Zhurnakov (Positive Technologies) & [@Progi1984](https://github/Progi1984) in [GHSA-42hm-pq2f-3r7m](https://github.com/PHPOffice/Math/security/advisories/GHSA-42hm-pq2f-3r7m) -------------------------------------------------------------------------------- /docs/usage/writers.md: -------------------------------------------------------------------------------- 1 | 2 | ## Writers 3 | ### MathML 4 | The name of the writer is `MathML`. 5 | 6 | ``` php 7 | add(new Element\Operator('+')); 15 | 16 | $writer = new MathML(); 17 | $output = $writer->write($math); 18 | ``` 19 | 20 | ### OfficeMathML 21 | 22 | The name of the writer is `OfficeMathML`. 23 | 24 | ``` php 25 | add(new Element\Operator('+')); 33 | 34 | $writer = new OfficeMathML(); 35 | $output = $writer->write($math); 36 | ``` 37 | 38 | ## Methods 39 | 40 | ### writer 41 | 42 | The method has one parameter : 43 | 44 | * `PhpOffice\Math\Math` **$math** 45 | 46 | The method returns a `string`. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpoffice/math", 3 | "description": "Math - Manipulate Math Formula", 4 | "keywords": ["PHP","mathml", "officemathml"], 5 | "homepage": "https://phpoffice.github.io/Math/", 6 | "type": "library", 7 | "license": "MIT", 8 | "autoload": { 9 | "psr-4": { 10 | "PhpOffice\\Math\\": "src/Math/" 11 | } 12 | }, 13 | "autoload-dev": { 14 | "psr-4": { 15 | "Tests\\PhpOffice\\Math\\": "tests/Math/" 16 | } 17 | }, 18 | "authors": [ 19 | { 20 | "name": "Progi1984", 21 | "homepage": "https://lefevre.dev" 22 | } 23 | ], 24 | "require": { 25 | "php": "^7.1|^8.0", 26 | "ext-dom": "*", 27 | "ext-xml": "*" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^7.0 || ^9.0", 31 | "phpstan/phpstan": "^0.12.88 || ^1.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /docs/changes/0.2.0.md: -------------------------------------------------------------------------------- 1 | # 0.2.0 2 | 3 | ## Enhancements 4 | 5 | - N/A 6 | 7 | ## Bug fixes 8 | 9 | - MathML Reader : Support for mrow in mfrac by [@Progi1984](https://github/Progi1984) in [#16](https://github.com/PHPOffice/Math/pull/16) 10 | 11 | ## Miscellaneous 12 | 13 | - Github Action : Roave BC Check by [@Progi1984](https://github/Progi1984) in [#9](https://github.com/PHPOffice/Math/pull/9) 14 | - Bump actions/checkout from 2 to 4 by [@dependabot](https://github/dependabot) in [#10](https://github.com/PHPOffice/Math/pull/10) 15 | - Bump actions/setup-python from 2 to 4 by [@dependabot](https://github/dependabot) in [#11](https://github.com/PHPOffice/Math/pull/11) 16 | - Bump actions/setup-python from 4 to 5 by [@dependabot](https://github/dependabot) in [#12](https://github.com/PHPOffice/Math/pull/12) 17 | - Bump peaceiris/actions-gh-pages from 3 to 4 by [@dependabot](https://github/dependabot) in [#13](https://github.com/PHPOffice/Math/pull/13) -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | ./tests/Math 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | src 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Math/Element/Superscript.php: -------------------------------------------------------------------------------- 1 | setBase($base); 20 | $this->setSuperscript($superscript); 21 | } 22 | 23 | public function getBase(): AbstractElement 24 | { 25 | return $this->base; 26 | } 27 | 28 | public function getSuperscript(): AbstractElement 29 | { 30 | return $this->superscript; 31 | } 32 | 33 | public function setBase(AbstractElement $element): self 34 | { 35 | $this->base = $element; 36 | 37 | return $this; 38 | } 39 | 40 | public function setSuperscript(AbstractElement $element): self 41 | { 42 | $this->superscript = $element; 43 | 44 | return $this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Math/Element/Fraction.php: -------------------------------------------------------------------------------- 1 | setNumerator($numerator); 20 | $this->setDenominator($denominator); 21 | } 22 | 23 | public function getDenominator(): AbstractElement 24 | { 25 | return $this->denominator; 26 | } 27 | 28 | public function getNumerator(): AbstractElement 29 | { 30 | return $this->numerator; 31 | } 32 | 33 | public function setDenominator(AbstractElement $element): self 34 | { 35 | $this->denominator = $element; 36 | 37 | return $this; 38 | } 39 | 40 | public function setNumerator(AbstractElement $element): self 41 | { 42 | $this->numerator = $element; 43 | 44 | return $this; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 PhpSpreadsheet Authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/Math/Element/SemanticsTest.php: -------------------------------------------------------------------------------- 1 | assertIsArray($semantics->getAnnotations()); 17 | $this->assertCount(0, $semantics->getAnnotations()); 18 | } 19 | 20 | public function testAnnotation(): void 21 | { 22 | $semantics = new Semantics(); 23 | 24 | $this->assertIsArray($semantics->getAnnotations()); 25 | $this->assertCount(0, $semantics->getAnnotations()); 26 | 27 | $this->assertInstanceOf(Semantics::class, $semantics->addAnnotation('encoding', 'content')); 28 | $this->assertEquals(['encoding' => 'content'], $semantics->getAnnotations()); 29 | $this->assertCount(1, $semantics->getAnnotations()); 30 | 31 | $this->assertEquals('content', $semantics->getAnnotation('encoding')); 32 | $this->assertNull($semantics->getAnnotation('notexisting')); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/usage/elements/fraction.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | To create a fraction, use the `PhpOffice\Math\Element\Fraction` class. 4 | 5 | ### Methods 6 | #### getDenominator 7 | 8 | The method has no parameter. 9 | 10 | #### getNumerator 11 | 12 | The method has no parameter. 13 | 14 | #### setDenominator 15 | 16 | The method has one parameter : 17 | 18 | * `PhpOffice\Math\Element\AbstractElement` **$element** 19 | 20 | #### setNumerator 21 | 22 | The method has one parameter : 23 | 24 | * `PhpOffice\Math\Element\AbstractElement` **$element** 25 | 26 | ## Example 27 | 28 | ### Math 29 | 30 | 31 | a 32 | 3 33 | 34 | 35 | 36 | ### XML 37 | ``` xml 38 | 39 | 40 | a 41 | 3 42 | 43 | 44 | ``` 45 | 46 | ### PHP 47 | 48 | ``` php 49 | setDenominator(new Element\Identifier('a')); 58 | $fraction->setNumerator(new Element\Numeric(3)); 59 | 60 | $math->add($fraction); 61 | ``` -------------------------------------------------------------------------------- /docs/usage/elements/superscript.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | To attach a superscript to an expression, use the `PhpOffice\Math\Element\Superscript` class. 4 | 5 | ### Methods 6 | #### getBase 7 | 8 | The method has no parameter. 9 | 10 | #### getSuperscript 11 | 12 | The method has no parameter. 13 | 14 | #### setBase 15 | 16 | The method has one parameter : 17 | 18 | * `PhpOffice\Math\Element\AbstractElement` **$element** 19 | 20 | #### setSuperscript 21 | 22 | The method has one parameter : 23 | 24 | * `PhpOffice\Math\Element\AbstractElement` **$element** 25 | 26 | ## Example 27 | 28 | ### Math 29 | 30 | 31 | X 32 | 2 33 | 34 | 35 | 36 | ### XML 37 | ``` xml 38 | 39 | 40 | X 41 | 2 42 | 43 | 44 | ``` 45 | 46 | ### PHP 47 | 48 | ``` php 49 | setBase(new Element\Identifier('X')); 58 | $superscript->setSuperscript(new Element\Numeric(2)); 59 | 60 | $math->add($superscript); 61 | ``` -------------------------------------------------------------------------------- /docs/usage/readers.md: -------------------------------------------------------------------------------- 1 | 2 | ## Readers 3 | ### MathML 4 | The name of the reader is `MathML`. 5 | 6 | ``` php 7 | read( 13 | ' 14 | 15 | 16 | a 17 | ' 18 | ); 19 | ``` 20 | 21 | ### OfficeMathML 22 | The name of the reader is `OfficeMathML`. 23 | 24 | ``` php 25 | read( 31 | ' 32 | 33 | 34 | π 35 | 2 36 | 37 | 38 | ' 39 | ); 40 | ``` 41 | 42 | ## Methods 43 | 44 | ### read 45 | 46 | The method has one parameter : 47 | 48 | * `string` **$content** 49 | 50 | The method returns a `PhpOffice\Math\Math` object. -------------------------------------------------------------------------------- /docs/usage/elements/row.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | To create a row, use the `PhpOffice\Math\Element\Row` class. 4 | 5 | ### Methods 6 | #### add 7 | 8 | The method add an element to the row. 9 | The method has one parameter : 10 | 11 | * `PhpOffice\Math\Element\AbstractElement` **$element** 12 | 13 | #### getElements 14 | 15 | The method return all elements of the row. 16 | 17 | #### remove 18 | 19 | The method remove an element to the row. 20 | The method has one parameter : 21 | 22 | * `PhpOffice\Math\Element\AbstractElement` **$element** 23 | 24 | ## Example 25 | 26 | ### Math 27 | 28 | 29 | 1 30 | + 31 | K 32 | 33 | 34 | 35 | ### XML 36 | ``` xml 37 | 38 | 39 | 1 40 | + 41 | K 42 | 43 | 44 | ``` 45 | 46 | ### PHP 47 | 48 | ``` php 49 | add(new Element\Numeric(1)); 58 | $row->add(new Element\Operator('+')); 59 | $row->add(new Element\Identifier('K')); 60 | 61 | $math->add($row); 62 | ``` -------------------------------------------------------------------------------- /docs/changes/0.1.0.md: -------------------------------------------------------------------------------- 1 | # 0.1.0 2 | 3 | ## Enhancements 4 | 5 | - Initial version by [@Progi1984](https://github/Progi1984) 6 | - MathML Reader : Support for Semantics by [@Progi1984](https://github/Progi1984) in [#4](https://github.com/PHPOffice/Math/pull/4) 7 | - PHPUnit : Improved Unit Tests by [@Progi1984](https://github/Progi1984) in [#8](https://github.com/PHPOffice/Math/pull/8) 8 | 9 | ## Bug fixes 10 | 11 | - N/A 12 | 13 | ## Miscellaneous 14 | - Github Actions : PHPCSFixer by [@Progi1984](https://github/Progi1984) in [#1](https://github.com/PHPOffice/Math/pull/1) 15 | - Github Actions : PHPStan by [@Progi1984](https://github/Progi1984) in [#2](https://github.com/PHPOffice/Math/pull/2) 16 | - Removed dependency friendsofphp/php-cs-fixer by [@Progi1984](https://github/Progi1984) in [#3](https://github.com/PHPOffice/Math/pull/3) 17 | - Github Actions : Dependabot by [@Progi1984](https://github/Progi1984) in [#5](https://github.com/PHPOffice/Math/pull/5) 18 | - Bump actions/checkout from 2 to 4 by [@dependabot](https://github/dependabot) in [#6](https://github.com/PHPOffice/Math/pull/6) 19 | - Added documentation (MkDocs / Coverage / PHPDoc) by [@Progi1984](https://github/Progi1984) in [#7](https://github.com/PHPOffice/Math/pull/7) -------------------------------------------------------------------------------- /tests/Math/Element/AbstractGroupElementTest.php: -------------------------------------------------------------------------------- 1 | assertIsArray($row->getElements()); 17 | $this->assertCount(0, $row->getElements()); 18 | } 19 | 20 | public function testAdd(): void 21 | { 22 | $identifierA = new Element\Identifier('a'); 23 | $row = new Element\Row(); 24 | 25 | $this->assertCount(0, $row->getElements()); 26 | 27 | $this->assertInstanceOf(Element\AbstractGroupElement::class, $row->add($identifierA)); 28 | 29 | $this->assertCount(1, $row->getElements()); 30 | $this->assertEquals([$identifierA], $row->getElements()); 31 | } 32 | 33 | public function testRemove(): void 34 | { 35 | $identifierA = new Element\Identifier('a'); 36 | 37 | $row = new Element\Row(); 38 | $row->add($identifierA); 39 | 40 | $this->assertCount(1, $row->getElements()); 41 | 42 | $this->assertInstanceOf(Element\AbstractGroupElement::class, $row->remove($identifierA)); 43 | 44 | $this->assertCount(0, $row->getElements()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | setUsingCache(true) 7 | ->setRiskyAllowed(true) 8 | ->setRules([ 9 | '@Symfony' => true, 10 | 'array_indentation' => true, 11 | 'cast_spaces' => [ 12 | 'space' => 'single', 13 | ], 14 | 'combine_consecutive_issets' => true, 15 | 'concat_space' => [ 16 | 'spacing' => 'one', 17 | ], 18 | 'error_suppression' => [ 19 | 'mute_deprecation_error' => false, 20 | 'noise_remaining_usages' => false, 21 | 'noise_remaining_usages_exclude' => [], 22 | ], 23 | 'function_to_constant' => false, 24 | 'global_namespace_import' => true, 25 | 'method_chaining_indentation' => true, 26 | 'no_alias_functions' => false, 27 | 'no_superfluous_phpdoc_tags' => false, 28 | 'non_printable_character' => [ 29 | 'use_escape_sequences_in_strings' => true, 30 | ], 31 | 'phpdoc_align' => [ 32 | 'align' => 'left', 33 | ], 34 | 'phpdoc_summary' => false, 35 | 'protected_to_private' => false, 36 | 'self_accessor' => false, 37 | 'yoda_style' => false, 38 | 'single_line_throw' => false, 39 | 'no_alias_language_construct_call' => false, 40 | ]) 41 | ->getFinder() 42 | ->in(__DIR__) 43 | ->exclude('vendor'); 44 | 45 | return $config; -------------------------------------------------------------------------------- /tests/Math/Element/SuperscriptTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Element\Identifier::class, $superscript->getBase()); 18 | $this->assertInstanceOf(Element\Identifier::class, $superscript->getSuperscript()); 19 | } 20 | 21 | public function testBase(): void 22 | { 23 | $identifierA = new Element\Identifier('a'); 24 | $identifierB = new Element\Identifier('b'); 25 | $identifierC = new Element\Identifier('c'); 26 | 27 | $superscript = new Superscript($identifierA, $identifierB); 28 | 29 | $this->assertEquals($identifierA, $superscript->getBase()); 30 | $this->assertInstanceOf(Superscript::class, $superscript->setBase($identifierC)); 31 | $this->assertEquals($identifierC, $superscript->getBase()); 32 | } 33 | 34 | public function testSuperscript(): void 35 | { 36 | $identifierA = new Element\Identifier('a'); 37 | $identifierB = new Element\Identifier('b'); 38 | $identifierC = new Element\Identifier('c'); 39 | 40 | $superscript = new Superscript($identifierA, $identifierB); 41 | 42 | $this->assertEquals($identifierB, $superscript->getSuperscript()); 43 | $this->assertInstanceOf(Superscript::class, $superscript->setSuperscript($identifierC)); 44 | $this->assertEquals($identifierC, $superscript->getSuperscript()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v5 15 | ### MkDocs 16 | - name: Setup Python 17 | uses: actions/setup-python@v6 18 | with: 19 | python-version: 3.x 20 | - name: Install Python Dependencies 21 | run: pip install mkdocs-material autolink-references-mkdocs-plugin 22 | - name: Build documentation 23 | run: mkdocs build --site-dir public 24 | ### PHPUnit 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: 8.1 29 | extensions: dom, xml 30 | coverage: xdebug 31 | - name: Create directory public/coverage 32 | run: mkdir ./public/coverage 33 | - name: Install PHP Dependencies 34 | run: composer install --ansi --prefer-dist --no-interaction --no-progress 35 | - name: Build Coverage Report 36 | run: XDEBUG_MODE=coverage ./vendor/bin/phpunit -c ./ --coverage-text --coverage-html ./public/coverage 37 | ### PHPDoc 38 | - name: Create directory public/docs 39 | run: mkdir ./public/docs 40 | - name: Install PhpDocumentor 41 | run: wget https://phpdoc.org/phpDocumentor.phar && chmod +x phpDocumentor.phar 42 | - name: Build Documentation 43 | run: ./phpDocumentor.phar run -d ./src -t ./public/docs 44 | 45 | ### Deploy 46 | - name: Deploy 47 | uses: peaceiris/actions-gh-pages@v4 48 | if: github.ref == 'refs/heads/master' 49 | with: 50 | github_token: ${{ secrets.GITHUB_TOKEN }} 51 | publish_dir: ./public -------------------------------------------------------------------------------- /tests/Math/Element/FractionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($identifierA, $fraction->getNumerator()); 21 | $this->assertEquals($identifierB, $fraction->getDenominator()); 22 | } 23 | 24 | public function testBase(): void 25 | { 26 | $identifierA = new Element\Identifier('a'); 27 | $identifierB = new Element\Identifier('b'); 28 | $identifierC = new Element\Identifier('c'); 29 | 30 | $fraction = new Fraction($identifierA, $identifierB); 31 | 32 | $this->assertEquals($identifierA, $fraction->getNumerator()); 33 | $this->assertInstanceOf(Fraction::class, $fraction->setNumerator($identifierC)); 34 | $this->assertEquals($identifierC, $fraction->getNumerator()); 35 | } 36 | 37 | public function testFraction(): void 38 | { 39 | $identifierA = new Element\Identifier('a'); 40 | $identifierB = new Element\Identifier('b'); 41 | $identifierC = new Element\Identifier('c'); 42 | 43 | $fraction = new Fraction($identifierA, $identifierB); 44 | 45 | $this->assertEquals($identifierB, $fraction->getDenominator()); 46 | $this->assertInstanceOf(Fraction::class, $fraction->setDenominator($identifierC)); 47 | $this->assertEquals($identifierC, $fraction->getDenominator()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/usage/elements/semantics.md: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | To create a semantics, use the `PhpOffice\Math\Element\Semantics` class. 4 | 5 | ### Methods 6 | #### add 7 | 8 | The method add an element to the `semantics` element. 9 | The method has one parameter : 10 | 11 | * `PhpOffice\Math\Element\AbstractElement` **$element** 12 | 13 | #### addAnnotation 14 | 15 | The method add an annotation to the `semantics` element. 16 | The method has two parameters : 17 | 18 | * `string` **$encoding** 19 | * `string` **$annotation** 20 | 21 | #### getAnnotation 22 | 23 | The method return an annotation based on its encoding. 24 | The method has one parameter : 25 | 26 | * `string` **$encoding** 27 | 28 | #### getAnnotations 29 | 30 | The method return alls annotation of the `semantics` element. 31 | The method has no parameter. 32 | 33 | #### getElements 34 | 35 | The method return all elements of the `semantics` element. 36 | 37 | #### remove 38 | 39 | The method remove an element to the `semantics` element. 40 | The method has one parameter : 41 | 42 | * `PhpOffice\Math\Element\AbstractElement` **$element** 43 | 44 | ## Example 45 | 46 | ### Math 47 | 48 | 49 | y 50 | 51 | y 52 | 53 | 54 | 55 | ### XML 56 | ``` xml 57 | 58 | 59 | y 60 | 61 | y 62 | 63 | 64 | ``` 65 | 66 | ### PHP 67 | 68 | ``` php 69 | add(new Element\Identifier('y')); 78 | $semantics->addAnnotation('application/x-tex', ' y '); 79 | 80 | $math->add($semantics); 81 | ``` -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Math 2 | site_url: https://phpoffice.github.io/Math 3 | repo_url: https://github.com/PHPOffice/Math 4 | repo_name: PHPOffice/Math 5 | edit_uri: edit/master/docs/ 6 | 7 | ## Theme 8 | theme: 9 | name: material 10 | palette: 11 | primary: grey 12 | features: 13 | - search.highlight 14 | - search.suggest 15 | 16 | ## Plugins 17 | plugins: 18 | - search 19 | 20 | ## Config 21 | extra: 22 | generator: false 23 | extra_javascript: 24 | - assets/mathjax.js 25 | - https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?version=3.111.0&features=es6 26 | - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js 27 | markdown_extensions: 28 | ## Syntax highlighting 29 | - pymdownx.highlight 30 | - pymdownx.superfences 31 | ## Support for Math 32 | - pymdownx.arithmatex: 33 | generic: true 34 | ## Support for emojis 35 | - pymdownx.emoji: 36 | emoji_index: !!python/name:materialx.emoji.twemoji 37 | emoji_generator: !!python/name:materialx.emoji.to_svg 38 | ## Support for call-outs 39 | - admonition 40 | - pymdownx.details 41 | use_directory_urls: false 42 | 43 | ## Navigation 44 | nav: 45 | - Introduction: 'index.md' 46 | - Install: 'install.md' 47 | - Usage: 48 | - Elements: 49 | - Fraction: 'usage/elements/fraction.md' 50 | - Identifier: 'usage/elements/identifier.md' 51 | - Numeric: 'usage/elements/numeric.md' 52 | - Operator: 'usage/elements/operator.md' 53 | - Row: 'usage/elements/row.md' 54 | - Semantics: 'usage/elements/semantics.md' 55 | - Superscript: 'usage/elements/superscript.md' 56 | - Readers: 'usage/readers.md' 57 | - Writers: 'usage/writers.md' 58 | - Credits: 'credits.md' 59 | - Releases: 60 | - '0.3.0 (WIP)': 'changes/0.3.0.md' 61 | - '0.2.0': 'changes/0.2.0.md' 62 | - '0.1.0': 'changes/0.1.0.md' 63 | - Developers: 64 | - 'Coveralls': 'https://coveralls.io/github/PHPOffice/Math' 65 | - 'Code Coverage': 'coverage/index.html' 66 | - 'PHPDoc': 'docs/index.html' 67 | -------------------------------------------------------------------------------- /tests/Math/Writer/WriterTestCase.php: -------------------------------------------------------------------------------- 1 | loadXML($content); 17 | $xmlSource = $dom->saveXML(); 18 | 19 | if (is_string($xmlSource)) { 20 | $dom->loadXML($xmlSource); 21 | $dom->schemaValidate(dirname(__DIR__, 2) . '/resources/schema/mathml3/mathml3.xsd'); 22 | 23 | $error = libxml_get_last_error(); 24 | if ($error instanceof LibXMLError) { 25 | $this->failXmlError($error, $xmlSource); 26 | } else { 27 | $this->assertTrue(true); 28 | } 29 | 30 | return; 31 | } 32 | 33 | $this->fail(sprintf('The XML is not valid : %s', $content)); 34 | } 35 | 36 | /** 37 | * @param array $params 38 | */ 39 | protected function failXmlError(LibXMLError $error, string $source, array $params = []): void 40 | { 41 | switch ($error->level) { 42 | case LIBXML_ERR_WARNING: 43 | $errorType = 'warning'; 44 | break; 45 | case LIBXML_ERR_ERROR: 46 | $errorType = 'error'; 47 | break; 48 | case LIBXML_ERR_FATAL: 49 | $errorType = 'fatal'; 50 | break; 51 | default: 52 | $errorType = 'Error'; 53 | break; 54 | } 55 | $errorLine = (int) $error->line; 56 | $contents = explode("\n", $source); 57 | $lines = []; 58 | if (isset($contents[$errorLine - 2])) { 59 | $lines[] = '>> ' . $contents[$errorLine - 2]; 60 | } 61 | if (isset($contents[$errorLine - 1])) { 62 | $lines[] = '>>> ' . $contents[$errorLine - 1]; 63 | } 64 | if (isset($contents[$errorLine])) { 65 | $lines[] = '>> ' . $contents[$errorLine]; 66 | } 67 | $paramStr = ''; 68 | if (!empty($params)) { 69 | $paramStr .= "\n" . ' - Parameters :' . "\n"; 70 | foreach ($params as $key => $val) { 71 | $paramStr .= ' - ' . $key . ' : ' . $val . "\n"; 72 | } 73 | } 74 | $this->fail(sprintf( 75 | "Validation %s :\n - - Line : %s\n - Message : %s - Lines :\n%s%s", 76 | $errorType, 77 | $error->line, 78 | $error->message, 79 | implode(PHP_EOL, $lines), 80 | $paramStr 81 | )); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Math/Writer/OfficeMathMLTest.php: -------------------------------------------------------------------------------- 1 | add($fraction); 23 | 24 | $writer = new OfficeMathML(); 25 | $output = $writer->write($math); 26 | 27 | $expected = '' 28 | . '' 29 | . '' 30 | . 'π' 31 | . '2' 32 | . '' 33 | . '' 34 | . ''; 35 | $this->assertEquals($expected, $output); 36 | } 37 | 38 | public function testWriteRow(): void 39 | { 40 | $math = new Math(); 41 | 42 | $row = new Element\Row(); 43 | $math->add($row); 44 | 45 | $row->add(new Element\Identifier('x')); 46 | 47 | $writer = new OfficeMathML(); 48 | $output = $writer->write($math); 49 | 50 | $expected = '' 51 | . '' 52 | . 'x' 53 | . '' 54 | . ''; 55 | $this->assertEquals($expected, $output); 56 | } 57 | 58 | public function testWriteNotImplemented(): void 59 | { 60 | $this->expectException(NotImplementedException::class); 61 | if (method_exists($this, 'expectExceptionMessageRegExp')) { 62 | $this->expectExceptionMessageRegExp('/PhpOffice\\\Math\\\Writer\\\OfficeMathML::getElementTagName : The element of the class/'); 63 | $this->expectExceptionMessageRegExp('/has no tag name/'); 64 | } else { 65 | // @phpstan-ignore-next-line 66 | $this->expectExceptionMessageMatches('/PhpOffice\\\Math\\\Writer\\\OfficeMathML::getElementTagName : The element of the class/'); 67 | // @phpstan-ignore-next-line 68 | $this->expectExceptionMessageMatches('/has no tag name/'); 69 | } 70 | 71 | $math = new Math(); 72 | 73 | $object = new class extends Element\AbstractElement {}; 74 | $math->add($object); 75 | 76 | $writer = new OfficeMathML(); 77 | $output = $writer->write($math); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Math/Reader/Security/XmlScanner.php: -------------------------------------------------------------------------------- 1 | $split_length 35 | * @param string|null $encoding 36 | * 37 | * @return array|bool|null 38 | */ 39 | public static function mb_str_split(string $string, int $split_length = 1, ?string $encoding = null) 40 | { 41 | if (extension_loaded('mbstring')) { 42 | if (function_exists('mb_str_split')) { 43 | return mb_str_split($string, $split_length, $encoding); 44 | } 45 | } 46 | // @phpstan-ignore-next-line 47 | if (null !== $string && !\is_scalar($string) && !(\is_object($string) && method_exists($string, '__toString'))) { 48 | trigger_error('mb_str_split() expects parameter 1 to be string, ' . \gettype($string) . ' given', \E_USER_WARNING); 49 | 50 | return null; 51 | } 52 | 53 | // @phpstan-ignore-next-line 54 | if (1 > $split_length = (int) $split_length) { 55 | trigger_error('The length of each segment must be greater than zero', \E_USER_WARNING); 56 | 57 | return false; 58 | } 59 | 60 | if (null === $encoding) { 61 | $encoding = mb_internal_encoding(); 62 | } 63 | 64 | if ('UTF-8' === $encoding || \in_array(strtoupper($encoding), ['UTF-8', 'UTF8'], true)) { 65 | return preg_split("/(.{{$split_length}})/u", $string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY); 66 | } 67 | 68 | $result = []; 69 | $length = mb_strlen($string, $encoding); 70 | 71 | for ($i = 0; $i < $length; $i += $split_length) { 72 | $result[] = mb_substr($string, $i, $split_length, $encoding); 73 | } 74 | 75 | return $result; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Math/Writer/OfficeMathML.php: -------------------------------------------------------------------------------- 1 | output = new XMLWriter(); 23 | $this->output->openMemory(); 24 | $this->output->startElement('m:oMathPara'); 25 | $this->output->writeAttribute('xmlns:m', 'http://schemas.openxmlformats.org/officeDocument/2006/math'); 26 | $this->output->startElement('m:oMath'); 27 | 28 | foreach ($math->getElements() as $element) { 29 | $this->writeElementItem($element); 30 | } 31 | 32 | $this->output->endElement(); 33 | $this->output->endElement(); 34 | 35 | return $this->output->outputMemory(); 36 | } 37 | 38 | protected function writeElementItem(Element\AbstractElement $element): void 39 | { 40 | // Element\Row 41 | if ($element instanceof Element\Row) { 42 | foreach ($element->getElements() as $childElement) { 43 | $this->writeElementItem($childElement); 44 | } 45 | 46 | return; 47 | } 48 | 49 | // Element\Fraction 50 | if ($element instanceof Element\Fraction) { 51 | $this->output->startElement($this->getElementTagName($element)); 52 | $this->output->startElement('m:num'); 53 | $this->writeElementItem($element->getNumerator()); 54 | $this->output->endElement(); 55 | $this->output->startElement('m:den'); 56 | $this->writeElementItem($element->getDenominator()); 57 | $this->output->endElement(); 58 | $this->output->endElement(); 59 | 60 | return; 61 | } 62 | 63 | if ($element instanceof Element\Identifier 64 | || $element instanceof Element\Numeric 65 | || $element instanceof Element\Operator) { 66 | $this->output->startElement('m:r'); 67 | $this->output->startElement('m:t'); 68 | $this->output->text((string) $element->getValue()); 69 | $this->output->endElement(); 70 | $this->output->endElement(); 71 | 72 | return; 73 | } 74 | 75 | // Check if managed 76 | $this->getElementTagName($element); 77 | } 78 | 79 | protected function getElementTagName(Element\AbstractElement $element): string 80 | { 81 | // Group 82 | if ($element instanceof Element\AbstractGroupElement) { 83 | /* 84 | throw new NotImplementedException(sprintf( 85 | '%s : The element of the class `%s` has no tag name', 86 | __METHOD__, 87 | get_class($element) 88 | )); 89 | */ 90 | } 91 | 92 | if ($element instanceof Element\Fraction) { 93 | return 'm:f'; 94 | } 95 | 96 | throw new NotImplementedException(sprintf( 97 | '%s : The element of the class `%s` has no tag name', 98 | __METHOD__, 99 | get_class($element) 100 | )); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Math 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/phpoffice/math/v/stable.png)](https://packagist.org/packages/phpoffice/math) 4 | [![Coverage Status](https://coveralls.io/repos/github/PHPOffice/Math/badge.svg?branch=master)](https://coveralls.io/github/PHPOffice/Math?branch=master) 5 | [![Total Downloads](https://poser.pugx.org/phpoffice/math/downloads.png)](https://packagist.org/packages/phpoffice/math) 6 | [![License](https://poser.pugx.org/phpoffice/math/license.png)](https://packagist.org/packages/phpoffice/math) 7 | [![CI](https://github.com/PHPOffice/Math/actions/workflows/php.yml/badge.svg)](https://github.com/PHPOffice/Math/actions/workflows/php.yml) 8 | 9 | Math is a library written in pure PHP that provides a set of classes to manipulate different formula file formats, i.e. [MathML](https://en.wikipedia.org/wiki/MathML) and [Office MathML (OOML)](https://en.wikipedia.org/wiki/Office_Open_XML_file_formats#Office_MathML_(OMML)). 10 | 11 | Math is an open source project licensed under the terms of [MIT](https://github.com/PHPOffice/Math/blob/master/LICENCE). Math is aimed to be a high quality software product by incorporating [continuous integration and unit testing](https://github.com/PHPOffice/Math/actions/workflows/php.yml). You can learn more about Math by reading this Developers'Documentation and the [API Documentation](http://phpoffice.github.io/Math/docs/) 12 | 13 | If you have any questions, please ask on [StackOverFlow](https://stackoverflow.com/questions/tagged/phpoffice-math) 14 | 15 | Read more about Math: 16 | 17 | - [Features](#features) 18 | - [Requirements](#requirements) 19 | - [Installation](#installation) 20 | - [Contributing](#contributing) 21 | - [Developers' Documentation](https://phpoffice.github.io/Math/) 22 | 23 | ## Features 24 | 25 | - Insert elements: 26 | 27 | * Basic : 28 | 29 | * Identifier : a 30 | * Operator : + 31 | * Numeric : 2 32 | 33 | * Simple : 34 | 35 | * Fraction : a3 36 | * Superscript : a3 37 | 38 | * Architectural : 39 | 40 | * Row 41 | * Semantics 42 | 43 | - Support 44 | 45 | * MathML 46 | * OfficeMathML 47 | ## Requirements 48 | 49 | Math requires the following: 50 | 51 | - PHP 7.1+ 52 | - [XML Parser extension](http://www.php.net/manual/en/xml.installation.php) 53 | - [XMLWriter extension](http://php.net/manual/en/book.xmlwriter.php) 54 | 55 | ## Installation 56 | 57 | Math is installed via [Composer](https://getcomposer.org/). 58 | To [add a dependency](https://getcomposer.org/doc/04-schema.md#package-links) to Math in your project, either 59 | 60 | Run the following to use the latest stable version 61 | ```sh 62 | composer require phpoffice/math 63 | ``` 64 | or if you want the latest unreleased version 65 | ```sh 66 | composer require phpoffice/math:dev-master 67 | ``` 68 | 69 | ## Contributing 70 | 71 | We welcome everyone to contribute to PHPWord. Below are some of the things that you can do to contribute. 72 | 73 | - [Fork us](https://github.com/PHPOffice/Math/fork) and [request a pull](https://github.com/PHPOffice/Math/pulls) to the [master](https://github.com/PHPOffice/Math/tree/master) branch. 74 | - Submit [bug reports or feature requests](https://github.com/PHPOffice/Math/issues) to GitHub. 75 | - Follow [@PHPOffice](https://twitter.com/PHPOffice) on Twitter. 76 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | Math is a library written in pure PHP that provides a set of classes to manipulate different formula file formats, i.e. [MathML](https://en.wikipedia.org/wiki/MathML) and [Office MathML (OOML)](https://en.wikipedia.org/wiki/Office_Open_XML_file_formats#Office_MathML_(OMML)). 4 | 5 | Math is an open source project licensed under the terms of [MIT](https://github.com/PHPOffice/Math/blob/master/LICENCE). Math is aimed to be a high quality software product by incorporating [continuous integration and unit testing](https://github.com/PHPOffice/Math/actions/workflows/php.yml). You can learn more about Math by reading this Developers'Documentation and the [API Documentation](http://phpoffice.github.io/Math/docs/develop/) 6 | 7 | ## Features 8 | 9 | - Insert elements: 10 | 11 | * Basic : 12 | 13 | * Identifier : a 14 | * Operator : + 15 | * Numeric : 2 16 | 17 | * Simple : 18 | 19 | * Fraction : a3 20 | * Superscript : a3 21 | 22 | * Architectural : 23 | 24 | * Row 25 | * Semantics 26 | 27 | ## Support 28 | 29 | ### Readers 30 | 31 | | Features | | MathML | Office MathML | 32 | |---------------------------|----------------------|:----------------:|:----------------:| 33 | | **Basic** | Identifier | :material-check: | :material-check: | 34 | | | Operator | :material-check: | :material-check: | 35 | | | Numeric | :material-check: | :material-check: | 36 | | **Simple** | Fraction | :material-check: | :material-check: | 37 | | | Superscript | :material-check: | | 38 | | **Architectural** | Row | :material-check: | | 39 | | | Semantics | :material-check: | | 40 | 41 | ### Writers 42 | 43 | | Features | | MathML | Office MathML | 44 | |---------------------------|----------------------|:----------------:|:----------------:| 45 | | **Basic** | Identifier | :material-check: | :material-check: | 46 | | | Operator | :material-check: | :material-check: | 47 | | | Numeric | :material-check: | :material-check: | 48 | | **Simple** | Fraction | :material-check: | :material-check: | 49 | | | Superscript | :material-check: | | 50 | | **Architectural** | Row | :material-check: | :material-check: | 51 | | | Semantics | :material-check: | | 52 | 53 | ## Contributing 54 | 55 | We welcome everyone to contribute to Math. Below are some of the things that you can do to contribute: 56 | 57 | - [Fork us](https://github.com/PHPOffice/Math/fork) and [request a pull](https://github.com/PHPOffice/Math/pulls) to the [master](https://github.com/PHPOffice/Math/tree/master) branch 58 | - Submit [bug reports or feature requests](https://github.com/PHPOffice/Math/issues) to GitHub 59 | - Follow [@PHPOffice](https://twitter.com/PHPOffice) on Twitter 60 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | jobs: 9 | php-cs-fixer: 10 | name: PHP CS Fixer 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Setup PHP 14 | uses: shivammathur/setup-php@v2 15 | with: 16 | php-version: '7.4' 17 | extensions: xml 18 | 19 | - uses: actions/checkout@v5 20 | 21 | - name: Validate composer config 22 | run: composer validate --strict 23 | 24 | - name: Composer Install 25 | run: composer global require friendsofphp/php-cs-fixer 26 | 27 | - name: Add environment path 28 | run: export PATH="$PATH:$HOME/.composer/vendor/bin" 29 | 30 | - name: Run PHPCSFixer 31 | run: php-cs-fixer fix --dry-run --diff 32 | 33 | phpstan: 34 | name: PHP Static Analysis 35 | runs-on: ubuntu-latest 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | php: 40 | - '7.1' 41 | - '7.2' 42 | - '7.3' 43 | - '7.4' 44 | - '8.0' 45 | - '8.1' 46 | - '8.2' 47 | - '8.3' 48 | - '8.4' 49 | steps: 50 | - name: Setup PHP 51 | uses: shivammathur/setup-php@v2 52 | with: 53 | php-version: ${{ matrix.php }} 54 | extensions: xml 55 | 56 | - uses: actions/checkout@v5 57 | 58 | - name: Composer Install 59 | run: composer install --ansi --prefer-dist --no-interaction --no-progress 60 | 61 | - name: Run phpstan 62 | run: ./vendor/bin/phpstan analyse -c phpstan.neon.dist 63 | 64 | phpunit: 65 | name: PHPUnit 66 | runs-on: ubuntu-latest 67 | strategy: 68 | fail-fast: false 69 | matrix: 70 | php: 71 | - '7.1' 72 | - '7.2' 73 | - '7.3' 74 | - '7.4' 75 | - '8.0' 76 | - '8.1' 77 | - '8.2' 78 | - '8.3' 79 | - '8.4' 80 | steps: 81 | - name: Setup PHP 82 | uses: shivammathur/setup-php@v2 83 | with: 84 | php-version: ${{ matrix.php }} 85 | extensions: xml 86 | coverage: ${{ (matrix.php == '8.1') && 'xdebug' || 'none' }} 87 | 88 | - uses: actions/checkout@v5 89 | 90 | - name: Install dependencies 91 | run: composer install --ansi --prefer-dist --no-interaction --no-progress 92 | 93 | - name: Run PHPUnit 94 | if: matrix.php != '8.1' 95 | run: ./vendor/bin/phpunit -c phpunit.xml.dist 96 | 97 | - name: Run PHPUnit (w CodeCoverage) 98 | if: matrix.php == '8.1' 99 | run: XDEBUG_MODE=coverage ./vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover build/clover.xml 100 | 101 | - name: Upload coverage results to Coveralls 102 | if: matrix.php == '8.1' 103 | env: 104 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 105 | run: | 106 | wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar 107 | chmod +x php-coveralls.phar 108 | php php-coveralls.phar --coverage_clover=build/clover.xml --json_path=build/coveralls-upload.json -vvv 109 | 110 | roave-backwards-compatibility-check: 111 | name: Roave Backwards Compatibility Check 112 | runs-on: ubuntu-latest 113 | steps: 114 | - uses: actions/checkout@v5 115 | with: 116 | fetch-depth: 0 117 | - name: "Check for BC breaks" 118 | run: docker run -u $(id -u) -v $(pwd):/app nyholm/roave-bc-check-ga 119 | -------------------------------------------------------------------------------- /tests/resources/schema/mathml3/mathml3-common.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /tests/Math/Writer/MathMLTest.php: -------------------------------------------------------------------------------- 1 | add($row); 22 | 23 | $row->add(new Element\Identifier('a')); 24 | $row->add(clone $opTimes); 25 | 26 | $superscript = new Element\Superscript( 27 | new Element\Identifier('x'), 28 | new Element\Numeric(2) 29 | ); 30 | $row->add($superscript); 31 | 32 | $row->add(new Element\Operator('+')); 33 | 34 | $row->add(new Element\Identifier('b')); 35 | $row->add(clone $opTimes); 36 | $row->add(new Element\Identifier('x')); 37 | 38 | $row->add(new Element\Operator('+')); 39 | 40 | $row->add(new Element\Identifier('c')); 41 | 42 | $writer = new MathML(); 43 | $output = $writer->write($math); 44 | 45 | $expected = '' 46 | . PHP_EOL 47 | . '' 48 | . '' 49 | . 'a&InvisibleTimes;x2+b&InvisibleTimes;x+c' 50 | . '' 51 | . '' 52 | . PHP_EOL; 53 | $this->assertEquals($expected, $output); 54 | $this->assertIsSchemaMathMLValid($output); 55 | } 56 | 57 | public function testWriteFraction(): void 58 | { 59 | $math = new Math(); 60 | 61 | $fraction = new Element\Fraction( 62 | new Element\Identifier('π'), 63 | new Element\Numeric(2) 64 | ); 65 | $math->add($fraction); 66 | 67 | $writer = new MathML(); 68 | $output = $writer->write($math); 69 | 70 | $expected = '' 71 | . PHP_EOL 72 | . '' 73 | . '' 74 | . '' 75 | . 'π2' 76 | . '' 77 | . '' 78 | . PHP_EOL; 79 | $this->assertEquals($expected, $output); 80 | $this->assertIsSchemaMathMLValid($output); 81 | } 82 | 83 | public function testWriteSemantics(): void 84 | { 85 | $opTimes = new Element\Operator('⁢'); 86 | 87 | $math = new Math(); 88 | 89 | $semantics = new Element\Semantics(); 90 | $semantics->add(new Element\Identifier('y')); 91 | $semantics->addAnnotation('application/x-tex', ' y '); 92 | 93 | $math->add($semantics); 94 | 95 | $writer = new MathML(); 96 | $output = $writer->write($math); 97 | 98 | $expected = '' 99 | . PHP_EOL 100 | . '' 101 | . '' 102 | . '' 103 | . 'y' 104 | . ' y ' 105 | . '' 106 | . '' 107 | . PHP_EOL; 108 | $this->assertEquals($expected, $output); 109 | $this->assertIsSchemaMathMLValid($output); 110 | } 111 | 112 | public function testWriteNotImplemented(): void 113 | { 114 | $this->expectException(NotImplementedException::class); 115 | if (method_exists($this, 'expectExceptionMessageRegExp')) { 116 | $this->expectExceptionMessageRegExp('/PhpOffice\\\Math\\\Writer\\\MathML::getElementTagName : The element of the class/'); 117 | $this->expectExceptionMessageRegExp('/has no tag name/'); 118 | } else { 119 | // @phpstan-ignore-next-line 120 | $this->expectExceptionMessageMatches('/PhpOffice\\\Math\\\Writer\\\MathML::getElementTagName : The element of the class/'); 121 | // @phpstan-ignore-next-line 122 | $this->expectExceptionMessageMatches('/has no tag name/'); 123 | } 124 | 125 | $math = new Math(); 126 | 127 | $object = new class extends Element\AbstractElement {}; 128 | $math->add($object); 129 | 130 | $writer = new MathML(); 131 | $output = $writer->write($math); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Math/Reader/OfficeMathML.php: -------------------------------------------------------------------------------- 1 | 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 | } 134 | -------------------------------------------------------------------------------- /src/Math/Reader/MathML.php: -------------------------------------------------------------------------------- 1 | xmlScanner = XmlScanner::getInstance(); 32 | } 33 | 34 | public function read(string $content): ?Math 35 | { 36 | $content = $this->xmlScanner->scan($content); 37 | $content = str_replace( 38 | [ 39 | '⁢', 40 | ], 41 | [ 42 | '', 43 | ], 44 | $content 45 | ); 46 | 47 | $this->dom = new DOMDocument(); 48 | $this->dom->loadXML($content); 49 | 50 | $this->math = new Math(); 51 | $this->parseNode(null, $this->math); 52 | 53 | return $this->math; 54 | } 55 | 56 | /** 57 | * @param Math|Element\AbstractGroupElement $parent 58 | */ 59 | protected function parseNode(?DOMNode $nodeRowElement, $parent): void 60 | { 61 | $this->xpath = new DOMXPath($this->dom); 62 | foreach ($this->xpath->query('*', $nodeRowElement) ?: [] as $nodeElement) { 63 | if ($parent instanceof Element\Semantics 64 | && $nodeElement instanceof DOMElement 65 | && $nodeElement->nodeName == 'annotation') { 66 | $parent->addAnnotation( 67 | $nodeElement->getAttribute('encoding'), 68 | trim($nodeElement->nodeValue) 69 | ); 70 | 71 | continue; 72 | } 73 | 74 | $parent->add($this->getElement($nodeElement)); 75 | } 76 | } 77 | 78 | protected function getElement(DOMNode $nodeElement): Element\AbstractElement 79 | { 80 | $nodeValue = trim($nodeElement->nodeValue); 81 | switch ($nodeElement->nodeName) { 82 | case 'mfrac': 83 | $nodeList = $this->xpath->query('*', $nodeElement); 84 | if ($nodeList && $nodeList->length == 2) { 85 | return new Element\Fraction( 86 | $this->getElement($nodeList->item(0)), 87 | $this->getElement($nodeList->item(1)) 88 | ); 89 | } 90 | 91 | throw new InvalidInputException(sprintf( 92 | '%s : The tag `%s` has not two subelements', 93 | __METHOD__, 94 | $nodeElement->nodeName 95 | )); 96 | case 'mi': 97 | return new Element\Identifier($nodeValue); 98 | case 'mn': 99 | return new Element\Numeric(floatval($nodeValue)); 100 | case 'mo': 101 | if (empty($nodeValue)) { 102 | $nodeList = $this->xpath->query('*', $nodeElement); 103 | if ( 104 | $nodeList 105 | && $nodeList->length == 1 106 | && $nodeList->item(0)->nodeName == 'mchar' 107 | && $nodeList->item(0) instanceof DOMElement 108 | && $nodeList->item(0)->hasAttribute('name') 109 | ) { 110 | $nodeValue = $nodeList->item(0)->getAttribute('name'); 111 | } 112 | } 113 | 114 | return new Element\Operator($nodeValue); 115 | case 'mrow': 116 | $mrow = new Element\Row(); 117 | 118 | $this->parseNode($nodeElement, $mrow); 119 | 120 | return $mrow; 121 | case 'msup': 122 | $nodeList = $this->xpath->query('*', $nodeElement); 123 | if ($nodeList && $nodeList->length == 2) { 124 | return new Element\Superscript( 125 | $this->getElement($nodeList->item(0)), 126 | $this->getElement($nodeList->item(1)) 127 | ); 128 | } 129 | 130 | throw new InvalidInputException(sprintf( 131 | '%s : The tag `%s` has not two subelements', 132 | __METHOD__, 133 | $nodeElement->nodeName 134 | )); 135 | case 'semantics': 136 | $semantics = new Element\Semantics(); 137 | 138 | $this->parseNode($nodeElement, $semantics); 139 | 140 | return $semantics; 141 | default: 142 | throw new NotImplementedException(sprintf( 143 | '%s : The tag `%s` is not implemented', 144 | __METHOD__, 145 | $nodeElement->nodeName 146 | )); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Math/Writer/MathML.php: -------------------------------------------------------------------------------- 1 | output = new XMLWriter(); 23 | $this->output->openMemory(); 24 | $this->output->startDocument('1.0', 'UTF-8'); 25 | $this->output->writeDtd('math', '-//W3C//DTD MathML 2.0//EN', 'http://www.w3.org/Math/DTD/mathml2/mathml2.dtd'); 26 | $this->output->startElement('math'); 27 | $this->output->writeAttribute('xmlns', 'http://www.w3.org/1998/Math/MathML'); 28 | 29 | foreach ($math->getElements() as $element) { 30 | $this->writeElementItem($element); 31 | } 32 | 33 | $this->output->endElement(); 34 | $this->output->endDocument(); 35 | 36 | return $this->output->outputMemory(); 37 | } 38 | 39 | protected function writeElementItem(Element\AbstractElement $element): void 40 | { 41 | $tagName = $this->getElementTagName($element); 42 | 43 | // Element\AbstractGroupElement 44 | if ($element instanceof Element\Semantics) { 45 | $this->output->startElement($tagName); 46 | // Write elements 47 | foreach ($element->getElements() as $childElement) { 48 | $this->writeElementItem($childElement); 49 | } 50 | 51 | // Write annotations 52 | foreach ($element->getAnnotations() as $encoding => $annotation) { 53 | $this->output->startElement('annotation'); 54 | $this->output->writeAttribute('encoding', $encoding); 55 | $this->output->text($annotation); 56 | $this->output->endElement(); 57 | } 58 | $this->output->endElement(); 59 | 60 | return; 61 | } 62 | 63 | // Element\AbstractGroupElement 64 | if ($element instanceof Element\AbstractGroupElement) { 65 | $this->output->startElement($tagName); 66 | foreach ($element->getElements() as $childElement) { 67 | $this->writeElementItem($childElement); 68 | } 69 | $this->output->endElement(); 70 | 71 | return; 72 | } 73 | 74 | // Element\Superscript 75 | if ($element instanceof Element\Superscript) { 76 | $this->output->startElement($tagName); 77 | $this->writeElementItem($element->getBase()); 78 | $this->writeElementItem($element->getSuperscript()); 79 | $this->output->endElement(); 80 | 81 | return; 82 | } 83 | 84 | // Element\Fraction 85 | if ($element instanceof Element\Fraction) { 86 | $this->output->startElement($tagName); 87 | $this->writeElementItem($element->getNumerator()); 88 | $this->writeElementItem($element->getDenominator()); 89 | $this->output->endElement(); 90 | 91 | return; 92 | } 93 | 94 | if ($element instanceof Element\Identifier 95 | || $element instanceof Element\Numeric 96 | || $element instanceof Element\Operator) { 97 | $this->output->startElement($tagName); 98 | $this->output->text((string) $element->getValue()); 99 | $this->output->endElement(); 100 | 101 | return; 102 | } 103 | 104 | /* 105 | throw new NotImplementedException(sprintf( 106 | '%s : The class `%s` is not implemented', 107 | __METHOD__, 108 | get_class($element) 109 | )); 110 | */ 111 | } 112 | 113 | protected function getElementTagName(Element\AbstractElement $element): string 114 | { 115 | // Group 116 | if ($element instanceof Element\Row) { 117 | return 'mrow'; 118 | } 119 | if ($element instanceof Element\AbstractGroupElement) { 120 | /* 121 | throw new NotImplementedException(sprintf( 122 | '%s : The element of the class `%s` has no tag name', 123 | __METHOD__, 124 | get_class($element) 125 | )); 126 | */ 127 | } 128 | 129 | if ($element instanceof Element\Superscript) { 130 | return 'msup'; 131 | } 132 | if ($element instanceof Element\Fraction) { 133 | return 'mfrac'; 134 | } 135 | if ($element instanceof Element\Identifier) { 136 | return 'mi'; 137 | } 138 | if ($element instanceof Element\Numeric) { 139 | return 'mn'; 140 | } 141 | if ($element instanceof Element\Operator) { 142 | return 'mo'; 143 | } 144 | if ($element instanceof Element\Semantics) { 145 | return 'semantics'; 146 | } 147 | 148 | throw new NotImplementedException(sprintf( 149 | '%s : The element of the class `%s` has no tag name', 150 | __METHOD__, 151 | get_class($element) 152 | )); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /tests/resources/schema/mathml3/mathml3-strict-content.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /tests/Math/Reader/OfficeMathMLTest.php: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 2 22 | π 23 | 24 | 25 | '; 26 | 27 | $reader = new OfficeMathML(); 28 | $math = $reader->read($content); 29 | $this->assertInstanceOf(Math::class, $math); 30 | 31 | $elements = $math->getElements(); 32 | $this->assertCount(1, $elements); 33 | $this->assertInstanceOf(Element\Row::class, $elements[0]); 34 | 35 | /** @var Element\Row $element */ 36 | $element = $elements[0]; 37 | $subElements = $element->getElements(); 38 | $this->assertCount(1, $subElements); 39 | $this->assertInstanceOf(Element\Fraction::class, $subElements[0]); 40 | 41 | /** @var Element\Fraction $subElement */ 42 | $subElement = $subElements[0]; 43 | 44 | /** @var Element\Identifier $numerator */ 45 | $numerator = $subElement->getNumerator(); 46 | $this->assertInstanceOf(Element\Numeric::class, $numerator); 47 | $this->assertEquals(2, $numerator->getValue()); 48 | 49 | /** @var Element\Numeric $denominator */ 50 | $denominator = $subElement->getDenominator(); 51 | $this->assertInstanceOf(Element\Identifier::class, $denominator); 52 | $this->assertEquals('π', $denominator->getValue()); 53 | } 54 | 55 | public function testReadWithWTag(): void 56 | { 57 | $content = ' 58 | 59 | 60 | 61 | 62 | π 63 | 64 | 65 | 66 | 67 | 68 | 2 69 | 70 | 71 | 72 | 73 | 74 | + 75 | 76 | 77 | 78 | a 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 2 87 | 88 | '; 89 | 90 | $reader = new OfficeMathML(); 91 | $math = $reader->read($content); 92 | $this->assertInstanceOf(Math::class, $math); 93 | 94 | $elements = $math->getElements(); 95 | $this->assertCount(5, $elements); 96 | 97 | /** @var Element\Fraction $element */ 98 | $element = $elements[0]; 99 | $this->assertInstanceOf(Element\Fraction::class, $element); 100 | /** @var Element\Identifier $numerator */ 101 | $numerator = $element->getNumerator(); 102 | $this->assertInstanceOf(Element\Identifier::class, $numerator); 103 | $this->assertEquals('π', $numerator->getValue()); 104 | /** @var Element\Numeric $denominator */ 105 | $denominator = $element->getDenominator(); 106 | $this->assertInstanceOf(Element\Numeric::class, $denominator); 107 | $this->assertEquals(2, $denominator->getValue()); 108 | 109 | /** @var Element\Operator $element */ 110 | $element = $elements[1]; 111 | $this->assertInstanceOf(Element\Operator::class, $element); 112 | $this->assertEquals('+', $element->getValue()); 113 | 114 | /** @var Element\Identifier $element */ 115 | $element = $elements[2]; 116 | $this->assertInstanceOf(Element\Identifier::class, $element); 117 | $this->assertEquals('a', $element->getValue()); 118 | 119 | /** @var Element\Operator $element */ 120 | $element = $elements[3]; 121 | $this->assertInstanceOf(Element\Operator::class, $element); 122 | $this->assertEquals('∗', $element->getValue()); 123 | 124 | /** @var Element\Numeric $element */ 125 | $element = $elements[4]; 126 | $this->assertInstanceOf(Element\Numeric::class, $element); 127 | $this->assertEquals(2, $element->getValue()); 128 | } 129 | 130 | public function testReadFractionNoNumerator(): void 131 | { 132 | $this->expectException(InvalidInputException::class); 133 | $this->expectExceptionMessage('PhpOffice\Math\Reader\OfficeMathML::getElement : The tag `m:f` has no numerator defined'); 134 | 135 | $content = ' 136 | 137 | 138 | 2 139 | 140 | 141 | '; 142 | 143 | $reader = new OfficeMathML(); 144 | $math = $reader->read($content); 145 | } 146 | 147 | public function testReadFractionNoDenominator(): void 148 | { 149 | $this->expectException(InvalidInputException::class); 150 | $this->expectExceptionMessage('PhpOffice\Math\Reader\OfficeMathML::getElement : The tag `m:f` has no denominator defined'); 151 | 152 | $content = ' 153 | 154 | 155 | π 156 | 157 | 158 | '; 159 | 160 | $reader = new OfficeMathML(); 161 | $math = $reader->read($content); 162 | } 163 | 164 | public function testReadBasicNoText(): void 165 | { 166 | $this->expectException(InvalidInputException::class); 167 | $this->expectExceptionMessage('PhpOffice\Math\Reader\OfficeMathML::getElement : The tag `m:r` has no tag `m:t` defined'); 168 | 169 | $content = ' 170 | 171 | 172 | a 173 | 174 | 175 | '; 176 | 177 | $reader = new OfficeMathML(); 178 | $math = $reader->read($content); 179 | } 180 | 181 | public function testReadNotImplemented(): void 182 | { 183 | $this->expectException(NotImplementedException::class); 184 | $this->expectExceptionMessage('PhpOffice\Math\Reader\OfficeMathML::getElement : The tag `m:mnotexisting` is not implemented'); 185 | 186 | $content = ' 187 | 188 | 189 | π 190 | 191 | 192 | '; 193 | 194 | $reader = new OfficeMathML(); 195 | $math = $reader->read($content); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /tests/Math/Reader/MathMLTest.php: -------------------------------------------------------------------------------- 1 | 20 | 21 | 22 | 23 | a x2 24 | +bx 25 | +c 26 | 27 | '; 28 | 29 | $reader = new MathML(); 30 | $math = $reader->read($content); 31 | $this->assertInstanceOf(Math::class, $math); 32 | 33 | $elements = $math->getElements(); 34 | $this->assertCount(1, $elements); 35 | $this->assertInstanceOf(Element\Row::class, $elements[0]); 36 | 37 | /** @var Element\Row $element */ 38 | $element = $elements[0]; 39 | $subElements = $element->getElements(); 40 | $this->assertCount(9, $subElements); 41 | 42 | /** @var Element\Identifier $subElement */ 43 | $subElement = $subElements[0]; 44 | $this->assertInstanceOf(Element\Identifier::class, $subElement); 45 | $this->assertEquals('a', $subElement->getValue()); 46 | 47 | /** @var Element\Identifier $subElement */ 48 | $subElement = $subElements[1]; 49 | $this->assertInstanceOf(Element\Operator::class, $subElement); 50 | $this->assertEquals('InvisibleTimes', $subElement->getValue()); 51 | 52 | /** @var Element\Superscript $subElement */ 53 | $subElement = $subElements[2]; 54 | $this->assertInstanceOf(Element\Superscript::class, $subElements[2]); 55 | 56 | /** @var Element\Identifier $base */ 57 | $base = $subElement->getBase(); 58 | $this->assertInstanceOf(Element\Identifier::class, $base); 59 | $this->assertEquals('x', $base->getValue()); 60 | 61 | /** @var Element\Numeric $superscript */ 62 | $superscript = $subElement->getSuperscript(); 63 | $this->assertInstanceOf(Element\Numeric::class, $superscript); 64 | $this->assertEquals(2, $superscript->getValue()); 65 | 66 | /** @var Element\Operator $subElement */ 67 | $subElement = $subElements[3]; 68 | $this->assertInstanceOf(Element\Operator::class, $subElement); 69 | $this->assertEquals('+', $subElement->getValue()); 70 | 71 | /** @var Element\Identifier $subElement */ 72 | $subElement = $subElements[4]; 73 | $this->assertInstanceOf(Element\Identifier::class, $subElement); 74 | $this->assertEquals('b', $subElement->getValue()); 75 | 76 | /** @var Element\Operator $subElement */ 77 | $subElement = $subElements[5]; 78 | $this->assertInstanceOf(Element\Operator::class, $subElement); 79 | $this->assertEquals('InvisibleTimes', $subElement->getValue()); 80 | 81 | /** @var Element\Identifier $subElement */ 82 | $subElement = $subElements[6]; 83 | $this->assertInstanceOf(Element\Identifier::class, $subElement); 84 | $this->assertEquals('x', $subElement->getValue()); 85 | 86 | /** @var Element\Operator $subElement */ 87 | $subElement = $subElements[7]; 88 | $this->assertInstanceOf(Element\Operator::class, $subElement); 89 | $this->assertEquals('+', $subElement->getValue()); 90 | 91 | /** @var Element\Identifier $subElement */ 92 | $subElement = $subElements[8]; 93 | $this->assertInstanceOf(Element\Identifier::class, $subElement); 94 | $this->assertEquals('c', $subElement->getValue()); 95 | } 96 | 97 | public function testReadFraction(): void 98 | { 99 | $content = ' 100 | 101 | 102 | 103 | 104 | a 105 | b 106 | 107 | 108 | c 109 | d 110 | 111 | 112 | '; 113 | 114 | $reader = new MathML(); 115 | $math = $reader->read($content); 116 | $this->assertInstanceOf(Math::class, $math); 117 | 118 | $elements = $math->getElements(); 119 | $this->assertCount(1, $elements); 120 | $this->assertInstanceOf(Element\Fraction::class, $elements[0]); 121 | 122 | /** @var Element\Fraction $element */ 123 | $element = $elements[0]; 124 | 125 | $this->assertInstanceOf(Element\Fraction::class, $element->getNumerator()); 126 | /** @var Element\Fraction $subElement */ 127 | $subElement = $element->getNumerator(); 128 | 129 | /** @var Element\Identifier $numerator */ 130 | $numerator = $subElement->getNumerator(); 131 | $this->assertInstanceOf(Element\Identifier::class, $numerator); 132 | $this->assertEquals('a', $numerator->getValue()); 133 | /** @var Element\Identifier $denominator */ 134 | $denominator = $subElement->getDenominator(); 135 | $this->assertInstanceOf(Element\Identifier::class, $denominator); 136 | $this->assertEquals('b', $denominator->getValue()); 137 | 138 | $this->assertInstanceOf(Element\Fraction::class, $element->getDenominator()); 139 | /** @var Element\Fraction $subElement */ 140 | $subElement = $element->getDenominator(); 141 | 142 | /** @var Element\Identifier $numerator */ 143 | $numerator = $subElement->getNumerator(); 144 | $this->assertInstanceOf(Element\Identifier::class, $numerator); 145 | $this->assertEquals('c', $numerator->getValue()); 146 | /** @var Element\Identifier $denominator */ 147 | $denominator = $subElement->getDenominator(); 148 | $this->assertInstanceOf(Element\Identifier::class, $denominator); 149 | $this->assertEquals('d', $denominator->getValue()); 150 | } 151 | 152 | public function testReadFractionInvalid(): void 153 | { 154 | $this->expectException(InvalidInputException::class); 155 | $this->expectExceptionMessage('PhpOffice\Math\Reader\MathML::getElement : The tag `mfrac` has not two subelements'); 156 | 157 | $content = ' 158 | 159 | 160 | 161 | a 162 | 163 | '; 164 | 165 | $reader = new MathML(); 166 | $math = $reader->read($content); 167 | } 168 | 169 | public function testReadFractionWithRow(): void 170 | { 171 | $content = ' 172 | 173 | 174 | 175 | 176 | 3 177 | - 178 | x 179 | 180 | 2 181 | 182 | '; 183 | 184 | $reader = new MathML(); 185 | $math = $reader->read($content); 186 | $this->assertInstanceOf(Math::class, $math); 187 | 188 | $elements = $math->getElements(); 189 | $this->assertCount(1, $elements); 190 | $this->assertInstanceOf(Element\Fraction::class, $elements[0]); 191 | 192 | /** @var Element\Fraction $element */ 193 | $element = $elements[0]; 194 | 195 | $this->assertInstanceOf(Element\Row::class, $element->getNumerator()); 196 | /** @var Element\Row $subElement */ 197 | $subElement = $element->getNumerator(); 198 | 199 | $subsubElements = $subElement->getElements(); 200 | $this->assertCount(3, $subsubElements); 201 | 202 | /** @var Element\Numeric $subsubElement */ 203 | $subsubElement = $subsubElements[0]; 204 | $this->assertInstanceOf(Element\Numeric::class, $subsubElement); 205 | $this->assertEquals('3', $subsubElement->getValue()); 206 | 207 | /** @var Element\Operator $subsubElement */ 208 | $subsubElement = $subsubElements[1]; 209 | $this->assertInstanceOf(Element\Operator::class, $subsubElement); 210 | $this->assertEquals('-', $subsubElement->getValue()); 211 | 212 | /** @var Element\Identifier $subsubElement */ 213 | $subsubElement = $subsubElements[2]; 214 | $this->assertInstanceOf(Element\Identifier::class, $subsubElement); 215 | $this->assertEquals('x', $subsubElement->getValue()); 216 | 217 | $this->assertInstanceOf(Element\Numeric::class, $element->getDenominator()); 218 | /** @var Element\Numeric $subElement */ 219 | $subElement = $element->getDenominator(); 220 | $this->assertEquals('2', $subElement->getValue()); 221 | } 222 | 223 | public function testReadSuperscriptInvalid(): void 224 | { 225 | $this->expectException(InvalidInputException::class); 226 | $this->expectExceptionMessage('PhpOffice\Math\Reader\MathML::getElement : The tag `msup` has not two subelements'); 227 | 228 | $content = ' 229 | 230 | 231 | 232 | a 233 | 234 | '; 235 | 236 | $reader = new MathML(); 237 | $math = $reader->read($content); 238 | } 239 | 240 | public function testReadSemantics(): void 241 | { 242 | $content = ' 243 | 244 | 245 | 246 | 247 | π 248 | 2 249 | 250 | + 251 | 252 | a 253 | 254 | 2 255 | 256 | 257 | {π} over {2} + { a } * 2 258 | 259 | '; 260 | 261 | $reader = new MathML(); 262 | $math = $reader->read($content); 263 | $this->assertInstanceOf(Math::class, $math); 264 | 265 | $elements = $math->getElements(); 266 | $this->assertCount(1, $elements); 267 | $this->assertInstanceOf(Element\Semantics::class, $elements[0]); 268 | 269 | /** @var Element\Semantics $element */ 270 | $element = $elements[0]; 271 | 272 | // Check MathML 273 | $subElements = $element->getElements(); 274 | $this->assertCount(1, $subElements); 275 | $this->assertInstanceOf(Element\Row::class, $subElements[0]); 276 | 277 | // Check Annotation 278 | $this->assertCount(1, $element->getAnnotations()); 279 | $this->assertEquals('{π} over {2} + { a } * 2', $element->getAnnotation('StarMath 5.0')); 280 | } 281 | 282 | public function testReadNotImplemented(): void 283 | { 284 | $this->expectException(NotImplementedException::class); 285 | $this->expectExceptionMessage('PhpOffice\Math\Reader\MathML::getElement : The tag `mnotexisting` is not implemented'); 286 | 287 | $content = ' 288 | 289 | 290 | 291 | a 292 | 293 | '; 294 | 295 | $reader = new MathML(); 296 | $math = $reader->read($content); 297 | } 298 | 299 | public function testReadSecurity(): void 300 | { 301 | $this->expectException(SecurityException::class); 302 | $this->expectExceptionMessage('Detected use of ENTITY in XML, loading aborted to prevent XXE/XEE attacks'); 303 | 304 | $content = ' M'; 305 | 306 | $reader = new MathML(); 307 | $math = $reader->read($content); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /tests/resources/schema/mathml3/mathml3-content.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | --------------------------------------------------------------------------------