@kamilio/wire 0.1.2 → 0.1.3
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/bin/wire-mcp.mjs +1 -1
- package/dist/adapters/root.d.ts +5 -2
- package/dist/adapters/root.d.ts.map +1 -1
- package/dist/adapters/root.js +58 -30
- package/dist/adapters/root.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +6 -26
- package/dist/auth.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/executable.d.ts +3 -1
- package/dist/executable.d.ts.map +1 -1
- package/dist/executable.js +5 -4
- package/dist/executable.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +4 -0
- package/dist/hooks.js.map +1 -1
- package/node_modules/provider-asana/dist/asana-project.d.ts.map +1 -1
- package/node_modules/provider-asana/dist/asana-project.js +3 -0
- package/node_modules/provider-asana/dist/asana-project.js.map +1 -1
- package/node_modules/provider-asana/src/asana-project.ts +2 -0
- package/node_modules/provider-chatgpt/dist/chatgpt.d.ts.map +1 -1
- package/node_modules/provider-chatgpt/dist/chatgpt.js +2 -0
- package/node_modules/provider-chatgpt/dist/chatgpt.js.map +1 -1
- package/node_modules/provider-chatgpt/src/chatgpt.ts +1 -0
- package/node_modules/provider-gmail/dist/gmail.d.ts.map +1 -1
- package/node_modules/provider-gmail/dist/gmail.js +6 -2
- package/node_modules/provider-gmail/dist/gmail.js.map +1 -1
- package/node_modules/provider-gmail/src/gmail.ts +7 -2
- package/node_modules/provider-google-docs/dist/google-docs.d.ts.map +1 -1
- package/node_modules/provider-google-docs/dist/google-docs.js +370 -9
- package/node_modules/provider-google-docs/dist/google-docs.js.map +1 -1
- package/node_modules/provider-google-docs/dist/google-forms.d.ts +28 -0
- package/node_modules/provider-google-docs/dist/google-forms.d.ts.map +1 -0
- package/node_modules/provider-google-docs/dist/google-forms.js +191 -0
- package/node_modules/provider-google-docs/dist/google-forms.js.map +1 -0
- package/node_modules/provider-google-docs/dist/index.d.ts +26 -0
- package/node_modules/provider-google-docs/dist/index.d.ts.map +1 -1
- package/node_modules/provider-google-docs/dist/index.js +3 -1
- package/node_modules/provider-google-docs/dist/index.js.map +1 -1
- package/node_modules/provider-google-docs/src/google-docs.ts +339 -10
- package/node_modules/provider-google-docs/src/google-forms.ts +179 -0
- package/node_modules/provider-google-docs/src/index.ts +3 -1
- package/node_modules/provider-notion/dist/notion-sync.d.ts +1 -1
- package/node_modules/provider-notion/dist/notion-sync.d.ts.map +1 -1
- package/node_modules/provider-notion/dist/notion-sync.js +51 -29
- package/node_modules/provider-notion/dist/notion-sync.js.map +1 -1
- package/node_modules/provider-notion/src/notion-sync.ts +52 -26
- package/node_modules/provider-slack/dist/slack.d.ts.map +1 -1
- package/node_modules/provider-slack/dist/slack.js +14 -7
- package/node_modules/provider-slack/dist/slack.js.map +1 -1
- package/node_modules/provider-slack/src/slack.ts +14 -5
- package/node_modules/provider-zoom/dist/zoom-hub.d.ts.map +1 -1
- package/node_modules/provider-zoom/dist/zoom-hub.js +42 -35
- package/node_modules/provider-zoom/dist/zoom-hub.js.map +1 -1
- package/node_modules/provider-zoom/src/zoom-hub.ts +40 -35
- package/node_modules/wire-core/dist/core/model.d.ts +1 -1
- package/node_modules/wire-core/dist/core/model.d.ts.map +1 -1
- package/node_modules/wire-core/dist/index.d.ts +3 -2
- package/node_modules/wire-core/dist/index.d.ts.map +1 -1
- package/node_modules/wire-core/dist/index.js +2 -2
- package/node_modules/wire-core/dist/index.js.map +1 -1
- package/node_modules/wire-core/dist/operations.d.ts +3 -0
- package/node_modules/wire-core/dist/operations.d.ts.map +1 -1
- package/node_modules/wire-core/dist/operations.js +46 -7
- package/node_modules/wire-core/dist/operations.js.map +1 -1
- package/node_modules/wire-core/dist/ports.d.ts +1 -0
- package/node_modules/wire-core/dist/ports.d.ts.map +1 -1
- package/node_modules/wire-core/dist/runtime/chrome.d.ts.map +1 -1
- package/node_modules/wire-core/dist/runtime/chrome.js +27 -4
- package/node_modules/wire-core/dist/runtime/chrome.js.map +1 -1
- package/node_modules/wire-core/dist/runtime/cookies.d.ts.map +1 -1
- package/node_modules/wire-core/dist/runtime/cookies.js +7 -2
- package/node_modules/wire-core/dist/runtime/cookies.js.map +1 -1
- package/node_modules/wire-core/dist/runtime/node.d.ts.map +1 -1
- package/node_modules/wire-core/dist/runtime/node.js +4 -0
- package/node_modules/wire-core/dist/runtime/node.js.map +1 -1
- package/node_modules/wire-core/dist/storage/registry.d.ts.map +1 -1
- package/node_modules/wire-core/dist/storage/registry.js +95 -208
- package/node_modules/wire-core/dist/storage/registry.js.map +1 -1
- package/node_modules/wire-core/dist/storage/workspace.d.ts +3 -0
- package/node_modules/wire-core/dist/storage/workspace.d.ts.map +1 -1
- package/node_modules/wire-core/dist/storage/workspace.js +10 -1
- package/node_modules/wire-core/dist/storage/workspace.js.map +1 -1
- package/node_modules/wire-core/package.json +4 -0
- package/node_modules/wire-core/src/core/model.ts +1 -1
- package/node_modules/wire-core/src/index.ts +3 -2
- package/node_modules/wire-core/src/operations.ts +52 -8
- package/node_modules/wire-core/src/ports.ts +1 -0
- package/node_modules/wire-core/src/runtime/chrome.ts +30 -5
- package/node_modules/wire-core/src/runtime/cookies.ts +4 -2
- package/node_modules/wire-core/src/runtime/node.ts +4 -0
- package/node_modules/wire-core/src/storage/registry.ts +48 -15
- package/node_modules/wire-core/src/storage/workspace.ts +10 -1
- package/package.json +1 -1
- package/src/adapters/root.ts +51 -31
- package/src/auth.ts +7 -20
- package/src/cli.ts +2 -2
- package/src/executable.ts +5 -4
- package/src/hooks.ts +5 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineService } from "wire-core";
|
|
2
|
+
import { inflateRawSync } from "node:zlib";
|
|
2
3
|
import type { FetchedDocument, JsonObject, JsonValue, RuntimeCapabilities, Source } from "wire-core";
|
|
3
4
|
|
|
4
5
|
async function cookieHeader(runtime: RuntimeCapabilities): Promise<string> {
|
|
@@ -17,7 +18,7 @@ function filenameTitle(value: string, label: string): string {
|
|
|
17
18
|
const bare = /filename=([^;]+)/i.exec(value);
|
|
18
19
|
if (encoded === null && quoted === null && bare === null) throw new Error(`Google ${label} did not include a filename`);
|
|
19
20
|
const filename = encoded !== null ? decodeURIComponent(encoded[1]!) : quoted !== null ? quoted[1]! : bare![1]!.trim();
|
|
20
|
-
return filename.replace(/\.(csv|html|md|txt|xlsx)$/i, "");
|
|
21
|
+
return filename.replace(/\.(csv|html|md|pptx|txt|xlsx)$/i, "");
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
async function googleExport(runtime: RuntimeCapabilities, url: string, label: string): Promise<Readonly<{ title: string; text: string }>> {
|
|
@@ -29,6 +30,15 @@ async function googleExport(runtime: RuntimeCapabilities, url: string, label: st
|
|
|
29
30
|
return Object.freeze({ title: filenameTitle(disposition, label), text: await response.text() });
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
async function googleExportBytes(runtime: RuntimeCapabilities, url: string, label: string): Promise<Readonly<{ title: string; bytes: Uint8Array }>> {
|
|
34
|
+
const response = await runtime.http.request(url, { headers: { Cookie: await cookieHeader(runtime) } });
|
|
35
|
+
if (response.status === 401 || response.status === 403) throw cookieAuthenticationError();
|
|
36
|
+
if (!response.ok) throw new Error(`Google ${label} failed: HTTP ${response.status}`);
|
|
37
|
+
const disposition = response.headers.get("content-disposition");
|
|
38
|
+
if (disposition === null) throw cookieAuthenticationError();
|
|
39
|
+
return Object.freeze({ title: filenameTitle(disposition, label), bytes: new Uint8Array(await response.arrayBuffer()) });
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
async function googleText(runtime: RuntimeCapabilities, url: string, label: string): Promise<string> {
|
|
33
43
|
const response = await runtime.http.request(url, { headers: { Cookie: await cookieHeader(runtime) } });
|
|
34
44
|
if (response.status === 401 || response.status === 403) throw cookieAuthenticationError();
|
|
@@ -174,6 +184,268 @@ function documentTab(source: Source): string | null {
|
|
|
174
184
|
return typeof tab === "string" && tab !== "" ? tab : null;
|
|
175
185
|
}
|
|
176
186
|
|
|
187
|
+
type XmlChild = XmlNode | string;
|
|
188
|
+
type XmlNode = Readonly<{ name: string; attributes: ReadonlyMap<string, string>; children: readonly XmlChild[] }>;
|
|
189
|
+
|
|
190
|
+
function decodeXml(value: string): string {
|
|
191
|
+
return value.replace(/&(#x[0-9a-fA-F]+|#\d+|amp|lt|gt|quot|apos);/g, (_match, entity: string) => {
|
|
192
|
+
if (entity.startsWith("#x")) return String.fromCodePoint(Number.parseInt(entity.slice(2), 16));
|
|
193
|
+
if (entity.startsWith("#")) return String.fromCodePoint(Number.parseInt(entity.slice(1), 10));
|
|
194
|
+
if (entity === "amp") return "&";
|
|
195
|
+
if (entity === "lt") return "<";
|
|
196
|
+
if (entity === "gt") return ">";
|
|
197
|
+
if (entity === "quot") return "\"";
|
|
198
|
+
if (entity === "apos") return "'";
|
|
199
|
+
throw new Error(`PPTX XML unknown entity: ${entity}`);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function xmlTagEnd(xml: string, start: number): number {
|
|
204
|
+
let quote: string | null = null;
|
|
205
|
+
for (let index = start + 1; index < xml.length; index += 1) {
|
|
206
|
+
const char = xml[index]!;
|
|
207
|
+
if (quote === null && char === ">") return index;
|
|
208
|
+
if (quote === null && (char === "\"" || char === "'")) quote = char;
|
|
209
|
+
else if (quote === char) quote = null;
|
|
210
|
+
}
|
|
211
|
+
throw new Error("PPTX XML tag is unterminated");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseAttributes(value: string): ReadonlyMap<string, string> {
|
|
215
|
+
const attributes = new Map<string, string>();
|
|
216
|
+
let index = 0;
|
|
217
|
+
while (index < value.length) {
|
|
218
|
+
while (index < value.length && /\s/.test(value[index]!)) index += 1;
|
|
219
|
+
if (index >= value.length) break;
|
|
220
|
+
const nameStart = index;
|
|
221
|
+
while (index < value.length && !/[\s=]/.test(value[index]!)) index += 1;
|
|
222
|
+
const name = value.slice(nameStart, index);
|
|
223
|
+
while (index < value.length && /\s/.test(value[index]!)) index += 1;
|
|
224
|
+
if (value[index] !== "=") throw new Error(`PPTX XML attribute ${name} is invalid`);
|
|
225
|
+
index += 1;
|
|
226
|
+
while (index < value.length && /\s/.test(value[index]!)) index += 1;
|
|
227
|
+
const quote = value[index]!;
|
|
228
|
+
if (quote !== "\"" && quote !== "'") throw new Error(`PPTX XML attribute ${name} is unquoted`);
|
|
229
|
+
index += 1;
|
|
230
|
+
const textStart = index;
|
|
231
|
+
while (index < value.length && value[index] !== quote) index += 1;
|
|
232
|
+
attributes.set(name, decodeXml(value.slice(textStart, index)));
|
|
233
|
+
index += 1;
|
|
234
|
+
}
|
|
235
|
+
return attributes;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function parseXml(xml: string): XmlNode {
|
|
239
|
+
const root: { name: string; attributes: ReadonlyMap<string, string>; children: XmlChild[] } = { name: "#document", attributes: new Map(), children: [] };
|
|
240
|
+
const stack: { name: string; attributes: ReadonlyMap<string, string>; children: XmlChild[] }[] = [root];
|
|
241
|
+
let index = 0;
|
|
242
|
+
while (index < xml.length) {
|
|
243
|
+
if (xml.startsWith("<?", index)) {
|
|
244
|
+
index = xml.indexOf("?>", index) + 2;
|
|
245
|
+
} else if (xml.startsWith("<!--", index)) {
|
|
246
|
+
index = xml.indexOf("-->", index) + 3;
|
|
247
|
+
} else if (xml.startsWith("<![CDATA[", index)) {
|
|
248
|
+
const end = xml.indexOf("]]>", index);
|
|
249
|
+
stack[stack.length - 1]!.children.push(xml.slice(index + 9, end));
|
|
250
|
+
index = end + 3;
|
|
251
|
+
} else if (xml.startsWith("</", index)) {
|
|
252
|
+
const end = xmlTagEnd(xml, index);
|
|
253
|
+
const name = xml.slice(index + 2, end).trim();
|
|
254
|
+
const node = stack.pop()!;
|
|
255
|
+
if (node.name !== name) throw new Error(`PPTX XML closing tag mismatch: ${name}`);
|
|
256
|
+
stack[stack.length - 1]!.children.push(Object.freeze({ name: node.name, attributes: node.attributes, children: Object.freeze(node.children) }));
|
|
257
|
+
index = end + 1;
|
|
258
|
+
} else if (xml[index] === "<") {
|
|
259
|
+
const end = xmlTagEnd(xml, index);
|
|
260
|
+
const raw = xml.slice(index + 1, end).trim();
|
|
261
|
+
const selfClosing = raw.endsWith("/");
|
|
262
|
+
const tag = selfClosing ? raw.slice(0, -1).trimEnd() : raw;
|
|
263
|
+
const nameEnd = tag.search(/\s/);
|
|
264
|
+
const name = nameEnd === -1 ? tag : tag.slice(0, nameEnd);
|
|
265
|
+
const attributes = parseAttributes(nameEnd === -1 ? "" : tag.slice(nameEnd + 1));
|
|
266
|
+
if (selfClosing) stack[stack.length - 1]!.children.push(Object.freeze({ name, attributes, children: Object.freeze([]) }));
|
|
267
|
+
else stack.push({ name, attributes, children: [] });
|
|
268
|
+
index = end + 1;
|
|
269
|
+
} else {
|
|
270
|
+
const end = xml.indexOf("<", index);
|
|
271
|
+
const text = decodeXml(xml.slice(index, end === -1 ? xml.length : end));
|
|
272
|
+
if (text !== "") stack[stack.length - 1]!.children.push(text);
|
|
273
|
+
index = end === -1 ? xml.length : end;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (stack.length !== 1) throw new Error("PPTX XML document is unclosed");
|
|
277
|
+
return Object.freeze({ name: root.name, attributes: root.attributes, children: Object.freeze(root.children) });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isXmlNode(value: XmlChild): value is XmlNode {
|
|
281
|
+
return typeof value !== "string";
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function xmlChildren(node: XmlNode, name: string): readonly XmlNode[] {
|
|
285
|
+
return node.children.filter(isXmlNode).filter((child) => child.name === name);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function xmlDescendants(node: XmlNode, name: string): readonly XmlNode[] {
|
|
289
|
+
return node.children.filter(isXmlNode).flatMap((child) => child.name === name ? [child, ...xmlDescendants(child, name)] : xmlDescendants(child, name));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function xmlText(node: XmlNode): string {
|
|
293
|
+
return node.children.map((child) => typeof child === "string" ? child : xmlText(child)).join("");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function xmlAttribute(node: XmlNode, name: string): string {
|
|
297
|
+
const value = node.attributes.get(name);
|
|
298
|
+
if (value === undefined) throw new Error(`PPTX XML missing ${name}`);
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function zipEndOfCentralDirectory(bytes: Uint8Array): number {
|
|
303
|
+
for (let index = bytes.length - 22; index >= Math.max(0, bytes.length - 65557); index -= 1) {
|
|
304
|
+
if (bytes[index] === 0x50 && bytes[index + 1] === 0x4b && bytes[index + 2] === 0x05 && bytes[index + 3] === 0x06) return index;
|
|
305
|
+
}
|
|
306
|
+
throw new Error("PPTX ZIP missing central directory");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function unzipFiles(bytes: Uint8Array): ReadonlyMap<string, Uint8Array> {
|
|
310
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
311
|
+
const end = zipEndOfCentralDirectory(bytes);
|
|
312
|
+
const totalEntries = view.getUint16(end + 10, true);
|
|
313
|
+
let centralOffset = view.getUint32(end + 16, true);
|
|
314
|
+
const files = new Map<string, Uint8Array>();
|
|
315
|
+
const decoder = new TextDecoder();
|
|
316
|
+
for (let entryIndex = 0; entryIndex < totalEntries; entryIndex += 1) {
|
|
317
|
+
if (view.getUint32(centralOffset, true) !== 0x02014b50) throw new Error("PPTX ZIP central directory entry is invalid");
|
|
318
|
+
const method = view.getUint16(centralOffset + 10, true);
|
|
319
|
+
const compressedSize = view.getUint32(centralOffset + 20, true);
|
|
320
|
+
const nameLength = view.getUint16(centralOffset + 28, true);
|
|
321
|
+
const extraLength = view.getUint16(centralOffset + 30, true);
|
|
322
|
+
const commentLength = view.getUint16(centralOffset + 32, true);
|
|
323
|
+
const localOffset = view.getUint32(centralOffset + 42, true);
|
|
324
|
+
const name = decoder.decode(bytes.subarray(centralOffset + 46, centralOffset + 46 + nameLength));
|
|
325
|
+
if (view.getUint32(localOffset, true) !== 0x04034b50) throw new Error("PPTX ZIP local file entry is invalid");
|
|
326
|
+
const localNameLength = view.getUint16(localOffset + 26, true);
|
|
327
|
+
const localExtraLength = view.getUint16(localOffset + 28, true);
|
|
328
|
+
const dataStart = localOffset + 30 + localNameLength + localExtraLength;
|
|
329
|
+
const compressed = bytes.subarray(dataStart, dataStart + compressedSize);
|
|
330
|
+
if (method === 0) files.set(name, compressed);
|
|
331
|
+
else if (method === 8) files.set(name, inflateRawSync(compressed));
|
|
332
|
+
else throw new Error(`PPTX ZIP compression method is unsupported: ${method}`);
|
|
333
|
+
centralOffset += 46 + nameLength + extraLength + commentLength;
|
|
334
|
+
}
|
|
335
|
+
return files;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function zipText(files: ReadonlyMap<string, Uint8Array>, name: string): string {
|
|
339
|
+
const bytes = files.get(name);
|
|
340
|
+
if (bytes === undefined) throw new Error(`PPTX missing ${name}`);
|
|
341
|
+
return new TextDecoder().decode(bytes);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function relationshipTargets(xml: string): ReadonlyMap<string, string> {
|
|
345
|
+
const relationships = new Map<string, string>();
|
|
346
|
+
for (const relationship of xmlDescendants(parseXml(xml), "Relationship")) relationships.set(xmlAttribute(relationship, "Id"), xmlAttribute(relationship, "Target"));
|
|
347
|
+
return relationships;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function resolvePartPath(basePath: string, target: string): string {
|
|
351
|
+
const parts = basePath.split("/");
|
|
352
|
+
parts.pop();
|
|
353
|
+
for (const segment of target.split("/")) {
|
|
354
|
+
if (segment === "..") parts.pop();
|
|
355
|
+
else if (segment !== ".") parts.push(segment);
|
|
356
|
+
}
|
|
357
|
+
return parts.join("/");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function slidePartPaths(files: ReadonlyMap<string, Uint8Array>): readonly string[] {
|
|
361
|
+
const presentation = parseXml(zipText(files, "ppt/presentation.xml"));
|
|
362
|
+
const relationships = relationshipTargets(zipText(files, "ppt/_rels/presentation.xml.rels"));
|
|
363
|
+
const slides = xmlDescendants(presentation, "p:sldId");
|
|
364
|
+
if (slides.length === 0) throw new Error("PPTX presentation has no slides");
|
|
365
|
+
return slides.map((slide) => {
|
|
366
|
+
const id = xmlAttribute(slide, "r:id");
|
|
367
|
+
const target = relationships.get(id);
|
|
368
|
+
if (target === undefined) throw new Error(`PPTX presentation missing slide relationship ${id}`);
|
|
369
|
+
return resolvePartPath("ppt/presentation.xml", target);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function slideRelationshipsPath(slidePath: string): string {
|
|
374
|
+
const parts = slidePath.split("/");
|
|
375
|
+
const filename = parts.pop();
|
|
376
|
+
if (filename === undefined) throw new Error("PPTX slide path is invalid");
|
|
377
|
+
parts.push("_rels", `${filename}.rels`);
|
|
378
|
+
return parts.join("/");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function markdownSpan(value: string, left: string, right: string): string {
|
|
382
|
+
const leading = /^[ \t]*/.exec(value)![0];
|
|
383
|
+
const trailing = /[ \t]*$/.exec(value)![0];
|
|
384
|
+
const core = value.slice(leading.length, value.length - trailing.length);
|
|
385
|
+
return core === "" ? value : `${leading}${left}${core}${right}${trailing}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function linkMarkdown(value: string, target: string): string {
|
|
389
|
+
const leading = /^[ \t]*/.exec(value)![0];
|
|
390
|
+
const trailing = /[ \t]*$/.exec(value)![0];
|
|
391
|
+
const core = value.slice(leading.length, value.length - trailing.length);
|
|
392
|
+
return core === "" ? value : `${leading}[${core.replace(/[[\]\\]/g, "\\$&")}](${target.replace(/\)/g, "%29")})${trailing}`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function formattedRunMarkdown(run: XmlNode, relationships: ReadonlyMap<string, string>): string {
|
|
396
|
+
const text = xmlDescendants(run, "a:t").map(xmlText).join("");
|
|
397
|
+
const runProperties = xmlChildren(run, "a:rPr")[0];
|
|
398
|
+
if (runProperties === undefined) return text;
|
|
399
|
+
const hyperlink = xmlDescendants(runProperties, "a:hlinkClick")[0];
|
|
400
|
+
let markdown = text;
|
|
401
|
+
if (runProperties.attributes.get("u") !== undefined && runProperties.attributes.get("u") !== "none" && hyperlink === undefined) markdown = markdownSpan(markdown, "<u>", "</u>");
|
|
402
|
+
if (runProperties.attributes.get("strike") !== undefined && runProperties.attributes.get("strike") !== "noStrike") markdown = markdownSpan(markdown, "~~", "~~");
|
|
403
|
+
if (runProperties.attributes.get("b") === "1" && runProperties.attributes.get("i") === "1") markdown = markdownSpan(markdown, "***", "***");
|
|
404
|
+
else if (runProperties.attributes.get("b") === "1") markdown = markdownSpan(markdown, "**", "**");
|
|
405
|
+
else if (runProperties.attributes.get("i") === "1") markdown = markdownSpan(markdown, "_", "_");
|
|
406
|
+
if (hyperlink !== undefined) {
|
|
407
|
+
const id = xmlAttribute(hyperlink, "r:id");
|
|
408
|
+
const target = relationships.get(id);
|
|
409
|
+
if (target === undefined) throw new Error(`PPTX slide missing hyperlink relationship ${id}`);
|
|
410
|
+
markdown = linkMarkdown(markdown, target);
|
|
411
|
+
}
|
|
412
|
+
return markdown;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function paragraphMarkdown(paragraph: XmlNode, relationships: ReadonlyMap<string, string>): Readonly<{ text: string; list: "bullet" | "number" | null; level: number }> {
|
|
416
|
+
const pPr = xmlChildren(paragraph, "a:pPr")[0];
|
|
417
|
+
const list = pPr === undefined || xmlChildren(pPr, "a:buNone").length !== 0 ? null : xmlChildren(pPr, "a:buAutoNum").length !== 0 ? "number" : xmlChildren(pPr, "a:buChar").length !== 0 ? "bullet" : null;
|
|
418
|
+
const levelValue = pPr === undefined ? undefined : pPr.attributes.get("lvl");
|
|
419
|
+
const level = levelValue === undefined ? 0 : Number(levelValue);
|
|
420
|
+
const pieces: string[] = [];
|
|
421
|
+
for (const child of paragraph.children.filter(isXmlNode)) {
|
|
422
|
+
if (child.name === "a:br") pieces.push("\n");
|
|
423
|
+
if (child.name === "a:r" || child.name === "a:fld") pieces.push(formattedRunMarkdown(child, relationships));
|
|
424
|
+
}
|
|
425
|
+
return Object.freeze({ text: pieces.join("").replace(/[ \t]+$/gm, ""), list, level });
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function slideMarkdown(slide: XmlNode, relationships: ReadonlyMap<string, string>): string {
|
|
429
|
+
const paragraphs = xmlDescendants(slide, "a:p").map((paragraph) => paragraphMarkdown(paragraph, relationships)).filter((paragraph) => paragraph.text.trim() !== "");
|
|
430
|
+
const lines = paragraphs.map((paragraph, index) => {
|
|
431
|
+
if (index === 0 && paragraph.list === null) return `## ${paragraph.text.trim()}`;
|
|
432
|
+
if (paragraph.list === "bullet") return `${" ".repeat(paragraph.level)}- ${paragraph.text.trim()}`;
|
|
433
|
+
if (paragraph.list === "number") return `${" ".repeat(paragraph.level)}1. ${paragraph.text.trim()}`;
|
|
434
|
+
return paragraph.text.trim();
|
|
435
|
+
});
|
|
436
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function slidesMarkdown(bytes: Uint8Array): string {
|
|
440
|
+
const files = unzipFiles(bytes);
|
|
441
|
+
const slides = slidePartPaths(files).map((slidePath) => {
|
|
442
|
+
const slide = parseXml(zipText(files, slidePath));
|
|
443
|
+
const relationships = relationshipTargets(zipText(files, slideRelationshipsPath(slidePath)));
|
|
444
|
+
return slideMarkdown(slide, relationships);
|
|
445
|
+
});
|
|
446
|
+
return `${slides.join("\n\n---\n\n")}\n`;
|
|
447
|
+
}
|
|
448
|
+
|
|
177
449
|
function urlParam(url: URL, name: string): string | null {
|
|
178
450
|
const query = url.searchParams.get(name);
|
|
179
451
|
const hash = new URLSearchParams(url.hash.slice(1)).get(name);
|
|
@@ -284,20 +556,57 @@ function docMarkdownEqual(left: string, right: string): boolean {
|
|
|
284
556
|
return comparableDocMarkdown(left) === comparableDocMarkdown(right);
|
|
285
557
|
}
|
|
286
558
|
|
|
287
|
-
|
|
559
|
+
type TextEdit = Readonly<{ base: string; local: string; before: string; after: string; localAfter: string }>;
|
|
560
|
+
type MarkdownSpan = Readonly<{ start: number; end: number }>;
|
|
561
|
+
|
|
562
|
+
function textEdit(base: string, local: string): TextEdit {
|
|
288
563
|
let start = 0;
|
|
289
564
|
while (start < base.length && start < local.length && base[start] === local[start]) start += 1;
|
|
290
565
|
for (let window = 80; window >= 10; window -= 10) {
|
|
291
566
|
for (let baseEnd = start; baseEnd <= Math.min(base.length - window, start + 2000); baseEnd += 1) {
|
|
292
567
|
const anchor = base.slice(baseEnd, baseEnd + window);
|
|
293
568
|
const localEnd = local.indexOf(anchor, start);
|
|
294
|
-
if (localEnd !== -1 && localEnd <= start + 2000) return Object.freeze({ base: base.slice(start, baseEnd), local: local.slice(start, localEnd), before: base.slice(0, start), after: base.slice(baseEnd) });
|
|
569
|
+
if (localEnd !== -1 && localEnd <= start + 2000) return Object.freeze({ base: base.slice(start, baseEnd), local: local.slice(start, localEnd), before: base.slice(0, start), after: base.slice(baseEnd), localAfter: local.slice(localEnd) });
|
|
295
570
|
}
|
|
296
571
|
}
|
|
297
572
|
let baseEnd = base.length;
|
|
298
573
|
let localEnd = local.length;
|
|
299
574
|
while (baseEnd > start && localEnd > start && base[baseEnd - 1] === local[localEnd - 1]) { baseEnd -= 1; localEnd -= 1; }
|
|
300
|
-
return Object.freeze({ base: base.slice(start, baseEnd), local: local.slice(start, localEnd), before: base.slice(0, start), after: base.slice(baseEnd) });
|
|
575
|
+
return Object.freeze({ base: base.slice(start, baseEnd), local: local.slice(start, localEnd), before: base.slice(0, start), after: base.slice(baseEnd), localAfter: local.slice(localEnd) });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function textEdits(base: string, local: string): readonly TextEdit[] {
|
|
579
|
+
const edits: TextEdit[] = [];
|
|
580
|
+
let basePrefix = "";
|
|
581
|
+
let baseRest = base;
|
|
582
|
+
let localRest = local;
|
|
583
|
+
while (baseRest !== localRest) {
|
|
584
|
+
const edit = textEdit(baseRest, localRest);
|
|
585
|
+
const before = `${basePrefix}${edit.before}`;
|
|
586
|
+
edits.push(Object.freeze({ ...edit, before }));
|
|
587
|
+
basePrefix = base.slice(0, base.length - edit.after.length);
|
|
588
|
+
baseRest = edit.after;
|
|
589
|
+
localRest = edit.localAfter;
|
|
590
|
+
}
|
|
591
|
+
return edits;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function inlineMarkdownSpans(value: string): readonly MarkdownSpan[] {
|
|
595
|
+
const spans: MarkdownSpan[] = [];
|
|
596
|
+
for (const pattern of [/\[[^\]\n]+\]\([^)]+\)/g, /!\[[^\]\n]*\]\([^)]+\)/g, /`[^`\n]+`/g, /~~[^~\n]+~~/g, /\*\*[^*\n]+\*\*/g, /\*[^*\n]+\*/g, /(^|[^\w])__[^_\n]+__(?=[^\w]|$)/g, /(^|[^\w])_[^_\n]+_(?=[^\w]|$)/g, /<((?:https?|mailto):[^>\s]+)>/g, /<\/?(?:u|sup|sub|mark|span)(?:\s+[^>]*)?>/gi]) {
|
|
597
|
+
let match = pattern.exec(value);
|
|
598
|
+
while (match !== null) {
|
|
599
|
+
spans.push(Object.freeze({ start: match.index, end: match.index + match[0]!.length }));
|
|
600
|
+
match = pattern.exec(value);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return spans;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function editTouchesMarkdownSpan(edit: TextEdit, value: string, edited: string): boolean {
|
|
607
|
+
const start = edit.before.length;
|
|
608
|
+
const end = start + edited.length;
|
|
609
|
+
return inlineMarkdownSpans(value).some((span) => start === end ? start > span.start && start < span.end : start < span.end && end > span.start);
|
|
301
610
|
}
|
|
302
611
|
|
|
303
612
|
function context(value: string, side: "before" | "after", length: number): string {
|
|
@@ -368,15 +677,16 @@ function docTextRange(text: string, edit: Readonly<{ base: string; before: strin
|
|
|
368
677
|
}
|
|
369
678
|
|
|
370
679
|
async function uploadDocText(runtime: RuntimeCapabilities, documentId: string, key: string | null, tab: string, baseMarkdown: string, localMarkdown: string): Promise<void> {
|
|
371
|
-
const
|
|
680
|
+
const edits = textEdits(baseMarkdown, localMarkdown);
|
|
681
|
+
if (edits.some((edit) => editTouchesMarkdownSpan(edit, baseMarkdown, edit.base) || editTouchesMarkdownSpan(edit, localMarkdown, edit.local))) throw new Error("Google Docs sync cannot preserve formatting in edited text");
|
|
372
682
|
const editUrl = docEditUrl(documentId, key, tab);
|
|
373
683
|
const session = docSession(await googleText(runtime, editUrl, "Docs editor"));
|
|
374
|
-
const
|
|
684
|
+
const ranges = edits.map((edit) => ({ edit, range: docTextRange(session.text, edit) })).sort((left, right) => right.range.start - left.range.start);
|
|
375
685
|
const sid = runtime.clock.now().getTime().toString(16).padStart(16, "0").slice(-16);
|
|
376
|
-
const commands = [
|
|
686
|
+
const commands = ranges.flatMap(({ edit, range }) => [
|
|
377
687
|
...(range.start === range.end ? [] : [{ ty: "ds", si: range.start + 1, ei: range.end }]),
|
|
378
688
|
...(edit.local === "" ? [] : [{ ty: "is", ibi: range.start + 1, s: markdownText(edit.local) }]),
|
|
379
|
-
];
|
|
689
|
+
]);
|
|
380
690
|
const params = new URLSearchParams({ id: documentId, sid, vc: "1", c: "1", w: "1", flr: "0", smv: "2147483647", smb: "[2147483647, AAE=]", token: session.token, ouid: session.ouid, includes_info_params: "true", cros_files: "false", nded: "false", tab });
|
|
381
691
|
const body = new URLSearchParams({ rev: String(session.revision), bundles: JSON.stringify([{ commands, sid, reqId: 0 }]) });
|
|
382
692
|
const response = await runtime.http.request(`https://docs.google.com/document/d/${encodeURIComponent(documentId)}/save?${params}`, { method: "POST", headers: { Cookie: await cookieHeader(runtime), "content-type": "application/x-www-form-urlencoded;charset=UTF-8", origin: "https://docs.google.com", referer: editUrl }, body: body.toString() });
|
|
@@ -401,6 +711,12 @@ async function fetchGoogleDocument(runtime: RuntimeCapabilities, source: Source)
|
|
|
401
711
|
exportUrl = `https://docs.google.com/spreadsheets/d/${encodeURIComponent(documentId)}/export?${query}`;
|
|
402
712
|
label = "Sheets CSV export";
|
|
403
713
|
sheetGid = gid;
|
|
714
|
+
} else if (source.type === "presentation") {
|
|
715
|
+
const query = new URLSearchParams({ format: "pptx" });
|
|
716
|
+
const key = resourceKey(source);
|
|
717
|
+
if (key !== null) query.set("resourcekey", key);
|
|
718
|
+
exportUrl = `https://docs.google.com/presentation/d/${encodeURIComponent(documentId)}/export?${query}`;
|
|
719
|
+
label = "Slides PPTX export";
|
|
404
720
|
} else {
|
|
405
721
|
const query = new URLSearchParams({ format: "md" });
|
|
406
722
|
const key = resourceKey(source);
|
|
@@ -410,6 +726,11 @@ async function fetchGoogleDocument(runtime: RuntimeCapabilities, source: Source)
|
|
|
410
726
|
exportUrl = `https://docs.google.com/document/d/${encodeURIComponent(documentId)}/export?${query}`;
|
|
411
727
|
label = "Docs Markdown export";
|
|
412
728
|
}
|
|
729
|
+
if (source.type === "presentation") {
|
|
730
|
+
const exported = await googleExportBytes(runtime, exportUrl, label);
|
|
731
|
+
const markdown = slidesMarkdown(exported.bytes);
|
|
732
|
+
return Object.freeze({ title: exported.title, markdown, data: { document_id: documentId, title: exported.title, output_path: null, sheet_gid: sheetGid, markdown, rows: null, presentation: true } });
|
|
733
|
+
}
|
|
413
734
|
const exported = await googleExport(runtime, exportUrl, label);
|
|
414
735
|
const rows = source["sheet_gid"] === undefined ? null : parseCsv(exported.text);
|
|
415
736
|
const markdown = rows === null ? exported.text : rowsMarkdown(rows);
|
|
@@ -419,6 +740,13 @@ async function fetchGoogleDocument(runtime: RuntimeCapabilities, source: Source)
|
|
|
419
740
|
|
|
420
741
|
async function synchronizeGoogleDocument(runtime: RuntimeCapabilities, _url: string, source: Source, base: JsonValue, markdown: string): Promise<FetchedDocument> {
|
|
421
742
|
const remote = await fetchGoogleDocument(runtime, source);
|
|
743
|
+
if (source.type === "presentation") {
|
|
744
|
+
const baseMarkdown = objectValue(base)["markdown"];
|
|
745
|
+
if (typeof baseMarkdown !== "string") throw new Error("Google sync base must include markdown");
|
|
746
|
+
if (markdown === baseMarkdown || markdown === remote.markdown) return remote;
|
|
747
|
+
if (remote.markdown === baseMarkdown) throw new Error("Google Slides sync is download-only. Revert local edits or use `wire download <url>` for a fresh copy.");
|
|
748
|
+
throw new Error("Google Slides changed remotely and locally. Resolve the conflict in Google Slides or the local Markdown file before syncing again.");
|
|
749
|
+
}
|
|
422
750
|
const baseMarkdown = objectValue(base)["markdown"];
|
|
423
751
|
const label = source["sheet_gid"] === undefined ? "Google Docs" : "Google Sheets";
|
|
424
752
|
if (typeof baseMarkdown !== "string") throw new Error("Google sync base must include markdown");
|
|
@@ -448,12 +776,13 @@ async function synchronizeGoogleDocument(runtime: RuntimeCapabilities, _url: str
|
|
|
448
776
|
|
|
449
777
|
export const googleDocsService = defineService<RuntimeCapabilities>({
|
|
450
778
|
name: "google-docs",
|
|
451
|
-
matches: (url) => url.hostname === "docs.google.com" && /^\/(document|spreadsheets)(?:\/u\/\d+)?\/d\/[^/]+(?:\/.*)?$/.test(url.pathname),
|
|
779
|
+
matches: (url) => url.hostname === "docs.google.com" && /^\/(document|presentation|spreadsheets)(?:\/u\/\d+)?\/d\/[^/]+(?:\/.*)?$/.test(url.pathname),
|
|
452
780
|
parse: (url) => {
|
|
453
|
-
const match = /^\/(document|spreadsheets)(?:\/u\/\d+)?\/d\/([^/]+)(?:\/.*)?$/.exec(url.pathname)!;
|
|
781
|
+
const match = /^\/(document|presentation|spreadsheets)(?:\/u\/\d+)?\/d\/([^/]+)(?:\/.*)?$/.exec(url.pathname)!;
|
|
454
782
|
const documentId = match[2]!;
|
|
455
783
|
const key = urlParam(url, "resourcekey");
|
|
456
784
|
const resource_key = key === null || key === "" ? {} : { resource_key: key };
|
|
785
|
+
if (match[1] === "presentation") return Object.freeze({ service: "google-docs", identifier: documentId, type: "presentation", ...resource_key });
|
|
457
786
|
if (match[1] === "spreadsheets") {
|
|
458
787
|
const queryGid = url.searchParams.get("gid");
|
|
459
788
|
const hashGid = new URLSearchParams(url.hash.slice(1)).get("gid");
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { defineService } from "wire-core";
|
|
2
|
+
import type { FetchedDocument, JsonObject, JsonValue, RuntimeCapabilities, Source } from "wire-core";
|
|
3
|
+
|
|
4
|
+
type FormAnswer = Readonly<{ questionId: string; text: string }>;
|
|
5
|
+
|
|
6
|
+
function formId(url: URL): string {
|
|
7
|
+
return /^\/forms(?:\/u\/\d+)?\/d\/([^/]+)(?:\/.*)?$/.exec(url.pathname)![1]!;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function formUrl(id: string): string {
|
|
11
|
+
return `https://docs.google.com/forms/d/${encodeURIComponent(id)}/edit`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formsAuthError(): Error {
|
|
15
|
+
return new Error("Google Forms API authentication is missing or expired. Set GOOGLE_FORMS_TOKEN_FILE to an OAuth token with Forms scopes, then retry.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function apiDisabledError(body: JsonObject): Error | null {
|
|
19
|
+
const error = body["error"] as JsonObject | undefined;
|
|
20
|
+
if (error === undefined) return null;
|
|
21
|
+
const details = Array.isArray(error["details"]) ? error["details"] as readonly JsonObject[] : [];
|
|
22
|
+
const serviceDisabled = details.find((detail) => {
|
|
23
|
+
const metadata = detail["metadata"] as JsonObject | undefined;
|
|
24
|
+
return metadata !== undefined && metadata["service"] === "forms.googleapis.com";
|
|
25
|
+
});
|
|
26
|
+
if (serviceDisabled === undefined) return null;
|
|
27
|
+
const metadata = serviceDisabled["metadata"] as JsonObject;
|
|
28
|
+
const activationUrl = typeof metadata["activationUrl"] === "string" ? metadata["activationUrl"] : typeof metadata["containerInfo"] === "string" ? `https://console.developers.google.com/apis/api/forms.googleapis.com/overview?project=${metadata["containerInfo"]}` : "https://console.developers.google.com/apis/api/forms.googleapis.com/overview";
|
|
29
|
+
return new Error(`Google Forms API is disabled. Enable it at ${activationUrl} then retry.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function formsJson(runtime: RuntimeCapabilities, url: string, label: string): Promise<JsonObject> {
|
|
33
|
+
const token = await runtime.googleFormsTokens.load();
|
|
34
|
+
const response = await runtime.http.request(url, { headers: { authorization: `Bearer ${token.token}` } });
|
|
35
|
+
const text = await response.text();
|
|
36
|
+
const body = text === "" ? {} : JSON.parse(text) as JsonObject;
|
|
37
|
+
if (response.status === 401) throw formsAuthError();
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
const disabled = apiDisabledError(body);
|
|
40
|
+
if (disabled !== null) throw disabled;
|
|
41
|
+
const error = body["error"] as JsonObject | undefined;
|
|
42
|
+
const message = typeof error?.["message"] === "string" ? error["message"] : text;
|
|
43
|
+
if (response.status === 403 && /insufficient authentication scopes/i.test(message)) throw new Error("Google Forms API token is missing required scopes. Regenerate GOOGLE_FORMS_TOKEN_FILE with forms.body and forms.responses.readonly scopes.");
|
|
44
|
+
throw new Error(`Google Forms API ${label} failed: HTTP ${response.status}${message === "" ? "" : ` ${message}`}`);
|
|
45
|
+
}
|
|
46
|
+
return body;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function optionalString(value: JsonValue | undefined): string | null {
|
|
50
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function jsonObject(value: JsonValue): value is JsonObject {
|
|
54
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function optionsMarkdown(options: JsonValue | undefined): string | null {
|
|
58
|
+
if (!Array.isArray(options)) return null;
|
|
59
|
+
return options.map((option) => {
|
|
60
|
+
const object = option as JsonObject;
|
|
61
|
+
return object["value"] as string;
|
|
62
|
+
}).join(", ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function questionMarkdown(item: JsonObject): readonly string[] {
|
|
66
|
+
const lines = [`- ${item["title"] as string}`];
|
|
67
|
+
const questionItem = item["questionItem"] as JsonObject | undefined;
|
|
68
|
+
if (questionItem === undefined) return lines;
|
|
69
|
+
const question = questionItem["question"] as JsonObject;
|
|
70
|
+
lines.push(` - itemId: ${item["itemId"] as string}`);
|
|
71
|
+
lines.push(` - questionId: ${question["questionId"] as string}`);
|
|
72
|
+
if (question["required"] !== undefined) lines.push(` - required: ${String(question["required"])}`);
|
|
73
|
+
const textQuestion = question["textQuestion"] as JsonObject | undefined;
|
|
74
|
+
if (textQuestion !== undefined) lines.push(` - type: ${textQuestion["paragraph"] === true ? "paragraph" : "short_text"}`);
|
|
75
|
+
const choiceQuestion = question["choiceQuestion"] as JsonObject | undefined;
|
|
76
|
+
if (choiceQuestion !== undefined) {
|
|
77
|
+
lines.push(` - type: ${choiceQuestion["type"] as string}`);
|
|
78
|
+
const options = optionsMarkdown(choiceQuestion["options"]);
|
|
79
|
+
if (options !== null) lines.push(` - options: ${options}`);
|
|
80
|
+
}
|
|
81
|
+
const scaleQuestion = question["scaleQuestion"] as JsonObject | undefined;
|
|
82
|
+
if (scaleQuestion !== undefined) {
|
|
83
|
+
lines.push(" - type: scale");
|
|
84
|
+
lines.push(` - range: ${String(scaleQuestion["low"])} to ${String(scaleQuestion["high"])}`);
|
|
85
|
+
const lowLabel = optionalString(scaleQuestion["lowLabel"]);
|
|
86
|
+
const highLabel = optionalString(scaleQuestion["highLabel"]);
|
|
87
|
+
if (lowLabel !== null) lines.push(` - lowLabel: ${lowLabel}`);
|
|
88
|
+
if (highLabel !== null) lines.push(` - highLabel: ${highLabel}`);
|
|
89
|
+
}
|
|
90
|
+
if (question["dateQuestion"] !== undefined) lines.push(" - type: date");
|
|
91
|
+
if (question["timeQuestion"] !== undefined) lines.push(" - type: time");
|
|
92
|
+
const ratingQuestion = question["ratingQuestion"] as JsonObject | undefined;
|
|
93
|
+
if (ratingQuestion !== undefined) {
|
|
94
|
+
lines.push(" - type: rating");
|
|
95
|
+
lines.push(` - level: ${String(ratingQuestion["ratingScaleLevel"])}`);
|
|
96
|
+
lines.push(` - icon: ${String(ratingQuestion["iconType"])}`);
|
|
97
|
+
}
|
|
98
|
+
if (question["fileUploadQuestion"] !== undefined) lines.push(" - type: file_upload");
|
|
99
|
+
if (question["rowQuestion"] !== undefined) lines.push(" - type: row");
|
|
100
|
+
return lines;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function answerTexts(answer: JsonObject): readonly string[] {
|
|
104
|
+
const textAnswers = answer["textAnswers"] as JsonObject | undefined;
|
|
105
|
+
if (textAnswers !== undefined && Array.isArray(textAnswers["answers"])) return (textAnswers["answers"] as readonly JsonObject[]).map((value) => value["value"] as string);
|
|
106
|
+
return [JSON.stringify(answer)];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formAnswers(response: JsonObject): readonly FormAnswer[] {
|
|
110
|
+
const answers = response["answers"] as JsonObject | undefined;
|
|
111
|
+
if (answers === undefined) return [];
|
|
112
|
+
return Object.entries(answers).flatMap(([questionId, answer]) => answerTexts(answer as JsonObject).map((text) => ({ questionId, text })));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formMarkdown(form: JsonObject, responses: readonly JsonObject[]): string {
|
|
116
|
+
const info = form["info"] as JsonObject;
|
|
117
|
+
const title = info["title"] as string;
|
|
118
|
+
const id = form["formId"] as string;
|
|
119
|
+
const lines = [`# ${title}`, "", `- Form ID: ${id}`, `- Edit: ${formUrl(id)}`];
|
|
120
|
+
const responder = optionalString(form["responderUri"]);
|
|
121
|
+
if (responder !== null) lines.push(`- Responder: ${responder}`);
|
|
122
|
+
const publish = (form["publishSettings"] as JsonObject | undefined)?.["publishState"] as JsonObject | undefined;
|
|
123
|
+
if (publish !== undefined) {
|
|
124
|
+
lines.push(`- Published: ${String(publish["isPublished"])}`);
|
|
125
|
+
lines.push(`- Accepting responses: ${String(publish["isAcceptingResponses"])}`);
|
|
126
|
+
}
|
|
127
|
+
lines.push("", "## Items");
|
|
128
|
+
const items = form["items"] as readonly JsonObject[] | undefined;
|
|
129
|
+
if (items === undefined || items.length === 0) lines.push("No items.");
|
|
130
|
+
else for (const item of items) lines.push(...questionMarkdown(item));
|
|
131
|
+
lines.push("", "## Responses", "", `Response count: ${responses.length}`);
|
|
132
|
+
for (const response of responses) {
|
|
133
|
+
lines.push("", `### ${response["responseId"] as string}`);
|
|
134
|
+
const createTime = optionalString(response["createTime"]);
|
|
135
|
+
const submitted = optionalString(response["lastSubmittedTime"]);
|
|
136
|
+
if (createTime !== null) lines.push(`- Created: ${createTime}`);
|
|
137
|
+
if (submitted !== null) lines.push(`- Submitted: ${submitted}`);
|
|
138
|
+
for (const answer of formAnswers(response)) lines.push(`- ${answer.questionId}: ${answer.text}`);
|
|
139
|
+
}
|
|
140
|
+
return `${lines.join("\n")}\n`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function allResponses(runtime: RuntimeCapabilities, id: string): Promise<readonly JsonObject[]> {
|
|
144
|
+
const responses: JsonObject[] = [];
|
|
145
|
+
let pageToken: string | undefined;
|
|
146
|
+
for (;;) {
|
|
147
|
+
const url = new URL(`https://forms.googleapis.com/v1/forms/${encodeURIComponent(id)}/responses`);
|
|
148
|
+
if (pageToken !== undefined) url.searchParams.set("pageToken", pageToken);
|
|
149
|
+
const body = await formsJson(runtime, url.toString(), "responses.list");
|
|
150
|
+
if (Array.isArray(body["responses"])) responses.push(...body["responses"] as readonly JsonObject[]);
|
|
151
|
+
if (typeof body["nextPageToken"] !== "string") break;
|
|
152
|
+
pageToken = body["nextPageToken"];
|
|
153
|
+
}
|
|
154
|
+
return responses;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function fetchGoogleForm(runtime: RuntimeCapabilities, source: Source): Promise<FetchedDocument> {
|
|
158
|
+
const form = await formsJson(runtime, `https://forms.googleapis.com/v1/forms/${encodeURIComponent(source.identifier)}`, "forms.get");
|
|
159
|
+
const responses = await allResponses(runtime, source.identifier);
|
|
160
|
+
const info = form["info"] as JsonObject;
|
|
161
|
+
const markdown = formMarkdown(form, responses);
|
|
162
|
+
return Object.freeze({
|
|
163
|
+
title: info["title"] as string,
|
|
164
|
+
markdown,
|
|
165
|
+
data: { form, responses, markdown },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const googleFormsService = defineService<RuntimeCapabilities>({
|
|
170
|
+
name: "google-forms",
|
|
171
|
+
matches: (url) => url.hostname === "docs.google.com" && /^\/forms(?:\/u\/\d+)?\/d\/[^/]+(?:\/.*)?$/.test(url.pathname),
|
|
172
|
+
parse: (url) => Object.freeze({ service: "google-forms", identifier: formId(url), type: "form" }),
|
|
173
|
+
fetch: (runtime, _url, source) => fetchGoogleForm(runtime, source),
|
|
174
|
+
synchronize: async (runtime, _url, source, base, markdown) => {
|
|
175
|
+
const baseMarkdown = jsonObject(base) && typeof base["markdown"] === "string" ? base["markdown"] : null;
|
|
176
|
+
if (baseMarkdown !== null && markdown !== baseMarkdown) throw new Error("Google Forms sync is download-only. Revert local edits or use `wire download <url>` for a fresh copy.");
|
|
177
|
+
return fetchGoogleForm(runtime, source);
|
|
178
|
+
},
|
|
179
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { googleDocsService } from "./google-docs.js";
|
|
2
|
+
import { googleFormsService } from "./google-forms.js";
|
|
2
3
|
|
|
3
4
|
export { googleDocsService } from "./google-docs.js";
|
|
5
|
+
export { googleFormsService } from "./google-forms.js";
|
|
4
6
|
export const googleDocsProvider = Object.freeze({
|
|
5
|
-
services: [googleDocsService] as const,
|
|
7
|
+
services: [googleFormsService, googleDocsService] as const,
|
|
6
8
|
});
|
|
@@ -34,7 +34,7 @@ export type NotionDiffSummary = Readonly<{
|
|
|
34
34
|
}>;
|
|
35
35
|
type UserMentions = Readonly<Record<string, string>>;
|
|
36
36
|
export declare function parseNotionMarkdown(markdown: string): readonly NotionBlock[];
|
|
37
|
-
export declare function buildNotionCreateOperations(blocks: readonly NotionBlock[], pageId: string, spaceId: string, userId: string, currentTime: number, createId?: () => string): Readonly<{
|
|
37
|
+
export declare function buildNotionCreateOperations(blocks: readonly NotionBlock[], pageId: string, spaceId: string, userId: string, currentTime: number, createId?: () => string, initialTableColumnOrder?: readonly string[]): Readonly<{
|
|
38
38
|
operations: readonly NotionOperation[];
|
|
39
39
|
topLevelIds: readonly string[];
|
|
40
40
|
}>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"notion-sync.d.ts","sourceRoot":"","sources":["../src/notion-sync.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAElG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAErD,MAAM,MAAM,cAAc,GAAG,SAAS,EAAE,EAAE,CAAC;AAC3C,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,cAAc,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AACzI,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAA;CAAE,CAAC,CAAC;AACtG,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IAAE,OAAO,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,SAAS,SAAS,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,CAAC;AAC9H,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,UAAU,CAAA;CAAE,CAAC,CAAC;AACrI,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAChH,KAAK,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"notion-sync.d.ts","sourceRoot":"","sources":["../src/notion-sync.ts"],"names":[],"mappings":"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"}
|