Your IP : 18.117.167.132


Current Path : /var/www/axolotl/data/www/msk.axolotls.ru/bitrix/modules/documentgenerator/lib/body/
Upload File :
Current File : /var/www/axolotl/data/www/msk.axolotls.ru/bitrix/modules/documentgenerator/lib/body/docxxml.php

<?php

namespace Bitrix\DocumentGenerator\Body;

use Bitrix\DocumentGenerator\DataProvider;
use Bitrix\DocumentGenerator\DataProvider\ArrayDataProvider;
use Bitrix\DocumentGenerator\Value;
use Bitrix\Main\Result;
use Bitrix\Main\Text\Encoding;
use Bitrix\Main\Web\DOM;

class DocxXml extends Xml
{
	protected const EMPTY_IMAGE_PLACEHOLDER = '{__SystemEmptyImage}';
	protected const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace';

	public const NUMBERING_TYPE_ORDERED = 'ordered';
	public const NUMBERING_TYPE_UNORDERED = 'unordered';

	protected $arrayImageValues = [];
	protected $numberingIds = [];

	/**
	 * Parse $content, process commands, fill values.
	 * Returns true on success, false on failure.
	 *
	 * @return Result
	 */
	public function process(): Result
	{
		$result = new Result();

		$data = [];
		$this->processArrays();
		$this->clearRowsWithoutValues();
		$data['imageData'] = $this->processImages();
		$this->content = $this->replacePlaceholders();
		$data['numberingIds'] = $this->numberingIds;
		$result->setData($data);

		return $result;
	}

	/**
	 * @return array
	 */
	public static function getNamespaces(): array
	{
		return array_merge(parent::getNamespaces(), [
			'w' => 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
			'wp' => 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing',
			'a' => 'http://schemas.openxmlformats.org/drawingml/2006/main',
			'o' => 'urn:schemas-microsoft-com:office:office',
			'r' => 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
			'v' => 'urn:schemas-microsoft-com:vml',
			'wps' => 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape',
			'wpg' => 'http://schemas.microsoft.com/office/word/2010/wordprocessingGroup',
			'mc' => 'http://schemas.openxmlformats.org/markup-compatibility/2006',
			'w10' => 'urn:schemas-microsoft-com:office:word',
		]);
	}

	/**
	 * @return string
	 */
	public static function getMainPrefix(): string
	{
		return 'w';
	}

	/**
	 * Normalizes content of the document, removing unnecessary tags between {}
	 */
	public function normalizeContent(): void
	{
		$this->initDomDocument();
		$bracketNodes = [];
		$nodes = $this->xpath->query('//w:t[text()[contains(.,"{")]]');
		foreach($nodes as $node)
		{
			$bracketNodes[] = $this->getParentParagraphNode($node, 4);
		}
		$nodes = $this->xpath->query('//w:t[text()[contains(.,"}")]]');
		foreach($nodes as $node)
		{
			$bracketNodes[] = $this->getParentParagraphNode($node, 4);
		}
		$bracketNodes = $this->getUniqueObjects($bracketNodes);
		foreach($bracketNodes as $bracketNode)
		{
			/** @var \DOMElement $bracketNode */
			$rowNodes = $bracketNode->getElementsByTagNameNS(static::getNamespaces()['w'], 'r');
			if($rowNodes)
			{
				$this->normalizeNodeList($rowNodes, $bracketNode);
			}
		}
		$this->saveContent();
		$this->clearPlaceholdersInAttributes();
	}

	/**
	 * Clears all placeholder-like strings that are inside attributes and not images
	 */
	protected function clearPlaceholdersInAttributes(): void
	{
		$placeholdersToClear = [];
		$imagePlaceholders = $this->findImages();
		$allPlaceholders = $this->getPlaceholders();
		foreach($allPlaceholders as $placeholder)
		{
			$node = $this->findPlaceholderNode($placeholder);
			if(!$node && !isset($imagePlaceholders[$placeholder]))
			{
				$placeholdersToClear[$placeholder] = $placeholder;
			}
		}

		if(!empty($placeholdersToClear))
		{
			$this->content = preg_replace_callback(
				static::$valuesPattern,
				static function($matches) use ($placeholdersToClear) {
					if($matches[2] && isset($placeholdersToClear[$matches[2]]))
					{
						return '';
					}

					return $matches[0];
				},
				$this->content
			);
		}
	}

