Skip to content

Commit ba079b3

Browse files
committed
Various Writers RichText TextElement Should Inherit Cell Style
Fix PHPOffice#1154, which went stale over 5 years ago, and is now reopened. RichText elements can be a Run, which sets its own style, or a TextElement, which doesn't. As the issue states, TextElement handling is inconsistent. This PR forces it to inherit the style of the cell. As implemented, this will change it to a Run when it is read in, but there should be no practical difference as far as the end-user is concerned. There is no change as far as Run is concerned. The user suggested 3 options - TextElement and Run both inherit for any unspecified style elements, TextElement inherits and Run does not, neither inherits. Option 2 makes most sense to me. Option 1 may not even be possible (we can't tell if, say, the user explicityl set Italic to false, or if that was just the default choice). Tests are added for all of Xlsx, Xls, and Html. Html reading of RichText elements is not well-supported. The tests are fairly difficult to understand. A new sample is added to demonstrate this change.
1 parent 6b2767c commit ba079b3

File tree

10 files changed

+340
-34
lines changed

10 files changed

+340
-34
lines changed

samples/Basic4/42b_RichText.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
use PhpOffice\PhpSpreadsheet\RichText\RichText;
4+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
5+
6+
require __DIR__ . '/../Header.php';
7+
8+
/** @var PhpOffice\PhpSpreadsheet\Helper\Sample $helper */
9+
$helper->log('Create new Spreadsheet object');
10+
$spreadsheet = new Spreadsheet();
11+
$sheet = $spreadsheet->getActiveSheet();
12+
13+
$rtf = new RichText();
14+
$rtf->createText('~Cell Style~');
15+
$rtf->createTextRun('~RTF Style~')->getFont()?->setItalic(true);
16+
$rtf->createText('~No Style~');
17+
18+
$sheet->getCell('A1')->setValue($rtf);
19+
$sheet->getStyle('A1')->getFont()->setBold(true);
20+
21+
// Save
22+
$helper->write($spreadsheet, __FILE__, ['Xlsx', 'Xls', 'Html']);

src/PhpSpreadsheet/Writer/Html.php

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,24 +1096,30 @@ private function createCSSStyleAlignment(Alignment $alignment): array
10961096
*
10971097
* @return string[]
10981098
*/
1099-
private function createCSSStyleFont(Font $font): array
1099+
private function createCSSStyleFont(Font $font, bool $useDefaults = false): array
11001100
{
11011101
// Construct CSS
11021102
$css = [];
11031103

11041104
// Create CSS
11051105
if ($font->getBold()) {
11061106
$css['font-weight'] = 'bold';
1107+
} elseif ($useDefaults) {
1108+
$css['font-weight'] = 'normal';
11071109
}
11081110
if ($font->getUnderline() != Font::UNDERLINE_NONE && $font->getStrikethrough()) {
11091111
$css['text-decoration'] = 'underline line-through';
11101112
} elseif ($font->getUnderline() != Font::UNDERLINE_NONE) {
11111113
$css['text-decoration'] = 'underline';
11121114
} elseif ($font->getStrikethrough()) {
11131115
$css['text-decoration'] = 'line-through';
1116+
} elseif ($useDefaults) {
1117+
$css['text-decoration'] = 'normal';
11141118
}
11151119
if ($font->getItalic()) {
11161120
$css['font-style'] = 'italic';
1121+
} elseif ($useDefaults) {
1122+
$css['font-style'] = 'normal';
11171123
}
11181124

11191125
$css['color'] = '#' . $font->getColor()->getRGB();
@@ -1344,22 +1350,23 @@ private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, i
13441350
return [$cell, $cssClass, $coordinate];
13451351
}
13461352

