function escapeHtml(text) { return text .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function sanitizeHref(url) { const trimmed = url.trim(); if (/^https?:\/\//i.test(trimmed)) { return escapeHtml(trimmed); } return null; } function sanitizeLanguage(language) { const trimmed = language.trim(); if (!trimmed) return 'text'; if (/^[a-zA-Z0-9#+.-]+$/.test(trimmed)) { return escapeHtml(trimmed); } return 'text'; } function parseOrgInline(text) { const placeholders = []; let working = escapeHtml(text); const stash = (html) => { const key = `\x00ORG${placeholders.length}PH\x00`; placeholders.push(html); return key; }; working = working.replace( /\[\[([^\]]+)\]\[([^\]]+)\]\]/g, (_, url, label) => { const href = sanitizeHref(url); if (!href) return escapeHtml(`[[${url}][${label}]]`); return stash( `${escapeHtml(label)}`, ); }, ); working = working.replace(/\[\[([^\]]+)\]\]/g, (_, url) => { const href = sanitizeHref(url); if (!href) return escapeHtml(`[[${url}]]`); return stash(`${href}`); }); working = working.replace(/~([^~]+)~/g, (_, code) => `${code}`); working = working.replace(/=([^=]+)=/g, (_, code) => `${code}`); working = working.replace(/\*([^*]+)\*/g, (_, bold) => `${bold}`); working = working.replace(/\/([^/]+)\//g, (_, italic) => `${italic}`); placeholders.forEach((html, index) => { working = working.replace(`\x00ORG${index}PH\x00`, html); }); return working; } function parseHeading(line) { const match = line.match(/^(\*{1,4})\s+(?:(TODO|DONE)\s+)?(.+)$/); if (!match) return null; const level = Math.min(match[1].length, 4); const keyword = match[2]; const title = match[3].trim(); const tag = `h${level}`; let badge = ''; if (keyword === 'TODO') { badge = 'TODO '; } else if (keyword === 'DONE') { badge = 'DONE '; } return `<${tag}>${badge}${parseOrgInline(title)}`; } function parseListBlock(lines) { const items = lines .filter((line) => /^[-+]\s+/.test(line)) .map((line) => `
  • ${parseOrgInline(line.replace(/^[-+]\s+/, ''))}
  • `); if (items.length === 0) return ''; return ``; } function parseParagraphBlock(lines) { const html = lines.map((line) => parseOrgInline(line)).join('
    '); return `

    ${html}

    `; } function extractSpecialBlocks(input) { const lines = input.replace(/\r\n/g, '\n').split('\n'); const blocks = []; let i = 0; while (i < lines.length) { const srcMatch = lines[i].match(/^#\+BEGIN_SRC\s*(\S*)?\s*$/i); const quoteMatch = lines[i].match(/^#\+BEGIN_QUOTE\s*$/i); if (srcMatch) { const language = sanitizeLanguage((srcMatch[1] || '').trim()); const codeLines = []; i += 1; while (i < lines.length && !/^#\+END_SRC\s*$/i.test(lines[i])) { codeLines.push(lines[i]); i += 1; } if (i < lines.length) i += 1; const langClass = ` class="language-${language}"`; blocks.push({ type: 'html', html: `
    ${escapeHtml(codeLines.join('\n'))}
    `, }); continue; } if (quoteMatch) { const quoteLines = []; i += 1; while (i < lines.length && !/^#\+END_QUOTE\s*$/i.test(lines[i])) { quoteLines.push(lines[i]); i += 1; } if (i < lines.length) i += 1; const inner = quoteLines.length === 0 ? '' : quoteLines.map((l) => parseOrgInline(l)).join('
    '); blocks.push({ type: 'html', html: `
    ${inner}
    `, }); continue; } const textLines = []; while ( i < lines.length && !/^#\+BEGIN_SRC/i.test(lines[i]) && !/^#\+BEGIN_QUOTE/i.test(lines[i]) ) { textLines.push(lines[i]); i += 1; } if (textLines.length > 0) { blocks.push({ type: 'text', lines: textLines }); } } return blocks; } function parseTextBlock(lines) { const chunks = []; let current = []; const flush = () => { if (current.length === 0) return; chunks.push([...current]); current = []; }; for (const line of lines) { if (line.trim() === '') { flush(); } else { current.push(line); } } flush(); return chunks .map((chunk) => { if (chunk.every((line) => /^[-+]\s+/.test(line))) { return parseListBlock(chunk); } if (chunk.length === 1) { const heading = parseHeading(chunk[0]); if (heading) return heading; if (/^[-+]\s+/.test(chunk[0])) return parseListBlock(chunk); } const heading = parseHeading(chunk[0]); if (heading) { return heading + (chunk.length > 1 ? parseParagraphBlock(chunk.slice(1)) : ''); } return parseParagraphBlock(chunk); }) .join(''); } export function parseOrgMode(input) { if (!input || !input.trim()) { return ''; } const blocks = extractSpecialBlocks(input); const htmlParts = []; for (const block of blocks) { if (block.type === 'html') { htmlParts.push(block.html); } else { htmlParts.push(parseTextBlock(block.lines)); } } return htmlParts.join(''); }