From 5e147c4ffeb87a7188e377ec89ff55501bea5d50 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 6 Feb 2024 15:23:05 -0800 Subject: [PATCH 01/24] WIP Do Not Install All my unimplemented changes. Not intended for install. --- phpstan-baseline.neon | 5 - samples/Sample_45_RTLTitles.php | 35 ++ src/PhpWord/PhpWord.php | 5 +- src/PhpWord/Reader/Word2007/AbstractPart.php | 65 ++- src/PhpWord/Reader/Word2007/Styles.php | 6 +- src/PhpWord/Settings.php | 5 + src/PhpWord/Shared/Html.php | 209 +++++-- src/PhpWord/Shared/HtmlColours.php | 549 ++++++++++++++++++ .../Shared/Microsoft/PasswordEncoder.php | 11 +- src/PhpWord/Shared/ZipArchive.php | 10 +- src/PhpWord/SimpleType/TextDirection.php | 55 ++ src/PhpWord/Style.php | 10 +- src/PhpWord/Style/AbstractStyle.php | 15 + src/PhpWord/Style/Border.php | 8 + src/PhpWord/Style/Paragraph.php | 48 +- src/PhpWord/Style/Table.php | 113 ++++ src/PhpWord/TemplateProcessor.php | 12 +- src/PhpWord/Writer/HTML/Element/Table.php | 5 +- src/PhpWord/Writer/HTML/Element/Title.php | 13 +- src/PhpWord/Writer/HTML/Part/Head.php | 10 +- src/PhpWord/Writer/HTML/Style/Font.php | 7 + src/PhpWord/Writer/HTML/Style/Table.php | 6 +- src/PhpWord/Writer/RTF.php | 2 +- src/PhpWord/Writer/RTF/Element/Title.php | 9 +- src/PhpWord/Writer/Word2007/Element/Table.php | 10 +- src/PhpWord/Writer/Word2007/Part/Styles.php | 17 +- src/PhpWord/Writer/Word2007/Style/Font.php | 5 +- .../Writer/Word2007/Style/MarginBorder.php | 18 +- .../Writer/Word2007/Style/Paragraph.php | 5 + src/PhpWord/Writer/Word2007/Style/Table.php | 3 + .../Reader/Word2007/StyleTableTest.php | 55 ++ tests/PhpWordTests/SettingsRtlTest.php | 81 +++ tests/PhpWordTests/SettingsTest.php | 16 - tests/PhpWordTests/Shared/Html2402Test.php | 208 +++++++ tests/PhpWordTests/Shared/HtmlFullTest.php | 93 +++ .../PhpWordTests/Shared/HtmlHeadingsTest.php | 66 +++ tests/PhpWordTests/Shared/HtmlRtlTest.php | 180 ++++++ tests/PhpWordTests/Shared/HtmlTest.php | 39 +- .../TemplateProcessorSectionTest.php | 92 +++ tests/PhpWordTests/TemplateProcessorTest.php | 10 + tests/PhpWordTests/Writer/HTML/FontTest.php | 56 +- tests/PhpWordTests/Writer/HTML/Helper.php | 11 +- tests/PhpWordTests/Writer/HTML/PartTest.php | 10 +- .../Writer/ODText/Part/ContentTest.php | 10 + .../Writer/ODText/Style/Paragraph2Test.php | 6 +- .../Writer/RTF/RichTextTitleTest.php | 50 ++ .../Writer/Word2007/Element/TableTest.php | 147 +++++ .../_files/documents/word.2474.docx | Bin 0 -> 27593 bytes 48 files changed, 2190 insertions(+), 211 deletions(-) create mode 100644 samples/Sample_45_RTLTitles.php create mode 100644 src/PhpWord/Shared/HtmlColours.php create mode 100644 src/PhpWord/SimpleType/TextDirection.php create mode 100644 tests/PhpWordTests/Reader/Word2007/StyleTableTest.php create mode 100644 tests/PhpWordTests/SettingsRtlTest.php create mode 100644 tests/PhpWordTests/Shared/Html2402Test.php create mode 100644 tests/PhpWordTests/Shared/HtmlFullTest.php create mode 100644 tests/PhpWordTests/Shared/HtmlHeadingsTest.php create mode 100644 tests/PhpWordTests/Shared/HtmlRtlTest.php create mode 100644 tests/PhpWordTests/TemplateProcessorSectionTest.php create mode 100644 tests/PhpWordTests/Writer/RTF/RichTextTitleTest.php create mode 100644 tests/PhpWordTests/Writer/Word2007/Element/TableTest.php create mode 100644 tests/PhpWordTests/_files/documents/word.2474.docx diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2e44745b3d..e07918f6b6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -400,11 +400,6 @@ parameters: count: 1 path: src/PhpWord/Shared/Html.php - - - message: "#^Cannot call method setBorderSize\\(\\) on PhpOffice\\\\PhpWord\\\\Style\\\\Table\\|string\\.$#" - count: 1 - path: src/PhpWord/Shared/Html.php - - message: "#^Cannot call method setStyleName\\(\\) on PhpOffice\\\\PhpWord\\\\Style\\\\Table\\|string\\.$#" count: 1 diff --git a/samples/Sample_45_RTLTitles.php b/samples/Sample_45_RTLTitles.php new file mode 100644 index 0000000000..83dd9b9872 --- /dev/null +++ b/samples/Sample_45_RTLTitles.php @@ -0,0 +1,35 @@ +setDefaultFontName('DejaVu Sans'); // for good rendition of PDF +$rendererName = Settings::PDF_RENDERER_MPDF; +$rendererLibraryPath = $vendorDirPath . '/mpdf/mpdf'; +Settings::setPdfRenderer($rendererName, $rendererLibraryPath); + +// Define styles for headers +$phpWord->addTitleStyle(1, ['bold' => true, 'name' => 'Arial', 'size' => 16], []); +//var_dump($x); +$phpWord->addTitleStyle(2, ['bold' => true, 'name' => 'Arial', 'size' => 14], []); +$phpWord->addTitleStyle(3, ['bold' => true, 'name' => 'Arial', 'size' => 12], []); +$phpWord->addTitleStyle(4, ['bold' => true, 'name' => 'Arial', 'size' => 10], []); + +// New section +$section = $phpWord->addSection(); +$htmlContent = '

مرحبا 1

تجربة 2

تجربة تجربة

هناك hello هنا 4

مرحبا here كلمة انجليزي.

'; +SharedHtml::addHtml($section, $htmlContent, false, false); + +// Save file +echo write($phpWord, basename(__FILE__, '.php'), $writers); +if (!CLI) { + include_once 'Sample_Footer.php'; +} +Settings::setDefaultRtl(false); diff --git a/src/PhpWord/PhpWord.php b/src/PhpWord/PhpWord.php index a7aa95ce45..c85306e67c 100644 --- a/src/PhpWord/PhpWord.php +++ b/src/PhpWord/PhpWord.php @@ -20,6 +20,7 @@ use BadMethodCallException; use PhpOffice\PhpWord\Element\Section; use PhpOffice\PhpWord\Exception\Exception; +use PhpOffice\PhpWord\Style\Font; /** * PHPWord main class. @@ -284,9 +285,9 @@ public function setDefaultFontSize($fontSize): void * * @return \PhpOffice\PhpWord\Style\Paragraph */ - public function setDefaultParagraphStyle($styles) + public function setDefaultParagraphStyle($styles, ?Font $fontStyles = null) { - return Style::setDefaultParagraphStyle($styles); + return Style::setDefaultParagraphStyle($styles, $fontStyles); } /** diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index 95799387ed..a92e6d5958 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -592,35 +592,46 @@ protected function readTableStyle(XMLReader $xmlReader, DOMElement $domNode) $borders = array_merge($margins, ['insideH', 'insideV']); if ($xmlReader->elementExists('w:tblPr', $domNode)) { + $tblStyleName = ''; if ($xmlReader->elementExists('w:tblPr/w:tblStyle', $domNode)) { - $style = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle'); - } else { - $styleNode = $xmlReader->getElement('w:tblPr', $domNode); - $styleDefs = []; - foreach ($margins as $side) { - $ucfSide = ucfirst($side); - $styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w']; - } - foreach ($borders as $side) { - $ucfSide = ucfirst($side); - $styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz']; - $styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color']; - $styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val']; - } - $styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type']; - $styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual']; - $styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w']; - $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs); - - $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode); - if ($tablePositionNode !== null) { - $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode); - } + $tblStyleName = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle'); + } + $styleNode = $xmlReader->getElement('w:tblPr', $domNode); + $styleDefs = []; - $indentNode = $xmlReader->getElement('w:tblInd', $styleNode); - if ($indentNode !== null) { - $style['indent'] = $this->readTableIndent($xmlReader, $indentNode); - } + foreach ($margins as $side) { + $ucfSide = ucfirst($side); + $styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w']; + } + foreach ($borders as $side) { + $ucfSide = ucfirst($side); + $styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz']; + $styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color']; + $styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val']; + } + $styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type']; + $styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual']; + $styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w']; + $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs); + + $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode); + if ($tablePositionNode !== null) { + $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode); + } + + $indentNode = $xmlReader->getElement('w:tblInd', $styleNode); + if ($indentNode !== null) { + $style['indent'] = $this->readTableIndent($xmlReader, $indentNode); + } + if ($xmlReader->elementExists('w:basedOn', $domNode)) { + $style['basedOn'] = $xmlReader->getAttribute('w:val', $domNode, 'w:basedOn'); + } + if ($tblStyleName !== '') { + $style['tblStyle'] = $tblStyleName; + } + // this may be unneeded + if ($xmlReader->elementExists('w:name', $domNode)) { + $style['styleName'] = $xmlReader->getAttribute('w:val', $domNode, 'w:name'); } } diff --git a/src/PhpWord/Reader/Word2007/Styles.php b/src/PhpWord/Reader/Word2007/Styles.php index 760adf9493..f67bc77463 100644 --- a/src/PhpWord/Reader/Word2007/Styles.php +++ b/src/PhpWord/Reader/Word2007/Styles.php @@ -65,8 +65,9 @@ public function read(PhpWord $phpWord): void foreach ($nodes as $node) { $type = $xmlReader->getAttribute('w:type', $node); $name = $xmlReader->getAttribute('w:val', $node, 'w:name'); + $styleId = $xmlReader->getAttribute('w:styleId', $node); if (null === $name) { - $name = $xmlReader->getAttribute('w:styleId', $node); + $name = $styleId; } $headingMatches = []; preg_match('/Heading\s*(\d)/i', $name, $headingMatches); @@ -98,7 +99,8 @@ public function read(PhpWord $phpWord): void case 'table': $tStyle = $this->readTableStyle($xmlReader, $node); if (!empty($tStyle)) { - $phpWord->addTableStyle($name, $tStyle); + $newTable = $phpWord->addTableStyle($styleId, $tStyle); + $newTable->setStyleName($name); } break; diff --git a/src/PhpWord/Settings.php b/src/PhpWord/Settings.php index 984486ccfe..b43bf05228 100644 --- a/src/PhpWord/Settings.php +++ b/src/PhpWord/Settings.php @@ -15,6 +15,8 @@ namespace PhpOffice\PhpWord; +use PhpOffice\PhpWord\SimpleType\TextDirection; + /** * PHPWord settings class. * @@ -397,6 +399,9 @@ public static function setDefaultFontSize($value): bool public static function setDefaultRtl(?bool $defaultRtl): void { self::$defaultRtl = $defaultRtl; + if ($defaultRtl === true && Style::getStyle('Normal') === null) { + Style::setDefaultParagraphStyle(['bidi' => true, 'textDirection' => TextDirection::RLTB], ['rtl' => true]); + } } public static function isDefaultRtl(): ?bool diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 2022f7da09..21d8404ddc 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -25,9 +25,14 @@ use PhpOffice\PhpWord\Element\AbstractContainer; use PhpOffice\PhpWord\Element\Row; use PhpOffice\PhpWord\Element\Table; +use PhpOffice\PhpWord\Element\TextRun; +use PhpOffice\PhpWord\Metadata\DocInfo; +use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Settings; +use PhpOffice\PhpWord\SimpleType\Border; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\NumberFormat; +use PhpOffice\PhpWord\SimpleType\TextDirection; use PhpOffice\PhpWord\Style\Paragraph; /** @@ -37,6 +42,8 @@ */ class Html { + private const SPECIAL_BORDER_WIDTHS = ['thin' => '0.5pt', 'thick' => '3.5pt', 'medium' => '2.0pt']; + private const RGB_REGEXP = '/^\s*rgb\s*[(]\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*[)]\s*$/'; protected static $listIndex = 0; @@ -45,6 +52,9 @@ class Html protected static $options; + /** @var ?DocInfo */ + protected static $docInfo; + /** * @var Css */ @@ -69,6 +79,14 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit * which could be applied when such an element occurs in the parseNode function. */ static::$options = $options; + static::$docInfo = null; + if (method_exists($element, 'getPhpWord')) { + /** @var ?PhpWord */ + $phpWord = $element->getPhpWord(); + if ($phpWord !== null) { + static::$docInfo = $phpWord->getDocInfo(); + } + } // Preprocess: remove all line ends, decode HTML entity, // fix ampersand and angle brackets and add body tag for HTML fragments @@ -84,17 +102,20 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit // Load DOM if (\PHP_VERSION_ID < 80000) { - $orignalLibEntityLoader = libxml_disable_entity_loader(true); + $orignalLibEntityLoader = libxml_disable_entity_loader(true); // @codeCoverageIgnore } $dom = new DOMDocument(); $dom->preserveWhiteSpace = $preserveWhiteSpace; $dom->loadXML($html); static::$xpath = new DOMXPath($dom); - $node = $dom->getElementsByTagName('body'); + $node = $dom->getElementsByTagName('html'); + if (count($node) === 0) { + $node = $dom->getElementsByTagName('body'); + } static::parseNode($node->item(0), $element); if (\PHP_VERSION_ID < 80000) { - libxml_disable_entity_loader($orignalLibEntityLoader); + libxml_disable_entity_loader($orignalLibEntityLoader); // @codeCoverageIgnore } } @@ -106,12 +127,20 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit * * @return array */ - protected static function parseInlineStyle($node, $styles = []) + protected static function parseInlineStyle($node, &$styles) { if (XML_ELEMENT_NODE == $node->nodeType) { $attributes = $node->attributes; // get all the attributes(eg: id, class) - $bidi = ($attributes['dir'] ?? '') === 'rtl'; + $bidi = false; + $direction = isset($attributes['dir']) ? $attributes['dir']->value : ''; + if ($direction === 'rtl') { + $bidi = $styles['bidi'] = $styles['rtl'] = true; + $styles['textDirection'] = TextDirection::RLTB; + } elseif ($direction === 'ltr') { + $bidi = $styles['bidi'] = $styles['rtl'] = false; + $styles['textDirection'] = TextDirection::LRTB; + } foreach ($attributes as $attribute) { $val = $attribute->value; switch (strtolower($attribute->name)) { @@ -144,7 +173,7 @@ protected static function parseInlineStyle($node, $styles = []) break; case 'bgcolor': // tables, rows, cells e.g. - $styles['bgColor'] = self::convertRgb($val); + HtmlColours::setArrayColour($styles, 'bgColor', self::convertRgb($val)); break; case 'valign': @@ -195,6 +224,46 @@ protected static function parseNode($node, $element, $styles = [], $data = []): return; } + if ($node->nodeName === 'title') { + if (self::$docInfo !== null) { + $docTitle = $node->nodeValue; + if ($docTitle !== 'PHPWord' && trim($docTitle) !== '') { // default + self::$docInfo->setTitle($node->nodeValue); + } + } + + return; + } + if ($node->nodeName === 'meta') { + if (self::$docInfo !== null) { + $attributes = $node->attributes; + $name = $attributes->getNamedItem('name'); + $content = $attributes->getNamedItem('content'); + if ($name !== null && $content !== null) { + $mapArray = ['author' => 'creator']; + $others = [ + 'title', + 'description', + 'subject', + 'keywords', + 'category', + 'company', + 'manager', + ]; + $nameValue = $name->nodeValue; + $propertyName = $mapArray[$nameValue] ?? (in_array($nameValue, $others, true) ? $nameValue : ''); + $method = 'set' . ucfirst($propertyName); + if (method_exists(self::$docInfo, $method)) { + self::$docInfo->$method($content->nodeValue); + } + } + } + + return; + } + if ($node->nodeName === 'script') { + return; + } // Populate styles array $styleTypes = ['font', 'paragraph', 'list', 'table', 'row', 'cell']; @@ -208,12 +277,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], @@ -308,7 +377,12 @@ protected static function parseParagraph($node, $element, &$styles) return $element->addPageBreak(); } - return $element->addTextRun($styles['paragraph']); + $newElement = $element->addTextRun($styles['paragraph']); + if (isset($styles['paragraph']['className']) && $newElement->getParagraphStyle() instanceof Paragraph) { + $newElement->getParagraphStyle()->setStyleName($styles['paragraph']['className']); + } + + return $newElement; } /** @@ -339,21 +413,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; } /** @@ -373,7 +448,11 @@ protected static function parseText($node, $element, &$styles): void } if (is_callable([$element, 'addText'])) { - $element->addText($node->nodeValue, $styles['font'], $styles['paragraph']); + $font = $styles['font']; + if (isset($font['className']) && count($font) === 1) { + $font = $styles['font']['className']; + } + $element->addText($node->nodeValue, $font, $styles['paragraph']); } } @@ -423,9 +502,10 @@ protected static function parseTable($node, $element, &$styles) } $attributes = $node->attributes; - if ($attributes->getNamedItem('border') !== null) { + if ($attributes->getNamedItem('border') !== null && is_object($newElement->getStyle())) { $border = (int) $attributes->getNamedItem('border')->value; - $newElement->getStyle()->setBorderSize(Converter::pixelToTwip($border)); + $newElement->getStyle()->setBorderSize((int) Converter::pixelToTwip($border)); + $newElement->getStyle()->setBorderStyle(($border === 0) ? 'none' : 'single'); } return $newElement; @@ -710,6 +790,7 @@ protected static function parseStyleDeclarations(array $selectors, array $styles case 'direction': $styles['rtl'] = $value === 'rtl'; $styles['bidi'] = $value === 'rtl'; + $styles['textDirection'] = ($value === 'rtl') ? TextDirection::RLTB : TextDirection::LRTB; break; case 'font-size': @@ -722,11 +803,11 @@ protected static function parseStyleDeclarations(array $selectors, array $styles break; case 'color': - $styles['color'] = self::convertRgb($value); + HtmlColours::setArrayColour($styles, 'color', self::convertRgb($value)); break; case 'background-color': - $styles['bgColor'] = self::convertRgb($value); + HtmlColours::setArrayColour($styles, 'bgColor', self::convertRgb($value)); break; case 'line-height': @@ -806,7 +887,7 @@ protected static function parseStyleDeclarations(array $selectors, array $styles break; case 'border-width': - $styles['borderSize'] = Converter::cssToPoint($value); + $styles['borderSize'] = Converter::cssToPoint(self::SPECIAL_BORDER_WIDTHS[$value] ?? $value); break; case 'border-style': @@ -836,29 +917,46 @@ protected static function parseStyleDeclarations(array $selectors, array $styles case 'border-bottom': case 'border-right': case 'border-left': - // must have exact order [width color style], e.g. "1px #0011CC solid" or "2pt green solid" - // Word does not accept shortened hex colors e.g. #CCC, only full e.g. #CCCCCC - if (preg_match('/([0-9]+[^0-9]*)\s+(\#[a-fA-F0-9]+|[a-zA-Z]+)\s+([a-z]+)/', $value, $matches)) { - if (false !== strpos($property, '-')) { - $tmp = explode('-', $property); - $which = $tmp[1]; - $which = ucfirst($which); // e.g. bottom -> Bottom - } else { - $which = ''; - } - // Note - border width normalization: - // Width of border in Word is calculated differently than HTML borders, usually showing up too bold. - // Smallest 1px (or 1pt) appears in Word like 2-3px/pt in HTML once converted to twips. - // Therefore we need to normalize converted twip value to cca 1/2 of value. - // This may be adjusted, if better ratio or formula found. - // BC change: up to ver. 0.17.0 was $size converted to points - Converter::cssToPoint($size) - $size = Converter::cssToTwip($matches[1]); + $stylePattern = '/(^|\\s)(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)(\\s|$)/'; + if (!preg_match($stylePattern, $value, $matches)) { + break; + } + $borderStyle = $matches[2]; + $value = preg_replace($stylePattern, ' ', $value) ?? ''; + $borderSize = $borderColor = null; + $sizePattern = '/(^|\\s)([0-9]+([.][0-9]+)?+(%|[a-z]*)|thick|thin|medium)(\\s|$)/'; + if (preg_match($sizePattern, $value, $matches)) { + $borderSize = $matches[2]; + $borderSize = self::SPECIAL_BORDER_WIDTHS[$borderSize] ?? $borderSize; + $value = preg_replace($sizePattern, ' ', $value) ?? ''; + } + $colorPattern = '/(^|\\s)([#][a-fA-F0-9]{6}|[#][a-fA-F0-9]{3}|[a-z][a-z0-9]+)(\\s|$)/'; + if (preg_match($colorPattern, $value, $matches)) { + $borderColor = HtmlColours::convertColour($matches[2]); + } + if (false !== strpos($property, '-')) { + $tmp = explode('-', $property); + $which = $tmp[1]; + $which = ucfirst($which); // e.g. bottom -> Bottom + } else { + $which = ''; + } + // Note - border width normalization: + // Width of border in Word is calculated differently than HTML borders, usually showing up too bold. + // Smallest 1px (or 1pt) appears in Word like 2-3px/pt in HTML once converted to twips. + // Therefore we need to normalize converted twip value to cca 1/2 of value. + // This may be adjusted, if better ratio or formula found. + // BC change: up to ver. 0.17.0 was $size converted to points - Converter::cssToPoint($size) + if ($borderSize !== null) { + $size = Converter::cssToTwip($borderSize); $size = (int) ($size / 2); // valid variants may be e.g. borderSize, borderTopSize, borderLeftColor, etc .. $styles["border{$which}Size"] = $size; // twips - $styles["border{$which}Color"] = trim($matches[2], '#'); - $styles["border{$which}Style"] = self::mapBorderStyle($matches[3]); } + if (!empty($borderColor)) { + $styles["border{$which}Color"] = $borderColor; + } + $styles["border{$which}Style"] = self::mapBorderStyle($borderStyle); break; case 'vertical-align': @@ -1008,6 +1106,8 @@ protected static function mapBorderStyle($cssBorderStyle) case 'dotted': case 'double': return $cssBorderStyle; + case 'hidden': + return 'none'; default: return 'single'; } @@ -1015,14 +1115,14 @@ protected static function mapBorderStyle($cssBorderStyle) protected static function mapBorderColor(&$styles, $cssBorderColor): void { - $numColors = substr_count($cssBorderColor, '#'); + $colors = explode(' ', $cssBorderColor); + $numColors = count($colors); if ($numColors === 1) { - $styles['borderColor'] = trim($cssBorderColor, '#'); - } elseif ($numColors > 1) { - $colors = explode(' ', $cssBorderColor); + HtmlColours::setArrayColour($styles, 'borderColor', $cssBorderColor); + } else { $borders = ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor']; for ($i = 0; $i < min(4, $numColors, count($colors)); ++$i) { - $styles[$borders[$i]] = trim($colors[$i], '#'); + HtmlColours::setArrayColour($styles, $borders[$i], $colors[$i]); } } } @@ -1148,7 +1248,8 @@ protected static function parseLink($node, $element, &$styles) */ protected static function parseHorizRule($node, $element): void { - $styles = self::parseInlineStyle($node); + $unusedStyle = []; + $styles = self::parseInlineStyle($node, $unusedStyle); //
is implemented as an empty paragraph - extending 100% inside the section // Some properties may be controlled, e.g.
diff --git a/src/PhpWord/Shared/HtmlColours.php b/src/PhpWord/Shared/HtmlColours.php new file mode 100644 index 0000000000..40bc0096c6 --- /dev/null +++ b/src/PhpWord/Shared/HtmlColours.php @@ -0,0 +1,549 @@ + 'f0f8ff', + 'antiquewhite' => 'faebd7', + 'antiquewhite1' => 'ffefdb', + 'antiquewhite2' => 'eedfcc', + 'antiquewhite3' => 'cdc0b0', + 'antiquewhite4' => '8b8378', + 'aqua' => '00ffff', + 'aquamarine1' => '7fffd4', + 'aquamarine2' => '76eec6', + 'aquamarine4' => '458b74', + 'azure1' => 'f0ffff', + 'azure2' => 'e0eeee', + 'azure3' => 'c1cdcd', + 'azure4' => '838b8b', + 'beige' => 'f5f5dc', + 'bisque1' => 'ffe4c4', + 'bisque2' => 'eed5b7', + 'bisque3' => 'cdb79e', + 'bisque4' => '8b7d6b', + 'black' => '000000', + 'blanchedalmond' => 'ffebcd', + 'blue' => '0000ff', + 'blue1' => '0000ff', + 'blue2' => '0000ee', + 'blue4' => '00008b', + 'blueviolet' => '8a2be2', + 'brown' => 'a52a2a', + 'brown1' => 'ff4040', + 'brown2' => 'ee3b3b', + 'brown3' => 'cd3333', + 'brown4' => '8b2323', + 'burlywood' => 'deb887', + 'burlywood1' => 'ffd39b', + 'burlywood2' => 'eec591', + 'burlywood3' => 'cdaa7d', + 'burlywood4' => '8b7355', + 'cadetblue' => '5f9ea0', + 'cadetblue1' => '98f5ff', + 'cadetblue2' => '8ee5ee', + 'cadetblue3' => '7ac5cd', + 'cadetblue4' => '53868b', + 'chartreuse1' => '7fff00', + 'chartreuse2' => '76ee00', + 'chartreuse3' => '66cd00', + 'chartreuse4' => '458b00', + 'chocolate' => 'd2691e', + 'chocolate1' => 'ff7f24', + 'chocolate2' => 'ee7621', + 'chocolate3' => 'cd661d', + 'coral' => 'ff7f50', + 'coral1' => 'ff7256', + 'coral2' => 'ee6a50', + 'coral3' => 'cd5b45', + 'coral4' => '8b3e2f', + 'cornflowerblue' => '6495ed', + 'cornsilk1' => 'fff8dc', + 'cornsilk2' => 'eee8cd', + 'cornsilk3' => 'cdc8b1', + 'cornsilk4' => '8b8878', + 'cyan1' => '00ffff', + 'cyan2' => '00eeee', + 'cyan3' => '00cdcd', + 'cyan4' => '008b8b', + 'darkgoldenrod' => 'b8860b', + 'darkgoldenrod1' => 'ffb90f', + 'darkgoldenrod2' => 'eead0e', + 'darkgoldenrod3' => 'cd950c', + 'darkgoldenrod4' => '8b6508', + 'darkgreen' => '006400', + 'darkkhaki' => 'bdb76b', + 'darkolivegreen' => '556b2f', + 'darkolivegreen1' => 'caff70', + 'darkolivegreen2' => 'bcee68', + 'darkolivegreen3' => 'a2cd5a', + 'darkolivegreen4' => '6e8b3d', + 'darkorange' => 'ff8c00', + 'darkorange1' => 'ff7f00', + 'darkorange2' => 'ee7600', + 'darkorange3' => 'cd6600', + 'darkorange4' => '8b4500', + 'darkorchid' => '9932cc', + 'darkorchid1' => 'bf3eff', + 'darkorchid2' => 'b23aee', + 'darkorchid3' => '9a32cd', + 'darkorchid4' => '68228b', + 'darksalmon' => 'e9967a', + 'darkseagreen' => '8fbc8f', + 'darkseagreen1' => 'c1ffc1', + 'darkseagreen2' => 'b4eeb4', + 'darkseagreen3' => '9bcd9b', + 'darkseagreen4' => '698b69', + 'darkslateblue' => '483d8b', + 'darkslategray' => '2f4f4f', + 'darkslategray1' => '97ffff', + 'darkslategray2' => '8deeee', + 'darkslategray3' => '79cdcd', + 'darkslategray4' => '528b8b', + 'darkturquoise' => '00ced1', + 'darkviolet' => '9400d3', + 'deeppink1' => 'ff1493', + 'deeppink2' => 'ee1289', + 'deeppink3' => 'cd1076', + 'deeppink4' => '8b0a50', + 'deepskyblue1' => '00bfff', + 'deepskyblue2' => '00b2ee', + 'deepskyblue3' => '009acd', + 'deepskyblue4' => '00688b', + 'dimgray' => '696969', + 'dodgerblue1' => '1e90ff', + 'dodgerblue2' => '1c86ee', + 'dodgerblue3' => '1874cd', + 'dodgerblue4' => '104e8b', + 'firebrick' => 'b22222', + 'firebrick1' => 'ff3030', + 'firebrick2' => 'ee2c2c', + 'firebrick3' => 'cd2626', + 'firebrick4' => '8b1a1a', + 'floralwhite' => 'fffaf0', + 'forestgreen' => '228b22', + 'fuchsia' => 'ff00ff', + 'gainsboro' => 'dcdcdc', + 'ghostwhite' => 'f8f8ff', + 'gold1' => 'ffd700', + 'gold2' => 'eec900', + 'gold3' => 'cdad00', + 'gold4' => '8b7500', + 'goldenrod' => 'daa520', + 'goldenrod1' => 'ffc125', + 'goldenrod2' => 'eeb422', + 'goldenrod3' => 'cd9b1d', + 'goldenrod4' => '8b6914', + 'gray' => 'bebebe', + 'gray1' => '030303', + 'gray10' => '1a1a1a', + 'gray11' => '1c1c1c', + 'gray12' => '1f1f1f', + 'gray13' => '212121', + 'gray14' => '242424', + 'gray15' => '262626', + 'gray16' => '292929', + 'gray17' => '2b2b2b', + 'gray18' => '2e2e2e', + 'gray19' => '303030', + 'gray2' => '050505', + 'gray20' => '333333', + 'gray21' => '363636', + 'gray22' => '383838', + 'gray23' => '3b3b3b', + 'gray24' => '3d3d3d', + 'gray25' => '404040', + 'gray26' => '424242', + 'gray27' => '454545', + 'gray28' => '474747', + 'gray29' => '4a4a4a', + 'gray3' => '080808', + 'gray30' => '4d4d4d', + 'gray31' => '4f4f4f', + 'gray32' => '525252', + 'gray33' => '545454', + 'gray34' => '575757', + 'gray35' => '595959', + 'gray36' => '5c5c5c', + 'gray37' => '5e5e5e', + 'gray38' => '616161', + 'gray39' => '636363', + 'gray4' => '0a0a0a', + 'gray40' => '666666', + 'gray41' => '696969', + 'gray42' => '6b6b6b', + 'gray43' => '6e6e6e', + 'gray44' => '707070', + 'gray45' => '737373', + 'gray46' => '757575', + 'gray47' => '787878', + 'gray48' => '7a7a7a', + 'gray49' => '7d7d7d', + 'gray5' => '0d0d0d', + 'gray50' => '7f7f7f', + 'gray51' => '828282', + 'gray52' => '858585', + 'gray53' => '878787', + 'gray54' => '8a8a8a', + 'gray55' => '8c8c8c', + 'gray56' => '8f8f8f', + 'gray57' => '919191', + 'gray58' => '949494', + 'gray59' => '969696', + 'gray6' => '0f0f0f', + 'gray60' => '999999', + 'gray61' => '9c9c9c', + 'gray62' => '9e9e9e', + 'gray63' => 'a1a1a1', + 'gray64' => 'a3a3a3', + 'gray65' => 'a6a6a6', + 'gray66' => 'a8a8a8', + 'gray67' => 'ababab', + 'gray68' => 'adadad', + 'gray69' => 'b0b0b0', + 'gray7' => '121212', + 'gray70' => 'b3b3b3', + 'gray71' => 'b5b5b5', + 'gray72' => 'b8b8b8', + 'gray73' => 'bababa', + 'gray74' => 'bdbdbd', + 'gray75' => 'bfbfbf', + 'gray76' => 'c2c2c2', + 'gray77' => 'c4c4c4', + 'gray78' => 'c7c7c7', + 'gray79' => 'c9c9c9', + 'gray8' => '141414', + 'gray80' => 'cccccc', + 'gray81' => 'cfcfcf', + 'gray82' => 'd1d1d1', + 'gray83' => 'd4d4d4', + 'gray84' => 'd6d6d6', + 'gray85' => 'd9d9d9', + 'gray86' => 'dbdbdb', + 'gray87' => 'dedede', + 'gray88' => 'e0e0e0', + 'gray89' => 'e3e3e3', + 'gray9' => '171717', + 'gray90' => 'e5e5e5', + 'gray91' => 'e8e8e8', + 'gray92' => 'ebebeb', + 'gray93' => 'ededed', + 'gray94' => 'f0f0f0', + 'gray95' => 'f2f2f2', + 'gray97' => 'f7f7f7', + 'gray98' => 'fafafa', + 'gray99' => 'fcfcfc', + 'green' => '00ff00', + 'green1' => '00ff00', + 'green2' => '00ee00', + 'green3' => '00cd00', + 'green4' => '008b00', + 'greenyellow' => 'adff2f', + 'honeydew1' => 'f0fff0', + 'honeydew2' => 'e0eee0', + 'honeydew3' => 'c1cdc1', + 'honeydew4' => '838b83', + 'hotpink' => 'ff69b4', + 'hotpink1' => 'ff6eb4', + 'hotpink2' => 'ee6aa7', + 'hotpink3' => 'cd6090', + 'hotpink4' => '8b3a62', + 'indianred' => 'cd5c5c', + 'indianred1' => 'ff6a6a', + 'indianred2' => 'ee6363', + 'indianred3' => 'cd5555', + 'indianred4' => '8b3a3a', + 'ivory1' => 'fffff0', + 'ivory2' => 'eeeee0', + 'ivory3' => 'cdcdc1', + 'ivory4' => '8b8b83', + 'khaki' => 'f0e68c', + 'khaki1' => 'fff68f', + 'khaki2' => 'eee685', + 'khaki3' => 'cdc673', + 'khaki4' => '8b864e', + 'lavender' => 'e6e6fa', + 'lavenderblush1' => 'fff0f5', + 'lavenderblush2' => 'eee0e5', + 'lavenderblush3' => 'cdc1c5', + 'lavenderblush4' => '8b8386', + 'lawngreen' => '7cfc00', + 'lemonchiffon1' => 'fffacd', + 'lemonchiffon2' => 'eee9bf', + 'lemonchiffon3' => 'cdc9a5', + 'lemonchiffon4' => '8b8970', + 'light' => 'eedd82', + 'lightblue' => 'add8e6', + 'lightblue1' => 'bfefff', + 'lightblue2' => 'b2dfee', + 'lightblue3' => '9ac0cd', + 'lightblue4' => '68838b', + 'lightcoral' => 'f08080', + 'lightcyan1' => 'e0ffff', + 'lightcyan2' => 'd1eeee', + 'lightcyan3' => 'b4cdcd', + 'lightcyan4' => '7a8b8b', + 'lightgoldenrod1' => 'ffec8b', + 'lightgoldenrod2' => 'eedc82', + 'lightgoldenrod3' => 'cdbe70', + 'lightgoldenrod4' => '8b814c', + 'lightgoldenrodyellow' => 'fafad2', + 'lightgray' => 'd3d3d3', + 'lightpink' => 'ffb6c1', + 'lightpink1' => 'ffaeb9', + 'lightpink2' => 'eea2ad', + 'lightpink3' => 'cd8c95', + 'lightpink4' => '8b5f65', + 'lightsalmon1' => 'ffa07a', + 'lightsalmon2' => 'ee9572', + 'lightsalmon3' => 'cd8162', + 'lightsalmon4' => '8b5742', + 'lightseagreen' => '20b2aa', + 'lightskyblue' => '87cefa', + 'lightskyblue1' => 'b0e2ff', + 'lightskyblue2' => 'a4d3ee', + 'lightskyblue3' => '8db6cd', + 'lightskyblue4' => '607b8b', + 'lightslateblue' => '8470ff', + 'lightslategray' => '778899', + 'lightsteelblue' => 'b0c4de', + 'lightsteelblue1' => 'cae1ff', + 'lightsteelblue2' => 'bcd2ee', + 'lightsteelblue3' => 'a2b5cd', + 'lightsteelblue4' => '6e7b8b', + 'lightyellow1' => 'ffffe0', + 'lightyellow2' => 'eeeed1', + 'lightyellow3' => 'cdcdb4', + 'lightyellow4' => '8b8b7a', + 'lime' => '00ff00', + 'limegreen' => '32cd32', + 'linen' => 'faf0e6', + 'magenta' => 'ff00ff', + 'magenta2' => 'ee00ee', + 'magenta3' => 'cd00cd', + 'magenta4' => '8b008b', + 'maroon' => 'b03060', + 'maroon1' => 'ff34b3', + 'maroon2' => 'ee30a7', + 'maroon3' => 'cd2990', + 'maroon4' => '8b1c62', + 'medium' => '66cdaa', + 'mediumaquamarine' => '66cdaa', + 'mediumblue' => '0000cd', + 'mediumorchid' => 'ba55d3', + 'mediumorchid1' => 'e066ff', + 'mediumorchid2' => 'd15fee', + 'mediumorchid3' => 'b452cd', + 'mediumorchid4' => '7a378b', + 'mediumpurple' => '9370db', + 'mediumpurple1' => 'ab82ff', + 'mediumpurple2' => '9f79ee', + 'mediumpurple3' => '8968cd', + 'mediumpurple4' => '5d478b', + 'mediumseagreen' => '3cb371', + 'mediumslateblue' => '7b68ee', + 'mediumspringgreen' => '00fa9a', + 'mediumturquoise' => '48d1cc', + 'mediumvioletred' => 'c71585', + 'midnightblue' => '191970', + 'mintcream' => 'f5fffa', + 'mistyrose1' => 'ffe4e1', + 'mistyrose2' => 'eed5d2', + 'mistyrose3' => 'cdb7b5', + 'mistyrose4' => '8b7d7b', + 'moccasin' => 'ffe4b5', + 'navajowhite1' => 'ffdead', + 'navajowhite2' => 'eecfa1', + 'navajowhite3' => 'cdb38b', + 'navajowhite4' => '8b795e', + 'navy' => '000080', + 'navyblue' => '000080', + 'oldlace' => 'fdf5e6', + 'olive' => '808000', + 'olivedrab' => '6b8e23', + 'olivedrab1' => 'c0ff3e', + 'olivedrab2' => 'b3ee3a', + 'olivedrab4' => '698b22', + 'orange' => 'ffa500', + 'orange1' => 'ffa500', + 'orange2' => 'ee9a00', + 'orange3' => 'cd8500', + 'orange4' => '8b5a00', + 'orangered1' => 'ff4500', + 'orangered2' => 'ee4000', + 'orangered3' => 'cd3700', + 'orangered4' => '8b2500', + 'orchid' => 'da70d6', + 'orchid1' => 'ff83fa', + 'orchid2' => 'ee7ae9', + 'orchid3' => 'cd69c9', + 'orchid4' => '8b4789', + 'pale' => 'db7093', + 'palegoldenrod' => 'eee8aa', + 'palegreen' => '98fb98', + 'palegreen1' => '9aff9a', + 'palegreen2' => '90ee90', + 'palegreen3' => '7ccd7c', + 'palegreen4' => '548b54', + 'paleturquoise' => 'afeeee', + 'paleturquoise1' => 'bbffff', + 'paleturquoise2' => 'aeeeee', + 'paleturquoise3' => '96cdcd', + 'paleturquoise4' => '668b8b', + 'palevioletred' => 'db7093', + 'palevioletred1' => 'ff82ab', + 'palevioletred2' => 'ee799f', + 'palevioletred3' => 'cd6889', + 'palevioletred4' => '8b475d', + 'papayawhip' => 'ffefd5', + 'peachpuff1' => 'ffdab9', + 'peachpuff2' => 'eecbad', + 'peachpuff3' => 'cdaf95', + 'peachpuff4' => '8b7765', + 'pink' => 'ffc0cb', + 'pink1' => 'ffb5c5', + 'pink2' => 'eea9b8', + 'pink3' => 'cd919e', + 'pink4' => '8b636c', + 'plum' => 'dda0dd', + 'plum1' => 'ffbbff', + 'plum2' => 'eeaeee', + 'plum3' => 'cd96cd', + 'plum4' => '8b668b', + 'powderblue' => 'b0e0e6', + 'purple' => 'a020f0', + 'rebeccapurple' => '663399', + 'purple1' => '9b30ff', + 'purple2' => '912cee', + 'purple3' => '7d26cd', + 'purple4' => '551a8b', + 'red' => 'ff0000', + 'red1' => 'ff0000', + 'red2' => 'ee0000', + 'red3' => 'cd0000', + 'red4' => '8b0000', + 'rosybrown' => 'bc8f8f', + 'rosybrown1' => 'ffc1c1', + 'rosybrown2' => 'eeb4b4', + 'rosybrown3' => 'cd9b9b', + 'rosybrown4' => '8b6969', + 'royalblue' => '4169e1', + 'royalblue1' => '4876ff', + 'royalblue2' => '436eee', + 'royalblue3' => '3a5fcd', + 'royalblue4' => '27408b', + 'saddlebrown' => '8b4513', + 'salmon' => 'fa8072', + 'salmon1' => 'ff8c69', + 'salmon2' => 'ee8262', + 'salmon3' => 'cd7054', + 'salmon4' => '8b4c39', + 'sandybrown' => 'f4a460', + 'seagreen1' => '54ff9f', + 'seagreen2' => '4eee94', + 'seagreen3' => '43cd80', + 'seagreen4' => '2e8b57', + 'seashell1' => 'fff5ee', + 'seashell2' => 'eee5de', + 'seashell3' => 'cdc5bf', + 'seashell4' => '8b8682', + 'sienna' => 'a0522d', + 'sienna1' => 'ff8247', + 'sienna2' => 'ee7942', + 'sienna3' => 'cd6839', + 'sienna4' => '8b4726', + 'silver' => 'c0c0c0', + 'skyblue' => '87ceeb', + 'skyblue1' => '87ceff', + 'skyblue2' => '7ec0ee', + 'skyblue3' => '6ca6cd', + 'skyblue4' => '4a708b', + 'slateblue' => '6a5acd', + 'slateblue1' => '836fff', + 'slateblue2' => '7a67ee', + 'slateblue3' => '6959cd', + 'slateblue4' => '473c8b', + 'slategray' => '708090', + 'slategray1' => 'c6e2ff', + 'slategray2' => 'b9d3ee', + 'slategray3' => '9fb6cd', + 'slategray4' => '6c7b8b', + 'snow1' => 'fffafa', + 'snow2' => 'eee9e9', + 'snow3' => 'cdc9c9', + 'snow4' => '8b8989', + 'springgreen1' => '00ff7f', + 'springgreen2' => '00ee76', + 'springgreen3' => '00cd66', + 'springgreen4' => '008b45', + 'steelblue' => '4682b4', + 'steelblue1' => '63b8ff', + 'steelblue2' => '5cacee', + 'steelblue3' => '4f94cd', + 'steelblue4' => '36648b', + 'tan' => 'd2b48c', + 'tan1' => 'ffa54f', + 'tan2' => 'ee9a49', + 'tan3' => 'cd853f', + 'tan4' => '8b5a2b', + 'teal' => '008080', + 'thistle' => 'd8bfd8', + 'thistle1' => 'ffe1ff', + 'thistle2' => 'eed2ee', + 'thistle3' => 'cdb5cd', + 'thistle4' => '8b7b8b', + 'tomato1' => 'ff6347', + 'tomato2' => 'ee5c42', + 'tomato3' => 'cd4f39', + 'tomato4' => '8b3626', + 'turquoise' => '40e0d0', + 'turquoise1' => '00f5ff', + 'turquoise2' => '00e5ee', + 'turquoise3' => '00c5cd', + 'turquoise4' => '00868b', + 'violet' => 'ee82ee', + 'violetred' => 'd02090', + 'violetred1' => 'ff3e96', + 'violetred2' => 'ee3a8c', + 'violetred3' => 'cd3278', + 'violetred4' => '8b2252', + 'wheat' => 'f5deb3', + 'wheat1' => 'ffe7ba', + 'wheat2' => 'eed8ae', + 'wheat3' => 'cdba96', + 'wheat4' => '8b7e66', + 'white' => 'ffffff', + 'whitesmoke' => 'f5f5f5', + 'yellow' => 'ffff00', + 'yellow1' => 'ffff00', + 'yellow2' => 'eeee00', + 'yellow3' => 'cdcd00', + 'yellow4' => '8b8b00', + 'yellowgreen' => '9acd32', + ]; + + public static function colourNameLookup(string $colorName): string + { + return self::COLOUR_MAP[$colorName] ?? ''; + } + + public static function convertColour(string $colorName): string + { + $colorName = trim($colorName); + if (preg_match('/^[#][a-fA-F0-9]{6}$/', $colorName) === 1) { + return substr($colorName, 1); + } + if (preg_match('/^[#][a-fA-F0-9]{3}$/', $colorName) === 1) { + return "{$colorName[1]}{$colorName[1]}{$colorName[2]}{$colorName[2]}{$colorName[3]}{$colorName[3]}"; + } + + return self::COLOUR_MAP[$colorName] ?? $colorName; + } + + public static function setArrayColour(array &$array, string $index, string $colorName): void + { + $array[$index] = self::convertColour($colorName); + } +} diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index 5ff42e49b9..cb6e26e620 100644 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -34,6 +34,9 @@ class PasswordEncoder const ALGORITHM_MAC = 'MAC'; const ALGORITHM_HMAC = 'HMAC'; + private const ALL_ONE_BITS = (PHP_INT_SIZE > 4) ? 0xFFFFFFFF : -1; + private const HIGH_ORDER_BIT = (PHP_INT_SIZE > 4) ? 0x80000000 : PHP_INT_MIN; + /** * Mapping between algorithm name and algorithm ID. * @@ -128,7 +131,7 @@ public static function hashPassword($password, $algorithmName = self::ALGORITHM_ // build low-order word and hig-order word and combine them $combinedKey = self::buildCombinedKey($byteChars); // build reversed hexadecimal string - $hex = str_pad(strtoupper(dechex($combinedKey & 0xFFFFFFFF)), 8, '0', \STR_PAD_LEFT); + $hex = str_pad(strtoupper(dechex($combinedKey & self::ALL_ONE_BITS)), 8, '0', \STR_PAD_LEFT); $reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]; $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); @@ -232,10 +235,10 @@ private static function buildCombinedKey($byteChars) */ private static function int32($value) { - $value = ($value & 0xFFFFFFFF); + $value = ($value & self::ALL_ONE_BITS); - if ($value & 0x80000000) { - $value = -((~$value & 0xFFFFFFFF) + 1); + if ($value & self::HIGH_ORDER_BIT) { + $value = -((~$value & self::ALL_ONE_BITS) + 1); } return $value; diff --git a/src/PhpWord/Shared/ZipArchive.php b/src/PhpWord/Shared/ZipArchive.php index ce4d22533e..f120756d8b 100644 --- a/src/PhpWord/Shared/ZipArchive.php +++ b/src/PhpWord/Shared/ZipArchive.php @@ -20,6 +20,7 @@ use PclZip; use PhpOffice\PhpWord\Exception\Exception; use PhpOffice\PhpWord\Settings; +use Throwable; /** * ZipArchive wrapper. @@ -162,13 +163,16 @@ public function open($filename, $flags = null) * Close the active archive. * * @return bool - * - * @codeCoverageIgnore Can't find any test case. Uncomment when found. */ public function close() { if (!$this->usePclzip) { - if ($this->zip->close() === false) { + try { + $result = @$this->zip->close(); + } catch (Throwable $e) { + $result = false; + } + if ($result === false) { throw new Exception("Could not close zip file {$this->filename}: "); } } diff --git a/src/PhpWord/SimpleType/TextDirection.php b/src/PhpWord/SimpleType/TextDirection.php new file mode 100644 index 0000000000..0797fa9294 --- /dev/null +++ b/src/PhpWord/SimpleType/TextDirection.php @@ -0,0 +1,55 @@ +getParagraph(); } /** diff --git a/src/PhpWord/Style/AbstractStyle.php b/src/PhpWord/Style/AbstractStyle.php index 4e5def618d..1fbcdcd3d4 100644 --- a/src/PhpWord/Style/AbstractStyle.php +++ b/src/PhpWord/Style/AbstractStyle.php @@ -50,6 +50,9 @@ abstract class AbstractStyle */ protected $aliases = []; + /** @var string */ + protected $basedOn = ''; + /** * Is this an automatic style? (Used primarily in OpenDocument driver). * @@ -83,6 +86,18 @@ public function setStyleName($value) return $this; } + public function getBasedOn(): string + { + return $this->basedOn; + } + + public function setBasedOn(string $value): self + { + $this->basedOn = $value; + + return $this; + } + /** * Get index number. * diff --git a/src/PhpWord/Style/Border.php b/src/PhpWord/Style/Border.php index 28e340c040..8be7298840 100644 --- a/src/PhpWord/Style/Border.php +++ b/src/PhpWord/Style/Border.php @@ -528,6 +528,14 @@ public function setBorderBottomStyle($value = null) public function hasBorder() { $borders = $this->getBorderSize(); + if ($borders !== array_filter($borders, 'is_null')) { + return true; + } + $borders = $this->getBorderColor(); + if ($borders !== array_filter($borders, 'is_null')) { + return true; + } + $borders = $this->getBorderStyle(); return $borders !== array_filter($borders, 'is_null'); } diff --git a/src/PhpWord/Style/Paragraph.php b/src/PhpWord/Style/Paragraph.php index c77617403d..a2ab326400 100644 --- a/src/PhpWord/Style/Paragraph.php +++ b/src/PhpWord/Style/Paragraph.php @@ -22,6 +22,7 @@ use PhpOffice\PhpWord\Shared\Text; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\TextAlignment; +use PhpOffice\PhpWord\SimpleType\TextDirection; /** * Paragraph style. @@ -69,7 +70,7 @@ class Paragraph extends Border * * @var string */ - private $basedOn = 'Normal'; + protected $basedOn = 'Normal'; /** * Style for next paragraph. @@ -181,6 +182,13 @@ class Paragraph extends Border */ private $textAlignment; + /** + * Text direction right or left, top or bottom. + * + * @var string + */ + private $textDirection = ''; + /** * Suppress hyphenation for paragraph. * @@ -241,6 +249,7 @@ public function getStyleValues() 'contextualSpacing' => $this->hasContextualSpacing(), 'bidi' => $this->isBidi(), 'textAlignment' => $this->getTextAlignment(), + 'textDirection' => $this->getTextDirection(), 'suppressAutoHyphens' => $this->hasSuppressAutoHyphens(), ]; @@ -273,30 +282,6 @@ public function setAlignment($value) return $this; } - /** - * Get parent style ID. - * - * @return string - */ - public function getBasedOn() - { - return $this->basedOn; - } - - /** - * Set parent style ID. - * - * @param string $value - * - * @return self - */ - public function setBasedOn($value = 'Normal') - { - $this->basedOn = $value; - - return $this; - } - /** * Get style for next paragraph. * @@ -807,6 +792,19 @@ public function setTextAlignment($textAlignment) return $this; } + public function getTextDirection(): string + { + return ($this->textDirection === '' && $this->isBidi()) ? TextDirection::TBRL : $this->textDirection; + } + + public function setTextDirection(string $textDirection): self + { + TextDirection::validate($textDirection); + $this->textDirection = $textDirection; + + return $this; + } + /** * @return bool */ diff --git a/src/PhpWord/Style/Table.php b/src/PhpWord/Style/Table.php index 3adb1a38f5..59c467affd 100644 --- a/src/PhpWord/Style/Table.php +++ b/src/PhpWord/Style/Table.php @@ -110,6 +110,20 @@ class Table extends Border */ private $borderInsideVColor; + /** + * Border style inside horizontal. + * + * @var string + */ + protected $borderInsideHStyle = ''; + + /** + * Border style inside vertical. + * + * @var string + */ + protected $borderInsideVStyle = ''; + /** * Shading. * @@ -168,6 +182,9 @@ class Table extends Border */ private $bidiVisual; + /** @var string */ + private $tblStyle = ''; + /** * Create new table style. * @@ -260,6 +277,42 @@ public function getBorderSize() ]; } + /** + * Get border style. + * + * @return string[] + */ + public function getBorderStyle() + { + return [ + $this->getBorderTopStyle(), + $this->getBorderLeftStyle(), + $this->getBorderRightStyle(), + $this->getBorderBottomStyle(), + $this->getBorderInsideHStyle(), + $this->getBorderInsideVStyle(), + ]; + } + + /** + * Set border style. + * + * @param string $value + * + * @return self + */ + public function setBorderStyle($value = null) + { + $this->setBorderTopStyle($value); + $this->setBorderLeftStyle($value); + $this->setBorderRightStyle($value); + $this->setBorderBottomStyle($value); + $this->setBorderInsideHStyle($value); + $this->setBorderInsideVStyle($value); + + return $this; + } + /** * Set TLRBHV Border Size. * @@ -315,6 +368,26 @@ public function setBorderColor($value = null) return $this; } + /** + * Get border style inside horizontal. + * + * @return string + */ + public function getBorderInsideHStyle() + { + return (string) $this->getTableOnlyProperty('borderInsideHStyle'); + } + + /** + * Get border style inside vertical. + * + * @return string + */ + public function getBorderInsideVStyle() + { + return (string) $this->getTableOnlyProperty('borderInsideVStyle'); + } + /** * Get border size inside horizontal. * @@ -337,6 +410,34 @@ public function setBorderInsideHSize($value = null) return $this->setTableOnlyProperty('borderInsideHSize', $value); } + /** + * Set border style inside horizontal. + * + * @param string $value + * + * @return self + */ + public function setBorderInsideHStyle($value = '') + { + $this->setTableOnlyProperty('borderInsideHStyle', $value, false); + + return $this; + } + + /** + * Set border style inside horizontal. + * + * @param ?string $value + * + * @return self + */ + public function setBorderInsideVStyle($value = null) + { + $this->setTableOnlyProperty('borderInsideVStyle', $value, false); + + return $this; + } + /** * Get border color inside horizontal. * @@ -791,4 +892,16 @@ public function setBidiVisual($bidi) return $this; } + + public function getTblStyle(): string + { + return $this->tblStyle; + } + + public function setTblStyle(string $tblStyle): self + { + $this->tblStyle = $tblStyle; + + return $this; + } } diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 8aee40c546..840520e008 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -275,9 +275,13 @@ protected static function ensureUtf8Encoded($subject) /** * @param string $search */ - public function setComplexValue($search, Element\AbstractElement $complexType): void + public function setComplexValue($search, Element\AbstractElement $complexType, bool $multiple = false): void { + $originalSearch = $search; $elementName = substr(get_class($complexType), strrpos(get_class($complexType), '\\') + 1); + if ($elementName === 'Section') { + $elementName = 'Container'; + } $objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName; $xmlWriter = new XMLWriter(); @@ -297,6 +301,9 @@ public function setComplexValue($search, Element\AbstractElement $complexType): $search = static::ensureMacroCompleted($search); $this->replaceXmlBlock($search, $xmlWriter->getData(), 'w:r'); + if ($multiple === true) { + $this->setComplexValue($originalSearch, $complexType, true); + } } /** @@ -305,6 +312,9 @@ public function setComplexValue($search, Element\AbstractElement $complexType): public function setComplexBlock($search, Element\AbstractElement $complexType): void { $elementName = substr(get_class($complexType), strrpos(get_class($complexType), '\\') + 1); + if ($elementName === 'Section') { + $elementName = 'Container'; + } $objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName; $xmlWriter = new XMLWriter(); diff --git a/src/PhpWord/Writer/HTML/Element/Table.php b/src/PhpWord/Writer/HTML/Element/Table.php index c7a23d2fe1..742d09ffd5 100644 --- a/src/PhpWord/Writer/HTML/Element/Table.php +++ b/src/PhpWord/Writer/HTML/Element/Table.php @@ -41,7 +41,8 @@ public function write() $rows = $this->element->getRows(); $rowCount = count($rows); if ($rowCount > 0) { - $content .= 'getTableStyle($this->element->getStyle()) . '>' . PHP_EOL; + $tableCss = $this->getTableStyle($this->element->getStyle()); + $content .= '' . PHP_EOL; for ($i = 0; $i < $rowCount; ++$i) { /** @var \PhpOffice\PhpWord\Element\Row $row Type hint */ @@ -53,7 +54,7 @@ public function write() $rowCellCount = count($rowCells); for ($j = 0; $j < $rowCellCount; ++$j) { $cellStyle = $rowCells[$j]->getStyle(); - $cellStyleCss = $this->getTableStyle($cellStyle); + $cellStyleCss = $this->getTableStyle($cellStyle) ?: $tableCss; $cellBgColor = $cellStyle->getBgColor(); $cellFgColor = null; if ($cellBgColor && $cellBgColor !== 'auto') { 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..e31432ef1c 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', ], @@ -119,6 +118,9 @@ private function writeStyles(): string 'td' => [ 'border' => '1px solid black', ], + 'th' => [ + 'border' => '1px solid black', + ], ] as $selector => $style) { $styleWriter = new GenericStyleWriter($style); $css .= $selector . ' {' . $styleWriter->write() . '}' . PHP_EOL; @@ -137,8 +139,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/src/PhpWord/Writer/HTML/Style/Font.php b/src/PhpWord/Writer/HTML/Style/Font.php index eb59d02d1e..29b35687a7 100644 --- a/src/PhpWord/Writer/HTML/Style/Font.php +++ b/src/PhpWord/Writer/HTML/Style/Font.php @@ -73,6 +73,13 @@ public function write() } elseif ($style->isRTL() === false) { $css['direction'] = 'ltr'; } + $shading = $style->getShading(); + if ($shading !== null) { + $fill = $shading->getFill(); + if (!empty($fill)) { + $css['background-color'] = preg_match('/^[0-9a-fA-F]{6}$/', $fill) ? "#$fill" : $fill; + } + } return $this->assembleCss($css); } diff --git a/src/PhpWord/Writer/HTML/Style/Table.php b/src/PhpWord/Writer/HTML/Style/Table.php index d2c318a69f..f4b50f56c8 100644 --- a/src/PhpWord/Writer/HTML/Style/Table.php +++ b/src/PhpWord/Writer/HTML/Style/Table.php @@ -54,7 +54,7 @@ public function write() if ($outval === 'single') { $outval = 'solid'; } - if (is_string($outval) && 1 == preg_match('/^[a-z]+$/', $outval)) { + if (is_string($outval) && 1 === preg_match('/^[a-z]+$/', $outval)) { $css['border-' . lcfirst($direction) . '-style'] = $outval; } } @@ -62,7 +62,9 @@ public function write() $method = 'getBorder' . $direction . 'Color'; if (method_exists($style, $method)) { $outval = $style->{$method}(); - if (is_string($outval) && 1 == preg_match('/^[a-z]+$/', $outval)) { + if (is_string($outval) && 1 === preg_match('/^[a-fA-F0-9]{6}$/', $outval)) { + $css['border-' . lcfirst($direction) . '-color'] = "#$outval"; + } elseif (is_string($outval) && 1 === preg_match('/^[a-z][a-z0-9]+$/', $outval)) { $css['border-' . lcfirst($direction) . '-color'] = $outval; } } diff --git a/src/PhpWord/Writer/RTF.php b/src/PhpWord/Writer/RTF.php index 0a04d4f53e..e588b8a1d5 100644 --- a/src/PhpWord/Writer/RTF.php +++ b/src/PhpWord/Writer/RTF.php @@ -67,7 +67,7 @@ public function save(string $filename): void * * @since 0.11.0 */ - private function getContent() + public function getContent() { $content = ''; diff --git a/src/PhpWord/Writer/RTF/Element/Title.php b/src/PhpWord/Writer/RTF/Element/Title.php index fb11da7849..cef9038571 100644 --- a/src/PhpWord/Writer/RTF/Element/Title.php +++ b/src/PhpWord/Writer/RTF/Element/Title.php @@ -57,8 +57,13 @@ public function write() { /** @var \PhpOffice\PhpWord\Element\Title $element Type hint */ $element = $this->element; + $text = method_exists($element, 'getText') ? $element->getText() : null; + // check for text run + if (is_object($text) && method_exists($text, 'getText')) { + $text = $text->getText(); + } $elementClass = str_replace('\\Writer\\RTF', '', static::class); - if (!$element instanceof $elementClass || !is_string($element->getText())) { + if (!$element instanceof $elementClass || !is_string($text)) { return ''; } @@ -82,7 +87,7 @@ public function write() $content .= '{'; $content .= $this->writeFontStyle(); - $content .= $this->writeText($element->getText()); + $content .= $this->writeText($text); $content .= '}'; $content .= $this->writeClosing(); $content .= $endout; diff --git a/src/PhpWord/Writer/Word2007/Element/Table.php b/src/PhpWord/Writer/Word2007/Element/Table.php index 9364fe45c1..a32cc19639 100644 --- a/src/PhpWord/Writer/Word2007/Element/Table.php +++ b/src/PhpWord/Writer/Word2007/Element/Table.php @@ -103,8 +103,14 @@ private function writeRow(XMLWriter $xmlWriter, RowElement $row): void } // Write cells - foreach ($row->getCells() as $cell) { - $this->writeCell($xmlWriter, $cell); + $cells = $row->getCells(); + if (count($cells) === 0) { + // issue 2505 - Word treats doc as corrupt if row without cell + $this->writeCell($xmlWriter, new CellElement()); + } else { + foreach ($cells as $cell) { + $this->writeCell($xmlWriter, $cell); + } } $xmlWriter->endElement(); // w:tr diff --git a/src/PhpWord/Writer/Word2007/Part/Styles.php b/src/PhpWord/Writer/Word2007/Part/Styles.php index 2112fd3ce6..41ff553c3c 100644 --- a/src/PhpWord/Writer/Word2007/Part/Styles.php +++ b/src/PhpWord/Writer/Word2007/Part/Styles.php @@ -59,12 +59,13 @@ public function write() if ($styleName == 'Normal') { continue; } + $name = $style->getStyleName(); // Get style class and execute if the private method exists $styleClass = substr(get_class($style), strrpos(get_class($style), '\\') + 1); $method = "write{$styleClass}Style"; if (method_exists($this, $method)) { - $this->$method($xmlWriter, $styleName, $style); + $this->$method($xmlWriter, $styleName, $style, $name); } } } @@ -163,7 +164,7 @@ private function writeDefaultStyles(XMLWriter $xmlWriter, $styles): void * * @param string $styleName */ - private function writeFontStyle(XMLWriter $xmlWriter, $styleName, FontStyle $style): void + private function writeFontStyle(XMLWriter $xmlWriter, $styleName, FontStyle $style, string $name): void { $paragraphStyle = $style->getParagraph(); $styleType = $style->getStyleType(); @@ -204,7 +205,7 @@ private function writeFontStyle(XMLWriter $xmlWriter, $styleName, FontStyle $sty // Parent style if (null !== $paragraphStyle) { - if ($paragraphStyle->getStyleName() != null) { + if (!empty($paragraphStyle->getStyleName())) { $xmlWriter->writeElementBlock('w:basedOn', 'w:val', $paragraphStyle->getStyleName()); } elseif ($paragraphStyle->getBasedOn() != null) { $xmlWriter->writeElementBlock('w:basedOn', 'w:val', $paragraphStyle->getBasedOn()); @@ -229,7 +230,7 @@ private function writeFontStyle(XMLWriter $xmlWriter, $styleName, FontStyle $sty * * @param string $styleName */ - private function writeParagraphStyle(XMLWriter $xmlWriter, $styleName, ParagraphStyle $style): void + private function writeParagraphStyle(XMLWriter $xmlWriter, $styleName, ParagraphStyle $style, string $name): void { $xmlWriter->startElement('w:style'); $xmlWriter->writeAttribute('w:type', 'paragraph'); @@ -241,7 +242,7 @@ private function writeParagraphStyle(XMLWriter $xmlWriter, $styleName, Paragraph // Parent style $basedOn = $style->getBasedOn(); - $xmlWriter->writeElementIf(null !== $basedOn, 'w:basedOn', 'w:val', $basedOn); + $xmlWriter->writeElementIf('' !== $basedOn, 'w:basedOn', 'w:val', $basedOn); // Next paragraph style $next = $style->getNext(); @@ -259,15 +260,17 @@ private function writeParagraphStyle(XMLWriter $xmlWriter, $styleName, Paragraph * * @param string $styleName */ - private function writeTableStyle(XMLWriter $xmlWriter, $styleName, TableStyle $style): void + private function writeTableStyle(XMLWriter $xmlWriter, $styleName, TableStyle $style, string $name): void { $xmlWriter->startElement('w:style'); $xmlWriter->writeAttribute('w:type', 'table'); $xmlWriter->writeAttribute('w:customStyle', '1'); $xmlWriter->writeAttribute('w:styleId', $styleName); $xmlWriter->startElement('w:name'); - $xmlWriter->writeAttribute('w:val', $styleName); + $xmlWriter->writeAttribute('w:val', $name ?: $styleName); $xmlWriter->endElement(); + $basedOn = $style->getBasedOn(); + $xmlWriter->writeElementIf('' !== $basedOn, 'w:basedOn', 'w:val', $basedOn); $xmlWriter->startElement('w:uiPriority'); $xmlWriter->writeAttribute('w:val', '99'); $xmlWriter->endElement(); diff --git a/src/PhpWord/Writer/Word2007/Style/Font.php b/src/PhpWord/Writer/Word2007/Style/Font.php index 8d9697715c..16b4f9ec95 100644 --- a/src/PhpWord/Writer/Word2007/Style/Font.php +++ b/src/PhpWord/Writer/Word2007/Style/Font.php @@ -153,10 +153,7 @@ private function writeStyle(): void } // RTL - if ($this->isInline === true) { - $styleName = $style->getStyleName(); - $xmlWriter->writeElementIf($styleName === null && $style->isRTL(), 'w:rtl'); - } + $xmlWriter->writeElementIf($style->isRTL(), 'w:rtl'); // Position $xmlWriter->writeElementIf($style->getPosition() !== null, 'w:position', 'w:val', $style->getPosition()); diff --git a/src/PhpWord/Writer/Word2007/Style/MarginBorder.php b/src/PhpWord/Writer/Word2007/Style/MarginBorder.php index 8d08eec3cc..15cf0e4eeb 100644 --- a/src/PhpWord/Writer/Word2007/Style/MarginBorder.php +++ b/src/PhpWord/Writer/Word2007/Style/MarginBorder.php @@ -64,14 +64,12 @@ public function write(): void $sides = ['top', 'left', 'right', 'bottom', 'insideH', 'insideV']; foreach ($this->sizes as $i => $size) { - if ($size !== null) { - $color = null; - if (isset($this->colors[$i])) { - $color = $this->colors[$i]; - } - $style = $this->styles[$i] ?? 'single'; - $this->writeSide($xmlWriter, $sides[$i], $this->sizes[$i], $color, $style); + $color = null; + if (isset($this->colors[$i])) { + $color = $this->colors[$i]; } + $style = $this->styles[$i] ?? 'single'; + $this->writeSide($xmlWriter, $sides[$i], $this->sizes[$i], $color, $style); } } @@ -79,8 +77,8 @@ public function write(): void * Write side. * * @param string $side - * @param int $width - * @param string $color + * @param ?int $width + * @param ?string $color * @param string $borderStyle */ private function writeSide(XMLWriter $xmlWriter, $side, $width, $color = null, $borderStyle = 'solid'): void @@ -93,7 +91,7 @@ private function writeSide(XMLWriter $xmlWriter, $side, $width, $color = null, $ } } $xmlWriter->writeAttribute('w:val', $borderStyle); - $xmlWriter->writeAttribute('w:sz', $width); + $xmlWriter->writeAttributeIf($width != null, 'w:sz', $width); $xmlWriter->writeAttributeIf($color != null, 'w:color', $color); if (!empty($this->attributes)) { if (isset($this->attributes['space'])) { diff --git a/src/PhpWord/Writer/Word2007/Style/Paragraph.php b/src/PhpWord/Writer/Word2007/Style/Paragraph.php index d4ec87a1ab..f66c4df430 100644 --- a/src/PhpWord/Writer/Word2007/Style/Paragraph.php +++ b/src/PhpWord/Writer/Word2007/Style/Paragraph.php @@ -105,6 +105,11 @@ private function writeStyle(): void //Right to left $xmlWriter->writeElementIf($styles['bidi'] === true, 'w:bidi'); + if ($styles['textDirection'] !== '') { + $xmlWriter->startElement('w:textDirection'); + $xmlWriter->writeAttribute('w:val', $styles['textDirection']); + $xmlWriter->endElement(); // w:textDirection + } //Paragraph contextualSpacing $xmlWriter->writeElementIf($styles['contextualSpacing'] === true, 'w:contextualSpacing'); diff --git a/src/PhpWord/Writer/Word2007/Style/Table.php b/src/PhpWord/Writer/Word2007/Style/Table.php index 05cec492ca..ea600a2403 100644 --- a/src/PhpWord/Writer/Word2007/Style/Table.php +++ b/src/PhpWord/Writer/Word2007/Style/Table.php @@ -63,6 +63,8 @@ private function writeStyle(XMLWriter $xmlWriter, TableStyle $style): void { // w:tblPr $xmlWriter->startElement('w:tblPr'); + $tblStyle = $style->getTblStyle(); + $xmlWriter->writeElementIf($tblStyle !== '', 'w:tblStyle', 'w:val', $tblStyle); // Table alignment if ('' !== $style->getAlignment()) { @@ -139,6 +141,7 @@ private function writeBorder(XMLWriter $xmlWriter, TableStyle $style): void $styleWriter = new MarginBorder($xmlWriter); $styleWriter->setSizes($style->getBorderSize()); $styleWriter->setColors($style->getBorderColor()); + $styleWriter->setStyles($style->getBorderStyle()); $styleWriter->write(); $xmlWriter->endElement(); // w:tblBorders diff --git a/tests/PhpWordTests/Reader/Word2007/StyleTableTest.php b/tests/PhpWordTests/Reader/Word2007/StyleTableTest.php new file mode 100644 index 0000000000..fd1a69c0b8 --- /dev/null +++ b/tests/PhpWordTests/Reader/Word2007/StyleTableTest.php @@ -0,0 +1,55 @@ +load($file); + self::assertSame('Times New Roman', $phpWord->getDefaultFontName()); + + $elements = $phpWord->getSection(0)->getElements(); + self::assertInstanceOf(Table::class, $elements[2]); + $style = $elements[2]->getStyle(); + self::assertIsObject($style); + self::assertSame('Tablaconcuadrcula', $style->getTblStyle()); + self::assertSame('none', $style->getBorderTopStyle()); + $baseStyle = Style::getStyle('Tablaconcuadrcula'); + self::assertInstanceOf(TableStyle::class, $baseStyle); + self::assertSame('Table Grid', $baseStyle->getStyleName()); + self::assertSame('Tablanormal', $baseStyle->getBasedOn()); + self::assertSame('single', $baseStyle->getBorderTopStyle()); + } +} diff --git a/tests/PhpWordTests/SettingsRtlTest.php b/tests/PhpWordTests/SettingsRtlTest.php new file mode 100644 index 0000000000..d9e85242e1 --- /dev/null +++ b/tests/PhpWordTests/SettingsRtlTest.php @@ -0,0 +1,81 @@ +defaultRtl = Settings::isDefaultRtl(); + } + + protected function tearDown(): void + { + Settings::setDefaultRtl($this->defaultRtl); + } + + public function testSetGetDefaultRtl(): void + { + self::assertNull(Settings::isDefaultRtl()); + Settings::setDefaultRtl(true); + self::assertTrue(Settings::isDefaultRtl()); + Settings::setDefaultRtl(false); + self::assertFalse(Settings::isDefaultRtl()); + Settings::setDefaultRtl(null); + self::assertNull(Settings::isDefaultRtl()); + } + + public function testNormalStyleAdded(): void + { + $phpWord = new PhpWord(); + self::assertNull(Settings::isDefaultRtl()); + Settings::setDefaultRtl(true); + $style = Style::getStyle('Normal'); + self::assertInstanceOf(Font::class, $style); + self::assertTrue($style->isRtl()); + $paragraph = $style->getParagraph(); + self::assertTrue($paragraph->isBidi()); + self::assertSame(TextDirection::RLTB, $paragraph->getTextDirection()); + } + + public function testNormalStyleNotReplaced(): void + { + $phpWord = new PhpWord(); + $phpWord->setDefaultParagraphStyle([]); + $style = Style::getStyle('Normal'); + self::assertInstanceOf(Paragraph::class, $style); + self::assertNotTrue($style->isBidi()); + self::assertSame(TextDirection::NONE, $style->getTextDirection()); + } +} diff --git a/tests/PhpWordTests/SettingsTest.php b/tests/PhpWordTests/SettingsTest.php index 46c72eab28..15b79189a1 100644 --- a/tests/PhpWordTests/SettingsTest.php +++ b/tests/PhpWordTests/SettingsTest.php @@ -53,9 +53,6 @@ class SettingsTest extends TestCase private $zipClass; - /** @var bool */ - private $defaultRtl; - protected function setUp(): void { $this->compatibility = Settings::hasCompatibility(); @@ -69,7 +66,6 @@ protected function setUp(): void $this->pdfRendererPath = Settings::getPdfRendererPath(); $this->tempDir = Settings::getTempDir(); $this->zipClass = Settings::getZipClass(); - $this->defaultRtl = Settings::isDefaultRtl(); } protected function tearDown(): void @@ -85,7 +81,6 @@ protected function tearDown(): void Settings::setPdfRendererPath($this->pdfRendererPath); Settings::setTempDir($this->tempDir); Settings::setZipClass($this->zipClass); - Settings::setDefaultRtl($this->defaultRtl); } /** @@ -108,17 +103,6 @@ public function testSetGetOutputEscapingEnabled(): void self::assertTrue(Settings::isOutputEscapingEnabled()); } - public function testSetGetDefaultRtl(): void - { - self::assertNull(Settings::isDefaultRtl()); - Settings::setDefaultRtl(true); - self::assertTrue(Settings::isDefaultRtl()); - Settings::setDefaultRtl(false); - self::assertFalse(Settings::isDefaultRtl()); - Settings::setDefaultRtl(null); - self::assertNull(Settings::isDefaultRtl()); - } - /** * Test set/get zip class. */ diff --git a/tests/PhpWordTests/Shared/Html2402Test.php b/tests/PhpWordTests/Shared/Html2402Test.php new file mode 100644 index 0000000000..f3f7f78c0a --- /dev/null +++ b/tests/PhpWordTests/Shared/Html2402Test.php @@ -0,0 +1,208 @@ + + + + header a + header b + header c + + + + 12 + This is bold text6 + + +HTML; + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, $html, false, false); + $elements = $section->getElements(); + $table = $elements[0]; + self::assertInstanceOf(Table::class, $table); + $style = $table->getStyle(); + self::assertInstanceOf(TableStyle::class, $style); + self::assertSame('none', $style->getBorderBottomStyle()); + $rows = $table->getRows(); + self::assertCount(3, $rows); + $cells = $rows[1]->getCells(); + self::assertCount(2, $cells); + self::assertSame('dotted', $cells[0]->getStyle()->getBorderRightStyle()); + self::assertSame('FF0000', $cells[0]->getStyle()->getBorderRightColor()); + self::assertEmpty($cells[1]->getStyle()->getBorderRightStyle()); + $writer = new HtmlWriter($phpWord); + $content = $writer->getContent(); + $substring = 'table-layout: auto; border-top-style: none; border-top-width: 0pt; border-left-style: none; border-left-width: 0pt; border-bottom-style: none; border-bottom-width: 0pt; border-right-style: none; border-right-width: 0pt;'; + $count = substr_count($content, $substring); + $expected = substr_count($content, 'header c', $content); + } + + public function testParseTableStyleBorderNone(): void + { + $html = << + + + + + + + + + + + +
header aheader bheader c
12
This is bold text6
+HTML; + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, $html, false, false); + $elements = $section->getElements(); + $table = $elements[0]; + self::assertInstanceOf(Table::class, $table); + $style = $table->getStyle(); + self::assertInstanceOf(TableStyle::class, $style); + self::assertSame('none', $style->getBorderBottomStyle()); + $rows = $table->getRows(); + self::assertCount(3, $rows); + $cells = $rows[1]->getCells(); + self::assertCount(2, $cells); + self::assertSame('dotted', $cells[0]->getStyle()->getBorderRightStyle()); + self::assertSame('ff0000', $cells[0]->getStyle()->getBorderRightColor()); + self::assertEmpty($cells[1]->getStyle()->getBorderRightStyle()); + $writer = new HtmlWriter($phpWord); + $content = $writer->getContent(); + $substring = 'table-layout: auto; border-top-style: none; border-left-style: none; border-bottom-style: none; border-right-style: none;'; + $count = substr_count($content, $substring); + $expected = substr_count($content, ' + + + + + + + + + + + +
header aheader bheader c
12
This is bold text6
+HTML; + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, $html, false, false); + $elements = $section->getElements(); + $table = $elements[0]; + self::assertInstanceOf(Table::class, $table); + $style = $table->getStyle(); + self::assertInstanceOf(TableStyle::class, $style); + self::assertSame('none', $style->getBorderBottomStyle()); + $rows = $table->getRows(); + self::assertCount(3, $rows); + $cells = $rows[1]->getCells(); + self::assertCount(2, $cells); + self::assertSame('dotted', $cells[0]->getStyle()->getBorderRightStyle()); + self::assertSame('ff0000', $cells[0]->getStyle()->getBorderRightColor()); + self::assertEmpty($cells[1]->getStyle()->getBorderRightStyle()); + } + + public function testParseTableStyleBorder2px(): void + { + $html = << + + + header a + header b + header c + + + + 12 + This is bold text6 + + +HTML; + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, $html, false, false); + $elements = $section->getElements(); + $table = $elements[0]; + self::assertInstanceOf(Table::class, $table); + $style = $table->getStyle(); + self::assertInstanceOf(TableStyle::class, $style); + self::assertSame('dashed', $style->getBorderBottomStyle()); + self::assertSame('dashed', $style->getBorderInsideHStyle()); + self::assertSame('dashed', $style->getBorderInsideVStyle()); + self::assertSame(15, $style->getBorderBottomSize()); + self::assertSame('00ff00', $style->getBorderBottomColor()); + $rows = $table->getRows(); + self::assertCount(3, $rows); + $cells = $rows[1]->getCells(); + self::assertCount(2, $cells); + self::assertSame('dotted', $cells[0]->getStyle()->getBorderRightStyle()); + self::assertSame('ff0000', $cells[0]->getStyle()->getBorderRightColor()); + self::assertEmpty($cells[1]->getStyle()->getBorderRightStyle()); + $writer = new HtmlWriter($phpWord); + $content = $writer->getContent(); + + $substring = 'table-layout: auto; border-top-style: dashed; border-top-color: #00ff00; border-top-width: 0.75pt; border-left-style: dashed; border-left-color: #00ff00; border-left-width: 0.75pt; border-bottom-style: dashed; border-bottom-color: #00ff00; border-bottom-width: 0.75pt; border-right-style: dashed; border-right-color: #00ff00; border-right-width: 0.75pt;'; + $count = substr_count($content, $substring); + $expected = substr_count($content, 'addSection(); + $htmlContent = << + + + +Testing Head Section + + + + + + +

This is bold text.

+ + +EOF; + Html::addHtml($section, $htmlContent, true, true); + self::assertSame('Testing Head Section', $phpWord->getDocInfo()->getTitle()); + self::assertSame('PhpWord Test', $phpWord->getDocInfo()->getCreator()); + self::assertSame('testing html read including meta tags', $phpWord->getDocInfo()->getDescription()); + $elements = $section->getElements(); + self::assertCount(1, $elements); + $element = $elements[0]; + self::assertInstanceOf(TextRun::class, $element); + $textElements = $element->getElements(); + self::assertCount(3, $textElements); + + $textElement = $textElements[0]; + self::assertInstanceOf(Text::class, $textElement); + $style = $textElement->getFontStyle(); + self::assertInstanceOf(Font::class, $style); + self::assertNotTrue($style->isBold()); + self::assertSame('This is ', $textElement->getText()); + + $textElement = $textElements[1]; + self::assertInstanceOf(Text::class, $textElement); + $style = $textElement->getFontStyle(); + self::assertInstanceOf(Font::class, $style); + self::assertTrue($style->isBold()); + self::assertSame('bold', $textElement->getText()); + + $textElement = $textElements[2]; + self::assertInstanceOf(Text::class, $textElement); + $style = $textElement->getFontStyle(); + self::assertInstanceOf(Font::class, $style); + self::assertNotTrue($style->isBold()); + self::assertSame(' text.', $textElement->getText()); + } +} 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/Shared/HtmlRtlTest.php b/tests/PhpWordTests/Shared/HtmlRtlTest.php new file mode 100644 index 0000000000..219437fd9c --- /dev/null +++ b/tests/PhpWordTests/Shared/HtmlRtlTest.php @@ -0,0 +1,180 @@ +addSection(); + $html = '

test1.

'; + $html .= '

test2.

'; + $html .= '

test3.

'; + Html::addHtml($section, $html); + $elements = $section->getElements(); + self::assertCount(3, $elements); + + $index = 0; + $element = $elements[$index]; + self::assertInstanceOf(TextRun::class, $element); + $paragraphStyle = $element->getParagraphStyle(); + self::assertInstanceOf(Paragraph::class, $paragraphStyle); + self::assertTrue($paragraphStyle->isBidi()); + self::assertSame('tbRl', $paragraphStyle->getTextDirection()); + $textElements = $element->getElements(); + self::assertCount(1, $textElements); + $textElement = $textElements[0]; + self::assertInstanceOf(Text::class, $textElement); + self::assertInstanceOf(Font::class, $textElement->getFontStyle()); + self::assertTrue($textElement->getFontStyle()->isRtl()); + + $index = 1; + $element = $elements[$index]; + self::assertInstanceOf(TextRun::class, $element); + $paragraphStyle = $element->getParagraphStyle(); + self::assertInstanceOf(Paragraph::class, $paragraphStyle); + self::assertFalse($paragraphStyle->isBidi()); + self::assertSame('lrTb', $paragraphStyle->getTextDirection()); + $textElements = $element->getElements(); + self::assertCount(1, $textElements); + $textElement = $textElements[0]; + self::assertInstanceOf(Text::class, $textElement); + self::assertInstanceOf(Font::class, $textElement->getFontStyle()); + self::assertFalse($textElement->getFontStyle()->isRtl()); + + $index = 2; + $element = $elements[$index]; + self::assertInstanceOf(TextRun::class, $element); + $paragraphStyle = $element->getParagraphStyle(); + self::assertInstanceOf(Paragraph::class, $paragraphStyle); + self::assertNull($paragraphStyle->isBidi()); + self::assertSame('', $paragraphStyle->getTextDirection()); + $textElements = $element->getElements(); + self::assertCount(1, $textElements); + $textElement = $textElements[0]; + self::assertInstanceOf(Text::class, $textElement); + self::assertInstanceOf(Font::class, $textElement->getFontStyle()); + self::assertNull($textElement->getFontStyle()->isRtl()); + } + + public function testParseHtmlDir(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $html = '

test1.

'; + $html .= '

test2.

'; + $html .= '

test3.

'; + Html::addHtml($section, $html); + $elements = $section->getElements(); + self::assertCount(3, $elements); + + $index = 0; + $element = $elements[$index]; + self::assertInstanceOf(TextRun::class, $element); + $paragraphStyle = $element->getParagraphStyle(); + self::assertInstanceOf(Paragraph::class, $paragraphStyle); + self::assertTrue($paragraphStyle->isBidi()); + self::assertSame('tbRl', $paragraphStyle->getTextDirection()); + $textElements = $element->getElements(); + self::assertCount(1, $textElements); + $textElement = $textElements[0]; + self::assertInstanceOf(Text::class, $textElement); + self::assertInstanceOf(Font::class, $textElement->getFontStyle()); + self::assertTrue($textElement->getFontStyle()->isRtl()); + + $index = 1; + $element = $elements[$index]; + self::assertInstanceOf(TextRun::class, $element); + $paragraphStyle = $element->getParagraphStyle(); + self::assertInstanceOf(Paragraph::class, $paragraphStyle); + self::assertFalse($paragraphStyle->isBidi()); + self::assertSame('lrTb', $paragraphStyle->getTextDirection()); + $textElements = $element->getElements(); + self::assertCount(1, $textElements); + $textElement = $textElements[0]; + self::assertInstanceOf(Text::class, $textElement); + self::assertInstanceOf(Font::class, $textElement->getFontStyle()); + self::assertFalse($textElement->getFontStyle()->isRtl()); + + $index = 2; + $element = $elements[$index]; + self::assertInstanceOf(TextRun::class, $element); + $paragraphStyle = $element->getParagraphStyle(); + self::assertInstanceOf(Paragraph::class, $paragraphStyle); + self::assertNull($paragraphStyle->isBidi()); + self::assertSame('', $paragraphStyle->getTextDirection()); + $textElements = $element->getElements(); + self::assertCount(1, $textElements); + $textElement = $textElements[0]; + self::assertInstanceOf(Text::class, $textElement); + self::assertInstanceOf(Font::class, $textElement->getFontStyle()); + self::assertNull($textElement->getFontStyle()->isRtl()); + } + + public function testCssClassNameOnPElement(): void + { + $phpWord = new PhpWord(); + $phpWord->addFontStyle('customClass', ['bold' => true], ['borderBottomSize' => 3, 'borderBottomColor' => '#00ff00', 'textDirection' => 'tbRl']); + $section = $phpWord->addSection(); + $html = '

test1.

'; + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord); + $path = '/w:document/w:body/w:p'; + $paragraphPath = $path . '/w:pPr'; + $element = $doc->getElement($paragraphPath . '/w:pStyle'); + self::assertSame('customClass', $element->getAttribute('w:val')); + $textPath = $path . '/w:r/w:t'; + self::assertSame('test1.', $doc->getElement($textPath)->nodeValue); + self::assertSame('customClass', $doc->getElement($path . '/w:r/w:rPr/w:rStyle')->getAttribute('w:val')); + + // Styles + $file = 'word/styles.xml'; + $path = '/w:styles/w:style[@w:styleId="customClass"]'; + $paragraphPath = $path . '/w:pPr'; + $element = $doc->getElement($paragraphPath . '/w:pBdr/w:bottom', $file); + self::assertSame('#00ff00', $element->getAttribute('w:color')); + $element = $doc->getElement($paragraphPath . '/w:textDirection', $file); + self::assertSame('tbRl', $element->getAttribute('w:val')); + $fontPath = $path . '/w:rPr'; + $element = $doc->getElement($fontPath . '/w:b', $file); + self::assertSame('1', $element->getAttribute('w:val')); + } +} diff --git a/tests/PhpWordTests/Shared/HtmlTest.php b/tests/PhpWordTests/Shared/HtmlTest.php index c8640509de..765d79f906 100644 --- a/tests/PhpWordTests/Shared/HtmlTest.php +++ b/tests/PhpWordTests/Shared/HtmlTest.php @@ -20,10 +20,14 @@ use Exception; 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\Html; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\LineSpacingRule; +use PhpOffice\PhpWord\Style; +use PhpOffice\PhpWord\Style\Font; use PhpOffice\PhpWord\Style\Paragraph; use PhpOffice\PhpWordTests\AbstractWebServerEmbeddedTest; use PhpOffice\PhpWordTests\TestHelperDOCX; @@ -154,6 +158,37 @@ public function testParseStyleTableClassName(): void self::assertEquals('pStyle', $section->getElement(0)->getStyle()->getStyleName()); } + public function testSpanClassName(): void + { + $phpWord = new PhpWord(); + $phpWord->addFontStyle('boldtext', ['bold' => true]); + $html = '

This is bold text.

'; + $section = $phpWord->addSection(); + Html::addHtml($section, $html); + self::assertTrue(true); + $element = $section->getElements()[0]; + self::assertInstanceOf(TextRun::class, $element); + $textElements = $element->getElements(); + self::assertCount(3, $textElements); + + $text = $textElements[0]; + self::assertInstanceOf(Text::class, $text); + self::assertInstanceOf(Font::class, $text->getFontStyle()); + self::assertNotTrue($text->getFontStyle()->isBold()); + + $text = $textElements[1]; + self::assertInstanceOf(Text::class, $text); + self::assertSame('boldtext', $text->getFontStyle()); + $style = Style::getStyle('boldtext'); + self::assertInstanceOf(Font::class, $style); + self::assertTrue($style->isBold()); + + $text = $textElements[2]; + self::assertInstanceOf(Text::class, $text); + self::assertInstanceOf(Font::class, $text->getFontStyle()); + self::assertNotTrue($text->getFontStyle()->isBold()); + } + /** * Test underline. */ @@ -635,7 +670,7 @@ public function testParseTableStyleAttributeInlineStyle(): void $xpath = '/w:document/w:body/w:tbl/w:tr[1]/w:tc[1]/w:tcPr/w:shd'; self::assertTrue($doc->elementExists($xpath)); - self::assertEquals('red', $doc->getElement($xpath)->getAttribute('w:fill')); + self::assertEquals('ff0000', $doc->getElement($xpath)->getAttribute('w:fill')); } /** @@ -1016,7 +1051,7 @@ public function testParseHorizontalRule(): void self::assertTrue($doc->elementExists($xpath)); self::assertEquals('single', $doc->getElement($xpath)->getAttribute('w:val')); self::assertEquals((int) (5 * 15 / 2), $doc->getElement($xpath)->getAttribute('w:sz')); - self::assertEquals('lightblue', $doc->getElement($xpath)->getAttribute('w:color')); + self::assertEquals('add8e6', $doc->getElement($xpath)->getAttribute('w:color')); $xpath = '/w:document/w:body/w:p[4]/w:pPr/w:spacing'; self::assertTrue($doc->elementExists($xpath)); diff --git a/tests/PhpWordTests/TemplateProcessorSectionTest.php b/tests/PhpWordTests/TemplateProcessorSectionTest.php new file mode 100644 index 0000000000..0402d4fc66 --- /dev/null +++ b/tests/PhpWordTests/TemplateProcessorSectionTest.php @@ -0,0 +1,92 @@ +templateProcessor = new TemplateProcessor($filename); + + return $this->templateProcessor; + } + + protected function tearDown(): void + { + if ($this->templateProcessor !== null) { + $filename = $this->templateProcessor->getTempDocumentFilename(); + $this->templateProcessor = null; + if (file_exists($filename)) { + @unlink($filename); + } + } + } + + public function testSetComplexSection(): void + { + $templateProcessor = $this->getTemplateProcessor(__DIR__ . '/_files/templates/document22-xml.docx'); + $html = ' +

 Bug Report:

+

BugTracker X is ${facing1} an issue.

+

BugTracker X is ${facing2} an issue.

+

BugTracker X is ${facing1} an issue.

+ '; + $section = new Section(0); + Html::addHtml($section, $html, false, false); + $templateProcessor->setComplexBlock('test', $section); + $facing1 = new TextRun(); + $facing1->addText('facing', ['bold' => true]); + $facing2 = new TextRun(); + $facing2->addText('facing', ['italic' => true]); + + $templateProcessor->setComplexBlock('test', $section); + $templateProcessor->setComplexValue('facing1', $facing1, true); + $templateProcessor->setComplexValue('facing2', $facing2); + + $docName = $templateProcessor->save(); + $docFound = file_exists($docName); + self::assertTrue($docFound); + $contents = file_get_contents("zip://$docName#word/document2.xml"); + unlink($docName); + self::assertNotFalse($contents); + $contents = preg_replace('/>\s+<', $contents) ?? ''; + self::assertStringContainsString('Test', $contents); + $count = substr_count($contents, 'facing'); + self::assertSame(2, $count, 'should be 2 bold strings'); + $count = substr_count($contents, 'facing'); + self::assertSame(1, $count, 'should be 1 italic string'); + self::assertStringNotContainsString('$', $contents, 'no leftover macros'); + self::assertStringNotContainsString('facing1', $contents, 'no leftover replaced string1'); + self::assertStringNotContainsString('facing2', $contents, 'no leftover replaced string2'); + } +} diff --git a/tests/PhpWordTests/TemplateProcessorTest.php b/tests/PhpWordTests/TemplateProcessorTest.php index 49e88d1b5b..b8ad970ced 100644 --- a/tests/PhpWordTests/TemplateProcessorTest.php +++ b/tests/PhpWordTests/TemplateProcessorTest.php @@ -25,6 +25,7 @@ use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\TemplateProcessor; +use Throwable; use TypeError; use ZipArchive; @@ -63,12 +64,21 @@ protected function tearDown(): void * * @covers ::__construct * @covers ::__destruct + * @covers \PhpOffice\PhpWord\Shared\ZipArchive::close */ public function testTheConstruct(): void { $object = $this->getTemplateProcessor(__DIR__ . '/_files/templates/blank.docx'); self::assertInstanceOf('PhpOffice\\PhpWord\\TemplateProcessor', $object); self::assertEquals([], $object->getVariables()); + $object->save(); + + try { + $object->zip()->close(); + self::fail('Expected exception for double close'); + } catch (Throwable $e) { + // nothing to do here + } } /** 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); } } diff --git a/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php b/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php index f86507ce06..834b4d4fbb 100644 --- a/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php +++ b/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWordTests\Writer\ODText\Part; use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWordTests\TestHelperDOCX; @@ -28,11 +29,20 @@ */ class ContentTest extends \PHPUnit\Framework\TestCase { + /** @var string */ + private $defaultFontName; + /** * Executed before each method of the class. */ + protected function setUp(): void + { + $this->defaultFontName = Settings::getDefaultFontName(); + } + protected function tearDown(): void { + Settings::setDefaultFontName($this->defaultFontName); TestHelperDOCX::clear(); } diff --git a/tests/PhpWordTests/Writer/ODText/Style/Paragraph2Test.php b/tests/PhpWordTests/Writer/ODText/Style/Paragraph2Test.php index b638b380b6..8481d01f88 100644 --- a/tests/PhpWordTests/Writer/ODText/Style/Paragraph2Test.php +++ b/tests/PhpWordTests/Writer/ODText/Style/Paragraph2Test.php @@ -49,14 +49,17 @@ public function testTextAlign(): void $element .= '/style:paragraph-properties'; self::assertTrue($doc->elementExists($element)); self::assertEquals('right', $doc->getElementAttribute($element, 'fo:text-align')); + self::assertEquals('rl-tb', $doc->getElementAttribute($element, 'style:writing-mode')); $element = "$s2a/style:style[6]/style:paragraph-properties"; self::assertTrue($doc->elementExists($element)); self::assertEquals('right', $doc->getElementAttribute($element, 'fo:text-align')); + self::assertEquals('rl-tb', $doc->getElementAttribute($element, 'style:writing-mode')); $element = "$s2a/style:style[8]/style:paragraph-properties"; self::assertTrue($doc->elementExists($element)); self::assertEquals('left', $doc->getElementAttribute($element, 'fo:text-align')); + self::assertEquals('rl-tb', $doc->getElementAttribute($element, 'style:writing-mode')); $doc->setDefaultFile('styles.xml'); $element = '/office:document-styles/office:styles/style:style'; @@ -64,7 +67,8 @@ public function testTextAlign(): void self::assertEquals('Normal', $doc->getElementAttribute($element, 'style:name')); $element .= '/style:paragraph-properties'; self::assertTrue($doc->elementExists($element)); - self::assertEquals('left', $doc->getElementAttribute($element, 'fo:text-align')); + self::assertEquals('right', $doc->getElementAttribute($element, 'fo:text-align')); + self::assertEquals('rl-tb', $doc->getElementAttribute($element, 'style:writing-mode')); } /** diff --git a/tests/PhpWordTests/Writer/RTF/RichTextTitleTest.php b/tests/PhpWordTests/Writer/RTF/RichTextTitleTest.php new file mode 100644 index 0000000000..0578c17cad --- /dev/null +++ b/tests/PhpWordTests/Writer/RTF/RichTextTitleTest.php @@ -0,0 +1,50 @@ +addSection(); + $htmlContent = '

This is heading 1

This is heading 2

'; + Html::addHtml($section, $htmlContent, false, false); + $elements = $section->getElements(); + self::assertInstanceOf(Title::class, $elements[0]); + self::assertInstanceOf(TextRun::class, $elements[0]->getText()); + + $writer = new RTF($phpWord); + $contents = $writer->getContent(); + self::assertStringContainsString('{This is heading 1}\par', $contents); + self::assertStringContainsString('{This is heading 2}\par', $contents); + } +} diff --git a/tests/PhpWordTests/Writer/Word2007/Element/TableTest.php b/tests/PhpWordTests/Writer/Word2007/Element/TableTest.php new file mode 100644 index 0000000000..57010893ae --- /dev/null +++ b/tests/PhpWordTests/Writer/Word2007/Element/TableTest.php @@ -0,0 +1,147 @@ +addSection(); + $section->addText('Before table (normal).'); + $table = $section->addTable(['width' => 5000, 'unit' => TblWidth::PERCENT]); + $row = $table->addRow(); + $tc = $table->addCell(); + $tc->addText('R1C1'); + $tc = $table->addCell(); + $tc->addText('R1C2'); + $row = $table->addRow(); + $tc = $table->addCell(); + $tc->addText('R2C1'); + $tc = $table->addCell(); + $tc->addText('R2C2'); + $row = $table->addRow(); + $tc = $table->addCell(); + $tc->addText('R3C1'); + $tc = $table->addCell(); + $tc->addText('R3C2'); + $section->addText('After table.'); + + $doc = TestHelperDOCX::getDocument($phpWord); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl[2]'), 'should be only 1 table'); + + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]/w:tc')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]/w:tc[2]')); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]/w:tc[3]')); + + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[2]')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[2]/w:tc')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[2]/w:tc[2]')); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl/w:tr[2]/w:tc[3]')); + + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[3]')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[3]/w:tc')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[3]/w:tc[2]')); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl/w:tr[3]/w:tc[3]')); + } + + public static function testSomeRowWithNoCells(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $section->addText('Before table (row 2 has no cells).'); + $table = $section->addTable(['width' => 5000, 'unit' => TblWidth::PERCENT]); + $row = $table->addRow(); + $tc = $table->addCell(); + $tc->addText('R1C1'); + $tc = $table->addCell(); + $tc->addText('R1C2'); + $row = $table->addRow(); + $row = $table->addRow(); + $tc = $table->addCell(); + $tc->addText('R3C1'); + $tc = $table->addCell(); + $tc->addText('R3C2'); + $section->addText('After table.'); + + $doc = TestHelperDOCX::getDocument($phpWord); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl[2]'), 'should be only 1 table'); + + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]/w:tc')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]/w:tc[2]')); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]/w:tc[3]')); + + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[2]')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[2]/w:tc')); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl/w:tr[2]/w:tc[2]')); + + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[3]')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[3]/w:tc')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[3]/w:tc[2]')); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl/w:tr[3]/w:tc[3]')); + } + + public static function testOnly1RowWithNoCells(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $section->addText('Before table (only 1 row and it has no cells).'); + $table = $section->addTable(['width' => 5000, 'unit' => TblWidth::PERCENT]); + $row = $table->addRow(); + $section->addText('After table.'); + + $doc = TestHelperDOCX::getDocument($phpWord); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl[2]'), 'only 1 table should be written'); + + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]')); + self::assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]/w:tc')); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl/w:tr[1]/w:tc[2]')); + + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl/w:tr[2]')); + } + + public static function testNoRows(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $section->addText('Before table (no rows therefore omitted).'); + $table = $section->addTable(['width' => 5000, 'unit' => TblWidth::PERCENT]); + $section->addText('After table.'); + + $doc = TestHelperDOCX::getDocument($phpWord); + self::assertFalse($doc->elementExists('/w:document/w:body/w:tbl[1]'), 'no table should be written'); + } +} diff --git a/tests/PhpWordTests/_files/documents/word.2474.docx b/tests/PhpWordTests/_files/documents/word.2474.docx new file mode 100644 index 0000000000000000000000000000000000000000..8ecbaef2b3bb00517c31c2087cefd094963f2963 GIT binary patch literal 27593 zcmeFYRct0vlD7Go?J_enmzkNFxy;PW%*@Qp%*=_59}| zrIe0R%1}hS@hBqXr9ePY0N?;f002M;P|A|`ECd1o7`|VT0g%9&Lbf(e#x_p6%IZ@spuiM)0O0TY|L6QKw!l!bjNBkSOvttPd+1NqCKEe;$m;#R5yUtQq!nuW z)-W&1Th^y*`;}_Jx@zRYJw?jqj~=-!R=f=_wZthxvP3iOZK8f0v`YnO}l(;`rHxEYi8sOrXDxJ!8lm=}_++&n#@ecTX3PlfdKZ=aAZU_!K(*V6E z&vfk%aQ*u5RIsc#8v>Lj;Eiwa9@>e*^x?*>&IX4n^oRj-iUh-dH_B~kyPJh%lPj~B zmk6^JfcG_VD@JVsoi~x2$t=KD8^c~GsEu!fV)@MG`x z>i|ALi z&Cc)mqS!5b<$VSJwY@1-5Z@Ol0O0Ej1R($4VjC|WtM&TZVP(HV7y3K4bsdbY9O-EP z@%%qA{l7Sn|I4peCG=Z-$29+S;78zGx58RKMxiX7;p{rbDg>magf#N{AB&aG5AHvI zfVEHc#U|z#5@$UevqYVDlC*EIQdD3gx}lc;>I`bWb-4kOgS(0u+?MV3U^0)MOg_em zB`C)OBh^tOX0X8HU&7L+`j8KQ3O^l|LYos&&MX>|G#2LAtE@by`7q_BGb}Azh--R6 zRPc!Ogy42eWBU9s;j2$&qKS(g*lN%oQa(#;V~n<-e5A&*qNgLxiZ`hGfpyb0cji&p zTRI#bObwL}6;Ai&Wuk?o@p&9^gvrNxO{m_ayK9S`r7dv(t#JPuAH~MXm^Y#TOC3St9E9O=Wf%p5m@G80orG%iJd?sIfu zooW!;q3s44KZx?p3Vu1fo#xJT5*QdI&P$v5jYjCDCSrTj2;*Uh3M?}Z4?ANaPyc8^ zSqUlz*Q7NRjK1aLIcZU8(c+0_Xs9_vuO(3$Ay&2GmK+73b=}eS;XGM zGBjKSR|nzO%MV_o=wusbD*8ne#{>zg(JX#NKaU@E9}-{1I)*RWuwFDruSk_W?-J0M zWm4B>#61jb^LJdF!FTmQ6);^NhHj4(2|_PZ`+JAo6EhD{oF!;6#sk@l*cegfqO7D2 z+k3DvFQM1sp!iIHQWrKFRHg$LdL$aQ16_j9LVau4wfLpWf_mH%ycFss-guaEI`b+H zuDE}%Bg9JAv5!^e%)1lEnYy{Rxy2nbgt#v9Rpny7Xz|^dh>=TJi3ls4_GebEx@CeO zI6geyK?d@1LCjPsxEhV0nAu3E$P|VOOdw9Bro�v5Mn(nYgT%6ZP325V~XzAmL0M z$cdWAxIh#>ebo4{Y8}FrEDZN-T+@4;Uxx4MLt<*B%c|1@A+vM11qxbu;*p zr;bEAP98NC`yAG&J?nAqYI`_sHq!pVpN2SeNVAZVGJzIz%Af~)B-uENq??uQn3gh6 zR3B(tvuM#d^gGIuKwI|Z;LGXf5>fu1(8OroQ0mL(q5cN7;H~<4Ne-Y=J+@1Eg&oT(r|_4HAbV7Tr2> zNy!ser8n(s8z0oy${#75%<2!xBk^5Y{g5y+q|gr870Y}^YiE4g7TQ!)84g@j2Z_8p zQ%Cn>*_2d&lX-8dB9`vFN1!JRNDqAv@f1;G8AU43$=52+5`L;+&v?c$WoPJw4cs@< zY~!_$CT5@3@|+ZHX4N*W$*khf;d2%!o_|=SOJA&VXXUILco?o@mD9f2w6NpbL~^vu zZjSbhLQy%%%n0s3Hw}N;X?mGn1=l$0N1l5)u6urPtZi<^biLw`Sv_;}P{+&05 zv3MVw(HqL}6pFn+ncaRWLl;%=Mb*`KwY7qAJs2&Jf0~)^tng{ZiuVEkkF?jNx^A;a zkMNQFlh5xuwL)3)!fh!uS-x|gH*^SvM6zJVX8q$e8dp*%jGRI8Lw)-Mo_`XXWZTIUOByEKParQil9Kx-GEqgrCm&X~~^(`(umY-R#3>h(n*u@8*{m*ZEWuAJ=fIIWQ$u z+oq;rsl=lONg{5UPCZ8*EX)L8{&kksB*BtYW^zz4H&=y(Hcp8$dK9i*o~%hwa_B&i zZaNq-fl>Cu8TVDh0#Gku=zy5|3~d}?$i4?`m*W^Nzvvfq91lhC84aT<2+R%lE5-7+ z%3`Wr(lXl881UDR*+m9BP3Xy{w z(24@_m_VO=?zxf~5gSnk38f)YqXoPPFMEH%2#VUqPfySsr^Q3*$xhVZ z4;Y2opX{-sC(xUvsC(&2P)m4YVeWYk5R~eBw-p*D(uaqkR>6$a04RF!beMybCje1W zM-ylJapDxJjB;h%g#4e#m`V0h2TT)@O5A}Cz!Z4FFa|}Q6(Sp5Un1_iojONH=fnig z)65KRC@b0wZ4y=^kQ-N74Y$L+)>pun^iowZ}6KXc^OEP&g=oj zG(Xq3)`P7tOPV~d|LD3TYBjG^#>jfAhHssjuBpU&)(JdeXN-hya~%^43l?a;MiO+~ z%IhXf9mpo6tDkPo&U3t1^R~5@7w>oPgAngH=H2^{R7TMUH6#z-OD8CAj?%D zkyOc7xrMF7p1?T$ z(cNt9E08ZfX$lC6)?4N5?o#XKCy#?$ExEMjryVD&61c$UXcYlb{3HLIcOq3BjUrxH z?oJm0uK*HTXf`&r5m+pi(_bQ;wwjcb8_n}Rtf{J^*_TKQ4)tS3_y$}UDg-CE9B9mu zi%yWy;N9!9dB!W!Pz+4=Z|5qi?=cgeayj!f&{G67;Zt}XczR&|B`3Xf0<^&{asdXl z#a~%-WMK?ERKCcnguyH)}rG!Mv7wrG^?&Ro6((lP}8 z`59*|c=y!!S7+>J9@~TnOv@KvuH%K{-Y}_F>ATQ#N9z3>h5-%7CqoMa>7f?)e9JwJA`eg^rCKSg9 zV#NK>O=!V^*&MQ50Ys4o#`p_XywbghYP$M3{+;mD1<#y=#QG>6d?XwWo*g|Xy+2JV zqRBm3Og!FeVtBw0Z2r_TqxjVz?W&_cFR6Q~ZnR7TSX(p^sx|H|%xI!4{|0xI6x#Bm zU`cafP*RtzI>2Z@g=NB;kmFqXRFrCDs3rlQeVS6gBMK{tgvtIunzY=k5+qxSDNq80 z!yCm9c`Cy__(rs3tygkAe=P)#{r9`awb?KAPlh>1v5OLsrU(YI_ihtIC>HWIO}AfW z9{rD^Xu04!@Bm9`Q5`f`LnS7HtA=%l`FQgsy9(|4tS0Cxwg&JXwi1Z2I|)eN)j#^! zRM+*&MTJIKHLJvC&Ekci<*eq^@zRcVOMA3U=(hInT8iGO+~~M8A2^roRJFk$^XYvR ziH*ASi^oy!5^?k@se3nnN85yJ)9XizG7KArDP8+MGlfhu@8Yv2d^%TRs#W#2--I0_ zOGonNqLUu*=UF8Qth|^U6{^D9;k7C!6tS)O%yfN@merFi;dVaZTJ9p| z*yUlzECl~*$csT?g*yg!ixA!^idyKJi8MtLXAZh+T_Z{`=&6kVFqY z`0ewT-#-8UK#{SH(SLFIb%%8Vgpg~l1=m@-Yl1OTnG0zp^6U-ggeM@#0W@TL4E~gl zCotU2(JAq98E<`(ov%OIe?(ec$x}SU3Yby7KmxGw-GU4em3;pAZTI4lQ|*`sxe}3g-K0SaHJKu6iM0=5$6*i?L$b(13cZFXR0W}VItGFT8uXT?^=Xx1 zg!t^B%PB%hH?^F5i@k?2m)X56#-dRrY1V^G6UfFf9xO5j^v7{%4XU7{!~oRc2lFB` z;6!Qi8Y@6DqJh;bIg9?X&!Zlr?ThQDHsOe6xJXpm7fst6Y=Y2|?7G;|p$Pr%sQ@t& z?>2TY=tshQl~l5rPC2EsfGK#l4ifpbgz$$MPg+fo%pgH_KZA&sK#9BJAyM>NI#$wq ziTOvuljbsQYsi6-qP0c!8FHs$4>G%9P$rRueEi~X1-oqdRb>d)6cdO+So&5ZSpj2#&Mk>iWf)ME=d(fqc>GQJRui`>I2)PYG3WH%8(M|{V+QsIN{)oOf@*vC!W=(@KaK7Q(B3BQ6BaOi}kjh$0J8Zd>QEpj6X^VmqkMQ z5_aH=F=TYT+81%yJ22#A90_khM{kq?J;f3P!Xqg{+!;vtC)O!fqG69H5nqCbF0nIT z!mBQ^w;lOcVPI%Hrk^C0v=drzjT08m7u{li$zwQ5pAx@;=c~){u$KeHHw80fET%3XKH|?Q zm!1{zPe&qu-+>H-+qHcrqAvV<+HiR87wa9m@*{!vA@aZ(Tz($_eaGLislDlwZH%|TdnMxVXdNbg*;)9$$OC&D}OW2}7*3U4JSuDXR z)uK?buGYA&$@R^D9*7-)m3hn>ae#^P^AD~I z0K3CqKS0l*jo>1~8m2{(SOo$ZWQrTTBGu0z?}02dQR$xAN`nlRwR zf?d3}c?T7eaxL^R8m!Ktg^F1c7H@t>p#Vw=?S}41?Vh9r-o1AO=wuAy%HPy`*VcL!{SrpPsU3H%pQg=9b`bs|(M zV3EBFG6-%hStXw% zvOk*F6cUZ`n6ETxZZ_n7^ILNhEREBEqb_09RIP4Y$wBt3EoY_FXr zI)GdO$a@)QCEy@X!;N8)h%Y*+IF#wci^QP^Pq`%FIXNr%Yd(J_<{3uzVa0$4J_-W0 z%@|7z6M6{!`zz2~9$%gf(3^EAI2<7LjH0;>U+yu@p|5EYZ}g#+l;ePJB!V+-{}U*f5c8}`s6XW0 z5&-USQi>lnUP<{Ucu*jHhS5)I3!t;dE-rF#jbs`J3iHC2z?5W=96R1AkvRJ@6`J7g zTu(L}JV(3SeN9Tw7Bg|Mq83wLhv$^ z<_~@?vnx{wo|}wwSCsyn;!2OuYcJZkei1-gd?(_!E#!|ugp?65+oJuC3`s45dU|Rv zxj#H6eHDSpF<+d^LrVQTFYwE-Zko86z5uP}>##Z83*+D~JgLEmqe=2SAQ{Cy^-f72 zyLsub;_ujC4lCqtkmcZVb~DBOytMA6jbDi-@S*NHR1Ff0+j9muZ zK5>o3I@(3UX68)xv?GotgMm<=%OfvpH?pS*b z?`%%8H0_0ru`C`rTy`kHVlp9=MMs?w0Y>`)*>>k5k;YCwpD4U?|A^Yokh1D7t{N~T zI_RZJ#Vuz^TR5hy5xP(_g=gFa-tXY}zT z2WawZ$l!)8wbFQvUsvwOF4He%VZY1s+CBw&_9SIApxX16--*PVx1y;em%D6rNaWTmhJoOPBo{-bos|{PxYjVRBx2Fa_lCCWaOQLv zW7Vg;+m%O>v8f=%6CygLlsxI zB(AJ)^jcBiH<^$dQYdv36q6ZY{s_G<$A@NdIYYFPRP|_p zPj;Z45sfIyx{;N=L{B6sK65We9(2?VJDb;h{0loXyp#<2OvI$Y_RA%bidC6Kc6=o9 zwA`|a*g{hp-gwKjU9#OAE6n4LDKvq`kG#xHreprPniCx^h*b=}|{!3Ad6jN4uU#FMPqh z<)Z$u#zp)i%ul%OXQa{)2eBO7zS3UDw2%8!l@e5ow_(}C)3GWV zOJKBV&ZiQho-LA&lFJG8j*Ei;R>Q$~V{@qr{=Qf4Rq2xZg~*y{6s}s2h^mW>DyggCXP>}3ct_78(JX2!09ovn`H*2P1>)v`2XfxD^v(TkY_MSdf0`rd(5@S<{@mFLRv3{LS3K4rXe`+iV*K;4$h=zG` z+aRv$T;bKl$DkX&7*UyoT%)KdS91!P3JXK2lDD0kg1FsONTP0cy5tb$e+ntErA|-@ z#nhmrWE%}{MzsLj>xgEjk23tp3Re=izBvy_?>slM_1wSpiluu(p_vkm@5G`iVK2FZ zjr7A6a&Xx<$FdF>*BlBl#cX`!#j90OLb3|fSI)eGo$}+VlQQ(^GF#piPq3er;apA{ zU!?u^>t1e?|hGMPi39BylSojpaRgQ^5ZaAK(%Ov&jE7MH%mH1fC&fleEceu4;V@`o=eaVQIrwN z=Z#k98@{K7(>+KFylC!gu6TUhb^&9f}7t-OhV#_#p!f!M@ z(m>NOPL+56%?OFPy~+A=X$x(S)t%WXZ7zZK;1s<({`$9lRLbADd`SNupcwxHpj2b` zJ5c?$K@RvZ;Ns8?e+~aku{{D$Fm6Y0x6}c49{Y9e5ch?b&`_#%O>1Hbt7h=w<~F@c zSpGlq)5^c)rzhNh^3xjKfAG`d$p1%v;{PA{$zJUmQP>9v)I=O9w(N?o5CcZ?5m3+r z3arQzcTyYQ!^Cl9NpcrIhXeX*w%yN(09(IiMe!U!^Ob@((@>Ur!#F z`X0I1XfPxCCduyRF4vh}qOLNo42zE2FpNbk)C@d%BlP09Y#z~YpQ`40^X)Z|;(g$;??AL*?!IR;H zT#>-gxLyO=ywx!w^Up{RL>NUhDzn=O;=iws#yqw$0OkTR%s<^2pb;1KW|_^VH^;Qe zxa_N1ry%VrIpX&TINNKzIHJky@Ht37rx&r#tgfUVDY4|@_a>2{hmn2qGhzAn_|*4r z@oDdcc5q=+GuXUsv?k~esYnZ#pJU`}twZxqW=Ysw`Ar4BA4=#~wfm~}#U!9_mPKnO z3z$u6>c1>ZeE0afum3?$FMj`xo&Zt{POZKT3?#Jy`!odg^yxr5%~a z>4T_%$8i7&v(N*XA+z;rN)`#pKGg14{-q(8Xebq7S6eDF3PU9&Hg|w*$z|@Ja|OHmnf+f!mRO+X+XX z7@#agg}yWD0S$+stR4=(&7afD-tdFx(9|ZN`!Obg89P0HJ0Sen1(-Ve#DYPtvE;NH zDuMSwOUQu--WW*}+f4#mgiHd-gTw*;f^Mn{z#E8glzCPH3JLYQEiw}2OE-pqQj4IS zJp8o&pZJ6m`JB^-sj-8RwMU`wijRU&Wi!G8!;}_S=k6LXlheDk`G4aR$G^oVPwS$m zeEm>=|M~PY4;idC3f=-B+>LE4tgy7ZPo=FeSU<>463FpC$HK(2-=N+US%e^#e;h+-?Z z!92M96MFc-3x5n$lHH-|Xz0NgRcDvD+*oL7B#Vr^jlOZ``djP(zR*g@j5X(ZQgck~ zBtM)Z8gCZuwFA?jR+DUvhqMmcyuwESySEZVelGMJ0+({@i6rG*5hMk-MZwM+Us98h zo}T($4vgoBkJ3L8=DkCCP^pjS=~)HVO(Q4W*KfV)5?n6F+}Qg)XJP=-NU|)4cS>QK zopb!tZg%pO%r10$w^CWxK>X%P)o^x1cjca?$9I{vCOZe|~}4 z63kqF(4&G09D!x#-t4f&Llk*Cx2C}|R^|h8xYT@xQiIW~xNlnF3~RkOXrkSBjMkhw zE@LSE~7)U!D z2_5!MyBZR>x;=_a$sAXpUS(>g!lvuflxb&4<51TJi#`kziWNDjoDA&GLuItxDsq1yy|lE$?f8sD&wr2G*bjqO{FeIb z#>nFw?JR5^d73}zIjO9honU}SmohQ$Iu>bEm3GfS9;-iT#W^^qbv%95R!A`|JG}n^ z1+<^vgP77DxT-D<=_pVGKHOzG2dbRDdU7hMLZ>Z>aE#yn+99_D6epxAKtx5id7 z{C#sT$l5fg=DcUKJj=fZEMu{)pZ!VIfg6SS>9Xd36RxjG-3RmFijL%yM>>eX`&pju zaku^6*V5ZJ<4SCNbm{~3ba69q5*>nedA zCz|${xnTPvu;@HBF(H$fXj6tB2*~xc00+)>*hM>X^|NXOiDLsg+;a!s^Eoa!sf)mq zZb9GVBqpAebcB4`EX>^m4%(>>)yu7M7{qbttU^({B3b|@MBjhD8pO&=__O!L6~;nH|Epu=rWwy6O3@25@T438BsUP(S52X(3J`i z?4&7G&l$1lruPF6df@qON*GNc{a88{C@hAyWPcTPcLV1T12Z)mpwq&fJv+(ox=bUE$e!MOUr_XXJZ z7a%UWKo5~rdvPkyyGrKXM*8x((x)QRQr&qI(euJQK-ctK3T3KSvRZH2^F?WOiiP>b zKLMx}4|?cy^u(j`l5NO7@76nEunz^!(OS>&B-|;?ojIUFQ2|)MU~qp~lBCmsvnT?)%>p8YjjdW?5PN;sO?bQfkB z@N(|A;hyQAUmjt6G%P#vN)1VTKfgOr|Js0hgSZet_?~r%#sUCP|FZ$*WM*t_O!v>{ zKf6#D>YFy}tSCMBb*= zGJ!hRhi!gX%`EDl;~9_`uNKMFp6me+&tioAz0V$uJ&wbtJb%hQ&+?NRVlNTdw%oYm z@ry_!YZHIlO1zh-J_n-Ji{}@FUVKgo^K^~j`?_G|M#3CNjC%s(3-G2xHtQAbc}PI+P#%DRmANs5);ednU7OyW$uI4Ua;fYJ)#*!Y;a_)&3Aeb!Si3?QY#`L&UX17;IKoSDLra? zL=v}gdiH*EGW0DRw4@1>-cz~vl=oU*f7;Wi0b)?t^9ITzE}01U9fm>2x#ylF9UhQ7 zE1)aI_+2-avzlQZwW5;Yb={t~w^G|a-QJJ)r=@2bZnj%u5y0IKR@9585&2P|0wfI0toJgMHozqYv3@ zKN8YOA8b7CSgEvRFtnTN?FNxoN_}u2+|CDet6ynS0-^CV$EK=bx`G-?xu;zVy?rcU zms2#Z^`U!L+5}6iHfyC!FkXu4S44uR;*_CV&qtM{ipw?)JY-$t{8cpT~dmg*UjHbP9? z1`XmN(asm?>Z{xCdVMA|^*mbe+|tWgeZ}Uko9!8s>_D0tigQY#dCl7KdNR77pv=rkjH>LBw2B&BYBl|%y$DZAnpm*im*ZlK^;Z%*av zxnWkfvTF1LBJWk57)Damp<5sTDYR<4j4VU6b!;>iJ<1vL>2sO8il`lS z>i;Y>l#ln!hc`Zl=&9sGOT1D`)$}D%|1qQhL3P3U=?8UWfI9`sWN2?&&_=3SS#9=V z(z03*H_6n-sUXcjx7*QGVmK+pw3(Bvtt(@aQ9-i#vgq9!e?~9tI0IB5E7R)Z)h4v1 z*F5JPsY1EAuCY?tWV20ae-F~1*Md1&P=di#X02gu0nvqkF<6~?)WftlTiX9rp#mZ7 zT)Wtuor>)As#WtI-&I2!wv+L*BV`6s;jz)79JHfZG1U?X4d%HU-G?2rzyxFI-G;pe zth%q(c~GE|6>{*GAr9;NT-{$qa1kSSV(O8c(PPX8t!~e`ORj6(yR!^$suK6?Gfz8h z?DJ>3=%dePI>5weml+#*_b=N&HIEvB5;PdFoy|Inc*~-fWlR}S1$FD0T@QW5;1Geo zP?|OdA<7n*UWNu^n&q1{&^>v&5QABU}IA4T^8pJo*pXcf~9s9%VonbhjqneRc>Om5$ zb46aO1YbEslo~CiU3KJX=#fC_1>uHgPg{LWezMtp1J{{XmCD;0DtpH@1*7Ezv!~E;@DUZ!V2&MC@ zL~9w4ovjXy=bJN|%jmEv1>ni9}4#dhQMrN5=Y!iH+6it2Y5- z!{;|fw$fbn!EVzn3DWw%^{g^{(`_y#!1z`RxZ^FE)l1u!uziD82Ujy__NuD)ZJvAG zhV9SGsB&1>_O|k7*;6tr0s&_hE_GJZ$oIA|JX0S~N4zxl%wT6j*tt2Lw7e#MK8E~9 zN4tis?}^bYX1uLRX!wo9xfe;teMo=3r@5JCQN8V7;!rt9oOw>pbnu;?hYI=&{-!DI zjcpiu$0(?Fhq53)G;eTRmSk`%luWawvPC<4sQPp9XM-8}GI+4P2NS^Y?H9UZ914BY zQNGx_zIAuocGK@ed5T%RwpJ~8!0f_RYxhH3CN;EEcZBz*6H5;4k$}E-1Sea5< zH}9{H<}GW_^?^JtSZJjOqr00wc+yP29(watr`Ssk2JQo0IIpAETR1~k4b~r~f8DnY zd~QB5Jdi>dGnrphUt+IkZg1YR$zwpa6460q4;3<npu` zT)ZMt$E3gTalbG@5X{iu+w$+7k1QLBK8uO3!)I6-iT+ByR5{IZPL%=D`qWQMe(ScU z7l3NE2Xms!?#ADLA3gzQJ;P_du4noR=${q(!;&;+ZS=OatUJx}_;Ek2#k+vc%NAkd z^8%qfz91jVAo7t|3gRKk9m{B6a7YzYCcX=Ks6ed{WG}!GVV7Ri2B#`WNLaIFb*lN4d6D zoTuh*B*?!eF>@-~`zh@BQwWKX*)igWh{85XlR`v|STLT1{I>EaB%T7QLr9}4X`kgr zps=wU*c%=^2WRYz-vb4_*GX6#gwH54=DsksCZ-8D@JcHer^{g3!Os{$ZW*k>nzaLQpuVV0m62 zS~xvT5^F!3NF@u=sn}c?DXY+&q!F^ZiW!YTq6}zQOMf^t%^Q1Y>9E zKTa|}M;#<6aGx05$F`v{WRMlEPQAd!EpA3Jt7clRjG2*39cb&&G-8?#(_PwZcDc!B>E+datTj5eY8(y;eXu}h|*sxY+W$Iz_JfCzl^4yr(v0wqxuCDwU zXt)g%CA*SYaIrRYF>-t+*_qk6xd0g|3ElYhIJRteZ@57>jkgZf?b?wIH+PRPP&z2u zO_}*=sXlXe)wVvZ5$zH?lV_;(?sEK_J$ulx_jFgDQ%BTmX@A!R$1-Dyj@;llc^wWy z)v0W=s`N7wXz(3|%L-$G}P092B;$6K@raXfEC~Bj-d;v|(dZ zwy{^rZKL!gqkYPlIvK?3ZaquQ&rD7Bh?&?xFWbuTt<1d%%r8-9F#WFCHmkccH4HtW z?*WqrOxpbpCLBCZuI-QyoumDa@h}&N%?zWC2oRe!3A>KWb2L}%`&psU$Oc{7juvW* z4**oi!Xo{%xl=LzWxX$hOJ7L;r4;mF_Q~my!H1xGRjg_cjB1cOl-3sSa_9ZjR;l@t z%Ic%$f<>w4xygksu_!w=XKxT=P^kiKa4nJhnh%6q#zDg}8=3jUeP4N3xEmt@*Ps&mXdF7GU@vMyP$I6A^KcymcX!pnD(B%E$M;io9R{T%7rT@6(RFj00M~<(Kfg z3x*7!02n$uI@wyQSzFPWI~iO5lhOzZNB|Uh1mBnb=Tj9Y02e@yAW|#n0f?YgL+Fb; z`6ZLl=AbkVkXgf=j{&!}T{?l%l0EuJ{i}5S>$-s}$P5a*ehfNZWSQMe3^JM@sk7yZ zN~Befz$E;)+GBKDdaC5OnaK_3Q4^qKQ@r0&)-a7Ex{#%g-0|Lzx>ZDBeCug?9_4|N zZ%FJ3KN%PzL|^{h?)1Xa^(sl*ofjrWsc}KmPS6zbj->;|&0p*iIZG{GyN8ZKL~}xS z_vzp67sIM{NAG_Br^>fIF#g3J@(#9kj{m=mk`?#QWz>-C!0y1g&F1x@d>Z?Ra`Y2t ztGdc__He?Mz{{xUde_`R*-d-$TM6;)*t7f4?AcDZIkyqL&V0Wnr5+|5y_?thJ&?S# zqVt1;Y?B@Ctjv+nj>uA3GR#Ey*Ni5Sc72ELAKMy7c&Y3Ac#aw=FMT4O z`R_zIlAo{?mD;?>m{F8d`3@}5_-3l6R&1#oOs|p&3%|_kc zjmXTFIop?}n=+>kH8OL83o`k=U=y*2%##Zt%DAxr4>bC< zsS=YI;?1Y065q$oVvJ>l7RAP&a2L%vNdAWAo=_jR-7*nDQhs0Ala&hCyC>}3>vNYE zVN<|Sk;6fbgCh%@pJC$OYxD>b-j)%iZzfrQY>oGrSP4xkf zr1PZq%_^=R`AJ)8V`C5HI^{$M4G$$%nZZ)6$8gLbHQ}A~yhNBD`P=r2EAEXB(Jpsr zvn>=yC(U9enY9*b5b*}-dHoNm{r=xnM8YgChIP6vj$fsH)aTi!*YC_#fco!Nt(T=7Ht}T@>mU>JZ4JCX|`g4T!E6p$c1u zBBSbXt}()z0=`C1G+&Q#bvBbPCne#|-Z0Plww0*r1nw(Qw;DO^3E-*h7oCl;mtuPh zr9*~obLVaH(SYxs*i(I`4aG>1jd7nEljnTMX!Eo$L zGejymZ29BZRj79JP}G z7L)*be4MsWPj6#AYu}%gIuxX!38~f%+wK=OA08)LhhEax3H9g+b&Z{YUgTeBXVw&* zj~DwBW!sK$myN=_wpX*(TD9KHUx{v5EN)R5r$C_2wb~qkkefJrY0X=?peL!mpQaW; z6wHr~h#tHcrs7&buq5^V)CMopRCW6DN;0fx1T0qaCx+YW1;eX#fU?vg533zwY&1st zh7VOekio|0i_6urO^ZJ_T6#hkeXTU)rsWe2-J-+J|ffc(b z(I3=O5DX~Qg~}C$A%_NixSCmTkkZCK%30;>fL|i-e!cK$Z>`OOV`N6}J04bVf1aOS z1yYP@`T=d1_^H0p$F)|gb+gy?e!hJvfmjx-CGu8rmN&ns24Bp0HJ!kwK3<46!&#T^ z_NuYBcriQp7pusNJWEHpG4QP(7{}gndu&|D3sOJE*spq)mgGff%E7p{Qc!k7D#=f$WC_k}}29 zU}q7@rXtB?Nji{8>iKQjZj#^%c@NB^$$>6m4~Ytu@&_U}6MG+b51GY!gRk5wLqr$c zO+YnpcBLUr6od1WMl}53&r`J#;2LY2nI`j@kb3Kd&uG+<;E#r1G@^N1$FK5&6M|DG zhP5q1FBV>@W=kRJS*Y3McVdBz@B=3=yJeT5&)M4ZE-v>~QA2EbCcCMFzBjEnT*0ja5Ql6YTw_B~yyp1dmlP$?FgUt|p$ zy&t{P)}6?!m?u0n&9;fR&Ofw-0PVyzA+{(vUsN|%m7OK_bufZmpYLks1G~K)oi~c>9tu~ zCr`RgJ12VK9&WsjFkE*AG{~T#>jgL4RXzk(;!RLdD-6h>6-8wKJARsw(cb6bAgdp9GbV?&#(nyDdfHcw|Ih6D_-m6#T-rs-l&GW2h zIA`;&v(A})_E~4`wcg#_#-QFZ|QjDX-<3Qq82K++o& z^P`@%R=UzU4qlWCFN^rXaS}YyQlIQ;7?@of=_&kGXspHE`{Jy?Q)qTWM6^>-m+bo<8tAKN1)3egY;nUhFk!1Vcu>UnL5EFnMYzwVOF-(PlfjXpr zmmfMgyIKETd$^-B>o~`Y?YUTVjW{X1E6P7+o*TUr5;N8nqHQGBNh9eOBt8f+ zls-?n_PGwZ`h5Q(Kdd|s4asYhJWFQPhyY^>7|%wMc^LM^+i&$De(c;sgbMOfEBmKX z_v8TN6{~yB$LFWp6MYQ5jxiE6>qsJGBBR#FD;VClvu7A~l~;^!H6Fsm>6TZ0^O<8pGq&azLUPZ#E>wF#Rxwoqp&B>UuH~&_WEM%)7|41o z>QC9-)Jx1`n9rNzy6;ERPsfy2A@62k%)lz-yyzECHZdG2uK%Egm0+#k9SCFI)e&a~ zj`pTU$nnU{eEsdE-gQk#9gGye4=fA^+7>nXiiFV)k~B^rkUw<`ArOcVp=E~Z!_uEa zi@cUO_BB0@RzM@h+JZ_+J3oTgUV*tVNA=tKyiSMmqe%D|eG$wL@W|>n%-!s_`8TwZ<2fD2|LuHUuKy(y)rYOk6Dv=9NzhI*5KAr$C6+dH(@r)SJZxSqax)MQ zarAt+3Z%M8Csn{RaKLZ&MBUQtMs?t&RPvj75Owc*2!4&k6XMn?JZg(O$h2XSA;9*vm#2UIAqOfE#s zib&OeaZy0Bt)J)eA+dFBbM4%X*oSU?nM11+a)1BD1<^{__KhX{{_7OlbA5I#`*eEY z{3r4hfjQ9<`h}Kb80o3U=G!JwToav`nIc}hZ(nXt{ zs0Qi!c64V-N3-X}a$ra{QyKIJx`yM}9exoR2>2@F3%3CzRaQ`^xK2vyAl4;eNcxiZ z%9lu&2>OTrtM)hScoy6W$z$f}xNk>=w@RxO^xVhI(~rMz$mO6-&9V*{;Vlo97|nU_ z?oq8fUVGb%neMPrQr#mIo!{gcUkb=wWuyIS@{k}Z#bAijXjWijRN3AtZM-q-vgqY_ z)VpdujS>In$g&|{F3PgbAsM3+nk8G>oT|0=dsR(#KVXFd}5kcF^O60`Y z*UIL;zkOjLr>s~rk0IH(>@Y}SzKY?Hkl$3zGrop_OWUk{vuoK*L!_Lif|l(~_k3O* z6a4UNJ8WYxlI+#N>FGpBIsZYTzV@QmONu#2O$i{+@CqjVNEn(8p5&f&K+!sSE_UsF z@a$7>Uu^zpx|^nml0Ly;Cyg@IVJ}WhFF>QCX)p_UU+>srSep3!$$M!FH+BVi5z66S zOC35ga9ZTgm<%>{Zij7H)#Ih_LnobYxXB~k z8W{||kxO8jEz0j7zXgyJ6w@3D82WEGv>|^ydnu9})^SvBn1A3Uqp=!}3cntT1Qi6R zz{0P``+VU$Ak^>NU*>EJSnOB!b(6V=%Ax17{^H`S8p+t_ypjukeP9?jiCsupzh%eT zk!Yj~K+}HnmJoIVznDis`eGFcV)JZ2C=h-OdzWqveq2HrPpYU#3cs}V_FHg`kP=UI>RYv)R?Q#i)2<@<`j?}A_Oeg4pPoC(~44Og!y z5=72uN|L`oVZJOw0%7!4Kd?IrNnTY{kp8@0t>u`)HANHYA=AOD;4Z`Tu2j0kHoA#s z%QJ_Pv~K$2+vO_4MTZnTo7W^MHe9*k#<57`bsCC8s=iemaUO_KX#!_3mAeSIs#7l`*4uOwGzHcqccB)`%d`Kw4sj*k(7&PlHma|$zD9p)(?Cvz=Y}Qs1oGsqVnL?-;(70g%q!U1uF0P znIs$>{%HL9Pd%R&nD$l+FyVI#Fz^2@mhdhHDH#}dBrT@Va2Sb&>R5nceJ^bKjvLwkb+;9|3hn$3pye{Q(TV#Q=BW* z&gJDl4Ekvp=KZyT$8&v(l*luRlyFdyNb%ghmYEefGrB6uKRLa=K`C!k&KT@zkt>k) zE+_)Il-yv@r8;?0aEs>hE1pP-Ve&>2ftWufh`8W@<|$syz+azdUpr9XcrKwNANyfh z4&eGwoW{dhF$LPt*~$1)?UxUY$w^1OAg=}LDPuqL3$%FxIyZR{7?Kp>W3QFO`ML-H zu-4orNF1-O%Um4OvYz!3iSP4vo5RnT!pO$<3-O}ch51|Srpi7erV$7o6=YdMER-Ns z{(y+lL~bE~-s#Q*Pm5~m%x~2lOTjhbSwyws4T@;__Fzkx6?vZ5j&F$p&_fj&b5CI2`+-a?K==t$A(^nm%ah~@ zF%TzBd`;wAk2DSSH_XIGvNRv%k`>d!wpVESYOAKM6w${1&l3qRPDn0raa9q5TC81T zoJR~|txcWZ#E>pg!sA6H?|otpA7OqI^2bp=0*8-`2vorcm5j&?Gv8MOR2!|feP}; z#w#UtHPL3qGRn}@w~*Zz+wBzRwGR(h)JJK(!Az@({ZFHvMDWqdFS{x@gy8jXmC=YP z9*l=giDIg0Jh_H7$S`ZH**?;f!6GY~W%$a=ZYYY-KjS4N%OgPVo0CCX_c(q`t!Rbz zGraVGvKCGm+lag+=yu}k*6|5Y?izz-83W>Q8rPG8GQgZ$v{qGf3@JSs>`CL&;@yCl zm;g9ea7@IKwPR;Ghav@QFznQ+gviGIt6Tb_Wg3)-OivVEjP4su)H+)g)pQJ;J!`&I zq6nYY{)D(KFV#B6O@$QAYSW5fMyf)QTTB-1qINx3@y@7u!e$2KW*tRIrj8~(%hnx* z-iTI6E}8=`sh-KWH+)l2 zuFvE zJFl8o>W`e-B-vyU%&4HjpXRrCUf-DER8Z!jE}nK9MQ`vVo14Y(b77@vR0?bOt1<3} zvaR%K(w~hT!S>lUo_vq2SK7i#P5qgKE>^2lSGne!i)v;d(~WuQN8H(kMJO_|DgcNq zmS(hfkj(RI%9Xu!t*Y#%N7VVX%JkFWXhPB;{5_gMiSqEx6OKc@`46&Yvjwe@`#cJ_ zd`#q0)zfP#8cm+N;<5fIg+4rhvnm^r^EX>l4kmF~VDCvI-6-|erhFA&>@5KteI(C>~GVUs!i6U9wU7mK+rXYq9!G(*hFhffaT$P9sl@5 z_~k}Xd@GbnGW1(H*2yqf_HHj=DPUst2^BQRM1ZD=JN1T42YI}?9xUxLn=<3olB~k z7K6d8#F(iXT4t7-;>hToO5#+IkUHzgREAI+P|`)J<-{=WZpM>xovyS0{`#mDqRl~` z-W5lpQd^kWColv*XZQXMWFr6Tu4HidD_AkqO@ zYp~&h`SOc5H1P26ltnD8frkUoX&*Xx7#Q52p$S(LL*?I6PEz9(?Vv<4J&W|aLh4O? z{0qq@YOyk-p9;sgRkhCUJrM0NZ=-zZFs9JT<$&!vE#6Gv=^5n{%%SMTXJRP$XebJmM~w>ZrE6Gq3;X0I#F29HPP+_t{RJlTwQZM?2N`tbovWTz^w3X`2 z=GvV_9#w+p)Q=5IgC+9QN83%1<~$2;Y@gEYC^s>`V%-9$SQ1=tS`CzSb>%lDc{4yh zTBLhj**%v^hEDzJ3#@n^2hMQcJ`l@!k6y+h$?e@`(WAO1#H`Dmz{WyN-{kyNuoDTX znN^W`o~t7KrNrhF)0McdAfoW`3xhSI+$*KyQQ`0{xDlYqClujKJvV3xP<6@(@ofIS zUhK*za_5)#Z_Q1dWDGQOBOsu6vyksmr+@$W6ADT~u+WGVJhbNTH(D%PXB7iOYm@I2 zS95BA(PFh^RJ%T&t0M&TMkcG-Og)V)(q-9o+4Vu1EDyKNmrgfG^oVS3UoiEEBqMoR z0GQ+`P2-Mw0In4Si?%q)53^0OxRM8}s2l`;&7hj16vjtd3k|-s7ZV|Bj2KIz=O;*g zHixFT*L6P2XIAXMRIi3!vSB&69GqgI6DFe2MP#6!7UFA9_{`r}y)#GqCF;P(fn^%} zlD6bw3;C076wHvh5J~b*sY-uY9-piw~t(3a4KP$*0IQlarPFXU6zE*fv;6VPJioQayM}LsxUT^>TL&9}L zT%^a*N|B;-XM&7Wtab!I+h87VIIt$fU|Z}sv=PE`boLT|Af(Ih9ZaUuJuFaf@FdzHJjolXY*yRJMY1N=CjI zt2v^xu9Cn+*UeKJWJqBhhzWM`L{x8H$yjW;`gT);Y@$F-q3!#atW{Kthgj14o$uqJ z1!-K=z;VGV`dwOgNviS0D_iV&wk&!hvw#Lia(;jBQfRhxtp(x-uA%AbhvQ)-L%N^z zd4X&x2wactaCb>|&ey}r1c^&zcs3H)9(i(nd6s;gj|7@8IMVyiTc&CORy+whUeZEu z8!RZLA(Y4PulaFyBRj{x?ieT`_y6-4LNDFxm_@5u4osg##7l~>1+wb{!a@h_+IZ2@ z=W2=J%@$)kidM3K#>qZ!vFV63T3iJ8k*$`?s67>^i@@vQhZ$Z)(g6xH){h_Ww&h!` zXc(HJ31uivD6J}|L=0MOy!n=;)S|-H=4nrUpEyY4(X={KE2WTxo)Yl{x*!oKxSa8P z6+qAFZy?*O!tRKo!g2b>#Yagt&KYyvi!&xisTjDWCoY6sojn3T#HU_kmRDc_Y%aft zH(IBtnA)Bn{l?5 z8#e_1@k8v~X5@#X{uhDZVREHQS8C1-^lPtI;`_c4g{LesNdX1Ehe>bw-+V;Jp6?HG zwJ1?QIGz&sr1o=|PQ*X8sK%810{=(_{v}uE$AYeWERAIiH;OReNR2*V)jEx!=?ln2 zGwfP7mrI2#kV^#FIU!k7o;rz3EYyx~h?NKo94>RzHpn?YuOSC*T9}}9TSx9Fqp`p@`0o9a2B%-^6?owPToiGg0y26IW^ zJ!00Ueo(hS!V1iufY7u0g_g-c=S&w%NDz5j?#CJUK}0s4m*C4eUrX6|P z7#f}P(Wmxr=gHfM=fHY(jalweN|3Pn`FZq$*4)L|{EJZjh$}iNqBB7r5_TGm`kQ!{ zPd;cB1-&;*`VW_0k<%o}r<>Y@Z#!?;<;*UY-M#_yX5c%w5V$NeoF15bgLc#Z@Kl_| z1l&8IEez1UN)GL(zcRM|rjz|AZ|ncLWWRUUAD^&~6(P_?0T7n1mloR1?NaBS_haSD zGCB*%pf?grNk20hP`9*LY*~NH?7Ybke0!<*zS$qB!&#MYo>^%5oRADnR35?W^4bzM zq^rA~4R^GSjHq^gL(1brmkiS34o-2l1TRNuu3hQ$5FraFjw4~cmGJkOus|FP!HbJ;%?{JE6IPvH0OIp`sOTUg^R z@a`G|zo1vpim!jIIB*yKCl&H9Fbqr_`VaX3AV$8c=`NS;FI{9%n$LeT+up_BFoRU51fgXbGA>(0}ob+(qB5ult35&G`d; zx7zNmio5j{zf|<_{ZR3HwZ&cdpP8e-z%Vc@(A>aZqtZX)(Om_9rds~4K=|=p1-}w6 z?`pZ5arjG*$CDpt@kjpQUHsjIgkShn>7Q5OpY(*g;JYD%Utm7XAK*Iygu5E@8bV-!hfM*V9cOi`@dZByYN4qp5NiThQGmoxIlN&e?E$SM=u!v e{tN!sgXy^}Jan-5o^*@@^AQdPX4Ulj*8c%+K=Wn* literal 0 HcmV?d00001 From 0ac841e338884e7cac15ea11ff9d623fb4013b1b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 6 Feb 2024 16:13:40 -0800 Subject: [PATCH 02/24] Update Html.php --- src/PhpWord/Shared/Html.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 21d8404ddc..6f168164c6 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -109,7 +109,7 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit $dom->loadXML($html); static::$xpath = new DOMXPath($dom); $node = $dom->getElementsByTagName('html'); - if (count($node) === 0) { + if (count($node) === 0 || $node->item(0) === null) { $node = $dom->getElementsByTagName('body'); } From 39fbabc5e81512078d9f15251d957701da56dca0 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 9 May 2024 00:37:05 -0700 Subject: [PATCH 03/24] Load Html as Html Not Xml Better from a technical standpoint. Pre-processing much simplified. Some post-processing added because loadHTML preserveWhiteSpace=false does not work well. Not-UTF8 html can now be loaded. No need to surround snippets with body tag. --- src/PhpWord/Shared/Html.php | 69 +++++++++++++++--- .../PhpWordTests/Reader/Html/CharsetTest.php | 63 ++++++++++++++++ .../Reader/{ => Html}/HTMLTest.php | 4 +- tests/PhpWordTests/Shared/HtmlTest.php | 40 +++++++--- .../_files/html/charset.ISO-8859-1.html | 17 +++++ .../_files/html/charset.ISO-8859-1.html4.html | 17 +++++ .../_files/html/charset.ISO-8859-2.html | 17 +++++ .../_files/html/charset.UTF-16.bebom.html | Bin 0 -> 604 bytes .../_files/html/charset.UTF-16.lebom.html | Bin 0 -> 604 bytes .../_files/html/charset.UTF-8.bom.html | 16 ++++ .../_files/html/charset.UTF-8.html | 17 +++++ .../_files/html/charset.gb18030.html | 9 +++ .../_files/html/charset.nocharset.html | 8 ++ .../_files/html/charset.unknown.html | 17 +++++ 14 files changed, 270 insertions(+), 24 deletions(-) create mode 100644 tests/PhpWordTests/Reader/Html/CharsetTest.php rename tests/PhpWordTests/Reader/{ => Html}/HTMLTest.php (92%) create mode 100644 tests/PhpWordTests/_files/html/charset.ISO-8859-1.html create mode 100644 tests/PhpWordTests/_files/html/charset.ISO-8859-1.html4.html create mode 100644 tests/PhpWordTests/_files/html/charset.ISO-8859-2.html create mode 100644 tests/PhpWordTests/_files/html/charset.UTF-16.bebom.html create mode 100644 tests/PhpWordTests/_files/html/charset.UTF-16.lebom.html create mode 100644 tests/PhpWordTests/_files/html/charset.UTF-8.bom.html create mode 100644 tests/PhpWordTests/_files/html/charset.UTF-8.html create mode 100644 tests/PhpWordTests/_files/html/charset.gb18030.html create mode 100644 tests/PhpWordTests/_files/html/charset.nocharset.html create mode 100644 tests/PhpWordTests/_files/html/charset.unknown.html diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 6f168164c6..9210aec19c 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -46,6 +46,8 @@ class Html private const RGB_REGEXP = '/^\s*rgb\s*[(]\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*[)]\s*$/'; + private const DECLARES_CHARSET = '/ charset=/i'; + protected static $listIndex = 0; protected static $xpath; @@ -55,6 +57,9 @@ class Html /** @var ?DocInfo */ protected static $docInfo; + /** @var bool */ + private static $addbody = false; + /** * @var Css */ @@ -88,16 +93,14 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit } } - // Preprocess: remove all line ends, decode HTML entity, - // fix ampersand and angle brackets and add body tag for HTML fragments - $html = str_replace(["\n", "\r"], '', $html); - $html = str_replace(['<', '>', '&', '"'], ['_lt_', '_gt_', '_amp_', '_quot_'], $html); - $html = html_entity_decode($html, ENT_QUOTES, 'UTF-8'); - $html = str_replace('&', '&', $html); - $html = str_replace(['_lt_', '_gt_', '_amp_', '_quot_'], ['<', '>', '&', '"'], $html); - - if (false === $fullHTML) { - $html = '' . $html . ''; + if (substr($html, 0, 2) === "\xfe\xff" || substr($html, 0, 2) === "\xff\xfe") { + $html = mb_convert_encoding($html, 'UTF-8', 'UTF-16'); + } + if (substr($html, 0, 3) === "\xEF\xBB\xBF") { + $html = substr($html, 3); + } + if (self::$addbody && false === $fullHTML) { + $html = '' . $html . ''; // @codeCoverageIgnore } // Load DOM @@ -105,8 +108,20 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit $orignalLibEntityLoader = libxml_disable_entity_loader(true); // @codeCoverageIgnore } $dom = new DOMDocument(); + $html = self::replaceNonAsciiIfNeeded($html); $dom->preserveWhiteSpace = $preserveWhiteSpace; - $dom->loadXML($html); + + try { + $result = $dom->loadHTML($html); + $exceptionMessage = 'DOM loadHTML failed'; + } catch (Exception $e) { + $result = false; + $exceptionMessage = $e->getMessage(); + } + if ($result === false) { + throw new Exception($exceptionMessage); + } + self::removeAnnoyingWhitespaceTextNodes($dom); static::$xpath = new DOMXPath($dom); $node = $dom->getElementsByTagName('html'); if (count($node) === 0 || $node->item(0) === null) { @@ -119,6 +134,38 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit } } + // https://www.php.net/manual/en/domdocument.loadhtml.php + private static function removeAnnoyingWhitespaceTextNodes(DOMNode $node): void + { + if ($node->hasChildNodes()) { + for ($i = $node->childNodes->length - 1; $i >= 0; --$i) { + self::removeAnnoyingWhitespaceTextNodes($node->childNodes->item($i)); + } + } + if ($node->nodeType === XML_TEXT_NODE && !$node->hasChildNodes() && !$node->hasAttributes() && empty(trim($node->textContent))) { + $node->parentNode->removeChild($node); + } + } + + private static function replaceNonAscii(array $matches): string + { + return '&#' . mb_ord($matches[0], 'UTF-8') . ';'; + } + + private static function replaceNonAsciiIfNeeded(string $convert): ?string + { + if (preg_match(self::DECLARES_CHARSET, $convert) !== 1) { + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable $callback */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + } + + return $convert; + } + /** * parse Inline style of a node. * diff --git a/tests/PhpWordTests/Reader/Html/CharsetTest.php b/tests/PhpWordTests/Reader/Html/CharsetTest.php new file mode 100644 index 0000000000..60e80964a2 --- /dev/null +++ b/tests/PhpWordTests/Reader/Html/CharsetTest.php @@ -0,0 +1,63 @@ +expectException(Throwable::class); + $this->expectExceptionMessage('unknown encoding'); + } + $directory = 'tests/PhpWordTests/_files/html'; + $reader = new HTML(); + $doc = $reader->load("$directory/$filename"); + $sections = $doc->getSections(); + self::assertCount(1, $sections); + $section = $sections[0]; + $elements = $section->getElements(); + $element = $elements[0]; + self::assertInstanceOf(TextRun::class, $element); + self::assertSame($expectedResult, $element->getText()); + } + + public static function providerCharset(): array + { + return [ + ['charset.ISO-8859-1.html', 'À1'], + ['charset.ISO-8859-1.html4.html', 'À1'], + ['charset.ISO-8859-2.html', 'Ŕ1'], + ['charset.nocharset.html', 'À1'], + ['charset.UTF-8.html', 'À1'], + ['charset.UTF-8.bom.html', 'À1'], + ['charset.UTF-16.bebom.html', 'À1'], + ['charset.UTF-16.lebom.html', 'À1'], + ['charset.gb18030.html', '电视机'], + ['charset.unknown.html', 'exception'], + ]; + } +} diff --git a/tests/PhpWordTests/Reader/HTMLTest.php b/tests/PhpWordTests/Reader/Html/HTMLTest.php similarity index 92% rename from tests/PhpWordTests/Reader/HTMLTest.php rename to tests/PhpWordTests/Reader/Html/HTMLTest.php index f091e5a275..c0e150e495 100644 --- a/tests/PhpWordTests/Reader/HTMLTest.php +++ b/tests/PhpWordTests/Reader/Html/HTMLTest.php @@ -15,7 +15,7 @@ * @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3 */ -namespace PhpOffice\PhpWordTests\Reader; +namespace PhpOffice\PhpWordTests\Reader\Html; use Exception; use PhpOffice\PhpWord\IOFactory; @@ -34,7 +34,7 @@ class HTMLTest extends \PHPUnit\Framework\TestCase */ public function testLoad(): void { - $filename = __DIR__ . '/../_files/documents/reader.html'; + $filename = 'tests/PhpWordTests/_files/documents/reader.html'; $phpWord = IOFactory::load($filename, 'HTML'); self::assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $phpWord); } diff --git a/tests/PhpWordTests/Shared/HtmlTest.php b/tests/PhpWordTests/Shared/HtmlTest.php index 765d79f906..76e8273ec4 100644 --- a/tests/PhpWordTests/Shared/HtmlTest.php +++ b/tests/PhpWordTests/Shared/HtmlTest.php @@ -23,6 +23,7 @@ use PhpOffice\PhpWord\Element\Text; use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Shared\Html; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\LineSpacingRule; @@ -52,6 +53,7 @@ protected function tearDown(): void */ public function testAddHtml(): void { + Settings::setOutputEscapingEnabled(true); $content = ''; // Default @@ -80,16 +82,31 @@ public function testAddHtml(): void // Other parts $section = $phpWord->addSection(); $content = ''; + $expectd = ''; $content .= '
HeaderContent
'; $content .= ''; $content .= '
  1. Bullet
