diff --git a/CRM/Civioffice/DocumentRendererType/LocalUnoconv/PhpWordTemplateProcessor.php b/CRM/Civioffice/DocumentRendererType/LocalUnoconv/PhpWordTemplateProcessor.php
index 80e2d7e..5e533b6 100644
--- a/CRM/Civioffice/DocumentRendererType/LocalUnoconv/PhpWordTemplateProcessor.php
+++ b/CRM/Civioffice/DocumentRendererType/LocalUnoconv/PhpWordTemplateProcessor.php
@@ -121,7 +121,7 @@ public function replaceHtmlToken(string $macroVariable, string $renderedTokenMes
else {
// ... or as HTML: Render all elements and insert in the text
// run or paragraph containing the macro.
- $this->setElementsValue($macroVariable, $elements);
+ $this->setElementsValue($macroVariable, $elements, TRUE);
}
} while (in_array($macroVariable, $this->getVariables(), TRUE));
}
@@ -143,13 +143,15 @@ public function replaceHtmlToken(string $macroVariable, string $renderedTokenMes
* surrounding texts, text runs or paragraphs before and after the macro,
* depending on the types of elements to insert.
*
- * @param string $search
* @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.
*
- * @return void
* @throws \PhpOffice\PhpWord\Exception\Exception
*/
- public function setElementsValue(string $search, array $elements): void {
+ public function setElementsValue(string $search, array $elements, bool $inheritStyle = FALSE): void {
+ $search = static::ensureMacroCompleted($search);
$elementsData = '';
$hasParagraphs = FALSE;
foreach ($elements as $element) {
@@ -174,9 +176,15 @@ public function setElementsValue(string $search, array $elements): void {
if (is_array($where)) {
/** @phpstan-var array{start: int, end: int} $where */
$block = $this->getSlice($where['start'], $where['end']);
- $parts = $hasParagraphs ? $this->splitParagraphIntoParagraphs($block) : $this->splitTextIntoTexts($block);
+ $paragraphStyle = '';
+ $textRunStyle = '';
+ $parts = $hasParagraphs
+ ? $this->splitParagraphIntoParagraphs($block, $paragraphStyle, $textRunStyle)
+ : $this->splitTextIntoTexts($block, $textRunStyle);
+ if ($inheritStyle) {
+ $elementsData = str_replace(['', ''], [$paragraphStyle, $textRunStyle], $elementsData);
+ }
$this->replaceXmlBlock($search, $parts, $blockType);
- $search = static::ensureMacroCompleted($search);
$this->replaceXmlBlock($search, $elementsData, $blockType);
}
}
@@ -184,48 +192,97 @@ public function setElementsValue(string $search, array $elements): void {
/**
* Splits a w:p into a list of w:p where each ${macro} is in a separate w:p.
*
- * @param string $paragraph
+ * @param string $extractedParagraphStyle
+ * Is set to the extracted paragraph style (w:pPr).
+ * @param string $extractedTextRunStyle
+ * Is set to the extracted text run style (w:rPr).
*
- * @return string
* @throws \PhpOffice\PhpWord\Exception\Exception
*/
- public function splitParagraphIntoParagraphs(string $paragraph): string {
- $matches = [];
- if (1 === preg_match('/()/i', $paragraph, $matches)) {
- $extractedStyle = $matches[0];
- }
- else {
- $extractedStyle = '';
- }
+ public function splitParagraphIntoParagraphs(
+ string $paragraph,
+ string &$extractedParagraphStyle = '',
+ string &$extractedTextRunStyle = ''
+ ): string {
if (NULL === $paragraph = preg_replace('/>\s+', '><', $paragraph)) {
throw new PhpWord\Exception\Exception('Error processing PhpWord document.');
}
+
+ $matches = [];
+ preg_match('##i', $paragraph, $matches);
+ $extractedParagraphStyle = $matches[0] ?? '';
+
+ preg_match('##i', $paragraph, $matches);
+ $extractedTextRunStyle = $matches[0] ?? '';
+
$result = str_replace(
[
+ '',
'${',
'}',
],
[
- '' . $extractedStyle . '${',
- '}' . $extractedStyle . '',
+ '',
+ sprintf(
+ '%s%s${',
+ $extractedParagraphStyle,
+ $extractedTextRunStyle
+ ),
+ sprintf(
+ '}%s%s',
+ $extractedParagraphStyle,
+ $extractedTextRunStyle
+ ),
],
$paragraph
);
// Remove empty paragraphs that might have been created before/after the
// macro.
+ $emptyParagraph = sprintf(
+ '%s%s',
+ $extractedParagraphStyle,
+ $extractedTextRunStyle
+ );
+
+ return str_replace($emptyParagraph, '', $result);
+ }
+
+ /**
+ * @inheritDoc
+ * Adds output parameter for extracted style.
+ *
+ * @param string $extractedStyle
+ * Is set to the extracted text run style (w:rPr).
+ *
+ * @throws \PhpOffice\PhpWord\Exception\Exception
+ */
+ protected function splitTextIntoTexts($text, string &$extractedStyle = '') {
+ if (NULL === $unformattedText = preg_replace('/>\s+', '><', $text)) {
+ throw new PhpWord\Exception\Exception('Error processing PhpWord document.');
+ }
+
+ $matches = [];
+ preg_match('//i', $unformattedText, $matches);
+ $extractedStyle = $matches[0] ?? '';
+
+ if (!$this->textNeedsSplitting($text)) {
+ return $text;
+ }
+
$result = str_replace(
+ ['', '${', '}'],
[
- '' . $extractedStyle . '',
- '',
+ '',
+ '' . $extractedStyle . '${',
+ '}' . $extractedStyle . '',
],
- [
- '',
- '',
- ],
- $result
+ $unformattedText
);
- return $result;
+
+ $emptyTextRun = '' . $extractedStyle . '';
+
+ return str_replace($emptyTextRun, '', $result);
}
}
diff --git a/tests/phpunit/CRM/Civioffice/DocumentRendererType/LocalUnoconv/CRM_Civioffice_DocumentRendererType_LocalUnoconv_PhpWordTemplateProcessorTest.php b/tests/phpunit/CRM/Civioffice/DocumentRendererType/LocalUnoconv/CRM_Civioffice_DocumentRendererType_LocalUnoconv_PhpWordTemplateProcessorTest.php
new file mode 100644
index 0000000..b67aa6a
--- /dev/null
+++ b/tests/phpunit/CRM/Civioffice/DocumentRendererType/LocalUnoconv/CRM_Civioffice_DocumentRendererType_LocalUnoconv_PhpWordTemplateProcessorTest.php
@@ -0,0 +1,368 @@
+.
+ */
+
+declare(strict_types = 1);
+
+namespace CRM\Civioffice\DocumentRendererType\LocalUnoconv;
+
+use Civi\Civioffice\DocumentRendererType\LocalUnoconv\TestablePhpWordTemplateProcessor;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers \CRM_Civioffice_DocumentRendererType_LocalUnoconv_PhpWordTemplateProcessor
+ */
+final class CRM_Civioffice_DocumentRendererType_LocalUnoconv_PhpWordTemplateProcessorTest extends TestCase {
+
+ public function testReplaceSimple(): void {
+ $mainPart = <<
+
+
+
+
+
+
+
+
+
+ Foo {place.holder} bar
+
+
+
+
+EOD;
+
+ $templateProcessor = new TestablePhpWordTemplateProcessor($mainPart);
+ $templateProcessor->civiTokensToMacros();
+ $templateProcessor->replaceHtmlToken('place.holder', 'test 123');
+
+ $expectedMainPart = <<
+
+
+
+
+
+
+
+
+
+ Foo test 123 bar
+
+
+
+
+EOD;
+
+ static::assertXmlStringEqualsXmlString($expectedMainPart, $templateProcessor->getMainPart());
+ }
+
+ public function testReplaceSpan(): void {
+ $mainPart = <<
+
+
+
+
+
+
+
+
+
+ Foo {place.holder} bar
+
+
+
+
+EOD;
+
+ $templateProcessor = new TestablePhpWordTemplateProcessor($mainPart);
+ $templateProcessor->civiTokensToMacros();
+ $templateProcessor->replaceHtmlToken('place.holder', 'test 123');
+
+ $expectedMainPart = <<
+
+
+
+
+
+
+
+
+
+ Foo test 123 bar
+
+
+
+
+EOD;
+
+ static::assertXmlStringEqualsXmlString($expectedMainPart, $templateProcessor->getMainPart());
+ }
+
+ public function testReplaceSpanMultiple(): void {
+ $mainPart = <<
+
+
+
+
+
+
+
+
+
+ Foo {place.holder}
+
+
+
+
+EOD;
+
+ $templateProcessor = new TestablePhpWordTemplateProcessor($mainPart);
+ $templateProcessor->civiTokensToMacros();
+ $templateProcessor->replaceHtmlToken('place.holder', 'test 123');
+
+ $expectedMainPart = <<
+
+
+
+
+
+
+
+
+
+ Foo
+
+
+
+
+
+ test
+
+
+
+
+
+ 123
+
+
+
+
+EOD;
+
+ static::assertXmlStringEqualsXmlString($expectedMainPart, $templateProcessor->getMainPart());
+ }
+
+ /**
+ * When a token is replaced with a paragraph, property tags (pPr, rRr) should
+ * be copied into each paragraph/text run.
+ */
+ public function testReplaceParagraphEnclosed(): void {
+ $mainPart = <<
+
+
+
+
+
+
+
+
+
+ Foo {place.holder} bar
+
+
+
+
+EOD;
+
+ $templateProcessor = new TestablePhpWordTemplateProcessor($mainPart);
+ $templateProcessor->civiTokensToMacros();
+ $templateProcessor->replaceHtmlToken('place.holder', 'test 123
');
+
+ $expectedMainPart = <<
+
+
+
+
+
+
+
+
+
+ Foo
+
+
+
+
+
+
+
+
+
+
+ test 123
+
+
+
+
+
+
+
+
+
+
+ bar
+
+
+
+
+EOD;
+
+ static::assertXmlStringEqualsXmlString($expectedMainPart, $templateProcessor->getMainPart());
+ }
+
+ /**
+ * Test that the result contains no empty paragraph, if the token is at the
+ * beginning of a paragraph.
+ *
+ * When a token is replaced with a paragraph, property tags (pPr, rRr) should
+ * be copied into each paragraph/text run.
+ */
+ public function testReplaceParagraphStart(): void {
+ $mainPart = <<
+
+
+
+
+
+
+
+
+
+ {place.holder} bar
+
+
+
+
+EOD;
+
+ $templateProcessor = new TestablePhpWordTemplateProcessor($mainPart);
+ $templateProcessor->civiTokensToMacros();
+ $templateProcessor->replaceHtmlToken('place.holder', 'test 123
');
+
+ $expectedMainPart = <<
+
+
+
+
+
+
+
+
+
+ test 123
+
+
+
+
+
+
+
+
+
+
+ bar
+
+
+
+
+EOD;
+
+ static::assertXmlStringEqualsXmlString($expectedMainPart, $templateProcessor->getMainPart());
+ }
+
+ /**
+ * Test that the result contains no empty paragraph, if the token is at the
+ * end of a paragraph.
+ *
+ * When a token is replaced with a paragraph, property tags (pPr, rRr) should
+ * be copied into each paragraph/text run.
+ */
+ public function testReplaceParagraphEnd(): void {
+ $mainPart = <<
+
+
+
+
+
+
+
+
+
+ Foo {place.holder}
+
+
+
+
+EOD;
+
+ $templateProcessor = new TestablePhpWordTemplateProcessor($mainPart);
+ $templateProcessor->civiTokensToMacros();
+ $templateProcessor->replaceHtmlToken('place.holder', 'test 123
');
+
+ $expectedMainPart = <<
+
+
+
+
+
+
+
+
+
+ Foo
+
+
+
+
+
+
+
+
+
+
+ test 123
+
+
+
+
+EOD;
+
+ static::assertXmlStringEqualsXmlString($expectedMainPart, $templateProcessor->getMainPart());
+ }
+
+}
diff --git a/tests/phpunit/Civi/Civioffice/DocumentRendererType/LocalUnoconv/TestablePhpWordTemplateProcessor.php b/tests/phpunit/Civi/Civioffice/DocumentRendererType/LocalUnoconv/TestablePhpWordTemplateProcessor.php
new file mode 100644
index 0000000..5998049
--- /dev/null
+++ b/tests/phpunit/Civi/Civioffice/DocumentRendererType/LocalUnoconv/TestablePhpWordTemplateProcessor.php
@@ -0,0 +1,39 @@
+.
+ */
+
+declare(strict_types = 1);
+
+namespace Civi\Civioffice\DocumentRendererType\LocalUnoconv;
+
+/**
+ * phpcs:disable Generic.Files.LineLength.TooLong
+ */
+final class TestablePhpWordTemplateProcessor extends \CRM_Civioffice_DocumentRendererType_LocalUnoconv_PhpWordTemplateProcessor {
+// phpcs:enable
+
+ /**
+ * @phpstan-ignore-next-line Parent constructor is not called.
+ */
+ public function __construct(string $mainPart) {
+ $this->tempDocumentMainPart = $mainPart;
+ }
+
+ public function getMainPart(): string {
+ return $this->tempDocumentMainPart;
+ }
+
+}