	/**
	 * Walk through $nodeList, finds start node and text with all placeholders
	 *
	 * @param \DOMNodeList $nodeList
	 * @param \DOMElement|null $parentNode
	 */
	protected function normalizeNodeList(\DOMNodeList $nodeList, \DOMElement $parentNode): void
	{
		$deleteNodes = [];
		$startNode = $endNode = false;
		$text = '';
		foreach($nodeList as $node)
		{
			$startNodeFound = false;
			/** @var \DOMElement $node */
			if(!$startNode && mb_strpos($node->textContent, '{') !== false)
			{
				$startNode = $node;
				$startNodeFound = true;
			}
			if($startNode && !$endNode)
			{
				$text .= $node->textContent;
				if(!$startNodeFound)
				{
					$deleteNodes[] = $node;
				}
				$lastClosedBracketPosition = mb_strrpos($text, '}');
				$lastOpenBracketPosition = mb_strrpos($text, '{');
				if($lastClosedBracketPosition === false ||
					(
						$lastOpenBracketPosition !== false &&
						$lastOpenBracketPosition > $lastClosedBracketPosition)
				)
				{
					continue;
				}
			}
			$closedBracketsFound = substr_count($node->textContent, '}');
			if($startNode && !$endNode && $closedBracketsFound > 0)
			{
				$endNode = $node;
			}
			if($startNode && $endNode)
			{
				$this->normalizeTextNode($startNode, $text);
				if($parentNode)
				{
					$parentNode->normalize();
				}
				$startNode = $endNode = false;
				$text = '';
			}
		}
		foreach($deleteNodes as $deleteNode)
		{
			/** @var \DOMElement $deleteNode */
			if($deleteNode->parentNode)
			{
				$deleteNode->parentNode->removeChild($deleteNode);
			}
		}
	}

	/**
	 * Change text nodes content
	 *
	 * @param \DOMElement $rowNode
	 * @param $textContent
	 */
	protected function normalizeTextNode(\DOMElement $rowNode, string $textContent): void
	{
		$textNodes = $rowNode->getElementsByTagNameNS(static::getNamespaces()['w'], 't');
		if($textNodes->length === 0)
		{
			return;
		}
		if($textNodes->length === 1)
		{
			$node = $textNodes->item(0);
			$node->nodeValue = $textContent;
			if($textContent !== trim($textContent))
			{
				$this->addPreserveSpacesAttribute($node);
			}
			return;
		}
		$deleteNodes = [];
		$startNode = false;
		foreach($textNodes as $node)
		{
			$startNodeFound = false;
			if(!$startNode && mb_strpos($node->textContent, '{') !== false)
			{
				$startNode = $node;
				$startNodeFound = true;
			}
			if(!$startNodeFound)
			{
				$deleteNodes[] = $node;
			}
		}
		if($startNode)
		{
			$startNode->nodeValue = $textContent;
		}
		foreach($deleteNodes as $deleteNode)
		{
			/** @var \DOMElement $deleteNode */
			if($deleteNode->parentNode)
			{
				$deleteNode->parentNode->removeChild($deleteNode);
			}
		}
	}

	protected function addPreserveSpacesAttribute(\DOMElement $node): void
	{
		$attributes = $node->attributes;
		$xmlNamespace = static::XML_NAMESPACE;
		$spacesAttribute = $attributes->getNamedItemNS($xmlNamespace, 'space');
		if(!$spacesAttribute)
		{
			$node->setAttributeNS($xmlNamespace, 'space', 'preserve');
		}
	}

	/**
	 * Finds first parent paragraph (<w:p>) node.
	 *
	 * @param \DOMNode $node
	 * @param int $maxLevels
	 * @return null|\DOMNode
	 */
	protected function getParentParagraphNode(\DOMNode $node, int $maxLevels = 10): ?\DOMNode
	{
		return $this->getParentNodeType($node, ['w:p'], $maxLevels);
	}

	/**
	 * Finds first table row (<w:tr>) node.
	 *
	 * @param \DOMNode $node
	 * @param int $maxLevels
	 * @return null|\DOMNode
	 */
	protected function getParentTableRowNode(\DOMNode $node, int $maxLevels = 10): ?\DOMNode
	{
		return $this->getParentNodeType($node, ['w:tr'], $maxLevels);
	}

	/**
	 * Finds first parent with nodeName from $nodeNames list.
	 *
	 * @param \DOMNode $node
	 * @param array $nodeNames
	 * @param int $maxLevels
	 * @return null|\DOMNode
	 */
	protected function getParentNodeType(\DOMNode $node, array $nodeNames, int $maxLevels = 10): ?\DOMNode
	{
		while($maxLevels-- > 0)
		{
			if ($node->nodeName === 'w:body')
			{
				break;
			}

			if(in_array($node->nodeName, $nodeNames, true))
			{
				return $node;
			}

			$node = $node->parentNode;
		}

		return null;
	}

	/**
	 * Finds arrays in $this->values.
	 * For these values tries to find .BLOCK_START and .BLOCK_END marks.
	 * All content between them considered as block for multiplying.
	 * Such block fills with values for each row in array and inserts into $this->document.
	 * Start, end marks and original nodes are being deleted.
	 */
	protected function processArrays(): void
	{
		foreach($this->values as $placeholder => $list)
		{
			if($list instanceof ArrayDataProvider)
			{
				$this->initDomDocument();
				$block = $this->collectMultiplyNodes($placeholder);
				while(isset($block['content']) && !empty($block['content']))
				{
					$this->processMultiplyingBlock($list, $placeholder, $block);
					$block = $this->collectMultiplyNodes($placeholder);
				}
				$this->saveContent();
			}
		}
	}

