Html   F
last analyzed

Complexity

Total Complexity 217

Size/Duplication

Total Lines 1260
Duplicated Lines 0 %

Test Coverage

Coverage 98.31%

Importance

Changes 0
Metric Value
wmc 217
eloc 639
dl 0
loc 1260
ccs 582
cts 592
cp 0.9831
rs 1.961
c 0
b 0
f 0

49 Methods

Rating   Name   Duplication   Size   Complexity  
A getTableStartColumn() 0 3 1
A releaseTableStartColumn() 0 5 1
A setTableStartColumn() 0 9 2
B processDomElementSpanEtc() 0 25 9
A processDomElementImg() 0 6 2
A canRead() 0 18 4
A loadSpreadsheetFromFile() 0 7 1
A processDomElementTitle() 0 14 3
A __construct() 0 4 1
A readBeginning() 0 5 1
A containsTags() 0 3 1
A readEnding() 0 20 3
B convertBoolean() 0 20 8
B flushCell() 0 45 9
A endsWithTag() 0 3 1
A processDomElementH1Etc() 0 25 6
A processDomElementLi() 0 18 5
A processDomElementBody() 0 16 3
A processDomElementA() 0 21 6
A processDomElementBr() 0 14 4
A startsWithTag() 0 3 1
A processDomElementHr() 0 10 2
A loadIntoExisting() 0 24 5
A replaceNonAsciiIfNeeded() 0 12 3
A processDomElementHeight() 0 4 2
B getStyleArray() 0 29 8
C insertImage() 0 54 12
A processDomElementThTdOther() 0 6 3
A processDomElement() 0 14 5
A getStyleColor() 0 8 2
A processDomElementTr() 0 17 4
A replaceNonAscii() 0 3 1
B processDomElementTable() 0 26 7
C loadProperties() 0 72 17
A processDomElementAlign() 0 4 2
A setBorderStyle() 0 22 3
A getBorderMappings() 0 3 1
B processDomElementThTd() 0 56 9
A processDomElementDataFormat() 0 4 2
A getSheetIndex() 0 3 1
A processDomElementWidth() 0 4 2
A loadFromString() 0 21 4
A processDomElementVAlign() 0 4 2
A processDomElementBgcolor() 0 8 2
A listWorksheetInfo() 0 17 2
A setSheetIndex() 0 5 1
A loadDocument() 0 18 2
F applyInlineStyle() 0 172 40
A getBorderStyle() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Html often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Html, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Reader;
4
5
use DOMAttr;
6
use DOMDocument;
7
use DOMElement;
8
use DOMNode;
9
use DOMText;
10
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
11
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
12
use PhpOffice\PhpSpreadsheet\Cell\DataType;
13
use PhpOffice\PhpSpreadsheet\Comment;
14
use PhpOffice\PhpSpreadsheet\Document\Properties;
15
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
16
use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension;
17
use PhpOffice\PhpSpreadsheet\Helper\Html as HelperHtml;
18
use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner;
19
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
20
use PhpOffice\PhpSpreadsheet\Spreadsheet;
21
use PhpOffice\PhpSpreadsheet\Style\Border;
22
use PhpOffice\PhpSpreadsheet\Style\Color;
23
use PhpOffice\PhpSpreadsheet\Style\Fill;
24
use PhpOffice\PhpSpreadsheet\Style\Font;
25
use PhpOffice\PhpSpreadsheet\Style\Style;
26
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
27
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
28
use Throwable;
29
30
class Html extends BaseReader
31
{
32
    /**
33
     * Sample size to read to determine if it's HTML or not.
34
     */
35
    const TEST_SAMPLE_SIZE = 2048;
36
37
    private const STARTS_WITH_BOM = '/^(?:\xfe\xff|\xff\xfe|\xEF\xBB\xBF)/';
38
39
    private const DECLARES_CHARSET = '/\bcharset=/i';
40
41
    /**
42
     * Input encoding.
43
     */
44
    protected string $inputEncoding = 'ANSI';
45
46
    /**
47
     * Sheet index to read.
48
     */
49
    protected int $sheetIndex = 0;
50
51
    /**
52
     * Formats.
53
     */
54
    protected const FORMATS = [
55
        'h1' => [
56
            'font' => [
57
                'bold' => true,
58
                'size' => 24,
59
            ],
60
        ], //    Bold, 24pt
61
        'h2' => [
62
            'font' => [
63
                'bold' => true,
64
                'size' => 18,
65
            ],
66
        ], //    Bold, 18pt
67
        'h3' => [
68
            'font' => [
69
                'bold' => true,
70
                'size' => 13.5,
71
            ],
72
        ], //    Bold, 13.5pt
73
        'h4' => [
74
            'font' => [
75
                'bold' => true,
76
                'size' => 12,
77
            ],
78
        ], //    Bold, 12pt
79
        'h5' => [
80
            'font' => [
81
                'bold' => true,
82
                'size' => 10,
83
            ],
84
        ], //    Bold, 10pt
85
        'h6' => [
86
            'font' => [
87
                'bold' => true,
88
                'size' => 7.5,
89
            ],
90
        ], //    Bold, 7.5pt
91
        'a' => [
92
            'font' => [
93
                'underline' => true,
94
                'color' => [
95
                    'argb' => Color::COLOR_BLUE,
96
                ],
97
            ],
98
        ], //    Blue underlined
99
        'hr' => [
100
            'borders' => [
101
                'bottom' => [
102
                    'borderStyle' => Border::BORDER_THIN,
103
                    'color' => [
104
                        Color::COLOR_BLACK,
105
                    ],
106
                ],
107
            ],
108
        ], //    Bottom border
109
        'strong' => [
110
            'font' => [
111
                'bold' => true,
112
            ],
113
        ], //    Bold
114
        'b' => [
115
            'font' => [
116
                'bold' => true,
117
            ],
118
        ], //    Bold
119
        'i' => [
120
            'font' => [
121
                'italic' => true,
122
            ],
123
        ], //    Italic
124
        'em' => [
125
            'font' => [
126
                'italic' => true,
127
            ],
128
        ], //    Italic
129
    ];
130
131
    /** @var array<string, bool> */
132
    protected array $rowspan = [];
133
134
    /**
135
     * Create a new HTML Reader instance.
136
     */
137 532
    public function __construct()
138
    {
139 532
        parent::__construct();
140 532
        $this->securityScanner = XmlScanner::getInstance($this);
141
    }
142
143
    /**
144
     * Validate that the current file is an HTML file.
145
     */
146 507
    public function canRead(string $filename): bool
147
    {
148
        // Check if file exists
149
        try {
150 507
            $this->openFile($filename);
151 1
        } catch (Exception) {
152 1
            return false;
153
        }
154
155 506
        $beginning = preg_replace(self::STARTS_WITH_BOM, '', $this->readBeginning()) ?? '';
156
157 506
        $startWithTag = self::startsWithTag($beginning);
158 506
        $containsTags = self::containsTags($beginning);
159 506
        $endsWithTag = self::endsWithTag($this->readEnding());
160
161 506
        fclose($this->fileHandle);
162
163 506
        return $startWithTag && $containsTags && $endsWithTag;
164
    }
165
166 506
    private function readBeginning(): string
167
    {
168 506
        fseek($this->fileHandle, 0);
169
170 506
        return (string) fread($this->fileHandle, self::TEST_SAMPLE_SIZE);
171
    }
172
173 506
    private function readEnding(): string
174
    {
175 506
        $meta = stream_get_meta_data($this->fileHandle);
176
        // Phpstan incorrectly flags following line for Php8.2-, corrected in 8.3
177 506
        $filename = $meta['uri']; //@phpstan-ignore-line
178
179 506
        clearstatcache(true, $filename);
180 506
        $size = (int) filesize($filename);
181 506
        if ($size === 0) {
182 2
            return '';
183
        }
184
185 504
        $blockSize = self::TEST_SAMPLE_SIZE;
186 504
        if ($size < $blockSize) {
187 53
            $blockSize = $size;
188
        }
189
190 504
        fseek($this->fileHandle, $size - $blockSize);
191
192 504
        return (string) fread($this->fileHandle, $blockSize);
193
    }
194
195 506
    private static function startsWithTag(string $data): bool
196
    {
197 506
        return str_starts_with(trim($data), '<');
198
    }
199
200 506
    private static function endsWithTag(string $data): bool
201
    {
202 506
        return str_ends_with(trim($data), '>');
203
    }
204
205 506
    private static function containsTags(string $data): bool
206
    {
207 506
        return strlen($data) !== strlen(strip_tags($data));
208
    }
209
210
    /**
211
     * Loads Spreadsheet from file.
212
     */
213 486
    public function loadSpreadsheetFromFile(string $filename): Spreadsheet
214
    {
215 486
        $spreadsheet = $this->newSpreadsheet();
216 486
        $spreadsheet->setValueBinder($this->valueBinder);
217
218
        // Load into this instance
219 486
        return $this->loadIntoExisting($filename, $spreadsheet);
220
    }
221
222
    /**
223
     * Data Array used for testing only, should write to
224
     * Spreadsheet object on completion of tests.
225
     *
226
     * @var mixed[][]
227
     */
228
    protected array $dataArray = [];
229
230
    protected int $tableLevel = 0;
231
232
    /** @var string[] */
233
    protected array $nestedColumn = ['A'];
234
235 504
    protected function setTableStartColumn(string $column): string
236
    {
237 504
        if ($this->tableLevel == 0) {
238 504
            $column = 'A';
239
        }
240 504
        ++$this->tableLevel;
241 504
        $this->nestedColumn[$this->tableLevel] = $column;
242
243 504
        return $this->nestedColumn[$this->tableLevel];
244
    }
245
246 500
    protected function getTableStartColumn(): string
247
    {
248 500
        return $this->nestedColumn[$this->tableLevel];
249
    }
250
251 497
    protected function releaseTableStartColumn(): string
252
    {
253 497
        --$this->tableLevel;
254
255 497
        return array_pop($this->nestedColumn) ?? '';
256
    }
257
258
    /**
259
     * Flush cell.
260
     *
261
     * @param string[] $attributeArray
262
     *
263
     * @param-out string $cellContent In one case, it can be bool
264
     */
265 505
    protected function flushCell(Worksheet $sheet, string $column, int|string $row, mixed &$cellContent, array $attributeArray): void
266
    {
267 505
        if (is_string($cellContent)) {
268
            //    Simple String content
269 505
            if (trim($cellContent) > '') {
270
                //    Only actually write it if there's content in the string
271
                //    Write to worksheet to be done here...
272
                //    ... we return the cell, so we can mess about with styles more easily
273
274
                // Set cell value explicitly if there is data-type attribute
275 483
                if (isset($attributeArray['data-type'])) {
276 5
                    $datatype = $attributeArray['data-type'];
277 5
                    if (in_array($datatype, [DataType::TYPE_STRING, DataType::TYPE_STRING2, DataType::TYPE_INLINE])) {
278
                        //Prevent to Excel treat string with beginning equal sign or convert big numbers to scientific number
279 5
                        if (str_starts_with($cellContent, '=')) {
280 1
                            $sheet->getCell($column . $row)
281 1
                                ->getStyle()
282 1
                                ->setQuotePrefix(true);
283
                        }
284
                    }
285 5
                    if ($datatype === DataType::TYPE_BOOL) {
286
                        // This is the case where we can set cellContent to bool rather than string
287 5
                        $cellContent = self::convertBoolean($cellContent); //* @phpstan-ignore-line
288 5
                        if (!is_bool($cellContent)) {
289 1
                            $attributeArray['data-type'] = DataType::TYPE_STRING;
290
                        }
291
                    }
292
293
                    //catching the Exception and ignoring the invalid data types
294
                    try {
295 5
                        $sheet->setCellValueExplicit($column . $row, $cellContent, $attributeArray['data-type']);
296 1
                    } catch (SpreadsheetException) {
297 1
                        $sheet->setCellValue($column . $row, $cellContent);
298
                    }
299
                } else {
300 482
                    $sheet->setCellValue($column . $row, $cellContent);
301
                }
302 483
                $this->dataArray[$row][$column] = $cellContent;
303
            }
304
        } else {
305
            //    We have a Rich Text run
306
            //    TODO
307
            $this->dataArray[$row][$column] = 'RICH TEXT: ' . StringHelper::convertToString($cellContent);
308
        }
309 505
        $cellContent = (string) '';
310
    }
311
312
    /** @var array<int, array<int, string>> */
313
    private static array $falseTrueArray = [];
314
315 5
    private static function convertBoolean(?string $cellContent): bool|string
316
    {
317 5
        if ($cellContent === '1') {
318 1
            return true;
319
        }
320 4
        if ($cellContent === '0' || $cellContent === '' || $cellContent === null) {
321
            return false;
322
        }
323 4
        if (empty(self::$falseTrueArray)) {
324 1
            $calc = Calculation::getInstance();
325 1
            self::$falseTrueArray = $calc->getFalseTrueArray();
326
        }
327 4
        if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[1], true)) {
328 4
            return true;
329
        }
330 4
        if (in_array(mb_strtoupper($cellContent), self::$falseTrueArray[0], true)) {
331 4
            return false;
332
        }
333
334 1
        return $cellContent;
335
    }
336
337 505
    private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void
338
    {
339 505
        $attributeArray = [];
340
        /** @var DOMAttr $attribute */
341 505
        foreach (($child->attributes ?? []) as $attribute) {
342 491
            $attributeArray[$attribute->name] = $attribute->value;
343
        }
344
345 505
        if ($child->nodeName === 'body') {
346 505
            $row = 1;
347 505
            $column = 'A';
348 505
            $cellContent = '';
349 505
            $this->tableLevel = 0;
350 505
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
351
        } else {
352 505
            $this->processDomElementTitle($sheet, $row, $column, $cellContent, $child, $attributeArray);
353
        }
354
    }
355
356
    /** @param string[] $attributeArray */
357 505
    private function processDomElementTitle(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
358
    {
359 505
        if ($child->nodeName === 'title') {
360 459
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
361
362
            try {
363 459
                $sheet->setTitle($cellContent, true, true);
364 456
                $sheet->getParent()?->getProperties()?->setTitle($cellContent);
365 3
            } catch (SpreadsheetException) {
366
                // leave default title if too long or illegal chars
367
            }
368 459
            $cellContent = '';
369
        } else {
370 505
            $this->processDomElementSpanEtc($sheet, $row, $column, $cellContent, $child, $attributeArray);
371
        }
372
    }
373
374
    private const SPAN_ETC = ['span', 'div', 'font', 'i', 'em', 'strong', 'b'];
375
376
    /** @param string[] $attributeArray */
377 505
    private function processDomElementSpanEtc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
378
    {
379 505
        if (in_array((string) $child->nodeName, self::SPAN_ETC, true)) {
380 444
            if (isset($attributeArray['class']) && $attributeArray['class'] === 'comment') {
381 9
                $sheet->getComment($column . $row)
382 9
                    ->getText()
383 9
                    ->createTextRun($child->textContent);
384 9
                if (isset($attributeArray['dir']) && $attributeArray['dir'] === 'rtl') {
385 1
                    $sheet->getComment($column . $row)->setTextboxDirection(Comment::TEXTBOX_DIRECTION_RTL);
386
                }
387 9
                if (isset($attributeArray['style'])) {
388 2
                    $alignStyle = $attributeArray['style'];
389 2
                    if (preg_match('/\btext-align:\s*(left|right|center|justify)\b/', (string) $alignStyle, $matches) === 1) {
390 2
                        $sheet->getComment($column . $row)->setAlignment($matches[1]);
391
                    }
392
                }
393
            } else {
394 444
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
395
            }
396
397 444
            if (isset(self::FORMATS[$child->nodeName])) {
398 2
                $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
399
            }
400
        } else {
401 505
            $this->processDomElementHr($sheet, $row, $column, $cellContent, $child, $attributeArray);
402
        }
403
    }
404
405
    /** @param string[] $attributeArray */
406 505
    private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
407
    {
408 505
        if ($child->nodeName === 'hr') {
409 1
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
410 1
            ++$row;
411 1
            $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
412 1
            ++$row;
413
        }
414
        // fall through to br
415 505
        $this->processDomElementBr($sheet, $row, $column, $cellContent, $child, $attributeArray);
416
    }
417
418
    /** @param string[] $attributeArray */
419 505
    private function processDomElementBr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
420
    {
421 505
        if ($child->nodeName === 'br' || $child->nodeName === 'hr') {
422 4
            if ($this->tableLevel > 0) {
423
                //    If we're inside a table, replace with a newline and set the cell to wrap
424 4
                $cellContent .= "\n";
425 4
                $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
426
            } else {
427
                //    Otherwise flush our existing content and move the row cursor on
428 1
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
429 1
                ++$row;
430
            }
431
        } else {
432 505
            $this->processDomElementA($sheet, $row, $column, $cellContent, $child, $attributeArray);
433
        }
434
    }
435
436
    /** @param string[] $attributeArray */
437 505
    private function processDomElementA(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
438
    {
439 505
        if ($child->nodeName === 'a') {
440 12
            foreach ($attributeArray as $attributeName => $attributeValue) {
441
                switch ($attributeName) {
442 12
                    case 'href':
443 3
                        $sheet->getCell($column . $row)->getHyperlink()->setUrl($attributeValue);
444 3
                        $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
445
446 3
                        break;
447 10
                    case 'class':
448 9
                        if ($attributeValue === 'comment-indicator') {
449 9
                            break; // Ignore - it's just a red square.
450
                        }
451
                }
452
            }
453
            // no idea why this should be needed
454
            //$cellContent .= ' ';
455 12
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
456
        } else {
457 505
            $this->processDomElementH1Etc($sheet, $row, $column, $cellContent, $child, $attributeArray);
458
        }
459
    }
460
461
    private const H1_ETC = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ol', 'ul', 'p'];
462
463
    /** @param string[] $attributeArray */
464 505
    private function processDomElementH1Etc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
465
    {
466 505
        if (in_array((string) $child->nodeName, self::H1_ETC, true)) {
467 2
            if ($this->tableLevel > 0) {
468
                //    If we're inside a table, replace with a newline
469 1
                $cellContent .= $cellContent ? "\n" : '';
470 1
                $sheet->getStyle($column . $row)->getAlignment()->setWrapText(true);
471 1
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
472
            } else {
473 2
                if ($cellContent > '') {
474 1
                    $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
475 1
                    ++$row;
476
                }
477 2
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
478 2
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
479
480 2
                if (isset(self::FORMATS[$child->nodeName])) {
481 1
                    $sheet->getStyle($column . $row)->applyFromArray(self::FORMATS[$child->nodeName]);
482
                }
483
484 2
                ++$row;
485 2
                $column = 'A';
486
            }
487
        } else {
488 505
            $this->processDomElementLi($sheet, $row, $column, $cellContent, $child, $attributeArray);
489
        }
490
    }
491
492
    /** @param string[] $attributeArray */
493 505
    private function processDomElementLi(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
494
    {
495 505
        if ($child->nodeName === 'li') {
496 2
            if ($this->tableLevel > 0) {
497
                //    If we're inside a table, replace with a newline
498 1
                $cellContent .= $cellContent ? "\n" : '';
499 1
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
500
            } else {
501 2
                if ($cellContent > '') {
502 1
                    $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
503
                }
504 2
                ++$row;
505 2
                $this->processDomElement($child, $sheet, $row, $column, $cellContent);
506 2
                $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
507 2
                $column = 'A';
508
            }
509
        } else {
510 505
            $this->processDomElementImg($sheet, $row, $column, $cellContent, $child, $attributeArray);
511
        }
512
    }
513
514
    /** @param string[] $attributeArray */
515 505
    private function processDomElementImg(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
516
    {
517 505
        if ($child->nodeName === 'img') {
518 19
            $this->insertImage($sheet, $column, $row, $attributeArray);
519
        } else {
520 505
            $this->processDomElementTable($sheet, $row, $column, $cellContent, $child, $attributeArray);
521
        }
522
    }
523
524
    private string $currentColumn = 'A';
525
526
    /** @param string[] $attributeArray */
527 505
    private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
528
    {
529 505
        if ($child->nodeName === 'table') {
530 504
            if (isset($attributeArray['class'])) {
531 447
                $classes = explode(' ', $attributeArray['class']);
532 447
                $sheet->setShowGridlines(in_array('gridlines', $classes, true));
533 447
                $sheet->setPrintGridlines(in_array('gridlinesp', $classes, true));
534
            }
535 504
            if ('rtl' === ($attributeArray['dir'] ?? '')) {
536 2
                $sheet->setRightToLeft(true);
537
            }
538 504
            $this->currentColumn = 'A';
539 504
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
540 504
            $column = $this->setTableStartColumn($column);
541 504
            if ($this->tableLevel > 1 && $row > 1) {
542 2
                --$row;
543
            }
544 504
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
545 497
            $column = $this->releaseTableStartColumn();
546 497
            if ($this->tableLevel > 1) {
547 2
                ++$column; //* @phpstan-ignore-line
548
            } else {
549 497
                ++$row;
550
            }
551
        } else {
552 505
            $this->processDomElementTr($sheet, $row, $column, $cellContent, $child, $attributeArray);
553
        }
554
    }
555
556
    /** @param string[] $attributeArray */
557 505
    private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
558
    {
559 505
        if ($child->nodeName === 'col') {
560 442
            $this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray);
561 442
            ++$this->currentColumn;
562 505
        } elseif ($child->nodeName === 'tr') {
563 500
            $column = $this->getTableStartColumn();
564 500
            $cellContent = '';
565 500
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
566
567 493
            if (isset($attributeArray['height'])) {
568 1
                $sheet->getRowDimension($row)->setRowHeight((float) $attributeArray['height']);
569
            }
570
571 493
            ++$row;
572
        } else {
573 505
            $this->processDomElementThTdOther($sheet, $row, $column, $cellContent, $child, $attributeArray);
574
        }
575
    }
576
577
    /** @param string[] $attributeArray */
578 505
    private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
579
    {
580 505
        if ($child->nodeName !== 'td' && $child->nodeName !== 'th') {
581 505
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
582
        } else {
583 500
            $this->processDomElementThTd($sheet, $row, $column, $cellContent, $child, $attributeArray);
584
        }
585
    }
586
587
    /** @param string[] $attributeArray */
588 493
    private function processDomElementBgcolor(Worksheet $sheet, int $row, string $column, array $attributeArray): void
589
    {
590 493
        if (isset($attributeArray['bgcolor'])) {
591 1
            $sheet->getStyle("$column$row")->applyFromArray(
592 1
                [
593 1
                    'fill' => [
594 1
                        'fillType' => Fill::FILL_SOLID,
595 1
                        'color' => ['rgb' => $this->getStyleColor($attributeArray['bgcolor'])],
596 1
                    ],
597 1
                ]
598 1
            );
599
        }
600
    }
601
602
    /** @param string[] $attributeArray */
603 493
    private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void
604
    {
605 493
        if (isset($attributeArray['width'])) {
606 1
            $sheet->getColumnDimension($column)->setWidth((new CssDimension($attributeArray['width']))->width());
607
        }
608
    }
609
610
    /** @param string[] $attributeArray */
611 493
    private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void
612
    {
613 493
        if (isset($attributeArray['height'])) {
614 1
            $sheet->getRowDimension($row)->setRowHeight((new CssDimension($attributeArray['height']))->height());
615
        }
616
    }
617
618
    /** @param string[] $attributeArray */
619 493
    private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
620
    {
621 493
        if (isset($attributeArray['align'])) {
622 1
            $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']);
623
        }
624
    }
625
626
    /** @param string[] $attributeArray */
627 493
    private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
628
    {
629 493
        if (isset($attributeArray['valign'])) {
630 1
            $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']);
631
        }
632
    }
633
634
    /** @param string[] $attributeArray */
635 493
    private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void
636
    {
637 493
        if (isset($attributeArray['data-format'])) {
638 1
            $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']);
639
        }
640
    }
641
642
    /** @param string[] $attributeArray */
643 500
    private function processDomElementThTd(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
644
    {
645 500
        while (isset($this->rowspan[$column . $row])) {
646 3
            $temp = (string) $column;
647 3
            ++$temp;
648 3
            $column = (string) $temp;
649
        }
650 500
        $this->processDomElement($child, $sheet, $row, $column, $cellContent); // ++$column above confuses Phpstan
651
652
        // apply inline style
653 493
        $this->applyInlineStyle($sheet, $row, $column, $attributeArray);
654
655
        /** @var string $cellContent */
656 493
        $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
657
658 493
        $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray);
659 493
        $this->processDomElementWidth($sheet, $column, $attributeArray);
660 493
        $this->processDomElementHeight($sheet, $row, $attributeArray);
661 493
        $this->processDomElementAlign($sheet, $row, $column, $attributeArray);
662 493
        $this->processDomElementVAlign($sheet, $row, $column, $attributeArray);
663 493
        $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray);
664
665 493
        if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
666
            //create merging rowspan and colspan
667 2
            $columnTo = $column;
668 2
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
669
                /** @var string $columnTo */
670 2
                ++$columnTo;
671
            }
672 2
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
673 2
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
674 2
                $this->rowspan[$value] = true;
675
            }
676 2
            $sheet->mergeCells($range);
677
            //* @phpstan-ignore-next-line
678 2
            $column = $columnTo; // ++$columnTo above confuses phpstan
679 493
        } elseif (isset($attributeArray['rowspan'])) {
680
            //create merging rowspan
681 3
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
682 3
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
683 3
                $this->rowspan[$value] = true;
684
            }
685 3
            $sheet->mergeCells($range);
686 493
        } elseif (isset($attributeArray['colspan'])) {
687
            //create merging colspan
688 3
            $columnTo = $column;
689 3
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
690
                /** @var string $columnTo */
691 3
                ++$columnTo;
692
            }
693 3
            $sheet->mergeCells($column . $row . ':' . $columnTo . $row);
694
            //* @phpstan-ignore-next-line
695 3
            $column = $columnTo; // ++$columnTo above confuses phpstan
696
        }
697
698 493
        ++$column; //* @phpstan-ignore-line
699
    }
700
701 505
    protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void
702
    {
703 505
        foreach ($element->childNodes as $child) {
704 505
            if ($child instanceof DOMText) {
705 502
                $domText = (string) preg_replace('/\s+/', ' ', trim($child->nodeValue ?? ''));
706 502
                if ($domText === "\u{a0}") {
707 8
                    $domText = '';
708
                }
709
                //    simply append the text if the cell content is a plain text string
710 502
                $cellContent .= $domText;
711
                //    but if we have a rich text run instead, we need to append it correctly
712
                //    TODO
713 505
            } elseif ($child instanceof DOMElement) {
714 505
                $this->processDomElementBody($sheet, $row, $column, $cellContent, $child);
715
            }
716
        }
717
    }
718
719
    /**
720
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
721
     */
722 486
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
723
    {
724
        // Validate
725 486
        if (!$this->canRead($filename)) {
726 1
            throw new Exception($filename . ' is an Invalid HTML file.');
727
        }
728
729
        // Create a new DOM object
730 485
        $dom = new DOMDocument();
731
732
        // Reload the HTML file into the DOM object
733
        try {
734 485
            $convert = $this->getSecurityScannerOrThrow()->scanFile($filename);
735 484
            $convert = self::replaceNonAsciiIfNeeded($convert);
736 484
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
737 2
        } catch (Throwable $e) {
738 2
            $loaded = false;
739
        }
740 485
        if ($loaded === false) {
741 2
            throw new Exception('Failed to load ' . $filename . ' as a DOM Document', 0, $e ?? null);
742
        }
743 483
        self::loadProperties($dom, $spreadsheet);
744
745 483
        return $this->loadDocument($dom, $spreadsheet);
746
    }
747
748 505
    private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void
749
    {
750 505
        $properties = $spreadsheet->getProperties();
751 505
        foreach ($dom->getElementsByTagName('meta') as $meta) {
752 452
            $metaContent = (string) $meta->getAttribute('content');
753 452
            if ($metaContent !== '') {
754 444
                $metaName = (string) $meta->getAttribute('name');
755
                switch ($metaName) {
756 444
                    case 'author':
757 442
                        $properties->setCreator($metaContent);
758
759 442
                        break;
760 444
                    case 'category':
761 1
                        $properties->setCategory($metaContent);
762
763 1
                        break;
764 444
                    case 'company':
765 1
                        $properties->setCompany($metaContent);
766
767 1
                        break;
768 444
                    case 'created':
769 442
                        $properties->setCreated($metaContent);
770
771 442
                        break;
772 444
                    case 'description':
773 1
                        $properties->setDescription($metaContent);
774
775 1
                        break;
776 444
                    case 'keywords':
777 1
                        $properties->setKeywords($metaContent);
778
779 1
                        break;
780 444
                    case 'lastModifiedBy':
781 442
                        $properties->setLastModifiedBy($metaContent);
782
783 442
                        break;
784 444
                    case 'manager':
785 1
                        $properties->setManager($metaContent);
786
787 1
                        break;
788 444
                    case 'modified':
789 442
                        $properties->setModified($metaContent);
790
791 442
                        break;
792 444
                    case 'subject':
793 1
                        $properties->setSubject($metaContent);
794
795 1
                        break;
796 444
                    case 'title':
797 440
                        $properties->setTitle($metaContent);
798
799 440
                        break;
800 444
                    case 'viewport':
801 1
                        $properties->setViewport($metaContent);
802
803 1
                        break;
804
                    default:
805 444
                        if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) {
806 1
                            match ($matches[1]) {
807 1
                                'bool' => $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN),
808 1
                                'float' => $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT),
809 1
                                'int' => $properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER),
810 1
                                'date' => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE),
811
                                // string
812 1
                                default => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING),
813 1
                            };
814
                        }
815
                }
816
            }
817
        }
818 505
        if (!empty($dom->baseURI)) {
819 1
            $properties->setHyperlinkBase($dom->baseURI);
820
        }
821
    }
822
823
    /** @param string[] $matches */
824 21
    private static function replaceNonAscii(array $matches): string
825
    {
826 21
        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
827
    }
828
829 506
    private static function replaceNonAsciiIfNeeded(string $convert): ?string
830
    {
831 506
        if (preg_match(self::STARTS_WITH_BOM, $convert) !== 1 && preg_match(self::DECLARES_CHARSET, $convert) !== 1) {
832 53
            $lowend = "\u{80}";
833 53
            $highend = "\u{10ffff}";
834 53
            $regexp = "/[$lowend-$highend]/u";
835
            /** @var callable $callback */
836 53
            $callback = [self::class, 'replaceNonAscii'];
837 53
            $convert = preg_replace_callback($regexp, $callback, $convert);
838
        }
839
840 506
        return $convert;
841
    }
842
843
    /**
844
     * Spreadsheet from content.
845
     */
846 22
    public function loadFromString(string $content, ?Spreadsheet $spreadsheet = null): Spreadsheet
847
    {
848
        //    Create a new DOM object
849 22
        $dom = new DOMDocument();
850
851
        //    Reload the HTML file into the DOM object
852
        try {
853 22
            $convert = $this->getSecurityScannerOrThrow()->scan($content);
854 22
            $convert = self::replaceNonAsciiIfNeeded($convert);
855 22
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
856
        } catch (Throwable $e) {
857
            $loaded = false;
858
        }
859 22
        if ($loaded === false) {
860
            throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
861
        }
862 22
        $spreadsheet = $spreadsheet ?? $this->newSpreadsheet();
863 22
        $spreadsheet->setValueBinder($this->valueBinder);
864 22
        self::loadProperties($dom, $spreadsheet);
865
866 22
        return $this->loadDocument($dom, $spreadsheet);
867
    }
868
869
    /**
870
     * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance.
871
     */
872 505
    private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet
873
    {
874 505
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
875 2
            $spreadsheet->createSheet();
876
        }
877 505
        $spreadsheet->setActiveSheetIndex($this->sheetIndex);
878
879
        // Discard white space
880 505
        $document->preserveWhiteSpace = false;
881
882 505
        $row = 0;
883 505
        $column = 'A';
884 505
        $content = '';
885 505
        $this->rowspan = [];
886 505
        $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content);
887
888
        // Return
889 498
        return $spreadsheet;
890
    }
891
892
    /**
893
     * Get sheet index.
894
     */
895 1
    public function getSheetIndex(): int
896
    {
897 1
        return $this->sheetIndex;
898
    }
899
900
    /**
901
     * Set sheet index.
902
     *
903
     * @param int $sheetIndex Sheet index
904
     *
905
     * @return $this
906
     */
907 2
    public function setSheetIndex(int $sheetIndex): static
908
    {
909 2
        $this->sheetIndex = $sheetIndex;
910
911 2
        return $this;
912
    }
913
914
    /**
915
     * Apply inline css inline style.
916
     *
917
     * NOTES :
918
     * Currently only intended for td & th element,
919
     * and only takes 'background-color' and 'color'; property with HEX color
920
     *
921
     * TODO :
922
     * - Implement to other propertie, such as border
923
     *
924
     * @param string[] $attributeArray
925
     */
926 493
    private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, array $attributeArray): void
927
    {
928 493
        if (!isset($attributeArray['style'])) {
929 487
            return;
930
        }
931
932 16
        if ($row <= 0 || $column === '') {
933 1
            $cellStyle = new Style();
934 16
        } elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
935 1
            $columnTo = $column;
936 1
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
937
                /** @var string $columnTo */
938 1
                ++$columnTo;
939
            }
940 1
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
941 1
            $cellStyle = $sheet->getStyle($range);
942 16
        } elseif (isset($attributeArray['rowspan'])) {
943 1
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
944 1
            $cellStyle = $sheet->getStyle($range);
945 16
        } elseif (isset($attributeArray['colspan'])) {
946 1
            $columnTo = $column;
947 1
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
948
                /** @var string $columnTo */
949 1
                ++$columnTo;
950
            }
951 1
            $range = $column . $row . ':' . $columnTo . $row;
952 1
            $cellStyle = $sheet->getStyle($range);
953
        } else {
954 16
            $cellStyle = $sheet->getStyle($column . $row);
955
        }
956
957
        // add color styles (background & text) from dom element,currently support : td & th, using ONLY inline css style with RGB color
958 16
        $styles = explode(';', $attributeArray['style']);
959 16
        foreach ($styles as $st) {
960 16
            $value = explode(':', $st);
961 16
            $styleName = trim($value[0]);
962 16
            $styleValue = isset($value[1]) ? trim($value[1]) : null;
963 16
            $styleValueString = (string) $styleValue;
964
965 16
            if (!$styleName) {
966 12
                continue;
967
            }
968
969
            switch ($styleName) {
970 16
                case 'background':
971 16
                case 'background-color':
972 3
                    $styleColor = $this->getStyleColor($styleValueString);
973
974 3
                    if (!$styleColor) {
975 1
                        continue 2;
976
                    }
977
978 3
                    $cellStyle->applyFromArray(['fill' => ['fillType' => Fill::FILL_SOLID, 'color' => ['rgb' => $styleColor]]]);
979
980 3
                    break;
981 16
                case 'color':
982 3
                    $styleColor = $this->getStyleColor($styleValueString);
983
984 3
                    if (!$styleColor) {
985 1
                        continue 2;
986
                    }
987
988 3
                    $cellStyle->applyFromArray(['font' => ['color' => ['rgb' => $styleColor]]]);
989
990 3
                    break;
991
992 13
                case 'border':
993 3
                    $this->setBorderStyle($cellStyle, $styleValueString, 'allBorders');
994
995 3
                    break;
996
997 11
                case 'border-top':
998 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'top');
999
1000 1
                    break;
1001
1002 11
                case 'border-bottom':
1003 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'bottom');
1004
1005 1
                    break;
1006
1007 11
                case 'border-left':
1008 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'left');
1009
1010 1
                    break;
1011
1012 11
                case 'border-right':
1013 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'right');
1014
1015 1
                    break;
1016
1017 10
                case 'font-size':
1018 1
                    $cellStyle->getFont()->setSize(
1019 1
                        (float) $styleValue
1020 1
                    );
1021
1022 1
                    break;
1023
1024 10
                case 'font-weight':
1025 1
                    if ($styleValue === 'bold' || $styleValue >= 500) {
1026 1
                        $cellStyle->getFont()->setBold(true);
1027
                    }
1028
1029 1
                    break;
1030
1031 10
                case 'font-style':
1032 1
                    if ($styleValue === 'italic') {
1033 1
                        $cellStyle->getFont()->setItalic(true);
1034
                    }
1035
1036 1
                    break;
1037
1038 10
                case 'font-family':
1039 1
                    $cellStyle->getFont()->setName(str_replace('\'', '', $styleValueString));
1040
1041 1
                    break;
1042
1043 10
                case 'text-decoration':
1044
                    switch ($styleValue) {
1045 1
                        case 'underline':
1046 1
                            $cellStyle->getFont()->setUnderline(Font::UNDERLINE_SINGLE);
1047
1048 1
                            break;
1049 1
                        case 'line-through':
1050 1
                            $cellStyle->getFont()->setStrikethrough(true);
1051
1052 1
                            break;
1053
                    }
1054
1055 1
                    break;
1056
1057 9
                case 'text-align':
1058 1
                    $cellStyle->getAlignment()->setHorizontal($styleValueString);
1059
1060 1
                    break;
1061
1062 9
                case 'vertical-align':
1063 2
                    $cellStyle->getAlignment()->setVertical($styleValueString);
1064
1065 2
                    break;
1066
1067 9
                case 'width':
1068 2
                    if ($column !== '') {
1069 2
                        $sheet->getColumnDimension($column)->setWidth(
1070 2
                            (new CssDimension($styleValue ?? ''))->width()
1071 2
                        );
1072
                    }
1073
1074 2
                    break;
1075
1076 7
                case 'height':
1077 1
                    if ($row > 0) {
1078 1
                        $sheet->getRowDimension($row)->setRowHeight(
1079 1
                            (new CssDimension($styleValue ?? ''))->height()
1080 1
                        );
1081
                    }
1082
1083 1
                    break;
1084
1085 6
                case 'word-wrap':
1086 1
                    $cellStyle->getAlignment()->setWrapText(
1087 1
                        $styleValue === 'break-word'
1088 1
                    );
1089
1090 1
                    break;
1091
1092 6
                case 'text-indent':
1093 2
                    $cellStyle->getAlignment()->setIndent(
1094 2
                        (int) str_replace(['px'], '', $styleValueString)
1095 2
                    );
1096
1097 2
                    break;
1098
            }
1099
        }
1100
    }
1101
1102
    /**
1103
     * Check if has #, so we can get clean hex.
1104
     */
1105 7
    public function getStyleColor(?string $value): string
1106
    {
1107 7
        $value = (string) $value;
1108 7
        if (str_starts_with($value, '#')) {
1109 5
            return substr($value, 1);
1110
        }
1111
1112 4
        return HelperHtml::colourNameLookup($value);
1113
    }
1114
1115
    /** @param string[] $attributes */
1116 19
    private function insertImage(Worksheet $sheet, string $column, int $row, array $attributes): void
1117
    {
1118 19
        if (!isset($attributes['src'])) {
1119 1
            return;
1120
        }
1121 18
        $styleArray = self::getStyleArray($attributes);
1122
1123 18
        $src = $attributes['src'];
1124 18
        if (substr($src, 0, 5) !== 'data:') {
1125 14
            $src = urldecode($src);
1126
        }
1127 18
        $width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null);
1128 18
        $height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null);
1129 18
        $name = $attributes['alt'] ?? null;
1130
1131 18
        $drawing = new Drawing();
1132 18
        $drawing->setPath($src, false, allowExternal: $this->allowExternalImages);
1133 11
        if ($drawing->getPath() === '') {
1134 3
            return;
1135
        }
1136 8
        $drawing->setWorksheet($sheet);
1137 8
        $drawing->setCoordinates($column . $row);
1138 8
        $drawing->setOffsetX(0);
1139 8
        $drawing->setOffsetY(10);
1140 8
        $drawing->setResizeProportional(true);
1141
1142 8
        if ($name) {
1143 7
            $drawing->setName($name);
1144
        }
1145
1146
        /** @var null|scalar $width */
