Completed
Push — master ( c2a964...f9b1b7 )
by
unknown
36s queued 26s
created

Html::processDomElementA()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 21
ccs 9
cts 9
cp 1
rs 9.2222
cc 6
nc 6
nop 6
crap 6
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
            $temp = (string) $column;
644
            ++$temp;
645 2
            $column = (string) $temp;
646 2
        }
647 2
        $this->processDomElement($child, $sheet, $row, $column, $cellContent); // ++$column above confuses Phpstan
648
649 2
        // apply inline style
650
        $this->applyInlineStyle($sheet, $row, $column, $attributeArray);
651 2
652 488
        /** @var string $cellContent */
653
        $this->flushCell($sheet, $column, $row, $cellContent, $attributeArray);
654 3
655 3
        $this->processDomElementBgcolor($sheet, $row, $column, $attributeArray);
656 3
        $this->processDomElementWidth($sheet, $column, $attributeArray);
657
        $this->processDomElementHeight($sheet, $row, $attributeArray);
658 3
        $this->processDomElementAlign($sheet, $row, $column, $attributeArray);
659 488
        $this->processDomElementVAlign($sheet, $row, $column, $attributeArray);
660
        $this->processDomElementDataFormat($sheet, $row, $column, $attributeArray);
661 3
662 3
        if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
663 3
            //create merging rowspan and colspan
664
            $columnTo = $column;
665 3
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
666
                /** @var string $columnTo */
667 3
                ++$columnTo;
668
            }
669
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
670 488
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
671
                $this->rowspan[$value] = true;
672
            }
673 500
            $sheet->mergeCells($range);
674
            //* @phpstan-ignore-next-line
675 500
            $column = $columnTo; // ++$columnTo above confuses phpstan
676 500
        } elseif (isset($attributeArray['rowspan'])) {
677 497
            //create merging rowspan
678 497
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
679 8
            foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
680
                $this->rowspan[$value] = true;
681
            }
682 497
            $sheet->mergeCells($range);
683
        } elseif (isset($attributeArray['colspan'])) {
684
            //create merging colspan
685 500
            $columnTo = $column;
686 500
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
687
                /** @var string $columnTo */
688
                ++$columnTo;
689
            }
690
            $sheet->mergeCells($column . $row . ':' . $columnTo . $row);
691
            //* @phpstan-ignore-next-line
692
            $column = $columnTo; // ++$columnTo above confuses phpstan
693
        }
694 484
695
        ++$column; //* @phpstan-ignore-line
696
    }
697 484
698 1
    protected function processDomElement(DOMNode $element, Worksheet $sheet, int &$row, string &$column, string &$cellContent): void
699
    {
700
        foreach ($element->childNodes as $child) {
701
            if ($child instanceof DOMText) {
702 483
                $domText = (string) preg_replace('/\s+/', ' ', trim($child->nodeValue ?? ''));
703
                if ($domText === "\u{a0}") {
704
                    $domText = '';
705
                }
706 483
                //    simply append the text if the cell content is a plain text string
707 482
                $cellContent .= $domText;
708 482
                //    but if we have a rich text run instead, we need to append it correctly
709 2
                //    TODO
710 2
            } elseif ($child instanceof DOMElement) {
711
                $this->processDomElementBody($sheet, $row, $column, $cellContent, $child);
712 483
            }
713 2
        }
714
    }
715 481
716
    /**
717 481
     * Loads PhpSpreadsheet from file into PhpSpreadsheet instance.
718
     */
719
    public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet
720 500
    {
721
        // Validate
722 500
        if (!$this->canRead($filename)) {
723 500
            throw new Exception($filename . ' is an Invalid HTML file.');
724 452
        }
725 452
726 444
        // Create a new DOM object
727
        $dom = new DOMDocument();
728 444
729 442
        // Reload the HTML file into the DOM object
730
        try {
731 442
            $convert = $this->getSecurityScannerOrThrow()->scanFile($filename);
732 444
            $convert = self::replaceNonAsciiIfNeeded($convert);
733 1
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
734
        } catch (Throwable $e) {
735 1
            $loaded = false;
736 444
        }
737 1
        if ($loaded === false) {
738
            throw new Exception('Failed to load ' . $filename . ' as a DOM Document', 0, $e ?? null);
739 1
        }
740 444
        self::loadProperties($dom, $spreadsheet);
741 442
742
        return $this->loadDocument($dom, $spreadsheet);
743 442
    }
