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; + } + +}