Skip to content

Commit ca5f081

Browse files
committed
Html parser (addHtml) - support horizontal rule <hr/>
1 parent e180cfe commit ca5f081

File tree

2 files changed

+101
-8
lines changed

2 files changed

+101
-8
lines changed

src/PhpWord/Shared/Html.php

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ protected static function parseNode($node, $element, $styles = array(), $data =
183183
'img' => array('Image', $node, $element, $styles, null, null, null),
184184
'br' => array('LineBreak', null, $element, $styles, null, null, null),
185185
'a' => array('Link', $node, $element, $styles, null, null, null),
186+
'hr' => array('HorizRule', $node, $element, $styles, null, null, null),
186187
);
187188

188189
$newElement = null;
@@ -630,10 +631,27 @@ protected static function parseStyle($attribute, $styles)
630631
}
631632
break;
632633
case 'border':
633-
if (preg_match('/([0-9]+[^0-9]*)\s+(\#[a-fA-F0-9]+)\s+([a-z]+)/', $cValue, $matches)) {
634-
$styles['borderSize'] = Converter::cssToPoint($matches[1]);
635-
$styles['borderColor'] = trim($matches[2], '#');
636-
$styles['borderStyle'] = self::mapBorderStyle($matches[3]);
634+
case 'border-top':
635+
case 'border-bottom':
636+
case 'border-right':
637+
case 'border-left':
638+
// must have exact order [width color style], e.g. "1px #0011CC solid" or "2pt green solid"
639+
// Word does not accept shortened hex colors e.g. #CCC, only full e.g. #CCCCCC
640+
if (preg_match('/([0-9]+[^0-9]*)\s+(\#[a-fA-F0-9]+|[a-zA-Z]+)\s+([a-z]+)/', $cValue, $matches)) {
641+
if(false !== strpos($cKey, '-')){
642+
$which = explode('-', $cKey)[1];
643+
$which = ucfirst($which); // e.g. bottom -> Bottom
644+
}else{
645+
$which = '';
646+
}
647+
// 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
648+
$size = strtolower(trim($matches[1]));
649+
// (!) BC change: up to ver. 0.17.0 Converter was incorrectly converting to points - Converter::cssToPoint($matches[1])
650+
$size = ($size == '1px') ? 1 : Converter::cssToTwip($size);
651+
// valid variants may be e.g. borderSize, borderTopSize, borderLeftColor, etc ..
652+
$styles["border{$which}Size"] = $size; // twips
653+
$styles["border{$which}Color"] = trim($matches[2], '#');
654+
$styles["border{$which}Style"] = self::mapBorderStyle($matches[3]);
637655
}
638656
break;
639657
}
@@ -835,4 +853,39 @@ protected static function parseLink($node, $element, &$styles)
835853

836854
return $element->addLink($target, $node->textContent, $styles['font'], $styles['paragraph']);
837855
}
856+
857+
/**
858+
* Render horizontal rule
859+
* Note: Word rule is not the same as HTML's <hr> since it does not support width and thus neither alignment
860+
*
861+
* @param \DOMNode $node
862+
* @param \PhpOffice\PhpWord\Element\AbstractContainer $element
863+
*/
864+
protected static function parseHorizRule($node, $element)
865+
{
866+
$styles = self::parseInlineStyle($node);
867+
868+
// <hr> is implemented as an empty paragraph - extending 100% inside the section
869+
// Some properties may be controlled, e.g. <hr style="border-bottom: 3px #DDDDDD solid; margin-bottom: 0;">
870+
871+
$fontStyle = $styles + ['size' => 3];
872+
873+
$paragraphStyle = $styles + [
874+
'lineHeight' => 0.25, // multiply default line height - e.g. 1, 1.5 etc
875+
'spacing' => 0, // twip
876+
'spaceBefore' => 120, // twip, 240/2 (default line height)
877+
'spaceAfter' => 120, // twip
878+
'borderBottomSize' => empty($styles['line-height']) ? 1 : $styles['line-height'],
879+
'borderBottomColor' => empty($styles['color']) ? '000000' : $styles['color'],
880+
'borderBottomStyle' => 'single', // same as "solid"
881+
];
882+
883+
$element->addText("", $fontStyle, $paragraphStyle);
884+
885+
// Notes: <hr/> cannot be:
886+
// - table - throws error "cannot be inside textruns", e.g. lists
887+
// - line - that is a shape, has different behaviour
888+
// - repeated text, e.g. underline "_", because of unpredictable line wrapping
889+
}
890+
838891
}

