From 7301f7d8021969fb6cd4636b33a828b522dfe398 Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Sun, 24 Dec 2023 15:58:37 -0800
Subject: [PATCH 1/7] Html Reader Process Titles as Headings Not Paragraphs
Fix #1692. Builds on work started some time ago by @0b10011, to whom primary credit is due.
Html Reader does not process the `head` section of the document, and, in particular, does not process its `style` section. It will, however, process inline styles, so 0b10011's model of adding the title as a text run (with styles) will work well once this change is applied. However, that model would not deal with the alternative method of assigning a Title Style, and just adding the title as text. In order to accommodate that, I have removed the declaration of heading font styles in the head section, and now generate them all inline in the body. This has the added benefit of being able to read the doc as html, then saving it as docx, preserving, at least in part, any user-defined font styles. Note that html does have pre-defined title styles, but docx does not.
@constip suggests in the original issue that margin top and bottom are being applied too frequently. I believe that was addressed by recently merged PR #2475. It is also suggested that the `*` css selector be dropped in favor of `body`. 2475 added the body selector. I agree that this renders the `*` selector unnecessary, and, as stated in the issue, it can cause problems. This PR drops that selector. It is also suggested that `loadHTML` be used instead of `loadXML`. This is not as easy a change as it seems, because loadHTML uses ISO-8859-1 charset rather than UTF-8, so I will not attempt that change.
---
docs/changes/1.x/1.3.0.md | 15 +++++
src/PhpWord/Shared/Html.php | 34 +++++-----
src/PhpWord/Writer/HTML/Element/Title.php | 8 ++-
src/PhpWord/Writer/HTML/Part/Head.php | 7 +-
.../PhpWordTests/Shared/HtmlHeadingsTest.php | 66 +++++++++++++++++++
tests/PhpWordTests/Writer/HTML/FontTest.php | 56 ++++++++--------
tests/PhpWordTests/Writer/HTML/Helper.php | 11 +++-
tests/PhpWordTests/Writer/HTML/PartTest.php | 10 ++-
8 files changed, 155 insertions(+), 52 deletions(-)
create mode 100644 docs/changes/1.x/1.3.0.md
create mode 100644 tests/PhpWordTests/Shared/HtmlHeadingsTest.php
diff --git a/docs/changes/1.x/1.3.0.md b/docs/changes/1.x/1.3.0.md
new file mode 100644
index 0000000000..432394dbf6
--- /dev/null
+++ b/docs/changes/1.x/1.3.0.md
@@ -0,0 +1,15 @@
+# [1.3.0](https://github.com/PHPOffice/PHPWord/tree/1.3.0) (WIP)
+
+[Full Changelog](https://github.com/PHPOffice/PHPWord/compare/1.2.0...1.3.0)
+
+## Enhancements
+
+### Bug fixes
+
+- MsDoc Reader : Correct Font Size Calculation by [@oleibman](https://github.com/oleibman) Issue [#2526](https://github.com/PHPOffice/PHPWord/issues/2526) PR [#2531](https://github.com/PHPOffice/PHPWord/pull/2531)
+- Html Reader : Process Titles as Headings not Paragraphs [@0b10011](https://github.com/0b10011) and [@oleibman](https://github.com/oleibman) Issue [#1692](https://github.com/PHPOffice/PHPWord/issues/1692) PR [#2533](https://github.com/PHPOffice/PHPWord/pull/2533)
+
+### Miscellaneous
+
+
+### BC Breaks
\ No newline at end of file
diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php
index 0a9b23979c..1828de3b06 100644
--- a/src/PhpWord/Shared/Html.php
+++ b/src/PhpWord/Shared/Html.php
@@ -25,6 +25,7 @@
use PhpOffice\PhpWord\Element\AbstractContainer;
use PhpOffice\PhpWord\Element\Row;
use PhpOffice\PhpWord\Element\Table;
+use PhpOffice\PhpWord\Element\TextRun;
use PhpOffice\PhpWord\Settings;
use PhpOffice\PhpWord\SimpleType\Jc;
use PhpOffice\PhpWord\SimpleType\NumberFormat;
@@ -206,12 +207,12 @@ protected static function parseNode($node, $element, $styles = [], $data = []):
$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],
+ '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],
@@ -337,21 +338,22 @@ protected static function parseInput($node, $element, &$styles): void
/**
* Parse heading node.
*
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- * @param array &$styles
- * @param string $argument1 Name of heading style
- *
- * @return \PhpOffice\PhpWord\Element\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 $headingStyle): TextRun
{
- $styles['paragraph'] = $argument1;
- $newElement = $element->addTextRun($styles['paragraph']);
+ self::parseInlineStyle($node, $styles['font']);
+ // Create a TextRun to hold styles and text
+ $styles['paragraph'] = $headingStyle;
+ $textRun = new TextRun($styles['paragraph']);
- return $newElement;
+ // Create a title with level corresponding to number in heading style
+ // (Eg, Heading1 = 1)
+ $element->addTitle($textRun, (int) ltrim($headingStyle, 'Heading'));
+
+ // Return TextRun so children are parsed
+ return $textRun;
}
/**
diff --git a/src/PhpWord/Writer/HTML/Element/Title.php b/src/PhpWord/Writer/HTML/Element/Title.php
index 65e6cb090b..90ed8391ee 100644
--- a/src/PhpWord/Writer/HTML/Element/Title.php
+++ b/src/PhpWord/Writer/HTML/Element/Title.php
@@ -46,8 +46,14 @@ public function write()
$writer = new Container($this->parentWriter, $text);
$text = $writer->write();
}
+ $css = '';
+ $style = \PhpOffice\PhpWord\Style::getStyle('Heading_' . $this->element->getDepth());
+ if ($style !== null) {
+ $styleWriter = new \PhpOffice\PhpWord\Writer\HTML\Style\Font($style);
+ $css = ' style="' . $styleWriter->write() . '"';
+ }
- $content = "<{$tag}>{$text}{$tag}>" . PHP_EOL;
+ $content = "<{$tag}{$css}>{$text}{$tag}>" . PHP_EOL;
return $content;
}
diff --git a/src/PhpWord/Writer/HTML/Part/Head.php b/src/PhpWord/Writer/HTML/Part/Head.php
index 0f3f86e3d2..17530d1e82 100644
--- a/src/PhpWord/Writer/HTML/Part/Head.php
+++ b/src/PhpWord/Writer/HTML/Part/Head.php
@@ -90,17 +90,16 @@ private function writeStyles(): string
'font-family' => $this->getFontFamily(Settings::getDefaultFontName(), $this->getParentWriter()->getDefaultGenericFont()),
'font-size' => Settings::getDefaultFontSize() . 'pt',
];
- // Mpdf sometimes needs separate tag for body; doesn't harm others.
- $bodyarray = $astarray;
$defaultWhiteSpace = $this->getParentWriter()->getDefaultWhiteSpace();
if ($defaultWhiteSpace) {
$astarray['white-space'] = $defaultWhiteSpace;
}
+ $bodyarray = $astarray;
foreach ([
'body' => $bodyarray,
- '*' => $astarray,
+ //'*' => $astarray,
'a.NoteRef' => [
'text-decoration' => 'none',
],
@@ -137,8 +136,8 @@ private function writeStyles(): string
$style = $styleParagraph;
} else {
$name = '.' . $name;
+ $css .= "{$name} {" . $styleWriter->write() . '}' . PHP_EOL;
}
- $css .= "{$name} {" . $styleWriter->write() . '}' . PHP_EOL;
}
if ($style instanceof Paragraph) {
$styleWriter = new ParagraphStyleWriter($style);
diff --git a/tests/PhpWordTests/Shared/HtmlHeadingsTest.php b/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
new file mode 100644
index 0000000000..4704e8b3e1
--- /dev/null
+++ b/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
@@ -0,0 +1,66 @@
+addTitleStyle(1, ['size' => 20]);
+ $section = $originalDoc->addSection();
+ $expectedStrings = [];
+ $section->addTitle('Title 1', 1);
+ $expectedStrings[] = '
Title 1
';
+ for ($i = 2; $i <= 6; ++$i) {
+ $textRun = new TextRun();
+ $textRun->addText('Title ');
+ $textRun->addText("$i", ['italic' => true]);
+ $section->addTitle($textRun, $i);
+ $expectedStrings[] = "Title $i";
+ }
+ $writer = new HtmlWriter($originalDoc);
+ $content = $writer->getContent();
+ foreach ($expectedStrings as $expectedString) {
+ self::assertStringContainsString($expectedString, $content);
+ }
+
+ $newDoc = new PhpWord();
+ $newSection = $newDoc->addSection();
+ SharedHtml::addHtml($newSection, $content, true);
+ $newWriter = new HtmlWriter($newDoc);
+ $newContent = $newWriter->getContent();
+ // Reader transforms Text to TextRun,
+ // but result is functionally the same.
+ $firstStringAsTextRun = 'Title 1
';
+ self::assertSame($content, str_replace($firstStringAsTextRun, $expectedStrings[0], $newContent));
+ }
+}
diff --git a/tests/PhpWordTests/Writer/HTML/FontTest.php b/tests/PhpWordTests/Writer/HTML/FontTest.php
index 442c2639c9..08a8fca6a4 100644
--- a/tests/PhpWordTests/Writer/HTML/FontTest.php
+++ b/tests/PhpWordTests/Writer/HTML/FontTest.php
@@ -84,23 +84,23 @@ public function testFontNames1(): void
self::assertEquals('style5', Helper::getTextContent($xpath, '/html/body/div/p[6]/span', 'class'));
$style = Helper::getTextContent($xpath, '/html/head/style');
- $prg = preg_match('/^[*][^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
- self::assertEquals('* {font-family: \'Courier New\'; font-size: 12pt;}', $matches[0]);
+ $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches);
+ self::assertSame(1, $prg);
+ self::assertEquals('body {font-family: \'Courier New\'; font-size: 12pt;}', $matches[0]);
$prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style1 {font-family: \'Tahoma\'; font-size: 10pt; color: #1B2232; font-weight: bold;}', $matches[0]);
$prg = preg_match('/^[.]style2[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style2 {font-family: \'Arial\'; font-size: 10pt;}', $matches[0]);
$prg = preg_match('/^[.]style3[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style3 {font-family: \'hack attempt'}; display:none\'; font-size: 10pt;}', $matches[0]);
$prg = preg_match('/^[.]style4[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style4 {font-family: \'padmaa 1.1\'; font-size: 10pt; font-weight: bold;}', $matches[0]);
$prg = preg_match('/^[.]style5[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style5 {font-family: \'MingLiU-ExtB\'; font-size: 10pt; font-weight: bold;}', $matches[0]);
}
@@ -134,20 +134,20 @@ public function testFontNames2(): void
self::assertEquals('style4', Helper::getTextContent($xpath, '/html/body/div/p[5]/span', 'class'));
$style = Helper::getTextContent($xpath, '/html/head/style');
- $prg = preg_match('/^[*][^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
- self::assertEquals('* {font-family: \'Courier New\'; font-size: 12pt;}', $matches[0]);
+ $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches);
+ self::assertSame(1, $prg);
+ self::assertEquals('body {font-family: \'Courier New\'; font-size: 12pt;}', $matches[0]);
$prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style1 {font-family: \'Tahoma\'; font-size: 10pt; color: #1B2232; font-weight: bold;}', $matches[0]);
$prg = preg_match('/^[.]style2[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style2 {font-family: \'Arial\', sans-serif; font-size: 10pt;}', $matches[0]);
$prg = preg_match('/^[.]style3[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style3 {font-family: \'DejaVu Sans Monospace\', monospace; font-size: 10pt;}', $matches[0]);
$prg = preg_match('/^[.]style4[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style4 {font-family: \'Arial\'; font-size: 10pt;}', $matches[0]);
}
@@ -181,20 +181,20 @@ public function testFontNames3(): void
self::assertEquals('style4', Helper::getTextContent($xpath, '/html/body/div/p[5]/span', 'class'));
$style = Helper::getTextContent($xpath, '/html/head/style');
- $prg = preg_match('/^[*][^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
- self::assertEquals('* {font-family: \'Courier New\', monospace; font-size: 12pt;}', $matches[0]);
+ $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches);
+ self::assertSame(1, $prg);
+ self::assertEquals('body {font-family: \'Courier New\', monospace; font-size: 12pt;}', $matches[0]);
$prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style1 {font-family: \'Tahoma\'; font-size: 10pt; color: #1B2232; font-weight: bold;}', $matches[0]);
$prg = preg_match('/^[.]style2[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style2 {font-family: \'Arial\', sans-serif; font-size: 10pt;}', $matches[0]);
$prg = preg_match('/^[.]style3[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style3 {font-family: \'DejaVu Sans Monospace\', monospace; font-size: 10pt;}', $matches[0]);
$prg = preg_match('/^[.]style4[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style4 {font-family: \'Arial\'; font-size: 10pt;}', $matches[0]);
}
@@ -221,19 +221,19 @@ public function testWhiteSpace(): void
$xpath = new DOMXPath($dom);
$style = Helper::getTextContent($xpath, '/html/head/style');
- self::assertNotFalse(preg_match('/^[*][^\\r\\n]*/m', $style, $matches));
- self::assertEquals('* {font-family: \'Arial\'; font-size: 12pt; white-space: pre-wrap;}', $matches[0]);
+ self::assertNotFalse(preg_match('/^body[^\\r\\n]*/m', $style, $matches));
+ self::assertEquals('body {font-family: \'Arial\'; font-size: 12pt; white-space: pre-wrap;}', $matches[0]);
$prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style1 {font-family: \'Courier New\'; font-size: 10pt; white-space: pre-wrap;}', $matches[0]);
$prg = preg_match('/^[.]style2[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style2 {font-family: \'Courier New\'; font-size: 10pt;}', $matches[0]);
$prg = preg_match('/^[.]style3[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style3 {font-family: \'Courier New\'; font-size: 10pt; white-space: normal;}', $matches[0]);
$prg = preg_match('/^[.]style4[^\\r\\n]*/m', $style, $matches);
- self::assertNotFalse($prg);
+ self::assertSame(1, $prg);
self::assertEquals('.style4 {font-family: \'Courier New\'; font-size: 10pt;}', $matches[0]);
}
diff --git a/tests/PhpWordTests/Writer/HTML/Helper.php b/tests/PhpWordTests/Writer/HTML/Helper.php
index b777d4be14..555145d0d6 100644
--- a/tests/PhpWordTests/Writer/HTML/Helper.php
+++ b/tests/PhpWordTests/Writer/HTML/Helper.php
@@ -64,7 +64,7 @@ public static function getNamedItem(DOMXPath $xpath, string $query, string $name
if ($item2 === null) {
self::fail('Unexpected null return requesting item');
} else {
- $returnValue = $item2->attributes->getNamedItem($namedItem);
+ $returnVal = $item2->attributes->getNamedItem($namedItem);
}
}
@@ -94,4 +94,13 @@ public static function getAsHTML(PhpWord $phpWord, string $defaultWhiteSpace = '
return $dom;
}
+
+ public static function getHtmlString(PhpWord $phpWord, string $defaultWhiteSpace = '', string $defaultGenericFont = ''): string
+ {
+ $htmlWriter = new HTML($phpWord);
+ $htmlWriter->setDefaultWhiteSpace($defaultWhiteSpace);
+ $htmlWriter->setDefaultGenericFont($defaultGenericFont);
+
+ return $htmlWriter->getContent();
+ }
}
diff --git a/tests/PhpWordTests/Writer/HTML/PartTest.php b/tests/PhpWordTests/Writer/HTML/PartTest.php
index 9515932ac8..e919b80b5b 100644
--- a/tests/PhpWordTests/Writer/HTML/PartTest.php
+++ b/tests/PhpWordTests/Writer/HTML/PartTest.php
@@ -178,11 +178,17 @@ public function testTitleStyles(): void
$xpath = new DOMXPath($dom);
$style = Helper::getTextContent($xpath, '/html/head/style');
- self::assertNotFalse(strpos($style, 'h1 {font-family: \'Calibri\'; font-weight: bold;}'));
+ //self::assertNotFalse(strpos($style, 'h1 {font-family: \'Calibri\'; font-weight: bold;}'));
self::assertNotFalse(strpos($style, 'h1 {margin-top: 0.5pt; margin-bottom: 0.5pt;}'));
- self::assertNotFalse(strpos($style, 'h2 {font-family: \'Times New Roman\'; font-style: italic;}'));
+ //self::assertNotFalse(strpos($style, 'h2 {font-family: \'Times New Roman\'; font-style: italic;}'));
self::assertNotFalse(strpos($style, 'h2 {margin-top: 0.25pt; margin-bottom: 0.25pt;}'));
self::assertEquals(1, Helper::getLength($xpath, '/html/body/div/h1'));
self::assertEquals(2, Helper::getLength($xpath, '/html/body/div/h2'));
+ // code for getNamedItem had been erroneous
+ self::assertSame("font-family: 'Calibri'; font-weight: bold;", Helper::getNamedItem($xpath, '/html/body/div/h1', 'style')->textContent);
+ $html = Helper::getHtmlString($phpWord);
+ self::assertStringContainsString('Header 1 #1
', $html);
+ self::assertStringContainsString('Header 2 #1
', $html);
+ self::assertStringContainsString('Header 2 #2
', $html);
}
}
From 3638e1b2255b1208cfa04f6557260d341ade7c8a Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Sun, 7 Jan 2024 09:58:14 -0800
Subject: [PATCH 2/7] Changes Requested by @Progi1984
---
docs/changes/1.x/1.3.0.md | 15 ---------------
docs/changes/2.x/2.0.0.md | 23 +++++++++++++++++++++++
src/PhpWord/Writer/HTML/Element/Title.php | 9 ++++++---
3 files changed, 29 insertions(+), 18 deletions(-)
delete mode 100644 docs/changes/1.x/1.3.0.md
create mode 100644 docs/changes/2.x/2.0.0.md
diff --git a/docs/changes/1.x/1.3.0.md b/docs/changes/1.x/1.3.0.md
deleted file mode 100644
index 432394dbf6..0000000000
--- a/docs/changes/1.x/1.3.0.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# [1.3.0](https://github.com/PHPOffice/PHPWord/tree/1.3.0) (WIP)
-
-[Full Changelog](https://github.com/PHPOffice/PHPWord/compare/1.2.0...1.3.0)
-
-## Enhancements
-
-### Bug fixes
-
-- MsDoc Reader : Correct Font Size Calculation by [@oleibman](https://github.com/oleibman) Issue [#2526](https://github.com/PHPOffice/PHPWord/issues/2526) PR [#2531](https://github.com/PHPOffice/PHPWord/pull/2531)
-- Html Reader : Process Titles as Headings not Paragraphs [@0b10011](https://github.com/0b10011) and [@oleibman](https://github.com/oleibman) Issue [#1692](https://github.com/PHPOffice/PHPWord/issues/1692) PR [#2533](https://github.com/PHPOffice/PHPWord/pull/2533)
-
-### Miscellaneous
-
-
-### BC Breaks
\ No newline at end of file
diff --git a/docs/changes/2.x/2.0.0.md b/docs/changes/2.x/2.0.0.md
new file mode 100644
index 0000000000..d534608be6
--- /dev/null
+++ b/docs/changes/2.x/2.0.0.md
@@ -0,0 +1,23 @@
+# [2.0.0](https://github.com/PHPOffice/PHPWord/tree/2.0.0) (WIP)
+
+[Full Changelog](https://github.com/PHPOffice/PHPWord/compare/1.2.0...2.0.0)
+
+## Enhancements
+
+### Bug fixes
+
+- MsDoc Reader : Correct Font Size Calculation by [@oleibman](https://github.com/oleibman) fixing [#2526](https://github.com/PHPOffice/PHPWord/issues/2526) in [#2531](https://github.com/PHPOffice/PHPWord/pull/2531)
+- TemplateProcessor Persist File After Destruct [@oleibman](https://github.com/oleibman) fixing [#2539](https://github.com/PHPOffice/PHPWord/issues/2539) in [#2542](https://github.com/PHPOffice/PHPWord/pull/2531)
+- Html Reader : Process Titles as Headings not Paragraphs [@0b10011](https://github.com/0b10011) and [@oleibman](https://github.com/oleibman) Issue [#1692](https://github.com/PHPOffice/PHPWord/issues/1692) PR [#2533](https://github.com/PHPOffice/PHPWord/pull/2533)
+
+### Miscellaneous
+
+- Bump dompdf/dompdf from 2.0.3 to 2.0.4 by [@dependabot](https://github.com/dependabot) in [#2530](https://github.com/PHPOffice/PHPWord/pull/2530)
+- Bump phpunit/phpunit from 9.6.13 to 9.6.14 by [@dependabot](https://github.com/dependabot) in [#2519](https://github.com/PHPOffice/PHPWord/pull/2519)
+- Bump mpdf/mpdf from 8.2.0 to 8.2.2 by [@dependabot](https://github.com/dependabot) in [#2518](https://github.com/PHPOffice/PHPWord/pull/2518)
+- Bump phpmd/phpmd from 2.14.1 to 2.15.0 by [@dependabot](https://github.com/dependabot) in [#2538](https://github.com/PHPOffice/PHPWord/pull/2538)
+- Bump phpunit/phpunit from 9.6.14 to 9.6.15 by [@dependabot](https://github.com/dependabot) in [#2537](https://github.com/PHPOffice/PHPWord/pull/2537)
+- Bump symfony/process from 5.4.28 to 5.4.34 by [@dependabot](https://github.com/dependabot) in [#2536](https://github.com/PHPOffice/PHPWord/pull/2536)
+- Allow rgb() when converting Html [@oleibman](https://github.com/oleibman) fixing [#2508](https://github.com/PHPOffice/PHPWord/issues/2508) in [#2512](https://github.com/PHPOffice/PHPWord/pull/2512)
+
+### BC Breaks
diff --git a/src/PhpWord/Writer/HTML/Element/Title.php b/src/PhpWord/Writer/HTML/Element/Title.php
index 90ed8391ee..6454b45cf9 100644
--- a/src/PhpWord/Writer/HTML/Element/Title.php
+++ b/src/PhpWord/Writer/HTML/Element/Title.php
@@ -17,7 +17,10 @@
namespace PhpOffice\PhpWord\Writer\HTML\Element;
+use PhpOffice\PhpWord\Element\Title as PhpWordTitle;
+use PhpOffice\PhpWord\Style;
use PhpOffice\PhpWord\Writer\HTML;
+use PhpOffice\PhpWord\Writer\HTML\Style\Font;
/**
* TextRun element HTML writer.
@@ -33,7 +36,7 @@ class Title extends AbstractElement
*/
public function write()
{
- if (!$this->element instanceof \PhpOffice\PhpWord\Element\Title) {
+ if (!$this->element instanceof PhpWordTitle) {
return '';
}
@@ -47,9 +50,9 @@ public function write()
$text = $writer->write();
}
$css = '';
- $style = \PhpOffice\PhpWord\Style::getStyle('Heading_' . $this->element->getDepth());
+ $style = Style::getStyle('Heading_' . $this->element->getDepth());
if ($style !== null) {
- $styleWriter = new \PhpOffice\PhpWord\Writer\HTML\Style\Font($style);
+ $styleWriter = new Font($style);
$css = ' style="' . $styleWriter->write() . '"';
}
From 6e09f4c42b4547e680f23fad37030b74c8bb1da3 Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Thu, 9 Jan 2025 17:07:18 -0800
Subject: [PATCH 3/7] Update 2.0.0.md
---
docs/changes/2.x/2.0.0.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/docs/changes/2.x/2.0.0.md b/docs/changes/2.x/2.0.0.md
index 6f10bb171e..c5fbaf4b52 100644
--- a/docs/changes/2.x/2.0.0.md
+++ b/docs/changes/2.x/2.0.0.md
@@ -9,7 +9,6 @@
### Bug fixes
- MsDoc Reader : Correct Font Size Calculation by [@oleibman](https://github.com/oleibman) fixing [#2526](https://github.com/PHPOffice/PHPWord/issues/2526) in [#2531](https://github.com/PHPOffice/PHPWord/pull/2531)
-- Html Reader : Process Titles as Headings not Paragraphs [@0b10011](https://github.com/0b10011) and [@oleibman](https://github.com/oleibman) Issue [#1692](https://github.com/PHPOffice/PHPWord/issues/1692) PR [#2533](https://github.com/PHPOffice/PHPWord/pull/2533)
- TemplateProcessor Persist File After Destruct [@oleibman](https://github.com/oleibman) fixing [#2539](https://github.com/PHPOffice/PHPWord/issues/2539) in [#2545](https://github.com/PHPOffice/PHPWord/pull/2545)
- bug: TemplateProcessor fix multiline values [@gimler](https://github.com/gimler) fixing [#268](https://github.com/PHPOffice/PHPWord/issues/268), [#2323](https://github.com/PHPOffice/PHPWord/issues/2323) and [#2486](https://github.com/PHPOffice/PHPWord/issues/2486) in [#2522](https://github.com/PHPOffice/PHPWord/pull/2522)
From 03ad7ecffef7d6410bac57bc7e8c612444031e38 Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Thu, 9 Jan 2025 17:41:43 -0800
Subject: [PATCH 4/7] Php-cs-fixer Recommendations
---
composer.json | 2 +-
src/PhpWord/Shared/Html.php | 4 ++--
tests/PhpWordTests/Shared/HtmlHeadingsTest.php | 1 +
3 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/composer.json b/composer.json
index efebe941e7..2d06f890b3 100644
--- a/composer.json
+++ b/composer.json
@@ -122,7 +122,7 @@
"phpmd/phpmd": "^2.13",
"phpstan/phpstan": "^0.12.88 || ^1.0.0",
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
- "phpunit/phpunit": ">=7.0",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0 || ^10.0",
"symfony/process": "^4.4 || ^5.0",
"tecnickcom/tcpdf": "^6.5"
},
diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php
index c6f266dfb7..578e1d2402 100644
--- a/src/PhpWord/Shared/Html.php
+++ b/src/PhpWord/Shared/Html.php
@@ -303,7 +303,7 @@ protected static function parseChildNodes($node, $element, $styles, $data): void
* @param AbstractContainer $element
* @param array &$styles
*
- * @return \PhpOffice\PhpWord\Element\PageBreak|\PhpOffice\PhpWord\Element\TextRun
+ * @return \PhpOffice\PhpWord\Element\PageBreak|TextRun
*/
protected static function parseParagraph($node, $element, &$styles)
{
@@ -466,7 +466,7 @@ protected static function parseRow($node, $element, &$styles)
* @param Table $element
* @param array &$styles
*
- * @return \PhpOffice\PhpWord\Element\Cell|\PhpOffice\PhpWord\Element\TextRun $element
+ * @return \PhpOffice\PhpWord\Element\Cell|TextRun $element
*/
protected static function parseCell($node, $element, &$styles)
{
diff --git a/tests/PhpWordTests/Shared/HtmlHeadingsTest.php b/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
index 4704e8b3e1..2f11386d04 100644
--- a/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
+++ b/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
@@ -1,4 +1,5 @@
Date: Tue, 28 Jan 2025 21:44:38 -0800
Subject: [PATCH 5/7] Update FontTest.php
---
tests/PhpWordTests/Writer/HTML/FontTest.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/PhpWordTests/Writer/HTML/FontTest.php b/tests/PhpWordTests/Writer/HTML/FontTest.php
index 0248f03979..50a239377d 100644
--- a/tests/PhpWordTests/Writer/HTML/FontTest.php
+++ b/tests/PhpWordTests/Writer/HTML/FontTest.php
@@ -220,7 +220,7 @@ public function testFontNames3(): void
self::assertEquals('style4', Helper::getTextContent($xpath, '/html/body/div/p[5]/span', 'class'));
$style = Helper::getTextContent($xpath, '/html/head/style');
- $prg = preg_match('/body[*][^\\r\\n]*/m', $style, $matches);
+ $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches);
self::assertNotEmpty($matches);
self::assertSame(1, $prg);
self::assertEquals('body {font-family: \'Courier New\', monospace; font-size: 12pt; color: #000000;}', $matches[0]);
From f3f7d9a472961608e8f08702b3689e25daea2e0d Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Wed, 5 Feb 2025 15:41:20 -0800
Subject: [PATCH 6/7] Catch Up
---
src/PhpWord/Shared/Html.php | 8 ++++----
src/PhpWord/Writer/HTML/Element/Title.php | 15 +++++++++------
src/PhpWord/Writer/HTML/Part/Head.php | 1 +
tests/PhpWordTests/Shared/HtmlHeadingsTest.php | 9 ++++-----
tests/PhpWordTests/Shared/HtmlTest.php | 5 +++++
tests/PhpWordTests/Writer/HTML/PartTest.php | 12 +++++-------
6 files changed, 28 insertions(+), 22 deletions(-)
diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php
index d0f859b6b2..7a965467b8 100644
--- a/src/PhpWord/Shared/Html.php
+++ b/src/PhpWord/Shared/Html.php
@@ -350,10 +350,10 @@ protected static function parseInput($node, $element, &$styles): void
*/
protected static function parseHeading(DOMNode $node, AbstractContainer $element, array &$styles, string $headingStyle): TextRun
{
- self::parseInlineStyle($node, $styles['font']);
- // Create a TextRun to hold styles and text
- $styles['paragraph'] = $headingStyle;
- $textRun = new TextRun($styles['paragraph']);
+ $style = new Paragraph();
+ $style->setStyleName($headingStyle);
+ $style->setStyleByArray(self::parseInlineStyle($node, $styles['paragraph']));
+ $textRun = new TextRun($style);
// Create a title with level corresponding to number in heading style
// (Eg, Heading1 = 1)
diff --git a/src/PhpWord/Writer/HTML/Element/Title.php b/src/PhpWord/Writer/HTML/Element/Title.php
index 1348a30a75..e759bfdcd6 100644
--- a/src/PhpWord/Writer/HTML/Element/Title.php
+++ b/src/PhpWord/Writer/HTML/Element/Title.php
@@ -19,9 +19,8 @@
namespace PhpOffice\PhpWord\Writer\HTML\Element;
use PhpOffice\PhpWord\Element\Title as PhpWordTitle;
-use PhpOffice\PhpWord\Style;
use PhpOffice\PhpWord\Writer\HTML;
-use PhpOffice\PhpWord\Writer\HTML\Style\Font;
+use PhpOffice\PhpWord\Writer\HTML\Style\Paragraph;
/**
* TextRun element HTML writer.
@@ -44,17 +43,21 @@ public function write()
$tag = 'h' . $this->element->getDepth();
$text = $this->element->getText();
+ $paragraphStyle = null;
if (is_string($text)) {
$text = $this->parentWriter->escapeHTML($text);
} else {
+ $paragraphStyle = $text->getParagraphStyle();
$writer = new Container($this->parentWriter, $text);
$text = $writer->write();
}
$css = '';
- $style = Style::getStyle('Heading_' . $this->element->getDepth());
- if ($style !== null) {
- $styleWriter = new Font($style);
- $css = ' style="' . $styleWriter->write() . '"';
+ if (is_object($paragraphStyle)) {
+ $styleWriter = new Paragraph($paragraphStyle);
+ $write = $styleWriter->write();
+ if ($write !== '') {
+ $css = " style=\"$write\"";
+ }
}
$content = "<{$tag}{$css}>{$text}{$tag}>" . PHP_EOL;
diff --git a/src/PhpWord/Writer/HTML/Part/Head.php b/src/PhpWord/Writer/HTML/Part/Head.php
index 26a8ac9919..e7274769b1 100644
--- a/src/PhpWord/Writer/HTML/Part/Head.php
+++ b/src/PhpWord/Writer/HTML/Part/Head.php
@@ -134,6 +134,7 @@ private function writeStyles(): string
$styleWriter = new FontStyleWriter($style);
if ($style->getStyleType() == 'title') {
$name = str_replace('Heading_', 'h', $name);
+ $css .= "{$name} {" . $styleWriter->write() . '}' . PHP_EOL;
$styleParagraph = $style->getParagraph();
$style = $styleParagraph;
} else {
diff --git a/tests/PhpWordTests/Shared/HtmlHeadingsTest.php b/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
index 2f11386d04..8ecc95b773 100644
--- a/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
+++ b/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
@@ -40,7 +40,7 @@ public function testRoundTripHeadings(): void
$section = $originalDoc->addSection();
$expectedStrings = [];
$section->addTitle('Title 1', 1);
- $expectedStrings[] = 'Title 1
';
+ $expectedStrings[] = 'Title 1
';
for ($i = 2; $i <= 6; ++$i) {
$textRun = new TextRun();
$textRun->addText('Title ');
@@ -59,9 +59,8 @@ public function testRoundTripHeadings(): void
SharedHtml::addHtml($newSection, $content, true);
$newWriter = new HtmlWriter($newDoc);
$newContent = $newWriter->getContent();
- // Reader transforms Text to TextRun,
- // but result is functionally the same.
- $firstStringAsTextRun = 'Title 1
';
- self::assertSame($content, str_replace($firstStringAsTextRun, $expectedStrings[0], $newContent));
+
+ // This needs work
+ self::assertSame($newContent, str_replace('h1 {font-size: 20pt;}' . PHP_EOL, '', $content));
}
}
diff --git a/tests/PhpWordTests/Shared/HtmlTest.php b/tests/PhpWordTests/Shared/HtmlTest.php
index 42d8aa598b..11be8c7f65 100644
--- a/tests/PhpWordTests/Shared/HtmlTest.php
+++ b/tests/PhpWordTests/Shared/HtmlTest.php
@@ -24,6 +24,7 @@
use PhpOffice\PhpWord\Element\Table;
use PhpOffice\PhpWord\Element\Text;
use PhpOffice\PhpWord\Element\TextRun;
+use PhpOffice\PhpWord\Element\Title;
use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\Shared\Converter;
use PhpOffice\PhpWord\Shared\Html;
@@ -116,6 +117,8 @@ public function testParseHeader(): void
self::assertCount(1, $section->getElements());
$element = $section->getElement(0);
+ self::assertInstanceOf(Title::class, $element);
+ $element = $element->getText();
self::assertInstanceOf(TextRun::class, $element);
self::assertInstanceOf(Paragraph::class, $element->getParagraphStyle());
self::assertEquals('Heading1', $element->getParagraphStyle()->getStyleName());
@@ -137,6 +140,8 @@ public function testParseHeaderStyle(): void
self::assertCount(1, $section->getElements());
$element = $section->getElement(0);
+ self::assertInstanceOf(Title::class, $element);
+ $element = $element->getText();
self::assertInstanceOf(TextRun::class, $element);
self::assertInstanceOf(Paragraph::class, $element->getParagraphStyle());
self::assertEquals('Heading1', $element->getParagraphStyle()->getStyleName());
diff --git a/tests/PhpWordTests/Writer/HTML/PartTest.php b/tests/PhpWordTests/Writer/HTML/PartTest.php
index a9d91b9530..2ccb96a799 100644
--- a/tests/PhpWordTests/Writer/HTML/PartTest.php
+++ b/tests/PhpWordTests/Writer/HTML/PartTest.php
@@ -179,17 +179,15 @@ public function testTitleStyles(): void
$xpath = new DOMXPath($dom);
$style = Helper::getTextContent($xpath, '/html/head/style');
- //self::assertNotFalse(strpos($style, 'h1 {font-family: \'Calibri\'; font-weight: bold;}'));
+ self::assertNotFalse(strpos($style, 'h1 {font-family: \'Calibri\'; font-weight: bold;}'));
self::assertNotFalse(strpos($style, 'h1 {margin-top: 0.5pt; margin-bottom: 0.5pt;}'));
- //self::assertNotFalse(strpos($style, 'h2 {font-family: \'Times New Roman\'; font-style: italic;}'));
+ self::assertNotFalse(strpos($style, 'h2 {font-family: \'Times New Roman\'; font-style: italic;}'));
self::assertNotFalse(strpos($style, 'h2 {margin-top: 0.25pt; margin-bottom: 0.25pt;}'));
self::assertEquals(1, Helper::getLength($xpath, '/html/body/div/h1'));
self::assertEquals(2, Helper::getLength($xpath, '/html/body/div/h2'));
- // code for getNamedItem had been erroneous
- self::assertSame("font-family: 'Calibri'; font-weight: bold;", Helper::getNamedItem($xpath, '/html/body/div/h1', 'style')->textContent);
$html = Helper::getHtmlString($phpWord);
- self::assertStringContainsString('Header 1 #1
', $html);
- self::assertStringContainsString('Header 2 #1
', $html);
- self::assertStringContainsString('Header 2 #2
', $html);
+ self::assertStringContainsString('Header 1 #1
', $html);
+ self::assertStringContainsString('Header 2 #1
', $html);
+ self::assertStringContainsString('Header 2 #2
', $html);
}
}
From e740deb7b12fa019756ff9708082a769b0f62782 Mon Sep 17 00:00:00 2001
From: oleibman <10341515+oleibman@users.noreply.github.com>
Date: Thu, 6 Feb 2025 00:19:49 -0800
Subject: [PATCH 7/7] Html Writer Duplicate Header Styles in Style Tags
Nominally redundant, but makes things easier for Html Reader.
---
src/PhpWord/Writer/HTML/Element/Title.php | 18 +++++++++++++++---
tests/PhpWordTests/Shared/HtmlHeadingsTest.php | 16 +++++++++++++---
tests/PhpWordTests/Writer/HTML/PartTest.php | 6 +++---
3 files changed, 31 insertions(+), 9 deletions(-)
diff --git a/src/PhpWord/Writer/HTML/Element/Title.php b/src/PhpWord/Writer/HTML/Element/Title.php
index e759bfdcd6..2456a0a601 100644
--- a/src/PhpWord/Writer/HTML/Element/Title.php
+++ b/src/PhpWord/Writer/HTML/Element/Title.php
@@ -19,7 +19,9 @@
namespace PhpOffice\PhpWord\Writer\HTML\Element;
use PhpOffice\PhpWord\Element\Title as PhpWordTitle;
+use PhpOffice\PhpWord\Style;
use PhpOffice\PhpWord\Writer\HTML;
+use PhpOffice\PhpWord\Writer\HTML\Style\Font;
use PhpOffice\PhpWord\Writer\HTML\Style\Paragraph;
/**
@@ -52,13 +54,23 @@ public function write()
$text = $writer->write();
}
$css = '';
+ $write1 = $write2 = $write3 = '';
+ $style = Style::getStyle('Heading_' . $this->element->getDepth());
+ if ($style !== null) {
+ $styleWriter = new Font($style);
+ $write1 = $styleWriter->write();
+ }
if (is_object($paragraphStyle)) {
$styleWriter = new Paragraph($paragraphStyle);
- $write = $styleWriter->write();
- if ($write !== '') {
- $css = " style=\"$write\"";
+ $write3 = $styleWriter->write();
+ if ($write1 !== '' && $write3 !== '') {
+ $write2 = ' ';
}
}
+ $css = "$write1$write2$write3";
+ if ($css !== '') {
+ $css = " style=\"$css\"";
+ }
$content = "<{$tag}{$css}>{$text}{$tag}>" . PHP_EOL;
diff --git a/tests/PhpWordTests/Shared/HtmlHeadingsTest.php b/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
index 8ecc95b773..331935fbae 100644
--- a/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
+++ b/tests/PhpWordTests/Shared/HtmlHeadingsTest.php
@@ -40,7 +40,7 @@ public function testRoundTripHeadings(): void
$section = $originalDoc->addSection();
$expectedStrings = [];
$section->addTitle('Title 1', 1);
- $expectedStrings[] = 'Title 1
';
+ $expectedStrings[] = 'Title 1
';
for ($i = 2; $i <= 6; ++$i) {
$textRun = new TextRun();
$textRun->addText('Title ');
@@ -59,8 +59,18 @@ public function testRoundTripHeadings(): void
SharedHtml::addHtml($newSection, $content, true);
$newWriter = new HtmlWriter($newDoc);
$newContent = $newWriter->getContent();
+ // Reader does not yet support h1 declaration in css.
+ $content = str_replace('h1 {font-size: 20pt;}' . PHP_EOL, '', $content);
- // This needs work
- self::assertSame($newContent, str_replace('h1 {font-size: 20pt;}' . PHP_EOL, '', $content));
+ // Reader transforms Text to TextRun,
+ // but result is functionally the same.
+ self::assertSame(
+ $newContent,
+ str_replace(
+ 'Title 1
',
+ 'Title 1
',
+ $content
+ )
+ );
}
}
diff --git a/tests/PhpWordTests/Writer/HTML/PartTest.php b/tests/PhpWordTests/Writer/HTML/PartTest.php
index 2ccb96a799..0fe43c2350 100644
--- a/tests/PhpWordTests/Writer/HTML/PartTest.php
+++ b/tests/PhpWordTests/Writer/HTML/PartTest.php
@@ -186,8 +186,8 @@ public function testTitleStyles(): void
self::assertEquals(1, Helper::getLength($xpath, '/html/body/div/h1'));
self::assertEquals(2, Helper::getLength($xpath, '/html/body/div/h2'));
$html = Helper::getHtmlString($phpWord);
- self::assertStringContainsString('Header 1 #1
', $html);
- self::assertStringContainsString('Header 2 #1
', $html);
- self::assertStringContainsString('Header 2 #2
', $html);
+ self::assertStringContainsString('Header 1 #1
', $html);
+ self::assertStringContainsString('Header 2 #1
', $html);
+ self::assertStringContainsString('Header 2 #2
', $html);
}
}