// 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, `
Failed to render diagram
`);
}
}
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, `
Failed to render diagram
`);
}
}
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, `
Failed to render SVG
`);
}
}
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}${cellType}>`
).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 += `
');
}
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('