1
14
15export interface TableContent {
16 rows: string[][];
17 width?: number;
18}
19
20export class TableOpError extends Error {}
21
22function asTable(content: unknown): TableContent {
23 if (typeof content !== "object" || content === null) throw new TableOpError("table content must be an object");
24 const c = content as Record<string, unknown>;
25 if (!Array.isArray(c.rows)) throw new TableOpError("table content.rows must be an array");
26 for (const r of c.rows) {
27 if (!Array.isArray(r) || !r.every((x) => typeof x === "string")) throw new TableOpError("table content.rows must be string[][]");
28 }
29 const t: TableContent = { rows: (c.rows as string[][]).map((r) => [...r]) };
30 if (typeof c.width === "number") t.width = c.width;
31 return t;
32}
33
34function normalizeWidth(rows: string[][]): string[][] {
35
36
37 const w = rows.reduce((m, r) => Math.max(m, r.length), 0);
38 return rows.map((r) => {
39 const out = r.slice(0, w);
40 while (out.length < w) out.push("");
41 return out;
42 });
43}
44
45
46export function tableColumns(content: unknown): number {
47 const t = asTable(content);
48 return t.rows.reduce((m, r) => Math.max(m, r.length), 0);
49}
50
51
52export function tableRows(content: unknown): number {
53 return asTable(content).rows.length;
54}
55
56function withRows(content: unknown, rows: string[][]): TableContent {
57 const t = asTable(content);
58 const out: TableContent = { rows: normalizeWidth(rows) };
59 if (t.width !== undefined) out.width = t.width;
60 return out;
61}
62
63
64export function insertRow(content: unknown, afterRowIndex: number): TableContent {
65 const t = asTable(content);
66 const w = tableColumns(t);
67 const idx = afterRowIndex === -1 ? 0 : afterRowIndex + 1;
68 if (idx < 0 || idx > t.rows.length) throw new TableOpError(`row index out of range: ${afterRowIndex}`);
69 const rows = [...t.rows];
70 rows.splice(idx, 0, new Array(w).fill(""));
71 return withRows(t, rows);
72}
73
74
75export function removeRow(content: unknown, rowIndex: number): TableContent {
76 const t = asTable(content);
77 if (rowIndex < 0 || rowIndex >= t.rows.length) throw new TableOpError(`row index out of range: ${rowIndex}`);
78 if (t.rows.length <= 1) throw new TableOpError("cannot remove the only row");
79 const rows = t.rows.filter((_, i) => i !== rowIndex);
80 return withRows(t, rows);
81}
82
83
84export function moveRow(content: unknown, from: number, toIndex: number): TableContent {
85 const t = asTable(content);
86 if (from < 0 || from >= t.rows.length) throw new TableOpError(`from row out of range: ${from}`);
87 if (toIndex < 0 || toIndex >= t.rows.length) throw new TableOpError(`to row out of range: ${toIndex}`);
88 const rows = [...t.rows];
89 const moved = rows.splice(from, 1)[0];
90 if (moved === undefined) throw new TableOpError(`from row out of range: ${from}`);
91 rows.splice(toIndex, 0, moved);
92 return withRows(t, rows);
93}
94
95
96export function insertColumn(content: unknown, afterColIndex: number): TableContent {
97 const t = asTable(content);
98 const idx = afterColIndex === -1 ? 0 : afterColIndex + 1;
99 const rows = normalizeWidth(t.rows).map((r) => {
100 const out = [...r];
101 out.splice(idx, 0, "");
102 return out;
103 });
104 return withRows(t, rows);
105}
106
107
108export function removeColumn(content: unknown, colIndex: number): TableContent {
109 const t = asTable(content);
110 const w = tableColumns(t);
111 if (colIndex < 0 || colIndex >= w) throw new TableOpError(`column index out of range: ${colIndex}`);
112 if (w <= 1) throw new TableOpError("cannot remove the only column");
113 const rows = normalizeWidth(t.rows).map((r) => r.filter((_, i) => i !== colIndex));
114 return withRows(t, rows);
115}
116
117
118export function moveColumn(content: unknown, from: number, toIndex: number): TableContent {
119 const t = asTable(content);
120 const w = tableColumns(t);
121 if (from < 0 || from >= w) throw new TableOpError(`from column out of range: ${from}`);
122 if (toIndex < 0 || toIndex >= w) throw new TableOpError(`to column out of range: ${toIndex}`);
123 const rows = normalizeWidth(t.rows).map((r) => {
124 const out = [...r];
125 const c = out.splice(from, 1)[0] ?? "";
126 out.splice(toIndex, 0, c);
127 return out;
128 });
129 return withRows(t, rows);
130}
131
132
133export function setCell(content: unknown, row: number, col: number, value: string): TableContent {
134 const t = asTable(content);
135 const w = tableColumns(t);
136 if (row < 0 || row >= t.rows.length) throw new TableOpError(`row out of range: ${row}`);
137 if (col < 0 || col >= w) throw new TableOpError(`column out of range: ${col}`);
138 const rows = normalizeWidth(t.rows).map((r, i) => (i === row ? r.map((c, j) => (j === col ? value : c)) : r));
139 return withRows(t, rows);
140}
141