Passed
Push — master ( dc46b5...1dfb25 )
by
unknown
04:19
created

PhpDocxTemplate::resolveParagraph()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 10
rs 10
ccs 7
cts 7
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", "</w:t><w:br/><w:t xml:space=\"preserve\">", $xml);
227
228 5
        $xml = str_replace(
229 5
            "\f",
230 5
            sprintf("</w:t></w:r></w:p>
231
                                    <w:p><w:r><w:br w:type=\"page\"/></w:r></w:p>
232 5
                                    <w:p>%s<w:r>%s<w:t xml:space=\"preserve\">", $paragraphProperties, $runProperties),
233 5
            $xml
234
        );
235
236 5
        return $xml;
237
    }
238
239
    /**
240
     * Strip tags from matches
241
     *
242
     * @param array $matches - matches
243
     *
244
     * @return string
245
     */
246 6
    private function stripTags(array $matches): string
247
    {
248 6
        return preg_replace('/<\/w:t>.*?(<w:t>|<w:t [^>]*>)/mu', '', $matches[0]);
249
    }
250
251
    /**
252
     * Parse colspan
253
     *
254
     * @param array $matches - matches
255
     *
256
     * @return string
257
     */
258 1
    private function colspan(array $matches): string
259
    {
260 1
        $cellXml = $matches[1] . $matches[3];
261 1
        $cellXml = preg_replace('/<w:r[ >](?:(?!<w:r[ >]).)*<w:t><\/w:t>.*?<\/w:r>/mu', '', $cellXml);
262 1
        $cellXml = preg_replace('/<w:gridSpan[^\/]*\/>/mu', '', $cellXml, 1);
263 1
        return preg_replace(
264 1
            '/(<w:tcPr[^>]*>)/mu',
265 1
            sprintf('${1}<w:gridSpan w:val="{{%s}}"/>', $matches[2]),
266 1
            $cellXml
267
        );
268
    }
269
270
    /**
271
     * Parse cellbg
272
     *
273
     * @param array $matches - matches
274
     *
275
     * @return string
276
     */
277 1
    private function cellbg(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:shd[^\/]*\/>/mu', '', $cellXml, 1);
282 1
        return preg_replace(
283 1
            '/(<w:tcPr[^>]*>)/mu',
284 1
            sprintf('${1}<w:shd w:val="clear" w:color="auto" w:fill="{{%s}}"/>', $matches[2]),
285 1
            $cellXml
286
        );
287
    }
288
289
    /**
290
     * Parse vm
291
     *
292
     * @param array $matches - matches
293
     *
294
     * @return string
295
     */
296 1
    private function vMergeTc(array $matches): string
297
    {
298 1
        return preg_replace_callback(
299 1
            '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*vm\s*%})(.*?)(<\/w:t>)/mu',
300 1
            array(get_class($this), 'vMerge'),
301 1
            $matches[0]
302
        );
303
    }
304
305
    /**
306
     * Continue parsing vm
307
     *
308
     * @param array $matches - matches
309
     *
310
     * @return string
311
     */
312 1
    private function vMerge(array $matches): string
313
    {
314
        return '<w:vMerge w:val="{% if loop.first %}restart{% else %}continue{% endif %}"/>' .
315 1
            $matches[1] .  // Everything between ``</w:tcPr>`` and ``<w:t>``.
316 1
            "{% if loop.first %}" .
317 1
            $matches[2] .  // Everything before ``{% vm %}``.
318 1
            $matches[3] .  // Everything after ``{% vm %}``.
319 1
            "{% endif %}" .
320 1
            $matches[4];  // ``</w:t>``.
321
    }
322
323
    /**
324
     * Parse hm
325
     *
326
     * @param array $matches - matches
327
     *
328
     * @return string
329
     */
330 1
    private function hMergeTc(array $matches): string
331
    {
332 1
        $xmlToPatch = $matches[0];
333 1
        if (strpos($xmlToPatch, 'w:gridSpan') !== false) {
334
            $xmlToPatch = preg_replace_callback(
335
                '/(w:gridSpan w:val=")(\d+)(")/mu',
336
                array(get_class($this), 'withGridspan'),
337
                $xmlToPatch
338
            );
339
            $xmlToPatch = preg_replace('/{%\s*hm\s*%}/mu', '', $xmlToPatch);
340
        } else {
341 1
            $xmlToPatch = preg_replace_callback(
342 1
                '/(<\/w:tcPr[ >].*?<w:t(?:.*?)>)(.*?)(?:{%\s*hm\s*%})(.*?)(<\/w:t>)/mu',
343 1
                array(get_class($this), 'withoutGridspan'),
344 1
                $xmlToPatch
345
            );
346
        }
347
348 1
        return "{% if loop.first %}" . $xmlToPatch . "{% endif %}";
349
    }
350
351
    private function withGridspan(array $matches): string
352
    {
353
        return $matches[1] . // ``w:gridSpan w:val="``.
354
            '{{ ' . $matches[2] . ' * loop.length }}' . // Content of ``w:val``, multiplied by loop length.
355
            $matches[3];  // Closing quotation mark.
356
    }
357
358 1
    private function withoutGridspan(array $matches): string
359
    {
360
        return '<w:gridSpan w:val="{{ loop.length }}"/>' .
361 1
            $matches[1] . // Everything between ``</w:tcPr>`` and ``<w:t>``.
362 1
            $matches[2] . // Everything before ``{% hm %}``.
363 1
            $matches[3] . // Everything after ``{% hm %}``.
364 1
            $matches[4]; // ``</w:t>``.
365
    }
366
367
    /**
368
     * Clean tags in matches
369
     *
370
     * @param array $matches - matches
371
     *
372
     * @return string
373
     */
374 6
    private function cleanTags(array $matches): string
375
    {
376 6
        return str_replace(
377 6
            ["&#8216;", '&lt;', '&gt;', '“', '”', "‘", "’"],
378 6
            ["'", '<', '>', '"', '"', "'", "'"],
379 6
            $matches[0]
380
        );
381
    }
382
383
    /**
384
     * Render xml
385
     *
386
     * @param string $srcXml - source xml
387
     * @param array $context - data to be rendered
388
     *
389
     * @return string
390
     */
391 5
    private function renderXml(string $srcXml, array $context): string
392
    {
393 5
        $srcXml = str_replace('<w:p>', "\n<w:p>", $srcXml);
394
        
395 5
        $template = new Environment(new ArrayLoader([
396 5
            'index' => $srcXml,
397
        ]));
398 5
        $dstXml = $template->render('index', $context);
399
400 5
        $dstXml = str_replace(
401 5
            ["\n<w:p>", "{_{", '}_}', '{_%', '%_}'],
402 5
            ['<w:p>', "{{", '}}', '{%', '%}'],
403 5
            $dstXml
404
        );
405
406
        // fix xml after rendering
407 5
        $dstXml = preg_replace(
408
            '/<w:p [^>]*>(?:<w:r [^>]*><w:t [^>]*>\s*<\/w:t><\/w:r>)?(?:<w:pPr><w:ind w:left="360"\/>' .
409 5
            '<\/w:pPr>)?<w:r [^>]*>(?:<w:t\/>|<w:t [^>]*><\/w:t>|<w:t [^>]*\/>|<w:t><\/w:t>)<\/w:r><\/w:p>/mu',
410 5
            '',
411 5
            $dstXml
412
        );
413
414 5
        $dstXml = $this->resolveListing($dstXml);
415
416 5
        return $dstXml;
417
    }
418
419
    /**
420
     * Build xml
421
     *
422
     * @param array $context - data to be rendered
423
     *
424
     * @return string
425
     */
426 5
    public function buildXml(array $context): string
427
    {
428 5
        $xml = $this->getXml();
429 5
        $xml = $this->patchXml($xml);
430 5
        $xml = $this->renderXml($xml, $context);
431 5
        return $xml;
432
    }
433
434
    /**
435
     * Render document
436
     *
437
     * @param array $context - data to be rendered
438
     */
439 4
    public function render(array $context): void
440
    {
441 4
        $xmlSrc = $this->buildXml($context);
442 4
        $newXml = $this->docx->fixTables($xmlSrc);
443 4
        $this->updateXml($newXml);
444 4
    }
445
446
    /**
447
     * Save document
448
     *
449
     * @param string $path - target path
450
     */
451 1
    public function save(string $path): void
452
    {
453
        //$this->preProcessing();
454 1
        $this->docx->save($path);
455
        //$this->postProcessing($path);
456 1
    }
457
458
    /**
459
     * Clean everything after rendering
460
     */
461 5
    public function close(): void
462
    {
463 5
        $this->docx->close();
464 5
    }
465
}
466