419 lines
20 KiB
JavaScript
419 lines
20 KiB
JavaScript
// Recognize HTML syntax (including quoted >), not clinical comparisons such as <1 month.
|
|
var HTML_TAG_PATTERN = /<!--[\s\S]*?-->|<\/[A-Za-z][A-Za-z0-9-]*\s*>|<[A-Za-z][A-Za-z0-9-]*(?:\s+[A-Za-z_:][A-Za-z0-9_.:-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`\x00-\x20]+))?)*\s*\/?>/;
|
|
|
|
// Mirrors the Open WebUI safeImageUrl allowlist: data:image/* and same-origin
|
|
// URLs only. External/protocol-relative image URLs never render as <img>;
|
|
// they fall back to their alt text instead.
|
|
export function safeImageUrl(src) {
|
|
var s = String(src == null ? '' : src).trim();
|
|
if (!s) return '';
|
|
if (/^data:image\/[a-z0-9.+-]+;/i.test(s)) return s;
|
|
if (/^blob:/i.test(s)) return s;
|
|
try {
|
|
var base = (typeof window !== 'undefined' && window.location && window.location.href) || 'https://synthetic.invalid/';
|
|
var url = new URL(s, base);
|
|
if (url.protocol === 'http:' || url.protocol === 'https:') return url.origin === new URL(base).origin ? s : '';
|
|
if (url.protocol === 'file:' || url.protocol === 'javascript:' || url.protocol === 'data:') return '';
|
|
return url.origin === new URL(base).origin ? s : ''; // relative paths resolve to our own origin only
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
export function renderAssistantMarkdown(md, sources, options) {
|
|
var opts = options || {};
|
|
var text = String(md || '');
|
|
var embedded = textProtection(text, 'html');
|
|
// Literal/embedded content bypasses prose, math and citation rewriting, then
|
|
// rejoins the parsed HTML before the single sanitization boundary.
|
|
text = protectBlocks(text, opts, function(code, lang) {
|
|
lang = String(lang || '').toLowerCase();
|
|
var html;
|
|
// Percent-encoding keeps Mermaid arrows intact through DOMPurify.
|
|
if (lang === 'mermaid') html = '<div class="assistant-mermaid" data-mermaid="' + escapeAttr(encodeURIComponent(code)) + '">Rendering graph...</div>';
|
|
else if (lang === 'chart' || lang === 'chartjs') html = '<canvas class="assistant-chart" data-chart="' + escapeAttr(code) + '"></canvas>';
|
|
else html = '<pre><code>' + escapeHtml(code) + '</code></pre>';
|
|
return embedded.hold(html);
|
|
}, true);
|
|
var inlineCode = textProtection(text, 'code');
|
|
text = text.replace(/(`+)[\s\S]*?\1/g, inlineCode.hold);
|
|
var links = textProtection(text, 'links');
|
|
text = text.replace(new RegExp(
|
|
/!?\[[^\]\n]*\]\([^\n]*?\)|https?:\/\/(?:\\[^\s]|[^\s<>|\\])+|<(?:https?:\/\/|mailto:)[^\s<>]+>|<[^\s<>@]+@[^\s<>@]+>/.source + '|' + HTML_TAG_PATTERN.source,
|
|
'gi'), function(match) {
|
|
if (match.charAt(0) === '!') {
|
|
var imageLink = match.match(/^!\[([^\]\n]*)\]\(([^\n]*?)\)$/);
|
|
if (imageLink && !safeImageUrl(imageLink[2])) return links.hold(escapeHtml(imageLink[1] || 'Image'), match);
|
|
}
|
|
return links.hold(match);
|
|
});
|
|
text = text.replace(/\\\[((?:\d+\s*,\s*)*\d+)\\\]/g, '[$1]');
|
|
text = renderLatexText(text, opts.katex, embedded.hold);
|
|
var limited = false;
|
|
text = normalizeMarkdownText(text, Object.assign({}, opts, {
|
|
onUnrecoverable: function(raw) {
|
|
limited = true;
|
|
raw = inlineCode.restore(links.restore(embedded.restore(raw, true)));
|
|
return embedded.hold('<pre>' + escapeHtml(raw) + '</pre>', raw);
|
|
}
|
|
}));
|
|
text = links.restore(normalizeAdjacentCitationClusters(text, sources || []));
|
|
|
|
text = inlineCode.restore(text);
|
|
var html;
|
|
if (opts.marked && typeof opts.marked.parse === 'function') {
|
|
html = opts.marked.parse(text, { breaks: true, gfm: true });
|
|
} else if (opts.markdownIt && typeof opts.markdownIt.render === 'function') {
|
|
html = opts.markdownIt.render(text);
|
|
} else {
|
|
html = fallbackMarkdown(text);
|
|
}
|
|
|
|
html = renderCitationLinks(html, sources || [], opts);
|
|
html = embedded.restore(html);
|
|
html = wrapTables(html);
|
|
if (limited) html += '<p class="assistant-render-notice" role="note">Could not safely recover this flattened table. Stored text is shown unchanged; missing cells or rows cannot be recovered.</p>';
|
|
if (opts.notice) html += '<p class="assistant-render-notice" role="note">' + escapeHtml(opts.notice) + '</p>';
|
|
|
|
return typeof opts.sanitize === 'function' ? opts.sanitize(html) : html;
|
|
}
|
|
|
|
function wrapTables(html) {
|
|
return String(html || '')
|
|
.replace(/<table(\s[^>]*)?>/g, '<div class="assistant-table-scroll" tabindex="0" role="region" aria-label="Scrollable table"><div class="assistant-table-actions"><button type="button" data-assistant-table-copy>Copy raw</button><button type="button" data-assistant-table-csv>Export CSV</button></div><table$1>')
|
|
.replace(/<\/table>/g, '</table></div>');
|
|
}
|
|
|
|
export function renderCitationLinks(html, sources, options) {
|
|
var opts = options || {};
|
|
return String(html || '').replace(new RegExp(
|
|
/<(pre|code|a)\b[^>]*>[\s\S]*?<\/\1>/.source + '|' + HTML_TAG_PATTERN.source + '|' + /\[((?:\d+\s*,\s*)*\d+)\]/.source,
|
|
'gi'), function (match, tag, cluster) {
|
|
if (!cluster) return match;
|
|
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 '<a class="assistant-cite" href="#' + escapeAttr(opts.citationTargetPrefix || 'assistant-source-') + n + '" data-source-number="' + n + '" title="' + escapeHtml(label) + '" aria-label="' + escapeAttr(label) + '">' + text + '</a>';
|
|
}).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(', ') + ']';
|
|
}
|
|
|
|
// Keep Markdown structure and literal syntax out of prose cleanups. The installed
|
|
// parser identifies complete blocks; the no-parser path only shields pipe lines.
|
|
function textProtection(text, kind) {
|
|
var prefix = '\uE000' + (kind || 'markdown') + ':';
|
|
while (text.indexOf(prefix) !== -1) prefix += '\uE000';
|
|
var values = [];
|
|
var originals = [];
|
|
var pattern = new RegExp(prefix + '(\\d+)\uE001', 'g');
|
|
return {
|
|
hold: function(value, original) {
|
|
originals.push(typeof original === 'string' ? original : value);
|
|
return prefix + (values.push(value) - 1) + '\uE001';
|
|
},
|
|
restore: function(value, raw) {
|
|
var previous;
|
|
do {
|
|
previous = value;
|
|
value = value.replace(pattern, function(_, i) { return (raw ? originals : values)[Number(i)]; });
|
|
} while (value !== previous);
|
|
return value;
|
|
}
|
|
};
|
|
}
|
|
|
|
function protectBlocks(text, opts, hold, codeOnly) {
|
|
if (opts.marked && typeof opts.marked.lexer === 'function') {
|
|
return opts.marked.lexer(text, { gfm: true }).map(function(token) {
|
|
if (codeOnly) return token.type === 'code' ? hold(token.text, token.lang) + '\n\n' : token.raw;
|
|
if (['space', 'paragraph', 'heading'].includes(token.type)) return token.raw;
|
|
return hold(token.type === 'table' ? normalizeTableSourceCitationCells(stripTrailingMarkerLine(token.raw)) :
|
|
token.type === 'code' || token.type === 'html' ? token.raw : stripTrailingMarkerLine(token.raw)) + '\n\n';
|
|
}).join('');
|
|
}
|
|
if (opts.markdownIt && typeof opts.markdownIt.parse === 'function') {
|
|
var lines = text.split('\n');
|
|
var ranges = opts.markdownIt.parse(text, {}).filter(function(token) {
|
|
return token.map && (codeOnly ? ['fence', 'code_block'] :
|
|
['table_open', 'fence', 'code_block', 'html_block', 'blockquote_open', 'bullet_list_open', 'ordered_list_open']).includes(token.type);
|
|
});
|
|
var end = 0;
|
|
ranges.forEach(function(token) {
|
|
if (token.map[0] < end) return;
|
|
end = token.map[1];
|
|
var raw = lines.slice(token.map[0], end).join('\n');
|
|
lines[token.map[0]] = codeOnly ? hold(token.content, token.info) : hold(token.type === 'table_open' ? normalizeTableSourceCitationCells(stripTrailingMarkerLine(raw)) :
|
|
['fence', 'code_block', 'html_block'].includes(token.type) ? raw : stripTrailingMarkerLine(raw));
|
|
for (var i = token.map[0] + 1; i < end; i++) lines[i] = '';
|
|
});
|
|
return lines.join('\n');
|
|
}
|
|
text = text.replace(/^ {0,3}(`{3,}|~{3,})([^\n]*)\n([\s\S]*?)(?:^ {0,3}\1[^\n]*(?:\n|$)|(?![\s\S]))/gm, function(raw, fence, lang, code) {
|
|
return codeOnly ? hold(code, lang.trim()) : hold(raw);
|
|
});
|
|
return text.replace(/(?:^(?: {4}|\t)[^\n]*(?:\n|$))+/gm, function(raw) {
|
|
return codeOnly ? hold(raw.replace(/^(?: {4}|\t)/gm, ''), '') : hold(raw);
|
|
});
|
|
}
|
|
|
|
function stripTrailingMarkerLine(text) {
|
|
return text.replace(/\n[ \t]*(?:\*\*|__|\*|_)[ \t]*\n?$/, '\n');
|
|
}
|
|
|
|
export function normalizeMarkdownText(text, options) {
|
|
var opts = options || {};
|
|
text = String(text || '').replace(/\r\n/g, '\n');
|
|
var protectedText = textProtection(text);
|
|
text = protectBlocks(text, opts, protectedText.hold);
|
|
// These spans must not become headings/lists or apparent legacy row boundaries.
|
|
text = text.replace(/(`+)[\s\S]*?\1|\$\$[\s\S]*?\$\$|\\\[[\s\S]*?\\\]|\\\([\s\S]*?\\\)|\$[^$\n]+\$|!?\[[^\]\n]*\]\([^\n]*?\)|https?:\/\/(?:\\[^\s]|[^\s<>|\\])+|\\\|/gi, protectedText.hold);
|
|
text = text.split('\n').map(function(line) {
|
|
if (!/\|[ \t]*:?-{2,}:?[ \t]*\|/.test(line)) return line;
|
|
var recovered = recoverLegacyTableLine(line);
|
|
if (recovered !== null) return recovered;
|
|
// A standalone delimiter row belongs to existing multiline Markdown.
|
|
if (/^[ \t]*\|?[ :|\t-]+$/.test(line)) return line;
|
|
var start = line.indexOf('|');
|
|
var raw = protectedText.restore(line.slice(start));
|
|
var literal = typeof opts.onUnrecoverable === 'function' ? opts.onUnrecoverable(raw) : raw;
|
|
return line.slice(0, start) + '\n\n' + protectedText.hold(literal);
|
|
}).join('\n');
|
|
text = protectBlocks(text, opts, protectedText.hold);
|
|
// Also leave incomplete/non-GFM pipe structures alone, rather than guessing.
|
|
text = text.replace(/^[^\n]*\|[^\n]*(?:\n[^\n]*\|[^\n]*)*/gm, function(block) {
|
|
return protectedText.hold(normalizeTableSourceCitationCells(block));
|
|
});
|
|
return protectedText.restore(normalizeProse(text));
|
|
}
|
|
|
|
// Only outer-pipe rows separated by "| |", with a header + alignment row and
|
|
// identical nonempty cell counts, survive whitespace collapse unambiguously.
|
|
// Empty cells, trailing prose, partial rows and mixed separators are not guessed.
|
|
function recoverLegacyTableLine(line) {
|
|
var start = line.indexOf('|');
|
|
var prefix = line.slice(0, start);
|
|
var candidate = line.slice(start).trimEnd();
|
|
if (start < 0 || !candidate.endsWith('|')) return null;
|
|
var rows = candidate.split(/(?<=\|)[ \t]+(?=\|)/);
|
|
if (rows.length < 3) return null;
|
|
var cells = rows.map(tableCells);
|
|
if (cells.some(function(row) { return row.length < 2 || row.some(function(cell) { return !cell; }); })) return null;
|
|
function separator(row) { return row.every(function(cell) { return /^:?-{2,}:?$/.test(cell); }); }
|
|
var output = [];
|
|
for (var i = 0; i < rows.length;) {
|
|
if (!cells[i + 1] || !separator(cells[i + 1]) || cells[i].length !== cells[i + 1].length || separator(cells[i])) return null;
|
|
var width = cells[i].length;
|
|
var end = i + 2;
|
|
while (end < rows.length && !(cells[end + 1] && separator(cells[end + 1]))) {
|
|
if (cells[end].length !== width || separator(cells[end])) return null;
|
|
end++;
|
|
}
|
|
if (end === i + 2) return null;
|
|
output.push(rows.slice(i, end).join('\n'));
|
|
i = end;
|
|
}
|
|
return (prefix.trim() ? prefix + '\n\n' : '') + output.join('\n\n');
|
|
}
|
|
|
|
function normalizeProse(text) {
|
|
return stripOrphanMarkdownMarkers(String(text || '')
|
|
.replace(/([^\n])\n+\s*(\[(?:\d+\s*,\s*)*\d+\])\s*(?:\n+\s*([.,;:]))?(?=\s*(?:\n|$))/g, '$1 $2$3')
|
|
.replace(/([.!?])\s*[-–—]\s+(\*\*)?/g, '$1\n- $2')
|
|
.replace(/(:)\s*[-–—]\s+(\*\*)?/g, '$1\n- $2')
|
|
.replace(/([.!?])\s+(\d+\.\s+[A-Z][A-Za-z][^\n]{0,80})/g, '$1\n$2')
|
|
.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')
|
|
.trim());
|
|
}
|
|
|
|
export function normalizeTableSourceCitationCells(text) {
|
|
var lines = String(text || '').split('\n');
|
|
for (var i = 0; i < lines.length - 1; i++) {
|
|
if (!/^\s*\|.*\|\s*$/.test(lines[i]) || !/^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(lines[i + 1])) continue;
|
|
var header = tableCells(lines[i]);
|
|
var sourceCols = [];
|
|
header.forEach(function(cell, idx) {
|
|
if (/^(?:source|sources|source\(s\)|citation|citations|citation\(s\)|reference|references|ref|refs)$/i.test(cell.trim())) sourceCols.push(idx);
|
|
});
|
|
if (!sourceCols.length) continue;
|
|
var j = i + 2;
|
|
while (j < lines.length && /^\s*\|.*\|\s*$/.test(lines[j])) {
|
|
lines[j] = rewriteTableCells(lines[j], sourceCols, function(cell) {
|
|
return normalizeBareCitationCell(cell);
|
|
});
|
|
j++;
|
|
}
|
|
i = j - 1;
|
|
}
|
|
return lines.join('\n');
|
|
}
|
|
|
|
function rewriteTableCells(line, indexes, fn) {
|
|
var trimmed = String(line || '').trim();
|
|
var leading = /^\|/.test(trimmed);
|
|
var trailing = /\|$/.test(trimmed);
|
|
var cells = tableCells(line);
|
|
indexes.forEach(function(idx) {
|
|
if (idx < cells.length) cells[idx] = fn(cells[idx]);
|
|
});
|
|
return (leading ? '| ' : '') + cells.join(' | ') + (trailing ? ' |' : '');
|
|
}
|
|
|
|
function normalizeBareCitationCell(cell) {
|
|
var text = String(cell || '').trim();
|
|
if (/^\[(?:\d+\s*,\s*)*\d+\]$/.test(text)) return text;
|
|
if (/^\d+(?:\s*,\s*\d+)*$/.test(text)) return '[' + text.replace(/\s*,\s*/g, ', ') + ']';
|
|
return text.replace(/(^|\s)(\d+(?:\s*,\s*\d+)+)(?=$|\s)/g, function(match, prefix, nums) {
|
|
return prefix + '[' + nums.replace(/\s*,\s*/g, ', ') + ']';
|
|
});
|
|
}
|
|
|
|
export function stripOrphanMarkdownMarkers(text) {
|
|
return String(text || '')
|
|
.replace(/\s*(?:\*\*|__|\*|_)\s*$/g, '')
|
|
.replace(/\s*(?:\*\*|__)?\s*(?:Figure|Fig\.)\s*(?:\*\*|__)?\s*$/i, '')
|
|
.trim();
|
|
}
|
|
|
|
export function fallbackMarkdown(text) {
|
|
var html = escapeHtml(text)
|
|
.replace(/^### (.*)$/gm, '<h3>$1</h3>')
|
|
.replace(/^## (.*)$/gm, '<h2>$1</h2>')
|
|
.replace(/^# (.*)$/gm, '<h1>$1</h1>')
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
|
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
|
.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
html = html.split(/\n{2,}/).map(function (block) {
|
|
if (/^\s*<h\d/.test(block) && block.indexOf('\n') !== -1) {
|
|
var parts = block.split('\n');
|
|
return parts.shift() + fallbackMarkdown(parts.join('\n'));
|
|
}
|
|
if (/^\s*<(h\d|ul|ol|pre|div)/.test(block)) return block;
|
|
var lines = block.split('\n');
|
|
if (isMarkdownTable(lines)) return renderFallbackTable(lines);
|
|
if (lines.some(function (l) { return /^\s*[-*] /.test(l); })) return renderMixedList(lines);
|
|
if (lines.every(function (l) { return /^\s*[-*] /.test(l) || !l.trim(); })) {
|
|
return '<ul>' + lines.filter(Boolean).map(function (l) { return '<li>' + l.replace(/^\s*[-*] /, '') + '</li>'; }).join('') + '</ul>';
|
|
}
|
|
if (lines.every(function (l) { return /^\s*\d+\. /.test(l) || !l.trim(); })) {
|
|
return '<ol>' + lines.filter(Boolean).map(function (l) { return '<li>' + l.replace(/^\s*\d+\. /, '') + '</li>'; }).join('') + '</ol>';
|
|
}
|
|
return '<p>' + block.replace(/\n/g, '<br>') + '</p>';
|
|
}).join('');
|
|
return html;
|
|
}
|
|
|
|
function renderMixedList(lines) {
|
|
var html = '';
|
|
var list = [];
|
|
var paragraph = [];
|
|
lines.forEach(function(line) {
|
|
if (/^\s*[-*] /.test(line)) {
|
|
if (paragraph.length) {
|
|
html += '<p>' + paragraph.join('<br>') + '</p>';
|
|
paragraph = [];
|
|
}
|
|
list.push(line.replace(/^\s*[-*] /, ''));
|
|
return;
|
|
}
|
|
if (list.length) {
|
|
html += '<ul>' + list.map(function(item) { return '<li>' + item + '</li>'; }).join('') + '</ul>';
|
|
list = [];
|
|
}
|
|
if (line.trim()) paragraph.push(line);
|
|
});
|
|
if (paragraph.length) html += '<p>' + paragraph.join('<br>') + '</p>';
|
|
if (list.length) html += '<ul>' + list.map(function(item) { return '<li>' + item + '</li>'; }).join('') + '</ul>';
|
|
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 '<table><thead><tr>' + header.map(function(cell) { return '<th>' + cell + '</th>'; }).join('') + '</tr></thead><tbody>' +
|
|
rows.map(function(row) { return '<tr>' + row.map(function(cell) { return '<td>' + cell + '</td>'; }).join('') + '</tr>'; }).join('') +
|
|
'</tbody></table>';
|
|
}
|
|
|
|
function tableCells(line) {
|
|
return String(line || '').trim().replace(/^\|/, '').replace(/\|$/, '').split(/(?<!\\)\|/).map(function(cell) { return cell.trim(); });
|
|
}
|
|
|
|
export function renderLatexText(text, katex, hold) {
|
|
if (!katex && !hold) return text;
|
|
function render(raw, expr, display) {
|
|
var html = katex ? safeKatex(katex, expr, display) : escapeHtml(raw);
|
|
return hold ? hold(html, raw) : html;
|
|
}
|
|
return String(text || '')
|
|
.replace(/\$\$([\s\S]+?)\$\$|\\\[([\s\S]+?)\\\]|\$([^$\n]+?)\$|\\\((.+?)\\\)|\\ce\{([^{}\n]*)\}|\\pu\{([^{}\n]*)\}/g, function(raw, dollars, brackets, inline, parens, ce, pu) {
|
|
var display = !!(dollars || brackets);
|
|
if (dollars || brackets) return render(raw, dollars || brackets, display);
|
|
if (inline !== undefined) return render(raw, inline, false);
|
|
if (parens !== undefined) return render(raw, parens, false);
|
|
if (ce !== undefined) return render(raw, '\\ce{' + ce + '}', false);
|
|
if (pu !== undefined) return render(raw, '\\pu{' + pu + '}', false);
|
|
return raw;
|
|
});
|
|
}
|
|
|
|
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, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
export function escapeAttr(s) {
|
|
return escapeHtml(s).replace(/'/g, ''');
|
|
}
|