Merge pull request #4783 from oleibman/dataformula

Writer Html Optionally Save Formulas as Data Attributes
This commit is contained in:
oleibman
2026-01-21 20:30:15 +00:00
committed by GitHub
4 changed files with 203 additions and 12 deletions
+13 -1
View File
@@ -303,6 +303,10 @@ class Html extends BaseReader
// ... we return the cell, so we can mess about with styles more easily
// Set cell value explicitly if there is data-type attribute
if (isset($attributeArray['data-checkbox'])) {
$sheet->getStyle($column . $row)
->setCheckBox(true);
}
if (isset($attributeArray['data-type'])) {
$datatype = $attributeArray['data-type'];
if (in_array($datatype, [DataType::TYPE_STRING, DataType::TYPE_STRING2, DataType::TYPE_INLINE])) {
@@ -335,7 +339,15 @@ class Html extends BaseReader
$hyperlink = $sheet->hyperlinkExists($column . $row) ? $sheet->getHyperlink($column . $row) : null;
try {
$sheet->setCellValueExplicit($column . $row, $cellContent, $attributeArray['data-type']);
if (isset($attributeArray['data-formula'])) {
$sheet->setCellValueExplicit($column . $row, $attributeArray['data-formula'], DataType::TYPE_FORMULA);
$sheet->getCell($column . $row)
->setCalculatedValue(
$cellContent
);
} else {
$sheet->setCellValueExplicit($column . $row, $cellContent, $attributeArray['data-type']);
}
} catch (SpreadsheetException) {
$sheet->setCellValue($column . $row, $cellContent);
}
+31 -8
View File
@@ -96,6 +96,15 @@ class Html extends BaseWriter
return $this;
}
protected bool $dataFormula = false;
public function setDataFormula(bool $dataFormula): self
{
$this->dataFormula = $dataFormula;
return $this;
}
/**
* Use inline CSS?
*/
@@ -1701,20 +1710,24 @@ class Html extends BaseWriter
$htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
// Column start
$html .= ' <' . $cellType;
if ($worksheet->getStyle($coordinate)->getCheckbox()) {
$html .= ' data-checkbox="1"';
}
$dataType = $worksheet->getCell($coordinate)->getDataType();
if ($this->betterBoolean) {
$dataType = $worksheet->getCell($coordinate)->getDataType();
if ($dataType === DataType::TYPE_BOOL) {
$html .= ' data-type="' . DataType::TYPE_BOOL . '"';
} elseif ($dataType === DataType::TYPE_FORMULA && $this->preCalculateFormulas) {
try {
if (
is_bool(
$worksheet
->getCell($coordinate)
->getCalculatedValue()
)
) {
$calculatedValue = $worksheet
->getCell($coordinate)
->getCalculatedValue();
if (is_bool($calculatedValue)) {
$html .= ' data-type="' . DataType::TYPE_BOOL . '"';
} elseif ($this->dataFormula && is_string($calculatedValue)) {
$html .= ' data-type="' . DataType::TYPE_STRING . '"';
} elseif ($this->dataFormula && (is_int($calculatedValue) || is_float($calculatedValue))) {
$html .= ' data-type="' . DataType::TYPE_NUMERIC . '"';
}
} catch (CalculationException) {
$html .= ' data-type="' . DataType::TYPE_ERROR . '"';
@@ -1723,6 +1736,16 @@ class Html extends BaseWriter
$html .= ' data-type="' . DataType::TYPE_STRING . '"';
}
}
if ($dataType === DataType::TYPE_FORMULA && $this->dataFormula) {
if ($this->preCalculateFormulas) {
$html .= ' data-formula="'
. htmlspecialchars(
$worksheet->getCell($coordinate)
->getValueString()
)
. '"';
}
}
$holdCss = '';
if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) {
$html .= ' class="' . $cssClass . '"';
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Reader\Html;
use PhpOffice\PhpSpreadsheet\Reader\Html as HtmlReader;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class DataFormulaTest extends AbstractFunctional
{
public function testDataFormula(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('A1', true);
$sheet->setCellValue('A2', false);
$sheet->setCellValue('A3', false); // no checkbox
$sheet->getStyle('A1')->setCheckBox(true);
$sheet->getStyle('A2')->setCheckBox(true);
$sheet->setCellValue('B1', '=AND(TRUE,TRUE)');
$sheet->setCellValue('B2', '=AND(TRUE,FALSE)');
$sheet->setCellValue('B3', '=AND(FALSE,TRUE)'); // no checkbox
$sheet->getStyle('B1')->setCheckBox(true);
$sheet->getStyle('B2')->setCheckBox(true);
$sheet->setCellValue('C1', '="A"&"B"&"C"');
$sheet->setCellValue('C2', 5);
$sheet->setCellValue('C3', '=3+2');
$writer = new HtmlWriter($spreadsheet);
$this->writeDataFormula($writer);
$content = $writer->generateHtmlAll();
$expected = '<td data-checkbox="1" data-type="b" class="column0 style1 b">☑</td>';
self::assertStringContainsString($expected, $content, 'bool non-formula');
$expected = '<td data-checkbox="1" data-type="b" data-formula="=AND(TRUE,TRUE)" class="column1 style1 b">☑</td>';
self::assertStringContainsString($expected, $content, 'bool formula');
$expected = '<td data-type="s" data-formula="=&quot;A&quot;&amp;&quot;B&quot;&amp;&quot;C&quot;" class="column2 style0 s">ABC</td>';
self::assertStringContainsString($expected, $content, 'string formula requiring escaped characters');
$expected = '<td class="column2 style0 n">5</td>';
self::assertStringContainsString($expected, $content, 'numeric non-formula');
$expected = '<td data-type="n" data-formula="=3+2" class="column2 style0 n">5</td>';
self::assertStringContainsString($expected, $content, 'numeric formula');
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Html', null, $this->writeDataFormula(...));
$spreadsheet->disconnectWorksheets();
$rsheet = $reloadedSpreadsheet->getActiveSheet();
self::assertTrue($rsheet->getCell('A1')->getValue());
self::assertFalse($rsheet->getCell('A2')->getValue());
self::assertFalse($rsheet->getCell('A3')->getValue());
self::assertTrue($rsheet->getStyle('A1')->getCheckBox());
self::assertTrue($rsheet->getStyle('A2')->getCheckBox());
self::assertFalse($rsheet->getStyle('A3')->getCheckBox());
self::assertSame('=AND(TRUE,TRUE)', $rsheet->getCell('B1')->getValue());
self::assertTrue(
$rsheet->getCell('B1')->getOldCalculatedValue()
);
self::assertTrue(
$rsheet->getCell('B1')->getCalculatedValue()
);
self::assertTrue($rsheet->getStyle('B1')->getCheckBox());
self::assertSame('=AND(TRUE,FALSE)', $rsheet->getCell('B2')->getValue());
self::assertFalse(
$rsheet->getCell('B2')->getOldCalculatedValue()
);
self::assertFalse(
$rsheet->getCell('B2')->getCalculatedValue()
);
self::assertTrue($rsheet->getStyle('B2')->getCheckBox());
self::assertSame('=AND(FALSE,TRUE)', $rsheet->getCell('B3')->getValue());
self::assertFalse(
$rsheet->getCell('B3')->getOldCalculatedValue()
);
self::assertFalse(
$rsheet->getCell('B3')->getCalculatedValue()
);
self::assertFalse($rsheet->getStyle('B3')->getCheckBox());
self::assertSame('="A"&"B"&"C"', $rsheet->getCell('C1')->getValue());
self::assertSame(
'ABC',
$rsheet->getCell('C1')->getOldCalculatedValue()
);
self::assertSame(
'ABC',
$rsheet->getCell('C1')->getCalculatedValue()
);
self::assertSame(5, $rsheet->getCell('C2')->getValue());
self::assertNull(
$rsheet->getCell('C2')->getOldCalculatedValue()
);
self::assertSame(
5,
$rsheet->getCell('C2')->getCalculatedValue()
);
self::assertSame('=3+2', $rsheet->getCell('C3')->getValue());
self::assertSame(
5,
$rsheet->getCell('C3')->getOldCalculatedValue()
);
self::assertSame(
5,
$rsheet->getCell('C3')->getCalculatedValue()
);
$reloadedSpreadsheet->disconnectWorksheets();
}
private function writeDataFormula(HtmlWriter $writer): void
{
$writer->setDataFormula(true);
}
public function testNoPreCalculate(): void
{
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('A1', true);
$sheet->setCellValue('A2', false);
$sheet->setCellValue('A3', false); // no checkbox
$sheet->getStyle('A1')->setCheckBox(true);
$sheet->getStyle('A2')->setCheckBox(true);
$sheet->setCellValue('B1', '=AND(TRUE,TRUE)');
$sheet->setCellValue('B2', '=AND(TRUE,FALSE)');
$sheet->setCellValue('B3', '=AND(FALSE,TRUE)'); // no checkbox
$sheet->getStyle('B1')->setCheckBox(true);
$sheet->getStyle('B2')->setCheckBox(true);
$sheet->setCellValue('C1', '="A"&"B"&"C"');
$sheet->setCellValue('C2', 5);
$sheet->setCellValue('C3', '=3+2');
$writer = new HtmlWriter($spreadsheet);
$this->writeDataFormula($writer);
$writer->setPreCalculateFormulas(false);
$content = $writer->generateHtmlAll();
$expected = '<td data-checkbox="1" data-type="b" class="column0 style1 b">☑</td>';
self::assertStringContainsString($expected, $content, 'bool non-formula');
$expected = '<td data-checkbox="1" class="column1 style1 f">=AND(TRUE,TRUE)</td>';
self::assertStringContainsString($expected, $content, 'bool formula');
$expected = '<td class="column2 style0 f">=&quot;A&quot;&amp;&quot;B&quot;&amp;&quot;C&quot;</td>';
self::assertStringContainsString($expected, $content, 'string formula requiring escaped characters');
$spreadsheet->disconnectWorksheets();
$reader = new HtmlReader();
$reloadedSpreadsheet = $reader->loadFromString($content);
$rsheet = $reloadedSpreadsheet->getActiveSheet();
self::assertTrue($rsheet->getStyle('B1')->getCheckBox());
$reloadedSpreadsheet->disconnectWorksheets();
}
}
@@ -82,9 +82,8 @@ class StyleCheckBoxTest extends AbstractFunctional
self::assertSame('=AND(TRUE,TRUE)', $sheet->getCell('B1')->getValue());
self::assertSame('=AND(FALSE,TRUE)', $sheet->getCell('B2')->getValue());
self::assertSame('=AND(TRUE,FALSE)', $sheet->getCell('B3')->getValue());
// Formulas not evaluated, so no checkboxes.
self::assertFalse($sheet->getStyle('B1')->getCheckBox());
self::assertFalse($sheet->getStyle('B2')->getCheckBox());
self::assertTrue($sheet->getStyle('B1')->getCheckBox());
self::assertTrue($sheet->getStyle('B2')->getCheckBox());
self::assertFalse($sheet->getStyle('B3')->getCheckBox());
$spreadsheet->disconnectWorksheets();