Skip to content

Commit

Permalink
Additional template processing for tables
Browse files Browse the repository at this point in the history
- insertion of a new table at a placeholder
- deletion of a table containing a placeholder
- deletion of a table row containing a placeholder
  • Loading branch information
ejn committed Jun 27, 2017
1 parent 02508f2 commit 7863394
Show file tree
Hide file tree
Showing 5 changed files with 314 additions and 1 deletion.
217 changes: 216 additions & 1 deletion src/PhpWord/TemplateProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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 = '<w:tbl><w:tblPr><w:tblStyle w:val="Tabellenraster"/><w:tblW w:w="0" w:type="auto"/><w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/></w:tblPr><w:tblGrid>%s</w:tblGrid><w:tr>%s</w:tr></w:tbl>';

/**
* sprintf Template for a gridCol element in a table definition
*
* sprintf arguments:
* 1: d, width of the column in Twips
*/
const TABLEGRIDCOL_TEMPLATE = '<w:gridCol w:w="%d"/>';

/**
* 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 = '<w:tc><w:tcPr><w:tcW w:w="%d" w:type="dxa"/></w:tcPr><w:p><w:r><w:t>%s</w:t></w:r></w:p></w:tc>';

/**
* ZipArchive object.
*
Expand Down Expand Up @@ -147,7 +173,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 @@ -256,6 +282,43 @@ 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.
*
Expand Down Expand Up @@ -315,6 +378,21 @@ 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.
*
Expand Down Expand Up @@ -584,4 +662,141 @@ 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. <w:p>...${macro}...</w:p>
*
* 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 (false === $start) {
return false;
}

$end = $this->findXmlBlockEnd($macroPos, $blockType);
if (false === $end) {
return false;
}

return array('start' => $start, 'end' => $end);
}

/**
* Find start and end of XML block containing the given block macro
* e.g. <w:p>...${macro}...${/macro}...</w:p>
*
* 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 (false === $macroStartPos) {
return false;
}

$macroEndPos = $this->findMacro('/' . $macro, $macroStartPos);
if (false === $macroEndPos) {
return false;
}

$start = $this->findXmlBlockStart($macroStartPos, $blockType);
if (false === $start) {
return false;
}

$end = $this->findXmlBlockEnd($macroEndPos, $blockType);
if (false === $end) {
return false;
}

return array('start' => $start, 'end' => $end);
}

/**
* Find the position of (the start of) a macro
*
* Returns false 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|boolean
*/
protected function findMacro($search, $offset = 0)
{
$search = static::ensureMacroCompleted($search);

return strpos($this->tempDocumentMainPart, $search, $offset);
}

/**
* 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|boolean FALSE if block start not found
* @throws \PhpOffice\PhpWord\Exception\Exception
*/
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;
}

/**
* Find the nearest block end position after $offset
*
* @param integer $offset Search position
* @param string $blockType XML Block tag
* @return integer|boolean FALSE if block end not found
*/
protected function findXmlBlockEnd($offset, $blockType)
{
$blockEndStart = strpos($this->tempDocumentMainPart, '</' . $blockType . '>', $offset);
if (false === $blockEndStart) {
return false;
}
// return position of end of tag
return $blockEndStart + 3 + strlen($blockType);
}
}
98 changes: 98 additions & 0 deletions tests/PhpWord/TemplateProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,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, '<w:gridCol w:w="6000"/><w:gridCol w:w="1440"/><w:gridCol w:w="1440"/>'));
$this->assertTrue(false !== strpos($mainDoc, '<w:tc><w:tcPr><w:tcW w:w="6000" w:type="dxa"/></w:tcPr><w:p><w:r><w:t>${myCol1#}</w:t></w:r></w:p></w:tc>'));
$this->assertTrue(false !== strpos($mainDoc, '<w:tc><w:tcPr><w:tcW w:w="1440" w:type="dxa"/></w:tcPr><w:p><w:r><w:t>${myCol2#}</w:t></w:r></w:p></w:tc>'));
$this->assertTrue(false !== strpos($mainDoc, '<w:tc><w:tcPr><w:tcW w:w="1440" w:type="dxa"/></w:tcPr><w:p><w:r><w:t>${myCol3#}</w:t></w:r></w:p></w:tc>'));
}

/**
* @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('/<w:tbl>/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('/<w:tr /u', $mainDoc);
$this->assertTrue($numMatches === 2);
}
}
Binary file not shown.
Binary file added tests/PhpWord/_files/templates/table-delete.docx
Binary file not shown.
Binary file added tests/PhpWord/_files/templates/table-insert.docx
Binary file not shown.

0 comments on commit 7863394

Please sign in to comment.