mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-09-16 21:16:57 +00:00
Ods Reader Process Nested table-row
Fix #4528. Fix #2507. Ods Reader expects `table:table-row` to descend directly from `table:table`. But it turns out that it can be nested under `table:table-header-rows`, `table:table-rows`, or `table:table-row-group`. Similar considerations apply to `table:table-column`. Ods Reader is changed to process the nesting tags, and therefore the nested tags underneath them. This is a major change, but it doesn't seem to have broken any existing tests or samples. No attempt is made to add any of the nesting tags in Ods Writer. It should be noted that 4528 has an additional problem, which has been reported as issue #4530. It is not fixed by this PR.
This commit is contained in:
+556
-300
@@ -354,317 +354,90 @@ class Ods extends BaseReader
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $childNode->nodeName;
|
||||
|
||||
// Remove ns from node name
|
||||
if (str_contains($key, ':')) {
|
||||
$keyChunks = explode(':', $key);
|
||||
$key = array_pop($keyChunks);
|
||||
}
|
||||
$key = self::extractNodeName($childNode->nodeName);
|
||||
|
||||
switch ($key) {
|
||||
case 'table-header-rows':
|
||||
/// TODO :: Figure this out. This is only a partial implementation I guess.
|
||||
// ($rowData it's not used at all and I'm not sure that PHPExcel
|
||||
// has an API for this)
|
||||
case 'table-rows':
|
||||
$this->processTableHeaderRows(
|
||||
$childNode,
|
||||
$tableNs,
|
||||
$rowID,
|
||||
$worksheetName,
|
||||
$officeNs,
|
||||
$textNs,
|
||||
$xlinkNs,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
case 'table-row-group':
|
||||
$this->processTableRowGroup(
|
||||
$childNode,
|
||||
$tableNs,
|
||||
$rowID,
|
||||
$worksheetName,
|
||||
$officeNs,
|
||||
$textNs,
|
||||
$xlinkNs,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
case 'table-header-columns':
|
||||
case 'table-columns':
|
||||
$this->processTableHeaderColumns(
|
||||
$childNode,
|
||||
$tableNs,
|
||||
$columnWidths,
|
||||
$tableColumnIndex,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
case 'table-column-group':
|
||||
$this->processTableColumnGroup(
|
||||
$childNode,
|
||||
$tableNs,
|
||||
$columnWidths,
|
||||
$tableColumnIndex,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
// foreach ($rowData as $keyRowData => $cellData) {
|
||||
// $rowData = $cellData;
|
||||
// break;
|
||||
// }
|
||||
break;
|
||||
case 'table-column':
|
||||
if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
|
||||
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
|
||||
} else {
|
||||
$rowRepeats = 1;
|
||||
}
|
||||
$tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
|
||||
if (isset($columnWidths[$tableStyleName])) {
|
||||
$columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
|
||||
$tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex);
|
||||
for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) {
|
||||
/** @var string $tableColumnString */
|
||||
$spreadsheet->getActiveSheet()
|
||||
->getColumnDimension($tableColumnString)
|
||||
->setWidth($columnWidth->toUnit('cm'), 'cm');
|
||||
++$tableColumnString;
|
||||
}
|
||||
}
|
||||
$tableColumnIndex += $rowRepeats;
|
||||
$this->processTableColumn(
|
||||
$childNode,
|
||||
$tableNs,
|
||||
$columnWidths,
|
||||
$tableColumnIndex,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
case 'table-row':
|
||||
if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {
|
||||
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
|
||||
} else {
|
||||
$rowRepeats = 1;
|
||||
}
|
||||
|
||||
$columnID = 'A';
|
||||
/** @var DOMElement|DOMText $cellData */
|
||||
foreach ($childNode->childNodes as $cellData) {
|
||||
if ($cellData instanceof DOMText) {
|
||||
continue; // should just be whitespace
|
||||
}
|
||||
if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
|
||||
if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
|
||||
$colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
|
||||
} else {
|
||||
$colRepeats = 1;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $colRepeats; ++$i) {
|
||||
++$columnID;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialize variables
|
||||
$formatting = $hyperlink = null;
|
||||
$hasCalculatedValue = false;
|
||||
$cellDataFormula = '';
|
||||
$cellDataType = '';
|
||||
$cellDataRef = '';
|
||||
|
||||
if ($cellData->hasAttributeNS($tableNs, 'formula')) {
|
||||
$cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula');
|
||||
$hasCalculatedValue = true;
|
||||
}
|
||||
if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) {
|
||||
if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) {
|
||||
$cellDataType = 'array';
|
||||
$arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned');
|
||||
$arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned');
|
||||
$lastRow = $rowID + $arrayRow - 1;
|
||||
$lastCol = $columnID;
|
||||
while ($arrayCol > 1) {
|
||||
++$lastCol;
|
||||
--$arrayCol;
|
||||
}
|
||||
$cellDataRef = "$columnID$rowID:$lastCol$lastRow";
|
||||
}
|
||||
}
|
||||
|
||||
// Annotations
|
||||
$annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation');
|
||||
|
||||
if ($annotation->length > 0 && $annotation->item(0) !== null) {
|
||||
$textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p');
|
||||
$textNodeLength = $textNode->length;
|
||||
$newLineOwed = false;
|
||||
for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) {
|
||||
$textNodeItem = $textNode->item($textNodeIndex);
|
||||
if ($textNodeItem !== null) {
|
||||
$text = $this->scanElementForText($textNodeItem);
|
||||
if ($newLineOwed) {
|
||||
$spreadsheet->getActiveSheet()
|
||||
->getComment($columnID . $rowID)
|
||||
->getText()
|
||||
->createText("\n");
|
||||
}
|
||||
$newLineOwed = true;
|
||||
|
||||
$spreadsheet->getActiveSheet()
|
||||
->getComment($columnID . $rowID)
|
||||
->getText()
|
||||
->createText($this->parseRichText($text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
|
||||
/** @var DOMElement[] $paragraphs */
|
||||
$paragraphs = [];
|
||||
|
||||
foreach ($cellData->childNodes as $item) {
|
||||
/** @var DOMElement $item */
|
||||
|
||||
// Filter text:p elements
|
||||
if ($item->nodeName == 'text:p') {
|
||||
$paragraphs[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($paragraphs) > 0) {
|
||||
// Consolidate if there are multiple p records (maybe with spans as well)
|
||||
$dataArray = [];
|
||||
|
||||
// Text can have multiple text:p and within those, multiple text:span.
|
||||
// text:p newlines, but text:span does not.
|
||||
// Also, here we assume there is no text data is span fields are specified, since
|
||||
// we have no way of knowing proper positioning anyway.
|
||||
|
||||
foreach ($paragraphs as $pData) {
|
||||
$dataArray[] = $this->scanElementForText($pData);
|
||||
}
|
||||
$allCellDataText = implode("\n", $dataArray);
|
||||
|
||||
$type = $cellData->getAttributeNS($officeNs, 'value-type');
|
||||
|
||||
switch ($type) {
|
||||
case 'string':
|
||||
$type = DataType::TYPE_STRING;
|
||||
$dataValue = $allCellDataText;
|
||||
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
$link = $paragraph->getElementsByTagNameNS($textNs, 'a');
|
||||
if ($link->length > 0 && $link->item(0) !== null) {
|
||||
$hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href');
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'boolean':
|
||||
$type = DataType::TYPE_BOOL;
|
||||
$dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false;
|
||||
|
||||
break;
|
||||
case 'percentage':
|
||||
$type = DataType::TYPE_NUMERIC;
|
||||
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
|
||||
|
||||
// percentage should always be float
|
||||
//if (floor($dataValue) == $dataValue) {
|
||||
// $dataValue = (int) $dataValue;
|
||||
//}
|
||||
$formatting = NumberFormat::FORMAT_PERCENTAGE_00;
|
||||
|
||||
break;
|
||||
case 'currency':
|
||||
$type = DataType::TYPE_NUMERIC;
|
||||
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
|
||||
|
||||
if (floor($dataValue) == $dataValue) {
|
||||
$dataValue = (int) $dataValue;
|
||||
}
|
||||
$formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
|
||||
|
||||
break;
|
||||
case 'float':
|
||||
$type = DataType::TYPE_NUMERIC;
|
||||
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
|
||||
|
||||
if (floor($dataValue) == $dataValue) {
|
||||
if ($dataValue == (int) $dataValue) {
|
||||
$dataValue = (int) $dataValue;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'date':
|
||||
$type = DataType::TYPE_NUMERIC;
|
||||
$value = $cellData->getAttributeNS($officeNs, 'date-value');
|
||||
$dataValue = Date::convertIsoDate($value);
|
||||
|
||||
if ($dataValue != floor($dataValue)) {
|
||||
$formatting = NumberFormat::FORMAT_DATE_XLSX15
|
||||
. ' '
|
||||
. NumberFormat::FORMAT_DATE_TIME4;
|
||||
} else {
|
||||
$formatting = NumberFormat::FORMAT_DATE_XLSX15;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'time':
|
||||
$type = DataType::TYPE_NUMERIC;
|
||||
|
||||
$timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
|
||||
|
||||
$dataValue = Date::PHPToExcel(
|
||||
strtotime(
|
||||
'01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? [])
|
||||
)
|
||||
);
|
||||
$formatting = NumberFormat::FORMAT_DATE_TIME4;
|
||||
|
||||
break;
|
||||
default:
|
||||
$dataValue = null;
|
||||
}
|
||||
} else {
|
||||
$type = DataType::TYPE_NULL;
|
||||
$dataValue = null;
|
||||
}
|
||||
|
||||
if ($hasCalculatedValue) {
|
||||
$type = DataType::TYPE_FORMULA;
|
||||
$cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1);
|
||||
$cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula);
|
||||
}
|
||||
|
||||
if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
|
||||
$colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
|
||||
} else {
|
||||
$colRepeats = 1;
|
||||
}
|
||||
|
||||
if ($type !== null) { // @phpstan-ignore-line
|
||||
for ($i = 0; $i < $colRepeats; ++$i) {
|
||||
if ($i > 0) {
|
||||
++$columnID;
|
||||
}
|
||||
|
||||
if ($type !== DataType::TYPE_NULL) {
|
||||
for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
|
||||
$rID = $rowID + $rowAdjust;
|
||||
|
||||
$cell = $spreadsheet->getActiveSheet()
|
||||
->getCell($columnID . $rID);
|
||||
|
||||
// Set value
|
||||
if ($hasCalculatedValue) {
|
||||
$cell->setValueExplicit($cellDataFormula, $type);
|
||||
if ($cellDataType === 'array') {
|
||||
$cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]);
|
||||
}
|
||||
} elseif ($type !== '' || $dataValue !== null) {
|
||||
$cell->setValueExplicit($dataValue, $type);
|
||||
}
|
||||
|
||||
if ($hasCalculatedValue) {
|
||||
$cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC);
|
||||
}
|
||||
|
||||
// Set other properties
|
||||
if ($formatting !== null) {
|
||||
$spreadsheet->getActiveSheet()
|
||||
->getStyle($columnID . $rID)
|
||||
->getNumberFormat()
|
||||
->setFormatCode($formatting);
|
||||
} else {
|
||||
$spreadsheet->getActiveSheet()
|
||||
->getStyle($columnID . $rID)
|
||||
->getNumberFormat()
|
||||
->setFormatCode(NumberFormat::FORMAT_GENERAL);
|
||||
}
|
||||
|
||||
if ($hyperlink !== null) {
|
||||
if ($hyperlink[0] === '#') {
|
||||
$hyperlink = 'sheet://' . substr($hyperlink, 1);
|
||||
}
|
||||
$cell->getHyperlink()
|
||||
->setUrl($hyperlink);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merged cells
|
||||
$this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet);
|
||||
|
||||
++$columnID;
|
||||
}
|
||||
$rowID += $rowRepeats;
|
||||
$this->processTableRow(
|
||||
$childNode,
|
||||
$tableNs,
|
||||
$rowID,
|
||||
$worksheetName,
|
||||
$officeNs,
|
||||
$textNs,
|
||||
$xlinkNs,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
$pageSettings->setVisibilityForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName);
|
||||
$pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName);
|
||||
$pageSettings->setVisibilityForWorksheet(
|
||||
$spreadsheet->getActiveSheet(),
|
||||
$worksheetStyleName
|
||||
);
|
||||
$pageSettings->setPrintSettingsForWorksheet(
|
||||
$spreadsheet->getActiveSheet(),
|
||||
$worksheetStyleName
|
||||
);
|
||||
++$worksheetID;
|
||||
}
|
||||
|
||||
@@ -681,6 +454,491 @@ class Ods extends BaseReader
|
||||
return $spreadsheet;
|
||||
}
|
||||
|
||||
private function processTableHeaderRows(
|
||||
DOMElement $childNode,
|
||||
string $tableNs,
|
||||
int &$rowID,
|
||||
string $worksheetName,
|
||||
string $officeNs,
|
||||
string $textNs,
|
||||
string $xlinkNs,
|
||||
Spreadsheet $spreadsheet
|
||||
): void {
|
||||
foreach ($childNode->childNodes as $grandchildNode) {
|
||||
/** @var DOMElement $grandchildNode */
|
||||
$grandkey = self::extractNodeName($grandchildNode->nodeName);
|
||||
switch ($grandkey) {
|
||||
case 'table-row':
|
||||
$this->processTableRow(
|
||||
$grandchildNode,
|
||||
$tableNs,
|
||||
$rowID,
|
||||
$worksheetName,
|
||||
$officeNs,
|
||||
$textNs,
|
||||
$xlinkNs,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processTableRowGroup(
|
||||
DOMElement $childNode,
|
||||
string $tableNs,
|
||||
int &$rowID,
|
||||
string $worksheetName,
|
||||
string $officeNs,
|
||||
string $textNs,
|
||||
string $xlinkNs,
|
||||
Spreadsheet $spreadsheet
|
||||
): void {
|
||||
foreach ($childNode->childNodes as $grandchildNode) {
|
||||
/** @var DOMElement $grandchildNode */
|
||||
$grandkey = self::extractNodeName($grandchildNode->nodeName);
|
||||
switch ($grandkey) {
|
||||
case 'table-row':
|
||||
$this->processTableRow(
|
||||
$grandchildNode,
|
||||
$tableNs,
|
||||
$rowID,
|
||||
$worksheetName,
|
||||
$officeNs,
|
||||
$textNs,
|
||||
$xlinkNs,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
case 'table-header-rows':
|
||||
case 'table-rows':
|
||||
$this->processTableHeaderRows(
|
||||
$grandchildNode,
|
||||
$tableNs,
|
||||
$rowID,
|
||||
$worksheetName,
|
||||
$officeNs,
|
||||
$textNs,
|
||||
$xlinkNs,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
case 'table-row-group':
|
||||
$this->processTableRowGroup(
|
||||
$grandchildNode,
|
||||
$tableNs,
|
||||
$rowID,
|
||||
$worksheetName,
|
||||
$officeNs,
|
||||
$textNs,
|
||||
$xlinkNs,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function processTableRow(
|
||||
DOMElement $childNode,
|
||||
string $tableNs,
|
||||
int &$rowID,
|
||||
string $worksheetName,
|
||||
string $officeNs,
|
||||
string $textNs,
|
||||
string $xlinkNs,
|
||||
Spreadsheet $spreadsheet
|
||||
): void {
|
||||
if ($childNode->hasAttributeNS($tableNs, 'number-rows-repeated')) {
|
||||
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-rows-repeated');
|
||||
} else {
|
||||
$rowRepeats = 1;
|
||||
}
|
||||
|
||||
$columnID = 'A';
|
||||
/** @var DOMElement|DOMText $cellData */
|
||||
foreach ($childNode->childNodes as $cellData) {
|
||||
if ($cellData instanceof DOMText) {
|
||||
continue; // should just be whitespace
|
||||
}
|
||||
if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) {
|
||||
if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
|
||||
$colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
|
||||
} else {
|
||||
$colRepeats = 1;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $colRepeats; ++$i) {
|
||||
++$columnID;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialize variables
|
||||
$formatting = $hyperlink = null;
|
||||
$hasCalculatedValue = false;
|
||||
$cellDataFormula = '';
|
||||
$cellDataType = '';
|
||||
$cellDataRef = '';
|
||||
|
||||
if ($cellData->hasAttributeNS($tableNs, 'formula')) {
|
||||
$cellDataFormula = $cellData->getAttributeNS($tableNs, 'formula');
|
||||
$hasCalculatedValue = true;
|
||||
}
|
||||
if ($cellData->hasAttributeNS($tableNs, 'number-matrix-columns-spanned')) {
|
||||
if ($cellData->hasAttributeNS($tableNs, 'number-matrix-rows-spanned')) {
|
||||
$cellDataType = 'array';
|
||||
$arrayRow = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-rows-spanned');
|
||||
$arrayCol = (int) $cellData->getAttributeNS($tableNs, 'number-matrix-columns-spanned');
|
||||
$lastRow = $rowID + $arrayRow - 1;
|
||||
$lastCol = $columnID;
|
||||
while ($arrayCol > 1) {
|
||||
++$lastCol;
|
||||
--$arrayCol;
|
||||
}
|
||||
$cellDataRef = "$columnID$rowID:$lastCol$lastRow";
|
||||
}
|
||||
}
|
||||
|
||||
// Annotations
|
||||
$annotation = $cellData->getElementsByTagNameNS($officeNs, 'annotation');
|
||||
|
||||
if ($annotation->length > 0 && $annotation->item(0) !== null) {
|
||||
$textNode = $annotation->item(0)->getElementsByTagNameNS($textNs, 'p');
|
||||
$textNodeLength = $textNode->length;
|
||||
$newLineOwed = false;
|
||||
for ($textNodeIndex = 0; $textNodeIndex < $textNodeLength; ++$textNodeIndex) {
|
||||
$textNodeItem = $textNode->item($textNodeIndex);
|
||||
if ($textNodeItem !== null) {
|
||||
$text = $this->scanElementForText($textNodeItem);
|
||||
if ($newLineOwed) {
|
||||
$spreadsheet->getActiveSheet()
|
||||
->getComment($columnID . $rowID)
|
||||
->getText()
|
||||
->createText("\n");
|
||||
}
|
||||
$newLineOwed = true;
|
||||
|
||||
$spreadsheet->getActiveSheet()
|
||||
->getComment($columnID . $rowID)
|
||||
->getText()
|
||||
->createText(
|
||||
$this->parseRichText($text)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
|
||||
/** @var DOMElement[] $paragraphs */
|
||||
$paragraphs = [];
|
||||
|
||||
foreach ($cellData->childNodes as $item) {
|
||||
/** @var DOMElement $item */
|
||||
|
||||
// Filter text:p elements
|
||||
if ($item->nodeName == 'text:p') {
|
||||
$paragraphs[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($paragraphs) > 0) {
|
||||
// Consolidate if there are multiple p records (maybe with spans as well)
|
||||
$dataArray = [];
|
||||
|
||||
// Text can have multiple text:p and within those, multiple text:span.
|
||||
// text:p newlines, but text:span does not.
|
||||
// Also, here we assume there is no text data is span fields are specified, since
|
||||
// we have no way of knowing proper positioning anyway.
|
||||
|
||||
foreach ($paragraphs as $pData) {
|
||||
$dataArray[] = $this->scanElementForText($pData);
|
||||
}
|
||||
$allCellDataText = implode("\n", $dataArray);
|
||||
|
||||
$type = $cellData->getAttributeNS($officeNs, 'value-type');
|
||||
|
||||
switch ($type) {
|
||||
case 'string':
|
||||
$type = DataType::TYPE_STRING;
|
||||
$dataValue = $allCellDataText;
|
||||
|
||||
foreach ($paragraphs as $paragraph) {
|
||||
$link = $paragraph->getElementsByTagNameNS($textNs, 'a');
|
||||
if ($link->length > 0 && $link->item(0) !== null) {
|
||||
$hyperlink = $link->item(0)->getAttributeNS($xlinkNs, 'href');
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'boolean':
|
||||
$type = DataType::TYPE_BOOL;
|
||||
$dataValue = ($cellData->getAttributeNS($officeNs, 'boolean-value') === 'true') ? true : false;
|
||||
|
||||
break;
|
||||
case 'percentage':
|
||||
$type = DataType::TYPE_NUMERIC;
|
||||
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
|
||||
|
||||
// percentage should always be float
|
||||
//if (floor($dataValue) == $dataValue) {
|
||||
// $dataValue = (int) $dataValue;
|
||||
//}
|
||||
$formatting = NumberFormat::FORMAT_PERCENTAGE_00;
|
||||
|
||||
break;
|
||||
case 'currency':
|
||||
$type = DataType::TYPE_NUMERIC;
|
||||
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
|
||||
|
||||
if (floor($dataValue) == $dataValue) {
|
||||
$dataValue = (int) $dataValue;
|
||||
}
|
||||
$formatting = NumberFormat::FORMAT_CURRENCY_USD_INTEGER;
|
||||
|
||||
break;
|
||||
case 'float':
|
||||
$type = DataType::TYPE_NUMERIC;
|
||||
$dataValue = (float) $cellData->getAttributeNS($officeNs, 'value');
|
||||
|
||||
if (floor($dataValue) == $dataValue) {
|
||||
if ($dataValue == (int) $dataValue) {
|
||||
$dataValue = (int) $dataValue;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case 'date':
|
||||
$type = DataType::TYPE_NUMERIC;
|
||||
$value = $cellData->getAttributeNS($officeNs, 'date-value');
|
||||
$dataValue = Date::convertIsoDate($value);
|
||||
|
||||
if ($dataValue != floor($dataValue)) {
|
||||
$formatting = NumberFormat::FORMAT_DATE_XLSX15
|
||||
. ' '
|
||||
. NumberFormat::FORMAT_DATE_TIME4;
|
||||
} else {
|
||||
$formatting = NumberFormat::FORMAT_DATE_XLSX15;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'time':
|
||||
$type = DataType::TYPE_NUMERIC;
|
||||
|
||||
$timeValue = $cellData->getAttributeNS($officeNs, 'time-value');
|
||||
|
||||
$dataValue = Date::PHPToExcel(
|
||||
strtotime(
|
||||
'01-01-1970 ' . implode(':', sscanf($timeValue, 'PT%dH%dM%dS') ?? [])
|
||||
)
|
||||
);
|
||||
$formatting = NumberFormat::FORMAT_DATE_TIME4;
|
||||
|
||||
break;
|
||||
default:
|
||||
$dataValue = null;
|
||||
}
|
||||
} else {
|
||||
$type = DataType::TYPE_NULL;
|
||||
$dataValue = null;
|
||||
}
|
||||
|
||||
if ($hasCalculatedValue) {
|
||||
$type = DataType::TYPE_FORMULA;
|
||||
$cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1);
|
||||
$cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula);
|
||||
}
|
||||
|
||||
if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) {
|
||||
$colRepeats = (int) $cellData->getAttributeNS($tableNs, 'number-columns-repeated');
|
||||
} else {
|
||||
$colRepeats = 1;
|
||||
}
|
||||
|
||||
if ($type !== null) { // @phpstan-ignore-line
|
||||
for ($i = 0; $i < $colRepeats; ++$i) {
|
||||
if ($i > 0) {
|
||||
++$columnID;
|
||||
}
|
||||
|
||||
if ($type !== DataType::TYPE_NULL) {
|
||||
for ($rowAdjust = 0; $rowAdjust < $rowRepeats; ++$rowAdjust) {
|
||||
$rID = $rowID + $rowAdjust;
|
||||
|
||||
$cell = $spreadsheet->getActiveSheet()
|
||||
->getCell($columnID . $rID);
|
||||
|
||||
// Set value
|
||||
if ($hasCalculatedValue) {
|
||||
$cell->setValueExplicit($cellDataFormula, $type);
|
||||
if ($cellDataType === 'array') {
|
||||
$cell->setFormulaAttributes(['t' => 'array', 'ref' => $cellDataRef]);
|
||||
}
|
||||
} elseif ($type !== '' || $dataValue !== null) {
|
||||
$cell->setValueExplicit($dataValue, $type);
|
||||
}
|
||||
|
||||
if ($hasCalculatedValue) {
|
||||
$cell->setCalculatedValue($dataValue, $type === DataType::TYPE_NUMERIC);
|
||||
}
|
||||
|
||||
// Set other properties
|
||||
if ($formatting !== null) {
|
||||
$spreadsheet->getActiveSheet()
|
||||
->getStyle($columnID . $rID)
|
||||
->getNumberFormat()
|
||||
->setFormatCode($formatting);
|
||||
} else {
|
||||
$spreadsheet->getActiveSheet()
|
||||
->getStyle($columnID . $rID)
|
||||
->getNumberFormat()
|
||||
->setFormatCode(NumberFormat::FORMAT_GENERAL);
|
||||
}
|
||||
|
||||
if ($hyperlink !== null) {
|
||||
if ($hyperlink[0] === '#') {
|
||||
$hyperlink = 'sheet://' . substr($hyperlink, 1);
|
||||
}
|
||||
$cell->getHyperlink()
|
||||
->setUrl($hyperlink);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merged cells
|
||||
$this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet);
|
||||
|
||||
++$columnID;
|
||||
}
|
||||
$rowID += $rowRepeats;
|
||||
}
|
||||
|
||||
private static function extractNodeName(string $key): string
|
||||
{
|
||||
// Remove ns from node name
|
||||
if (str_contains($key, ':')) {
|
||||
$keyChunks = explode(':', $key);
|
||||
$key = array_pop($keyChunks);
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $columnWidths
|
||||
*/
|
||||
private function processTableHeaderColumns(
|
||||
DOMElement $childNode,
|
||||
string $tableNs,
|
||||
array $columnWidths,
|
||||
int &$tableColumnIndex,
|
||||
Spreadsheet $spreadsheet
|
||||
): void {
|
||||
foreach ($childNode->childNodes as $grandchildNode) {
|
||||
/** @var DOMElement $grandchildNode */
|
||||
$grandkey = self::extractNodeName($grandchildNode->nodeName);
|
||||
switch ($grandkey) {
|
||||
case 'table-column':
|
||||
$this->processTableColumn(
|
||||
$grandchildNode,
|
||||
$tableNs,
|
||||
$columnWidths,
|
||||
$tableColumnIndex,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $columnWidths
|
||||
*/
|
||||
private function processTableColumnGroup(
|
||||
DOMElement $childNode,
|
||||
string $tableNs,
|
||||
array $columnWidths,
|
||||
int &$tableColumnIndex,
|
||||
Spreadsheet $spreadsheet
|
||||
): void {
|
||||
foreach ($childNode->childNodes as $grandchildNode) {
|
||||
/** @var DOMElement $grandchildNode */
|
||||
$grandkey = self::extractNodeName($grandchildNode->nodeName);
|
||||
switch ($grandkey) {
|
||||
case 'table-column':
|
||||
$this->processTableColumn(
|
||||
$grandchildNode,
|
||||
$tableNs,
|
||||
$columnWidths,
|
||||
$tableColumnIndex,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
case 'table-header-columns':
|
||||
case 'table-columns':
|
||||
$this->processTableHeaderColumns(
|
||||
$grandchildNode,
|
||||
$tableNs,
|
||||
$columnWidths,
|
||||
$tableColumnIndex,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
case 'table-column-group':
|
||||
$this->processTableColumnGroup(
|
||||
$grandchildNode,
|
||||
$tableNs,
|
||||
$columnWidths,
|
||||
$tableColumnIndex,
|
||||
$spreadsheet
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $columnWidths
|
||||
*/
|
||||
private function processTableColumn(
|
||||
DOMElement $childNode,
|
||||
string $tableNs,
|
||||
array $columnWidths,
|
||||
int &$tableColumnIndex,
|
||||
Spreadsheet $spreadsheet
|
||||
): void {
|
||||
if ($childNode->hasAttributeNS($tableNs, 'number-columns-repeated')) {
|
||||
$rowRepeats = (int) $childNode->getAttributeNS($tableNs, 'number-columns-repeated');
|
||||
} else {
|
||||
$rowRepeats = 1;
|
||||
}
|
||||
$tableStyleName = $childNode->getAttributeNS($tableNs, 'style-name');
|
||||
if (isset($columnWidths[$tableStyleName])) {
|
||||
$columnWidth = new HelperDimension($columnWidths[$tableStyleName]);
|
||||
$tableColumnString = Coordinate::stringFromColumnIndex($tableColumnIndex);
|
||||
for ($rowRepeats2 = $rowRepeats; $rowRepeats2 > 0; --$rowRepeats2) {
|
||||
/** @var string $tableColumnString */
|
||||
$spreadsheet->getActiveSheet()
|
||||
->getColumnDimension($tableColumnString)
|
||||
->setWidth($columnWidth->toUnit('cm'), 'cm');
|
||||
++$tableColumnString;
|
||||
}
|
||||
}
|
||||
$tableColumnIndex += $rowRepeats;
|
||||
}
|
||||
|
||||
private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void
|
||||
{
|
||||
$dom = new DOMDocument('1.01', 'UTF-8');
|
||||
@@ -688,9 +946,7 @@ class Ods extends BaseReader
|
||||
$this->getSecurityScannerOrThrow()
|
||||
->scan($zip->getFromName('settings.xml'))
|
||||
);
|
||||
//$xlinkNs = $dom->lookupNamespaceUri('xlink');
|
||||
$configNs = (string) $dom->lookupNamespaceUri('config');
|
||||
//$oooNs = $dom->lookupNamespaceUri('ooo');
|
||||
$officeNs = (string) $dom->lookupNamespaceUri('office');
|
||||
$settings = $dom->getElementsByTagNameNS($officeNs, 'settings')
|
||||
->item(0);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Reader\Ods;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Reader\Ods as OdsReader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class NestedTableRowTest extends TestCase
|
||||
{
|
||||
public function testTableHeaderRows(): void
|
||||
{
|
||||
$infile = 'tests/data/Reader/Ods/issue.4528.ods';
|
||||
$reader = new OdsReader();
|
||||
$spreadsheet = $reader->load($infile);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
self::assertSame('Atterissage', $sheet->getCell('AS1')->getValue());
|
||||
self::assertNull($sheet->getCell('AS2')->getValue());
|
||||
self::assertSame('jour', $sheet->getCell('AS3')->getValue());
|
||||
self::assertSame('=SUM(Y3:INDIRECT(CONCATENATE("Y",$C$3)))', $sheet->getCell('AS4')->getValue());
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
|
||||
public function testTableRowGroup(): void
|
||||
{
|
||||
$infile = 'tests/data/Reader/Ods/issue.2507.ods';
|
||||
$reader = new OdsReader();
|
||||
$spreadsheet = $reader->load($infile);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$values = $sheet->rangeToArray('B3:C7', null, false, false);
|
||||
$expected = [
|
||||
['Номенклатура', "Складское наличие,\nКол-во"], // before table-row-group
|
||||
['Квадрат 140х140мм ст.5ХНМ (т)', 0.225], // within table-row-group
|
||||
['Квадрат 200х200мм ст.3 (т)', 1.700],
|
||||
['Квадрат 210х210мм ст.65Г (т)', 0.280],
|
||||
['Квадрат 250х250мм ст.45 (т)', 0.133],
|
||||
];
|
||||
self::assertSame($expected, $values);
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user