diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 46c539dcc3..ed772171e7 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -1,7 +1,9 @@ name: CI on: - - push - - pull_request + pull_request: + push: + branches: + - master jobs: test: runs-on: ubuntu-latest diff --git a/composer.json b/composer.json index 6890bf6d9f..4a0a5789f1 100644 --- a/composer.json +++ b/composer.json @@ -68,7 +68,8 @@ "ext-dom": "*", "ext-json": "*", "ext-xml": "*", - "laminas/laminas-escaper": ">=2.6" + "laminas/laminas-escaper": ">=2.6", + "phpoffice/math": "^0.1" }, "require-dev": { "ext-zip": "*", diff --git a/composer.lock b/composer.lock index a4557e01da..76d598a86f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2d45739d1122eb36186b5cfe7cd6d6ab", + "content-hash": "23680170abecc52de95d0833296ced64", "packages": [ { "name": "laminas/laminas-escaper", @@ -67,6 +67,59 @@ } ], "time": "2022-10-10T10:11:09+00:00" + }, + { + "name": "phpoffice/math", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/Math.git", + "reference": "f0f8cad98624459c540cdd61d2a174d834471773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/f0f8cad98624459c540cdd61d2a174d834471773", + "reference": "f0f8cad98624459c540cdd61d2a174d834471773", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpunit/phpunit": "^7.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\Math\\": "src/Math/", + "Tests\\PhpOffice\\Math\\": "tests/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Progi1984", + "homepage": "https://lefevre.dev" + } + ], + "description": "Math - Manipulate Math Formula", + "homepage": "https://phpoffice.github.io/Math/", + "keywords": [ + "MathML", + "officemathml", + "php" + ], + "support": { + "issues": "https://github.com/PHPOffice/Math/issues", + "source": "https://github.com/PHPOffice/Math/tree/0.1.0" + }, + "time": "2023-09-25T12:08:20+00:00" } ], "packages-dev": [ diff --git a/docs/changes/1.x/1.2.0.md b/docs/changes/1.x/1.2.0.md index 70172884f7..40becc441d 100644 --- a/docs/changes/1.x/1.2.0.md +++ b/docs/changes/1.x/1.2.0.md @@ -14,6 +14,7 @@ - Word2007 Reader : Added support for Comments by [@shaedrich](https://github.com/shaedrich) in [#2161](https://github.com/PHPOffice/PHPWord/pull/2161) & [#2469](https://github.com/PHPOffice/PHPWord/pull/2469) - Word2007 Reader/Writer: Permit book-fold printing by [@potofcoffee](https://github.com/potofcoffee) in [#2225](https://github.com/PHPOffice/PHPWord/pull/2225) & [#2470](https://github.com/PHPOffice/PHPWord/pull/2470) - Word2007 Writer : Add PageNumber to TOC by [@jet-desk](https://github.com/jet-desk) in [#1652](https://github.com/PHPOffice/PHPWord/pull/1652) & [#2471](https://github.com/PHPOffice/PHPWord/pull/2471) +- Word2007 Reader/Writer + ODText Reader/Writer : Add Element Formula in by [@Progi1984](https://github.com/Progi1984) in [#2477](https://github.com/PHPOffice/PHPWord/pull/2477) ### Bug fixes diff --git a/docs/index.md b/docs/index.md index bd38dd3238..dd600689d7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -66,8 +66,8 @@ Below are the supported features for each file formats. | **Graphs** | 2D basic graphs | :material-check: | | | | | | | 2D advanced graphs | | | | | | | | 3D graphs | :material-check: | | | | | -| **Math** | OMML support | | | | | | -| | MathML support | | | | | | +| **Math** | OMML support | :material-check: | | | | | +| | MathML support | | :material-check: | | | | | **Bonus** | Encryption | | | | | | | | Protection | | | | | | @@ -99,8 +99,8 @@ Below are the supported features for each file formats. | **Graphs** | 2D basic graphs | | | | | | | | 2D advanced graphs | | | | | | | | 3D graphs | | | | | | -| **Math** | OMML support | | | | | | -| | MathML support | | | | | | +| **Math** | OMML support | :material-check: | | | | | +| | MathML support | | :material-check: | | | | | **Bonus** | Encryption | | | | | | | | Protection | | | | | | diff --git a/docs/usage/elements/formula.md b/docs/usage/elements/formula.md new file mode 100644 index 0000000000..a114b73e38 --- /dev/null +++ b/docs/usage/elements/formula.md @@ -0,0 +1,21 @@ +# Formula + +Formula can be added using + +``` php +setDenominator(new Element\Numeric(2)) + ->setNumerator(new Element\Identifier('π')) +; + +$math = new Math(); +$math->add($fraction); + +$formula = $section->addFormula($math); +``` \ No newline at end of file diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index 884ec29385..f9b2822a12 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord\Element; use BadMethodCallException; +use PhpOffice\Math\Math; use ReflectionClass; /** @@ -47,6 +48,7 @@ * @method Chart addChart(string $type, array $categories, array $values, array $style = null, $seriesName = null) * @method FormField addFormField(string $type, mixed $fStyle = null, mixed $pStyle = null) * @method SDT addSDT(string $type) + * @method Formula addFormula(Math $math) * @method \PhpOffice\PhpWord\Element\OLEObject addObject(string $source, mixed $style = null) deprecated, use addOLEObject instead * * @since 0.10.0 @@ -88,6 +90,7 @@ public function __call($function, $args) 'Footnote', 'Endnote', 'CheckBox', 'TextBox', 'Field', 'Line', 'Shape', 'Title', 'TOC', 'PageBreak', 'Chart', 'FormField', 'SDT', 'Comment', + 'Formula', ]; $functions = []; foreach ($elements as $element) { diff --git a/src/PhpWord/Element/Formula.php b/src/PhpWord/Element/Formula.php new file mode 100644 index 0000000000..ca9f5d6b4b --- /dev/null +++ b/src/PhpWord/Element/Formula.php @@ -0,0 +1,53 @@ +setMath($math); + } + + public function setMath(Math $math): self + { + $this->math = $math; + + return $this; + } + + public function getMath(): Math + { + return $this->math; + } +} diff --git a/src/PhpWord/Reader/ODText.php b/src/PhpWord/Reader/ODText.php index aba280db01..d7f8344443 100644 --- a/src/PhpWord/Reader/ODText.php +++ b/src/PhpWord/Reader/ODText.php @@ -53,13 +53,8 @@ public function load($docFile) /** * Read document part. - * - * @param array $relationships - * @param string $partName - * @param string $docFile - * @param string $xmlFile */ - private function readPart(PhpWord $phpWord, $relationships, $partName, $docFile, $xmlFile): void + private function readPart(PhpWord $phpWord, array $relationships, string $partName, string $docFile, string $xmlFile): void { $partClass = "PhpOffice\\PhpWord\\Reader\\ODText\\{$partName}"; if (class_exists($partClass)) { @@ -72,12 +67,8 @@ private function readPart(PhpWord $phpWord, $relationships, $partName, $docFile, /** * Read all relationship files. - * - * @param string $docFile - * - * @return array */ - private function readRelationships($docFile) + private function readRelationships(string $docFile): array { $rels = []; $xmlFile = 'META-INF/manifest.xml'; diff --git a/src/PhpWord/Reader/ODText/Content.php b/src/PhpWord/Reader/ODText/Content.php index ccbc5eec96..45cb0704db 100644 --- a/src/PhpWord/Reader/ODText/Content.php +++ b/src/PhpWord/Reader/ODText/Content.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord\Reader\ODText; use DateTime; +use PhpOffice\Math\Reader\MathML; use PhpOffice\PhpWord\Element\TrackChange; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Shared\XMLReader; @@ -51,37 +52,55 @@ public function read(PhpWord $phpWord): void break; case 'text:p': // Paragraph - $children = $node->childNodes; - foreach ($children as $child) { - switch ($child->nodeName) { - case 'text:change-start': - $changeId = $child->getAttribute('text:change-id'); - if (isset($trackedChanges[$changeId])) { - $changed = $trackedChanges[$changeId]; - } + $element = $xmlReader->getElement('draw:frame/draw:object', $node); + if ($element) { + $mathFile = str_replace('./', '', $element->getAttribute('xlink:href')) . '/content.xml'; - break; - case 'text:change-end': - unset($changed); + $xmlReaderObject = new XMLReader(); + $mathElement = $xmlReaderObject->getDomFromZip($this->docFile, $mathFile); + if ($mathElement) { + $mathXML = $mathElement->saveXML($mathElement); - break; - case 'text:change': - $changeId = $child->getAttribute('text:change-id'); - if (isset($trackedChanges[$changeId])) { - $changed = $trackedChanges[$changeId]; - } + if (is_string($mathXML)) { + $reader = new MathML(); + $math = $reader->read($mathXML); - break; + $section->addFormula($math); + } } - } + } else { + $children = $node->childNodes; + foreach ($children as $child) { + switch ($child->nodeName) { + case 'text:change-start': + $changeId = $child->getAttribute('text:change-id'); + if (isset($trackedChanges[$changeId])) { + $changed = $trackedChanges[$changeId]; + } + + break; + case 'text:change-end': + unset($changed); - $element = $section->addText($node->nodeValue); - if (isset($changed) && is_array($changed)) { - $element->setTrackChange($changed['changed']); - if (isset($changed['textNodes'])) { - foreach ($changed['textNodes'] as $changedNode) { - $element = $section->addText($changedNode->nodeValue); - $element->setTrackChange($changed['changed']); + break; + case 'text:change': + $changeId = $child->getAttribute('text:change-id'); + if (isset($trackedChanges[$changeId])) { + $changed = $trackedChanges[$changeId]; + } + + break; + } + } + + $element = $section->addText($node->nodeValue); + if (isset($changed) && is_array($changed)) { + $element->setTrackChange($changed['changed']); + if (isset($changed['textNodes'])) { + foreach ($changed['textNodes'] as $changedNode) { + $element = $section->addText($changedNode->nodeValue); + $element->setTrackChange($changed['changed']); + } } } } diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index dc61b09356..e9272ae71d 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -20,6 +20,7 @@ use DateTime; use DOMElement; use InvalidArgumentException; +use PhpOffice\Math\Reader\OfficeMathML; use PhpOffice\PhpWord\ComplexType\TblWidth as TblWidthComplexType; use PhpOffice\PhpWord\Element\AbstractContainer; use PhpOffice\PhpWord\Element\AbstractElement; @@ -189,25 +190,7 @@ protected function getCommentReference(string $id): array protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart = 'document'): void { // Paragraph style - $paragraphStyle = null; - $headingDepth = null; - if ($xmlReader->elementExists('w:commentReference', $domNode) - || $xmlReader->elementExists('w:commentRangeStart', $domNode) - || $xmlReader->elementExists('w:commentRangeEnd', $domNode) - ) { - $nodes = $xmlReader->getElements('w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode); - $node = current(iterator_to_array($nodes)); - if ($node) { - $attributeIdentifier = $node->attributes->getNamedItem('id'); - if ($attributeIdentifier) { - $id = $attributeIdentifier->nodeValue; - } - } - } - if ($xmlReader->elementExists('w:pPr', $domNode)) { - $paragraphStyle = $this->readParagraphStyle($xmlReader, $domNode); - $headingDepth = $this->getHeadingDepth($paragraphStyle); - } + $paragraphStyle = $xmlReader->elementExists('w:pPr', $domNode) ? $this->readParagraphStyle($xmlReader, $domNode) : null; // PreserveText if ($xmlReader->elementExists('w:r/w:instrText', $domNode)) { @@ -234,8 +217,27 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par } } $parent->addPreserveText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'), $fontStyle, $paragraphStyle); - } elseif ($xmlReader->elementExists('w:pPr/w:numPr', $domNode)) { - // List item + + return; + } + + // Formula + $xmlReader->registerNamespace('m', 'http://schemas.openxmlformats.org/officeDocument/2006/math'); + if ($xmlReader->elementExists('m:oMath', $domNode)) { + $mathElement = $xmlReader->getElement('m:oMath', $domNode); + $mathXML = $mathElement->ownerDocument->saveXML($mathElement); + if (is_string($mathXML)) { + $reader = new OfficeMathML(); + $math = $reader->read($mathXML); + + $parent->addFormula($math); + } + + return; + } + + // List item + if ($xmlReader->elementExists('w:pPr/w:numPr', $domNode)) { $numId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:numId'); $levelId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:ilvl'); $nodes = $xmlReader->getElements('*', $domNode); @@ -245,8 +247,13 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par foreach ($nodes as $node) { $this->readRun($xmlReader, $node, $listItemRun, $docPart, $paragraphStyle); } - } elseif ($headingDepth !== null) { - // Heading or Title + + return; + } + + // Heading or Title + $headingDepth = $xmlReader->elementExists('w:pPr', $domNode) ? $this->getHeadingDepth($paragraphStyle) : null; + if ($headingDepth !== null) { $textContent = null; $nodes = $xmlReader->getElements('w:r|w:hyperlink', $domNode); if ($nodes->length === 1) { @@ -258,17 +265,19 @@ protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $par } } $parent->addTitle($textContent, $headingDepth); + + return; + } + + // Text and TextRun + $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode); + if (0 === $textRunContainers) { + $parent->addTextBreak(null, $paragraphStyle); } else { - // Text and TextRun - $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode); - if (0 === $textRunContainers) { - $parent->addTextBreak(null, $paragraphStyle); - } else { - $nodes = $xmlReader->getElements('*', $domNode); - $paragraph = $parent->addTextRun($paragraphStyle); - foreach ($nodes as $node) { - $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle); - } + $nodes = $xmlReader->getElements('*', $domNode); + $paragraph = $parent->addTextRun($paragraphStyle); + foreach ($nodes as $node) { + $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle); } } } diff --git a/src/PhpWord/Writer/ODText.php b/src/PhpWord/Writer/ODText.php index 10d9d701fa..6d54706c8e 100644 --- a/src/PhpWord/Writer/ODText.php +++ b/src/PhpWord/Writer/ODText.php @@ -17,8 +17,12 @@ namespace PhpOffice\PhpWord\Writer; +use PhpOffice\Math\Writer\MathML; +use PhpOffice\PhpWord\Element\AbstractElement; +use PhpOffice\PhpWord\Element\Formula; use PhpOffice\PhpWord\Media; use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\Writer\ODText\Part\AbstractPart; /** * ODText writer. @@ -27,6 +31,11 @@ */ class ODText extends AbstractWriter implements WriterInterface { + /** + * @var AbstractElement[] + */ + protected $objects = []; + /** * Create new ODText writer. * @@ -77,8 +86,28 @@ public function save($filename = null): void // Write parts foreach ($this->parts as $partName => $fileName) { - if ($fileName != '') { - $zip->addFromString($fileName, $this->getWriterPart($partName)->write()); + if ($fileName === '') { + continue; + } + $part = $this->getWriterPart($partName); + if (!$part instanceof AbstractPart) { + continue; + } + + $part->setObjects($this->objects); + + $zip->addFromString($fileName, $part->write()); + + $this->objects = $part->getObjects(); + } + + // Write objects charts + if (!empty($this->objects)) { + $writer = new MathML(); + foreach ($this->objects as $idxObject => $object) { + if ($object instanceof Formula) { + $zip->addFromString('Formula' . $idxObject . '/content.xml', $writer->write($object->getMath())); + } } } diff --git a/src/PhpWord/Writer/ODText/Element/Formula.php b/src/PhpWord/Writer/ODText/Element/Formula.php new file mode 100644 index 0000000000..ddb1d81aee --- /dev/null +++ b/src/PhpWord/Writer/ODText/Element/Formula.php @@ -0,0 +1,74 @@ +getXmlWriter(); + $element = $this->getElement(); + if (!$element instanceof ElementFormula) { + return; + } + + $part = $this->getPart(); + if (!$part instanceof AbstractPart) { + return; + } + + $objectIdx = $part->addObject($element); + + //$style = $element->getStyle(); + //$width = Converter::pixelToCm($style->getWidth()); + //$height = Converter::pixelToCm($style->getHeight()); + + $xmlWriter->startElement('text:p'); + $xmlWriter->writeAttribute('text:style-name', 'OB' . $objectIdx); + + $xmlWriter->startElement('draw:frame'); + $xmlWriter->writeAttribute('draw:name', $element->getElementId()); + $xmlWriter->writeAttribute('text:anchor-type', 'as-char'); + //$xmlWriter->writeAttribute('svg:width', $width . 'cm'); + //$xmlWriter->writeAttribute('svg:height', $height . 'cm'); + //$xmlWriter->writeAttribute('draw:z-index', $mediaIndex); + + $xmlWriter->startElement('draw:object'); + $xmlWriter->writeAttribute('xlink:href', 'Formula' . $objectIdx); + $xmlWriter->writeAttribute('xlink:type', 'simple'); + $xmlWriter->writeAttribute('xlink:show', 'embed'); + $xmlWriter->writeAttribute('xlink:actuate', 'onLoad'); + $xmlWriter->endElement(); // draw:object + + $xmlWriter->endElement(); // draw:frame + + $xmlWriter->endElement(); // text:p + } +} diff --git a/src/PhpWord/Writer/ODText/Part/AbstractPart.php b/src/PhpWord/Writer/ODText/Part/AbstractPart.php index 4db5ce6e09..59035ff787 100644 --- a/src/PhpWord/Writer/ODText/Part/AbstractPart.php +++ b/src/PhpWord/Writer/ODText/Part/AbstractPart.php @@ -17,6 +17,7 @@ namespace PhpOffice\PhpWord\Writer\ODText\Part; +use PhpOffice\PhpWord\Element\AbstractElement; use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Shared\XMLWriter; use PhpOffice\PhpWord\Style; @@ -28,6 +29,11 @@ */ abstract class AbstractPart extends Word2007AbstractPart { + /** + * @var AbstractElement[] + */ + protected $objects = []; + /** * @var string Date format */ @@ -102,4 +108,29 @@ protected function writeFontFaces(XMLWriter $xmlWriter): void } $xmlWriter->endElement(); } + + public function addObject(AbstractElement $object): int + { + $this->objects[] = $object; + + return count($this->objects) - 1; + } + + /** + * @param AbstractElement[] $objects + */ + public function setObjects(array $objects): self + { + $this->objects = $objects; + + return $this; + } + + /** + * @return AbstractElement[] + */ + public function getObjects(): array + { + return $this->objects; + } } diff --git a/src/PhpWord/Writer/ODText/Part/Content.php b/src/PhpWord/Writer/ODText/Part/Content.php index 8c96650240..0c0607a06f 100644 --- a/src/PhpWord/Writer/ODText/Part/Content.php +++ b/src/PhpWord/Writer/ODText/Part/Content.php @@ -135,8 +135,11 @@ public function write() $xmlWriter->startElement('text:p'); $xmlWriter->writeAttribute('text:style-name', 'SB' . $section->getSectionId()); $xmlWriter->endElement(); + $containerWriter = new Container($xmlWriter, $section); + $containerWriter->setPart($this); $containerWriter->write(); + $xmlWriter->endElement(); // text:section } diff --git a/src/PhpWord/Writer/ODText/Part/Manifest.php b/src/PhpWord/Writer/ODText/Part/Manifest.php index 7d428b2c76..37fb797935 100644 --- a/src/PhpWord/Writer/ODText/Part/Manifest.php +++ b/src/PhpWord/Writer/ODText/Part/Manifest.php @@ -17,7 +17,9 @@ namespace PhpOffice\PhpWord\Writer\ODText\Part; +use PhpOffice\PhpWord\Element\Formula; use PhpOffice\PhpWord\Media; +use PhpOffice\PhpWord\Writer\ODText; /** * ODText manifest part writer: META-INF/manifest.xml. @@ -31,7 +33,6 @@ class Manifest extends AbstractPart */ public function write() { - $parts = ['content.xml', 'meta.xml', 'styles.xml']; $xmlWriter = $this->getXmlWriter(); $xmlWriter->startDocument('1.0', 'UTF-8'); @@ -46,7 +47,7 @@ public function write() $xmlWriter->endElement(); // Parts - foreach ($parts as $part) { + foreach (['content.xml', 'meta.xml', 'styles.xml'] as $part) { $xmlWriter->startElement('manifest:file-entry'); $xmlWriter->writeAttribute('manifest:media-type', 'text/xml'); $xmlWriter->writeAttribute('manifest:full-path', $part); @@ -64,6 +65,20 @@ public function write() } } + foreach ($this->getObjects() as $idxObject => $object) { + if ($object instanceof Formula) { + $xmlWriter->startElement('manifest:file-entry'); + $xmlWriter->writeAttribute('manifest:full-path', 'Formula' . $idxObject . '/content.xml'); + $xmlWriter->writeAttribute('manifest:media-type', 'text/xml'); + $xmlWriter->endElement(); + $xmlWriter->startElement('manifest:file-entry'); + $xmlWriter->writeAttribute('manifest:full-path', 'Formula' . $idxObject . '/'); + $xmlWriter->writeAttribute('manifest:version', '1.2'); + $xmlWriter->writeAttribute('manifest:media-type', 'application/vnd.oasis.opendocument.formula'); + $xmlWriter->endElement(); + } + } + $xmlWriter->endElement(); // manifest:manifest return $xmlWriter->getData(); diff --git a/src/PhpWord/Writer/Word2007/Element/AbstractElement.php b/src/PhpWord/Writer/Word2007/Element/AbstractElement.php index a7bfeab78b..abd1324aad 100644 --- a/src/PhpWord/Writer/Word2007/Element/AbstractElement.php +++ b/src/PhpWord/Writer/Word2007/Element/AbstractElement.php @@ -21,6 +21,7 @@ use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Shared\Text as SharedText; use PhpOffice\PhpWord\Shared\XMLWriter; +use PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart; /** * Abstract element writer. @@ -50,6 +51,11 @@ abstract class AbstractElement */ protected $withoutP = false; + /** + * @var null|AbstractPart + */ + protected $part; + /** * Write element. */ @@ -224,4 +230,16 @@ protected function writeText($content) return $this->getXmlWriter()->writeRaw($content); } + + public function setPart(?AbstractPart $part): self + { + $this->part = $part; + + return $this; + } + + public function getPart(): ?AbstractPart + { + return $this->part; + } } diff --git a/src/PhpWord/Writer/Word2007/Element/Container.php b/src/PhpWord/Writer/Word2007/Element/Container.php index b6db45197a..491e813c9e 100644 --- a/src/PhpWord/Writer/Word2007/Element/Container.php +++ b/src/PhpWord/Writer/Word2007/Element/Container.php @@ -83,6 +83,7 @@ private function writeElement(XMLWriter $xmlWriter, Element $element, $withoutP) if (class_exists($writerClass)) { /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $writer Type hint */ $writer = new $writerClass($xmlWriter, $element, $withoutP); + $writer->setPart($this->getPart()); $writer->write(); } diff --git a/src/PhpWord/Writer/Word2007/Element/Formula.php b/src/PhpWord/Writer/Word2007/Element/Formula.php new file mode 100644 index 0000000000..6abb74b782 --- /dev/null +++ b/src/PhpWord/Writer/Word2007/Element/Formula.php @@ -0,0 +1,50 @@ +getElement(); + if (!$element instanceof FormulaElement) { + return; + } + + $this->startElementP(); + + $xmlWriter = $this->getXmlWriter(); + + $xmlWriter->startElement('w:r'); + $xmlWriter->writeElement('w:rPr'); + $xmlWriter->endElement(); + + $xmlWriter->writeRaw((new OfficeMathML())->write($element->getMath())); + + $this->endElementP(); + } +} diff --git a/tests/PhpWordTests/Element/AbstractElementTest.php b/tests/PhpWordTests/Element/AbstractElementTest.php index 8a0a878eb2..a059712b1a 100644 --- a/tests/PhpWordTests/Element/AbstractElementTest.php +++ b/tests/PhpWordTests/Element/AbstractElementTest.php @@ -17,6 +17,8 @@ namespace PhpOffice\PhpWordTests\Element; +use PhpOffice\PhpWord\Element\AbstractElement; + /** * Test class for PhpOffice\PhpWord\Element\AbstractElement. */ @@ -27,7 +29,7 @@ class AbstractElementTest extends \PHPUnit\Framework\TestCase */ public function testElementIndex(): void { - $stub = $this->getMockForAbstractClass('\PhpOffice\PhpWord\Element\AbstractElement'); + $stub = $this->getMockForAbstractClass(AbstractElement::class); $ival = mt_rand(0, 100); $stub->setElementIndex($ival); self::assertEquals($ival, $stub->getElementIndex()); @@ -38,7 +40,7 @@ public function testElementIndex(): void */ public function testElementId(): void { - $stub = $this->getMockForAbstractClass('\PhpOffice\PhpWord\Element\AbstractElement'); + $stub = $this->getMockForAbstractClass(AbstractElement::class); $stub->setElementId(); self::assertEquals(6, strlen($stub->getElementId())); } diff --git a/tests/PhpWordTests/Element/FormulaTest.php b/tests/PhpWordTests/Element/FormulaTest.php new file mode 100644 index 0000000000..fef5c2221e --- /dev/null +++ b/tests/PhpWordTests/Element/FormulaTest.php @@ -0,0 +1,64 @@ +add(new Element\Fraction( + new Element\Numeric(2), + new Element\Identifier('π') + )); + + $element = new Formula(new Math()); + + self::assertInstanceOf(Formula::class, $element); + self::assertEquals(new Math(), $element->getMath()); + self::assertNotEquals($math, $element->getMath()); + + self::assertInstanceOf(Formula::class, $element->setMath($math)); + self::assertNotEquals(new Math(), $element->getMath()); + self::assertEquals($math, $element->getMath()); + } +} diff --git a/tests/PhpWordTests/Reader/ODTextTest.php b/tests/PhpWordTests/Reader/ODTextTest.php index e4baf8d480..20c5916c7d 100644 --- a/tests/PhpWordTests/Reader/ODTextTest.php +++ b/tests/PhpWordTests/Reader/ODTextTest.php @@ -17,7 +17,11 @@ namespace PhpOffice\PhpWordTests\Reader; +use PhpOffice\Math\Element; +use PhpOffice\PhpWord\Element\Formula; +use PhpOffice\PhpWord\Element\Section; use PhpOffice\PhpWord\IOFactory; +use PhpOffice\PhpWord\PhpWord; /** * Test class for PhpOffice\PhpWord\Reader\ODText. @@ -33,8 +37,31 @@ class ODTextTest extends \PHPUnit\Framework\TestCase */ public function testLoad(): void { - $filename = __DIR__ . '/../_files/documents/reader.odt'; - $phpWord = IOFactory::load($filename, 'ODText'); - self::assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $phpWord); + $phpWord = IOFactory::load(dirname(__DIR__, 1) . '/_files/documents/reader.odt', 'ODText'); + self::assertInstanceOf(PhpWord::class, $phpWord); + } + + public function testLoadFormula(): void + { + $phpWord = IOFactory::load(dirname(__DIR__, 1) . '/_files/documents/reader-formula.odt', 'ODText'); + + self::assertInstanceOf(PhpWord::class, $phpWord); + + $sections = $phpWord->getSections(); + self::assertCount(1, $sections); + + $section = $sections[0]; + self::assertInstanceOf(Section::class, $section); + + $elements = $section->getElements(); + self::assertCount(1, $elements); + + $element = $elements[0]; + self::assertInstanceOf(Formula::class, $element); + + $elements = $element->getMath()->getElements(); + self::assertCount(1, $elements); + + self::assertInstanceOf(Element\Semantics::class, $elements[0]); } } diff --git a/tests/PhpWordTests/Reader/Word2007Test.php b/tests/PhpWordTests/Reader/Word2007Test.php index e42f0110d5..1d8674d729 100644 --- a/tests/PhpWordTests/Reader/Word2007Test.php +++ b/tests/PhpWordTests/Reader/Word2007Test.php @@ -18,8 +18,11 @@ namespace PhpOffice\PhpWordTests\Reader; use DateTime; +use PhpOffice\Math\Element; use PhpOffice\PhpWord\Element\Comment; +use PhpOffice\PhpWord\Element\Formula; use PhpOffice\PhpWord\Element\Image; +use PhpOffice\PhpWord\Element\Section; use PhpOffice\PhpWord\Element\Text; use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\IOFactory; @@ -159,4 +162,58 @@ public function testLoadComments(): void self::assertInstanceOf(Font::class, $fontStyle); self::assertEquals('de-DE', $fontStyle->getLang()->getLatin()); } + + public function testLoadFormula(): void + { + $phpWord = IOFactory::load(dirname(__DIR__, 1) . '/_files/documents/reader-formula.docx'); + + self::assertInstanceOf(PhpWord::class, $phpWord); + + $sections = $phpWord->getSections(); + self::assertCount(1, $sections); + + $section = $sections[0]; + self::assertInstanceOf(Section::class, $section); + + $elements = $section->getElements(); + self::assertCount(1, $elements); + + $element = $elements[0]; + self::assertInstanceOf(Formula::class, $element); + + $elements = $element->getMath()->getElements(); + self::assertCount(5, $elements); + + /** @var Element\Fraction $element */ + $element = $elements[0]; + self::assertInstanceOf(Element\Fraction::class, $element); + /** @var Element\Identifier $numerator */ + $numerator = $element->getNumerator(); + self::assertInstanceOf(Element\Identifier::class, $numerator); + self::assertEquals('π', $numerator->getValue()); + /** @var Element\Numeric $denominator */ + $denominator = $element->getDenominator(); + self::assertInstanceOf(Element\Numeric::class, $denominator); + self::assertEquals(2, $denominator->getValue()); + + /** @var Element\Operator $element */ + $element = $elements[1]; + self::assertInstanceOf(Element\Operator::class, $element); + self::assertEquals('+', $element->getValue()); + + /** @var Element\Identifier $element */ + $element = $elements[2]; + self::assertInstanceOf(Element\Identifier::class, $element); + self::assertEquals('a', $element->getValue()); + + /** @var Element\Operator $element */ + $element = $elements[3]; + self::assertInstanceOf(Element\Operator::class, $element); + self::assertEquals('∗', $element->getValue()); + + /** @var Element\Numeric $element */ + $element = $elements[4]; + self::assertInstanceOf(Element\Numeric::class, $element); + self::assertEquals(2, $element->getValue()); + } } diff --git a/tests/PhpWordTests/Writer/ODText/Element/FormulaTest.php b/tests/PhpWordTests/Writer/ODText/Element/FormulaTest.php new file mode 100644 index 0000000000..9276372105 --- /dev/null +++ b/tests/PhpWordTests/Writer/ODText/Element/FormulaTest.php @@ -0,0 +1,71 @@ +add( + new Element\Fraction( + new Element\Numeric(2), + new Element\Identifier('π') + ) + ) + ->add( + new Element\Operator('+') + ) + ->add( + new Element\Identifier('a') + ) + ->add( + new Element\Operator('∗') + ) + ->add( + new Element\Numeric(2) + ); + + $phpWord = new PhpWord(); + + $section = $phpWord->addSection(); + $section->addFormula($math); + + $doc = TestHelperDOCX::getDocument($phpWord, 'ODText'); + + self::assertTrue($doc->elementExists('/office:document-content/office:body/office:text/text:section/text:p/draw:frame/draw:object')); + } +} diff --git a/tests/PhpWordTests/Writer/ODText/Part/ManifestTest.php b/tests/PhpWordTests/Writer/ODText/Part/ManifestTest.php new file mode 100644 index 0000000000..01a8053c8f --- /dev/null +++ b/tests/PhpWordTests/Writer/ODText/Part/ManifestTest.php @@ -0,0 +1,100 @@ +addSection(); + + $doc = TestHelperDOCX::getDocument($phpWord, 'ODText'); + + self::assertFalse($doc->elementExists( + '/manifest:manifest/manifest:file-entry[@manifest:full-path="Formula0/content.xml"]', + 'META-INF/manifest.xml' + )); + self::assertFalse($doc->elementExists( + '/manifest:manifest/manifest:file-entry[@manifest:full-path="Formula0/"]', + 'META-INF/manifest.xml' + )); + } + + public function testWriteFormula(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + + $math = new Math(); + $math->add( + new Element\Fraction( + new Element\Numeric(2), + new Element\Identifier('π') + ) + ); + $section->addFormula($math); + + $doc = TestHelperDOCX::getDocument($phpWord, 'ODText'); + + self::assertTrue($doc->elementExists( + '/manifest:manifest/manifest:file-entry[@manifest:full-path="Formula0/content.xml"]', + 'META-INF/manifest.xml' + )); + self::assertEquals('text/xml', $doc->getElementAttribute( + '/manifest:manifest/manifest:file-entry[@manifest:full-path="Formula0/content.xml"]', + 'manifest:media-type', + 'META-INF/manifest.xml' + )); + + self::assertTrue($doc->elementExists( + '/manifest:manifest/manifest:file-entry[@manifest:full-path="Formula0/"]', + 'META-INF/manifest.xml' + )); + self::assertEquals('1.2', $doc->getElementAttribute( + '/manifest:manifest/manifest:file-entry[@manifest:full-path="Formula0/"]', + 'manifest:version', + 'META-INF/manifest.xml' + )); + self::assertEquals('application/vnd.oasis.opendocument.formula', $doc->getElementAttribute( + '/manifest:manifest/manifest:file-entry[@manifest:full-path="Formula0/"]', + 'manifest:media-type', + 'META-INF/manifest.xml' + )); + } +} diff --git a/tests/PhpWordTests/Writer/Word2007/Element/FormulaTest.php b/tests/PhpWordTests/Writer/Word2007/Element/FormulaTest.php new file mode 100644 index 0000000000..b9acb1c226 --- /dev/null +++ b/tests/PhpWordTests/Writer/Word2007/Element/FormulaTest.php @@ -0,0 +1,72 @@ +add( + new Element\Fraction( + new Element\Numeric(2), + new Element\Identifier('π') + ) + ) + ->add( + new Element\Operator('+') + ) + ->add( + new Element\Identifier('a') + ) + ->add( + new Element\Operator('∗') + ) + ->add( + new Element\Numeric(2) + ); + + $phpWord = new PhpWord(); + + $section = $phpWord->addSection(); + $section->addFormula($math); + + $doc = TestHelperDOCX::getDocument($phpWord); + + self::assertTrue($doc->elementExists('/w:document/w:body/w:p[1]/m:oMathPara')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:p[1]/m:oMathPara/m:oMath')); + } +} diff --git a/tests/PhpWordTests/Writer/Word2007/Part/DocumentTest.php b/tests/PhpWordTests/Writer/Word2007/Part/DocumentTest.php index 108142d6f2..afb22f17b7 100644 --- a/tests/PhpWordTests/Writer/Word2007/Part/DocumentTest.php +++ b/tests/PhpWordTests/Writer/Word2007/Part/DocumentTest.php @@ -408,10 +408,10 @@ public function testWriteImage(): void $style = $element->getAttribute('style'); // Try to address CI coverage issue for PHP 7.1 and 7.2 when using regex match assertions - if (method_exists(static::class, 'assertRegExp')) { - self::assertRegExp('/z\-index:\-[0-9]*/', $style); - } else { + if (method_exists(static::class, 'assertMatchesRegularExpression')) { self::assertMatchesRegularExpression('/z\-index:\-[0-9]*/', $style); + } else { + self::assertRegExp('/z\-index:\-[0-9]*/', $style); } // square diff --git a/tests/PhpWordTests/XmlDocument.php b/tests/PhpWordTests/XmlDocument.php index 80d64c4357..92e19d3bb9 100644 --- a/tests/PhpWordTests/XmlDocument.php +++ b/tests/PhpWordTests/XmlDocument.php @@ -64,48 +64,55 @@ class XmlDocument private $defaultFile = 'word/document.xml'; /** - * Get default file. + * Create new instance. * - * @return string + * @param string $path + */ + public function __construct($path) + { + $this->path = realpath($path); + } + + /** + * Get default file. */ - public function getDefaultFile() + public function getDefaultFile(): string { return $this->defaultFile; } /** * Set default file. - * - * @param string $file - * - * @return string */ - public function setDefaultFile($file) + public function setDefaultFile(string $file): string { $temp = $this->defaultFile; + $this->defaultFile = $file; return $temp; } /** - * Create new instance. - * - * @param string $path + * Get file name. */ - public function __construct($path) + public function getFile(): string { - $this->path = realpath($path); + return $this->file; + } + + /** + * Get path. + */ + public function getPath(): string + { + return $this->path; } /** * Get DOM from file. - * - * @param string $file - * - * @return DOMDocument */ - public function getFileDom($file = '') + public function getFileDom(string $file = ''): DOMDocument { if (!$file) { $file = $this->defaultFile; @@ -133,12 +140,9 @@ public function getFileDom($file = '') /** * Get node list. * - * @param string $path - * @param string $file - * * @return DOMNodeList */ - public function getNodeList($path, $file = '') + public function getNodeList(string $path, string $file = ''): DOMNodeList { if (!$file) { $file = $this->defaultFile; @@ -157,73 +161,25 @@ public function getNodeList($path, $file = '') /** * Get element. - * - * @param string $path - * @param string $file - * - * @return null|DOMElement - */ - public function getElement($path, $file = '') - { - if (!$file) { - $file = $this->defaultFile; - } - $elements = $this->getNodeList($path, $file); - - return $elements->item(0); - } - - /** - * Get file name. - * - * @return string - */ - public function getFile() - { - return $this->file; - } - - /** - * Get path. - * - * @return string */ - public function getPath() + public function getElement(string $path, string $file = ''): ?DOMElement { - return $this->path; + return $this->getNodeList($path, $file)->item(0); } /** * Get element attribute. - * - * @param string $path - * @param string $attribute - * @param string $file - * - * @return string */ - public function getElementAttribute($path, $attribute, $file = '') + public function getElementAttribute(string $path, string $attribute, string $file = ''): string { - if (!$file) { - $file = $this->defaultFile; - } - return $this->getElement($path, $file)->getAttribute($attribute); } /** * Check if element exists. - * - * @param string $path - * @param string $file - * - * @return bool */ - public function elementExists($path, $file = '') + public function elementExists(string $path, string $file = ''): bool { - if (!$file) { - $file = $this->defaultFile; - } $nodeList = $this->getNodeList($path, $file); return $nodeList->length != 0; @@ -232,17 +188,10 @@ public function elementExists($path, $file = '') /** * Returns the xml, or part of it as a formatted string. * - * @param string $path - * @param string $file - * * @return false|string */ - public function printXml($path = '/', $file = '') + public function printXml(string $path = '/', string $file = '') { - if (!$file) { - $file = $this->defaultFile; - } - $element = $this->getElement($path, $file); $newdoc = new DOMDocument(); diff --git a/tests/PhpWordTests/_files/documents/reader-formula.docx b/tests/PhpWordTests/_files/documents/reader-formula.docx new file mode 100644 index 0000000000..0e40f6672c Binary files /dev/null and b/tests/PhpWordTests/_files/documents/reader-formula.docx differ diff --git a/tests/PhpWordTests/_files/documents/reader-formula.odt b/tests/PhpWordTests/_files/documents/reader-formula.odt new file mode 100644 index 0000000000..f94c44afbb Binary files /dev/null and b/tests/PhpWordTests/_files/documents/reader-formula.odt differ