'; $content .= "'Single Quoted Text'"; + $expectd .= "'Single Quoted Text'"; $content .= '"Double Quoted Text"'; - $content .= '& Ampersand'; + $expectd .= '"Double Quoted Text"'; + $content .= '& Ampersand'; + $expectd .= '& Ampersand'; $content .= '<>“‘’«»‹›'; + $expectd .= '<>“‘’«»‹›'; $content .= '&•°…™©®—'; + $expectd .= '&•°…™©®—'; $content .= '–   ²³¼½¾'; + $expectd .= "–\u{a0}  ²³¼½¾"; Html::addHtml($section, $content); + $elements = $section->getElements(); + foreach ($elements as $element) { + if ($element instanceof Text) { + self::assertSame($expectd, $element->getText()); + + break; + } + } } /** @@ -110,7 +127,7 @@ public function testParseFullHtml(): void */ public function testParseHtmlEntities(): void { - \PhpOffice\PhpWord\Settings::setOutputEscapingEnabled(true); + Settings::setOutputEscapingEnabled(true); $phpWord = new PhpWord(); $section = $phpWord->addSection(); Html::addHtml($section, 'text with entities <my text>'); @@ -138,13 +155,14 @@ public function testParseStyle(): void Html::addHtml($section, $html); $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); - self::assertTrue($doc->elementExists('/w:document/w:body/w:p[2]')); - self::assertTrue($doc->elementExists('/w:document/w:body/w:p[2]/w:r')); - self::assertTrue($doc->elementExists('/w:document/w:body/w:p[2]/w:r/w:t')); - self::assertEquals('Calculator', $doc->getElement('/w:document/w:body/w:p[2]/w:r/w:t')->nodeValue); - self::assertTrue($doc->elementExists('/w:document/w:body/w:p[2]/w:r/w:rPr')); - self::assertTrue($doc->elementExists('/w:document/w:body/w:p[2]/w:r/w:rPr/w:sz')); - self::assertEquals('22.5', $doc->getElementAttribute('/w:document/w:body/w:p[2]/w:r/w:rPr/w:sz', 'w:val')); + $element = '/w:document/w:body/w:p'; + self::assertTrue($doc->elementExists($element)); + self::assertTrue($doc->elementExists("$element/w:r")); + self::assertTrue($doc->elementExists("$element/w:r/w:t")); + self::assertEquals('Calculator', $doc->getElement("$element/w:r/w:t")->nodeValue); + self::assertTrue($doc->elementExists("$element/w:r/w:rPr")); + self::assertTrue($doc->elementExists("$element/w:r/w:rPr/w:sz")); + self::assertEquals('22.5', $doc->getElementAttribute("$element/w:r/w:rPr/w:sz", 'w:val')); } public function testParseStyleTableClassName(): void @@ -778,7 +796,7 @@ public function testParseListWithFormat(): void { $phpWord = new PhpWord(); $section = $phpWord->addSection(); - $html = preg_replace('/\s+/', ' ', ''; Html::addHtml($section, $html, false, false); $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); diff --git a/tests/PhpWordTests/_files/html/charset.ISO-8859-1.html b/tests/PhpWordTests/_files/html/charset.ISO-8859-1.html new file mode 100644 index 0000000000..fd27c975f3 --- /dev/null +++ b/tests/PhpWordTests/_files/html/charset.ISO-8859-1.html @@ -0,0 +1,17 @@ + + + + + ISO-8859-1 + + +

