Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,23 @@ export function ShapeElement({ elementInfo, selectElement }: ShapeElementProps)
defaultColor: '#000000',
};
if (!elementInfo.text) return defaultText;
return elementInfo.text;
return {
...elementInfo.text,
content: typeof elementInfo.text.content === 'string' ? elementInfo.text.content : '',
};
}, [elementInfo.text]);
const viewBoxWidth =
Array.isArray(elementInfo.viewBox) &&
Number.isFinite(elementInfo.viewBox[0]) &&
elementInfo.viewBox[0] > 0
? elementInfo.viewBox[0]
: elementInfo.width || 1;
const viewBoxHeight =
Array.isArray(elementInfo.viewBox) &&
Number.isFinite(elementInfo.viewBox[1]) &&
elementInfo.viewBox[1] > 0
? elementInfo.viewBox[1]
: elementInfo.height || 1;

// Update text content
const updateText = useCallback(
Expand All @@ -84,7 +99,8 @@ export function ShapeElement({ elementInfo, selectElement }: ShapeElementProps)
const checkEmptyText = useCallback(() => {
if (!elementInfo.text) return;

const pureText = elementInfo.text.content.replace(/<[^>]+>/g, '');
const content = typeof elementInfo.text.content === 'string' ? elementInfo.text.content : '';
const pureText = content.replace(/<[^>]+>/g, '');
if (!pureText) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 'text' is specific to PPTShapeElement, not in keyof PPTElement union
removeElementProps({ id: elementInfo.id, propName: 'text' as any });
Expand Down Expand Up @@ -144,8 +160,8 @@ export function ShapeElement({ elementInfo, selectElement }: ShapeElementProps)
)}
</defs>
<g
transform={`scale(${elementInfo.width / elementInfo.viewBox[0]}, ${
elementInfo.height / elementInfo.viewBox[1]
transform={`scale(${elementInfo.width / viewBoxWidth}, ${
elementInfo.height / viewBoxHeight
}) translate(0,0) matrix(1,0,0,1,0,0)`}
>
<path
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ interface StaticTableProps {
*/
export function StaticTable({ elementInfo }: StaticTableProps) {
const { width, data, colWidths, cellMinHeight, outline, theme } = elementInfo;
const tableData = useMemo(() => (Array.isArray(data) ? data : []), [data]);
const tableColWidths = useMemo(() => (Array.isArray(colWidths) ? colWidths : []), [colWidths]);
const safeWidth = Number.isFinite(width) && width > 0 ? width : 1;
const safeCellMinHeight =
Number.isFinite(cellMinHeight) && cellMinHeight >= 0 ? cellMinHeight : 40;

const hiddenCells = useMemo(() => getHiddenCells(data), [data]);
const hiddenCells = useMemo(() => getHiddenCells(tableData), [tableData]);

const [subThemeDark, subThemeLight] = useMemo(() => {
if (!theme) return ['', ''];
Expand All @@ -42,8 +47,8 @@ export function StaticTable({ elementInfo }: StaticTableProps) {
if (cellBackcolor) return cellBackcolor;
if (!theme) return undefined;

const rowCount = data.length;
const colCount = data[0]?.length ?? 0;
const rowCount = tableData.length;
const colCount = tableData[0]?.length ?? 0;

// Row header (first row) gets theme color
if (theme.rowHeader && rowIdx === 0) return theme.color;
Expand All @@ -66,7 +71,7 @@ export function StaticTable({ elementInfo }: StaticTableProps) {
*/
const getHeaderTextColor = (rowIdx: number): string | undefined => {
if (!theme) return undefined;
const rowCount = data.length;
const rowCount = tableData.length;
if (theme.rowHeader && rowIdx === 0) return '#fff';
if (theme.rowFooter && rowIdx === rowCount - 1) return '#fff';
return undefined;
Expand All @@ -81,19 +86,22 @@ export function StaticTable({ elementInfo }: StaticTableProps) {
}}
>
<colgroup>
{colWidths.map((w, i) => (
<col key={i} style={{ width: `${w * width}px` }} />
{tableColWidths.map((w, i) => (
<col key={i} style={{ width: `${(Number.isFinite(w) ? w : 1) * safeWidth}px` }} />
))}
</colgroup>
<tbody>
{data.map((row, rowIdx) => (
<tr key={rowIdx} style={{ height: `${cellMinHeight}px` }}>
{row.map((cell, colIdx) => {
{tableData.map((row, rowIdx) => (
<tr key={rowIdx} style={{ height: `${safeCellMinHeight}px` }}>
{(Array.isArray(row) ? row : []).map((cell, colIdx) => {
if (hiddenCells.has(`${rowIdx}_${colIdx}`)) return null;
if (!cell) return null;

const bgColor = getCellBg(rowIdx, colIdx, cell.style?.backcolor);
const headerColor = getHeaderTextColor(rowIdx);
const textStyle = getTextStyle(cell.style);
const colspan = Number.isFinite(cell.colspan) && cell.colspan > 0 ? cell.colspan : 1;
const rowspan = Number.isFinite(cell.rowspan) && cell.rowspan > 0 ? cell.rowspan : 1;

// Header text color should be overridden only if cell doesn't have its own color
if (headerColor && !cell.style?.color) {
Expand All @@ -102,9 +110,9 @@ export function StaticTable({ elementInfo }: StaticTableProps) {

return (
<td
key={cell.id}
colSpan={cell.colspan > 1 ? cell.colspan : undefined}
rowSpan={cell.rowspan > 1 ? cell.rowspan : undefined}
key={cell.id || `${rowIdx}-${colIdx}`}
colSpan={colspan > 1 ? colspan : undefined}
rowSpan={rowspan > 1 ? rowspan : undefined}
style={{
border: borderStyle,
backgroundColor: bgColor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export function getTextStyle(style?: TableCellStyle): CSSProperties {
/**
* Format text: convert \n to <br/> and spaces to &nbsp;
*/
export function formatText(text: string): string {
export function formatText(text: unknown): string {
if (typeof text !== 'string') return '';
return text.replace(/\n/g, '<br/>').replace(/ /g, '&nbsp;');
}

Expand All @@ -39,16 +40,19 @@ export function getHiddenCells(data: TableCell[][]): Set<string> {
const hidden = new Set<string>();

for (let rowIdx = 0; rowIdx < data.length; rowIdx++) {
const row = data[rowIdx];
if (!Array.isArray(row)) continue;

let realColIdx = 0;
for (let colIdx = 0; colIdx < data[rowIdx].length; colIdx++) {
for (let colIdx = 0; colIdx < row.length; colIdx++) {
// Skip positions already occupied by a previous merge
while (hidden.has(`${rowIdx}_${realColIdx}`)) {
realColIdx++;
}

const cell = data[rowIdx][colIdx];
const colspan = cell.colspan ?? 1;
const rowspan = cell.rowspan ?? 1;
const cell = row[colIdx];
const colspan = Number.isFinite(cell?.colspan) && cell.colspan > 0 ? cell.colspan : 1;
const rowspan = Number.isFinite(cell?.rowspan) && cell.rowspan > 0 ? cell.rowspan : 1;

if (colspan > 1 || rowspan > 1) {
for (let r = 0; r < rowspan; r++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ export function TextElement({ elementInfo, selectElement }: TextElementProps) {
const checkEmptyText = useCallback(() => {
const debouncedCheck = debounce(
() => {
const pureText = elementInfo.content.replace(/<[^>]+>/g, '');
const content = typeof elementInfo.content === 'string' ? elementInfo.content : '';
const pureText = content.replace(/<[^>]+>/g, '');
if (!pureText) deleteElement(elementInfo.id);
},
300,
Expand Down Expand Up @@ -209,7 +210,7 @@ export function TextElement({ elementInfo, selectElement }: TextElementProps) {
defaultColor={elementInfo.defaultColor}
defaultFontName={elementInfo.defaultFontName}
editable={!elementInfo.lock}
value={elementInfo.content}
value={typeof elementInfo.content === 'string' ? elementInfo.content : ''}
onUpdate={({ value, ignore }) => updateContent(value, ignore)}
onMouseDown={(e) => handleSelectElement(e as React.MouseEvent, false)}
/>
Expand Down
12 changes: 12 additions & 0 deletions tests/slide-renderer/table-utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';

import { getHiddenCells } from '@/components/slide-renderer/components/element/TableElement/tableUtils';

describe('tableUtils', () => {
it('tolerates malformed rows and null cells when computing hidden cells', () => {
const data = [[{ id: 'a', text: 'x', colspan: 2 }, null], null, 5] as never;

expect(() => getHiddenCells(data)).not.toThrow();
expect(getHiddenCells(data)).toEqual(new Set(['0_1']));
});
});
Loading