Skip to content

Introduce deleteRow() method for TemplateProcessor #1017

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

Open
wants to merge 6 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
123 changes: 118 additions & 5 deletions src/PhpWord/TemplateProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

class TemplateProcessor
{

const MAXIMUM_REPLACEMENTS_DEFAULT = -1;

/**
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
Expand All @@ -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('#<w:vMerge/>#', $tmpXmlRow) &&
!preg_match('#<w:vMerge w:val="continue" />#', $tmpXmlRow)) {
!preg_match('#<w:vMerge w:val="continue" />#', $tmpXmlRow)
) {
break;
}
// This row was a spanned row, update $rowEnd and search for the next row.
Expand Down Expand Up @@ -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, '<w:tr') === 1) {
$this->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('#<w:vMerge w:val="restart"/>#', $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('#<w:vMerge/>#', $tmpXmlRow) &&
!preg_match('#<w:vMerge w:val="continue" />#', $tmpXmlRow)
) {
break;
}

$tableStart = $this->findTableStart($extraRowEnd + 1);
$tableEnd = $this->findTableEnd($extraRowEnd + 1);
$xmlTable = $this->getSlice($tableStart, $tableEnd);
if (substr_count($xmlTable, '<w:tr') === 1) {
$this->tempDocumentMainPart = $this->getSlice(0, $tableStart) . $this->getSlice($tableEnd);

return;
} else {
$this->tempDocumentMainPart = $this->getSlice(0, $extraRowStart) . $this->getSlice($extraRowEnd);
}
}
}
}

/**
* Delete a block of text.
*
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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, '<w:tbl ',
((strlen($this->tempDocumentMainPart) - $offset) * -1));

if (!$rowStart) {
$rowStart = strrpos($this->tempDocumentMainPart, '<w:tbl>',
((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.
*
Expand All @@ -544,10 +643,12 @@ protected function getFooterName($index)
*/
protected function findRowStart($offset)
{
$rowStart = strrpos($this->tempDocumentMainPart, '<w:tr ', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
$rowStart = strrpos($this->tempDocumentMainPart, '<w:tr ',
((strlen($this->tempDocumentMainPart) - $offset) * -1));

if (!$rowStart) {
$rowStart = strrpos($this->tempDocumentMainPart, '<w:tr>', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
$rowStart = strrpos($this->tempDocumentMainPart, '<w:tr>',
((strlen($this->tempDocumentMainPart) - $offset) * -1));
}
if (!$rowStart) {
throw new Exception('Can not find the start position of the row to clone.');
Expand All @@ -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, '</w:tbl>', $offset) + 7;
}

/**
* Find the end position of the nearest table row after $offset.
*
Expand Down
27 changes: 27 additions & 0 deletions tests/PhpWord/TemplateProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added tests/PhpWord/_files/templates/delete-row.docx
Binary file not shown.