From c4bcba97416afd7bbe4f934a7b2f6d231515e936 Mon Sep 17 00:00:00 2001 From: Smaine Milianni Date: Fri, 15 Jul 2022 01:26:53 +0100 Subject: [PATCH] allow to customize macro in TemplateProcessor --- docs/templates-processing.rst | 26 +- src/PhpWord/TemplateProcessor.php | 57 +- tests/PhpWordTests/TemplateProcessorTest.php | 572 ++++++++++++++++++ .../clone-merge-with-custom-macro.docx | Bin 0 -> 8566 bytes .../document22-with-custom-macro-xml.docx | Bin 0 -> 11144 bytes .../header-footer-with-custom-macro.docx | Bin 0 -> 15776 bytes 6 files changed, 645 insertions(+), 10 deletions(-) create mode 100644 tests/PhpWordTests/_files/templates/clone-merge-with-custom-macro.docx create mode 100644 tests/PhpWordTests/_files/templates/document22-with-custom-macro-xml.docx create mode 100644 tests/PhpWordTests/_files/templates/header-footer-with-custom-macro.docx diff --git a/docs/templates-processing.rst b/docs/templates-processing.rst index 7d0ef2e654..df6231287d 100644 --- a/docs/templates-processing.rst +++ b/docs/templates-processing.rst @@ -4,7 +4,7 @@ Templates processing ==================== You can create an OOXML document template with included search-patterns (macros) which can be replaced by any value you wish. Only single-line values can be replaced. -Macros are defined like this: ``${search-pattern}``. +By default Macros are defined like this: ``${search-pattern}`` but you can define custom macros. To load a template file, create a new instance of the TemplateProcessor. .. code-block:: php @@ -35,6 +35,30 @@ You can also set multiple values by passing all of them in an array. $templateProcessor->setValues(array('firstname' => 'John', 'lastname' => 'Doe')); +setMacroOpeningChars +"""""""" +You can define a custom opening macro. The following will set ``{#`` as the opening search pattern. + +.. code-block:: php + + $templateProcessor->setMacroOpeningChars('{#'); + +setMacroClosingChars +"""""""" +You can define a custom closing macro. The following will set ``#}`` as the closing search pattern. + +.. code-block:: php + + $templateProcessor->setMacroClosingChars('#}'); + +setMacroChars +"""""""" +You can define a custom opening and closing macro at the same time . The following will set the search-pattern like this: ``{#search-pattern#}`` . + +.. code-block:: php + + $templateProcessor->setMacroChars('{#', '#}'); + setImageValue """"""""""""" The search-pattern model for images can be like: diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 1cf3242984..6421dff0b3 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -94,6 +94,10 @@ class TemplateProcessor */ protected $tempDocumentNewImages = []; + protected static $macroOpeningChars = '${'; + + protected static $macroClosingChars = '}'; + /** * @since 0.12.0 Throws CreateTemporaryFileException and CopyFileException instead of Exception * @@ -238,8 +242,8 @@ public function applyXslStyleSheet($xslDomDocument, $xslOptions = [], $xslOption */ protected static function ensureMacroCompleted($macro) { - if (substr($macro, 0, 2) !== '${' && substr($macro, -1) !== '}') { - $macro = '${' . $macro . '}'; + if (substr($macro, 0, 2) !== self::$macroOpeningChars && substr($macro, -1) !== self::$macroClosingChars) { + $macro = self::$macroOpeningChars . $macro . self::$macroClosingChars; } return $macro; @@ -792,8 +796,12 @@ public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVaria { $xmlBlock = null; $matches = []; + $escapedMacroOpeningChars = self::$macroOpeningChars; + $escapedMacroClosingChars = self::$macroClosingChars; preg_match( - '/(.*((?s)))(.*)((?s))/is', + //'/(.*((?s)))(.*)((?s))/is', + '/(.*((?s)))(.*)((?s))/is', + //'/(.*((?s)))(.*)((?s))/is', $this->tempDocumentMainPart, $matches ); @@ -832,8 +840,10 @@ public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVaria public function replaceBlock($blockname, $replacement): void { $matches = []; + $escapedMacroOpeningChars = preg_quote(self::$macroOpeningChars); + $escapedMacroClosingChars = preg_quote(self::$macroClosingChars); preg_match( - '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', + '/(<\?xml.*)(' . $escapedMacroOpeningChars . $blockname . $escapedMacroClosingChars . '<\/w:.*?p>)(.*)()/is', $this->tempDocumentMainPart, $matches ); @@ -949,8 +959,12 @@ public function saveAs($fileName): void */ protected function fixBrokenMacros($documentPart) { + $brokenMacroOpeningChars = substr(self::$macroOpeningChars, 0, 1); + $endMacroOpeningChars = substr(self::$macroOpeningChars, 1); + $macroClosingChars = self::$macroClosingChars; + return preg_replace_callback( - '/\$(?:\{|[^{$]*\>\{)[^}$]*\}/U', + '/\\' . $brokenMacroOpeningChars . '(?:\\' . $endMacroOpeningChars . '|[^{$]*\>\{)[^' . $macroClosingChars . '$]*\}/U', function ($match) { return strip_tags($match[0]); }, @@ -989,7 +1003,10 @@ protected function setValueForPart($search, $replace, $documentPartXML, $limit) protected function getVariablesForPart($documentPartXML) { $matches = []; - preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches); + $escapedMacroOpeningChars = preg_quote(self::$macroOpeningChars); + $escapedMacroClosingChars = preg_quote(self::$macroClosingChars); + + preg_match_all("/$escapedMacroOpeningChars(.*?)$escapedMacroClosingChars/i", $documentPartXML, $matches); return $matches[1]; } @@ -1141,8 +1158,11 @@ protected function getSlice($startPosition, $endPosition = 0) protected function indexClonedVariables($count, $xmlBlock) { $results = []; + $escapedMacroOpeningChars = preg_quote(self::$macroOpeningChars); + $escapedMacroClosingChars = preg_quote(self::$macroClosingChars); + for ($i = 1; $i <= $count; ++$i) { - $results[] = preg_replace('/\$\{([^:]*?)(:.*?)?\}/', '\${\1#' . $i . '\2}', $xmlBlock); + $results[] = preg_replace("/$escapedMacroOpeningChars([^:]*?)(:.*?)?$escapedMacroClosingChars/", self::$macroOpeningChars . '\1#' . $i . '\2' . self::$macroClosingChars, $xmlBlock); } return $results; @@ -1297,7 +1317,7 @@ protected function splitTextIntoTexts($text) } $unformattedText = preg_replace('/>\s+<', $text); - $result = str_replace(['${', '}'], ['' . $extractedStyle . '${', '}' . $extractedStyle . ''], $unformattedText); + $result = str_replace([self::$macroOpeningChars, self::$macroClosingChars], ['' . $extractedStyle . '' . self::$macroOpeningChars, self::$macroClosingChars . '' . $extractedStyle . ''], $unformattedText); return str_replace(['' . $extractedStyle . '', '', ''], ['', '', ''], $result); } @@ -1311,6 +1331,25 @@ protected function splitTextIntoTexts($text) */ protected function textNeedsSplitting($text) { - return preg_match('/[^>]\${|}[^<]/i', $text) == 1; + $escapedMacroOpeningChars = preg_quote(self::$macroOpeningChars); + $escapedMacroClosingChars = preg_quote(self::$macroClosingChars); + + return 1 === preg_match('/[^>]' . $escapedMacroOpeningChars . '|' . $escapedMacroClosingChars . '[^<]/i', $text); + } + + public function setMacroOpeningChars(string $macroOpeningChars): void + { + self::$macroOpeningChars = $macroOpeningChars; + } + + public function setMacroClosingChars(string $macroClosingChars): void + { + self::$macroClosingChars = $macroClosingChars; + } + + public function setMacroChars(string $macroOpeningChars, string $macroClosingChars): void + { + self::$macroOpeningChars = $macroOpeningChars; + self::$macroClosingChars = $macroClosingChars; } } diff --git a/tests/PhpWordTests/TemplateProcessorTest.php b/tests/PhpWordTests/TemplateProcessorTest.php index fc975fab57..e80bf7754d 100644 --- a/tests/PhpWordTests/TemplateProcessorTest.php +++ b/tests/PhpWordTests/TemplateProcessorTest.php @@ -206,6 +206,33 @@ public function testCloneRow(): void self::assertTrue($docFound); } + /** + * @covers ::cloneRow + * @covers ::saveAs + * @covers ::setValue + */ + public function testCloneRowWithCustomMacro(): void + { + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/clone-merge-with-custom-macro.docx'); + + $templateProcessor->setMacroOpeningChars('{#'); + $templateProcessor->setMacroClosingChars('#}'); + + self::assertEquals( + ['tableHeader', 'userId', 'userName', 'userLocation'], + $templateProcessor->getVariables() + ); + + $docName = 'clone-test-result.docx'; + $templateProcessor->setValue('tableHeader', utf8_decode('ééé')); + $templateProcessor->cloneRow('userId', 1); + $templateProcessor->setValue('userId#1', 'Test'); + $templateProcessor->saveAs($docName); + $docFound = file_exists($docName); + unlink($docName); + self::assertTrue($docFound); + } + /** * @covers ::cloneRow * @covers ::saveAs @@ -275,6 +302,68 @@ public function testCloneNotExistingRowShouldThrowException(): void $templateProcessor->cloneRow('fake_search', 2); } + /** + * @covers ::cloneRow + * @covers ::saveAs + * @covers ::setValue + */ + public function testCloneRowAndSetValuesWithCustomMacro(): void + { + $mainPart = ' + + + + + + + + {{userId}} + + + + + + + {{userName}} + + + + + + + + + + + + + + + {{userLocation}} + + + + + '; + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setMacroOpeningChars('{{'); + $templateProcessor->setMacroClosingChars('}}'); + + self::assertEquals( + ['userId', 'userName', 'userLocation'], + $templateProcessor->getVariables() + ); + + $values = [ + ['userId' => 1, 'userName' => 'Batman', 'userLocation' => 'Gotham City'], + ['userId' => 2, 'userName' => 'Superman', 'userLocation' => 'Metropolis'], + ]; + $templateProcessor->setValue('tableHeader', 'My clonable table'); + $templateProcessor->cloneRowAndSetValues('userId', $values); + self::assertStringContainsString('Superman', $templateProcessor->getMainPart()); + self::assertStringContainsString('Metropolis', $templateProcessor->getMainPart()); + } + /** * @covers ::saveAs * @covers ::setValue @@ -296,6 +385,29 @@ public function testMacrosCanBeReplacedInHeaderAndFooter(): void self::assertTrue($docFound); } + /** + * @covers ::saveAs + * @covers ::setValue + */ + public function testCustomMacrosCanBeReplacedInHeaderAndFooter(): void + { + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/header-footer-with-custom-macro.docx'); + $templateProcessor->setMacroOpeningChars('{{'); + $templateProcessor->setMacroClosingChars('}}'); + + self::assertEquals(['documentContent', 'headerValue:100:100', 'footerValue'], $templateProcessor->getVariables()); + + $macroNames = ['headerValue', 'documentContent', 'footerValue']; + $macroValues = ['Header Value', 'Document text.', 'Footer Value']; + $templateProcessor->setValue($macroNames, $macroValues); + + $docName = 'header-footer-test-result.docx'; + $templateProcessor->saveAs($docName); + $docFound = file_exists($docName); + unlink($docName); + self::assertTrue($docFound); + } + /** * @covers ::setValue */ @@ -311,6 +423,22 @@ public function testSetValue(): void ); } + /** + * @covers ::setValue + */ + public function testSetValueWithCustomMacro(): void + { + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/clone-merge-with-custom-macro.docx'); + $templateProcessor->setMacroChars('{#', '#}'); + Settings::setOutputEscapingEnabled(true); + $helloworld = "hello\nworld"; + $templateProcessor->setValue('userName', $helloworld); + self::assertEquals( + ['tableHeader', 'userId', 'userLocation'], + $templateProcessor->getVariables() + ); + } + public function testSetComplexValue(): void { $title = new TextRun(); @@ -364,6 +492,60 @@ public function testSetComplexValue(): void self::assertEquals(preg_replace('/>\s+<', $result), preg_replace('/>\s+<', $templateProcessor->getMainPart())); } + public function testSetComplexValueWithCustomMacro(): void + { + $title = new TextRun(); + $title->addText('This is my title'); + + $firstname = new Text('Donald'); + $lastname = new Text('Duck'); + + $mainPart = ' + + + Hello {{document-title}} + + + + + Hello {{firstname}} {{lastname}} + + '; + + $result = ' + + + + + This is my title + + + + + Hello + + + + Donald + + + + + + + Duck + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setMacroChars('{{', '}}'); + $templateProcessor->setComplexBlock('document-title', $title); + $templateProcessor->setComplexValue('firstname', $firstname); + $templateProcessor->setComplexValue('lastname', $lastname); + + self::assertEquals(preg_replace('/>\s+<', $result), preg_replace('/>\s+<', $templateProcessor->getMainPart())); + } + /** * @covers ::setValues */ @@ -382,6 +564,25 @@ public function testSetValues(): void self::assertStringContainsString('Hello John Doe', $templateProcessor->getMainPart()); } + /** + * @covers ::setValues + */ + public function testSetValuesWithCustomMacro(): void + { + $mainPart = ' + + + Hello {#firstname#} {#lastname#} + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setMacroChars('{#', '#}'); + $templateProcessor->setValues(['firstname' => 'John', 'lastname' => 'Doe']); + + self::assertStringContainsString('Hello John Doe', $templateProcessor->getMainPart()); + } + /** * @covers ::setImageValue */ @@ -521,6 +722,44 @@ public function testGetVariableCountCountsHowManyTimesEachPlaceholderIsPresent() ); } + /** + * @covers ::getVariableCount + */ + public function testGetVariableCountCountsHowManyTimesEachPlaceholderIsPresentWithCustomMacro(): void + { + // create template with placeholders + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $header = $section->addHeader(); + $header->addText('{{a_field_that_is_present_three_times}}'); + $footer = $section->addFooter(); + $footer->addText('{{a_field_that_is_present_twice}}'); + $section2 = $phpWord->addSection(); + $section2->addText(' + {{a_field_that_is_present_one_time}} + {{a_field_that_is_present_three_times}} + {{a_field_that_is_present_twice}} + {{a_field_that_is_present_three_times}} + '); + $objWriter = IOFactory::createWriter($phpWord); + $templatePath = 'test.docx'; + $objWriter->save($templatePath); + + $templateProcessor = new TemplateProcessor($templatePath); + $templateProcessor->setMacroChars('{{', '}}'); + $variableCount = $templateProcessor->getVariableCount(); + unlink($templatePath); + + self::assertEquals( + [ + 'a_field_that_is_present_three_times' => 3, + 'a_field_that_is_present_twice' => 2, + 'a_field_that_is_present_one_time' => 1, + ], + $variableCount + ); + } + /** * @covers ::cloneBlock */ @@ -574,6 +813,61 @@ public function testCloneBlockCanCloneABlockTwice(): void } } + /** + * @covers ::cloneBlock + */ + public function testCloneBlockCanCloneABlockTwiceWithCustomMacro(): void + { + // create template with placeholders and block + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $documentElements = [ + 'Title: {{title}}', + '{{subreport}}', + '{{subreport.id}}: {{subreport.text}}. ', + '{{/subreport}}', + ]; + foreach ($documentElements as $documentElement) { + $section->addText($documentElement); + } + + $objWriter = IOFactory::createWriter($phpWord); + $templatePath = 'test.docx'; + $objWriter->save($templatePath); + + // replace placeholders and save the file + $templateProcessor = new TemplateProcessor($templatePath); + $templateProcessor->setMacroChars('{{', '}}'); + $templateProcessor->setValue('title', 'Some title'); + $templateProcessor->cloneBlock('subreport', 2); + $templateProcessor->setValue('subreport.id', '123', 1); + $templateProcessor->setValue('subreport.text', 'Some text', 1); + $templateProcessor->setValue('subreport.id', '456', 1); + $templateProcessor->setValue('subreport.text', 'Some other text', 1); + $templateProcessor->saveAs($templatePath); + + // assert the block has been cloned twice + // and the placeholders have been replaced correctly + $phpWord = IOFactory::load($templatePath); + $sections = $phpWord->getSections(); + /** @var \PhpOffice\PhpWord\Element\TextRun[] $actualElements */ + $actualElements = $sections[0]->getElements(); + + unlink($templatePath); + $expectedElements = [ + 'Title: Some title', + '123: Some text. ', + '456: Some other text. ', + ]; + self::assertCount(count($expectedElements), $actualElements); + foreach ($expectedElements as $i => $expectedElement) { + self::assertEquals( + $expectedElement, + $actualElements[$i]->getElement(0)->getText() + ); + } + } + /** * @covers ::cloneBlock */ @@ -603,6 +897,36 @@ public function testCloneBlock(): void self::assertEquals(3, substr_count($templateProcessor->getMainPart(), 'This block will be cloned with ${variable}')); } + /** + * @covers ::cloneBlock + */ + public function testCloneBlockWithCustomMacro(): void + { + $mainPart = ' + + + + {{CLONEME}} + + + + + This block will be cloned with {{variable}} + + + + + {{/CLONEME}} + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setMacroChars('{{', '}}'); + $templateProcessor->cloneBlock('CLONEME', 3); + + self::assertEquals(3, substr_count($templateProcessor->getMainPart(), 'This block will be cloned with {{variable}}')); + } + /** * @covers ::cloneBlock */ @@ -634,6 +958,38 @@ public function testCloneBlockWithVariables(): void self::assertStringContainsString('Address ${address#3}, Street ${street#3}', $templateProcessor->getMainPart()); } + /** + * @covers ::cloneBlock + */ + public function testCloneBlockWithVariablesAndCustomMacro(): void + { + $mainPart = ' + + + + {{CLONEME}} + + + + + Address {{address}}, Street {{street}} + + + + + {{/CLONEME}} + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setMacroChars('{{', '}}'); + $templateProcessor->cloneBlock('CLONEME', 3, true, true); + + self::assertStringContainsString('Address {{address#1}}, Street {{street#1}}', $templateProcessor->getMainPart()); + self::assertStringContainsString('Address {{address#2}}, Street {{street#2}}', $templateProcessor->getMainPart()); + self::assertStringContainsString('Address {{address#3}}, Street {{street#3}}', $templateProcessor->getMainPart()); + } + public function testCloneBlockWithVariableReplacements(): void { $mainPart = ' @@ -667,6 +1023,40 @@ public function testCloneBlockWithVariableReplacements(): void self::assertStringContainsString('City: Rome, Street: Via della Conciliazione', $templateProcessor->getMainPart()); } + public function testCloneBlockWithVariableReplacementsAndCustomMacro(): void + { + $mainPart = ' + + + + {{CLONEME}} + + + + + City: {{city}}, Street: {{street}} + + + + + {{/CLONEME}} + + '; + + $replacements = [ + ['city' => 'London', 'street' => 'Baker Street'], + ['city' => 'New York', 'street' => '5th Avenue'], + ['city' => 'Rome', 'street' => 'Via della Conciliazione'], + ]; + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setMacroChars('{{', '}}'); + $templateProcessor->cloneBlock('CLONEME', 0, true, false, $replacements); + + self::assertStringContainsString('City: London, Street: Baker Street', $templateProcessor->getMainPart()); + self::assertStringContainsString('City: New York, Street: 5th Avenue', $templateProcessor->getMainPart()); + self::assertStringContainsString('City: Rome, Street: Via della Conciliazione', $templateProcessor->getMainPart()); + } + /** * Template macros can be fixed. * @@ -698,6 +1088,38 @@ public function testFixBrokenMacros(): void self::assertEquals('$15,000.00. ${variable_name}', $fixed); } + /** + * Template macros can be fixed even with cutome macro. + * + * @covers ::fixBrokenMacros + */ + public function testFixBrokenMacrosWithCustomMacro(): void + { + $templateProcessor = new TestableTemplateProcesor(); + $templateProcessor->setMacroChars('{{', '}}'); + + $fixed = $templateProcessor->fixBrokenMacros('normal text'); + self::assertEquals('normal text', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('{{documentContent}}'); + self::assertEquals('{{documentContent}}', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('{{documentContent}}'); + self::assertEquals('{{documentContent}}', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('$1500{{documentContent}}'); + self::assertEquals('$1500{{documentContent}}', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('$1500{{documentContent}}'); + self::assertEquals('$1500{{documentContent}}', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('25$ plus some info {hint}'); + self::assertEquals('25$ plus some info {hint}', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('$15,000.00. {{variable_name}}'); + self::assertEquals('$15,000.00. {{variable_name}}', $fixed); + } + /** * @covers ::getMainPartName */ @@ -710,6 +1132,19 @@ public function testMainPartNameDetection(): void self::assertEquals($variables, $templateProcessor->getVariables()); } + /** + * @covers ::getMainPartName + */ + public function testMainPartNameDetectionWithCustomMacro(): void + { + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/document22-with-custom-macro-xml.docx'); + $templateProcessor->setMacroOpeningChars('{#'); + $templateProcessor->setMacroClosingChars('#}'); + $variables = ['test']; + + self::assertEquals($variables, $templateProcessor->getVariables()); + } + /** * @covers ::getVariables */ @@ -727,6 +1162,25 @@ public function testGetVariables(): void self::assertEquals(['variable_name'], $variables); } + /** + * @covers ::getVariables + */ + public function testGetVariablesWithCustomMacro(): void + { + $templateProcessor = new TestableTemplateProcesor(); + $templateProcessor->setMacroOpeningChars('{{'); + $templateProcessor->setMacroClosingChars('}}'); + + $variables = $templateProcessor->getVariablesForPart('normal text'); + self::assertEquals([], $variables); + + $variables = $templateProcessor->getVariablesForPart('{{documentContent}}'); + self::assertEquals(['documentContent'], $variables); + + $variables = $templateProcessor->getVariablesForPart('{15,000.00. {{variable_name}}'); + self::assertEquals(['variable_name'], $variables); + } + /** * @covers ::textNeedsSplitting */ @@ -742,6 +1196,22 @@ public function testTextNeedsSplitting(): void self::assertFalse($templateProcessor->textNeedsSplitting($splitText)); } + /** + * @covers ::textNeedsSplitting + */ + public function testTextNeedsSplittingWithCustomMacro(): void + { + $templateProcessor = new TestableTemplateProcesor(); + $templateProcessor->setMacroChars('{{', '}}'); + + self::assertFalse($templateProcessor->textNeedsSplitting('{{nothing-to-replace}}')); + + $text = 'Hello {{firstname}} {{lastname}}'; + self::assertTrue($templateProcessor->textNeedsSplitting($text)); + $splitText = $templateProcessor->splitTextIntoTexts($text); + self::assertFalse($templateProcessor->textNeedsSplitting($splitText)); + } + /** * @covers ::splitTextIntoTexts */ @@ -756,6 +1226,21 @@ public function testSplitTextIntoTexts(): void self::assertEquals('Hello ${firstname} ${lastname}', $splitText); } + /** + * @covers ::splitTextIntoTexts + */ + public function testSplitTextIntoTextsWithCustomMacro(): void + { + $templateProcessor = new TestableTemplateProcesor(); + $templateProcessor->setMacroChars('{{', '}}'); + + $splitText = $templateProcessor->splitTextIntoTexts('{{nothing-to-replace}}'); + self::assertEquals('{{nothing-to-replace}}', $splitText); + + $splitText = $templateProcessor->splitTextIntoTexts('Hello {{firstname}} {{lastname}}'); + self::assertEquals('Hello {{firstname}} {{lastname}}', $splitText); + } + public function testFindXmlBlockStart(): void { $toFind = ' @@ -799,6 +1284,50 @@ public function testFindXmlBlockStart(): void self::assertEquals($toFind, $templateProcessor->getSlice($position['start'], $position['end'])); } + public function testFindXmlBlockStartWithCustomMacro(): void + { + $toFind = ' + + + + + This whole paragraph will be replaced with my {{title}} + '; + $mainPart = ' + + + + + + + {{value1}} {{value2}} + + + + + + + . + + + + + + + + + + ' . $toFind . ' + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setMacroChars('{{', '}}'); + $position = $templateProcessor->findContainingXmlBlockForMacro('{{title}}', 'w:r'); + + self::assertEquals($toFind, $templateProcessor->getSlice($position['start'], $position['end'])); + } + public function testShouldReturnFalseIfXmlBlockNotFound(): void { $mainPart = ' @@ -826,6 +1355,34 @@ public function testShouldReturnFalseIfXmlBlockNotFound(): void self::assertFalse($result); } + public function testShouldReturnFalseIfXmlBlockNotFoundWithCustomMacro(): void + { + $mainPart = ' + + + + + + this is my text containing a ${macro} + + + '; + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setMacroChars('{{', '}}'); + + //non-existing macro + $result = $templateProcessor->findContainingXmlBlockForMacro('{{fake-macro}}', 'w:p'); + self::assertFalse($result); + + //existing macro but not inside node looked for + $result = $templateProcessor->findContainingXmlBlockForMacro('{{macro}}', 'w:fake-node'); + self::assertFalse($result); + + //existing macro but end tag not found after macro + $result = $templateProcessor->findContainingXmlBlockForMacro('{{macro}}', 'w:rPr'); + self::assertFalse($result); + } + public function testShouldMakeFieldsUpdateOnOpen(): void { $settingsPart = ' @@ -839,4 +1396,19 @@ public function testShouldMakeFieldsUpdateOnOpen(): void $templateProcessor->setUpdateFields(false); self::assertStringContainsString('', $templateProcessor->getSettingsPart()); } + + public function testShouldMakeFieldsUpdateOnOpenWithCustomMacro(): void + { + $settingsPart = ' + + '; + $templateProcessor = new TestableTemplateProcesor(null, $settingsPart); + $templateProcessor->setMacroChars('{{', '}}'); + + $templateProcessor->setUpdateFields(true); + self::assertStringContainsString('', $templateProcessor->getSettingsPart()); + + $templateProcessor->setUpdateFields(false); + self::assertStringContainsString('', $templateProcessor->getSettingsPart()); + } } diff --git a/tests/PhpWordTests/_files/templates/clone-merge-with-custom-macro.docx b/tests/PhpWordTests/_files/templates/clone-merge-with-custom-macro.docx new file mode 100644 index 0000000000000000000000000000000000000000..e023ed07657efb28f93ecc5537c9572e69027873 GIT binary patch literal 8566 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 literal 0 HcmV?d00001 diff --git a/tests/PhpWordTests/_files/templates/header-footer-with-custom-macro.docx b/tests/PhpWordTests/_files/templates/header-footer-with-custom-macro.docx new file mode 100644 index 0000000000000000000000000000000000000000..19d5669335774617d4188360de393e410882211d GIT binary patch literal 15776 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 literal 0 HcmV?d00001