export function renderAssistantMarkdown(md, sources, options) { var opts = options || {}; var text = stripOrphanMarkdownMarkers(repairTabSeparatedTables(repairMarkdownTables(normalizeMarkdownText(md)))); var codeBlocks = []; text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, function (_, lang, code) { var idx = codeBlocks.length; codeBlocks.push({ lang: (lang || '').toLowerCase(), code: code }); return '\n@@CODEBLOCK_' + idx + '@@\n'; }); text = stripSourcesSection(text); text = renderLatexText(text, opts.katex); text = normalizeAdjacentCitationClusters(text, sources || []); var html; if (opts.markdownIt && typeof opts.markdownIt.render === 'function') { html = opts.markdownIt.render(text); } else if (opts.marked && typeof opts.marked.parse === 'function') { html = opts.marked.parse(text, { breaks: true, gfm: true }); } else { html = fallbackMarkdown(text); } html = renderCitationLinks(html, sources || [], opts); html = html.replace(/@@CODEBLOCK_(\d+)@@/g, function (_, idx) { var block = codeBlocks[Number(idx)] || { lang: '', code: '' }; if (block.lang === 'mermaid') return '
Rendering graph...
'; if (block.lang === 'chart' || block.lang === 'chartjs') return ''; return '
' + escapeHtml(block.code) + '
'; }); return typeof opts.sanitize === 'function' ? opts.sanitize(html) : html; } export function renderCitationLinks(html, sources, options) { var opts = options || {}; return String(html || '').replace(/\[((?:\d+\s*,\s*)*\d+)\]/g, function (match, cluster) { var nums = cluster.split(',').map(function (n) { return Number(n.trim()); }).filter(function (n) { return Number.isInteger(n) && n > 0; }); if (!nums.length || nums.some(function (n) { return !sources[n - 1]; })) return match; return nums.map(function (n) { var source = sources[n - 1]; var title = source ? source.title || source.resource || 'Source' : 'Source'; var page = source && (source.page || source.page_number || source.pageNumber); var label = 'Source ' + n + ': ' + title + (page ? ', page ' + page : ''); var text = opts.citationLabel === 'number' ? String(n) : 'src'; return '' + text + ''; }).join(' '); }); } export function normalizeAdjacentCitationClusters(text, sources) { var available = Array.isArray(sources) ? sources : []; var normalized = String(text || '').replace(/((?:\[(?:\d+\s*,\s*)*\d+\]\s*)+)\[((?:\d+\s*,\s*)*\d+)(?=$|[.,;:])/g, function(match, completeClusters, trailingCluster) { var nums = citationNumbers(completeClusters).concat(parseCitationCluster(trailingCluster)); if (!allCitationsAvailable(nums, available)) return match; return formatCitationCluster(nums); }); return normalized.replace(/(?:\[(?:\d+\s*,\s*)*\d+\]\s*){2,}/g, function(match) { var nums = citationNumbers(match); if (!allCitationsAvailable(nums, available)) return match; return formatCitationCluster(nums); }); } function citationNumbers(text) { var clusters = String(text || '').match(/\[((?:\d+\s*,\s*)*\d+)\]/g) || []; var nums = []; clusters.forEach(function(cluster) { nums = nums.concat(parseCitationCluster(cluster.slice(1, -1))); }); return nums; } function parseCitationCluster(cluster) { return String(cluster || '').split(',').map(function(n) { return Number(n.trim()); }).filter(function(n) { return Number.isInteger(n) && n > 0; }); } function allCitationsAvailable(nums, sources) { return nums.length > 0 && nums.every(function(n) { return sources[n - 1]; }); } function formatCitationCluster(nums) { nums = Array.from(new Set(nums)).sort(function(a, b) { return a - b; }); return '[' + nums.join(', ') + ']'; } export function stripSourcesSection(text) { return String(text || '') .replace(/\s*(?:-{3,}\s*)?(?:#{1,6}\s*)?(?:Sources|References)\s*:?\s*[\s\S]*$/i, '') .trim(); } export function normalizeMarkdownText(text) { return stripOrphanMarkdownMarkers(String(text || '') .replace(/\r\n/g, '\n') .replace(/(\[(?:\d+\s*,\s*)*\d+\])\s*[-–—]\s*/g, '$1\n- ') .replace(/([.!?])\s*[-–—]\s+(\*\*)?/g, '$1\n- $2') .replace(/(:)\s*[-–—]\s+(\*\*)?/g, '$1\n- $2') .replace(/(\[(?:\d+\s*,\s*)*\d+\]\.)\s+(\d+\.\s+[A-Z][A-Za-z][^\n]{0,80})/g, '$1\n$2') .replace(/([.!?])\s+(\d+\.\s+[A-Z][A-Za-z][^\n]{0,80})/g, '$1\n$2') .replace(/(\[(?:\d+\s*,\s*)*\d+\])(?=\s*(?:[A-Z][A-Za-z]+\s+){1,4}(?:deficits?|distress|apnoea|apnea|vomiting|seizures?|signs?|symptoms?|criteria|indications?|risk|oxygen|saturation|dehydration|lethargy|toxicity)\b)/g, '$1\n') .replace(/([^\n])\s+(#{1,4}\s+)/g, '$1\n\n$2') .replace(/(#{1,4}\s+[^\n]+?)\s+(-\s+)/g, '$1\n\n$2') .replace(/(#{1,4}\s+[^\n]+)\n(-\s+)/g, '$1\n\n$2') .replace(/([^\n])\s+(-\s+(?:Mainstay|Medications|Hospitalization|Other therapies|Prevention|Short-acting|Anticholinergics|Systemic|Adjuncts|Long-term|Infants|Differentiating|Persistent|Severe|Need for|Inadequate)\b)/g, '$1\n$2') .replace(/([^\n])\s+(\|\s*Aspect\s*\|)/gi, '$1\n\n$2') .replace(/([^\n])\s+(\|\s*[-:]+\s*\|)/g, '$1\n$2') .replace(/\s+(\|\s*[^\n|]+\s*\|\s*[^\n|]+\s*\|)/g, '\n$1') .replace(/([^\n])\s+(-\s+[^\n])/g, '$1\n$2') .trim()); } export function stripOrphanMarkdownMarkers(text) { return String(text || '').replace(/(?:\n|^)\s*(?:\*\*|__|\*|_)\s*$/g, '').trim(); } export function repairTabSeparatedTables(text) { var lines = String(text || '').split('\n'); var out = []; var i = 0; while (i < lines.length) { if (!/\t/.test(lines[i]) || i + 1 >= lines.length || !/\t/.test(lines[i + 1])) { out.push(lines[i]); i += 1; continue; } if (out.length && out[out.length - 1].trim()) out.push(''); var tableLines = []; while (i < lines.length && shouldKeepTabTableLine(lines[i], tableLines.length)) { tableLines.push(lines[i]); i += 1; } var header = tableLines[0].split('\t').map(function(cell) { return cell.trim(); }); var rows = tabTableRows(tableLines.slice(1), header.length); out.push('| ' + header.join(' | ') + ' |'); out.push('| ' + header.map(function() { return '---'; }).join(' | ') + ' |'); rows.forEach(function(cells) { out.push('| ' + cells.map(cleanTableCell).join(' | ') + ' |'); }); } return out.join('\n'); } function shouldKeepTabTableLine(line, count) { if (/\t/.test(line)) return true; if (!count || !String(line || '').trim()) return false; if (/^\s*(#{1,6}\s+|References\b|Sources\b)/i.test(line)) return false; return /^\s*(?:\d+\s*){1,4}$/.test(line) || /^[^.!?]{1,90}$/.test(line); } function tabTableRows(lines, width) { var rows = []; var pending = null; lines.forEach(function(line) { var cells = String(line || '').split('\t').map(function(cell) { return cell.trim(); }); if (cells.length === 1 && pending) { pending[pending.length - 1] = [pending[pending.length - 1], cells[0]].filter(Boolean).join(' '); return; } if (pending && cells.length >= width && looksCitationCell(cells[0]) && cells[width - 1]) { pending[pending.length - 1] = [pending[pending.length - 1], cells[0]].filter(Boolean).join(' '); rows.push(normalizeTabRow(pending, width)); pending = [cells[width - 1]]; return; } if (pending && cells.length >= width) { rows.push(normalizeTabRow(pending, width)); pending = null; } if (cells.length >= width) { rows.push(normalizeTabRow(cells, width)); return; } if (pending) { pending[pending.length - 1] = [pending[pending.length - 1], cells.join(' ')].filter(Boolean).join(' '); } else { pending = cells; } }); if (pending) rows.push(normalizeTabRow(pending, width)); return rows; } function normalizeTabRow(cells, width) { cells = cells.slice(); while (cells.length < width) cells.push(''); if (width === 3 && looksCitationCell(cells[0]) && cells[2]) { return [cells[2], '', cells[0]]; } if (width === 3 && /\b(\d+\s*){1,4}\s+(?:Nonoperative|Surgical|Diagnostic|Recurrence|Initial|Further)\b/i.test(cells[1] || '')) { var split = splitCitationPrefix(cells[1]); cells[1] = split.citation; if (split.rest && cells[2]) cells[2] = split.rest + ': ' + cells[2]; } return cells.slice(0, width); } function splitCitationPrefix(text) { var m = String(text || '').match(/^((?:\d+\s*){1,4})\s+(.+)$/); return m ? { citation: m[1].trim(), rest: m[2].trim() } : { citation: text, rest: '' }; } function looksCitationCell(text) { return /^(?:\d+\s*){1,5}$/.test(String(text || '').trim()); } function cleanTableCell(cell) { return String(cell || '').replace(/(?:^|\s+)If you need[\s\S]*$/i, '').trim(); } export function repairMarkdownTables(text) { var lines = String(text || '').split('\n'); var repaired = []; var i = 0; while (i < lines.length) { if (isSplitTableHeader(lines, i)) { lines[i] = mergeSplitTableRows(lines[i], lines[i + 1]); lines.splice(i + 1, 1); } if (!isPotentialTableHeader(lines, i)) { repaired.push(lines[i]); i += 1; continue; } var expected = tableCells(lines[i]).length; if (repaired.length && repaired[repaired.length - 1].trim()) repaired.push(''); repaired.push(lines[i], normalizeTableSeparator(lines[i + 1], expected)); i += 2; while (i < lines.length) { var row = lines[i]; if (!row.trim()) { i += 1; continue; } if (!/^\s*\|/.test(row)) break; var merged = row; var guard = 0; while (tableCells(merged).length < expected && i + 1 < lines.length && /^\s*\|/.test(lines[i + 1]) && guard < 4) { merged = mergeSplitTableRows(merged, lines[i + 1]); i += 1; guard += 1; } repaired.push(trimTableCellsToExpected(merged, expected)); i += 1; } continue; } return repaired.join('\n'); } function isPotentialTableHeader(lines, idx) { return idx + 1 < lines.length && /^\s*\|.*\|\s*$/.test(lines[idx]) && /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(lines[idx + 1]); } function isSplitTableHeader(lines, idx) { return idx + 2 < lines.length && /^\s*\|.*\|?\s*$/.test(lines[idx]) && /^\s*\|.*\|\s*$/.test(lines[idx + 1]) && /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(lines[idx + 2]); } function normalizeTableSeparator(line, expected) { var cells = tableCells(line); while (cells.length < expected) cells.push('---'); return '| ' + cells.slice(0, expected).map(function(cell) { return /^:?-{2,}:?$/.test(cell) ? cell : '---'; }).join(' | ') + ' |'; } function mergeSplitTableRows(left, right) { var a = tableCells(left); var b = tableCells(right); if (!a.length) return right; a[a.length - 1] = (a[a.length - 1] + ' ' + (b.shift() || '')).trim(); return '| ' + a.concat(b).join(' | ') + ' |'; } function trimTableCellsToExpected(line, expected) { var cells = tableCells(line); while (cells.length < expected) cells.push(''); return '| ' + cells.slice(0, expected).join(' | ') + ' |'; } export function fallbackMarkdown(text) { var html = escapeHtml(text) .replace(/^### (.*)$/gm, '

$1

') .replace(/^## (.*)$/gm, '

$1

') .replace(/^# (.*)$/gm, '

$1

') .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/`([^`]+)`/g, '$1'); html = html.split(/\n{2,}/).map(function (block) { if (/^\s*' + lines.filter(Boolean).map(function (l) { return '
  • ' + l.replace(/^\s*[-*] /, '') + '
  • '; }).join('') + ''; } if (lines.every(function (l) { return /^\s*\d+\. /.test(l) || !l.trim(); })) { return '
      ' + lines.filter(Boolean).map(function (l) { return '
    1. ' + l.replace(/^\s*\d+\. /, '') + '
    2. '; }).join('') + '
    '; } return '

    ' + block.replace(/\n/g, '
    ') + '

    '; }).join(''); return html; } function renderMixedList(lines) { var html = ''; var list = []; var paragraph = []; lines.forEach(function(line) { if (/^\s*[-*] /.test(line)) { if (paragraph.length) { html += '

    ' + paragraph.join('
    ') + '

    '; paragraph = []; } list.push(line.replace(/^\s*[-*] /, '')); return; } if (list.length) { html += ''; list = []; } if (line.trim()) paragraph.push(line); }); if (paragraph.length) html += '

    ' + paragraph.join('
    ') + '

    '; if (list.length) html += ''; return html; } function isMarkdownTable(lines) { return lines.length >= 2 && /^\s*\|.*\|\s*$/.test(lines[0]) && /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(lines[1]); } function renderFallbackTable(lines) { var header = tableCells(lines[0]); var rows = lines.slice(2).filter(function(line) { return /^\s*\|.*\|\s*$/.test(line); }).map(tableCells); return '' + header.map(function(cell) { return ''; }).join('') + '' + rows.map(function(row) { return '' + row.map(function(cell) { return ''; }).join('') + ''; }).join('') + '
    ' + cell + '
    ' + cell + '
    '; } function tableCells(line) { return String(line || '').trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map(function(cell) { return cell.trim(); }); } export function renderLatexText(text, katex) { if (!katex) return text; return String(text || '') .replace(/\$\$([\s\S]+?)\$\$/g, function(_, expr) { return safeKatex(katex, expr, true); }) .replace(/\\\[([\s\S]+?)\\\]/g, function(_, expr) { return safeKatex(katex, expr, true); }) .replace(/\$([^$\n]+?)\$/g, function(_, expr) { return safeKatex(katex, expr, false); }) .replace(/\\\((.+?)\\\)/g, function(_, expr) { return safeKatex(katex, expr, false); }); } function safeKatex(katex, expr, displayMode) { try { return katex.renderToString(expr, { displayMode: displayMode, throwOnError: false }); } catch (e) { return escapeHtml(expr); } } export function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } export function escapeAttr(s) { return escapeHtml(s).replace(/'/g, '''); }