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 1/9] 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 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 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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);