diff --git a/.changeset/brave-baths-behave.md b/.changeset/brave-baths-behave.md new file mode 100644 index 000000000..b688a1faf --- /dev/null +++ b/.changeset/brave-baths-behave.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: Prevent HTML tags from being escaped in sandbox label rendering diff --git a/.changeset/ten-plums-bet.md b/.changeset/ten-plums-bet.md new file mode 100644 index 000000000..f00a41090 --- /dev/null +++ b/.changeset/ten-plums-bet.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: Support ComponentQueue_Ext to prevent parsing error diff --git a/.github/workflows/e2e-timings.yml b/.github/workflows/e2e-timings.yml index e59903df6..21f6b4049 100644 --- a/.github/workflows/e2e-timings.yml +++ b/.github/workflows/e2e-timings.yml @@ -58,7 +58,7 @@ jobs: echo "EOF" >> $GITHUB_OUTPUT - name: Commit and create pull request - uses: peter-evans/create-pull-request@915d841dae6a4f191bb78faf61a257411d7be4d2 + uses: peter-evans/create-pull-request@0edc001d28a2959cd7a6b505629f1d82f0a6e67d with: add-paths: | cypress/timings.json diff --git a/cypress/helpers/util.ts b/cypress/helpers/util.ts index 0332178f6..51268c2a9 100644 --- a/cypress/helpers/util.ts +++ b/cypress/helpers/util.ts @@ -98,12 +98,21 @@ export const openURLAndVerifyRendering = ( cy.visit(url); cy.window().should('have.property', 'rendered', true); - cy.get('svg').should('be.visible'); - // cspell:ignore viewbox - cy.get('svg').should('not.have.attr', 'viewbox'); - if (validation) { - cy.get('svg').should(validation); + // Handle sandbox mode where SVG is inside an iframe + if (options.securityLevel === 'sandbox') { + cy.get('iframe').should('be.visible'); + if (validation) { + cy.get('iframe').should(validation); + } + } else { + cy.get('svg').should('be.visible'); + // cspell:ignore viewbox + cy.get('svg').should('not.have.attr', 'viewbox'); + + if (validation) { + cy.get('svg').should(validation); + } } if (screenshot) { diff --git a/cypress/integration/rendering/c4.spec.js b/cypress/integration/rendering/c4.spec.js index 00e71adec..92b834d41 100644 --- a/cypress/integration/rendering/c4.spec.js +++ b/cypress/integration/rendering/c4.spec.js @@ -114,4 +114,28 @@ describe('C4 diagram', () => { {} ); }); + it('C4.6 should render C4Context diagram with ComponentQueue_Ext', () => { + imgSnapshotTest( + ` + C4Context + title System Context diagram with ComponentQueue_Ext + + Enterprise_Boundary(b0, "BankBoundary0") { + Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.") + + System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") + + Enterprise_Boundary(b1, "BankBoundary") { + ComponentQueue_Ext(msgQueue, "Message Queue", "RabbitMQ", "External message queue system for processing banking transactions") + System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.") + } + } + + BiRel(customerA, SystemAA, "Uses") + Rel(SystemAA, msgQueue, "Sends messages to") + Rel(SystemAA, SystemC, "Sends e-mails", "SMTP") + `, + {} + ); + }); }); diff --git a/cypress/integration/rendering/flowchart-v2.spec.js b/cypress/integration/rendering/flowchart-v2.spec.js index 5ef32c269..cd3676fbf 100644 --- a/cypress/integration/rendering/flowchart-v2.spec.js +++ b/cypress/integration/rendering/flowchart-v2.spec.js @@ -79,6 +79,18 @@ describe('Flowchart v2', () => { { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } ); }); + it('6a: should render complex HTML in labels with sandbox security', () => { + imgSnapshotTest( + `flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car] + `, + { securityLevel: 'sandbox', flowchart: { htmlLabels: true } } + ); + }); it('7: should render a flowchart when useMaxWidth is true (default)', () => { renderGraph( `flowchart TD diff --git a/cypress/timings.json b/cypress/timings.json index 8a6a4c00e..6635346b7 100644 --- a/cypress/timings.json +++ b/cypress/timings.json @@ -2,227 +2,227 @@ "durations": [ { "spec": "cypress/integration/other/configuration.spec.js", - "duration": 5841 + "duration": 6099 }, { "spec": "cypress/integration/other/external-diagrams.spec.js", - "duration": 2138 + "duration": 2236 }, { "spec": "cypress/integration/other/ghsa.spec.js", - "duration": 3370 + "duration": 3405 }, { "spec": "cypress/integration/other/iife.spec.js", - "duration": 2052 + "duration": 2176 }, { "spec": "cypress/integration/other/interaction.spec.js", - "duration": 12243 + "duration": 12300 }, { "spec": "cypress/integration/other/rerender.spec.js", - "duration": 2065 + "duration": 2089 }, { "spec": "cypress/integration/other/xss.spec.js", - "duration": 31288 + "duration": 32033 }, { "spec": "cypress/integration/rendering/appli.spec.js", - "duration": 3421 + "duration": 3672 }, { "spec": "cypress/integration/rendering/architecture.spec.ts", - "duration": 97 + "duration": 103 }, { "spec": "cypress/integration/rendering/block.spec.js", - "duration": 18500 + "duration": 18135 }, { "spec": "cypress/integration/rendering/c4.spec.js", - "duration": 5793 + "duration": 5661 }, { "spec": "cypress/integration/rendering/classDiagram-elk-v3.spec.js", - "duration": 40966 + "duration": 41456 }, { "spec": "cypress/integration/rendering/classDiagram-handDrawn-v3.spec.js", - "duration": 39176 + "duration": 38910 }, { "spec": "cypress/integration/rendering/classDiagram-v2.spec.js", - "duration": 23468 + "duration": 24120 }, { "spec": "cypress/integration/rendering/classDiagram-v3.spec.js", - "duration": 38291 + "duration": 38454 }, { "spec": "cypress/integration/rendering/classDiagram.spec.js", - "duration": 16949 + "duration": 17099 }, { "spec": "cypress/integration/rendering/conf-and-directives.spec.js", - "duration": 9480 + "duration": 9844 }, { "spec": "cypress/integration/rendering/current.spec.js", - "duration": 2753 + "duration": 2951 }, { "spec": "cypress/integration/rendering/erDiagram-unified.spec.js", - "duration": 88028 + "duration": 90081 }, { "spec": "cypress/integration/rendering/erDiagram.spec.js", - "duration": 15615 + "duration": 19496 }, { "spec": "cypress/integration/rendering/errorDiagram.spec.js", - "duration": 3706 + "duration": 3829 }, { "spec": "cypress/integration/rendering/flowchart-elk.spec.js", - "duration": 43905 + "duration": 42517 }, { "spec": "cypress/integration/rendering/flowchart-handDrawn.spec.js", - "duration": 31217 + "duration": 31541 }, { "spec": "cypress/integration/rendering/flowchart-icon.spec.js", - "duration": 7531 + "duration": 7749 }, { "spec": "cypress/integration/rendering/flowchart-shape-alias.spec.ts", - "duration": 25423 + "duration": 25230 }, { "spec": "cypress/integration/rendering/flowchart-v2.spec.js", - "duration": 49664 + "duration": 49359 }, { "spec": "cypress/integration/rendering/flowchart.spec.js", - "duration": 32525 + "duration": 33028 }, { "spec": "cypress/integration/rendering/gantt.spec.js", - "duration": 20915 + "duration": 22271 }, { "spec": "cypress/integration/rendering/gitGraph.spec.js", - "duration": 53556 + "duration": 51837 }, { "spec": "cypress/integration/rendering/iconShape.spec.ts", - "duration": 283038 + "duration": 285060 }, { "spec": "cypress/integration/rendering/imageShape.spec.ts", - "duration": 59434 + "duration": 59517 }, { "spec": "cypress/integration/rendering/info.spec.ts", - "duration": 3101 + "duration": 3501 }, { "spec": "cypress/integration/rendering/journey.spec.js", - "duration": 7099 + "duration": 7405 }, { "spec": "cypress/integration/rendering/kanban.spec.ts", - "duration": 7567 + "duration": 7975 }, { "spec": "cypress/integration/rendering/katex.spec.js", - "duration": 3817 + "duration": 4312 }, { "spec": "cypress/integration/rendering/marker_unique_id.spec.js", - "duration": 2624 + "duration": 2630 }, { "spec": "cypress/integration/rendering/mindmap-tidy-tree.spec.js", - "duration": 4246 + "duration": 4541 }, { "spec": "cypress/integration/rendering/mindmap.spec.ts", - "duration": 11967 + "duration": 12134 }, { "spec": "cypress/integration/rendering/newShapes.spec.ts", - "duration": 151914 + "duration": 151160 }, { "spec": "cypress/integration/rendering/oldShapes.spec.ts", - "duration": 116698 + "duration": 118044 }, { "spec": "cypress/integration/rendering/packet.spec.ts", - "duration": 4967 + "duration": 5166 }, { "spec": "cypress/integration/rendering/pie.spec.ts", - "duration": 6700 + "duration": 7074 }, { "spec": "cypress/integration/rendering/quadrantChart.spec.js", - "duration": 8963 + "duration": 9518 }, { "spec": "cypress/integration/rendering/radar.spec.js", - "duration": 5540 + "duration": 5846 }, { "spec": "cypress/integration/rendering/requirement.spec.js", - "duration": 2782 + "duration": 3089 }, { "spec": "cypress/integration/rendering/requirementDiagram-unified.spec.js", - "duration": 54797 + "duration": 55361 }, { "spec": "cypress/integration/rendering/sankey.spec.ts", - "duration": 6914 + "duration": 7236 }, { "spec": "cypress/integration/rendering/sequencediagram-v2.spec.js", - "duration": 20481 + "duration": 26057 }, { "spec": "cypress/integration/rendering/sequencediagram.spec.js", - "duration": 38490 + "duration": 48401 }, { "spec": "cypress/integration/rendering/stateDiagram-v2.spec.js", - "duration": 30766 + "duration": 30364 }, { "spec": "cypress/integration/rendering/stateDiagram.spec.js", - "duration": 16705 + "duration": 16862 }, { "spec": "cypress/integration/rendering/theme.spec.js", - "duration": 30928 + "duration": 30553 }, { "spec": "cypress/integration/rendering/timeline.spec.ts", - "duration": 8424 + "duration": 8962 }, { "spec": "cypress/integration/rendering/treemap.spec.ts", - "duration": 12533 + "duration": 12486 }, { "spec": "cypress/integration/rendering/xyChart.spec.js", - "duration": 21197 + "duration": 21718 }, { "spec": "cypress/integration/rendering/zenuml.spec.js", - "duration": 3455 + "duration": 3882 } ] } diff --git a/package.json b/package.json index 40ffea765..667ad230e 100644 --- a/package.json +++ b/package.json @@ -63,11 +63,11 @@ ] }, "devDependencies": { - "@applitools/eyes-cypress": "^3.55.4", - "@argos-ci/cypress": "^6.1.5", + "@applitools/eyes-cypress": "^3.56.3", + "@argos-ci/cypress": "^6.2.1", "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.29.7", - "@cspell/eslint-plugin": "^8.19.4", + "@cspell/eslint-plugin": "^9.3.0", "@cypress/code-coverage": "^3.14.7", "@eslint/js": "^9.26.0", "@rollup/plugin-typescript": "^12.1.4", @@ -77,7 +77,7 @@ "@types/jsdom": "^21.1.7", "@types/lodash": "^4.17.20", "@types/mdast": "^4.0.4", - "@types/node": "^22.18.13", + "@types/node": "^22.19.0", "@types/rollup-plugin-visualizer": "^5.0.3", "@vitest/coverage-v8": "^3.2.4", "@vitest/spy": "^3.2.4", @@ -88,23 +88,23 @@ "cors": "^2.8.5", "cpy-cli": "^5.0.0", "cross-env": "^7.0.3", - "cspell": "^9.2.2", + "cspell": "^9.3.0", "cypress": "^14.5.4", "cypress-image-snapshot": "^4.0.1", "cypress-split": "^1.24.25", "esbuild": "^0.25.12", "eslint": "^9.26.0", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-cypress": "^4.3.0", + "eslint-plugin-cypress": "^5.2.0", "eslint-plugin-html": "^8.1.3", - "eslint-plugin-jest": "^28.14.0", - "eslint-plugin-jsdoc": "^50.8.0", + "eslint-plugin-jest": "^29.0.1", + "eslint-plugin-jsdoc": "^61.1.12", "eslint-plugin-json": "^4.0.1", "eslint-plugin-lodash": "^8.0.0", "eslint-plugin-markdown": "^5.1.0", "eslint-plugin-no-only-tests": "^3.3.0", "eslint-plugin-tsdoc": "^0.4.0", - "eslint-plugin-unicorn": "^59.0.1", + "eslint-plugin-unicorn": "^62.0.0", "express": "^5.1.0", "globals": "^16.4.0", "globby": "^14.1.0", @@ -127,7 +127,7 @@ "tsx": "^4.20.6", "typescript": "~5.7.3", "typescript-eslint": "^8.38.0", - "vite": "^7.0.7", + "vite": "^7.0.8", "vite-plugin-istanbul": "^7.0.0", "vitest": "^3.2.4" }, diff --git a/packages/mermaid/package.json b/packages/mermaid/package.json index f98537b52..b02b26fc8 100644 --- a/packages/mermaid/package.json +++ b/packages/mermaid/package.json @@ -78,7 +78,7 @@ "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.19", - "dompurify": "^3.2.5", + "dompurify": "^3.3.0", "katex": "^0.16.25", "khroma": "^2.1.0", "lodash-es": "^4.17.21", diff --git a/packages/mermaid/src/diagrams/c4/parser/c4Component.spec.js b/packages/mermaid/src/diagrams/c4/parser/c4Component.spec.js new file mode 100644 index 000000000..70d93d8be --- /dev/null +++ b/packages/mermaid/src/diagrams/c4/parser/c4Component.spec.js @@ -0,0 +1,58 @@ +import c4Db from '../c4Db.js'; +import c4 from './c4Diagram.jison'; +import { setConfig } from '../../../config.js'; + +setConfig({ + securityLevel: 'strict', +}); + +describe.each([ + ['Component', 'component'], + ['ComponentDb', 'component_db'], + ['ComponentQueue', 'component_queue'], + ['Component_Ext', 'external_component'], + ['ComponentDb_Ext', 'external_component_db'], + ['ComponentQueue_Ext', 'external_component_queue'], +])('parsing a C4 %s', function (macroName, elementName) { + beforeEach(function () { + c4.parser.yy = c4Db; + c4.parser.yy.clear(); + }); + + it('should parse a C4 diagram with one Component correctly', function () { + c4.parser.parse(`C4Component +title Component diagram for Internet Banking Component +${macroName}(ComponentAA, "Internet Banking Component", "Technology", "Allows customers to view information about their bank accounts, and make payments.")`); + + const yy = c4.parser.yy; + + const shapes = yy.getC4ShapeArray(); + expect(shapes.length).toBe(1); + const onlyShape = shapes[0]; + + expect(onlyShape).toMatchObject({ + alias: 'ComponentAA', + descr: { + text: 'Allows customers to view information about their bank accounts, and make payments.', + }, + label: { + text: 'Internet Banking Component', + }, + techn: { + text: 'Technology', + }, + typeC4Shape: { + text: elementName, + }, + }); + }); + + it('should handle a trailing whitespaces after Component', function () { + const whitespace = ' '; + const rendered = c4.parser.parse(`C4Component${whitespace} +title Component diagram for Internet Banking Component${whitespace} +${macroName}(ComponentAA, "Internet Banking Component", "Technology", "Allows customers to view information about their bank accounts, and make payments.")${whitespace}`); + + expect(rendered).toBe(true); + }); +}); diff --git a/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison b/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison index 63856f044..f0ce80d33 100644 --- a/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison +++ b/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison @@ -158,10 +158,10 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} "UpdateRelStyle" { this.begin("update_rel_style"); return 'UPDATE_REL_STYLE';} "UpdateLayoutConfig" { this.begin("update_layout_config"); return 'UPDATE_LAYOUT_CONFIG';} -<> return "EOF_IN_STRUCT"; -[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";} -[(] { this.begin("attribute"); } -[)] { this.popState();this.popState();} +<> return "EOF_IN_STRUCT"; +[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";} +[(] { this.begin("attribute"); } +[)] { this.popState();this.popState();} ",," { return 'ATTRIBUTE_EMPTY';} "," { } diff --git a/packages/mermaid/src/diagrams/common/common.spec.ts b/packages/mermaid/src/diagrams/common/common.spec.ts index 3c7e0fdb8..edaf0b6dd 100644 --- a/packages/mermaid/src/diagrams/common/common.spec.ts +++ b/packages/mermaid/src/diagrams/common/common.spec.ts @@ -70,6 +70,31 @@ describe('Sanitize text', () => { }); expect(result).not.toContain('javascript:alert(1)'); }); + + it('should allow HTML tags in sandbox mode', () => { + const htmlStr = '

This is a bold text

'; + const result = sanitizeText(htmlStr, { + securityLevel: 'sandbox', + flowchart: { htmlLabels: true }, + }); + expect(result).toContain('

'); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain('

'); + }); + + it('should remove script tags in sandbox mode', () => { + const maliciousStr = '

Hello world

'; + const result = sanitizeText(maliciousStr, { + securityLevel: 'sandbox', + flowchart: { htmlLabels: true }, + }); + expect(result).not.toContain('