// Requires importing: // // // // // // // // // // // // // // // // // // // // // // // When served with lollms, just use // // // // // // // Don't forget to get the css too // Make sure there is a global variable called mr that instanciate MarkdownRenderer // mr = new MarkdownRenderer() class MarkdownRenderer { async renderMermaidDiagrams(text) { const mermaidCodeRegex = /```mermaid\n([\s\S]*?)```/g; const matches = text.match(mermaidCodeRegex); if (!matches) return text; for (const match of matches) { const mermaidCode = match.replace(/```mermaid\n/, '').replace(/```$/, ''); const uniqueId = 'mermaid-' + Math.random().toString(36).substr(2, 9); try { const result = await mermaid.render(uniqueId, mermaidCode); const htmlCode = `
${result.svg}
`; text = text.replace(match, htmlCode); } catch (error) { console.error('Mermaid rendering failed:', error); text = text.replace(match, ``); } } return text; } async renderGraphvizDiagrams(text) { // Check if viz.js is loaded if (typeof Viz === 'undefined') { console.warn('Viz.js is not loaded. Graphviz diagrams will not be rendered.'); return text; } const graphvizCodeRegex = /```graphviz\n([\s\S]*?)```/g; const matches = text.match(graphvizCodeRegex); if (!matches) return text; for (const match of matches) { const graphvizCode = match.replace(/```graphviz\n/, '').replace(/```$/, ''); const uniqueId = 'graphviz-' + Math.random().toString(36).substr(2, 9); try { const viz = new Viz(); const result = await viz.renderSVGElement(graphvizCode); const svgString = new XMLSerializer().serializeToString(result); const htmlCode = `
${svgString}
`; text = text.replace(match, htmlCode); } catch (error) { console.error('Graphviz rendering failed:', error); text = text.replace(match, ``); } } return text; } // Helper object for Graphviz operations gv = { zoomGraphviz: function(id, factor) { const element = document.getElementById(id); if (element) { const currentScale = element.style.transform ? parseFloat(element.style.transform.replace('scale(', '').replace(')', '')) : 1; const newScale = currentScale * factor; element.style.transform = `scale(${newScale})`; } }, saveGraphvizAsPNG: function(id) { const svg = document.getElementById(id).querySelector('svg'); if (svg) { const svgData = new XMLSerializer().serializeToString(svg); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const img = new Image(); img.onload = function() { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); const pngUrl = canvas.toDataURL('image/png'); const link = document.createElement('a'); link.href = pngUrl; link.download = 'graphviz_diagram.png'; link.click(); }; img.src = 'data:image/svg+xml;base64,' + btoa(svgData); } }, saveGraphvizAsSVG: function(id) { const svg = document.getElementById(id).querySelector('svg'); if (svg) { const svgData = new XMLSerializer().serializeToString(svg); const blob = new Blob([svgData], {type: 'image/svg+xml;charset=utf-8'}); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'graphviz_diagram.svg'; link.click(); URL.revokeObjectURL(url); } } }; async renderSVG(text) { const svgCodeRegex = /```svg\n([\s\S]*?)```/g; const matches = text.match(svgCodeRegex); if (!matches) return text; for (const match of matches) { const svgCode = match.replace(/```svg\n/, '').replace(/```$/, ''); const uniqueId = 'svg-' + Math.random().toString(36).substr(2, 9); try { // Wrap the SVG code in a div with a unique ID const htmlCode = `
${svgCode}
`; text = text.replace(match, htmlCode); } catch (error) { console.error('SVG rendering failed:', error); text = text.replace(match, ``); } } return text; } renderCodeBlocks(text) { const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g; return text.replace(codeBlockRegex, (match, language, code) => { language = language || 'plaintext'; let highlightedCode; try { highlightedCode = hljs.highlight(code.trim(), { language: language }).value; } catch (error) { console.warn(`Language '${language}' is not supported by highlight.js. Falling back to plaintext.`); highlightedCode = hljs.highlight(code.trim(), { language: 'plaintext' }).value; } const lines = highlightedCode.split('\n'); const numberedLines = lines.map((line, index) => `
${(index + 1).toString().padStart(2, '0')} ${line}
` ).join(''); return `
${language}
${numberedLines}
`; }); } copyCode(button) { const codeBlock = button.closest('.code-block'); const codeLines = codeBlock.querySelectorAll('.line-content'); const codeText = Array.from(codeLines).map(line => line.textContent).join('\n'); navigator.clipboard.writeText(codeText).then(() => { button.textContent = 'Copied!'; setTimeout(() => { button.textContent = 'Copy'; }, 2000); }).catch(err => { console.error('Failed to copy text: ', err); button.textContent = 'Failed'; setTimeout(() => { button.textContent = 'Copy'; }, 2000); }); } handleInlineCode(text) { return text.replace(/`([^`]+)`/g, function(match, code) { return `${code}`; }); } handleLatexEquations(text) { if (typeof katex === 'undefined') { console.error('KaTeX is not loaded. Make sure to include KaTeX scripts and CSS.'); return text; } // Function to render a single equation function renderEquation(match, p1, p2, p3, offset, string) { const equation = p1 || p2 || p3; const isDisplayMode = match.startsWith('\\[') || match.startsWith('$$'); try { return katex.renderToString(equation, { displayMode: isDisplayMode, throwOnError: false, output: 'html' }); } catch (e) { console.error("KaTeX rendering error:", e); return `${match}`; } } // Handle display equations: \[...\] and $$...$$ text = text.replace(/\\\[([\s\S]*?)\\\]|\$\$([\s\S]*?)\$\$/g, renderEquation); // Handle inline equations: \(...\) and $...$ // Be careful not to match single $ used for currency text = text.replace(/\\\(([\s\S]*?)\\\)|\$(\S.*?\S|\S)\$/g, renderEquation); return text; } async handleTables(text) { let alignments = []; let tableRows = []; let isInTable = false; let hasHeader = false; // Process the text line by line text = text.split('\n').map(line => { // Check if the line is a table row if (line.trim().startsWith('|') && line.trim().endsWith('|')) { isInTable = true; const tableRow = line.trim().slice(1, -1); // Remove leading and trailing | const cells = tableRow.split('|').map(cell => cell.trim()); if (cells.every(cell => cell.match(/^:?-+:?$/))) { // This is the header separator row alignments = cells.map(cell => { if (cell.startsWith(':') && cell.endsWith(':')) return 'center'; if (cell.endsWith(':')) return 'right'; return 'left'; }); hasHeader = true; return ''; // Remove separator row } const cellType = !hasHeader ? 'th' : 'td'; const renderedCells = cells.map((cell, cellIndex) => `<${cellType} class="border px-4 py-2" style="text-align: ${alignments[cellIndex] || 'left'};">${cell}` ).join(''); tableRows.push(`${renderedCells}`); return ''; // Remove the original Markdown line } else if (isInTable) { // We've reached the end of the table isInTable = false; hasHeader = false; const tableContent = tableRows.join(''); tableRows = []; // Reset for next table return `${tableContent}
`; } return line; // Return non-table lines unchanged }).join('\n'); // Handle case where table is at the end of the text if (isInTable) { const tableContent = tableRows.join(''); text += `${tableContent}
`; } return text } handleHeaders(text) { return text.replace(/^(#{1,6})\s+(.*?)$/gm, function(match, hashes, content) { const level = hashes.length; const fontSize = 2.5 - (level * 0.3); // Decreasing font size for each level return `${content}`; }); } handleBoldText(text) { return text.replace(/\*\*(.*?)\*\*/g, '$1'); } handleItalicText(text) { return text.replace(/\*(.*?)\*/g, '$1'); } handleLinks(text) { return text.replace(/\[([^\]]+)\]\(([^\)]+)\)/g, '$1'); } handleUnorderedLists(text) { return text.replace(/^\s*[-*+]\s+(.*?)$/gm, '
  • $1
  • ') .replace(/(
  • .*<\/li>)/s, ''); } handleOrderedLists(text) { return text.replace(/^\s*(\d+)\.\s+(.*?)$/gm, '
  • $2
  • ') .replace(/(
  • .*<\/li>)/s, '
      $1
    '); } handleBlockquotes(text) { return text.replace(/^>\s+(.*?)$/gm, '
    $1
    '); } handleHorizontalRules(text) { return text.replace(/^(-{3,}|_{3,}|\*{3,})$/gm, '
    '); } handleParagraphs(text) { let lines = text.split('\n'); let inList = false; let inCodeBlock = false; let result = []; let currentParagraph = ''; for (let i = 0; i < lines.length; i++) { let line = lines[i].trim(); // Check for code blocks if (line.startsWith('```')) { if (currentParagraph) { result.push('

    ' + currentParagraph + '

    '); currentParagraph = ''; } inCodeBlock = !inCodeBlock; result.push(line); continue; } // If we're in a code block, don't process the line if (inCodeBlock) { result.push(line); continue; } // Check for list items if (line.match(/^[-*+]\s/) || line.match(/^\d+\.\s/)) { if (currentParagraph) { result.push('

    ' + currentParagraph + '

    '); currentParagraph = ''; } if (!inList) { result.push(''); inList = false; } if (currentParagraph) { result.push('

    ' + currentParagraph + '

    '); currentParagraph = ''; } } // Regular text else { if (inList) { result.push(''); inList = false; } currentParagraph += (currentParagraph ? ' ' : '') + line; } } // Close any open list if (inList) { result.push(''); } // Add any remaining paragraph if (currentParagraph) { result.push('

    ' + currentParagraph + '

    '); } return result.join('\n'); } initMathJax() { // Configure MathJax window.MathJax = { tex: { inlineMath: [['$', '$']], displayMath: [['$$', '$$'], ['\\[', '\\]']] }, svg: { fontCache: 'global' }, startup: { ready: () => { MathJax.startup.defaultReady(); MathJax.startup.promise.then(() => { console.log('MathJax is loaded and ready'); // You can add any post-initialization logic here }); } } }; // Load MathJax if (!window.MathJax) { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js'; script.async = true; document.head.appendChild(script); } } async renderMarkdown(text) { // Handle Mermaid graphs first text = await this.renderMermaidDiagrams(text); text = await this.renderGraphvizDiagrams(text); // Handle code blocks with syntax highlighting and copy button text = await this.renderCodeBlocks(text); // Handle SVG graphs first text = await this.renderSVG(text); // Handle inline code text = this.handleInlineCode(text); // Handle LaTeX-style math equations text = this.handleLatexEquations(text); // Handle tables text = await this.handleTables(text); // Handle headers text = this.handleHeaders(text); // Handle bold text text = this.handleBoldText(text); // Handle italic text text = this.handleItalicText(text); // Handle links text = this.handleLinks(text); // Handle unordered lists text = this.handleUnorderedLists(text); // Handle ordered lists text = this.handleOrderedLists(text); // Handle blockquotes text = this.handleBlockquotes(text); // Handle horizontal rules text = this.handleHorizontalRules(text); // Handle paragraphs text = this.handleParagraphs(text); return text; } initMermaid() { if (typeof mermaid !== 'undefined') { mermaid.initialize({ startOnLoad: false }); } else { console.error('Mermaid library is not loaded'); } } initPrism() { if (typeof Prism !== 'undefined') { // No further initialization needed } else { console.error('Prism library is not loaded'); } } // Helper functions for Mermaid and code block handling saveMermaidAsPNG(id) { const svg = document.querySelector(`#${id} svg`); const svgData = new XMLSerializer().serializeToString(svg); // Create a canvas with a higher resolution const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); // Set a scale factor for higher resolution (e.g., 2 for double resolution) const scaleFactor = 2; const img = new Image(); img.onload = function() { canvas.width = img.width * scaleFactor; canvas.height = img.height * scaleFactor; // Scale the context to draw the image at a higher resolution ctx.scale(scaleFactor, scaleFactor); ctx.drawImage(img, 0, 0); const pngFile = canvas.toDataURL("image/png"); const downloadLink = document.createElement("a"); downloadLink.download = "mermaid_diagram.png"; downloadLink.href = pngFile; downloadLink.click(); }; img.src = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svgData))); } saveMermaidAsSVG(id) { const svg = document.querySelector(`#${id} svg`); const svgData = new XMLSerializer().serializeToString(svg); const svgBlob = new Blob([svgData], {type: "image/svg+xml;charset=utf-8"}); const svgUrl = URL.createObjectURL(svgBlob); const downloadLink = document.createElement("a"); downloadLink.href = svgUrl; downloadLink.download = "mermaid_diagram.svg"; document.body.appendChild(downloadLink); downloadLink.click(); document.body.removeChild(downloadLink); } zoomMermaid(id, factor) { const svg = document.getElementById(id).firstElementChild; const currentScale = svg.style.transform ? parseFloat(svg.style.transform.replace('scale(', '').replace(')', '')) : 1; const newScale = currentScale * factor; svg.style.transform = `scale(${newScale})`; } async highlightCode(code, language) { // Make sure the language is supported by your highlighting library const supportedLanguage = Prism.languages[language] ? language : 'plaintext'; return Prism.highlight(code, Prism.languages[supportedLanguage], supportedLanguage); } // Helper function to escape HTML special characters escapeHtml(unsafe) { if (typeof unsafe !== 'string') { console.log("Found unsafe string:", text) return ''; } return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } }