// ========================================== // ChatPane Kit — Markdown Renderer // ========================================== // Wraps window.marked + window.DOMPurify with fallback. // Independently importable utility. // // v0.37.10: Task list rendering, code block language extraction. // // Usage: // import { renderMarkdown } from './markdown.js'; // const html = renderMarkdown('**bold** text'); let _lastInput = ''; let _lastOutput = ''; const _escMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; function _escapeHtml(text) { return text.replace(/[&<>"']/g, c => _escMap[c]); } /** * Configure marked with task list support if available. */ let _configured = false; function _ensureConfigured() { if (_configured) return; _configured = true; if (typeof marked === 'undefined') return; marked.setOptions({ breaks: true, gfm: true, }); // Custom renderer for task list items const renderer = new marked.Renderer(); const origListItem = renderer.listitem?.bind(renderer); renderer.listitem = function(text, task, checked) { if (task) { const checkbox = checked ? '' : ''; return '
  • ' + checkbox + ' ' + text + '
  • \n'; } if (origListItem) return origListItem(text, task, checked); return '
  • ' + text + '
  • \n'; }; marked.use({ renderer }); } /** * Render markdown text to sanitized HTML. * Falls back to escaped plaintext when marked/DOMPurify unavailable. * * @param {string} text — raw markdown * @returns {string} sanitized HTML */ export function renderMarkdown(text) { if (!text) return ''; if (text === _lastInput) return _lastOutput; let result; if (typeof marked !== 'undefined' && typeof DOMPurify !== 'undefined') { _ensureConfigured(); result = DOMPurify.sanitize(marked.parse(text)); } else { result = _escapeHtml(text); } _lastInput = text; _lastOutput = result; return result; }