Passed
Push — master ( 0cb205...76cfc5 )
by Bingo
03:16
created

PhpDocxTemplate::resolveListing()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 4
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
rs 10
ccs 5
cts 5
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
use PhpDocxTemplate\Twig\Impl\{
10
    ImageExtension,
11
    RenderListener
12
};
13
14
/**
15
 * Class PhpDocxTemplate
16
 *
17
 * @package PhpDocxTemplate
18
 */
19
class PhpDocxTemplate
20
{
21
    private const NEWLINE_XML = '</w:t><w:br/><w:t xml:space="preserve">';
22
    private const NEWPARAGRAPH_XML = '</w:t></w:r></w:p><w:p><w:r><w:t xml:space="preserve">';
23
    private const TAB_XML = '</w:t></w:r><w:r><w:tab/></w:r><w:r><w:t xml:space="preserve">';
24
    private const PAGE_BREAK = '</w:t><w:br w:type="page"/><w:t xml:space="preserve">';
25
26
    private const HEADER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
27
    private const FOOTER_URI = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
28
29
    private $docx;
30
    private $crcToNewMedia;
31
    private $crcToNewEmbedded;
32
    private $picToReplace;
33
    private $picMap;
34
35
    /**
36
     * Construct an instance of PhpDocxTemplate
37
     *
38
     * @param string $path - path to the template
39
     */
40 11
    public function __construct(string $path)
41
    {
42 11
        $this->docx = new DocxDocument($path);
43 11
        $this->crcToNewMedia = [];
44 11
        $this->crcToNewEmbedded = [];
45 11
        $this->picToReplace = [];
46 11
        $this->picMap = [];
47 11
    }
48
49
    /**
50
     * Convert DOM to string
51
     *
52
     * @param DOMDocument $dom - DOM to be converted
53
     *
54
     * @return string
55
     */
56 9
    public function xmlToString(DOMDocument $dom): string
57
    {
58
        //return $el->ownerDocument->saveXML($el);
59 9
        return $dom->saveXML();
60
    }
61
62
    /**
63
     * Get document wrapper
64
     *
65
     * @return DocxDocument
66
     */
67 2
    public function getDocx(): DocxDocument
68
    {
69 2
        return $this->docx;
70
    }
71
72
    /**
73
     * Convert document.xml contents as string
74
     *
75
     * @return string
76
     */
77 8
    public function getXml(): string
78
    {
79 8
        return $this->xmlToString($this->docx->getDOMDocument());
80
    }
81
82
    /**
83
     * Write document.xml contents to file
84
     */
85
    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...
86
    {
87
        file_put_contents($path, $this->getXml());
88
    }
89
90
    /**
91
     * Update document.xml contents to file
92
     *
93
     * @param DOMDocument $xml - new contents
94
     */
95 6
    private function updateXml(DOMDocument $xml): void
96
    {
97 6
        $this->docx->updateDOMDocument($xml);
98 6
    }
99
100
    /**
101
     * Patch initial xml
102
     *
103
     * @param string $xml - initial xml
104
     */
105 8
    public function patchXml(string $xml): string
106
    {
107 8
        $xml = preg_replace('/(?<={)(<[^>]*>)+(?=[\{%])|(?<=[%\}])(<[^>]*>)+(?=\})/mu', '', $xml);
108 8
        $xml = preg_replace_callback(
109 8
            '/{%(?:(?!%}).)*|{{(?:(?!}}).)*/mu',
110 8
            array(get_class($this), 'stripTags'),
111 8
            $xml
112
        );
113 8
        $xml = preg_replace_callback(
114 8
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*colspan\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
115 8
            array(get_class($this), 'colspan'),
116 8
            $xml
117
        );
118 8
        $xml = preg_replace_callback(
119 8
            '/(<w:tc[ >](?:(?!<w:tc[ >]).)*){%\s*cellbg\s+([^%]*)\s*%}(.*?<\/w:tc>)/mu',
120 8
            array(get_class($this), 'cellbg'),
121 8
            $xml
122
        );
123
        // avoid {{r and {%r tags to strip MS xml tags too far
124
        // ensure space preservation when splitting
125 8
        $xml = preg_replace(
126 8
            '/<w:t>((?:(?!<w:t>).)*)({{r\s.*?}}|{%r\s.*?%})/mu',
127 8
            '<w:t xml:space="preserve">${1}${2}',
128 8
            $xml
129
        );
130 8
        $xml = preg_replace(
131 8
            '/({{r\s.*?}}|{%r\s.*?%})/mu',
132 8
            '</w:t></w:r><w:r><w:t xml:space="preserve">${1}</w:t></w:r><w:r><w:t xml:space="preserve">',
133 8
            $xml
134
        );
135
136
        // {%- will merge with previous paragraph text
137 8
        $xml = preg_replace(
138 8
            '/<\/w:t>(?:(?!<\/w:t>).)*?{%-/mu',
139 8
            '{%',
140 8
            $xml
141
        );
142
143
        // -%} will merge with next paragraph text
144 8
        $xml = preg_replace(
145 8
            '/-%}(?:(?!<w:t[ >]).)*?<w:t[^>]*?>/mu',
146 8
            '%}',
147 8
            $xml
148
        );
149
150
        // replace into xml code the row/paragraph/run containing
151
        // {%y xxx %} or {{y xxx}} template tag
152
        // by {% xxx %} or {{ xx }} without any surronding <w:y> tags
153 8
        $tokens = ['tr', 'tc', 'p', 'r'];
154 8
        foreach ($tokens as $token) {
155 8
            $regex = '/';
156 8
            $regex .= str_replace("%s", $token, '<w:%s[ >](?:(?!<w:%s[ >]).)*({%|{{)%s ([^}%]*(?:%}|}})).*?<\/w:%s>');
157 8
            $regex .= '/mu';
158 8
            $xml = preg_replace(
159 8
                $regex,
160 8
                '${1} ${2}',
161 8
                $xml
162
            );
163
        }
164
165 8
        $xml = preg_replace_callback(
166 8
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*vm\s*%}.*?<\/w:tc[ >]/mu',
167 8
            array(get_class($this), 'vMergeTc'),
168 8
            $xml
169
        );
170
171 8
        $xml = preg_replace_callback(
172 8
            '/<w:tc[ >](?:(?!<w:tc[ >]).)*?{%\s*hm\s*%}.*?<\/w:tc[ >]/mu',
173 8
            array(get_class($this), 'hMergeTc'),
174 8
            $xml
175
        );
176
177 8
        $xml = preg_replace_callback(
178 8
            '/(?<=\{[\{%])(.*?)(?=[\}%]})/mu',
179 8
            array(get_class($this), 'cleanTags'),
180 8
            $xml
181
        );
182
183 8
        return $xml;
184
    }
185
186 7
    private function resolveListing(string $xml): string
187
    {
188 7
        return preg_replace_callback(
189 7
            '/<w:p\b(?:[^>]*)?>.*?<\/w:p>/mus',
190 7
            array(get_class($this), 'resolveParagraph'),
191 7
            $xml
192
        );
193
    }
194
195 7
    private function resolveParagraph(array $matches): string
196
    {
197 7
        preg_match("/<w:pPr>.*<\/w:pPr>/mus", $matches[0], $paragraphProperties);
198
199 7
        return preg_replace_callback(
200 7
            '/<w:r\b(?:[^>]*)?>.*?<\/w:r>/mus',
201
            function ($m) use ($paragraphProperties) {
202 7
                return $this->resolveRun($paragraphProperties[0] ?? '', $m);
203 7
            },
204 7
            $matches[0]
205
        );
206
    }
207
208 7
    private function resolveRun(string $paragraphProperties, array $matches): string
209
    {
210 7
        preg_match("/<w:rPr>.*<\/w:rPr>/mus", $matches[0], $runProperties);
211
212 7
        return preg_replace_callback(
213 7
            '/<w:t\b(?:[^>]*)?>.*?<\/w:t>/mus',
214
            function ($m) use ($paragraphProperties, $runProperties) {
215 7
                return $this->resolveText($paragraphProperties, $runProperties[0] ?? '', $m);
216 7
            },
217 7
            $matches[0]
218
        );
219
    }
220
221 7
    private function resolveText(string $paragraphProperties, string $runProperties, array $matches): string
222
    {
223 7
        $xml = str_replace(
224 7
            "\t",
225 7
            sprintf("</w:t></w:r>" .
226
                "<w:r>%s<w:tab/></w:r>" .
227 7
                "<w:r>%s<w:t xml:space=\"preserve\">", $runProperties, $runProperties),
228 7
            $matches[0]
229
        );
230
231 7
        $xml = str_replace(
232 7
            "\a",
233 7
            sprintf("</w:t></w:r></w:p>" .
234 7
                "<w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
235 7
            $xml
236
        );
237
238 7
        $xml = str_replace("\n", sprintf("</w:t>" .
239
            "</w:r>" .
240
            "</w:p>" .
241
            "<w:p>%s" .
242
            "<w:r>%s" .
243 7
            "<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties), $xml);
244
245 7
        $xml = str_replace(
246 7
            "\f",
247 7
            sprintf("</w:t></w:r></w:p>" .
248
                "<w:p><w:r><w:br w:type=\"page\"/></w:r></w:p>" .
249 7
                "<w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
250 7
            $xml
251
        );
252
253 7
        return $xml;
254
    }
255
256
    /**
257
     * Strip tags from matches
258
     *
259
     * @param array $matches - matches
260
     *
261
     * @return string
262
     */
263 8
    private function stripTags(array $matches): string
264
    {
265 8
        return preg_replace('/<\/w:t>.*?(<w:t>|<w:t [^>]*>)/mu', '', $matches[0]);
266
    }
267
268
    /**
269
     * Parse colspan
270
     *
271
     * @param array $matches - matches
272
     *
273
     * @return string
274
     */
275 1
    private function colspan(array $matches): string
276
    {
277 1
        $cellXml = $matches[1] . $matches[3];
278 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
279 1
        $cellXml = preg_replace('/<w:gridSpan[^\/]*\/>/mu', '', $cellXml, 1);
280 1
        return preg_replace(
281 1
            '/(<w:tcPr[^>]*>)/mu',
282 1
            sprintf('${1}<w:gridSpan w:val="{{%s}}"/>', $matches[2]),
283 1
            $cellXml
284
        );
285
    }
286
287
    /**
288
     * Parse cellbg
289
     *
290
     * @param array $matches - matches
291
     *
292
     * @return string
293
     */
294 1
    private function cellbg(array $matches): string
295
    {
296 1
        $cellXml = $matches[1] . $matches[3];
297 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
298 1
        $cellXml = preg_replace('/<w:shd[^\/]*\/>/mu', '', $cellXml, 1);
299 1
        return preg_replace(
300 1
            '/(<w:tcPr[^>]*>)/mu',
301 1
            sprintf('${1}<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>', $matches[2]),
302 1
            $cellXml
303
        );
304
    }
305
306
    /**
307
     * Parse vm
308
     *
309
     * @param array $matches - matches
310
     *
311
     * @return string
312
     */
313 1
    private function vMergeTc(array $matches): string
314
    {
315 1
        return preg_replace_callback(
316 1
            '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(<\/w:t>)/mu',
317 1
            array(get_class($this), 'vMerge'),
318 1
            $matches[0]
319
        );
320
    }
321
322
    /**
323
     * Continue parsing vm
324
     *
325
     * @param array $matches - matches
326
     *
327
     * @return string
328
     */
329 1
    private function vMerge(array $matches): string
330
    {
331
        return '<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>' .
332 1
            $matches[1] .  // Everything between ``</w:tcPr>`` and ``<w:t>``.
333 1
            "{% if loop.first %}" .
334 1
            $matches[2] .  // Everything before ``{% vm %}``.
335 1
            $matches[3] .  // Everything after ``{% vm %}``.
336 1
            "{% endif %}" .
337 1
            $matches[4];  // ``</w:t>``.
338
    }
339
340
    /**
341
     * Parse hm
342
     *
343
     * @param array $matches - matches
344
     *
345
     * @return string
346
     */
347 1
    private function hMergeTc(array $matches): string
348
    {
349 1
        $xmlToPatch = $matches[0];
350 1
        if (strpos($xmlToPatch, 'w:gridSpan') !== false) {
351
            $xmlToPatch = preg_replace_callback(
352
                '/(w:gridSpan w:val=")(\d+)(")/mu',
353
                array(get_class($this), 'withGridspan'),
354
                $xmlToPatch
355
            );
356
            $xmlToPatch = preg_replace('/{%\s*hm\s*%}/mu', '', $xmlToPatch);
357
        } else {
358 1
            $xmlToPatch = preg_replace_callback(
359 1
                '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(<\/w:t>)/mu',
360 1
                array(get_class($this), 'withoutGridspan'),
361 1
                $xmlToPatch
362
            );
363
        }
364
365 1
        return "{% if loop.first %}" . $xmlToPatch . "{% endif %}";
366
    }
367
368
    private function withGridspan(array $matches): string
369
    {
370
        return $matches[1] . // ``w:gridSpan w:val="``.
371
            '{{ ' . $matches[2] . ' * loop.length }}' . // Content of ``w:val``, multiplied by loop length.
372
            $matches[3];  // Closing quotation mark.
373
    }
374
375 1
    private function withoutGridspan(array $matches): string
376
    {
377
        return '<w:gridSpan w:val="{{ loop.length }}"/>' .
378 1
            $matches[1] . // Everything between ``</w:tcPr>`` and ``<w:t>``.
379 1
            $matches[2] . // Everything before ``{% hm %}``.
380 1
            $matches[3] . // Everything after ``{% hm %}``.
381 1
            $matches[4]; // ``</w:t>``.
382
    }
383
384
    /**
385
     * Clean tags in matches
386
     *
387
     * @param array $matches - matches
388
     *
389
     * @return string
390
     */
391 8
    private function cleanTags(array $matches): string
392
    {
393 8
        return str_replace(
394 8
            ["&#8216;", '&lt;', '&gt;', '“', '”', "‘", "’"],
395 8
            ["'", '<', '>', '"', '"', "'", "'"],
396 8
            $matches[0]
397
        );
398
    }
399
400
    /**
401
     * Render xml
402
     *
403
     * @param string $srcXml - source xml
404
     * @param array $context - data to be rendered
405
     *
406
     * @return string
407
     */
408 7
    private function renderXml(string $srcXml, array $context): string
409
    {
410 7
        $srcXml = str_replace('<w:p>', "\n<w:p>", $srcXml);
411
412 7
        $ext = new ImageExtension();
413 7
        $ext->addListener(
414 7
            new RenderListener()
415
        );
416
417 7
        $template = new Environment(new ArrayLoader([
418 7
            'index' => $srcXml,
419
        ]));
420 7
        $template->addExtension($ext);
421
422 7
        $dstXml = $template->render('index', $context);
423
424 7
        $dstXml = str_replace(
425 7
            ["\n<w:p>", "{_{", '}_}', '{_%', '%_}'],
426 7
            ['<w:p>', "{{", '}}', '{%', '%}'],
427 7
            $dstXml
428
        );
429
430
        // fix xml after rendering
431 7
        $dstXml = preg_replace(
432
            '/<w:p [^>]*>(?:<w:r [^>]*><w:t [^>]*>\s*<\/w:t><\/w:r>)?(?:<w:pPr><w:ind w:left="360"\/>' .
433 7
            '<\/w:pPr>)?<w:r [^>]*>(?:<w:t\/>|<w:t [^>]*><\/w:t>|<w:t [^>]*\/>|<w:t><\/w:t>)<\/w:r><\/w:p>/mu',
434 7
            '',
435 7
            $dstXml
436
        );
437
438 7
        $dstXml = $this->resolveListing($dstXml);
439
440 7
        return $dstXml;
441
    }
442
443
    /**
444
     * Build xml
445
     *
446
     * @param array $context - data to be rendered
447
     *
448
     * @return string
449
     */
450 7
    public function buildXml(array $context): string
451
    {
452 7
        $xml = $this->getXml();
453 7
        $xml = $this->patchXml($xml);
454 7
        $xml = $this->renderXml($xml, $context);
455 7
        return $xml;
456
    }
457
458
    /**
459
     * Render document
460
     *
461
     * @param array $context - data to be rendered
462
     */
463 6
    public function render(array $context): void
464
    {
465 6
        $xmlSrc = $this->buildXml($context);
466 6
        $newXml = $this->docx->fixTables($xmlSrc);
467 6
        $this->updateXml($newXml);
468 6
    }
469
470
    /**
471
     * Save document
472
     *
473
     * @param string $path - target path
474
     */
475 2
    public function save(string $path): void
476
    {
477
        //$this->preProcessing();
478 2
        $this->docx->save($path);
479
        //$this->postProcessing($path);
480 2
    }
481
482
    /**
483
     * Clean everything after rendering
484
     */
485 5
    public function close(): void
486
    {
487 5
        $this->docx->close();
488 5
    }
489
490
    /**
491
     * @param mixed $search
492
     * @param mixed $replace Path to image, or array("path" => xx, "width" => yy, "height" => zz)
493
     * @param int $limit
494
     */
495 1
    public function setImageValue($search, $replace, ?int $limit = null): void
0 ignored issues
show
Unused Code introduced by
The parameter $limit is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

495
    public function setImageValue($search, $replace, /** @scrutinizer ignore-unused */ ?int $limit = null): void

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
496
    {
497 1
        $this->docx->setImageValue($search, $replace, $limit = null);
0 ignored issues
show
Unused Code introduced by
The call to PhpDocxTemplate\DocxDocument::setImageValue() has too many arguments starting with $limit = null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

497
        $this->docx->/** @scrutinizer ignore-call */ 
498
                     setImageValue($search, $replace, $limit = null);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
498 1
    }
499
}
500