diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php
index e7a8d039c4..cbc238ceb3 100644
--- a/src/PhpWord/TemplateProcessor.php
+++ b/src/PhpWord/TemplateProcessor.php
@@ -22,6 +22,7 @@
use PhpOffice\PhpWord\Exception\CopyFileException;
use PhpOffice\PhpWord\Exception\CreateTemporaryFileException;
use PhpOffice\PhpWord\Exception\Exception;
+use PhpOffice\PhpWord\Shared\Converter;
use PhpOffice\PhpWord\Shared\ZipArchive;
use Zend\Stdlib\StringUtils;
@@ -29,6 +30,32 @@ class TemplateProcessor
{
const MAXIMUM_REPLACEMENTS_DEFAULT = -1;
+ /**
+ * sprintf Template for a table
+ *
+ * sprintf arguments:
+ * 1: s, gridCol definition elements
+ * 2: s, table cell elements
+ */
+ const TABLE_TEMPLATE = '%s%s';
+
+ /**
+ * sprintf Template for a gridCol element in a table definition
+ *
+ * sprintf arguments:
+ * 1: d, width of the column in Twips
+ */
+ const TABLEGRIDCOL_TEMPLATE = '';
+
+ /**
+ * sprintf Template for a table cell element in a table definition
+ *
+ * sprintf arguments:
+ * 1: d, width of the cell in Twips
+ * 2: s, cell text content
+ */
+ const TABLECELL_TEMPLATE = '%s';
+
/**
* ZipArchive object.
*
@@ -147,7 +174,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.
*
@@ -256,6 +283,44 @@ public function getVariables()
return array_unique($variables);
}
+ /**
+ * Insert a table at the place marked by the block template
+ * @param string $name Name of block template
+ * @param array $columns Array keyed on column (template) name, containing
+ * column width in CSS units (string) or twips (integer)
+ *
+ * @return \PhpOffice\PhpWord\TemplateProcessor
+ */
+ public function insertTable($name, array $columns)
+ {
+ $gridCols = '';
+ $cells = '';
+
+ foreach ($columns as $variable => $width) {
+ $twipWidth = is_string($width) ? Converter::cssToTwip($width) : $width;
+ $gridCols .= sprintf(static::TABLEGRIDCOL_TEMPLATE, $twipWidth);
+ $cells .= sprintf(static::TABLECELL_TEMPLATE, $twipWidth, static::ensureMacroCompleted($variable));
+ }
+
+ return $this->replaceXmlBlock($name, sprintf(static::TABLE_TEMPLATE, $gridCols, $cells));
+ }
+
+ /**
+ * Delete a table containing the given variable
+ *
+ * @param string $search
+ *
+ * @return \PhpOffice\PhpWord\TemplateProcessor
+ */
+ public function deleteTable($search)
+ {
+ $block = $this->findContainingXmlBlockForMacro($search, 'w:tbl');
+
+ $this->tempDocumentMainPart = $this->getSlice(0, $block['start']) . $this->getSlice($block['end']);
+
+ return $this;
+ }
+
/**
* Clone a table row in a template document.
*
@@ -315,6 +380,22 @@ public function cloneRow($search, $numberOfClones)
$this->tempDocumentMainPart = $result;
}
+ /**
+ * Delete a row containing the given variable
+ *
+ * @param string $search
+ *
+ * @return \PhpOffice\PhpWord\Template
+ */
+ public function deleteRow($search)
+ {
+ $block = $this->findContainingXmlBlockForMacro($search, 'w:tr');
+
+ $this->tempDocumentMainPart= $this->getSlice(0, $block['start']) . $this->getSlice($block['end']);
+
+ return $this;
+ }
+
/**
* Clone a block.
*
@@ -584,4 +665,138 @@ protected function getSlice($startPosition, $endPosition = 0)
return substr($this->tempDocumentMainPart, $startPosition, ($endPosition - $startPosition));
}
+
+ /**
+ * 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 \PhpOffice\PhpWord\TemplateProcessor Fluent interface
+ */
+ protected function replaceXmlBlock($macro, $block, $blockType = 'w:p')
+ {
+ $where = $this->findContainingXmlBlockForMacro($macro, $blockType);
+ if (false !== $where) {
+ $this->tempDocumentMainPart = $this->getSlice(0, $where['start']) . $block . $this->getSlice($where['end']);
+ }
+ return $this;
+ }
+
+ /**
+ * Find start and end of XML block containing the given macro
+ * e.g. ...${macro}...
+ *
+ * Note that only the first instance of the macro will be found
+ *
+ * @param string $macro Name of macro
+ * @param string $blockType XML tag for block
+ * @return boolean|integer[] FALSE if not found, otherwise array with start and end
+ */
+ protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p')
+ {
+ $macroPos = $this->findMacro($macro);
+ if (false === $macroPos) {
+ return false;
+ }
+
+ $start = $this->findXmlBlockStart($macroPos, $blockType);
+ if (0 > $start) {
+ return false;
+ }
+
+ $end = $this->findXmlBlockEnd($macroPos, $blockType);
+ if (0 > $end) {
+ return false;
+ }
+
+ return array('start' => $start, 'end' => $end);
+ }
+
+ /**
+ * Find start and end of XML block containing the given block macro
+ * e.g. ...${macro}...${/macro}...
+ *
+ * Note that only the first instance of the macro will be found
+ *
+ * @param string $macro Name of macro
+ * @param string $blockType XML tag for block
+ * @return boolean|integer[] FALSE if not found, otherwise array with start and end
+ */
+ protected function findContainingXmlBlockForBlockMacro($macro, $blockType = 'w:p')
+ {
+ $macroStartPos = $this->findMacro($macro);
+ if (0 > $macroStartPos) {
+ return false;
+ }
+
+ $macroEndPos = $this->findMacro('/' . $macro, $macroStartPos);
+ if (0 > $macroEndPos) {
+ return false;
+ }
+
+ $start = $this->findXmlBlockStart($macroStartPos, $blockType);
+ if (0 > $start) {
+ return false;
+ }
+
+ $end = $this->findXmlBlockEnd($macroEndPos, $blockType);
+ if (0 > $end) {
+ return false;
+ }
+
+ return array('start' => $start, 'end' => $end);
+ }
+
+ /**
+ * Find the position of (the start of) a macro
+ *
+ * Returns -1 if not found, otherwise position of opening $
+ *
+ * Note that only the first instance of the macro will be found
+ *
+ * @param string $search Macro name
+ * @param string $offset Offset from which to start searching
+ * @return integer -1 if macro not found
+ */
+ protected function findMacro($search, $offset = 0)
+ {
+ $search = static::ensureMacroCompleted($search);
+
+ $pos = strpos($this->tempDocumentMainPart, $search, $offset);
+
+ return ($pos === false) ? -1 : $pos;
+ }
+
+ /**
+ * Find the start position of the nearest XML block start before $offset
+ *
+ * @param integer $offset Search position
+ * @param string $blockType XML Block tag
+ * @return integer -1 if block start not found
+ */
+ protected function findXmlBlockStart($offset, $blockType)
+ {
+ // first try XML tag with attributes
+ $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
+ if (false === $blockStart) {
+ // also try XML tag without attributes
+ $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . '>', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
+ }
+ return ($blockStart === false) ? -1 : $blockStart;
+ }
+
+ /**
+ * Find the nearest block end position after $offset
+ *
+ * @param integer $offset Search position
+ * @param string $blockType XML Block tag
+ * @return integer -1 if block end not found
+ */
+ protected function findXmlBlockEnd($offset, $blockType)
+ {
+ $blockEndStart = strpos($this->tempDocumentMainPart, '' . $blockType . '>', $offset);
+ // return position of end of tag if found, otherwise -1
+ return ($blockEndStart === false) ? -1 : $blockEndStart + 3 + strlen($blockType);
+ }
}
diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php
index 11b43cf454..91b881a8ce 100644
--- a/tests/PhpWord/TemplateProcessorTest.php
+++ b/tests/PhpWord/TemplateProcessorTest.php
@@ -17,6 +17,8 @@
namespace PhpOffice\PhpWord;
+use PhpOffice\PhpWord\Shared\ZipArchive;
+
/**
* @covers \PhpOffice\PhpWord\TemplateProcessor
* @coversDefaultClass \PhpOffice\PhpWord\TemplateProcessor
@@ -223,4 +225,102 @@ public function testCloneDeleteBlock()
unlink($docName);
$this->assertTrue($docFound);
}
+
+ /**
+ * @covers ::insertTable
+ * @covers ::saveAs
+ * @test
+ */
+ public function testInsertTable()
+ {
+ $docName = 'table-insert.docx';
+ $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/' . $docName);
+
+ $this->assertEquals(
+ array('myTable', 'otherContent'),
+ $templateProcessor->getVariables()
+ );
+
+ $templateProcessor->insertTable('myTable', array('myCol1#' => 6000, 'myCol2#' => '1in', 'myCol3#' => '72pt'));
+ $this->assertEquals(
+ array('myCol1#', 'myCol2#', 'myCol3#', 'otherContent'),
+ $templateProcessor->getVariables()
+ );
+ $templateProcessor->saveAs($docName);
+ $docFound = file_exists($docName);
+ $zip = new ZipArchive();
+ $zip->open($docName);
+ $mainDoc = $zip->getFromName('word/document.xml');
+ $zip->close();
+ unlink($docName);
+ $this->assertTrue($docFound);
+ $this->assertTrue(false !== strpos($mainDoc, ''));
+ $this->assertTrue(false !== strpos($mainDoc, '${myCol1#}'));
+ $this->assertTrue(false !== strpos($mainDoc, '${myCol2#}'));
+ $this->assertTrue(false !== strpos($mainDoc, '${myCol3#}'));
+ }
+
+ /**
+ * @covers ::deleteTable
+ * @covers ::saveAs
+ * @test
+ */
+ public function testDeleteTable()
+ {
+ $docName = 'table-delete.docx';
+ $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/' . $docName);
+
+ $this->assertEquals(
+ array('KEEP_THIS_TABLE', 'DELETE_THIS_TABLE'),
+ $templateProcessor->getVariables()
+ );
+
+ $templateProcessor->deleteTable('DELETE_THIS_TABLE');
+ $this->assertEquals(
+ array('KEEP_THIS_TABLE'),
+ $templateProcessor->getVariables()
+ );
+ $templateProcessor->saveAs($docName);
+ $docFound = file_exists($docName);
+ $zip = new ZipArchive();
+ $zip->open($docName);
+ $mainDoc = $zip->getFromName('word/document.xml');
+ $zip->close();
+ unlink($docName);
+ $this->assertTrue($docFound);
+ $numMatches = preg_match_all('//u', $mainDoc);
+ $this->assertTrue($numMatches === 1);
+ }
+
+ /**
+ * @covers ::deleteRow
+ * @covers ::saveAs
+ * @test
+ */
+ public function testDeleteRow()
+ {
+ $docName = 'table-delete-row.docx';
+ $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/' . $docName);
+
+ $this->assertEquals(
+ array('KEEP_THE_HEADER', 'DELETE_THIS_ROW', 'RETAIN_THIS_ROW'),
+ $templateProcessor->getVariables()
+ );
+
+ $templateProcessor->deleteRow('DELETE_THIS_ROW');
+ $this->assertEquals(
+ array('KEEP_THE_HEADER', 'RETAIN_THIS_ROW'),
+ $templateProcessor->getVariables()
+ );
+ $templateProcessor->saveAs($docName);
+ $docFound = file_exists($docName);
+ $zip = new ZipArchive();
+ $zip->open($docName);
+ $mainDoc = $zip->getFromName('word/document.xml');
+ $zip->close();
+ unlink($docName);
+ $this->assertTrue($docFound);
+ $numMatches = preg_match_all('/assertTrue($numMatches === 2);
+ }
}
diff --git a/tests/PhpWord/_files/templates/table-delete-row.docx b/tests/PhpWord/_files/templates/table-delete-row.docx
new file mode 100644
index 0000000000..738d044d67
Binary files /dev/null and b/tests/PhpWord/_files/templates/table-delete-row.docx differ
diff --git a/tests/PhpWord/_files/templates/table-delete.docx b/tests/PhpWord/_files/templates/table-delete.docx
new file mode 100644
index 0000000000..203a214e53
Binary files /dev/null and b/tests/PhpWord/_files/templates/table-delete.docx differ
diff --git a/tests/PhpWord/_files/templates/table-insert.docx b/tests/PhpWord/_files/templates/table-insert.docx
new file mode 100644
index 0000000000..82fd58c885
Binary files /dev/null and b/tests/PhpWord/_files/templates/table-insert.docx differ