1347-
private function generateRowCellDataValueRich(RichText $richText): string
1353+
private function generateRowCellDataValueRich(RichText $richText, ?Font $defaultFont = null): string
13481354
{
13491355
$cellData = '';
13501356
// Loop through rich text elements
13511357
$elements = $richText->getRichTextElements();
13521358
foreach ($elements as $element) {
13531359
// Rich text start?
1354-
if ($element instanceof Run) {
1360+
$font = ($element instanceof Run) ? $element->getFont() : $defaultFont;
1361+
if ($element instanceof Run || $font !== null) {
13551362
$cellEnd = '';
1356-
if ($element->getFont() !== null) {
1357-
$cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($element->getFont())) . '">';
1363+
if ($font !== null) {
1364+
$cellData .= '<span style="' . $this->assembleCSS($this->createCSSStyleFont($font, true)) . '">';
13581365

1359-
if ($element->getFont()->getSuperscript()) {
1366+
if ($font->getSuperscript()) {
13601367
$cellData .= '<sup>';
13611368
$cellEnd = '</sup>';
1362-
} elseif ($element->getFont()->getSubscript()) {
1369+
} elseif ($font->getSubscript()) {
13631370
$cellData .= '<sub>';
13641371
$cellEnd = '</sub>';
13651372
}
@@ -1387,7 +1394,7 @@ private function generateRowCellDataValueRich(RichText $richText): string
13871394
private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void
13881395
{
13891396
if ($cell->getValue() instanceof RichText) {
1390-
$cellData .= $this->generateRowCellDataValueRich($cell->getValue());
1397+
$cellData .= $this->generateRowCellDataValueRich($cell->getValue(), $cell->getStyle()->getFont());
13911398
} else {
13921399
if ($this->preCalculateFormulas) {
13931400
try {

src/PhpSpreadsheet/Writer/Xls.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,26 @@ public function save($filename, int $flags = 0): void
151151
$cell = $this->writerWorksheets[$i]->phpSheet->getCellCollection()->get($coordinate);
152152
$cVal = $cell->getValue();
153153
if ($cVal instanceof RichText) {
154+
$active = $this->spreadsheet->getActiveSheetIndex();
155+
$sheet = $cell->getWorksheet();
156+
$selected = $sheet->getSelectedCells();
157+
$font = $cell->getStyle()->getFont();
158+
$sheet->setSelectedCells($selected);
159+
if ($active > -1) {
160+
$this->spreadsheet
161+
->setActiveSheetIndex($active);
162+
}
163+
$this->writerWorksheets[$i]
164+
->fontHashIndex[$font->getHashCode()] = $this->writerWorkbook->addFont($font);
154165
$elements = $cVal->getRichTextElements();
155166
foreach ($elements as $element) {
156167
if ($element instanceof Run) {
157168
$font = $element->getFont();
158169
if ($font !== null) {
159-
$this->writerWorksheets[$i]->fontHashIndex[$font->getHashCode()] = $this->writerWorkbook->addFont($font);
170+
$this->writerWorksheets[$i]
171+
->fontHashIndex[
172+
$font->getHashCode()
173+
] = $this->writerWorkbook->addFont($font);
160174
}
161175
}
162176
}

src/PhpSpreadsheet/Writer/Xls/Worksheet.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,19 @@ public function close(): void
381381
if ($getFont !== null) {
382382
$str_fontidx = $this->fontHashIndex[$getFont->getHashCode()];
383383
}
384+
} else {
385+
$styleArray = $this->phpSheet
386+
->getParent()
387+
?->getCellXfCollection();
388+
if ($styleArray !== null) {
389+
$font = $styleArray[$xfIndex - 15] ?? null;
390+
if ($font !== null) {
391+
$font = $font->getFont();
392+
}
393+
if ($font !== null) {
394+
$str_fontidx = $this->fontHashIndex[$font->getHashCode()];
395+
}
396+
}
384397
}
385398
$arrcRun[] = ['strlen' => $str_pos, 'fontidx' => $str_fontidx];
386399
// Position FROM

src/PhpSpreadsheet/Writer/Xlsx/StringTable.php

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PhpOffice\PhpSpreadsheet\RichText\Run;
1111
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1212
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
13+
use PhpOffice\PhpSpreadsheet\Style\Font;
1314
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as ActualWorksheet;
1415

1516
class StringTable extends WriterPart
@@ -111,7 +112,7 @@ public function writeStringTable(array $stringTable): string
111112
*
112113
* @param ?string $prefix Optional Namespace prefix
113114
*/
114-
public function writeRichText(XMLWriter $objWriter, RichText $richText, ?string $prefix = null): void
115+
public function writeRichText(XMLWriter $objWriter, RichText $richText, ?string $prefix = null, ?Font $defaultFont = null): void
115116
{
116117
if ($prefix !== null) {
117118
$prefix .= ':';
@@ -122,63 +123,64 @@ public function writeRichText(XMLWriter $objWriter, RichText $richText, ?string
122123
foreach ($elements as $element) {
123124
// r
124125
$objWriter->startElement($prefix . 'r');
126+
$font = ($element instanceof Run) ? $element->getFont() : $defaultFont;
125127

126128
// rPr
127-
if ($element instanceof Run && $element->getFont() !== null) {
129+
if ($font !== null) {
128130
// rPr
129131
$objWriter->startElement($prefix . 'rPr');
130132

131133
// rFont
132-
if ($element->getFont()->getName() !== null) {
134+
if ($font->getName() !== null) {
133135
$objWriter->startElement($prefix . 'rFont');
134-
$objWriter->writeAttribute('val', $element->getFont()->getName());
136+
$objWriter->writeAttribute('val', $font->getName());
135137
$objWriter->endElement();
136138
}
137139

138140
// Bold
139141
$objWriter->startElement($prefix . 'b');
140-
$objWriter->writeAttribute('val', ($element->getFont()->getBold() ? 'true' : 'false'));
142+
$objWriter->writeAttribute('val', ($font->getBold() ? 'true' : 'false'));
141143
$objWriter->endElement();
142144

143145
// Italic
144146
$objWriter->startElement($prefix . 'i');
145-
$objWriter->writeAttribute('val', ($element->getFont()->getItalic() ? 'true' : 'false'));
147+
$objWriter->writeAttribute('val', ($font->getItalic() ? 'true' : 'false'));
146148
$objWriter->endElement();
147149

148150
// Superscript / subscript
149-
if ($element->getFont()->getSuperscript() || $element->getFont()->getSubscript()) {
151+
if ($font->getSuperscript() || $font->getSubscript()) {
150152
$objWriter->startElement($prefix . 'vertAlign');
151-
if ($element->getFont()->getSuperscript()) {
153+
if ($font->getSuperscript()) {
152154
$objWriter->writeAttribute('val', 'superscript');
153-
} elseif ($element->getFont()->getSubscript()) {
155+
} elseif ($font->getSubscript()) {
154156
$objWriter->writeAttribute('val', 'subscript');
155157
}
156158
$objWriter->endElement();
157159
}
158160

159161
// Strikethrough
160162
$objWriter->startElement($prefix . 'strike');
161-
$objWriter->writeAttribute('val', ($element->getFont()->getStrikethrough() ? 'true' : 'false'));
163+
$objWriter->writeAttribute('val', ($font->getStrikethrough() ? 'true' : 'false'));
162164
$objWriter->endElement();
163165

164166
// Color
165-
if ($element->getFont()->getColor()->getARGB() !== null) {
167+
if ($font->getColor()->getARGB() !== null) {
166168
$objWriter->startElement($prefix . 'color');
167-
$objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB());
169+
$objWriter->writeAttribute('rgb', $font->getColor()->getARGB());
168170
$objWriter->endElement();
169171
}
170172

171173
// Size
172-
if ($element->getFont()->getSize() !== null) {
174+
if ($font->getSize() !== null) {
173175
$objWriter->startElement($prefix . 'sz');
174-
$objWriter->writeAttribute('val', (string) $element->getFont()->getSize());
176+
$objWriter->writeAttribute('val', (string) $font->getSize());
175177
$objWriter->endElement();
176178
}
177179

178180
// Underline
179-
if ($element->getFont()->getUnderline() !== null) {
181+
if ($font->getUnderline() !== null) {
180182
$objWriter->startElement($prefix . 'u');
181-
$objWriter->writeAttribute('val', $element->getFont()->getUnderline());
183+
$objWriter->writeAttribute('val', $font->getUnderline());
182184
$objWriter->endElement();
183185
}
184186

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalColorScale;
1818
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar;
1919
use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension;
20+
use PhpOffice\PhpSpreadsheet\Style\Font;
2021
use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension;
2122
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
2223
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet as PhpspreadsheetWorksheet;
@@ -1431,19 +1432,26 @@ private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $w
14311432
$objWriter->endElement();
14321433
}
14331434

1434-
private function writeCellInlineStr(XMLWriter $objWriter, string $mappedType, RichText|string $cellValue): void
1435+
private function writeCellInlineStr(XMLWriter $objWriter, string $mappedType, RichText|string $cellValue, ?Font $font): void
14351436
{
14361437
$objWriter->writeAttribute('t', $mappedType);
14371438
if (!$cellValue instanceof RichText) {
14381439
$objWriter->startElement('is');
14391440
$objWriter->writeElement(
14401441
't',
1441-
StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue, Settings::htmlEntityFlags()))
1442+
StringHelper::controlCharacterPHP2OOXML(
1443+
htmlspecialchars(
1444+
$cellValue,
1445+
Settings::htmlEntityFlags()
1446+
)
1447+
)
14421448
);
14431449
$objWriter->endElement();
14441450
} else {
14451451
$objWriter->startElement('is');
1446-
$this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $cellValue);
1452+
$this->getParentWriter()
1453+
->getWriterPartstringtable()
1454+
->writeRichText($objWriter, $cellValue, null, $font);
14471455
$objWriter->endElement();
14481456
}
14491457
}
@@ -1612,6 +1620,13 @@ private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksh
16121620
if (empty($xfi) && !$writeValue) {
16131621
return;
16141622
}
1623+
$styleArray = $this->getParentWriter()
1624+
->getSpreadsheet()
1625+
->getCellXfCollection();
1626+
$font = $styleArray[$xfi] ?? null;
1627+
if ($font !== null) {
1628+
$font = $font->getFont();
1629+
}
16151630
$objWriter->startElement('c');
16161631
$objWriter->writeAttribute('r', $cellAddress);
16171632
$mappedType = $pCell->getDataType();
@@ -1642,7 +1657,7 @@ private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksh
16421657
case 'inlinestr': // Inline string
16431658
/** @var RichText|string */
16441659
$richText = $cellValue;
1645-
$this->writeCellInlineStr($objWriter, $mappedType, $richText);
1660+
$this->writeCellInlineStr($objWriter, $mappedType, $richText, $font);
16461661

16471662
break;
16481663
case 's': // String

tests/PhpSpreadsheetTests/Writer/Html/HtmlCommentsTest.php

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PhpOffice\PhpSpreadsheet\Spreadsheet;
99
use PhpOffice\PhpSpreadsheet\Writer\Html;
1010
use PhpOffice\PhpSpreadsheetTests\Functional;
11+
use PHPUnit\Framework\Attributes\DataProvider;
1112

1213
class HtmlCommentsTest extends Functional\AbstractFunctional
1314
{
@@ -60,15 +61,15 @@ public static function providerCommentRichText(): array
6061
. 'comment.</div>' . PHP_EOL
6162
. 'Comment</td>'],
6263
'single line simple rich text' => [$richSingle, '<td class="column0 style0 s"><a class="comment-indicator"></a><div class="comment">'
63-
. "<span style=\"font-weight:bold; color:#000000; font-family:'Calibri'; font-size:11pt\">I am comment.</span></div>" . PHP_EOL
64+
. "<span style=\"font-weight:bold; text-decoration:normal; font-style:normal; color:#000000; font-family:'Calibri'; font-size:11pt\">I am comment.</span></div>" . PHP_EOL
6465
. 'Comment</td>'],
6566
'multi-line simple rich text' => [$richMultiSimple, '<td class="column0 style0 s"><a class="comment-indicator"></a><div class="comment">'
66-
. "<span style=\"font-weight:bold; color:#000000; font-family:'Calibri'; font-size:11pt\">I am <br />" . PHP_EOL
67+
. "<span style=\"font-weight:bold; text-decoration:normal; font-style:normal; color:#000000; font-family:'Calibri'; font-size:11pt\">I am <br />" . PHP_EOL
6768
. 'multi-line<br />' . PHP_EOL
6869
. 'comment.</span></div>' . PHP_EOL
6970
. 'Comment</td>'],
7071
'multi-line mixed rich text' => [$richMultiMixed, '<td class="column0 style0 s"><a class="comment-indicator"></a><div class="comment">I am<br />' . PHP_EOL
71-
. "<span style=\"font-weight:bold; color:#000000; font-family:'Calibri'; font-size:11pt\">multi-line</span><br />" . PHP_EOL
72+
. "<span style=\"font-weight:bold; text-decoration:normal; font-style:normal; color:#000000; font-family:'Calibri'; font-size:11pt\">multi-line</span><br />" . PHP_EOL
7273
. 'comment!</div>' . PHP_EOL
7374
. 'Comment</td>'],
7475
'script single' => [$scriptSingle, '<td class="column0 style0 s"><a class="comment-indicator"></a><div class="comment">'
@@ -77,12 +78,14 @@ public static function providerCommentRichText(): array
7778
];
7879
}
7980

80-
#[\PHPUnit\Framework\Attributes\DataProvider('providerCommentRichText')]
81+
#[DataProvider('providerCommentRichText')]
8182
public function testComments(RichText $richText, string $expected): void
8283
{
8384
$spreadsheet = new Spreadsheet();
8485

85-
$spreadsheet->getActiveSheet()->getCell('A1')->setValue('Comment');
86+
$spreadsheet->getActiveSheet()
87+
->getCell('A1')
88+
->setValue('Comment');
8689

8790
$spreadsheet->getActiveSheet()
8891
->getComment('A1')
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Writer\Html;
6+
7+
use PhpOffice\PhpSpreadsheet\RichText\RichText;
8+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
9+
use PhpOffice\PhpSpreadsheet\Writer\Html as HtmlWriter;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class RichTextTest extends TestCase
13+
{
14+
public function testRichText(): void
15+
{
16+
$spreadsheet = new Spreadsheet();
17+
$sheet = $spreadsheet->getActiveSheet();
18+
$rtf = new RichText();
19+
$rtf->createText('~Cell Style~');
20+
$rtf->createTextRun('~RTF Style~')->getFont()?->setItalic(true);
21+
$rtf->createText('~No Style~');
22+
$sheet->getCell('A1')->setValue($rtf);
23+
$sheet->getStyle('A1')->getFont()->setBold(true);
24+
25+
$fontStyle = $sheet->getStyle('A1')->getFont();
26+
self::assertTrue($fontStyle->getBold());
27+
self::assertFalse($fontStyle->getItalic());
28+
29+
$a1Value = $sheet->getCell('A1')->getValue();
30+
self::assertInstanceOf(RichText::class, $a1Value);
31+
$elements = $a1Value->getRichTextElements();
32+
self::assertCount(3, $elements);
33+
self::assertNull($elements[0]->getFont());
34+
$fontStyle = $elements[1]->getFont();
35+
self::assertNotNull($fontStyle);
36+
self::assertFalse($fontStyle->getBold());
37+
self::assertTrue($fontStyle->getItalic());
38+
self::assertNull($elements[0]->getFont());
39+
40+
$writer = new HtmlWriter($spreadsheet);
41+
$html = $writer->generateHtmlAll();
42+
self::assertStringContainsString('td.style1, th.style1 { vertical-align:bottom; border-bottom:none #000000; border-top:none #000000; border-left:none #000000; border-right:none #000000; font-weight:bold; color:#000000; font-family:\'Calibri\'; font-size:11pt }', $html, 'cell style');
43+
44+
self::assertStringContainsString('<td class="column0 style1 inlineStr"><span style="font-weight:bold; text-decoration:normal; font-style:normal; color:#000000; font-family:\'Calibri\'; font-size:11pt">~Cell Style~</span>', $html, 'cell style and first text element');
45+
46+
self::assertStringContainsString('<span style="font-weight:normal; text-decoration:normal; font-style:italic; color:#000000; font-family:\'Calibri\'; font-size:11pt">~RTF Style~</span>', $html, 'second text element');
47+
48+
self::assertStringContainsString('<span style="font-weight:bold; text-decoration:normal; font-style:normal; color:#000000; font-family:\'Calibri\'; font-size:11pt">~No Style~</span></td>', $html, 'third text element');
49+
50+
$spreadsheet->disconnectWorksheets();
51+
}
52+
}

0 commit comments

Comments
 (0)