1

+

B1

+

1

+

D1

+

2

+

B2

+

C2

+

2

+ + diff --git a/tests/PhpWordTests/_files/html/charset.ISO-8859-1.html4.html b/tests/PhpWordTests/_files/html/charset.ISO-8859-1.html4.html new file mode 100644 index 0000000000..8a14894517 --- /dev/null +++ b/tests/PhpWordTests/_files/html/charset.ISO-8859-1.html4.html @@ -0,0 +1,17 @@ + + + + + ISO-8859-1 Html4 Doctype and Meta + + +

1

+

B1

+

1

+

D1

+

2

+

B2

+

C2

+

2

+ + diff --git a/tests/PhpWordTests/_files/html/charset.ISO-8859-2.html b/tests/PhpWordTests/_files/html/charset.ISO-8859-2.html new file mode 100644 index 0000000000..c2b494ff99 --- /dev/null +++ b/tests/PhpWordTests/_files/html/charset.ISO-8859-2.html @@ -0,0 +1,17 @@ + + + + + ISO-8859-2 + + +

1

+

B1

+

1

+

D1

+

2

+

B2

+

C2

+

2

+ + diff --git a/tests/PhpWordTests/_files/html/charset.UTF-16.bebom.html b/tests/PhpWordTests/_files/html/charset.UTF-16.bebom.html new file mode 100644 index 0000000000000000000000000000000000000000..6b29e7d2b011f7d8099fd407244dad7ef0d3121a GIT binary patch literal 604 zcmezOpFsf%Z5R|8Tp0WroEbtGA{hc0T!G>l3?&S?3^@#T3|vU+V4?~@nM8&>hI9s7 z26cv1AWI!XWhzib3YusMLnhGN9H0(6hESkwZVb8%h74v5HVpbeafBSQo%%qxU^6R; zAs=XdC5jC&AHirFh614c0iYcq8$fJ?$uKoA8lu*T1hvmeQ0qd1+QSS+B)FG&waz4% UdjYFjP$*&xcaS(NZ2-d<0BnUsYybcN literal 0 HcmV?d00001 diff --git a/tests/PhpWordTests/_files/html/charset.UTF-16.lebom.html b/tests/PhpWordTests/_files/html/charset.UTF-16.lebom.html new file mode 100644 index 0000000000000000000000000000000000000000..4ba47a81395d2f8076299b471e0357e1287ce1e3 GIT binary patch literal 604 zcmezWPk{jfZ5R|8Tp0WroEbtGA{hc0T!G>l3?&S?3^@#T3|vrE3Jf-IQ3aq(B10ZS zI)g2PIzuXurH-L86{sQwO|*m|6KHM@P=_5uD9|=H23-b21~Z_^`ap4n9I~DIKwGhy zmBf$_G`|wX2AGdvv<*W6Q2qeW4v-BXHo|0>8W;^x>qLUu=On0gAwlh71|t&OOT1cV U63o4TRV^qKv4uNG967}R0Iu{zYybcN literal 0 HcmV?d00001 diff --git a/tests/PhpWordTests/_files/html/charset.UTF-8.bom.html b/tests/PhpWordTests/_files/html/charset.UTF-8.bom.html new file mode 100644 index 0000000000..5a49399018 --- /dev/null +++ b/tests/PhpWordTests/_files/html/charset.UTF-8.bom.html @@ -0,0 +1,16 @@ + + + + UTF-8 + + +

À1

+

B1

+

ç1

+

D1

+

Ã2

+

B2

+

C2

+

Ð2

+ + diff --git a/tests/PhpWordTests/_files/html/charset.UTF-8.html b/tests/PhpWordTests/_files/html/charset.UTF-8.html new file mode 100644 index 0000000000..9ae5a8e343 --- /dev/null +++ b/tests/PhpWordTests/_files/html/charset.UTF-8.html @@ -0,0 +1,17 @@ + + + + + UTF-8 + + +

À1

+

B1

+

ç1

+

D1

+

Ã2

+

B2

+

C2

+

Ð2

+ + diff --git a/tests/PhpWordTests/_files/html/charset.gb18030.html b/tests/PhpWordTests/_files/html/charset.gb18030.html new file mode 100644 index 0000000000..271a55fc54 --- /dev/null +++ b/tests/PhpWordTests/_files/html/charset.gb18030.html @@ -0,0 +1,9 @@ + + + +gb18030 + + +

ӻ

+ + diff --git a/tests/PhpWordTests/_files/html/charset.nocharset.html b/tests/PhpWordTests/_files/html/charset.nocharset.html new file mode 100644 index 0000000000..d6829b2edc --- /dev/null +++ b/tests/PhpWordTests/_files/html/charset.nocharset.html @@ -0,0 +1,8 @@ +

À1

+

B1

+

ç1

+

D1

+

Ã2

+

B2

+

C2

+

Ð2

diff --git a/tests/PhpWordTests/_files/html/charset.unknown.html b/tests/PhpWordTests/_files/html/charset.unknown.html new file mode 100644 index 0000000000..189638a80f --- /dev/null +++ b/tests/PhpWordTests/_files/html/charset.unknown.html @@ -0,0 +1,17 @@ + + + + + UTF-8 + + +

À1

+

B1

+

ç1

+

D1

+

Ã2

+

B2

+

C2

+

Ð2

+ + From cf1fd2a112419e72878b40a7589212f2dfd29c3c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 9 May 2024 17:35:13 -0700 Subject: [PATCH 04/24] Permit Some Backwards Compatibility It's debatable, but allow unescaped ampersand and unknown charset. --- phpunit.xml.dist | 4 ++-- src/PhpWord/Shared/Html.php | 4 ++-- tests/PhpWordTests/Reader/Html/CharsetTest.php | 2 +- tests/PhpWordTests/Shared/HtmlTest.php | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6f1f5445ab..ff0c676fad 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -7,10 +7,10 @@ ./src/PhpWord/Shared/PCLZip - + diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 9210aec19c..2d68806f33 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -112,14 +112,14 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit $dom->preserveWhiteSpace = $preserveWhiteSpace; try { - $result = $dom->loadHTML($html); + $result = $dom->loadHTML($html, LIBXML_NOWARNING | LIBXML_NOERROR); $exceptionMessage = 'DOM loadHTML failed'; } catch (Exception $e) { $result = false; $exceptionMessage = $e->getMessage(); } if ($result === false) { - throw new Exception($exceptionMessage); + throw new Exception($exceptionMessage); // @codeCoverageIgnore } self::removeAnnoyingWhitespaceTextNodes($dom); static::$xpath = new DOMXPath($dom); diff --git a/tests/PhpWordTests/Reader/Html/CharsetTest.php b/tests/PhpWordTests/Reader/Html/CharsetTest.php index 60e80964a2..4c73d70bb0 100644 --- a/tests/PhpWordTests/Reader/Html/CharsetTest.php +++ b/tests/PhpWordTests/Reader/Html/CharsetTest.php @@ -57,7 +57,7 @@ public static function providerCharset(): array ['charset.UTF-16.bebom.html', 'À1'], ['charset.UTF-16.lebom.html', 'À1'], ['charset.gb18030.html', '电视机'], - ['charset.unknown.html', 'exception'], + 'loadhtml gives its best shot' => ['charset.unknown.html', "Ã\u{80}1"], ]; } } diff --git a/tests/PhpWordTests/Shared/HtmlTest.php b/tests/PhpWordTests/Shared/HtmlTest.php index 76e8273ec4..3fa4b5c1d0 100644 --- a/tests/PhpWordTests/Shared/HtmlTest.php +++ b/tests/PhpWordTests/Shared/HtmlTest.php @@ -90,14 +90,14 @@ public function testAddHtml(): void $expectd .= "'Single Quoted Text'"; $content .= '"Double Quoted Text"'; $expectd .= '"Double Quoted Text"'; - $content .= '& Ampersand'; + $content .= '& Ampersand'; $expectd .= '& Ampersand'; - $content .= '<>“‘’«»‹›'; - $expectd .= '<>“‘’«»‹›'; + $content .= '<>“”‘’«»‹›'; + $expectd .= '<>“”‘’«»‹›'; $content .= '&•°…™©®—'; $expectd .= '&•°…™©®—'; $content .= '–   ²³¼½¾'; - $expectd .= "–\u{a0}  ²³¼½¾"; + $expectd .= "–\u{a0}\u{2003}\u{2002}²³¼½¾"; Html::addHtml($section, $content); $elements = $section->getElements(); foreach ($elements as $element) { From 2f799c2ba9be781106ac84545a58b58b2fc00f98 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 16 Aug 2024 08:10:24 -0700 Subject: [PATCH 05/24] Phpstan False Positives --- src/PhpWord/Shared/Html.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index d78338526a..2d96c375f7 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -180,7 +180,7 @@ protected static function parseInlineStyle($node, &$styles) $attributes = $node->attributes; // get all the attributes(eg: id, class) $bidi = false; - $direction = isset($attributes['dir']) ? $attributes['dir']->value : ''; + $direction = isset($attributes['dir']) ? $attributes['dir']->value : ''; // @phpstan-ignore-line if ($direction === 'rtl') { $bidi = $styles['bidi'] = $styles['rtl'] = true; $styles['textDirection'] = TextDirection::RLTB; @@ -550,7 +550,7 @@ protected static function parseTable($node, $element, &$styles) $attributes = $node->attributes; if ($attributes->getNamedItem('border') !== null && is_object($newElement->getStyle())) { - $border = (int) $attributes->getNamedItem('border')->value; + $border = (int) $attributes->getNamedItem('border')->value; // @phpstan-ignore-line $newElement->getStyle()->setBorderSize((int) Converter::pixelToTwip($border)); $newElement->getStyle()->setBorderStyle(($border === 0) ? 'none' : 'single'); } From d9120c7f3782496d606ec087baa524ced709a51e Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Fri, 16 Aug 2024 08:39:59 -0700 Subject: [PATCH 06/24] Improve Resolution of Phpstan Problem --- src/PhpWord/Shared/Html.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 2d96c375f7..7fa9c6f36f 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -180,7 +180,8 @@ protected static function parseInlineStyle($node, &$styles) $attributes = $node->attributes; // get all the attributes(eg: id, class) $bidi = false; - $direction = isset($attributes['dir']) ? $attributes['dir']->value : ''; // @phpstan-ignore-line + $attrDir = $attributes->getNamedItem('dir'); + $direction = isset($attrDir) ? $attrDir->nodeValue : ''; if ($direction === 'rtl') { $bidi = $styles['bidi'] = $styles['rtl'] = true; $styles['textDirection'] = TextDirection::RLTB; @@ -550,7 +551,7 @@ protected static function parseTable($node, $element, &$styles) $attributes = $node->attributes; if ($attributes->getNamedItem('border') !== null && is_object($newElement->getStyle())) { - $border = (int) $attributes->getNamedItem('border')->value; // @phpstan-ignore-line + $border = (int) $attributes->getNamedItem('border')->nodeValue; $newElement->getStyle()->setBorderSize((int) Converter::pixelToTwip($border)); $newElement->getStyle()->setBorderStyle(($border === 0) ? 'none' : 'single'); } From 1093a3b3ad5295a71cd3d7dbb5ed56d4f5de69c6 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 3 Sep 2024 23:34:34 -0700 Subject: [PATCH 07/24] Update Sample_45_RTLTitles.php --- samples/Sample_45_RTLTitles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Sample_45_RTLTitles.php b/samples/Sample_45_RTLTitles.php index 83dd9b9872..e39510c97f 100644 --- a/samples/Sample_45_RTLTitles.php +++ b/samples/Sample_45_RTLTitles.php @@ -6,7 +6,7 @@ use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Shared\Html as SharedHtml; -// Suggested by issue 2427. +// Suggested by issue #2427. echo date('H:i:s'), ' Create new PhpWord object', EOL; $phpWord = new PhpWord(); Settings::setDefaultRtl(true); From 74918d0909c8224b1a46a4dfbf1494f11e8dd5ed Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Tue, 3 Sep 2024 23:38:32 -0700 Subject: [PATCH 08/24] Update Sample_45_RTLTitles.php --- samples/Sample_45_RTLTitles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Sample_45_RTLTitles.php b/samples/Sample_45_RTLTitles.php index e39510c97f..83dd9b9872 100644 --- a/samples/Sample_45_RTLTitles.php +++ b/samples/Sample_45_RTLTitles.php @@ -6,7 +6,7 @@ use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Shared\Html as SharedHtml; -// Suggested by issue #2427. +// Suggested by issue 2427. echo date('H:i:s'), ' Create new PhpWord object', EOL; $phpWord = new PhpWord(); Settings::setDefaultRtl(true); From 28b1b08ee77462e87b0b69d364f1688d0e0eacd7 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 4 Sep 2024 00:20:17 -0700 Subject: [PATCH 09/24] Update Sample_45_RTLTitles.php --- samples/Sample_45_RTLTitles.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/Sample_45_RTLTitles.php b/samples/Sample_45_RTLTitles.php index 83dd9b9872..e39510c97f 100644 --- a/samples/Sample_45_RTLTitles.php +++ b/samples/Sample_45_RTLTitles.php @@ -6,7 +6,7 @@ use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Shared\Html as SharedHtml; -// Suggested by issue 2427. +// Suggested by issue #2427. echo date('H:i:s'), ' Create new PhpWord object', EOL; $phpWord = new PhpWord(); Settings::setDefaultRtl(true); From 32a73505a08fd5c2136d4c128046e0585c4be5ab Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:50:15 -0800 Subject: [PATCH 10/24] Update HTMLTest.php --- tests/PhpWordTests/Reader/Html/HTMLTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PhpWordTests/Reader/Html/HTMLTest.php b/tests/PhpWordTests/Reader/Html/HTMLTest.php index c0e150e495..9b16662c08 100644 --- a/tests/PhpWordTests/Reader/Html/HTMLTest.php +++ b/tests/PhpWordTests/Reader/Html/HTMLTest.php @@ -1,4 +1,5 @@ Date: Thu, 9 Jan 2025 15:29:38 -0800 Subject: [PATCH 11/24] Move One Test to Avoid Merge Conflict --- tests/PhpWordTests/Reader/{Html => }/HTMLTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename tests/PhpWordTests/Reader/{Html => }/HTMLTest.php (92%) diff --git a/tests/PhpWordTests/Reader/Html/HTMLTest.php b/tests/PhpWordTests/Reader/HTMLTest.php similarity index 92% rename from tests/PhpWordTests/Reader/Html/HTMLTest.php rename to tests/PhpWordTests/Reader/HTMLTest.php index 9b16662c08..7a35a06f78 100644 --- a/tests/PhpWordTests/Reader/Html/HTMLTest.php +++ b/tests/PhpWordTests/Reader/HTMLTest.php @@ -16,7 +16,7 @@ * @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3 */ -namespace PhpOffice\PhpWordTests\Reader\Html; +namespace PhpOffice\PhpWordTests\Reader; use Exception; use PhpOffice\PhpWord\IOFactory; @@ -35,7 +35,7 @@ class HTMLTest extends \PHPUnit\Framework\TestCase */ public function testLoad(): void { - $filename = 'tests/PhpWordTests/_files/documents/reader.html'; + $filename = __DIR__ . '/../_files/documents/reader.html'; $phpWord = IOFactory::load($filename, 'HTML'); self::assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $phpWord); } From e9316c07c277036fb875b0f322e8da55533666ba Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:48:25 -0800 Subject: [PATCH 12/24] Try to Correct Merge Conflicts in Writer/HTML/FontTest --- tests/PhpWordTests/Writer/HTML/FontTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/PhpWordTests/Writer/HTML/FontTest.php b/tests/PhpWordTests/Writer/HTML/FontTest.php index 08a8fca6a4..20ab8d9920 100644 --- a/tests/PhpWordTests/Writer/HTML/FontTest.php +++ b/tests/PhpWordTests/Writer/HTML/FontTest.php @@ -1,4 +1,5 @@ Date: Thu, 9 Jan 2025 16:03:07 -0800 Subject: [PATCH 13/24] Trying FontTest One More Time Before Giving Up --- tests/PhpWordTests/Writer/HTML/FontTest.php | 42 ++++++++++----------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/PhpWordTests/Writer/HTML/FontTest.php b/tests/PhpWordTests/Writer/HTML/FontTest.php index 20ab8d9920..6416a44d2f 100644 --- a/tests/PhpWordTests/Writer/HTML/FontTest.php +++ b/tests/PhpWordTests/Writer/HTML/FontTest.php @@ -87,27 +87,27 @@ public function testFontNames1(): void $style = Helper::getTextContent($xpath, '/html/head/style'); $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); - self::assertSame(1, $prg); + self::assertNotFalse($prg); self::assertEquals('body {font-family: \'Courier New\'; font-size: 12pt;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); - self::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($prg); self::assertEquals('.style5 {font-family: \'MingLiU-ExtB\'; font-size: 10pt; font-weight: bold;}', $matches[0]); } @@ -143,23 +143,23 @@ public function testFontNames2(): void $style = Helper::getTextContent($xpath, '/html/head/style'); $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); - self::assertSame(1, $prg); + self::assertNotFalse($prg); self::assertEquals('body {font-family: \'Courier New\'; font-size: 12pt;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); - self::assertSame(1, $prg); + self::assertNotFalse($prg); self::assertEquals('.style1 {font-family: \'Tahoma\'; font-size: 10pt; color: #1B2232; font-weight: bold;}', $matches[0]); - self::assertNotEmpty($matches); $prg = preg_match('/^[.]style2[^\\r\\n]*/m', $style, $matches); - self::assertSame(1, $prg); + self::assertNotEmpty($matches); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($prg); self::assertEquals('.style4 {font-family: \'Arial\'; font-size: 10pt;}', $matches[0]); } @@ -195,23 +195,23 @@ public function testFontNames3(): void $style = Helper::getTextContent($xpath, '/html/head/style'); $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); - self::assertSame(1, $prg); + self::assertNotFalse($prg); self::assertEquals('body {font-family: \'Courier New\', monospace; font-size: 12pt;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); - self::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($prg); self::assertEquals('.style4 {font-family: \'Arial\'; font-size: 10pt;}', $matches[0]); } @@ -242,19 +242,19 @@ public function testWhiteSpace(): void 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::assertNotEmpty($matches); - self::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($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::assertSame(1, $prg); + self::assertNotFalse($prg); self::assertEquals('.style4 {font-family: \'Courier New\'; font-size: 10pt;}', $matches[0]); } From 8e6726c24a8e6a5db7c3dea0f6f3b17eb1064b5a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 9 Jan 2025 16:55:59 -0800 Subject: [PATCH 14/24] Try to Catch Up --- composer.json | 2 +- src/PhpWord/Shared/Html.php | 4 ++-- src/PhpWord/SimpleType/TextDirection.php | 1 + tests/PhpWordTests/Reader/Html/CharsetTest.php | 1 + tests/PhpWordTests/Reader/Word2007/StyleTableTest.php | 1 + tests/PhpWordTests/SettingsRtlTest.php | 1 + tests/PhpWordTests/Shared/Html2402Test.php | 1 + tests/PhpWordTests/Shared/HtmlFullTest.php | 1 + tests/PhpWordTests/Shared/HtmlHeadingsTest.php | 1 + tests/PhpWordTests/Shared/HtmlRtlTest.php | 5 +++-- tests/PhpWordTests/Shared/HtmlTest.php | 8 ++++---- tests/PhpWordTests/TemplateProcessorSectionTest.php | 1 + tests/PhpWordTests/Writer/RTF/RichTextTitleTest.php | 1 + 13 files changed, 19 insertions(+), 9 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 7c2fcab0e6..81573cd83d 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -417,7 +417,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) { @@ -590,7 +590,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/src/PhpWord/SimpleType/TextDirection.php b/src/PhpWord/SimpleType/TextDirection.php index 0797fa9294..ab1a045337 100644 --- a/src/PhpWord/SimpleType/TextDirection.php +++ b/src/PhpWord/SimpleType/TextDirection.php @@ -1,4 +1,5 @@ addSection([ - 'orientation' => \PhpOffice\PhpWord\Style\Section::ORIENTATION_LANDSCAPE, + 'orientation' => Style\Section::ORIENTATION_LANDSCAPE, ]); // borders & backgrounds are here just for better visual comparison @@ -548,7 +548,7 @@ public function testParseTableRowHeight(): void { $phpWord = new PhpWord(); $section = $phpWord->addSection([ - 'orientation' => \PhpOffice\PhpWord\Style\Section::ORIENTATION_LANDSCAPE, + 'orientation' => Style\Section::ORIENTATION_LANDSCAPE, ]); $html = <<addSection([ - 'orientation' => \PhpOffice\PhpWord\Style\Section::ORIENTATION_LANDSCAPE, + 'orientation' => Style\Section::ORIENTATION_LANDSCAPE, ]); // borders & backgrounds are here just for better visual comparison @@ -670,7 +670,7 @@ public function testParseTableStyleAttributeInlineStyle(): void { $phpWord = new PhpWord(); $section = $phpWord->addSection([ - 'orientation' => \PhpOffice\PhpWord\Style\Section::ORIENTATION_LANDSCAPE, + 'orientation' => Style\Section::ORIENTATION_LANDSCAPE, ]); $html = ' diff --git a/tests/PhpWordTests/TemplateProcessorSectionTest.php b/tests/PhpWordTests/TemplateProcessorSectionTest.php index 0402d4fc66..a6e9d61163 100644 --- a/tests/PhpWordTests/TemplateProcessorSectionTest.php +++ b/tests/PhpWordTests/TemplateProcessorSectionTest.php @@ -1,4 +1,5 @@ Date: Sun, 12 Jan 2025 14:45:02 -0800 Subject: [PATCH 15/24] Experiment with Different Phpunit Xml Dists --- .github/workflows/php.yml | 14 +++++++++++--- phpunit.7.8.xml.dist | 12 ++++++++++++ phpunit10.xml.dist | 20 ++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 phpunit.7.8.xml.dist create mode 100644 phpunit10.xml.dist diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 0e2e9ea96d..b403e57d8a 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -83,11 +83,19 @@ jobs: - name: Composer Install run: composer install --ansi --prefer-dist --no-interaction --no-progress - - name: Run phpunit - if: matrix.php != '7.3' + - name: Run phpunit 7.1 7.2 + if: matrix.php == '7.1' || matrix.php == '7.2' + run: ./vendor/bin/phpunit -c phpunit.7.8.xml.dist --no-coverage + + - name: Run phpunit 7.4 8.0 + if: matrix.php == '7.4' || matrix.php == '8.0' run: ./vendor/bin/phpunit -c phpunit.xml.dist --no-coverage - - name: Run phpunit + - name: Run phpunit 8.1 8.2 8.3 8.4 + if: matrix.php == '8.1' || matrix.php == '8.2' || matrix.php == '8.3' || matrix.php == '8.4' + run: ./vendor/bin/phpunit -c phpunit10.xml.dist --no-coverage + + - name: Run phpunit 7.3 if: matrix.php == '7.3' run: ./vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover build/clover.xml diff --git a/phpunit.7.8.xml.dist b/phpunit.7.8.xml.dist new file mode 100644 index 0000000000..a8327fb286 --- /dev/null +++ b/phpunit.7.8.xml.dist @@ -0,0 +1,12 @@ + + + + + + + + ./tests/PhpWordTests + + + + diff --git a/phpunit10.xml.dist b/phpunit10.xml.dist new file mode 100644 index 0000000000..b97fbee3fb --- /dev/null +++ b/phpunit10.xml.dist @@ -0,0 +1,20 @@ + + + + + + + + ./tests/PhpWordTests + + + + + + ./src + + + ./src/PhpWord/Shared/PCLZip + + + From 737cb3708a9be2bd0f13c42b15ca8d635baa2e7b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 12 Jan 2025 14:53:38 -0800 Subject: [PATCH 16/24] Remove Logging from 7.8 Config --- phpunit.7.8.xml.dist | 1 - 1 file changed, 1 deletion(-) diff --git a/phpunit.7.8.xml.dist b/phpunit.7.8.xml.dist index a8327fb286..22e77a0ea6 100644 --- a/phpunit.7.8.xml.dist +++ b/phpunit.7.8.xml.dist @@ -8,5 +8,4 @@ ./tests/PhpWordTests - From 508464223d3b5dae4e9524fd2ab3768248b2b62f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:13:50 -0800 Subject: [PATCH 17/24] More Phpunit Tweaking --- .github/workflows/php.yml | 4 ++-- phpunit.xml.dist | 22 +++++++++------------- phpunit9.xml.dist | 24 ++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 15 deletions(-) create mode 100644 phpunit9.xml.dist diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index b403e57d8a..016120d913 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -89,7 +89,7 @@ jobs: - name: Run phpunit 7.4 8.0 if: matrix.php == '7.4' || matrix.php == '8.0' - run: ./vendor/bin/phpunit -c phpunit.xml.dist --no-coverage + run: ./vendor/bin/phpunit -c phpunit9.xml.dist --no-coverage - name: Run phpunit 8.1 8.2 8.3 8.4 if: matrix.php == '8.1' || matrix.php == '8.2' || matrix.php == '8.3' || matrix.php == '8.4' @@ -97,7 +97,7 @@ jobs: - name: Run phpunit 7.3 if: matrix.php == '7.3' - run: ./vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover build/clover.xml + run: ./vendor/bin/phpunit -c phpunit9.xml.dist --coverage-clover build/clover.xml - name: Upload coverage results to Coveralls if: matrix.php == '7.3' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ff0c676fad..b97fbee3fb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,17 +1,5 @@ - - - - ./src - - - ./src/PhpWord/Shared/PCLZip - - - + @@ -21,4 +9,12 @@ + + + ./src + + + ./src/PhpWord/Shared/PCLZip + + diff --git a/phpunit9.xml.dist b/phpunit9.xml.dist new file mode 100644 index 0000000000..ff0c676fad --- /dev/null +++ b/phpunit9.xml.dist @@ -0,0 +1,24 @@ + + + + + ./src + + + ./src/PhpWord/Shared/PCLZip + + + + + + + + + ./tests/PhpWordTests + + + + From d1a16fef6c785311810115213f68d4a04eaf4e3e Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:43:15 -0800 Subject: [PATCH 18/24] Update HtmlTest.php --- tests/PhpWordTests/Shared/HtmlTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PhpWordTests/Shared/HtmlTest.php b/tests/PhpWordTests/Shared/HtmlTest.php index cf4d627c92..bc0899a8ac 100644 --- a/tests/PhpWordTests/Shared/HtmlTest.php +++ b/tests/PhpWordTests/Shared/HtmlTest.php @@ -28,9 +28,9 @@ use PhpOffice\PhpWord\Shared\Html; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\LineSpacingRule; +use PhpOffice\PhpWord\SimpleType\TblWidth; use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style\Font; -use PhpOffice\PhpWord\SimpleType\TblWidth; use PhpOffice\PhpWord\Style\Paragraph; use PhpOffice\PhpWordTests\AbstractWebServerEmbedded; use PhpOffice\PhpWordTests\TestHelperDOCX; From f82eb47f8f6327e8883a6d921ac3b73017d6b78b Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:00:10 -0800 Subject: [PATCH 19/24] Eliminate JSON Dependency JSON is used in exactly 2 places in the code and should not be used in either. XMLWriter uses it to cast a float to string. Aside from being silly, this actually causes a problem in Shared/HtmlTest, where one of the results should be 4000 but is instead tested for 3999.9999... The error should have been immaterial (a simple cast will give the correct answer), but the test was wrong, insisting on an exact match for a floating point answer. It should use assertEqualsWithDelta. Next, the reason why JSON got it "wrong" was because the conversions in Shared/Converter use the wrong order of operations - multiplications should be performed before divisions to help avoid rounding problems, but Converter was doing the divisions first. All the conversions there are changed to multiply before dividing. Finally, Shared/Html checks for width units of points or pixels, but should check for inches and centimers as well. Writer/Html outputs tracking properties as a JSON string for some unfathomable reason. It is changed to output each of the three (author, id, and date) as its own discrete property. Tests didn't actually confirm the value of the properties; they do now. Oh, yes, htmlspecialchars is needed for author and id, otherwise the html may wind up broken. Some simple changes are made to TestHelperDOCX to avoid intermittent problems on Windows, and to make the code a little cleaner. --- .gitignore | 3 +- composer.json | 1 - src/PhpWord/Shared/Converter.php | 24 +++++++-------- src/PhpWord/Shared/Html.php | 6 ++++ src/PhpWord/Shared/XMLWriter.php | 2 +- src/PhpWord/Writer/HTML/Element/Text.php | 24 ++++++++------- tests/PhpWordTests/Shared/HtmlTest.php | 7 +++-- tests/PhpWordTests/TestHelperDOCX.php | 24 +++++++++++---- .../PhpWordTests/Writer/HTML/ElementTest.php | 30 ++++++++++++++++++- 9 files changed, 86 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index 0b9d0608d0..3ebe0dc1a8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ phpword.ini /nbproject /.php_cs.cache /.phpunit.result.cache -/public \ No newline at end of file +/.phpunit.cache +/public diff --git a/composer.json b/composer.json index 2d06f890b3..9487640f2a 100644 --- a/composer.json +++ b/composer.json @@ -109,7 +109,6 @@ "php": "^7.1|^8.0", "ext-dom": "*", "ext-gd": "*", - "ext-json": "*", "ext-xml": "*", "ext-zip": "*", "phpoffice/math": "^0.2" diff --git a/src/PhpWord/Shared/Converter.php b/src/PhpWord/Shared/Converter.php index 17d2e1a05d..82e11c0790 100644 --- a/src/PhpWord/Shared/Converter.php +++ b/src/PhpWord/Shared/Converter.php @@ -40,7 +40,7 @@ class Converter */ public static function cmToTwip($centimeter = 1) { - return $centimeter / self::INCH_TO_CM * self::INCH_TO_TWIP; + return $centimeter * self::INCH_TO_TWIP / self::INCH_TO_CM; } /** @@ -64,7 +64,7 @@ public static function cmToInch($centimeter = 1) */ public static function cmToPixel($centimeter = 1) { - return $centimeter / self::INCH_TO_CM * self::INCH_TO_PIXEL; + return $centimeter * self::INCH_TO_PIXEL / self::INCH_TO_CM; } /** @@ -76,7 +76,7 @@ public static function cmToPixel($centimeter = 1) */ public static function cmToPoint($centimeter = 1) { - return $centimeter / self::INCH_TO_CM * self::INCH_TO_POINT; + return $centimeter * self::INCH_TO_POINT / self::INCH_TO_CM; } /** @@ -88,7 +88,7 @@ public static function cmToPoint($centimeter = 1) */ public static function cmToEmu($centimeter = 1) { - return round($centimeter / self::INCH_TO_CM * self::INCH_TO_PIXEL * self::PIXEL_TO_EMU); + return round($centimeter * self::INCH_TO_PIXEL * self::PIXEL_TO_EMU / self::INCH_TO_CM); } /** @@ -160,7 +160,7 @@ public static function inchToEmu($inch = 1) */ public static function pixelToTwip($pixel = 1) { - return $pixel / self::INCH_TO_PIXEL * self::INCH_TO_TWIP; + return $pixel * self::INCH_TO_TWIP / self::INCH_TO_PIXEL; } /** @@ -172,7 +172,7 @@ public static function pixelToTwip($pixel = 1) */ public static function pixelToCm($pixel = 1) { - return $pixel / self::INCH_TO_PIXEL * self::INCH_TO_CM; + return $pixel * self::INCH_TO_CM / self::INCH_TO_PIXEL; } /** @@ -184,7 +184,7 @@ public static function pixelToCm($pixel = 1) */ public static function pixelToPoint($pixel = 1) { - return $pixel / self::INCH_TO_PIXEL * self::INCH_TO_POINT; + return $pixel * self::INCH_TO_POINT / self::INCH_TO_PIXEL; } /** @@ -208,7 +208,7 @@ public static function pixelToEmu($pixel = 1) */ public static function pointToTwip($point = 1) { - return $point / self::INCH_TO_POINT * self::INCH_TO_TWIP; + return $point * self::INCH_TO_TWIP / self::INCH_TO_POINT; } /** @@ -220,7 +220,7 @@ public static function pointToTwip($point = 1) */ public static function pointToPixel($point = 1) { - return $point / self::INCH_TO_POINT * self::INCH_TO_PIXEL; + return $point * self::INCH_TO_PIXEL / self::INCH_TO_POINT; } /** @@ -232,7 +232,7 @@ public static function pointToPixel($point = 1) */ public static function pointToEmu($point = 1) { - return round($point / self::INCH_TO_POINT * self::INCH_TO_PIXEL * self::PIXEL_TO_EMU); + return round($point * self::INCH_TO_PIXEL * self::PIXEL_TO_EMU / self::INCH_TO_POINT); } /** @@ -244,7 +244,7 @@ public static function pointToEmu($point = 1) */ public static function pointToCm($point = 1) { - return $point / self::INCH_TO_POINT * self::INCH_TO_CM; + return $point * self::INCH_TO_CM / self::INCH_TO_POINT; } /** @@ -268,7 +268,7 @@ public static function emuToPixel($emu = 1) */ public static function picaToPoint($pica = 1) { - return $pica / self::INCH_TO_PICA * self::INCH_TO_POINT; + return $pica * self::INCH_TO_POINT / self::INCH_TO_PICA; } /** diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 9b57f77adc..d1bf13d7f7 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -1477,6 +1477,12 @@ protected static function convertHtmlSize(string $size): float if (false !== strpos($size, 'px')) { return (float) str_replace('px', '', $size); } + if (false !== strpos($size, 'cm')) { + return Converter::cmToPixel((float) str_replace('cm', '', $size)); + } + if (false !== strpos($size, 'in')) { + return Converter::inchToPixel((float) str_replace('in', '', $size)); + } return (float) $size; } diff --git a/src/PhpWord/Shared/XMLWriter.php b/src/PhpWord/Shared/XMLWriter.php index 8dc28e1184..8b267b694c 100644 --- a/src/PhpWord/Shared/XMLWriter.php +++ b/src/PhpWord/Shared/XMLWriter.php @@ -180,7 +180,7 @@ public function writeAttributeIf($condition, $attribute, $value): void public function writeAttribute($name, $value) { if (is_float($value)) { - $value = json_encode($value); + $value = (string) $value; } return parent::writeAttribute($name, $value ?? ''); diff --git a/src/PhpWord/Writer/HTML/Element/Text.php b/src/PhpWord/Writer/HTML/Element/Text.php index 312d3a19c2..c6363d1da4 100644 --- a/src/PhpWord/Writer/HTML/Element/Text.php +++ b/src/PhpWord/Writer/HTML/Element/Text.php @@ -163,20 +163,22 @@ private function writeTrackChangeOpening() $content = ''; if (($changed->getChangeType() == TrackChange::INSERTED)) { - $content .= 'getChangeType() == TrackChange::DELETED) { - $content .= ' ['author' => $changed->getAuthor(), 'id' => $this->element->getElementId()]]; - if ($changed->getDate() != null) { - $changedProp['changed']['date'] = $changed->getDate()->format('Y-m-d\TH:i:s\Z'); + $author = htmlspecialchars($changed->getAuthor(), ENT_QUOTES); + $content .= " data-phpword-chg-author='$author'"; + $elementId = htmlspecialchars($this->element->getElementId(), ENT_QUOTES); + $content .= " data-phpword-chg-id='$elementId'"; + $date = $changed->getDate(); + if ($date !== null) { + $dateout = $date->format('Y-m-d\TH:i:s\Z'); + $content .= " data-phpword-chg-timestamp='$dateout'"; } - $content .= json_encode($changedProp); - $content .= '\' '; - $content .= 'title="' . $changed->getAuthor(); - if ($changed->getDate() != null) { - $dateUser = $changed->getDate()->format('Y-m-d H:i:s'); + $content .= ' title="' . $author; + if ($date !== null) { + $dateUser = $date->format('Y-m-d H:i:s'); $content .= ' - ' . $dateUser; } $content .= '">'; diff --git a/tests/PhpWordTests/Shared/HtmlTest.php b/tests/PhpWordTests/Shared/HtmlTest.php index 142a93e5a5..5131d6ea44 100644 --- a/tests/PhpWordTests/Shared/HtmlTest.php +++ b/tests/PhpWordTests/Shared/HtmlTest.php @@ -256,7 +256,8 @@ public function testParseWidth(string $htmlSize, float $docxSize, string $docxUn $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); $xpath = '/w:document/w:body/w:tbl/w:tblPr/w:tblW'; self::assertTrue($doc->elementExists($xpath)); - self::assertEquals($docxSize, $doc->getElement($xpath)->getAttribute('w:w')); + $actual = (float) $doc->getElement($xpath)->getAttribute('w:w'); + self::assertEqualsWithDelta($docxSize, $actual, 1.0e-12); self::assertEquals($docxUnit, $doc->getElement($xpath)->getAttribute('w:type')); } @@ -1371,9 +1372,11 @@ public static function providerParseWidth(): array return [ ['auto', 5000, TblWidth::PERCENT], ['100%', 5000, TblWidth::PERCENT], - ['200pt', 3999.999999999999, TblWidth::TWIP], + ['200pt', 4000, TblWidth::TWIP], ['300px', 4500, TblWidth::TWIP], ['400', 6000, TblWidth::TWIP], + ['2in', 2880, TblWidth::TWIP], + ['2.54cm', 1440, TblWidth::TWIP], ]; } diff --git a/tests/PhpWordTests/TestHelperDOCX.php b/tests/PhpWordTests/TestHelperDOCX.php index 2a6fbabae0..5ebe799cfc 100644 --- a/tests/PhpWordTests/TestHelperDOCX.php +++ b/tests/PhpWordTests/TestHelperDOCX.php @@ -47,13 +47,14 @@ class TestHelperDOCX */ public static function getDocument(PhpWord $phpWord, $writerName = 'Word2007') { + $tempdir = self::getTempDirPhpunit(); self::$file = tempnam(Settings::getTempDir(), 'PhpWord'); if (false === self::$file) { throw new CreateTemporaryFileException(); } - if (!is_dir(Settings::getTempDir() . '/PhpWord_Unit_Test/')) { - mkdir(Settings::getTempDir() . '/PhpWord_Unit_Test/'); + if (!is_dir($tempdir)) { + mkdir($tempdir); } $xmlWriter = IOFactory::createWriter($phpWord, $writerName); @@ -62,11 +63,11 @@ public static function getDocument(PhpWord $phpWord, $writerName = 'Word2007') $zip = new ZipArchive(); $res = $zip->open(self::$file); if (true === $res) { - $zip->extractTo(Settings::getTempDir() . '/PhpWord_Unit_Test/'); + $zip->extractTo($tempdir); $zip->close(); } - $doc = new XmlDocument(Settings::getTempDir() . '/PhpWord_Unit_Test/'); + $doc = new XmlDocument($tempdir); if ($writerName === 'ODText') { $doc->setDefaultFile('content.xml'); } @@ -83,8 +84,9 @@ public static function clear(): void unlink(self::$file); self::$file = ''; } - if (is_dir(Settings::getTempDir() . '/PhpWord_Unit_Test/')) { - self::deleteDir(Settings::getTempDir() . '/PhpWord_Unit_Test/'); + $tempdir = self::getTempDirPhpunit(); + if (is_dir($tempdir)) { + self::deleteDir($tempdir); } } @@ -117,4 +119,14 @@ public static function getFile() { return self::$file; } + + /** + * Get temporary directory for PhpUnit. + * + * @return string + */ + private static function getTempDirPhpunit() + { + return Settings::getTempDir() . '/PhpWord_Unit_Test'; + } } diff --git a/tests/PhpWordTests/Writer/HTML/ElementTest.php b/tests/PhpWordTests/Writer/HTML/ElementTest.php index 3b2580381f..28e785a237 100644 --- a/tests/PhpWordTests/Writer/HTML/ElementTest.php +++ b/tests/PhpWordTests/Writer/HTML/ElementTest.php @@ -72,13 +72,41 @@ public function testWriteTrackChanges(): void $text = $section->addText('my dummy text'); $text->setChangeInfo(TrackChange::INSERTED, 'author name'); $text2 = $section->addText('my other text'); - $text2->setTrackChange(new TrackChange(TrackChange::DELETED, 'another author', new DateTime())); + $deleteTime = new DateTime(); + $deleteAuthor = "Spec O'char"; + $deleteTrack = new TrackChange(TrackChange::DELETED, $deleteAuthor, $deleteTime); + $text2->setTrackChange($deleteTrack); $dom = Helper::getAsHTML($phpWord); $xpath = new DOMXPath($dom); self::assertEquals(1, $xpath->query('/html/body/div/p[1]/ins')->length); self::assertEquals(1, $xpath->query('/html/body/div/p[2]/del')->length); + $node = $xpath->query('/html/body/div/p[2]/del'); + self::assertNotFalse($node); + $allAttributes = $node[0]->attributes; + self::assertCount(4, $allAttributes); + $node = $xpath->query('/html/body/div/p[2]/del'); + self::assertNotFalse($node); + + $attributes = $node[0]->attributes[0]; + self::assertSame('data-phpword-chg-author', $attributes->name); + self::assertSame($deleteAuthor, $attributes->value); + + $text2Id = $text2->getElementId(); + $attributes = $node[0]->attributes[1]; + self::assertSame('data-phpword-chg-id', $attributes->name); + self::assertSame($text2Id, $attributes->value); + + $attributes = $node[0]->attributes[2]; + self::assertSame('data-phpword-chg-timestamp', $attributes->name); + self::assertSame($deleteTime->format('Y-m-d\TH:i:s\Z'), $attributes->value); + + $attributes = $node[0]->attributes[3]; + self::assertSame('title', $attributes->name); + $expected = $deleteAuthor . ' - ' + . $deleteTime->format('Y-m-d H:i:s'); + self::assertSame($expected, $attributes->value); } /** From 3ace7706180af59c7b10b3b0bab5a8615849c92a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 30 Jan 2025 21:20:49 -0800 Subject: [PATCH 20/24] Improve Test Coverage --- src/PhpWord/Shared/Html.php | 10 +- src/PhpWord/Shared/HtmlColours.php | 5 - tests/PhpWordTests/Shared/Html2Test.php | 140 ++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 tests/PhpWordTests/Shared/Html2Test.php diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index d1bf13d7f7..26bc83e3bb 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -36,6 +36,7 @@ use PhpOffice\PhpWord\SimpleType\NumberFormat; use PhpOffice\PhpWord\SimpleType\TextDirection; use PhpOffice\PhpWord\Style\Paragraph; +use Throwable; /** * Common Html functions. @@ -114,20 +115,20 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit $dom->preserveWhiteSpace = $preserveWhiteSpace; try { - $result = $dom->loadHTML($html, LIBXML_NOWARNING | LIBXML_NOERROR); + $result = @$dom->loadHTML($html); $exceptionMessage = 'DOM loadHTML failed'; - } catch (Exception $e) { + } catch (Throwable $e) { $result = false; $exceptionMessage = $e->getMessage(); } if ($result === false) { - throw new Exception($exceptionMessage); // @codeCoverageIgnore + throw new Exception($exceptionMessage); } self::removeAnnoyingWhitespaceTextNodes($dom); static::$xpath = new DOMXPath($dom); $node = $dom->getElementsByTagName('html'); if (count($node) === 0 || $node->item(0) === null) { - $node = $dom->getElementsByTagName('body'); + $node = $dom->getElementsByTagName('body'); // @codeCoverageIgnore } static::parseNode($node->item(0), $element); @@ -1319,7 +1320,6 @@ protected static function mapListType($cssListType) return NumberFormat::LOWER_ROMAN; // i, ii, iii, iv, .. case 'I': return NumberFormat::UPPER_ROMAN; // I, II, III, IV, .. - case '1': default: return NumberFormat::DECIMAL; // 1, 2, 3, .. } diff --git a/src/PhpWord/Shared/HtmlColours.php b/src/PhpWord/Shared/HtmlColours.php index 40bc0096c6..c50acfd5da 100644 --- a/src/PhpWord/Shared/HtmlColours.php +++ b/src/PhpWord/Shared/HtmlColours.php @@ -524,11 +524,6 @@ class HtmlColours 'yellowgreen' => '9acd32', ]; - public static function colourNameLookup(string $colorName): string - { - return self::COLOUR_MAP[$colorName] ?? ''; - } - public static function convertColour(string $colorName): string { $colorName = trim($colorName); diff --git a/tests/PhpWordTests/Shared/Html2Test.php b/tests/PhpWordTests/Shared/Html2Test.php new file mode 100644 index 0000000000..95866d4b07 --- /dev/null +++ b/tests/PhpWordTests/Shared/Html2Test.php @@ -0,0 +1,140 @@ +expectException(Exception::class); + $this->expectExceptionMessage('loadHTML'); + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, ''); + } + + public function testCssOnIdElement(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $html = '' + . '' + . 'Id Test' + . '' + . '' + . '

test1.

' + . ''; + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord); + $marginPath = '/w:document/w:body/w:p/w:pPr/w:spacing'; + self::assertSame('150', $doc->getElement($marginPath)->getAttribute('w:before')); + self::assertSame('150', $doc->getElement($marginPath)->getAttribute('w:after')); + $path = '/w:document/w:body/w:p/w:r'; + self::assertSame('test1.', $doc->getElement($path)->nodeValue); + $boldPath = $path . '/w:rPr/w:b'; + self::assertSame('1', $doc->getElement($boldPath)->getAttribute('w:val')); + } + + public function testListTypes(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $html = '
  1. Decimal number first
  2. second
' + . '
  1. Lowercase first
  2. second
' + . '
  1. Uppercase first
  2. second
' + . '
  1. Lower roman first
  2. second
' + . '
  1. Upper roman first
  2. second
'; + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord); + + $item = 1; + $expected = '1'; + $path = "/w:document/w:body/w:p[$item]"; + self::assertSame('Decimal number first', $doc->getElement("$path/w:r")->nodeValue); + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + ++$item; + $path = "/w:document/w:body/w:p[$item]"; + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + + ++$item; + $expected = '2'; + $path = "/w:document/w:body/w:p[$item]"; + self::assertSame('Lowercase first', $doc->getElement("$path/w:r")->nodeValue); + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + ++$item; + $path = "/w:document/w:body/w:p[$item]"; + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + + ++$item; + $expected = '3'; + $path = "/w:document/w:body/w:p[$item]"; + self::assertSame('Uppercase first', $doc->getElement("$path/w:r")->nodeValue); + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + ++$item; + $path = "/w:document/w:body/w:p[$item]"; + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + + ++$item; + $expected = '4'; + $path = "/w:document/w:body/w:p[$item]"; + self::assertSame('Lower roman first', $doc->getElement("$path/w:r")->nodeValue); + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + ++$item; + $path = "/w:document/w:body/w:p[$item]"; + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + + ++$item; + $expected = '5'; + $path = "/w:document/w:body/w:p[$item]"; + self::assertSame('Upper roman first', $doc->getElement("$path/w:r")->nodeValue); + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + ++$item; + $path = "/w:document/w:body/w:p[$item]"; + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + } +} From b1720b95ddac49ee56d9a2b7cde3a736ced484a9 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 30 Jan 2025 22:18:41 -0800 Subject: [PATCH 21/24] A Bit More Coverage --- tests/PhpWordTests/Shared/Html2Test.php | 64 +++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/PhpWordTests/Shared/Html2Test.php b/tests/PhpWordTests/Shared/Html2Test.php index 95866d4b07..64a71932dd 100644 --- a/tests/PhpWordTests/Shared/Html2Test.php +++ b/tests/PhpWordTests/Shared/Html2Test.php @@ -137,4 +137,68 @@ public function testListTypes(): void $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); } + + public function testPadding(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $html = '
' + . '' + . '' + . '' + . '' + . '' + . '' + . '
2020 30
20 30 4020 30 40 50
'; + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord); + + $item = 1; + $td = 1; + $path = "/w:document/w:body/w:tbl/w:tr[$item]/w:tc[$td]"; + self::assertSame('20', $doc->getElement("$path/w:p/w:r")->nodeValue); + $tcMarPath = $path . '/w:tcPr/w:tcMar'; + self::assertSame('300', $doc->getElement($tcMarPath . '/w:top')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:start')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:bottom')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:end')->getAttribute('w:w')); + + ++$td; + $path = "/w:document/w:body/w:tbl/w:tr[$item]/w:tc[$td]"; + self::assertSame('20 30', $doc->getElement("$path/w:p/w:r")->nodeValue); + $tcMarPath = $path . '/w:tcPr/w:tcMar'; + self::assertSame('300', $doc->getElement($tcMarPath . '/w:top')->getAttribute('w:w')); + self::assertSame('450', $doc->getElement($tcMarPath . '/w:start')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:bottom')->getAttribute('w:w')); + self::assertSame('450', $doc->getElement($tcMarPath . '/w:end')->getAttribute('w:w')); + + $item = 1; + $td = 1; + $path = "/w:document/w:body/w:tbl/w:tr[$item]/w:tc[$td]"; + self::assertSame('20', $doc->getElement("$path/w:p/w:r")->nodeValue); + $tcMarPath = $path . '/w:tcPr/w:tcMar'; + self::assertSame('300', $doc->getElement($tcMarPath . '/w:top')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:start')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:bottom')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:end')->getAttribute('w:w')); + + ++$item; + $td = 1; + $path = "/w:document/w:body/w:tbl/w:tr[$item]/w:tc[$td]"; + self::assertSame('20 30 40', $doc->getElement("$path/w:p/w:r")->nodeValue); + $tcMarPath = $path . '/w:tcPr/w:tcMar'; + self::assertSame('300', $doc->getElement($tcMarPath . '/w:top')->getAttribute('w:w')); + self::assertSame('450', $doc->getElement($tcMarPath . '/w:start')->getAttribute('w:w')); + self::assertSame('600', $doc->getElement($tcMarPath . '/w:bottom')->getAttribute('w:w')); + self::assertSame('450', $doc->getElement($tcMarPath . '/w:end')->getAttribute('w:w')); + + ++$td; + $path = "/w:document/w:body/w:tbl/w:tr[$item]/w:tc[$td]"; + self::assertSame('20 30 40 50', $doc->getElement("$path/w:p/w:r")->nodeValue); + $tcMarPath = $path . '/w:tcPr/w:tcMar'; + self::assertSame('300', $doc->getElement($tcMarPath . '/w:top')->getAttribute('w:w')); + self::assertSame('750', $doc->getElement($tcMarPath . '/w:start')->getAttribute('w:w')); + self::assertSame('600', $doc->getElement($tcMarPath . '/w:bottom')->getAttribute('w:w')); + self::assertSame('450', $doc->getElement($tcMarPath . '/w:end')->getAttribute('w:w')); + } } From 16904a2a0cea68176db99dbcc60c52e804b8212a Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 5 Feb 2025 22:24:41 -0800 Subject: [PATCH 22/24] 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 c574bd969d..9cc3abe5b7 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -470,10 +470,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}" . PHP_EOL; diff --git a/src/PhpWord/Writer/HTML/Part/Head.php b/src/PhpWord/Writer/HTML/Part/Head.php index 0f295e58c3..f8af52ccb1 100644 --- a/src/PhpWord/Writer/HTML/Part/Head.php +++ b/src/PhpWord/Writer/HTML/Part/Head.php @@ -137,6 +137,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 d50a5176d7..50f1e699a6 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\Settings; use PhpOffice\PhpWord\Shared\Converter; @@ -134,6 +135,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()); @@ -155,6 +158,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 92a4c5e1e45eb11ce5880671fd9a508a4bb2685f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Thu, 6 Feb 2025 00:25:14 -0800 Subject: [PATCH 23/24] Html Writer Duplicate Header Styles in Style Tags Nominally redundant, but makes things easier for Html Reader. --- src/PhpWord/Writer/HTML/Element/Title.php | 19 +++++++++++++++---- .../PhpWordTests/Shared/HtmlHeadingsTest.php | 16 +++++++++++++--- tests/PhpWordTests/Writer/HTML/PartTest.php | 6 +++--- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/PhpWord/Writer/HTML/Element/Title.php b/src/PhpWord/Writer/HTML/Element/Title.php index e759bfdcd6..43806a648c 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; /** @@ -51,14 +53,23 @@ public function write() $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); - $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}" . 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); } } From 60dbd8cd26e1abcb966c5977a155dc6ff591e62f Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 12 Feb 2025 13:11:27 -0800 Subject: [PATCH 24/24] Keep Up With 2533 --- src/PhpWord/Writer/HTML/Element/Title.php | 4 ++-- src/PhpWord/Writer/HTML/Part/Head.php | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PhpWord/Writer/HTML/Element/Title.php b/src/PhpWord/Writer/HTML/Element/Title.php index 43806a648c..f3aeb395f4 100644 --- a/src/PhpWord/Writer/HTML/Element/Title.php +++ b/src/PhpWord/Writer/HTML/Element/Title.php @@ -18,7 +18,7 @@ namespace PhpOffice\PhpWord\Writer\HTML\Element; -use PhpOffice\PhpWord\Element\Title as PhpWordTitle; +use PhpOffice\PhpWord\Element\Title as ElementTitle; use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Writer\HTML; use PhpOffice\PhpWord\Writer\HTML\Style\Font; @@ -38,7 +38,7 @@ class Title extends AbstractElement */ public function write() { - if (!$this->element instanceof PhpWordTitle) { + if (!$this->element instanceof ElementTitle) { return ''; } diff --git a/src/PhpWord/Writer/HTML/Part/Head.php b/src/PhpWord/Writer/HTML/Part/Head.php index f8af52ccb1..4da526075d 100644 --- a/src/PhpWord/Writer/HTML/Part/Head.php +++ b/src/PhpWord/Writer/HTML/Part/Head.php @@ -101,7 +101,6 @@ private function writeStyles(): string foreach ([ 'body' => $bodyarray, - //'*' => $astarray, 'a.NoteRef' => [ 'text-decoration' => 'none', ],