	protected function processMultiplyingBlock(ArrayDataProvider $dataProvider, string $placeholder, array $block): void
	{
		$dataProvider->rewind();
		$indexToPrint = $block[static::ARRAY_INDEX_MODIFIER] ?? null;
		$isPrintEmpty = ($indexToPrint !== null && !$dataProvider->getValue($indexToPrint));
		if($isPrintEmpty)
		{
			$indexToPrint = 0;
		}
		foreach($dataProvider as $index => $value)
		{
			if($indexToPrint !== null && $index !== $indexToPrint)
			{
			    continue;
            }
			foreach($block['nodes'] as $key => $node)
			{
				/** @var \DOMElement $node */
				$content = $block['content'][$key];
				$fieldNames = static::matchFieldNames($content);
				$multipleValues = $this->getValuesForMultiplyingBlock($placeholder, $dataProvider, $value, $fieldNames);
				if($isPrintEmpty)
				{
					$multipleValues = array_fill_keys(array_keys($multipleValues), '');
				}
				$values = array_merge($this->values, $multipleValues);
				$blockContent = preg_replace_callback(
					static::$valuesPattern,
					function($matches) use ($values) {
						if($matches[2] && array_key_exists($matches[2], $values))
						{
							// multiply images
							if($this->isImageValue($matches[2], $values))
							{
								if($values[$matches[2]])
								{
									// in case someone inserted image placeholder as text - prevent looping
									$placeholder = $matches[1];
									if(!$matches[3])
									{
										$placeholder .= '~';
									}
									$placeholder .= static::DO_NOT_INSERT_VALUE_MODIFIER;

									return '{'.$placeholder.'}';
								}

								return static::EMPTY_IMAGE_PLACEHOLDER;
							}

							return $this->printValue($values[$matches[2]], $matches[2], $matches[3]);
						}

						return '';
					}, $content
				);
				$innerXml = new DocxXml($blockContent);
				$imageData = $innerXml->findImages(true);
				foreach($imageData as $imagePlaceholder => $data)
				{
					if($this->isImageValue($imagePlaceholder, $values))
					{
						foreach($data['innerIDs'] as $id)
						{
							$this->arrayImageValues['values'][$id] = $values[$imagePlaceholder];
							$this->arrayImageValues['originalId'][$id] = $data['originalId'][$id];
						}
					}
				}
				$nodeToLoad = $block['nodes'][count($block['nodes']) - 1];
				$blockDocument = $innerXml->getDomDocument();
				foreach($blockDocument->childNodes as $blockNode)
				{
					$blockNode = $this->document->importNode($blockNode, true);
					$nodeToLoad->parentNode->insertBefore($blockNode, $nodeToLoad);
				}
			}
		}
		if(isset($block['startNode']))
		{
			$block['startNode']->parentNode->removeChild($block['startNode']);
		}
		foreach($block['nodes'] as $node)
		{
			$node->parentNode->removeChild($node);
		}
		if(isset($block['endNode']) && $block['endNode'])
		{
			$block['endNode']->parentNode->removeChild($block['endNode']);
		}
		if($indexToPrint)
		{
			die;
		}
	}

	/**
	 * Finds {$placeholder.START_BLOCK} node.
	 * Collect all nodes after this while last node does not contain {$placeholder.END_BLOCK}
	 * Returns array with startNode, content nodes and endNode.
	 *
	 * @param $placeholder
	 * @return array
	 */
	protected function collectMultiplyNodes(string $placeholder): array
	{
		$result = [];
		$startNode = $this->findPlaceholderNode($placeholder.'.'.static::BLOCK_START_PLACEHOLDER);
		if(!$startNode)
		{
			// try to find by magic
			$tableRowNodes = [];
			$linkedPlaceholders = $this->getLinkedPlaceholders($placeholder);
			foreach($linkedPlaceholders as $linkedPlaceholder)
			{
				$linkedPlaceholderNodes = $this->findPlaceholderNodes($linkedPlaceholder);
				foreach($linkedPlaceholderNodes as $node)
				{
					if(mb_strpos($node->textContent, static::DO_NOT_INSERT_VALUE_MODIFIER) === false)
					{
						$tableRowNodes[] = $this->getParentTableRowNode($node);
					}
				}
			}
			$tableRowNodes = $this->getUniqueObjects($tableRowNodes);
			if(!empty($tableRowNodes))
			{
				/** @var \DOMElement[] $tableRowNodes */
				$result = [
					'content' => [$tableRowNodes[0]->C14N()],
					'nodes' => [$tableRowNodes[0]],
				];
			}
			return $result;
		}
		if(
			preg_match(static::$valuesPattern, $startNode->nodeValue, $placeholderData)
			&& !empty($placeholderData[3])
		)
		{
			$modifierData = Value::parseModifier($placeholderData[3]);
			if(isset($modifierData[static::ARRAY_INDEX_MODIFIER]))
			{
				$result[static::ARRAY_INDEX_MODIFIER] = (int) $modifierData[static::ARRAY_INDEX_MODIFIER];
			}
		}

		$startNode = $this->getParentParagraphNode($startNode);
		if(!$startNode)
		{
			return $result;
		}
		$nodes = [];
		$result['endNode'] = false;
		$result['startNode'] = $startNode;
		$maxTags = 20;
		$node = $startNode->nextSibling;
		while($maxTags-- > 0 && $node)
		{
			if(strpos($node->nodeValue, '{'.$placeholder.'.'.static::BLOCK_END_PLACEHOLDER.'}') !== false)
			{
				$result['endNode'] = $node;
				break;
			}
			$nodes[] = $node;
			$node = $node->nextSibling;
		}
		if(!$result['endNode'])
		{
			$nodes = [];
		}
		foreach($nodes as $node)
		{
			$result['content'][] = $node->C14N();
			$result['nodes'][] = $node;
		}

		return $result;
	}

