diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 0e2e9ea96d..016120d913 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -83,13 +83,21 @@ jobs: - name: Composer Install run: composer install --ansi --prefer-dist --no-interaction --no-progress - - name: Run phpunit - if: matrix.php != '7.3' - run: ./vendor/bin/phpunit -c phpunit.xml.dist --no-coverage + - name: Run phpunit 7.1 7.2 + if: matrix.php == '7.1' || matrix.php == '7.2' + run: ./vendor/bin/phpunit -c phpunit.7.8.xml.dist --no-coverage - - name: Run phpunit + - name: Run phpunit 7.4 8.0 + if: matrix.php == '7.4' || matrix.php == '8.0' + run: ./vendor/bin/phpunit -c phpunit9.xml.dist --no-coverage + + - name: Run phpunit 8.1 8.2 8.3 8.4 + if: matrix.php == '8.1' || matrix.php == '8.2' || matrix.php == '8.3' || matrix.php == '8.4' + run: ./vendor/bin/phpunit -c phpunit10.xml.dist --no-coverage + + - name: Run phpunit 7.3 if: matrix.php == '7.3' - run: ./vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover build/clover.xml + run: ./vendor/bin/phpunit -c phpunit9.xml.dist --coverage-clover build/clover.xml - name: Upload coverage results to Coveralls if: matrix.php == '7.3' diff --git a/.gitignore b/.gitignore index 6918df72e6..f7a641d0a9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ phpword.ini /nbproject /.php_cs.cache /.phpunit.result.cache -/public \ No newline at end of file +/.phpunit.cache +/public diff --git a/composer.json b/composer.json index 5055105d16..20be8557d9 100644 --- a/composer.json +++ b/composer.json @@ -108,9 +108,8 @@ "require": { "php": "^7.1|^8.0", "ext-dom": "*", - "ext-gd": "*", + "ext-gd": "*", "ext-zip": "*", - "ext-json": "*", "ext-xml": "*", "phpoffice/math": "^0.2" }, @@ -122,7 +121,7 @@ "phpmd/phpmd": "^2.13", "phpstan/phpstan": "^0.12.88 || ^1.0.0", "phpstan/phpstan-phpunit": "^1.0 || ^2.0", - "phpunit/phpunit": ">=7.0", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0 || ^10.0", "symfony/process": "^4.4 || ^5.0", "tecnickcom/tcpdf": "^6.5" }, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 86a542769a..5e1f9cd2c2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -380,11 +380,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/phpunit.7.8.xml.dist b/phpunit.7.8.xml.dist new file mode 100644 index 0000000000..22e77a0ea6 --- /dev/null +++ b/phpunit.7.8.xml.dist @@ -0,0 +1,11 @@ + + + + + + + + ./tests/PhpWordTests + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6f1f5445ab..b97fbee3fb 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,17 +1,5 @@ - - - - ./src - - - ./src/PhpWord/Shared/PCLZip - - - - - - + @@ -21,4 +9,12 @@ + + + ./src + + + ./src/PhpWord/Shared/PCLZip + + diff --git a/phpunit10.xml.dist b/phpunit10.xml.dist new file mode 100644 index 0000000000..b97fbee3fb --- /dev/null +++ b/phpunit10.xml.dist @@ -0,0 +1,20 @@ + + + + + + + + ./tests/PhpWordTests + + + + + + ./src + + + ./src/PhpWord/Shared/PCLZip + + + diff --git a/phpunit9.xml.dist b/phpunit9.xml.dist new file mode 100644 index 0000000000..ff0c676fad --- /dev/null +++ b/phpunit9.xml.dist @@ -0,0 +1,24 @@ + + + + + ./src + + + ./src/PhpWord/Shared/PCLZip + + + + + + + + + ./tests/PhpWordTests + + + + diff --git a/samples/Sample_45_RTLTitles.php b/samples/Sample_45_RTLTitles.php new file mode 100644 index 0000000000..e39510c97f --- /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 c3200fe857..11e678a1ff 100644 --- a/src/PhpWord/PhpWord.php +++ b/src/PhpWord/PhpWord.php @@ -21,6 +21,7 @@ use BadMethodCallException; use PhpOffice\PhpWord\Element\Section; use PhpOffice\PhpWord\Exception\Exception; +use PhpOffice\PhpWord\Style\Font; /** * PHPWord main class. @@ -318,9 +319,9 @@ public function setDefaultFontSize($fontSize): void * * @return 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 9d49573d69..3fe94f6c9d 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -793,35 +793,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 d0777c3026..81aaf203c1 100644 --- a/src/PhpWord/Reader/Word2007/Styles.php +++ b/src/PhpWord/Reader/Word2007/Styles.php @@ -69,8 +69,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); @@ -102,7 +103,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 16f49166fa..c4b7c38dfc 100644 --- a/src/PhpWord/Settings.php +++ b/src/PhpWord/Settings.php @@ -16,6 +16,8 @@ namespace PhpOffice\PhpWord; +use PhpOffice\PhpWord\SimpleType\TextDirection; + /** * PHPWord settings class. * @@ -453,6 +455,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/Converter.php b/src/PhpWord/Shared/Converter.php index 17d2e1a05d..82e11c0790 100644 --- a/src/PhpWord/Shared/Converter.php +++ b/src/PhpWord/Shared/Converter.php @@ -40,7 +40,7 @@ class Converter */ public static function cmToTwip($centimeter = 1) { - return $centimeter / self::INCH_TO_CM * self::INCH_TO_TWIP; + return $centimeter * self::INCH_TO_TWIP / self::INCH_TO_CM; } /** @@ -64,7 +64,7 @@ public static function cmToInch($centimeter = 1) */ public static function cmToPixel($centimeter = 1) { - return $centimeter / self::INCH_TO_CM * self::INCH_TO_PIXEL; + return $centimeter * self::INCH_TO_PIXEL / self::INCH_TO_CM; } /** @@ -76,7 +76,7 @@ public static function cmToPixel($centimeter = 1) */ public static function cmToPoint($centimeter = 1) { - return $centimeter / self::INCH_TO_CM * self::INCH_TO_POINT; + return $centimeter * self::INCH_TO_POINT / self::INCH_TO_CM; } /** @@ -88,7 +88,7 @@ public static function cmToPoint($centimeter = 1) */ public static function cmToEmu($centimeter = 1) { - return round($centimeter / self::INCH_TO_CM * self::INCH_TO_PIXEL * self::PIXEL_TO_EMU); + return round($centimeter * self::INCH_TO_PIXEL * self::PIXEL_TO_EMU / self::INCH_TO_CM); } /** @@ -160,7 +160,7 @@ public static function inchToEmu($inch = 1) */ public static function pixelToTwip($pixel = 1) { - return $pixel / self::INCH_TO_PIXEL * self::INCH_TO_TWIP; + return $pixel * self::INCH_TO_TWIP / self::INCH_TO_PIXEL; } /** @@ -172,7 +172,7 @@ public static function pixelToTwip($pixel = 1) */ public static function pixelToCm($pixel = 1) { - return $pixel / self::INCH_TO_PIXEL * self::INCH_TO_CM; + return $pixel * self::INCH_TO_CM / self::INCH_TO_PIXEL; } /** @@ -184,7 +184,7 @@ public static function pixelToCm($pixel = 1) */ public static function pixelToPoint($pixel = 1) { - return $pixel / self::INCH_TO_PIXEL * self::INCH_TO_POINT; + return $pixel * self::INCH_TO_POINT / self::INCH_TO_PIXEL; } /** @@ -208,7 +208,7 @@ public static function pixelToEmu($pixel = 1) */ public static function pointToTwip($point = 1) { - return $point / self::INCH_TO_POINT * self::INCH_TO_TWIP; + return $point * self::INCH_TO_TWIP / self::INCH_TO_POINT; } /** @@ -220,7 +220,7 @@ public static function pointToTwip($point = 1) */ public static function pointToPixel($point = 1) { - return $point / self::INCH_TO_POINT * self::INCH_TO_PIXEL; + return $point * self::INCH_TO_PIXEL / self::INCH_TO_POINT; } /** @@ -232,7 +232,7 @@ public static function pointToPixel($point = 1) */ public static function pointToEmu($point = 1) { - return round($point / self::INCH_TO_POINT * self::INCH_TO_PIXEL * self::PIXEL_TO_EMU); + return round($point * self::INCH_TO_PIXEL * self::PIXEL_TO_EMU / self::INCH_TO_POINT); } /** @@ -244,7 +244,7 @@ public static function pointToEmu($point = 1) */ public static function pointToCm($point = 1) { - return $point / self::INCH_TO_POINT * self::INCH_TO_CM; + return $point * self::INCH_TO_CM / self::INCH_TO_POINT; } /** @@ -268,7 +268,7 @@ public static function emuToPixel($emu = 1) */ public static function picaToPoint($pica = 1) { - return $pica / self::INCH_TO_PICA * self::INCH_TO_POINT; + return $pica * self::INCH_TO_POINT / self::INCH_TO_PICA; } /** diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 170dc5dff3..9cc3abe5b7 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -28,10 +28,15 @@ 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; +use Throwable; /** * Common Html functions. @@ -40,14 +45,24 @@ */ 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*$/'; + private const DECLARES_CHARSET = '/ charset=/i'; + protected static $listIndex = 0; protected static $xpath; protected static $options; + /** @var ?DocInfo */ + protected static $docInfo; + + /** @var bool */ + private static $addbody = false; + /** * @var Css */ @@ -72,35 +87,88 @@ 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 - $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 if (\PHP_VERSION_ID < 80000) { - $orignalLibEntityLoader = libxml_disable_entity_loader(true); + $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 (Throwable $e) { + $result = false; + $exceptionMessage = $e->getMessage(); + } + if ($result === false) { + throw new Exception($exceptionMessage); + } + self::removeAnnoyingWhitespaceTextNodes($dom); static::$xpath = new DOMXPath($dom); - $node = $dom->getElementsByTagName('body'); + $node = $dom->getElementsByTagName('html'); + if (count($node) === 0 || $node->item(0) === null) { + $node = $dom->getElementsByTagName('body'); // @codeCoverageIgnore + } static::parseNode($node->item(0), $element); if (\PHP_VERSION_ID < 80000) { - libxml_disable_entity_loader($orignalLibEntityLoader); + libxml_disable_entity_loader($orignalLibEntityLoader); // @codeCoverageIgnore + } + } + + // 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. * @@ -109,14 +177,21 @@ 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) - $attributeDir = $attributes->getNamedItem('dir'); - $attributeDirValue = $attributeDir ? $attributeDir->nodeValue : ''; - $bidi = $attributeDirValue === 'rtl'; + $bidi = false; + $attrDir = $attributes->getNamedItem('dir'); + $direction = isset($attrDir) ? $attrDir->nodeValue : ''; + 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)) { @@ -149,7 +224,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': @@ -200,6 +275,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']; @@ -314,7 +429,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; } /** @@ -345,18 +465,22 @@ protected static function parseInput($node, $element, &$styles): void /** * Parse heading node. * - * @param string $argument1 Name of heading style - * * @todo Think of a clever way of defining header styles, now it is only based on the assumption, that * Heading1 - Heading6 are already defined somewhere */ - protected static function parseHeading(DOMNode $node, AbstractContainer $element, array &$styles, string $argument1): TextRun + protected static function parseHeading(DOMNode $node, AbstractContainer $element, array &$styles, string $headingStyle): TextRun { $style = new Paragraph(); - $style->setStyleName($argument1); + $style->setStyleName($headingStyle); $style->setStyleByArray(self::parseInlineStyle($node, $styles['paragraph'])); + $textRun = new TextRun($style); - return $element->addTextRun($style); + // 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; } /** @@ -376,7 +500,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']); } } @@ -426,9 +554,10 @@ protected static function parseTable($node, $element, &$styles) } $attributes = $node->attributes; - if ($attributes->getNamedItem('border')) { + if ($attributes->getNamedItem('border') !== null && is_object($newElement->getStyle())) { $border = (int) $attributes->getNamedItem('border')->nodeValue; - $newElement->getStyle()->setBorderSize(Converter::pixelToTwip($border)); + $newElement->getStyle()->setBorderSize((int) Converter::pixelToTwip($border)); + $newElement->getStyle()->setBorderStyle(($border === 0) ? 'none' : 'single'); } return $newElement; @@ -717,6 +846,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': @@ -729,11 +859,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': @@ -865,7 +995,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': @@ -895,29 +1025,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': @@ -1066,6 +1213,8 @@ protected static function mapBorderStyle($cssBorderStyle) case 'dotted': case 'double': return $cssBorderStyle; + case 'hidden': + return 'none'; default: return 'single'; } @@ -1073,14 +1222,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]); } } } @@ -1171,7 +1320,6 @@ protected static function mapListType($cssListType) return NumberFormat::LOWER_ROMAN; // i, ii, iii, iv, .. case 'I': return NumberFormat::UPPER_ROMAN; // I, II, III, IV, .. - case '1': default: return NumberFormat::DECIMAL; // 1, 2, 3, .. } @@ -1227,7 +1375,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.
@@ -1328,6 +1477,12 @@ protected static function convertHtmlSize(string $size): float if (false !== strpos($size, 'px')) { return (float) str_replace('px', '', $size); } + if (false !== strpos($size, 'cm')) { + return Converter::cmToPixel((float) str_replace('cm', '', $size)); + } + if (false !== strpos($size, 'in')) { + return Converter::inchToPixel((float) str_replace('in', '', $size)); + } return (float) $size; } diff --git a/src/PhpWord/Shared/HtmlColours.php b/src/PhpWord/Shared/HtmlColours.php new file mode 100644 index 0000000000..c50acfd5da --- /dev/null +++ b/src/PhpWord/Shared/HtmlColours.php @@ -0,0 +1,544 @@ + '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 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/XMLWriter.php b/src/PhpWord/Shared/XMLWriter.php index 8dc28e1184..8b267b694c 100644 --- a/src/PhpWord/Shared/XMLWriter.php +++ b/src/PhpWord/Shared/XMLWriter.php @@ -180,7 +180,7 @@ public function writeAttributeIf($condition, $attribute, $value): void public function writeAttribute($name, $value) { if (is_float($value)) { - $value = json_encode($value); + $value = (string) $value; } return parent::writeAttribute($name, $value ?? ''); diff --git a/src/PhpWord/SimpleType/TextDirection.php b/src/PhpWord/SimpleType/TextDirection.php new file mode 100644 index 0000000000..ab1a045337 --- /dev/null +++ b/src/PhpWord/SimpleType/TextDirection.php @@ -0,0 +1,56 @@ +getParagraph(); } /** diff --git a/src/PhpWord/Style/AbstractStyle.php b/src/PhpWord/Style/AbstractStyle.php index 338ae6a5ef..5a553c43c4 100644 --- a/src/PhpWord/Style/AbstractStyle.php +++ b/src/PhpWord/Style/AbstractStyle.php @@ -51,6 +51,9 @@ abstract class AbstractStyle */ protected $aliases = []; + /** @var string */ + protected $basedOn = ''; + /** * Is this an automatic style? (Used primarily in OpenDocument driver). * @@ -84,6 +87,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 722e09931a..d97b884fdb 100644 --- a/src/PhpWord/Style/Border.php +++ b/src/PhpWord/Style/Border.php @@ -529,6 +529,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 5ab7ade673..3f27398f16 100644 --- a/src/PhpWord/Style/Paragraph.php +++ b/src/PhpWord/Style/Paragraph.php @@ -23,6 +23,7 @@ use PhpOffice\PhpWord\Shared\Text; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\TextAlignment; +use PhpOffice\PhpWord\SimpleType\TextDirection; /** * Paragraph style. @@ -70,7 +71,7 @@ class Paragraph extends Border * * @var string */ - private $basedOn = 'Normal'; + protected $basedOn = 'Normal'; /** * Style for next paragraph. @@ -182,6 +183,13 @@ class Paragraph extends Border */ private $textAlignment; + /** + * Text direction right or left, top or bottom. + * + * @var string + */ + private $textDirection = ''; + /** * Suppress hyphenation for paragraph. * @@ -242,6 +250,7 @@ public function getStyleValues() 'contextualSpacing' => $this->hasContextualSpacing(), 'bidi' => $this->isBidi(), 'textAlignment' => $this->getTextAlignment(), + 'textDirection' => $this->getTextDirection(), 'suppressAutoHyphens' => $this->hasSuppressAutoHyphens(), ]; @@ -274,30 +283,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. * @@ -874,6 +859,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 21e8a74823..36f03feae8 100644 --- a/src/PhpWord/Style/Table.php +++ b/src/PhpWord/Style/Table.php @@ -111,6 +111,20 @@ class Table extends Border */ private $borderInsideVColor; + /** + * Border style inside horizontal. + * + * @var string + */ + protected $borderInsideHStyle = ''; + + /** + * Border style inside vertical. + * + * @var string + */ + protected $borderInsideVStyle = ''; + /** * Shading. * @@ -169,6 +183,9 @@ class Table extends Border */ private $bidiVisual; + /** @var string */ + private $tblStyle = ''; + /** * Create new table style. * @@ -261,6 +278,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. * @@ -316,6 +369,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. * @@ -338,6 +411,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. * @@ -792,4 +893,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 7dd99af291..7d44c97adb 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 b66e2f8c92..98f69e3f3f 100644 --- a/src/PhpWord/Writer/HTML/Element/Table.php +++ b/src/PhpWord/Writer/HTML/Element/Table.php @@ -42,7 +42,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 */ @@ -54,7 +55,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/Text.php b/src/PhpWord/Writer/HTML/Element/Text.php index 7be95a5c76..2df802c904 100644 --- a/src/PhpWord/Writer/HTML/Element/Text.php +++ b/src/PhpWord/Writer/HTML/Element/Text.php @@ -163,20 +163,22 @@ private function writeTrackChangeOpening() $content = ''; if (($changed->getChangeType() == TrackChange::INSERTED)) { - $content .= 'getChangeType() == TrackChange::DELETED) { - $content .= ' ['author' => $changed->getAuthor(), 'id' => $this->element->getElementId()]]; - if ($changed->getDate() != null) { - $changedProp['changed']['date'] = $changed->getDate()->format('Y-m-d\TH:i:s\Z'); + $author = htmlspecialchars($changed->getAuthor(), ENT_QUOTES); + $content .= " data-phpword-chg-author='$author'"; + $elementId = htmlspecialchars($this->element->getElementId(), ENT_QUOTES); + $content .= " data-phpword-chg-id='$elementId'"; + $date = $changed->getDate(); + if ($date !== null) { + $dateout = $date->format('Y-m-d\TH:i:s\Z'); + $content .= " data-phpword-chg-timestamp='$dateout'"; } - $content .= json_encode($changedProp); - $content .= '\' '; - $content .= 'title="' . $changed->getAuthor(); - if ($changed->getDate() != null) { - $dateUser = $changed->getDate()->format('Y-m-d H:i:s'); + $content .= ' title="' . $author; + if ($date !== null) { + $dateUser = $date->format('Y-m-d H:i:s'); $content .= ' - ' . $dateUser; } $content .= '">'; diff --git a/src/PhpWord/Writer/HTML/Element/Title.php b/src/PhpWord/Writer/HTML/Element/Title.php index 84b0b199fc..f3aeb395f4 100644 --- a/src/PhpWord/Writer/HTML/Element/Title.php +++ b/src/PhpWord/Writer/HTML/Element/Title.php @@ -18,7 +18,11 @@ namespace PhpOffice\PhpWord\Writer\HTML\Element; +use PhpOffice\PhpWord\Element\Title as ElementTitle; +use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Writer\HTML; +use PhpOffice\PhpWord\Writer\HTML\Style\Font; +use PhpOffice\PhpWord\Writer\HTML\Style\Paragraph; /** * TextRun element HTML writer. @@ -34,21 +38,40 @@ class Title extends AbstractElement */ public function write() { - if (!$this->element instanceof \PhpOffice\PhpWord\Element\Title) { + if (!$this->element instanceof ElementTitle) { return ''; } $tag = 'h' . $this->element->getDepth(); $text = $this->element->getText(); + $paragraphStyle = null; if (is_string($text)) { $text = $this->parentWriter->escapeHTML($text); } else { + $paragraphStyle = $text->getParagraphStyle(); $writer = new Container($this->parentWriter, $text); $text = $writer->write(); } + $write1 = $write2 = $write3 = ''; + $style = Style::getStyle('Heading_' . $this->element->getDepth()); + if ($style !== null) { + $styleWriter = new Font($style); + $write1 = $styleWriter->write(); + } + if (is_object($paragraphStyle)) { + $styleWriter = new Paragraph($paragraphStyle); + $write3 = $styleWriter->write(); + if ($write1 !== '' && $write3 !== '') { + $write2 = ' '; + } + } + $css = "$write1$write2$write3"; + if ($css !== '') { + $css = " style=\"$css\""; + } - $content = "<{$tag}>{$text}" . PHP_EOL; + $content = "<{$tag}{$css}>{$text}" . PHP_EOL; return $content; } diff --git a/src/PhpWord/Writer/HTML/Part/Head.php b/src/PhpWord/Writer/HTML/Part/Head.php index 79235e1c4a..4da526075d 100644 --- a/src/PhpWord/Writer/HTML/Part/Head.php +++ b/src/PhpWord/Writer/HTML/Part/Head.php @@ -92,17 +92,15 @@ private function writeStyles(): string 'font-size' => Settings::getDefaultFontSize() . 'pt', 'color' => "#{$defaultFontColor}", ]; - // Mpdf sometimes needs separate tag for body; doesn't harm others. - $bodyarray = $astarray; $defaultWhiteSpace = $this->getParentWriter()->getDefaultWhiteSpace(); if ($defaultWhiteSpace) { $astarray['white-space'] = $defaultWhiteSpace; } + $bodyarray = $astarray; foreach ([ 'body' => $bodyarray, - '*' => $astarray, 'a.NoteRef' => [ 'text-decoration' => 'none', ], @@ -121,6 +119,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; @@ -135,12 +136,13 @@ private function writeStyles(): string $styleWriter = new FontStyleWriter($style); if ($style->getStyleType() == 'title') { $name = str_replace('Heading_', 'h', $name); + $css .= "{$name} {" . $styleWriter->write() . '}' . PHP_EOL; $styleParagraph = $style->getParagraph(); $style = $styleParagraph; } else { $name = '.' . $name; + $css .= "{$name} {" . $styleWriter->write() . '}' . PHP_EOL; } - $css .= "{$name} {" . $styleWriter->write() . '}' . PHP_EOL; } if ($style instanceof Paragraph) { $styleWriter = new ParagraphStyleWriter($style); diff --git a/src/PhpWord/Writer/HTML/Style/Font.php b/src/PhpWord/Writer/HTML/Style/Font.php index 1001b64d2e..07cb6f64f3 100644 --- a/src/PhpWord/Writer/HTML/Style/Font.php +++ b/src/PhpWord/Writer/HTML/Style/Font.php @@ -74,6 +74,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 a59d1cdba4..d14d2dd1ea 100644 --- a/src/PhpWord/Writer/HTML/Style/Table.php +++ b/src/PhpWord/Writer/HTML/Style/Table.php @@ -58,7 +58,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; } } @@ -66,7 +66,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 390311aa26..91784e982d 100644 --- a/src/PhpWord/Writer/RTF.php +++ b/src/PhpWord/Writer/RTF.php @@ -68,7 +68,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 06266897b3..b47f5d2cb8 100644 --- a/src/PhpWord/Writer/RTF/Element/Title.php +++ b/src/PhpWord/Writer/RTF/Element/Title.php @@ -58,16 +58,16 @@ 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) { + if (!$element instanceof $elementClass || !is_string($text)) { return ''; } - $textToWrite = $element->getText(); - if ($textToWrite instanceof \PhpOffice\PhpWord\Element\TextRun) { - $textToWrite = $textToWrite->getText(); // gets text from TextRun - } - $this->getStyles(); $content = ''; @@ -88,7 +88,7 @@ public function write() $content .= '{'; $content .= $this->writeFontStyle(); - $content .= $this->writeText($textToWrite); + $content .= $this->writeText($text); $content .= '}'; $content .= $this->writeClosing(); $content .= $endout; diff --git a/src/PhpWord/Writer/Word2007/Part/Styles.php b/src/PhpWord/Writer/Word2007/Part/Styles.php index edf0314cc2..63e981820b 100644 --- a/src/PhpWord/Writer/Word2007/Part/Styles.php +++ b/src/PhpWord/Writer/Word2007/Part/Styles.php @@ -60,12 +60,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); } } } @@ -169,7 +170,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(); @@ -210,7 +211,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()); @@ -235,7 +236,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'); @@ -247,7 +248,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(); @@ -265,15 +266,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 623e8d5e72..f146307a6d 100644 --- a/src/PhpWord/Writer/Word2007/Style/Font.php +++ b/src/PhpWord/Writer/Word2007/Style/Font.php @@ -154,10 +154,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 b464e66ffb..ed405442a1 100644 --- a/src/PhpWord/Writer/Word2007/Style/MarginBorder.php +++ b/src/PhpWord/Writer/Word2007/Style/MarginBorder.php @@ -65,14 +65,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); } } @@ -80,8 +78,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 @@ -94,7 +92,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 55f51a54d6..9c0c6143cf 100644 --- a/src/PhpWord/Writer/Word2007/Style/Paragraph.php +++ b/src/PhpWord/Writer/Word2007/Style/Paragraph.php @@ -106,6 +106,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 446fc3b1a4..efa6614149 100644 --- a/src/PhpWord/Writer/Word2007/Style/Table.php +++ b/src/PhpWord/Writer/Word2007/Style/Table.php @@ -64,6 +64,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()) { @@ -140,6 +142,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/Html/CharsetTest.php b/tests/PhpWordTests/Reader/Html/CharsetTest.php new file mode 100644 index 0000000000..575fa0d14e --- /dev/null +++ b/tests/PhpWordTests/Reader/Html/CharsetTest.php @@ -0,0 +1,64 @@ +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', '电视机'], + 'loadhtml gives its best shot' => ['charset.unknown.html', "Ã\u{80}1"], + ]; + } +} diff --git a/tests/PhpWordTests/Reader/Word2007/StyleTableTest.php b/tests/PhpWordTests/Reader/Word2007/StyleTableTest.php new file mode 100644 index 0000000000..cb8c255018 --- /dev/null +++ b/tests/PhpWordTests/Reader/Word2007/StyleTableTest.php @@ -0,0 +1,56 @@ +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..67747c7c0a --- /dev/null +++ b/tests/PhpWordTests/SettingsRtlTest.php @@ -0,0 +1,82 @@ +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 13b32e1293..d0549f190c 100644 --- a/tests/PhpWordTests/SettingsTest.php +++ b/tests/PhpWordTests/SettingsTest.php @@ -57,9 +57,6 @@ class SettingsTest extends TestCase private $zipClass; - /** @var bool */ - private $defaultRtl; - protected function setUp(): void { $this->compatibility = Settings::hasCompatibility(); @@ -74,7 +71,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 @@ -91,7 +87,6 @@ protected function tearDown(): void Settings::setPdfRendererPath($this->pdfRendererPath); Settings::setTempDir($this->tempDir); Settings::setZipClass($this->zipClass); - Settings::setDefaultRtl($this->defaultRtl); } /** @@ -114,17 +109,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..043aa0361e --- /dev/null +++ b/tests/PhpWordTests/Shared/Html2402Test.php @@ -0,0 +1,209 @@ + + + + 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, 'expectException(Exception::class); + $this->expectExceptionMessage('loadHTML'); + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, ''); + } + + public function testCssOnIdElement(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $html = '' + . '' + . 'Id Test' + . '' + . '' + . '

test1.

' + . ''; + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord); + $marginPath = '/w:document/w:body/w:p/w:pPr/w:spacing'; + self::assertSame('150', $doc->getElement($marginPath)->getAttribute('w:before')); + self::assertSame('150', $doc->getElement($marginPath)->getAttribute('w:after')); + $path = '/w:document/w:body/w:p/w:r'; + self::assertSame('test1.', $doc->getElement($path)->nodeValue); + $boldPath = $path . '/w:rPr/w:b'; + self::assertSame('1', $doc->getElement($boldPath)->getAttribute('w:val')); + } + + public function testListTypes(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $html = '
  1. Decimal number first
  2. second
' + . '
  1. Lowercase first
  2. second
' + . '
  1. Uppercase first
  2. second
' + . '
  1. Lower roman first
  2. second
' + . '
  1. Upper roman first
  2. second
'; + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord); + + $item = 1; + $expected = '1'; + $path = "/w:document/w:body/w:p[$item]"; + self::assertSame('Decimal number first', $doc->getElement("$path/w:r")->nodeValue); + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + ++$item; + $path = "/w:document/w:body/w:p[$item]"; + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + + ++$item; + $expected = '2'; + $path = "/w:document/w:body/w:p[$item]"; + self::assertSame('Lowercase first', $doc->getElement("$path/w:r")->nodeValue); + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + ++$item; + $path = "/w:document/w:body/w:p[$item]"; + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + + ++$item; + $expected = '3'; + $path = "/w:document/w:body/w:p[$item]"; + self::assertSame('Uppercase first', $doc->getElement("$path/w:r")->nodeValue); + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + ++$item; + $path = "/w:document/w:body/w:p[$item]"; + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + + ++$item; + $expected = '4'; + $path = "/w:document/w:body/w:p[$item]"; + self::assertSame('Lower roman first', $doc->getElement("$path/w:r")->nodeValue); + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + ++$item; + $path = "/w:document/w:body/w:p[$item]"; + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + + ++$item; + $expected = '5'; + $path = "/w:document/w:body/w:p[$item]"; + self::assertSame('Upper roman first', $doc->getElement("$path/w:r")->nodeValue); + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + ++$item; + $path = "/w:document/w:body/w:p[$item]"; + $numIdPath = $path . '/w:pPr/w:numPr/w:numId'; + self::assertSame($expected, $doc->getElement($numIdPath)->getAttribute('w:val')); + } + + public function testPadding(): void + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $html = '
' + . '' + . '' + . '' + . '' + . '' + . '' + . '
2020 30
20 30 4020 30 40 50
'; + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord); + + $item = 1; + $td = 1; + $path = "/w:document/w:body/w:tbl/w:tr[$item]/w:tc[$td]"; + self::assertSame('20', $doc->getElement("$path/w:p/w:r")->nodeValue); + $tcMarPath = $path . '/w:tcPr/w:tcMar'; + self::assertSame('300', $doc->getElement($tcMarPath . '/w:top')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:start')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:bottom')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:end')->getAttribute('w:w')); + + ++$td; + $path = "/w:document/w:body/w:tbl/w:tr[$item]/w:tc[$td]"; + self::assertSame('20 30', $doc->getElement("$path/w:p/w:r")->nodeValue); + $tcMarPath = $path . '/w:tcPr/w:tcMar'; + self::assertSame('300', $doc->getElement($tcMarPath . '/w:top')->getAttribute('w:w')); + self::assertSame('450', $doc->getElement($tcMarPath . '/w:start')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:bottom')->getAttribute('w:w')); + self::assertSame('450', $doc->getElement($tcMarPath . '/w:end')->getAttribute('w:w')); + + $item = 1; + $td = 1; + $path = "/w:document/w:body/w:tbl/w:tr[$item]/w:tc[$td]"; + self::assertSame('20', $doc->getElement("$path/w:p/w:r")->nodeValue); + $tcMarPath = $path . '/w:tcPr/w:tcMar'; + self::assertSame('300', $doc->getElement($tcMarPath . '/w:top')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:start')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:bottom')->getAttribute('w:w')); + self::assertSame('300', $doc->getElement($tcMarPath . '/w:end')->getAttribute('w:w')); + + ++$item; + $td = 1; + $path = "/w:document/w:body/w:tbl/w:tr[$item]/w:tc[$td]"; + self::assertSame('20 30 40', $doc->getElement("$path/w:p/w:r")->nodeValue); + $tcMarPath = $path . '/w:tcPr/w:tcMar'; + self::assertSame('300', $doc->getElement($tcMarPath . '/w:top')->getAttribute('w:w')); + self::assertSame('450', $doc->getElement($tcMarPath . '/w:start')->getAttribute('w:w')); + self::assertSame('600', $doc->getElement($tcMarPath . '/w:bottom')->getAttribute('w:w')); + self::assertSame('450', $doc->getElement($tcMarPath . '/w:end')->getAttribute('w:w')); + + ++$td; + $path = "/w:document/w:body/w:tbl/w:tr[$item]/w:tc[$td]"; + self::assertSame('20 30 40 50', $doc->getElement("$path/w:p/w:r")->nodeValue); + $tcMarPath = $path . '/w:tcPr/w:tcMar'; + self::assertSame('300', $doc->getElement($tcMarPath . '/w:top')->getAttribute('w:w')); + self::assertSame('750', $doc->getElement($tcMarPath . '/w:start')->getAttribute('w:w')); + self::assertSame('600', $doc->getElement($tcMarPath . '/w:bottom')->getAttribute('w:w')); + self::assertSame('450', $doc->getElement($tcMarPath . '/w:end')->getAttribute('w:w')); + } +} diff --git a/tests/PhpWordTests/Shared/HtmlFullTest.php b/tests/PhpWordTests/Shared/HtmlFullTest.php new file mode 100644 index 0000000000..53ba093184 --- /dev/null +++ b/tests/PhpWordTests/Shared/HtmlFullTest.php @@ -0,0 +1,94 @@ +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..331935fbae --- /dev/null +++ b/tests/PhpWordTests/Shared/HtmlHeadingsTest.php @@ -0,0 +1,76 @@ +addTitleStyle(1, ['size' => 20]); + $section = $originalDoc->addSection(); + $expectedStrings = []; + $section->addTitle('Title 1', 1); + $expectedStrings[] = '

