/** * Table node parser — handles graphicFrame elements containing a:tbl. */ import { emuToPx } from '../../parser/units' import type { SafeXmlNode } from '../../parser/xml-parser' import { type BaseNodeData, parseBaseProps } from './base-node' import { parseTextBody, type TextBody } from './shape-node' 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 } 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, } }