import type { TipTapJSON } from "@/lib/drive/richtext-import" import type { DocsTableBorder } from "@/lib/drive/docs-table-types" const BORDER_STYLES = new Set([ "single", "dashed", "dotted", "double", "dotDash", "dotDotDash", "none", ]) function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value) } function parseCssBorderValue(value: string): DocsTableBorder | null { const trimmed = value.trim() if (!trimmed || trimmed === "none" || trimmed === "0") return null const parts = trimmed.split(/\s+/) if (parts.length < 2) return null const size = parseFloat(parts[0] ?? "") const styleToken = parts[1] ?? "solid" const color = parts.slice(2).join(" ") || "#000000" const style = styleToken === "solid" ? "single" : styleToken return { size: Number.isFinite(size) ? Math.max(1, Math.round(size * 8)) : 8, style, color, } } /** Normalize border attrs for @docen/export-docx (DOCX Border objects). */ export function normalizeTableBorderForExport( border: unknown ): DocsTableBorder | null { if (!border) return null if (typeof border === "string") return parseCssBorderValue(border) if (!isRecord(border)) return null const styleRaw = typeof border.style === "string" ? border.style : "single" const style = styleRaw === "solid" ? "single" : styleRaw if (style === "none" || style === "nil") return null const color = typeof border.color === "string" && border.color ? border.color : "#000000" const size = typeof border.size === "number" && Number.isFinite(border.size) ? border.size : 8 const normalized: DocsTableBorder = { size, style: BORDER_STYLES.has(style) ? style : "single", color, } if (typeof border.space === "number" && Number.isFinite(border.space)) { normalized.space = border.space } return normalized } function normalizeCellAttrsForExport(attrs: Record): Record { const next = { ...attrs } if (typeof next.backgroundColor === "string") { next.backgroundColor = next.backgroundColor.trim() || null } if (Array.isArray(next.colwidth)) { const colwidth = next.colwidth .map((value) => (typeof value === "number" && Number.isFinite(value) ? value : null)) .filter((value): value is number => value != null && value > 0) if (colwidth.length) next.colwidth = colwidth else delete next.colwidth } for (const side of ["borderTop", "borderRight", "borderBottom", "borderLeft"] as const) { const normalized = normalizeTableBorderForExport(next[side]) if (normalized) next[side] = normalized else delete next[side] } return next } function normalizeTableNodeForExport(node: TipTapJSON): TipTapJSON { if (!isRecord(node) || node.type !== "table") return node const rows = (Array.isArray(node.content) ? node.content : []) as TipTapJSON[] const firstRow = rows.find((row) => isRecord(row) && row.type === "tableRow") const firstRowCells = (Array.isArray(firstRow?.content) ? firstRow.content : []) as TipTapJSON[] const colwidths = firstRowCells .map((cell) => { if (!isRecord(cell) || !isRecord(cell.attrs)) return null const raw = cell.attrs.colwidth if (Array.isArray(raw) && typeof raw[0] === "number") return raw[0] if (typeof raw === "number") return raw return null }) .filter((value): value is number => value != null && value > 0) const tableAttrs = isRecord(node.attrs) ? { ...node.attrs } : {} if (tableAttrs.layout === "fixed" && !colwidths.length) { delete tableAttrs.layout } const content = rows.map((row) => { if (!isRecord(row) || row.type !== "tableRow") return row const rowAttrs = isRecord(row.attrs) ? { ...row.attrs } : {} if (typeof rowAttrs.rowHeight === "string" && !rowAttrs.rowHeight.trim()) { delete rowAttrs.rowHeight } if ( rowAttrs.rowHeightRule !== "exact" && rowAttrs.rowHeightRule !== "atLeast" ) { delete rowAttrs.rowHeightRule } const cells = Array.isArray(row.content) ? row.content : [] const normalizedCells = cells.map((cell, cellIndex) => { if ( !isRecord(cell) || (cell.type !== "tableCell" && cell.type !== "tableHeader") ) { return cell } const attrs = normalizeCellAttrsForExport( isRecord(cell.attrs) ? { ...cell.attrs } : {} ) if (!attrs.colwidth && colwidths[cellIndex]) { attrs.colwidth = [colwidths[cellIndex]] } const cellContent = Array.isArray(cell.content) ? cell.content.map((child) => prepareTablesForDocxExport(child)) : [{ type: "paragraph" }] return { ...cell, attrs: Object.keys(attrs).length ? attrs : undefined, content: cellContent, } }) return { ...row, attrs: Object.keys(rowAttrs).length ? rowAttrs : undefined, content: normalizedCells, } }) return { ...node, attrs: Object.keys(tableAttrs).length ? tableAttrs : undefined, content, } } /** Walk TipTap JSON and normalize table attrs before DOCX export. */ export function prepareTablesForDocxExport(content: TipTapJSON): TipTapJSON { if (!isRecord(content)) return content if (content.type === "table") { return normalizeTableNodeForExport(content) } if (Array.isArray(content.content)) { return { ...content, content: content.content.map((child) => prepareTablesForDocxExport(child)), } } return content }