Title 1

'; + for ($i = 2; $i <= 6; ++$i) { + $textRun = new TextRun(); + $textRun->addText('Title '); + $textRun->addText("$i", ['italic' => true]); + $section->addTitle($textRun, $i); + $expectedStrings[] = "Title $i"; + } + $writer = new HtmlWriter($originalDoc); + $content = $writer->getContent(); + foreach ($expectedStrings as $expectedString) { + self::assertStringContainsString($expectedString, $content); + } + + $newDoc = new PhpWord(); + $newSection = $newDoc->addSection(); + SharedHtml::addHtml($newSection, $content, true); + $newWriter = new HtmlWriter($newDoc); + $newContent = $newWriter->getContent(); + // Reader does not yet support h1 declaration in css. + $content = str_replace('h1 {font-size: 20pt;}' . PHP_EOL, '', $content); + + // Reader transforms Text to TextRun, + // but result is functionally the same. + self::assertSame( + $newContent, + str_replace( + '

Title 1

', + '

Title 1

', + $content + ) + ); + } +} diff --git a/tests/PhpWordTests/Shared/HtmlRtlTest.php b/tests/PhpWordTests/Shared/HtmlRtlTest.php new file mode 100644 index 0000000000..918c1cd6af --- /dev/null +++ b/tests/PhpWordTests/Shared/HtmlRtlTest.php @@ -0,0 +1,181 @@ +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 42d8aa598b..50f1e699a6 100644 --- a/tests/PhpWordTests/Shared/HtmlTest.php +++ b/tests/PhpWordTests/Shared/HtmlTest.php @@ -24,12 +24,15 @@ use PhpOffice\PhpWord\Element\Table; use PhpOffice\PhpWord\Element\Text; use PhpOffice\PhpWord\Element\TextRun; +use PhpOffice\PhpWord\Element\Title; use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\Shared\Converter; use PhpOffice\PhpWord\Shared\Html; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\LineSpacingRule; use PhpOffice\PhpWord\SimpleType\TblWidth; +use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style\Font; use PhpOffice\PhpWord\Style\Paragraph; use PhpOffice\PhpWordTests\AbstractWebServerEmbedded; @@ -55,6 +58,7 @@ protected function tearDown(): void */ public function testAddHtml(): void { + Settings::setOutputEscapingEnabled(true); $content = ''; // Default @@ -83,16 +87,31 @@ public function testAddHtml(): void // Other parts $section = $phpWord->addSection(); $content = ''; + $expectd = ''; $content .= '
HeaderContent
'; $content .= '
  • Bullet
    • Bullet
