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/Writer/Word2007/Element/TOCTest.php b/tests/PhpWordTests/Writer/Word2007/Element/TOCTest.php index 95e79114aa..c7054c195a 100644 --- a/tests/PhpWordTests/Writer/Word2007/Element/TOCTest.php +++ b/tests/PhpWordTests/Writer/Word2007/Element/TOCTest.php @@ -66,7 +66,10 @@ public function testWriteTitleWithoutpageNumber(): void //more than one title and random text for create more than one page for ($i = 1; $i <= 10; ++$i) { $section->addTitle('Title ' . $i, 1); - $content = file_get_contents('https://loripsum.net/api/10/long'); + // This server seems to have disappeared. + //$content = file_get_contents('https://loripsum.net/api/10/long'); + // Contents recovered using wayback machine. + $content = file_get_contents('tests/PhpWordTests/_files/html/loripsum.net.html'); \PhpOffice\PhpWord\Shared\Html::addHtml($section, $content ? $content : '', false, false); $section->addPageBreak(); } 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

+ + diff --git a/tests/PhpWordTests/_files/html/loripsum.net.html b/tests/PhpWordTests/_files/html/loripsum.net.html new file mode 100644 index 0000000000..7cf1357e71 --- /dev/null +++ b/tests/PhpWordTests/_files/html/loripsum.net.html @@ -0,0 +1,19 @@ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cupiditates non Epicuri divisione finiebat, sed sua satietate. Hic, qui utrumque probat, ambobus debuit uti, sicut facit re, neque tamen dividit verbis. Quibus natura iure responderit non esse verum aliunde finem beate vivendi, a se principia rei gerendae peti; Quibus rebus vita consentiens virtutibusque respondens recta et honesta et constans et naturae congruens existimari potest. Illa videamus, quae a te de amicitia dicta sunt. Addo etiam illud, multa iam mihi dare signa puerum et pudoris et ingenii, sed aetatem vides. Quibus rebus vita consentiens virtutibusque respondens recta et honesta et constans et naturae congruens existimari potest. Duo Reges: constructio interrete. Quid, cum volumus nomina eorum, qui quid gesserint, nota nobis esse, parentes, patriam, multa praeterea minime necessaria? Apparet statim, quae sint officia, quae actiones.

+ +

Nam ante Aristippus, et ille melius. Quamquam id quidem licebit iis existimare, qui legerint. Inde igitur, inquit, ordiendum est. Quo modo autem optimum, si bonum praeterea nullum est? Non dolere, inquam, istud quam vim habeat postea videro; Perfecto enim et concluso neque virtutibus neque amicitiis usquam locum esse, si ad voluptatem omnia referantur, nihil praeterea est magnopere dicendum. Maximas vero virtutes iacere omnis necesse est voluptate dominante. Tertium autem omnibus aut maximis rebus iis, quae secundum naturam sint, fruentem vivere.

+ +

Huic ego, si negaret quicquam interesse ad beate vivendum quali uteretur victu, concederem, laudarem etiam; Maximas vero virtutes iacere omnis necesse est voluptate dominante. Polemoni et iam ante Aristoteli ea prima visa sunt, quae paulo ante dixi. Atqui haec patefactio quasi rerum opertarum, cum quid quidque sit aperitur, definitio est. Num igitur utiliorem tibi hunc Triarium putas esse posse, quam si tua sint Puteolis granaria?

+ +

Unum est sine dolore esse, alterum cum voluptate. Sed quoniam et advesperascit et mihi ad villam revertendum est, nunc quidem hactenus; Quid enim mihi potest esse optatius quam cum Catone, omnium virtutum auctore, de virtutibus disputare? Ergo infelix una molestia, fellx rursus, cum is ipse anulus in praecordiis piscis inventus est? Neque enim disputari sine reprehensione nec cum iracundia aut pertinacia recte disputari potest. Paulum, cum regem Persem captum adduceret, eodem flumine invectio?

+ +

Qui-vere falsone, quaerere mittimus-dicitur oculis se privasse; Res enim concurrent contrariae. Tenesne igitur, inquam, Hieronymus Rhodius quid dicat esse summum bonum, quo putet omnia referri oportere? Etenim nec iustitia nec amicitia esse omnino poterunt, nisi ipsae per se expetuntur. Invidiosum nomen est, infame, suspectum. Et ille ridens: Video, inquit, quid agas;

+ +

Satis est tibi in te, satis in legibus, satis in mediocribus amicitiis praesidii. Gracchum patrem non beatiorem fuisse quam fillum, cum alter stabilire rem publicam studuerit, alter evertere. Nunc dicam de voluptate, nihil scilicet novi, ea tamen, quae te ipsum probaturum esse confidam. Quid, si reviviscant Platonis illi et deinceps qui eorum auditores fuerunt, et tecum ita loquantur? Quid, si reviviscant Platonis illi et deinceps qui eorum auditores fuerunt, et tecum ita loquantur? Sed residamus, inquit, si placet. Illa videamus, quae a te de amicitia dicta sunt. A villa enim, credo, et: Si ibi te esse scissem, ad te ipse venissem. Si ista mala sunt, in quae potest incidere sapiens, sapientem esse non esse ad beate vivendum satis.

+ +

Intrandum est igitur in rerum naturam et penitus quid ea postulet pervidendum; Cum autem venissemus in Academiae non sine causa nobilitata spatia, solitudo erat ea, quam volueramus.

+ +

Non igitur potestis voluptate omnia dirigentes aut tueri aut retinere virtutem. Ne tum quidem te respicies et cogitabis sibi quemque natum esse et suis voluptatibus? Quae sunt igitur communia vobis cum antiquis, iis sic utamur quasi concessis; Scripsit enim et multis saepe verbis et breviter arteque in eo libro, quem modo nominavi, mortem nihil ad nos pertinere. An me, inquam, nisi te audire vellem, censes haec dicturum fuisse? Quae similitudo in genere etiam humano apparet.

+ +

Tum mihi Piso: Quid ergo? Ita multo sanguine profuso in laetitia et in victoria est mortuus. Et ille ridens: Video, inquit, quid agas; Sequitur disserendi ratio cognitioque naturae; Virtutis, magnitudinis animi, patientiae, fortitudinis fomentis dolor mitigari solet. -delector enim, quamquam te non possum, ut ais, corrumpere, delector, inquam, et familia vestra et nomine. Polemoni et iam ante Aristoteli ea prima visa sunt, quae paulo ante dixi. Graecum enim hunc versum nostis omnes-: Suavis laborum est praeteritorum memoria.

+ +

Itaque ne iustitiam quidem recte quis dixerit per se ipsam optabilem, sed quia iucunditatis vel plurimum afferat. An, partus ancillae sitne in fructu habendus, disseretur inter principes civitatis, P. Cum autem assumpta ratío est, tanto in dominatu locatur, ut omnia illa prima naturae hulus tutelae subiciantur. Similiter sensus, cum accessit ad naturam, tuetur illam quidem, sed etiam se tuetur; Non semper, inquam; Potius ergo illa dicantur: turpe esse, viri non esse debilitari dolore, frangi, succumbere. Quia voluptatem hanc esse sentiunt omnes, quam sensus accipiens movetur et iucunditate quadam perfunditur. Cum sciret confestim esse moriendum eamque mortem ardentiore studio peteret, quam Epicurus voluptatem petendam putat.