Skip to content
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

Templating tables #1086

Open
wants to merge 2 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
117 changes: 117 additions & 0 deletions src/PhpWord/Shared/Converter.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

namespace PhpOffice\PhpWord\Shared;

use PhpOffice\PhpWord\Exception\InvalidStyleException;

/**
* Common converter functions
*/
Expand Down Expand Up @@ -227,6 +229,121 @@ public static function emuToPixel($emu = 1)
return round($emu / self::PIXEL_TO_EMU);
}

/**
* Convert an absolute CSS measurement to pixels
*
* Units for absolute CSS measurements are cm, mm, in, px, pt and pc
*
* Note that the result will be rounded to the nearest pixel
*
* @param string $cssMeasurement If no measurement unit is included then cm
* is assumed
*
* @throws \PHPOffice\PhpWord\Exception\InvalidStyleException
*
* @return float
*/
public static function cssToPixel($cssMeasurement = '1cm')
{
$units = trim(preg_replace('/^-?(?:\\d+\\.\\d+|\\.?\\d+)/', '', trim($cssMeasurement)));
$value = preg_replace('/\\D+$/', '', trim($cssMeasurement));
if ((strlen($value) > 0) && ($value[0] == '.')) {
$value = '0' . $value;
}
switch (strtolower($units)) {
case 'in':
$pixel = $value * static::INCH_TO_PIXEL;
break;
case 'cm':
case '':
$pixel = ($value / static::INCH_TO_CM) * static::INCH_TO_PIXEL;
break;
case 'mm':
$pixel = ($value / (10 * static::INCH_TO_CM)) * static::INCH_TO_PIXEL;
break;
case 'pt':
$pixel = ($value / static::INCH_TO_POINT) * static::INCH_TO_PIXEL;
break;
case 'pc':
$pixel = ($value / (12 * static::INCH_TO_POINT)) * static::INCH_TO_PIXEL;
break;
case 'px':
$pixel = floatval($value);
break;
default:
throw new InvalidStyleException($cssMeasurement . ' is an unsupported CSS measurement');
}
return $pixel;
}

/**
* Convert an absolute CSS measurement to EMU
*
* Units for absolute CSS measurements are cm, mm, in, px, pt and pc
*
* @param string $cssMeasurement If no measurement unit is included then cm
* is assumed
*
* @throws \PHPOffice\PhpWord\Exception\InvalidStyleException
*
* @return float
*/
public static function cssToEmu($cssMeasurement = '1cm')
{
return static::cssToPixel($cssMeasurement) * static::PIXEL_TO_EMU;
}

/**
* Convert an absolute CSS measurement to point
*
* Units for absolute CSS measurements are cm, mm, in, px, pt and pc
*
* @param string $cssMeasurement If no measurement unit is included then cm
* is assumed
*
* @throws \PHPOffice\PhpWord\Exception\InvalidStyleException
*
* @return float
*/
public static function cssToPoint($cssMeasurement = '1cm')
{
return static::cssToPixel($cssMeasurement) * static::INCH_TO_POINT / static::INCH_TO_PIXEL;
}

/**
* Convert an absolute CSS measurement to twip
*
* Units for absolute CSS measurements are cm, mm, in, px, pt and pc
*
* @param string $cssMeasurement If no measurement unit is included then cm
* is assumed
*
* @throws \PHPOffice\PhpWord\Exception\InvalidStyleException
*
* @return float
*/
public static function cssToTwip($cssMeasurement = '1cm')
{
return static::cssToPixel($cssMeasurement) * static::INCH_TO_TWIP / static::INCH_TO_PIXEL;
}

/**
* Convert an absolute CSS measurement to centimeter
*
* Units for absolute CSS measurements are cm, mm, in, px, pt and pc
*
* @param string $cssMeasurement If no measurement unit is included then cm
* is assumed
*
* @throws \PHPOffice\PhpWord\Exception\InvalidStyleException
*
* @return float
*/
public static function cssToCm($cssMeasurement = '1cm')
{
return static::cssToPixel($cssMeasurement) * static::INCH_TO_CM / static::INCH_TO_PIXEL;
}

/**
* Convert degree to angle
*
Expand Down
217 changes: 216 additions & 1 deletion src/PhpWord/TemplateProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,40 @@
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;

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 +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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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. <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 (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. <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 (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);
}
}