'; $content .= '
  1. Bullet
'; $content .= "'Single Quoted Text'"; + $expectd .= "'Single Quoted Text'"; $content .= '"Double Quoted Text"'; + $expectd .= '"Double Quoted Text"'; $content .= '& Ampersand'; - $content .= '<>“‘’«»‹›'; + $expectd .= '& Ampersand'; + $content .= '<>“”‘’«»‹›'; + $expectd .= '<>“”‘’«»‹›'; $content .= '&•°…™©®—'; + $expectd .= '&•°…™©®—'; $content .= '–   ²³¼½¾'; + $expectd .= "–\u{a0}\u{2003}\u{2002}²³¼½¾"; Html::addHtml($section, $content); + $elements = $section->getElements(); + foreach ($elements as $element) { + if ($element instanceof Text) { + self::assertSame($expectd, $element->getText()); + + break; + } + } } /** @@ -116,6 +135,8 @@ public function testParseHeader(): void self::assertCount(1, $section->getElements()); $element = $section->getElement(0); + self::assertInstanceOf(Title::class, $element); + $element = $element->getText(); self::assertInstanceOf(TextRun::class, $element); self::assertInstanceOf(Paragraph::class, $element->getParagraphStyle()); self::assertEquals('Heading1', $element->getParagraphStyle()->getStyleName()); @@ -137,6 +158,8 @@ public function testParseHeaderStyle(): void self::assertCount(1, $section->getElements()); $element = $section->getElement(0); + self::assertInstanceOf(Title::class, $element); + $element = $element->getText(); self::assertInstanceOf(TextRun::class, $element); self::assertInstanceOf(Paragraph::class, $element->getParagraphStyle()); self::assertEquals('Heading1', $element->getParagraphStyle()->getStyleName()); @@ -155,7 +178,7 @@ public function testParseHeaderStyle(): 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>'); @@ -183,13 +206,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 @@ -203,6 +227,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 text-decoration style. */ @@ -248,7 +303,8 @@ public function testParseWidth(string $htmlSize, float $docxSize, string $docxUn $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); $xpath = '/w:document/w:body/w:tbl/w:tblPr/w:tblW'; self::assertTrue($doc->elementExists($xpath)); - self::assertEquals($docxSize, $doc->getElement($xpath)->getAttribute('w:w')); + $actual = (float) $doc->getElement($xpath)->getAttribute('w:w'); + self::assertEqualsWithDelta($docxSize, $actual, 1.0e-12); self::assertEquals($docxUnit, $doc->getElement($xpath)->getAttribute('w:type')); } @@ -558,7 +614,7 @@ public function testParseTableAndCellWidth(): void { $phpWord = new PhpWord(); $section = $phpWord->addSection([ - 'orientation' => \PhpOffice\PhpWord\Style\Section::ORIENTATION_LANDSCAPE, + 'orientation' => Style\Section::ORIENTATION_LANDSCAPE, ]); // borders & backgrounds are here just for better visual comparison @@ -627,7 +683,7 @@ public function testParseTableRowHeight(): void { $phpWord = new PhpWord(); $section = $phpWord->addSection([ - 'orientation' => \PhpOffice\PhpWord\Style\Section::ORIENTATION_LANDSCAPE, + 'orientation' => Style\Section::ORIENTATION_LANDSCAPE, ]); $html = <<addSection([ - 'orientation' => \PhpOffice\PhpWord\Style\Section::ORIENTATION_LANDSCAPE, + 'orientation' => Style\Section::ORIENTATION_LANDSCAPE, ]); // borders & backgrounds are here just for better visual comparison @@ -749,7 +805,7 @@ public function testParseTableStyleAttributeInlineStyle(): void { $phpWord = new PhpWord(); $section = $phpWord->addSection([ - 'orientation' => \PhpOffice\PhpWord\Style\Section::ORIENTATION_LANDSCAPE, + 'orientation' => Style\Section::ORIENTATION_LANDSCAPE, ]); $html = ' @@ -768,7 +824,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')); } /** @@ -876,7 +932,7 @@ public function testParseListWithFormat(): void { $phpWord = new PhpWord(); $section = $phpWord->addSection(); - $html = preg_replace('/\s+/', ' ', '
    + $html = '
    • Some text before list item1 bold with text after bold @@ -888,7 +944,7 @@ public function testParseListWithFormat(): void list item2
    • -
    '); +
'; Html::addHtml($section, $html, false, false); $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); @@ -1230,7 +1286,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)); @@ -1363,9 +1419,11 @@ public static function providerParseWidth(): array return [ ['auto', 5000, TblWidth::PERCENT], ['100%', 5000, TblWidth::PERCENT], - ['200pt', 3999.999999999999, TblWidth::TWIP], + ['200pt', 4000, TblWidth::TWIP], ['300px', 4500, TblWidth::TWIP], ['400', 6000, TblWidth::TWIP], + ['2in', 2880, TblWidth::TWIP], + ['2.54cm', 1440, TblWidth::TWIP], ]; } diff --git a/tests/PhpWordTests/TemplateProcessorSectionTest.php b/tests/PhpWordTests/TemplateProcessorSectionTest.php new file mode 100644 index 0000000000..a6e9d61163 --- /dev/null +++ b/tests/PhpWordTests/TemplateProcessorSectionTest.php @@ -0,0 +1,93 @@ +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/TestHelperDOCX.php b/tests/PhpWordTests/TestHelperDOCX.php index 2a6fbabae0..5ebe799cfc 100644 --- a/tests/PhpWordTests/TestHelperDOCX.php +++ b/tests/PhpWordTests/TestHelperDOCX.php @@ -47,13 +47,14 @@ class TestHelperDOCX */ public static function getDocument(PhpWord $phpWord, $writerName = 'Word2007') { + $tempdir = self::getTempDirPhpunit(); self::$file = tempnam(Settings::getTempDir(), 'PhpWord'); if (false === self::$file) { throw new CreateTemporaryFileException(); } - if (!is_dir(Settings::getTempDir() . '/PhpWord_Unit_Test/')) { - mkdir(Settings::getTempDir() . '/PhpWord_Unit_Test/'); + if (!is_dir($tempdir)) { + mkdir($tempdir); } $xmlWriter = IOFactory::createWriter($phpWord, $writerName); @@ -62,11 +63,11 @@ public static function getDocument(PhpWord $phpWord, $writerName = 'Word2007') $zip = new ZipArchive(); $res = $zip->open(self::$file); if (true === $res) { - $zip->extractTo(Settings::getTempDir() . '/PhpWord_Unit_Test/'); + $zip->extractTo($tempdir); $zip->close(); } - $doc = new XmlDocument(Settings::getTempDir() . '/PhpWord_Unit_Test/'); + $doc = new XmlDocument($tempdir); if ($writerName === 'ODText') { $doc->setDefaultFile('content.xml'); } @@ -83,8 +84,9 @@ public static function clear(): void unlink(self::$file); self::$file = ''; } - if (is_dir(Settings::getTempDir() . '/PhpWord_Unit_Test/')) { - self::deleteDir(Settings::getTempDir() . '/PhpWord_Unit_Test/'); + $tempdir = self::getTempDirPhpunit(); + if (is_dir($tempdir)) { + self::deleteDir($tempdir); } } @@ -117,4 +119,14 @@ public static function getFile() { return self::$file; } + + /** + * Get temporary directory for PhpUnit. + * + * @return string + */ + private static function getTempDirPhpunit() + { + return Settings::getTempDir() . '/PhpWord_Unit_Test'; + } } diff --git a/tests/PhpWordTests/Writer/HTML/ElementTest.php b/tests/PhpWordTests/Writer/HTML/ElementTest.php index 3b2580381f..28e785a237 100644 --- a/tests/PhpWordTests/Writer/HTML/ElementTest.php +++ b/tests/PhpWordTests/Writer/HTML/ElementTest.php @@ -72,13 +72,41 @@ public function testWriteTrackChanges(): void $text = $section->addText('my dummy text'); $text->setChangeInfo(TrackChange::INSERTED, 'author name'); $text2 = $section->addText('my other text'); - $text2->setTrackChange(new TrackChange(TrackChange::DELETED, 'another author', new DateTime())); + $deleteTime = new DateTime(); + $deleteAuthor = "Spec O'char"; + $deleteTrack = new TrackChange(TrackChange::DELETED, $deleteAuthor, $deleteTime); + $text2->setTrackChange($deleteTrack); $dom = Helper::getAsHTML($phpWord); $xpath = new DOMXPath($dom); self::assertEquals(1, $xpath->query('/html/body/div/p[1]/ins')->length); self::assertEquals(1, $xpath->query('/html/body/div/p[2]/del')->length); + $node = $xpath->query('/html/body/div/p[2]/del'); + self::assertNotFalse($node); + $allAttributes = $node[0]->attributes; + self::assertCount(4, $allAttributes); + $node = $xpath->query('/html/body/div/p[2]/del'); + self::assertNotFalse($node); + + $attributes = $node[0]->attributes[0]; + self::assertSame('data-phpword-chg-author', $attributes->name); + self::assertSame($deleteAuthor, $attributes->value); + + $text2Id = $text2->getElementId(); + $attributes = $node[0]->attributes[1]; + self::assertSame('data-phpword-chg-id', $attributes->name); + self::assertSame($text2Id, $attributes->value); + + $attributes = $node[0]->attributes[2]; + self::assertSame('data-phpword-chg-timestamp', $attributes->name); + self::assertSame($deleteTime->format('Y-m-d\TH:i:s\Z'), $attributes->value); + + $attributes = $node[0]->attributes[3]; + self::assertSame('title', $attributes->name); + $expected = $deleteAuthor . ' - ' + . $deleteTime->format('Y-m-d H:i:s'); + self::assertSame($expected, $attributes->value); } /** diff --git a/tests/PhpWordTests/Writer/HTML/FontTest.php b/tests/PhpWordTests/Writer/HTML/FontTest.php index 0a203b7237..485cd2ae82 100644 --- a/tests/PhpWordTests/Writer/HTML/FontTest.php +++ b/tests/PhpWordTests/Writer/HTML/FontTest.php @@ -121,10 +121,10 @@ public function testFontNames1(): void self::assertEquals('style5', Helper::getTextContent($xpath, '/html/body/div/p[6]/span', 'class')); $style = Helper::getTextContent($xpath, '/html/head/style'); - $prg = preg_match('/^[*][^\\r\\n]*/m', $style, $matches); + $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); self::assertNotFalse($prg); - self::assertEquals('* {font-family: \'Courier New\'; font-size: 12pt; color: #000000;}', $matches[0]); + self::assertEquals('body {font-family: \'Courier New\'; font-size: 12pt; color: #000000;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); self::assertNotFalse($prg); @@ -177,10 +177,10 @@ public function testFontNames2(): void self::assertEquals('style4', Helper::getTextContent($xpath, '/html/body/div/p[5]/span', 'class')); $style = Helper::getTextContent($xpath, '/html/head/style'); - $prg = preg_match('/^[*][^\\r\\n]*/m', $style, $matches); + $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); self::assertNotFalse($prg); - self::assertEquals('* {font-family: \'Courier New\'; font-size: 12pt; color: #000000;}', $matches[0]); + self::assertEquals('body {font-family: \'Courier New\'; font-size: 12pt; color: #000000;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); self::assertNotFalse($prg); @@ -229,10 +229,10 @@ public function testFontNames3(): void self::assertEquals('style4', Helper::getTextContent($xpath, '/html/body/div/p[5]/span', 'class')); $style = Helper::getTextContent($xpath, '/html/head/style'); - $prg = preg_match('/^[*][^\\r\\n]*/m', $style, $matches); + $prg = preg_match('/^body[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); self::assertNotFalse($prg); - self::assertEquals('* {font-family: \'Courier New\', monospace; font-size: 12pt; color: #000000;}', $matches[0]); + self::assertEquals('body {font-family: \'Courier New\', monospace; font-size: 12pt; color: #000000;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); self::assertNotFalse($prg); @@ -274,8 +274,8 @@ public function testWhiteSpace(): void $xpath = new DOMXPath($dom); $style = Helper::getTextContent($xpath, '/html/head/style'); - self::assertNotFalse(preg_match('/^[*][^\\r\\n]*/m', $style, $matches)); - self::assertEquals('* {font-family: \'Arial\'; font-size: 12pt; color: #000000; white-space: pre-wrap;}', $matches[0]); + self::assertNotFalse(preg_match('/^body[^\\r\\n]*/m', $style, $matches)); + self::assertEquals('body {font-family: \'Arial\'; font-size: 12pt; color: #000000; white-space: pre-wrap;}', $matches[0]); $prg = preg_match('/^[.]style1[^\\r\\n]*/m', $style, $matches); self::assertNotEmpty($matches); self::assertNotFalse($prg); diff --git a/tests/PhpWordTests/Writer/HTML/Helper.php b/tests/PhpWordTests/Writer/HTML/Helper.php index 37f640d28a..2e7ee343c3 100644 --- a/tests/PhpWordTests/Writer/HTML/Helper.php +++ b/tests/PhpWordTests/Writer/HTML/Helper.php @@ -67,7 +67,7 @@ public static function getNamedItem(DOMXPath $xpath, string $query, string $name if ($item2 === null) { self::fail('Unexpected null return requesting item'); } else { - $returnValue = $item2->attributes->getNamedItem($namedItem); + $returnVal = $item2->attributes->getNamedItem($namedItem); } } @@ -125,4 +125,13 @@ public static function getAsHTML(PhpWord $phpWord, string $defaultWhiteSpace = ' return $dom; } + + public static function getHtmlString(PhpWord $phpWord, string $defaultWhiteSpace = '', string $defaultGenericFont = ''): string + { + $htmlWriter = new HTML($phpWord); + $htmlWriter->setDefaultWhiteSpace($defaultWhiteSpace); + $htmlWriter->setDefaultGenericFont($defaultGenericFont); + + return $htmlWriter->getContent(); + } } diff --git a/tests/PhpWordTests/Writer/HTML/PartTest.php b/tests/PhpWordTests/Writer/HTML/PartTest.php index b6748a58c5..0fe43c2350 100644 --- a/tests/PhpWordTests/Writer/HTML/PartTest.php +++ b/tests/PhpWordTests/Writer/HTML/PartTest.php @@ -185,5 +185,9 @@ public function testTitleStyles(): void self::assertNotFalse(strpos($style, 'h2 {margin-top: 0.25pt; margin-bottom: 0.25pt;}')); self::assertEquals(1, Helper::getLength($xpath, '/html/body/div/h1')); self::assertEquals(2, Helper::getLength($xpath, '/html/body/div/h2')); + $html = Helper::getHtmlString($phpWord); + self::assertStringContainsString('

Header 1 #1

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

Header 2 #1

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

Header 2 #2

', $html); } } diff --git a/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php b/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php index e1dc78a32b..e6cad15ea0 100644 --- a/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php +++ b/tests/PhpWordTests/Writer/ODText/Part/ContentTest.php @@ -19,6 +19,7 @@ namespace PhpOffice\PhpWordTests\Writer\ODText\Part; use PhpOffice\PhpWord\PhpWord; +use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWordTests\TestHelperDOCX; @@ -29,11 +30,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 bc02ca8ea0..4d59e1a90e 100644 --- a/tests/PhpWordTests/Writer/ODText/Style/Paragraph2Test.php +++ b/tests/PhpWordTests/Writer/ODText/Style/Paragraph2Test.php @@ -50,14 +50,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'; @@ -65,7 +68,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..72d22e5eb2 --- /dev/null +++ b/tests/PhpWordTests/Writer/RTF/RichTextTitleTest.php @@ -0,0 +1,51 @@ +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/_files/documents/word.2474.docx b/tests/PhpWordTests/_files/documents/word.2474.docx new file mode 100644 index 0000000000..8ecbaef2b3 Binary files /dev/null and b/tests/PhpWordTests/_files/documents/word.2474.docx differ 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 0000000000..6b29e7d2b0 Binary files /dev/null and b/tests/PhpWordTests/_files/html/charset.UTF-16.bebom.html differ 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 0000000000..4ba47a8139 Binary files /dev/null and b/tests/PhpWordTests/_files/html/charset.UTF-16.lebom.html differ 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

+ +