Skip to content

Commit bb2c961

Browse files
authored
Merge pull request #74 from elijahr/elijahr/insert-table-with-data
feat: add insertTableWithData tool for pre-populated table insertion
2 parents e8b53fc + b3f4b53 commit bb2c961

File tree

3 files changed

+512
-0
lines changed

3 files changed

+512
-0
lines changed

src/tools/docs/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { register as deleteRange } from './deleteRange.js';
99

1010
// Structure
1111
import { register as insertTable } from './insertTable.js';
12+
import { register as insertTableWithData } from './insertTableWithData.js';
1213
import { register as insertPageBreak } from './insertPageBreak.js';
1314
import { register as insertImage } from './insertImage.js';
1415

@@ -26,6 +27,7 @@ export function registerDocsTools(server: FastMCP) {
2627

2728
// Structure
2829
insertTable(server);
30+
insertTableWithData(server);
2931
insertPageBreak(server);
3032
insertImage(server);
3133

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { buildInsertTableWithDataRequests } from './insertTableWithData.js';
3+
4+
describe('buildInsertTableWithDataRequests', () => {
5+
describe('table structure', () => {
6+
it('should create an insertTable request with correct dimensions', () => {
7+
const requests = buildInsertTableWithDataRequests(
8+
[
9+
['A', 'B'],
10+
['C', 'D'],
11+
],
12+
1,
13+
false
14+
);
15+
16+
const tableReq = requests.find((r) => r.insertTable);
17+
expect(tableReq).toBeDefined();
18+
expect(tableReq!.insertTable!.rows).toBe(2);
19+
expect(tableReq!.insertTable!.columns).toBe(2);
20+
expect(tableReq!.insertTable!.location!.index).toBe(1);
21+
});
22+
23+
it('should create a single-cell table', () => {
24+
const requests = buildInsertTableWithDataRequests([['Only']], 1, false);
25+
26+
const tableReq = requests.find((r) => r.insertTable);
27+
expect(tableReq!.insertTable!.rows).toBe(1);
28+
expect(tableReq!.insertTable!.columns).toBe(1);
29+
30+
const insertTexts = requests.filter((r) => r.insertText);
31+
expect(insertTexts).toHaveLength(1);
32+
expect(insertTexts[0].insertText!.text).toBe('Only');
33+
});
34+
35+
it('should pad ragged rows to the widest row', () => {
36+
const requests = buildInsertTableWithDataRequests([['A', 'B', 'C'], ['D']], 1, false);
37+
38+
const tableReq = requests.find((r) => r.insertTable);
39+
expect(tableReq!.insertTable!.rows).toBe(2);
40+
expect(tableReq!.insertTable!.columns).toBe(3);
41+
42+
// Only non-empty cells get insertText: A, B, C, D
43+
const insertTexts = requests.filter((r) => r.insertText);
44+
expect(insertTexts).toHaveLength(4);
45+
});
46+
});
47+
48+
describe('cell index math', () => {
49+
// For a 2x2 table at T=1, numCols=2:
50+
// baseCellIndex(1, r, c, 2) = 1 + 4 + r * (1 + 4) + 2*c = 5 + 5r + 2c
51+
// cell(0,0) base = 5, cell(0,1) base = 7
52+
// cell(1,0) base = 10, cell(1,1) base = 12
53+
54+
it('should insert text at correct indices for a 2x2 table', () => {
55+
const requests = buildInsertTableWithDataRequests(
56+
[
57+
['A', 'B'],
58+
['C', 'D'],
59+
],
60+
1,
61+
false
62+
);
63+
64+
const insertTexts = requests.filter((r) => r.insertText);
65+
expect(insertTexts).toHaveLength(4);
66+
67+
// cell(0,0): base=5, cumulative=0 → index=5
68+
expect(insertTexts[0].insertText!.text).toBe('A');
69+
expect(insertTexts[0].insertText!.location!.index).toBe(5);
70+
71+
// cell(0,1): base=7, cumulative=1 (len of "A") → index=8
72+
expect(insertTexts[1].insertText!.text).toBe('B');
73+
expect(insertTexts[1].insertText!.location!.index).toBe(8);
74+
75+
// cell(1,0): base=10, cumulative=2 (len of "A"+"B") → index=12
76+
expect(insertTexts[2].insertText!.text).toBe('C');
77+
expect(insertTexts[2].insertText!.location!.index).toBe(12);
78+
79+
// cell(1,1): base=12, cumulative=3 (len of "A"+"B"+"C") → index=15
80+
expect(insertTexts[3].insertText!.text).toBe('D');
81+
expect(insertTexts[3].insertText!.location!.index).toBe(15);
82+
});
83+
84+
it('should handle multi-character cell text correctly', () => {
85+
const requests = buildInsertTableWithDataRequests(
86+
[
87+
['Hello', 'World'],
88+
['Foo', 'Bar'],
89+
],
90+
1,
91+
false
92+
);
93+
94+
const insertTexts = requests.filter((r) => r.insertText);
95+
96+
// cell(0,0): base=5, cumulative=0 → 5
97+
expect(insertTexts[0].insertText!.location!.index).toBe(5);
98+
99+
// cell(0,1): base=7, cumulative=5 ("Hello") → 12
100+
expect(insertTexts[1].insertText!.location!.index).toBe(12);
101+
102+
// cell(1,0): base=10, cumulative=10 ("Hello"+"World") → 20
103+
expect(insertTexts[2].insertText!.location!.index).toBe(20);
104+
105+
// cell(1,1): base=12, cumulative=13 ("Hello"+"World"+"Foo") → 25
106+
expect(insertTexts[3].insertText!.location!.index).toBe(25);
107+
});
108+
109+
it('should work with a different starting index', () => {
110+
const requests = buildInsertTableWithDataRequests([['X']], 50, false);
111+
112+
const tableReq = requests.find((r) => r.insertTable);
113+
expect(tableReq!.insertTable!.location!.index).toBe(50);
114+
115+
const insertTexts = requests.filter((r) => r.insertText);
116+
// cell(0,0): 50 + 4 + 0 + 0 = 54
117+
expect(insertTexts[0].insertText!.location!.index).toBe(54);
118+
});
119+
120+
it('should handle a 3x3 table correctly', () => {
121+
const requests = buildInsertTableWithDataRequests(
122+
[
123+
['a', 'b', 'c'],
124+
['d', 'e', 'f'],
125+
['g', 'h', 'i'],
126+
],
127+
1,
128+
false
129+
);
130+
131+
const insertTexts = requests.filter((r) => r.insertText);
132+
expect(insertTexts).toHaveLength(9);
133+
134+
// For 3x3 at T=1: baseCellIndex = 1 + 4 + r*(1+6) + 2c = 5 + 7r + 2c
135+
// row 0: bases = 5, 7, 9
136+
// row 1: bases = 12, 14, 16
137+
// row 2: bases = 19, 21, 23
138+
139+
// cell(0,0): base=5, cum=0 → 5
140+
expect(insertTexts[0].insertText!.location!.index).toBe(5);
141+
// cell(0,1): base=7, cum=1 → 8
142+
expect(insertTexts[1].insertText!.location!.index).toBe(8);
143+
// cell(0,2): base=9, cum=2 → 11
144+
expect(insertTexts[2].insertText!.location!.index).toBe(11);
145+
// cell(1,0): base=12, cum=3 → 15
146+
expect(insertTexts[3].insertText!.location!.index).toBe(15);
147+
// cell(1,1): base=14, cum=4 → 18
148+
expect(insertTexts[4].insertText!.location!.index).toBe(18);
149+
// cell(1,2): base=16, cum=5 → 21
150+
expect(insertTexts[5].insertText!.location!.index).toBe(21);
151+
// cell(2,0): base=19, cum=6 → 25
152+
expect(insertTexts[6].insertText!.location!.index).toBe(25);
153+
// cell(2,1): base=21, cum=7 → 28
154+
expect(insertTexts[7].insertText!.location!.index).toBe(28);
155+
// cell(2,2): base=23, cum=8 → 31
156+
expect(insertTexts[8].insertText!.location!.index).toBe(31);
157+
});
158+
});
159+
160+
describe('empty cells', () => {
161+
it('should skip insertText for empty string cells', () => {
162+
const requests = buildInsertTableWithDataRequests(
163+
[
164+
['A', ''],
165+
['', 'D'],
166+
],
167+
1,
168+
false
169+
);
170+
171+
const insertTexts = requests.filter((r) => r.insertText);
172+
expect(insertTexts).toHaveLength(2);
173+
expect(insertTexts[0].insertText!.text).toBe('A');
174+
expect(insertTexts[1].insertText!.text).toBe('D');
175+
});
176+
177+
it('should compute correct indices when cells are skipped', () => {
178+
// 2x2 at T=1, but cell(0,1) is empty
179+
const requests = buildInsertTableWithDataRequests(
180+
[
181+
['A', ''],
182+
['C', 'D'],
183+
],
184+
1,
185+
false
186+
);
187+
188+
const insertTexts = requests.filter((r) => r.insertText);
189+
expect(insertTexts).toHaveLength(3);
190+
191+
// cell(0,0): base=5, cum=0 → 5
192+
expect(insertTexts[0].insertText!.location!.index).toBe(5);
193+
// cell(0,1) skipped
194+
// cell(1,0): base=10, cum=1 ("A") → 11
195+
expect(insertTexts[1].insertText!.location!.index).toBe(11);
196+
// cell(1,1): base=12, cum=2 ("A"+"C") → 14
197+
expect(insertTexts[2].insertText!.location!.index).toBe(14);
198+
});
199+
});
200+
201+
describe('header row bolding', () => {
202+
it('should add updateTextStyle requests for header cells when hasHeaderRow=true', () => {
203+
const requests = buildInsertTableWithDataRequests(
204+
[
205+
['Name', 'Age'],
206+
['Alice', '30'],
207+
],
208+
1,
209+
true
210+
);
211+
212+
const styleReqs = requests.filter((r) => r.updateTextStyle);
213+
expect(styleReqs).toHaveLength(2);
214+
215+
// "Name" bold: range starts at 5 (base for cell(0,0), cum=0), ends at 5+4=9
216+
expect(styleReqs[0].updateTextStyle!.textStyle!.bold).toBe(true);
217+
expect(styleReqs[0].updateTextStyle!.range!.startIndex).toBe(5);
218+
expect(styleReqs[0].updateTextStyle!.range!.endIndex).toBe(9);
219+
220+
// "Age" bold: base for cell(0,1)=7, cum=4 ("Name") → 11, ends at 11+3=14
221+
expect(styleReqs[1].updateTextStyle!.textStyle!.bold).toBe(true);
222+
expect(styleReqs[1].updateTextStyle!.range!.startIndex).toBe(11);
223+
expect(styleReqs[1].updateTextStyle!.range!.endIndex).toBe(14);
224+
});
225+
226+
it('should not add style requests when hasHeaderRow=false', () => {
227+
const requests = buildInsertTableWithDataRequests(
228+
[
229+
['Name', 'Age'],
230+
['Alice', '30'],
231+
],
232+
1,
233+
false
234+
);
235+
236+
const styleReqs = requests.filter((r) => r.updateTextStyle);
237+
expect(styleReqs).toHaveLength(0);
238+
});
239+
240+
it('should not bold empty header cells', () => {
241+
const requests = buildInsertTableWithDataRequests(
242+
[
243+
['Name', ''],
244+
['Alice', '30'],
245+
],
246+
1,
247+
true
248+
);
249+
250+
const styleReqs = requests.filter((r) => r.updateTextStyle);
251+
expect(styleReqs).toHaveLength(1);
252+
expect(styleReqs[0].updateTextStyle!.range!.startIndex).toBe(5);
253+
});
254+
255+
it('should not bold non-header rows', () => {
256+
const requests = buildInsertTableWithDataRequests(
257+
[
258+
['H1', 'H2'],
259+
['A', 'B'],
260+
['C', 'D'],
261+
],
262+
1,
263+
true
264+
);
265+
266+
const styleReqs = requests.filter((r) => r.updateTextStyle);
267+
// Only 2 bold requests (for H1 and H2), not for A, B, C, D
268+
expect(styleReqs).toHaveLength(2);
269+
});
270+
});
271+
272+
describe('tabId propagation', () => {
273+
it('should include tabId in all request locations when provided', () => {
274+
const tabId = 'tab123';
275+
const requests = buildInsertTableWithDataRequests(
276+
[
277+
['A', 'B'],
278+
['C', 'D'],
279+
],
280+
1,
281+
true,
282+
tabId
283+
);
284+
285+
const tableReq = requests.find((r) => r.insertTable);
286+
expect((tableReq!.insertTable!.location as any).tabId).toBe(tabId);
287+
288+
const insertTexts = requests.filter((r) => r.insertText);
289+
for (const req of insertTexts) {
290+
expect((req.insertText!.location as any).tabId).toBe(tabId);
291+
}
292+
293+
const styleReqs = requests.filter((r) => r.updateTextStyle);
294+
for (const req of styleReqs) {
295+
expect(req.updateTextStyle!.range!.tabId).toBe(tabId);
296+
}
297+
});
298+
299+
it('should not include tabId when not provided', () => {
300+
const requests = buildInsertTableWithDataRequests([['A']], 1, false);
301+
302+
const tableReq = requests.find((r) => r.insertTable);
303+
expect((tableReq!.insertTable!.location as any).tabId).toBeUndefined();
304+
305+
const insertTexts = requests.filter((r) => r.insertText);
306+
expect((insertTexts[0].insertText!.location as any).tabId).toBeUndefined();
307+
});
308+
});
309+
310+
describe('error handling', () => {
311+
it('should throw on empty data array', () => {
312+
expect(() => buildInsertTableWithDataRequests([], 1, false)).toThrow();
313+
});
314+
315+
it('should throw on data with only empty rows', () => {
316+
expect(() => buildInsertTableWithDataRequests([[]], 1, false)).toThrow();
317+
});
318+
});
319+
});

0 commit comments

Comments
 (0)