0 directories, 46 files

util

Home / tinai / util
/**
 * Automatic repairs for common Mermaid syntax problems that models frequently
 * emit. repair_mermaid_code runs each fixer in order after a failed render;
 * a fixer returns the corrected code, or null when it does not apply.
 */

/**
 * Returns true when the code declares a flowchart, the only diagram type the
 * node-label fixers apply to.
 * @param {string} code - The mermaid source.
 * @returns {boolean}
 */
function is_flowchart(code) {
	return /^\s*(?:%%[^\n]*\n)*\s*(?:graph|flowchart)\b/.test(code);
}

/**
 * Replaces each bare "quoted string" on the line — outside of node-shape
 * brackets, edge-label pipes, and quoted labels — using the callback's
 * replacement, and returns the new line.
 * @param {string} line - A single mermaid source line.
 * @param {function(string): string} replace - Maps the quoted text to its replacement.
 * @returns {string}
 */
function replace_quoted_tokens(line, replace) {
	let out = '';
	let quote = null;
	let closer = null;
	for (let i = 0; i < line.length; i++) {
		const ch = line[i];
		if (closer !== null) {
			out += ch;
			if (ch === closer) closer = null;
			continue;
		}
		if (quote !== null) {
			if (ch === '"') {
				out += replace(quote) + '["' + quote + '"]';
				quote = null;
			} else {
				quote += ch;
			}
			continue;
		}
		if (ch === '"') {
			quote = '';
			continue;
		}
		if ('[({|'.includes(ch)) {
			closer = { '[': ']', '(': ')', '{': '}', '|': '|' }[ch];
		}
		out += ch;
	}
	if (quote !== null) out += '"' + quote;
	return out;
}

/**
 * Converts bare quoted node labels ("Step 1") into mermaid's bracketed form
 * (n1["Step 1"]), reusing one generated id per distinct label so repeated
 * references stay the same node.
 * @param {string} code - The mermaid source.
 * @returns {string|null} The repaired code, or null when nothing changed.
 */
function fix_quoted_node_labels(code) {
	const ids = new Map();
	let count = 0;
	const lines = code.split('\n').map(line => {
		const trimmed = line.trim();
		if (trimmed.startsWith('%%') || /^subgraph\b/.test(trimmed)) return line;
		return replace_quoted_tokens(line, label => {
			if (!ids.has(label)) {
				count += 1;
				ids.set(label, 'q' + count);
			}
			return ids.get(label);
		});
	});
	if (count === 0) return null;
	return lines.join('\n');
}

/**
 * Wraps square-bracket node labels that carry unquoted parentheses, braces,
 * or brackets in quotes: A[Step (one)] becomes A["Step (one)"].
 * @param {string} code - The mermaid source.
 * @returns {string|null} The repaired code, or null when nothing changed.
 */
function fix_label_special_chars(code) {
	let changed = false;
	const lines = code.split('\n').map(line => {
		const trimmed = line.trim();
		if (trimmed.startsWith('%%') || /^subgraph\b/.test(trimmed)) return line;
		const fixed = line.replace(/(?<![\[(])\[(?![\[(])([^[\]]*)\](?![\])])/g, (match, label) => {
			const text = label.trim();
			if (text.includes('"') || !/[(){}[\]]/.test(text)) return match;
			changed = true;
			return '[' + label.replace(text, '"' + text + '"') + ']';
		});
		return fixed;
	});
	return changed ? lines.join('\n') : null;
}

/**
 * Renames bare lowercase "end" node ids — a reserved flowchart word — to a
 * bracketed node keeping the visible label. A line that is only "end" closes
 * a subgraph and is left alone.
 * @param {string} code - The mermaid source.
 * @returns {string|null} The repaired code, or null when nothing changed.
 */
function fix_reserved_end_id(code) {
	let changed = false;
	const lines = code.split('\n').map(line => {
		const trimmed = line.trim();
		if (trimmed.startsWith('%%') || /^subgraph\b/.test(trimmed) || /^end$/i.test(trimmed)) return line;
		if (!/--+>|-{3,}|-\.+->?|={2,}>|~~/.test(line)) return line;
		const fixed = line.replace(/(^|[\s;>])(end)(?=$|[\s;]|$)/g, (match, before) => {
			changed = true;
			return before + 'end_node["end"]';
		});
		return fixed;
	});
	return changed ? lines.join('\n') : null;
}

const FIXERS = [
	{ applies: is_flowchart, fix: fix_quoted_node_labels },
	{ applies: is_flowchart, fix: fix_label_special_chars },
	{ applies: is_flowchart, fix: fix_reserved_end_id }
];

/**
 * Applies each syntax fixer in order after a failed render.
 * @param {string} code - The mermaid source that failed to render.
 * @returns {{code: string, applied: number}} The repaired code and how many
 *   fixes changed it.
 */
export function repair_mermaid_code(code) {
	let current = code;
	let applied = 0;
	for (const fixer of FIXERS) {
		if (!fixer.applies(current)) continue;
		const fixed = fixer.fix(current);
		if (fixed !== null && fixed !== current) {
			current = fixed;
			applied += 1;
		}
	}
	return { code: current, applied };
}
🌐
mermaid-fixes.js ×
Type: Web, text/plain
4.60 Kilobytes
Last Modified 2026-11-04 09:48:28
⬇ Download File