	/**
	 * Generate array of values to change in multiplying block.
	 *
	 * @param string $placeholder
	 * @param ArrayDataProvider $list
	 * @param DataProvider|array $data
	 * @param array $fieldNames
	 * @return array
	 */
	protected function getValuesForMultiplyingBlock(string $placeholder, ArrayDataProvider $list, $data, array $fieldNames): array
	{
		$values = [];
		foreach($fieldNames as $fullName)
		{
			[$providerName, $fieldName] = explode('.', $fullName);
			if($providerName === $placeholder && $fieldName)
			{
				$values[$fullName] = $list->getValue($fieldName);
			}
			else
			{
				$value = $this->values[$fullName];
				if(is_string($value))
				{
					$valueNameParts = explode('.', $value);
					if($valueNameParts[0] === $placeholder)
					{
						if($valueNameParts[1] === $list->getItemKey() && count($valueNameParts) > 2)
						{
							$name = implode('.', array_slice($valueNameParts, 2));
							if($data instanceof DataProvider)
							{
								$value = $data->getValue($name);
							}
							elseif(is_array($data))
							{
								$value = $data[$name];
							}
						}
						else
						{
							$value = $list->getValue($valueNameParts[1]);
						}
					}
				}
				$values[$fullName] = $value;
			}
		}

		return $values;
	}

	/**
	 * Delete table rows for empty values.
	 */
	protected function clearRowsWithoutValues(): void
	{
		$fieldsToHide = [static::EMPTY_IMAGE_PLACEHOLDER];
		$fields = $this->getFields();
		foreach($fields as $placeholder => $field)
		{
			if(
				($field['TYPE'] === DataProvider::FIELD_TYPE_IMAGE || $field['TYPE'] === DataProvider::FIELD_TYPE_STAMP) ||
				(isset($field['OPTIONS']['IS_ARRAY']) && $field['OPTIONS']['IS_ARRAY'] === true)
			)
			{
				$fieldsToHide[] = $placeholder;
			}
		}
		$fieldsToHide = array_unique($fieldsToHide);
		if(!empty($fieldsToHide))
		{
			$nodesToDelete = [];
			$this->initDomDocument();
			foreach($fieldsToHide as $placeholder)
			{
				if(
					$fields[$placeholder]['HIDE_ROW'] === 'Y'
					&& ($this->values[$placeholder] === null
						|| $this->values[$placeholder] === '')
				)
				{
					$nodes = $this->findPlaceholderNodes($placeholder);
					foreach($nodes as $node)
					{
						$parentRow = $this->getParentTableRowNode($node, 5);
						if($parentRow)
						{
							$nodesToDelete[] = $parentRow;
						}
						else
						{
							$parentRow = $this->getParentParagraphNode($node, 3);
							if($parentRow)
							{
								$nodesToDelete[] = $parentRow;
							}
						}
					}
				}
			}
			$nodesToDelete = $this->getUniqueObjects($nodesToDelete);
			foreach($nodesToDelete as $node)
			{
				$node->parentNode->removeChild($node);
			}
			$this->saveContent();
		}
	}

	/**
	 * Returns array where key is a placeholder and value is an array of image ids.
	 *
	 * @return array
	 */
	protected function processImages(): array
	{
		$imageData = $this->findImages(true);
		foreach($imageData as $placeholder => &$image)
		{
			if(empty($this->values[$placeholder]) || $this->values[$placeholder] === ' ')
			{
				foreach($image['drawingNode'] as $key => $node)
				{
					/** @var \DOMNode $node */
					$node->parentNode->removeChild($node);
					unset($image['drawingNode'][$key]);
				}
			}
		}
		$this->saveContent();

		return $imageData;
	}

