• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PHPWord / 6182302352

14 Sep 2023 07:29AM UTC coverage: 81.553% (+0.07%) from 81.484%
6182302352

push

github

web-flow
Merge pull request #2469 from PHPOffice/pr2161

Word2007 Reader : Added support for Comments

90 of 90 new or added lines in 4 files covered. (100.0%)

10977 of 13460 relevant lines covered (81.55%)

24.04 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.49
/src/PhpWord/Reader/Word2007/AbstractPart.php
1
<?php
2
/**
3
 * This file is part of PHPWord - A pure PHP library for reading and writing
4
 * word processing documents.
5
 *
6
 * PHPWord is free software distributed under the terms of the GNU Lesser
7
 * General Public License version 3 as published by the Free Software Foundation.
8
 *
9
 * For the full copyright and license information, please read the LICENSE
10
 * file that was distributed with this source code. For the full list of
11
 * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
12
 *
13
 * @see         https://github.com/PHPOffice/PHPWord
14
 *
15
 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
16
 */
17

18
namespace PhpOffice\PhpWord\Reader\Word2007;
19

20
use DateTime;
21
use DOMElement;
22
use InvalidArgumentException;
23
use PhpOffice\PhpWord\ComplexType\TblWidth as TblWidthComplexType;
24
use PhpOffice\PhpWord\Element\AbstractContainer;
25
use PhpOffice\PhpWord\Element\AbstractElement;
26
use PhpOffice\PhpWord\Element\TextRun;
27
use PhpOffice\PhpWord\Element\TrackChange;
28
use PhpOffice\PhpWord\PhpWord;
29
use PhpOffice\PhpWord\Shared\XMLReader;
30

31
/**
32
 * Abstract part reader.
33
 *
34
 * This class is inherited by ODText reader
35
 *
36
 * @since 0.10.0
37
 */
