|
18 | 18 | namespace PhpOffice\PhpWord;
|
19 | 19 |
|
20 | 20 | use PhpOffice\Common\Text;
|
| 21 | +use PhpOffice\Common\XMLWriter; |
21 | 22 | use PhpOffice\PhpWord\Escaper\RegExp;
|
22 | 23 | use PhpOffice\PhpWord\Escaper\Xml;
|
23 | 24 | use PhpOffice\PhpWord\Exception\CopyFileException;
|
@@ -249,6 +250,46 @@ protected static function ensureUtf8Encoded($subject)
|
249 | 250 | return $subject;
|
250 | 251 | }
|
251 | 252 |
|
| 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 | + |
252 | 293 | /**
|
253 | 294 | * @param mixed $search
|
254 | 295 | * @param mixed $replace
|
@@ -685,6 +726,7 @@ public function cloneRowAndSetValues($search, $values)
|
685 | 726 | public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVariables = false, $variableReplacements = null)
|
686 | 727 | {
|
687 | 728 | $xmlBlock = null;
|
| 729 | + $matches = array(); |
688 | 730 | preg_match(
|
689 | 731 | '/(<\?xml.*)(<w:p\b.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p\b.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
|
690 | 732 | $this->tempDocumentMainPart,
|
@@ -724,6 +766,7 @@ public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVaria
|
724 | 766 | */
|
725 | 767 | public function replaceBlock($blockname, $replacement)
|
726 | 768 | {
|
| 769 | + $matches = array(); |
727 | 770 | preg_match(
|
728 | 771 | '/(<\?xml.*)(<w:p.*>\${' . $blockname . '}<\/w:.*?p>)(.*)(<w:p.*\${\/' . $blockname . '}<\/w:.*?p>)/is',
|
729 | 772 | $this->tempDocumentMainPart,
|
@@ -865,6 +908,7 @@ protected function setValueForPart($search, $replace, $documentPartXML, $limit)
|
865 | 908 | */
|
866 | 909 | protected function getVariablesForPart($documentPartXML)
|
867 | 910 | {
|
| 911 | + $matches = array(); |
868 | 912 | preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches);
|
869 | 913 |
|
870 | 914 | return $matches[1];
|
@@ -893,6 +937,7 @@ protected function getMainPartName()
|
893 | 937 |
|
894 | 938 | $pattern = '~PartName="\/(word\/document.*?\.xml)" ContentType="application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document\.main\+xml"~';
|
895 | 939 |
|
| 940 | + $matches = array(); |
896 | 941 | preg_match($pattern, $contentTypes, $matches);
|
897 | 942 |
|
898 | 943 | return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml';
|
@@ -1031,4 +1076,141 @@ protected function replaceClonedVariables($variableReplacements, $xmlBlock)
|
1031 | 1076 |
|
1032 | 1077 | return $results;
|
1033 | 1078 | }
|
| 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 (is_array($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 (0 > $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 not found or if resulting string does not contain the macro we are searching for |
| 1120 | + if (0 > $end || strstr($this->getSlice($start, $end), $macro) === false) { |
| 1121 | + return false; |
| 1122 | + } |
| 1123 | + |
| 1124 | + return array('start' => $start, 'end' => $end); |
| 1125 | + } |
| 1126 | + |
| 1127 | + /** |
| 1128 | + * Find the position of (the start of) a macro |
| 1129 | + * |
| 1130 | + * Returns -1 if not found, otherwise position of opening $ |
| 1131 | + * |
| 1132 | + * Note that only the first instance of the macro will be found |
| 1133 | + * |
| 1134 | + * @param string $search Macro name |
| 1135 | + * @param int $offset Offset from which to start searching |
| 1136 | + * @return int -1 if macro not found |
| 1137 | + */ |
| 1138 | + protected function findMacro($search, $offset = 0) |
| 1139 | + { |
| 1140 | + $search = static::ensureMacroCompleted($search); |
| 1141 | + $pos = strpos($this->tempDocumentMainPart, $search, $offset); |
| 1142 | + |
| 1143 | + return ($pos === false) ? -1 : $pos; |
| 1144 | + } |
| 1145 | + |
| 1146 | + /** |
| 1147 | + * Find the start position of the nearest XML block start before $offset |
| 1148 | + * |
| 1149 | + * @param int $offset Search position |
| 1150 | + * @param string $blockType XML Block tag |
| 1151 | + * @return int -1 if block start not found |
| 1152 | + */ |
| 1153 | + protected function findXmlBlockStart($offset, $blockType) |
| 1154 | + { |
| 1155 | + $reverseOffset = (strlen($this->tempDocumentMainPart) - $offset) * -1; |
| 1156 | + // first try XML tag with attributes |
| 1157 | + $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', $reverseOffset); |
| 1158 | + // if not found, or if found but contains the XML tag without attribute |
| 1159 | + if (false === $blockStart || strrpos($this->getSlice($blockStart, $offset), '<' . $blockType . '>')) { |
| 1160 | + // also try XML tag without attributes |
| 1161 | + $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . '>', $reverseOffset); |
| 1162 | + } |
| 1163 | + |
| 1164 | + return ($blockStart === false) ? -1 : $blockStart; |
| 1165 | + } |
| 1166 | + |
| 1167 | + /** |
| 1168 | + * Find the nearest block end position after $offset |
| 1169 | + * |
| 1170 | + * @param int $offset Search position |
| 1171 | + * @param string $blockType XML Block tag |
| 1172 | + * @return int -1 if block end not found |
| 1173 | + */ |
| 1174 | + protected function findXmlBlockEnd($offset, $blockType) |
| 1175 | + { |
| 1176 | + $blockEndStart = strpos($this->tempDocumentMainPart, '</' . $blockType . '>', $offset); |
| 1177 | + // return position of end of tag if found, otherwise -1 |
| 1178 | + |
| 1179 | + return ($blockEndStart === false) ? -1 : $blockEndStart + 3 + strlen($blockType); |
| 1180 | + } |
| 1181 | + |
| 1182 | + /** |
| 1183 | + * Splits a w:r/w:t into a list of w:r where each ${macro} is in a separate w:r |
| 1184 | + * |
| 1185 | + * @param string $text |
| 1186 | + * @return string |
| 1187 | + */ |
| 1188 | + protected function splitTextIntoTexts($text) |
| 1189 | + { |
| 1190 | + if (!$this->textNeedsSplitting($text)) { |
| 1191 | + return $text; |
| 1192 | + } |
| 1193 | + $matches = array(); |
| 1194 | + if (preg_match('/(<w:rPr.*<\/w:rPr>)/i', $text, $matches)) { |
| 1195 | + $extractedStyle = $matches[0]; |
| 1196 | + } else { |
| 1197 | + $extractedStyle = ''; |
| 1198 | + } |
| 1199 | + |
| 1200 | + $unformattedText = preg_replace('/>\s+</', '><', $text); |
| 1201 | + $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); |
| 1202 | + |
| 1203 | + 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); |
| 1204 | + } |
| 1205 | + |
| 1206 | + /** |
| 1207 | + * Returns true if string contains a macro that is not in it's own w:r |
| 1208 | + * |
| 1209 | + * @param string $text |
| 1210 | + * @return bool |
| 1211 | + */ |
| 1212 | + protected function textNeedsSplitting($text) |
| 1213 | + { |
| 1214 | + return preg_match('/[^>]\${|}[^<]/i', $text) == 1; |
| 1215 | + } |
1034 | 1216 | }
|
0 commit comments