123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059 |
- <?php
- /**
- * This file is part of PHPWord - A pure PHP library for reading and writing
- * word processing documents.
- *
- * PHPWord is free software distributed under the terms of the GNU Lesser
- * General Public License version 3 as published by the Free Software Foundation.
- *
- * For the full copyright and license information, please read the LICENSE
- * file that was distributed with this source code. For the full list of
- * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
- *
- * @see https://github.com/PHPOffice/PHPWord
- * @copyright 2010-2018 PHPWord contributors
- * @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
- */
- namespace PhpOffice\PhpWord\Shared;
- use PhpOffice\PhpWord\Element\AbstractContainer;
- use PhpOffice\PhpWord\Element\Row;
- use PhpOffice\PhpWord\Element\Table;
- use PhpOffice\PhpWord\Settings;
- use PhpOffice\PhpWord\SimpleType\Jc;
- use PhpOffice\PhpWord\SimpleType\NumberFormat;
- use PhpOffice\PhpWord\Style\Paragraph;
- /**
- * Common Html functions
- *
- * @SuppressWarnings(PHPMD.UnusedPrivateMethod) For readWPNode
- */
- class Html
- {
- protected static $listIndex = 0;
- protected static $xpath;
- protected static $options;
- /**
- * Add HTML parts.
- *
- * Note: $stylesheet parameter is removed to avoid PHPMD error for unused parameter
- * Warning: Do not pass user-generated HTML here, as that would allow an attacker to read arbitrary
- * files or perform server-side request forgery by passing local file paths or URLs in <img>.
- *
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element Where the parts need to be added
- * @param string $html The code to parse
- * @param bool $fullHTML If it's a full HTML, no need to add 'body' tag
- * @param bool $preserveWhiteSpace If false, the whitespaces between nodes will be removed
- * @param array $options:
- * + IMG_SRC_SEARCH: optional to speed up images loading from remote url when files can be found locally
- * + IMG_SRC_REPLACE: optional to speed up images loading from remote url when files can be found locally
- */
- public static function addHtml($element, $html, $fullHTML = false, $preserveWhiteSpace = true, $options = null)
- {
- /*
- * @todo parse $stylesheet for default styles. Should result in an array based on id, class and element,
- * which could be applied when such an element occurs in the parseNode function.
- */
- self::$options = $options;
- // 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_', '_quot_'), $html);
- $html = html_entity_decode($html, ENT_QUOTES, 'UTF-8');
- $html = str_replace('&', '&', $html);
- $html = str_replace(array('_lt_', '_gt_', '_amp_', '_quot_'), array('<', '>', '&', '"'), $html);
- if (false === $fullHTML) {
- $html = '<body>' . $html . '</body>';
- }
- // Load DOM
- if (\PHP_VERSION_ID < 80000) {
- $orignalLibEntityLoader = libxml_disable_entity_loader(true);
- }
- $dom = new \DOMDocument();
- $dom->preserveWhiteSpace = $preserveWhiteSpace;
- $dom->loadXML($html);
- self::$xpath = new \DOMXPath($dom);
- $node = $dom->getElementsByTagName('body');
- self::parseNode($node->item(0), $element);
- if (\PHP_VERSION_ID < 80000) {
- libxml_disable_entity_loader($orignalLibEntityLoader);
- }
- }
- /**
- * parse Inline style of a node
- *
- * @param \DOMNode $node Node to check on attributes and to compile a style array
- * @param array $styles is supplied, the inline style attributes are added to the already existing style
- * @return array
- */
- protected static function parseInlineStyle($node, $styles = array())
- {
- if (XML_ELEMENT_NODE == $node->nodeType) {
- $attributes = $node->attributes; // get all the attributes(eg: id, class)
- foreach ($attributes as $attribute) {
- $val = $attribute->value;
- switch (strtolower($attribute->name)) {
- case 'style':
- $styles = self::parseStyle($attribute, $styles);
- break;
- case 'align':
- $styles['alignment'] = self::mapAlign(trim($val));
- break;
- case 'lang':
- $styles['lang'] = $val;
- break;
- case 'width':
- // tables, cells
- if (false !== strpos($val, '%')) {
- // e.g. <table width="100%"> or <td width="50%">
- $styles['width'] = (int) $val * 50;
- $styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT;
- } else {
- // e.g. <table width="250> where "250" = 250px (always pixels)
- $styles['width'] = Converter::pixelToTwip($val);
- $styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP;
- }
- break;
- case 'cellspacing':
- // tables e.g. <table cellspacing="2">, where "2" = 2px (always pixels)
- $val = (int) $val . 'px';
- $styles['cellSpacing'] = Converter::cssToTwip($val);
- break;
- case 'bgcolor':
- // tables, rows, cells e.g. <tr bgColor="#FF0000">
- $styles['bgColor'] = trim($val, '# ');
- break;
- case 'valign':
- // cells e.g. <td valign="middle">
- if (preg_match('#(?:top|bottom|middle|baseline)#i', $val, $matches)) {
- $styles['valign'] = self::mapAlignVertical($matches[0]);
- }
- break;
- }
- }
- }
- return $styles;
- }
- /**
- * Parse a node and add a corresponding element to the parent element.
- *
- * @param \DOMNode $node node to parse
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element object to add an element corresponding with the node
- * @param array $styles Array with all styles
- * @param array $data Array to transport data to a next level in the DOM tree, for example level of listitems
- */
- protected static function parseNode($node, $element, $styles = array(), $data = array())
- {
- // Populate styles array
- $styleTypes = array('font', 'paragraph', 'list', 'table', 'row', 'cell');
- foreach ($styleTypes as $styleType) {
- if (!isset($styles[$styleType])) {
- $styles[$styleType] = array();
- }
- }
- // Node mapping table
- $nodes = array(
- // $method $node $element $styles $data $argument1 $argument2
- 'p' => array('Paragraph', $node, $element, $styles, null, null, null),
- 'h1' => array('Heading', null, $element, $styles, null, 'Heading1', null),
- 'h2' => array('Heading', null, $element, $styles, null, 'Heading2', null),
- 'h3' => array('Heading', null, $element, $styles, null, 'Heading3', null),
- 'h4' => array('Heading', null, $element, $styles, null, 'Heading4', null),
- 'h5' => array('Heading', null, $element, $styles, null, 'Heading5', null),
- 'h6' => array('Heading', null, $element, $styles, null, 'Heading6', null),
- '#text' => array('Text', $node, $element, $styles, null, null, null),
- 'strong' => array('Property', null, null, $styles, null, 'bold', true),
- 'b' => array('Property', null, null, $styles, null, 'bold', true),
- 'em' => array('Property', null, null, $styles, null, 'italic', true),
- 'i' => array('Property', null, null, $styles, null, 'italic', true),
- 'u' => array('Property', null, null, $styles, null, 'underline', 'single'),
- 'sup' => array('Property', null, null, $styles, null, 'superScript', true),
- 'sub' => array('Property', null, null, $styles, null, 'subScript', true),
- 'span' => array('Span', $node, null, $styles, null, null, null),
- 'font' => array('Span', $node, null, $styles, null, null, null),
- 'table' => array('Table', $node, $element, $styles, null, null, null),
- 'tr' => array('Row', $node, $element, $styles, null, null, null),
- 'td' => array('Cell', $node, $element, $styles, null, null, null),
- 'th' => array('Cell', $node, $element, $styles, null, null, null),
- 'ul' => array('List', $node, $element, $styles, $data, null, null),
- 'ol' => array('List', $node, $element, $styles, $data, null, null),
- 'li' => array('ListItem', $node, $element, $styles, $data, null, null),
- '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),
- 'input' => array('Input', $node, $element, $styles, null, null, null),
- 'hr' => array('HorizRule', $node, $element, $styles, null, null, null),
- );
- $newElement = null;
- $keys = array('node', 'element', 'styles', 'data', 'argument1', 'argument2');
- if (isset($nodes[$node->nodeName])) {
- // Execute method based on node mapping table and return $newElement or null
- // Arguments are passed by reference
- $arguments = array();
- $args = array();
- list($method, $args[0], $args[1], $args[2], $args[3], $args[4], $args[5]) = $nodes[$node->nodeName];
- for ($i = 0; $i <= 5; $i++) {
- if ($args[$i] !== null) {
- $arguments[$keys[$i]] = &$args[$i];
- }
- }
- $method = "parse{$method}";
- $newElement = call_user_func_array(array('PhpOffice\PhpWord\Shared\Html', $method), array_values($arguments));
- // Retrieve back variables from arguments
- foreach ($keys as $key) {
- if (array_key_exists($key, $arguments)) {
- $$key = $arguments[$key];
- }
- }
- }
- if ($newElement === null) {
- $newElement = $element;
- }
- static::parseChildNodes($node, $newElement, $styles, $data);
- }
- /**
- * Parse child nodes.
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- * @param array $styles
- * @param array $data
- */
- protected static function parseChildNodes($node, $element, $styles, $data)
- {
- if ('li' != $node->nodeName) {
- $cNodes = $node->childNodes;
- if (!empty($cNodes)) {
- foreach ($cNodes as $cNode) {
- if ($element instanceof AbstractContainer || $element instanceof Table || $element instanceof Row) {
- self::parseNode($cNode, $element, $styles, $data);
- }
- }
- }
- }
- }
- /**
- * Parse paragraph node
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- * @param array &$styles
- * @return \PhpOffice\PhpWord\Element\TextRun
- */
- protected static function parseParagraph($node, $element, &$styles)
- {
- $styles['paragraph'] = self::recursiveParseStylesInHierarchy($node, $styles['paragraph']);
- $newElement = $element->addTextRun($styles['paragraph']);
- return $newElement;
- }
- /**
- * Parse input node
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- * @param array &$styles
- */
- protected static function parseInput($node, $element, &$styles)
- {
- $attributes = $node->attributes;
- if (null === $attributes->getNamedItem('type')) {
- return;
- }
- $inputType = $attributes->getNamedItem('type')->value;
- switch ($inputType) {
- case 'checkbox':
- $checked = ($checked = $attributes->getNamedItem('checked')) && $checked->value === 'true' ? true : false;
- $textrun = $element->addTextRun($styles['paragraph']);
- $textrun->addFormField('checkbox')->setValue($checked);
- break;
- }
- }
- /**
- * Parse heading node
- *
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- * @param array &$styles
- * @param string $argument1 Name of heading style
- * @return \PhpOffice\PhpWord\Element\TextRun
- *
- * @todo Think of a clever way of defining header styles, now it is only based on the assumption, that
- * Heading1 - Heading6 are already defined somewhere
- */
- protected static function parseHeading($element, &$styles, $argument1)
- {
- $styles['paragraph'] = $argument1;
- $newElement = $element->addTextRun($styles['paragraph']);
- return $newElement;
- }
- /**
- * Parse text node
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- * @param array &$styles
- */
- protected static function parseText($node, $element, &$styles)
- {
- $styles['font'] = self::recursiveParseStylesInHierarchy($node, $styles['font']);
- //alignment applies on paragraph, not on font. Let's copy it there
- if (isset($styles['font']['alignment']) && is_array($styles['paragraph'])) {
- $styles['paragraph']['alignment'] = $styles['font']['alignment'];
- }
- if (is_callable(array($element, 'addText'))) {
- $element->addText($node->nodeValue, $styles['font'], $styles['paragraph']);
- }
- }
- /**
- * Parse property node
- *
- * @param array &$styles
- * @param string $argument1 Style name
- * @param string $argument2 Style value
- */
- protected static function parseProperty(&$styles, $argument1, $argument2)
- {
- $styles['font'][$argument1] = $argument2;
- }
- /**
- * Parse span node
- *
- * @param \DOMNode $node
- * @param array &$styles
- */
- protected static function parseSpan($node, &$styles)
- {
- self::parseInlineStyle($node, $styles['font']);
- }
- /**
- * Parse table node
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- * @param array &$styles
- * @return Table $element
- *
- * @todo As soon as TableItem, RowItem and CellItem support relative width and height
- */
- protected static function parseTable($node, $element, &$styles)
- {
- $elementStyles = self::parseInlineStyle($node, $styles['table']);
- $newElement = $element->addTable($elementStyles);
- // $attributes = $node->attributes;
- // if ($attributes->getNamedItem('width') !== null) {
- // $newElement->setWidth($attributes->getNamedItem('width')->value);
- // }
- // if ($attributes->getNamedItem('height') !== null) {
- // $newElement->setHeight($attributes->getNamedItem('height')->value);
- // }
- // if ($attributes->getNamedItem('width') !== null) {
- // $newElement=$element->addCell($width=$attributes->getNamedItem('width')->value);
- // }
- return $newElement;
- }
- /**
- * Parse a table row
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\Table $element
- * @param array &$styles
- * @return Row $element
- */
- protected static function parseRow($node, $element, &$styles)
- {
- $rowStyles = self::parseInlineStyle($node, $styles['row']);
- if ($node->parentNode->nodeName == 'thead') {
- $rowStyles['tblHeader'] = true;
- }
- return $element->addRow(null, $rowStyles);
- }
- /**
- * Parse table cell
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\Table $element
- * @param array &$styles
- * @return \PhpOffice\PhpWord\Element\Cell|\PhpOffice\PhpWord\Element\TextRun $element
- */
- protected static function parseCell($node, $element, &$styles)
- {
- $cellStyles = self::recursiveParseStylesInHierarchy($node, $styles['cell']);
- $colspan = $node->getAttribute('colspan');
- if (!empty($colspan)) {
- $cellStyles['gridSpan'] = $colspan - 0;
- }
- // 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::filterOutNonInheritedStyles(self::parseInlineStyle($node, $styles['paragraph'])));
- }
- return $cell;
- }
- /**
- * Checks if $node contains an HTML element that cannot be added to TextRun
- *
- * @param \DOMNode $node
- * @return bool Returns true if the node contains an HTML element that cannot be added to TextRun
- */
- protected static function shouldAddTextRun(\DOMNode $node)
- {
- $containsBlockElement = self::$xpath->query('.//table|./p|./ul|./ol', $node)->length > 0;
- if ($containsBlockElement) {
- return false;
- }
- return true;
- }
- /**
- * Recursively parses styles on parent nodes
- * TODO if too slow, add caching of parent nodes, !! everything is static here so watch out for concurrency !!
- *
- * @param \DOMNode $node
- * @param array &$styles
- */
- protected static function recursiveParseStylesInHierarchy(\DOMNode $node, array $style)
- {
- $parentStyle = array();
- if ($node->parentNode != null && XML_ELEMENT_NODE == $node->parentNode->nodeType) {
- $parentStyle = self::recursiveParseStylesInHierarchy($node->parentNode, array());
- }
- if ($node->nodeName === '#text') {
- $parentStyle = array_merge($parentStyle, $style);
- } else {
- $parentStyle = self::filterOutNonInheritedStyles($parentStyle);
- }
- $style = self::parseInlineStyle($node, $parentStyle);
- return $style;
- }
- /**
- * Removes non-inherited styles from array
- *
- * @param array &$styles
- */
- protected static function filterOutNonInheritedStyles(array $styles)
- {
- $nonInheritedStyles = array(
- 'borderSize',
- 'borderTopSize',
- 'borderRightSize',
- 'borderBottomSize',
- 'borderLeftSize',
- 'borderColor',
- 'borderTopColor',
- 'borderRightColor',
- 'borderBottomColor',
- 'borderLeftColor',
- 'borderStyle',
- 'spaceAfter',
- 'spaceBefore',
- 'underline',
- 'strikethrough',
- 'hidden',
- );
- $styles = array_diff_key($styles, array_flip($nonInheritedStyles));
- return $styles;
- }
- /**
- * Parse list node
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- * @param array &$styles
- * @param array &$data
- */
- protected static function parseList($node, $element, &$styles, &$data)
- {
- $isOrderedList = $node->nodeName === 'ol';
- if (isset($data['listdepth'])) {
- $data['listdepth']++;
- } else {
- $data['listdepth'] = 0;
- $styles['list'] = 'listStyle_' . self::$listIndex++;
- $style = $element->getPhpWord()->addNumberingStyle($styles['list'], self::getListStyle($isOrderedList));
- // extract attributes start & type e.g. <ol type="A" start="3">
- $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);
- }
- $type = $type ? self::mapListType($type) : null;
- if ($type) {
- $level->setFormat($type);
- }
- }
- if ($node->parentNode->nodeName === 'li') {
- return $element->getParent();
- }
- }
- /**
- * @param bool $isOrderedList
- * @return array
- */
- protected static function getListStyle($isOrderedList)
- {
- if ($isOrderedList) {
- return array(
- 'type' => 'multilevel',
- 'levels' => array(
- array('format' => NumberFormat::DECIMAL, 'text' => '%1.', 'alignment' => 'left', 'tabPos' => 720, 'left' => 720, 'hanging' => 360),
- array('format' => NumberFormat::LOWER_LETTER, 'text' => '%2.', 'alignment' => 'left', 'tabPos' => 1440, 'left' => 1440, 'hanging' => 360),
- array('format' => NumberFormat::LOWER_ROMAN, 'text' => '%3.', 'alignment' => 'right', 'tabPos' => 2160, 'left' => 2160, 'hanging' => 180),
- array('format' => NumberFormat::DECIMAL, 'text' => '%4.', 'alignment' => 'left', 'tabPos' => 2880, 'left' => 2880, 'hanging' => 360),
- array('format' => NumberFormat::LOWER_LETTER, 'text' => '%5.', 'alignment' => 'left', 'tabPos' => 3600, 'left' => 3600, 'hanging' => 360),
- array('format' => NumberFormat::LOWER_ROMAN, 'text' => '%6.', 'alignment' => 'right', 'tabPos' => 4320, 'left' => 4320, 'hanging' => 180),
- array('format' => NumberFormat::DECIMAL, 'text' => '%7.', 'alignment' => 'left', 'tabPos' => 5040, 'left' => 5040, 'hanging' => 360),
- array('format' => NumberFormat::LOWER_LETTER, 'text' => '%8.', 'alignment' => 'left', 'tabPos' => 5760, 'left' => 5760, 'hanging' => 360),
- array('format' => NumberFormat::LOWER_ROMAN, 'text' => '%9.', 'alignment' => 'right', 'tabPos' => 6480, 'left' => 6480, 'hanging' => 180),
- ),
- );
- }
- return array(
- 'type' => 'hybridMultilevel',
- 'levels' => array(
- array('format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 720, 'left' => 720, 'hanging' => 360, 'font' => 'Symbol', 'hint' => 'default'),
- array('format' => NumberFormat::BULLET, 'text' => 'o', 'alignment' => 'left', 'tabPos' => 1440, 'left' => 1440, 'hanging' => 360, 'font' => 'Courier New', 'hint' => 'default'),
- array('format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 2160, 'left' => 2160, 'hanging' => 360, 'font' => 'Wingdings', 'hint' => 'default'),
- array('format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 2880, 'left' => 2880, 'hanging' => 360, 'font' => 'Symbol', 'hint' => 'default'),
- array('format' => NumberFormat::BULLET, 'text' => 'o', 'alignment' => 'left', 'tabPos' => 3600, 'left' => 3600, 'hanging' => 360, 'font' => 'Courier New', 'hint' => 'default'),
- array('format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 4320, 'left' => 4320, 'hanging' => 360, 'font' => 'Wingdings', 'hint' => 'default'),
- array('format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 5040, 'left' => 5040, 'hanging' => 360, 'font' => 'Symbol', 'hint' => 'default'),
- array('format' => NumberFormat::BULLET, 'text' => 'o', 'alignment' => 'left', 'tabPos' => 5760, 'left' => 5760, 'hanging' => 360, 'font' => 'Courier New', 'hint' => 'default'),
- array('format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 6480, 'left' => 6480, 'hanging' => 360, 'font' => 'Wingdings', 'hint' => 'default'),
- ),
- );
- }
- /**
- * Parse list item node
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- * @param array &$styles
- * @param array $data
- *
- * @todo This function is almost the same like `parseChildNodes`. Merged?
- * @todo As soon as ListItem inherits from AbstractContainer or TextRun delete parsing part of childNodes
- */
- protected static function parseListItem($node, $element, &$styles, $data)
- {
- $cNodes = $node->childNodes;
- if (!empty($cNodes)) {
- $listRun = $element->addListItemRun($data['listdepth'], $styles['list'], $styles['paragraph']);
- foreach ($cNodes as $cNode) {
- self::parseNode($cNode, $listRun, $styles, $data);
- }
- }
- }
- /**
- * Parse style
- *
- * @param \DOMAttr $attribute
- * @param array $styles
- * @return array
- */
- protected static function parseStyle($attribute, $styles)
- {
- $properties = explode(';', trim($attribute->value, " \t\n\r\0\x0B;"));
- foreach ($properties as $property) {
- list($cKey, $cValue) = array_pad(explode(':', $property, 2), 2, null);
- $cValue = trim($cValue);
- $cKey = strtolower(trim($cKey));
- switch ($cKey) {
- case 'text-decoration':
- switch ($cValue) {
- case 'underline':
- $styles['underline'] = 'single';
- break;
- case 'line-through':
- $styles['strikethrough'] = true;
- break;
- }
- break;
- case 'text-align':
- $styles['alignment'] = self::mapAlign($cValue);
- break;
- case 'display':
- $styles['hidden'] = $cValue === 'none' || $cValue === 'hidden';
- break;
- case 'direction':
- $styles['rtl'] = $cValue === 'rtl';
- break;
- case 'font-size':
- $styles['size'] = Converter::cssToPoint($cValue);
- break;
- case 'font-family':
- $cValue = array_map('trim', explode(',', $cValue));
- $styles['name'] = ucwords($cValue[0]);
- break;
- case 'color':
- $styles['color'] = trim($cValue, '#');
- break;
- case 'background-color':
- $styles['bgColor'] = trim($cValue, '#');
- break;
- case 'line-height':
- $matches = array();
- if ($cValue === 'normal') {
- $spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::AUTO;
- $spacing = 0;
- } elseif (preg_match('/([0-9]+\.?[0-9]*[a-z]+)/', $cValue, $matches)) {
- //matches number with a unit, e.g. 12px, 15pt, 20mm, ...
- $spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::EXACT;
- $spacing = Converter::cssToTwip($matches[1]);
- } elseif (preg_match('/([0-9]+)%/', $cValue, $matches)) {
- //matches percentages
- $spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::AUTO;
- //we are subtracting 1 line height because the Spacing writer is adding one line
- $spacing = ((((int) $matches[1]) / 100) * Paragraph::LINE_HEIGHT) - Paragraph::LINE_HEIGHT;
- } else {
- //any other, wich is a multiplier. E.g. 1.2
- $spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::AUTO;
- //we are subtracting 1 line height because the Spacing writer is adding one line
- $spacing = ($cValue * Paragraph::LINE_HEIGHT) - Paragraph::LINE_HEIGHT;
- }
- $styles['spacingLineRule'] = $spacingLineRule;
- $styles['line-spacing'] = $spacing;
- break;
- case 'letter-spacing':
- $styles['letter-spacing'] = Converter::cssToTwip($cValue);
- break;
- case 'text-indent':
- $styles['indentation']['firstLine'] = Converter::cssToTwip($cValue);
- break;
- case 'font-weight':
- $tValue = false;
- if (preg_match('#bold#', $cValue)) {
- $tValue = true; // also match bolder
- }
- $styles['bold'] = $tValue;
- break;
- case 'font-style':
- $tValue = false;
- if (preg_match('#(?:italic|oblique)#', $cValue)) {
- $tValue = true;
- }
- $styles['italic'] = $tValue;
- break;
- case 'margin':
- $cValue = Converter::cssToTwip($cValue);
- $styles['spaceBefore'] = $cValue;
- $styles['spaceAfter'] = $cValue;
- break;
- case 'margin-top':
- // BC change: up to ver. 0.17.0 incorrectly converted to points - Converter::cssToPoint($cValue)
- $styles['spaceBefore'] = Converter::cssToTwip($cValue);
- break;
- case 'margin-bottom':
- // 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);
- break;
- case 'border-width':
- $styles['borderSize'] = Converter::cssToPoint($cValue);
- break;
- case 'border-style':
- $styles['borderStyle'] = self::mapBorderStyle($cValue);
- break;
- case 'width':
- if (preg_match('/([0-9]+[a-z]+)/', $cValue, $matches)) {
- $styles['width'] = Converter::cssToTwip($matches[1]);
- $styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP;
- } elseif (preg_match('/([0-9]+)%/', $cValue, $matches)) {
- $styles['width'] = $matches[1] * 50;
- $styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT;
- } elseif (preg_match('/([0-9]+)/', $cValue, $matches)) {
- $styles['width'] = $matches[1];
- $styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::AUTO;
- }
- break;
- case 'border':
- 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, '-')) {
- $tmp = explode('-', $cKey);
- $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]);
- $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]);
- }
- 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;
- }
- }
- return $styles;
- }
- /**
- * Parse image node
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- *
- * @return \PhpOffice\PhpWord\Element\Image
- **/
- protected static function parseImage($node, $element)
- {
- $style = array();
- $src = null;
- foreach ($node->attributes as $attribute) {
- switch ($attribute->name) {
- case 'src':
- $src = $attribute->value;
- break;
- case 'width':
- $width = $attribute->value;
- $style['width'] = $width;
- $style['unit'] = \PhpOffice\PhpWord\Style\Image::UNIT_PX;
- break;
- case 'height':
- $height = $attribute->value;
- $style['height'] = $height;
- $style['unit'] = \PhpOffice\PhpWord\Style\Image::UNIT_PX;
- break;
- case 'style':
- $styleattr = explode(';', $attribute->value);
- foreach ($styleattr as $attr) {
- if (strpos($attr, ':')) {
- list($k, $v) = explode(':', $attr);
- switch ($k) {
- case 'float':
- if (trim($v) == 'right') {
- $style['hPos'] = \PhpOffice\PhpWord\Style\Image::POS_RIGHT;
- $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_MARGIN; // inner section area
- $style['pos'] = \PhpOffice\PhpWord\Style\Image::POS_RELATIVE;
- $style['wrap'] = \PhpOffice\PhpWord\Style\Image::WRAP_TIGHT;
- $style['overlap'] = true;
- }
- break;
- }
- }
- }
- break;
- }
- }
- $originSrc = $src;
- if (strpos($src, 'data:image') !== false) {
- $tmpDir = Settings::getTempDir() . '/';
- $match = array();
- preg_match('/data:image\/(\w+);base64,(.+)/', $src, $match);
- $src = $imgFile = $tmpDir . uniqid() . '.' . $match[1];
- $ifp = fopen($imgFile, 'wb');
- if ($ifp !== false) {
- fwrite($ifp, base64_decode($match[2]));
- fclose($ifp);
- }
- }
- $src = urldecode($src);
- if (!is_file($src)
- && !is_null(self::$options)
- && isset(self::$options['IMG_SRC_SEARCH'])
- && isset(self::$options['IMG_SRC_REPLACE'])) {
- $src = str_replace(self::$options['IMG_SRC_SEARCH'], self::$options['IMG_SRC_REPLACE'], $src);
- }
- if (!is_file($src)) {
- if ($imgBlob = @file_get_contents($src)) {
- $tmpDir = Settings::getTempDir() . '/';
- $match = array();
- preg_match('/.+\.(\w+)$/', $src, $match);
- $src = $tmpDir . uniqid() . '.' . $match[1];
- $ifp = fopen($src, 'wb');
- if ($ifp !== false) {
- fwrite($ifp, $imgBlob);
- fclose($ifp);
- }
- }
- }
- if (is_file($src)) {
- $newElement = $element->addImage($src, $style);
- } else {
- throw new \Exception("Could not load image $originSrc");
- }
- return $newElement;
- }
- /**
- * Transforms a CSS border style into a word border style
- *
- * @param string $cssBorderStyle
- * @return null|string
- */
- protected static function mapBorderStyle($cssBorderStyle)
- {
- switch ($cssBorderStyle) {
- case 'none':
- case 'dashed':
- case 'dotted':
- case 'double':
- return $cssBorderStyle;
- default:
- return 'single';
- }
- }
- protected static function mapBorderColor(&$styles, $cssBorderColor)
- {
- $numColors = substr_count($cssBorderColor, '#');
- if ($numColors === 1) {
- $styles['borderColor'] = trim($cssBorderColor, '#');
- } elseif ($numColors > 1) {
- $colors = explode(' ', $cssBorderColor);
- $borders = array('borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor');
- for ($i = 0; $i < min(4, $numColors, count($colors)); $i++) {
- $styles[$borders[$i]] = trim($colors[$i], '#');
- }
- }
- }
- /**
- * Transforms a HTML/CSS alignment into a \PhpOffice\PhpWord\SimpleType\Jc
- *
- * @param string $cssAlignment
- * @return string|null
- */
- protected static function mapAlign($cssAlignment)
- {
- switch ($cssAlignment) {
- case 'right':
- return Jc::END;
- case 'center':
- return Jc::CENTER;
- case 'justify':
- return Jc::BOTH;
- default:
- return Jc::START;
- }
- }
- /**
- * Transforms a HTML/CSS vertical alignment
- *
- * @param string $alignment
- * @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:
- // @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 '';
- }
- }
- /**
- * 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
- *
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- */
- protected static function parseLineBreak($element)
- {
- $element->addTextBreak();
- }
- /**
- * Parse link node
- *
- * @param \DOMNode $node
- * @param \PhpOffice\PhpWord\Element\AbstractContainer $element
- * @param array $styles
- */
- protected static function parseLink($node, $element, &$styles)
- {
- $target = null;
- foreach ($node->attributes as $attribute) {
- switch ($attribute->name) {
- case 'href':
- $target = $attribute->value;
- break;
- }
- }
- $styles['font'] = self::parseInlineStyle($node, $styles['font']);
- if (strpos($target, '#') === 0) {
- return $element->addLink(substr($target, 1), $node->textContent, $styles['font'], $styles['paragraph'], true);
- }
- return $element->addLink($target, $node->textContent, $styles['font'], $styles['paragraph']);
- }
- /**
- * Render horizontal rule
- * Note: Word rule is not the same as HTML's <hr> 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);
- // <hr> is implemented as an empty paragraph - extending 100% inside the section
- // Some properties may be controlled, e.g. <hr style="border-bottom: 3px #DDDDDD solid; margin-bottom: 0;">
- $fontStyle = $styles + array('size' => 3);
- $paragraphStyle = $styles + array(
- '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: <hr/> 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
- }
- }
|