Merge pull request #2261 from ismail1432/customize-macro

Allow to customize macro delimiters in TemplateProcessor
This commit is contained in:
Progi1984 2022-12-23 19:36:07 +01:00 committed by GitHub
commit b453cf00ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 645 additions and 10 deletions

View File

@ -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:

View File

@ -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 @@ class TemplateProcessor
*/
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;
@ -856,8 +860,12 @@ class TemplateProcessor
{
$xmlBlock = null;
$matches = [];
$escapedMacroOpeningChars = self::$macroOpeningChars;
$escapedMacroClosingChars = self::$macroClosingChars;
preg_match(
'/(.*((?s)<w:p\b(?:(?!<w:p\b).)*?\${' . $blockname . '}<\/w:.*?p>))(.*)((?s)<w:p\b(?:(?!<w:p\b).)[^$]*?\${\/' . $blockname . '}<\/w:.*?p>)/is',
//'/(.*((?s)<w:p\b(?:(?!<w:p\b).)*?\{{' . $blockname . '}<\/w:.*?p>))(.*)((?s)<w:p\b(?:(?!<w:p\b).)[^$]*?\{{\/' . $blockname . '}<\/w:.*?p>)/is',
'/(.*((?s)<w:p\b(?:(?!<w:p\b).)*?\\' . $escapedMacroOpeningChars . $blockname . $escapedMacroClosingChars . '<\/w:.*?p>))(.*)((?s)<w:p\b(?:(?!<w:p\b).)[^$]*?\\' . $escapedMacroOpeningChars . '\/' . $blockname . $escapedMacroClosingChars . '<\/w:.*?p>)/is',
//'/(.*((?s)<w:p\b(?:(?!<w:p\b).)*?\\'. $escapedMacroOpeningChars . $blockname . '}<\/w:.*?p>))(.*)((?s)<w:p\b(?:(?!<w:p\b).)[^$]*?\\'.$escapedMacroOpeningChars.'\/' . $blockname . '}<\/w:.*?p>)/is',
$this->tempDocumentMainPart,
$matches
);
@ -896,8 +904,10 @@ class TemplateProcessor
public function replaceBlock($blockname, $replacement): void
{
$matches = [];
$escapedMacroOpeningChars = preg_quote(self::$macroOpeningChars);
$escapedMacroClosingChars = preg_quote(self::$macroClosingChars);
preg_match(
'/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
'/(<\?xml.*)(<w:p.*>' . $escapedMacroOpeningChars . $blockname . $escapedMacroClosingChars . '<\/w:.*?p>)(.*)(<w:p.*' . $escapedMacroOpeningChars . '\/' . $blockname . $escapedMacroClosingChars . '<\/w:.*?p>)/is',
$this->tempDocumentMainPart,
$matches
);
@ -1013,8 +1023,12 @@ class TemplateProcessor
*/
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]);
},
@ -1053,7 +1067,10 @@ class TemplateProcessor
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];
}
@ -1238,8 +1255,11 @@ class TemplateProcessor
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;
@ -1394,7 +1414,7 @@ class TemplateProcessor
}
$unformattedText = preg_replace('/>\s+</', '><', $text);
$result = str_replace(['${', '}'], ['</w:t></w:r><w:r>' . $extractedStyle . '<w:t xml:space="preserve">${', '}</w:t></w:r><w:r>' . $extractedStyle . '<w:t xml:space="preserve">'], $unformattedText);
$result = str_replace([self::$macroOpeningChars, self::$macroClosingChars], ['</w:t></w:r><w:r>' . $extractedStyle . '<w:t xml:space="preserve">' . self::$macroOpeningChars, self::$macroClosingChars . '</w:t></w:r><w:r>' . $extractedStyle . '<w:t xml:space="preserve">'], $unformattedText);
return str_replace(['<w:r>' . $extractedStyle . '<w:t xml:space="preserve"></w:t></w:r>', '<w:r><w:t xml:space="preserve"></w:t></w:r>', '<w:t>'], ['', '', '<w:t xml:space="preserve">'], $result);
}
@ -1408,6 +1428,25 @@ class TemplateProcessor
*/
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;
}
}

