mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-06 15:52:43 +00:00
Html Writer/Reader New data Attributes
Html Reader recognizes a `data-format` attribute for storing the number format associated with a cell. However, Html Writer does not set this attribute. This PR offers a new Writer property `preserveFormatAndValue` and setter; when `true`, for any cell which uses a number format other than the default `General` the writer will generate a `data-format` attribute and, for strings or numbers, it will also generate a `data-value` attribute. This will increase the accuracy of the spreadsheet when reading/writing Html. To make this change less abstract, assume a cell which has a value of `1234` and a number format of `$#,###.00`. The cell displayed in the generated Html will appear, regardless of the setting of the new property, as `$1,234.00`. However, the Reader currently treats it as a string with that value. If the html has been generated with the new property set to `true`, the Reader will treat it as a numeric cell with a value of `1234` and a style of `#,###.00`. This permits a more accurate reproduction of the original spreadsheet. I suspect that setting the new property, aside from causing a slight increase in file size, will not cause any significant breaks. However, I'm not totally convinced, so the property will default to `false` (no change in behavior). The default may change in a future breaking release.
This commit is contained in:
@@ -340,13 +340,21 @@ class Html extends BaseReader
|
||||
|
||||
try {
|
||||
if (isset($attributeArray['data-formula'])) {
|
||||
$sheet->setCellValueExplicit($column . $row, $attributeArray['data-formula'], DataType::TYPE_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']);
|
||||
$sheet->setCellValueExplicit(
|
||||
$column . $row,
|
||||
$attributeArray['data-value'] ?? $cellContent,
|
||||
$attributeArray['data-type']
|
||||
);
|
||||
}
|
||||
} catch (SpreadsheetException) {
|
||||
$sheet->setCellValue($column . $row, $cellContent);
|
||||
@@ -357,7 +365,7 @@ class Html extends BaseReader
|
||||
if ($sheet->hyperlinkExists($column . $row)) {
|
||||
$hyperlink = $sheet->getHyperlink($column . $row);
|
||||
}
|
||||
$sheet->setCellValue($column . $row, $cellContent);
|
||||
$sheet->setCellValue($column . $row, $attributeArray['data-value'] ?? $cellContent);
|
||||
$sheet->setHyperlink($column . $row, $hyperlink);
|
||||
}
|
||||
$this->dataArray[$row][$column] = $cellContent; // @phpstan-ignore-line
|
||||
|
||||
@@ -67,12 +67,12 @@ class Html extends BaseWriter
|
||||
/**
|
||||
* Sheet index to write.
|
||||
*/
|
||||
private ?int $sheetIndex = 0;
|
||||
protected ?int $sheetIndex = 0;
|
||||
|
||||
/**
|
||||
* Images root.
|
||||
*/
|
||||
private string $imagesRoot = '';
|
||||
protected string $imagesRoot = '';
|
||||
|
||||
/**
|
||||
* embed images, or link to images.
|
||||
@@ -105,6 +105,15 @@ class Html extends BaseWriter
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected bool $preserveFormatAndValue = false;
|
||||
|
||||
public function setPreserveFormatAndValue(bool $preserveFormatAndValue): self
|
||||
{
|
||||
$this->preserveFormatAndValue = $preserveFormatAndValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use inline CSS?
|
||||
*/
|
||||
@@ -1714,6 +1723,9 @@ class Html extends BaseWriter
|
||||
$html .= ' data-checkbox="1"';
|
||||
}
|
||||
$dataType = $worksheet->getCell($coordinate)->getDataType();
|
||||
$numberFormat = $worksheet->getStyle($coordinate)
|
||||
->getNumberFormat()
|
||||
->getFormatCode() ?? NumberFormat::FORMAT_GENERAL;
|
||||
if ($this->betterBoolean) {
|
||||
if ($dataType === DataType::TYPE_BOOL) {
|
||||
$html .= ' data-type="' . DataType::TYPE_BOOL . '"';
|
||||
@@ -1732,13 +1744,31 @@ class Html extends BaseWriter
|
||||
} catch (CalculationException) {
|
||||
$html .= ' data-type="' . DataType::TYPE_ERROR . '"';
|
||||
}
|
||||
} elseif (is_numeric($cellData) && $worksheet->getCell($coordinate)->getDataType() === DataType::TYPE_STRING) {
|
||||
} elseif ((is_numeric($cellData) || $this->preserveFormatAndValue) && $worksheet->getCell($coordinate)->getDataType() === DataType::TYPE_STRING) {
|
||||
$html .= ' data-type="' . DataType::TYPE_STRING . '"';
|
||||
} elseif ($dataType === DataType::TYPE_NUMERIC && $this->preserveFormatAndValue && $numberFormat !== NumberFormat::FORMAT_GENERAL) {
|
||||
$html .= ' data-type="' . DataType::TYPE_NUMERIC . '"';
|
||||
}
|
||||
}
|
||||
if ($dataType === DataType::TYPE_FORMULA && $this->dataFormula) {
|
||||
if ($this->preCalculateFormulas) {
|
||||
$html .= ' data-formula="'
|
||||
if ($this->preserveFormatAndValue) {
|
||||
if ($numberFormat !== NumberFormat::FORMAT_GENERAL) {
|
||||
$html .= ' data-format="' . htmlspecialchars($numberFormat) . '"';
|
||||
}
|
||||
}
|
||||
if ($dataType === DataType::TYPE_FORMULA) {
|
||||
if ($this->dataFormula) {
|
||||
if ($this->preCalculateFormulas) {
|
||||
$html .= ' data-formula="'
|
||||
. htmlspecialchars(
|
||||
$worksheet->getCell($coordinate)
|
||||
->getValueString()
|
||||
)
|
||||
. '"';
|
||||
}
|
||||
}
|
||||
} elseif ($dataType === DataType::TYPE_NUMERIC || $dataType === DataType::TYPE_STRING) {
|
||||
if ($this->preserveFormatAndValue && $numberFormat !== NumberFormat::FORMAT_GENERAL) {
|
||||
$html .= ' data-value="'
|
||||
. htmlspecialchars(
|
||||
$worksheet->getCell($coordinate)
|
||||
->getValueString()
|
||||
@@ -1746,6 +1776,7 @@ class Html extends BaseWriter
|
||||
. '"';
|
||||
}
|
||||
}
|
||||
|
||||
$holdCss = '';
|
||||
if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) {
|
||||
$html .= ' class="' . $cssClass . '"';
|
||||
|
||||
@@ -210,27 +210,6 @@ class HtmlTest extends TestCase
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public function testCanApplyInlineDataFormat(): void
|
||||
{
|
||||
$html = '<table>
|
||||
<tr>
|
||||
<td data-format="mmm-yy">2019-02-02 12:34:00</td>
|
||||
<td data-format="#.000">3</td>
|
||||
<td data-format="#.000">x</td>
|
||||
</tr>
|
||||
</table>';
|
||||
$spreadsheet = HtmlHelper::loadHtmlStringIntoSpreadsheet($html);
|
||||
$sheet = $spreadsheet->getSheet(0);
|
||||
|
||||
self::assertEquals('mmm-yy', $sheet->getStyle('A1')->getNumberFormat()->getFormatCode());
|
||||
self::assertEquals('2019-02-02 12:34:00', $sheet->getCell('A1')->getFormattedValue(), 'field is string not number so not formatted');
|
||||
self::assertEquals('#.000', $sheet->getStyle('B1')->getNumberFormat()->getFormatCode());
|
||||
self::assertEquals('3.000', $sheet->getCell('B1')->getFormattedValue(), 'format applied to numeric value');
|
||||
self::assertEquals('#.000', $sheet->getStyle('C1')->getNumberFormat()->getFormatCode());
|
||||
self::assertEquals('x', $sheet->getCell('C1')->getFormattedValue(), 'format not applied to non-numeric value');
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public function testCanApplyCellWrapping(): void
|
||||
{
|
||||
$html = '<table>
|
||||
@@ -289,7 +268,7 @@ class HtmlTest extends TestCase
|
||||
$spreadsheet = HtmlHelper::loadHtmlStringIntoSpreadsheet($html);
|
||||
$firstSheet = $spreadsheet->getSheet(0);
|
||||
$style = $firstSheet->getCell('C2')->getStyle();
|
||||
self::assertEquals(1, $style->getAlignment()->getIndent());
|
||||
self::assertSame(1, $style->getAlignment()->getIndent());
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
@@ -320,7 +299,7 @@ class HtmlTest extends TestCase
|
||||
];
|
||||
|
||||
foreach ($totalBorders as $border) {
|
||||
self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle());
|
||||
self::assertSame(Border::BORDER_THIN, $border->getBorderStyle());
|
||||
}
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
@@ -352,7 +331,7 @@ class HtmlTest extends TestCase
|
||||
];
|
||||
|
||||
foreach ($totalBorders as $border) {
|
||||
self::assertEquals(Border::BORDER_THIN, $border->getBorderStyle());
|
||||
self::assertSame(Border::BORDER_THIN, $border->getBorderStyle());
|
||||
}
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
@@ -377,28 +356,32 @@ class HtmlTest extends TestCase
|
||||
$firstSheet = $spreadsheet->getSheet(0);
|
||||
|
||||
// check boolean data type and true
|
||||
self::assertEquals(DataType::TYPE_BOOL, $firstSheet->getCell('A1')->getDataType());
|
||||
self::assertSame(DataType::TYPE_BOOL, $firstSheet->getCell('A1')->getDataType());
|
||||
self::assertIsBool($firstSheet->getCell('A1')->getValue());
|
||||
self::assertTrue($firstSheet->getCell('A1')->getValue());
|
||||
|
||||
// check string data type
|
||||
self::assertEquals(DataType::TYPE_STRING, $firstSheet->getCell('B1')->getDataType());
|
||||
self::assertSame(DataType::TYPE_STRING, $firstSheet->getCell('B1')->getDataType());
|
||||
self::assertIsString($firstSheet->getCell('B1')->getValue());
|
||||
|
||||
// check string with beginning equal sign (=B1) and string datatype,is not formula
|
||||
self::assertEquals(DataType::TYPE_STRING, $firstSheet->getCell('C1')->getDataType());
|
||||
self::assertEquals('=B1', $firstSheet->getCell('C1')->getValue());
|
||||
self::assertSame(DataType::TYPE_STRING, $firstSheet->getCell('C1')->getDataType());
|
||||
self::assertSame('=B1', $firstSheet->getCell('C1')->getValue());
|
||||
self::assertTrue($firstSheet->getCell('C1')->getStyle()->getQuotePrefix());
|
||||
|
||||
//check iso date
|
||||
self::assertEqualsWithDelta($firstSheet->getCell('D1')->getValue(), 44613.43090277778, 1.0e-12);
|
||||
|
||||
//null
|
||||
self::assertEquals($firstSheet->getCell('E1')->getValue(), null);
|
||||
self::assertNull($firstSheet->getCell('E1')->getValue());
|
||||
|
||||
// check boolean data type and true
|
||||
self::assertEquals(DataType::TYPE_BOOL, $firstSheet->getCell('F1')->getDataType());
|
||||
self::assertSame(DataType::TYPE_BOOL, $firstSheet->getCell('F1')->getDataType());
|
||||
self::assertIsBool($firstSheet->getCell('F1')->getValue());
|
||||
self::assertFalse($firstSheet->getCell('F1')->getValue());
|
||||
|
||||
self::assertSame(DataType::TYPE_STRING, $firstSheet->getCell('G1')->getDataType());
|
||||
self::assertSame('text with invalid datatype', $firstSheet->getCell('G1')->getValue());
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Reader\Html;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PreserveFormatTest extends TestCase
|
||||
{
|
||||
public function testCanApplyInlineDataFormat(): void
|
||||
{
|
||||
$html = '<table>
|
||||
<tr>
|
||||
<td data-format="mmm-yy">2019-02-02 12:34:00</td>
|
||||
<td data-format="#.000">3</td>
|
||||
<td data-format="#.000">x</td>
|
||||
<td data-format="$#,###.00" data-value="1234">$1,234.00</td>
|
||||
</tr>
|
||||
</table>';
|
||||
$spreadsheet = HtmlHelper::loadHtmlStringIntoSpreadsheet($html);
|
||||
$sheet = $spreadsheet->getSheet(0);
|
||||
|
||||
self::assertSame('mmm-yy', $sheet->getStyle('A1')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame('2019-02-02 12:34:00', $sheet->getCell('A1')->getFormattedValue(), 'field is string not number so not formatted');
|
||||
self::assertSame('#.000', $sheet->getStyle('B1')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame('3.000', $sheet->getCell('B1')->getFormattedValue(), 'format applied to numeric value');
|
||||
self::assertSame('#.000', $sheet->getStyle('C1')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame('x', $sheet->getCell('C1')->getFormattedValue(), 'format not applied to non-numeric value');
|
||||
self::assertSame('$#,###.00', $sheet->getStyle('D1')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame('$1,234.00', $sheet->getCell('D1')->getFormattedValue());
|
||||
self::assertSame(1234, $sheet->getCell('D1')->getValue());
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public static function testPreserve(): void
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$payload = '<img src=x onerror=alert(document.domain)>';
|
||||
$formatCode = '@';
|
||||
$sheet->setCellValue('A1', $payload);
|
||||
$sheet->getStyle('A1')
|
||||
->getNumberFormat()
|
||||
->setFormatCode($formatCode);
|
||||
$sheet->setCellValue('A2', 3.00);
|
||||
$sheet->setCellValue('A3', 3.00);
|
||||
$sheet->getStyle('A3')
|
||||
->getNumberFormat()
|
||||
->setFormatCode('0.00');
|
||||
$formatCode = '@ <"items">';
|
||||
$sheet->setCellValue('B1', $payload);
|
||||
$sheet->getStyle('B1')
|
||||
->getNumberFormat()
|
||||
->setFormatCode($formatCode);
|
||||
$sheet->setCellValue('B2', 1234);
|
||||
$sheet->getStyle('B2')
|
||||
->getNumberFormat()
|
||||
->setFormatCode('$#,###.00');
|
||||
$sheet->setCellValue('B3', '=2*5');
|
||||
$sheet->getStyle('B3')
|
||||
->getNumberFormat()
|
||||
->setFormatCode('0.0');
|
||||
$writer = new HtmlWriter($spreadsheet);
|
||||
$writer->setPreserveFormatAndValue(true)
|
||||
->setDataFormula(true);
|
||||
$html = $writer->generateHtmlAll();
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
$expected = [
|
||||
'A1' => '<td data-type="s" data-format="@" data-value="<img src=x onerror=alert(document.domain)>" class="column0 style1 s"><img src=x onerror=alert(document.domain)></td>',
|
||||
'B1' => '<td data-type="s" data-format="@ <"items">" data-value="<img src=x onerror=alert(document.domain)>" class="column1 style3 s"><img src=x onerror=alert(document.domain)> <items></td>',
|
||||
'A2' => '<td class="column0 style0 n">3</td>',
|
||||
'B2' => '<td data-type="n" data-format="$#,###.00" data-value="1234" class="column1 style4 n">$1,234.00</td>',
|
||||
'C1' => '<td data-type="n" data-format="0.00" data-value="3" class="column0 style2 n">3.00</td>',
|
||||
'C2' => '<td data-type="n" data-format="0.0" data-formula="=2*5" class="column1 style5 n">10.0</td>',
|
||||
];
|
||||
foreach ($expected as $key => $value) {
|
||||
self::assertStringContainsString($value, $html, "Cell $key");
|
||||
}
|
||||
$spreadsheet2 = HtmlHelper::loadHtmlStringIntoSpreadsheet($html);
|
||||
$sheet2 = $spreadsheet2->getActiveSheet();
|
||||
|
||||
self::assertSame('@', $sheet2->getStyle('A1')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame($payload, $sheet2->getCell('A1')->getValue());
|
||||
self::assertSame($payload, $sheet2->getCell('A1')->getFormattedValue());
|
||||
self::assertSame('@ <"items">', $sheet2->getStyle('B1')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame($payload, $sheet2->getCell('B1')->getValue());
|
||||
self::assertSame($payload . ' <items>', $sheet2->getCell('B1')->getFormattedValue());
|
||||
|
||||
self::assertSame('General', $sheet2->getStyle('A2')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame(3, $sheet2->getCell('A2')->getValue());
|
||||
self::assertSame('3', $sheet2->getCell('A2')->getFormattedValue());
|
||||
self::assertSame('$#,###.00', $sheet2->getStyle('B2')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame(1234, $sheet2->getCell('B2')->getValue());
|
||||
self::assertSame('$1,234.00', $sheet2->getCell('B2')->getFormattedValue());
|
||||
|
||||
self::assertSame('0.00', $sheet2->getStyle('A3')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame(3, $sheet2->getCell('A3')->getValue());
|
||||
self::assertSame('3.00', $sheet2->getCell('A3')->getFormattedValue());
|
||||
self::assertSame('0.0', $sheet2->getStyle('B3')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame('=2*5', $sheet2->getCell('B3')->getValue());
|
||||
self::assertSame('10.0', $sheet2->getCell('B3')->getFormattedValue());
|
||||
|
||||
$spreadsheet2->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public static function testNoPreserve(): void
|
||||
{
|
||||
// Same as above, without preserveFormatAndValue
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$payload = '<img src=x onerror=alert(document.domain)>';
|
||||
$formatCode = '@';
|
||||
$sheet->setCellValue('A1', $payload);
|
||||
$sheet->getStyle('A1')
|
||||
->getNumberFormat()
|
||||
->setFormatCode($formatCode);
|
||||
$sheet->setCellValue('A2', 3.00);
|
||||
$sheet->setCellValue('A3', 3.00);
|
||||
$sheet->getStyle('A3')
|
||||
->getNumberFormat()
|
||||
->setFormatCode('0.00');
|
||||
$formatCode = '@ <"items">';
|
||||
$sheet->setCellValue('B1', $payload);
|
||||
$sheet->getStyle('B1')
|
||||
->getNumberFormat()
|
||||
->setFormatCode($formatCode);
|
||||
$sheet->setCellValue('B2', 1234);
|
||||
$sheet->getStyle('B2')
|
||||
->getNumberFormat()
|
||||
->setFormatCode('$#,###.00');
|
||||
$sheet->setCellValue('B3', '=2*5');
|
||||
$sheet->getStyle('B3')
|
||||
->getNumberFormat()
|
||||
->setFormatCode('0.0');
|
||||
$writer = new HtmlWriter($spreadsheet);
|
||||
$html = $writer->generateHtmlAll();
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
$expected = [
|
||||
'A1' => '<td class="column0 style1 s"><img src=x onerror=alert(document.domain)></td>',
|
||||
'B1' => '<td class="column1 style3 s"><img src=x onerror=alert(document.domain)> <items></td>',
|
||||
'A2' => '<td class="column0 style0 n">3</td>',
|
||||
'B2' => '<td class="column1 style4 n">$1,234.00</td>',
|
||||
'C1' => '<td class="column0 style2 n">3.00</td>',
|
||||
'C2' => '<td class="column1 style5 n">10.0</td>',
|
||||
];
|
||||
foreach ($expected as $key => $value) {
|
||||
self::assertStringContainsString($value, $html, "Cell $key");
|
||||
}
|
||||
$spreadsheet2 = HtmlHelper::loadHtmlStringIntoSpreadsheet($html);
|
||||
$sheet2 = $spreadsheet2->getActiveSheet();
|
||||
|
||||
self::assertSame('General', $sheet2->getStyle('A1')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame($payload, $sheet2->getCell('A1')->getValue());
|
||||
self::assertSame($payload, $sheet2->getCell('A1')->getFormattedValue());
|
||||
self::assertSame('General', $sheet2->getStyle('B1')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame($payload . ' <items>', $sheet2->getCell('B1')->getValue());
|
||||
self::assertSame($payload . ' <items>', $sheet2->getCell('B1')->getFormattedValue());
|
||||
|
||||
self::assertSame('General', $sheet2->getStyle('A2')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame(3, $sheet2->getCell('A2')->getValue());
|
||||
self::assertSame('3', $sheet2->getCell('A2')->getFormattedValue());
|
||||
self::assertSame('General', $sheet2->getStyle('B2')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame('$1,234.00', $sheet2->getCell('B2')->getValue());
|
||||
self::assertSame('$1,234.00', $sheet2->getCell('B2')->getFormattedValue());
|
||||
|
||||
self::assertSame('General', $sheet2->getStyle('A3')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame(3.00, $sheet2->getCell('A3')->getValue());
|
||||
self::assertSame('3', $sheet2->getCell('A3')->getFormattedValue());
|
||||
self::assertSame('General', $sheet2->getStyle('B3')->getNumberFormat()->getFormatCode());
|
||||
self::assertSame(10.0, $sheet2->getCell('B3')->getValue());
|
||||
self::assertSame('10', $sheet2->getCell('B3')->getFormattedValue());
|
||||
|
||||
$spreadsheet2->disconnectWorksheets();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user