744 444
745 1
    private static function loadProperties(DOMDocument $dom, Spreadsheet $spreadsheet): void
746
    {
747 1
        $properties = $spreadsheet->getProperties();
748 444
        foreach ($dom->getElementsByTagName('meta') as $meta) {
749 1
            $metaContent = (string) $meta->getAttribute('content');
750
            if ($metaContent !== '') {
751 1
                $metaName = (string) $meta->getAttribute('name');
752 444
                switch ($metaName) {
753 442
                    case 'author':
754
                        $properties->setCreator($metaContent);
755 442
756 444
                        break;
757 1
                    case 'category':
758
                        $properties->setCategory($metaContent);
759 1
760 444
                        break;
761 442
                    case 'company':
762
                        $properties->setCompany($metaContent);
763 442
764 444
                        break;
765 1
                    case 'created':
766
                        $properties->setCreated($metaContent);
767 1
768 444
                        break;
769 440
                    case 'description':
770
                        $properties->setDescription($metaContent);
771 440
772 444
                        break;
773 1
                    case 'keywords':
774
                        $properties->setKeywords($metaContent);
775 1
776
                        break;
777 444
                    case 'lastModifiedBy':
778 1
                        $properties->setLastModifiedBy($metaContent);
779 1
780 1
                        break;
781 1
                    case 'manager':
782 1
                        $properties->setManager($metaContent);
783
784 1
                        break;
785 1
                    case 'modified':
786
                        $properties->setModified($metaContent);
787
788
                        break;
789
                    case 'subject':
790 500
                        $properties->setSubject($metaContent);
791 1
792
                        break;
793
                    case 'title':
794
                        $properties->setTitle($metaContent);
795 19
796
                        break;
797 19
                    case 'viewport':
798
                        $properties->setViewport($metaContent);
799
800 501
                        break;
801
                    default:
802 501
                        if (preg_match('/^custom[.](bool|date|float|int|string)[.](.+)$/', $metaName, $matches) === 1) {
803 48
                            match ($matches[1]) {
804 48
                                'bool' => $properties->setCustomProperty($matches[2], (bool) $metaContent, Properties::PROPERTY_TYPE_BOOLEAN),
805 48
                                'float' => $properties->setCustomProperty($matches[2], (float) $metaContent, Properties::PROPERTY_TYPE_FLOAT),
806
                                'int' => $properties->setCustomProperty($matches[2], (int) $metaContent, Properties::PROPERTY_TYPE_INTEGER),
807 48
                                'date' => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_DATE),
808 48
                                // string
809
                                default => $properties->setCustomProperty($matches[2], $metaContent, Properties::PROPERTY_TYPE_STRING),
810
                            };
811 501
                        }
812
                }
813
            }
814
        }
815
        if (!empty($dom->baseURI)) {
816
            $properties->setHyperlinkBase($dom->baseURI);
817 19
        }
818
    }
819
820 19
    /** @param string[] $matches */
821
    private static function replaceNonAscii(array $matches): string
822
    {
823
        return '&#' . mb_ord($matches[0], 'UTF-8') . ';';
824 19
    }
825 19
826 19
    private static function replaceNonAsciiIfNeeded(string $convert): ?string
827
    {
828
        if (preg_match(self::STARTS_WITH_BOM, $convert) !== 1 && preg_match(self::DECLARES_CHARSET, $convert) !== 1) {
829
            $lowend = "\u{80}";
830 19
            $highend = "\u{10ffff}";
831
            $regexp = "/[$lowend-$highend]/u";
832
            /** @var callable $callback */
833 19
            $callback = [self::class, 'replaceNonAscii'];
834 19
            $convert = preg_replace_callback($regexp, $callback, $convert);
835 19
        }
836
837 19
        return $convert;
838
    }
839
840
    /**
841
     * Spreadsheet from content.
842
     */
843 500
    public function loadFromString(string $content, ?Spreadsheet $spreadsheet = null): Spreadsheet
844
    {
845 500
        //    Create a new DOM object
846 2
        $dom = new DOMDocument();
847
848 500
        //    Reload the HTML file into the DOM object
849
        try {
850
            $convert = $this->getSecurityScannerOrThrow()->scan($content);
851 500
            $convert = self::replaceNonAsciiIfNeeded($convert);
852
            $loaded = ($convert === null) ? false : $dom->loadHTML($convert);
853 500
        } catch (Throwable $e) {
854 500
            $loaded = false;
855 500
        }
856 500
        if ($loaded === false) {
857 500
            throw new Exception('Failed to load content as a DOM Document', 0, $e ?? null);
858
        }
859
        $spreadsheet = $spreadsheet ?? $this->newSpreadsheet();
860 493
        $spreadsheet->setValueBinder($this->valueBinder);
861
        self::loadProperties($dom, $spreadsheet);
862
863
        return $this->loadDocument($dom, $spreadsheet);
864
    }
865
866 1
    /**
867
     * Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance.
868 1
     */
869
    private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet
870
    {
871
        while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
872
            $spreadsheet->createSheet();
873
        }
874
        $spreadsheet->setActiveSheetIndex($this->sheetIndex);
875
876
        // Discard white space
877
        $document->preserveWhiteSpace = false;
878 2
879
        $row = 0;
880 2
        $column = 'A';
881
        $content = '';
882 2
        $this->rowspan = [];
883
        $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content);
884
885
        // Return
886
        return $spreadsheet;
887
    }
888
889
    /**
890
     * Get sheet index.
891
     */
892
    public function getSheetIndex(): int
893
    {
894
        return $this->sheetIndex;
895 488
    }
896
897 488
    /**
898 482
     * Set sheet index.
899
     *
900
     * @param int $sheetIndex Sheet index
901 16
     *
902 1
     * @return $this
903 16
     */
904 1
    public function setSheetIndex(int $sheetIndex): static
905 1
    {
906 1
        $this->sheetIndex = $sheetIndex;
907
908 1
        return $this;
909 1
    }
910 16
911 1
    /**
912 1
     * Apply inline css inline style.
913 16
     *
914 1
     * NOTES :
915 1
     * Currently only intended for td & th element,
916 1
     * and only takes 'background-color' and 'color'; property with HEX color
917
     *
918 1
     * TODO :
919 1
     * - Implement to other propertie, such as border
920
     *
921 16
     * @param string[] $attributeArray
922
     */
923
    private function applyInlineStyle(Worksheet &$sheet, int $row, string $column, array $attributeArray): void
924
    {
925 16
        if (!isset($attributeArray['style'])) {
926 16
            return;
927 16
        }
928 16
929 16
        if ($row <= 0 || $column === '') {
930 16
            $cellStyle = new Style();
931
        } elseif (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
932 16
            $columnTo = $column;
933 12
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
934
                /** @var string $columnTo */
935
                ++$columnTo;
936
            }
937 16
            $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
938 16
            $cellStyle = $sheet->getStyle($range);
939 3
        } elseif (isset($attributeArray['rowspan'])) {
940
            $range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
941 3
            $cellStyle = $sheet->getStyle($range);
942 1
        } elseif (isset($attributeArray['colspan'])) {
943
            $columnTo = $column;
944
            for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
945 3
                /** @var string $columnTo */
946
                ++$columnTo;
947 3
            }
948 16
            $range = $column . $row . ':' . $columnTo . $row;
949 3
            $cellStyle = $sheet->getStyle($range);
950
        } else {
951 3
            $cellStyle = $sheet->getStyle($column . $row);
952 1
        }
953
954
        // add color styles (background & text) from dom element,currently support : td & th, using ONLY inline css style with RGB color
955 3
        $styles = explode(';', $attributeArray['style']);