View File

@ -232,6 +232,33 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
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
@ -301,6 +328,68 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
$templateProcessor->cloneRow('fake_search', 2);
}
/**
* @covers ::cloneRow
* @covers ::saveAs
* @covers ::setValue
*/
public function testCloneRowAndSetValuesWithCustomMacro(): void
{
$mainPart = '<w:tbl>
<w:tr>
<w:tc>
<w:tcPr>
<w:vMerge w:val="restart"/>
</w:tcPr>
<w:p>
<w:r>
<w:t>{{userId}}</w:t>
</w:r>
</w:p>
</w:tc>
<w:tc>
<w:p>
<w:r>
<w:t>{{userName}}</w:t>
</w:r>
</w:p>
</w:tc>
</w:tr>
<w:tr>
<w:tc>
<w:tcPr>
<w:vMerge/>
</w:tcPr>
<w:p/>
</w:tc>
<w:tc>
<w:p>
<w:r>
<w:t>{{userLocation}}</w:t>
</w:r>
</w:p>
</w:tc>
</w:tr>
</w:tbl>';
$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('<w:t>Superman</w:t>', $templateProcessor->getMainPart());
self::assertStringContainsString('<w:t>Metropolis</w:t>', $templateProcessor->getMainPart());
}
/**
* @covers ::saveAs
* @covers ::setValue
@ -322,6 +411,29 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
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
*/
@ -337,6 +449,22 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
);
}
/**
* @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();
@ -390,6 +518,60 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
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 = '<?xml version="1.0" encoding="UTF-8"?>
<w:p>
<w:r>
<w:t xml:space="preserve">Hello {{document-title}}</w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t xml:space="preserve">Hello {{firstname}} {{lastname}}</w:t>
</w:r>
</w:p>';
$result = '<?xml version="1.0" encoding="UTF-8"?>
<w:p>
<w:pPr/>
<w:r>
<w:rPr/>
<w:t xml:space="preserve">This is my title</w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t xml:space="preserve">Hello </w:t>
</w:r>
<w:r>
<w:rPr/>
<w:t xml:space="preserve">Donald</w:t>
</w:r>
<w:r>
<w:t xml:space="preserve"> </w:t>
</w:r>
<w:r>
<w:rPr/>
<w:t xml:space="preserve">Duck</w:t>
</w:r>
</w:p>';
$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
*/
@ -408,6 +590,25 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
self::assertStringContainsString('Hello John Doe', $templateProcessor->getMainPart());
}
/**
* @covers ::setValues
*/
public function testSetValuesWithCustomMacro(): void
{
$mainPart = '<?xml version="1.0" encoding="UTF-8"?>
<w:p>
<w:r>
<w:t xml:space="preserve">Hello {#firstname#} {#lastname#}</w:t>
</w:r>
</w:p>';
$templateProcessor = new TestableTemplateProcesor($mainPart);
$templateProcessor->setMacroChars('{#', '#}');
$templateProcessor->setValues(['firstname' => 'John', 'lastname' => 'Doe']);
self::assertStringContainsString('Hello John Doe', $templateProcessor->getMainPart());
}
/**
* @covers ::setImageValue
*/
@ -547,6 +748,44 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
);
}
/**
* @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
*/
@ -600,6 +839,61 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
}
}
/**
* @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
*/
@ -629,6 +923,36 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
self::assertEquals(3, substr_count($templateProcessor->getMainPart(), 'This block will be cloned with ${variable}'));
}
/**
* @covers ::cloneBlock
*/
public function testCloneBlockWithCustomMacro(): void
{
$mainPart = '<?xml version="1.0" encoding="UTF-8"?>
<w:p>
<w:r>
<w:rPr></w:rPr>
<w:t>{{CLONEME}}</w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t xml:space="preserve">This block will be cloned with {{variable}}</w:t>
</w:r>
</w:p>
<w:p>
<w:r w:rsidRPr="00204FED">
<w:t>{{/CLONEME}}</w:t>
</w:r>
</w:p>';
$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
*/
@ -660,6 +984,38 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
self::assertStringContainsString('Address ${address#3}, Street ${street#3}', $templateProcessor->getMainPart());
}
/**
* @covers ::cloneBlock
*/
public function testCloneBlockWithVariablesAndCustomMacro(): void
{
$mainPart = '<?xml version="1.0" encoding="UTF-8"?>
<w:p>
<w:r>
<w:rPr></w:rPr>
<w:t>{{CLONEME}}</w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t xml:space="preserve">Address {{address}}, Street {{street}}</w:t>
</w:r>
</w:p>
<w:p>
<w:r w:rsidRPr="00204FED">
<w:t>{{/CLONEME}}</w:t>
</w:r>
</w:p>';
$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 = '<?xml version="1.0" encoding="UTF-8"?>
@ -693,6 +1049,40 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
self::assertStringContainsString('City: Rome, Street: Via della Conciliazione', $templateProcessor->getMainPart());
}
public function testCloneBlockWithVariableReplacementsAndCustomMacro(): void
{
$mainPart = '<?xml version="1.0" encoding="UTF-8"?>
<w:p>
<w:r>
<w:rPr></w:rPr>
<w:t>{{CLONEME}}</w:t>
</w:r>
</w:p>
<w:p>
<w:r>
<w:t xml:space="preserve">City: {{city}}, Street: {{street}}</w:t>
</w:r>
</w:p>
<w:p>
<w:r w:rsidRPr="00204FED">
<w:t>{{/CLONEME}}</w:t>
</w:r>
</w:p>';
$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.
*
@ -724,6 +1114,38 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
self::assertEquals('<w:t>$</w:t></w:r><w:bookmarkStart w:id="0" w:name="_GoBack"/><w:bookmarkEnd w:id="0"/><w:r><w:t xml:space="preserve">15,000.00. </w:t></w:r><w:r w:rsidR="0056499B"><w:t>${variable_name}</w:t></w:r>', $fixed);
}
/**
* Template macros can be fixed even with cutome macro.
*
* @covers ::fixBrokenMacros
*/
public function testFixBrokenMacrosWithCustomMacro(): void
{
$templateProcessor = new TestableTemplateProcesor();
$templateProcessor->setMacroChars('{{', '}}');
$fixed = $templateProcessor->fixBrokenMacros('<w:r><w:t>normal text</w:t></w:r>');
self::assertEquals('<w:r><w:t>normal text</w:t></w:r>', $fixed);
$fixed = $templateProcessor->fixBrokenMacros('<w:r><w:t>{{documentContent}}</w:t></w:r>');
self::assertEquals('<w:r><w:t>{{documentContent}}</w:t></w:r>', $fixed);
$fixed = $templateProcessor->fixBrokenMacros('<w:r><w:t>{</w:t><w:t>{documentContent}}</w:t></w:r>');
self::assertEquals('<w:r><w:t>{{documentContent}}</w:t></w:r>', $fixed);
$fixed = $templateProcessor->fixBrokenMacros('<w:r><w:t>$1500</w:t><w:t>{{documentContent}}</w:t></w:r>');
self::assertEquals('<w:r><w:t>$1500</w:t><w:t>{{documentContent}}</w:t></w:r>', $fixed);
$fixed = $templateProcessor->fixBrokenMacros('<w:r><w:t>$1500</w:t><w:t>{</w:t><w:t>{documentContent}}</w:t></w:r>');
self::assertEquals('<w:r><w:t>$1500</w:t><w:t>{{documentContent}}</w:t></w:r>', $fixed);
$fixed = $templateProcessor->fixBrokenMacros('<w:r><w:t>25$ plus some info {hint}</w:t></w:r>');
self::assertEquals('<w:r><w:t>25$ plus some info {hint}</w:t></w:r>', $fixed);
$fixed = $templateProcessor->fixBrokenMacros('<w:t>$</w:t></w:r><w:bookmarkStart w:id="0" w:name="_GoBack"/><w:bookmarkEnd w:id="0"/><w:r><w:t xml:space="preserve">15,000.00. </w:t></w:r><w:r w:rsidR="0056499B"><w:t>{</w:t></w:r><w:r w:rsidR="00573DFD" w:rsidRPr="00573DFD"><w:rPr><w:iCs/></w:rPr><w:t>{</w:t></w:r><w:proofErr w:type="spellStart"/><w:r w:rsidR="00573DFD" w:rsidRPr="00573DFD"><w:rPr><w:iCs/></w:rPr><w:t>variable_name</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r w:rsidR="00573DFD" w:rsidRPr="00573DFD"><w:rPr><w:iCs/></w:rPr><w:t>}}</w:t></w:r>');
self::assertEquals('<w:t>$</w:t></w:r><w:bookmarkStart w:id="0" w:name="_GoBack"/><w:bookmarkEnd w:id="0"/><w:r><w:t xml:space="preserve">15,000.00. </w:t></w:r><w:r w:rsidR="0056499B"><w:t>{{variable_name}}</w:t></w:r>', $fixed);
}
/**
* @covers ::getMainPartName
*/
@ -736,6 +1158,19 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
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
*/
@ -753,6 +1188,25 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
self::assertEquals(['variable_name'], $variables);
}
/**
* @covers ::getVariables
*/
public function testGetVariablesWithCustomMacro(): void
{
$templateProcessor = new TestableTemplateProcesor();
$templateProcessor->setMacroOpeningChars('{{');
$templateProcessor->setMacroClosingChars('}}');
$variables = $templateProcessor->getVariablesForPart('<w:r><w:t>normal text</w:t></w:r>');
self::assertEquals([], $variables);
$variables = $templateProcessor->getVariablesForPart('<w:r><w:t>{{documentContent}}</w:t></w:r>');
self::assertEquals(['documentContent'], $variables);
$variables = $templateProcessor->getVariablesForPart('<w:t>{</w:t></w:r><w:bookmarkStart w:id="0" w:name="_GoBack"/><w:bookmarkEnd w:id="0"/><w:r><w:t xml:space="preserve">15,000.00. </w:t></w:r><w:r w:rsidR="0056499B"><w:t>{</w:t></w:r><w:r w:rsidR="00573DFD" w:rsidRPr="00573DFD"><w:rPr><w:iCs/></w:rPr><w:t>{</w:t></w:r><w:proofErr w:type="spellStart"/><w:r w:rsidR="00573DFD" w:rsidRPr="00573DFD"><w:rPr><w:iCs/></w:rPr><w:t>variable_name</w:t></w:r><w:proofErr w:type="spellEnd"/><w:r w:rsidR="00573DFD" w:rsidRPr="00573DFD"><w:rPr><w:iCs/></w:rPr><w:t>}}</w:t></w:r>');
self::assertEquals(['variable_name'], $variables);
}
/**
* @covers ::textNeedsSplitting
*/
@ -768,6 +1222,22 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
self::assertFalse($templateProcessor->textNeedsSplitting($splitText));
}
/**
* @covers ::textNeedsSplitting
*/
public function testTextNeedsSplittingWithCustomMacro(): void
{
$templateProcessor = new TestableTemplateProcesor();
$templateProcessor->setMacroChars('{{', '}}');
self::assertFalse($templateProcessor->textNeedsSplitting('<w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">{{nothing-to-replace}}</w:t></w:r>'));
$text = '<w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">Hello {{firstname}} {{lastname}}</w:t></w:r>';
self::assertTrue($templateProcessor->textNeedsSplitting($text));
$splitText = $templateProcessor->splitTextIntoTexts($text);
self::assertFalse($templateProcessor->textNeedsSplitting($splitText));
}
/**
* @covers ::splitTextIntoTexts
*/
@ -782,6 +1252,21 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
self::assertEquals('<w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">Hello </w:t></w:r><w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">${firstname}</w:t></w:r><w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve"> </w:t></w:r><w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">${lastname}</w:t></w:r>', $splitText);
}
/**
* @covers ::splitTextIntoTexts
*/
public function testSplitTextIntoTextsWithCustomMacro(): void
{
$templateProcessor = new TestableTemplateProcesor();
$templateProcessor->setMacroChars('{{', '}}');
$splitText = $templateProcessor->splitTextIntoTexts('<w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">{{nothing-to-replace}}</w:t></w:r>');
self::assertEquals('<w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">{{nothing-to-replace}}</w:t></w:r>', $splitText);
$splitText = $templateProcessor->splitTextIntoTexts('<w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">Hello {{firstname}} {{lastname}}</w:t></w:r>');
self::assertEquals('<w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">Hello </w:t></w:r><w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">{{firstname}}</w:t></w:r><w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve"> </w:t></w:r><w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">{{lastname}}</w:t></w:r>', $splitText);
}
public function testFindXmlBlockStart(): void
{
$toFind = '<w:r>
@ -825,6 +1310,50 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
self::assertEquals($toFind, $templateProcessor->getSlice($position['start'], $position['end']));
}
public function testFindXmlBlockStartWithCustomMacro(): void
{
$toFind = '<w:r>
<w:rPr>
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
<w:lang w:val="en-GB"/>
</w:rPr>
<w:t>This whole paragraph will be replaced with my {{title}}</w:t>
</w:r>';
$mainPart = '<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid wp14">
<w:p w14:paraId="165D45AF" w14:textId="7FEC9B41" w:rsidR="005B1098" w:rsidRDefault="005B1098">
<w:r w:rsidR="00A045B2">
<w:rPr>
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
<w:lang w:val="en-GB"/>
</w:rPr>
<w:t xml:space="preserve"> {{value1}} {{value2}}</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
<w:lang w:val="en-GB"/>
</w:rPr>
<w:t>.</w:t>
</w:r>
</w:p>
<w:p w14:paraId="330D1954" w14:textId="0AB1D347" w:rsidR="00156568" w:rsidRDefault="00156568">
<w:pPr>
<w:rPr>
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
<w:lang w:val="en-GB"/>
</w:rPr>
</w:pPr>
' . $toFind . '
</w:p>
</w:document>';
$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 = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
@ -852,6 +1381,34 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
self::assertFalse($result);
}
public function testShouldReturnFalseIfXmlBlockNotFoundWithCustomMacro(): void
{
$mainPart = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>
<w:r>
<w:rPr>
<w:lang w:val="en-GB"/>
</w:rPr>
<w:t xml:space="preserve">this is my text containing a ${macro}</w:t>
</w:r>
</w:p>
</w:document>';
$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 = '<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
@ -865,4 +1422,19 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase
$templateProcessor->setUpdateFields(false);
self::assertStringContainsString('<w:updateFields w:val="false"/>', $templateProcessor->getSettingsPart());
}
public function testShouldMakeFieldsUpdateOnOpenWithCustomMacro(): void
{
$settingsPart = '<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:zoom w:percent="100"/>
</w:settings>';
$templateProcessor = new TestableTemplateProcesor(null, $settingsPart);
$templateProcessor->setMacroChars('{{', '}}');
$templateProcessor->setUpdateFields(true);
self::assertStringContainsString('<w:updateFields w:val="true"/>', $templateProcessor->getSettingsPart());
$templateProcessor->setUpdateFields(false);
self::assertStringContainsString('<w:updateFields w:val="false"/>', $templateProcessor->getSettingsPart());
}
}