google-tools-mcp 1.2.11 → 2.0.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/LICENSE +21 -21
- package/README.md +555 -276
- package/dist/cachedToolsList.js +52 -52
- package/dist/googleDocsApiHelpers.js +76 -12
- package/dist/helpers.js +572 -252
- package/dist/httpAuth.js +296 -0
- package/dist/index.js +162 -11
- package/dist/logger.js +87 -87
- package/dist/markdown-transformer/docsToMarkdown.js +222 -21
- package/dist/markdown-transformer/index.js +19 -9
- package/dist/markdown-transformer/markdownToDocs.js +458 -28
- package/dist/readTracker.js +136 -17
- package/dist/sessionContext.js +37 -0
- package/dist/setup.js +187 -9
- package/dist/tools/calendar/getBusy.js +64 -64
- package/dist/tools/calendar/getEvents.js +141 -141
- package/dist/tools/calendar/getFree.js +225 -225
- package/dist/tools/calendar/index.js +19 -19
- package/dist/tools/calendar/listCalendars.js +38 -38
- package/dist/tools/calendar/listRecurringInstances.js +83 -83
- package/dist/tools/calendar/manageCalendar.js +121 -121
- package/dist/tools/calendar/manageEvent.js +258 -258
- package/dist/tools/calendar/moveEvent.js +66 -66
- package/dist/tools/docs/addTab.js +10 -12
- package/dist/tools/docs/appendToGoogleDoc.js +9 -4
- package/dist/tools/docs/deleteRange.js +9 -4
- package/dist/tools/docs/findAndReplace.js +9 -4
- package/dist/tools/docs/formatting/applyParagraphStyle.js +4 -1
- package/dist/tools/docs/formatting/getFormatting.js +171 -171
- package/dist/tools/docs/insertImage.js +15 -2
- package/dist/tools/docs/insertPageBreak.js +4 -1
- package/dist/tools/docs/insertTable.js +4 -1
- package/dist/tools/docs/insertTableWithData.js +4 -1
- package/dist/tools/docs/modifyText.js +14 -4
- package/dist/tools/docs/modifyText.test.js +84 -84
- package/dist/tools/docs/readGoogleDoc.js +71 -11
- package/dist/tools/docs/renameTab.js +12 -14
- package/dist/tools/drafts.js +165 -165
- package/dist/tools/drive/createDocument.js +13 -1
- package/dist/tools/drive/downloadFile.js +268 -268
- package/dist/tools/drive/getFileInfo.js +48 -48
- package/dist/tools/drive/getFilePath.js +50 -50
- package/dist/tools/drive/listDriveFiles.js +112 -112
- package/dist/tools/drive/listSharedDrives.js +52 -52
- package/dist/tools/drive/listSharedWithMe.js +97 -97
- package/dist/tools/drive/uploadFile.js +111 -111
- package/dist/tools/extras/index.js +9 -9
- package/dist/tools/extras/readDriveFile.js +212 -206
- package/dist/tools/extras/readFile.js +84 -84
- package/dist/tools/extras/searchFileContents.js +81 -81
- package/dist/tools/forms/batchUpdateForm.js +81 -81
- package/dist/tools/forms/createForm.js +54 -54
- package/dist/tools/forms/getForm.js +118 -118
- package/dist/tools/forms/getFormResponse.js +45 -45
- package/dist/tools/forms/index.js +15 -15
- package/dist/tools/forms/listFormResponses.js +56 -56
- package/dist/tools/forms/setPublishSettings.js +59 -59
- package/dist/tools/gmail/drafts.js +165 -165
- package/dist/tools/gmail/labels.js +54 -83
- package/dist/tools/gmail/messages.js +444 -441
- package/dist/tools/gmail/settings.js +358 -528
- package/dist/tools/gmail/threads.js +286 -164
- package/dist/tools/index.js +511 -473
- package/dist/tools/labels.js +103 -103
- package/dist/tools/legacyAliases.js +426 -0
- package/dist/tools/maps/directions.js +72 -0
- package/dist/tools/maps/geocode.js +16 -0
- package/dist/tools/maps/index.js +15 -0
- package/dist/tools/maps/mapsClient.js +83 -0
- package/dist/tools/maps/placeDetails.js +16 -0
- package/dist/tools/maps/reverseGeocode.js +19 -0
- package/dist/tools/maps/searchNearby.js +54 -0
- package/dist/tools/maps/searchPlaces.js +24 -0
- package/dist/tools/messages.js +448 -448
- package/dist/tools/settings.js +528 -528
- package/dist/tools/slides/createPresentation.js +104 -104
- package/dist/tools/slides/createShape.js +92 -92
- package/dist/tools/slides/createTextBox.js +82 -82
- package/dist/tools/slides/deleteSlide.js +30 -30
- package/dist/tools/slides/duplicateSlide.js +37 -37
- package/dist/tools/slides/exportThumbnail.js +42 -42
- package/dist/tools/slides/formatParagraph.js +72 -72
- package/dist/tools/slides/formatText.js +84 -84
- package/dist/tools/slides/getPresentation.js +87 -87
- package/dist/tools/slides/index.js +33 -33
- package/dist/tools/slides/reorderSlides.js +41 -41
- package/dist/tools/slides/replaceAllText.js +46 -46
- package/dist/tools/slides/setBackground.js +58 -58
- package/dist/tools/slides/speakerNotes.js +102 -102
- package/dist/tools/slides/styleShape.js +111 -111
- package/dist/tools/slides/updatePresentation.js +128 -128
- package/dist/tools/threads.js +145 -145
- package/dist/tools/utils/appendMarkdownToGoogleDoc.js +31 -7
- package/dist/tools/utils/replaceDocumentWithMarkdown.js +72 -9
- package/dist/updateCheck.js +202 -0
- package/dist/workspace.js +122 -0
- package/package.json +82 -80
|
@@ -4,6 +4,134 @@
|
|
|
4
4
|
* When these are detected on a text run, we render backtick code in markdown.
|
|
5
5
|
*/
|
|
6
6
|
const CODE_FONT_FAMILIES = new Set(['Roboto Mono', 'Courier New', 'Consolas', 'monospace']);
|
|
7
|
+
/**
|
|
8
|
+
* Inspects the body content that replaceDocumentWithMarkdown will delete and
|
|
9
|
+
* re-insert, and reports anything docsJsonToMarkdown cannot represent at all.
|
|
10
|
+
* Returns an array of human-readable warning strings (empty if the content
|
|
11
|
+
* converts losslessly). Call before replaceDocumentWithMarkdown to warn the AI
|
|
12
|
+
* about what a body replacement will permanently lose.
|
|
13
|
+
*
|
|
14
|
+
* Checked: inline/positioned images and footnote references, neither of which
|
|
15
|
+
* has any markdown representation in either direction of this converter; a
|
|
16
|
+
* generated table of contents; and, generically, every OTHER Docs API
|
|
17
|
+
* `ParagraphElement` variant that `extractFormattedText()` does not render.
|
|
18
|
+
*
|
|
19
|
+
* `extractFormattedText()` only ever renders `textRun` (see below). The Docs
|
|
20
|
+
* API `ParagraphElement` union also defines `autoText`, `pageBreak`,
|
|
21
|
+
* `columnBreak`, `horizontalRule`, `equation`, `person`, `richLink`, and
|
|
22
|
+
* `dateElement` (https://developers.google.com/workspace/docs/api/reference/rest/v1/documents#ParagraphElement).
|
|
23
|
+
* None of those has a markdown representation, so a body replacement deletes
|
|
24
|
+
* them permanently and the current markdown output never mentions they were
|
|
25
|
+
* there. Rather than enumerate that list (and silently miss whatever variant
|
|
26
|
+
* Google adds next), this scanner is deny-by-default: any paragraph-element
|
|
27
|
+
* key other than `startIndex`/`endIndex` (position metadata) and the three
|
|
28
|
+
* keys explicitly handled elsewhere in this function (`textRun`, rendered;
|
|
29
|
+
* `inlineObjectElement`/`footnoteReference`, already counted above) is
|
|
30
|
+
* treated as an unhandled content variant and reported by name.
|
|
31
|
+
*
|
|
32
|
+
* NOT checked (intentionally): custom text/highlight colors and non-default
|
|
33
|
+
* paragraph alignment. Those round-trip losslessly through the rich-markdown
|
|
34
|
+
* HTML extensions this converter emits by default (`<span style="color:...">`,
|
|
35
|
+
* `<p align="...">`, table alignment markers) and their inverse parsing in
|
|
36
|
+
* markdownToDocs.js, so warning about them here would be inaccurate.
|
|
37
|
+
*
|
|
38
|
+
* IMPORTANT — accuracy: every warning is derived from `bodyContent` itself,
|
|
39
|
+
* i.e. the exact body (a specific tab's body in tab mode, the document body
|
|
40
|
+
* otherwise) that the replacement mutates. This deliberately does NOT inspect
|
|
41
|
+
* document-level `headers`/`footers`, which are separate document segments a
|
|
42
|
+
* body-content replacement does not delete, nor the global `inlineObjects`/
|
|
43
|
+
* `footnotes` maps, which can include content belonging to OTHER tabs that a
|
|
44
|
+
* scoped replacement never touches.
|
|
45
|
+
*
|
|
46
|
+
* @param {Array} bodyContent structural elements of the body being replaced
|
|
47
|
+
* (e.g. `contentSource.body.content`).
|
|
48
|
+
* @returns {string[]} warnings
|
|
49
|
+
*/
|
|
50
|
+
// ParagraphElement keys this converter already accounts for: startIndex/
|
|
51
|
+
// endIndex are position metadata (not content); textRun is rendered by
|
|
52
|
+
// extractFormattedText(); inlineObjectElement/footnoteReference are counted
|
|
53
|
+
// as their own dedicated warnings below. Any other key on a ParagraphElement
|
|
54
|
+
// is, by the Docs API's own union contract, an unhandled content-bearing
|
|
55
|
+
// variant (autoText, pageBreak, columnBreak, horizontalRule, equation,
|
|
56
|
+
// person, richLink, dateElement, or a future addition) that extractFormattedText
|
|
57
|
+
// silently drops.
|
|
58
|
+
const HANDLED_PARAGRAPH_ELEMENT_KEYS = new Set([
|
|
59
|
+
'startIndex',
|
|
60
|
+
'endIndex',
|
|
61
|
+
'textRun',
|
|
62
|
+
'inlineObjectElement',
|
|
63
|
+
'footnoteReference',
|
|
64
|
+
]);
|
|
65
|
+
export function checkMarkdownFidelity(bodyContent) {
|
|
66
|
+
const warnings = [];
|
|
67
|
+
let imageCount = 0;
|
|
68
|
+
let footnoteCount = 0;
|
|
69
|
+
let tocCount = 0;
|
|
70
|
+
const unhandledElementCounts = {};
|
|
71
|
+
function scanParagraphElements(elements) {
|
|
72
|
+
for (const pe of elements) {
|
|
73
|
+
// Inline images embedded in the body flow — deleted with the body.
|
|
74
|
+
if (pe.inlineObjectElement) {
|
|
75
|
+
imageCount++;
|
|
76
|
+
}
|
|
77
|
+
// Footnote references live in the body; deleting the body removes them
|
|
78
|
+
// (and Docs then drops the orphaned footnote).
|
|
79
|
+
if (pe.footnoteReference) {
|
|
80
|
+
footnoteCount++;
|
|
81
|
+
}
|
|
82
|
+
// Deny-by-default: anything that isn't a key we explicitly handle is an
|
|
83
|
+
// unhandled ParagraphElement variant. Grouped and named below rather
|
|
84
|
+
// than warning per element.
|
|
85
|
+
for (const key of Object.keys(pe)) {
|
|
86
|
+
if (!HANDLED_PARAGRAPH_ELEMENT_KEYS.has(key)) {
|
|
87
|
+
unhandledElementCounts[key] = (unhandledElementCounts[key] ?? 0) + 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function scanBodyContent(content) {
|
|
93
|
+
for (const element of content) {
|
|
94
|
+
if (element.paragraph) {
|
|
95
|
+
// Positioned (floating) images anchored to this paragraph are also
|
|
96
|
+
// removed when the paragraph is deleted.
|
|
97
|
+
if (Array.isArray(element.paragraph.positionedObjectIds)) {
|
|
98
|
+
imageCount += element.paragraph.positionedObjectIds.length;
|
|
99
|
+
}
|
|
100
|
+
scanParagraphElements(element.paragraph.elements ?? []);
|
|
101
|
+
}
|
|
102
|
+
else if (element.table) {
|
|
103
|
+
for (const row of (element.table.tableRows ?? [])) {
|
|
104
|
+
for (const cell of (row.tableCells ?? [])) {
|
|
105
|
+
scanBodyContent(cell.content ?? []);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// A generated table of contents sits in the body, so the replacement
|
|
110
|
+
// deletes it, and markdown has no way to express one, so the importer
|
|
111
|
+
// cannot put it back. Without this the whole round trip drops the TOC
|
|
112
|
+
// with nothing said about it.
|
|
113
|
+
else if (element.tableOfContents) {
|
|
114
|
+
tocCount++;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
scanBodyContent(bodyContent ?? []);
|
|
119
|
+
if (imageCount > 0) {
|
|
120
|
+
warnings.push(`${imageCount} image(s) — will be removed`);
|
|
121
|
+
}
|
|
122
|
+
if (footnoteCount > 0) {
|
|
123
|
+
warnings.push(`${footnoteCount} footnote(s) — will be removed`);
|
|
124
|
+
}
|
|
125
|
+
if (tocCount > 0) {
|
|
126
|
+
warnings.push(`${tocCount} table(s) of contents — will be removed (markdown cannot express a generated TOC; reinsert it in Docs afterward)`);
|
|
127
|
+
}
|
|
128
|
+
const unhandledKeys = Object.keys(unhandledElementCounts).sort();
|
|
129
|
+
if (unhandledKeys.length > 0) {
|
|
130
|
+
const parts = unhandledKeys.map((key) => `${unhandledElementCounts[key]} ${key}`).join(', ');
|
|
131
|
+
warnings.push(`${parts} — unsupported content type(s) with no markdown representation; will be removed`);
|
|
132
|
+
}
|
|
133
|
+
return warnings;
|
|
134
|
+
}
|
|
7
135
|
// --- Main Conversion ---
|
|
8
136
|
/**
|
|
9
137
|
* Converts Google Docs JSON structure to a markdown string.
|
|
@@ -15,34 +143,43 @@ const CODE_FONT_FAMILIES = new Set(['Roboto Mono', 'Courier New', 'Consolas', 'm
|
|
|
15
143
|
* underline, links, code), ordered & unordered lists with nesting, tables,
|
|
16
144
|
* and section breaks.
|
|
17
145
|
*/
|
|
18
|
-
export function docsJsonToMarkdown(docData) {
|
|
146
|
+
export function docsJsonToMarkdown(docData, options = {}) {
|
|
19
147
|
const body = docData.body;
|
|
20
148
|
if (!body?.content) {
|
|
21
149
|
return '';
|
|
22
150
|
}
|
|
23
151
|
const lists = docData.lists ?? {};
|
|
152
|
+
const conversionOptions = {
|
|
153
|
+
richMarkdown: options.plainMarkdown ? false : options.richMarkdown ?? true,
|
|
154
|
+
};
|
|
24
155
|
let markdown = '';
|
|
25
156
|
for (const element of body.content) {
|
|
26
157
|
if (element.paragraph) {
|
|
27
|
-
markdown += convertParagraph(element.paragraph, lists);
|
|
158
|
+
markdown += convertParagraph(element.paragraph, lists, conversionOptions);
|
|
28
159
|
}
|
|
29
160
|
else if (element.table) {
|
|
30
|
-
markdown += convertTable(element.table);
|
|
161
|
+
markdown += convertTable(element.table, conversionOptions);
|
|
31
162
|
}
|
|
32
163
|
else if (element.sectionBreak) {
|
|
164
|
+
if (isInitialDocumentSectionBreak(element)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
33
167
|
markdown += '\n---\n\n';
|
|
34
168
|
}
|
|
35
169
|
}
|
|
36
170
|
return markdown.trim();
|
|
37
171
|
}
|
|
172
|
+
function isInitialDocumentSectionBreak(element) {
|
|
173
|
+
return element.endIndex === 1 && element.startIndex === undefined;
|
|
174
|
+
}
|
|
38
175
|
// --- Paragraph Conversion ---
|
|
39
|
-
function convertParagraph(paragraph, lists) {
|
|
176
|
+
function convertParagraph(paragraph, lists, options) {
|
|
40
177
|
// 1. Determine paragraph type
|
|
41
178
|
const headingLevel = getHeadingLevel(paragraph);
|
|
42
179
|
const listInfo = getListInfo(paragraph, lists);
|
|
43
180
|
// 2. Extract text content with inline formatting
|
|
44
181
|
const elements = paragraph.elements ?? [];
|
|
45
|
-
const text = extractFormattedText(elements);
|
|
182
|
+
const text = extractFormattedText(elements, options);
|
|
46
183
|
// 3. Format based on type
|
|
47
184
|
if (headingLevel && text.trim()) {
|
|
48
185
|
const hashes = '#'.repeat(Math.min(headingLevel, 6));
|
|
@@ -54,7 +191,15 @@ function convertParagraph(paragraph, lists) {
|
|
|
54
191
|
return `${indent}${marker} ${text.trim()}\n`;
|
|
55
192
|
}
|
|
56
193
|
if (text.trim()) {
|
|
57
|
-
|
|
194
|
+
const trimmed = text.trim();
|
|
195
|
+
if (options.richMarkdown && isBlockquoteParagraph(paragraph)) {
|
|
196
|
+
return `<blockquote>${trimmed}</blockquote>\n\n`;
|
|
197
|
+
}
|
|
198
|
+
const alignment = paragraphAlignmentToHtml(paragraph.paragraphStyle?.alignment);
|
|
199
|
+
if (options.richMarkdown && alignment) {
|
|
200
|
+
return `<p align="${alignment}">${trimmed}</p>\n\n`;
|
|
201
|
+
}
|
|
202
|
+
return `${trimmed}\n\n`;
|
|
58
203
|
}
|
|
59
204
|
return '\n';
|
|
60
205
|
}
|
|
@@ -91,16 +236,16 @@ function getListInfo(paragraph, lists) {
|
|
|
91
236
|
return { ordered, nestingLevel };
|
|
92
237
|
}
|
|
93
238
|
// --- Text Run Conversion ---
|
|
94
|
-
function extractFormattedText(elements) {
|
|
239
|
+
function extractFormattedText(elements, options) {
|
|
95
240
|
let result = '';
|
|
96
241
|
for (const element of elements) {
|
|
97
242
|
if (element.textRun) {
|
|
98
|
-
result += convertTextRun(element.textRun);
|
|
243
|
+
result += convertTextRun(element.textRun, options);
|
|
99
244
|
}
|
|
100
245
|
}
|
|
101
246
|
return result;
|
|
102
247
|
}
|
|
103
|
-
function convertTextRun(textRun) {
|
|
248
|
+
function convertTextRun(textRun, options) {
|
|
104
249
|
let text = textRun.content ?? '';
|
|
105
250
|
const style = textRun.textStyle;
|
|
106
251
|
if (!style)
|
|
@@ -134,20 +279,72 @@ function convertTextRun(textRun) {
|
|
|
134
279
|
if (style.strikethrough) {
|
|
135
280
|
formatted = `~~${formatted}~~`;
|
|
136
281
|
}
|
|
137
|
-
if (style.underline && !style.link) {
|
|
282
|
+
if (options.richMarkdown && style.underline && !style.link) {
|
|
138
283
|
formatted = `<u>${formatted}</u>`;
|
|
139
284
|
}
|
|
285
|
+
if (options.richMarkdown) {
|
|
286
|
+
formatted = applyRichTextStyle(formatted, style);
|
|
287
|
+
}
|
|
140
288
|
if (style.link?.url) {
|
|
141
289
|
formatted = `[${formatted}](${style.link.url})`;
|
|
142
290
|
}
|
|
143
291
|
return formatted + (trailingNewline ? '\n' : '');
|
|
144
292
|
}
|
|
293
|
+
function applyRichTextStyle(text, style) {
|
|
294
|
+
const styles = [];
|
|
295
|
+
const fg = rgbColorToHex(style.foregroundColor?.color?.rgbColor);
|
|
296
|
+
const bg = rgbColorToHex(style.backgroundColor?.color?.rgbColor);
|
|
297
|
+
const fontSize = style.fontSize?.magnitude;
|
|
298
|
+
const fontFamily = style.weightedFontFamily?.fontFamily;
|
|
299
|
+
if (fg)
|
|
300
|
+
styles.push(`color:${fg}`);
|
|
301
|
+
if (bg)
|
|
302
|
+
styles.push(`background-color:${bg}`);
|
|
303
|
+
if (typeof fontSize === 'number')
|
|
304
|
+
styles.push(`font-size:${fontSize}pt`);
|
|
305
|
+
if (fontFamily && !CODE_FONT_FAMILIES.has(fontFamily))
|
|
306
|
+
styles.push(`font-family:${escapeHtmlAttr(fontFamily)}`);
|
|
307
|
+
if (styles.length === 0)
|
|
308
|
+
return text;
|
|
309
|
+
return `<span style="${styles.join(';')}">${text}</span>`;
|
|
310
|
+
}
|
|
145
311
|
function isCodeStyled(style) {
|
|
146
312
|
const fontFamily = style.weightedFontFamily?.fontFamily;
|
|
147
313
|
return typeof fontFamily === 'string' && CODE_FONT_FAMILIES.has(fontFamily);
|
|
148
314
|
}
|
|
315
|
+
function isBlockquoteParagraph(paragraph) {
|
|
316
|
+
const style = paragraph.paragraphStyle;
|
|
317
|
+
return Boolean(style?.borderLeft ||
|
|
318
|
+
style?.indentStart?.magnitude >= 30 ||
|
|
319
|
+
style?.indentFirstLine?.magnitude >= 30);
|
|
320
|
+
}
|
|
321
|
+
function paragraphAlignmentToHtml(alignment) {
|
|
322
|
+
switch (alignment) {
|
|
323
|
+
case 'CENTER':
|
|
324
|
+
return 'center';
|
|
325
|
+
case 'END':
|
|
326
|
+
case 'RIGHT':
|
|
327
|
+
return 'right';
|
|
328
|
+
case 'JUSTIFIED':
|
|
329
|
+
return 'justify';
|
|
330
|
+
default:
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function rgbColorToHex(rgb) {
|
|
335
|
+
if (!rgb)
|
|
336
|
+
return null;
|
|
337
|
+
const toHex = (value) => {
|
|
338
|
+
const normalized = Math.max(0, Math.min(255, Math.round((value ?? 0) * 255)));
|
|
339
|
+
return normalized.toString(16).padStart(2, '0');
|
|
340
|
+
};
|
|
341
|
+
return `#${toHex(rgb.red)}${toHex(rgb.green)}${toHex(rgb.blue)}`;
|
|
342
|
+
}
|
|
343
|
+
function escapeHtmlAttr(value) {
|
|
344
|
+
return value.replace(/&/g, '&').replace(/"/g, '"');
|
|
345
|
+
}
|
|
149
346
|
// --- Table Conversion ---
|
|
150
|
-
function convertTable(table) {
|
|
347
|
+
function convertTable(table, options) {
|
|
151
348
|
if (!table.tableRows || table.tableRows.length === 0) {
|
|
152
349
|
return '';
|
|
153
350
|
}
|
|
@@ -162,15 +359,15 @@ function convertTable(table) {
|
|
|
162
359
|
continue;
|
|
163
360
|
let rowText = '|';
|
|
164
361
|
for (const cell of row.tableCells) {
|
|
165
|
-
const cellText = extractCellText(cell);
|
|
362
|
+
const cellText = extractCellText(cell, options);
|
|
166
363
|
rowText += ` ${cellText} |`;
|
|
167
364
|
}
|
|
168
365
|
markdown += rowText + '\n';
|
|
169
366
|
// Add header separator after the first row
|
|
170
367
|
if (isFirstRow) {
|
|
171
368
|
let separator = '|';
|
|
172
|
-
for (
|
|
173
|
-
separator +=
|
|
369
|
+
for (const cell of row.tableCells) {
|
|
370
|
+
separator += ` ${tableAlignmentMarker(cell)} |`;
|
|
174
371
|
}
|
|
175
372
|
markdown += separator + '\n';
|
|
176
373
|
isFirstRow = false;
|
|
@@ -242,18 +439,22 @@ function convertCodeBlockTable(table) {
|
|
|
242
439
|
}
|
|
243
440
|
return '\n```\n' + codeText + '\n```\n\n';
|
|
244
441
|
}
|
|
245
|
-
function
|
|
442
|
+
function tableAlignmentMarker(cell) {
|
|
443
|
+
const alignment = paragraphAlignmentToHtml(cell.content?.[0]?.paragraph?.paragraphStyle?.alignment);
|
|
444
|
+
if (alignment === 'center')
|
|
445
|
+
return ':---:';
|
|
446
|
+
if (alignment === 'right')
|
|
447
|
+
return '---:';
|
|
448
|
+
return '---';
|
|
449
|
+
}
|
|
450
|
+
function extractCellText(cell, options) {
|
|
246
451
|
let text = '';
|
|
247
452
|
if (!cell.content)
|
|
248
453
|
return text;
|
|
249
454
|
for (const element of cell.content) {
|
|
250
455
|
if (element.paragraph?.elements) {
|
|
251
|
-
|
|
252
|
-
if (pe.textRun?.content) {
|
|
253
|
-
text += pe.textRun.content.replace(/\n/g, ' ').trim();
|
|
254
|
-
}
|
|
255
|
-
}
|
|
456
|
+
text += extractFormattedText(element.paragraph.elements, options).replace(/\n/g, ' ');
|
|
256
457
|
}
|
|
257
458
|
}
|
|
258
|
-
return text;
|
|
459
|
+
return text.trim().replace(/\|/g, '\\|');
|
|
259
460
|
}
|
|
@@ -12,24 +12,31 @@
|
|
|
12
12
|
import { docsJsonToMarkdown } from './docsToMarkdown.js';
|
|
13
13
|
import { convertMarkdownToRequests } from './markdownToDocs.js';
|
|
14
14
|
import { executeBatchUpdateWithSplitting, findTabById } from '../googleDocsApiHelpers.js';
|
|
15
|
-
export { docsJsonToMarkdown } from './docsToMarkdown.js';
|
|
15
|
+
export { docsJsonToMarkdown, checkMarkdownFidelity } from './docsToMarkdown.js';
|
|
16
16
|
/** Formats InsertMarkdownResult into a concise human-readable debug summary. */
|
|
17
17
|
export function formatInsertResult(result) {
|
|
18
18
|
const lines = [];
|
|
19
|
+
if (result.warnings?.length) {
|
|
20
|
+
lines.push('WARNINGS (content dropped):');
|
|
21
|
+
for (const warning of result.warnings) {
|
|
22
|
+
lines.push(` - ${warning}`);
|
|
23
|
+
}
|
|
24
|
+
lines.push('');
|
|
25
|
+
}
|
|
19
26
|
lines.push(`Markdown insert completed in ${result.totalElapsedMs}ms`);
|
|
20
27
|
lines.push(` Parse: ${result.parseElapsedMs}ms`);
|
|
21
28
|
lines.push(` Requests: ${result.totalRequests} total (${Object.entries(result.requestsByType)
|
|
22
29
|
.map(([k, v]) => `${v} ${k}`)
|
|
23
30
|
.join(', ')})`);
|
|
24
31
|
lines.push(` API calls: ${result.batchUpdate.totalApiCalls} batchUpdate calls in ${result.batchUpdate.totalElapsedMs}ms`);
|
|
25
|
-
const
|
|
26
|
-
if (phases
|
|
32
|
+
const phases = result.batchUpdate.phases;
|
|
33
|
+
if (phases?.delete?.requests > 0) {
|
|
27
34
|
lines.push(` Delete phase: ${phases.delete.requests} requests, ${phases.delete.apiCalls} calls, ${phases.delete.elapsedMs}ms`);
|
|
28
35
|
}
|
|
29
|
-
if (phases
|
|
36
|
+
if (phases?.insert?.requests > 0) {
|
|
30
37
|
lines.push(` Insert phase: ${phases.insert.requests} requests, ${phases.insert.apiCalls} calls, ${phases.insert.elapsedMs}ms`);
|
|
31
38
|
}
|
|
32
|
-
if (phases
|
|
39
|
+
if (phases?.format?.requests > 0) {
|
|
33
40
|
lines.push(` Format phase: ${phases.format.requests} requests, ${phases.format.apiCalls} calls, ${phases.format.elapsedMs}ms`);
|
|
34
41
|
}
|
|
35
42
|
return lines.join('\n');
|
|
@@ -61,12 +68,12 @@ export async function extractMarkdown(docs, documentId, options) {
|
|
|
61
68
|
return docsJsonToMarkdown({
|
|
62
69
|
body: targetTab.documentTab.body,
|
|
63
70
|
lists: targetTab.documentTab.lists,
|
|
64
|
-
});
|
|
71
|
+
}, options);
|
|
65
72
|
}
|
|
66
73
|
return docsJsonToMarkdown({
|
|
67
74
|
body: res.data.body,
|
|
68
75
|
lists: res.data.lists,
|
|
69
|
-
});
|
|
76
|
+
}, options);
|
|
70
77
|
}
|
|
71
78
|
// --- insertMarkdown ---
|
|
72
79
|
/**
|
|
@@ -85,6 +92,7 @@ export async function insertMarkdown(docs, documentId, markdown, options) {
|
|
|
85
92
|
const overallStart = performance.now();
|
|
86
93
|
const startIndex = options?.startIndex ?? 1;
|
|
87
94
|
const tabId = options?.tabId;
|
|
95
|
+
const writeControl = options?.writeControl;
|
|
88
96
|
// Fetch the document's default text style so we can explicitly set
|
|
89
97
|
// foreground color on inserted text (fixes issue #14 — text without
|
|
90
98
|
// explicit color shows "no color selected" in the Docs color picker).
|
|
@@ -109,7 +117,7 @@ export async function insertMarkdown(docs, documentId, markdown, options) {
|
|
|
109
117
|
...(options?.firstHeadingAsTitle && { firstHeadingAsTitle: true }),
|
|
110
118
|
...(defaultForegroundColor && { defaultForegroundColor }),
|
|
111
119
|
};
|
|
112
|
-
const requests = convertMarkdownToRequests(markdown, startIndex, tabId, conversionOptions);
|
|
120
|
+
const { requests, warnings } = convertMarkdownToRequests(markdown, startIndex, tabId, conversionOptions);
|
|
113
121
|
const parseElapsedMs = Math.round(performance.now() - parseStart);
|
|
114
122
|
// Count requests by type
|
|
115
123
|
const requestsByType = {};
|
|
@@ -119,6 +127,7 @@ export async function insertMarkdown(docs, documentId, markdown, options) {
|
|
|
119
127
|
}
|
|
120
128
|
if (requests.length === 0) {
|
|
121
129
|
return {
|
|
130
|
+
warnings,
|
|
122
131
|
totalRequests: 0,
|
|
123
132
|
requestsByType,
|
|
124
133
|
parseElapsedMs,
|
|
@@ -135,8 +144,9 @@ export async function insertMarkdown(docs, documentId, markdown, options) {
|
|
|
135
144
|
totalElapsedMs: Math.round(performance.now() - overallStart),
|
|
136
145
|
};
|
|
137
146
|
}
|
|
138
|
-
const batchUpdate = await executeBatchUpdateWithSplitting(docs, documentId, requests);
|
|
147
|
+
const batchUpdate = await executeBatchUpdateWithSplitting(docs, documentId, requests, undefined, writeControl);
|
|
139
148
|
return {
|
|
149
|
+
warnings,
|
|
140
150
|
totalRequests: requests.length,
|
|
141
151
|
requestsByType,
|
|
142
152
|
parseElapsedMs,
|