Passed
Pull Request — master (#4459)
by Owen
13:04
created

Html::processDomElementAlign()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

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