	/**
	 * Get all drawing nodes marked with placeholders.
	 * If $generateNewImageIds is true - will replace relation ids to new values.
	 *
	 * @param bool $generateNewImageIds
	 * @param \DOMNode|null $contextNode
	 * @return array
	 */
	public function findImages(bool $generateNewImageIds = false, \DOMNode $contextNode = null): array
	{
		$this->initDomDocument();
		if($contextNode)
		{
			$imageDescriptions = $this->xpath->query('//w:drawing//wp:docPr', $contextNode);
		}
		else
		{
			$imageDescriptions = $this->xpath->query('//w:drawing//wp:docPr');
		}
		$placeholders = [];
		foreach($imageDescriptions as $description)
		{
			/** @var \DOMElement $description */
			if($description->hasAttributes())
			{
				$name = $description->attributes->getNamedItem('name');
				$descr = $description->attributes->getNamedItem('descr');
				$placeholder = null;
				if($descr)
				{
					$placeholder = static::getCodeFromPlaceholder($descr->nodeValue);
				}
				if(!$placeholder && $name)
				{
					$placeholder = static::getCodeFromPlaceholder($name->nodeValue);
				}
				if($placeholder)
				{
					if(!isset($placeholders[$placeholder]))
					{
						$placeholders[$placeholder] = [
							'drawingNode' => [],
							'innerIDs' => [],
						];
					}
					$placeholders[$placeholder]['drawingNode'][] = $description->parentNode->parentNode;
					$embeds = $description->parentNode->getElementsByTagNameNS(static::getNamespaces()['a'], 'blip');
					if($embeds->length > 0)
					{
						/** @var \DOMNode $embed */
						$embed = $embeds[0];
						if($innerImageId = $embed->attributes->getNamedItemNS('http://schemas.openxmlformats.org/officeDocument/2006/relationships', 'embed'))
						{
							/** @var \DOMAttr $innerImageId */
							$imageId = $innerImageId->value;
							if($generateNewImageIds && !isset($this->arrayImageValues['originalId'][$imageId]))
							{
								$newImageId = static::getRandomId('rId', true);
								$placeholders[$placeholder]['originalId'][$newImageId] = $imageId;
								$imageId = $innerImageId->value = $newImageId;
							}
							if(!in_array($imageId, $placeholders[$placeholder]['innerIDs']))
							{
								$placeholders[$placeholder]['innerIDs'][] = $imageId;
								if(isset($this->arrayImageValues['values'][$imageId]))
								{
									$placeholders[$placeholder]['values'][$imageId] = $this->arrayImageValues['values'][$imageId];
									$placeholders[$placeholder]['originalId'][$imageId] = $this->arrayImageValues['originalId'][$imageId];
								}
							}
						}
					}
				}
			}
		}

		if(!empty($placeholders) && $generateNewImageIds)
		{
			$this->saveContent();
		}

		return $placeholders;
	}

	/**
	 * @param mixed $value
	 * @param string $placeholder
	 * @param string $modifier
	 * @param array $params
	 * @return string
	 */
	protected function printValue($value, $placeholder, $modifier = '', array $params = []): string
	{
		$value = parent::printValue($value, $placeholder, $modifier);
		if(empty($value))
		{
			return (string) $value;
		}
		if (ToUpper(SITE_CHARSET) !== 'UTF-8')
		{
			if(is_array($value) || is_object($value))
			{
				$value = '';
			}
			elseif(!Encoding::detectUtf8($value))
			{
				$value = Encoding::convertEncoding($value, SITE_CHARSET, 'UTF-8');
			}
		}
		if(is_string($value))
		{
			if($this->isImageValue($placeholder, $this->values))
			{
				return '';
			}

			if($this->isHtml($value))
			{
				$context = [];
				if(isset($params['currentNode']) && $params['currentNode'] instanceof \DOMElement)
				{
					$context['rowProperties'] = $this->getRowPropertyNodeValue($params['currentNode']);
				}
				$value = $this->htmlToXml($value, $context);
			}
			else
			{
				$value = $this->prepareTextValue($value);
			}
		}

		return $value;
	}

	/**
	 * @param string $placeholder
	 * @param array $values
	 * @param array|null $fields
	 * @return bool
	 */
	protected function isImageValue(string $placeholder, array $values, array $fields = null): bool
	{
		if(!$fields)
		{
			$fields = $this->fields;
		}

		return (
			array_key_exists($placeholder, $values) &&
			isset($fields[$placeholder]['TYPE']) &&
			(
				$fields[$placeholder]['TYPE'] === DataProvider::FIELD_TYPE_IMAGE
				|| $fields[$placeholder]['TYPE'] === DataProvider::FIELD_TYPE_STAMP
			)
		);
	}

