Skip to content

Commit a68461b

Browse files
committed
TemplateProcessor: added a method to replace multiple XML blocks
1 parent 1be7a80 commit a68461b

File tree

3 files changed

+177
-0
lines changed

3 files changed

+177
-0
lines changed

src/PhpWord/TemplateProcessor.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,12 +1353,41 @@ public function replaceXmlBlock($macro, $block, $blockType = 'w:p')
13531353
return $this;
13541354
}
13551355

1356+
/**
1357+
* Replace an XML block surrounding a macro with a new block.
1358+
*
1359+
* @param string $macro Name of macro
1360+
* @param string $block New block content
1361+
* @param string $blockType XML tag type of block
1362+
*
1363+
* @return TemplateProcessor Fluent interface
1364+
*/
1365+
public function replaceMultipleXmlBlocks($macro, $block, $blockType = 'w:p'): self
1366+
{
1367+
$offset = 0;
1368+
while (true) {
1369+
$where = $this->findAllContainingXmlBlockForMacro($macro, $blockType, $offset);
1370+
1371+
if (false === $where) {
1372+
break;
1373+
}
1374+
1375+
$this->tempDocumentMainPart = $this->getSlice(0, $where['start']) . $block . $this->getSlice($where['end']);
1376+
1377+
$offset = $where['start'] + strlen($block);
1378+
}
1379+
1380+
return $this;
1381+
}
1382+
13561383
/**
13571384
* Find start and end of XML block containing the given macro
13581385
* e.g. <w:p>...${macro}...</w:p>.
13591386
*
13601387
* Note that only the first instance of the macro will be found
13611388
*
1389+
* @see findAllContainingXmlBlockForMacro for finding all instances
1390+
*
13621391
* @param string $macro Name of macro
13631392
* @param string $blockType XML tag for block
13641393
*
@@ -1383,6 +1412,42 @@ protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p')
13831412
return ['start' => $start, 'end' => $end];
13841413
}
13851414

1415+
/**
1416+
* Find start and end of XML block containing the given macro
1417+
* e.g. <w:p>...${macro}...</w:p>.
1418+
*
1419+
* Unlike `findContainingXmlBlockForMacro`, this method searches for all occurrences
1420+
* of the macro starting from the specified offset.
1421+
*
1422+
* @param string $macro Name of macro
1423+
* @param string $blockType XML tag for block
1424+
* @param int $offset Position to start searching for the macro
1425+
*
1426+
* @return array{start: int, end: int}|false FALSE if not found, otherwise array with start and end
1427+
*/
1428+
protected function findAllContainingXmlBlockForMacro($macro, $blockType = 'w:p', $offset = 0)
1429+
{
1430+
$macroPos = $this->findMacro($macro, $offset);
1431+
1432+
if (0 > $macroPos) {
1433+
return false;
1434+
}
1435+
1436+
$start = $this->findXmlBlockStart($macroPos, $blockType);
1437+
1438+
if (0 > $start) {
1439+
return false;
1440+
}
1441+
1442+
$end = $this->findXmlBlockEnd($start, $blockType);
1443+
$slice = $this->getSlice($start, $end);
1444+
if ($end < 0 || strpos($slice, $macro) === false) {
1445+
return false;
1446+
}
1447+
1448+
return ['start' => $start, 'end' => $end];
1449+
}
1450+
13861451
/**
13871452
* Find the position of (the start of) a macro.
13881453
*

tests/PhpWordTests/TemplateProcessorTest.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,6 +1613,113 @@ public function testShouldReturnFalseIfXmlBlockNotFoundWithCustomMacro(): void
16131613
self::assertFalse($result);
16141614
}
16151615

1616+
/**
1617+
* @covers ::findAllContainingXmlBlockForMacro
1618+
*/
1619+
public function testFindAllContainingXmlBlockForMacro(): void
1620+
{
1621+
$toFind = '<w:r>
1622+
<w:rPr>
1623+
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
1624+
<w:lang w:val="en-GB"/>
1625+
</w:rPr>
1626+
<w:t>This is the first ${macro}</w:t>
1627+
</w:r>';
1628+
1629+
$mainPart = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1630+
<w:p>
1631+
<w:r>
1632+
<w:rPr>
1633+
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
1634+
<w:lang w:val="en-GB"/>
1635+
</w:rPr>
1636+
<w:t>Some text without macro</w:t>
1637+
</w:r>
1638+
</w:p>
1639+
<w:p>' . $toFind . '</w:p>
1640+
<w:p>
1641+
<w:r>
1642+
<w:rPr>
1643+
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
1644+
<w:lang w:val="en-GB"/>
1645+
</w:rPr>
1646+
<w:t>This is the second ${macro}</w:t>
1647+
</w:r>
1648+
</w:p>
1649+
</w:document>';
1650+
1651+
$templateProcessor = new TestableTemplateProcesor($mainPart);
1652+
1653+
$firstOccurrence = $templateProcessor->findAllContainingXmlBlockForMacro('${macro}', 'w:r');
1654+
1655+
self::assertNotFalse($firstOccurrence);
1656+
1657+
self::assertEquals(
1658+
$toFind,
1659+
$templateProcessor->getSlice($firstOccurrence['start'], $firstOccurrence['end'])
1660+
);
1661+
1662+
$secondOccurrence = $templateProcessor->findAllContainingXmlBlockForMacro('${macro}', 'w:r', $firstOccurrence['end']);
1663+
1664+
self::assertNotFalse($secondOccurrence);
1665+
1666+
$expectedSecond = '<w:r>
1667+
<w:rPr>
1668+
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
1669+
<w:lang w:val="en-GB"/>
1670+
</w:rPr>
1671+
<w:t>This is the second ${macro}</w:t>
1672+
</w:r>';
1673+
1674+
self::assertEquals($expectedSecond, $templateProcessor->getSlice($secondOccurrence['start'], $secondOccurrence['end']));
1675+
}
1676+
1677+
/**
1678+
* @covers ::replaceXmlBlock
1679+
*/
1680+
public function testReplaceXmlBlock(): void
1681+
{
1682+
$originalXml = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1683+
<w:p>${macro}</w:p>
1684+
<w:p>Some text</w:p>
1685+
<w:p>${macro}</w:p>
1686+
</w:document>';
1687+
1688+
$expectedXml = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1689+
<w:p>New content</w:p>
1690+
<w:p>Some text</w:p>
1691+
<w:p>${macro}</w:p>
1692+
</w:document>';
1693+
1694+
$templateProcessor = new TestableTemplateProcesor($originalXml);
1695+
$templateProcessor->replaceXmlBlock('${macro}', '<w:p>New content</w:p>');
1696+
1697+
self::assertEquals($expectedXml, $templateProcessor->getMainPart());
1698+
}
1699+
1700+
/**
1701+
* @covers ::replaceMultipleXmlBlocks
1702+
*/
1703+
public function testReplaceMultipleXmlBlocks(): void
1704+
{
1705+
$originalXml = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1706+
<w:p>${macro}</w:p>
1707+
<w:p>Some text</w:p>
1708+
<w:p>${macro}</w:p>
1709+
</w:document>';
1710+
1711+
$expectedXml = '<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
1712+
<w:p>New content</w:p>
1713+
<w:p>Some text</w:p>
1714+
<w:p>New content</w:p>
1715+
</w:document>';
1716+
1717+
$templateProcessor = new TestableTemplateProcesor($originalXml);
1718+
$templateProcessor->replaceMultipleXmlBlocks('${macro}', '<w:p>New content</w:p>');
1719+
1720+
self::assertEquals($expectedXml, $templateProcessor->getMainPart());
1721+
}
1722+
16161723
public function testShouldMakeFieldsUpdateOnOpen(): void
16171724
{
16181725
$settingsPart = '<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">

tests/PhpWordTests/TestableTemplateProcesor.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ public function findContainingXmlBlockForMacro($macro, $blockType = 'w:p')
6666
return parent::findContainingXmlBlockForMacro($macro, $blockType);
6767
}
6868

69+
public function findAllContainingXmlBlockForMacro($macro, $blockType = 'w:p', $offset = 0)
70+
{
71+
return parent::findAllContainingXmlBlockForMacro($macro, $blockType, $offset);
72+
}
73+
6974
public function getSlice($startPosition, $endPosition = 0)
7075
{
7176
return parent::getSlice($startPosition, $endPosition);

0 commit comments

Comments
 (0)