diff --git a/docs/changes/2.x/2.0.0.md b/docs/changes/2.x/2.0.0.md index c5fbaf4b52..6f10bb171e 100644 --- a/docs/changes/2.x/2.0.0.md +++ b/docs/changes/2.x/2.0.0.md @@ -9,6 +9,7 @@ ### 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) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 2022f7da09..6be54b37a7 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; @@ -208,12 +209,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], @@ -339,21 +340,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..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 ''; } @@ -46,8 +49,14 @@ public function write() $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() . '"'; + } - $content = "<{$tag}>{$text}" . PHP_EOL; + $content = "<{$tag}{$css}>{$text}" . 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); } }