	/**
	 * @return string
	 */
	protected function getBreakLineTag(): string
	{
		return '</w:t><w:br/><w:t>';
	}

	/**
	 * @param $string
	 * @return bool
	 */
	protected function isHtml($string): bool
	{
		return (preg_match('/<\s?[^\>]*\/?\s?>/i', $string) != false);
	}

	/**
	 * Converts html to xml with the same rendering.
	 *
	 * @param string $html
	 * @param array $context
	 * @return string
	 */
	protected function htmlToXml(string $html, array $context = []): string
	{
		$htmlDocument = new DOM\Document();
		$htmlDocument->loadHTML($html);
		$result = $this->htmlNodeToXml($htmlDocument, $context);
		if(!empty($result))
		{
			$result = '</w:t></w:r>'.$result.'<w:r><w:t>';
		}
		return $result;
	}

	/**
	 * @param DOM\Node $node
	 * @param array $properties
	 * @return DOM\DisplayProperties
	 */
	protected function getDisplayProperties(DOM\Node $node, array $properties = []): DOM\DisplayProperties
	{
		return new DOM\DisplayProperties($node, $properties);
	}

	/**
	 * Recursively converts html node to xml.
	 *
	 * @param DOM\Node $node
	 * @param array $context
	 * @return string
	 */
	protected function htmlNodeToXml(DOM\Node $node, array &$context = []): string
	{
		$result = '';

		$this->deleteLastBreakLineInBlockTag($node);
		$displayProperties = $this->getDisplayProperties($node);

		if($displayProperties->isHidden())
		{
			return $result;
		}
		$nodes = $node->getChildNodes();
		$nodeName = mb_strtolower($node->getNodeName());
		if($nodeName === 'ul')
		{
			$context['currentList'] = [
				'type' => static::NUMBERING_TYPE_UNORDERED,
				'id' => $this->getRandomId('numberingValue', false),
			];
		}
		elseif($nodeName === 'ol')
		{
			$context['currentList'] = [
				'type' => static::NUMBERING_TYPE_ORDERED,
				'id' => $this->getRandomId('numberingValue', false),
			];
		}
		elseif($nodeName === 'li')
		{
			$context['showNumber'] = true;
		}

		if($displayProperties->isDisplayBlock())
		{
			$context['display'] = DOM\DisplayProperties::DISPLAY_BLOCK;
		}
		if(!isset($context['font']) || !is_array($context['font']))
		{
			$context['font'] = [];
		}
		$context['font'] = array_merge($context['font'], $displayProperties->getProperties()['font']);
		// The trick is in order we get tags. We have to carry $context all along.
		// First we have 'b' tag and then we have #text tag. But they are on the same level of hierarchy.
		// So we have to put 'bold font' into context and we need to know about it in the next tag.
		/** @var DOM\Node $childNode */
		foreach($nodes as $childNode)
		{
			$nodeValue = str_replace("\n", '', $childNode->getNodeValue());
			if($context['display'] === DOM\DisplayProperties::DISPLAY_BLOCK || $displayProperties->isDisplayBlock())
			{
				$nodeValue = trim($nodeValue);
			}
			$childNodeName = mb_strtolower($childNode->getNodeName());
			if($childNodeName === 'br')
			{
				$result .= '<w:r>';
				$result .= '<w:br/>';
				$result .= '</w:r>';
			}
			elseif($childNode instanceof DOM\Text && !empty($nodeValue))
			{
				if(isset($context['showNumber']) && isset($context['currentList']))
				{
					$result .= '</w:p>';
					$result .= '<w:p>';
					$this->numberingIds[$context['currentList']['id']] = $context['currentList'];
					$result .= '<w:pPr>';
					$result .= '<w:numPr>';
					$result .= '<w:ilvl w:val="0" />';
					$result .= '<w:numId w:val="'.$context['currentList']['id'].'" />';
					$result .= '</w:numPr>';
					$result .= '</w:pPr>';
					unset($context['showNumber']);
					$context['display'] = $displayProperties->getProperties()[DOM\DisplayProperties::DISPLAY];
					$result .= '<w:r>';
				}
				elseif($context['display'] === DOM\DisplayProperties::DISPLAY_BLOCK)
				{
					$result .= '<w:r>';
					$result .= '<w:br/>';
					$context['display'] = $displayProperties->getProperties()[DOM\DisplayProperties::DISPLAY];
				}
				else
				{
					$result .= '<w:r>';
				}
				$result .= $this->addRowPropertiesTag($context);
				$result .= '<w:t xml:space="preserve">';
				$result .= $this->prepareTextValue($nodeValue);
				$result .= '</w:t>';
				$result .= '</w:r>';
			}
			else
			{
				$result .= $this->htmlNodeToXml($childNode, $context);
			}
		}

		if($nodeName === 'ul' || $nodeName === 'ol')
		{
			unset($context['currentList']);
			$result .= '</w:p>';
			$result .= '<w:p>';
		}
		elseif($nodeName === 'li')
		{
			unset($context['showNumber']);
		}
		$context['font'] = array_diff_assoc($context['font'], $displayProperties->getProperties()['font']);

		return $result;
	}

