PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / libraries / vendor / PhpSpreadsheet / Reader / Html.php

Html.php in TablePress – Tables in WordPress made easy 3.4, at libraries/vendor/PhpSpreadsheet/Reader/Html.php

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