From 5914e8ec3752f72ec6c9b372533986ee13954021 Mon Sep 17 00:00:00 2001
From: Sambit Chakraborty <58760654+Sambit003@users.noreply.github.com>
Date: Mon, 3 Feb 2025 12:14:09 +0530
Subject: [PATCH 1/3] Writer EPub3 : Added support (#2724)
* Feat: Added Epub3 writer support
* Add: Tests
* Fix: Code convention
* Update: composer.lock file with latest requirement
* Update: composer.lock file with latest requirement
* Update: composer.lock file with latest requirement
* Remove: composer.lock
* Update: Code conventions for PHP 8.x & Update: .gitignore to include composer.lock (in support of PR #2722 : Autoload)
* Add: Text & Image Element
* Improvement of Epub3 elements code
* Add: Unit tests for full EPub3 codebase
* Fix: Code convention errors for EPub3 Unit tests
* Fix: Added the suggestions
* Revert: composer.json changes -> Now again included the gd & zip extension
* Update: composer.json
* Add: Generating Epub samples with adherence to the Epub 3 checking procedures
* Fix: Null type error in php8.x runtime
* Update: Changelog
---
.gitignore | 1 +
composer.json | 4 +-
docs/changes/1.x/1.4.0.md | 2 +
samples/Sample_Header.php | 2 +-
src/PhpWord/IOFactory.php | 2 +-
src/PhpWord/Shared/ZipArchive.php | 11 ++
src/PhpWord/Writer/EPub3.php | 91 ++++++++++++
.../Writer/EPub3/Element/AbstractElement.php | 45 ++++++
src/PhpWord/Writer/EPub3/Element/Image.php | 45 ++++++
src/PhpWord/Writer/EPub3/Element/Text.php | 50 +++++++
src/PhpWord/Writer/EPub3/Part.php | 45 ++++++
.../Writer/EPub3/Part/AbstractPart.php | 56 ++++++++
src/PhpWord/Writer/EPub3/Part/Content.php | 130 ++++++++++++++++++
.../Writer/EPub3/Part/ContentXhtml.php | 117 ++++++++++++++++
src/PhpWord/Writer/EPub3/Part/Manifest.php | 40 ++++++
src/PhpWord/Writer/EPub3/Part/Meta.php | 74 ++++++++++
src/PhpWord/Writer/EPub3/Part/Mimetype.php | 33 +++++
src/PhpWord/Writer/EPub3/Part/Nav.php | 53 +++++++
.../Writer/EPub3/Style/AbstractStyle.php | 75 ++++++++++
src/PhpWord/Writer/EPub3/Style/Font.php | 39 ++++++
src/PhpWord/Writer/EPub3/Style/Paragraph.php | 39 ++++++
src/PhpWord/Writer/EPub3/Style/Table.php | 43 ++++++
src/PhpWord/Writer/WriterPartInterface.php | 10 ++
.../Writer/EPub3/Element/ImageTest.php | 69 ++++++++++
.../Writer/EPub3/Element/TextTest.php | 83 +++++++++++
.../PhpWordTests/Writer/EPub3/ElementTest.php | 26 ++++
.../Writer/EPub3/Part/AbstractPartTest.php | 28 ++++
.../Writer/EPub3/Part/ContentTest.php | 38 +++++
.../Writer/EPub3/Part/ManifestTest.php | 35 +++++
.../Writer/EPub3/Part/MetaTest.php | 53 +++++++
.../Writer/EPub3/Part/MimetypeTest.php | 32 +++++
.../Writer/EPub3/Part/NavTest.php | 49 +++++++
tests/PhpWordTests/Writer/EPub3/PartTest.php | 28 ++++
.../Writer/EPub3/Style/AbstractStyleTest.php | 24 ++++
.../Writer/EPub3/Style/FontTest.php | 25 ++++
.../Writer/EPub3/Style/ParagraphTest.php | 25 ++++
.../Writer/EPub3/Style/TableTest.php | 28 ++++
tests/PhpWordTests/Writer/EPub3/StyleTest.php | 23 ++++
tests/PhpWordTests/Writer/EPub3Test.php | 126 +++++++++++++++++
39 files changed, 1695 insertions(+), 4 deletions(-)
create mode 100644 src/PhpWord/Writer/EPub3.php
create mode 100644 src/PhpWord/Writer/EPub3/Element/AbstractElement.php
create mode 100644 src/PhpWord/Writer/EPub3/Element/Image.php
create mode 100644 src/PhpWord/Writer/EPub3/Element/Text.php
create mode 100644 src/PhpWord/Writer/EPub3/Part.php
create mode 100644 src/PhpWord/Writer/EPub3/Part/AbstractPart.php
create mode 100644 src/PhpWord/Writer/EPub3/Part/Content.php
create mode 100644 src/PhpWord/Writer/EPub3/Part/ContentXhtml.php
create mode 100644 src/PhpWord/Writer/EPub3/Part/Manifest.php
create mode 100644 src/PhpWord/Writer/EPub3/Part/Meta.php
create mode 100644 src/PhpWord/Writer/EPub3/Part/Mimetype.php
create mode 100644 src/PhpWord/Writer/EPub3/Part/Nav.php
create mode 100644 src/PhpWord/Writer/EPub3/Style/AbstractStyle.php
create mode 100644 src/PhpWord/Writer/EPub3/Style/Font.php
create mode 100644 src/PhpWord/Writer/EPub3/Style/Paragraph.php
create mode 100644 src/PhpWord/Writer/EPub3/Style/Table.php
create mode 100644 src/PhpWord/Writer/WriterPartInterface.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Element/ImageTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Element/TextTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/ElementTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/AbstractPartTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/ContentTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/ManifestTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/MetaTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/MimetypeTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Part/NavTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/PartTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Style/AbstractStyleTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Style/FontTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Style/ParagraphTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/Style/TableTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3/StyleTest.php
create mode 100644 tests/PhpWordTests/Writer/EPub3Test.php
diff --git a/.gitignore b/.gitignore
index 0b9d0608d0..6918df72e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ _build
/build
phpunit.xml
composer.phar
+composer.lock
vendor
/report
/build
diff --git a/composer.json b/composer.json
index efebe941e7..5055105d16 100644
--- a/composer.json
+++ b/composer.json
@@ -108,10 +108,10 @@
"require": {
"php": "^7.1|^8.0",
"ext-dom": "*",
- "ext-gd": "*",
+ "ext-gd": "*",
+ "ext-zip": "*",
"ext-json": "*",
"ext-xml": "*",
- "ext-zip": "*",
"phpoffice/math": "^0.2"
},
"require-dev": {
diff --git a/docs/changes/1.x/1.4.0.md b/docs/changes/1.x/1.4.0.md
index f12bb05b0d..96b86a1bac 100644
--- a/docs/changes/1.x/1.4.0.md
+++ b/docs/changes/1.x/1.4.0.md
@@ -16,6 +16,8 @@
- Writer ODText: Support Default font color by [@MichaelPFrey](https://github.com/MichaelPFrey) in [#2735](https://github.com/PHPOffice/PHPWord/pull/2735)
- Add basic ruby text (phonetic guide) support for Word2007 and HTML Reader/Writer, RTF Writer, basic support for ODT writing by [@Deadpikle](https://github.com/Deadpikle) in [#2727](https://github.com/PHPOffice/PHPWord/pull/2727)
+- Added Support for Writer Epub3 by [@Sambit003](https://github.com/Sambit003) in [#2724](https://github.com/PHPOffice/PHPWord/pull/2724)
+
### Bug fixes
- Writer ODText: Support for images inside a textRun by [@Progi1984](https://github.com/Progi1984) fixing [#2240](https://github.com/PHPOffice/PHPWord/issues/2240) in [#2668](https://github.com/PHPOffice/PHPWord/pull/2668)
diff --git a/samples/Sample_Header.php b/samples/Sample_Header.php
index eab7033275..57bb10a4c6 100644
--- a/samples/Sample_Header.php
+++ b/samples/Sample_Header.php
@@ -31,7 +31,7 @@
}
// Set writers
-$writers = ['Word2007' => 'docx', 'ODText' => 'odt', 'RTF' => 'rtf', 'HTML' => 'html', 'PDF' => 'pdf'];
+$writers = ['Word2007' => 'docx', 'ODText' => 'odt', 'RTF' => 'rtf', 'HTML' => 'html', 'PDF' => 'pdf', 'EPub3' => 'epub'];
// Set PDF renderer
if (null === Settings::getPdfRendererPath()) {
diff --git a/src/PhpWord/IOFactory.php b/src/PhpWord/IOFactory.php
index 55c374a8b9..50c419cae2 100644
--- a/src/PhpWord/IOFactory.php
+++ b/src/PhpWord/IOFactory.php
@@ -36,7 +36,7 @@ abstract class IOFactory
*/
public static function createWriter(PhpWord $phpWord, $name = 'Word2007')
{
- if ($name !== 'WriterInterface' && !in_array($name, ['ODText', 'RTF', 'Word2007', 'HTML', 'PDF'], true)) {
+ if ($name !== 'WriterInterface' && !in_array($name, ['ODText', 'RTF', 'Word2007', 'HTML', 'PDF', 'EPub3'], true)) {
throw new Exception("\"{$name}\" is not a valid writer.");
}
diff --git a/src/PhpWord/Shared/ZipArchive.php b/src/PhpWord/Shared/ZipArchive.php
index 3ee3869191..bce7f18e0f 100644
--- a/src/PhpWord/Shared/ZipArchive.php
+++ b/src/PhpWord/Shared/ZipArchive.php
@@ -423,4 +423,15 @@ public function pclzipLocateName($filename)
return ($listIndex > -1) ? $listIndex : false;
}
+
+ /**
+ * Add an empty directory to the zip archive (emulate \ZipArchive).
+ *
+ * @param string $dirname Directory name to add to the zip archive
+ */
+ public function addEmptyDir(string $dirname): bool
+ {
+ // Create a directory entry by adding an empty file with trailing slash
+ return $this->addFromString(rtrim($dirname, '/') . '/', '');
+ }
}
diff --git a/src/PhpWord/Writer/EPub3.php b/src/PhpWord/Writer/EPub3.php
new file mode 100644
index 0000000000..b2ed9700d1
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3.php
@@ -0,0 +1,91 @@
+setPhpWord($phpWord);
+
+ // Create parts
+ $this->parts = [
+ 'Mimetype' => 'mimetype',
+ 'Content' => 'content.opf',
+ 'Toc' => 'toc.ncx',
+ 'Styles' => 'styles.css',
+ 'Manifest' => 'META-INF/container.xml',
+ 'Nav' => 'nav.xhtml',
+ 'ContentXhtml' => 'content.xhtml',
+ ];
+ foreach (array_keys($this->parts) as $partName) {
+ $partClass = static::class . '\\Part\\' . $partName;
+ if (class_exists($partClass)) {
+ /** @var WriterPartInterface $part */
+ $part = new $partClass($partName === 'Content' || $partName === 'ContentXhtml' ? $phpWord : null);
+ $part->setParentWriter($this);
+ $this->writerParts[strtolower($partName)] = $part;
+ }
+ }
+
+ // Set package paths
+ $this->mediaPaths = ['image' => 'Images/', 'object' => 'Objects/'];
+ }
+
+ /**
+ * Save PhpWord to file.
+ */
+ public function save(string $filename): void
+ {
+ $filename = $this->getTempFile($filename);
+ $zip = $this->getZipArchive($filename);
+
+ // Add mimetype first without compression
+ $zip->addFromString('mimetype', 'application/epub+zip');
+ $zip->addEmptyDir('META-INF');
+
+ // Add other files
+ foreach ($this->parts as $partName => $fileName) {
+ if ($fileName === '') {
+ continue;
+ }
+ $part = $this->getWriterPart($partName);
+ if (!$part instanceof AbstractPart) {
+ continue;
+ }
+ $zip->addFromString($fileName, $part->write());
+ }
+
+ // Close zip archive
+ $zip->close();
+
+ // Cleanup temp file
+ $this->cleanupTempFile();
+ }
+}
diff --git a/src/PhpWord/Writer/EPub3/Element/AbstractElement.php b/src/PhpWord/Writer/EPub3/Element/AbstractElement.php
new file mode 100644
index 0000000000..c95efec0f6
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3/Element/AbstractElement.php
@@ -0,0 +1,45 @@
+getXmlWriter();
+ $xmlWriter->setIndent(false);
+ $element = $this->getElement();
+ if (!$element instanceof ImageElement) {
+ return;
+ }
+ $mediaIndex = $element->getMediaIndex();
+ $target = 'media/image' . $mediaIndex . '.' . $element->getImageExtension();
+ if (!$this->withoutP) {
+ $xmlWriter->startElement('p');
+ }
+ $xmlWriter->startElement('img');
+ $xmlWriter->writeAttribute('src', $target);
+ $style = '';
+ if ($element->getStyle()->getWidth() !== null) {
+ $style .= 'width:' . $element->getStyle()->getWidth() . 'px;';
+ }
+ if ($element->getStyle()->getHeight() !== null) {
+ $style .= 'height:' . $element->getStyle()->getHeight() . 'px;';
+ }
+ if ($style !== '') {
+ $xmlWriter->writeAttribute('style', $style);
+ }
+ $xmlWriter->endElement(); // img
+ if (!$this->withoutP) {
+ $xmlWriter->endElement(); // p
+ }
+ }
+}
diff --git a/src/PhpWord/Writer/EPub3/Element/Text.php b/src/PhpWord/Writer/EPub3/Element/Text.php
new file mode 100644
index 0000000000..2e138c0509
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3/Element/Text.php
@@ -0,0 +1,50 @@
+getXmlWriter();
+ $xmlWriter->setIndent(true);
+ $xmlWriter->setIndentString(' ');
+ $element = $this->getElement();
+ if (!$element instanceof \PhpOffice\PhpWord\Element\Text) {
+ return;
+ }
+
+ $fontStyle = $element->getFontStyle();
+ $paragraphStyle = $element->getParagraphStyle();
+
+ if (!$this->withoutP) {
+ $xmlWriter->startElement('p');
+ if (is_string($paragraphStyle) && $paragraphStyle !== '') {
+ $xmlWriter->writeAttribute('class', $paragraphStyle);
+ }
+ }
+
+ if (!empty($fontStyle)) {
+ $xmlWriter->startElement('span');
+ if (is_string($fontStyle)) {
+ $xmlWriter->writeAttribute('class', $fontStyle);
+ }
+ }
+
+ $xmlWriter->text($element->getText());
+
+ if (!empty($fontStyle)) {
+ $xmlWriter->endElement(); // span
+ }
+
+ if (!$this->withoutP) {
+ $xmlWriter->endElement(); // p
+ }
+ }
+}
diff --git a/src/PhpWord/Writer/EPub3/Part.php b/src/PhpWord/Writer/EPub3/Part.php
new file mode 100644
index 0000000000..25dfa6654d
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3/Part.php
@@ -0,0 +1,45 @@
+parentWriter = $writer;
+ }
+
+ /**
+ * Get parent writer.
+ */
+ public function getParentWriter(): AbstractWriter
+ {
+ return $this->parentWriter;
+ }
+
+ /**
+ * Write part content.
+ */
+ abstract public function write(): string;
+}
diff --git a/src/PhpWord/Writer/EPub3/Part/Content.php b/src/PhpWord/Writer/EPub3/Part/Content.php
new file mode 100644
index 0000000000..217c56cc1f
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3/Part/Content.php
@@ -0,0 +1,130 @@
+phpWord = $phpWord;
+ }
+
+ /**
+ * Get XML Writer.
+ *
+ * @return XMLWriter
+ */
+ protected function getXmlWriter()
+ {
+ $xmlWriter = new XMLWriter();
+ $xmlWriter->openMemory();
+ $xmlWriter->startDocument('1.0', 'UTF-8');
+
+ return $xmlWriter;
+ }
+
+ /**
+ * Write part content.
+ */
+ public function write(): string
+ {
+ if ($this->phpWord === null) {
+ throw new Exception('No PhpWord assigned.');
+ }
+
+ $xmlWriter = $this->getXmlWriter();
+ $docInfo = $this->phpWord->getDocInfo();
+
+ // Write package
+ $xmlWriter->startElement('package');
+ $xmlWriter->writeAttribute('xmlns', 'http://www.idpf.org/2007/opf');
+ $xmlWriter->writeAttribute('version', '3.0');
+ $xmlWriter->writeAttribute('unique-identifier', 'book-id');
+ $xmlWriter->writeAttribute('xml:lang', 'en');
+
+ // Write metadata
+ $xmlWriter->startElement('metadata');
+ $xmlWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
+ $xmlWriter->writeAttribute('xmlns:opf', 'http://www.idpf.org/2007/opf');
+
+ // Required elements
+ $xmlWriter->startElement('dc:identifier');
+ $xmlWriter->writeAttribute('id', 'book-id');
+ $xmlWriter->text('book-id-' . uniqid());
+ $xmlWriter->endElement();
+ $xmlWriter->writeElement('dc:title', $docInfo->getTitle() ?: 'Untitled');
+ $xmlWriter->writeElement('dc:language', 'en');
+
+ // Required modified timestamp
+ $xmlWriter->startElement('meta');
+ $xmlWriter->writeAttribute('property', 'dcterms:modified');
+ $xmlWriter->text(date('Y-m-d\TH:i:s\Z'));
+ $xmlWriter->endElement();
+
+ $xmlWriter->endElement(); // metadata
+
+ // Write manifest
+ $xmlWriter->startElement('manifest');
+
+ // Add nav document (required)
+ $xmlWriter->startElement('item');
+ $xmlWriter->writeAttribute('id', 'nav');
+ $xmlWriter->writeAttribute('href', 'nav.xhtml');
+ $xmlWriter->writeAttribute('media-type', 'application/xhtml+xml');
+ $xmlWriter->writeAttribute('properties', 'nav');
+ $xmlWriter->endElement();
+
+ // Add content document
+ $xmlWriter->startElement('item');
+ $xmlWriter->writeAttribute('id', 'content');
+ $xmlWriter->writeAttribute('href', 'content.xhtml');
+ $xmlWriter->writeAttribute('media-type', 'application/xhtml+xml');
+ $xmlWriter->endElement();
+
+ $xmlWriter->endElement(); // manifest
+
+ // Write spine
+ $xmlWriter->startElement('spine');
+ $xmlWriter->startElement('itemref');
+ $xmlWriter->writeAttribute('idref', 'content');
+ $xmlWriter->endElement();
+ $xmlWriter->endElement(); // spine
+
+ $xmlWriter->endElement(); // package
+
+ return $xmlWriter->outputMemory(true);
+ }
+}
diff --git a/src/PhpWord/Writer/EPub3/Part/ContentXhtml.php b/src/PhpWord/Writer/EPub3/Part/ContentXhtml.php
new file mode 100644
index 0000000000..3ebd82638e
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3/Part/ContentXhtml.php
@@ -0,0 +1,117 @@
+phpWord = $phpWord;
+ }
+
+ /**
+ * Get XML Writer.
+ *
+ * @return XMLWriter
+ */
+ protected function getXmlWriter()
+ {
+ $xmlWriter = new XMLWriter();
+ $xmlWriter->openMemory();
+
+ return $xmlWriter;
+ }
+
+ /**
+ * Write part content.
+ */
+ public function write(): string
+ {
+ if ($this->phpWord === null) {
+ throw new \PhpOffice\PhpWord\Exception\Exception('No PhpWord assigned.');
+ }
+
+ $xmlWriter = $this->getXmlWriter();
+
+ $xmlWriter->startDocument('1.0', 'UTF-8');
+ $xmlWriter->startElement('html');
+ $xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
+ $xmlWriter->writeAttribute('xmlns:epub', 'http://www.idpf.org/2007/ops');
+ $xmlWriter->startElement('head');
+ $xmlWriter->writeElement('title', $this->phpWord->getDocInfo()->getTitle() ?: 'Untitled');
+ $xmlWriter->endElement(); // head
+ $xmlWriter->startElement('body');
+
+ // Write sections content
+ foreach ($this->phpWord->getSections() as $section) {
+ $xmlWriter->startElement('div');
+ $xmlWriter->writeAttribute('class', 'section');
+
+ foreach ($section->getElements() as $element) {
+ if ($element instanceof TextRun) {
+ $xmlWriter->startElement('p');
+ $this->writeTextRun($element, $xmlWriter);
+ $xmlWriter->endElement(); // p
+ } elseif (method_exists($element, 'getText')) {
+ $text = $element->getText();
+ $xmlWriter->startElement('p');
+ if ($text instanceof TextRun) {
+ $this->writeTextRun($text, $xmlWriter);
+ } elseif ($text !== null) {
+ $xmlWriter->text((string) $text);
+ }
+ $xmlWriter->endElement(); // p
+ }
+ }
+
+ $xmlWriter->endElement(); // div
+ }
+
+ $xmlWriter->endElement(); // body
+ $xmlWriter->endElement(); // html
+
+ return $xmlWriter->outputMemory(true);
+ }
+
+ protected function writeTextElement(\PhpOffice\PhpWord\Element\AbstractElement $textElement, XMLWriter $xmlWriter): void
+ {
+ if ($textElement instanceof Text) {
+ $text = $textElement->getText();
+ if ($text !== null) {
+ $xmlWriter->text((string) $text);
+ }
+ } elseif (is_object($textElement) && method_exists($textElement, 'getText')) {
+ $text = $textElement->getText();
+ if ($text instanceof TextRun) {
+ $this->writeTextRun($text, $xmlWriter);
+ } elseif ($text !== null) {
+ $xmlWriter->text((string) $text);
+ }
+ }
+ }
+
+ protected function writeTextRun(TextRun $textRun, XMLWriter $xmlWriter): void
+ {
+ foreach ($textRun->getElements() as $element) {
+ $this->writeTextElement($element, $xmlWriter);
+ }
+ }
+}
diff --git a/src/PhpWord/Writer/EPub3/Part/Manifest.php b/src/PhpWord/Writer/EPub3/Part/Manifest.php
new file mode 100644
index 0000000000..fcb0d1f428
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3/Part/Manifest.php
@@ -0,0 +1,40 @@
+';
+ $content .= '';
+ $content .= '';
+ $content .= '';
+ $content .= '';
+ $content .= '';
+
+ return $content;
+ }
+}
diff --git a/src/PhpWord/Writer/EPub3/Part/Meta.php b/src/PhpWord/Writer/EPub3/Part/Meta.php
new file mode 100644
index 0000000000..4b01097653
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3/Part/Meta.php
@@ -0,0 +1,74 @@
+openMemory();
+ $xmlWriter->startDocument('1.0', 'UTF-8');
+
+ return $xmlWriter;
+ }
+
+ /**
+ * Write part content.
+ */
+ public function write(): string
+ {
+ $xmlWriter = $this->getXmlWriter();
+
+ $xmlWriter->startElement('metadata');
+ $xmlWriter->writeAttribute('xmlns', 'http://www.idpf.org/2007/opf');
+ $xmlWriter->writeAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/');
+
+ // Write basic metadata
+ $title = $this->getParentWriter()->getPhpWord()->getDocInfo()->getTitle() ?: 'Sample EPub3 Document';
+ $xmlWriter->writeRaw('' . htmlspecialchars($title, ENT_QUOTES) . '');
+ $xmlWriter->writeElement('dc:language', 'en');
+ $xmlWriter->writeElement('dc:identifier', 'urn:uuid:12345');
+ $xmlWriter->writeAttribute('id', 'bookid');
+
+ // Write document info if available
+ $docInfo = $this->getParentWriter()->getPhpWord()->getDocInfo();
+ if ($docInfo->getCreator()) {
+ $xmlWriter->writeElement('dc:creator', $docInfo->getCreator());
+ }
+
+ // Write modification date
+ $xmlWriter->startElement('meta');
+ $xmlWriter->writeAttribute('property', 'dcterms:modified');
+ $xmlWriter->text('2023-01-01T00:00:00Z');
+ $xmlWriter->endElement();
+
+ $xmlWriter->endElement(); // metadata
+
+ return $xmlWriter->getData();
+ }
+}
diff --git a/src/PhpWord/Writer/EPub3/Part/Mimetype.php b/src/PhpWord/Writer/EPub3/Part/Mimetype.php
new file mode 100644
index 0000000000..8e5b7d41ba
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3/Part/Mimetype.php
@@ -0,0 +1,33 @@
+openMemory();
+
+ return $xmlWriter;
+ }
+
+ public function write(): string
+ {
+ $xmlWriter = $this->getXmlWriter();
+
+ $xmlWriter->startDocument('1.0', 'UTF-8');
+ $xmlWriter->startElement('html');
+ $xmlWriter->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
+ $xmlWriter->writeAttribute('xmlns:epub', 'http://www.idpf.org/2007/ops');
+
+ $xmlWriter->startElement('head');
+ $xmlWriter->writeElement('title', 'Navigation');
+ $xmlWriter->endElement(); // head
+
+ $xmlWriter->startElement('body');
+ $xmlWriter->startElement('nav');
+ $xmlWriter->writeAttribute('epub:type', 'toc');
+ $xmlWriter->writeAttribute('id', 'toc');
+
+ // Add navigation items here if needed
+ $xmlWriter->writeElement('h1', 'Table of Contents');
+ $xmlWriter->startElement('ol');
+ // Add at least one list item to satisfy EPUB 3.3 requirements
+ $xmlWriter->startElement('li');
+ $xmlWriter->startElement('a');
+ $xmlWriter->writeAttribute('href', 'content.xhtml');
+ $xmlWriter->text('Content');
+ $xmlWriter->endElement(); // a
+ $xmlWriter->endElement(); // li
+ $xmlWriter->endElement(); // ol
+
+ $xmlWriter->endElement(); // nav
+ $xmlWriter->endElement(); // body
+ $xmlWriter->endElement(); // html
+
+ return $xmlWriter->outputMemory();
+ }
+}
diff --git a/src/PhpWord/Writer/EPub3/Style/AbstractStyle.php b/src/PhpWord/Writer/EPub3/Style/AbstractStyle.php
new file mode 100644
index 0000000000..82cd488b62
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3/Style/AbstractStyle.php
@@ -0,0 +1,75 @@
+parentWriter = $writer;
+
+ return $this;
+ }
+
+ /**
+ * Set XML Writer.
+ */
+ public function setXmlWriter(XMLWriter $writer): self
+ {
+ $this->xmlWriter = $writer;
+
+ return $this;
+ }
+
+ /**
+ * Get parent writer.
+ */
+ public function getParentWriter(): AbstractWriter
+ {
+ return $this->parentWriter;
+ }
+
+ /**
+ * Write style content.
+ */
+ abstract public function write(): string;
+}
diff --git a/src/PhpWord/Writer/EPub3/Style/Font.php b/src/PhpWord/Writer/EPub3/Style/Font.php
new file mode 100644
index 0000000000..4e35eeee41
--- /dev/null
+++ b/src/PhpWord/Writer/EPub3/Style/Font.php
@@ -0,0 +1,39 @@
+xmlWriter = new XMLWriter();
+ $style = new ImageStyle();
+ $style->setWidth(100);
+ $style->setHeight(100);
+ $this->element = new Image('tests/PhpWordTests/_files/images/earth.jpg', $style);
+ $this->writer = new ImageWriter($this->xmlWriter, $this->element);
+ }
+
+ public function testWrite(): void
+ {
+ $this->writer->write();
+
+ $expected = '

';
+ self::assertEquals($expected, $this->xmlWriter->getData());
+ }
+
+ public function testWriteWithoutP(): void
+ {
+ $style = new ImageStyle();
+ $style->setWidth(100);
+ $style->setHeight(100);
+ $this->element = new Image('tests/PhpWordTests/_files/images/earth.jpg', $style);
+ $this->writer = new ImageWriter($this->xmlWriter, $this->element, true);
+
+ $this->writer->write();
+
+ $expected = '
';
+ self::assertEquals($expected, $this->xmlWriter->getData());
+ }
+
+ public function testWriteWithInvalidElement(): void
+ {
+ $invalidElement = $this->createMock(\PhpOffice\PhpWord\Element\AbstractElement::class);
+ $writer = new ImageWriter($this->xmlWriter, $invalidElement);
+
+ $writer->write();
+
+ self::assertEquals('', $this->xmlWriter->getData());
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/Element/TextTest.php b/tests/PhpWordTests/Writer/EPub3/Element/TextTest.php
new file mode 100644
index 0000000000..38490691d5
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/Element/TextTest.php
@@ -0,0 +1,83 @@
+xmlWriter = new XMLWriter();
+ $this->element = new Text('Sample Text');
+ $this->writer = new TextWriter($this->xmlWriter, $this->element);
+ }
+
+ public function testWrite(): void
+ {
+ $this->writer->write();
+
+ $expected = "\n Sample Text\n
\n";
+ self::assertEquals($expected, $this->xmlWriter->getData());
+ }
+
+ public function testWriteWithFontStyle(): void
+ {
+ $this->element->setFontStyle('customStyle');
+
+ $this->writer->write();
+
+ $expected = "\n Sample Text\n
\n";
+ self::assertEquals($expected, $this->xmlWriter->getData());
+ }
+
+ public function testWriteWithParagraphStyle(): void
+ {
+ $this->element->setParagraphStyle('paragraphStyle');
+
+ $this->writer->write();
+
+ $expected = "\n Sample Text\n
\n";
+ self::assertEquals($expected, $this->xmlWriter->getData());
+ }
+
+ public function testWriteWithoutP(): void
+ {
+ $text = new Text('Sample Text');
+ $xmlWriter = new XMLWriter();
+ $this->writer = new TextWriter($xmlWriter, $text, true);
+
+ $this->writer->write();
+
+ $expected = "Sample Text\n";
+ self::assertEquals($expected, $xmlWriter->getData());
+ }
+
+ public function testWriteWithInvalidElement(): void
+ {
+ $invalidElement = $this->createMock(\PhpOffice\PhpWord\Element\AbstractElement::class);
+ $writer = new TextWriter($this->xmlWriter, $invalidElement);
+
+ $writer->write();
+
+ self::assertEquals('', $this->xmlWriter->getData());
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/ElementTest.php b/tests/PhpWordTests/Writer/EPub3/ElementTest.php
new file mode 100644
index 0000000000..af745d9176
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/ElementTest.php
@@ -0,0 +1,26 @@
+expectException(\PhpOffice\PhpWord\Exception\Exception::class);
+
+ $element = $this->createMock(AbstractElement::class);
+ WriterElement::getElementClass($element);
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/Part/AbstractPartTest.php b/tests/PhpWordTests/Writer/EPub3/Part/AbstractPartTest.php
new file mode 100644
index 0000000000..5151eea24c
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/Part/AbstractPartTest.php
@@ -0,0 +1,28 @@
+part = $this->getMockForAbstractClass(AbstractPart::class);
+ }
+
+ public function testParentWriter(): void
+ {
+ $writer = new EPub3();
+ $this->part->setParentWriter($writer);
+
+ self::assertInstanceOf(EPub3::class, $this->part->getParentWriter());
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/Part/ContentTest.php b/tests/PhpWordTests/Writer/EPub3/Part/ContentTest.php
new file mode 100644
index 0000000000..6745ea2eda
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/Part/ContentTest.php
@@ -0,0 +1,38 @@
+content = new Content($phpWord);
+ $section = $phpWord->addSection();
+ $section->addText('Test content');
+
+ $writer = new EPub3($phpWord);
+ $this->content->setParentWriter($writer);
+ }
+
+ public function testWrite(): void
+ {
+ $result = $this->content->write();
+
+ self::assertIsString($result);
+ self::assertStringContainsString('', $result);
+ self::assertStringContainsString('', $result);
+ self::assertStringContainsString('', $result);
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/Part/ManifestTest.php b/tests/PhpWordTests/Writer/EPub3/Part/ManifestTest.php
new file mode 100644
index 0000000000..d1755da9af
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/Part/ManifestTest.php
@@ -0,0 +1,35 @@
+manifest = new Manifest();
+ $phpWord = new PhpWord();
+ $writer = new EPub3($phpWord);
+ $this->manifest->setParentWriter($writer);
+ }
+
+ public function testWrite(): void
+ {
+ $result = $this->manifest->write();
+
+ self::assertStringContainsString('', $result);
+ self::assertIsString($result);
+ self::assertStringContainsString('', $result);
+ self::assertStringContainsString('meta = new Meta();
+ $phpWord = new PhpWord();
+ $writer = new EPub3($phpWord);
+ $this->meta->setParentWriter($writer);
+ }
+
+ public function testWrite(): void
+ {
+ $result = $this->meta->write();
+
+ self::assertIsString($result);
+ self::assertStringContainsString('', $result);
+ self::assertStringContainsString('getDocInfo();
+ $properties->setCreator('PHPWord');
+ $properties->setTitle('Test Title');
+ $properties->setKeywords('test, keywords');
+
+ $writer = new EPub3($phpWord);
+ $this->meta->setParentWriter($writer);
+
+ $expected = '\nTest Titleenurn:uuid:12345PHPWord2023-01-01T00:00:00Z';
+
+ $result = $this->meta->write();
+
+ self::assertStringContainsString('PHPWord', $result);
+ self::assertStringContainsString('Test Title', $result);
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/Part/MimetypeTest.php b/tests/PhpWordTests/Writer/EPub3/Part/MimetypeTest.php
new file mode 100644
index 0000000000..c1cd57f705
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/Part/MimetypeTest.php
@@ -0,0 +1,32 @@
+mimetype = new Mimetype();
+ $phpWord = new PhpWord();
+ $writer = new EPub3($phpWord);
+ $this->mimetype->setParentWriter($writer);
+ }
+
+ public function testWrite(): void
+ {
+ $result = $this->mimetype->write();
+
+ self::assertIsString($result);
+ self::assertEquals('application/epub+zip', $result);
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/Part/NavTest.php b/tests/PhpWordTests/Writer/EPub3/Part/NavTest.php
new file mode 100644
index 0000000000..a050f95511
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/Part/NavTest.php
@@ -0,0 +1,49 @@
+write();
+
+ // Test that valid XML is generated
+ $dom = new DOMDocument();
+ $dom->loadXML($xml);
+
+ // Test required XML elements and attributes exist
+ self::assertEquals('html', $dom->documentElement->nodeName);
+ self::assertEquals('http://www.w3.org/1999/xhtml', $dom->documentElement->getAttribute('xmlns'));
+ self::assertEquals('http://www.idpf.org/2007/ops', $dom->documentElement->getAttribute('xmlns:epub'));
+
+ // Test nav element
+ $navElements = $dom->getElementsByTagName('nav');
+ self::assertEquals(1, $navElements->length);
+ $navElement = $navElements->item(0);
+ self::assertEquals('toc', $navElement->getAttribute('epub:type'));
+ self::assertEquals('toc', $navElement->getAttribute('id'));
+
+ // Test title exists
+ $titleElements = $dom->getElementsByTagName('title');
+ self::assertEquals(1, $titleElements->length);
+ self::assertEquals('Navigation', $titleElements->item(0)->nodeValue);
+
+ // Test TOC header exists
+ $h1Elements = $dom->getElementsByTagName('h1');
+ self::assertEquals(1, $h1Elements->length);
+ self::assertEquals('Table of Contents', $h1Elements->item(0)->nodeValue);
+
+ // Test TOC list structure exists
+ $olElements = $dom->getElementsByTagName('ol');
+ self::assertEquals(1, $olElements->length);
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/PartTest.php b/tests/PhpWordTests/Writer/EPub3/PartTest.php
new file mode 100644
index 0000000000..ed91c60bcb
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/PartTest.php
@@ -0,0 +1,28 @@
+expectException(\PhpOffice\PhpWord\Exception\Exception::class);
+
+ Part::getPartClass('InvalidType');
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/Style/AbstractStyleTest.php b/tests/PhpWordTests/Writer/EPub3/Style/AbstractStyleTest.php
new file mode 100644
index 0000000000..fb9135f0cd
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/Style/AbstractStyleTest.php
@@ -0,0 +1,24 @@
+getMockForAbstractClass(AbstractStyle::class);
+
+ $result = $style->setParentWriter($parentWriter);
+
+ self::assertSame($style, $result);
+ self::assertSame($parentWriter, $style->getParentWriter());
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/Style/FontTest.php b/tests/PhpWordTests/Writer/EPub3/Style/FontTest.php
new file mode 100644
index 0000000000..26572af3a4
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/Style/FontTest.php
@@ -0,0 +1,25 @@
+write();
+
+ self::assertStringContainsString('font-family: "Times New Roman", Times, serif;', $content);
+ self::assertStringContainsString('font-size: 12pt;', $content);
+ self::assertStringContainsString('color: #000000;', $content);
+ self::assertStringStartsWith('body {', $content);
+ self::assertStringEndsWith('}', $content);
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/Style/ParagraphTest.php b/tests/PhpWordTests/Writer/EPub3/Style/ParagraphTest.php
new file mode 100644
index 0000000000..bcaab0bab1
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/Style/ParagraphTest.php
@@ -0,0 +1,25 @@
+write();
+
+ self::assertStringContainsString('margin-top: 0;', $content);
+ self::assertStringContainsString('margin-bottom: 1em;', $content);
+ self::assertStringContainsString('text-align: left;', $content);
+ self::assertStringStartsWith('p {', $content);
+ self::assertStringEndsWith('}', $content);
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/Style/TableTest.php b/tests/PhpWordTests/Writer/EPub3/Style/TableTest.php
new file mode 100644
index 0000000000..1d6ff0d6bb
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/Style/TableTest.php
@@ -0,0 +1,28 @@
+write();
+
+ self::assertStringContainsString('border-collapse: collapse;', $content);
+ self::assertStringContainsString('width: 100%;', $content);
+ self::assertStringContainsString('border: 1px solid black;', $content);
+ self::assertStringContainsString('padding: 8px;', $content);
+ self::assertStringContainsString('text-align: left;', $content);
+ self::assertStringContainsString('table {', $content);
+ self::assertStringContainsString('th, td {', $content);
+ self::assertStringEndsWith('}', $content);
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3/StyleTest.php b/tests/PhpWordTests/Writer/EPub3/StyleTest.php
new file mode 100644
index 0000000000..15db0d581d
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3/StyleTest.php
@@ -0,0 +1,23 @@
+setXmlWriter($xmlWriter);
+ $object->write();
+
+ self::assertEquals('', $xmlWriter->getData());
+ }
+ }
+}
diff --git a/tests/PhpWordTests/Writer/EPub3Test.php b/tests/PhpWordTests/Writer/EPub3Test.php
new file mode 100644
index 0000000000..acdbb5e32f
--- /dev/null
+++ b/tests/PhpWordTests/Writer/EPub3Test.php
@@ -0,0 +1,126 @@
+getPhpWord());
+ self::assertEquals('./', $object->getDiskCachingDirectory());
+ foreach (['Content', 'Manifest', 'Mimetype'] as $part) {
+ self::assertInstanceOf(
+ "PhpOffice\\PhpWord\\Writer\\Epub3\\Part\\{$part}",
+ $object->getWriterPart($part)
+ );
+ self::assertInstanceOf(
+ 'PhpOffice\\PhpWord\\Writer\\Epub3',
+ $object->getWriterPart($part)->getParentWriter()
+ );
+ }
+ }
+
+ /**
+ * Test construction with null.
+ */
+ public function testConstructWithNull(): void
+ {
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('No PhpWord assigned.');
+
+ $writer = new EPub3();
+ $writer->getWriterPart('content')->write();
+ }
+
+ /**
+ * Test saving document.
+ */
+ public function testSave(): void
+ {
+ $imageSrc = __DIR__ . '/../_files/images/PhpWord.png';
+ $file = __DIR__ . '/../_files/temp.epub';
+
+ $phpWord = new PhpWord();
+ $section = $phpWord->addSection();
+ $section->addText('Test 1');
+ $section->addTextBreak();
+ $section->addText('Test 2', null, ['alignment' => Jc::CENTER]);
+ $section->addLink('https://github.com/PHPOffice/PHPWord');
+ $section->addTitle('Test', 1);
+ $section->addPageBreak();
+ $section->addImage($imageSrc);
+ $writer = new EPub3($phpWord);
+ $writer->save($file);
+ self::assertFileExists($file);
+ unlink($file);
+ }
+
+ /**
+ * Test PHP output.
+ */
+ public function testSavePhpOutput(): void
+ {
+ $phpWord = new PhpWord();
+ $section = $phpWord->addSection();
+ $section->addText('Test');
+ $writer = new EPub3($phpWord);
+ ob_start();
+ $writer->save('php://output');
+ $contents = ob_get_contents();
+ self::assertTrue(ob_end_clean());
+ self::assertNotEmpty($contents);
+ }
+
+ /**
+ * Test disk caching.
+ */
+ public function testSetGetUseDiskCaching(): void
+ {
+ $object = new EPub3();
+ $object->setUseDiskCaching(true, PHPWORD_TESTS_BASE_DIR);
+ self::assertTrue($object->isUseDiskCaching());
+ self::assertEquals(PHPWORD_TESTS_BASE_DIR, $object->getDiskCachingDirectory());
+ }
+
+ /**
+ * Test disk caching exception.
+ */
+ public function testSetUseDiskCachingException(): void
+ {
+ $this->expectException(Exception::class);
+ $dir = implode(DIRECTORY_SEPARATOR, [PHPWORD_TESTS_BASE_DIR, 'foo']);
+
+ $object = new EPub3();
+ $object->setUseDiskCaching(true, $dir);
+ }
+}
From 2f270f294fc476c2f723533e9fe71cb6863c4b1b Mon Sep 17 00:00:00 2001
From: Progi1984
Date: Mon, 3 Feb 2025 08:15:35 +0100
Subject: [PATCH 2/3] Reader HTML: Support font styles for h1/h6 (#2737)
---
docs/changes/1.x/1.4.0.md | 1 +
src/PhpWord/Shared/Html.php | 33 +++++++++----------
src/PhpWord/Style/Font.php | 6 ++--
tests/PhpWordTests/Shared/HtmlTest.php | 45 ++++++++++++++++++++++++++
4 files changed, 63 insertions(+), 22 deletions(-)
diff --git a/docs/changes/1.x/1.4.0.md b/docs/changes/1.x/1.4.0.md
index 96b86a1bac..0f225edd64 100644
--- a/docs/changes/1.x/1.4.0.md
+++ b/docs/changes/1.x/1.4.0.md
@@ -15,6 +15,7 @@
- Writer HTML: Support Default font color by [@MichaelPFrey](https://github.com/MichaelPFrey) in [#2731](https://github.com/PHPOffice/PHPWord/pull/2731)
- Writer ODText: Support Default font color by [@MichaelPFrey](https://github.com/MichaelPFrey) in [#2735](https://github.com/PHPOffice/PHPWord/pull/2735)
- Add basic ruby text (phonetic guide) support for Word2007 and HTML Reader/Writer, RTF Writer, basic support for ODT writing by [@Deadpikle](https://github.com/Deadpikle) in [#2727](https://github.com/PHPOffice/PHPWord/pull/2727)
+- Reader HTML: Support font styles for h1/h6 by [@Progi1984](https://github.com/Progi1984) fixing [#2619](https://github.com/PHPOffice/PHPWord/issues/2619) in [#2737](https://github.com/PHPOffice/PHPWord/pull/2737)
- Added Support for Writer Epub3 by [@Sambit003](https://github.com/Sambit003) in [#2724](https://github.com/PHPOffice/PHPWord/pull/2724)
diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php
index cdaf8d8c87..170dc5dff3 100644
--- a/src/PhpWord/Shared/Html.php
+++ b/src/PhpWord/Shared/Html.php
@@ -211,16 +211,16 @@ protected static function parseNode($node, $element, $styles = [], $data = []):
// Node mapping table
$nodes = [
- // $method $node $element $styles $data $argument1 $argument2
- 'p' => ['Paragraph', $node, $element, $styles, null, null, null],
- 'h1' => ['Heading', null, $element, $styles, null, 'Heading1', null],
- 'h2' => ['Heading', null, $element, $styles, null, 'Heading2', null],
- 'h3' => ['Heading', null, $element, $styles, null, 'Heading3', null],
- 'h4' => ['Heading', null, $element, $styles, null, 'Heading4', null],
- 'h5' => ['Heading', null, $element, $styles, null, 'Heading5', null],
- 'h6' => ['Heading', null, $element, $styles, null, 'Heading6', null],
- '#text' => ['Text', $node, $element, $styles, null, null, null],
- 'strong' => ['Property', null, null, $styles, null, 'bold', true],
+ // $method $node $element $styles $data $argument1 $argument2
+ 'p' => ['Paragraph', $node, $element, $styles, null, null, null],
+ 'h1' => ['Heading', $node, $element, $styles, null, 'Heading1', null],
+ 'h2' => ['Heading', $node, $element, $styles, null, 'Heading2', null],
+ 'h3' => ['Heading', $node, $element, $styles, null, 'Heading3', null],
+ 'h4' => ['Heading', $node, $element, $styles, null, 'Heading4', null],
+ 'h5' => ['Heading', $node, $element, $styles, null, 'Heading5', null],
+ 'h6' => ['Heading', $node, $element, $styles, null, 'Heading6', null],
+ '#text' => ['Text', $node, $element, $styles, null, null, null],
+ 'strong' => ['Property', null, null, $styles, null, 'bold', true],
'b' => ['Property', null, null, $styles, null, 'bold', true],
'em' => ['Property', null, null, $styles, null, 'italic', true],
'i' => ['Property', null, null, $styles, null, 'italic', true],
@@ -345,21 +345,18 @@ protected static function parseInput($node, $element, &$styles): void
/**
* Parse heading node.
*
- * @param AbstractContainer $element
- * @param array &$styles
* @param string $argument1 Name of heading style
*
- * @return TextRun
- *
* @todo Think of a clever way of defining header styles, now it is only based on the assumption, that
* Heading1 - Heading6 are already defined somewhere
*/
- protected static function parseHeading($element, &$styles, $argument1)
+ protected static function parseHeading(DOMNode $node, AbstractContainer $element, array &$styles, string $argument1): TextRun
{
- $styles['paragraph'] = $argument1;
- $newElement = $element->addTextRun($styles['paragraph']);
+ $style = new Paragraph();
+ $style->setStyleName($argument1);
+ $style->setStyleByArray(self::parseInlineStyle($node, $styles['paragraph']));
- return $newElement;
+ return $element->addTextRun($style);
}
/**
diff --git a/src/PhpWord/Style/Font.php b/src/PhpWord/Style/Font.php
index fe7f621454..f03e8899d1 100644
--- a/src/PhpWord/Style/Font.php
+++ b/src/PhpWord/Style/Font.php
@@ -109,7 +109,7 @@ class Font extends AbstractStyle
/**
* Font color.
*
- * @var string
+ * @var null|string
*/
private $color;
@@ -426,10 +426,8 @@ public function setSize($value = null)
/**
* Get font color.
- *
- * @return string
*/
- public function getColor()
+ public function getColor(): ?string
{
return $this->color;
}
diff --git a/tests/PhpWordTests/Shared/HtmlTest.php b/tests/PhpWordTests/Shared/HtmlTest.php
index 117f5cb01d..42d8aa598b 100644
--- a/tests/PhpWordTests/Shared/HtmlTest.php
+++ b/tests/PhpWordTests/Shared/HtmlTest.php
@@ -22,12 +22,15 @@
use PhpOffice\PhpWord\ComplexType\RubyProperties;
use PhpOffice\PhpWord\Element\Section;
use PhpOffice\PhpWord\Element\Table;
+use PhpOffice\PhpWord\Element\Text;
+use PhpOffice\PhpWord\Element\TextRun;
use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\Shared\Converter;
use PhpOffice\PhpWord\Shared\Html;
use PhpOffice\PhpWord\SimpleType\Jc;
use PhpOffice\PhpWord\SimpleType\LineSpacingRule;
use PhpOffice\PhpWord\SimpleType\TblWidth;
+use PhpOffice\PhpWord\Style\Font;
use PhpOffice\PhpWord\Style\Paragraph;
use PhpOffice\PhpWordTests\AbstractWebServerEmbedded;
use PhpOffice\PhpWordTests\TestHelperDOCX;
@@ -105,6 +108,48 @@ public function testParseFullHtml(): void
self::assertCount(2, $section->getElements());
}
+ public function testParseHeader(): void
+ {
+ $phpWord = new PhpWord();
+ $section = $phpWord->addSection();
+ Html::addHtml($section, 'Text
');
+
+ self::assertCount(1, $section->getElements());
+ $element = $section->getElement(0);
+ self::assertInstanceOf(TextRun::class, $element);
+ self::assertInstanceOf(Paragraph::class, $element->getParagraphStyle());
+ self::assertEquals('Heading1', $element->getParagraphStyle()->getStyleName());
+ self::assertEquals('', $element->getParagraphStyle()->getAlignment());
+ self::assertEquals('Text', $element->getText());
+ self::assertCount(1, $element->getElements());
+ $subElement = $element->getElement(0);
+ self::assertInstanceOf(Text::class, $subElement);
+ self::assertInstanceOf(Font::class, $subElement->getFontStyle());
+ self::assertNull($subElement->getFontStyle()->getColor());
+ self::assertEquals('Text', $subElement->getText());
+ }
+
+ public function testParseHeaderStyle(): void
+ {
+ $phpWord = new PhpWord();
+ $section = $phpWord->addSection();
+ Html::addHtml($section, 'Text
');
+
+ self::assertCount(1, $section->getElements());
+ $element = $section->getElement(0);
+ self::assertInstanceOf(TextRun::class, $element);
+ self::assertInstanceOf(Paragraph::class, $element->getParagraphStyle());
+ self::assertEquals('Heading1', $element->getParagraphStyle()->getStyleName());
+ self::assertEquals('center', $element->getParagraphStyle()->getAlignment());
+ self::assertEquals('Text', $element->getText());
+ self::assertCount(1, $element->getElements());
+ $subElement = $element->getElement(0);
+ self::assertInstanceOf(Text::class, $subElement);
+ self::assertInstanceOf(Font::class, $subElement->getFontStyle());
+ self::assertEquals('ff0000', $subElement->getFontStyle()->getColor());
+ self::assertEquals('Text', $subElement->getText());
+ }
+
/**
* Test HTML entities.
*/
From 1be7a807f2a89c48ddac833e2de01dc29d6c0afb Mon Sep 17 00:00:00 2001
From: Progi1984
Date: Mon, 3 Feb 2025 09:13:23 +0100
Subject: [PATCH 3/3] Writer HTML: Fixed null string for Text Elements (#2738)
Co-authored-by: armagedon007 <000yurik000@gmail.com>
---
docs/changes/1.x/1.4.0.md | 4 +-
src/PhpWord/Element/Link.php | 4 +-
src/PhpWord/Element/Text.php | 4 +-
src/PhpWord/Writer/HTML/Element/Text.php | 2 +-
.../Writer/HTML/Element/TextTest.php | 43 +++++++++++++++++++
5 files changed, 48 insertions(+), 9 deletions(-)
create mode 100644 tests/PhpWordTests/Writer/HTML/Element/TextTest.php
diff --git a/docs/changes/1.x/1.4.0.md b/docs/changes/1.x/1.4.0.md
index 0f225edd64..2afadebd5f 100644
--- a/docs/changes/1.x/1.4.0.md
+++ b/docs/changes/1.x/1.4.0.md
@@ -16,8 +16,7 @@
- Writer ODText: Support Default font color by [@MichaelPFrey](https://github.com/MichaelPFrey) in [#2735](https://github.com/PHPOffice/PHPWord/pull/2735)
- Add basic ruby text (phonetic guide) support for Word2007 and HTML Reader/Writer, RTF Writer, basic support for ODT writing by [@Deadpikle](https://github.com/Deadpikle) in [#2727](https://github.com/PHPOffice/PHPWord/pull/2727)
- Reader HTML: Support font styles for h1/h6 by [@Progi1984](https://github.com/Progi1984) fixing [#2619](https://github.com/PHPOffice/PHPWord/issues/2619) in [#2737](https://github.com/PHPOffice/PHPWord/pull/2737)
-
-- Added Support for Writer Epub3 by [@Sambit003](https://github.com/Sambit003) in [#2724](https://github.com/PHPOffice/PHPWord/pull/2724)
+- Writer EPub3: Basic support by [@Sambit003](https://github.com/Sambit003) fixing [#55](https://github.com/PHPOffice/PHPWord/issues/55) in [#2724](https://github.com/PHPOffice/PHPWord/pull/2724)
### Bug fixes
@@ -27,6 +26,7 @@
- Reader Word2007: Respect paragraph indent units by [@tugmaks](https://github.com/tugmaks) & [@Progi1984](https://github.com/Progi1984) fixing [#507](https://github.com/PHPOffice/PHPWord/issues/507) in [#2726](https://github.com/PHPOffice/PHPWord/pull/2726)
- Reader Word2007: Support Header elements within Title elements by [@SpraxDev](https://github.com/SpraxDev) fixing [#2616](https://github.com/PHPOffice/PHPWord/issues/2616), [#2426](https://github.com/PHPOffice/PHPWord/issues/2426) in [#2674](https://github.com/PHPOffice/PHPWord/pull/2674)
- Reader HTML: Support for inherit value for property line-height by [@Progi1984](https://github.com/Progi1984) fixing [#2683](https://github.com/PHPOffice/PHPWord/issues/2683) in [#2733](https://github.com/PHPOffice/PHPWord/pull/2733)
+- Writer HTML: Fixed null string for Text Elements by [@armagedon007](https://github.com/armagedon007) and [@Progi1984](https://github.com/Progi1984) in [#2738](https://github.com/PHPOffice/PHPWord/pull/2738)
### Miscellaneous
diff --git a/src/PhpWord/Element/Link.php b/src/PhpWord/Element/Link.php
index 755fd7ba0f..a0deb0a5ec 100644
--- a/src/PhpWord/Element/Link.php
+++ b/src/PhpWord/Element/Link.php
@@ -99,10 +99,8 @@ public function getSource()
/**
* Get link text.
- *
- * @return string
*/
- public function getText()
+ public function getText(): string
{
return $this->text;
}
diff --git a/src/PhpWord/Element/Text.php b/src/PhpWord/Element/Text.php
index ea5f5f87a4..f20b273e02 100644
--- a/src/PhpWord/Element/Text.php
+++ b/src/PhpWord/Element/Text.php
@@ -147,10 +147,8 @@ public function setText($text)
/**
* Get Text content.
- *
- * @return ?string
*/
- public function getText()
+ public function getText(): ?string
{
return $this->text;
}
diff --git a/src/PhpWord/Writer/HTML/Element/Text.php b/src/PhpWord/Writer/HTML/Element/Text.php
index 312d3a19c2..7be95a5c76 100644
--- a/src/PhpWord/Writer/HTML/Element/Text.php
+++ b/src/PhpWord/Writer/HTML/Element/Text.php
@@ -73,7 +73,7 @@ public function write()
/** @var \PhpOffice\PhpWord\Element\Text $element Type hint */
$element = $this->element;
- $text = $this->parentWriter->escapeHTML($element->getText());
+ $text = $this->parentWriter->escapeHTML($element->getText() ?? '');
if (!$this->withoutP && !trim($text)) {
$text = ' ';
}
diff --git a/tests/PhpWordTests/Writer/HTML/Element/TextTest.php b/tests/PhpWordTests/Writer/HTML/Element/TextTest.php
new file mode 100644
index 0000000000..2dfdd53be3
--- /dev/null
+++ b/tests/PhpWordTests/Writer/HTML/Element/TextTest.php
@@ -0,0 +1,43 @@
+ ' . PHP_EOL, $object->write());
+ }
+
+ public function testHTMLEmptyString(): void
+ {
+ $writer = new HTML();
+ $object = new Text($writer, new BaseText(''));
+
+ self::assertEquals('
' . PHP_EOL, $object->write());
+ }
+}