gdocs-mcp 0.1.0 → 0.2.0
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/README.md +1 -1
- package/dist/index.js +13 -2
- package/dist/presets/built-in.d.ts +2 -0
- package/dist/presets/built-in.js +270 -0
- package/dist/presets/config.d.ts +14 -0
- package/dist/presets/config.js +87 -0
- package/dist/presets/converter.d.ts +12 -0
- package/dist/presets/converter.js +289 -0
- package/dist/presets/types.d.ts +59 -0
- package/dist/presets/types.js +6 -0
- package/dist/tools/apply-style-preset.d.ts +18 -0
- package/dist/tools/apply-style-preset.js +208 -0
- package/dist/tools/delete-style-preset.d.ts +8 -0
- package/dist/tools/delete-style-preset.js +12 -0
- package/dist/tools/extract-document-styles.d.ts +12 -0
- package/dist/tools/extract-document-styles.js +149 -0
- package/dist/tools/list-style-presets.d.ts +18 -0
- package/dist/tools/list-style-presets.js +29 -0
- package/dist/tools/set-active-preset.d.ts +8 -0
- package/dist/tools/set-active-preset.js +12 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Open-source MCP server for Google Docs and Sheets. Give Claude (or any MCP-compatible AI) the ability to read, create, edit, and search your Google Docs — with OAuth tokens that never leave your machine.
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**Open-source. Self-hosted. Your tokens never leave your machine.**
|
|
6
6
|
|
|
7
7
|
## Quick Start
|
|
8
8
|
|
package/dist/index.js
CHANGED
|
@@ -12,9 +12,14 @@ import { UpdateDocumentStyleSchema, updateDocumentStyle } from './tools/update-d
|
|
|
12
12
|
import { UpdateDocumentSchema, updateDocument } from './tools/update-document.js';
|
|
13
13
|
import { UnmergeTableCellsSchema, unmergeTableCells } from './tools/unmerge-table-cells.js';
|
|
14
14
|
import { GetChartsSchema, getCharts } from './tools/get-charts.js';
|
|
15
|
+
import { ApplyStylePresetSchema, applyStylePreset } from './tools/apply-style-preset.js';
|
|
16
|
+
import { ExtractDocumentStylesSchema, extractDocumentStyles } from './tools/extract-document-styles.js';
|
|
17
|
+
import { ListStylePresetsSchema, listStylePresets } from './tools/list-style-presets.js';
|
|
18
|
+
import { SetActivePresetSchema, setActivePreset } from './tools/set-active-preset.js';
|
|
19
|
+
import { DeleteStylePresetSchema, deleteStylePreset } from './tools/delete-style-preset.js';
|
|
15
20
|
const server = new McpServer({
|
|
16
21
|
name: 'gdocs-mcp',
|
|
17
|
-
version: '0.
|
|
22
|
+
version: '0.2.0',
|
|
18
23
|
});
|
|
19
24
|
function formatError(err) {
|
|
20
25
|
if (err instanceof Error) {
|
|
@@ -64,10 +69,16 @@ registerTool('update_document', 'Apply batch updates to a document (insertText,
|
|
|
64
69
|
registerTool('unmerge_table_cells', 'Unmerge previously merged cells in a document table', UnmergeTableCellsSchema, unmergeTableCells);
|
|
65
70
|
// Google Sheets tools
|
|
66
71
|
registerTool('get_charts', 'List all charts in a Google Sheets spreadsheet with IDs and specs', GetChartsSchema, getCharts);
|
|
72
|
+
// Style preset tools
|
|
73
|
+
registerTool('apply_style_preset', 'Apply a style preset to a document. Updates named styles (TITLE, HEADING_1-6, NORMAL_TEXT, SUBTITLE), document margins, and table formatting in a single API call.', ApplyStylePresetSchema, applyStylePreset);
|
|
74
|
+
registerTool('extract_document_styles', 'Extract styles from a Google Doc and optionally save as a reusable preset. Returns font, size, color, spacing for all named styles, margins, and table formatting.', ExtractDocumentStylesSchema, extractDocumentStyles);
|
|
75
|
+
registerTool('list_style_presets', 'List all available style presets (built-in: clean, corporate, classic, minimal + user-defined) with typography summaries.', ListStylePresetsSchema, listStylePresets);
|
|
76
|
+
registerTool('set_active_preset', 'Set the active style preset. The active preset auto-applies when creating new documents.', SetActivePresetSchema, setActivePreset);
|
|
77
|
+
registerTool('delete_style_preset', 'Delete a user-defined style preset. Cannot delete built-in presets.', DeleteStylePresetSchema, deleteStylePreset);
|
|
67
78
|
async function main() {
|
|
68
79
|
const transport = new StdioServerTransport();
|
|
69
80
|
await server.connect(transport);
|
|
70
|
-
console.error('gdocs-mcp v0.
|
|
81
|
+
console.error('gdocs-mcp v0.2.0 started');
|
|
71
82
|
}
|
|
72
83
|
main().catch((err) => {
|
|
73
84
|
console.error('Failed to start gdocs-mcp:', err);
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
export const BUILT_IN_PRESETS = {
|
|
2
|
+
clean: {
|
|
3
|
+
document: {
|
|
4
|
+
marginTop: 72,
|
|
5
|
+
marginBottom: 72,
|
|
6
|
+
marginLeft: 72,
|
|
7
|
+
marginRight: 72,
|
|
8
|
+
},
|
|
9
|
+
styles: {
|
|
10
|
+
TITLE: {
|
|
11
|
+
fontFamily: 'Inter',
|
|
12
|
+
fontSize: 28,
|
|
13
|
+
bold: true,
|
|
14
|
+
color: '#1a1a2e',
|
|
15
|
+
spaceBelow: 4,
|
|
16
|
+
},
|
|
17
|
+
SUBTITLE: {
|
|
18
|
+
fontFamily: 'Inter',
|
|
19
|
+
fontSize: 13,
|
|
20
|
+
italic: true,
|
|
21
|
+
color: '#666666',
|
|
22
|
+
spaceBelow: 16,
|
|
23
|
+
},
|
|
24
|
+
HEADING_1: {
|
|
25
|
+
fontFamily: 'Inter',
|
|
26
|
+
fontSize: 22,
|
|
27
|
+
bold: true,
|
|
28
|
+
color: '#1a1a2e',
|
|
29
|
+
spaceAbove: 24,
|
|
30
|
+
spaceBelow: 8,
|
|
31
|
+
keepWithNext: true,
|
|
32
|
+
},
|
|
33
|
+
HEADING_2: {
|
|
34
|
+
fontFamily: 'Inter',
|
|
35
|
+
fontSize: 16,
|
|
36
|
+
bold: true,
|
|
37
|
+
color: '#333333',
|
|
38
|
+
spaceAbove: 18,
|
|
39
|
+
spaceBelow: 6,
|
|
40
|
+
keepWithNext: true,
|
|
41
|
+
},
|
|
42
|
+
HEADING_3: {
|
|
43
|
+
fontFamily: 'Inter',
|
|
44
|
+
fontSize: 13,
|
|
45
|
+
bold: true,
|
|
46
|
+
color: '#555555',
|
|
47
|
+
spaceAbove: 14,
|
|
48
|
+
spaceBelow: 4,
|
|
49
|
+
keepWithNext: true,
|
|
50
|
+
},
|
|
51
|
+
NORMAL_TEXT: {
|
|
52
|
+
fontFamily: 'Inter',
|
|
53
|
+
fontSize: 11,
|
|
54
|
+
color: '#1a1a1a',
|
|
55
|
+
lineSpacing: 150,
|
|
56
|
+
spaceBelow: 6,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
table: {
|
|
60
|
+
headerBackground: '#1a1a2e',
|
|
61
|
+
headerTextColor: '#ffffff',
|
|
62
|
+
headerBold: true,
|
|
63
|
+
borderColor: '#dddddd',
|
|
64
|
+
borderWidth: 0.5,
|
|
65
|
+
alternateRowBackground: '#f9f9f9',
|
|
66
|
+
cellPadding: 5,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
corporate: {
|
|
70
|
+
document: {
|
|
71
|
+
marginTop: 72,
|
|
72
|
+
marginBottom: 72,
|
|
73
|
+
marginLeft: 72,
|
|
74
|
+
marginRight: 72,
|
|
75
|
+
},
|
|
76
|
+
styles: {
|
|
77
|
+
TITLE: {
|
|
78
|
+
fontFamily: 'Arial',
|
|
79
|
+
fontSize: 24,
|
|
80
|
+
bold: true,
|
|
81
|
+
color: '#333333',
|
|
82
|
+
spaceBelow: 4,
|
|
83
|
+
},
|
|
84
|
+
SUBTITLE: {
|
|
85
|
+
fontFamily: 'Arial',
|
|
86
|
+
fontSize: 12,
|
|
87
|
+
italic: true,
|
|
88
|
+
color: '#666666',
|
|
89
|
+
spaceBelow: 14,
|
|
90
|
+
},
|
|
91
|
+
HEADING_1: {
|
|
92
|
+
fontFamily: 'Arial',
|
|
93
|
+
fontSize: 18,
|
|
94
|
+
bold: true,
|
|
95
|
+
color: '#333333',
|
|
96
|
+
spaceAbove: 20,
|
|
97
|
+
spaceBelow: 6,
|
|
98
|
+
keepWithNext: true,
|
|
99
|
+
},
|
|
100
|
+
HEADING_2: {
|
|
101
|
+
fontFamily: 'Arial',
|
|
102
|
+
fontSize: 14,
|
|
103
|
+
bold: true,
|
|
104
|
+
color: '#444444',
|
|
105
|
+
spaceAbove: 16,
|
|
106
|
+
spaceBelow: 4,
|
|
107
|
+
keepWithNext: true,
|
|
108
|
+
},
|
|
109
|
+
HEADING_3: {
|
|
110
|
+
fontFamily: 'Arial',
|
|
111
|
+
fontSize: 12,
|
|
112
|
+
bold: true,
|
|
113
|
+
color: '#555555',
|
|
114
|
+
spaceAbove: 12,
|
|
115
|
+
spaceBelow: 4,
|
|
116
|
+
keepWithNext: true,
|
|
117
|
+
},
|
|
118
|
+
NORMAL_TEXT: {
|
|
119
|
+
fontFamily: 'Arial',
|
|
120
|
+
fontSize: 11,
|
|
121
|
+
color: '#333333',
|
|
122
|
+
lineSpacing: 140,
|
|
123
|
+
spaceBelow: 4,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
table: {
|
|
127
|
+
headerBackground: '#4472C4',
|
|
128
|
+
headerTextColor: '#ffffff',
|
|
129
|
+
headerBold: true,
|
|
130
|
+
borderColor: '#cccccc',
|
|
131
|
+
borderWidth: 0.5,
|
|
132
|
+
alternateRowBackground: '#f2f2f2',
|
|
133
|
+
cellPadding: 5,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
classic: {
|
|
137
|
+
document: {
|
|
138
|
+
marginTop: 72,
|
|
139
|
+
marginBottom: 72,
|
|
140
|
+
marginLeft: 72,
|
|
141
|
+
marginRight: 72,
|
|
142
|
+
},
|
|
143
|
+
styles: {
|
|
144
|
+
TITLE: {
|
|
145
|
+
fontFamily: 'Georgia',
|
|
146
|
+
fontSize: 26,
|
|
147
|
+
bold: true,
|
|
148
|
+
color: '#1a3c6e',
|
|
149
|
+
spaceBelow: 4,
|
|
150
|
+
},
|
|
151
|
+
SUBTITLE: {
|
|
152
|
+
fontFamily: 'Garamond',
|
|
153
|
+
fontSize: 13,
|
|
154
|
+
italic: true,
|
|
155
|
+
color: '#555555',
|
|
156
|
+
spaceBelow: 16,
|
|
157
|
+
},
|
|
158
|
+
HEADING_1: {
|
|
159
|
+
fontFamily: 'Georgia',
|
|
160
|
+
fontSize: 20,
|
|
161
|
+
bold: true,
|
|
162
|
+
color: '#1a3c6e',
|
|
163
|
+
spaceAbove: 22,
|
|
164
|
+
spaceBelow: 8,
|
|
165
|
+
keepWithNext: true,
|
|
166
|
+
},
|
|
167
|
+
HEADING_2: {
|
|
168
|
+
fontFamily: 'Georgia',
|
|
169
|
+
fontSize: 15,
|
|
170
|
+
bold: true,
|
|
171
|
+
color: '#2a5a8e',
|
|
172
|
+
spaceAbove: 18,
|
|
173
|
+
spaceBelow: 6,
|
|
174
|
+
keepWithNext: true,
|
|
175
|
+
},
|
|
176
|
+
HEADING_3: {
|
|
177
|
+
fontFamily: 'Georgia',
|
|
178
|
+
fontSize: 12,
|
|
179
|
+
bold: true,
|
|
180
|
+
italic: true,
|
|
181
|
+
color: '#3a6a9e',
|
|
182
|
+
spaceAbove: 14,
|
|
183
|
+
spaceBelow: 4,
|
|
184
|
+
keepWithNext: true,
|
|
185
|
+
},
|
|
186
|
+
NORMAL_TEXT: {
|
|
187
|
+
fontFamily: 'Garamond',
|
|
188
|
+
fontSize: 11,
|
|
189
|
+
color: '#222222',
|
|
190
|
+
lineSpacing: 140,
|
|
191
|
+
spaceBelow: 6,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
table: {
|
|
195
|
+
headerBackground: '#1a3c6e',
|
|
196
|
+
headerTextColor: '#ffffff',
|
|
197
|
+
headerBold: true,
|
|
198
|
+
borderColor: '#999999',
|
|
199
|
+
borderWidth: 0.75,
|
|
200
|
+
alternateRowBackground: '#eef3f8',
|
|
201
|
+
cellPadding: 5,
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
minimal: {
|
|
205
|
+
document: {
|
|
206
|
+
marginTop: 72,
|
|
207
|
+
marginBottom: 72,
|
|
208
|
+
marginLeft: 72,
|
|
209
|
+
marginRight: 72,
|
|
210
|
+
},
|
|
211
|
+
styles: {
|
|
212
|
+
TITLE: {
|
|
213
|
+
fontFamily: 'Roboto',
|
|
214
|
+
fontSize: 24,
|
|
215
|
+
bold: false,
|
|
216
|
+
color: '#000000',
|
|
217
|
+
spaceBelow: 4,
|
|
218
|
+
},
|
|
219
|
+
SUBTITLE: {
|
|
220
|
+
fontFamily: 'Roboto',
|
|
221
|
+
fontSize: 12,
|
|
222
|
+
color: '#888888',
|
|
223
|
+
spaceBelow: 16,
|
|
224
|
+
},
|
|
225
|
+
HEADING_1: {
|
|
226
|
+
fontFamily: 'Roboto',
|
|
227
|
+
fontSize: 18,
|
|
228
|
+
bold: false,
|
|
229
|
+
color: '#000000',
|
|
230
|
+
spaceAbove: 24,
|
|
231
|
+
spaceBelow: 8,
|
|
232
|
+
keepWithNext: true,
|
|
233
|
+
},
|
|
234
|
+
HEADING_2: {
|
|
235
|
+
fontFamily: 'Roboto',
|
|
236
|
+
fontSize: 14,
|
|
237
|
+
bold: true,
|
|
238
|
+
color: '#333333',
|
|
239
|
+
spaceAbove: 18,
|
|
240
|
+
spaceBelow: 6,
|
|
241
|
+
keepWithNext: true,
|
|
242
|
+
},
|
|
243
|
+
HEADING_3: {
|
|
244
|
+
fontFamily: 'Roboto',
|
|
245
|
+
fontSize: 11,
|
|
246
|
+
bold: true,
|
|
247
|
+
color: '#555555',
|
|
248
|
+
spaceAbove: 14,
|
|
249
|
+
spaceBelow: 4,
|
|
250
|
+
keepWithNext: true,
|
|
251
|
+
},
|
|
252
|
+
NORMAL_TEXT: {
|
|
253
|
+
fontFamily: 'Roboto',
|
|
254
|
+
fontSize: 10.5,
|
|
255
|
+
color: '#222222',
|
|
256
|
+
lineSpacing: 150,
|
|
257
|
+
spaceBelow: 6,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
table: {
|
|
261
|
+
headerBackground: '#f5f5f5',
|
|
262
|
+
headerTextColor: '#000000',
|
|
263
|
+
headerBold: true,
|
|
264
|
+
borderColor: '#e0e0e0',
|
|
265
|
+
borderWidth: 0.5,
|
|
266
|
+
alternateRowBackground: '#fafafa',
|
|
267
|
+
cellPadding: 5,
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StylePreset } from './types.js';
|
|
2
|
+
export declare function getPreset(presetName?: string): {
|
|
3
|
+
name: string;
|
|
4
|
+
preset: StylePreset;
|
|
5
|
+
};
|
|
6
|
+
export declare function listAllPresets(): Record<string, {
|
|
7
|
+
source: 'built-in' | 'user';
|
|
8
|
+
preset: StylePreset;
|
|
9
|
+
}>;
|
|
10
|
+
export declare function listAllPresetNames(): string[];
|
|
11
|
+
export declare function savePreset(name: string, preset: StylePreset): void;
|
|
12
|
+
export declare function deletePreset(name: string): void;
|
|
13
|
+
export declare function setActivePreset(name: string): void;
|
|
14
|
+
export declare function getActivePresetName(): string | undefined;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { BUILT_IN_PRESETS } from './built-in.js';
|
|
4
|
+
const CONFIG_DIR = path.join(process.env.HOME || '~', '.gdocs-mcp');
|
|
5
|
+
const STYLES_PATH = path.join(CONFIG_DIR, 'styles.json');
|
|
6
|
+
const BUILT_IN_NAMES = new Set(Object.keys(BUILT_IN_PRESETS));
|
|
7
|
+
function readConfig() {
|
|
8
|
+
if (!fs.existsSync(STYLES_PATH)) {
|
|
9
|
+
return { presets: {} };
|
|
10
|
+
}
|
|
11
|
+
return JSON.parse(fs.readFileSync(STYLES_PATH, 'utf8'));
|
|
12
|
+
}
|
|
13
|
+
function writeConfig(config) {
|
|
14
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
15
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
16
|
+
}
|
|
17
|
+
fs.writeFileSync(STYLES_PATH, JSON.stringify(config, null, 2));
|
|
18
|
+
fs.chmodSync(STYLES_PATH, 0o600);
|
|
19
|
+
}
|
|
20
|
+
export function getPreset(presetName) {
|
|
21
|
+
const config = readConfig();
|
|
22
|
+
const name = presetName || config.activePreset;
|
|
23
|
+
if (!name) {
|
|
24
|
+
throw new Error('No preset specified and no activePreset set. Run set_active_preset or pass a presetName.');
|
|
25
|
+
}
|
|
26
|
+
// User presets override built-ins
|
|
27
|
+
const preset = config.presets[name] || BUILT_IN_PRESETS[name];
|
|
28
|
+
if (!preset) {
|
|
29
|
+
const available = listAllPresetNames();
|
|
30
|
+
throw new Error(`Preset "${name}" not found. Available: ${available.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
return { name, preset };
|
|
33
|
+
}
|
|
34
|
+
export function listAllPresets() {
|
|
35
|
+
const config = readConfig();
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const [name, preset] of Object.entries(BUILT_IN_PRESETS)) {
|
|
38
|
+
result[name] = { source: 'built-in', preset };
|
|
39
|
+
}
|
|
40
|
+
// User presets override built-ins
|
|
41
|
+
for (const [name, preset] of Object.entries(config.presets)) {
|
|
42
|
+
result[name] = { source: BUILT_IN_NAMES.has(name) ? 'built-in' : 'user', preset };
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
export function listAllPresetNames() {
|
|
47
|
+
return Object.keys(listAllPresets());
|
|
48
|
+
}
|
|
49
|
+
export function savePreset(name, preset) {
|
|
50
|
+
const config = readConfig();
|
|
51
|
+
config.presets = { ...config.presets, [name]: preset };
|
|
52
|
+
writeConfig(config);
|
|
53
|
+
}
|
|
54
|
+
export function deletePreset(name) {
|
|
55
|
+
if (BUILT_IN_NAMES.has(name)) {
|
|
56
|
+
const config = readConfig();
|
|
57
|
+
if (!config.presets[name]) {
|
|
58
|
+
throw new Error(`Cannot delete built-in preset "${name}". You can override it by creating a preset with the same name.`);
|
|
59
|
+
}
|
|
60
|
+
// User override of built-in — remove the override, built-in remains
|
|
61
|
+
const { [name]: _, ...rest } = config.presets;
|
|
62
|
+
writeConfig({ ...config, presets: rest });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const config = readConfig();
|
|
66
|
+
if (!config.presets[name]) {
|
|
67
|
+
throw new Error(`Preset "${name}" not found.`);
|
|
68
|
+
}
|
|
69
|
+
const { [name]: _, ...rest } = config.presets;
|
|
70
|
+
const updated = { ...config, presets: rest };
|
|
71
|
+
// Clear activePreset if it was the deleted one
|
|
72
|
+
if (config.activePreset === name) {
|
|
73
|
+
delete updated.activePreset;
|
|
74
|
+
}
|
|
75
|
+
writeConfig(updated);
|
|
76
|
+
}
|
|
77
|
+
export function setActivePreset(name) {
|
|
78
|
+
const allNames = listAllPresetNames();
|
|
79
|
+
if (!allNames.includes(name)) {
|
|
80
|
+
throw new Error(`Preset "${name}" not found. Available: ${allNames.join(', ')}`);
|
|
81
|
+
}
|
|
82
|
+
const config = readConfig();
|
|
83
|
+
writeConfig({ ...config, activePreset: name });
|
|
84
|
+
}
|
|
85
|
+
export function getActivePresetName() {
|
|
86
|
+
return readConfig().activePreset;
|
|
87
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NamedStyleConfig, DocumentStyleConfig, TableStyleConfig } from './types.js';
|
|
2
|
+
export declare function buildNamedStyleRequest(styles: Record<string, NamedStyleConfig>): {
|
|
3
|
+
updateNamedStyles: any;
|
|
4
|
+
};
|
|
5
|
+
export declare function buildDocumentStyleRequest(config: DocumentStyleConfig): {
|
|
6
|
+
updateDocumentStyle: any;
|
|
7
|
+
};
|
|
8
|
+
export declare function buildTableStyleRequests(tableConfig: TableStyleConfig, tables: Array<{
|
|
9
|
+
startIndex: number;
|
|
10
|
+
rows: number;
|
|
11
|
+
cols: number;
|
|
12
|
+
}>): any[];
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
function hexToRgb(hex) {
|
|
2
|
+
const h = hex.replace('#', '');
|
|
3
|
+
return {
|
|
4
|
+
red: parseInt(h.substring(0, 2), 16) / 255,
|
|
5
|
+
green: parseInt(h.substring(2, 4), 16) / 255,
|
|
6
|
+
blue: parseInt(h.substring(4, 6), 16) / 255,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
function toColor(hex) {
|
|
10
|
+
return { color: { rgbColor: hexToRgb(hex) } };
|
|
11
|
+
}
|
|
12
|
+
function toDimension(magnitude, unit = 'PT') {
|
|
13
|
+
return { magnitude, unit };
|
|
14
|
+
}
|
|
15
|
+
function toBorder(config) {
|
|
16
|
+
return {
|
|
17
|
+
color: toColor(config.color),
|
|
18
|
+
width: toDimension(config.width),
|
|
19
|
+
padding: toDimension(config.padding),
|
|
20
|
+
dashStyle: 'SOLID',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function buildNamedStyleRequest(styles) {
|
|
24
|
+
const namedStyles = [];
|
|
25
|
+
const fieldParts = new Set();
|
|
26
|
+
for (const [styleType, config] of Object.entries(styles)) {
|
|
27
|
+
const textStyle = {};
|
|
28
|
+
const paragraphStyle = {};
|
|
29
|
+
// Text style properties
|
|
30
|
+
if (config.fontFamily !== undefined) {
|
|
31
|
+
textStyle.weightedFontFamily = { fontFamily: config.fontFamily };
|
|
32
|
+
fieldParts.add('textStyle.weightedFontFamily');
|
|
33
|
+
}
|
|
34
|
+
if (config.fontSize !== undefined) {
|
|
35
|
+
textStyle.fontSize = toDimension(config.fontSize);
|
|
36
|
+
fieldParts.add('textStyle.fontSize');
|
|
37
|
+
}
|
|
38
|
+
if (config.bold !== undefined) {
|
|
39
|
+
textStyle.bold = config.bold;
|
|
40
|
+
fieldParts.add('textStyle.bold');
|
|
41
|
+
}
|
|
42
|
+
if (config.italic !== undefined) {
|
|
43
|
+
textStyle.italic = config.italic;
|
|
44
|
+
fieldParts.add('textStyle.italic');
|
|
45
|
+
}
|
|
46
|
+
if (config.underline !== undefined) {
|
|
47
|
+
textStyle.underline = config.underline;
|
|
48
|
+
fieldParts.add('textStyle.underline');
|
|
49
|
+
}
|
|
50
|
+
if (config.strikethrough !== undefined) {
|
|
51
|
+
textStyle.strikethrough = config.strikethrough;
|
|
52
|
+
fieldParts.add('textStyle.strikethrough');
|
|
53
|
+
}
|
|
54
|
+
if (config.smallCaps !== undefined) {
|
|
55
|
+
textStyle.smallCaps = config.smallCaps;
|
|
56
|
+
fieldParts.add('textStyle.smallCaps');
|
|
57
|
+
}
|
|
58
|
+
if (config.color !== undefined) {
|
|
59
|
+
textStyle.foregroundColor = toColor(config.color);
|
|
60
|
+
fieldParts.add('textStyle.foregroundColor');
|
|
61
|
+
}
|
|
62
|
+
if (config.backgroundColor !== undefined) {
|
|
63
|
+
textStyle.backgroundColor = toColor(config.backgroundColor);
|
|
64
|
+
fieldParts.add('textStyle.backgroundColor');
|
|
65
|
+
}
|
|
66
|
+
if (config.baselineOffset !== undefined) {
|
|
67
|
+
textStyle.baselineOffset = config.baselineOffset;
|
|
68
|
+
fieldParts.add('textStyle.baselineOffset');
|
|
69
|
+
}
|
|
70
|
+
// Paragraph style properties
|
|
71
|
+
if (config.alignment !== undefined) {
|
|
72
|
+
paragraphStyle.alignment = config.alignment;
|
|
73
|
+
fieldParts.add('paragraphStyle.alignment');
|
|
74
|
+
}
|
|
75
|
+
if (config.lineSpacing !== undefined) {
|
|
76
|
+
paragraphStyle.lineSpacing = config.lineSpacing;
|
|
77
|
+
fieldParts.add('paragraphStyle.lineSpacing');
|
|
78
|
+
}
|
|
79
|
+
if (config.spaceAbove !== undefined) {
|
|
80
|
+
paragraphStyle.spaceAbove = toDimension(config.spaceAbove);
|
|
81
|
+
fieldParts.add('paragraphStyle.spaceAbove');
|
|
82
|
+
}
|
|
83
|
+
if (config.spaceBelow !== undefined) {
|
|
84
|
+
paragraphStyle.spaceBelow = toDimension(config.spaceBelow);
|
|
85
|
+
fieldParts.add('paragraphStyle.spaceBelow');
|
|
86
|
+
}
|
|
87
|
+
if (config.indentFirstLine !== undefined) {
|
|
88
|
+
paragraphStyle.indentFirstLine = toDimension(config.indentFirstLine);
|
|
89
|
+
fieldParts.add('paragraphStyle.indentFirstLine');
|
|
90
|
+
}
|
|
91
|
+
if (config.indentStart !== undefined) {
|
|
92
|
+
paragraphStyle.indentStart = toDimension(config.indentStart);
|
|
93
|
+
fieldParts.add('paragraphStyle.indentStart');
|
|
94
|
+
}
|
|
95
|
+
if (config.indentEnd !== undefined) {
|
|
96
|
+
paragraphStyle.indentEnd = toDimension(config.indentEnd);
|
|
97
|
+
fieldParts.add('paragraphStyle.indentEnd');
|
|
98
|
+
}
|
|
99
|
+
if (config.keepLinesTogether !== undefined) {
|
|
100
|
+
paragraphStyle.keepLinesTogether = config.keepLinesTogether;
|
|
101
|
+
fieldParts.add('paragraphStyle.keepLinesTogether');
|
|
102
|
+
}
|
|
103
|
+
if (config.keepWithNext !== undefined) {
|
|
104
|
+
paragraphStyle.keepWithNext = config.keepWithNext;
|
|
105
|
+
fieldParts.add('paragraphStyle.keepWithNext');
|
|
106
|
+
}
|
|
107
|
+
if (config.direction !== undefined) {
|
|
108
|
+
paragraphStyle.direction = config.direction;
|
|
109
|
+
fieldParts.add('paragraphStyle.direction');
|
|
110
|
+
}
|
|
111
|
+
if (config.borderTop !== undefined) {
|
|
112
|
+
paragraphStyle.borderTop = toBorder(config.borderTop);
|
|
113
|
+
fieldParts.add('paragraphStyle.borderTop');
|
|
114
|
+
}
|
|
115
|
+
if (config.borderBottom !== undefined) {
|
|
116
|
+
paragraphStyle.borderBottom = toBorder(config.borderBottom);
|
|
117
|
+
fieldParts.add('paragraphStyle.borderBottom');
|
|
118
|
+
}
|
|
119
|
+
if (config.borderLeft !== undefined) {
|
|
120
|
+
paragraphStyle.borderLeft = toBorder(config.borderLeft);
|
|
121
|
+
fieldParts.add('paragraphStyle.borderLeft');
|
|
122
|
+
}
|
|
123
|
+
if (config.borderRight !== undefined) {
|
|
124
|
+
paragraphStyle.borderRight = toBorder(config.borderRight);
|
|
125
|
+
fieldParts.add('paragraphStyle.borderRight');
|
|
126
|
+
}
|
|
127
|
+
namedStyles.push({
|
|
128
|
+
namedStyleType: styleType,
|
|
129
|
+
textStyle: Object.keys(textStyle).length > 0 ? textStyle : undefined,
|
|
130
|
+
paragraphStyle: Object.keys(paragraphStyle).length > 0 ? paragraphStyle : undefined,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
updateNamedStyles: {
|
|
135
|
+
namedStyles: { styles: namedStyles },
|
|
136
|
+
fields: Array.from(fieldParts).join(','),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function buildDocumentStyleRequest(config) {
|
|
141
|
+
const documentStyle = {};
|
|
142
|
+
const fields = [];
|
|
143
|
+
if (config.marginTop !== undefined) {
|
|
144
|
+
documentStyle.marginTop = toDimension(config.marginTop);
|
|
145
|
+
fields.push('marginTop');
|
|
146
|
+
}
|
|
147
|
+
if (config.marginBottom !== undefined) {
|
|
148
|
+
documentStyle.marginBottom = toDimension(config.marginBottom);
|
|
149
|
+
fields.push('marginBottom');
|
|
150
|
+
}
|
|
151
|
+
if (config.marginLeft !== undefined) {
|
|
152
|
+
documentStyle.marginLeft = toDimension(config.marginLeft);
|
|
153
|
+
fields.push('marginLeft');
|
|
154
|
+
}
|
|
155
|
+
if (config.marginRight !== undefined) {
|
|
156
|
+
documentStyle.marginRight = toDimension(config.marginRight);
|
|
157
|
+
fields.push('marginRight');
|
|
158
|
+
}
|
|
159
|
+
if (config.pageWidth !== undefined || config.pageHeight !== undefined) {
|
|
160
|
+
documentStyle.pageSize = {};
|
|
161
|
+
if (config.pageWidth !== undefined) {
|
|
162
|
+
documentStyle.pageSize.width = toDimension(config.pageWidth);
|
|
163
|
+
fields.push('pageSize.width');
|
|
164
|
+
}
|
|
165
|
+
if (config.pageHeight !== undefined) {
|
|
166
|
+
documentStyle.pageSize.height = toDimension(config.pageHeight);
|
|
167
|
+
fields.push('pageSize.height');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
updateDocumentStyle: {
|
|
172
|
+
documentStyle,
|
|
173
|
+
fields: fields.join(','),
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export function buildTableStyleRequests(tableConfig, tables) {
|
|
178
|
+
const requests = [];
|
|
179
|
+
for (const table of tables) {
|
|
180
|
+
// Header row (row 0)
|
|
181
|
+
if (tableConfig.headerBackground || tableConfig.headerTextColor || tableConfig.headerBold) {
|
|
182
|
+
const cellStyle = {};
|
|
183
|
+
const cellFields = [];
|
|
184
|
+
if (tableConfig.headerBackground) {
|
|
185
|
+
cellStyle.backgroundColor = toColor(tableConfig.headerBackground);
|
|
186
|
+
cellFields.push('backgroundColor');
|
|
187
|
+
}
|
|
188
|
+
requests.push({
|
|
189
|
+
updateTableCellStyle: {
|
|
190
|
+
tableRange: {
|
|
191
|
+
tableCellLocation: {
|
|
192
|
+
tableStartLocation: { index: table.startIndex },
|
|
193
|
+
rowIndex: 0,
|
|
194
|
+
columnIndex: 0,
|
|
195
|
+
},
|
|
196
|
+
rowSpan: 1,
|
|
197
|
+
columnSpan: table.cols,
|
|
198
|
+
},
|
|
199
|
+
tableCellStyle: cellStyle,
|
|
200
|
+
fields: cellFields.join(','),
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
// Bold + color for header text requires updateTextStyle on the header row range
|
|
204
|
+
// This is handled separately since tableCellStyle doesn't control text formatting
|
|
205
|
+
}
|
|
206
|
+
// Cell borders for all cells
|
|
207
|
+
if (tableConfig.borderColor || tableConfig.borderWidth) {
|
|
208
|
+
const border = {};
|
|
209
|
+
if (tableConfig.borderColor) {
|
|
210
|
+
border.color = toColor(tableConfig.borderColor);
|
|
211
|
+
}
|
|
212
|
+
if (tableConfig.borderWidth !== undefined) {
|
|
213
|
+
border.width = toDimension(tableConfig.borderWidth);
|
|
214
|
+
}
|
|
215
|
+
border.dashStyle = 'SOLID';
|
|
216
|
+
const cellStyle = {
|
|
217
|
+
borderTop: border,
|
|
218
|
+
borderBottom: border,
|
|
219
|
+
borderLeft: border,
|
|
220
|
+
borderRight: border,
|
|
221
|
+
};
|
|
222
|
+
requests.push({
|
|
223
|
+
updateTableCellStyle: {
|
|
224
|
+
tableRange: {
|
|
225
|
+
tableCellLocation: {
|
|
226
|
+
tableStartLocation: { index: table.startIndex },
|
|
227
|
+
rowIndex: 0,
|
|
228
|
+
columnIndex: 0,
|
|
229
|
+
},
|
|
230
|
+
rowSpan: table.rows,
|
|
231
|
+
columnSpan: table.cols,
|
|
232
|
+
},
|
|
233
|
+
tableCellStyle: cellStyle,
|
|
234
|
+
fields: 'borderTop,borderBottom,borderLeft,borderRight',
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// Cell padding for all cells
|
|
239
|
+
if (tableConfig.cellPadding !== undefined) {
|
|
240
|
+
const padding = toDimension(tableConfig.cellPadding);
|
|
241
|
+
requests.push({
|
|
242
|
+
updateTableCellStyle: {
|
|
243
|
+
tableRange: {
|
|
244
|
+
tableCellLocation: {
|
|
245
|
+
tableStartLocation: { index: table.startIndex },
|
|
246
|
+
rowIndex: 0,
|
|
247
|
+
columnIndex: 0,
|
|
248
|
+
},
|
|
249
|
+
rowSpan: table.rows,
|
|
250
|
+
columnSpan: table.cols,
|
|
251
|
+
},
|
|
252
|
+
tableCellStyle: {
|
|
253
|
+
paddingTop: padding,
|
|
254
|
+
paddingBottom: padding,
|
|
255
|
+
paddingLeft: padding,
|
|
256
|
+
paddingRight: padding,
|
|
257
|
+
},
|
|
258
|
+
fields: 'paddingTop,paddingBottom,paddingLeft,paddingRight',
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
// Alternating row backgrounds (rows 1+)
|
|
263
|
+
if (tableConfig.alternateRowBackground && table.rows > 1) {
|
|
264
|
+
for (let row = 1; row < table.rows; row++) {
|
|
265
|
+
const bg = row % 2 === 0
|
|
266
|
+
? tableConfig.alternateRowBackground
|
|
267
|
+
: '#ffffff';
|
|
268
|
+
requests.push({
|
|
269
|
+
updateTableCellStyle: {
|
|
270
|
+
tableRange: {
|
|
271
|
+
tableCellLocation: {
|
|
272
|
+
tableStartLocation: { index: table.startIndex },
|
|
273
|
+
rowIndex: row,
|
|
274
|
+
columnIndex: 0,
|
|
275
|
+
},
|
|
276
|
+
rowSpan: 1,
|
|
277
|
+
columnSpan: table.cols,
|
|
278
|
+
},
|
|
279
|
+
tableCellStyle: {
|
|
280
|
+
backgroundColor: toColor(bg),
|
|
281
|
+
},
|
|
282
|
+
fields: 'backgroundColor',
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return requests;
|
|
289
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface ParagraphBorderConfig {
|
|
2
|
+
color: string;
|
|
3
|
+
width: number;
|
|
4
|
+
padding: number;
|
|
5
|
+
}
|
|
6
|
+
export interface NamedStyleConfig {
|
|
7
|
+
fontFamily?: string;
|
|
8
|
+
fontSize?: number;
|
|
9
|
+
bold?: boolean;
|
|
10
|
+
italic?: boolean;
|
|
11
|
+
underline?: boolean;
|
|
12
|
+
strikethrough?: boolean;
|
|
13
|
+
smallCaps?: boolean;
|
|
14
|
+
color?: string;
|
|
15
|
+
backgroundColor?: string;
|
|
16
|
+
baselineOffset?: 'NONE' | 'SUPERSCRIPT' | 'SUBSCRIPT';
|
|
17
|
+
alignment?: 'START' | 'CENTER' | 'END' | 'JUSTIFIED';
|
|
18
|
+
lineSpacing?: number;
|
|
19
|
+
spaceAbove?: number;
|
|
20
|
+
spaceBelow?: number;
|
|
21
|
+
indentFirstLine?: number;
|
|
22
|
+
indentStart?: number;
|
|
23
|
+
indentEnd?: number;
|
|
24
|
+
keepLinesTogether?: boolean;
|
|
25
|
+
keepWithNext?: boolean;
|
|
26
|
+
direction?: 'LEFT_TO_RIGHT' | 'RIGHT_TO_LEFT';
|
|
27
|
+
borderTop?: ParagraphBorderConfig;
|
|
28
|
+
borderBottom?: ParagraphBorderConfig;
|
|
29
|
+
borderLeft?: ParagraphBorderConfig;
|
|
30
|
+
borderRight?: ParagraphBorderConfig;
|
|
31
|
+
}
|
|
32
|
+
export interface DocumentStyleConfig {
|
|
33
|
+
marginTop?: number;
|
|
34
|
+
marginBottom?: number;
|
|
35
|
+
marginLeft?: number;
|
|
36
|
+
marginRight?: number;
|
|
37
|
+
pageWidth?: number;
|
|
38
|
+
pageHeight?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface TableStyleConfig {
|
|
41
|
+
headerBackground?: string;
|
|
42
|
+
headerTextColor?: string;
|
|
43
|
+
headerBold?: boolean;
|
|
44
|
+
borderColor?: string;
|
|
45
|
+
borderWidth?: number;
|
|
46
|
+
cellPadding?: number;
|
|
47
|
+
alternateRowBackground?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface StylePreset {
|
|
50
|
+
document?: DocumentStyleConfig;
|
|
51
|
+
styles?: Record<string, NamedStyleConfig>;
|
|
52
|
+
table?: TableStyleConfig;
|
|
53
|
+
}
|
|
54
|
+
export interface StylesConfig {
|
|
55
|
+
activePreset?: string;
|
|
56
|
+
presets: Record<string, StylePreset>;
|
|
57
|
+
}
|
|
58
|
+
export type NamedStyleType = 'TITLE' | 'SUBTITLE' | 'HEADING_1' | 'HEADING_2' | 'HEADING_3' | 'HEADING_4' | 'HEADING_5' | 'HEADING_6' | 'NORMAL_TEXT';
|
|
59
|
+
export declare const VALID_NAMED_STYLE_TYPES: NamedStyleType[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const ApplyStylePresetSchema: z.ZodObject<{
|
|
3
|
+
documentId: z.ZodString;
|
|
4
|
+
presetName: z.ZodOptional<z.ZodString>;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export declare function applyStylePreset(args: z.infer<typeof ApplyStylePresetSchema>): Promise<{
|
|
7
|
+
documentId: string;
|
|
8
|
+
presetName: string;
|
|
9
|
+
applied: boolean;
|
|
10
|
+
reason: string;
|
|
11
|
+
requestCount?: undefined;
|
|
12
|
+
} | {
|
|
13
|
+
documentId: string;
|
|
14
|
+
presetName: string;
|
|
15
|
+
applied: boolean;
|
|
16
|
+
requestCount: number;
|
|
17
|
+
reason?: undefined;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
import { getPreset } from '../presets/config.js';
|
|
5
|
+
import { buildDocumentStyleRequest, buildTableStyleRequests } from '../presets/converter.js';
|
|
6
|
+
export const ApplyStylePresetSchema = z.object({
|
|
7
|
+
documentId: z.string().describe('The Google Doc document ID'),
|
|
8
|
+
presetName: z.string().optional().describe('Name of the preset to apply. If omitted, uses the active preset from styles.json.'),
|
|
9
|
+
});
|
|
10
|
+
export async function applyStylePreset(args) {
|
|
11
|
+
const auth = getAuthClient();
|
|
12
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
13
|
+
const { name, preset } = getPreset(args.presetName);
|
|
14
|
+
// Read document to get paragraph structure and tables
|
|
15
|
+
const doc = await docs.documents.get({ documentId: args.documentId });
|
|
16
|
+
const content = doc.data.body?.content || [];
|
|
17
|
+
const requests = [];
|
|
18
|
+
// 1. Apply named styles by finding paragraphs of each type and updating them
|
|
19
|
+
if (preset.styles && Object.keys(preset.styles).length > 0) {
|
|
20
|
+
const paragraphsByStyle = groupParagraphsByStyle(content);
|
|
21
|
+
for (const [styleType, config] of Object.entries(preset.styles)) {
|
|
22
|
+
const paragraphs = paragraphsByStyle[styleType] || [];
|
|
23
|
+
if (paragraphs.length === 0)
|
|
24
|
+
continue;
|
|
25
|
+
for (const para of paragraphs) {
|
|
26
|
+
const textStyleReq = buildTextStyleRequest(config, para.startIndex, para.endIndex);
|
|
27
|
+
if (textStyleReq)
|
|
28
|
+
requests.push(textStyleReq);
|
|
29
|
+
const paraStyleReq = buildParagraphStyleRequest(config, para.startIndex, para.endIndex);
|
|
30
|
+
if (paraStyleReq)
|
|
31
|
+
requests.push(paraStyleReq);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// 2. Document style (margins, page size)
|
|
36
|
+
if (preset.document && Object.keys(preset.document).length > 0) {
|
|
37
|
+
requests.push(buildDocumentStyleRequest(preset.document));
|
|
38
|
+
}
|
|
39
|
+
// 3. Table styles
|
|
40
|
+
if (preset.table && Object.keys(preset.table).length > 0) {
|
|
41
|
+
const tables = extractTables(content);
|
|
42
|
+
if (tables.length > 0) {
|
|
43
|
+
const tableRequests = buildTableStyleRequests(preset.table, tables);
|
|
44
|
+
requests.push(...tableRequests);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (requests.length === 0) {
|
|
48
|
+
return { documentId: args.documentId, presetName: name, applied: false, reason: 'Preset has no style definitions or document has no matching content' };
|
|
49
|
+
}
|
|
50
|
+
await docs.documents.batchUpdate({
|
|
51
|
+
documentId: args.documentId,
|
|
52
|
+
requestBody: { requests },
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
documentId: args.documentId,
|
|
56
|
+
presetName: name,
|
|
57
|
+
applied: true,
|
|
58
|
+
requestCount: requests.length,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function hexToRgb(hex) {
|
|
62
|
+
const h = hex.replace('#', '');
|
|
63
|
+
return {
|
|
64
|
+
red: parseInt(h.substring(0, 2), 16) / 255,
|
|
65
|
+
green: parseInt(h.substring(2, 4), 16) / 255,
|
|
66
|
+
blue: parseInt(h.substring(4, 6), 16) / 255,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function toColor(hex) {
|
|
70
|
+
return { color: { rgbColor: hexToRgb(hex) } };
|
|
71
|
+
}
|
|
72
|
+
function toDimension(magnitude) {
|
|
73
|
+
return { magnitude, unit: 'PT' };
|
|
74
|
+
}
|
|
75
|
+
function buildTextStyleRequest(config, startIndex, endIndex) {
|
|
76
|
+
const textStyle = {};
|
|
77
|
+
const fields = [];
|
|
78
|
+
if (config.fontFamily !== undefined) {
|
|
79
|
+
textStyle.weightedFontFamily = { fontFamily: config.fontFamily };
|
|
80
|
+
fields.push('weightedFontFamily');
|
|
81
|
+
}
|
|
82
|
+
if (config.fontSize !== undefined) {
|
|
83
|
+
textStyle.fontSize = toDimension(config.fontSize);
|
|
84
|
+
fields.push('fontSize');
|
|
85
|
+
}
|
|
86
|
+
if (config.bold !== undefined) {
|
|
87
|
+
textStyle.bold = config.bold;
|
|
88
|
+
fields.push('bold');
|
|
89
|
+
}
|
|
90
|
+
if (config.italic !== undefined) {
|
|
91
|
+
textStyle.italic = config.italic;
|
|
92
|
+
fields.push('italic');
|
|
93
|
+
}
|
|
94
|
+
if (config.underline !== undefined) {
|
|
95
|
+
textStyle.underline = config.underline;
|
|
96
|
+
fields.push('underline');
|
|
97
|
+
}
|
|
98
|
+
if (config.strikethrough !== undefined) {
|
|
99
|
+
textStyle.strikethrough = config.strikethrough;
|
|
100
|
+
fields.push('strikethrough');
|
|
101
|
+
}
|
|
102
|
+
if (config.smallCaps !== undefined) {
|
|
103
|
+
textStyle.smallCaps = config.smallCaps;
|
|
104
|
+
fields.push('smallCaps');
|
|
105
|
+
}
|
|
106
|
+
if (config.color !== undefined) {
|
|
107
|
+
textStyle.foregroundColor = toColor(config.color);
|
|
108
|
+
fields.push('foregroundColor');
|
|
109
|
+
}
|
|
110
|
+
if (config.backgroundColor !== undefined) {
|
|
111
|
+
textStyle.backgroundColor = toColor(config.backgroundColor);
|
|
112
|
+
fields.push('backgroundColor');
|
|
113
|
+
}
|
|
114
|
+
if (config.baselineOffset !== undefined) {
|
|
115
|
+
textStyle.baselineOffset = config.baselineOffset;
|
|
116
|
+
fields.push('baselineOffset');
|
|
117
|
+
}
|
|
118
|
+
if (fields.length === 0)
|
|
119
|
+
return null;
|
|
120
|
+
return {
|
|
121
|
+
updateTextStyle: {
|
|
122
|
+
range: { startIndex, endIndex },
|
|
123
|
+
textStyle,
|
|
124
|
+
fields: fields.join(','),
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function buildParagraphStyleRequest(config, startIndex, endIndex) {
|
|
129
|
+
const paragraphStyle = {};
|
|
130
|
+
const fields = [];
|
|
131
|
+
if (config.alignment !== undefined) {
|
|
132
|
+
paragraphStyle.alignment = config.alignment;
|
|
133
|
+
fields.push('alignment');
|
|
134
|
+
}
|
|
135
|
+
if (config.lineSpacing !== undefined) {
|
|
136
|
+
paragraphStyle.lineSpacing = config.lineSpacing;
|
|
137
|
+
fields.push('lineSpacing');
|
|
138
|
+
}
|
|
139
|
+
if (config.spaceAbove !== undefined) {
|
|
140
|
+
paragraphStyle.spaceAbove = toDimension(config.spaceAbove);
|
|
141
|
+
fields.push('spaceAbove');
|
|
142
|
+
}
|
|
143
|
+
if (config.spaceBelow !== undefined) {
|
|
144
|
+
paragraphStyle.spaceBelow = toDimension(config.spaceBelow);
|
|
145
|
+
fields.push('spaceBelow');
|
|
146
|
+
}
|
|
147
|
+
if (config.indentFirstLine !== undefined) {
|
|
148
|
+
paragraphStyle.indentFirstLine = toDimension(config.indentFirstLine);
|
|
149
|
+
fields.push('indentFirstLine');
|
|
150
|
+
}
|
|
151
|
+
if (config.indentStart !== undefined) {
|
|
152
|
+
paragraphStyle.indentStart = toDimension(config.indentStart);
|
|
153
|
+
fields.push('indentStart');
|
|
154
|
+
}
|
|
155
|
+
if (config.indentEnd !== undefined) {
|
|
156
|
+
paragraphStyle.indentEnd = toDimension(config.indentEnd);
|
|
157
|
+
fields.push('indentEnd');
|
|
158
|
+
}
|
|
159
|
+
if (config.keepLinesTogether !== undefined) {
|
|
160
|
+
paragraphStyle.keepLinesTogether = config.keepLinesTogether;
|
|
161
|
+
fields.push('keepLinesTogether');
|
|
162
|
+
}
|
|
163
|
+
if (config.keepWithNext !== undefined) {
|
|
164
|
+
paragraphStyle.keepWithNext = config.keepWithNext;
|
|
165
|
+
fields.push('keepWithNext');
|
|
166
|
+
}
|
|
167
|
+
if (config.direction !== undefined) {
|
|
168
|
+
paragraphStyle.direction = config.direction;
|
|
169
|
+
fields.push('direction');
|
|
170
|
+
}
|
|
171
|
+
if (fields.length === 0)
|
|
172
|
+
return null;
|
|
173
|
+
return {
|
|
174
|
+
updateParagraphStyle: {
|
|
175
|
+
range: { startIndex, endIndex },
|
|
176
|
+
paragraphStyle,
|
|
177
|
+
fields: fields.join(','),
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function groupParagraphsByStyle(content) {
|
|
182
|
+
const groups = {};
|
|
183
|
+
for (const element of content) {
|
|
184
|
+
if (!element.paragraph)
|
|
185
|
+
continue;
|
|
186
|
+
const styleType = element.paragraph.paragraphStyle?.namedStyleType || 'NORMAL_TEXT';
|
|
187
|
+
if (!groups[styleType])
|
|
188
|
+
groups[styleType] = [];
|
|
189
|
+
groups[styleType].push({
|
|
190
|
+
startIndex: element.startIndex || 0,
|
|
191
|
+
endIndex: element.endIndex || 0,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return groups;
|
|
195
|
+
}
|
|
196
|
+
function extractTables(content) {
|
|
197
|
+
const tables = [];
|
|
198
|
+
for (const element of content) {
|
|
199
|
+
if (element.table) {
|
|
200
|
+
tables.push({
|
|
201
|
+
startIndex: element.startIndex,
|
|
202
|
+
rows: element.table.rows || 0,
|
|
203
|
+
cols: element.table.columns || 0,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return tables;
|
|
208
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const DeleteStylePresetSchema: z.ZodObject<{
|
|
3
|
+
presetName: z.ZodString;
|
|
4
|
+
}, z.core.$strip>;
|
|
5
|
+
export declare function deleteStylePreset(args: z.infer<typeof DeleteStylePresetSchema>): Promise<{
|
|
6
|
+
deleted: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { deletePreset } from '../presets/config.js';
|
|
3
|
+
export const DeleteStylePresetSchema = z.object({
|
|
4
|
+
presetName: z.string().describe('Name of the preset to delete. Cannot delete built-in presets (clean, corporate, classic, minimal).'),
|
|
5
|
+
});
|
|
6
|
+
export async function deleteStylePreset(args) {
|
|
7
|
+
deletePreset(args.presetName);
|
|
8
|
+
return {
|
|
9
|
+
deleted: args.presetName,
|
|
10
|
+
message: `Preset "${args.presetName}" deleted.`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { StylePreset } from '../presets/types.js';
|
|
3
|
+
export declare const ExtractDocumentStylesSchema: z.ZodObject<{
|
|
4
|
+
documentId: z.ZodString;
|
|
5
|
+
saveAs: z.ZodOptional<z.ZodString>;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export declare function extractDocumentStyles(args: z.infer<typeof ExtractDocumentStylesSchema>): Promise<{
|
|
8
|
+
documentId: string;
|
|
9
|
+
title: string | null | undefined;
|
|
10
|
+
preset: StylePreset;
|
|
11
|
+
savedAs: string | null;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getAuthClient } from '../auth/google-auth.js';
|
|
4
|
+
import { savePreset } from '../presets/config.js';
|
|
5
|
+
import { VALID_NAMED_STYLE_TYPES } from '../presets/types.js';
|
|
6
|
+
export const ExtractDocumentStylesSchema = z.object({
|
|
7
|
+
documentId: z.string().describe('The Google Doc document ID to extract styles from'),
|
|
8
|
+
saveAs: z.string().optional().describe('If provided, saves the extracted styles as a named preset in ~/.gdocs-mcp/styles.json'),
|
|
9
|
+
});
|
|
10
|
+
export async function extractDocumentStyles(args) {
|
|
11
|
+
const auth = getAuthClient();
|
|
12
|
+
const docs = google.docs({ version: 'v1', auth });
|
|
13
|
+
const doc = await docs.documents.get({ documentId: args.documentId });
|
|
14
|
+
const preset = {
|
|
15
|
+
document: extractDocumentStyle(doc.data.documentStyle),
|
|
16
|
+
styles: extractNamedStyles(doc.data.namedStyles),
|
|
17
|
+
table: extractTableStyles(doc.data.body?.content || []),
|
|
18
|
+
};
|
|
19
|
+
if (args.saveAs) {
|
|
20
|
+
savePreset(args.saveAs, preset);
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
documentId: args.documentId,
|
|
24
|
+
title: doc.data.title,
|
|
25
|
+
preset,
|
|
26
|
+
savedAs: args.saveAs || null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function extractDocumentStyle(docStyle) {
|
|
30
|
+
const config = {};
|
|
31
|
+
if (docStyle?.marginTop?.magnitude !== undefined)
|
|
32
|
+
config.marginTop = docStyle.marginTop.magnitude;
|
|
33
|
+
if (docStyle?.marginBottom?.magnitude !== undefined)
|
|
34
|
+
config.marginBottom = docStyle.marginBottom.magnitude;
|
|
35
|
+
if (docStyle?.marginLeft?.magnitude !== undefined)
|
|
36
|
+
config.marginLeft = docStyle.marginLeft.magnitude;
|
|
37
|
+
if (docStyle?.marginRight?.magnitude !== undefined)
|
|
38
|
+
config.marginRight = docStyle.marginRight.magnitude;
|
|
39
|
+
if (docStyle?.pageSize?.width?.magnitude !== undefined)
|
|
40
|
+
config.pageWidth = docStyle.pageSize.width.magnitude;
|
|
41
|
+
if (docStyle?.pageSize?.height?.magnitude !== undefined)
|
|
42
|
+
config.pageHeight = docStyle.pageSize.height.magnitude;
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
function extractNamedStyles(namedStyles) {
|
|
46
|
+
const result = {};
|
|
47
|
+
if (!namedStyles?.styles)
|
|
48
|
+
return result;
|
|
49
|
+
for (const style of namedStyles.styles) {
|
|
50
|
+
const type = style.namedStyleType;
|
|
51
|
+
if (!VALID_NAMED_STYLE_TYPES.includes(type))
|
|
52
|
+
continue;
|
|
53
|
+
const config = {};
|
|
54
|
+
const ts = style.textStyle || {};
|
|
55
|
+
const ps = style.paragraphStyle || {};
|
|
56
|
+
// Text style
|
|
57
|
+
if (ts.weightedFontFamily?.fontFamily)
|
|
58
|
+
config.fontFamily = ts.weightedFontFamily.fontFamily;
|
|
59
|
+
if (ts.fontSize?.magnitude !== undefined)
|
|
60
|
+
config.fontSize = ts.fontSize.magnitude;
|
|
61
|
+
if (ts.bold !== undefined)
|
|
62
|
+
config.bold = ts.bold;
|
|
63
|
+
if (ts.italic !== undefined)
|
|
64
|
+
config.italic = ts.italic;
|
|
65
|
+
if (ts.underline !== undefined)
|
|
66
|
+
config.underline = ts.underline;
|
|
67
|
+
if (ts.strikethrough !== undefined)
|
|
68
|
+
config.strikethrough = ts.strikethrough;
|
|
69
|
+
if (ts.smallCaps !== undefined)
|
|
70
|
+
config.smallCaps = ts.smallCaps;
|
|
71
|
+
if (ts.foregroundColor?.color?.rgbColor)
|
|
72
|
+
config.color = rgbToHex(ts.foregroundColor.color.rgbColor);
|
|
73
|
+
if (ts.backgroundColor?.color?.rgbColor)
|
|
74
|
+
config.backgroundColor = rgbToHex(ts.backgroundColor.color.rgbColor);
|
|
75
|
+
if (ts.baselineOffset)
|
|
76
|
+
config.baselineOffset = ts.baselineOffset;
|
|
77
|
+
// Paragraph style
|
|
78
|
+
if (ps.alignment)
|
|
79
|
+
config.alignment = ps.alignment;
|
|
80
|
+
if (ps.lineSpacing !== undefined)
|
|
81
|
+
config.lineSpacing = ps.lineSpacing;
|
|
82
|
+
if (ps.spaceAbove?.magnitude !== undefined)
|
|
83
|
+
config.spaceAbove = ps.spaceAbove.magnitude;
|
|
84
|
+
if (ps.spaceBelow?.magnitude !== undefined)
|
|
85
|
+
config.spaceBelow = ps.spaceBelow.magnitude;
|
|
86
|
+
if (ps.indentFirstLine?.magnitude !== undefined)
|
|
87
|
+
config.indentFirstLine = ps.indentFirstLine.magnitude;
|
|
88
|
+
if (ps.indentStart?.magnitude !== undefined)
|
|
89
|
+
config.indentStart = ps.indentStart.magnitude;
|
|
90
|
+
if (ps.indentEnd?.magnitude !== undefined)
|
|
91
|
+
config.indentEnd = ps.indentEnd.magnitude;
|
|
92
|
+
if (ps.keepLinesTogether !== undefined)
|
|
93
|
+
config.keepLinesTogether = ps.keepLinesTogether;
|
|
94
|
+
if (ps.keepWithNext !== undefined)
|
|
95
|
+
config.keepWithNext = ps.keepWithNext;
|
|
96
|
+
if (ps.direction)
|
|
97
|
+
config.direction = ps.direction;
|
|
98
|
+
if (Object.keys(config).length > 0) {
|
|
99
|
+
result[type] = config;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
function extractTableStyles(content) {
|
|
105
|
+
// Find first table and extract its styling as the template
|
|
106
|
+
for (const element of content) {
|
|
107
|
+
if (!element.table)
|
|
108
|
+
continue;
|
|
109
|
+
const table = element.table;
|
|
110
|
+
const firstRow = table.tableRows?.[0];
|
|
111
|
+
if (!firstRow)
|
|
112
|
+
continue;
|
|
113
|
+
const config = {};
|
|
114
|
+
const firstCell = firstRow.tableCells?.[0];
|
|
115
|
+
if (firstCell?.tableCellStyle) {
|
|
116
|
+
const cs = firstCell.tableCellStyle;
|
|
117
|
+
if (cs.backgroundColor?.color?.rgbColor) {
|
|
118
|
+
config.headerBackground = rgbToHex(cs.backgroundColor.color.rgbColor);
|
|
119
|
+
}
|
|
120
|
+
if (cs.borderTop?.width?.magnitude !== undefined) {
|
|
121
|
+
config.borderWidth = cs.borderTop.width.magnitude;
|
|
122
|
+
}
|
|
123
|
+
if (cs.borderTop?.color?.color?.rgbColor) {
|
|
124
|
+
config.borderColor = rgbToHex(cs.borderTop.color.color.rgbColor);
|
|
125
|
+
}
|
|
126
|
+
if (cs.paddingTop?.magnitude !== undefined) {
|
|
127
|
+
config.cellPadding = cs.paddingTop.magnitude;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Check second row for alternating background
|
|
131
|
+
if (table.tableRows?.length > 2) {
|
|
132
|
+
const thirdRow = table.tableRows[2];
|
|
133
|
+
const thirdCell = thirdRow?.tableCells?.[0];
|
|
134
|
+
if (thirdCell?.tableCellStyle?.backgroundColor?.color?.rgbColor) {
|
|
135
|
+
config.alternateRowBackground = rgbToHex(thirdCell.tableCellStyle.backgroundColor.color.rgbColor);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (Object.keys(config).length > 0)
|
|
139
|
+
return config;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
function rgbToHex(rgb) {
|
|
145
|
+
const r = Math.round((rgb.red || 0) * 255);
|
|
146
|
+
const g = Math.round((rgb.green || 0) * 255);
|
|
147
|
+
const b = Math.round((rgb.blue || 0) * 255);
|
|
148
|
+
return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
|
|
149
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const ListStylePresetsSchema: z.ZodObject<{}, z.core.$strip>;
|
|
3
|
+
export declare function listStylePresets(): Promise<{
|
|
4
|
+
activePreset: string;
|
|
5
|
+
presets: {
|
|
6
|
+
name: string;
|
|
7
|
+
source: "built-in" | "user";
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
summary: {
|
|
10
|
+
bodyFont: string;
|
|
11
|
+
bodySize: string | number;
|
|
12
|
+
headingFont: string;
|
|
13
|
+
headingColor: string;
|
|
14
|
+
lineSpacing: string | number;
|
|
15
|
+
};
|
|
16
|
+
}[];
|
|
17
|
+
count: number;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { listAllPresets, getActivePresetName } from '../presets/config.js';
|
|
3
|
+
export const ListStylePresetsSchema = z.object({});
|
|
4
|
+
export async function listStylePresets() {
|
|
5
|
+
const presets = listAllPresets();
|
|
6
|
+
const activePreset = getActivePresetName();
|
|
7
|
+
const result = Object.entries(presets).map(([name, { source, preset }]) => {
|
|
8
|
+
const styles = preset.styles || {};
|
|
9
|
+
const normalText = styles.NORMAL_TEXT || {};
|
|
10
|
+
const heading1 = styles.HEADING_1 || {};
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
source,
|
|
14
|
+
isActive: name === activePreset,
|
|
15
|
+
summary: {
|
|
16
|
+
bodyFont: normalText.fontFamily || 'default',
|
|
17
|
+
bodySize: normalText.fontSize || 'default',
|
|
18
|
+
headingFont: heading1.fontFamily || normalText.fontFamily || 'default',
|
|
19
|
+
headingColor: heading1.color || 'default',
|
|
20
|
+
lineSpacing: normalText.lineSpacing || 'default',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
activePreset: activePreset || 'none',
|
|
26
|
+
presets: result,
|
|
27
|
+
count: result.length,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const SetActivePresetSchema: z.ZodObject<{
|
|
3
|
+
presetName: z.ZodString;
|
|
4
|
+
}, z.core.$strip>;
|
|
5
|
+
export declare function setActivePreset(args: z.infer<typeof SetActivePresetSchema>): Promise<{
|
|
6
|
+
activePreset: string;
|
|
7
|
+
message: string;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { setActivePreset as setActive } from '../presets/config.js';
|
|
3
|
+
export const SetActivePresetSchema = z.object({
|
|
4
|
+
presetName: z.string().describe('Name of the preset to set as active. The active preset auto-applies when creating new documents.'),
|
|
5
|
+
});
|
|
6
|
+
export async function setActivePreset(args) {
|
|
7
|
+
setActive(args.presetName);
|
|
8
|
+
return {
|
|
9
|
+
activePreset: args.presetName,
|
|
10
|
+
message: `Active preset set to "${args.presetName}". New documents will use this preset automatically.`,
|
|
11
|
+
};
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gdocs-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Open-source MCP server for Google Docs and Sheets. Self-hosted, local OAuth, no third-party token storage.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"repository": {
|
|
32
32
|
"type": "git",
|
|
33
|
-
"url": "https://github.com/
|
|
33
|
+
"url": "https://github.com/Apurwa/gdocs-mcp"
|
|
34
34
|
},
|
|
35
35
|
"engines": {
|
|
36
36
|
"node": ">=18"
|