Skip to content

Instantly share code, notes, and snippets.

@domkirby
Last active December 22, 2025 19:33
Show Gist options
  • Select an option

  • Save domkirby/99c6af000b5d6ef89ca1cf20a063f86b to your computer and use it in GitHub Desktop.

Select an option

Save domkirby/99c6af000b5d6ef89ca1cf20a063f86b to your computer and use it in GitHub Desktop.
For use in n8n, converts Google Doc JSON to cleanish HTML
//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