Last active
December 22, 2025 19:33
-
-
Save domkirby/99c6af000b5d6ef89ca1cf20a063f86b to your computer and use it in GitHub Desktop.
For use in n8n, converts Google Doc JSON to cleanish HTML
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //written by claude (use at your own risk inside a code node in N8N) | |
| const doc = $input.first().json; | |
| let html = ''; | |
| let listStack = []; // Track nested lists | |
| // Helper function to process text runs with formatting | |
| function processTextRuns(elements) { | |
| if (!elements) return ''; | |
| return elements.map(el => { | |
| if (!el.textRun) return ''; | |
| let text = el.textRun.content; | |
| const style = el.textRun.textStyle; | |
| if (!style) return text; | |
| // Check for hyperlinks first | |
| if (style.link && style.link.url) { | |
| text = `<a href="${style.link.url}">${text}</a>`; | |
| } | |
| // Apply formatting in order: bold, italic, underline | |
| if (style.bold) { | |
| text = `<strong>${text}</strong>`; | |
| } | |
| if (style.italic) { | |
| text = `<em>${text}</em>`; | |
| } | |
| if (style.underline) { | |
| text = `<u>${text}</u>`; | |
| } | |
| return text; | |
| }).join(''); | |
| } | |
| // Loop through the document content | |
| if (doc.body && doc.body.content) { | |
| for (const element of doc.body.content) { | |
| if (element.paragraph) { | |
| const paragraph = element.paragraph; | |
| // Extract text with formatting | |
| let text = processTextRuns(paragraph.elements).trim(); | |
| // Skip empty paragraphs | |
| if (!text) continue; | |
| // Check if this is a list item | |
| const bullet = paragraph.bullet; | |
| if (bullet) { | |
| // Determine list type (ordered vs unordered) | |
| const listId = bullet.listId; | |
| const nestingLevel = bullet.nestingLevel || 0; | |
| // Get list properties to determine if ordered or unordered | |
| const listProperties = doc.lists?.[listId]; | |
| const isOrdered = listProperties?.listProperties?.nestingLevels?.[nestingLevel]?.glyphType?.includes('DECIMAL') || | |
| listProperties?.listProperties?.nestingLevels?.[nestingLevel]?.glyphType?.includes('ALPHA'); | |
| const listTag = isOrdered ? 'ol' : 'ul'; | |
| // Close lists if we need to go back up levels | |
| while (listStack.length > nestingLevel + 1) { | |
| const closingTag = listStack.pop(); | |
| html += `</${closingTag}>\n`; | |
| } | |
| // Open new list if needed | |
| if (listStack.length === nestingLevel) { | |
| html += `<${listTag}>\n`; | |
| listStack.push(listTag); | |
| } | |
| // Add list item | |
| html += `<li>${text}</li>\n`; | |
| } else { | |
| // Close any open lists | |
| while (listStack.length > 0) { | |
| const closingTag = listStack.pop(); | |
| html += `</${closingTag}>\n`; | |
| } | |
| // Determine the style for non-list paragraphs | |
| const style = paragraph.paragraphStyle?.namedStyleType || 'NORMAL_TEXT'; | |
| // Convert to appropriate HTML tag | |
| if (style === 'HEADING_1') { | |
| html += `<h1>${text}</h1>\n`; | |
| } else if (style === 'HEADING_2') { | |
| html += `<h2>${text}</h2>\n`; | |
| } else if (style === 'HEADING_3') { | |
| html += `<h3>${text}</h3>\n`; | |
| } else if (style === 'HEADING_4') { | |
| html += `<h4>${text}</h4>\n`; | |
| } else if (style === 'HEADING_5') { | |
| html += `<h5>${text}</h5>\n`; | |
| } else if (style === 'HEADING_6') { | |
| html += `<h6>${text}</h6>\n`; | |
| } else { | |
| html += `<p>${text}</p>\n`; | |
| } | |
| } | |
| } | |
| } | |
| // Close any remaining open lists | |
| while (listStack.length > 0) { | |
| const closingTag = listStack.pop(); | |
| html += `</${closingTag}>\n`; | |
| } | |
| } | |
| return { | |
| json: { | |
| content: html.trim() | |
| } | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment