gdocs-mcp 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/write-table.js +65 -28
- package/package.json +1 -1
|
@@ -5,8 +5,11 @@ import { getPreset } from '../presets/config.js';
|
|
|
5
5
|
const DEFAULT_HEADER_BG = { red: 0.043, green: 0.325, blue: 0.58 };
|
|
6
6
|
const DEFAULT_HEADER_TEXT = { red: 0.95, green: 0.95, blue: 0.95 };
|
|
7
7
|
const DEFAULT_PADDING = 5;
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const DEFAULT_PAGE_WIDTH = 612;
|
|
9
|
+
const DEFAULT_MARGIN = 72;
|
|
10
|
+
const DEFAULT_FONT_FAMILY = 'Arial';
|
|
11
|
+
const DEFAULT_FONT_SIZE = 11;
|
|
12
|
+
const DEFAULT_LINE_SPACING = 115;
|
|
10
13
|
const PT_PER_CHAR = 6;
|
|
11
14
|
const PADDING_PER_COL = 20;
|
|
12
15
|
export const WriteTableSchema = z.object({
|
|
@@ -33,16 +36,31 @@ export async function writeTable(args) {
|
|
|
33
36
|
const docs = google.docs({ version: 'v1', auth });
|
|
34
37
|
const apiStart = performance.now();
|
|
35
38
|
const colCount = args.headers.length;
|
|
36
|
-
const rowCount = args.rows.length + 1;
|
|
39
|
+
const rowCount = args.rows.length + 1;
|
|
37
40
|
if (colCount > 20 || rowCount > 20) {
|
|
38
41
|
throw new Error(`Table exceeds Google Docs limit of 20x20. Requested: ${rowCount} rows x ${colCount} columns.`);
|
|
39
42
|
}
|
|
40
|
-
// Validate row lengths match header count
|
|
41
43
|
for (let i = 0; i < args.rows.length; i++) {
|
|
42
44
|
if (args.rows[i].length !== colCount) {
|
|
43
45
|
throw new Error(`Row ${i} has ${args.rows[i].length} cells but expected ${colCount} (matching headers).`);
|
|
44
46
|
}
|
|
45
47
|
}
|
|
48
|
+
// Step 0: Read document style for actual page dimensions + body font
|
|
49
|
+
const docMeta = await docs.documents.get({
|
|
50
|
+
documentId: args.documentId,
|
|
51
|
+
fields: 'documentStyle,namedStyles',
|
|
52
|
+
});
|
|
53
|
+
const docStyle = docMeta.data.documentStyle || {};
|
|
54
|
+
const pageWidth = docStyle.pageSize?.width?.magnitude ?? DEFAULT_PAGE_WIDTH;
|
|
55
|
+
const marginLeft = docStyle.marginLeft?.magnitude ?? DEFAULT_MARGIN;
|
|
56
|
+
const marginRight = docStyle.marginRight?.magnitude ?? DEFAULT_MARGIN;
|
|
57
|
+
const usableWidth = pageWidth - marginLeft - marginRight;
|
|
58
|
+
// Get body font from named styles
|
|
59
|
+
const namedStyles = docMeta.data.namedStyles?.styles || [];
|
|
60
|
+
const normalTextStyle = namedStyles.find((s) => s.namedStyleType === 'NORMAL_TEXT');
|
|
61
|
+
const bodyFontFamily = normalTextStyle?.textStyle?.weightedFontFamily?.fontFamily ?? DEFAULT_FONT_FAMILY;
|
|
62
|
+
const bodyFontSize = normalTextStyle?.textStyle?.fontSize?.magnitude ?? DEFAULT_FONT_SIZE;
|
|
63
|
+
const bodyLineSpacing = normalTextStyle?.paragraphStyle?.lineSpacing ?? DEFAULT_LINE_SPACING;
|
|
46
64
|
// Step 1: Insert empty table
|
|
47
65
|
await docs.documents.batchUpdate({
|
|
48
66
|
documentId: args.documentId,
|
|
@@ -68,7 +86,7 @@ export async function writeTable(args) {
|
|
|
68
86
|
'Document may have been modified concurrently.');
|
|
69
87
|
}
|
|
70
88
|
const tableStartIndex = table.startIndex;
|
|
71
|
-
// Build all cell data
|
|
89
|
+
// Build all cell data
|
|
72
90
|
const allData = [args.headers, ...args.rows];
|
|
73
91
|
const cellInserts = [];
|
|
74
92
|
for (let r = 0; r < table.table.tableRows.length; r++) {
|
|
@@ -78,23 +96,21 @@ export async function writeTable(args) {
|
|
|
78
96
|
cellInserts.push({ index: insertAt, text: allData[r][c] });
|
|
79
97
|
}
|
|
80
98
|
}
|
|
81
|
-
// Step 3:
|
|
99
|
+
// Step 3: Insert text in reverse order
|
|
82
100
|
const requests = [];
|
|
83
|
-
// Insert text in reverse order to avoid index shifts
|
|
84
101
|
const sortedInserts = [...cellInserts].sort((a, b) => b.index - a.index);
|
|
85
102
|
for (const { index, text } of sortedInserts) {
|
|
86
103
|
if (text) {
|
|
87
104
|
requests.push({ insertText: { location: { index }, text } });
|
|
88
105
|
}
|
|
89
106
|
}
|
|
90
|
-
// Execute text insertion first (indices shift after this)
|
|
91
107
|
if (requests.length > 0) {
|
|
92
108
|
await docs.documents.batchUpdate({
|
|
93
109
|
documentId: args.documentId,
|
|
94
110
|
requestBody: { requests },
|
|
95
111
|
});
|
|
96
112
|
}
|
|
97
|
-
// Step 4: Re-read
|
|
113
|
+
// Step 4: Re-read for styling
|
|
98
114
|
const doc2 = await docs.documents.get({ documentId: args.documentId });
|
|
99
115
|
let table2 = null;
|
|
100
116
|
for (const element of doc2.data.body?.content || []) {
|
|
@@ -103,7 +119,6 @@ export async function writeTable(args) {
|
|
|
103
119
|
break;
|
|
104
120
|
}
|
|
105
121
|
}
|
|
106
|
-
// Find nearest table if exact match failed (startIndex may have shifted)
|
|
107
122
|
if (!table2) {
|
|
108
123
|
for (const element of doc2.data.body?.content || []) {
|
|
109
124
|
if (element.table && element.startIndex != null && element.startIndex >= args.index) {
|
|
@@ -115,25 +130,42 @@ export async function writeTable(args) {
|
|
|
115
130
|
if (!table2) {
|
|
116
131
|
return { documentId: args.documentId, tableCreated: true, styled: false, reason: 'Could not re-locate table for styling' };
|
|
117
132
|
}
|
|
118
|
-
// Resolve style
|
|
119
133
|
const resolvedStyle = resolveStyle(args.style);
|
|
120
134
|
const styleRequests = [];
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
135
|
+
// FIX 1 + 3: Apply explicit font to ALL cells (header + body)
|
|
136
|
+
for (let r = 0; r < table2.table.tableRows.length; r++) {
|
|
137
|
+
for (const cell of table2.table.tableRows[r].tableCells) {
|
|
138
|
+
const start = cell.content[0].startIndex;
|
|
139
|
+
const end = cell.content[cell.content.length - 1].endIndex - 1;
|
|
140
|
+
if (end <= start)
|
|
141
|
+
continue;
|
|
142
|
+
const isHeader = r === 0;
|
|
127
143
|
styleRequests.push({
|
|
128
144
|
updateTextStyle: {
|
|
129
145
|
range: { startIndex: start, endIndex: end },
|
|
130
146
|
textStyle: {
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
weightedFontFamily: { fontFamily: bodyFontFamily },
|
|
148
|
+
fontSize: { magnitude: bodyFontSize, unit: 'PT' },
|
|
149
|
+
bold: isHeader,
|
|
150
|
+
foregroundColor: isHeader
|
|
151
|
+
? { color: { rgbColor: resolvedStyle.headerTextRgb } }
|
|
152
|
+
: undefined,
|
|
133
153
|
},
|
|
134
|
-
fields:
|
|
154
|
+
fields: isHeader
|
|
155
|
+
? 'weightedFontFamily,fontSize,bold,foregroundColor'
|
|
156
|
+
: 'weightedFontFamily,fontSize,bold',
|
|
135
157
|
},
|
|
136
158
|
});
|
|
159
|
+
// Apply line spacing to body cells
|
|
160
|
+
if (!isHeader) {
|
|
161
|
+
styleRequests.push({
|
|
162
|
+
updateParagraphStyle: {
|
|
163
|
+
range: { startIndex: start, endIndex: end },
|
|
164
|
+
paragraphStyle: { lineSpacing: bodyLineSpacing },
|
|
165
|
+
fields: 'lineSpacing',
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
137
169
|
}
|
|
138
170
|
}
|
|
139
171
|
// Header row background
|
|
@@ -176,7 +208,7 @@ export async function writeTable(args) {
|
|
|
176
208
|
fields: 'paddingTop,paddingBottom,paddingLeft,paddingRight',
|
|
177
209
|
},
|
|
178
210
|
});
|
|
179
|
-
// Content-aware column widths
|
|
211
|
+
// FIX 2 + 4: Content-aware column widths that sum to full usable page width
|
|
180
212
|
const maxLengths = new Array(colCount).fill(0);
|
|
181
213
|
for (const row of allData) {
|
|
182
214
|
for (let c = 0; c < colCount; c++) {
|
|
@@ -185,24 +217,31 @@ export async function writeTable(args) {
|
|
|
185
217
|
}
|
|
186
218
|
const totalLen = maxLengths.reduce((a, b) => a + b, 0);
|
|
187
219
|
if (totalLen > 0) {
|
|
220
|
+
// Calculate raw proportional widths
|
|
221
|
+
const rawWidths = maxLengths.map(l => {
|
|
222
|
+
const contentWidth = l * PT_PER_CHAR + PADDING_PER_COL;
|
|
223
|
+
return Math.max(contentWidth, 50); // minimum 50pt per column
|
|
224
|
+
});
|
|
225
|
+
// Scale all widths proportionally to sum to exactly usableWidth
|
|
226
|
+
const rawTotal = rawWidths.reduce((a, b) => a + b, 0);
|
|
227
|
+
const scaledWidths = rawWidths.map(w => Math.round((w / rawTotal) * usableWidth));
|
|
228
|
+
// Adjust rounding error on last column
|
|
229
|
+
const scaledTotal = scaledWidths.reduce((a, b) => a + b, 0);
|
|
230
|
+
scaledWidths[scaledWidths.length - 1] += usableWidth - scaledTotal;
|
|
188
231
|
for (let c = 0; c < colCount; c++) {
|
|
189
|
-
const contentWidth = maxLengths[c] * PT_PER_CHAR + PADDING_PER_COL;
|
|
190
|
-
const maxWidth = USABLE_PAGE_WIDTH * MAX_COL_RATIO;
|
|
191
|
-
const width = Math.min(Math.max(contentWidth, 50), maxWidth);
|
|
192
232
|
styleRequests.push({
|
|
193
233
|
updateTableColumnProperties: {
|
|
194
234
|
tableStartLocation: { index: table2.startIndex },
|
|
195
235
|
columnIndices: [c],
|
|
196
236
|
tableColumnProperties: {
|
|
197
237
|
widthType: 'FIXED_WIDTH',
|
|
198
|
-
width: { magnitude:
|
|
238
|
+
width: { magnitude: scaledWidths[c], unit: 'PT' },
|
|
199
239
|
},
|
|
200
240
|
fields: 'widthType,width',
|
|
201
241
|
},
|
|
202
242
|
});
|
|
203
243
|
}
|
|
204
244
|
}
|
|
205
|
-
// Apply all styling
|
|
206
245
|
if (styleRequests.length > 0) {
|
|
207
246
|
await docs.documents.batchUpdate({
|
|
208
247
|
documentId: args.documentId,
|
|
@@ -225,7 +264,6 @@ function resolveStyle(explicit) {
|
|
|
225
264
|
let headerBgRgb = DEFAULT_HEADER_BG;
|
|
226
265
|
let headerTextRgb = DEFAULT_HEADER_TEXT;
|
|
227
266
|
let cellPadding = DEFAULT_PADDING;
|
|
228
|
-
// Try active preset
|
|
229
267
|
try {
|
|
230
268
|
const { preset } = getPreset();
|
|
231
269
|
if (preset.table?.headerBackground)
|
|
@@ -238,7 +276,6 @@ function resolveStyle(explicit) {
|
|
|
238
276
|
catch {
|
|
239
277
|
// No active preset, use defaults
|
|
240
278
|
}
|
|
241
|
-
// Explicit overrides
|
|
242
279
|
if (explicit?.headerBackground)
|
|
243
280
|
headerBgRgb = hexToRgb(explicit.headerBackground);
|
|
244
281
|
if (explicit?.headerTextColor)
|
package/package.json
CHANGED