Passed
Push — master ( 8f1d03...3f48a5 )
by Bingo
02:51
created

PhpDocxTemplate::hMergeTc()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.1922

Importance

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