diff --git a/composer.json b/composer.json index 5055105d16..d85f27e9b1 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 170dc5dff3..7a965467b8 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -345,18 +345,22 @@ protected static function parseInput($node, $element, &$styles): void /** * Parse heading node. * - * @param string $argument1 Name of heading style - * * @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(DOMNode $node, AbstractContainer $element, array &$styles, string $argument1): TextRun + protected static function parseHeading(DOMNode $node, AbstractContainer $element, array &$styles, string $headingStyle): TextRun { $style = new Paragraph(); - $style->setStyleName($argument1); + $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) + $element->addTitle($textRun, (int) ltrim($headingStyle, 'Heading')); - return $element->addTextRun($style); + // 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 84b0b199fc..ba9675b051 100644 --- a/src/PhpWord/Writer/HTML/Element/Title.php +++ b/src/PhpWord/Writer/HTML/Element/Title.php @@ -18,7 +18,11 @@ namespace PhpOffice\PhpWord\Writer\HTML\Element; +use PhpOffice\PhpWord\Element\Title as ElementTitle; +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. @@ -34,21 +38,41 @@ class Title extends AbstractElement */ public function write() { - if (!$this->element instanceof \PhpOffice\PhpWord\Element\Title) { + if (!$this->element instanceof ElementTitle) { return ''; } $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 = ''; + $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); + $write3 = $styleWriter->write(); + if ($write1 !== '' && $write3 !== '') { + $write2 = ' '; + } + } + $css = "$write1$write2$write3"; + if ($css !== '') { + $css = " style=\"$css\""; + } - $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 79235e1c4a..2bcb70e5f6 100644 --- a/src/PhpWord/Writer/HTML/Part/Head.php +++ b/src/PhpWord/Writer/HTML/Part/Head.php @@ -92,17 +92,15 @@ private function writeStyles(): string 'font-size' => Settings::getDefaultFontSize() . 'pt', 'color' => "#{$defaultFontColor}", ]; - // 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, 'a.NoteRef' => [ 'text-decoration' => 'none', ], @@ -135,12 +133,13 @@ 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 { $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..331935fbae --- /dev/null +++ b/tests/PhpWordTests/Shared/HtmlHeadingsTest.php @@ -0,0 +1,76 @@ +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 does not yet support h1 declaration in css. + $content = 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/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/FontTest.php b/tests/PhpWordTests/Writer/HTML/FontTest.php index 0a203b7237..50a239377d 100644 --- a/tests/PhpWordTests/Writer/HTML/FontTest.php +++ b/tests/PhpWordTests/Writer/HTML/FontTest.php @@ -121,29 +121,24 @@ 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); + $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); - self::assertNotFalse($prg); - self::assertEquals('* {font-family: \'Courier New\'; font-size: 12pt; color: #000000;}', $matches[0]); + self::assertSame(1, $prg); + self::assertEquals('body {font-family: \'Courier New\'; font-size: 12pt; color: #000000;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); - self::assertNotEmpty($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::assertNotEmpty($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::assertNotEmpty($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::assertNotEmpty($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::assertNotEmpty($matches); - self::assertNotFalse($prg); + self::assertSame(1, $prg); self::assertEquals('.style5 {font-family: \'MingLiU-ExtB\'; font-size: 10pt; font-weight: bold;}', $matches[0]); } @@ -177,25 +172,21 @@ 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); + $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); - self::assertNotFalse($prg); - self::assertEquals('* {font-family: \'Courier New\'; font-size: 12pt; color: #000000;}', $matches[0]); + self::assertSame(1, $prg); + self::assertEquals('body {font-family: \'Courier New\'; font-size: 12pt; color: #000000;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); - self::assertNotEmpty($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::assertNotEmpty($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::assertNotEmpty($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::assertNotEmpty($matches); - self::assertNotFalse($prg); + self::assertSame(1, $prg); self::assertEquals('.style4 {font-family: \'Arial\'; font-size: 10pt;}', $matches[0]); } @@ -229,25 +220,21 @@ 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); + $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); - self::assertNotFalse($prg); - self::assertEquals('* {font-family: \'Courier New\', monospace; font-size: 12pt; color: #000000;}', $matches[0]); + self::assertSame(1, $prg); + self::assertEquals('body {font-family: \'Courier New\', monospace; font-size: 12pt; color: #000000;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); - self::assertNotEmpty($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::assertNotEmpty($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::assertNotEmpty($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::assertNotEmpty($matches); - self::assertNotFalse($prg); + self::assertSame(1, $prg); self::assertEquals('.style4 {font-family: \'Arial\'; font-size: 10pt;}', $matches[0]); } @@ -274,23 +261,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; color: #000000; 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; color: #000000; white-space: pre-wrap;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); - self::assertNotEmpty($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::assertNotEmpty($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::assertNotEmpty($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::assertNotEmpty($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 37f640d28a..2e7ee343c3 100644 --- a/tests/PhpWordTests/Writer/HTML/Helper.php +++ b/tests/PhpWordTests/Writer/HTML/Helper.php @@ -67,7 +67,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); } } @@ -125,4 +125,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 b6748a58c5..0fe43c2350 100644 --- a/tests/PhpWordTests/Writer/HTML/PartTest.php +++ b/tests/PhpWordTests/Writer/HTML/PartTest.php @@ -185,5 +185,9 @@ public function testTitleStyles(): void 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')); + $html = Helper::getHtmlString($phpWord); + self::assertStringContainsString('

Header 1 #1

', $html); + self::assertStringContainsString('

Header 2 #1

', $html); + self::assertStringContainsString('

Header 2 #2

', $html); } }