UNPKG

mermaid-isomorphic

Version:

Transform mermaid diagrams in the browser or Node.js

188 lines 6.82 kB
import { chromium } from 'playwright'; /* c8 ignore start */ /** * Render mermaid diagrams in the browser. * * @param options * The options used to render the diagrams * @returns * A settled promise that holds the rendering results. */ async function renderDiagrams({ containerStyle, diagrams, iconPacks, mermaidConfig, prefix, screenshot }) { await Promise.all(Array.from(document.fonts, (font) => font.load())); const parser = new DOMParser(); const serializer = new XMLSerializer(); const container = document.createElement('div'); container.ariaHidden = 'true'; container.style.maxHeight = '0'; container.style.opacity = '0'; container.style.overflow = 'hidden'; Object.assign(container.style, containerStyle); document.body.append(container); mermaid.registerIconPacks(iconPacks); mermaid.initialize(mermaidConfig); /** * Get an aria value form a referencing attribute. * * @param element * The SVG element the get the value from. * @param attribute * The attribute whose value to get. * @returns * The aria value. */ // eslint-disable-next-line unicorn/consistent-function-scoping function getAriaValue(element, attribute) { const value = element.getAttribute(attribute); if (!value) { return; } let result = ''; for (const id of value.split(/\s+/)) { const node = element.getElementById(id); if (node) { result += node.textContent; } } return result; } return Promise.allSettled(diagrams.map(async (diagram, index) => { const id = `${prefix}-${index}`; try { const { svg } = await mermaid.render(id, diagram, container); const root = parser.parseFromString(svg, 'text/html'); const [element] = root.getElementsByTagName('svg'); const { height, width } = element.viewBox.baseVal; const description = getAriaValue(element, 'aria-describedby'); const title = getAriaValue(element, 'aria-labelledby'); if (screenshot) { document.body.append(element); } const result = { height, id, svg: serializer.serializeToString(element), width }; if (description) { result.description = description; } if (title) { result.title = title; } return result; } catch (error) { throw error instanceof Error ? { name: error.name, stack: error.stack, message: error.message } : error; } })); } /** * Launch a browser and a single browser context. * * @param browserType * The browser type to launch. * @param launchOptions * Optional launch options * @returns * A simple browser context wrapper */ async function getBrowser(browserType, launchOptions) { const browser = await browserType.launch(launchOptions); const context = await browser.newContext({ bypassCSP: true }); return { async close() { await context.close(); await browser.close(); }, newPage() { return context.newPage(); } }; } /** * Create a Mermaid renderer. * * The Mermaid renderer manages a browser instance. If multiple diagrams are being rendered * simultaneously, the internal browser instance will be re-used. If no diagrams are being rendered, * the browser will be closed. * * @param options * The options of the Mermaid renderer. * @returns * A function that renders Mermaid diagrams in the browser. */ export function createMermaidRenderer(options = {}) { const { browserType = chromium, launchOptions } = options; let browserPromise; let count = 0; return async (diagrams, renderOptions) => { count += 1; browserPromise ||= getBrowser(browserType, launchOptions); const context = await browserPromise; let page; let renderResults; try { page = await context.newPage(); await page.goto(import.meta.resolve('../index.html')); const promises = [ // We use url, not path. If we use path, the fonts can’t be resolved. page.addStyleTag({ url: import.meta.resolve('@fortawesome/fontawesome-free/css/all.css') }), page.addStyleTag({ url: import.meta.resolve('katex/dist/katex.css') }), page.addScriptTag({ url: import.meta.resolve('mermaid/dist/mermaid.js') }) ]; const css = renderOptions?.css; if (typeof css === 'string' || css instanceof URL) { promises.push(page.addStyleTag({ url: String(css) })); } else if (css) { for (const url of css) { promises.push(page.addStyleTag({ url: String(url) })); } } await Promise.all(promises); renderResults = await page.evaluate(renderDiagrams, { // Avoid error TS2589: Type instantiation is excessively deep and possibly infinite. containerStyle: (renderOptions?.containerStyle ?? {}), diagrams, screenshot: Boolean(renderOptions?.screenshot), mermaidConfig: { fontFamily: 'arial,sans-serif', ...renderOptions?.mermaidConfig }, prefix: renderOptions?.prefix ?? 'mermaid', iconPacks: renderOptions?.iconPacks ?? [] }); if (renderOptions?.screenshot) { for (const result of renderResults) { if (result.status === 'fulfilled') { result.value.screenshot = await page .locator(`#${result.value.id}`) .screenshot({ omitBackground: true }); } } } } finally { await page?.close(); count -= 1; if (!count) { browserPromise = undefined; context.close(); } } for (const result of renderResults) { if (result.status !== 'rejected') { continue; } const { reason } = result; if (reason && 'name' in reason && 'message' in reason && 'stack' in reason) { Object.setPrototypeOf(reason, Error.prototype); } } return renderResults; }; } //# sourceMappingURL=mermaid-isomorphic.js.map