Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TemplateProcessor: Added a method to replace multiple XML blocks #2741

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/changes/1.x/1.4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
- Add basic ruby text (phonetic guide) support for Word2007 and HTML Reader/Writer, RTF Writer, basic support for ODT writing by [@Deadpikle](https://github.com/Deadpikle) in [#2727](https://github.com/PHPOffice/PHPWord/pull/2727)
- Reader HTML: Support font styles for h1/h6 by [@Progi1984](https://github.com/Progi1984) fixing [#2619](https://github.com/PHPOffice/PHPWord/issues/2619) in [#2737](https://github.com/PHPOffice/PHPWord/pull/2737)
- Writer EPub3: Basic support by [@Sambit003](https://github.com/Sambit003) fixing [#55](https://github.com/PHPOffice/PHPWord/issues/55) in [#2724](https://github.com/PHPOffice/PHPWord/pull/2724)

- TemplateProcessor: Added a method to replace multiple XML blocks by [@sr-44](https://github.com/sr-44) in [#2741](https://github.com/PHPOffice/PHPWord/pull/2741)

### Bug fixes

- Writer ODText: Support for images inside a textRun by [@Progi1984](https://github.com/Progi1984) fixing [#2240](https://github.com/PHPOffice/PHPWord/issues/2240) in [#2668](https://github.com/PHPOffice/PHPWord/pull/2668)
Expand Down
65 changes: 65 additions & 0 deletions src/PhpWord/TemplateProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -1353,12 +1353,41 @@ public function replaceXmlBlock($macro, $block, $blockType = 'w:p')
return $this;
}

/**
* Replace an XML block surrounding a macro with a new block.
*
* @param string $macro Name of macro
* @param string $block New block content
* @param string $blockType XML tag type of block
*
* @return TemplateProcessor Fluent interface
*/
public function replaceMultipleXmlBlocks($macro, $block, $blockType = 'w:p')
{
$offset = 0;
while (true) {
$where = $this->findAllContainingXmlBlockForMacro($macro, $blockType, $offset);

if (false === $where) {
break;
}

$this->tempDocumentMainPart = $this->getSlice(0, $where['start']) . $block . $this->getSlice($where['end']);

$offset = $where['start'] + strlen($block);
}

return $this;
}

/**
* Find start and end of XML block containing the given macro
* e.g. <w:p>...${macro}...</w:p>.
*
* Note that only the first instance of the macro will be found
*
* @see findAllContainingXmlBlockForMacro for finding all instances
*
* @param string $macro Name of macro
* @param string $blockType XML tag for block
*
Expand All @@ -1383,6 +1412,42 @@ protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p')
return ['start' => $start, 'end' => $end];
}

/**
* Find start and end of XML block containing the given macro
* e.g. <w:p>...${macro}...</w:p>.
*
* Unlike `findContainingXmlBlockForMacro`, this method searches for all occurrences
* of the macro starting from the specified offset.
*
* @param string $macro Name of macro
* @param string $blockType XML tag for block
* @param int $offset Position to start searching for the macro
*
* @return array{start: int, end: int}|false FALSE if not found, otherwise array with start and end
*/
protected function findAllContainingXmlBlockForMacro($macro, $blockType = 'w:p', $offset = 0)
{
$macroPos = $this->findMacro($macro, $offset);

if (0 > $macroPos) {
return false;
}

$start = $this->findXmlBlockStart($macroPos, $blockType);

if (0 > $start) {
return false;
}

$end = $this->findXmlBlockEnd($start, $blockType);
$slice = $this->getSlice($start, $end);
if ($end < 0 || strpos($slice, $macro) === false) {
return false;
}

return ['start' => $start, 'end' => $end];
}

/**
* Find the position of (the start of) a macro.
*
Expand Down
107 changes: 107 additions & 0 deletions tests/PhpWordTests/TemplateProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,113 @@ public function testShouldReturnFalseIfXmlBlockNotFoundWithCustomMacro(): void
self::assertFalse($result);
}

/**
* @covers ::findAllContainingXmlBlockForMacro
*/
public function testFindAllContainingXmlBlockForMacro(): 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 is the first ${macro}</w:t>
</w:r>';

$mainPart = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>
<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>Some text without macro</w:t>
</w:r>
</w:p>
<w:p>' . $toFind . '</w:p>
<w:p>
<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 is the second ${macro}</w:t>
</w:r>
</w:p>
</w:document>';

$templateProcessor = new TestableTemplateProcesor($mainPart);

$firstOccurrence = $templateProcessor->findAllContainingXmlBlockForMacro('${macro}', 'w:r');

self::assertNotFalse($firstOccurrence);

self::assertEquals(
$toFind,
$templateProcessor->getSlice($firstOccurrence['start'], $firstOccurrence['end'])
);

$secondOccurrence = $templateProcessor->findAllContainingXmlBlockForMacro('${macro}', 'w:r', $firstOccurrence['end']);

self::assertNotFalse($secondOccurrence);

$expectedSecond = '<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 is the second ${macro}</w:t>
</w:r>';

self::assertEquals($expectedSecond, $templateProcessor->getSlice($secondOccurrence['start'], $secondOccurrence['end']));
}

/**
* @covers ::replaceXmlBlock
*/
public function testReplaceXmlBlock(): void
{
$originalXml = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>${macro}</w:p>
<w:p>Some text</w:p>
<w:p>${macro}</w:p>
</w:document>';

$expectedXml = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>New content</w:p>
<w:p>Some text</w:p>
<w:p>${macro}</w:p>
</w:document>';

$templateProcessor = new TestableTemplateProcesor($originalXml);
$templateProcessor->replaceXmlBlock('${macro}', '<w:p>New content</w:p>');

self::assertEquals($expectedXml, $templateProcessor->getMainPart());
}

/**
* @covers ::replaceMultipleXmlBlocks
*/
public function testReplaceMultipleXmlBlocks(): void
{
$originalXml = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>${macro}</w:p>
<w:p>Some text</w:p>
<w:p>${macro}</w:p>
</w:document>';

$expectedXml = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:p>New content</w:p>
<w:p>Some text</w:p>
<w:p>New content</w:p>
</w:document>';

$templateProcessor = new TestableTemplateProcesor($originalXml);
$templateProcessor->replaceMultipleXmlBlocks('${macro}', '<w:p>New content</w:p>');

self::assertEquals($expectedXml, $templateProcessor->getMainPart());
}

public function testShouldMakeFieldsUpdateOnOpen(): void
{
$settingsPart = '<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
Expand Down
5 changes: 5 additions & 0 deletions tests/PhpWordTests/TestableTemplateProcesor.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ public function findContainingXmlBlockForMacro($macro, $blockType = 'w:p')
return parent::findContainingXmlBlockForMacro($macro, $blockType);
}

public function findAllContainingXmlBlockForMacro($macro, $blockType = 'w:p', $offset = 0)
{
return parent::findAllContainingXmlBlockForMacro($macro, $blockType, $offset);
}

public function getSlice($startPosition, $endPosition = 0)
{
return parent::getSlice($startPosition, $endPosition);
Expand Down