Merge branch 'master' into develop

This commit is contained in:
Knut Sveidqvist
2023-05-24 19:20:47 +02:00
16 changed files with 278 additions and 646 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "mermaid",
"version": "10.2.0-rc.2",
"version": "10.2.0",
"description": "Markdown-ish syntax for generating flowcharts, sequence diagrams, class diagrams, gantt charts and git graphs.",
"type": "module",
"module": "./dist/mermaid.core.mjs",
@@ -53,7 +53,6 @@
},
"dependencies": {
"@braintree/sanitize-url": "^6.0.2",
"@khanacademy/simple-markdown": "^0.9.0",
"cytoscape": "^3.23.0",
"cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-fcose": "^2.1.0",
@@ -64,6 +63,7 @@
"elkjs": "^0.8.2",
"khroma": "^2.0.0",
"lodash-es": "^4.17.21",
"mdast-util-from-markdown": "^1.3.0",
"non-layered-tidy-tree-layout": "^2.0.2",
"stylis": "^4.1.3",
"ts-dedent": "^2.2.0",

View File

@@ -361,72 +361,6 @@ export const drawNote = function (elem, note, conf, diagObj) {
};
export const parseMember = function (text) {
// Note: these two regular expressions don't parse the official UML syntax for attributes
// and methods. They parse a Java-style syntax of the form
// "String name" (for attributes) and "String name(int x)" for methods
const fieldRegEx = /^([#+~-])?(\w+)(~\w+~|\[])?\s+(\w+) *([$*])?$/;
const methodRegEx = /^([#+|~-])?(\w+) *\( *(.*)\) *([$*])? *(\w*[[\]|~]*\s*\w*~?)$/;
let fieldMatch = text.match(fieldRegEx);
let methodMatch = text.match(methodRegEx);
if (fieldMatch && !methodMatch) {
return buildFieldDisplay(fieldMatch);
} else if (methodMatch) {
return buildMethodDisplay(methodMatch);
} else {
return buildLegacyDisplay(text);
}
};
const buildFieldDisplay = function (parsedText) {
let cssStyle = '';
let displayText = '';
try {
let visibility = parsedText[1] ? parsedText[1].trim() : '';
let fieldType = parsedText[2] ? parsedText[2].trim() : '';
let genericType = parsedText[3] ? parseGenericTypes(parsedText[3].trim()) : '';
let fieldName = parsedText[4] ? parsedText[4].trim() : '';
let classifier = parsedText[5] ? parsedText[5].trim() : '';
displayText = visibility + fieldType + genericType + ' ' + fieldName;
cssStyle = parseClassifier(classifier);
} catch (err) {
displayText = parsedText;
}
return {
displayText: displayText,
cssStyle: cssStyle,
};
};
const buildMethodDisplay = function (parsedText) {
let cssStyle = '';
let displayText = '';
try {
let visibility = parsedText[1] ? parsedText[1].trim() : '';
let methodName = parsedText[2] ? parsedText[2].trim() : '';
let parameters = parsedText[3] ? parseGenericTypes(parsedText[3].trim()) : '';
let classifier = parsedText[4] ? parsedText[4].trim() : '';
let returnType = parsedText[5] ? ' : ' + parseGenericTypes(parsedText[5]).trim() : '';
displayText = visibility + methodName + '(' + parameters + ')' + returnType;
cssStyle = parseClassifier(classifier);
} catch (err) {
displayText = parsedText;
}
return {
displayText: displayText,
cssStyle: cssStyle,
};
};
const buildLegacyDisplay = function (text) {
// if for some reason we don't have any match, use old format to parse text
let displayText = '';
let cssStyle = '';
let returnType = '';
@@ -444,14 +378,15 @@ const buildLegacyDisplay = function (text) {
cssStyle = parseClassifier(lastChar);
}
let startIndex = visibility === '' ? 0 : 1;
const startIndex = visibility === '' ? 0 : 1;
let endIndex = cssStyle === '' ? text.length : text.length - 1;
text = text.substring(startIndex, endIndex);
let methodStart = text.indexOf('(');
let methodEnd = text.indexOf(')');
const methodStart = text.indexOf('(');
const methodEnd = text.indexOf(')');
const isMethod = methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length;
if (methodStart > 1 && methodEnd > methodStart && methodEnd <= text.length) {
if (isMethod) {
let methodName = text.substring(0, methodStart).trim();
const parameters = text.substring(methodStart + 1, methodEnd);
@@ -478,7 +413,7 @@ const buildLegacyDisplay = function (text) {
}
} else {
// finally - if all else fails, just send the text back as written (other than parsing for generic types)
displayText = parseGenericTypes(text);
displayText = visibility + parseGenericTypes(text);
}
return {

View File

@@ -1,3 +1,6 @@
// import khroma from 'khroma';
import * as khroma from 'khroma';
/** Returns the styles given options */
export interface FlowChartStyleOptions {
arrowheadColor: string;
@@ -15,6 +18,18 @@ export interface FlowChartStyleOptions {
titleColor: string;
}
const fade = (color: string, opacity: number) => {
// @ts-ignore TODO: incorrect types from khroma
const channel = khroma.channel;
const r = channel(color, 'r');
const g = channel(color, 'g');
const b = channel(color, 'b');
// @ts-ignore incorrect types from khroma
return khroma.rgba(r, g, b, opacity);
};
const getStyles = (options: FlowChartStyleOptions) =>
`.label {
font-family: ${options.fontFamily};
@@ -82,6 +97,12 @@ const getStyles = (options: FlowChartStyleOptions) =>
text-align: center;
}
/* For html labels only */
.labelBkg {
background-color: ${fade(options.edgeLabelBackground, 0.5)};
// background-color:
}
.cluster rect {
fill: ${options.clusterBkg};
stroke: ${options.clusterBorder};

View File

@@ -578,7 +578,7 @@ export const drawLoop = function (elem, loopModel, labelText, conf) {
txt.class = 'labelText';
drawLabel(g, txt);
txt = svgDrawCommon.getTextObj();
txt = getTextObj();
txt.text = loopModel.title;
txt.x = loopModel.startx + labelBoxWidth / 2 + (loopModel.stopx - loopModel.startx) / 2;
txt.y = loopModel.starty + boxMargin + boxTextMargin;
@@ -764,6 +764,37 @@ export const insertArrowCrossHead = function (elem) {
// this is actual shape for arrowhead
};
export const getTextObj = function () {
return {
x: 0,
y: 0,
fill: undefined,
anchor: undefined,
style: '#666',
width: undefined,
height: undefined,
textMargin: 0,
rx: 0,
ry: 0,
tspan: true,
valign: undefined,
};
};
export const getNoteRect = function () {
return {
x: 0,
y: 0,
fill: '#EDF2AE',
stroke: '#666',
width: 100,
anchor: 'start',
height: 100,
rx: 0,
ry: 0,
};
};
const _drawTextCandidateFunc = (function () {
/**
* @param {any} content
@@ -1004,6 +1035,8 @@ export default {
insertDatabaseIcon,
insertComputerIcon,
insertClockIcon,
getTextObj,
getNoteRect,
popupMenu,
popdownMenu,
fixLifeLineHeights,

View File

@@ -39,7 +39,30 @@ flowchart LR
id1[This is the text in the box]
```
## Graph
#### Unicode text
Use `"` to enclose the unicode text.
```mermaid-example
flowchart LR
id["This ❤ Unicode"]
```
#### Markdown formatting
Use double quotes and backticks "\` text \`" to enclose the markdown text.
```mermaid-example
%%{init: {"flowchart": {"htmlLabels": false}} }%%
flowchart LR
markdown["`This **is** _Markdown_`"]
newLines["`Line1
Line 2
Line 3`"]
markdown --> newLines
```
### Direction
This statement declares the direction of the Flowchart.
@@ -57,15 +80,13 @@ flowchart LR
Start --> Stop
```
## Flowchart Orientation
Possible FlowChart orientations are:
- TB - top to bottom
- TD - top-down/ same as top to bottom
- BT - bottom to top
- RL - right to left
- LR - left to right
- TB - Top to bottom
- TD - Top-down/ same as top to bottom
- BT - Bottom to top
- RL - Right to left
- LR - Left to right
## Node shapes

View File

@@ -154,13 +154,7 @@ export const encodeEntities = function (text: string): string {
* @returns
*/
export const decodeEntities = function (text: string): string {
let txt = text;
txt = txt.replace(/fl°°/g, '&#');
txt = txt.replace(/fl°/g, '&');
txt = txt.replace(/¶ß/g, ';');
return txt;
return text.replace(/fl°°/g, '&#').replace(/fl°/g, '&').replace(/¶ß/g, ';');
};
// append !important; to each cssClass followed by a final !important, all enclosed in { }

View File

@@ -1,7 +1,4 @@
import { select } from 'd3';
import { log } from '../logger.js';
import { getConfig } from '../config.js';
import { evaluate } from '../diagrams/common/common.js';
import { decodeEntities } from '../mermaidAPI.js';
import { markdownToHTML, markdownToLines } from '../rendering-util/handle-markdown-text.js';
/**
@@ -19,9 +16,10 @@ function applyStyle(dom, styleFn) {
* @param {any} node
* @param width
* @param classes
* @param addBackground
* @returns {SVGForeignObjectElement} Node
*/
function addHtmlSpan(element, node, width, classes) {
function addHtmlSpan(element, node, width, classes, addBackground = false) {
const fo = element.append('foreignObject');
// const newEl = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
// const newEl = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
@@ -32,7 +30,8 @@ function addHtmlSpan(element, node, width, classes) {
const label = node.label;
const labelClass = node.isNode ? 'nodeLabel' : 'edgeLabel';
div.html(
`<span class="${labelClass} ${classes}" ` +
`
<span class="${labelClass} ${classes}" ` +
(node.labelStyle ? 'style="' + node.labelStyle + '"' : '') +
'>' +
label +
@@ -44,6 +43,9 @@ function addHtmlSpan(element, node, width, classes) {
div.style('white-space', 'nowrap');
div.style('max-width', width + 'px');
div.attr('xmlns', 'http://www.w3.org/1999/xhtml');
if (addBackground) {
div.attr('class', 'labelBkg');
}
let bbox = div.node().getBoundingClientRect();
if (bbox.width === width) {
@@ -203,21 +205,10 @@ export const createText = (
),
labelStyle: style.replace('fill:', 'color:'),
};
let vertexNode = addHtmlSpan(el, node, width, classes);
let vertexNode = addHtmlSpan(el, node, width, classes, addSvgBackground);
return vertexNode;
} else {
const structuredText = markdownToLines(text);
const special = ['"', "'", '.', ',', ':', ';', '!', '?', '(', ')', '[', ']', '{', '}'];
let lastWord;
structuredText.forEach((line) => {
line.forEach((word) => {
if (special.includes(word.content) && lastWord) {
lastWord.content += word.content;
word.content = '';
}
lastWord = word;
});
});
const svgLabel = createFormattedText(width, el, structuredText, addSvgBackground);
return svgLabel;
}

View File

@@ -1,61 +1,55 @@
import SimpleMarkdown from '@khanacademy/simple-markdown';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { dedent } from 'ts-dedent';
/**
*
* @param markdown
* @param {string} markdown markdown to process
* @returns {string} processed markdown
*/
function preprocessMarkdown(markdown) {
// Replace multiple newlines with a single newline
const withoutMultipleNewlines = markdown.replace(/\n{2,}/g, '\n');
// Remove extra spaces at the beginning of each line
const withoutExtraSpaces = withoutMultipleNewlines.replace(/^\s+/gm, '');
const withoutExtraSpaces = dedent(withoutMultipleNewlines);
return withoutExtraSpaces;
}
/**
*
* @param markdown
* @param {string} markdown markdown to split into lines
*/
export function markdownToLines(markdown) {
const preprocessedMarkdown = preprocessMarkdown(markdown);
const mdParse = SimpleMarkdown.defaultBlockParse;
const syntaxTree = mdParse(preprocessedMarkdown);
let lines = [[]];
const { children } = fromMarkdown(preprocessedMarkdown);
const lines = [[]];
let currentLine = 0;
/**
*
* @param node
* @param parentType
* @param {import('mdast').Content} node
* @param {string} [parentType]
*/
function processNode(node, parentType) {
function processNode(node, parentType = 'normal') {
if (node.type === 'text') {
const textLines = node.content.split('\n');
const textLines = node.value.split('\n');
textLines.forEach((textLine, index) => {
if (index !== 0) {
currentLine++;
lines.push([]);
}
// textLine.split(/ (?=[^!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]+)/).forEach((word) => {
textLine.split(' ').forEach((word) => {
if (word) {
lines[currentLine].push({ content: word, type: parentType || 'normal' });
lines[currentLine].push({ content: word, type: parentType });
}
});
});
} else if (node.type === 'strong' || node.type === 'em') {
node.content.forEach((contentNode) => {
} else if (node.type === 'strong' || node.type === 'emphasis') {
node.children.forEach((contentNode) => {
processNode(contentNode, node.type);
});
}
}
syntaxTree.forEach((treeNode) => {
children.forEach((treeNode) => {
if (treeNode.type === 'paragraph') {
treeNode.content.forEach((contentNode) => {
treeNode.children.forEach((contentNode) => {
processNode(contentNode);
});
}
@@ -65,30 +59,27 @@ export function markdownToLines(markdown) {
}
/**
*
* @param markdown
* @param {string} markdown markdown to convert to HTML
* @returns {string} HTML
*/
export function markdownToHTML(markdown) {
const mdParse = SimpleMarkdown.defaultBlockParse;
const syntaxTree = mdParse(markdown);
const { children } = fromMarkdown(markdown);
/**
*
* @param node
* @param {import('mdast').Content} node
*/
function output(node) {
if (node.type === 'text') {
return node.content.replace(/\n/g, '<br/>');
return node.value.replace(/\n/g, '<br/>');
} else if (node.type === 'strong') {
return `<strong>${node.content.map(output).join('')}</strong>`;
} else if (node.type === 'em') {
return `<em>${node.content.map(output).join('')}</em>`;
return `<strong>${node.children.map(output).join('')}</strong>`;
} else if (node.type === 'emphasis') {
return `<em>${node.children.map(output).join('')}</em>`;
} else if (node.type === 'paragraph') {
return `<p>${node.content.map(output).join('')}</p>`;
} else {
return '';
return `<p>${node.children.map(output).join('')}</p>`;
}
return `Unsupported markdown: ${node.type}`;
}
return syntaxTree.map(output).join('');
return children.map(output).join('');
}

View File

@@ -1,6 +1,5 @@
// import { test } from 'vitest';
import { markdownToLines, markdownToHTML } from './handle-markdown-text';
import { test } from 'vitest';
import { markdownToLines, markdownToHTML } from './handle-markdown-text.js';
import { test, expect } from 'vitest';
test('markdownToLines - Basic test', () => {
const input = `This is regular text
@@ -37,9 +36,9 @@ Here is a line *with an italic* section`;
{ content: 'is', type: 'normal' },
{ content: 'a', type: 'normal' },
{ content: 'line', type: 'normal' },
{ content: 'with', type: 'em' },
{ content: 'an', type: 'em' },
{ content: 'italic', type: 'em' },
{ content: 'with', type: 'emphasis' },
{ content: 'an', type: 'emphasis' },
{ content: 'italic', type: 'emphasis' },
{ content: 'section', type: 'normal' },
],
];
@@ -117,7 +116,6 @@ test('markdownToLines - paragraph 1', () => {
test('markdownToLines - paragraph', () => {
const input = `**Start** with
a second line`;
const expectedOutput = [
@@ -144,7 +142,7 @@ test('markdownToLines - Only italic formatting', () => {
{ content: 'This', type: 'normal' },
{ content: 'is', type: 'normal' },
{ content: 'an', type: 'normal' },
{ content: 'italic', type: 'em' },
{ content: 'italic', type: 'emphasis' },
{ content: 'test', type: 'normal' },
],
];
@@ -158,7 +156,7 @@ it('markdownToLines - Mixed formatting', () => {
const expectedOutput = [
[
{ content: 'Italic', type: 'em' },
{ content: 'Italic', type: 'emphasis' },
{ content: 'and', type: 'normal' },
{ content: 'bold', type: 'strong' },
{ content: 'formatting', type: 'normal' },
@@ -179,21 +177,15 @@ Word!`;
{ content: 'dog', type: 'normal' },
{ content: 'in', type: 'normal' },
{ content: 'the', type: 'strong' },
{ content: 'hog', type: 'normal' },
{ content: '.', type: 'normal' },
{ content: '.', type: 'normal' },
{ content: '.', type: 'normal' },
{ content: 'hog...', type: 'normal' },
{ content: 'a', type: 'normal' },
{ content: 'very', type: 'em' },
{ content: 'long', type: 'em' },
{ content: 'text', type: 'em' },
{ content: 'very', type: 'emphasis' },
{ content: 'long', type: 'emphasis' },
{ content: 'text', type: 'emphasis' },
{ content: 'about', type: 'normal' },
{ content: 'it', type: 'normal' },
],
[
{ content: 'Word', type: 'normal' },
{ content: '!', type: 'normal' },
],
[{ content: 'Word!', type: 'normal' }],
];
const output = markdownToLines(input);
@@ -246,8 +238,16 @@ test('markdownToHTML - Only italic formatting', () => {
test('markdownToHTML - Mixed formatting', () => {
const input = `*Italic* and **bold** formatting`;
const expectedOutput = `<p><em>Italic</em> and <strong>bold</strong> formatting</p>`;
const output = markdownToHTML(input);
expect(output).toEqual(expectedOutput);
});
test('markdownToHTML - Unsupported formatting', () => {
expect(
markdownToHTML(`Hello
- l1
- l2
- l3`)
).toMatchInlineSnapshot('"<p>Hello</p>Unsupported markdown: list"');
});

View File

@@ -1,4 +1,4 @@
import { darken, lighten, adjust, invert, isDark } from 'khroma';
import { darken, lighten, adjust, invert, isDark, toRgba } from 'khroma';
import { mkBorder } from './theme-helpers.js';
import {
oldAttributeBackgroundColorEven,