tests/PhpWord/Shared/HtmlTest.php

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,9 @@ public function testParseTableAndCellWidth()
702702
$this->assertEquals('dxa', $doc->getElement($xpath)->getAttribute('w:type'));
703703
}
704704

705+
/**
706+
* Test parsing background color for table rows and table cellspacing
707+
*/
705708
public function testParseCellspacingRowBgColor()
706709
{
707710
$phpWord = new \PhpOffice\PhpWord\PhpWord();
@@ -726,10 +729,6 @@ public function testParseCellspacingRowBgColor()
726729
Html::addHtml($section, $html);
727730
$doc = TestHelperDOCX::getDocument($phpWord, 'Word2007');
728731

729-
// uncomment to see results
730-
file_put_contents('./table_src.html', $html);
731-
file_put_contents('./table_result_'.time().'.docx', file_get_contents( TestHelperDOCX::getFile() ) );
732-
733732
$xpath = '/w:document/w:body/w:tbl/w:tblPr/w:tblCellSpacing';
734733
$this->assertTrue($doc->elementExists($xpath));
735734
$this->assertEquals(3 * 15, $doc->getElement($xpath)->getAttribute('w:w'));
@@ -744,4 +743,45 @@ public function testParseCellspacingRowBgColor()
744743
$this->assertEquals('FF0000', $doc->getElement($xpath)->getAttribute('w:fill'));
745744
}
746745

746+
/**
747+
* Parse horizontal rule
748+
*/
749+
public function testParseHorizRule()
750+
{
751+
$phpWord = new \PhpOffice\PhpWord\PhpWord();
752+
$section = $phpWord->addSection();
753+
754+
// borders & backgrounds are here just for better visual comparison
755+
$html = <<<HTML
756+
<p>Simple default rule:</p>
757+
<hr/>
758+
<p>Custom style rule:</p>
759+
<hr style="margin-top: 30px; margin-bottom: 0; border-bottom: 5px lightblue solid;" />
760+
<p>END</p>
761+
HTML;
762+
763+
Html::addHtml($section, $html);
764+
$doc = TestHelperDOCX::getDocument($phpWord, 'Word2007');
765+
766+
// default rule
767+
$xpath = '/w:document/w:body/w:p[2]/w:pPr/w:pBdr/w:bottom';
768+
$this->assertTrue($doc->elementExists($xpath));
769+
$this->assertEquals('single', $doc->getElement($xpath)->getAttribute('w:val')); // solid
770+
$this->assertEquals('1', $doc->getElement($xpath)->getAttribute('w:sz')); // 1 twip
771+
$this->assertEquals('000000', $doc->getElement($xpath)->getAttribute('w:color')); // black
772+
773+
// custom style rule
774+
$xpath = '/w:document/w:body/w:p[4]/w:pPr/w:pBdr/w:bottom';
775+
$this->assertTrue($doc->elementExists($xpath));
776+
$this->assertEquals('single', $doc->getElement($xpath)->getAttribute('w:val'));
777+
$this->assertEquals(5 * 15, $doc->getElement($xpath)->getAttribute('w:sz'));
778+
$this->assertEquals('lightblue', $doc->getElement($xpath)->getAttribute('w:color'));
779+
780+
$xpath = '/w:document/w:body/w:p[4]/w:pPr/w:spacing';
781+
$this->assertTrue($doc->elementExists($xpath));
782+
$this->assertEquals(22.5, $doc->getElement($xpath)->getAttribute('w:before'));
783+
$this->assertEquals(0, $doc->getElement($xpath)->getAttribute('w:after'));
784+
$this->assertEquals(240, $doc->getElement($xpath)->getAttribute('w:line'));
785+
}
786+
747787
}

0 commit comments

Comments
 (0)