Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix mermaidAPI.parse() behavior to match documentation, add tests to ensure behavior matches docs #3004

Merged
merged 6 commits into from May 10, 2022
4 changes: 2 additions & 2 deletions docs/newDiagram.md
Expand Up @@ -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:
Expand All @@ -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)
};
```

Expand Down
24 changes: 22 additions & 2 deletions src/mermaid.js
Expand Up @@ -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;
193 changes: 107 additions & 86 deletions src/mermaidAPI.js
Expand Up @@ -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()) ?
Comment on lines +163 to +164
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question here was because there are some places in the code that the parseError() method is called as
mermaid.parseError()

and a couple places where it is called as
global.mermaid.parseError()

These two should be identical, but it is more of a stylistic question for the mermaid code base.

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) {
Expand Down
21 changes: 19 additions & 2 deletions src/mermaidAPI.spec.js
@@ -1,3 +1,4 @@
import mermaid from './mermaid';
import mermaidAPI from './mermaidAPI';
import { assignWithDepth } from './utils';

Expand Down Expand Up @@ -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);
});
});
});