956
        foreach ($styles as $st) {
957 3
            $value = explode(':', $st);
958
            $styleName = trim($value[0]);
959 13
            $styleValue = isset($value[1]) ? trim($value[1]) : null;
960 3
            $styleValueString = (string) $styleValue;
961
962 3
            if (!$styleName) {
963
                continue;
964 11
            }
965 1
966
            switch ($styleName) {
967 1
                case 'background':
968
                case 'background-color':
969 11
                    $styleColor = $this->getStyleColor($styleValueString);
970 1
971
                    if (!$styleColor) {
972 1
                        continue 2;
973
                    }
974 11
975 1
                    $cellStyle->applyFromArray(['fill' => ['fillType' => Fill::FILL_SOLID, 'color' => ['rgb' => $styleColor]]]);
976
977 1
                    break;
978
                case 'color':
979 11
                    $styleColor = $this->getStyleColor($styleValueString);
980 1
981
                    if (!$styleColor) {
982 1
                        continue 2;
983
                    }
984 10
985 1
                    $cellStyle->applyFromArray(['font' => ['color' => ['rgb' => $styleColor]]]);
986 1
987 1
                    break;
988
989 1
                case 'border':
990
                    $this->setBorderStyle($cellStyle, $styleValueString, 'allBorders');
991 10
992 1
                    break;
993 1
994
                case 'border-top':
995
                    $this->setBorderStyle($cellStyle, $styleValueString, 'top');
996 1
997
                    break;
998 10
999 1
                case 'border-bottom':
1000 1
                    $this->setBorderStyle($cellStyle, $styleValueString, 'bottom');
1001
1002
                    break;
1003 1
1004
                case 'border-left':
1005 10
                    $this->setBorderStyle($cellStyle, $styleValueString, 'left');
1006 1
1007
                    break;
1008 1
1009
                case 'border-right':
1010 10
                    $this->setBorderStyle($cellStyle, $styleValueString, 'right');
1011
1012 1
                    break;
1013 1
1014
                case 'font-size':
1015 1
                    $cellStyle->getFont()->setSize(
1016 1
                        (float) $styleValue
1017 1
                    );
1018
1019 1
                    break;
1020
1021
                case 'font-weight':
1022 1
                    if ($styleValue === 'bold' || $styleValue >= 500) {
1023
                        $cellStyle->getFont()->setBold(true);
1024 9
                    }
1025 1
1026
                    break;
1027 1
1028
                case 'font-style':
1029 9
                    if ($styleValue === 'italic') {
1030 2
                        $cellStyle->getFont()->setItalic(true);
1031
                    }
1032 2
1033
                    break;
1034 9
1035 2
                case 'font-family':
1036 2
                    $cellStyle->getFont()->setName(str_replace('\'', '', $styleValueString));
1037 2
1038 2
                    break;
1039
1040
                case 'text-decoration':
1041 2
                    switch ($styleValue) {
1042
                        case 'underline':
1043 7
                            $cellStyle->getFont()->setUnderline(Font::UNDERLINE_SINGLE);
1044 1
1045 1
                            break;
1046 1
                        case 'line-through':
1047 1
                            $cellStyle->getFont()->setStrikethrough(true);
1048
1049
                            break;
1050 1
                    }
1051
1052 6
                    break;
1053 1
1054 1
                case 'text-align':
1055 1
                    $cellStyle->getAlignment()->setHorizontal($styleValueString);
1056
1057 1
                    break;
1058
1059 6
                case 'vertical-align':
1060 2
                    $cellStyle->getAlignment()->setVertical($styleValueString);
1061 2
1062 2
                    break;
1063
1064 2
                case 'width':
1065
                    if ($column !== '') {
1066
                        $sheet->getColumnDimension($column)->setWidth(
1067
                            (new CssDimension($styleValue ?? ''))->width()
1068
                        );
1069
                    }
1070
1071
                    break;
1072 7
1073
                case 'height':
1074 7
                    if ($row > 0) {
1075 7
                        $sheet->getRowDimension($row)->setRowHeight(
1076 5
                            (new CssDimension($styleValue ?? ''))->height()
1077
                        );
1078
                    }
1079 4
1080
                    break;
1081
1082 17
                case 'word-wrap':
1083
                    $cellStyle->getAlignment()->setWrapText(
1084 17
                        $styleValue === 'break-word'
1085 1
                    );
1086
1087 16
                    break;
1088
1089 16
                case 'text-indent':
1090 16
                    $cellStyle->getAlignment()->setIndent(
1091 12
                        (int) str_replace(['px'], '', $styleValueString)
1092
                    );
1093 16
1094 16
                    break;
1095 16
            }
1096
        }
