From 8210e3c80acf6bf0941b78fd291d192af60edbf0 Mon Sep 17 00:00:00 2001 From: Tim Maffett Date: Thu, 5 May 2022 11:47:46 -0700 Subject: [PATCH 1/4] add setParseErrorHandler,check for undefined mermaidAPI before using --- src/mermaid.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/mermaid.js b/src/mermaid.js index 38391c941b..e2475c601c 100644 --- a/src/mermaid.js +++ b/src/mermaid.js @@ -188,18 +188,38 @@ if (typeof document !== 'undefined') { ); } +/** + * ## setParseErrorHandler Alternativet 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 {function (err, hash)} newParseErrorHandler New parseError() callback. + */ +const setParseErrorHandler = function (newParseErrorHandler) { + mermaid.parseError = newParseErrorHandler; +}; + const mermaid = { startOnLoad: true, htmlLabels: true, mermaidAPI, - parse: mermaidAPI.parse, - render: mermaidAPI.render, + parse: mermaidAPI != undefined ? mermaidAPI.parse : null, + render: mermaidAPI != undefined ? mermaidAPI.render : null, init, initialize, contentLoaded, + + setParseErrorHandler, }; export default mermaid; From 67b6414693634bdb2c2fb5104bef8eb19f42f9b1 Mon Sep 17 00:00:00 2001 From: Tim Maffett Date: Thu, 5 May 2022 11:48:53 -0700 Subject: [PATCH 2/4] fix parse() to match spec (return true if valid, false if invalid (and a parseError handler is defined)) --- src/mermaidAPI.js | 193 +++++++++++++++++++++++++--------------------- 1 file changed, 107 insertions(+), 86 deletions(-) diff --git a/src/mermaidAPI.js b/src/mermaidAPI.js index 030b2e6ac9..d985c39443 100644 --- a/src/mermaidAPI.js +++ b/src/mermaidAPI.js @@ -63,99 +63,120 @@ import getStyles from './styles'; import theme from './themes'; import utils, { directiveSanitizer, assignWithDepth, sanitizeCss } from './utils'; import DOMPurify from 'dompurify'; +import mermaid from './mermaid'; /** * @param text * @returns {any} */ function parse(text) { - text = text + '\n'; - const cnf = configApi.getConfig(); - const graphInit = utils.detectInit(text, cnf); - if (graphInit) { - reinitialize(graphInit); - log.info('reinit ', graphInit); - } - const graphType = utils.detectType(text, cnf); - let parser; - - log.debug('Type ' + graphType); - switch (graphType) { - case 'gitGraph': - gitGraphAst.clear(); - parser = gitGraphParser; - parser.parser.yy = gitGraphAst; - break; - case 'flowchart': - flowDb.clear(); - parser = flowParser; - parser.parser.yy = flowDb; - break; - case 'flowchart-v2': - flowDb.clear(); - parser = flowParser; - parser.parser.yy = flowDb; - break; - case 'sequence': - sequenceDb.clear(); - parser = sequenceParser; - parser.parser.yy = sequenceDb; - break; - case 'gantt': - parser = ganttParser; - parser.parser.yy = ganttDb; - break; - case 'class': - parser = classParser; - parser.parser.yy = classDb; - break; - case 'classDiagram': - parser = classParser; - parser.parser.yy = classDb; - break; - case 'state': - parser = stateParser; - parser.parser.yy = stateDb; - break; - case 'stateDiagram': - parser = stateParser; - parser.parser.yy = stateDb; - break; - case 'info': - log.debug('info info info'); - parser = infoParser; - parser.parser.yy = infoDb; - break; - case 'pie': - log.debug('pie'); - parser = pieParser; - parser.parser.yy = pieDb; - break; - case 'er': - log.debug('er'); - parser = erParser; - parser.parser.yy = erDb; - break; - case 'journey': - log.debug('Journey'); - parser = journeyParser; - parser.parser.yy = journeyDb; - break; - case 'requirement': - case 'requirementDiagram': - log.debug('RequirementDiagram'); - parser = requirementParser; - parser.parser.yy = requirementDb; - break; + var parseEncounteredException = false; + try { + text = text + '\n'; + const cnf = configApi.getConfig(); + const graphInit = utils.detectInit(text, cnf); + if (graphInit) { + reinitialize(graphInit); + log.info('reinit ', graphInit); + } + const graphType = utils.detectType(text, cnf); + let parser; + + log.debug('Type ' + graphType); + switch (graphType) { + case 'gitGraph': + gitGraphAst.clear(); + parser = gitGraphParser; + parser.parser.yy = gitGraphAst; + break; + case 'flowchart': + flowDb.clear(); + parser = flowParser; + parser.parser.yy = flowDb; + break; + case 'flowchart-v2': + flowDb.clear(); + parser = flowParser; + parser.parser.yy = flowDb; + break; + case 'sequence': + sequenceDb.clear(); + parser = sequenceParser; + parser.parser.yy = sequenceDb; + break; + case 'gantt': + parser = ganttParser; + parser.parser.yy = ganttDb; + break; + case 'class': + parser = classParser; + parser.parser.yy = classDb; + break; + case 'classDiagram': + parser = classParser; + parser.parser.yy = classDb; + break; + case 'state': + parser = stateParser; + parser.parser.yy = stateDb; + break; + case 'stateDiagram': + parser = stateParser; + parser.parser.yy = stateDb; + break; + case 'info': + log.debug('info info info'); + parser = infoParser; + parser.parser.yy = infoDb; + break; + case 'pie': + log.debug('pie'); + parser = pieParser; + parser.parser.yy = pieDb; + break; + case 'er': + log.debug('er'); + parser = erParser; + parser.parser.yy = erDb; + break; + case 'journey': + log.debug('Journey'); + parser = journeyParser; + parser.parser.yy = journeyDb; + break; + case 'requirement': + case 'requirementDiagram': + log.debug('RequirementDiagram'); + parser = requirementParser; + parser.parser.yy = requirementDb; + break; + } + parser.parser.yy.graphType = graphType; + parser.parser.yy.parseError = (str, hash) => { + const error = { str, hash }; + throw error; + }; + + parser.parse(text); + } catch (error) { + parseEncounteredException = true; + // Is this the correct way to access mermiad's parseError() + // method ? (or global.mermaid.parseError()) ? + if (mermaid.parseError) { + if (error.str != undefined) { + // handle case where error string and hash were + // wrapped in object like`const error = { str, hash };` + mermaid.parseError(error.str, error.hash); + } else { + // assume it is just error string and pass it on + mermaid.parseError(error); + } + } else { + // No mermaid.parseError() handler defined, so re-throw it + throw error; + } } - parser.parser.yy.graphType = graphType; - parser.parser.yy.parseError = (str, hash) => { - const error = { str, hash }; - throw error; - }; - - parser.parse(text); - return parser; + return !parseEncounteredException; } export const encodeEntities = function (text) { From 0df80d2b52f7939c91d6535559672c3c23f12315 Mon Sep 17 00:00:00 2001 From: Tim Maffett Date: Thu, 5 May 2022 11:49:27 -0700 Subject: [PATCH 3/4] fix parseError documentation to match reality --- docs/newDiagram.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/newDiagram.md b/docs/newDiagram.md index 995bdaa5ca..e64f374df4 100644 --- a/docs/newDiagram.md +++ b/docs/newDiagram.md @@ -29,7 +29,7 @@ statement In the extract of the grammar above, it is defined that a call to the setTitle method in the data object will be done when parsing and the title keyword is encountered. ```tip -Make sure that the `parseError` function for the parser is defined and calling `mermaidPAI.parseError`. This way a common way of detecting parse errors is provided for the end-user. +Make sure that the `parseError` function for the parser is defined and calling `mermaid.parseError`. This way a common way of detecting parse errors is provided for the end-user. ``` For more info look in the example diagram type: @@ -38,7 +38,7 @@ The `yy` object has the following function: ```javascript exports.parseError = function(err, hash){ - mermaidAPI.parseError(err, hash) + mermaid.parseError(err, hash) }; ``` From ca8080a371acdda6d2d7bb38541437d37af2eafb Mon Sep 17 00:00:00 2001 From: Tim Maffett Date: Thu, 5 May 2022 11:50:11 -0700 Subject: [PATCH 4/4] add more test for mermaidAPI.parse() to ensure it matches documented behavior --- src/mermaidAPI.spec.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/mermaidAPI.spec.js b/src/mermaidAPI.spec.js index b6ffc6be62..f329eb1075 100644 --- a/src/mermaidAPI.spec.js +++ b/src/mermaidAPI.spec.js @@ -1,3 +1,4 @@ +import mermaid from './mermaid'; import mermaidAPI from './mermaidAPI'; import { assignWithDepth } from './utils'; @@ -119,12 +120,28 @@ describe('when using mermaidAPI and ', function () { expect(mermaidAPI.getConfig().dompurifyConfig.ADD_ATTR).toEqual(['onclick']); }); }); - describe('checking validity of input ', function () { - it('it should throw for an invalid definiton', function () { + describe('test mermaidApi.parse() for checking validity of input ', function () { + mermaid.parseError = undefined; // ensure it parseError undefined + it('it should throw for an invalid definiton (with no mermaid.parseError() defined)', function () { + expect(mermaid.parseError).toEqual(undefined); expect(() => mermaidAPI.parse('this is not a mermaid diagram definition')).toThrow(); }); it('it should not throw for a valid definiton', function () { expect(() => mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).not.toThrow(); }); + it('it should return false for invalid definiton WITH a parseError() callback defined', function () { + var parseErrorWasCalled = false; + // also test setParseErrorHandler() call working to set mermaid.parseError + mermaid.setParseErrorHandler(function (error, hash) { + // got here. + parseErrorWasCalled = true; + }); + expect(mermaid.parseError).not.toEqual(undefined); + expect(mermaidAPI.parse('this is not a mermaid diagram definition')).toEqual(false); + expect(parseErrorWasCalled).toEqual(true); + }); + it('it should return true for valid definiton', function () { + expect(mermaidAPI.parse('graph TD;A--x|text including URL space|B;')).toEqual(true); + }); }); });