/**
* Table node parser — handles graphicFrame elements containing a:tbl.
*/
import { SafeXmlNode } from '../../parser/XmlParser';
import { BaseNodeData, parseBaseProps } from './BaseNode';
import { TextBody, parseTextBody } from './ShapeNode';
import { emuToPx } from '../../parser/units';
export interface TableCell {
gridSpan: number;
rowSpan: number;
hMerge: boolean;
vMerge: boolean;
textBody?: TextBody;
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
properties?: SafeXmlNode;
}
export interface TableRow {
height: number;
cells: TableCell[];
}
export interface TableNodeData extends BaseNodeData {
nodeType: 'table';
columns: number[];
rows: TableRow[];
/** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */
properties?: SafeXmlNode;
tableStyleId?: string;
}
/**
* Parse a single table cell (`a:tc`).
*/
function parseCell(tcNode: SafeXmlNode): TableCell {
const gridSpan = tcNode.numAttr('gridSpan') ?? 1;
const rowSpan = tcNode.numAttr('rowSpan') ?? 1;
const hMerge = tcNode.attr('hMerge') === '1' || tcNode.attr('hMerge') === 'true';
const vMerge = tcNode.attr('vMerge') === '1' || tcNode.attr('vMerge') === 'true';
// Cell text body
const txBody = tcNode.child('txBody');
const textBody = parseTextBody(txBody);
// Cell properties
const tcPr = tcNode.child('tcPr');
return {
gridSpan,
rowSpan,
hMerge,
vMerge,
textBody,
properties: tcPr.exists() ? tcPr : undefined,
};
}
/**
* Parse a table row (`a:tr`).
*/
function parseRow(trNode: SafeXmlNode): TableRow {
const height = emuToPx(trNode.numAttr('h') ?? 0);
const cells: TableCell[] = [];
for (const tcNode of trNode.children('tc')) {
cells.push(parseCell(tcNode));
}
return { height, cells };
}
/**
* Locate the `a:tbl` element inside a graphicFrame.
* Path: `a:graphic > a:graphicData > a:tbl`
*/
function findTable(frameNode: SafeXmlNode): SafeXmlNode {
const graphic = frameNode.child('graphic');
const graphicData = graphic.child('graphicData');
return graphicData.child('tbl');
}
/**
* Extract the table style ID from tblPr.
* It can be in `a:tblStyle@val` or as a direct `tblStyle` attribute.
*/
function extractTableStyleId(tblPr: SafeXmlNode): string | undefined {
// Try {UUID} (most common in OOXML)
const tableStyleIdNode = tblPr.child('tableStyleId');
if (tableStyleIdNode.exists()) {
return tableStyleIdNode.text() || tableStyleIdNode.attr('val') || undefined;
}
// Try
const tblStyleNode = tblPr.child('tblStyle');
if (tblStyleNode.exists()) {
return tblStyleNode.attr('val') ?? (tblStyleNode.text() || undefined);
}
// Try direct attribute
return tblPr.attr('tblStyle') ?? undefined;
}
/**
* Parse a graphicFrame XML node containing a table into TableNodeData.
*/
export function parseTableNode(frameNode: SafeXmlNode): TableNodeData {
const base = parseBaseProps(frameNode);
const tbl = findTable(frameNode);
// --- Column widths ---
const tblGrid = tbl.child('tblGrid');
const columns: number[] = [];
for (const gridCol of tblGrid.children('gridCol')) {
columns.push(emuToPx(gridCol.numAttr('w') ?? 0));
}
// --- Rows ---
const rows: TableRow[] = [];
for (const trNode of tbl.children('tr')) {
rows.push(parseRow(trNode));
}
// --- Table properties ---
const tblPr = tbl.child('tblPr');
const tableStyleId = extractTableStyleId(tblPr);
return {
...base,
nodeType: 'table',
columns,
rows,
properties: tblPr.exists() ? tblPr : undefined,
tableStyleId,
};
}