188 lines
5.5 KiB
TypeScript
188 lines
5.5 KiB
TypeScript
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<string, unknown> {
|
|
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<string, unknown>): Record<string, unknown> {
|
|
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
|
|
}
|