diff --git a/cli/src/osf.ts b/cli/src/osf.ts index 91ecb7d..b9f283d 100644 --- a/cli/src/osf.ts +++ b/cli/src/osf.ts @@ -45,6 +45,35 @@ function extractText(run: TextRun): string { return ''; } +// Helper to convert TextRun objects to Markdown +function textRunToMarkdown(run: TextRun): string { + if (typeof run === 'string') { + return run; + } + if ('type' in run) { + if (run.type === 'link') { + return `[${run.text}](${run.url})`; + } + if (run.type === 'image') { + return `![${run.alt}](${run.url})`; + } + } + if ('text' in run) { + let text = run.text; + if ((run as any).bold) { + text = `**${text}**`; + } + if ((run as any).italic) { + text = `*${text}*`; + } + if ((run as any).underline) { + text = `__${text}__`; + } + return text; + } + return ''; +} + // Helper function to convert OSFValue to CellValue function toSpreadsheetData(data: Record | undefined): SpreadsheetData { if (!data) return {}; @@ -600,12 +629,28 @@ function exportMarkdown(doc: OSFDocument): string { for (const block of slide.content) { if (block.type === 'unordered_list') { for (const item of block.items) { - const itemText = item.content.map(extractText).join(''); + const itemText = item.content.map(textRunToMarkdown).join(''); out.push(`- ${itemText}`); } + } else if (block.type === 'ordered_list') { + block.items.forEach((item, idx) => { + const itemText = item.content.map(textRunToMarkdown).join(''); + out.push(`${idx + 1}. ${itemText}`); + }); } else if (block.type === 'paragraph') { - const paragraphText = block.content.map(extractText).join(''); + const paragraphText = block.content.map(textRunToMarkdown).join(''); out.push(paragraphText); + } else if (block.type === 'blockquote') { + for (const para of block.content) { + const quoteText = para.content.map(textRunToMarkdown).join(''); + out.push(`> ${quoteText}`); + } + } else if (block.type === 'code') { + out.push(`\u0060\u0060\u0060${block.language || ''}`); + out.push(block.content); + out.push('\u0060\u0060\u0060'); + } else if (block.type === 'image') { + out.push(`![${block.alt}](${block.url})`); } } } diff --git a/cli/tests/cli.test.ts b/cli/tests/cli.test.ts index b0f85a2..25c550a 100644 --- a/cli/tests/cli.test.ts +++ b/cli/tests/cli.test.ts @@ -60,6 +60,22 @@ const FORMULA_TEST_OSF = `@meta { formula (2,4): "=C1+C2"; }`; +const ADVANCED_OSF = + '@slide {\n' + + ' title: "Comprehensive Slide";\n\n' + + ' This is a paragraph with **bold**, *italic*, and __underlined__ text.\n\n' + + ' - Unordered item 1\n' + + ' - Unordered item 2\n\n' + + ' 1. Ordered item 1\n' + + ' 2. Ordered item 2\n\n' + + ' > This is a blockquote.\n\n' + + ' ```javascript\n' + + ' console.log("Hello, World!");\n' + + ' ```\n\n' + + ' ![An image alt text](https://example.com/image.png)\n\n' + + ' [A link to somewhere](https://example.com)\n' + + '}'; + const INVALID_OSF = `@meta { title: "Unclosed Block" // Missing closing brace`; @@ -294,6 +310,27 @@ describe('OSF CLI', () => { expect(result).toContain('15 *(calc)*'); // (115-100)/100*100 = 15 }); + it('should export advanced content to Markdown', () => { + const advancedFile = join(TEST_FIXTURES_DIR, 'advanced.osf'); + writeFileSync(advancedFile, ADVANCED_OSF, 'utf8'); + try { + const result = execSync(`node "${CLI_PATH}" export "${advancedFile}"`, { + encoding: 'utf8', + }); + + expect(result).toContain('1. Ordered item 1'); + expect(result).toContain('> This is a blockquote.'); + expect(result).toContain('```javascript'); + expect(result).toContain('console.log("Hello, World!");'); + expect(result).toContain('![An image alt text](https://example.com/image.png)'); + expect(result).toContain('[A link to somewhere](https://example.com)'); + } finally { + if (existsSync(advancedFile)) { + unlinkSync(advancedFile); + } + } + }); + it('should export OSF with formulas to Markdown', () => { const formulaFile = join(TEST_FIXTURES_DIR, 'formula_test.osf'); writeFileSync(formulaFile, FORMULA_TEST_OSF, 'utf8');