38
abstract class AbstractPart
39
{
40
    /**
41
     * Conversion method.
42
     *
43
     * @const int
44
     */
45
    const READ_VALUE = 'attributeValue';            // Read attribute value
46
    const READ_EQUAL = 'attributeEquals';           // Read `true` when attribute value equals specified value
47
    const READ_TRUE = 'attributeTrue';              // Read `true` when element exists
48
    const READ_FALSE = 'attributeFalse';            // Read `false` when element exists
49
    const READ_SIZE = 'attributeMultiplyByTwo';     // Read special attribute value for Font::$size
50

51
    /**
52
     * Document file.
53
     *
54
     * @var string
55
     */
56
    protected $docFile;
57

58
    /**
59
     * XML file.
60
     *
61
     * @var string
62
     */
63
    protected $xmlFile;
64

65
    /**
66
     * Part relationships.
67
     *
68
     * @var array
69
     */
70
    protected $rels = [];
71

72
    /**
73
     * Comment references.
74
     *
75
     * @var array<string, array<string, AbstractElement>>
76
     */
77
    protected $commentRefs = [];
78

79
    /**
80
     * Image Loading.
81
     *
82
     * @var bool
83
     */
84
    protected $imageLoading = true;
85

86
    /**
87
     * Read part.
88
     */
89
    abstract public function read(PhpWord $phpWord);
90

91
    /**
92
     * Create new instance.
93
     *
94
     * @param string $docFile
95
     * @param string $xmlFile
96
     */
97
    public function __construct($docFile, $xmlFile)
98
    {
99
        $this->docFile = $docFile;
29✔
100
        $this->xmlFile = $xmlFile;
29✔
101
    }
102

103
    /**
104
     * Set relationships.
105
     *
106
     * @param array $value
107
     */
108
    public function setRels($value): void
109
    {
110
        $this->rels = $value;
7✔
111
    }
112

113
    public function setImageLoading(bool $value): self
114
    {
115
        $this->imageLoading = $value;
6✔
116

117
        return $this;
6✔
118
    }
119

120
    public function hasImageLoading(): bool
121
    {
122
        return $this->imageLoading;
5✔
123
    }
124

125
    /**
126
     * Get comment references.
127
     *
128
     * @return array<string, array<string, null|AbstractElement>>
129
     */
130
    public function getCommentReferences(): array
131
    {
132
        return $this->commentRefs;
6✔
133
    }
134

135
    /**
136
     * Set comment references.
137
     *
138
     * @param array<string, array<string, null|AbstractElement>> $commentRefs
139
     */
140
    public function setCommentReferences(array $commentRefs): self
141
    {
142
        $this->commentRefs = $commentRefs;
6✔
143

144
        return $this;
6✔
145
    }
146

147
    /**
148
     * Set comment reference.
149
     */
150
    private function setCommentReference(string $type, string $id, AbstractElement $element): self
151
    {
152
        if (!in_array($type, ['start', 'end'])) {
1✔
153
            throw new InvalidArgumentException('Type must be "start" or "end"');
×
154
        }
155

156
        if (!array_key_exists($id, $this->commentRefs)) {
1✔
157
            $this->commentRefs[$id] = [
1✔
158
                'start' => null,
1✔
159
                'end' => null,
1✔
160
            ];
1✔
161
        }
162
        $this->commentRefs[$id][$type] = $element;
1✔
163

164
        return $this;
1✔
165
    }
166

167
    /**
168
     * Get comment reference.
169
     *
170
     * @return array<string, null|AbstractElement>
171
     */
172
    protected function getCommentReference(string $id): array
173
    {
174
        if (!array_key_exists($id, $this->commentRefs)) {
1✔
175
            throw new InvalidArgumentException(sprintf('Comment with id %s isn\'t referenced in document', $id));
×
176
        }
177

178
        return $this->commentRefs[$id];
1✔
179
    }
180

181
    /**
182
     * Read w:p.
183
     *
184
     * @param \PhpOffice\PhpWord\Element\AbstractContainer $parent
185
     * @param string $docPart
186
     *
187
     * @todo Get font style for preserve text
188
     */
189
    protected function readParagraph(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart = 'document'): void
190
    {
191
        // Paragraph style
192
        $paragraphStyle = null;
19✔
193
        $headingDepth = null;
19✔
194
        if ($xmlReader->elementExists('w:commentReference', $domNode)
19✔
195
            || $xmlReader->elementExists('w:commentRangeStart', $domNode)
19✔
196
            || $xmlReader->elementExists('w:commentRangeEnd', $domNode)
19✔
197
        ) {
198
            $nodes = $xmlReader->getElements('w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode);
1✔
199
            $node = current(iterator_to_array($nodes));
1✔
200
            if ($node) {
1✔
201
                $attributeIdentifier = $node->attributes->getNamedItem('id');
1✔
202
                if ($attributeIdentifier) {
1✔
203
                    $id = $attributeIdentifier->nodeValue;
1✔
204
                }
205
            }
206
        }
207
        if ($xmlReader->elementExists('w:pPr', $domNode)) {
19✔
208
            $paragraphStyle = $this->readParagraphStyle($xmlReader, $domNode);
9✔
209
            $headingDepth = $this->getHeadingDepth($paragraphStyle);
9✔
210
        }
211

212
        // PreserveText
213
        if ($xmlReader->elementExists('w:r/w:instrText', $domNode)) {
19✔
214
            $ignoreText = false;
1✔
215
            $textContent = '';
1✔
216
            $fontStyle = $this->readFontStyle($xmlReader, $domNode);
1✔
217
            $nodes = $xmlReader->getElements('w:r', $domNode);
1✔
218
            foreach ($nodes as $node) {
1✔
219
                $instrText = $xmlReader->getValue('w:instrText', $node);
1✔
220
                if ($xmlReader->elementExists('w:fldChar', $node)) {
1✔
221
                    $fldCharType = $xmlReader->getAttribute('w:fldCharType', $node, 'w:fldChar');
1✔
222
                    if ('begin' == $fldCharType) {
1✔
223
                        $ignoreText = true;
1✔
224
                    } elseif ('end' == $fldCharType) {
1✔
225
                        $ignoreText = false;
1✔
226
                    }
227
                }
228
                if (null !== $instrText) {
1✔
229
                    $textContent .= '{' . $instrText . '}';
1✔
230
                } else {
231
                    if (false === $ignoreText) {
1✔
232
                        $textContent .= $xmlReader->getValue('w:t', $node);
1✔
233
                    }
234
                }
235
            }
236
            $parent->addPreserveText(htmlspecialchars($textContent, ENT_QUOTES, 'UTF-8'), $fontStyle, $paragraphStyle);
1✔
237
        } elseif ($xmlReader->elementExists('w:pPr/w:numPr', $domNode)) {
19✔
238
            // List item
239
            $numId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:numId');
2✔
240
            $levelId = $xmlReader->getAttribute('w:val', $domNode, 'w:pPr/w:numPr/w:ilvl');
2✔
241
            $nodes = $xmlReader->getElements('*', $domNode);
2✔
242

243
            $listItemRun = $parent->addListItemRun($levelId, "PHPWordList{$numId}", $paragraphStyle);
2✔
244

245
            foreach ($nodes as $node) {
2✔
246
                $this->readRun($xmlReader, $node, $listItemRun, $docPart, $paragraphStyle);
2✔
247
            }
248
        } elseif ($headingDepth !== null) {
18✔
249
            // Heading or Title
250
            $textContent = null;
6✔
251
            $nodes = $xmlReader->getElements('w:r|w:hyperlink', $domNode);
6✔
252
            if ($nodes->length === 1) {
6✔
253
                $textContent = htmlspecialchars($xmlReader->getValue('w:t', $nodes->item(0)), ENT_QUOTES, 'UTF-8');
2✔
254
            } else {
255
                $textContent = new TextRun($paragraphStyle);
6✔
256
                foreach ($nodes as $node) {
6✔
257
                    $this->readRun($xmlReader, $node, $textContent, $docPart, $paragraphStyle);
6✔
258
                }
259
            }
260
            $parent->addTitle($textContent, $headingDepth);
6✔
261
        } else {
262
            // Text and TextRun
263
            $textRunContainers = $xmlReader->countElements('w:r|w:ins|w:del|w:hyperlink|w:smartTag|w:commentReference|w:commentRangeStart|w:commentRangeEnd', $domNode);
16✔
264
            if (0 === $textRunContainers) {
16✔
265
                $parent->addTextBreak(null, $paragraphStyle);
5✔
266
            } else {
267
                $nodes = $xmlReader->getElements('*', $domNode);
15✔
268
                $paragraph = $parent->addTextRun($paragraphStyle);
15✔
269
                foreach ($nodes as $node) {
15✔
270
                    $this->readRun($xmlReader, $node, $paragraph, $docPart, $paragraphStyle);
15✔
271
                }
272
            }
273
        }
274
    }
275

276
    /**
277
     * Returns the depth of the Heading, returns 0 for a Title.
278
     *
279
     * @param array $paragraphStyle
280
     *
281
     * @return null|number
282
     */
283
    private function getHeadingDepth(?array $paragraphStyle = null)
284
    {
285
        if (is_array($paragraphStyle) && isset($paragraphStyle['styleName'])) {
9✔
286
            if ('Title' === $paragraphStyle['styleName']) {
7✔
287
                return 0;
4✔
288
            }
289

290
            $headingMatches = [];
3✔
291
            preg_match('/Heading(\d)/', $paragraphStyle['styleName'], $headingMatches);
3✔
292
            if (!empty($headingMatches)) {
3✔
293
                return $headingMatches[1];
2✔
294
            }
295
        }
296

297
        return null;
7✔
298
    }
299

300
    /**
301
     * Read w:r.
302
     *
303
     * @param \PhpOffice\PhpWord\Element\AbstractContainer $parent
304
     * @param string $docPart
305
     * @param mixed $paragraphStyle
306
     *
307
     * @todo Footnote paragraph style
308
     */
309
    protected function readRun(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart, $paragraphStyle = null): void
310
    {
311
        if (in_array($domNode->nodeName, ['w:ins', 'w:del', 'w:smartTag', 'w:hyperlink', 'w:commentReference'])) {
18✔
312
            $nodes = $xmlReader->getElements('*', $domNode);
4✔
313
            foreach ($nodes as $node) {
4✔
314
                $this->readRun($xmlReader, $node, $parent, $docPart, $paragraphStyle);
4✔
315
            }
316
        } elseif ($domNode->nodeName == 'w:r') {
18✔
317
            $fontStyle = $this->readFontStyle($xmlReader, $domNode);
18✔
318
            $nodes = $xmlReader->getElements('*', $domNode);
18✔
319
            foreach ($nodes as $node) {
18✔
320
                $this->readRunChild($xmlReader, $node, $parent, $docPart, $paragraphStyle, $fontStyle);
18✔
321
            }
322
        }
323

324
        if ($xmlReader->elementExists('.//*["commentReference"=local-name()]', $domNode)) {
18✔
325
            $node = iterator_to_array($xmlReader->getElements('.//*["commentReference"=local-name()]', $domNode))[0];
1✔
326
            $attributeIdentifier = $node->attributes->getNamedItem('id');
1✔
327
            if ($attributeIdentifier) {
1✔
328
                $id = $attributeIdentifier->nodeValue;
1✔
329

330
                $this->setCommentReference('start', $id, $parent->getElement($parent->countElements() - 1));
1✔
331
                $this->setCommentReference('end', $id, $parent->getElement($parent->countElements() - 1));
1✔
332
            }
333
        }
334
    }
335

336
    /**
337
     * Parses nodes under w:r.
338
     *
339
     * @param string $docPart
340
     * @param mixed $paragraphStyle
341
     * @param mixed $fontStyle
342
     */
343
    protected function readRunChild(XMLReader $xmlReader, DOMElement $node, AbstractContainer $parent, $docPart, $paragraphStyle = null, $fontStyle = null): void
344
    {
345
        $runParent = $node->parentNode->parentNode;
18✔
346
        if ($node->nodeName == 'w:footnoteReference') {
18✔
347
            // Footnote
348
            $wId = $xmlReader->getAttribute('w:id', $node);
2✔
349
            $footnote = $parent->addFootnote();
2✔
350
            $footnote->setRelationId($wId);
2✔
351
        } elseif ($node->nodeName == 'w:endnoteReference') {
18✔
352
            // Endnote
353
            $wId = $xmlReader->getAttribute('w:id', $node);
2✔
354
            $endnote = $parent->addEndnote();
2✔
355
            $endnote->setRelationId($wId);
2✔
356
        } elseif ($node->nodeName == 'w:pict') {
18✔
357
            // Image
358
            $rId = $xmlReader->getAttribute('r:id', $node, 'v:shape/v:imagedata');
1✔
359
            $target = $this->getMediaTarget($docPart, $rId);
1✔
360
            if ($this->hasImageLoading() && null !== $target) {
1✔
361
                if ('External' == $this->getTargetMode($docPart, $rId)) {
1✔
362
                    $imageSource = $target;
×
363
                } else {
364
                    $imageSource = "zip://{$this->docFile}#{$target}";
1✔
365
                }
366
                $parent->addImage($imageSource);
1✔
367
            }
368
        } elseif ($node->nodeName == 'w:drawing') {
18✔
369
            // Office 2011 Image
370
            $xmlReader->registerNamespace('wp', 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing');
4✔
371
            $xmlReader->registerNamespace('r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships');
4✔
372
            $xmlReader->registerNamespace('pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture');
4✔
373
            $xmlReader->registerNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main');
4✔
374

375
            $name = $xmlReader->getAttribute('name', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr');
4✔
376
            $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip');
4✔
377
            if ($name === null && $embedId === null) { // some Converters puts images on a different path
4✔
378
                $name = $xmlReader->getAttribute('name', $node, 'wp:anchor/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr');
×
379
                $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:anchor/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip');
×
380
            }
381
            $target = $this->getMediaTarget($docPart, $embedId);
4✔
382
            if ($this->hasImageLoading() && null !== $target) {
4✔
383
                $imageSource = "zip://{$this->docFile}#{$target}";
2✔
384
                $parent->addImage($imageSource, null, false, $name);
4✔
385
            }
386
        } elseif ($node->nodeName == 'w:object') {
17✔
387
            // Object
388
            $rId = $xmlReader->getAttribute('r:id', $node, 'o:OLEObject');
1✔
389
            // $rIdIcon = $xmlReader->getAttribute('r:id', $domNode, 'w:object/v:shape/v:imagedata');
390
            $target = $this->getMediaTarget($docPart, $rId);
1✔
391
            if (null !== $target) {
1✔
392
                $textContent = "&lt;Object: {$target}>";
1✔
393
                $parent->addText($textContent, $fontStyle, $paragraphStyle);
1✔
394
            }
395
        } elseif ($node->nodeName == 'w:br') {
17✔
396
            $parent->addTextBreak();
2✔
397
        } elseif ($node->nodeName == 'w:tab') {
17✔
398
            $parent->addText("\t");
1✔
399
        } elseif ($node->nodeName == 'mc:AlternateContent') {
17✔
400
            if ($node->hasChildNodes()) {
1✔
401
                // Get fallback instead of mc:Choice to make sure it is compatible
402
                $fallbackElements = $node->getElementsByTagName('Fallback');
1✔
403

404
                if ($fallbackElements->length) {
1✔
405
                    $fallback = $fallbackElements->item(0);
1✔
406
                    // TextRun
407
                    $textContent = htmlspecialchars($fallback->nodeValue, ENT_QUOTES, 'UTF-8');
1✔
408

409
                    $parent->addText($textContent, $fontStyle, $paragraphStyle);
1✔
410
                }
411
            }
412
        } elseif ($node->nodeName == 'w:t' || $node->nodeName == 'w:delText') {
16✔
413
            // TextRun
414
            $textContent = htmlspecialchars($xmlReader->getValue('.', $node), ENT_QUOTES, 'UTF-8');
15✔
415

416
            if ($runParent->nodeName == 'w:hyperlink') {
15✔
417
                $rId = $xmlReader->getAttribute('r:id', $runParent);
1✔
418
                $target = $this->getMediaTarget($docPart, $rId);
1✔
419
                if (null !== $target) {
1✔
420
                    $parent->addLink($target, $textContent, $fontStyle, $paragraphStyle);
1✔
421
                } else {
422
                    $parent->addText($textContent, $fontStyle, $paragraphStyle);
1✔
423
                }
424
            } else {
425
                /** @var AbstractElement $element */
426
                $element = $parent->addText($textContent, $fontStyle, $paragraphStyle);
15✔
427
                if (in_array($runParent->nodeName, ['w:ins', 'w:del'])) {
15✔
428
                    $type = ($runParent->nodeName == 'w:del') ? TrackChange::DELETED : TrackChange::INSERTED;
2✔
429
                    $author = $runParent->getAttribute('w:author');
2✔
430
                    $date = DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $runParent->getAttribute('w:date'));
2✔
431
                    $date = $date instanceof DateTime ? $date : null;
2✔
432
                    $element->setChangeInfo($type, $author, $date);
15✔
433
                }
434
            }
435
        } elseif ($node->nodeName == 'w:softHyphen') {
12✔
436
            $element = $parent->addText("\u{200c}", $fontStyle, $paragraphStyle);
×
437
        }
438
    }
439

440
    /**
441
     * Read w:tbl.
442
     *
443
     * @param mixed $parent
444
     * @param string $docPart
445
     */
446
    protected function readTable(XMLReader $xmlReader, DOMElement $domNode, $parent, $docPart = 'document'): void
447
    {
448
        // Table style
449
        $tblStyle = null;
9✔
450
        if ($xmlReader->elementExists('w:tblPr', $domNode)) {
9✔
451
            $tblStyle = $this->readTableStyle($xmlReader, $domNode);
6✔
452
        }
453

454
        /** @var \PhpOffice\PhpWord\Element\Table $table Type hint */
455
        $table = $parent->addTable($tblStyle);
9✔
456
        $tblNodes = $xmlReader->getElements('*', $domNode);
9✔
457
        foreach ($tblNodes as $tblNode) {
9✔
458
            if ('w:tblGrid' == $tblNode->nodeName) { // Column
9✔
459
                // @todo Do something with table columns
460
            } elseif ('w:tr' == $tblNode->nodeName) { // Row
9✔
461
                $rowHeight = $xmlReader->getAttribute('w:val', $tblNode, 'w:trPr/w:trHeight');
4✔
462
                $rowHRule = $xmlReader->getAttribute('w:hRule', $tblNode, 'w:trPr/w:trHeight');
4✔
463
                $rowHRule = $rowHRule == 'exact';
4✔
464
                $rowStyle = [
4✔
465
                    'tblHeader' => $xmlReader->elementExists('w:trPr/w:tblHeader', $tblNode),
4✔
466
                    'cantSplit' => $xmlReader->elementExists('w:trPr/w:cantSplit', $tblNode),
4✔
467
                    'exactHeight' => $rowHRule,
4✔
468
                ];
4✔
469

470
                $row = $table->addRow($rowHeight, $rowStyle);
4✔
471
                $rowNodes = $xmlReader->getElements('*', $tblNode);
4✔
472
                foreach ($rowNodes as $rowNode) {
4✔
473
                    if ('w:trPr' == $rowNode->nodeName) { // Row style
4✔
474
                        // @todo Do something with row style
475
                    } elseif ('w:tc' == $rowNode->nodeName) { // Cell
4✔
476
                        $cellWidth = $xmlReader->getAttribute('w:w', $rowNode, 'w:tcPr/w:tcW');
4✔
477
                        $cellStyle = null;
4✔
478
                        if ($xmlReader->elementExists('w:tcPr', $rowNode)) {
4✔
479
                            $cellStyle = $this->readCellStyle($xmlReader, $rowNode);
3✔
480
                        }
481

482
                        $cell = $row->addCell($cellWidth, $cellStyle);
4✔
483
                        $cellNodes = $xmlReader->getElements('*', $rowNode);
4✔
484
                        foreach ($cellNodes as $cellNode) {
4✔
485
                            if ('w:p' == $cellNode->nodeName) { // Paragraph
4✔
486
                                $this->readParagraph($xmlReader, $cellNode, $cell, $docPart);
2✔
487
                            } elseif ($cellNode->nodeName == 'w:tbl') { // Table
4✔
488
                                $this->readTable($xmlReader, $cellNode, $cell, $docPart);
1✔
489
                            }
490
                        }
491
                    }
492
                }
493
            }
494
        }
495
    }
496

497
    /**
498
     * Read w:pPr.
499
     *
500
     * @return null|array
501
     */
502
    protected function readParagraphStyle(XMLReader $xmlReader, DOMElement $domNode)
503
    {
504
        if (!$xmlReader->elementExists('w:pPr', $domNode)) {
10✔
505
            return null;
7✔
506
        }
507

508
        $styleNode = $xmlReader->getElement('w:pPr', $domNode);
10✔
509
        $styleDefs = [
10✔
510
            'styleName' => [self::READ_VALUE, ['w:pStyle', 'w:name']],
10✔
511
            'alignment' => [self::READ_VALUE, 'w:jc'],
10✔
512
            'basedOn' => [self::READ_VALUE, 'w:basedOn'],
10✔
513
            'next' => [self::READ_VALUE, 'w:next'],
10✔
514
            'indent' => [self::READ_VALUE, 'w:ind', 'w:left'],
10✔
515
            'hanging' => [self::READ_VALUE, 'w:ind', 'w:hanging'],
10✔
516
            'spaceAfter' => [self::READ_VALUE, 'w:spacing', 'w:after'],
10✔
517
            'spaceBefore' => [self::READ_VALUE, 'w:spacing', 'w:before'],
10✔
518
            'widowControl' => [self::READ_FALSE, 'w:widowControl'],
10✔
519
            'keepNext' => [self::READ_TRUE,  'w:keepNext'],
10✔
520
            'keepLines' => [self::READ_TRUE,  'w:keepLines'],
10✔
521
            'pageBreakBefore' => [self::READ_TRUE,  'w:pageBreakBefore'],
10✔
522
            'contextualSpacing' => [self::READ_TRUE,  'w:contextualSpacing'],
10✔
523
            'bidi' => [self::READ_TRUE,  'w:bidi'],
10✔
524
            'suppressAutoHyphens' => [self::READ_TRUE,  'w:suppressAutoHyphens'],
10✔
525
        ];
10✔
526

527
        return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
10✔
528
    }
529

530
    /**
531
     * Read w:rPr.
532
     *
533
     * @return null|array
534
     */
535
    protected function readFontStyle(XMLReader $xmlReader, DOMElement $domNode)
536
    {
537
        if (null === $domNode) {
19✔
538
            return null;
×
539
        }
540
        // Hyperlink has an extra w:r child
541
        if ('w:hyperlink' == $domNode->nodeName) {
19✔
542
            $domNode = $xmlReader->getElement('w:r', $domNode);
×
543
        }
544
        if (!$xmlReader->elementExists('w:rPr', $domNode)) {
19✔
545
            return null;
15✔
546
        }
547

548
        $styleNode = $xmlReader->getElement('w:rPr', $domNode);
13✔
549
        $styleDefs = [
13✔
550
            'styleName' => [self::READ_VALUE, 'w:rStyle'],
13✔
551
            'name' => [self::READ_VALUE, 'w:rFonts', ['w:ascii', 'w:hAnsi', 'w:eastAsia', 'w:cs']],
13✔
552
            'hint' => [self::READ_VALUE, 'w:rFonts', 'w:hint'],
13✔
553
            'size' => [self::READ_SIZE,  ['w:sz', 'w:szCs']],
13✔
554
            'color' => [self::READ_VALUE, 'w:color'],
13✔
555
            'underline' => [self::READ_VALUE, 'w:u'],
13✔
556
            'bold' => [self::READ_TRUE,  'w:b'],
13✔
557
            'italic' => [self::READ_TRUE,  'w:i'],
13✔
558
            'strikethrough' => [self::READ_TRUE,  'w:strike'],
13✔
559
            'doubleStrikethrough' => [self::READ_TRUE,  'w:dstrike'],
13✔
560
            'smallCaps' => [self::READ_TRUE,  'w:smallCaps'],
13✔
561
            'allCaps' => [self::READ_TRUE,  'w:caps'],
13✔
562
            'superScript' => [self::READ_EQUAL, 'w:vertAlign', 'w:val', 'superscript'],
13✔
563
            'subScript' => [self::READ_EQUAL, 'w:vertAlign', 'w:val', 'subscript'],
13✔
564
            'fgColor' => [self::READ_VALUE, 'w:highlight'],
13✔
565
            'rtl' => [self::READ_TRUE,  'w:rtl'],
13✔
566
            'lang' => [self::READ_VALUE, 'w:lang'],
13✔
567
            'position' => [self::READ_VALUE, 'w:position'],
13✔
568
            'hidden' => [self::READ_TRUE,  'w:vanish'],
13✔
569
        ];
13✔
570

571
        return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
13✔
572
    }
573

574
    /**
575
     * Read w:tblPr.
576
     *
577
     * @return null|array|string
578
     *
579
     * @todo Capture w:tblStylePr w:type="firstRow"
580
     */
581
    protected function readTableStyle(XMLReader $xmlReader, DOMElement $domNode)
582
    {
583
        $style = null;
10✔
584
        $margins = ['top', 'left', 'bottom', 'right'];
10✔
585
        $borders = array_merge($margins, ['insideH', 'insideV']);
10✔
586

587
        if ($xmlReader->elementExists('w:tblPr', $domNode)) {
10✔
588
            if ($xmlReader->elementExists('w:tblPr/w:tblStyle', $domNode)) {
10✔
589
                $style = $xmlReader->getAttribute('w:val', $domNode, 'w:tblPr/w:tblStyle');
×
590
            } else {
591
                $styleNode = $xmlReader->getElement('w:tblPr', $domNode);
10✔
592
                $styleDefs = [];
10✔
593
                foreach ($margins as $side) {
10✔
594
                    $ucfSide = ucfirst($side);
10✔
595
                    $styleDefs["cellMargin$ucfSide"] = [self::READ_VALUE, "w:tblCellMar/w:$side", 'w:w'];
10✔
596
                }
597
                foreach ($borders as $side) {
10✔
598
                    $ucfSide = ucfirst($side);
10✔
599
                    $styleDefs["border{$ucfSide}Size"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:sz'];
10✔
600
                    $styleDefs["border{$ucfSide}Color"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:color'];
10✔
601
                    $styleDefs["border{$ucfSide}Style"] = [self::READ_VALUE, "w:tblBorders/w:$side", 'w:val'];
10✔
602
                }
603
                $styleDefs['layout'] = [self::READ_VALUE, 'w:tblLayout', 'w:type'];
10✔
604
                $styleDefs['bidiVisual'] = [self::READ_TRUE, 'w:bidiVisual'];
10✔
605
                $styleDefs['cellSpacing'] = [self::READ_VALUE, 'w:tblCellSpacing', 'w:w'];
10✔
606
                $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
10✔
607

608
                $tablePositionNode = $xmlReader->getElement('w:tblpPr', $styleNode);
10✔
609
                if ($tablePositionNode !== null) {
10✔
610
                    $style['position'] = $this->readTablePosition($xmlReader, $tablePositionNode);
1✔
611
                }
612

613
                $indentNode = $xmlReader->getElement('w:tblInd', $styleNode);
10✔
614
                if ($indentNode !== null) {
10✔
615
                    $style['indent'] = $this->readTableIndent($xmlReader, $indentNode);
5✔
616
                }
617
            }
618
        }
619

620
        return $style;
10✔
621
    }
622

623
    /**
624
     * Read w:tblpPr.
625
     *
626
     * @return array
627
     */
628
    private function readTablePosition(XMLReader $xmlReader, DOMElement $domNode)
629
    {
630
        $styleDefs = [
1✔
631
            'leftFromText' => [self::READ_VALUE, '.', 'w:leftFromText'],
1✔
632
            'rightFromText' => [self::READ_VALUE, '.', 'w:rightFromText'],
1✔
633
            'topFromText' => [self::READ_VALUE, '.', 'w:topFromText'],
1✔
634
            'bottomFromText' => [self::READ_VALUE, '.', 'w:bottomFromText'],
1✔
635
            'vertAnchor' => [self::READ_VALUE, '.', 'w:vertAnchor'],
1✔
636
            'horzAnchor' => [self::READ_VALUE, '.', 'w:horzAnchor'],
1✔
637
            'tblpXSpec' => [self::READ_VALUE, '.', 'w:tblpXSpec'],
1✔
638
            'tblpX' => [self::READ_VALUE, '.', 'w:tblpX'],
1✔
639
            'tblpYSpec' => [self::READ_VALUE, '.', 'w:tblpYSpec'],
1✔
640
            'tblpY' => [self::READ_VALUE, '.', 'w:tblpY'],
1✔
641
        ];
1✔
642

643
        return $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
1✔
644
    }
645

646
    /**
647
     * Read w:tblInd.
648
     *
649
     * @return TblWidthComplexType
650
     */
651
    private function readTableIndent(XMLReader $xmlReader, DOMElement $domNode)
652
    {
653
        $styleDefs = [
5✔
654
            'value' => [self::READ_VALUE, '.', 'w:w'],
5✔
655
            'type' => [self::READ_VALUE, '.', 'w:type'],
5✔
656
        ];
5✔
657
        $styleDefs = $this->readStyleDefs($xmlReader, $domNode, $styleDefs);
5✔
658

659
        return new TblWidthComplexType((int) $styleDefs['value'], $styleDefs['type']);
5✔
660
    }
661

662
    /**
663
     * Read w:tcPr.
664
     *
665
     * @return null|array
666
     */
667
    private function readCellStyle(XMLReader $xmlReader, DOMElement $domNode)
668
    {
669
        $styleDefs = [
3✔
670
            'valign' => [self::READ_VALUE, 'w:vAlign'],
3✔
671
            'textDirection' => [self::READ_VALUE, 'w:textDirection'],
3✔
672
            'gridSpan' => [self::READ_VALUE, 'w:gridSpan'],
3✔
673
            'vMerge' => [self::READ_VALUE, 'w:vMerge', null, null, 'continue'],
3✔
674
            'bgColor' => [self::READ_VALUE, 'w:shd', 'w:fill'],
3✔
675
            'noWrap' => [self::READ_VALUE, 'w:noWrap', null, null, true],
3✔
676
        ];
3✔
677
        $style = null;
3✔
678

679
        if ($xmlReader->elementExists('w:tcPr', $domNode)) {
3✔
680
            $styleNode = $xmlReader->getElement('w:tcPr', $domNode);
3✔
681

682
            $borders = ['top', 'left', 'bottom', 'right'];
3✔
683
            foreach ($borders as $side) {
3✔
684
                $ucfSide = ucfirst($side);
3✔
685

686
                $styleDefs['border' . $ucfSide . 'Size'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:sz'];
3✔
687
                $styleDefs['border' . $ucfSide . 'Color'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:color'];
3✔
688
                $styleDefs['border' . $ucfSide . 'Style'] = [self::READ_VALUE, 'w:tcBorders/w:' . $side, 'w:val'];
3✔
689
            }
690

691
            $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs);
3✔
692
        }
693

694
        return $style;
3✔
695
    }
696

697
    /**
698
     * Returns the first child element found.
699
     *
700
     * @param null|array|string $elements
701
     *
702
     * @return null|string
703
     */
704
    private function findPossibleElement(XMLReader $xmlReader, ?DOMElement $parentNode = null, $elements = null)
705
    {
706
        if (is_array($elements)) {
21✔
707
            //if element is an array, we take the first element that exists in the XML
708
            foreach ($elements as $possibleElement) {
13✔
709
                if ($xmlReader->elementExists($possibleElement, $parentNode)) {
13✔
710
                    return $possibleElement;
9✔
711
                }
712
            }
713
        } else {
714
            return $elements;
21✔
715
        }
716

717
        return null;
13✔
718
    }
719

720
    /**
721
     * Returns the first attribute found.
722
     *
723
     * @param array|string $attributes
724
     *
725
     * @return null|string
726
     */
727
    private function findPossibleAttribute(XMLReader $xmlReader, DOMElement $node, $attributes)
728
    {
729
        //if attribute is an array, we take the first attribute that exists in the XML
730
        if (is_array($attributes)) {
21✔
731
            foreach ($attributes as $possibleAttribute) {
8✔
732
                if ($xmlReader->getAttribute($possibleAttribute, $node)) {
8✔
733
                    return $possibleAttribute;
4✔
734
                }
735
            }
736

737
            return null;
5✔
738
        }
739

740
        return $attributes;
21✔
741
    }
742

743
    /**
744
     * Read style definition.
745
     *
746
     * @param DOMElement $parentNode
747
     * @param array $styleDefs
748
     *
749
     * @ignoreScrutinizerPatch
750
     *
751
     * @return array
752
     */
753
    protected function readStyleDefs(XMLReader $xmlReader, ?DOMElement $parentNode = null, $styleDefs = [])
754
    {
755
        $styles = [];
21✔
756

757
        foreach ($styleDefs as $styleProp => $styleVal) {
21✔
758
            [$method, $element, $attribute, $expected, $default] = array_pad($styleVal, 5, null);
21✔
759

760
            $element = $this->findPossibleElement($xmlReader, $parentNode, $element);
21✔
761
            if ($element === null) {
21✔
762
                continue;
13✔
763
            }
764

765
            if ($xmlReader->elementExists($element, $parentNode)) {
21✔
766
                $node = $xmlReader->getElement($element, $parentNode);
21✔
767

768
                $attribute = $this->findPossibleAttribute($xmlReader, $node, $attribute);
21✔
769

770
                // Use w:val as default if no attribute assigned
771
                $attribute = ($attribute === null) ? 'w:val' : $attribute;
21✔
772
                $attributeValue = $xmlReader->getAttribute($attribute, $node) ?? $default;
21✔
773

774
                $styleValue = $this->readStyleDef($method, $attributeValue, $expected);
21✔
775
                if ($styleValue !== null) {
21✔
776
                    $styles[$styleProp] = $styleValue;
21✔
777
                }
778
            }
779
        }
780

781
        return $styles;
21✔
782
    }
783

784
    /**
785
     * Return style definition based on conversion method.
786
     *
787
     * @param string $method
788
     *
789
     * @ignoreScrutinizerPatch
790
     *
791
     * @param null|string $attributeValue
792
     * @param mixed $expected
793
     *
794
     * @return mixed
795
     */
796
    private function readStyleDef($method, $attributeValue, $expected)
797
    {
798
        $style = $attributeValue;
21✔
799

800
        if (self::READ_SIZE == $method) {
21✔
801
            $style = $attributeValue / 2;
9✔
802
        } elseif (self::READ_TRUE == $method) {
21✔
803
            $style = $this->isOn($attributeValue);
12✔
804
        } elseif (self::READ_FALSE == $method) {
18✔
805
            $style = !$this->isOn($attributeValue);
2✔
806
        } elseif (self::READ_EQUAL == $method) {
18✔
807
            $style = $attributeValue == $expected;
1✔
808
        }
809

810
        return $style;
21✔
811
    }
812

813
    /**
814
     * Parses the value of the on/off value, null is considered true as it means the w:val attribute was not present.
815
     *
816
     * @see http://www.datypic.com/sc/ooxml/t-w_ST_OnOff.html
817
     *
818
     * @param string $value
819
     *
820
     * @return bool
821
     */
822
    private function isOn($value = null)
823
    {
824
        return $value === null || $value === '1' || $value === 'true' || $value === 'on';
12✔
825
    }
826

827
    /**
828
     * Returns the target of image, object, or link as stored in ::readMainRels.
829
     *
830
     * @param string $docPart
831
     * @param string $rId
832
     *
833
     * @return null|string
834
     */
835
    private function getMediaTarget($docPart, $rId)
836
    {
837
        $target = null;
5✔
838

839
        if (isset($this->rels[$docPart], $this->rels[$docPart][$rId])) {
5✔
840
            $target = $this->rels[$docPart][$rId]['target'];
4✔
841
        }
842

843
        return $target;
5✔
844
    }
845

846
    /**
847
     * Returns the target mode.
848
     *
849
     * @param string $docPart
850
     * @param string $rId
851
     *
852
     * @return null|string
853
     */
854
    private function getTargetMode($docPart, $rId)
855
    {
856
        $mode = null;
1✔
857

858
        if (isset($this->rels[$docPart], $this->rels[$docPart][$rId])) {
1✔
859
            $mode = $this->rels[$docPart][$rId]['targetMode'];
1✔
860
        }
861

862
        return $mode;
1✔
863
    }
864
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc