Html::releaseTableStartColumn()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 525
    public function __construct()
138
    {
139 525
        parent::__construct();
140 525
        $this->securityScanner = XmlScanner::getInstance($this);
141
    }
142
143
    /**
144
     * Validate that the current file is an HTML file.
145
     */
146 503
    public function canRead(string $filename): bool
147
    {
148
        // Check if file exists
149
        try {
150 503
            $this->openFile($filename);
151 1
        } catch (Exception) {
152 1
            return false;
153
        }
154
155 502
        $beginning = preg_replace(self::STARTS_WITH_BOM, '', $this->readBeginning()) ?? '';
156
157 502
        $startWithTag = self::startsWithTag($beginning);
158 502
        $containsTags = self::containsTags($beginning);
159 502
        $endsWithTag = self::endsWithTag($this->readEnding());
160
161 502
        fclose($this->fileHandle);
162
163 502
        return $startWithTag && $containsTags && $endsWithTag;
164
    }
165
166 502
    private function readBeginning(): string
167
    {
168 502
        fseek($this->fileHandle, 0);
169
170 502
        return (string) fread($this->fileHandle, self::TEST_SAMPLE_SIZE);
171
    }
172
173 502
    private function readEnding(): string
174
    {
175 502
        $meta = stream_get_meta_data($this->fileHandle);
176
        // Phpstan incorrectly flags following line for Php8.2-, corrected in 8.3
177 502
        $filename = $meta['uri']; //@phpstan-ignore-line
178
179 502
        clearstatcache(true, $filename);
180 502
        $size = (int) filesize($filename);
181 502
        if ($size === 0) {
182 1
            return '';
183
        }
184
185 501
        $blockSize = self::TEST_SAMPLE_SIZE;
186 501
        if ($size < $blockSize) {
187 50
            $blockSize = $size;
188
        }
189
190 501
        fseek($this->fileHandle, $size - $blockSize);
191
192 501
        return (string) fread($this->fileHandle, $blockSize);
193
    }
194
195 502
    private static function startsWithTag(string $data): bool
196
    {
197 502
        return str_starts_with(trim($data), '<');
198
    }
199
200 502
    private static function endsWithTag(string $data): bool
201
    {
202 502
        return str_ends_with(trim($data), '>');
203
    }
204
205 502
    private static function containsTags(string $data): bool
206
    {
207 502
        return strlen($data) !== strlen(strip_tags($data));
208
    }
209
210
    /**
211
     * Loads Spreadsheet from file.
212
     */
213 484
    public function loadSpreadsheetFromFile(string $filename): Spreadsheet
214
    {
215 484
        $spreadsheet = $this->newSpreadsheet();
216 484
        $spreadsheet->setValueBinder($this->valueBinder);
217
218
        // Load into this instance
219 484
        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 499
    protected function setTableStartColumn(string $column): string
236
    {
237 499
        if ($this->tableLevel == 0) {
238 499
            $column = 'A';
239
        }
240 499
        ++$this->tableLevel;
241 499
        $this->nestedColumn[$this->tableLevel] = $column;
242
243 499
        return $this->nestedColumn[$this->tableLevel];
244
    }
245
246 495
    protected function getTableStartColumn(): string
247
    {
248 495
        return $this->nestedColumn[$this->tableLevel];
249
    }
250
251 492
    protected function releaseTableStartColumn(): string
252
    {
253 492
        --$this->tableLevel;
254
255 492
        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 500
    protected function flushCell(Worksheet $sheet, string $column, int|string $row, mixed &$cellContent, array $attributeArray): void
266
    {
267 500
        if (is_string($cellContent)) {
268
            //    Simple String content
269 500
            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 480
                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 479
                    $sheet->setCellValue($column . $row, $cellContent);
301
                }
302 480
                $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 500
        $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 500
    private function processDomElementBody(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child): void
338
    {
339 500
        $attributeArray = [];
340
        /** @var DOMAttr $attribute */
341 500
        foreach (($child->attributes ?? []) as $attribute) {
342 486
            $attributeArray[$attribute->name] = $attribute->value;
343
        }
344
345 500
        if ($child->nodeName === 'body') {
346 500
            $row = 1;
347 500
            $column = 'A';
348 500
            $cellContent = '';
349 500
            $this->tableLevel = 0;
350 500
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
351
        } else {
352 500
            $this->processDomElementTitle($sheet, $row, $column, $cellContent, $child, $attributeArray);
353
        }
354
    }
355
356
    /** @param string[] $attributeArray */
357 500
    private function processDomElementTitle(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
358
    {
359 500
        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 500
            $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 500
    private function processDomElementSpanEtc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
378
    {
379 500
        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 500
            $this->processDomElementHr($sheet, $row, $column, $cellContent, $child, $attributeArray);
402
        }
403
    }
404
405
    /** @param string[] $attributeArray */
406 500
    private function processDomElementHr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
407
    {
408 500
        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 500
        $this->processDomElementBr($sheet, $row, $column, $cellContent, $child, $attributeArray);
416
    }
417
418
    /** @param string[] $attributeArray */
419 500
    private function processDomElementBr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
420
    {
421 500
        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 500
            $this->processDomElementA($sheet, $row, $column, $cellContent, $child, $attributeArray);
433
        }
434
    }
435
436
    /** @param string[] $attributeArray */
437 500
    private function processDomElementA(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
438
    {
439 500
        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 500
            $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 500
    private function processDomElementH1Etc(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
465
    {
466 500
        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 500
            $this->processDomElementLi($sheet, $row, $column, $cellContent, $child, $attributeArray);
489
        }
490
    }
491
492
    /** @param string[] $attributeArray */
493 500
    private function processDomElementLi(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
494
    {
495 500
        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 500
            $this->processDomElementImg($sheet, $row, $column, $cellContent, $child, $attributeArray);
511
        }
512
    }
513
514
    /** @param string[] $attributeArray */
515 500
    private function processDomElementImg(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
516
    {
517 500
        if ($child->nodeName === 'img') {
518 17
            $this->insertImage($sheet, $column, $row, $attributeArray);
519
        } else {
520 500
            $this->processDomElementTable($sheet, $row, $column, $cellContent, $child, $attributeArray);
521
        }
522
    }
523
524
    private string $currentColumn = 'A';
525
526
    /** @param string[] $attributeArray */
527 500
    private function processDomElementTable(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
528
    {
529 500
        if ($child->nodeName === 'table') {
530 499
            if (isset($attributeArray['class'])) {
531 444
                $classes = explode(' ', $attributeArray['class']);
532 444
                $sheet->setShowGridlines(in_array('gridlines', $classes, true));
533 444
                $sheet->setPrintGridlines(in_array('gridlinesp', $classes, true));
534
            }
535 499
            $this->currentColumn = 'A';
536 499
            $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
537 499
            $column = $this->setTableStartColumn($column);
538 499
            if ($this->tableLevel > 1 && $row > 1) {
539 2
                --$row;
540
            }
541 499
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
542 492
            $column = $this->releaseTableStartColumn();
543 492
            if ($this->tableLevel > 1) {
544 2
                ++$column; //* @phpstan-ignore-line
545
            } else {
546 492
                ++$row;
547
            }
548
        } else {
549 500
            $this->processDomElementTr($sheet, $row, $column, $cellContent, $child, $attributeArray);
550
        }
551
    }
552
553
    /** @param string[] $attributeArray */
554 500
    private function processDomElementTr(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
555
    {
556 500
        if ($child->nodeName === 'col') {
557 442
            $this->applyInlineStyle($sheet, -1, $this->currentColumn, $attributeArray);
558 442
            ++$this->currentColumn;
559 500
        } elseif ($child->nodeName === 'tr') {
560 495
            $column = $this->getTableStartColumn();
561 495
            $cellContent = '';
562 495
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
563
564 488
            if (isset($attributeArray['height'])) {
565 1
                $sheet->getRowDimension($row)->setRowHeight((float) $attributeArray['height']);
566
            }
567
568 488
            ++$row;
569
        } else {
570 500
            $this->processDomElementThTdOther($sheet, $row, $column, $cellContent, $child, $attributeArray);
571
        }
572
    }
573
574
    /** @param string[] $attributeArray */
575 500
    private function processDomElementThTdOther(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
576
    {
577 500
        if ($child->nodeName !== 'td' && $child->nodeName !== 'th') {
578 500
            $this->processDomElement($child, $sheet, $row, $column, $cellContent);
579
        } else {
580 495
            $this->processDomElementThTd($sheet, $row, $column, $cellContent, $child, $attributeArray);
581
        }
582
    }
583
584
    /** @param string[] $attributeArray */
585 488
    private function processDomElementBgcolor(Worksheet $sheet, int $row, string $column, array $attributeArray): void
586
    {
587 488
        if (isset($attributeArray['bgcolor'])) {
588 1
            $sheet->getStyle("$column$row")->applyFromArray(
589 1
                [
590 1
                    'fill' => [
591 1
                        'fillType' => Fill::FILL_SOLID,
592 1
                        'color' => ['rgb' => $this->getStyleColor($attributeArray['bgcolor'])],
593 1
                    ],
594 1
                ]
595 1
            );
596
        }
597
    }
598
599
    /** @param string[] $attributeArray */
600 488
    private function processDomElementWidth(Worksheet $sheet, string $column, array $attributeArray): void
601
    {
602 488
        if (isset($attributeArray['width'])) {
603 1
            $sheet->getColumnDimension($column)->setWidth((new CssDimension($attributeArray['width']))->width());
604
        }
605
    }
606
607
    /** @param string[] $attributeArray */
608 488
    private function processDomElementHeight(Worksheet $sheet, int $row, array $attributeArray): void
609
    {
610 488
        if (isset($attributeArray['height'])) {
611 1
            $sheet->getRowDimension($row)->setRowHeight((new CssDimension($attributeArray['height']))->height());
612
        }
613
    }
614
615
    /** @param string[] $attributeArray */
616 488
    private function processDomElementAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
617
    {
618 488
        if (isset($attributeArray['align'])) {
619 1
            $sheet->getStyle($column . $row)->getAlignment()->setHorizontal($attributeArray['align']);
620
        }
621
    }
622
623
    /** @param string[] $attributeArray */
624 488
    private function processDomElementVAlign(Worksheet $sheet, int $row, string $column, array $attributeArray): void
625
    {
626 488
        if (isset($attributeArray['valign'])) {
627 1
            $sheet->getStyle($column . $row)->getAlignment()->setVertical($attributeArray['valign']);
628
        }
629
    }
630
631
    /** @param string[] $attributeArray */
632 488
    private function processDomElementDataFormat(Worksheet $sheet, int $row, string $column, array $attributeArray): void
633
    {
634 488
        if (isset($attributeArray['data-format'])) {
635 1
            $sheet->getStyle($column . $row)->getNumberFormat()->setFormatCode($attributeArray['data-format']);
636
        }
637
    }
638
639
    /** @param string[] $attributeArray */
640 495
    private function processDomElementThTd(Worksheet $sheet, int &$row, string &$column, string &$cellContent, DOMElement $child, array &$attributeArray): void
641
    {
642 495
        while (isset($this->rowspan[$column . $row])) {
643 3
            $temp = (string) $column;
644 3
            ++$temp;
645 3
            $column = (string) $temp;
646
        }
647 495
        $this->processDomElement($child, $sheet, $row, $column, $cellContent); // ++$column above confuses Phpstan
648
649
        // apply inline style
650 488
        $this->applyInlineStyle($sheet, $row, $column, $attributeArray);
651
652
        /** @var string $cellContent */
653 488
        $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
654
655 488
        $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray);
656 488
        $this->processDomElementWidth($sheet, $column, $attributeArray);
657 488
        $this->processDomElementHeight($sheet, $row, $attributeArray);
658 488
        $this->processDomElementAlign($sheet, $row, $column, $attributeArray);
659 488
        $this->processDomElementVAlign($sheet, $row, $column, $attributeArray);
660 488
        $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray);
661
662 488
        if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
663
            //create merging rowspan and colspan
664 2
            $columnTo = $column;
665 2
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
666
                /** @var string $columnTo */
667 2
                ++$columnTo;
668
            }
669 2
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
670 2
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
671 2
                $this->rowspan[$value] = true;
672
            }
673 2
            $sheet->mergeCells($range);
674
            //* @phpstan-ignore-next-line
675 2
            $column = $columnTo; // ++$columnTo above confuses phpstan
676 488
        } elseif (isset($attributeArray['rowspan'])) {
677
            //create merging rowspan
678 3
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
679 3
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
680 3
                $this->rowspan[$value] = true;
681
            }
682 3
            $sheet->mergeCells($range);
683 488
        } elseif (isset($attributeArray['colspan'])) {
684
            //create merging colspan
685 3
            $columnTo = $column;
686 3
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
687
                /** @var string $columnTo */
688 3
                ++$columnTo;
689
            }
690 3
            $sheet->mergeCells($column . $row . ':' . $columnTo . $row);
691
            //* @phpstan-ignore-next-line
692 3
            $column = $columnTo; // ++$columnTo above confuses phpstan
693
        }
694
695 488
        ++$column; //* @phpstan-ignore-line
696
    }
697
698 500
    protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void
699
    {
700 500
        foreach ($element->childNodes as $child) {
701 500
            if ($child instanceof DOMText) {
702 497
                $domText = (string) preg_replace('/\s+/', ' ', trim($child->nodeValue ?? ''));
703 497
                if ($domText === "\u{a0}") {
704 8
                    $domText = '';
705
                }
706
                //    simply append the text if the cell content is a plain text string
707 497
                $cellContent .= $domText;
708
                //    but if we have a rich text run instead, we need to append it correctly
709
                //    TODO
710 500
            } elseif ($child instanceof DOMElement) {
711 500
                $this->processDomElementBody($sheet, $row, $column, $cellContent, $child);
712
            }
713
        }
714
    }
715
716
    /**
717
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
718
     */
719 484
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
720
    {
721
        // Validate
722 484
        if (!$this->canRead($filename)) {
723 1
            throw new Exception($filename . ' is an Invalid HTML file.');
724
        }
725
726
        // Create a new DOM object
727 483
        $dom = new DOMDocument();
728
729
        // Reload the HTML file into the DOM object
730
        try {
731 483
            $convert = $this->getSecurityScannerOrThrow()->scanFile($filename);
732 482
            $convert = self::replaceNonAsciiIfNeeded($convert);
733 482
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
734 2
        } catch (Throwable $e) {
735 2
            $loaded = false;
736
        }
737 483
        if ($loaded === false) {
738 2
            throw new Exception('Failed to load ' . $filename . ' as a DOM Document', 0, $e ?? null);
739
        }
740 481
        self::loadProperties($dom, $spreadsheet);
741
742 481
        return $this->loadDocument($dom, $spreadsheet);
743
    }
744
745 500
    private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void
746
    {
747 500
        $properties = $spreadsheet->getProperties();
748 500
        foreach ($dom->getElementsByTagName('meta') as $meta) {
749 452
            $metaContent = (string) $meta->getAttribute('content');
750 452
            if ($metaContent !== '') {
751 444
                $metaName = (string) $meta->getAttribute('name');
752
                switch ($metaName) {
753 444
                    case 'author':
754 442
                        $properties->setCreator($metaContent);
755
756 442
                        break;
757 444
                    case 'category':
758 1
                        $properties->setCategory($metaContent);
759
760 1
                        break;
761 444
                    case 'company':
762 1
                        $properties->setCompany($metaContent);
763
764 1
                        break;
765 444
                    case 'created':
766 442
                        $properties->setCreated($metaContent);
767
768 442
                        break;
769 444
                    case 'description':
770 1
                        $properties->setDescription($metaContent);
771
772 1
                        break;
773 444
                    case 'keywords':
774 1
                        $properties->setKeywords($metaContent);
775
776 1
                        break;
777 444
                    case 'lastModifiedBy':
778 442
                        $properties->setLastModifiedBy($metaContent);
779
780 442
                        break;
781 444
                    case 'manager':
782 1
                        $properties->setManager($metaContent);
783
784 1
                        break;
785 444
                    case 'modified':
786 442
                        $properties->setModified($metaContent);
787
788 442
                        break;
789 444
                    case 'subject':
790 1
                        $properties->setSubject($metaContent);
791
792 1
                        break;
793 444
                    case 'title':
794 440
                        $properties->setTitle($metaContent);
795
796 440
                        break;
797 444
                    case 'viewport':
798 1
                        $properties->setViewport($metaContent);
799
800 1
                        break;
801
                    default:
802 444
                        if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) {
803 1
                            match ($matches[1]) {
804 1
                                'bool' => $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN),
805 1
                                'float' => $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT),
806 1
                                'int' => $properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER),
807 1
                                'date' => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE),
808
                                // string
809 1
                                default => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING),
810 1
                            };
811
                        }
812
                }
813
            }
814
        }
815 500
        if (!empty($dom->baseURI)) {
816 1
            $properties->setHyperlinkBase($dom->baseURI);
817
        }
818
    }
819
820
    /** @param string[] $matches */
821 19
    private static function replaceNonAscii(array $matches): string
822
    {
823 19
        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
824
    }
825
826 501
    private static function replaceNonAsciiIfNeeded(string $convert): ?string
827
    {
828 501
        if (preg_match(self::STARTS_WITH_BOM, $convert) !== 1 && preg_match(self::DECLARES_CHARSET, $convert) !== 1) {
829 48
            $lowend = "\u{80}";
830 48
            $highend = "\u{10ffff}";
831 48
            $regexp = "/[$lowend-$highend]/u";
832
            /** @var callable $callback */
833 48
            $callback = [self::class, 'replaceNonAscii'];
834 48
            $convert = preg_replace_callback($regexp, $callback, $convert);
835
        }
836
837 501
        return $convert;
838
    }
839
840
    /**
841
     * Spreadsheet from content.
842
     */
843 19
    public function loadFromString(string $content, ?Spreadsheet $spreadsheet = null): Spreadsheet
844
    {
845
        //    Create a new DOM object
846 19
        $dom = new DOMDocument();
847
848
        //    Reload the HTML file into the DOM object
849
        try {
850 19
            $convert = $this->getSecurityScannerOrThrow()->scan($content);
851 19
            $convert = self::replaceNonAsciiIfNeeded($convert);
852 19
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
853
        } catch (Throwable $e) {
854
            $loaded = false;
855
        }
856 19
        if ($loaded === false) {
857
            throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
858
        }
859 19
        $spreadsheet = $spreadsheet ?? $this->newSpreadsheet();
860 19
        $spreadsheet->setValueBinder($this->valueBinder);
861 19
        self::loadProperties($dom, $spreadsheet);
862
863 19
        return $this->loadDocument($dom, $spreadsheet);
864
    }
865
866
    /**
867
     * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance.
868
     */
869 500
    private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet
870
    {
871 500
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
872 2
            $spreadsheet->createSheet();
873
        }
874 500
        $spreadsheet->setActiveSheetIndex($this->sheetIndex);
875
876
        // Discard white space
877 500
        $document->preserveWhiteSpace = false;
878
879 500
        $row = 0;
880 500
        $column = 'A';
881 500
        $content = '';
882 500
        $this->rowspan = [];
883 500
        $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content);
884
885
        // Return
886 493
        return $spreadsheet;
887
    }
888
889
    /**
890
     * Get sheet index.
891
     */
892 1
    public function getSheetIndex(): int
893
    {
894 1
        return $this->sheetIndex;
895
    }
896
897
    /**
898
     * Set sheet index.
899
     *
900
     * @param int $sheetIndex Sheet index
901
     *
902
     * @return $this
903
     */
904 2
    public function setSheetIndex(int $sheetIndex): static
905
    {
906 2
        $this->sheetIndex = $sheetIndex;
907
908 2
        return $this;
909
    }
910
911
    /**
912
     * Apply inline css inline style.
913
     *
914
     * NOTES :
915
     * Currently only intended for td & th element,
916
     * and only takes 'background-color' and 'color'; property with HEX color
917
     *
918
     * TODO :
919
     * - Implement to other propertie, such as border
920
     *
921
     * @param string[] $attributeArray
922
     */
923 488
    private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, array $attributeArray): void
924
    {
925 488
        if (!isset($attributeArray['style'])) {
926 482
            return;
927
        }
928
929 16
        if ($row <= 0 || $column === '') {
930 1
            $cellStyle = new Style();
931 16
        } elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
932 1
            $columnTo = $column;
933 1
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
934
                /** @var string $columnTo */
935 1
                ++$columnTo;
936
            }
937 1
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
938 1
            $cellStyle = $sheet->getStyle($range);
939 16
        } elseif (isset($attributeArray['rowspan'])) {
940 1
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
941 1
            $cellStyle = $sheet->getStyle($range);
942 16
        } elseif (isset($attributeArray['colspan'])) {
943 1
            $columnTo = $column;
944 1
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
945
                /** @var string $columnTo */
946 1
                ++$columnTo;
947
            }
948 1
            $range = $column . $row . ':' . $columnTo . $row;
949 1
            $cellStyle = $sheet->getStyle($range);
950
        } else {
951 16
            $cellStyle = $sheet->getStyle($column . $row);
952
        }
953
954
        // add color styles (background & text) from dom element,currently support : td & th, using ONLY inline css style with RGB color
955 16
        $styles = explode(';', $attributeArray['style']);
956 16
        foreach ($styles as $st) {
957 16
            $value = explode(':', $st);
958 16
            $styleName = trim($value[0]);
959 16
            $styleValue = isset($value[1]) ? trim($value[1]) : null;
960 16
            $styleValueString = (string) $styleValue;
961
962 16
            if (!$styleName) {
963 12
                continue;
964
            }
965
966
            switch ($styleName) {
967 16
                case 'background':
968 16
                case 'background-color':
969 3
                    $styleColor = $this->getStyleColor($styleValueString);
970
971 3
                    if (!$styleColor) {
972 1
                        continue 2;
973
                    }
974
975 3
                    $cellStyle->applyFromArray(['fill' => ['fillType' => Fill::FILL_SOLID, 'color' => ['rgb' => $styleColor]]]);
976
977 3
                    break;
978 16
                case 'color':
979 3
                    $styleColor = $this->getStyleColor($styleValueString);
980
981 3
                    if (!$styleColor) {
982 1
                        continue 2;
983
                    }
984
985 3
                    $cellStyle->applyFromArray(['font' => ['color' => ['rgb' => $styleColor]]]);
986
987 3
                    break;
988
989 13
                case 'border':
990 3
                    $this->setBorderStyle($cellStyle, $styleValueString, 'allBorders');
991
992 3
                    break;
993
994 11
                case 'border-top':
995 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'top');
996
997 1
                    break;
998
999 11
                case 'border-bottom':
1000 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'bottom');
1001
1002 1
                    break;
