From a67e778b50c561139de7f5c444e914126feb768d Mon Sep 17 00:00:00 2001 From: Mikko Date: Wed, 19 Mar 2025 14:08:36 +0200 Subject: [PATCH 1/2] Conditional and table formatting support for html writer --- CHANGELOG.md | 1 + docs/topics/conditional-formatting.md | 37 ++-- docs/topics/tables.md | 16 ++ .../Html/01_Basic_Conditional_Formatting.php | 25 +++ .../Html/02_More_Conditional_Formatting.php | 25 +++ samples/Html/03_Color_Scale.php | 25 +++ .../04_Table_Format_without_Conditional.php | 25 +++ .../Html/05_Table_Format_with_Conditional.php | 27 +++ .../templates/BasicConditionalFormatting.xlsx | Bin 0 -> 9048 bytes samples/templates/ColourScale.xlsx | Bin 0 -> 8798 bytes .../ConditionalFormattingConditions.xlsx | Bin 0 -> 5588 bytes samples/templates/TableFormat.xlsx | Bin 0 -> 9586 bytes src/PhpSpreadsheet/Reader/Xlsx.php | 15 +- .../Reader/Xlsx/ConditionalStyles.php | 7 + src/PhpSpreadsheet/Reader/Xlsx/Styles.php | 41 +++++ .../Reader/Xlsx/TableReader.php | 16 +- src/PhpSpreadsheet/Style/Conditional.php | 10 +- .../ConditionalFormatting/CellMatcher.php | 12 +- .../CellStyleAssessor.php | 29 ++- .../ConditionalColorScale.php | 164 +++++++++++++++++ src/PhpSpreadsheet/Worksheet/Table.php | 13 ++ .../Worksheet/Table/TableDxfsStyle.php | 170 ++++++++++++++++++ .../Worksheet/Table/TableStyle.php | 33 ++++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 60 +++++++ src/PhpSpreadsheet/Writer/BaseWriter.php | 42 +++++ src/PhpSpreadsheet/Writer/Html.php | 102 ++++++++++- .../Writer/Html/HtmlColourScaleTest.php | 79 ++++++++ .../Html/HtmlConditionalFormattingTest.php | 65 +++++++ ...tmlDifferentConditionalFormattingsTest.php | 94 ++++++++++ .../Writer/Html/HtmlTableFormatTest.php | 64 +++++++ .../HtmlTableFormatWithConditionalTest.php | 65 +++++++ 31 files changed, 1225 insertions(+), 37 deletions(-) create mode 100644 docs/topics/tables.md create mode 100644 samples/Html/01_Basic_Conditional_Formatting.php create mode 100644 samples/Html/02_More_Conditional_Formatting.php create mode 100644 samples/Html/03_Color_Scale.php create mode 100644 samples/Html/04_Table_Format_without_Conditional.php create mode 100644 samples/Html/05_Table_Format_with_Conditional.php create mode 100644 samples/templates/BasicConditionalFormatting.xlsx create mode 100644 samples/templates/ColourScale.xlsx create mode 100644 samples/templates/ConditionalFormattingConditions.xlsx create mode 100644 samples/templates/TableFormat.xlsx create mode 100644 src/PhpSpreadsheet/Worksheet/Table/TableDxfsStyle.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatTest.php create mode 100644 tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatWithConditionalTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a7c0e63c0d..0ca1b6c663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Add FormulaRange to IgnoredErrors. [PR #4393](https://github.com/PHPOffice/PhpSpreadsheet/pull/4393) - TextGrid improvements. [PR #4418](https://github.com/PHPOffice/PhpSpreadsheet/pull/4418) - Permit read to class which extends Spreadsheet. [Discussion #4402](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4402) [PR #4404](https://github.com/PHPOffice/PhpSpreadsheet/pull/4404) +- Conditional and table formatting support for html writer [PR #4412](https://github.com/PHPOffice/PhpSpreadsheet/pull/4412) ### Removed diff --git a/docs/topics/conditional-formatting.md b/docs/topics/conditional-formatting.md index 352acbef10..735215c628 100644 --- a/docs/topics/conditional-formatting.md +++ b/docs/topics/conditional-formatting.md @@ -143,20 +143,28 @@ Currently, the following Conditional Types are supported for the following Reade MS Excel | Conditional Type | Readers | Writers ---|---|---|--- -| Cell Value | Conditional::CONDITION_CELLIS | Xlsx | Xlsx, Xls -Specific Text | Conditional::CONDITION_CONTAINSTEXT | Xlsx | Xlsx - | Conditional::CONDITION_NOTCONTAINSTEXT | Xlsx | Xlsx - | Conditional::CONDITION_BEGINSWITH | Xlsx | Xlsx - | Conditional::CONDITION_ENDSWITH | Xlsx | Xlsx -Dates Occurring | Conditional::CONDITION_TIMEPERIOD | Xlsx | Xlsx -Blanks | Conditional::CONDITION_CONTAINSBLANKS | Xlsx | Xlsx -No Blanks | Conditional::CONDITION_NOTCONTAINSBLANKS | Xlsx | Xlsx -Errors | Conditional::CONDITION_CONTAINSERRORS | Xlsx | Xlsx -No Errors | Conditional::CONDITION_NOTCONTAINSERRORS | Xlsx | Xlsx -Duplicates/Unique | Conditional::CONDITION_DUPLICATES | Xlsx | Xlsx - | Conditional::CONDITION_UNIQUE | Xlsx | Xlsx -Use a formula | Conditional::CONDITION_EXPRESSION | Xlsx | Xlsx, Xls -Data Bars | Conditional::CONDITION_DATABAR | Xlsx | Xlsx +| Cell Value | Conditional::CONDITION_CELLIS | Xlsx | Xlsx, Xls, Html +Specific Text | Conditional::CONDITION_CONTAINSTEXT | Xlsx | Xlsx, Html + | Conditional::CONDITION_NOTCONTAINSTEXT | Xlsx | Xlsx, Html + | Conditional::CONDITION_BEGINSWITH | Xlsx | Xlsx, Html + | Conditional::CONDITION_ENDSWITH | Xlsx | Xlsx, Html +Dates Occurring | Conditional::CONDITION_TIMEPERIOD | Xlsx | Xlsx, Html +Blanks | Conditional::CONDITION_CONTAINSBLANKS | Xlsx | Xlsx, Html +No Blanks | Conditional::CONDITION_NOTCONTAINSBLANKS | Xlsx | Xlsx, Html +Errors | Conditional::CONDITION_CONTAINSERRORS | Xlsx | Xlsx, Html +No Errors | Conditional::CONDITION_NOTCONTAINSERRORS | Xlsx | Xlsx, Html +Duplicates/Unique | Conditional::CONDITION_DUPLICATES | Xlsx | Xlsx, Html + | Conditional::CONDITION_UNIQUE | Xlsx | Xlsx, Html +Use a formula | Conditional::CONDITION_EXPRESSION | Xlsx | Xlsx, Xls, Html +Data Bars | Conditional::CONDITION_DATABAR | Xlsx | Xlsx, Html +Colour Scales | Conditional::COLORSCALE | Xlsx | Html + +To enable conditional formatting for Html writer, use: + +```php + $writer = new HtmlWriter($spreadsheet); + $writer->setConditionalFormatting(true); +``` The following Conditional Types are currently not supported by any Readers or Writers: @@ -165,7 +173,6 @@ MS Excel | Conditional Type Above/Below Average | ? Top/Bottom Items | ? Top/Bottom %age | ? -Colour Scales |? Icon Sets | ? Unsupported types will by ignored by the Readers, and cannot be created through PHPSpreadsheet. diff --git a/docs/topics/tables.md b/docs/topics/tables.md new file mode 100644 index 0000000000..a16d036bf4 --- /dev/null +++ b/docs/topics/tables.md @@ -0,0 +1,16 @@ +# Tables + +## Introduction + +To make managing and analyzing a group of related data easier, you can turn a range of cells into an Excel table (previously known as an Excel list). + +## Support + +Currently tables are supported in Xlsx reader and Html Writer + +To enable table formatting for Html writer, use: + +```php + $writer = new HtmlWriter($spreadsheet); + $writer->setConditionalFormatting(true); +``` \ No newline at end of file diff --git a/samples/Html/01_Basic_Conditional_Formatting.php b/samples/Html/01_Basic_Conditional_Formatting.php new file mode 100644 index 0000000000..c2a1efd1a1 --- /dev/null +++ b/samples/Html/01_Basic_Conditional_Formatting.php @@ -0,0 +1,25 @@ +isCli() ? ('samples/templates/' . $inputFileName) : ('' . 'samples/templates/' . $inputFileName . ''); +$helper->log('Read ' . $codePath . ' with conditional formatting'); +$reader = IOFactory::createReader('Xlsx'); +$reader->setReadDataOnly(false); +$spreadsheet = $reader->load($inputFilePath); +$helper->log('Enable conditional formatting output'); + +function writerCallback(HtmlWriter $writer): void +{ + $writer->setPreCalculateFormulas(true); + $writer->setConditionalFormatting(true); +} + +// Save +$helper->write($spreadsheet, __FILE__, ['Html'], false, writerCallback: writerCallback(...)); diff --git a/samples/Html/02_More_Conditional_Formatting.php b/samples/Html/02_More_Conditional_Formatting.php new file mode 100644 index 0000000000..b8971f0e37 --- /dev/null +++ b/samples/Html/02_More_Conditional_Formatting.php @@ -0,0 +1,25 @@ +isCli() ? ('samples/templates/' . $inputFileName) : ('' . 'samples/templates/' . $inputFileName . ''); +$helper->log('Read ' . $codePath . ' with conditional formatting'); +$reader = IOFactory::createReader('Xlsx'); +$reader->setReadDataOnly(false); +$spreadsheet = $reader->load($inputFilePath); +$helper->log('Enable conditional formatting output'); + +function writerCallback(HtmlWriter $writer): void +{ + $writer->setPreCalculateFormulas(true); + $writer->setConditionalFormatting(true); +} + +// Save +$helper->write($spreadsheet, __FILE__, ['Html'], false, writerCallback: writerCallback(...)); diff --git a/samples/Html/03_Color_Scale.php b/samples/Html/03_Color_Scale.php new file mode 100644 index 0000000000..ed2296bfb4 --- /dev/null +++ b/samples/Html/03_Color_Scale.php @@ -0,0 +1,25 @@ +isCli() ? ('samples/templates/' . $inputFileName) : ('' . 'samples/templates/' . $inputFileName . ''); +$helper->log('Read ' . $codePath . ' with color scale'); +$reader = IOFactory::createReader('Xlsx'); +$reader->setReadDataOnly(false); +$spreadsheet = $reader->load($inputFilePath); +$helper->log('Enable conditional formatting output'); + +function writerCallback(HtmlWriter $writer): void +{ + $writer->setPreCalculateFormulas(true); + $writer->setConditionalFormatting(true); +} + +// Save +$helper->write($spreadsheet, __FILE__, ['Html'], false, writerCallback: writerCallback(...)); diff --git a/samples/Html/04_Table_Format_without_Conditional.php b/samples/Html/04_Table_Format_without_Conditional.php new file mode 100644 index 0000000000..396af5375a --- /dev/null +++ b/samples/Html/04_Table_Format_without_Conditional.php @@ -0,0 +1,25 @@ +isCli() ? ('samples/templates/' . $inputFileName) : ('' . 'samples/templates/' . $inputFileName . ''); +$helper->log('Read ' . $codePath); +$reader = IOFactory::createReader('Xlsx'); +$reader->setReadDataOnly(false); +$spreadsheet = $reader->load($inputFilePath); +$helper->log('Enable table formatting output'); + +function writerCallback(HtmlWriter $writer): void +{ + $writer->setPreCalculateFormulas(true); + $writer->setTableFormats(true); +} + +// Save +$helper->write($spreadsheet, __FILE__, ['Html'], false, writerCallback: writerCallback(...)); diff --git a/samples/Html/05_Table_Format_with_Conditional.php b/samples/Html/05_Table_Format_with_Conditional.php new file mode 100644 index 0000000000..bdfa6ab88c --- /dev/null +++ b/samples/Html/05_Table_Format_with_Conditional.php @@ -0,0 +1,27 @@ +isCli() ? ('samples/templates/' . $inputFileName) : ('' . 'samples/templates/' . $inputFileName . ''); +$helper->log('Read ' . $codePath); +$reader = IOFactory::createReader('Xlsx'); +$reader->setReadDataOnly(false); +$spreadsheet = $reader->load($inputFilePath); +$helper->log('Enable table formatting output'); +$helper->log('Enable conditional formatting output'); + +function writerCallback(HtmlWriter $writer): void +{ + $writer->setPreCalculateFormulas(true); + $writer->setTableFormats(true); + $writer->setConditionalFormatting(true); +} + +// Save +$helper->write($spreadsheet, __FILE__, ['Html'], false, writerCallback: writerCallback(...)); diff --git a/samples/templates/BasicConditionalFormatting.xlsx b/samples/templates/BasicConditionalFormatting.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..962c0583f3e7ddbb450d6ed72b3ecc7fd2ed5812 GIT binary patch literal 9048 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 MoQ5e8mb0_}2PZ?#oB#j- literal 0 HcmV?d00001 diff --git a/samples/templates/ColourScale.xlsx b/samples/templates/ColourScale.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..606f964dbdec040b628f43c6408cf1fd6fb3055e GIT binary patch literal 8798 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 $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles); $dxfs = $this->styleReader->dxfs($this->readDataOnly); + $tableStyles = $this->styleReader->tableStyles($this->readDataOnly); $styles = $this->styleReader->styles(); // Read content after setting the styles @@ -1000,7 +1001,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $this->readBackgroundImage($xmlSheetNS, $docSheet, dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels'); } - $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS); + $this->readTables($xmlSheetNS, $docSheet, $dir, $fileWorksheet, $zip, $mainNS, $tableStyles, $dxfs); if ($xmlSheetNS && $xmlSheetNS->mergeCells && $xmlSheetNS->mergeCells->mergeCell && !$this->readDataOnly) { foreach ($xmlSheetNS->mergeCells->mergeCell as $mergeCellx) { @@ -2311,12 +2312,14 @@ private function readTables( string $dir, string $fileWorksheet, ZipArchive $zip, - string $namespaceTable + string $namespaceTable, + array $tableStyles, + array $dxfs ): void { if ($xmlSheet && $xmlSheet->tableParts) { $attributes = $xmlSheet->tableParts->attributes() ?? ['count' => 0]; if (((int) $attributes['count']) > 0) { - $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable); + $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet, $namespaceTable, $tableStyles, $dxfs); } } } @@ -2327,7 +2330,9 @@ private function readTablesInTablesFile( string $fileWorksheet, ZipArchive $zip, Worksheet $docSheet, - string $namespaceTable + string $namespaceTable, + array $tableStyles, + array $dxfs ): void { foreach ($xmlSheet->tableParts->tablePart as $tablePart) { $relation = self::getAttributes($tablePart, Namespaces::SCHEMA_OFFICE_DOCUMENT); @@ -2346,7 +2351,7 @@ private function readTablesInTablesFile( if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) { $tableXml = $this->loadZip($relationshipFilePath, $namespaceTable); - (new TableReader($docSheet, $tableXml))->load(); + (new TableReader($docSheet, $tableXml))->load($tableStyles, $dxfs); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index a03fa71b24..436d9ffb83 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -193,6 +193,13 @@ private function setConditionalStyles(Worksheet $worksheet, array $conditionals, // N.B. In Excel UI, intersection is space and union is comma. // But in Xml, intersection is comma and union is space. $cellRangeReference = str_replace(['$', ' ', ',', '^'], ['', '^', ' ', ','], strtoupper($cellRangeReference)); + + foreach ($conditionalStyles as $cs) { + $scale = $cs->getColorScale(); + if ($scale !== null) { + $scale->setSqRef($cellRangeReference, $worksheet); + } + } $worksheet->getStyle($cellRangeReference)->setConditionalStyles($conditionalStyles); } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index 676ea81760..c1e27e9e32 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -12,6 +12,7 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Protection; use PhpOffice\PhpSpreadsheet\Style\Style; +use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableDxfsStyle; use SimpleXMLElement; use stdClass; @@ -447,6 +448,46 @@ public function dxfs(bool $readDataOnly = false): array return $dxfs; } + // get TableStyles + public function tableStyles(bool $readDataOnly = false): array + { + $tableStyles = []; + if (!$readDataOnly && $this->styleXml) { + // Conditional Styles + if ($this->styleXml->tableStyles) { + foreach ($this->styleXml->tableStyles->tableStyle as $s) { + $attrs = Xlsx::getAttributes($s); + if (isset($attrs['name'][0])) { + $style = new TableDxfsStyle((string) ($attrs['name'][0])); + foreach ($s->tableStyleElement as $e) { + $a = Xlsx::getAttributes($e); + if (isset($a['dxfId'][0], $a['type'][0])) { + switch ($a['type'][0]) { + case 'headerRow': + $style->setHeaderRow((int) ($a['dxfId'][0])); + + break; + case 'firstRowStripe': + $style->setFirstRowStripe((int) ($a['dxfId'][0])); + + break; + case 'secondRowStripe': + $style->setSecondRowStripe((int) ($a['dxfId'][0])); + + break; + default: + } + } + } + $tableStyles[] = $style; + } + } + } + } + + return $tableStyles; + } + public function styles(): array { return $this->styles; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php index a63c817d4f..c84b8198f6 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php @@ -25,20 +25,20 @@ public function __construct(Worksheet $workSheet, SimpleXMLElement $tableXml) /** * Loads Table into the Worksheet. */ - public function load(): void + public function load(array $tableStyles, array $dxfs): void { $this->tableAttributes = $this->tableXml->attributes() ?? []; // Remove all "$" in the table range $tableRange = (string) preg_replace('/\$/', '', $this->tableAttributes['ref'] ?? ''); if (str_contains($tableRange, ':')) { - $this->readTable($tableRange); + $this->readTable($tableRange, $tableStyles, $dxfs); } } /** * Read Table from xml. */ - private function readTable(string $tableRange): void + private function readTable(string $tableRange, array $tableStyles, array $dxfs): void { $table = new Table($tableRange); $table->setName((string) ($this->tableAttributes['displayName'] ?? '')); @@ -47,7 +47,7 @@ private function readTable(string $tableRange): void $this->readTableAutoFilter($table, $this->tableXml->autoFilter); $this->readTableColumns($table, $this->tableXml->tableColumns); - $this->readTableStyle($table, $this->tableXml->tableStyleInfo); + $this->readTableStyle($table, $this->tableXml->tableStyleInfo, $tableStyles, $dxfs); (new AutoFilter($table, $this->tableXml))->load(); $this->worksheet->addTable($table); @@ -100,7 +100,7 @@ private function readTableColumns(Table $table, SimpleXMLElement $tableColumnsXm /** * Reads TableStyle from xml. */ - private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml): void + private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml, array $tableStyles, array $dxfs): void { $tableStyle = new TableStyle(); $attributes = $tableStyleInfoXml->attributes(); @@ -110,6 +110,12 @@ private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXm $tableStyle->setShowColumnStripes((string) $attributes['showColumnStripes'] === '1'); $tableStyle->setShowFirstColumn((string) $attributes['showFirstColumn'] === '1'); $tableStyle->setShowLastColumn((string) $attributes['showLastColumn'] === '1'); + + foreach ($tableStyles as $style) { + if ($style->getName() === (string) $attributes['name']) { + $tableStyle->setTableDxfsStyle($style, $dxfs); + } + } } $table->setStyle($tableStyle); } diff --git a/src/PhpSpreadsheet/Style/Conditional.php b/src/PhpSpreadsheet/Style/Conditional.php index d476bdffd2..736b72be5e 100644 --- a/src/PhpSpreadsheet/Style/Conditional.php +++ b/src/PhpSpreadsheet/Style/Conditional.php @@ -269,8 +269,16 @@ public function addCondition($condition): static /** * Get Style. */ - public function getStyle(): Style + public function getStyle(mixed $cellData = null): Style { + if ($this->conditionType === self::CONDITION_COLORSCALE && $cellData !== null && $this->colorScale !== null && is_numeric($cellData)) { + $style = new Style(); + $style->getFill()->setFillType(Fill::FILL_SOLID); + $style->getFill()->getStartColor()->setARGB($this->colorScale->getColorForValue((float) $cellData)); + + return $style; + } + return $this->style; } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php index 61027975aa..6b6e59965f 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php @@ -111,6 +111,7 @@ public function evaluateConditional(Conditional $conditional): bool // Last 7 Days AND(TODAY()-FLOOR(,1)<=6,FLOOR(,1)<=TODAY()) Conditional::CONDITION_TIMEPERIOD, Conditional::CONDITION_EXPRESSION => $this->processExpression($conditional), + Conditional::CONDITION_COLORSCALE => $this->processColorScale($conditional), default => false, }; } @@ -141,8 +142,8 @@ protected function conditionCellAdjustment(array $matches): float|int|string { $column = $matches[6]; $row = $matches[7]; - if (!str_contains($column, '$')) { + // $column = Coordinate::stringFromColumnIndex($this->cellColumn); $column = Coordinate::columnIndexFromString($column); $column += $this->cellColumn - $this->referenceColumn; $column = Coordinate::stringFromColumnIndex($column); @@ -214,6 +215,15 @@ protected function processOperatorComparison(Conditional $conditional): bool return $this->evaluateExpression($expression); } + protected function processColorScale(Conditional $conditional): bool + { + if (is_numeric($this->wrapCellValue()) && $conditional->getColorScale()?->colorScaleReadyForUse()) { + return true; + } + + return false; + } + protected function processRangeOperator(Conditional $conditional): bool { $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions()); diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php index bcf59dee86..f8826f05fa 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php @@ -12,8 +12,11 @@ class CellStyleAssessor protected StyleMerger $styleMerger; + protected Cell $cell; + public function __construct(Cell $cell, string $conditionalRange) { + $this->cell = $cell; $this->cellMatcher = new CellMatcher($cell, $conditionalRange); $this->styleMerger = new StyleMerger($cell->getStyle()); } @@ -26,7 +29,7 @@ public function matchConditions(array $conditionalStyles = []): Style foreach ($conditionalStyles as $conditional) { if ($this->cellMatcher->evaluateConditional($conditional) === true) { // Merging the conditional style into the base style goes in here - $this->styleMerger->mergeStyle($conditional->getStyle()); + $this->styleMerger->mergeStyle($conditional->getStyle($this->cell->getValue())); if ($conditional->getStopIfTrue() === true) { break; } @@ -35,4 +38,28 @@ public function matchConditions(array $conditionalStyles = []): Style return $this->styleMerger->getStyle(); } + + /** + * @param Conditional[] $conditionalStyles + */ + public function matchConditionsReturnNullIfNoneMatched(array $conditionalStyles, string $cellData, bool $stopAtFirstMatch = false): ?Style + { + $matched = false; + $value = (float) $cellData; + foreach ($conditionalStyles as $conditional) { + if ($this->cellMatcher->evaluateConditional($conditional) === true) { + $matched = true; + // Merging the conditional style into the base style goes in here + $this->styleMerger->mergeStyle($conditional->getStyle($value)); + if ($conditional->getStopIfTrue() === true || $stopAtFirstMatch) { + break; + } + } + } + if ($matched) { + return $this->styleMerger->getStyle(); + } + + return null; + } } diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php index 7fcc08038d..e11abd122a 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalColorScale.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Percentiles; use PhpOffice\PhpSpreadsheet\Style\Color; class ConditionalColorScale @@ -18,6 +19,18 @@ class ConditionalColorScale private ?Color $maximumColor = null; + private ?string $sqref = null; + + private array $valueArray = []; + + private float $minValue = 0; + + private float $maxValue = 0; + + private float $midValue = 0; + + private ?\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet = null; + public function getMinimumConditionalFormatValueObject(): ?ConditionalFormatValueObject { return $this->minimumConditionalFormatValueObject; @@ -89,4 +102,155 @@ public function setMaximumColor(Color $maximumColor): self return $this; } + + public function getSqRef(): ?string + { + return $this->sqref; + } + + public function setSqRef(string $sqref, \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet): self + { + $this->sqref = $sqref; + $this->worksheet = $worksheet; + + return $this; + } + + public function setScaleArray(): self + { + if ($this->sqref !== null && $this->worksheet !== null) { + $values = $this->worksheet->rangesToArray($this->sqref, null, true, true, true); + $this->valueArray = []; + foreach ($values as $key => $value) { + foreach ($value as $k => $v) { + $this->valueArray[] = (float) $v; + } + } + $this->prepareColorScale(); + } + + return $this; + } + + public function getColorForValue(float $value): string + { + if ($this->minimumColor === null || $this->midpointColor === null || $this->maximumColor === null) { + return 'FF000000'; + } + $minColor = $this->minimumColor->getARGB(); + $midColor = $this->midpointColor->getARGB(); + $maxColor = $this->maximumColor->getARGB(); + + if ($minColor === null || $midColor === null || $maxColor === null) { + return 'FF000000'; + } + + if ($value <= $this->minValue) { + return $minColor; + } + if ($value >= $this->maxValue) { + return $maxColor; + } + if ($value == $this->midValue) { + return $midColor; + } + if ($value < $this->midValue) { + $blend = ($value - $this->minValue) / ($this->midValue - $this->minValue); + $alpha1 = hexdec(substr($minColor, 0, 2)); + $alpha2 = hexdec(substr($midColor, 0, 2)); + $red1 = hexdec(substr($minColor, 2, 2)); + $red2 = hexdec(substr($midColor, 2, 2)); + $green1 = hexdec(substr($minColor, 4, 2)); + $green2 = hexdec(substr($midColor, 4, 2)); + $blue1 = hexdec(substr($minColor, 6, 2)); + $blue2 = hexdec(substr($midColor, 6, 2)); + + return strtoupper(dechex((int) ($alpha2 * $blend + $alpha1 * (1 - $blend))) . '' . dechex((int) ($red2 * $blend + $red1 * (1 - $blend))) . '' . dechex((int) ($green2 * $blend + $green1 * (1 - $blend))) . '' . dechex((int) ($blue2 * $blend + $blue1 * (1 - $blend)))); + } + $blend = ($value - $this->midValue) / ($this->maxValue - $this->midValue); + $alpha1 = hexdec(substr($midColor, 0, 2)); + $alpha2 = hexdec(substr($maxColor, 0, 2)); + $red1 = hexdec(substr($midColor, 2, 2)); + $red2 = hexdec(substr($maxColor, 2, 2)); + $green1 = hexdec(substr($midColor, 4, 2)); + $green2 = hexdec(substr($maxColor, 4, 2)); + $blue1 = hexdec(substr($midColor, 6, 2)); + $blue2 = hexdec(substr($maxColor, 6, 2)); + + return strtoupper(dechex((int) ($alpha2 * $blend + $alpha1 * (1 - $blend))) . '' . dechex((int) ($red2 * $blend + $red1 * (1 - $blend))) . '' . dechex((int) ($green2 * $blend + $green1 * (1 - $blend))) . '' . dechex((int) ($blue2 * $blend + $blue1 * (1 - $blend)))); + } + + private function getLimitValue(string $type, float $value = 0, float $formula = 0): float + { + if (count($this->valueArray) === 0) { + return 0; + } + switch ($type) { + case 'min': + return (float) min($this->valueArray); + case 'max': + return (float) max($this->valueArray); + case 'percentile': + return (float) Percentiles::PERCENTILE($this->valueArray, (float) ($value / 100)); + case 'formula': + return $formula; + case 'percent': + $min = (float) min($this->valueArray); + $max = (float) max($this->valueArray); + + return $min + (float) ($value / 100) * ($max - $min); + default: + return 0; + } + } + + /** + * Prepares color scale for execution, see the first if for variables that must be set beforehand. + */ + public function prepareColorScale(): self + { + if ($this->minimumConditionalFormatValueObject !== null && $this->maximumConditionalFormatValueObject !== null && $this->minimumColor !== null && $this->maximumColor !== null) { + if ($this->midpointConditionalFormatValueObject !== null && $this->midpointConditionalFormatValueObject->getType() !== 'None') { + $this->minValue = $this->getLimitValue($this->minimumConditionalFormatValueObject->getType(), (float) $this->minimumConditionalFormatValueObject->getValue(), (float) $this->minimumConditionalFormatValueObject->getCellFormula()); + $this->midValue = $this->getLimitValue($this->midpointConditionalFormatValueObject->getType(), (float) $this->midpointConditionalFormatValueObject->getValue(), (float) $this->midpointConditionalFormatValueObject->getCellFormula()); + $this->maxValue = $this->getLimitValue($this->maximumConditionalFormatValueObject->getType(), (float) $this->maximumConditionalFormatValueObject->getValue(), (float) $this->maximumConditionalFormatValueObject->getCellFormula()); + } else { + $this->minValue = $this->getLimitValue($this->minimumConditionalFormatValueObject->getType(), (float) $this->minimumConditionalFormatValueObject->getValue(), (float) $this->minimumConditionalFormatValueObject->getCellFormula()); + $this->maxValue = $this->getLimitValue($this->maximumConditionalFormatValueObject->getType(), (float) $this->maximumConditionalFormatValueObject->getValue(), (float) $this->maximumConditionalFormatValueObject->getCellFormula()); + $this->midValue = (float) ($this->minValue + $this->maxValue) / 2; + $blend = 0.5; + + $minColor = $this->minimumColor->getARGB(); + $maxColor = $this->maximumColor->getARGB(); + + if ($minColor !== null && $maxColor !== null) { + $alpha1 = hexdec(substr($minColor, 0, 2)); + $alpha2 = hexdec(substr($maxColor, 0, 2)); + $red1 = hexdec(substr($minColor, 2, 2)); + $red2 = hexdec(substr($maxColor, 2, 2)); + $green1 = hexdec(substr($minColor, 4, 2)); + $green2 = hexdec(substr($maxColor, 4, 2)); + $blue1 = hexdec(substr($minColor, 6, 2)); + $blue2 = hexdec(substr($maxColor, 6, 2)); + $this->midpointColor = new Color(strtoupper(dechex((int) ($alpha2 * $blend + $alpha1 * (1 - $blend))) . '' . dechex((int) ($red2 * $blend + $red1 * (1 - $blend))) . '' . dechex((int) ($green2 * $blend + $green1 * (1 - $blend))) . '' . dechex((int) ($blue2 * $blend + $blue1 * (1 - $blend))))); + } else { + $this->midpointColor = null; + } + } + } + + return $this; + } + + /** + * Checks that all needed color scale data is in place. + */ + public function colorScaleReadyForUse(): bool + { + if ($this->minimumColor === null || $this->midpointColor === null || $this->maximumColor === null) { + return false; + } + + return true; + } } diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 7f5b876eee..072beab2d6 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -540,6 +540,19 @@ public function setAutoFilter(AutoFilter $autoFilter): self return $this; } + /** + * Get the row number on this table for given coordinates. + */ + public function getRowNumber(string $coordinate): int + { + $range = $this->getRange(); + $coords = Coordinate::splitRange($range); + $firstCell = Coordinate::coordinateFromString($coords[0][0]); + $thisCell = Coordinate::coordinateFromString($coordinate); + + return (int) $thisCell[1] - (int) $firstCell[1]; + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ diff --git a/src/PhpSpreadsheet/Worksheet/Table/TableDxfsStyle.php b/src/PhpSpreadsheet/Worksheet/Table/TableDxfsStyle.php new file mode 100644 index 0000000000..e674a71463 --- /dev/null +++ b/src/PhpSpreadsheet/Worksheet/Table/TableDxfsStyle.php @@ -0,0 +1,170 @@ +name = $name; + } + + /** + * Get name. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Set header row dxfs index. + */ + public function setHeaderRow(int $row): self + { + $this->headerRow = $row; + + return $this; + } + + /** + * Get header row dxfs index. + */ + public function getHeaderRow(): ?int + { + return $this->headerRow; + } + + /** + * Set first row stripe dxfs index. + */ + public function setFirstRowStripe(int $row): self + { + $this->firstRowStripe = $row; + + return $this; + } + + /** + * Get first row stripe dxfs index. + */ + public function getFirstRowStripe(): ?int + { + return $this->firstRowStripe; + } + + /** + * Set second row stripe dxfs index. + */ + public function setSecondRowStripe(int $row): self + { + $this->secondRowStripe = $row; + + return $this; + } + + /** + * Get second row stripe dxfs index. + */ + public function getSecondRowStripe(): ?int + { + return $this->secondRowStripe; + } + + /** + * Set Header row Style. + */ + public function setHeaderRowStyle(Style $style): self + { + $this->headerRowStyle = $style; + + return $this; + } + + /** + * Get Header row Style. + */ + public function getHeaderRowStyle(): ?Style + { + return $this->headerRowStyle; + } + + /** + * Set first row stripe Style. + */ + public function setFirstRowStripeStyle(Style $style): self + { + $this->firstRowStripeStyle = $style; + + return $this; + } + + /** + * Get first row stripe Style. + */ + public function getFirstRowStripeStyle(): ?Style + { + return $this->firstRowStripeStyle; + } + + /** + * Set second row stripe Style. + */ + public function setSecondRowStripeStyle(Style $style): self + { + $this->secondRowStripeStyle = $style; + + return $this; + } + + /** + * Get second row stripe Style. + */ + public function getSecondRowStripeStyle(): ?Style + { + return $this->secondRowStripeStyle; + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php b/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php index 81153027da..2c0173c19c 100644 --- a/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php +++ b/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php @@ -93,6 +93,11 @@ class TableStyle */ private bool $showColumnStripes = false; + /** + * TableDxfsStyle. + */ + private ?TableDxfsStyle $tableStyle = null; + /** * Table. */ @@ -198,6 +203,34 @@ public function setShowColumnStripes(bool $showColumnStripes): self return $this; } + /** + * Get this Style's Dxfs TableStyle. + */ + public function getTableDxfsStyle(): ?TableDxfsStyle + { + return $this->tableStyle; + } + + /** + * Set this Style's Dxfs TableStyle. + */ + public function setTableDxfsStyle(TableDxfsStyle $tableStyle, array $dxfs): self + { + $this->tableStyle = $tableStyle; + + if ($this->tableStyle->getHeaderRow() !== null && isset($dxfs[$this->tableStyle->getHeaderRow()])) { + $this->tableStyle->setHeaderRowStyle($dxfs[$this->tableStyle->getHeaderRow()]); + } + if ($this->tableStyle->getFirstRowStripe() !== null && isset($dxfs[$this->tableStyle->getFirstRowStripe()])) { + $this->tableStyle->setFirstRowStripeStyle($dxfs[$this->tableStyle->getFirstRowStripe()]); + } + if ($this->tableStyle->getSecondRowStripe() !== null && isset($dxfs[$this->tableStyle->getSecondRowStripe()])) { + $this->tableStyle->setSecondRowStripeStyle($dxfs[$this->tableStyle->getSecondRowStripe()]); + } + + return $this; + } + /** * Get this Style's Table. */ diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index daa1853bba..60bf3b12ab 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1414,6 +1414,32 @@ public function getStyle(AddressRange|CellAddress|int|string|array $cellCoordina return $this->getParentOrThrow()->getCellXfSupervisor(); } + /** + * Get table styles set for the for given cell. + * + * @param Cell $cell + * The Cell for which the tables are retrieved + */ + public function getTablesWithStylesForCell(Cell $cell): array + { + $retVal = []; + + foreach ($this->tableCollection as $table) { + /** @var Table $table */ + $dxfsTableStyle = $table->getStyle()->getTableDxfsStyle(); + if ($dxfsTableStyle !== null) { + if ($dxfsTableStyle->getHeaderRowStyle() !== null || $dxfsTableStyle->getFirstRowStripeStyle() !== null || $dxfsTableStyle->getSecondRowStripeStyle() !== null) { + $range = $table->getRange(); + if ($cell->isInRange($range)) { + $retVal[] = $table; + } + } + } + } + + return $retVal; + } + /** * Get conditional styles for a cell. * @@ -2888,6 +2914,40 @@ public function rangeToArray( return $returnValue; } + /** + * Create array from a multiple ranges of cells. (such as A1:A3,A15,B17:C17). + * + * @param null|bool|float|int|RichText|string $nullValue Value returned in the array entry if a cell doesn't exist + * @param bool $calculateFormulas Should formulas be calculated? + * @param bool $formatData Should formatting be applied to cell values? + * @param bool $returnCellRef False - Return a simple array of rows and columns indexed by number counting from zero + * True - Return rows and columns indexed by their actual row and column IDs + * @param bool $ignoreHidden False - Return values for rows/columns even if they are defined as hidden. + * True - Don't return values for rows/columns that are defined as hidden. + */ + public function rangesToArray( + string $ranges, + mixed $nullValue = null, + bool $calculateFormulas = true, + bool $formatData = true, + bool $returnCellRef = false, + bool $ignoreHidden = false, + bool $reduceArrays = false + ): array { + $returnValue = []; + + $parts = explode(',', $ranges); + foreach ($parts as $part) { + // Loop through rows + foreach ($this->rangeToArrayYieldRows($part, $nullValue, $calculateFormulas, $formatData, $returnCellRef, $ignoreHidden, $reduceArrays) as $rowRef => $rowArray) { + $returnValue[$rowRef] = $rowArray; + } + } + + // Return + return $returnValue; + } + /** * Create array from a range of cells, yielding each row in turn. * diff --git a/src/PhpSpreadsheet/Writer/BaseWriter.php b/src/PhpSpreadsheet/Writer/BaseWriter.php index 5e6d3cd49e..50df5b167a 100644 --- a/src/PhpSpreadsheet/Writer/BaseWriter.php +++ b/src/PhpSpreadsheet/Writer/BaseWriter.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Writer; +use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; + abstract class BaseWriter implements IWriter { /** @@ -17,6 +19,18 @@ abstract class BaseWriter implements IWriter */ protected bool $preCalculateFormulas = true; + /** + * Table formats + * Enables table formats in writer, disabled here, must be enabled in writer via a setter. + */ + protected bool $tableFormats = false; + + /** + * Conditional Formatting + * Enables conditional formatting in writer, disabled here, must be enabled in writer via a setter. + */ + protected bool $conditionalFormatting = false; + /** * Use disk caching where possible? */ @@ -58,6 +72,34 @@ public function setPreCalculateFormulas(bool $precalculateFormulas): self return $this; } + public function getTableFormats(): bool + { + return $this->tableFormats; + } + + public function setTableFormats(bool $tableFormats): self + { + if ($tableFormats) { + throw new PhpSpreadsheetException('Table formatting not implemented for this writer'); + } + + return $this; + } + + public function getConditionalFormatting(): bool + { + return $this->conditionalFormatting; + } + + public function setConditionalFormatting(bool $conditionalFormatting): self + { + if ($conditionalFormatting) { + throw new PhpSpreadsheetException('Conditional Formatting not implemented for this writer'); + } + + return $this; + } + public function getUseDiskCaching(): bool { return $this->useDiskCaching; diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index bc26c0ff28..4d7bfcafbe 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -4,6 +4,7 @@ use Composer\Pcre\Preg; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; @@ -22,6 +23,9 @@ use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Borders; +use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\StyleMerger; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; @@ -162,13 +166,10 @@ public function __construct(Spreadsheet $spreadsheet) public function save($filename, int $flags = 0): void { $this->processFlags($flags); - // Open file $this->openFileHandle($filename); - // Write html fwrite($this->fileHandle, $this->generateHTMLAll()); - // Close file $this->maybeCloseFileHandle(); } @@ -473,12 +474,24 @@ public function generateSheetData(): string // Loop all sheets $sheetId = 0; + + $activeSheet = $this->spreadsheet->getActiveSheetIndex(); + foreach ($sheets as $sheet) { + // save active cells + $selectedCells = $sheet->getSelectedCells(); // Write table header $html .= $this->generateTableHeader($sheet); $this->sheetCharts = []; $this->sheetDrawings = []; - + $condStylesCollection = $sheet->getConditionalStylesCollection(); + foreach ($condStylesCollection as $condStyles) { + foreach ($condStyles as $key => $cs) { + if ($cs->getConditionType() === Conditional::CONDITION_COLORSCALE) { + $cs->getColorScale()->setScaleArray(); + } + } + } // Get worksheet dimension [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension()); [$minCol, $minRow, $minColString] = Coordinate::indexesFromString($min); @@ -486,7 +499,6 @@ public function generateSheetData(): string $this->extendRowsAndColumns($sheet, $maxCol, $maxRow); [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow); - // Loop through cells $row = $minRow - 1; while ($row++ < $maxRow) { @@ -514,7 +526,6 @@ public function generateSheetData(): string $html .= $endTag; } - // Write table footer $html .= $this->generateTableFooter(); // Writing PDF? @@ -526,7 +537,9 @@ public function generateSheetData(): string // Next sheet ++$sheetId; + $sheet->setSelectedCells($selectedCells); } + $this->spreadsheet->setActiveSheetIndex($activeSheet); return $html; } @@ -1356,7 +1369,11 @@ private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, stri $cellData .= $this->generateRowCellDataValueRich($cell->getValue()); } else { if ($this->preCalculateFormulas) { - $origData = $cell->getCalculatedValue(); + try { + $origData = $cell->getCalculatedValue(); + } catch (CalculationException $exception) { + $origData = '#ERROR'; // mark as error, rather than crash everything + } if ($this->betterBoolean && is_bool($origData)) { $origData2 = $origData ? $this->getTrue : $this->getFalse; } else { @@ -1473,7 +1490,8 @@ private function generateRowWriteCell( array|string $cssClass, int $colNum, int $sheetIndex, - int $row + int $row, + array $condStyles = [] ): void { // Image? $htmlx = $this->writeImageInCell($coordinate); @@ -1540,8 +1558,57 @@ private function generateRowWriteCell( $html .= ' class="gridlines gridlinesp"'; } } + $html = $this->generateRowSpans($html, $rowSpan, $colSpan); + $tables = $worksheet->getTablesWithStylesForCell($worksheet->getCell($coordinate)); + if (count($tables) > 0 || count($condStyles) > 0) { + $matched = false; // TODO the style gotten from the merger overrides everything + $styleMerger = new StyleMerger($worksheet->getCell($coordinate)->getStyle()); + if ($this->tableFormats) { + if (count($tables) > 0) { + foreach ($tables as $ts) { + $dxfsTableStyle = $ts->getStyle()->getTableDxfsStyle(); + if ($dxfsTableStyle !== null) { + $tableRow = $ts->getRowNumber($coordinate); + if ($tableRow === 0 && $dxfsTableStyle->getHeaderRowStyle() !== null) { + $styleMerger->mergeStyle($dxfsTableStyle->getHeaderRowStyle()); + $matched = true; + } elseif ($tableRow % 2 === 1 && $dxfsTableStyle->getFirstRowStripeStyle() !== null) { + $styleMerger->mergeStyle($dxfsTableStyle->getFirstRowStripeStyle()); + $matched = true; + } elseif ($tableRow % 2 === 0 && $dxfsTableStyle->getSecondRowStripeStyle() !== null) { + $styleMerger->mergeStyle($dxfsTableStyle->getSecondRowStripeStyle()); + $matched = true; + } + } + } + } + } + if (count($condStyles) > 0 && $this->conditionalFormatting) { + if ($worksheet->getConditionalRange($coordinate) !== null) { + $assessor = new CellStyleAssessor($worksheet->getCell($coordinate), $worksheet->getConditionalRange($coordinate)); + } else { + $assessor = new CellStyleAssessor($worksheet->getCell($coordinate), $coordinate); + } + $matchedStyle = $assessor->matchConditionsReturnNullIfNoneMatched($condStyles, $cellData, true); + + if ($matchedStyle !== null) { + $matched = true; + // this is really slow + $styleMerger->mergeStyle($matchedStyle); + } + } + if ($matched) { + $styles = $this->createCSSStyle($styleMerger->getStyle()); + $html .= ' style="'; + foreach ($styles as $key => $value) { + $html .= $key . ':' . $value . ';'; + } + $html .= '"'; + } + } + $html .= '>'; $html .= $htmlx; @@ -1587,6 +1654,9 @@ private function generateRow(Worksheet $worksheet, array $values, int $row, stri // Cell Data $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass); + // Get an array of all styles + $condStyles = $worksheet->getStyle($coordinate)->getConditionalStyles(); + // Hyperlink? if ($worksheet->hyperlinkExists($coordinate) && !$worksheet->getHyperlink($coordinate)->isInternal()) { $url = $worksheet->getHyperlink($coordinate)->getUrl(); @@ -1636,7 +1706,7 @@ private function generateRow(Worksheet $worksheet, array $values, int $row, stri // Write if ($writeCell) { - $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row); + $this->generateRowWriteCell($html, $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row, $condStyles); } // Next column @@ -1738,6 +1808,20 @@ public function setUseInlineCss(bool $useInlineCss): static return $this; } + public function setTableFormats(bool $tableFormats): self + { + $this->tableFormats = $tableFormats; + + return $this; + } + + public function setConditionalFormatting(bool $conditionalFormatting): self + { + $this->conditionalFormatting = $conditionalFormatting; + + return $this; + } + /** * Add color to formatted string as inline style. * diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php new file mode 100644 index 0000000000..979e75bb0c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php @@ -0,0 +1,79 @@ +load($file); + $writer = new HtmlWriter($spreadsheet); + $writer->setConditionalFormatting(true); + $this->data = $writer->generateHtmlAll(); + $spreadsheet->disconnectWorksheets(); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('colourScaleProvider')] + public function testColourScaleHtmlOutput(int $rowNumber, array $expectedMatches): void + { + self::assertSame(1, preg_match('~~ms', $this->data, $matches)); + foreach ($expectedMatches as $i => $expected) { + self::assertStringContainsString($expected, $matches[0]); + } + } + + public static function colourScaleProvider(): array + { + return [ + 'row 0: low/high min/max with 80% midpoint' => [0, ['1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10']], + 'row 1: low/high 40%/80% with 50% midpoint' => [1, ['1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10']], + 'row 2: low/high/midpoint values 3/8/4 ' => [2, ['1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10']], + 'row 3: low/high with 30/80 percentile and 50% midpoint, one cell no value' => [3, ['1', + '2', + '3', + '4', + '2', + '9', + '9', + '9', + '', + '10']]]; + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php new file mode 100644 index 0000000000..0339248ca9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php @@ -0,0 +1,65 @@ +load($file); + $writer = new HtmlWriter($spreadsheet); + $writer->setConditionalFormatting(true); + $this->data = $writer->generateHtmlAll(); + $spreadsheet->disconnectWorksheets(); + } + + private function extractCell(string $coordinate): string + { + [$column, $row] = Coordinate::indexesFromString($coordinate); + --$column; + --$row; + // extract row into $matches + $match = preg_match('~~s', $this->data, $matches); + if ($match !== 1) { + return 'unable to match row'; + } + $rowData = $matches[0]; + // extract cell into $matches + $match = preg_match('~Jan<', 'no conditional styling for B1'], + ['F2', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#006100;font-family:\'Arial\';font-size:11pt;background-color:#C6EFCE;">120<', 'conditional style for F2'], + ['H2', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#9C5700;font-family:\'Arial\';font-size:11pt;background-color:#FFEB9C;">90<', 'conditional style for H2'], + ['F3', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#006100;font-family:\'Arial\';font-size:11pt;background-color:#C6EFCE;">70<', 'conditional style for cell F3'], + ['H3', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#9C5700;font-family:\'Arial\';font-size:11pt;background-color:#FFEB9C;">60<', 'conditional style for cell H3'], + ['F4', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#006100;font-family:\'Arial\';font-size:11pt;background-color:#C6EFCE;">1<', 'conditional style for cell F4'], + ['L4', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#9C0006;font-family:\'Arial\';font-size:11pt;background-color:#FFC7CE;">5<', 'conditional style for cell L4'], + ['F5', 'class="column5 style1 n">0<', 'no conditional styling for F5'], + ]; + foreach ($expectedMatches as $expected) { + [$coordinate, $expectedString, $message] = $expected; + $string = $this->extractCell($coordinate); + self::assertStringContainsString($expectedString, $string, $message); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php new file mode 100644 index 0000000000..38b7e976d2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php @@ -0,0 +1,94 @@ +load($file); + $writer = new HtmlWriter($spreadsheet); + $writer->setConditionalFormatting(true); + $this->data = $writer->generateHtmlAll(); + $spreadsheet->disconnectWorksheets(); + } + + private function extractCell(string $coordinate): string + { + [$column, $row] = Coordinate::indexesFromString($coordinate); + --$column; + --$row; + // extract row into $matches + $match = preg_match('~~s', $this->data, $matches); + if ($match !== 1) { + return 'unable to match row'; + } + $rowData = $matches[0]; + // extract cell into $matches + $match = preg_match('~1<', 'A1 equals hit'], + ['B1', 'class="column1 style1 n">2<', 'B1 equals miss'], + ['E1', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">1<', 'E1 equals horizontal reference hit'], + ['F1', 'class="column5 style1 n">2<', 'F1 equals horizontal reference miss'], + ['G1', 'class="column6 style1 n">3<', 'G1 equals horizontal reference miss'], + ['A2', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A2 text contains hit'], + ['B2', 'class="column1 style1 s">moi<', 'B2 text contains miss'], + ['A3', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A3 text does not contain hit'], + ['B3', 'class="column1 style1 s">moi<', 'B2 text does not contain miss'], + ['A4', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A4 text starts with hit'], + ['B4', 'class="column1 style1 s">moi<', 'B2 text starts with miss'], + ['A5', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A5 text ends with hit'], + ['B5', 'class="column1 style1 s">moi<', 'B5 text ends with miss'], + ['A6', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">2025/01/01<', 'A6 date after hit'], + ['B6', 'class="column1 style2 n">2020/01/01<', 'B6 date after miss'], + ['A7', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve vaan<', 'A7 text contains hit'], + ['B7', 'class="column1 style1 s">moi<', 'B7 text contains miss'], + ['A8', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A8 text does not contain hit'], + ['B8', 'class="column1 style1 s">terve vaan<', 'B2 does not contain miss'], + ['A9', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">#DIV/0!<', 'A10 own formula is error hit'], + ['B9', 'class="column1 style1 s">moi<', 'B9 own formula is error miss'], + ['A10', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">moi<', 'A10 own formula is not error hit'], + ['B10', 'class="column1 style3 s">#DIV/0!<', 'B10 own formula is not error miss'], + ['A11', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A11 own formula count instances of cell on line and hit when more than one hit'], + ['B11', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'B11 own formula count instances of cell on line and hit when more than one hit'], + ['C11', 'class="column2 style1 s">moi<', 'C11 own formula count instances of cell on line and hit when more than one miss'], + ['A12', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">moi<', 'A12 own formula count instances of cell on line and hit when at most 1 hit'], + ['B12', 'class="column1 style1 s">terve<', 'B12 own formula count instances of cell on line and hit when at most 1 miss'], + ['C12', 'class="column2 style1 s">terve<', 'C11 own formula count instances of cell on line and hit when at most 1 miss'], + ['A13', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">12<', 'A13 own formula self reference hit'], + ['B13', 'class="column1 style1 n">10<', 'B13 own formula self reference miss'], + ['A14', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">10<', 'A14 multiple conditional hits'], + ['B14', 'class="column1 style1 n">1<', 'B14 multiple conditionals miss'], + ['F7', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">1<', 'F7 equals vertical reference hit'], + ['F8', 'class="column5 style1 n">2<', 'F8 equals vertical reference miss'], + ['F9', 'class="column5 style1 n">3<', 'F9 equals vertical reference miss'], + ['F10', 'class="column5 style1 n">4<', 'F10 equals vertical reference miss'], + ]; + foreach ($expectedMatches as $expected) { + [$coordinate, $expectedString, $message] = $expected; + $string = $this->extractCell($coordinate); + self::assertStringContainsString($expectedString, $string, $message); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatTest.php new file mode 100644 index 0000000000..c78dda2591 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatTest.php @@ -0,0 +1,64 @@ +load($file); + $writer = new HtmlWriter($spreadsheet); + $writer->setTableFormats(true); + $this->data = $writer->generateHtmlAll(); + $spreadsheet->disconnectWorksheets(); + } + + private function extractCell(string $coordinate): string + { + [$column, $row] = Coordinate::indexesFromString($coordinate); + --$column; + --$row; + // extract row into $matches + $match = preg_match('~~s', $this->data, $matches); + if ($match !== 1) { + return 'unable to match row'; + } + $rowData = $matches[0]; + // extract cell into $matches + $match = preg_match('~Sep<', 'table style for header row cell J1'], + ['J2', 'background-color:#C0E4F5;">110<', 'table style for cell J2'], + ['I3', 'background-color:#82CAEB;">70<', 'table style for cell I3'], + ['J3', 'background-color:#82CAEB;">70<', 'table style for cell J3'], // as conditional calculations are off + ['K3', 'background-color:#82CAEB;">70<', 'table style for cell K3'], + ['J4', 'background-color:#C0E4F5;">1<', 'table style for cell J4'], + ['J5', 'background-color:#82CAEB;">1<', 'table style for cell J5'], + ]; + foreach ($expectedMatches as $expected) { + [$coordinate, $expectedString, $message] = $expected; + $string = $this->extractCell($coordinate); + self::assertStringContainsString($expectedString, $string, $message); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatWithConditionalTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatWithConditionalTest.php new file mode 100644 index 0000000000..1c8a16d27e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlTableFormatWithConditionalTest.php @@ -0,0 +1,65 @@ +load($file); + $writer = new HtmlWriter($spreadsheet); + $writer->setTableFormats(true); + $writer->setConditionalFormatting(true); + $this->data = $writer->generateHtmlAll(); + $spreadsheet->disconnectWorksheets(); + } + + private function extractCell(string $coordinate): string + { + [$column, $row] = Coordinate::indexesFromString($coordinate); + --$column; + --$row; + // extract row into $matches + $match = preg_match('~~s', $this->data, $matches); + if ($match !== 1) { + return 'unable to match row'; + } + $rowData = $matches[0]; + // extract cell into $matches + $match = preg_match('~Sep<', 'table style for header row cell J1'], + ['J2', 'background-color:#C0E4F5;">110<', 'table style for cell J2'], + ['I3', 'background-color:#82CAEB;">70<', 'table style for cell I3'], + ['J3', 'background-color:#B7E1CD;">70<', 'conditional style for cell J3'], // as conditional calculations are on + ['K3', 'background-color:#82CAEB;">70<', 'table style for cell K3'], + ['J4', 'background-color:#C0E4F5;">1<', 'table style for cell J4'], + ['J5', 'background-color:#82CAEB;">1<', 'table style for cell J5'], + ]; + foreach ($expectedMatches as $expected) { + [$coordinate, $expectedString, $message] = $expected; + $string = $this->extractCell($coordinate); + self::assertStringContainsString($expectedString, $string, $message); + } + } +} From 584c8662d77332aa6c0b16f2e66be44b38ecfe35 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Wed, 26 Mar 2025 23:47:03 -0700 Subject: [PATCH 2/2] Final Touch-up --- ... html_01_Basic_Conditional_Formatting.php} | 0 ...> html_02_More_Conditional_Formatting.php} | 0 ...olor_Scale.php => html_03_Color_Scale.php} | 0 ...l_04_Table_Format_without_Conditional.php} | 0 ...html_05_Table_Format_with_Conditional.php} | 0 .../Writer/Html/HtmlColourScaleTest.php | 84 ++++++++----------- .../Html/HtmlConditionalFormattingTest.php | 12 +-- ...tmlDifferentConditionalFormattingsTest.php | 34 ++++---- 8 files changed, 60 insertions(+), 70 deletions(-) rename samples/Html/{01_Basic_Conditional_Formatting.php => html_01_Basic_Conditional_Formatting.php} (100%) rename samples/Html/{02_More_Conditional_Formatting.php => html_02_More_Conditional_Formatting.php} (100%) rename samples/Html/{03_Color_Scale.php => html_03_Color_Scale.php} (100%) rename samples/Html/{04_Table_Format_without_Conditional.php => html_04_Table_Format_without_Conditional.php} (100%) rename samples/Html/{05_Table_Format_with_Conditional.php => html_05_Table_Format_with_Conditional.php} (100%) diff --git a/samples/Html/01_Basic_Conditional_Formatting.php b/samples/Html/html_01_Basic_Conditional_Formatting.php similarity index 100% rename from samples/Html/01_Basic_Conditional_Formatting.php rename to samples/Html/html_01_Basic_Conditional_Formatting.php diff --git a/samples/Html/02_More_Conditional_Formatting.php b/samples/Html/html_02_More_Conditional_Formatting.php similarity index 100% rename from samples/Html/02_More_Conditional_Formatting.php rename to samples/Html/html_02_More_Conditional_Formatting.php diff --git a/samples/Html/03_Color_Scale.php b/samples/Html/html_03_Color_Scale.php similarity index 100% rename from samples/Html/03_Color_Scale.php rename to samples/Html/html_03_Color_Scale.php diff --git a/samples/Html/04_Table_Format_without_Conditional.php b/samples/Html/html_04_Table_Format_without_Conditional.php similarity index 100% rename from samples/Html/04_Table_Format_without_Conditional.php rename to samples/Html/html_04_Table_Format_without_Conditional.php diff --git a/samples/Html/05_Table_Format_with_Conditional.php b/samples/Html/html_05_Table_Format_with_Conditional.php similarity index 100% rename from samples/Html/05_Table_Format_with_Conditional.php rename to samples/Html/html_05_Table_Format_with_Conditional.php diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php index 979e75bb0c..87a9fb6983 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlColourScaleTest.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Writer\Html; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter; use PHPUnit\Framework\TestCase; @@ -23,57 +24,46 @@ protected function setUp(): void $spreadsheet->disconnectWorksheets(); } - #[\PHPUnit\Framework\Attributes\DataProvider('colourScaleProvider')] - public function testColourScaleHtmlOutput(int $rowNumber, array $expectedMatches): void + private function extractCell(string $coordinate): string { - self::assertSame(1, preg_match('~~ms', $this->data, $matches)); - foreach ($expectedMatches as $i => $expected) { - self::assertStringContainsString($expected, $matches[0]); + [$column, $row] = Coordinate::indexesFromString($coordinate); + --$column; + --$row; + // extract row into $matches + $match = preg_match('~~s', $this->data, $matches); + if ($match !== 1) { + return 'unable to match row'; } + $rowData = $matches[0]; + // extract cell into $matches + $match = preg_match('~1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10']], - 'row 1: low/high 40%/80% with 50% midpoint' => [1, ['1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10']], - 'row 2: low/high/midpoint values 3/8/4 ' => [2, ['1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10']], - 'row 3: low/high with 30/80 percentile and 50% midpoint, one cell no value' => [3, ['1', - '2', - '3', - '4', - '2', - '9', - '9', - '9', - '', - '10']]]; + $expectedMatches = [ + ['E1', 'background-color:#B4CA76;">5<', 'cell E1'], + ['F1', 'background-color:#CBCD71;">6<', 'cell F1'], + ['G1', 'background-color:#E3D16C;">7<', 'cell G1'], + ['D2', 'background-color:#57BB8A;">4<', 'cell D2'], + ['E2', 'background-color:#A1C77A;">5<', 'cell E2'], + ['F2', 'background-color:#F1A36D;">6<', 'cell F2'], + ['D3', 'background-color:#FFD666;">4<', 'cell D3'], + ['G3', 'background-color:#EC926F;">7<', 'cell G3'], + ['H3', 'background-color:#E67C73;">8<', 'cell H3'], + ['A4', 'background-color:#57BB8A;">1<', 'cell A4'], + ['I4', 'null"><', 'empty cell I4'], + ['J4', 'background-color:#E67C73;">10<', 'cell J4'], + ]; + foreach ($expectedMatches as $expected) { + [$coordinate, $expectedString, $message] = $expected; + $string = $this->extractCell($coordinate); + self::assertStringContainsString($expectedString, $string, $message); + } } } diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php index 0339248ca9..c5a31e415c 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlConditionalFormattingTest.php @@ -48,12 +48,12 @@ public function testConditionalFormattingHtmLOutput(): void { $expectedMatches = [ ['B1', 'class="column1 style1 s">Jan<', 'no conditional styling for B1'], - ['F2', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#006100;font-family:\'Arial\';font-size:11pt;background-color:#C6EFCE;">120<', 'conditional style for F2'], - ['H2', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#9C5700;font-family:\'Arial\';font-size:11pt;background-color:#FFEB9C;">90<', 'conditional style for H2'], - ['F3', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#006100;font-family:\'Arial\';font-size:11pt;background-color:#C6EFCE;">70<', 'conditional style for cell F3'], - ['H3', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#9C5700;font-family:\'Arial\';font-size:11pt;background-color:#FFEB9C;">60<', 'conditional style for cell H3'], - ['F4', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#006100;font-family:\'Arial\';font-size:11pt;background-color:#C6EFCE;">1<', 'conditional style for cell F4'], - ['L4', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#9C0006;font-family:\'Arial\';font-size:11pt;background-color:#FFC7CE;">5<', 'conditional style for cell L4'], + ['F2', 'background-color:#C6EFCE;">120<', 'conditional style for F2'], + ['H2', 'background-color:#FFEB9C;">90<', 'conditional style for H2'], + ['F3', 'background-color:#C6EFCE;">70<', 'conditional style for cell F3'], + ['H3', 'background-color:#FFEB9C;">60<', 'conditional style for cell H3'], + ['F4', 'background-color:#C6EFCE;">1<', 'conditional style for cell F4'], + ['L4', 'background-color:#FFC7CE;">5<', 'conditional style for cell L4'], ['F5', 'class="column5 style1 n">0<', 'no conditional styling for F5'], ]; foreach ($expectedMatches as $expected) { diff --git a/tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php b/tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php index 38b7e976d2..4bdb886210 100644 --- a/tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Html/HtmlDifferentConditionalFormattingsTest.php @@ -47,40 +47,40 @@ private function extractCell(string $coordinate): string public function testConditionalFormattingRulesHtml(): void { $expectedMatches = [ - ['A1', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">1<', 'A1 equals hit'], + ['A1', 'background-color:#B7E1CD;">1<', 'A1 equals hit'], ['B1', 'class="column1 style1 n">2<', 'B1 equals miss'], - ['E1', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">1<', 'E1 equals horizontal reference hit'], + ['E1', 'background-color:#B7E1CD;">1<', 'E1 equals horizontal reference hit'], ['F1', 'class="column5 style1 n">2<', 'F1 equals horizontal reference miss'], ['G1', 'class="column6 style1 n">3<', 'G1 equals horizontal reference miss'], - ['A2', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A2 text contains hit'], + ['A2', 'background-color:#B7E1CD;">terve<', 'A2 text contains hit'], ['B2', 'class="column1 style1 s">moi<', 'B2 text contains miss'], - ['A3', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A3 text does not contain hit'], + ['A3', 'background-color:#B7E1CD;">terve<', 'A3 text does not contain hit'], ['B3', 'class="column1 style1 s">moi<', 'B2 text does not contain miss'], - ['A4', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A4 text starts with hit'], + ['A4', 'background-color:#B7E1CD;">terve<', 'A4 text starts with hit'], ['B4', 'class="column1 style1 s">moi<', 'B2 text starts with miss'], - ['A5', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A5 text ends with hit'], + ['A5', 'background-color:#B7E1CD;">terve<', 'A5 text ends with hit'], ['B5', 'class="column1 style1 s">moi<', 'B5 text ends with miss'], - ['A6', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">2025/01/01<', 'A6 date after hit'], + ['A6', 'background-color:#B7E1CD;">2025/01/01<', 'A6 date after hit'], ['B6', 'class="column1 style2 n">2020/01/01<', 'B6 date after miss'], - ['A7', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve vaan<', 'A7 text contains hit'], + ['A7', 'background-color:#B7E1CD;">terve vaan<', 'A7 text contains hit'], ['B7', 'class="column1 style1 s">moi<', 'B7 text contains miss'], - ['A8', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A8 text does not contain hit'], + ['A8', 'background-color:#B7E1CD;">terve<', 'A8 text does not contain hit'], ['B8', 'class="column1 style1 s">terve vaan<', 'B2 does not contain miss'], - ['A9', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">#DIV/0!<', 'A10 own formula is error hit'], + ['A9', 'background-color:#B7E1CD;">#DIV/0!<', 'A10 own formula is error hit'], ['B9', 'class="column1 style1 s">moi<', 'B9 own formula is error miss'], - ['A10', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">moi<', 'A10 own formula is not error hit'], + ['A10', 'background-color:#B7E1CD;">moi<', 'A10 own formula is not error hit'], ['B10', 'class="column1 style3 s">#DIV/0!<', 'B10 own formula is not error miss'], - ['A11', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'A11 own formula count instances of cell on line and hit when more than one hit'], - ['B11', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">terve<', 'B11 own formula count instances of cell on line and hit when more than one hit'], + ['A11', 'background-color:#B7E1CD;">terve<', 'A11 own formula count instances of cell on line and hit when more than one hit'], + ['B11', 'background-color:#B7E1CD;">terve<', 'B11 own formula count instances of cell on line and hit when more than one hit'], ['C11', 'class="column2 style1 s">moi<', 'C11 own formula count instances of cell on line and hit when more than one miss'], - ['A12', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">moi<', 'A12 own formula count instances of cell on line and hit when at most 1 hit'], + ['A12', 'background-color:#B7E1CD;">moi<', 'A12 own formula count instances of cell on line and hit when at most 1 hit'], ['B12', 'class="column1 style1 s">terve<', 'B12 own formula count instances of cell on line and hit when at most 1 miss'], ['C12', 'class="column2 style1 s">terve<', 'C11 own formula count instances of cell on line and hit when at most 1 miss'], - ['A13', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">12<', 'A13 own formula self reference hit'], + ['A13', 'background-color:#B7E1CD;">12<', 'A13 own formula self reference hit'], ['B13', 'class="column1 style1 n">10<', 'B13 own formula self reference miss'], - ['A14', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">10<', 'A14 multiple conditional hits'], + ['A14', 'background-color:#B7E1CD;">10<', 'A14 multiple conditional hits'], ['B14', 'class="column1 style1 n">1<', 'B14 multiple conditionals miss'], - ['F7', '"vertical-align:bottom;border-bottom:1px solid #000000 !important;border-top:1px solid #000000 !important;border-left:1px solid #000000 !important;border-right:1px solid #000000 !important;color:#000000;font-family:\'Arial\';font-size:11pt;background-color:#B7E1CD;">1<', 'F7 equals vertical reference hit'], + ['F7', 'background-color:#B7E1CD;">1<', 'F7 equals vertical reference hit'], ['F8', 'class="column5 style1 n">2<', 'F8 equals vertical reference miss'], ['F9', 'class="column5 style1 n">3<', 'F9 equals vertical reference miss'], ['F10', 'class="column5 style1 n">4<', 'F10 equals vertical reference miss'],