1097 16
    }
1098 16
1099 9
    /**
1100 1
     * Check if has #, so we can get clean hex.
1101
     */
1102 8
    public function getStyleColor(?string $value): string
1103 8
    {
1104 8
        $value = (string) $value;
1105 8
        if (str_starts_with($value, '#')) {
1106 8
            return substr($value, 1);
1107
        }
1108 8
1109 7
        return HelperHtml::colourNameLookup($value);
1110
    }
1111
1112 8
    /** @param string[] $attributes */
1113 5
    private function insertImage(Worksheet $sheet, string $column, int $row, array $attributes): void
1114 2
    {
1115
        if (!isset($attributes['src'])) {
1116 3
            return;
1117
        }
1118 3
        $styleArray = self::getStyleArray($attributes);
1119 1
1120
        $src = $attributes['src'];
1121
        if (substr($src, 0, 5) !== 'data:') {
1122 8
            $src = urldecode($src);
1123 8
        }
1124 8
        $width = isset($attributes['width']) ? (float) $attributes['width'] : ($styleArray['width'] ?? null);
1125
        $height = isset($attributes['height']) ? (float) $attributes['height'] : ($styleArray['height'] ?? null);
1126 8
        $name = $attributes['alt'] ?? null;
1127 8
1128 8
        $drawing = new Drawing();
1129
        $drawing->setPath($src, false);
1130 8
        if ($drawing->getPath() === '') {
1131
            return;
1132
        }
1133
        $drawing->setWorksheet($sheet);
1134
        $drawing->setCoordinates($column . $row);
1135
        $drawing->setOffsetX(0);
1136
        $drawing->setOffsetY(10);
1137
        $drawing->setResizeProportional(true);
1138 16
1139
        if ($name) {
1140 16
            $drawing->setName($name);
1141 16
        }
1142 4
1143 4
        /** @var null|scalar $width */
1144 4
        /** @var null|scalar $height */
1145 4
        if ($width) {
1146 4
            if ($height) {
1147 4
                $drawing->setWidthAndHeight((int) $width, (int) $height);
1148 4
            } else {
1149 4
                $drawing->setWidth((int) $width);
1150 4
            }
1151
        } elseif ($height) {
1152
            $drawing->setHeight((int) $height);
1153
        }
1154 4
1155 2
        $sheet->getColumnDimension($column)->setWidth(
1156 2
            $drawing->getWidth() / 6
1157
        );
1158
1159
        $sheet->getRowDimension($row)->setRowHeight(
1160
            $drawing->getHeight() * 0.9
1161 4
        );
1162
1163
        if (isset($styleArray['opacity'])) {
1164
            $opacity = $styleArray['opacity'];
1165
            if (is_numeric($opacity)) {
1166 16
                $drawing->setOpacity((int) ($opacity * 100000));
1167
            }
1168
        }
1169
    }
1170
1171
    /**
1172
     * @param string[] $attributes
1173
     *
1174
     * @return mixed[]
1175
     */
1176
    private static function getStyleArray(array $attributes): array
1177
    {
1178
        $styleArray = [];
1179
        if (isset($attributes['style'])) {
1180
            $styles = explode(';', $attributes['style']);
1181
            foreach ($styles as $style) {
1182
                $value = explode(':', $style);
1183
                if (count($value) === 2) {
1184
                    $arrayKey = trim($value[0]);
1185
                    $arrayValue = trim($value[1]);
1186 15
                    if ($arrayKey === 'width') {
1187
                        if (substr($arrayValue, -2) === 'px') {
1188 15
                            $arrayValue = (string) (((float) substr($arrayValue, 0, -2)));
1189
                        } else {
1190
                            $arrayValue = (new CssDimension($arrayValue))->width();
1191
                        }
1192
                    } elseif ($arrayKey === 'height') {
1193
                        if (substr($arrayValue, -2) === 'px') {
1194 3
                            $arrayValue = substr($arrayValue, 0, -2);
1195
                        } else {
1196 3
                            $arrayValue = (new CssDimension($arrayValue))->height();
1197
                        }
1198
                    }
1199 3
                    $styleArray[$arrayKey] = $arrayValue;
1200
                }
1201 3
            }
1202 1
        }
1203 1
1204
        return $styleArray;
1205 3
    }
