257 lines
6.7 KiB
JavaScript
257 lines
6.7 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Markdown to Google Docs Converter
|
|
*
|
|
* Converts markdown text to Google Docs API requests for proper formatting.
|
|
*
|
|
* Supports:
|
|
* - Headers (# ## ###)
|
|
* - Bold (**text**)
|
|
* - Links [text](url)
|
|
* - Bullet lists (- item)
|
|
* - Numbered lists (1. item)
|
|
* - Horizontal rules (---)
|
|
*/
|
|
|
|
/**
|
|
* Process inline formatting (bold, links) and return plain text + formatting requests
|
|
*/
|
|
function processInlineFormatting(text, startIndex) {
|
|
const requests = [];
|
|
let plainText = '';
|
|
let charIndex = startIndex;
|
|
let remaining = text;
|
|
|
|
while (remaining.length > 0) {
|
|
// Check for bold
|
|
const boldMatch = remaining.match(/^\*\*(.+?)\*\*/);
|
|
if (boldMatch) {
|
|
const boldText = boldMatch[1];
|
|
plainText += boldText;
|
|
requests.push({
|
|
updateTextStyle: {
|
|
range: {
|
|
startIndex: charIndex,
|
|
endIndex: charIndex + boldText.length,
|
|
},
|
|
textStyle: {
|
|
bold: true,
|
|
},
|
|
fields: 'bold',
|
|
},
|
|
});
|
|
charIndex += boldText.length;
|
|
remaining = remaining.substring(boldMatch[0].length);
|
|
continue;
|
|
}
|
|
|
|
// Check for links
|
|
const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
|
if (linkMatch) {
|
|
const linkText = linkMatch[1];
|
|
const linkUrl = linkMatch[2];
|
|
plainText += linkText;
|
|
requests.push({
|
|
updateTextStyle: {
|
|
range: {
|
|
startIndex: charIndex,
|
|
endIndex: charIndex + linkText.length,
|
|
},
|
|
textStyle: {
|
|
link: {
|
|
url: linkUrl,
|
|
},
|
|
},
|
|
fields: 'link',
|
|
},
|
|
});
|
|
charIndex += linkText.length;
|
|
remaining = remaining.substring(linkMatch[0].length);
|
|
continue;
|
|
}
|
|
|
|
// Regular character
|
|
plainText += remaining[0];
|
|
charIndex++;
|
|
remaining = remaining.substring(1);
|
|
}
|
|
|
|
return { plainText, requests, endIndex: charIndex };
|
|
}
|
|
|
|
/**
|
|
* Parse markdown and return plain text + formatting requests
|
|
* @param {string} markdown - The markdown text
|
|
* @returns {Object} { text: string, requests: Array }
|
|
*/
|
|
function parseMarkdown(markdown) {
|
|
const requests = [];
|
|
let plainText = '';
|
|
let currentIndex = 1; // Google Docs starts at index 1
|
|
|
|
const lines = markdown.split('\n');
|
|
|
|
for (let i = 0; i < lines.length; i++) {
|
|
let line = lines[i];
|
|
|
|
// Horizontal rule
|
|
if (line.match(/^---+$/)) {
|
|
const ruleLine = '─'.repeat(50) + '\n';
|
|
plainText += ruleLine;
|
|
currentIndex += ruleLine.length;
|
|
continue;
|
|
}
|
|
|
|
// Headers
|
|
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
if (headerMatch) {
|
|
const level = headerMatch[1].length;
|
|
const headerContent = headerMatch[2];
|
|
|
|
// Process inline formatting in header
|
|
const { plainText: processedText, requests: inlineRequests } =
|
|
processInlineFormatting(headerContent, currentIndex);
|
|
|
|
const fullLine = processedText + '\n';
|
|
|
|
const headingStyle = level === 1 ? 'HEADING_1' :
|
|
level === 2 ? 'HEADING_2' :
|
|
level === 3 ? 'HEADING_3' :
|
|
level === 4 ? 'HEADING_4' :
|
|
level === 5 ? 'HEADING_5' : 'HEADING_6';
|
|
|
|
requests.push({
|
|
updateParagraphStyle: {
|
|
range: {
|
|
startIndex: currentIndex,
|
|
endIndex: currentIndex + fullLine.length,
|
|
},
|
|
paragraphStyle: {
|
|
namedStyleType: headingStyle,
|
|
},
|
|
fields: 'namedStyleType',
|
|
},
|
|
});
|
|
|
|
requests.push(...inlineRequests);
|
|
plainText += fullLine;
|
|
currentIndex += fullLine.length;
|
|
continue;
|
|
}
|
|
|
|
// Bullet lists - process inline formatting within
|
|
const bulletMatch = line.match(/^[-*]\s+(.+)$/);
|
|
if (bulletMatch) {
|
|
const bulletContent = bulletMatch[1];
|
|
const { plainText: processedText, requests: inlineRequests } =
|
|
processInlineFormatting(bulletContent, currentIndex + 2); // +2 for "• "
|
|
|
|
const fullLine = '• ' + processedText + '\n';
|
|
plainText += fullLine;
|
|
requests.push(...inlineRequests);
|
|
currentIndex += fullLine.length;
|
|
continue;
|
|
}
|
|
|
|
// Numbered lists - process inline formatting within
|
|
const numberedMatch = line.match(/^(\d+)\.\s+(.+)$/);
|
|
if (numberedMatch) {
|
|
const num = numberedMatch[1];
|
|
const listContent = numberedMatch[2];
|
|
const prefix = num + '. ';
|
|
const { plainText: processedText, requests: inlineRequests } =
|
|
processInlineFormatting(listContent, currentIndex + prefix.length);
|
|
|
|
const fullLine = prefix + processedText + '\n';
|
|
plainText += fullLine;
|
|
requests.push(...inlineRequests);
|
|
currentIndex += fullLine.length;
|
|
continue;
|
|
}
|
|
|
|
// Regular line - process inline formatting
|
|
const { plainText: processedText, requests: inlineRequests } =
|
|
processInlineFormatting(line, currentIndex);
|
|
|
|
const fullLine = processedText + '\n';
|
|
plainText += fullLine;
|
|
requests.push(...inlineRequests);
|
|
currentIndex += fullLine.length;
|
|
}
|
|
|
|
return { text: plainText, requests };
|
|
}
|
|
|
|
/**
|
|
* Create a formatted Google Doc from markdown
|
|
* @param {Object} docs - Google Docs API instance
|
|
* @param {string} documentId - Document ID
|
|
* @param {string} markdown - Markdown content
|
|
*/
|
|
async function insertFormattedContent(docs, documentId, markdown) {
|
|
const { text, requests } = parseMarkdown(markdown);
|
|
|
|
// First, insert the plain text
|
|
await docs.documents.batchUpdate({
|
|
documentId,
|
|
requestBody: {
|
|
requests: [{
|
|
insertText: {
|
|
location: { index: 1 },
|
|
text,
|
|
},
|
|
}],
|
|
},
|
|
});
|
|
|
|
// Then apply formatting if there are any requests
|
|
if (requests.length > 0) {
|
|
await docs.documents.batchUpdate({
|
|
documentId,
|
|
requestBody: { requests },
|
|
});
|
|
}
|
|
|
|
return { textLength: text.length, formattingRequests: requests.length };
|
|
}
|
|
|
|
// CLI test
|
|
async function main() {
|
|
const testMarkdown = `# Main Title
|
|
|
|
## Section One
|
|
|
|
This is **bold text** and this is a [link](https://example.com).
|
|
|
|
- First bullet with [a link](https://test.com)
|
|
- Second bullet with **bold**
|
|
- Third bullet
|
|
|
|
### Subsection
|
|
|
|
1. Numbered item with [link](https://one.com)
|
|
2. Numbered item two
|
|
|
|
---
|
|
|
|
## Sources
|
|
|
|
- [Source One](https://source1.com)
|
|
- [Source Two](https://source2.com)
|
|
`;
|
|
|
|
const result = parseMarkdown(testMarkdown);
|
|
console.log('Plain text:');
|
|
console.log(result.text);
|
|
console.log('\nFormatting requests:', result.requests.length);
|
|
console.log('\nSample requests:');
|
|
result.requests.slice(0, 3).forEach(r => console.log(JSON.stringify(r, null, 2)));
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = { parseMarkdown, insertFormattedContent };
|