Passed
Push — master ( 1dfb25...ac9139 )
by
unknown
03:29
created

PhpDocxTemplate::resolveText()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 33
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 24
c 0
b 0
f 0
nc 1
nop 3
dl 0
loc 33
rs 9.536
ccs 19
cts 19
cp 1
crap 1
1
<?php
2
3
namespace PhpDocxTemplate;
4
5
use DOMDocument;
6
use DOMElement;
7
use Twig\Loader\ArrayLoader;
8
use Twig\Environment;
9
10
/**
11
 * Class PhpDocxTemplate
12
 *
13
 * @package PhpDocxTemplate
14
 */
15
class PhpDocxTemplate
16
{
17
    private const NEWLINE_XML = '</w:t><w:br/><w:t xml:space="preserve">';
18
    private const NEWPARAGRAPH_XML = '</w:t></w:r></w:p><w:p><w:r><w:t xml:space="preserve">';
19
    private const TAB_XML = '</w:t></w:r><w:r><w:tab/></w:r><w:r><w:t xml:space="preserve">';
20
    private const PAGE_BREAK = '</w:t><w:br w:type="page"/><w:t xml:space="preserve">';
21
22
    private const HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
23
    private const FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
24
25
    private $docx;
26
    private $crcToNewMedia;
27
    private $crcToNewEmbedded;
28
    private $picToReplace;
29
    private $picMap;
30
31
    /**
32
     * Construct an instance of PhpDocxTemplate
33
     *
34
     * @param string $path - path to the template
35
     */
36 9
    public function __construct(string $path)
37
    {
38 9
        $this->docx = new DocxDocument($path);
39 9
        $this->crcToNewMedia = [];
40 9
        $this->crcToNewEmbedded = [];
41 9
        $this->picToReplace = [];
42 9
        $this->picMap = [];
43 9
    }
44
45
    /**
46
     * Convert DOM to string
47
     *
48
     * @param DOMDocument $dom - DOM to be converted
49
     *
50
     * @return string
51
     */
52 7
    public function xmlToString(DOMDocument $dom): string
53
    {
54
        //return $el->ownerDocument->saveXML($el);
55 7
        return $dom->saveXML();
56
    }
57
58
    /**
59
     * Get document wrapper
60
     *
61
     * @return DocxDocument
62
     */
63 1
    public function getDocx(): DocxDocument
64
    {
65 1
        return $this->docx;
66
    }
67
68
    /**
69
     * Convert document.xml contents as string
70
     *
71
     * @return string
72
     */
73 6
    public function getXml(): string
74
    {
75 6
        return $this->xmlToString($this->docx->getDOMDocument());
76
    }
77
78
    /**
79
     * Write document.xml contents to file
80
     */
81
    private function writeXml(string $path): void
0 ignored issues
show
Unused Code introduced by
The method writeXml() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
82
    {
83
        file_put_contents($path, $this->getXml());
84
    }
85
86
    /**
87
     * Update document.xml contents to file
88
     *
89
     * @param DOMDocument $xml - new contents
90
     */
91 4
    private function updateXml(DOMDocument $xml): void
92
    {
93 4
        $this->docx->updateDOMDocument($xml);
94 4
    }
95
96
    /**
97
     * Patch initial xml
98
     *
99
     * @param string $xml - initial xml
100
     */
101 6
    public function patchXml(string $xml): string
102
    {
103 6
        $xml = preg_replace('/(?<={)(<[^>]*>)+(?=[\{%])|(?<=[%\}])(<[^>]*>)+(?=\})/mu', '', $xml);
104 6
        $xml = preg_replace_callback(
105 6
            '/{%(?:(?!%}).)*|{{(?:(?!}}).)*/mu',
106 6
            array(get_class($this), 'stripTags'),
107 6
            $xml
108
        );
109 6
        $xml = preg_replace_callback(
110 6
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
111 6
            array(get_class($this), 'colspan'),
112 6
            $xml
113
        );
114 6
        $xml = preg_replace_callback(
115 6
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
116 6
            array(get_class($this), 'cellbg'),
117 6
            $xml
118
        );
119
        // avoid {{r and {%r tags to strip MS xml tags too far
120
        // ensure space preservation when splitting
121 6
        $xml = preg_replace(
122 6
            '/<w:t>((?:(?!<w:t>).)*)({{r\s.*?}}|{%r\s.*?%})/mu',
123 6
            '<w:t xml:space="preserve">${1}${2}',
124 6
            $xml
125
        );
126 6
        $xml = preg_replace(
127 6
            '/({{r\s.*?}}|{%r\s.*?%})/mu',
128 6
            '</w:t></w:r><w:r><w:t xml:space="preserve">${1}</w:t></w:r><w:r><w:t xml:space="preserve">',
129 6
            $xml
130
        );
131
132
        // replace into xml code the row/paragraph/run containing
133
        // {%y xxx %} or {{y xxx}} template tag
134
        // by {% xxx %} or {{ xx }} without any surronding <w:y> tags
135 6
        $tokens = ['tr', 'tc', 'p', 'r'];
136 6
        foreach ($tokens as $token) {
137 6
            $regex = '/';
138 6
            $regex .= sprintf(
139 6
                '<w:%ss[ >](?:(?!<w:%ss[ >]).)*({%%|{{)%ss ([^}%%]*(?:%%}|}})).*?<\/w:%ss>',
140 6
                $token,
141 6
                $token,
142 6
                $token,
143 6
                $token
144
            );
145 6
            $regex .= '/mu';
146 6
            $xml = preg_replace(
147 6
                $regex,
148 6
                '${1} ${2}',
149 6
                $xml
150
            );
151
        }
152
153 6
        $xml = preg_replace_callback(
154 6
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?<\/w:tc[ >]/mu',
155 6
            array(get_class($this), 'vMergeTc'),
156 6
            $xml
157
        );
158
159 6
        $xml = preg_replace_callback(
160 6
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?<\/w:tc[ >]/mu',
161 6
            array(get_class($this), 'hMergeTc'),
162 6
            $xml
163
        );
164
165 6
        $xml = preg_replace_callback(
166 6
            '/(?<=\{[\{%])(.*?)(?=[\}%]})/mu',
167 6
            array(get_class($this), 'cleanTags'),
168 6
            $xml
169
        );
170
171 6
        return $xml;
172
    }
173
174 5
    private function resolveListing(string $xml): string
175
    {
176 5
        return preg_replace_callback(
177 5
            '/<w:p(?:[^>]*)?>.*?<\/w:p>/mus',
178 5
            array(get_class($this), 'resolveParagraph'),
179 5
            $xml
180
        );
181
    }
182
183 5
    private function resolveParagraph(array $matches): string
184
    {
185 5
        preg_match("/<w:pPr>.*<\/w:pPr>/mus", $matches[0], $paragraphProperties);
186
187 5
        return preg_replace_callback(
188 5
            '/<w:r(?:[^>]*)?>.*?<\/w:r>/mus',
189
            function ($m) {
190 5
                return $this->resolveRun($paragraphProperties[0] ?? '', $m);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $paragraphProperties seems to never exist and therefore isset should always be false.
Loading history...
191 5
            },
192 5
            $matches[0]
193
        );
194
    }
195
196 5
    private function resolveRun(string $paragraphProperties, array $matches): string
197
    {
198 5
        preg_match("/<w:rPr>.*<\/w:rPr>/mus", $matches[0], $runProperties);
199
200 5
        return preg_replace_callback(
201 5
            '/<w:t(?:[^>]*)?>.*?<\/w:t>/mus',
202
            function ($m) use ($paragraphProperties) {
203 5
                return $this->resolveText($paragraphProperties, $runProperties[0] ?? '', $m);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $runProperties seems to never exist and therefore isset should always be false.
Loading history...
204 5
            },
205 5
            $matches[0]
206
        );
207
    }
208
209 5
    private function resolveText(string $paragraphProperties, string $runProperties, array $matches): string
210
    {
211 5
        $xml = str_replace(
212 5
            "\t",
213 5
            sprintf("</w:t></w:r>" .
214
                "<w:r>%s<w:tab/></w:r>" .
215 5
                "<w:r>%s<w:t xml:space=\"preserve\">", $runProperties, $runProperties),
216 5
            $matches[0]
217
        );
218
219 5
        $xml = str_replace(
220 5
            "\a",
221 5
            sprintf("</w:t></w:r></w:p>" .
222 5
                "<w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
223 5
            $xml
224
        );
225
226 5
        $xml = str_replace("\n", sprintf("</w:t>" .
227
            "</w:r>" .
228
            "</w:p>" .
229
            "<w:p>%s" .
230
            "<w:r>%s" .
231 5
            "<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties), $xml);
232
233 5
        $xml = str_replace(
234 5
            "\f",
235 5
            sprintf("</w:t></w:r></w:p>" .
236
                "<w:p><w:r><w:br w:type=\"page\"/></w:r></w:p>" .
237 5
                "<w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
238 5
            $xml
239
        );
240
241 5
        return $xml;
242
    }
243
244
    /**
245
     * Strip tags from matches
246
     *
247
     * @param array $matches - matches
248
     *
249
     * @return string
250
     */
251 6
    private function stripTags(array $matches): string
252
    {
253 6
        return preg_replace('/<\/w:t>.*?(<w:t>|<w:t [^>]*>)/mu', '', $matches[0]);
254
    }
255
256
    /**
257
     * Parse colspan
258
     *
259
     * @param array $matches - matches
260
     *
261
     * @return string
262
     */
263 1
    private function colspan(array $matches): string
264
    {
265 1
        $cellXml = $matches[1] . $matches[3];
266 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
267 1
        $cellXml = preg_replace('/<w:gridSpan[^\/]*\/>/mu', '', $cellXml, 1);
268 1
        return preg_replace(
269 1
            '/(<w:tcPr[^>]*>)/mu',
270 1
            sprintf('${1}<w:gridSpan w:val="{{%s}}"/>', $matches[2]),
271 1
            $cellXml
272
        );
273
    }
274
275
    /**
276
     * Parse cellbg
277
     *
278
     * @param array $matches - matches
279
     *
280
     * @return string
281
     */
282 1
    private function cellbg(array $matches): string
283
    {
284 1
        $cellXml = $matches[1] . $matches[3];
285 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
286 1
        $cellXml = preg_replace('/<w:shd[^\/]*\/>/mu', '', $cellXml, 1);
287 1
        return preg_replace(
288 1
            '/(<w:tcPr[^>]*>)/mu',
289 1
            sprintf('${1}<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>', $matches[2]),
290 1
            $cellXml
291
        );
292
    }
293
294
    /**
295
     * Parse vm
296
     *
297
     * @param array $matches - matches
298
     *
299
     * @return string
300
     */
301 1
    private function vMergeTc(array $matches): string
302
    {
303 1
        return preg_replace_callback(
304 1
            '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(<\/w:t>)/mu',
305 1
            array(get_class($this), 'vMerge'),
306 1
            $matches[0]
307
        );
308
    }
309
310
    /**
311
     * Continue parsing vm
312
     *
313
     * @param array $matches - matches
314
     *
315
     * @return string
316
     */
317 1
    private function vMerge(array $matches): string
318
    {
319
        return '<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>' .
320 1
            $matches[1] .  // Everything between ``</w:tcPr>`` and ``<w:t>``.
321 1
            "{% if loop.first %}" .
322 1
            $matches[2] .  // Everything before ``{% vm %}``.
323 1
            $matches[3] .  // Everything after ``{% vm %}``.
324 1
            "{% endif %}" .
325 1
            $matches[4];  // ``</w:t>``.
326
    }
327
328
    /**
329
     * Parse hm
330
     *
331
     * @param array $matches - matches
332
     *
333
     * @return string
334
     */
335 1
    private function hMergeTc(array $matches): string
336
    {
337 1
        $xmlToPatch = $matches[0];
338 1
        if (strpos($xmlToPatch, 'w:gridSpan') !== false) {
339
            $xmlToPatch = preg_replace_callback(
340
                '/(w:gridSpan w:val=")(\d+)(")/mu',
341
                array(get_class($this), 'withGridspan'),
342
                $xmlToPatch
343
            );
344
            $xmlToPatch = preg_replace('/{%\s*hm\s*%}/mu', '', $xmlToPatch);
345
        } else {
346 1
            $xmlToPatch = preg_replace_callback(
347 1
                '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(<\/w:t>)/mu',
348 1
                array(get_class($this), 'withoutGridspan'),
349 1
                $xmlToPatch
350
            );
351
        }
352
353 1
        return "{% if loop.first %}" . $xmlToPatch . "{% endif %}";
354
    }
355
356
    private function withGridspan(array $matches): string
357
    {
358
        return $matches[1] . // ``w:gridSpan w:val="``.
359
            '{{ ' . $matches[2] . ' * loop.length }}' . // Content of ``w:val``, multiplied by loop length.
360
            $matches[3];  // Closing quotation mark.
361
    }
362
363 1
    private function withoutGridspan(array $matches): string
364
    {
365
        return '<w:gridSpan w:val="{{ loop.length }}"/>' .
366 1
            $matches[1] . // Everything between ``</w:tcPr>`` and ``<w:t>``.
367 1
            $matches[2] . // Everything before ``{% hm %}``.
368 1
            $matches[3] . // Everything after ``{% hm %}``.
369 1
            $matches[4]; // ``</w:t>``.
370
    }
371
372
    /**
373
     * Clean tags in matches
374
     *
375
     * @param array $matches - matches
376
     *
377
     * @return string
378
     */
379 6
    private function cleanTags(array $matches): string
380
    {
381 6
        return str_replace(
382 6
            ["&#8216;", '&lt;', '&gt;', '“', '”', "‘", "’"],
383 6
            ["'", '<', '>', '"', '"', "'", "'"],
384 6
            $matches[0]
385
        );
386
    }
387
388
    /**
389
     * Render xml
390
     *
391
     * @param string $srcXml - source xml
392
     * @param array $context - data to be rendered
393
     *
394
     * @return string
395
     */
396 5
    private function renderXml(string $srcXml, array $context): string
397
    {
398 5
        $srcXml = str_replace('<w:p>', "\n<w:p>", $srcXml);
399
400 5
        $template = new Environment(new ArrayLoader([
401 5
            'index' => $srcXml,
402
        ]));
403 5
        $dstXml = $template->render('index', $context);
404
405 5
        $dstXml = str_replace(
406 5
            ["\n<w:p>", "{_{", '}_}', '{_%', '%_}'],
407 5
            ['<w:p>', "{{", '}}', '{%', '%}'],
408 5
            $dstXml
409
        );
410
411
        // fix xml after rendering
412 5
        $dstXml = preg_replace(
413
            '/<w:p [^>]*>(?:<w:r [^>]*><w:t [^>]*>\s*<\/w:t><\/w:r>)?(?:<w:pPr><w:ind w:left="360"\/>' .
414 5
            '<\/w:pPr>)?<w:r [^>]*>(?:<w:t\/>|<w:t [^>]*><\/w:t>|<w:t [^>]*\/>|<w:t><\/w:t>)<\/w:r><\/w:p>/mu',
415 5
            '',
416 5
            $dstXml
417
        );
418
419 5
        $dstXml = $this->resolveListing($dstXml);
420
421 5
        return $dstXml;
422
    }
423
424
    /**
425
     * Build xml
426
     *
427
     * @param array $context - data to be rendered
428
     *
429
     * @return string
430
     */
431 5
    public function buildXml(array $context): string
432
    {
433 5
        $xml = $this->getXml();
434 5
        $xml = $this->patchXml($xml);
435 5
        $xml = $this->renderXml($xml, $context);
436 5
        return $xml;
437
    }
438
439
    /**
440
     * Render document
441
     *
442
     * @param array $context - data to be rendered
443
     */
444 4
    public function render(array $context): void
445
    {
446 4
        $xmlSrc = $this->buildXml($context);
447 4
        $newXml = $this->docx->fixTables($xmlSrc);
448 4
        $this->updateXml($newXml);
449 4
    }
450
451
    /**
452
     * Save document
453
     *
454
     * @param string $path - target path
455
     */
456 1
    public function save(string $path): void
457
    {
458
        //$this->preProcessing();
459 1
        $this->docx->save($path);
460
        //$this->postProcessing($path);
461 1
    }
462
463
    /**
464
     * Clean everything after rendering
465
     */
466 5
    public function close(): void
467
    {
468 5
        $this->docx->close();
469 5
    }
470
}
471