1003
1004 11
                case 'border-left':
1005 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'left');
1006
1007 1
                    break;
1008
1009 11
                case 'border-right':
1010 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'right');
1011
1012 1
                    break;
1013
1014 10
                case 'font-size':
1015 1
                    $cellStyle->getFont()->setSize(
1016 1
                        (float) $styleValue
1017 1
                    );
1018
1019 1
                    break;
1020
1021 10
                case 'font-weight':
1022 1
                    if ($styleValue === 'bold' || $styleValue >= 500) {
1023 1
                        $cellStyle->getFont()->setBold(true);
1024
                    }
1025
1026 1
                    break;
1027
1028 10
                case 'font-style':
1029 1
                    if ($styleValue === 'italic') {
1030 1
                        $cellStyle->getFont()->setItalic(true);
1031
                    }
1032
1033 1
                    break;
1034
1035 10
                case 'font-family':
1036 1
                    $cellStyle->getFont()->setName(str_replace('\'', '', $styleValueString));
1037
1038 1
                    break;
1039
1040 10
                case 'text-decoration':
1041
                    switch ($styleValue) {
1042 1
                        case 'underline':
1043 1
                            $cellStyle->getFont()->setUnderline(Font::UNDERLINE_SINGLE);
1044
1045 1
                            break;
1046 1
                        case 'line-through':
1047 1
                            $cellStyle->getFont()->setStrikethrough(true);
1048
1049 1
                            break;
1050
                    }
1051
1052 1
                    break;
1053
1054 9
                case 'text-align':
1055 1
                    $cellStyle->getAlignment()->setHorizontal($styleValueString);
1056
1057 1
                    break;
1058
1059 9
                case 'vertical-align':
1060 2
                    $cellStyle->getAlignment()->setVertical($styleValueString);
1061
1062 2
                    break;
1063
1064 9
                case 'width':
1065 2
                    if ($column !== '') {
1066 2
                        $sheet->getColumnDimension($column)->setWidth(
1067 2
                            (new CssDimension($styleValue ?? ''))->width()
1068 2
                        );
1069
                    }
1070
1071 2
                    break;
1072
1073 7
                case 'height':