	/**
	 * Delete last break line tag in blocks - to avoid excess break lines
	 *
	 * @param DOM\Node $node
	 * @throws DOM\DomException
	 */
	protected function deleteLastBreakLineInBlockTag(DOM\Node $node): void
	{
		$displayProperties = $this->getDisplayProperties($node);
		if($displayProperties->isDisplayBlock())
		{
			$hasSomeContent = (trim(strip_tags($node->getInnerHTML())) !== '');
			if(!$hasSomeContent)
			{
				return;
			}
			$previousNode = null;
			$childNodes = $node->getChildNodesArray();
			/** @var DOM\Node $childNode */
			foreach($childNodes as $index => $childNode)
			{
				if(!$previousNode)
				{
					$previousNode = $childNode;
					continue;
				}

				$previousNodeName = mb_strtolower($previousNode->getNodeName());
				if(
					!isset($childNodes[$index + 1])
					&& $previousNodeName === 'br' &&
					$childNode instanceof DOM\Text && empty($childNode->getNodeValue())
				)
				{
					$node->removeChild($previousNode);
					$node->removeChild($childNode);
				}
				$previousNode = $childNode;
			}
		}
	}

	/**
	 * Returns row-properties xml-tag based on font properties.
	 *
	 * @param array $properties
	 * @return string
	 */
	protected function addRowPropertiesTag(array $properties): string
	{
		$displayProperties = $this->getDisplayProperties(new DOM\Element('stub'), $properties);
		$result = '<w:rPr>';
		$result .= '<w:rStyle w:val="Del" />';
		if($displayProperties->isFontBold())
		{
			$result .= '<w:b/>';
		}
		if($displayProperties->isFontItalic())
		{
			$result .= '<w:i/>';
		}
		if($displayProperties->isFontDeleted())
		{
			$result .= '<w:strike/>';
		}
		if($displayProperties->isFontUnderlined())
		{
			$result .= '<w:u w:val="single" />';
		}
		if(isset($properties['rowProperties']) && is_string($properties['rowProperties']))
		{
			$result .= $properties['rowProperties'];
		}
		$result .= '</w:rPr>';

		return $result;
	}

