diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index e7a8d039c4..5e8530fdb7 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -27,6 +27,7 @@ class TemplateProcessor { + const MAXIMUM_REPLACEMENTS_DEFAULT = -1; /** @@ -147,7 +148,7 @@ protected function transformXml($xml, $xsltProcessor) /** * Applies XSL style sheet to template's parts. - * + * * Note: since the method doesn't make any guess on logic of the provided XSL style sheet, * make sure that output is correctly escaped. Otherwise you may get broken document. * @@ -274,7 +275,7 @@ public function cloneRow($search, $numberOfClones) $tagPos = strpos($this->tempDocumentMainPart, $search); if (!$tagPos) { - throw new Exception("Can not clone row, template variable not found or variable contains markup."); + throw new Exception(sprintf("Can not clone row %s, template variable not found or variable contains markup.", $search)); } $rowStart = $this->findRowStart($tagPos); @@ -297,7 +298,8 @@ public function cloneRow($search, $numberOfClones) // If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row. $tmpXmlRow = $this->getSlice($extraRowStart, $extraRowEnd); if (!preg_match('##', $tmpXmlRow) && - !preg_match('##', $tmpXmlRow)) { + !preg_match('##', $tmpXmlRow) + ) { break; } // This row was a spanned row, update $rowEnd and search for the next row. @@ -377,6 +379,77 @@ public function replaceBlock($blockname, $replacement) } } + /** + * Delete a table row in a template document. + * + * @param string $search + * + * @return void + * + * @throws \PhpOffice\PhpWord\Exception\Exception + */ + public function deleteRow($search) + { + if ('${' !== substr($search, 0, 2) && '}' !== substr($search, -1)) { + $search = '${' . $search . '}'; + } + + $tagPos = strpos($this->tempDocumentMainPart, $search); + if (!$tagPos) { + throw new Exception(sprintf("Can not delete row %s, template variable not found or variable contains markup.", $search)); + } + + $tableStart = $this->findTableStart($tagPos); + $tableEnd = $this->findTableEnd($tagPos); + $xmlTable = $this->getSlice($tableStart, $tableEnd); + + if (substr_count($xmlTable, 'tempDocumentMainPart = $this->getSlice(0, $tableStart) . $this->getSlice($tableEnd); + + return; + } + + $rowStart = $this->findRowStart($tagPos); + $rowEnd = $this->findRowEnd($tagPos); + $xmlRow = $this->getSlice($rowStart, $rowEnd); + + $this->tempDocumentMainPart = $this->getSlice(0, $rowStart) . $this->getSlice($rowEnd); + + // Check if there's a cell spanning multiple rows. + if (preg_match('##', $xmlRow)) { + // $extraRowStart = $rowEnd; + $extraRowStart = $rowStart; + while (true) { + $extraRowStart = $this->findRowStart($extraRowStart + 1); + $extraRowEnd = $this->findRowEnd($extraRowStart + 1); + + // If extraRowEnd is lower then 7, there was no next row found. + if ($extraRowEnd < 7) { + break; + } + + // If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row. + $tmpXmlRow = $this->getSlice($extraRowStart, $extraRowEnd); + if (!preg_match('##', $tmpXmlRow) && + !preg_match('##', $tmpXmlRow) + ) { + break; + } + + $tableStart = $this->findTableStart($extraRowEnd + 1); + $tableEnd = $this->findTableEnd($extraRowEnd + 1); + $xmlTable = $this->getSlice($tableStart, $tableEnd); + if (substr_count($xmlTable, 'tempDocumentMainPart = $this->getSlice(0, $tableStart) . $this->getSlice($tableEnd); + + return; + } else { + $this->tempDocumentMainPart = $this->getSlice(0, $extraRowStart) . $this->getSlice($extraRowEnd); + } + } + } + } + /** * Delete a block of text. * @@ -483,6 +556,7 @@ protected function setValueForPart($search, $replace, $documentPartXML, $limit) return str_replace($search, $replace, $documentPartXML); } else { $regExpEscaper = new RegExp(); + return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML, $limit); } } @@ -533,6 +607,31 @@ protected function getFooterName($index) return sprintf('word/footer%d.xml', $index); } + /** + * Find the start position of the nearest table before $offset. + * + * @param integer $offset + * + * @return integer + * + * @throws \PhpOffice\PhpWord\Exception\Exception + */ + protected function findTableStart($offset) + { + $rowStart = strrpos($this->tempDocumentMainPart, 'tempDocumentMainPart) - $offset) * -1)); + + if (!$rowStart) { + $rowStart = strrpos($this->tempDocumentMainPart, '', + ((strlen($this->tempDocumentMainPart) - $offset) * -1)); + } + if (!$rowStart) { + throw new Exception('Can not find the start position of the table.'); + } + + return $rowStart; + } + /** * Find the start position of the nearest table row before $offset. * @@ -544,10 +643,12 @@ protected function getFooterName($index) */ protected function findRowStart($offset) { - $rowStart = strrpos($this->tempDocumentMainPart, 'tempDocumentMainPart) - $offset) * -1)); + $rowStart = strrpos($this->tempDocumentMainPart, 'tempDocumentMainPart) - $offset) * -1)); if (!$rowStart) { - $rowStart = strrpos($this->tempDocumentMainPart, '', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); + $rowStart = strrpos($this->tempDocumentMainPart, '', + ((strlen($this->tempDocumentMainPart) - $offset) * -1)); } if (!$rowStart) { throw new Exception('Can not find the start position of the row to clone.'); @@ -556,6 +657,18 @@ protected function findRowStart($offset) return $rowStart; } + /** + * Find the end position of the nearest table row after $offset. + * + * @param integer $offset + * + * @return integer + */ + protected function findTableEnd($offset) + { + return strpos($this->tempDocumentMainPart, '', $offset) + 7; + } + /** * Find the end position of the nearest table row after $offset. * diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 11b43cf454..26cf76f262 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -177,6 +177,33 @@ public function testCloneRow() unlink($docName); $this->assertTrue($docFound); } + + /** + * @covers ::getVariables + * @covers ::deleteRow + * @covers ::saveAs + * @test + */ + public function testDeleteRow() + { + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/delete-row.docx'); + + $this->assertEquals( + array('deleteMe', 'deleteMeToo'), + $templateProcessor->getVariables() + ); + + $docName = 'delete-row-test-result.docx'; + $templateProcessor->deleteRow('deleteMe'); + $this->assertEquals( + array(), + $templateProcessor->getVariables() + ); + $templateProcessor->saveAs($docName); + $docFound = file_exists($docName); + unlink($docName); + $this->assertTrue($docFound); + } /** * @covers ::setValue diff --git a/tests/PhpWord/_files/templates/delete-row.docx b/tests/PhpWord/_files/templates/delete-row.docx new file mode 100644 index 0000000000..dd8d8a3188 Binary files /dev/null and b/tests/PhpWord/_files/templates/delete-row.docx differ