Merge branch 'develop' into #160-element-container

This commit is contained in:
Ivan Lanin 2014-04-02 20:02:36 +07:00
commit 237625d22a
6 changed files with 245 additions and 121 deletions

View File

@ -14,6 +14,7 @@ This is the changelog between releases of PHPWord. Releases are listed in revers
- Table: Add `exactHeight` to row style to define whether row height should be exact or atLeast - @jcarignan GH-168 - Table: Add `exactHeight` to row style to define whether row height should be exact or atLeast - @jcarignan GH-168
- Element: New `CheckBox` element for sections and table cells - @ozilion GH-156 - Element: New `CheckBox` element for sections and table cells - @ozilion GH-156
- Settings: Ability to use PCLZip as alternative to ZipArchive - @bskrtich @ivanlanin GH-106 GH-140 GH-185 - Settings: Ability to use PCLZip as alternative to ZipArchive - @bskrtich @ivanlanin GH-106 GH-140 GH-185
- Template: Ability to find & replace variables in headers & footers - @dgudgeon GH-190
- Table: Ability to add footnote in table cell - @ivanlanin GH-187 - Table: Ability to add footnote in table cell - @ivanlanin GH-187
- Footnote: Ability to add image in footnote - @ivanlanin GH-187 - Footnote: Ability to add image in footnote - @ivanlanin GH-187
- ListItem: Ability to add list item in header/footer - @ivanlanin GH-187 - ListItem: Ability to add list item in header/footer - @ivanlanin GH-187

View File

@ -7,6 +7,11 @@ $phpWord = new \PhpOffice\PhpWord\PhpWord();
$document = $phpWord->loadTemplate('resources/Sample_07_TemplateCloneRow.docx'); $document = $phpWord->loadTemplate('resources/Sample_07_TemplateCloneRow.docx');
// Variables on different parts of document
$document->setValue('weekday', date('l')); // On section/content
$document->setValue('time', date('H:i')); // On footer
$document->setValue('serverName', $_SERVER['SERVER_NAME']); // On header
// Simple table // Simple table
$document->cloneRow('rowValue', 10); $document->cloneRow('rowValue', 10);
@ -32,9 +37,6 @@ $document->setValue('rowNumber#8', '8');
$document->setValue('rowNumber#9', '9'); $document->setValue('rowNumber#9', '9');
$document->setValue('rowNumber#10', '10'); $document->setValue('rowNumber#10', '10');
$document->setValue('weekday', date('l'));
$document->setValue('time', date('H:i'));
// Table with a spanned cell // Table with a spanned cell
$document->cloneRow('userId', 3); $document->cloneRow('userId', 3);

View File

