diff --git a/components/google_docs/actions/append-image/append-image.mjs b/components/google_docs/actions/append-image/append-image.mjs index 5d68b97255407..0de004f40c654 100644 --- a/components/google_docs/actions/append-image/append-image.mjs +++ b/components/google_docs/actions/append-image/append-image.mjs @@ -4,7 +4,7 @@ export default { key: "google_docs-append-image", name: "Append Image to Document", description: "Appends an image to the end of a document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/request#InsertInlineImageRequest)", - version: "0.0.9", + version: "0.0.10", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/google_docs/actions/append-text/append-text.mjs b/components/google_docs/actions/append-text/append-text.mjs index 80d864eb903cd..650040cdcad69 100644 --- a/components/google_docs/actions/append-text/append-text.mjs +++ b/components/google_docs/actions/append-text/append-text.mjs @@ -4,7 +4,7 @@ export default { key: "google_docs-append-text", name: "Append Text", description: "Append text to an existing document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/request#InsertTextRequest)", - version: "0.1.8", + version: "0.1.9", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/google_docs/actions/create-document-from-template/create-document-from-template.mjs b/components/google_docs/actions/create-document-from-template/create-document-from-template.mjs index d612f0c5c94cf..a61f0e89d8019 100644 --- a/components/google_docs/actions/create-document-from-template/create-document-from-template.mjs +++ b/components/google_docs/actions/create-document-from-template/create-document-from-template.mjs @@ -13,7 +13,7 @@ export default { ...others, key: "google_docs-create-document-from-template", name: "Create New Document From Template", - version: "0.0.4", + version: "0.0.5", description, type, props: { diff --git a/components/google_docs/actions/create-document/create-document.mjs b/components/google_docs/actions/create-document/create-document.mjs index e323937bf47b3..35183cb82ea4f 100644 --- a/components/google_docs/actions/create-document/create-document.mjs +++ b/components/google_docs/actions/create-document/create-document.mjs @@ -4,7 +4,7 @@ export default { key: "google_docs-create-document", name: "Create a New Document", description: "Create a new document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/create)", - version: "0.1.8", + version: "0.2.0", annotations: { destructiveHint: false, openWorldHint: true, @@ -25,6 +25,12 @@ export default { ], optional: true, }, + useMarkdown: { + type: "boolean", + label: "Use Markdown Format", + description: "Enable markdown formatting support. When enabled, the text will be parsed as markdown and converted to Google Docs formatting (headings, bold, italic, lists, etc.)", + optional: true, + }, folderId: { propDefinition: [ googleDocs, @@ -39,14 +45,20 @@ export default { // Insert text if (this.text) { - await this.googleDocs.insertText(documentId, { - text: this.text, - }); + if (this.useMarkdown) { + // Use markdown formatting + await this.googleDocs.insertMarkdownText(documentId, this.text); + } else { + // Use plain text + await this.googleDocs.insertText(documentId, { + text: this.text, + }); + } } // Move file if (this.folderId) { - // Get file to get parents to remove + // Get file to get parents to remove const file = await this.googleDocs.getFile(documentId); // Move file, removing old parents, adding new parent folder diff --git a/components/google_docs/actions/find-document/find-document.mjs b/components/google_docs/actions/find-document/find-document.mjs index 269f35e495af6..cbc7c9ad99808 100644 --- a/components/google_docs/actions/find-document/find-document.mjs +++ b/components/google_docs/actions/find-document/find-document.mjs @@ -14,7 +14,7 @@ export default { ...others, key: "google_docs-find-document", name: "Find Document", - version: "0.0.3", + version: "0.0.4", description, type, props: { diff --git a/components/google_docs/actions/get-document/get-document.mjs b/components/google_docs/actions/get-document/get-document.mjs index f892e0c911d50..7189235985231 100644 --- a/components/google_docs/actions/get-document/get-document.mjs +++ b/components/google_docs/actions/get-document/get-document.mjs @@ -5,7 +5,7 @@ export default { key: "google_docs-get-document", name: "Get Document", description: "Get the contents of the latest version of a document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/get)", - version: "0.1.8", + version: "0.1.9", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/google_docs/actions/get-tab-content/get-tab-content.mjs b/components/google_docs/actions/get-tab-content/get-tab-content.mjs index 959f1b85a6b3e..341c24fbe6262 100644 --- a/components/google_docs/actions/get-tab-content/get-tab-content.mjs +++ b/components/google_docs/actions/get-tab-content/get-tab-content.mjs @@ -4,7 +4,7 @@ export default { key: "google_docs-get-tab-content", name: "Get Tab Content", description: "Get the content of a tab in a document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/get)", - version: "0.0.2", + version: "0.0.3", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/google_docs/actions/insert-page-break/insert-page-break.mjs b/components/google_docs/actions/insert-page-break/insert-page-break.mjs index 82e39db64ecfd..7c98940d5aa08 100644 --- a/components/google_docs/actions/insert-page-break/insert-page-break.mjs +++ b/components/google_docs/actions/insert-page-break/insert-page-break.mjs @@ -4,7 +4,7 @@ export default { key: "google_docs-insert-page-break", name: "Insert Page Break", description: "Insert a page break into a document. [See the documentation](https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/request#insertpagebreakrequest)", - version: "0.0.2", + version: "0.0.3", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/google_docs/actions/insert-table/insert-table.mjs b/components/google_docs/actions/insert-table/insert-table.mjs index 28605b9cb7dea..135a99797c94f 100644 --- a/components/google_docs/actions/insert-table/insert-table.mjs +++ b/components/google_docs/actions/insert-table/insert-table.mjs @@ -4,7 +4,7 @@ export default { key: "google_docs-insert-table", name: "Insert Table", description: "Insert a table into a document. [See the documentation](https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/request#inserttablerequest)", - version: "0.0.2", + version: "0.0.3", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/google_docs/actions/insert-text/insert-text.mjs b/components/google_docs/actions/insert-text/insert-text.mjs index 3ab738d608f71..f8466942cad95 100644 --- a/components/google_docs/actions/insert-text/insert-text.mjs +++ b/components/google_docs/actions/insert-text/insert-text.mjs @@ -4,7 +4,7 @@ export default { key: "google_docs-insert-text", name: "Insert Text", description: "Insert text into a document. [See the documentation](https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/request#inserttextrequest)", - version: "0.0.2", + version: "0.0.3", annotations: { destructiveHint: false, openWorldHint: true, diff --git a/components/google_docs/actions/replace-image/replace-image.mjs b/components/google_docs/actions/replace-image/replace-image.mjs index 67af49da5d13c..9a96518f7cbce 100644 --- a/components/google_docs/actions/replace-image/replace-image.mjs +++ b/components/google_docs/actions/replace-image/replace-image.mjs @@ -4,7 +4,7 @@ export default { key: "google_docs-replace-image", name: "Replace Image", description: "Replace image in a existing document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/request#ReplaceImageRequest)", - version: "0.0.9", + version: "0.0.10", annotations: { destructiveHint: true, openWorldHint: true, diff --git a/components/google_docs/actions/replace-text/replace-text.mjs b/components/google_docs/actions/replace-text/replace-text.mjs index cb793c3bb1d43..ddbb673b410cc 100644 --- a/components/google_docs/actions/replace-text/replace-text.mjs +++ b/components/google_docs/actions/replace-text/replace-text.mjs @@ -4,7 +4,7 @@ export default { key: "google_docs-replace-text", name: "Replace Text", description: "Replace all instances of matched text in an existing document. [See the documentation](https://developers.google.com/docs/api/reference/rest/v1/documents/request#ReplaceAllTextRequest)", - version: "0.0.9", + version: "0.0.10", annotations: { destructiveHint: true, openWorldHint: true, diff --git a/components/google_docs/common/markdown-parser.mjs b/components/google_docs/common/markdown-parser.mjs new file mode 100644 index 0000000000000..ab964b85d55a8 --- /dev/null +++ b/components/google_docs/common/markdown-parser.mjs @@ -0,0 +1,334 @@ +/** + * Markdown to Google Docs converter using markdown-it + * Converts markdown text to Google Docs API batch update requests + */ + +import MarkdownIt from "markdown-it"; + +/** + * Create a custom markdown-it instance configured for Google Docs conversion + * @returns {MarkdownIt} Configured markdown-it instance + */ +function createMarkdownParser() { + return new MarkdownIt({ + html: false, + breaks: true, + linkify: false, + }); +} + +/** + * Parse markdown and convert to Google Docs API requests + * @param {string} markdown - The markdown text to parse + * @returns {Object} Object with text and formatting requests + */ +function parseMarkdown(markdown) { + const md = createMarkdownParser(); + const tokens = md.parse(markdown, {}); + + const textContent = []; + const formattingRequests = []; + let currentIndex = 1; // Start after document header + + // Store state for heading and list detection + let nextIsHeading = false; + let headingLevel = 0; + let inBulletList = false; + let inOrderedList = false; + let listItemStartIndex = -1; + + tokens.forEach((token) => { + if (token.type === "heading_open") { + nextIsHeading = true; + headingLevel = parseInt(token.tag[1], 10); + } else if (token.type === "inline" && nextIsHeading) { + const textStartIndex = currentIndex; + const text = token.content; + + textContent.push(text); + currentIndex += text.length; + + // Apply heading style + formattingRequests.push({ + type: "updateParagraphStyle", + textRange: { + startIndex: textStartIndex, + endIndex: currentIndex, + }, + style: `HEADING_${headingLevel}`, + }); + + nextIsHeading = false; + } else if (token.type === "heading_close") { + // Add newline after heading + textContent.push("\n"); + currentIndex += 1; + } else if (token.type === "bullet_list_open") { + inBulletList = true; + } else if (token.type === "ordered_list_open") { + inOrderedList = true; + } else if (token.type === "list_item_open") { + listItemStartIndex = currentIndex; + } else if (token.type === "inline" && (inBulletList || inOrderedList)) { + const text = token.content; + + textContent.push(text); + currentIndex += text.length; + + // Apply bullet formatting + const bulletPreset = inOrderedList + ? "NUMBER_ASCENDING" + : "BULLET_DISC_CIRCLE_SQUARE"; + formattingRequests.push({ + type: "createParagraphBullets", + textRange: { + startIndex: listItemStartIndex, + endIndex: currentIndex, + }, + bulletPreset, + }); + } else if (token.type === "inline") { + // Regular paragraph text with potential formatting + const result = processInlineToken(token, textContent, formattingRequests, currentIndex); + currentIndex = result; + } else if (token.type === "paragraph_close") { + // Add newline after paragraph (but not after list items) + if (!inBulletList && !inOrderedList) { + textContent.push("\n"); + currentIndex += 1; + } + } else if (token.type === "list_item_close") { + // Add newline after list item + textContent.push("\n"); + currentIndex += 1; + } else if (token.type === "bullet_list_close") { + inBulletList = false; + // Add newline after list + textContent.push("\n"); + currentIndex += 1; + } else if (token.type === "ordered_list_close") { + inOrderedList = false; + // Add newline after list + textContent.push("\n"); + currentIndex += 1; + } + }); + + return { + text: textContent.join(""), + formattingRequests, + }; +} + +/** + * Process inline token content with formatting + * @returns {number} Updated currentIndex + */ +function processInlineToken(token, textContent, formattingRequests, startIndex) { + if (!token.children) { + return startIndex; + } + + let currentIndex = startIndex; + let isBold = false; + let isItalic = false; + let isCode = false; + + token.children.forEach((child) => { + if (child.type === "text") { + const textStartIndex = currentIndex; + const text = child.content; + textContent.push(text); + currentIndex += text.length; + + // Apply formatting if needed + if (isBold || isItalic || isCode) { + const formatting = { + bold: isBold, + italic: isItalic, + underline: false, + }; + + if (isCode) { + formatting.weightedFontFamily = { + fontFamily: "Courier New", + weight: 400, + }; + formatting.backgroundColor = { + color: { + rgbColor: { + red: 0.95, + green: 0.95, + blue: 0.95, + }, + }, + }; + } + + formattingRequests.push({ + type: "updateTextStyle", + textRange: { + startIndex: textStartIndex, + endIndex: currentIndex, + }, + formatting, + }); + } + } else if (child.type === "strong_open") { + isBold = true; + } else if (child.type === "strong_close") { + isBold = false; + } else if (child.type === "em_open") { + isItalic = true; + } else if (child.type === "em_close") { + isItalic = false; + } else if (child.type === "code_inline") { + const textStartIndex = currentIndex; + const text = child.content; + textContent.push(text); + currentIndex += text.length; + + formattingRequests.push({ + type: "updateTextStyle", + textRange: { + startIndex: textStartIndex, + endIndex: currentIndex, + }, + formatting: { + bold: false, + italic: false, + underline: false, + weightedFontFamily: { + fontFamily: "Courier New", + weight: 400, + }, + backgroundColor: { + color: { + rgbColor: { + red: 0.95, + green: 0.95, + blue: 0.95, + }, + }, + }, + }, + }); + } else if (child.type === "softbreak" || child.type === "hardbreak") { + textContent.push("\n"); + currentIndex += 1; + } + }); + + return currentIndex; +} + +/** + * Convert parsed markdown structure to Google Docs batchUpdate requests + * @param {Object} parseResult - Result from parseMarkdown() + * @returns {Array} Array of Google Docs API requests + */ +function convertToGoogleDocsRequests(parseResult) { + const { + text, + formattingRequests, + } = parseResult; + const batchRequests = []; + + // First, insert all the text + if (text) { + batchRequests.push({ + insertText: { + text, + location: { + index: 1, + }, + }, + }); + } + + // Then apply all formatting requests + formattingRequests.forEach((req) => { + const request = buildFormattingRequest(req); + if (request) { + batchRequests.push(request); + } + }); + + return batchRequests; +} + +/** + * Build formatting requests for Google Docs API + * @param {Object} req - Formatting request from parseMarkdown + * @returns {Object} Google Docs API request + */ +function buildFormattingRequest(req) { + switch (req.type) { + case "updateParagraphStyle": + return { + updateParagraphStyle: { + range: { + startIndex: req.textRange.startIndex, + endIndex: req.textRange.endIndex, + }, + paragraphStyle: { + namedStyleType: req.style, + }, + fields: "namedStyleType", + }, + }; + + case "updateTextStyle": { + const textStyle = { + bold: req.formatting.bold, + italic: req.formatting.italic, + underline: req.formatting.underline, + }; + const fields = [ + "bold", + "italic", + "underline", + ]; + + if (req.formatting.weightedFontFamily) { + textStyle.weightedFontFamily = req.formatting.weightedFontFamily; + fields.push("weightedFontFamily"); + } + + if (req.formatting.backgroundColor) { + textStyle.backgroundColor = req.formatting.backgroundColor; + fields.push("backgroundColor"); + } + + return { + updateTextStyle: { + range: { + startIndex: req.textRange.startIndex, + endIndex: req.textRange.endIndex, + }, + textStyle, + fields: fields.join(","), + }, + }; + } + + case "createParagraphBullets": + return { + createParagraphBullets: { + range: { + startIndex: req.textRange.startIndex, + endIndex: req.textRange.endIndex, + }, + bulletPreset: req.bulletPreset || "BULLET_DISC_CIRCLE_SQUARE", + }, + }; + + default: + return null; + } +} + +export default { + parseMarkdown, + convertToGoogleDocsRequests, +}; diff --git a/components/google_docs/google_docs.app.mjs b/components/google_docs/google_docs.app.mjs index 011d4e3338160..59c71566f602d 100644 --- a/components/google_docs/google_docs.app.mjs +++ b/components/google_docs/google_docs.app.mjs @@ -1,6 +1,7 @@ import docs from "@googleapis/docs"; import googleDrive from "@pipedream/google_drive"; import utils from "./common/utils.mjs"; +import markdownParser from "./common/markdown-parser.mjs"; export default { type: "app", @@ -179,5 +180,25 @@ export default { } return this.listFilesOptions(pageToken, request); }, + async insertMarkdownText(documentId, markdown) { + try { + const parseResult = markdownParser.parseMarkdown(markdown); + const batchRequests = markdownParser.convertToGoogleDocsRequests(parseResult); + + if (batchRequests.length === 0) { + return null; + } + + // Execute all requests in a single batch update + return this.docs().documents.batchUpdate({ + documentId, + requestBody: { + requests: batchRequests, + }, + }); + } catch (error) { + throw new Error(`Failed to insert markdown text: ${error.message}`); + } + }, }, }; diff --git a/components/google_docs/package.json b/components/google_docs/package.json index 8a96ecc7c8579..0ea9df7d34922 100644 --- a/components/google_docs/package.json +++ b/components/google_docs/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/google_docs", - "version": "0.5.1", + "version": "0.6.0", "description": "Pipedream Google_docs Components", "main": "google_docs.app.mjs", "keywords": [ @@ -14,6 +14,7 @@ }, "dependencies": { "@googleapis/docs": "^3.3.0", - "@pipedream/google_drive": "^1.1.1" + "@pipedream/google_drive": "^1.1.1", + "markdown-it": "^14.1.0" } } diff --git a/components/google_docs/sources/new-document-created/new-document-created.mjs b/components/google_docs/sources/new-document-created/new-document-created.mjs index 9789faa28a69e..346dbe60cf1b4 100644 --- a/components/google_docs/sources/new-document-created/new-document-created.mjs +++ b/components/google_docs/sources/new-document-created/new-document-created.mjs @@ -5,7 +5,7 @@ export default { key: "google_docs-new-document-created", name: "New Document Created (Instant)", description: "Emit new event when a new document is created in Google Docs. [See the documentation](https://developers.google.com/drive/api/reference/rest/v3/changes/watch)", - version: "0.0.5", + version: "0.0.6", type: "source", dedupe: "unique", methods: { diff --git a/components/google_docs/sources/new-or-updated-document/new-or-updated-document.mjs b/components/google_docs/sources/new-or-updated-document/new-or-updated-document.mjs index b41cd1785f38e..07b91d10dca7c 100644 --- a/components/google_docs/sources/new-or-updated-document/new-or-updated-document.mjs +++ b/components/google_docs/sources/new-or-updated-document/new-or-updated-document.mjs @@ -9,7 +9,7 @@ export default { key: "google_docs-new-or-updated-document", name: "New or Updated Document (Instant)", description: "Emit new event when a document is created or updated in Google Docs. [See the documentation](https://developers.google.com/drive/api/reference/rest/v3/changes/watch)", - version: "0.0.5", + version: "0.0.6", type: "source", dedupe: "unique", methods: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6210bd98ab4ef..5806c5d426d82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6111,6 +6111,9 @@ importers: '@pipedream/google_drive': specifier: ^1.1.1 version: 1.1.1 + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 components/google_drive: dependencies: @@ -40129,8 +40132,6 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: