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 0000000000..e023ed0765 Binary files /dev/null and b/tests/PhpWordTests/_files/templates/clone-merge-with-custom-macro.docx differ diff --git a/tests/PhpWordTests/_files/templates/document22-with-custom-macro-xml.docx b/tests/PhpWordTests/_files/templates/document22-with-custom-macro-xml.docx new file mode 100644 index 0000000000..5aba782b65 Binary files /dev/null and b/tests/PhpWordTests/_files/templates/document22-with-custom-macro-xml.docx differ 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 0000000000..19d5669335 Binary files /dev/null and b/tests/PhpWordTests/_files/templates/header-footer-with-custom-macro.docx differ