	/**
	 * @param \DOMElement $node
	 * @return string|null
	 */
	protected function getRowPropertyNodeValue(\DOMElement $node): ?string
	{
		if($node->nodeName === 'w:t')
		{
			$rowNode = $this->getParentNodeType($node, ['w:r'], 2);
		}
		else
		{
			$rowNode = $node;
		}
		/** @var \DOMElement $rowNode */
		if($rowNode && $rowNode->nodeName === 'w:r')
		{
			$propertyNodes = $rowNode->getElementsByTagNameNS(static::getNameSpaces()['w'], 'rPr');
			if($propertyNodes->length > 0)
			{
				$propertyNode = $propertyNodes->item(0);
				if($propertyNode)
				{
					return $propertyNode->nodeValue;
				}
			}
		}

		return null;
	}

//  This is not ready yet
//	/**
//	 * @param array $params
//	 * @return string
//	 */
//	protected function replacePlaceholders(array $params = [])
//	{
//		$placeholders = $this->getPlaceholders();
//		$allNodes = array_fill_keys($placeholders, []);
//		foreach($placeholders as $placeholder)
//		{
//			$allNodes[$placeholder] = $this->findPlaceholderNodes($placeholder);
//		}
//
//		foreach($allNodes as $placeholder => $nodes)
//		{
//			foreach($nodes as $node)
//			{
//				/** @var \DOMNode $node */
//				$textContent = parent::replacePlaceholders([
//					'content' => $node->nodeValue,
//					'currentNode' => $node,
//				]);
//			}
//		}
//
//		$this->saveContent();
//		return $this->content;
//	}

//	/**
//	 * Generates simple spreadsheets for array values.
//	 *
//	 * @deprecated
//	 */
//	protected function generateSpreadsheets()
//	{
//		foreach($this->values as $placeholder => $value)
//		{
//			if(is_array($value))
//			{
//				$this->initDomDocument();
//				//$pageWidth = $this->getFirstPageWidth();
//				$spreadsheet = $this->generateSpreadsheet($value);
//				if(!$spreadsheet)
//				{
//					continue;
//				}
//				$spreadsheet = $this->document->importNode($spreadsheet, true);
//				$textNodes = $this->xpath->query('//w:t[text()="{'.$placeholder.'}"]');
//				foreach($textNodes as $node)
//				{
//					$node->parentNode->parentNode->parentNode->replaceChild($spreadsheet, $node->parentNode->parentNode);
//				}
//			}
//		}
//		$this->saveDomDocument();
//	}
//
//	/**
//	 * Parses properties of the first page and get width from there.
//	 * Width calculates as full width minus left and right margin.
//	 *
//	 * @return bool|string
//	 */
//	protected function getFirstPageWidth()
//	{
//		static $pageWidth = null;
//		if($pageWidth === null)
//		{
//			$pageWidth = false;
//			$pageSizes = $this->xpath->query('//w:sectPr//w:pgSz');
//			if($pageSizes->length > 0)
//			{
//				$pageSize = $pageSizes->item(0);
//				$width = $pageSize->attributes->getNamedItemNS($this->wordNamespaces['w'], 'w');
//				if($width)
//				{
//					$pageWidth = $width->nodeValue;
//				}
//			}
//			if($pageWidth > 0)
//			{
//				$pageMargins = $this->xpath->query('//w:sectPr//w:pgMar');
//				if($pageMargins->length > 0)
//				{
//					$pageMargin = $pageMargins->item(0);
//					$left = $pageMargin->attributes->getNamedItemNS($this->wordNamespaces['w'], 'left');
//					if($left)
//					{
//						$pageWidth -= $left->nodeValue;
//					}
//					$right = $pageMargin->attributes->getNamedItemNS($this->wordNamespaces['w'], 'right');
//					if($right)
//					{
//						$pageWidth -= $right->nodeValue;
//					}
//				}
//			}
//		}
//
//		return $pageWidth;
//	}
//
//	/**
//	 * @param array $data
//	 * @return bool|\DOMNode
//	 */
//	protected function generateSpreadsheet(array $data)
//	{
//		if(!isset($data['DATA']))
//		{
//			return false;
//		}
//		$content = '<w:document ';
//		foreach($this->wordNamespaces as $prefix => $namespaceUri)
//		{
//			$content .= 'xmlns:'.$prefix.'="'.$namespaceUri.'" ';
//		}
//		$content .= '><w:tbl>
//		<w:tblPr>
//			<w:tblW w:w="5000" w:type="pct" />
//            <w:jc w:val="left" />
//            <w:tblInd w:w="100" w:type="dxa" />
//			<w:tblBorders>
//                <w:top w:val="single" w:sz="2" w:space="0" w:color="000000" />
//                <w:left w:val="single" w:sz="2" w:space="0" w:color="000000" />
//                <w:bottom w:val="single" w:sz="2" w:space="0" w:color="000000" />
//                <w:insideH w:val="single" w:sz="2" w:space="0" w:color="000000" />
//            </w:tblBorders>
//            <w:tblCellMar>
//                <w:top w:w="0" w:type="dxa" />
//                <w:left w:w="100" w:type="dxa" />
//                <w:bottom w:w="0" w:type="dxa" />
//                <w:right w:w="100" w:type="dxa" />
//            </w:tblCellMar>
//		</w:tblPr>';
//		foreach($data['DATA'] as $row => $data)
//		{
//			if($row == 0)
//			{
//				$columns = count($data);
//				$columnWidth = floor(5000 / $columns);
//				$content .= '<w:tblGrid>';
//				$content .= str_repeat('<w:gridCol w:w="'.$columnWidth.'" />', $columns);
//				$content .= '</w:tblGrid>';
//			}
//			$content .= '<w:tr>';
//			foreach($data as $column)
//			{
//				$content .= '<w:tc>
//					<w:tcPr>
//						<w:tcW w:w="'.$columnWidth.'" w:type="dxa" />
//                        <w:tcBorders>
//                            <w:top w:val="single" w:sz="2" w:space="0" w:color="000000" />
//                            <w:left w:val="single" w:sz="2" w:space="0" w:color="000000" />
//                            <w:bottom w:val="single" w:sz="2" w:space="0" w:color="000000" />
//                            <w:insideH w:val="single" w:sz="2" w:space="0" w:color="000000" />
//                        </w:tcBorders>
//					</w:tcPr>
//                    <w:p>
//                        <w:pPr>
//                            <w:spacing w:before="20" w:after="20" w:lineRule="auto" />
//                            <w:ind w:start="20" w:end="20" w:firstLine="0" />
//                            <w:contextualSpacing w:val="false" />
//                        </w:pPr>
//                        <w:r>
//                            <w:t>'.$column.'</w:t>
//                        </w:r>
//                    </w:p>
//                </w:tc>';
//			}
//			$content .= '</w:tr>';
//		}
//		$content .= '</w:tbl>
//		</w:document>';
//
//		$spreadsheet = new \DOMDocument();
//		$spreadsheet->loadXML($content);
//		$spreadsheetNodes = $spreadsheet->getElementsByTagNameNS($this->wordNamespaces['w'], 'tbl');
//		return $spreadsheetNodes->item(0);
//	}
}