1206 3
1207 3
    private const BORDER_MAPPINGS = [
1208 3
        'dash-dot' => Border::BORDER_DASHDOT,
1209 3
        'dash-dot-dot' => Border::BORDER_DASHDOTDOT,
1210
        'dashed' => Border::BORDER_DASHED,
1211 1
        'dotted' => Border::BORDER_DOTTED,
1212 1
        'double' => Border::BORDER_DOUBLE,
1213
        'hair' => Border::BORDER_HAIR,
1214
        'medium' => Border::BORDER_MEDIUM,
1215
        'medium-dashed' => Border::BORDER_MEDIUMDASHED,
1216 3
        'medium-dash-dot' => Border::BORDER_MEDIUMDASHDOT,
1217 3
        'medium-dash-dot-dot' => Border::BORDER_MEDIUMDASHDOTDOT,
1218 3
        'none' => Border::BORDER_NONE,
1219 3
        'slant-dash-dot' => Border::BORDER_SLANTDASHDOT,
1220 3
        'solid' => Border::BORDER_THIN,
1221 3
        'thick' => Border::BORDER_THICK,
1222 3
    ];
1223 3
1224
    /** @return array<string, string> */
1225
    public static function getBorderMappings(): array
1226
    {
1227
        return self::BORDER_MAPPINGS;
1228
    }
1229
1230
    /**
1231 1
     * Map html border style to PhpSpreadsheet border style.
1232
     */
1233 1
    public function getBorderStyle(string $style): ?string
1234 1
    {
1235 1
        return self::BORDER_MAPPINGS[$style] ?? null;
1236 1
    }
1237 1
1238 1
    private function setBorderStyle(Style $cellStyle, string $styleValue, string $type): void
1239 1
    {
1240 1
        if (trim($styleValue) === Border::BORDER_NONE) {
1241 1
            $borderStyle = Border::BORDER_NONE;
1242 1
            $color = null;
1243 1
        } else {
1244
            $borderArray = explode(' ', $styleValue);
1245 1
            $borderCount = count($borderArray);
1246
            if ($borderCount >= 3) {
1247 1
                $borderStyle = $borderArray[1];
1248
                $color = $borderArray[2];
1249
            } else {
1250
                $borderStyle = $borderArray[0];
1251
                $color = $borderArray[1] ?? null;
1252
            }
1253
        }
1254
1255
        $cellStyle->applyFromArray([
1256
            'borders' => [
1257
                $type => [
1258
                    'borderStyle' => $this->getBorderStyle($borderStyle),
1259
                    'color' => ['rgb' => $this->getStyleColor($color)],
1260
                ],
1261
            ],
1262
        ]);
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
    public function listWorksheetInfo(string $filename): array
1271
    {
1272
        $info = [];
1273
        $spreadsheet = $this->newSpreadsheet();
1274
        $this->loadIntoExisting($filename, $spreadsheet);
1275
        foreach ($spreadsheet->getAllSheets() as $sheet) {
1276
            $newEntry = ['worksheetName' => $sheet->getTitle()];
1277
            $newEntry['lastColumnLetter'] = $sheet->getHighestDataColumn();
1278
            $newEntry['lastColumnIndex'] = Coordinate::columnIndexFromString($sheet->getHighestDataColumn()) - 1;
1279
            $newEntry['totalRows'] = $sheet->getHighestDataRow();
1280
            $newEntry['totalColumns'] = $newEntry['lastColumnIndex'] + 1;
1281
            $newEntry['sheetState'] = Worksheet::SHEETSTATE_VISIBLE;
1282
            $info[] = $newEntry;
1283
        }
1284
        $spreadsheet->disconnectWorksheets();
1285
1286
        return $info;
1287
    }
1288
}
1289