mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-15 20:46:26 +00:00
Writer Html Optionally Save Formulas as Data Attributes
This will improve round-tripping from Xlsx to Html and back again. The calculated result will still be what is displayed when viewing the result in a browser, but, using the new option, the original formula can be reconstructed when PhpSpreadsheet reads the Html file. The ability to recognize this situation is automatically enabled in Html Reader, however, to avoid BC issues, it needs to be explicitly enabled for Html Writer. A new boolean property `dataFormula` and setter are added to Writer; it will default to current behavior `false` for now, but my plan is to change the default in the next breaking release. I cannot think of a decent use case for a getter, so have not added one.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,21 @@ class Html extends BaseWriter
|
||||
$htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate);
|
||||
// Column start
|
||||
$html .= ' <' . $cellType;
|
||||
$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 +1733,19 @@ 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()
|
||||
)
|
||||
. '"';
|
||||
} else {
|
||||
if ($worksheet->getStyle($coordinate)->getCheckbox()) {
|
||||
$html .= ' data-checkbox="1"';
|
||||
}
|
||||
}
|
||||
}
|
||||
$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-type="b" class="column0 style1 b">☑</td>';
|
||||
self::assertStringContainsString($expected, $content, 'bool non-formula');
|
||||
$expected = '<td 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="="A"&"B"&"C"" 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-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">="A"&"B"&"C"</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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user