knsv/mermaid

View on GitHub
packages/mermaid/src/mermaid.ts

Summary

Maintainability
B
6 hrs
Test Coverage
/**
 * Web page integration module for the mermaid framework. It uses the mermaidAPI for mermaid
 * functionality and to render the diagrams to svg code!
 */
import { registerIconPacks } from './rendering-util/icons.js';
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<HTMLElement>;
  /**
   * 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<RunOptions, 'suppressErrors'> = {
    querySelector: '.mermaid',
  }
) {
  const conf = mermaidAPI.getConfig();

  log.debug(`${!postRenderCallback ? 'No ' : ''}Callback function found`);

  let nodesToProcess: ArrayLike<HTMLElement>;
  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(/<br\s*\/?>/gi, '<br/>');

    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<HTMLElement>,
  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<unknown>)[] = [];
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;
  registerIconPacks: typeof registerIconPacks;
}

const mermaid: Mermaid = {
  startOnLoad: true,
  mermaidAPI,
  parse,
  render,
  init,
  run,
  registerExternalDiagrams,
  registerLayoutLoaders,
  initialize,
  parseError: undefined,
  contentLoaded,
  setParseErrorHandler,
  detectType,
  registerIconPacks,
};

export default mermaid;