1074 1
                    if ($row > 0) {
1075 1
                        $sheet->getRowDimension($row)->setRowHeight(
1076 1
                            (new CssDimension($styleValue ?? ''))->height()
1077 1
                        );
1078
                    }
1079
1080 1
                    break;
1081
1082 6
                case 'word-wrap':
1083 1
                    $cellStyle->getAlignment()->setWrapText(
1084 1
                        $styleValue === 'break-word'
1085 1
                    );
1086
1087 1
                    break;
1088
1089 6
                case 'text-indent':
1090 2
                    $cellStyle->getAlignment()->setIndent(
1091 2
                        (int) str_replace(['px'], '', $styleValueString)
1092 2
                    );
1093
1094 2
                    break;
1095
            }
1096
        }
1097
    }
1098
1099
    /**
1100
     * Check if has #, so we can get clean hex.
1101
     */
1102 7
    public function getStyleColor(?string $value): string
1103
    {
1104 7
        $value = (string) $value;
1105 7
        if (str_starts_with($value, '#')) {
1106 5
            return substr($value, 1);
1107
        }
1108
1109 4
        return HelperHtml::colourNameLookup($value);
1110
    }
1111
1112
    /** @param string[] $attributes */
1113 17
    private function insertImage(Worksheet $sheet, string $column, int $row, array $attributes): void
1114
    {
1115 17
        if (!isset($attributes['src'])) {
1116 1
            return;
1117
        }
1118 16
        $styleArray = self::getStyleArray($attributes);
1119
1120 16
        $src = $attributes['src'];
1121 16
        if (substr($src, 0, 5) !== 'data:') {
1122 12
            $src = urldecode($src);
1123
        }
1124 16
        $width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null);
1125 16
        $height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null);
1126 16
        $name = $attributes['alt'] ?? null;
1127
1128 16
        $drawing = new Drawing();
1129 16
        $drawing->setPath($src, false);
1130 9
        if ($drawing->getPath() === '') {
1131 1
            return;
1132
        }
1133 8
        $drawing->setWorksheet($sheet);
1134 8
        $drawing->setCoordinates($column . $row);
1135 8
        $drawing->setOffsetX(0);
1136 8
        $drawing->setOffsetY(10);
1137 8
        $drawing->setResizeProportional(true);
1138
1139 8
        if ($name) {
1140 7
            $drawing->setName($name);
1141
        }
1142
1143
        /** @var null|scalar $width */
1144
        /** @var null|scalar $height */
1145 8
        if ($width) {
1146 5
            if ($height) {
1147 2
                $drawing->setWidthAndHeight((int) $width, (int) $height);
1148
            } else {
1149 3
                $drawing->setWidth((int) $width);
1150
            }
1151 3
        } elseif ($height) {
1152 1
            $drawing->setHeight((int) $height);
1153
        }
1154
1155 8
        $sheet->getColumnDimension($column)->setWidth(
1156 8
            $drawing->getWidth() / 6
1157 8
        );
1158
1159 8
        $sheet->getRowDimension($row)->setRowHeight(
1160 8
            $drawing->getHeight() * 0.9
1161 8
        );
1162
1163 8
        if (isset($styleArray['opacity'])) {
1164
            $opacity = $styleArray['opacity'];
1165
            if (is_numeric($opacity)) {
1166
                $drawing->setOpacity((int) ($opacity * 100000));
1167
            }
1168
        }
1169
    }
1170
1171
    /**
1172
     * @param string[] $attributes
1173
     *
1174
     * @return mixed[]
1175
     */
1176 16
    private static function getStyleArray(array $attributes): array
1177
    {
1178 16
        $styleArray = [];
1179 16
        if (isset($attributes['style'])) {
1180 4
            $styles = explode(';', $attributes['style']);
1181 4
            foreach ($styles as $style) {
1182 4
                $value = explode(':', $style);
1183 4
                if (count($value) === 2) {
1184 4
                    $arrayKey = trim($value[0]);
1185 4
                    $arrayValue = trim($value[1]);
1186 4
                    if ($arrayKey === 'width') {
1187 4
                        if (substr($arrayValue, -2) === 'px') {
1188 4
                            $arrayValue = (string) (((float) substr($arrayValue, 0, -2)));
1189
                        } else {
1190
                            $arrayValue = (new CssDimension($arrayValue))->width();
1191
                        }
1192 4
                    } elseif ($arrayKey === 'height') {
1193 2
                        if (substr($arrayValue, -2) === 'px') {
1194 2
                            $arrayValue = substr($arrayValue, 0, -2);
1195
                        } else {
1196
                            $arrayValue = (new CssDimension($arrayValue))->height();
1197
                        }
1198
                    }
1199 4
                    $styleArray[$arrayKey] = $arrayValue;
1200
                }
1201
            }
1202
        }
1203
1204 16
        return $styleArray;
1205
    }
1206
1207
    private const BORDER_MAPPINGS = [
1208
        'dash-dot' => Border::BORDER_DASHDOT,
1209
        'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1210
        'dashed' => Border::BORDER_DASHED,
1211
        'dotted' => Border::BORDER_DOTTED,
1212
        'double' => Border::BORDER_DOUBLE,
1213
        'hair' => Border::BORDER_HAIR,
1214
        'medium' => Border::BORDER_MEDIUM,
1215
        'medium-dashed' => Border::BORDER_MEDIUMDASHED,
1216
        'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT,
1217
        'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT,
1218
        'none' => Border::BORDER_NONE,
1219
        'slant-dash-dot' => Border::BORDER_SLANTDASHDOT,
1220
        'solid' => Border::BORDER_THIN,
1221
        'thick' => Border::BORDER_THICK,
1222
    ];
1223
1224
    /** @return array<string, string> */
1225 15
    public static function getBorderMappings(): array
1226
    {
1227 15
        return self::BORDER_MAPPINGS;
1228
    }
1229
1230
    /**
1231
     * Map html border style to PhpSpreadsheet border style.
1232
     */
1233 3
    public function getBorderStyle(string $style): ?string
1234
    {
1235 3
        return self::BORDER_MAPPINGS[$style] ?? null;
1236
    }
1237
1238 3
    private function setBorderStyle(Style $cellStyle, string $styleValue, string $type): void
1239
    {
1240 3
        if (trim($styleValue) === Border::BORDER_NONE) {
1241 1
            $borderStyle = Border::BORDER_NONE;
1242 1
            $color = null;
1243
        } else {
1244 3
            $borderArray = explode(' ', $styleValue);
1245 3
            $borderCount = count($borderArray);
1246 3
            if ($borderCount >= 3) {
1247 3
                $borderStyle = $borderArray[1];
1248 3
                $color = $borderArray[2];
1249
            } else {
1250 1
                $borderStyle = $borderArray[0];
1251 1
                $color = $borderArray[1] ?? null;
1252
            }
1253
        }
1254
1255 3
        $cellStyle->applyFromArray([
1256 3
            'borders' => [
1257 3
                $type => [
1258 3
                    'borderStyle' => $this->getBorderStyle($borderStyle),
1259 3
                    'color' => ['rgb' => $this->getStyleColor($color)],
1260 3
                ],
1261 3
            ],
1262 3
        ]);
1263
    }
1264
1265
    /**
1266
     * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns).
1267
     *
1268
     * @return array<int, array{worksheetName: string, lastColumnLetter: string, lastColumnIndex: int, totalRows: int, totalColumns: int, sheetState: string}>
1269
     */
1270 1
    public function listWorksheetInfo(string $filename): array
1271
    {
1272 1
        $info = [];
1273 1
        $spreadsheet = $this->newSpreadsheet();
1274 1
        $this->loadIntoExisting($filename, $spreadsheet);
1275 1
        foreach ($spreadsheet->getAllSheets() as $sheet) {
1276 1
            $newEntry = ['worksheetName' => $sheet->getTitle()];
1277 1
            $newEntry['lastColumnLetter'] = $sheet->getHighestDataColumn();
1278 1
            $newEntry['lastColumnIndex'] = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()) - 1;
1279 1
            $newEntry['totalRows'] = $sheet->getHighestDataRow();
1280 1
            $newEntry['totalColumns'] = $newEntry['lastColumnIndex'] + 1;
1281 1
            $newEntry['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
1282 1
            $info[] = $newEntry;
1283
        }
1284 1
        $spreadsheet->disconnectWorksheets();
1285
1286 1
        return $info;
1287
    }
1288
}
1289