Skip to content

Commit 67f3bd3

Browse files
committed
Add methods to replace macro with ComplexType
1 parent af31fc5 commit 67f3bd3

File tree

6 files changed

+413
-1
lines changed

6 files changed

+413
-1
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ composer.phar
1313
vendor
1414
/report
1515
/build
16-
/samples/resources
1716
/samples/results
1817
/.settings
1918
phpword.ini
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
use PhpOffice\PhpWord\Element\Field;
3+
use PhpOffice\PhpWord\Element\Table;
4+
use PhpOffice\PhpWord\Element\TextRun;
5+
use PhpOffice\PhpWord\SimpleType\TblWidth;
6+
7+
include_once 'Sample_Header.php';
8+
9+
// Template processor instance creation
10+
echo date('H:i:s'), ' Creating new TemplateProcessor instance...', EOL;
11+
$templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor('resources/Sample_40_TemplateSetComplexValue.docx');
12+
13+
$title = new TextRun();
14+
$title->addText('This title has been set ', array('bold' => true, 'italic' => true, 'color' => 'blue'));
15+
$title->addText('dynamically', array('bold' => true, 'italic' => true, 'color' => 'red', 'underline' => 'single'));
16+
$templateProcessor->setComplexBlock('title', $title);
17+
18+
$inline = new TextRun();
19+
$inline->addText('by a red italic text', array('italic' => true, 'color' => 'red'));
20+
$templateProcessor->setComplexValue('inline', $inline);
21+
22+
$table = new Table(array('borderSize' => 12, 'borderColor' => 'green', 'width' => 6000, 'unit' => TblWidth::TWIP));
23+
$table->addRow();
24+
$table->addCell(150)->addText('Cell A1');
25+
$table->addCell(150)->addText('Cell A2');
26+
$table->addCell(150)->addText('Cell A3');
27+
$table->addRow();
28+
$table->addCell(150)->addText('Cell B1');
29+
$table->addCell(150)->addText('Cell B2');
30+
$table->addCell(150)->addText('Cell B3');
31+
$templateProcessor->setComplexBlock('table', $table);
32+
33+
$field = new Field('DATE', array('dateformat' => 'dddd d MMMM yyyy H:mm:ss'), array('PreserveFormat'));
34+
$templateProcessor->setComplexValue('field', $field);
35+
36+
// $link = new Link('https://github.com/PHPOffice/PHPWord');
37+
// $templateProcessor->setComplexValue('link', $link);
38+
39+
echo date('H:i:s'), ' Saving the result document...', EOL;
40+
$templateProcessor->saveAs('results/Sample_40_TemplateSetComplexValue.docx');
41+
42+
echo getEndingNotes(array('Word2007' => 'docx'), 'results/Sample_40_TemplateSetComplexValue.docx');
43+
if (!CLI) {
44+
include_once 'Sample_Footer.php';
45+
}
Binary file not shown.

src/PhpWord/TemplateProcessor.php

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
namespace PhpOffice\PhpWord;
1919

2020
use PhpOffice\Common\Text;
21+
use PhpOffice\Common\XMLWriter;
2122
use PhpOffice\PhpWord\Escaper\RegExp;
2223
use PhpOffice\PhpWord\Escaper\Xml;
2324
use PhpOffice\PhpWord\Exception\CopyFileException;
@@ -249,6 +250,46 @@ protected static function ensureUtf8Encoded($subject)
249250
return $subject;
250251
}
251252

253+
/**
254+
* @param string $search
255+
* @param \PhpOffice\PhpWord\Element\AbstractElement $complexType
256+
*/
257+
public function setComplexValue($search, \PhpOffice\PhpWord\Element\AbstractElement $complexType)
258+
{
259+
$elementName = substr(get_class($complexType), strrpos(get_class($complexType), '\\') + 1);
260+
$objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName;
261+
262+
$xmlWriter = new XMLWriter();
263+
/** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */
264+
$elementWriter = new $objectClass($xmlWriter, $complexType, true);
265+
$elementWriter->write();
266+
267+
$where = $this->findContainingXmlBlockForMacro($search, 'w:r');
268+
$block = $this->getSlice($where['start'], $where['end']);
269+
$textParts = $this->splitTextIntoTexts($block);
270+
$this->replaceXmlBlock($search, $textParts, 'w:r');
271+
272+
$search = static::ensureMacroCompleted($search);
273+
$this->replaceXmlBlock($search, $xmlWriter->getData(), 'w:r');
274+
}
275+
276+
/**
277+
* @param string $search
278+
* @param \PhpOffice\PhpWord\Element\AbstractElement $complexType
279+
*/
280+
public function setComplexBlock($search, \PhpOffice\PhpWord\Element\AbstractElement $complexType)
281+
{
282+
$elementName = substr(get_class($complexType), strrpos(get_class($complexType), '\\') + 1);
283+
$objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName;
284+
285+
$xmlWriter = new XMLWriter();
286+
/** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */
287+
$elementWriter = new $objectClass($xmlWriter, $complexType, false);
288+
$elementWriter->write();
289+
290+
$this->replaceXmlBlock($search, $xmlWriter->getData(), 'w:p');
291+
}
292+
252293
/**
253294
* @param mixed $search
254295
* @param mixed $replace
@@ -685,6 +726,7 @@ public function cloneRowAndSetValues($search, $values)
685726
public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVariables = false, $variableReplacements = null)
686727
{
687728
$xmlBlock = null;
729+
$matches = array();
688730
preg_match(
689731
'/(<\?xml.*)(<w:p\b.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p\b.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
690732
$this->tempDocumentMainPart,
@@ -724,6 +766,7 @@ public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVaria
724766
*/
725767
public function replaceBlock($blockname, $replacement)
726768
{
769+
$matches = array();
727770
preg_match(
728771
'/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
729772
$this->tempDocumentMainPart,
@@ -865,6 +908,7 @@ protected function setValueForPart($search, $replace, $documentPartXML, $limit)
865908
*/
866909
protected function getVariablesForPart($documentPartXML)
867910
{
911+
$matches = array();
868912
preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
869913

870914
return $matches[1];
@@ -893,6 +937,7 @@ protected function getMainPartName()
893937

894938
$pattern = '~PartName="\/(word\/document.*?\.xml)" ContentType="application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document\.main\+xml"~';
895939

940+
$matches = array();
896941
preg_match($pattern, $contentTypes, $matches);
897942

898943
return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml';
@@ -1031,4 +1076,171 @@ protected function replaceClonedVariables($variableReplacements, $xmlBlock)
10311076

10321077
return $results;
10331078
}
1079+
1080+
/**
1081+
* Replace an XML block surrounding a macro with a new block
1082+
*
1083+
* @param string $macro Name of macro
1084+
* @param string $block New block content
1085+
* @param string $blockType XML tag type of block
1086+
* @return \PhpOffice\PhpWord\TemplateProcessor Fluent interface
1087+
*/
1088+
protected function replaceXmlBlock($macro, $block, $blockType = 'w:p')
1089+
{
1090+
$where = $this->findContainingXmlBlockForMacro($macro, $blockType);
1091+
if (false !== $where) {
1092+
$this->tempDocumentMainPart = $this->getSlice(0, $where['start']) . $block . $this->getSlice($where['end']);
1093+
}
1094+
1095+
return $this;
1096+
}
1097+
1098+
/**
1099+
* Find start and end of XML block containing the given macro
1100+
* e.g. <w:p>...${macro}...</w:p>
1101+
*
1102+
* Note that only the first instance of the macro will be found
1103+
*
1104+
* @param string $macro Name of macro
1105+
* @param string $blockType XML tag for block
1106+
* @return bool|int[] FALSE if not found, otherwise array with start and end
1107+
*/
1108+
protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p')
1109+
{
1110+
$macroPos = $this->findMacro($macro);
1111+
if (false === $macroPos) {
1112+
return false;
1113+
}
1114+
$start = $this->findXmlBlockStart($macroPos, $blockType);
1115+
if (0 > $start) {
1116+
return false;
1117+
}
1118+
$end = $this->findXmlBlockEnd($start, $blockType);
1119+
if (0 > $end) {
1120+
return false;
1121+
}
1122+
1123+
return array('start' => $start, 'end' => $end);
1124+
}
1125+
1126+
/**
1127+
* Find start and end of XML block containing the given block macro
1128+
* e.g. <w:p>...${macro}...${/macro}...</w:p>
1129+
*
1130+
* Note that only the first instance of the macro will be found
1131+
*
1132+
* @param string $macro Name of macro
1133+
* @param string $blockType XML tag for block
1134+
* @return bool|int[] FALSE if not found, otherwise array with start and end
1135+
*/
1136+
protected function findContainingXmlBlockForBlockMacro($macro, $blockType = 'w:p')
1137+
{
1138+
$macroStartPos = $this->findMacro($macro);
1139+
if (0 > $macroStartPos) {
1140+
return false;
1141+
}
1142+
$macroEndPos = $this->findMacro('/' . $macro, $macroStartPos);
1143+
if (0 > $macroEndPos) {
1144+
return false;
1145+
}
1146+
$start = $this->findXmlBlockStart($macroStartPos, $blockType);
1147+
if (0 > $start) {
1148+
return false;
1149+
}
1150+
$end = $this->findXmlBlockEnd($macroEndPos, $blockType);
1151+
if (0 > $end) {
1152+
return false;
1153+
}
1154+
1155+
return array('start' => $start, 'end' => $end);
1156+
}
1157+
1158+
/**
1159+
* Find the position of (the start of) a macro
1160+
*
1161+
* Returns -1 if not found, otherwise position of opening $
1162+
*
1163+
* Note that only the first instance of the macro will be found
1164+
*
1165+
* @param string $search Macro name
1166+
* @param string $offset Offset from which to start searching
1167+
* @return int -1 if macro not found
1168+
*/
1169+
protected function findMacro($search, $offset = 0)
1170+
{
1171+
$search = static::ensureMacroCompleted($search);
1172+
$pos = strpos($this->tempDocumentMainPart, $search, $offset);
1173+
1174+
return ($pos === false) ? -1 : $pos;
1175+
}
1176+
1177+
/**
1178+
* Find the start position of the nearest XML block start before $offset
1179+
*
1180+
* @param int $offset Search position
1181+
* @param string $blockType XML Block tag
1182+
* @return int -1 if block start not found
1183+
*/
1184+
protected function findXmlBlockStart($offset, $blockType)
1185+
{
1186+
// first try XML tag with attributes
1187+
$blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
1188+
// if not found, or if found but contains the XML tag without attribute
1189+
if (false === $blockStart || strrpos($this->getSlice($blockStart, $offset), '<' . $blockType . '>')) {
1190+
// also try XML tag without attributes
1191+
$blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . '>', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
1192+
}
1193+
1194+
return ($blockStart === false) ? -1 : $blockStart;
1195+
}
1196+
1197+
/**
1198+
* Find the nearest block end position after $offset
1199+
*
1200+
* @param int $offset Search position
1201+
* @param string $blockType XML Block tag
1202+
* @return int -1 if block end not found
1203+
*/
1204+
protected function findXmlBlockEnd($offset, $blockType)
1205+
{
1206+
$blockEndStart = strpos($this->tempDocumentMainPart, '</' . $blockType . '>', $offset);
1207+
// return position of end of tag if found, otherwise -1
1208+
1209+
return ($blockEndStart === false) ? -1 : $blockEndStart + 3 + strlen($blockType);
1210+
}
1211+
1212+
/**
1213+
* Splits a w:r/w:t into a list of w:r where each ${macro} is in a separate w:r
1214+
*
1215+
* @param string $text
1216+
* @return string
1217+
*/
1218+
protected function splitTextIntoTexts($text)
1219+
{
1220+
if (!$this->textNeedsSplitting($text)) {
1221+
return $text;
1222+
}
1223+
$matches = array();
1224+
if (preg_match('/(<w:rPr.*<\/w:rPr>)/i', $text, $matches)) {
1225+
$extractedStyle = $matches[0];
1226+
} else {
1227+
$extractedStyle = '';
1228+
}
1229+
1230+
$unformattedText = preg_replace('/>\s+</', '><', $text);
1231+
$result = str_replace(array('${', '}'), array('</w:t></w:r><w:r>' . $extractedStyle . '<w:t xml:space="preserve">${', '}</w:t></w:r><w:r>' . $extractedStyle . '<w:t xml:space="preserve">'), $unformattedText);
1232+
1233+
return str_replace(array('<w:r>' . $extractedStyle . '<w:t xml:space="preserve"></w:t></w:r>', '<w:r><w:t xml:space="preserve"></w:t></w:r>', '<w:t>'), array('', '', '<w:t xml:space="preserve">'), $result);
1234+
}
1235+
1236+
/**
1237+
* Returns true if string contains a macro that is not in it's own w:r
1238+
*
1239+
* @param string $text
1240+
* @return bool
1241+
*/
1242+
protected function textNeedsSplitting($text)
1243+
{
1244+
return preg_match('/[^>]\${|}[^<]/i', $text) == 1;
1245+
}
10341246
}

0 commit comments

Comments
 (0)