1147
        /** @var null|scalar $height */
1148 8
        if ($width) {
1149 5
            if ($height) {
1150 2
                $drawing->setWidthAndHeight((int) $width, (int) $height);
1151
            } else {
1152 3
                $drawing->setWidth((int) $width);
1153
            }
1154 3
        } elseif ($height) {
1155 1
            $drawing->setHeight((int) $height);
1156
        }
1157
1158 8
        $sheet->getColumnDimension($column)->setWidth(
1159 8
            $drawing->getWidth() / 6
1160 8
        );
1161
1162 8
        $sheet->getRowDimension($row)->setRowHeight(
1163 8
            $drawing->getHeight() * 0.9
1164 8
        );
1165
1166 8
        if (isset($styleArray['opacity'])) {
1167
            $opacity = $styleArray['opacity'];
1168
            if (is_numeric($opacity)) {
1169
                $drawing->setOpacity((int) ($opacity * 100000));
1170
            }
1171
        }
1172
    }
1173
1174
    /**
1175
     * @param string[] $attributes
1176
     *
1177
     * @return mixed[]
1178
     */
1179 18
    private static function getStyleArray(array $attributes): array
1180
    {
1181 18
        $styleArray = [];
1182 18
        if (isset($attributes['style'])) {
1183 4
            $styles = explode(';', $attributes['style']);
1184 4
            foreach ($styles as $style) {
1185 4
                $value = explode(':', $style);
1186 4
                if (count($value) === 2) {
1187 4
                    $arrayKey = trim($value[0]);
1188 4
                    $arrayValue = trim($value[1]);
1189 4
                    if ($arrayKey === 'width') {
1190 4
                        if (substr($arrayValue, -2) === 'px') {
1191 4
                            $arrayValue = (string) (((float) substr($arrayValue, 0, -2)));
1192
                        } else {
1193
                            $arrayValue = (new CssDimension($arrayValue))->width();
1194
                        }
1195 4
                    } elseif ($arrayKey === 'height') {
1196 2
                        if (substr($arrayValue, -2) === 'px') {
1197 2
                            $arrayValue = substr($arrayValue, 0, -2);
1198
                        } else {
1199
                            $arrayValue = (new CssDimension($arrayValue))->height();
1200
                        }
1201
                    }
1202 4
                    $styleArray[$arrayKey] = $arrayValue;
1203
                }
1204
            }
1205
        }
1206
1207 18
        return $styleArray;
1208
    }
1209
1210
    private const BORDER_MAPPINGS = [
1211
        'dash-dot' => Border::BORDER_DASHDOT,
1212
        'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1213
        'dashed' => Border::BORDER_DASHED,
1214
        'dotted' => Border::BORDER_DOTTED,
1215
        'double' => Border::BORDER_DOUBLE,
1216
        'hair' => Border::BORDER_HAIR,
1217
        'medium' => Border::BORDER_MEDIUM,
1218
        'medium-dashed' => Border::BORDER_MEDIUMDASHED,
1219
        'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT,
1220
        'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT,
1221
        'none' => Border::BORDER_NONE,
1222
        'slant-dash-dot' => Border::BORDER_SLANTDASHDOT,
1223
        'solid' => Border::BORDER_THIN,
1224
        'thick' => Border::BORDER_THICK,
1225
    ];
1226
1227
    /** @return array<string, string> */
1228 15
    public static function getBorderMappings(): array
1229
    {
1230 15
        return self::BORDER_MAPPINGS;
1231
    }
1232
1233
    /**
1234
     * Map html border style to PhpSpreadsheet border style.
1235
     */
1236 3
    public function getBorderStyle(string $style): ?string
1237
    {
1238 3
        return self::BORDER_MAPPINGS[$style] ?? null;
1239
    }
1240
1241 3
    private function setBorderStyle(Style $cellStyle, string $styleValue, string $type): void
1242
    {
1243 3
        if (trim($styleValue) === Border::BORDER_NONE) {
1244 1
            $borderStyle = Border::BORDER_NONE;
1245 1
            $color = null;
1246
        } else {
1247 3
            $borderArray = explode(' ', $styleValue);
1248 3
            $borderCount = count($borderArray);
1249 3
            if ($borderCount >= 3) {
1250 3
                $borderStyle = $borderArray[1];
1251 3
                $color = $borderArray[2];
1252
            } else {
1253 1
                $borderStyle = $borderArray[0];
1254 1
                $color = $borderArray[1] ?? null;
1255
            }
1256
        }
1257
1258 3
        $cellStyle->applyFromArray([
1259 3
            'borders' => [
1260 3
                $type => [
1261 3
                    'borderStyle' => $this->getBorderStyle($borderStyle),
1262 3
                    'color' => ['rgb' => $this->getStyleColor($color)],
1263 3
                ],
1264 3
            ],
1265 3
        ]);
1266
    }
1267
1268
    /**
1269
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
1270
     *
1271
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
1272
     */
1273 1
    public function listWorksheetInfo(string $filename): array
1274
    {
1275 1
        $info = [];
1276 1
        $spreadsheet = $this->newSpreadsheet();
1277 1
        $this->loadIntoExisting($filename, $spreadsheet);
1278 1
        foreach ($spreadsheet->getAllSheets() as $sheet) {
1279 1
            $newEntry = ['worksheetName' => $sheet->getTitle()];
1280 1
            $newEntry['lastColumnLetter'] = $sheet->getHighestDataColumn();
1281 1
            $newEntry['lastColumnIndex'] = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()) - 1;
1282 1
            $newEntry['totalRows'] = $sheet->getHighestDataRow();
1283 1
            $newEntry['totalColumns'] = $newEntry['lastColumnIndex'] + 1;
1284 1
            $newEntry['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
1285 1
            $info[] = $newEntry;
1286
        }
1287 1
        $spreadsheet->disconnectWorksheets();
1288
1289 1
        return $info;
1290
    }
1291
}
1292