/** * Web page integration module for the mermaid framework. It uses the mermaidAPI for mermaid * functionality and to render the diagrams to svg code! */ import { dedent } from 'ts-dedent'; import type { MermaidConfig } from './config.type.js'; import { detectType, registerLazyLoadedDiagrams } from './diagram-api/detectType.js'; import { addDiagrams } from './diagram-api/diagram-orchestration.js'; import { loadRegisteredDiagrams } from './diagram-api/loadDiagram.js'; import type { ExternalDiagramDefinition, SVG, SVGGroup } from './diagram-api/types.js'; import type { ParseErrorFunction } from './Diagram.js'; import type { UnknownDiagramError } from './errors.js'; import type { InternalHelpers } from './internals.js'; import { log } from './logger.js'; import { mermaidAPI } from './mermaidAPI.js'; import type { LayoutLoaderDefinition, RenderOptions } from './rendering-util/render.js'; import { registerLayoutLoaders } from './rendering-util/render.js'; import type { LayoutData } from './rendering-util/types.js'; import type { ParseOptions, ParseResult, RenderResult } from './types.js'; import type { DetailedError } from './utils.js'; import utils, { isDetailedError } from './utils.js'; export type { DetailedError, ExternalDiagramDefinition, InternalHelpers, LayoutData, LayoutLoaderDefinition, MermaidConfig, ParseErrorFunction, ParseOptions, ParseResult, RenderOptions, RenderResult, SVG, SVGGroup, UnknownDiagramError, }; export interface RunOptions { /** * The query selector to use when finding elements to render. Default: `".mermaid"`. */ querySelector?: string; /** * The nodes to render. If this is set, `querySelector` will be ignored. */ nodes?: ArrayLike; /** * A callback to call after each diagram is rendered. */ postRenderCallback?: (id: string) => unknown; /** * If `true`, errors will be logged to the console, but not thrown. Default: `false` */ suppressErrors?: boolean; } const handleError = (error: unknown, errors: DetailedError[], parseError?: ParseErrorFunction) => { log.warn(error); if (isDetailedError(error)) { // handle case where error string and hash were // wrapped in object like`const error = { str, hash };` if (parseError) { parseError(error.str, error.hash); } errors.push({ ...error, message: error.str, error }); } else { // assume it is just error string and pass it on if (parseError) { parseError(error); } if (error instanceof Error) { errors.push({ str: error.message, message: error.message, hash: error.name, error, }); } } }; /** * ## run * * Function that goes through the document to find the chart definitions in there and render them. * * The function tags the processed attributes with the attribute data-processed and ignores found * elements with the attribute already set. This way the init function can be triggered several * times. * * ```mermaid * graph LR; * a(Find elements)-->b{Processed} * b-->|Yes|c(Leave element) * b-->|No |d(Transform) * ``` * * Renders the mermaid diagrams * * @param options - Optional runtime configs */ const run = async function ( options: RunOptions = { querySelector: '.mermaid', } ) { try { await runThrowsErrors(options); } catch (e) { if (isDetailedError(e)) { log.error(e.str); } if (mermaid.parseError) { mermaid.parseError(e as string); } if (!options.suppressErrors) { log.error('Use the suppressErrors option to suppress these errors'); throw e; } } }; const runThrowsErrors = async function ( { postRenderCallback, querySelector, nodes }: Omit = { querySelector: '.mermaid', } ) { const conf = mermaidAPI.getConfig(); log.debug(`${!postRenderCallback ? 'No ' : ''}Callback function found`); let nodesToProcess: ArrayLike; if (nodes) { nodesToProcess = nodes; } else if (querySelector) { nodesToProcess = document.querySelectorAll(querySelector); } else { throw new Error('Nodes and querySelector are both undefined'); } log.debug(`Found ${nodesToProcess.length} diagrams`); if (conf?.startOnLoad !== undefined) { log.debug('Start On Load: ' + conf?.startOnLoad); mermaidAPI.updateSiteConfig({ startOnLoad: conf?.startOnLoad }); } // generate the id of the diagram const idGenerator = new utils.InitIDGenerator(conf.deterministicIds, conf.deterministicIDSeed); let txt: string; const errors: DetailedError[] = []; // element is the current div with mermaid class // eslint-disable-next-line unicorn/prefer-spread for (const element of Array.from(nodesToProcess)) { log.info('Rendering diagram: ' + element.id); /*! Check if previously processed */ if (element.getAttribute('data-processed')) { continue; } element.setAttribute('data-processed', 'true'); const id = `mermaid-${idGenerator.next()}`; // Fetch the graph definition including tags txt = element.innerHTML; // transforms the html to pure text txt = dedent(utils.entityDecode(txt)) // removes indentation, required for YAML parsing .trim() .replace(//gi, '
'); const init = utils.detectInit(txt); if (init) { log.debug('Detected early reinit: ', init); } try { const { svg, bindFunctions } = await render(id, txt, element); element.innerHTML = svg; if (postRenderCallback) { await postRenderCallback(id); } if (bindFunctions) { bindFunctions(element); } } catch (error) { handleError(error, errors, mermaid.parseError); } } if (errors.length > 0) { // TODO: We should be throwing an error object. throw errors[0]; } }; /** * Used to set configurations for mermaid. * This function should be called before the run function. * @param config - Configuration object for mermaid. */ const initialize = function (config: MermaidConfig) { mermaidAPI.initialize(config); }; /** * ## init * * @deprecated Use {@link initialize} and {@link run} instead. * * Renders the mermaid diagrams * * @param config - **Deprecated**, please set configuration in {@link initialize}. * @param nodes - **Default**: `.mermaid`. One of the following: * - A DOM Node * - An array of DOM nodes (as would come from a jQuery selector) * - A W3C selector, a la `.mermaid` * @param callback - Called once for each rendered diagram's id. */ const init = async function ( config?: MermaidConfig, nodes?: string | HTMLElement | NodeListOf, callback?: (id: string) => unknown ) { log.warn('mermaid.init is deprecated. Please use run instead.'); if (config) { initialize(config); } const runOptions: RunOptions = { postRenderCallback: callback, querySelector: '.mermaid' }; if (typeof nodes === 'string') { runOptions.querySelector = nodes; } else if (nodes) { if (nodes instanceof HTMLElement) { runOptions.nodes = [nodes]; } else { runOptions.nodes = nodes; } } await run(runOptions); }; /** * Used to register external diagram types. * @param diagrams - Array of {@link ExternalDiagramDefinition}. * @param opts - If opts.lazyLoad is false, the diagrams will be loaded immediately. */ const registerExternalDiagrams = async ( diagrams: ExternalDiagramDefinition[], { lazyLoad = true, }: { lazyLoad?: boolean; } = {} ) => { addDiagrams(); registerLazyLoadedDiagrams(...diagrams); if (lazyLoad === false) { await loadRegisteredDiagrams(); } }; /** * ##contentLoaded Callback function that is called when page is loaded. This functions fetches * configuration for mermaid rendering and calls init for rendering the mermaid diagrams on the * page. */ const contentLoaded = function () { if (mermaid.startOnLoad) { const { startOnLoad } = mermaidAPI.getConfig(); if (startOnLoad) { mermaid.run().catch((err) => log.error('Mermaid failed to initialize', err)); } } }; if (typeof document !== 'undefined') { /*! * Wait for document loaded before starting the execution */ window.addEventListener('load', contentLoaded, false); } /** * ## setParseErrorHandler Alternative to directly setting parseError using: * * ```js * mermaid.parseError = function(err,hash) { * forExampleDisplayErrorInGui(err); // do something with the error * }; * ``` * * This is provided for environments where the mermaid object can't directly have a new member added * to it (eg. dart interop wrapper). (Initially there is no parseError member of mermaid). * * @param parseErrorHandler - New parseError() callback. */ const setParseErrorHandler = function (parseErrorHandler: (err: any, hash: any) => void) { mermaid.parseError = parseErrorHandler; }; const executionQueue: (() => Promise)[] = []; let executionQueueRunning = false; const executeQueue = async () => { if (executionQueueRunning) { return; } executionQueueRunning = true; while (executionQueue.length > 0) { const f = executionQueue.shift(); if (f) { try { await f(); } catch (e) { log.error('Error executing queue', e); } } } executionQueueRunning = false; }; /** * Parse the text and validate the syntax. * @param text - The mermaid diagram definition. * @param parseOptions - Options for parsing. @see {@link ParseOptions} * @returns If valid, {@link ParseResult} otherwise `false` if parseOptions.suppressErrors is `true`. * @throws Error if the diagram is invalid and parseOptions.suppressErrors is false or not set. * * @example * ```js * console.log(await mermaid.parse('flowchart \n a --> b')); * // { diagramType: 'flowchart-v2' } * console.log(await mermaid.parse('wrong \n a --> b', { suppressErrors: true })); * // false * console.log(await mermaid.parse('wrong \n a --> b', { suppressErrors: false })); * // throws Error * console.log(await mermaid.parse('wrong \n a --> b')); * // throws Error * ``` */ const parse: typeof mermaidAPI.parse = async (text, parseOptions) => { return new Promise((resolve, reject) => { // This promise will resolve when the render call is done. // It will be queued first and will be executed when it is first in line const performCall = () => new Promise((res, rej) => { mermaidAPI.parse(text, parseOptions).then( (r) => { // This resolves for the promise for the queue handling res(r); // This fulfills the promise sent to the value back to the original caller resolve(r); }, (e) => { log.error('Error parsing', e); mermaid.parseError?.(e); rej(e); reject(e); } ); }); executionQueue.push(performCall); executeQueue().catch(reject); }); }; /** * Function that renders an svg with a graph from a chart definition. Usage example below. * * ```javascript * element = document.querySelector('#graphDiv'); * const graphDefinition = 'graph TB\na-->b'; * const { svg, bindFunctions } = await mermaid.render('graphDiv', graphDefinition); * element.innerHTML = svg; * bindFunctions?.(element); * ``` * * @remarks * Multiple calls to this function will be enqueued to run serially. * * @param id - The id for the SVG element (the element to be rendered) * @param text - The text for the graph definition * @param container - HTML element where the svg will be inserted. (Is usually element with the .mermaid class) * If no svgContainingElement is provided then the SVG element will be appended to the body. * Selector to element in which a div with the graph temporarily will be * inserted. If one is provided a hidden div will be inserted in the body of the page instead. The * element will be removed when rendering is completed. * @returns Returns the SVG Definition and BindFunctions. */ const render: typeof mermaidAPI.render = (id, text, container) => { return new Promise((resolve, reject) => { // This promise will resolve when the mermaidAPI.render call is done. // It will be queued first and will be executed when it is first in line const performCall = () => new Promise((res, rej) => { mermaidAPI.render(id, text, container).then( (r) => { // This resolves for the promise for the queue handling res(r); // This fulfills the promise sent to the value back to the original caller resolve(r); }, (e) => { log.error('Error parsing', e); mermaid.parseError?.(e); rej(e); reject(e); } ); }); executionQueue.push(performCall); executeQueue().catch(reject); }); }; export interface Mermaid { startOnLoad: boolean; parseError?: ParseErrorFunction; /** * @deprecated Use {@link parse} and {@link render} instead. Please [open a discussion](https://github.com/mermaid-js/mermaid/discussions) if your use case does not fit the new API. * @internal */ mermaidAPI: typeof mermaidAPI; parse: typeof parse; render: typeof render; /** * @deprecated Use {@link initialize} and {@link run} instead. */ init: typeof init; run: typeof run; registerLayoutLoaders: typeof registerLayoutLoaders; registerExternalDiagrams: typeof registerExternalDiagrams; initialize: typeof initialize; contentLoaded: typeof contentLoaded; setParseErrorHandler: typeof setParseErrorHandler; detectType: typeof detectType; } const mermaid: Mermaid = { startOnLoad: true, mermaidAPI, parse, render, init, run, registerExternalDiagrams, registerLayoutLoaders, initialize, parseError: undefined, contentLoaded, setParseErrorHandler, detectType, }; export default mermaid;