/** * Lightweight AsciiDoc-to-HTML converter for news posts. * Handles: paragraphs, headings, bold, italic, monospace, links, lists, source blocks. */ import { escapeHtml, escapeAttr } from './escape'; import { sanitizeUrl } from './url-safety'; export function renderAsciiDocLite(text: string): string { if (!text) return ''; const output: string[] = []; let paragraphBuf: string[] = []; function flushParagraph() { if (paragraphBuf.length > 0) { output.push(`
${paragraphBuf.join(' ')}
`); paragraphBuf = []; } } const lines = text.split('\n'); let i = 0; let inSourceBlock = false; let sourceLines: string[] = []; while (i < lines.length) { const line = lines[i]; const trimmed = line.trim(); // Source block delimiter if (trimmed.match(/^-{4,}\s*$/) || trimmed.match(/^\.{4,}\s*$/)) { if (inSourceBlock) { output.push(`${sourceLines.map(escapeHtml).join('\n')}`);
sourceLines = [];
inSourceBlock = false;
} else {
flushParagraph();
inSourceBlock = true;
}
i++;
continue;
}
if (inSourceBlock) {
sourceLines.push(line);
i++;
continue;
}
// Empty line — paragraph break
if (!trimmed) {
flushParagraph();
i++;
continue;
}
// Headings
const headingMatch = trimmed.match(/^(={1,5})\s+(.+)$/);
if (headingMatch) {
flushParagraph();
const level = headingMatch[1].length + 1;
output.push(`$1');
// Bold: *text*
text = text.replace(/\*([^*]+)\*/g, '$1');
// Italic: _text_
text = text.replace(/_([^_]+)_/g, '$1');
return text;
}