diff --git a/cypress/platform/sidv.html b/cypress/platform/sidv.html new file mode 100644 index 000000000..c9bf56b7d --- /dev/null +++ b/cypress/platform/sidv.html @@ -0,0 +1,14 @@ + + +
+    none
+    hello world
+    
+ + + + diff --git a/package.json b/package.json index 4ad882420..166d23636 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "e2e": "start-server-and-test dev http://localhost:9000/ cypress", "ci": "vitest run", "test": "pnpm lint && vitest run", - "test:watch": "vitest --coverage --watch", + "test:watch": "vitest --watch", + "test:coverage": "vitest --coverage", "prepublishOnly": "pnpm build && pnpm test", "prepare": "concurrently \"husky install\" \"pnpm build\"", "pre-commit": "lint-staged" diff --git a/packages/mermaid/src/Diagram.ts b/packages/mermaid/src/Diagram.ts index 9bdc92079..0aa741994 100644 --- a/packages/mermaid/src/Diagram.ts +++ b/packages/mermaid/src/Diagram.ts @@ -8,11 +8,18 @@ export class Diagram { parser; renderer; db; + private detectTypeFailed = false; // eslint-disable-next-line @typescript-eslint/ban-types constructor(public txt: string, parseError?: Function) { const cnf = configApi.getConfig(); this.txt = txt; - this.type = detectType(txt, cnf); + try { + this.type = detectType(txt, cnf); + } catch (e) { + this.handleError(e, parseError); + this.type = 'error'; + this.detectTypeFailed = true; + } const diagram = getDiagram(this.type); log.debug('Type ' + this.type); // Setup diagram @@ -32,31 +39,39 @@ export class Diagram { // eslint-disable-next-line @typescript-eslint/ban-types parse(text: string, parseError?: Function): boolean { + if (this.detectTypeFailed) { + return false; + } try { text = text + '\n'; this.db.clear(); this.parser.parse(text); return true; } catch (error) { - // Is this the correct way to access mermiad's parseError() - // method ? (or global.mermaid.parseError()) ? - if (parseError) { - if (isDetailedError(error)) { - // handle case where error string and hash were - // wrapped in object like`const error = { str, hash };` - parseError(error.str, error.hash); - } else { - // assume it is just error string and pass it on - parseError(error); - } - } else { - // No mermaid.parseError() handler defined, so re-throw it - throw error; - } + this.handleError(error, parseError); } return false; } + // eslint-disable-next-line @typescript-eslint/ban-types + handleError(error: unknown, parseError?: Function) { + // Is this the correct way to access mermiad's parseError() + // method ? (or global.mermaid.parseError()) ? + if (parseError) { + if (isDetailedError(error)) { + // handle case where error string and hash were + // wrapped in object like`const error = { str, hash };` + parseError(error.str, error.hash); + } else { + // assume it is just error string and pass it on + parseError(error); + } + } else { + // No mermaid.parseError() handler defined, so re-throw it + throw error; + } + } + getParser() { return this.parser; } @@ -66,10 +81,8 @@ export class Diagram { } } -export default Diagram; - // eslint-disable-next-line @typescript-eslint/ban-types -export const getDiagramFromText = async (txt: string, parseError?: Function) => { +export const getDiagramFromText = async (txt: string, parseError?: Function): Promise => { const type = detectType(txt, configApi.getConfig()); try { // Trying to find the diagram @@ -79,24 +92,14 @@ export const getDiagramFromText = async (txt: string, parseError?: Function) => if (!loader) { throw new Error(`Diagram ${type} not found.`); } - // Diagram not avaiable, loading it - // const path = getPathForDiagram(type); - const { diagram } = await loader(); // eslint-disable-line @typescript-eslint/no-explicit-any - registerDiagram( - type, - { - db: diagram.db, - renderer: diagram.renderer, - parser: diagram.parser, - styles: diagram.styles, - }, - diagram.injectUtils - ); - // await loadDiagram('./packages/mermaid-mindmap/dist/mermaid-mindmap.js'); - // await loadDiagram(path + 'mermaid-' + type + '.js'); + // Diagram not available, loading it + const { diagram } = await loader(); + registerDiagram(type, diagram, undefined, diagram.injectUtils); // new diagram will try getDiagram again and if fails then it is a valid throw } // If either of the above worked, we have the diagram // logic and can continue return new Diagram(txt, parseError); }; + +export default Diagram; diff --git a/packages/mermaid/src/__mocks__/mermaidAPI.ts b/packages/mermaid/src/__mocks__/mermaidAPI.ts index f15db139f..08c5b7eea 100644 --- a/packages/mermaid/src/__mocks__/mermaidAPI.ts +++ b/packages/mermaid/src/__mocks__/mermaidAPI.ts @@ -11,17 +11,13 @@ import Diagram from '../Diagram'; // Normally, we could just do the following to get the original `parse()` // implementation, however, requireActual returns a promise and it's not documented how to use withing mock file. -let hasLoadedDiagrams = false; /** * @param text * @param parseError */ // eslint-disable-next-line @typescript-eslint/ban-types function parse(text: string, parseError?: Function): boolean { - if (!hasLoadedDiagrams) { - addDiagrams(); - hasLoadedDiagrams = true; - } + addDiagrams(); const diagram = new Diagram(text, parseError); return diagram.parse(text, parseError); } diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 1059d5709..2343bdd34 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -3,7 +3,7 @@ import DOMPurify from 'dompurify'; export interface MermaidConfig { - lazyLoadedDiagrams?: any; + lazyLoadedDiagrams?: string[]; theme?: string; themeVariables?: any; themeCSS?: string; diff --git a/packages/mermaid/src/diagram-api/detectType.ts b/packages/mermaid/src/diagram-api/detectType.ts index 340cbfbae..9536fded2 100644 --- a/packages/mermaid/src/diagram-api/detectType.ts +++ b/packages/mermaid/src/diagram-api/detectType.ts @@ -1,8 +1,7 @@ import { MermaidConfig } from '../config.type'; +import { log } from '../logger'; +import { DetectorRecord, DiagramDetector, DiagramLoader } from './types'; -export type DiagramDetector = (text: string, config?: MermaidConfig) => boolean; -export type DiagramLoader = (() => any) | null; -export type DetectorRecord = { detector: DiagramDetector; loader: DiagramLoader }; const directive = /[%]{2}[{]\s*(?:(?:(\w+)\s*:|(\w+))\s*(?:(?:(\w+))|((?:(?![}][%]{2}).|\r?\n)*))?\s*)(?:[}][%]{2})?/gi; const anyComment = /\s*%%.*\n/gm; @@ -34,26 +33,22 @@ const detectors: Record = {}; */ export const detectType = function (text: string, config?: MermaidConfig): string { text = text.replace(directive, '').replace(anyComment, '\n'); - - // console.log(detectors); - for (const [key, { detector }] of Object.entries(detectors)) { const diagram = detector(text, config); if (diagram) { return key; } } - // TODO: #3391 - // throw new Error(`No diagram type detected for text: ${text}`); - return 'flowchart'; + + throw new Error(`No diagram type detected for text: ${text}`); }; -export const addDetector = ( - key: string, - detector: DiagramDetector, - loader: DiagramLoader | null -) => { +export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => { + if (detectors[key]) { + throw new Error(`Detector with key ${key} already exists`); + } detectors[key] = { detector, loader }; + log.debug(`Detector with key ${key} added${loader ? ' with loader' : ''}`); }; export const getDiagramLoader = (key: string) => detectors[key].loader; diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index 1693f4f51..a26edb303 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -1,16 +1,4 @@ -import { - registerDiagram, - registerDetector, - DiagramDefinition, - DiagramDetector, -} from './diagramAPI'; - -// // @ts-ignore: TODO Fix ts errors -// import mindmapParser from '../diagrams/mindmap/parser/mindmap'; -// import * as mindmapDb from '../diagrams/mindmap/mindmapDb'; -// import { mindmapDetector } from '../diagrams/mindmap/mindmapDetector'; -// import mindmapRenderer from '../diagrams/mindmap/mindmapRenderer'; -// import mindmapStyles from '../diagrams/mindmap/styles'; +import { registerDiagram } from './diagramAPI'; // @ts-ignore: TODO Fix ts errors import gitGraphParser from '../diagrams/git/parser/gitGraph'; @@ -106,17 +94,15 @@ import { setConfig } from '../config'; import errorRenderer from '../diagrams/error/errorRenderer'; import errorStyles from '../diagrams/error/styles'; -const registerDiagramAndDetector = ( - id: string, - diagram: DiagramDefinition, - detector: DiagramDetector -) => { - registerDiagram(id, diagram); - registerDetector(id, detector); -}; - +let hasLoadedDiagrams = false; export const addDiagrams = () => { - registerDiagramAndDetector( + if (hasLoadedDiagrams) { + return; + } + // This is added here to avoid race-conditions. + // We could optimize the loading logic somehow. + hasLoadedDiagrams = true; + registerDiagram( 'error', // Special diagram with error messages but setup as a regular diagram { @@ -140,7 +126,7 @@ export const addDiagrams = () => { (text) => text.toLowerCase().trim() === 'error' ); - registerDiagramAndDetector( + registerDiagram( 'c4', { parser: c4Parser, @@ -153,7 +139,7 @@ export const addDiagrams = () => { }, c4Detector ); - registerDiagramAndDetector( + registerDiagram( 'class', { parser: classParser, @@ -170,7 +156,7 @@ export const addDiagrams = () => { }, classDetector ); - registerDiagramAndDetector( + registerDiagram( 'classDiagram', { parser: classParser, @@ -187,7 +173,7 @@ export const addDiagrams = () => { }, classDetectorV2 ); - registerDiagramAndDetector( + registerDiagram( 'er', { parser: erParser, @@ -197,7 +183,7 @@ export const addDiagrams = () => { }, erDetector ); - registerDiagramAndDetector( + registerDiagram( 'gantt', { parser: ganttParser, @@ -207,7 +193,7 @@ export const addDiagrams = () => { }, ganttDetector ); - registerDiagramAndDetector( + registerDiagram( 'info', { parser: infoParser, @@ -217,7 +203,7 @@ export const addDiagrams = () => { }, infoDetector ); - registerDiagramAndDetector( + registerDiagram( 'pie', { parser: pieParser, @@ -227,7 +213,7 @@ export const addDiagrams = () => { }, pieDetector ); - registerDiagramAndDetector( + registerDiagram( 'requirement', { parser: requirementParser, @@ -237,7 +223,7 @@ export const addDiagrams = () => { }, requirementDetector ); - registerDiagramAndDetector( + registerDiagram( 'sequence', { parser: sequenceParser, @@ -260,7 +246,7 @@ export const addDiagrams = () => { }, sequenceDetector ); - registerDiagramAndDetector( + registerDiagram( 'state', { parser: stateParser, @@ -277,7 +263,7 @@ export const addDiagrams = () => { }, stateDetector ); - registerDiagramAndDetector( + registerDiagram( 'stateDiagram', { parser: stateParser, @@ -294,7 +280,7 @@ export const addDiagrams = () => { }, stateDetectorV2 ); - registerDiagramAndDetector( + registerDiagram( 'journey', { parser: journeyParser, @@ -309,7 +295,7 @@ export const addDiagrams = () => { journeyDetector ); - registerDiagramAndDetector( + registerDiagram( 'flowchart', { parser: flowParser, @@ -329,7 +315,7 @@ export const addDiagrams = () => { }, flowDetector ); - registerDiagramAndDetector( + registerDiagram( 'flowchart-v2', { parser: flowParser, @@ -350,14 +336,9 @@ export const addDiagrams = () => { }, flowDetectorV2 ); - registerDiagramAndDetector( + registerDiagram( 'gitGraph', { parser: gitGraphParser, db: gitGraphDb, renderer: gitGraphRenderer, styles: gitGraphStyles }, gitGraphDetector ); - // registerDiagram( - // 'mindmap', - // { parser: mindmapParser, db: mindmapDb, renderer: mindmapRenderer, styles: mindmapStyles }, - // mindmapDetector - // ); }; diff --git a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts index 584a36fa3..ea546fbb6 100644 --- a/packages/mermaid/src/diagram-api/diagramAPI.spec.ts +++ b/packages/mermaid/src/diagram-api/diagramAPI.spec.ts @@ -1,6 +1,7 @@ -import { detectType, DiagramDetector } from './detectType'; -import { getDiagram, registerDiagram, registerDetector } from './diagramAPI'; +import { detectType } from './detectType'; +import { getDiagram, registerDiagram } from './diagramAPI'; import { addDiagrams } from './diagram-orchestration'; +import { DiagramDetector } from './types'; addDiagrams(); @@ -15,17 +16,22 @@ describe('DiagramAPI', () => { it('should handle diagram registrations', () => { expect(() => getDiagram('loki')).toThrow(); - expect(() => detectType('loki diagram')).not.toThrow(); // TODO: #3391 + expect(() => detectType('loki diagram')).toThrow( + 'No diagram type detected for text: loki diagram' + ); const detector: DiagramDetector = (str: string) => { return str.match('loki') !== null; }; - registerDetector('loki', detector); - registerDiagram('loki', { - db: {}, - parser: {}, - renderer: {}, - styles: {}, - }); + registerDiagram( + 'loki', + { + db: {}, + parser: {}, + renderer: {}, + styles: {}, + }, + detector + ); expect(getDiagram('loki')).not.toBeNull(); expect(detectType('loki diagram')).toBe('loki'); }); diff --git a/packages/mermaid/src/diagram-api/diagramAPI.ts b/packages/mermaid/src/diagram-api/diagramAPI.ts index 002619bbb..2bc8091ec 100644 --- a/packages/mermaid/src/diagram-api/diagramAPI.ts +++ b/packages/mermaid/src/diagram-api/diagramAPI.ts @@ -1,10 +1,10 @@ -import { addDetector, DiagramDetector as _DiagramDetector } from './detectType'; +import { addDetector } from './detectType'; import { log as _log, setLogLevel as _setLogLevel } from '../logger'; import { getConfig as _getConfig } from '../config'; import { sanitizeText as _sanitizeText } from '../diagrams/common/common'; -import { MermaidConfig } from '../config.type'; import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox'; import { addStylesForDiagram } from '../styles'; +import { DiagramDefinition, DiagramDetector } from './types'; /* Packaging and exposing resources for externa diagrams so that they can import @@ -13,41 +13,19 @@ import { addStylesForDiagram } from '../styles'; */ export const log = _log; export const setLogLevel = _setLogLevel; -export type DiagramDetector = _DiagramDetector; export const getConfig = _getConfig; export const sanitizeText = (text: string) => _sanitizeText(text, getConfig()); export const setupGraphViewbox = _setupGraphViewbox; -export interface InjectUtils { - _log: any; - _setLogLevel: any; - _getConfig: any; - _sanitizeText: any; - _setupGraphViewbox: any; -} - -export interface DiagramDefinition { - db: any; - renderer: any; - parser: any; - styles: any; - init?: (config: MermaidConfig) => void; - injectUtils?: (utils: InjectUtils) => void; -} - const diagrams: Record = {}; -const connectCallbacks: Record = {}; // TODO fix, eslint-disable-line @typescript-eslint/no-explicit-any export interface Detectors { [key: string]: DiagramDetector; } -export const registerDetector = (id: string, detector: DiagramDetector) => { - addDetector(id, detector, null); -}; - export const registerDiagram = ( id: string, diagram: DiagramDefinition, + detector?: DiagramDetector, callback?: ( _log: any, _setLogLevel: any, @@ -57,9 +35,12 @@ export const registerDiagram = ( ) => void ) => { if (diagrams[id]) { - log.warn(`Diagram ${id} already registered.`); + throw new Error(`Diagram ${id} already registered.`); } diagrams[id] = diagram; + if (detector) { + addDetector(id, detector); + } addStylesForDiagram(id, diagram.styles); if (typeof callback !== 'undefined') { callback(log, setLogLevel, getConfig, sanitizeText, setupGraphViewbox); @@ -72,19 +53,3 @@ export const getDiagram = (name: string): DiagramDefinition => { } throw new Error(`Diagram ${name} not found.`); }; - -/** - * - * @param sScriptSrc - */ -export const loadDiagram = (sScriptSrc: string) => - new Promise((resolve) => { - const oHead = document.getElementsByTagName('HEAD')[0]; - const oScript = document.createElement('script'); - oScript.type = 'text/javascript'; - oScript.src = sScriptSrc; - oHead.appendChild(oScript); - oScript.onload = () => { - resolve(true); - }; - }); diff --git a/packages/mermaid/src/diagram-api/text-wrap b/packages/mermaid/src/diagram-api/text-wrap deleted file mode 100644 index 173baecec..000000000 --- a/packages/mermaid/src/diagram-api/text-wrap +++ /dev/null @@ -1,227 +0,0 @@ -export const lineBreakRegex = //gi; - -/** - * Caches results of functions based on input - * - * @param {Function} fn Function to run - * @param {Function} resolver Function that resolves to an ID given arguments the `fn` takes - * @returns {Function} An optimized caching function - */ -const memoize = (fn, resolver) => { - let cache = {}; - return (...args) => { - let n = resolver ? resolver.apply(this, args) : args[0]; - if (n in cache) { - return cache[n]; - } else { - let result = fn(...args); - cache[n] = result; - return result; - } - }; -}; -/** - * This calculates the width of the given text, font size and family. - * - * @param {any} text - The text to calculate the width of - * @param {any} config - The config for fontSize, fontFamily, and fontWeight all impacting the resulting size - * @returns {any} - The width for the given text - */ -export const calculateTextWidth = function (text, config) { - config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config); - return calculateTextDimensions(text, config).width; -}; - -export const getTextObj = function () { - return { - x: 0, - y: 0, - fill: undefined, - anchor: 'start', - style: '#666', - width: 100, - height: 100, - textMargin: 0, - rx: 0, - ry: 0, - valign: undefined, - }; -}; - -/** - * Adds text to an element - * - * @param {SVGElement} elem Element to add text to - * @param {{ - * text: string; - * x: number; - * y: number; - * anchor: 'start' | 'middle' | 'end'; - * fontFamily: string; - * fontSize: string | number; - * fontWeight: string | number; - * fill: string; - * class: string | undefined; - * textMargin: number; - * }} textData - * @returns {SVGTextElement} Text element with given styling and content - */ -export const drawSimpleText = function (elem, textData) { - // Remove and ignore br:s - const nText = textData.text.replace(lineBreakRegex, ' '); - - const textElem = elem.append('text'); - textElem.attr('x', textData.x); - textElem.attr('y', textData.y); - textElem.style('text-anchor', textData.anchor); - textElem.style('font-family', textData.fontFamily); - textElem.style('font-size', textData.fontSize); - textElem.style('font-weight', textData.fontWeight); - textElem.attr('fill', textData.fill); - if (typeof textData.class !== 'undefined') { - textElem.attr('class', textData.class); - } - - const span = textElem.append('tspan'); - span.attr('x', textData.x + textData.textMargin * 2); - span.attr('fill', textData.fill); - span.text(nText); - - return textElem; -}; - -/** - * This calculates the dimensions of the given text, font size, font family, font weight, and margins. - * - * @param {any} text - The text to calculate the width of - * @param {any} config - The config for fontSize, fontFamily, fontWeight, and margin all impacting - * the resulting size - * @returns - The width for the given text - */ -export const calculateTextDimensions = memoize( - function (text, config) { - config = Object.assign({ fontSize: 12, fontWeight: 400, fontFamily: 'Arial' }, config); - const { fontSize, fontFamily, fontWeight } = config; - if (!text) { - return { width: 0, height: 0 }; - } - - // We can't really know if the user supplied font family will render on the user agent; - // thus, we'll take the max width between the user supplied font family, and a default - // of sans-serif. - const fontFamilies = ['sans-serif', fontFamily]; - const lines = text.split(common.lineBreakRegex); - let dims = []; - - const body = select('body'); - // We don't want to leak DOM elements - if a removal operation isn't available - // for any reason, do not continue. - if (!body.remove) { - return { width: 0, height: 0, lineHeight: 0 }; - } - - const g = body.append('svg'); - - for (let fontFamily of fontFamilies) { - let cheight = 0; - let dim = { width: 0, height: 0, lineHeight: 0 }; - for (let line of lines) { - const textObj = getTextObj(); - textObj.text = line; - const textElem = drawSimpleText(g, textObj) - .style('font-size', fontSize) - .style('font-weight', fontWeight) - .style('font-family', fontFamily); - - let bBox = (textElem._groups || textElem)[0][0].getBBox(); - dim.width = Math.round(Math.max(dim.width, bBox.width)); - cheight = Math.round(bBox.height); - dim.height += cheight; - dim.lineHeight = Math.round(Math.max(dim.lineHeight, cheight)); - } - dims.push(dim); - } - - g.remove(); - - let index = - isNaN(dims[1].height) || - isNaN(dims[1].width) || - isNaN(dims[1].lineHeight) || - (dims[0].height > dims[1].height && - dims[0].width > dims[1].width && - dims[0].lineHeight > dims[1].lineHeight) - ? 0 - : 1; - return dims[index]; - }, - (text, config) => `${text}-${config.fontSize}-${config.fontWeight}-${config.fontFamily}` -); - -const breakString = memoize( - (word, maxWidth, hyphenCharacter = '-', config) => { - config = Object.assign( - { fontSize: 12, fontWeight: 400, fontFamily: 'Arial', margin: 0 }, - config - ); - const characters = word.split(''); - const lines = []; - let currentLine = ''; - characters.forEach((character, index) => { - const nextLine = `${currentLine}${character}`; - const lineWidth = calculateTextWidth(nextLine, config); - if (lineWidth >= maxWidth) { - const currentCharacter = index + 1; - const isLastLine = characters.length === currentCharacter; - const hyphenatedNextLine = `${nextLine}${hyphenCharacter}`; - lines.push(isLastLine ? nextLine : hyphenatedNextLine); - currentLine = ''; - } else { - currentLine = nextLine; - } - }); - return { hyphenatedStrings: lines, remainingWord: currentLine }; - }, - (word, maxWidth, hyphenCharacter = '-', config) => - `${word}-${maxWidth}-${hyphenCharacter}-${config.fontSize}-${config.fontWeight}-${config.fontFamily}` -); - -export const wrapLabel = memoize( - (label, maxWidth, config) => { - if (!label) { - return label; - } - config = Object.assign( - { fontSize: 12, fontWeight: 400, fontFamily: 'Arial', joinWith: '
' }, - config - ); - if (lineBreakRegex.test(label)) { - return label; - } - const words = label.split(' '); - const completedLines = []; - let nextLine = ''; - words.forEach((word, index) => { - const wordLength = calculateTextWidth(`${word} `, config); - const nextLineLength = calculateTextWidth(nextLine, config); - if (wordLength > maxWidth) { - const { hyphenatedStrings, remainingWord } = breakString(word, maxWidth, '-', config); - completedLines.push(nextLine, ...hyphenatedStrings); - nextLine = remainingWord; - } else if (nextLineLength + wordLength >= maxWidth) { - completedLines.push(nextLine); - nextLine = word; - } else { - nextLine = [nextLine, word].filter(Boolean).join(' '); - } - const currentWord = index + 1; - const isLastWord = currentWord === words.length; - if (isLastWord) { - completedLines.push(nextLine); - } - }); - return completedLines.filter((line) => line !== '').join(config.joinWith); - }, - (label, maxWidth, config) => - `${label}-${maxWidth}-${config.fontSize}-${config.fontWeight}-${config.fontFamily}-${config.joinWith}` -); diff --git a/packages/mermaid/src/diagram-api/types.ts b/packages/mermaid/src/diagram-api/types.ts new file mode 100644 index 000000000..30ff25969 --- /dev/null +++ b/packages/mermaid/src/diagram-api/types.ts @@ -0,0 +1,26 @@ +import { MermaidConfig } from '../config.type'; + +export interface InjectUtils { + _log: any; + _setLogLevel: any; + _getConfig: any; + _sanitizeText: any; + _setupGraphViewbox: any; +} + +export interface DiagramDefinition { + db: any; + renderer: any; + parser: any; + styles: any; + init?: (config: MermaidConfig) => void; + injectUtils?: (utils: InjectUtils) => void; +} + +export interface DetectorRecord { + detector: DiagramDetector; + loader?: DiagramLoader; +} + +export type DiagramDetector = (text: string, config?: MermaidConfig) => boolean; +export type DiagramLoader = (() => Promise<{ id: string; diagram: DiagramDefinition }>) | null; diff --git a/packages/mermaid/src/diagrams/c4/c4Detector.ts b/packages/mermaid/src/diagrams/c4/c4Detector.ts index 2be62bff1..49ba95b8e 100644 --- a/packages/mermaid/src/diagrams/c4/c4Detector.ts +++ b/packages/mermaid/src/diagrams/c4/c4Detector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const c4Detector: DiagramDetector = (txt) => { return txt.match(/^\s*C4Context|C4Container|C4Component|C4Dynamic|C4Deployment/) !== null; diff --git a/packages/mermaid/src/diagrams/class/classDetector-V2.ts b/packages/mermaid/src/diagrams/class/classDetector-V2.ts index a0e270100..d65caf9a8 100644 --- a/packages/mermaid/src/diagrams/class/classDetector-V2.ts +++ b/packages/mermaid/src/diagrams/class/classDetector-V2.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const classDetectorV2: DiagramDetector = (txt, config) => { // If we have confgured to use dagre-wrapper then we should return true in this function for classDiagram code thus making it use the new class diagram diff --git a/packages/mermaid/src/diagrams/class/classDetector.ts b/packages/mermaid/src/diagrams/class/classDetector.ts index 19d8bd2f5..ef6389a60 100644 --- a/packages/mermaid/src/diagrams/class/classDetector.ts +++ b/packages/mermaid/src/diagrams/class/classDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const classDetector: DiagramDetector = (txt, config) => { // If we have confgured to use dagre-wrapper then we should never return true in this function diff --git a/packages/mermaid/src/diagrams/er/erDetector.ts b/packages/mermaid/src/diagrams/er/erDetector.ts index a17eafb81..5a87a949e 100644 --- a/packages/mermaid/src/diagrams/er/erDetector.ts +++ b/packages/mermaid/src/diagrams/er/erDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const erDetector: DiagramDetector = (txt) => { return txt.match(/^\s*erDiagram/) !== null; diff --git a/packages/mermaid/src/diagrams/flowchart/flowDb.js b/packages/mermaid/src/diagrams/flowchart/flowDb.js index 192da23d3..5aa203225 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDb.js +++ b/packages/mermaid/src/diagrams/flowchart/flowDb.js @@ -429,8 +429,7 @@ export const clear = function (ver = 'gen-1') { vertices = {}; classes = {}; edges = []; - funs = []; - funs.push(setupToolTips); + funs = [setupToolTips]; subGraphs = []; subGraphLookup = {}; subCount = 0; diff --git a/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts b/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts index f73748c79..c2ec736c7 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDetector-v2.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const flowDetectorV2: DiagramDetector = (txt, config) => { // If we have confgured to use dagre-wrapper then we should return true in this function for graph code thus making it use the new flowchart diagram diff --git a/packages/mermaid/src/diagrams/flowchart/flowDetector.ts b/packages/mermaid/src/diagrams/flowchart/flowDetector.ts index edc9096c0..740d12847 100644 --- a/packages/mermaid/src/diagrams/flowchart/flowDetector.ts +++ b/packages/mermaid/src/diagrams/flowchart/flowDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const flowDetector: DiagramDetector = (txt, config) => { // If we have confired to only use new flow charts this function shohuld always return false diff --git a/packages/mermaid/src/diagrams/gantt/ganttDetector.ts b/packages/mermaid/src/diagrams/gantt/ganttDetector.ts index 926792dcf..5de167010 100644 --- a/packages/mermaid/src/diagrams/gantt/ganttDetector.ts +++ b/packages/mermaid/src/diagrams/gantt/ganttDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const ganttDetector: DiagramDetector = (txt) => { return txt.match(/^\s*gantt/) !== null; diff --git a/packages/mermaid/src/diagrams/git/gitGraphDetector.ts b/packages/mermaid/src/diagrams/git/gitGraphDetector.ts index 1c0a015e7..f890501a5 100644 --- a/packages/mermaid/src/diagrams/git/gitGraphDetector.ts +++ b/packages/mermaid/src/diagrams/git/gitGraphDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const gitGraphDetector: DiagramDetector = (txt) => { return txt.match(/^\s*gitGraph/) !== null; diff --git a/packages/mermaid/src/diagrams/info/infoDetector.ts b/packages/mermaid/src/diagrams/info/infoDetector.ts index 68f2ac794..8bccb578f 100644 --- a/packages/mermaid/src/diagrams/info/infoDetector.ts +++ b/packages/mermaid/src/diagrams/info/infoDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const infoDetector: DiagramDetector = (txt) => { return txt.match(/^\s*info/) !== null; diff --git a/packages/mermaid/src/diagrams/pie/pieDetector.ts b/packages/mermaid/src/diagrams/pie/pieDetector.ts index 1e122b0e0..65a011c7a 100644 --- a/packages/mermaid/src/diagrams/pie/pieDetector.ts +++ b/packages/mermaid/src/diagrams/pie/pieDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const pieDetector: DiagramDetector = (txt) => { return txt.match(/^\s*pie/) !== null; diff --git a/packages/mermaid/src/diagrams/requirement/requirementDetector.ts b/packages/mermaid/src/diagrams/requirement/requirementDetector.ts index 2e1aa93ae..164da6c1a 100644 --- a/packages/mermaid/src/diagrams/requirement/requirementDetector.ts +++ b/packages/mermaid/src/diagrams/requirement/requirementDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const requirementDetector: DiagramDetector = (txt) => { return txt.match(/^\s*requirement(Diagram)?/) !== null; diff --git a/packages/mermaid/src/diagrams/sequence/sequenceDetector.ts b/packages/mermaid/src/diagrams/sequence/sequenceDetector.ts index e68433255..52640b134 100644 --- a/packages/mermaid/src/diagrams/sequence/sequenceDetector.ts +++ b/packages/mermaid/src/diagrams/sequence/sequenceDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const sequenceDetector: DiagramDetector = (txt) => { return txt.match(/^\s*sequenceDiagram/) !== null; diff --git a/packages/mermaid/src/diagrams/state/stateDetector-V2.ts b/packages/mermaid/src/diagrams/state/stateDetector-V2.ts index 8082a47bd..7fd9633c6 100644 --- a/packages/mermaid/src/diagrams/state/stateDetector-V2.ts +++ b/packages/mermaid/src/diagrams/state/stateDetector-V2.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const stateDetectorV2: DiagramDetector = (text, config) => { if (text.match(/^\s*stateDiagram-v2/) !== null) return true; diff --git a/packages/mermaid/src/diagrams/state/stateDetector.ts b/packages/mermaid/src/diagrams/state/stateDetector.ts index 79dd6586b..614f327c2 100644 --- a/packages/mermaid/src/diagrams/state/stateDetector.ts +++ b/packages/mermaid/src/diagrams/state/stateDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const stateDetector: DiagramDetector = (txt, config) => { // If we have confired to only use new state diagrams this function should always return false diff --git a/packages/mermaid/src/diagrams/user-journey/journeyDetector.ts b/packages/mermaid/src/diagrams/user-journey/journeyDetector.ts index 77c8688ae..535e7be9d 100644 --- a/packages/mermaid/src/diagrams/user-journey/journeyDetector.ts +++ b/packages/mermaid/src/diagrams/user-journey/journeyDetector.ts @@ -1,4 +1,4 @@ -import type { DiagramDetector } from '../../diagram-api/detectType'; +import type { DiagramDetector } from '../../diagram-api/types'; export const journeyDetector: DiagramDetector = (txt) => { return txt.match(/^\s*journey/) !== null; diff --git a/packages/mermaid/src/mermaid.ts b/packages/mermaid/src/mermaid.ts index 7ddbc9f06..ae6c62547 100644 --- a/packages/mermaid/src/mermaid.ts +++ b/packages/mermaid/src/mermaid.ts @@ -7,14 +7,6 @@ import { log } from './logger'; import utils from './utils'; import { mermaidAPI } from './mermaidAPI'; import { addDetector } from './diagram-api/detectType'; -import { - registerDiagram, - DiagramDefinition, - setLogLevel, - getConfig, - setupGraphViewbox, - sanitizeText, -} from './diagram-api/diagramAPI'; import { isDetailedError } from './utils'; /** @@ -53,17 +45,16 @@ const init = async function ( callback?: Function ) { try { - log.info('Detectors in init', mermaid.detectors); // eslint-disable-line const conf = mermaidAPI.getConfig(); - if (typeof conf.lazyLoadedDiagrams !== 'undefined' && conf.lazyLoadedDiagrams.length > 0) { - for (let i = 0; i < conf.lazyLoadedDiagrams.length; i++) { - const { id, detector, loadDiagram } = await import(conf.lazyLoadedDiagrams[i]); - addDetector(id, detector, loadDiagram); - } + if (conf?.lazyLoadedDiagrams && conf.lazyLoadedDiagrams.length > 0) { + // Load all lazy loaded diagrams in parallel + await Promise.allSettled( + conf.lazyLoadedDiagrams.map(async (diagram: string) => { + const { id, detector, loadDiagram } = await import(diagram); + addDetector(id, detector, loadDiagram); + }) + ); } - mermaid.detectors.forEach(({ id, detector, path }) => { - addDetector(id, detector, path); - }); await initThrowsErrors(config, nodes, callback); } catch (e) { log.warn('Syntax Error rendering'); @@ -218,22 +209,6 @@ const parse = (txt: string) => { return mermaidAPI.parse(txt, mermaid.parseError); }; -const connectDiagram = ( - id: string, - diagram: DiagramDefinition, - callback: ( - _log: any, - _setLogLevel: any, - _getConfig: any, - _sanitizeText: any, - _setupGraphViewbox: any - ) => void -) => { - registerDiagram(id, diagram, callback); - // Todo move this connect call to after the diagram is actually loaded. - callback(log, setLogLevel, getConfig, sanitizeText, setupGraphViewbox); -}; - const mermaid: { startOnLoad: boolean; diagrams: any; @@ -247,9 +222,6 @@ const mermaid: { initialize: typeof initialize; contentLoaded: typeof contentLoaded; setParseErrorHandler: typeof setParseErrorHandler; - // Array of functions to use for detecting diagram types - detectors: Array; // eslint-disable-line @typescript-eslint/no-explicit-any - connectDiagram: (id: string, diagram: DiagramDefinition, callback: (id: string) => void) => void; } = { startOnLoad: true, diagrams: {}, @@ -262,8 +234,6 @@ const mermaid: { parseError: undefined, contentLoaded, setParseErrorHandler, - detectors: [], - connectDiagram: connectDiagram, }; export default mermaid; diff --git a/packages/mermaid/src/mermaidAPI.ts b/packages/mermaid/src/mermaidAPI.ts index f9544ee44..7c967e5fd 100644 --- a/packages/mermaid/src/mermaidAPI.ts +++ b/packages/mermaid/src/mermaidAPI.ts @@ -18,7 +18,6 @@ import { compile, serialize, stringify } from 'stylis'; import pkg from '../package.json'; import * as configApi from './config'; import { addDiagrams } from './diagram-api/diagram-orchestration'; -import { addDetector } from './diagram-api/detectType'; import classDb from './diagrams/class/classDb'; import flowDb from './diagrams/flowchart/flowDb'; import flowRenderer from './diagrams/flowchart/flowRenderer'; @@ -34,18 +33,13 @@ import DOMPurify from 'dompurify'; import { MermaidConfig } from './config.type'; import { evaluate } from './diagrams/common/common'; -let hasLoadedDiagrams = false; - /** * @param text * @param parseError */ // eslint-disable-next-line @typescript-eslint/ban-types function parse(text: string, parseError?: Function): boolean { - if (!hasLoadedDiagrams) { - addDiagrams(); - hasLoadedDiagrams = true; - } + addDiagrams(); const diagram = new Diagram(text, parseError); return diagram.parse(text, parseError); } @@ -122,10 +116,7 @@ const render = async function ( cb: (svgCode: string, bindFunctions?: (element: Element) => void) => void, container?: Element ): Promise { - if (!hasLoadedDiagrams) { - addDiagrams(); - hasLoadedDiagrams = true; - } + addDiagrams(); configApi.reset(); text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;; const graphInit = utils.detectInit(text); @@ -486,11 +477,7 @@ async function initialize(options: MermaidConfig) { typeof options === 'object' ? configApi.setSiteConfig(options) : configApi.getSiteConfig(); setLogLevel(config.logLevel); - - if (!hasLoadedDiagrams) { - addDiagrams(); - hasLoadedDiagrams = true; - } + addDiagrams(); } export const mermaidAPI = Object.freeze({ diff --git a/todo-fix-root-level-tsconfig.json b/todo-fix-root-level-tsconfig.json deleted file mode 100644 index 0ffa0002e..000000000 --- a/todo-fix-root-level-tsconfig.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Projects */ - // "incremental": true /* Enable incremental compilation */, - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ES6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, - "lib": [ - "DOM", - "ES2021" - ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ - // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - - /* Modules */ - "module": "ES6" /* Specify what module code is generated. */, - "rootDir": "./src" /* Specify the root folder within your source files. */, - "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, - // "baseUrl": "./src" /* Specify the base directory to resolve non-relative module names. */, - // "paths": {} /* Specify a set of entries that re-map imports to additional lookup locations. */, - // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [] /* Specify multiple folders that act like `./node_modules/@types`. */, - "types": [ - "vitest/globals" - ] /* Specify type package names to be included without being referenced in a source file. */, - - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - "resolveJsonModule": true /* Enable importing .json files */, - // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */, - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ - - /* Emit */ - "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./dist" /* Specify an output folder for all emitted files. */, - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, - - /* Type Checking */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - }, - "include": ["./src/**/*.ts", "./package.json"] -}