@ -23,46 +23,72 @@ class Template
* *
* @var mixed * @var mixed
*/ */
private $_objZip; private $zipClass;
/** /**
* Temporary file name * Temporary file name
* *
* @var string * @var string
*/ */
private $_tempFileName; private $tempFileName;
/** /**
* Document XML * Document XML
* *
* @var string * @var string
*/ */
private $_documentXML; private $documentXML;
/**
* Document header XML
*
* @var string[]
*/
private $headerXMLs = array();
/**
* Document footer XML
*
* @var string[]
*/
private $footerXMLs = array();
/** /**
* Create a new Template Object * Create a new Template Object
* *
* @param string $strFilename * @param string $strFilename
* @throws \PhpOffice\PhpWord\Exception\Exception * @throws Exception
*/ */
public function __construct($strFilename) public function __construct($strFilename)
{ {
$this->_tempFileName = tempnam(sys_get_temp_dir(), ''); $this->tempFileName = tempnam(sys_get_temp_dir(), '');
if ($this->_tempFileName === false) { if ($this->tempFileName === false) {
throw new Exception('Could not create temporary file with unique name in the default temporary directory.'); throw new Exception('Could not create temporary file with unique name in the default temporary directory.');
} }
// Copy the source File to the temp File // Copy the source File to the temp File
if (!copy($strFilename, $this->_tempFileName)) { if (!copy($strFilename, $this->tempFileName)) {
throw new Exception("Could not copy the template from {$strFilename} to {$this->_tempFileName}."); throw new Exception("Could not copy the template from {$strFilename} to {$this->tempFileName}.");
} }
$zipClass = Settings::getZipClass(); $zipClass = Settings::getZipClass();
$this->_objZip = new $zipClass(); $this->zipClass = new $zipClass();
$this->_objZip->open($this->_tempFileName); $this->zipClass->open($this->tempFileName);
$this->_documentXML = $this->_objZip->getFromName('word/document.xml'); // Find and load headers and footers
$i = 1;
while ($this->zipClass->locateName($this->getHeaderName($i)) !== false) {
$this->headerXMLs[$i] = $this->zipClass->getFromName($this->getHeaderName($i));
$i++;
}
$i = 1;
while ($this->zipClass->locateName($this->getFooterName($i)) !== false) {
$this->footerXMLs[$i] = $this->zipClass->getFromName($this->getFooterName($i));
$i++;
}
$this->documentXML = $this->zipClass->getFromName('word/document.xml');
} }
/** /**
@ -71,7 +97,7 @@ class Template
* @param \DOMDocument $xslDOMDocument * @param \DOMDocument $xslDOMDocument
* @param array $xslOptions * @param array $xslOptions
* @param string $xslOptionsURI * @param string $xslOptionsURI
* @throws \PhpOffice\PhpWord\Exception\Exception * @throws Exception
*/ */
public function applyXslStyleSheet(&$xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '') public function applyXslStyleSheet(&$xslDOMDocument, $xslOptions = array(), $xslOptionsURI = '')
{ {
@ -84,7 +110,7 @@ class Template
} }
$xmlDOMDocument = new \DOMDocument(); $xmlDOMDocument = new \DOMDocument();
if ($xmlDOMDocument->loadXML($this->_documentXML) === false) { if ($xmlDOMDocument->loadXML($this->documentXML) === false) {
throw new Exception('Could not load XML from the given template.'); throw new Exception('Could not load XML from the given template.');
} }
@ -93,7 +119,7 @@ class Template
throw new Exception('Could not transform the given XML document.'); throw new Exception('Could not transform the given XML document.');
} }
$this->_documentXML = $xmlTransformed; $this->documentXML = $xmlTransformed;
} }
/** /**
@ -104,13 +130,151 @@ class Template
* @param integer $limit * @param integer $limit
*/ */
public function setValue($search, $replace, $limit = -1) public function setValue($search, $replace, $limit = -1)
{
foreach ($this->headerXMLs as $index => $headerXML) {
$this->headerXMLs[$index] = $this->setValueForPart($this->headerXMLs[$index], $search, $replace, $limit);
}
$this->documentXML = $this->setValueForPart($this->documentXML, $search, $replace, $limit);
foreach ($this->footerXMLs as $index => $headerXML) {
$this->footerXMLs[$index] = $this->setValueForPart($this->footerXMLs[$index], $search, $replace, $limit);
}
}
/**
* Returns array of all variables in template
* @return string[]
*/
public function getVariables()
{
$variables = $this->getVariablesForPart($this->documentXML);
foreach ($this->headerXMLs as $headerXML) {
$variables = array_merge($variables, $this->getVariablesForPart($headerXML));
}
foreach ($this->footerXMLs as $footerXML) {
$variables = array_merge($variables, $this->getVariablesForPart($footerXML));
}
return array_unique($variables);
}
/**
* Clone a table row in a template document
*
* @param string $search
* @param int $numberOfClones
* @throws Exception
*/
public function cloneRow($search, $numberOfClones)
{
if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') {
$search = '${' . $search . '}';
}
$tagPos = strpos($this->documentXML, $search);
if (!$tagPos) {
throw new Exception("Can not clone row, template variable not found or variable contains markup.");
}
$rowStart = $this->findRowStart($tagPos);
$rowEnd = $this->findRowEnd($tagPos);
$xmlRow = $this->getSlice($rowStart, $rowEnd);
// Check if there's a cell spanning multiple rows.
if (preg_match('#<w:vMerge w:val="restart"/>#', $xmlRow)) {
$extraRowStart = $rowEnd;
$extraRowEnd = $rowEnd;
while (true) {
$extraRowStart = $this->findRowStart($extraRowEnd + 1);
$extraRowEnd = $this->findRowEnd($extraRowEnd + 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;
}
// This row was a spanned row, update $rowEnd and search for the next row.
$rowEnd = $extraRowEnd;
}
$xmlRow = $this->getSlice($rowStart, $rowEnd);
}
$result = $this->getSlice(0, $rowStart);
for ($i = 1; $i <= $numberOfClones; $i++) {
$result .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlRow);
}
$result .= $this->getSlice($rowEnd);
$this->documentXML = $result;
}
/**
* Save XML to temporary file
*
* @return string
* @throws Exception
*/
public function save()
{
foreach ($this->headerXMLs as $index => $headerXML) {
$this->zipClass->addFromString($this->getHeaderName($index), $this->headerXMLs[$index]);
}
$this->zipClass->addFromString('word/document.xml', $this->documentXML);
foreach ($this->footerXMLs as $index => $headerXML) {
$this->zipClass->addFromString($this->getFooterName($index), $this->footerXMLs[$index]);
}
// Close zip file
if ($this->zipClass->close() === false) {
throw new Exception('Could not close zip file.');
}
return $this->tempFileName;
}
/**
* Save XML to defined name
*
* @param string $strFilename
*/
public function saveAs($strFilename)
{
$tempFilename = $this->save();
if (\file_exists($strFilename)) {
unlink($strFilename);
}
rename($tempFilename, $strFilename);
}
/**
* Find and replace placeholders in the given XML section.
*
* @param string $documentPartXML
* @param string $search
* @param mixed $replace
* @param integer $limit
* @return string
*/
protected function setValueForPart($documentPartXML, $search, $replace, $limit)
{ {
$pattern = '|\$\{([^\}]+)\}|U'; $pattern = '|\$\{([^\}]+)\}|U';
preg_match_all($pattern, $this->_documentXML, $matches); preg_match_all($pattern, $documentPartXML, $matches);
foreach ($matches[0] as $value) { foreach ($matches[0] as $value) {
$valueCleaned = preg_replace('/<[^>]+>/', '', $value); $valueCleaned = preg_replace('/<[^>]+>/', '', $value);
$valueCleaned = preg_replace('/<\/[^>]+>/', '', $valueCleaned); $valueCleaned = preg_replace('/<\/[^>]+>/', '', $valueCleaned);
$this->_documentXML = str_replace($value, $valueCleaned, $this->_documentXML); $documentPartXML = str_replace($value, $valueCleaned, $documentPartXML);
} }
if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') { if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') {
@ -130,30 +294,53 @@ class Template
$regExpDelim = '/'; $regExpDelim = '/';
$escapedSearch = preg_quote($search, $regExpDelim); $escapedSearch = preg_quote($search, $regExpDelim);
$this->_documentXML = preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $this->_documentXML, $limit); return preg_replace("{$regExpDelim}{$escapedSearch}{$regExpDelim}u", $replace, $documentPartXML, $limit);
} }
/** /**
* Returns array of all variables in template * Find all variables in $documentPartXML
* @param string $documentPartXML
* @return string[]
*/ */
public function getVariables() protected function getVariablesForPart($documentPartXML)
{ {
preg_match_all('/\$\{(.*?)}/i', $this->_documentXML, $matches); preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
return $matches[1]; return $matches[1];
} }
/**
* Get the name of the footer file for $index
* @param integer $index
* @return string
*/
private function getFooterName($index)
{
return sprintf('word/footer%d.xml', $index);
}
/**
* Get the name of the header file for $index
* @param integer $index
* @return string
*/
private function getHeaderName($index)
{
return sprintf('word/header%d.xml', $index);
}
/** /**
* Find the start position of the nearest table row before $offset * Find the start position of the nearest table row before $offset
* *
* @param int $offset * @param int $offset
* @return int * @return int
* @throws \PhpOffice\PhpWord\Exception\Exception * @throws Exception
*/ */
private function _findRowStart($offset) private function findRowStart($offset)
{ {
$rowStart = strrpos($this->_documentXML, "<w:tr ", ((strlen($this->_documentXML) - $offset) * -1)); $rowStart = strrpos($this->documentXML, "<w:tr ", ((strlen($this->documentXML) - $offset) * -1));
if (!$rowStart) { if (!$rowStart) {
$rowStart = strrpos($this->_documentXML, "<w:tr>", ((strlen($this->_documentXML) - $offset) * -1)); $rowStart = strrpos($this->documentXML, "<w:tr>", ((strlen($this->documentXML) - $offset) * -1));
} }
if (!$rowStart) { if (!$rowStart) {
throw new Exception("Can not find the start position of the row to clone."); throw new Exception("Can not find the start position of the row to clone.");
@ -167,9 +354,9 @@ class Template
* @param int $offset * @param int $offset
* @return int * @return int
*/ */
private function _findRowEnd($offset) private function findRowEnd($offset)
{ {
$rowEnd = strpos($this->_documentXML, "</w:tr>", $offset) + 7; $rowEnd = strpos($this->documentXML, "</w:tr>", $offset) + 7;
return $rowEnd; return $rowEnd;
} }
@ -180,100 +367,11 @@ class Template
* @param int $endPosition * @param int $endPosition
* @return string * @return string
*/ */
private function _getSlice($startPosition, $endPosition = 0) private function getSlice($startPosition, $endPosition = 0)
{ {
if (!$endPosition) { if (!$endPosition) {
$endPosition = strlen($this->_documentXML); $endPosition = strlen($this->documentXML);
} }
return substr($this->_documentXML, $startPosition, ($endPosition - $startPosition)); return substr($this->documentXML, $startPosition, ($endPosition - $startPosition));
}
/**
* Clone a table row in a template document
*
* @param string $search
* @param int $numberOfClones
* @throws \PhpOffice\PhpWord\Exception\Exception
*/
public function cloneRow($search, $numberOfClones)
{
if (substr($search, 0, 2) !== '${' && substr($search, -1) !== '}') {
$search = '${' . $search . '}';
}
$tagPos = strpos($this->_documentXML, $search);
if (!$tagPos) {
throw new Exception("Can not clone row, template variable not found or variable contains markup.");
}
$rowStart = $this->_findRowStart($tagPos);
$rowEnd = $this->_findRowEnd($tagPos);
$xmlRow = $this->_getSlice($rowStart, $rowEnd);
// Check if there's a cell spanning multiple rows.
if (preg_match('#<w:vMerge w:val="restart"/>#', $xmlRow)) {
$extraRowStart = $rowEnd;
$extraRowEnd = $rowEnd;
while (true) {
$extraRowStart = $this->_findRowStart($extraRowEnd + 1);
$extraRowEnd = $this->_findRowEnd($extraRowEnd + 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;
}
// This row was a spanned row, update $rowEnd and search for the next row.
$rowEnd = $extraRowEnd;
}
$xmlRow = $this->_getSlice($rowStart, $rowEnd);
}
$result = $this->_getSlice(0, $rowStart);
for ($i = 1; $i <= $numberOfClones; $i++) {
$result .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlRow);
}
$result .= $this->_getSlice($rowEnd);
$this->_documentXML = $result;
}
/**
* Save XML to temporary file
*
* @return string
* @throws \PhpOffice\PhpWord\Exception\Exception
*/
public function save()
{
$this->_objZip->addFromString('word/document.xml', $this->_documentXML);
// Close zip file
if ($this->_objZip->close() === false) {
throw new Exception('Could not close zip file.');
}
return $this->_tempFileName;
}
/**
* Save XML to defined name
*
* @param string $strFilename
*/
public function saveAs($strFilename)
{
$tempFilename = $this->save();
if (\file_exists($strFilename)) {
unlink($strFilename);
}
rename($tempFilename, $strFilename);
} }
} }

View File

@ -156,4 +156,27 @@ final class TemplateTest extends \PHPUnit_Framework_TestCase
$this->assertEquals($expectedVar, $actualVar); $this->assertEquals($expectedVar, $actualVar);
$this->assertTrue($docFound); $this->assertTrue($docFound);
} }
/**
* Replace variables in header and footer
*/
public function testVariablesCanBeReplacedInHeaderAndFooter()
{
$template = __DIR__ . "/_files/templates/header-footer.docx";
$expectedVar = array('documentContent', 'headerValue', 'footerValue');
$docName = 'header-footer-test-result.docx';
$document = new Template($template);
$actualVar = $document->getVariables();
$document->setValue('headerValue', 'Header Value');
$document->setValue('documentContent', 'Document text.');
$document->setValue('footerValue', 'Footer Value');
$document->saveAs($docName);
$docFound = file_exists($docName);
unlink($docName);
$this->assertEquals($expectedVar, $actualVar);
$this->assertTrue($docFound);
}
} }