From 701f770ab781d92723ca2e7ae1471466d349040d Mon Sep 17 00:00:00 2001 From: lubosdz Date: Fri, 10 Jul 2020 12:43:19 +0200 Subject: [PATCH 01/11] Html parser (addHtml) - support width in tables & cells --- src/PhpWord/Shared/Html.php | 19 ++++++++- tests/PhpWord/Shared/HtmlTest.php | 70 +++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 54e9509e5f..cb09af62b8 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -106,6 +106,19 @@ protected static function parseInlineStyle($node, $styles = array()) case 'lang': $styles['lang'] = $attribute->value; break; + case 'width': + // tables, cells + $val = trim($attribute->value); + if(false !== strpos($val, '%')){ + // e.g. or
+ $styles['width'] = intval($val) * 50; + $styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT; + }else{ + // e.g. addCell(null, $cellStyles); + + // set cell width to control column widths + $width = isset($cellStyles['width']) ? $cellStyles['width'] : null; + unset($cellStyles['width']); // would not apply + $cell = $element->addCell($width, $cellStyles); if (self::shouldAddTextRun($node)) { return $cell->addTextRun(self::parseInlineStyle($node, $styles['paragraph'])); diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 5bc9e2411a..5474eb0d1a 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -632,4 +632,74 @@ public function testParseLetterSpacing() $this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:rPr/w:spacing')); $this->assertEquals(150 * 15, $doc->getElement('/w:document/w:body/w:p/w:r/w:rPr/w:spacing')->getAttribute('w:val')); } + + /** + * Parse widths in tables and cells, which also allows for controlling column width + */ + public function testParseTableAndCellWidth() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection([ + 'orientation' => \PhpOffice\PhpWord\Style\Section::ORIENTATION_LANDSCAPE, + ]); + + // borders & backgrounds are here just for better visual comparison + $html = << + + + + +
25% + + + + + + + + + + + + + +
400px
T2.R2.C150ptT2.R2.C3
300pxT2.R3.C3
+
+HTML; + + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + // outer table grid + $xpath = '/w:document/w:body/w:tbl/w:tblGrid/w:gridCol'; + $this->assertTrue($doc->elementExists($xpath)); + $this->assertEquals(25 * 50, $doc->getElement($xpath)->getAttribute('w:w')); + $this->assertEquals('dxa', $doc->getElement($xpath)->getAttribute('w:type')); + + //
assertTrue($doc->elementExists($xpath)); + $this->assertEquals(6000, $doc->getElement($xpath)->getAttribute('w:w')); + $this->assertEquals('dxa', $doc->getElement($xpath)->getAttribute('w:type')); + + // assertTrue($doc->elementExists($xpath)); + $this->assertEquals(4500, $doc->getElement($xpath)->getAttribute('w:w')); + $this->assertEquals('dxa', $doc->getElement($xpath)->getAttribute('w:type')); + } + } From e180cfe456ee81c210fa0034c740655f25c91144 Mon Sep 17 00:00:00 2001 From: lubosdz Date: Sat, 11 Jul 2020 00:24:08 +0200 Subject: [PATCH 02/11] Html parser (addHtml) - support cellspacing, bgColor --- src/PhpWord/Shared/Html.php | 14 +++++++++-- tests/PhpWord/Shared/HtmlTest.php | 42 +++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index cb09af62b8..08004b7a99 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -96,7 +96,7 @@ protected static function parseInlineStyle($node, $styles = array()) $attributes = $node->attributes; // get all the attributes(eg: id, class) foreach ($attributes as $attribute) { - switch ($attribute->name) { + switch (strtolower($attribute->name)) { case 'style': $styles = self::parseStyle($attribute, $styles); break; @@ -119,6 +119,15 @@ protected static function parseInlineStyle($node, $styles = array()) $styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP; } break; + case 'cellspacing': + // tables e.g. , where "2" = 2px (always pixels) + $val = intval($attribute->value).'px'; + $styles['cellSpacing'] = Converter::cssToTwip($val); + break; + case 'bgcolor': + // tables, rows, cells e.g. + $styles['bgColor'] = trim($attribute->value, '# '); + break; } } } @@ -519,7 +528,8 @@ protected static function parseStyle($attribute, $styles) foreach ($properties as $property) { list($cKey, $cValue) = array_pad(explode(':', $property, 2), 2, null); $cValue = trim($cValue); - switch (trim($cKey)) { + $cKey = strtolower(trim($cKey)); + switch ($cKey) { case 'text-decoration': switch ($cValue) { case 'underline': diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 5474eb0d1a..61ebd5354d 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -702,4 +702,46 @@ public function testParseTableAndCellWidth() $this->assertEquals('dxa', $doc->getElement($xpath)->getAttribute('w:type')); } + public function testParseCellspacingRowBgColor() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection([ + 'orientation' => \PhpOffice\PhpWord\Style\Section::ORIENTATION_LANDSCAPE, + ]); + + // borders & backgrounds are here just for better visual comparison + $html = << + + + + + + + + +
AB
CD
+HTML; + + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + // uncomment to see results + file_put_contents('./table_src.html', $html); + file_put_contents('./table_result_'.time().'.docx', file_get_contents( TestHelperDOCX::getFile() ) ); + + $xpath = '/w:document/w:body/w:tbl/w:tblPr/w:tblCellSpacing'; + $this->assertTrue($doc->elementExists($xpath)); + $this->assertEquals(3 * 15, $doc->getElement($xpath)->getAttribute('w:w')); + $this->assertEquals('dxa', $doc->getElement($xpath)->getAttribute('w:type')); + + $xpath = '/w:document/w:body/w:tbl/w:tr[1]/w:tc[1]/w:tcPr/w:shd'; + $this->assertTrue($doc->elementExists($xpath)); + $this->assertEquals('lightgreen', $doc->getElement($xpath)->getAttribute('w:fill')); + + $xpath = '/w:document/w:body/w:tbl/w:tr[2]/w:tc[1]/w:tcPr/w:shd'; + $this->assertTrue($doc->elementExists($xpath)); + $this->assertEquals('FF0000', $doc->getElement($xpath)->getAttribute('w:fill')); + } + } From ca5f08130230066ffc9ac2feee797f08b6bc2faa Mon Sep 17 00:00:00 2001 From: lubosdz Date: Sat, 11 Jul 2020 15:42:28 +0200 Subject: [PATCH 03/11] Html parser (addHtml) - support horizontal rule
--- src/PhpWord/Shared/Html.php | 61 +++++++++++++++++++++++++++++-- tests/PhpWord/Shared/HtmlTest.php | 48 ++++++++++++++++++++++-- 2 files changed, 101 insertions(+), 8 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 08004b7a99..9e5d84b1b0 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -183,6 +183,7 @@ protected static function parseNode($node, $element, $styles = array(), $data = 'img' => array('Image', $node, $element, $styles, null, null, null), 'br' => array('LineBreak', null, $element, $styles, null, null, null), 'a' => array('Link', $node, $element, $styles, null, null, null), + 'hr' => array('HorizRule', $node, $element, $styles, null, null, null), ); $newElement = null; @@ -630,10 +631,27 @@ protected static function parseStyle($attribute, $styles) } break; case 'border': - if (preg_match('/([0-9]+[^0-9]*)\s+(\#[a-fA-F0-9]+)\s+([a-z]+)/', $cValue, $matches)) { - $styles['borderSize'] = Converter::cssToPoint($matches[1]); - $styles['borderColor'] = trim($matches[2], '#'); - $styles['borderStyle'] = self::mapBorderStyle($matches[3]); + case 'border-top': + 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]+)/', $cValue, $matches)) { + if(false !== strpos($cKey, '-')){ + $which = explode('-', $cKey)[1]; + $which = ucfirst($which); // e.g. bottom -> Bottom + }else{ + $which = ''; + } + // normalization: in HTML 1px means tinest possible line width, so we cannot convert 1px -> 15 twips, coz line'd be bold, we use smallest twip instead + $size = strtolower(trim($matches[1])); + // (!) BC change: up to ver. 0.17.0 Converter was incorrectly converting to points - Converter::cssToPoint($matches[1]) + $size = ($size == '1px') ? 1 : Converter::cssToTwip($size); + // 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]); } break; } @@ -835,4 +853,39 @@ protected static function parseLink($node, $element, &$styles) return $element->addLink($target, $node->textContent, $styles['font'], $styles['paragraph']); } + + /** + * Render horizontal rule + * Note: Word rule is not the same as HTML's
since it does not support width and thus neither alignment + * + * @param \DOMNode $node + * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + */ + protected static function parseHorizRule($node, $element) + { + $styles = self::parseInlineStyle($node); + + //
is implemented as an empty paragraph - extending 100% inside the section + // Some properties may be controlled, e.g.
+ + $fontStyle = $styles + ['size' => 3]; + + $paragraphStyle = $styles + [ + 'lineHeight' => 0.25, // multiply default line height - e.g. 1, 1.5 etc + 'spacing' => 0, // twip + 'spaceBefore' => 120, // twip, 240/2 (default line height) + 'spaceAfter' => 120, // twip + 'borderBottomSize' => empty($styles['line-height']) ? 1 : $styles['line-height'], + 'borderBottomColor' => empty($styles['color']) ? '000000' : $styles['color'], + 'borderBottomStyle' => 'single', // same as "solid" + ]; + + $element->addText("", $fontStyle, $paragraphStyle); + + // Notes:
cannot be: + // - table - throws error "cannot be inside textruns", e.g. lists + // - line - that is a shape, has different behaviour + // - repeated text, e.g. underline "_", because of unpredictable line wrapping + } + } diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 61ebd5354d..52d641af41 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -702,6 +702,9 @@ public function testParseTableAndCellWidth() $this->assertEquals('dxa', $doc->getElement($xpath)->getAttribute('w:type')); } + /** + * Test parsing background color for table rows and table cellspacing + */ public function testParseCellspacingRowBgColor() { $phpWord = new \PhpOffice\PhpWord\PhpWord(); @@ -726,10 +729,6 @@ public function testParseCellspacingRowBgColor() Html::addHtml($section, $html); $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); - // uncomment to see results - file_put_contents('./table_src.html', $html); - file_put_contents('./table_result_'.time().'.docx', file_get_contents( TestHelperDOCX::getFile() ) ); - $xpath = '/w:document/w:body/w:tbl/w:tblPr/w:tblCellSpacing'; $this->assertTrue($doc->elementExists($xpath)); $this->assertEquals(3 * 15, $doc->getElement($xpath)->getAttribute('w:w')); @@ -744,4 +743,45 @@ public function testParseCellspacingRowBgColor() $this->assertEquals('FF0000', $doc->getElement($xpath)->getAttribute('w:fill')); } + /** + * Parse horizontal rule + */ + public function testParseHorizRule() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + + // borders & backgrounds are here just for better visual comparison + $html = <<Simple default rule:

+
+

Custom style rule:

+
+

END

+HTML; + + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + // default rule + $xpath = '/w:document/w:body/w:p[2]/w:pPr/w:pBdr/w:bottom'; + $this->assertTrue($doc->elementExists($xpath)); + $this->assertEquals('single', $doc->getElement($xpath)->getAttribute('w:val')); // solid + $this->assertEquals('1', $doc->getElement($xpath)->getAttribute('w:sz')); // 1 twip + $this->assertEquals('000000', $doc->getElement($xpath)->getAttribute('w:color')); // black + + // custom style rule + $xpath = '/w:document/w:body/w:p[4]/w:pPr/w:pBdr/w:bottom'; + $this->assertTrue($doc->elementExists($xpath)); + $this->assertEquals('single', $doc->getElement($xpath)->getAttribute('w:val')); + $this->assertEquals(5 * 15, $doc->getElement($xpath)->getAttribute('w:sz')); + $this->assertEquals('lightblue', $doc->getElement($xpath)->getAttribute('w:color')); + + $xpath = '/w:document/w:body/w:p[4]/w:pPr/w:spacing'; + $this->assertTrue($doc->elementExists($xpath)); + $this->assertEquals(22.5, $doc->getElement($xpath)->getAttribute('w:before')); + $this->assertEquals(0, $doc->getElement($xpath)->getAttribute('w:after')); + $this->assertEquals(240, $doc->getElement($xpath)->getAttribute('w:line')); + } + } From 108c1cdc558dc3e53f01526b7cd540e78d26a4b1 Mon Sep 17 00:00:00 2001 From: lubosdz Date: Sat, 11 Jul 2020 17:20:36 +0200 Subject: [PATCH 04/11] Html parser (addHtml) - support attributes start, type in ordered list
    --- src/PhpWord/Shared/Html.php | 48 ++++++++++++++++++++++++- tests/PhpWord/Shared/HtmlTest.php | 60 +++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 9e5d84b1b0..7e0848b111 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -447,7 +447,31 @@ protected static function parseList($node, $element, &$styles, &$data) } else { $data['listdepth'] = 0; $styles['list'] = 'listStyle_' . self::$listIndex++; - $element->getPhpWord()->addNumberingStyle($styles['list'], self::getListStyle($isOrderedList)); + $style = $element->getPhpWord()->addNumberingStyle($styles['list'], self::getListStyle($isOrderedList)); + + // extract attributes start & type e.g.
      + $start = 0; + $type = ''; + foreach ($node->attributes as $attribute) { + switch ($attribute->name) { + case 'start': + $start = (int) $attribute->value; + break; + case 'type': + $type = $attribute->value; + break; + } + } + + $levels = $style->getLevels(); + /** @var \PhpOffice\PhpWord\Style\NumberingLevel */ + $level = $levels[0]; + if($start > 0){ + $level->setStart($start); + } + if($type && !!($type = self::mapListType($type))){ + $level->setFormat($type); + } } if ($node->parentNode->nodeName === 'li') { return $element->getParent(); @@ -818,6 +842,28 @@ protected static function mapAlign($cssAlignment) } } + /** + * Map list style for ordered list + * + * @param string $cssListType + */ + protected static function mapListType($cssListType) + { + switch ($cssListType) { + case 'a': + return NumberFormat::LOWER_LETTER; // a, b, c, .. + case 'A': + return NumberFormat::UPPER_LETTER; // A, B, C, .. + case 'i': + 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, .. + } + } + /** * Parse line break * diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 52d641af41..2c1e2d0790 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -784,4 +784,64 @@ public function testParseHorizRule() $this->assertEquals(240, $doc->getElement($xpath)->getAttribute('w:line')); } + /** + * Parse ordered list start & numbering style + */ + public function testParseOrderedList() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + + // borders & backgrounds are here just for better visual comparison + $html = << +
    1. standard ordered list line 1
    2. +
    3. standard ordered list line 2
    4. +
    + +
      +
    1. ordered list alphabetical, line 5 => E
    2. +
    3. ordered list alphabetical, line 6 => F
    4. +
    + +
      +
    1. ordered list roman lower, line 3 => iii
    2. +
    3. ordered list roman lower, line 4 => iv
    4. +
    + +HTML; + + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + // compare numbering file + $xmlFile = 'word/numbering.xml'; + + // default - decimal start = 1 + $xpath = '/w:numbering/w:abstractNum[1]/w:lvl[1]/w:start'; + $this->assertTrue($doc->elementExists($xpath, $xmlFile)); + $this->assertEquals('1', $doc->getElement($xpath, $xmlFile)->getAttribute('w:val')); + + $xpath = '/w:numbering/w:abstractNum[1]/w:lvl[1]/w:numFmt'; + $this->assertTrue($doc->elementExists($xpath, $xmlFile)); + $this->assertEquals('decimal', $doc->getElement($xpath, $xmlFile)->getAttribute('w:val')); + + // second list - start = 5, type A = upperLetter + $xpath = '/w:numbering/w:abstractNum[2]/w:lvl[1]/w:start'; + $this->assertTrue($doc->elementExists($xpath, $xmlFile)); + $this->assertEquals('5', $doc->getElement($xpath, $xmlFile)->getAttribute('w:val')); + + $xpath = '/w:numbering/w:abstractNum[2]/w:lvl[1]/w:numFmt'; + $this->assertTrue($doc->elementExists($xpath, $xmlFile)); + $this->assertEquals('upperLetter', $doc->getElement($xpath, $xmlFile)->getAttribute('w:val')); + + // third list - start = 3, type i = lowerRoman + $xpath = '/w:numbering/w:abstractNum[3]/w:lvl[1]/w:start'; + $this->assertTrue($doc->elementExists($xpath, $xmlFile)); + $this->assertEquals('3', $doc->getElement($xpath, $xmlFile)->getAttribute('w:val')); + + $xpath = '/w:numbering/w:abstractNum[3]/w:lvl[1]/w:numFmt'; + $this->assertTrue($doc->elementExists($xpath, $xmlFile)); + $this->assertEquals('lowerRoman', $doc->getElement($xpath, $xmlFile)->getAttribute('w:val')); + } } From 3066d47003e18461001828f513db3778a652a76f Mon Sep 17 00:00:00 2001 From: lubosdz Date: Sat, 11 Jul 2020 22:47:40 +0200 Subject: [PATCH 05/11] Html parser (addHtml) - support vertical-align, valign --- src/PhpWord/Shared/Html.php | 46 ++++++++++++++++++++++++++++--- tests/PhpWord/Shared/HtmlTest.php | 44 +++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 7e0848b111..bf1a688bb4 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -96,19 +96,19 @@ protected static function parseInlineStyle($node, $styles = array()) $attributes = $node->attributes; // get all the attributes(eg: id, class) foreach ($attributes as $attribute) { + $val = trim($attribute->value); switch (strtolower($attribute->name)) { case 'style': $styles = self::parseStyle($attribute, $styles); break; case 'align': - $styles['alignment'] = self::mapAlign($attribute->value); + $styles['alignment'] = self::mapAlign($val); break; case 'lang': - $styles['lang'] = $attribute->value; + $styles['lang'] = $val; break; case 'width': // tables, cells - $val = trim($attribute->value); if(false !== strpos($val, '%')){ // e.g. or - $styles['bgColor'] = trim($attribute->value, '# '); + $styles['bgColor'] = trim($val, '# '); + break; + case 'valign': + // cells e.g. + + + + + + +
    $styles['width'] = intval($val) * 50; @@ -126,7 +126,13 @@ protected static function parseInlineStyle($node, $styles = array()) break; case 'bgcolor': // tables, rows, cells e.g.
    + if (preg_match('#(?:top|bottom|middle|baseline)#i', $val, $matches)) { + $styles['valign'] = self::mapAlignVertical($matches[0]); + } break; } } @@ -678,6 +684,12 @@ protected static function parseStyle($attribute, $styles) $styles["border{$which}Style"] = self::mapBorderStyle($matches[3]); } break; + case 'vertical-align': + // https://developer.mozilla.org/en-US/docs/Web/CSS/vertical-align + if (preg_match('#(?:top|bottom|middle|sub|baseline)#i', $cValue, $matches)) { + $styles['valign'] = self::mapAlignVertical($matches[0]); + } + break; } } @@ -842,6 +854,32 @@ protected static function mapAlign($cssAlignment) } } + /** + * Transforms a HTML/CSS alignment into a \PhpOffice\PhpWord\SimpleType\Jc + * + * @param string $cssAlignment + * @return string|null + */ + protected static function mapAlignVertical($alignment) + { + $alignment = strtolower($alignment); + switch ($alignment) { + case 'top': + case 'baseline': + case 'bottom': + return $alignment; + case 'middle': + return 'center'; + case 'sub': + return 'bottom'; + case 'text-top': + case 'baseline': + return 'top'; + default: + return ''; + } + } + /** * Map list style for ordered list * diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 2c1e2d0790..67c9fcaa89 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -844,4 +844,48 @@ public function testParseOrderedList() $this->assertTrue($doc->elementExists($xpath, $xmlFile)); $this->assertEquals('lowerRoman', $doc->getElement($xpath, $xmlFile)->getAttribute('w:val')); } + + /** + * Parse ordered list start & numbering style + */ + public function testParseVerticalAlign() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + + // borders & backgrounds are here just for better visual comparison + $html = << +
    defaulttopmiddlebottom






    +HTML; + + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + // uncomment to see results + file_put_contents('./table_src.html', $html); + file_put_contents('./table_result_'.time().'.docx', file_get_contents( TestHelperDOCX::getFile() ) ); + + $xpath = '/w:document/w:body/w:tbl/w:tr/w:tc[1]/w:tcPr/w:vAlign'; + $this->assertFalse($doc->elementExists($xpath)); + + $xpath = '/w:document/w:body/w:tbl/w:tr/w:tc[2]/w:tcPr/w:vAlign'; + $this->assertTrue($doc->elementExists($xpath)); + $this->assertEquals('top', $doc->getElement($xpath)->getAttribute('w:val')); + + $xpath = '/w:document/w:body/w:tbl/w:tr/w:tc[3]/w:tcPr/w:vAlign'; + $this->assertTrue($doc->elementExists($xpath)); + $this->assertEquals('center', $doc->getElement($xpath)->getAttribute('w:val')); + + $xpath = '/w:document/w:body/w:tbl/w:tr/w:tc[4]/w:tcPr/w:vAlign'; + $this->assertTrue($doc->elementExists($xpath)); + $this->assertEquals('bottom', $doc->getElement($xpath)->getAttribute('w:val')); + } } From 889f4e338138c1ba4279e7fc92d925cb4469a670 Mon Sep 17 00:00:00 2001 From: lubosdz Date: Sat, 11 Jul 2020 23:03:51 +0200 Subject: [PATCH 06/11] fix converting margin to incorrect unit (points instead of twips) fix image alignment on float - relative to inner margin instead of page margin --- src/PhpWord/Shared/Html.php | 17 ++++++++++++----- tests/PhpWord/Shared/HtmlTest.php | 6 +----- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index bf1a688bb4..332dd6f8a3 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -633,11 +633,18 @@ protected static function parseStyle($attribute, $styles) } $styles['italic'] = $tValue; break; + case 'margin': + $cValue = Converter::cssToTwip($cValue); + $styles['spaceBefore'] = $cValue; + $styles['spaceAfter'] = $cValue; + break; case 'margin-top': - $styles['spaceBefore'] = Converter::cssToPoint($cValue); + // BC change: up to ver. 0.17.0 incorrectly converted to points - Converter::cssToPoint($cValue) + $styles['spaceBefore'] = Converter::cssToTwip($cValue); break; case 'margin-bottom': - $styles['spaceAfter'] = Converter::cssToPoint($cValue); + // BC change: up to ver. 0.17.0 incorrectly converted to points - Converter::cssToPoint($cValue) + $styles['spaceAfter'] = Converter::cssToTwip($cValue); break; case 'border-color': self::mapBorderColor($styles, $cValue); @@ -676,7 +683,7 @@ protected static function parseStyle($attribute, $styles) } // normalization: in HTML 1px means tinest possible line width, so we cannot convert 1px -> 15 twips, coz line'd be bold, we use smallest twip instead $size = strtolower(trim($matches[1])); - // (!) BC change: up to ver. 0.17.0 Converter was incorrectly converting to points - Converter::cssToPoint($matches[1]) + // BC change: up to ver. 0.17.0 incorrectly converted to points - Converter::cssToPoint($size) $size = ($size == '1px') ? 1 : Converter::cssToTwip($size); // valid variants may be e.g. borderSize, borderTopSize, borderLeftColor, etc .. $styles["border{$which}Size"] = $size; // twips @@ -732,14 +739,14 @@ protected static function parseImage($node, $element) case 'float': if (trim($v) == 'right') { $style['hPos'] = \PhpOffice\PhpWord\Style\Image::POS_RIGHT; - $style['hPosRelTo'] = \PhpOffice\PhpWord\Style\Image::POS_RELTO_PAGE; + $style['hPosRelTo'] = \PhpOffice\PhpWord\Style\Image::POS_RELTO_MARGIN; // inner section area $style['pos'] = \PhpOffice\PhpWord\Style\Image::POS_RELATIVE; $style['wrap'] = \PhpOffice\PhpWord\Style\Image::WRAP_TIGHT; $style['overlap'] = true; } if (trim($v) == 'left') { $style['hPos'] = \PhpOffice\PhpWord\Style\Image::POS_LEFT; - $style['hPosRelTo'] = \PhpOffice\PhpWord\Style\Image::POS_RELTO_PAGE; + $style['hPosRelTo'] = \PhpOffice\PhpWord\Style\Image::POS_RELTO_MARGIN; // inner section area $style['pos'] = \PhpOffice\PhpWord\Style\Image::POS_RELATIVE; $style['wrap'] = \PhpOffice\PhpWord\Style\Image::WRAP_TIGHT; $style['overlap'] = true; diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 67c9fcaa89..010d1918a0 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -779,7 +779,7 @@ public function testParseHorizRule() $xpath = '/w:document/w:body/w:p[4]/w:pPr/w:spacing'; $this->assertTrue($doc->elementExists($xpath)); - $this->assertEquals(22.5, $doc->getElement($xpath)->getAttribute('w:before')); + $this->assertEquals(450, $doc->getElement($xpath)->getAttribute('w:before')); $this->assertEquals(0, $doc->getElement($xpath)->getAttribute('w:after')); $this->assertEquals(240, $doc->getElement($xpath)->getAttribute('w:line')); } @@ -869,10 +869,6 @@ public function testParseVerticalAlign() Html::addHtml($section, $html); $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); - // uncomment to see results - file_put_contents('./table_src.html', $html); - file_put_contents('./table_result_'.time().'.docx', file_get_contents( TestHelperDOCX::getFile() ) ); - $xpath = '/w:document/w:body/w:tbl/w:tr/w:tc[1]/w:tcPr/w:vAlign'; $this->assertFalse($doc->elementExists($xpath)); From 38788e0c7e1e5b68cd86b3db29a6f8389a44328f Mon Sep 17 00:00:00 2001 From: lubosdz Date: Mon, 13 Jul 2020 18:48:27 +0200 Subject: [PATCH 07/11] Code style --- src/PhpWord/Shared/Html.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 332dd6f8a3..7d45b4ba45 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -109,11 +109,11 @@ protected static function parseInlineStyle($node, $styles = array()) break; case 'width': // tables, cells - if(false !== strpos($val, '%')){ - // e.g. or
    + if (false !== strpos($val, '%')) { + // e.g. or
    $styles['width'] = intval($val) * 50; $styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT; - }else{ + } else { // e.g. getLevels(); /** @var \PhpOffice\PhpWord\Style\NumberingLevel */ $level = $levels[0]; - if($start > 0){ + if ($start > 0) { $level->setStart($start); } - if($type && !!($type = self::mapListType($type))){ + if ($type && !!($type = self::mapListType($type))) { $level->setFormat($type); } } @@ -675,10 +675,10 @@ protected static function parseStyle($attribute, $styles) // 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]+)/', $cValue, $matches)) { - if(false !== strpos($cKey, '-')){ + if (false !== strpos($cKey, '-')) { $which = explode('-', $cKey)[1]; $which = ucfirst($which); // e.g. bottom -> Bottom - }else{ + } else { $which = ''; } // normalization: in HTML 1px means tinest possible line width, so we cannot convert 1px -> 15 twips, coz line'd be bold, we use smallest twip instead @@ -883,6 +883,10 @@ protected static function mapAlignVertical($alignment) case 'baseline': return 'top'; default: + // @discuss - which one should apply: + // - Word uses default vert. alignment: top + // - all browsers use default vert. alignment: middle + // Returning empty string means attribute wont be set so use Word default (top). return ''; } } From 70ad01550bab55c6f1f6558a36945777a18ffb53 Mon Sep 17 00:00:00 2001 From: lubosdz Date: Mon, 13 Jul 2020 19:16:38 +0200 Subject: [PATCH 08/11] Make scrunitizer happier --- src/PhpWord/Shared/Html.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 7d45b4ba45..edd418bf79 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -96,13 +96,13 @@ protected static function parseInlineStyle($node, $styles = array()) $attributes = $node->attributes; // get all the attributes(eg: id, class) foreach ($attributes as $attribute) { - $val = trim($attribute->value); + $val = $attribute->value; switch (strtolower($attribute->name)) { case 'style': $styles = self::parseStyle($attribute, $styles); break; case 'align': - $styles['alignment'] = self::mapAlign($val); + $styles['alignment'] = self::mapAlign(trim($val)); break; case 'lang': $styles['lang'] = $val; @@ -121,7 +121,7 @@ protected static function parseInlineStyle($node, $styles = array()) break; case 'cellspacing': // tables e.g.
    , where "2" = 2px (always pixels) - $val = intval($attribute->value).'px'; + $val = intval($val).'px'; $styles['cellSpacing'] = Converter::cssToTwip($val); break; case 'bgcolor': @@ -475,7 +475,8 @@ protected static function parseList($node, $element, &$styles, &$data) if ($start > 0) { $level->setStart($start); } - if ($type && !!($type = self::mapListType($type))) { + $type = $type ? self::mapListType($type) : null; + if ($type) { $level->setFormat($type); } } From 4448bda7215c7389bec955988004c4d13a5ac482 Mon Sep 17 00:00:00 2001 From: lubosdz Date: Tue, 14 Jul 2020 01:31:16 +0200 Subject: [PATCH 09/11] Better normalization for width of borders --- src/PhpWord/Shared/Html.php | 20 ++++++++++++-------- tests/PhpWord/Shared/HtmlTest.php | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index edd418bf79..c8a7fa69ce 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -682,10 +682,14 @@ protected static function parseStyle($attribute, $styles) } else { $which = ''; } - // normalization: in HTML 1px means tinest possible line width, so we cannot convert 1px -> 15 twips, coz line'd be bold, we use smallest twip instead - $size = strtolower(trim($matches[1])); - // BC change: up to ver. 0.17.0 incorrectly converted to points - Converter::cssToPoint($size) - $size = ($size == '1px') ? 1 : Converter::cssToTwip($size); + // 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]); + $size = intval($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], '#'); @@ -884,10 +888,10 @@ protected static function mapAlignVertical($alignment) case 'baseline': return 'top'; default: - // @discuss - which one should apply: - // - Word uses default vert. alignment: top - // - all browsers use default vert. alignment: middle - // Returning empty string means attribute wont be set so use Word default (top). + // @discuss - which one should apply: + // - Word uses default vert. alignment: top + // - all browsers use default vert. alignment: middle + // Returning empty string means attribute wont be set so use Word default (top). return ''; } } diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 010d1918a0..93df933727 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -774,7 +774,7 @@ public function testParseHorizRule() $xpath = '/w:document/w:body/w:p[4]/w:pPr/w:pBdr/w:bottom'; $this->assertTrue($doc->elementExists($xpath)); $this->assertEquals('single', $doc->getElement($xpath)->getAttribute('w:val')); - $this->assertEquals(5 * 15, $doc->getElement($xpath)->getAttribute('w:sz')); + $this->assertEquals(intval(5 * 15 / 2), $doc->getElement($xpath)->getAttribute('w:sz')); $this->assertEquals('lightblue', $doc->getElement($xpath)->getAttribute('w:color')); $xpath = '/w:document/w:body/w:p[4]/w:pPr/w:spacing'; From f69885e7b92e4e149c7a535aaa6dabf5c4d57117 Mon Sep 17 00:00:00 2001 From: lubosdz Date: Wed, 22 Jul 2020 10:04:12 +0200 Subject: [PATCH 10/11] fix bug - don't decode double quotes inside double quoted string --- src/PhpWord/Shared/Html.php | 4 ++-- tests/PhpWord/Shared/HtmlTest.php | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index c8a7fa69ce..d3e452e45f 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -62,10 +62,10 @@ public static function addHtml($element, $html, $fullHTML = false, $preserveWhit // Preprocess: remove all line ends, decode HTML entity, // fix ampersand and angle brackets and add body tag for HTML fragments $html = str_replace(array("\n", "\r"), '', $html); - $html = str_replace(array('<', '>', '&'), array('_lt_', '_gt_', '_amp_'), $html); + $html = str_replace(array('<', '>', '&', '"'), array('_lt_', '_gt_', '_amp_', '_quot_'), $html); $html = html_entity_decode($html, ENT_QUOTES, 'UTF-8'); $html = str_replace('&', '&', $html); - $html = str_replace(array('_lt_', '_gt_', '_amp_'), array('<', '>', '&'), $html); + $html = str_replace(array('_lt_', '_gt_', '_amp_', '_quot_'), array('<', '>', '&', '"'), $html); if (false === $fullHTML) { $html = '' . $html . ''; diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 93df933727..7a806c2624 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -884,4 +884,22 @@ public function testParseVerticalAlign() $this->assertTrue($doc->elementExists($xpath)); $this->assertEquals('bottom', $doc->getElement($xpath)->getAttribute('w:val')); } + + /** + * Fix bug - don't decode double quotes inside double quoted string + */ + public function testDontDecodeAlreadyEncodedDoubleQuotes() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + + // borders & backgrounds are here just for better visual comparison + $html = <<This would crash if inline quotes also decoded at loading XML into DOMDocument! +HTML; + + Html::addHtml($section, $html); + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + $this->assertTrue(is_object($doc)); + } } From 69632b3f0333cbdbd51f4c71cdbce67240144b2f Mon Sep 17 00:00:00 2001 From: lubosdz Date: Wed, 22 Jul 2020 10:46:23 +0200 Subject: [PATCH 11/11] remove extra line at the end, which possibly causes CI job fail --- src/PhpWord/Shared/Html.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index d3e452e45f..04200b31a5 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -987,5 +987,4 @@ protected static function parseHorizRule($node, $element) // - line - that is a shape, has different behaviour // - repeated text, e.g. underline "_", because of unpredictable line wrapping } - }