mirror of
https://github.com/mermaid-js/mermaid.git
synced 2026-05-23 20:10:38 +00:00
fix: prevent escaping < and & when htmlLabels: false
When creating labels using `htmlLabels: false`, e.g.
```mermaid
---
config:
htmlLabels: false
---
flowchart TD
A[2 < 4 && 12 > 14]
```
The SVG node label gets rendered as
`2 < 4 && 12 > 14`. This is fine for HTML text, where we
use `.innerHTML` to set the value. But for non-HTML Labels, we use
`.textContent`, so we need to pass the unescaped values.
Ideally we would stop calling DOMPurify on this label when
`.textContent` is used, since the content doesn't need to be sanitized,
but adding a quick `<`/`>`/`&`-> `<`/`>`/`&` also works.
I've adapted this commit from https://github.com/mermaid-js/mermaid/pull/6406.
Closes: https://github.com/mermaid-js/mermaid/pull/6406
Co-authored-by: khalil <5alil.landolsi@gmail.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'mermaid': patch
|
||||
---
|
||||
|
||||
fix: prevent escaping `<` and `&` when `htmlLabels: false`
|
||||
|
||||
user: @aloisklink
|
||||
user: @BambioGaming
|
||||
@@ -71,6 +71,15 @@ describe('Flowchart v2', () => {
|
||||
{ htmlLabels: false, flowchart: { htmlLabels: false } }
|
||||
);
|
||||
});
|
||||
it('5a: angle brackets should be work without html labels', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart TD
|
||||
a["**Plain text**:\n 5 > 3 && 2 < 4"]
|
||||
b["\`**Markdown**:<br> 5 > 3 && 2 < 4\`"]
|
||||
`,
|
||||
{ htmlLabels: false }
|
||||
);
|
||||
});
|
||||
it('6: should render non-escaped with html labels', () => {
|
||||
imgSnapshotTest(
|
||||
`flowchart TD
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { select } from 'd3';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { sanitizeText } from '../diagram-api/diagramAPI.js';
|
||||
import mermaid from '../mermaid.js';
|
||||
import { replaceIconSubstring } from './createText.js';
|
||||
import { createText, replaceIconSubstring } from './createText.js';
|
||||
|
||||
describe('replaceIconSubstring', () => {
|
||||
it('converts FontAwesome icon notations to HTML tags', async () => {
|
||||
@@ -61,3 +62,41 @@ describe('replaceIconSubstring', () => {
|
||||
expect(output).toContain(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createText', () => {
|
||||
beforeEach(() => {
|
||||
// JSDom has no SVGTSpanElement, so we need to mock getComputedTextLength to avoid errors in createText
|
||||
const mock = vi.mockObject(window.SVGElement.prototype);
|
||||
(mock as unknown as SVGTSpanElement).getComputedTextLength = vi.fn(() => 123.456);
|
||||
});
|
||||
|
||||
it.for([
|
||||
{
|
||||
useHtmlLabels: false,
|
||||
markdown: true,
|
||||
},
|
||||
{
|
||||
useHtmlLabels: true,
|
||||
markdown: true,
|
||||
},
|
||||
{
|
||||
useHtmlLabels: true,
|
||||
markdown: false,
|
||||
},
|
||||
{
|
||||
useHtmlLabels: true,
|
||||
markdown: false,
|
||||
},
|
||||
])(
|
||||
'decodes HTML entities in text when useHtmlLabels is $useHtmlLabels and markdown is $markdown',
|
||||
async ({ useHtmlLabels, markdown }) => {
|
||||
const input = '5 > 3 && 2 < 4';
|
||||
const expected = '5 > 3 && 2 < 4';
|
||||
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
const svgGroup = svg.appendChild(document.createElementNS('http://www.w3.org/2000/svg', 'g'));
|
||||
const output = await createText(select(svgGroup), input, { useHtmlLabels, markdown });
|
||||
expect(output.textContent).toEqual(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -177,6 +177,32 @@ function createFormattedText(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Our HTML code uses `.innerHTML` to apply the text,
|
||||
* however our plain text SVG code uses `.textContent` to apply the text,
|
||||
* which means that HTML entities are not decoded in SVG text.
|
||||
*
|
||||
* This means that we need to decode any HTML entities that `sanitizeText` encodes.
|
||||
*
|
||||
* TODO: If we're using `.textContent`, we can probably skip sanitization entirely.
|
||||
*/
|
||||
function decodeHTMLEntities(text: string): string {
|
||||
// We only need to decode the few entries that `sanitizeText` encodes.
|
||||
const regex = /&(amp|lt|gt);/g;
|
||||
return text.replace(regex, (match, entity) => {
|
||||
switch (entity) {
|
||||
case 'amp':
|
||||
return '&';
|
||||
case 'lt':
|
||||
return '<';
|
||||
case 'gt':
|
||||
return '>';
|
||||
default:
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the text content and styles of the given tspan element based on the
|
||||
* provided wrappedLine data.
|
||||
@@ -197,10 +223,10 @@ function updateTextContentAndStyles(
|
||||
.attr('class', 'text-inner-tspan')
|
||||
.attr('font-weight', word.type === 'strong' ? 'bold' : 'normal');
|
||||
if (index === 0) {
|
||||
innerTspan.text(word.content);
|
||||
innerTspan.text(decodeHTMLEntities(word.content));
|
||||
} else {
|
||||
// TODO: check what joiner to use.
|
||||
innerTspan.text(' ' + word.content);
|
||||
innerTspan.text(' ' + decodeHTMLEntities(word.content));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user