Skip to content

Commit

Permalink
Fix handling of styles
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominic Tubach committed Jun 20, 2024
1 parent cd4b8b7 commit 0199631
Show file tree
Hide file tree
Showing 5 changed files with 413 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

declare(strict_types = 1);

use Civi\Civioffice\PhpWord\StyleMerger;
use CRM_Civioffice_ExtensionUtil as E;
use PhpOffice\PhpWord;

Expand Down Expand Up @@ -111,13 +112,6 @@ public function replaceHtmlToken(string $macroVariable, string $renderedTokenMes
// will not be removed (i.e. leave an empty paragraph).
$this->setValue($macroVariable, '');
}
elseif (count($elements) === 1 && $elements[0] instanceof PhpWord\Element\Text) {
// ... either as plain text (if there is only a single Text
// element, which can't have style properties), ...
// Note: $rendered_token_message shouldn't be used directly
// because it may contain HTML entities.
$this->setValue($macroVariable, $elements[0]->getText());
}
else {
// ... or as HTML: Render all elements and insert in the text
// run or paragraph containing the macro.
Expand Down Expand Up @@ -145,14 +139,14 @@ public function replaceHtmlToken(string $macroVariable, string $renderedTokenMes
*
* @param \PhpOffice\PhpWord\Element\AbstractElement[] $elements
* @param bool $inheritStyle
* If TRUE and an element contains no style, it will be inherited from the
* paragraph/text run the macro is inside.
* If TRUE the style will be inherited from the paragraph/text run the macro
* is inside. If the element already contains styles, they will be merged.
*
* @throws \PhpOffice\PhpWord\Exception\Exception
*/
public function setElementsValue(string $search, array $elements, bool $inheritStyle = FALSE): void {
$search = static::ensureMacroCompleted($search);
$elementsData = '';
$elementsDataList = [];
$hasParagraphs = FALSE;
foreach ($elements as $element) {
$elementName = substr(
Expand All @@ -169,7 +163,7 @@ public function setElementsValue(string $search, array $elements, bool $inheritS
/** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */
$elementWriter = new $objectClass($xmlWriter, $element, !$withParagraph);
$elementWriter->write();
$elementsData .= $xmlWriter->getData();
$elementsDataList[] = preg_replace('/>\s+</', '><', $xmlWriter->getData());
}
$blockType = $hasParagraphs ? 'w:p' : 'w:r';
$where = $this->findContainingXmlBlockForMacro($search, $blockType);
Expand All @@ -182,10 +176,17 @@ public function setElementsValue(string $search, array $elements, bool $inheritS
? $this->splitParagraphIntoParagraphs($block, $paragraphStyle, $textRunStyle)
: $this->splitTextIntoTexts($block, $textRunStyle);
if ($inheritStyle) {
$elementsData = str_replace(['<w:pPr/>', '<w:rPr/>'], [$paragraphStyle, $textRunStyle], $elementsData);
$elementsDataList = preg_replace_callback_array([
'#<w:pPr/>#' => fn() => $paragraphStyle,
'#<w:pPr.*</w:pPr>#' => fn (array $matches) => StyleMerger::mergeStyles($matches[0], $paragraphStyle),
// <w:pPr> may contain <w:rPr> itself so we have to match for <w:rPr> inside of <w:r>
'#<w:r><w:rPr/>.*</w:r>#' => fn(array $matches) => str_replace('<w:rPr/>', $textRunStyle, $matches[0]),
'#<w:r>.*(<w:rPr.*</w:rPr>).*</w:r>#' => fn (array $matches) =>
preg_replace('#<w:rPr.*</w:rPr>#', StyleMerger::mergeStyles($matches[1], $textRunStyle), $matches[0]),
], $elementsDataList);
}
$this->replaceXmlBlock($search, $parts, $blockType);
$this->replaceXmlBlock($search, $elementsData, $blockType);
$this->replaceXmlBlock($search, implode('', $elementsDataList), $blockType);
}
}

Expand All @@ -212,8 +213,9 @@ public function splitParagraphIntoParagraphs(
preg_match('#<w:pPr.*</w:pPr>#i', $paragraph, $matches);
$extractedParagraphStyle = $matches[0] ?? '';

preg_match('#<w:rPr.*</w:rPr>#i', $paragraph, $matches);
$extractedTextRunStyle = $matches[0] ?? '';
// <w:pPr> may contain <w:rPr> itself so we have to match for <w:rPr> inside of <w:r>
preg_match('#<w:r>.*(<w:rPr.*</w:rPr>).*</w:r>#i', $paragraph, $matches);
$extractedTextRunStyle = $matches[1] ?? '';

$result = str_replace(
[
Expand Down
95 changes: 95 additions & 0 deletions Civi/Civioffice/PhpWord/StyleMerger.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
/*
* Copyright (C) 2024 SYSTOPIA GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation in version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

declare(strict_types = 1);

namespace Civi\Civioffice\PhpWord;

final class StyleMerger {

private \DOMElement $styleElement;

/**
* @phpstan-var array<string, \DOMElement>
*/
private array $elements = [];

public static function mergeStyles(string $style, string ...$styles): string {
$styleMerger = new self($style);
foreach ($styles as $styleToMerge) {
$styleMerger->merge($styleToMerge);
}

return $styleMerger->getStyleString();
}

public function __construct(string $style) {
$this->styleElement = $this->createStyleElement($style);
foreach ($this->styleElement->childNodes as $node) {
if ($node instanceof \DOMElement) {
$this->elements[$node->tagName] = $node;
}
}
}

public function merge(string $style): self {
$styleElement = $this->createStyleElement($style);
foreach ($styleElement->childNodes as $node) {
if ($node instanceof \DOMElement) {
// @todo Do we need recursive merging for some elements?
if (!isset($this->elements[$node->tagName])) {
// @phpstan-ignore-next-line
$importedNode = $this->styleElement->ownerDocument->importNode($node, TRUE);
if (!$importedNode instanceof \DOMElement) {
throw new \RuntimeException('Importing node failed');
}

$this->styleElement->appendChild($importedNode);
$this->elements[$node->tagName] = $importedNode;
}
}
}

return $this;
}

public function getStyleString(): string {
// @phpstan-ignore-next-line
return $this->styleElement->ownerDocument->saveXML($this->styleElement);
}

private function createStyleElement(string $style): \DOMElement {
if (NULL === $style = preg_replace('/>\s+</', '><', $style)) {
throw new \RuntimeException('Error processing style');
}

$doc = new \DOMDocument();
$doc->loadXML(
'<root xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">' . $style . '</root>'
);

// @phpstan-ignore-next-line
foreach ($doc->documentElement->childNodes as $node) {
if ($node instanceof \DOMElement) {
return $node;
}
}

throw new \RuntimeException('Could not create style element');
}

}
Loading

0 comments on commit 0199631

Please sign in to comment.