0 directories, 46 files

util

Home / tinai / util
/**
 * Mermaid rendering helpers shared by the chat renderer and the shared viewer
 * page: converts mermaid code blocks into rendered diagrams, repairs failing
 * diagrams once, and attaches the expand buttons that open the diagram overlay.
 */

import { escapeHtml } from './format-utils.js';
import { open_diagram_overlay, open_diagram_overlay_message } from './diagram-overlay.js';
import { repair_mermaid_code } from './mermaid-fixes.js';
import { mermaid } from './mermaid.js';

/**
 * Renders Mermaid diagrams in the specified container element.
 * @param {HTMLElement} container
 */
export async function render_mermaid_in_container(container) {
	if (typeof mermaid === 'undefined' || !container) return;

	// Convert any pre code.language-mermaid elements into .div-diagram-mermaid containers
	const codeNodes = Array.from(container.querySelectorAll('pre code.language-mermaid:not([data-processed="true"])'));
	for (const codeNode of codeNodes) {
		codeNode.setAttribute('data-processed', 'true');
		const pre = codeNode.parentElement;
		if (!pre) continue;
		const rawCode = codeNode.textContent || '';
		const div = document.createElement('div');
		div.className = 'div-diagram-mermaid';
		div.setAttribute('data-mermaid', rawCode.trim());
		div.textContent = rawCode.trim();
		pre.parentNode.replaceChild(div, pre);
	}

	const mermaidNodes = Array.from(container.querySelectorAll('.mermaid:not([data-processed="true"]), .div-diagram-mermaid:not([data-processed="true"])'))
		.filter(node => !node.querySelector('svg') && (node.getAttribute('data-mermaid') || node.textContent || '').trim().length > 0);

	for (let i = 0; i < mermaidNodes.length; i++) {
		const node = mermaidNodes[i];
		node.setAttribute('data-processed', 'true');
		const rawCode = node.getAttribute('data-mermaid') || node.textContent || '';
		const code = rawCode.trim();
		if (!code) continue;

		try {
			await render_mermaid_svg(node, code);
		} catch (e) {
			console.warn('Mermaid render error for diagram:', e);
			node.innerHTML = `<pre class="code-block language-mermaid sb-x" data-simplebar data-simplebar-auto-hide="false"><code>${escapeHtml(code)}</code></pre>`;
			attach_diagram_expand_button(node, code);
		}
	}
}

/**
 * Renders Mermaid code into a node and inserts the resulting SVG, retrying
 * once with automatic syntax repairs. Throws the render error when the
 * diagram still cannot be rendered.
 * @param {HTMLElement} node
 * @param {string} code
 */
export async function render_mermaid_svg(node, code) {
	const { svg, bind_functions, error } = await render_diagram_svg(code);
	if (!svg) throw error;
	node.innerHTML = '';
	node.appendChild(svg);
	attach_diagram_expand_button(node, code);
	if (typeof bind_functions === 'function') {
		bind_functions(node);
	}
}

/**
 * Renders Mermaid code to an SVG element, retrying once with automatic
 * syntax repairs applied. Mermaid's temporary render elements are cleaned
 * up between attempts.
 * @param {string} code - The mermaid source code.
 * @returns {Promise<{svg: SVGElement|null, bind_functions: function(Element): void|null, error: string}>}
 */
export async function render_diagram_svg(code) {
	const attempts = [code];
	const repaired = repair_mermaid_code(code);
	if (repaired.code !== code) attempts.push(repaired.code);
	let error = null;
	for (const attempt of attempts) {
		const id = 'mermaid_' + Date.now() + '_' + Math.random().toString(36).substring(2, 7);
		try {
			const result = /** @type {{svg?: string, bindFunctions?: function(Element): void}} */ (await mermaid.render(id, attempt));
			const holder = document.createElement('div');
			holder.innerHTML = result?.svg || '';
			const svg = holder.querySelector('svg');
			if (svg) {
				return { svg, bind_functions: result.bindFunctions || null, error: null };
			}
		} catch (e) {
			error = e;
		} finally {
			const errEl = document.getElementById(id) || document.getElementById('d' + id);
			if (errEl) errEl.remove();
		}
	}
	return { svg: null, bind_functions: null, error: error ? String(error?.message || error) : 'Diagram could not be rendered.' };
}

/**
 * Adds (or refreshes) the expand button on a diagram container, placed in
 * the container's top right corner. With a rendered SVG it opens the
 * diagram overlay; without one (code fallback) clicking retries the
 * render and shows the render error in the overlay when the diagram still
 * cannot be drawn.
 * @param {HTMLElement} node - The diagram container.
 * @param {string} code - The mermaid source used to retry rendering.
 */
export function attach_diagram_expand_button(node, code = '') {
	const svg = node.querySelector('svg');
	if (!svg && !code) return;
	let btn = node.querySelector('.div-diagram-expand-btn');
	if (!btn) {
		btn = document.createElement('button');
		btn.type = 'button';
		btn.className = 'div-diagram-expand-btn as-icon';
		btn.title = 'Expand Diagram';
		btn.textContent = '🔍';
		node.appendChild(btn);
	}
	btn.onclick = async () => {
		if (svg) {
			open_diagram_overlay(svg);
			return;
		}
		const { svg: overlay_svg, error } = await render_diagram_svg(code);
		if (overlay_svg) {
			open_diagram_overlay(overlay_svg);
		} else {
			open_diagram_overlay_message(error);
		}
	};
}
🌐
mermaid-render.js ×
Type: Web, text/x-java
5.10 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File