diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 493bacaf74..f59c8af31d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -62,8 +62,22 @@ jobs: ERROR_MESSAGE+=' `pnpm run --filter mermaid types:build-config`' ERROR_MESSAGE+=' on your local machine.' echo "::error title=Lint failure::${ERROR_MESSAGE}" - # make sure to return an error exitcode so that GitHub actions shows a red-cross - exit 1 + # make sure to return an error exitcode so that GitHub actions shows a red-cross + exit 1 + fi + + - name: Verify no circular dependencies + working-directory: ./packages/mermaid + shell: bash + run: | + if ! pnpm run --filter mermaid checkCircle; then + ERROR_MESSAGE='Circular dependency detected.' + ERROR_MESSAGE+=' This should be fixed by removing the circular dependency.' + ERROR_MESSAGE+=' Run `pnpm run --filter mermaid checkCircle` on your local machine' + ERROR_MESSAGE+=' to see the circular dependency.' + echo "::error title=Lint failure::${ERROR_MESSAGE}" + # make sure to return an error exitcode so that GitHub actions shows a red-cross + exit 1 fi - name: Verify Docs diff --git a/cSpell.json b/cSpell.json index af7a9ca46f..9e10e41d0c 100644 --- a/cSpell.json +++ b/cSpell.json @@ -106,6 +106,7 @@ "rects", "reda", "redmine", + "regexes", "rehype", "roledescription", "rozhkov", diff --git a/packages/mermaid/.madgerc b/packages/mermaid/.madgerc new file mode 100644 index 0000000000..1a558d9e62 --- /dev/null +++ b/packages/mermaid/.madgerc @@ -0,0 +1,22 @@ +{ + "detectiveOptions": { + "ts": { + "skipTypeImports": true + }, + "es6": { + "skipTypeImports": true + } + }, + "fileExtensions": [ + "js", + "ts" + ], + "excludeRegExp": [ + "node_modules", + "docs", + "vitepress", + "detector", + "Detector" + ], + "tsConfig": "./tsconfig.json" +} diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index d9eb8701f7..029b182657 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -38,6 +38,7 @@ "docs:verify-version": "ts-node-esm scripts/update-release-version.mts --verify", "types:build-config": "ts-node-esm --transpileOnly scripts/create-types-from-json-schema.mts", "types:verify-config": "ts-node-esm scripts/create-types-from-json-schema.mts --verify", + "checkCircle": "npx madge --circular ./src", "release": "pnpm build", "prepublishOnly": "cpy '../../README.*' ./ --cwd=. && pnpm -w run build" }, diff --git a/packages/mermaid/src/diagram-api/detectType.ts b/packages/mermaid/src/diagram-api/detectType.ts index 8351a67dff..aae9131551 100644 --- a/packages/mermaid/src/diagram-api/detectType.ts +++ b/packages/mermaid/src/diagram-api/detectType.ts @@ -6,14 +6,10 @@ import type { DiagramLoader, ExternalDiagramDefinition, } from './types.js'; -import { frontMatterRegex } from './frontmatter.js'; -import { getDiagram, registerDiagram } from './diagramAPI.js'; +import { anyCommentRegex, directiveRegex, frontMatterRegex } from './regexes.js'; import { UnknownDiagramError } from '../errors.js'; -const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi; -const anyComment = /\s*%%.*\n/gm; - -const detectors: Record = {}; +export const detectors: Record = {}; /** * Detects the type of the graph text. @@ -38,7 +34,10 @@ const detectors: Record = {}; * @returns A graph definition key */ export const detectType = function (text: string, config?: MermaidConfig): string { - text = text.replace(frontMatterRegex, '').replace(directive, '').replace(anyComment, '\n'); + text = text + .replace(frontMatterRegex, '') + .replace(directiveRegex, '') + .replace(anyCommentRegex, '\n'); for (const [key, { detector }] of Object.entries(detectors)) { const diagram = detector(text, config); if (diagram) { @@ -70,39 +69,6 @@ export const registerLazyLoadedDiagrams = (...diagrams: ExternalDiagramDefinitio } }; -export const loadRegisteredDiagrams = async () => { - log.debug(`Loading registered diagrams`); - // Load all lazy loaded diagrams in parallel - const results = await Promise.allSettled( - Object.entries(detectors).map(async ([key, { detector, loader }]) => { - if (loader) { - try { - getDiagram(key); - } catch (error) { - try { - // Register diagram if it is not already registered - const { diagram, id } = await loader(); - registerDiagram(id, diagram, detector); - } catch (err) { - // Remove failed diagram from detectors - log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`); - delete detectors[key]; - throw err; - } - } - } - }) - ); - const failed = results.filter((result) => result.status === 'rejected'); - if (failed.length > 0) { - log.error(`Failed to load ${failed.length} external diagrams`); - for (const res of failed) { - log.error(res); - } - throw new Error(`Failed to load ${failed.length} external diagrams`); - } -}; - export const addDetector = (key: string, detector: DiagramDetector, loader?: DiagramLoader) => { if (detectors[key]) { log.error(`Detector with key ${key} already exists`); diff --git a/packages/mermaid/src/diagram-api/diagramAPI.ts b/packages/mermaid/src/diagram-api/diagramAPI.ts index 3edd982bb4..00da66ffe4 100644 --- a/packages/mermaid/src/diagram-api/diagramAPI.ts +++ b/packages/mermaid/src/diagram-api/diagramAPI.ts @@ -4,7 +4,7 @@ import { getConfig as _getConfig } from '../config.js'; import { sanitizeText as _sanitizeText } from '../diagrams/common/common.js'; import { setupGraphViewbox as _setupGraphViewbox } from '../setupGraphViewbox.js'; import { addStylesForDiagram } from '../styles.js'; -import { DiagramDefinition, DiagramDetector } from './types.js'; +import type { DiagramDefinition, DiagramDetector } from './types.js'; import * as _commonDb from '../commonDb.js'; import { parseDirective as _parseDirective } from '../directiveUtils.js'; diff --git a/packages/mermaid/src/diagram-api/frontmatter.ts b/packages/mermaid/src/diagram-api/frontmatter.ts index f8d2e9c415..ab09487371 100644 --- a/packages/mermaid/src/diagram-api/frontmatter.ts +++ b/packages/mermaid/src/diagram-api/frontmatter.ts @@ -1,14 +1,8 @@ -import { DiagramDB } from './types.js'; +import type { DiagramDB } from './types.js'; +import { frontMatterRegex } from './regexes.js'; // The "* as yaml" part is necessary for tree-shaking import * as yaml from 'js-yaml'; -// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/). -// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10 -// Note that JS doesn't support the "\A" anchor, which means we can't use -// multiline mode. -// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents -export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s; - type FrontMatterMetadata = { title?: string; // Allows custom display modes. Currently used for compact mode in gantt charts. diff --git a/packages/mermaid/src/diagram-api/loadDiagram.ts b/packages/mermaid/src/diagram-api/loadDiagram.ts new file mode 100644 index 0000000000..c1b445bf64 --- /dev/null +++ b/packages/mermaid/src/diagram-api/loadDiagram.ts @@ -0,0 +1,36 @@ +import { log } from '../logger.js'; +import { detectors } from './detectType.js'; +import { getDiagram, registerDiagram } from './diagramAPI.js'; + +export const loadRegisteredDiagrams = async () => { + log.debug(`Loading registered diagrams`); + // Load all lazy loaded diagrams in parallel + const results = await Promise.allSettled( + Object.entries(detectors).map(async ([key, { detector, loader }]) => { + if (loader) { + try { + getDiagram(key); + } catch (error) { + try { + // Register diagram if it is not already registered + const { diagram, id } = await loader(); + registerDiagram(id, diagram, detector); + } catch (err) { + // Remove failed diagram from detectors + log.error(`Failed to load external diagram with key ${key}. Removing from detectors.`); + delete detectors[key]; + throw err; + } + } + } + }) + ); + const failed = results.filter((result) => result.status === 'rejected'); + if (failed.length > 0) { + log.error(`Failed to load ${failed.length} external diagrams`); + for (const res of failed) { + log.error(res); + } + throw new Error(`Failed to load ${failed.length} external diagrams`); + } +}; diff --git a/packages/mermaid/src/diagram-api/regexes.ts b/packages/mermaid/src/diagram-api/regexes.ts new file mode 100644 index 0000000000..bb688b9c2a --- /dev/null +++ b/packages/mermaid/src/diagram-api/regexes.ts @@ -0,0 +1,11 @@ +// Match Jekyll-style front matter blocks (https://jekyllrb.com/docs/front-matter/). +// Based on regex used by Jekyll: https://github.com/jekyll/jekyll/blob/6dd3cc21c40b98054851846425af06c64f9fb466/lib/jekyll/document.rb#L10 +// Note that JS doesn't support the "\A" anchor, which means we can't use +// multiline mode. +// Relevant YAML spec: https://yaml.org/spec/1.2.2/#914-explicit-documents +export const frontMatterRegex = /^-{3}\s*[\n\r](.*?)[\n\r]-{3}\s*[\n\r]+/s; + +export const directiveRegex = + /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi; + +export const anyCommentRegex = /\s*%%.*\n/gm; diff --git a/packages/mermaid/src/docs/package.json b/packages/mermaid/src/docs/package.json index a5cb5859c6..94ab7a535c 100644 --- a/packages/mermaid/src/docs/package.json +++ b/packages/mermaid/src/docs/package.json @@ -17,7 +17,8 @@ "dependencies": { "@vueuse/core": "^10.1.0", "jiti": "^1.18.2", - "vue": "^3.3" + "vue": "^3.3", + "mermaid": "workspace:^" }, "devDependencies": { "@iconify-json/carbon": "^1.1.16", diff --git a/packages/mermaid/src/mermaid.ts b/packages/mermaid/src/mermaid.ts index 30297e7b5e..d140ded4f6 100644 --- a/packages/mermaid/src/mermaid.ts +++ b/packages/mermaid/src/mermaid.ts @@ -7,11 +7,8 @@ import { MermaidConfig } from './config.type.js'; import { log } from './logger.js'; import utils from './utils.js'; import { mermaidAPI, ParseOptions, RenderResult } from './mermaidAPI.js'; -import { - registerLazyLoadedDiagrams, - loadRegisteredDiagrams, - detectType, -} from './diagram-api/detectType.js'; +import { registerLazyLoadedDiagrams, detectType } from './diagram-api/detectType.js'; +import { loadRegisteredDiagrams } from './diagram-api/loadDiagram.js'; import type { ParseErrorFunction } from './Diagram.js'; import { isDetailedError } from './utils.js'; import type { DetailedError } from './utils.js'; diff --git a/packages/mermaid/src/utils.ts b/packages/mermaid/src/utils.ts index 937f3f8f84..0c5eca2aee 100644 --- a/packages/mermaid/src/utils.ts +++ b/packages/mermaid/src/utils.ts @@ -32,6 +32,7 @@ import assignWithDepth from './assignWithDepth.js'; import { MermaidConfig } from './config.type.js'; import memoize from 'lodash-es/memoize.js'; import merge from 'lodash-es/merge.js'; +import { directiveRegex } from './diagram-api/regexes.js'; export const ZERO_WIDTH_SPACE = '\u200b'; @@ -58,7 +59,7 @@ const d3CurveTypes = { curveStepAfter: curveStepAfter, curveStepBefore: curveStepBefore, }; -const directive = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi; + const directiveWithoutOpen = /\s*(?:(\w+)(?=:):|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi; @@ -163,10 +164,10 @@ export const detectDirective = function ( ); let match; const result = []; - while ((match = directive.exec(text)) !== null) { + while ((match = directiveRegex.exec(text)) !== null) { // This is necessary to avoid infinite loops with zero-width matches - if (match.index === directive.lastIndex) { - directive.lastIndex++; + if (match.index === directiveRegex.lastIndex) { + directiveRegex.lastIndex++; } if ( (match && !type) || diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8dc7ec8462..42a336739f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -434,6 +434,9 @@ importers: jiti: specifier: ^1.18.2 version: 1.18.2 + mermaid: + specifier: workspace:^ + version: link:../.. vue: specifier: ^3.3 version: 3.3.4 @@ -486,6 +489,9 @@ importers: jiti: specifier: ^1.18.2 version: 1.18.2 + mermaid: + specifier: workspace:^ + version: link:../.. vue: specifier: ^3.3 version: 3.3.4