From b4d0442dd1628acb3f71681519e7f47fc8bacf55 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Wed, 25 Mar 2026 11:13:35 +1100 Subject: [PATCH 01/11] feat(treeView): add file tree enhancements icons, bare labels, annotations, highlights Extends treeView-beta (PR #7396 by @lee-treehouse and @Vikrantpalle) with file-tree-specific features: - Bare (unquoted) labels: src/, index.js, .gitignore - 30+ auto-detected file-type icons (JS, TS, Python, Docker, etc.) - :::class annotations (e.g. :::highlight for yellow background) - icon(name) overrides for custom icons - ## inline descriptions, column-aligned across all nodes - Directory auto-detection via trailing slash - Folder names with spaces (unquoted) - Two-pass rendering for description column alignment - Diagram-scoped icon symbol IDs (no duplicates on multi-diagram pages) - Side-by-side demo page showing input syntax next to rendered output - 26 new parseNodeContent unit tests + 8 new Langium parser tests - 5 new Cypress E2E tests - Comprehensive docs rewrite with all new features Grammar: NODE_CONTENT terminal captures rest-of-line; detailed annotation parsing (:::class, icon(), ##) delegated to TypeScript parseNodeContent(). Closes #7516 --- .changeset/enhance-treeview-filetree.md | 17 ++ .../integration/rendering/treeView.spec.ts | 74 ++++++- demos/treeView.html | 192 +++++++++++++---- packages/examples/src/examples/tree-view.ts | 29 +++ packages/mermaid/src/diagrams/treeView/db.ts | 24 ++- .../mermaid/src/diagrams/treeView/icons.ts | 203 ++++++++++++++++++ .../src/diagrams/treeView/parser.spec.ts | 181 ++++++++++++++++ .../mermaid/src/diagrams/treeView/parser.ts | 99 ++++++++- .../mermaid/src/diagrams/treeView/renderer.ts | 148 +++++++++++-- .../mermaid/src/diagrams/treeView/styles.ts | 20 +- .../mermaid/src/diagrams/treeView/types.ts | 19 +- packages/mermaid/src/docs/syntax/treeView.md | 133 ++++++++++-- .../src/language/treeView/treeView.langium | 17 +- .../src/language/treeView/valueConverter.ts | 3 - packages/parser/tests/treeView.test.ts | 63 +++++- 15 files changed, 1120 insertions(+), 102 deletions(-) create mode 100644 .changeset/enhance-treeview-filetree.md create mode 100644 packages/mermaid/src/diagrams/treeView/icons.ts create mode 100644 packages/mermaid/src/diagrams/treeView/parser.spec.ts diff --git a/.changeset/enhance-treeview-filetree.md b/.changeset/enhance-treeview-filetree.md new file mode 100644 index 00000000000..3867fbde6c5 --- /dev/null +++ b/.changeset/enhance-treeview-filetree.md @@ -0,0 +1,17 @@ +--- +'mermaid': minor +'@mermaid-js/parser': minor +--- + +Enhance treeView-beta with file tree features + +Extends the existing treeView-beta diagram with features useful for representing file/directory structures: + +- **Bare labels**: Node names no longer require quotes (`src/` instead of `"src/"`) +- **Auto-detected file icons**: 30+ file type icons (TypeScript, Python, Docker, etc.) resolved from filename/extension +- **Icon overrides**: `icon(docker)` syntax to manually specify an icon +- **CSS class annotations**: `:::highlight` syntax for styling individual nodes +- **Descriptions**: `## description text` appended after a node label for additional context +- **Comment support**: `%%` line comments within the tree body +- **Directory detection**: Trailing `/` on a label auto-sets the node type to directory with folder icon +- **New theme variables**: `iconColor` and `descriptionColor` for styling icons and descriptions diff --git a/cypress/integration/rendering/treeView.spec.ts b/cypress/integration/rendering/treeView.spec.ts index 837f7d2521f..d4fd87a2cd3 100644 --- a/cypress/integration/rendering/treeView.spec.ts +++ b/cypress/integration/rendering/treeView.spec.ts @@ -1,14 +1,14 @@ import { imgSnapshotTest } from '../../helpers/util'; describe('TreeView Diagram', () => { - it('should render a simple treeView diagram', () => { + it('should render a simple treeView diagram with quoted labels', () => { imgSnapshotTest( `treeView-beta "file1.ts"` ); }); - it('should render a complex treeView diagram', () => { + it('should render a complex treeView diagram with quoted labels', () => { imgSnapshotTest( `treeView-beta "root" @@ -26,7 +26,7 @@ describe('TreeView Diagram', () => { ); }); - it('should render a complex treeView diagram with multiple roots', () => { + it('should render with multiple roots and quoted labels', () => { imgSnapshotTest( `treeView-beta "folder1" @@ -43,7 +43,7 @@ describe('TreeView Diagram', () => { ); }); - it('should render a treeView diagram with custom config', () => { + it('should render with custom config and quoted labels', () => { imgSnapshotTest( ` --- @@ -72,4 +72,70 @@ treeView-beta ` ); }); + + it('should render bare (unquoted) labels with icons', () => { + imgSnapshotTest( + `treeView-beta + my-project/ + src/ + components/ + Button.tsx + Header.tsx + App.tsx + index.js + .gitignore + package.json + README.md` + ); + }); + + it('should render :::class annotations for highlighting', () => { + imgSnapshotTest( + `treeView-beta + src/ + components/ + Button.tsx :::highlight + Header.tsx + App.tsx :::highlight + index.js + package.json` + ); + }); + + it('should render ## descriptions', () => { + imgSnapshotTest( + `treeView-beta + src/ + index.js ## app entry point + config.ts ## runtime configuration + utils/ ## shared helpers + package.json ## project manifest + README.md` + ); + }); + + it('should render icon() overrides', () => { + imgSnapshotTest( + `treeView-beta + data/ + model.bin icon(database) + weights.h5 icon(database) + src/ + index.js` + ); + }); + + it('should render combined annotations', () => { + imgSnapshotTest( + `treeView-beta + my-project/ + src/ + App.tsx :::highlight icon(react) ## main component + index.js ## entry point + styles.css + .env ## environment variables + Dockerfile + package.json` + ); + }); }); diff --git a/demos/treeView.html b/demos/treeView.html index 174830fa102..41e2f96321c 100644 --- a/demos/treeView.html +++ b/demos/treeView.html @@ -9,62 +9,176 @@ body { font-family: 'Montserrat', sans-serif; margin: 0 auto; - max-width: 900px; + max-width: 1200px; padding: 20px; } - .mermaid { - margin: 30px 0; - } h1, h2 { color: #333; } - pre { - background-color: #f5f5f5; - padding: 15px; - border-radius: 5px; + .demo { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 32px; + align-items: start; + } + .demo .source { + background: #fff; + color: #333; + border: 1px solid #e0e0e0; + padding: 16px; + border-radius: 6px; + font-family: 'Consolas', 'Monaco', monospace; + font-size: 13px; + line-height: 1.5; + overflow-x: auto; + white-space: pre; + margin: 0; + } + .demo .rendered { + background: #fff; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 16px; + min-height: 80px; + } + .demo .rendered .mermaid { + margin: 0; } +

TreeView Diagram Demo

+

+ Each example shows the input syntax (left) alongside the + rendered output (right). +

-

Basic TreeView Example

-
-    treeView-beta
-    "docs"
+    

Basic TreeView (quoted labels)

+
+

+      
+
+treeView-beta
+"docs"
+    "build"
+    "make.bat"
+    "Makefile"
+    "out"
+    "source"
         "build"
-        "make.bat"
-        "Makefile"
-        "out"
-        "source"
-            "build"
-            "static"
-                "_templates"
-                "div. Files"
-    
+ "static" + "_templates" + "div. Files" +
+ + -

TreeView Config Example

-
-    ---
-    config:
+    

Bare Labels with Icons

+
+

+      
+
+treeView-beta
+my-project/
+    src/
+        components/
+            Button.tsx
+            Header.tsx
+        App.tsx
+        index.js
+    .gitignore
+    package.json
+    README.md
+        
+
+
+ +

Annotations: :::class, icon(), ##

+
+

+      
+
+treeView-beta
+my-project/
+    src/
+        App.tsx :::highlight icon(react) ## main component
+        index.js ## entry point
+        styles.css
+    data/
+        model.bin icon(database)
+    .env ## environment variables
+    Dockerfile
+    package.json
+        
+
+
+ +

Folder Names with Spaces

+
+

+      
+
+treeView-beta
+My Project/
+    Source Files/
+        App.tsx :::highlight ## main component
+        index.js ## entry point
+    My Documents/
+        meeting notes.md
+        project plan.md ## Q3 roadmap
+    .gitignore
+    README.md
+        
+
+
+ +

Custom Config

+
+

+      
+
+---
+config:
+    treeView:
+        rowIndent: 80
+        lineThickness: 3
+    themeVariables:
         treeView:
-            rowIndent: 80
-            lineThickness: 3
-        themeVariables:
-            treeView:
-                labelFontSize: '20px'
-                labelColor: '#FF0000'
-                lineColor: '#00FF00'
-    ---
-    treeView-beta
-    "packages"
-        "mermaid"
-            "src"
-        "parser"
-    
+ labelFontSize: '20px' + labelColor: '#FF0000' + lineColor: '#00FF00' +--- +treeView-beta +"packages" + "mermaid" + "src" + "parser" +
+ + + - import mermaid from './mermaid.esm.mjs'; - mermaid.initialize({ - logLevel: 1, - securityLevel: 'loose', - }); - - - + \ No newline at end of file diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index 5ccf7f59957..7d82c8fcf6b 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -1645,6 +1645,10 @@ export interface TreeViewDiagramConfig extends BaseDiagramConfig { * Thickness of the line */ lineThickness?: number; + /** + * Whether to show file/folder icons next to labels + */ + showIcons?: boolean; } /** * The object containing configurations specific for radar diagrams. diff --git a/packages/mermaid/src/diagrams/treeView/parser.spec.ts b/packages/mermaid/src/diagrams/treeView/parser.spec.ts index a3502e62e90..92a9ee602a5 100644 --- a/packages/mermaid/src/diagrams/treeView/parser.spec.ts +++ b/packages/mermaid/src/diagrams/treeView/parser.spec.ts @@ -178,4 +178,33 @@ describe('parseNodeContent', () => { expect(result.iconId).toBe('folder'); }); }); + + describe('icon suppression', () => { + it('should set iconId to none for icon(none)', () => { + const result = parseNodeContent('index.js icon(none)'); + expect(result.name).toBe('index.js'); + expect(result.iconId).toBe('none'); + }); + + it('should set iconId to none for empty icon()', () => { + const result = parseNodeContent('index.js icon()'); + expect(result.name).toBe('index.js'); + expect(result.iconId).toBe('none'); + }); + + it('should suppress icon on a directory', () => { + const result = parseNodeContent('src/ icon(none)'); + expect(result.name).toBe('src'); + expect(result.nodeType).toBe('directory'); + expect(result.iconId).toBe('none'); + }); + + it('should combine icon(none) with other annotations', () => { + const result = parseNodeContent('app.ts icon(none) :::highlight ## entry point'); + expect(result.name).toBe('app.ts'); + expect(result.iconId).toBe('none'); + expect(result.cssClass).toBe('highlight'); + expect(result.description).toBe('entry point'); + }); + }); }); diff --git a/packages/mermaid/src/diagrams/treeView/parser.ts b/packages/mermaid/src/diagrams/treeView/parser.ts index 11ed8f01dce..8e36e20734e 100644 --- a/packages/mermaid/src/diagrams/treeView/parser.ts +++ b/packages/mermaid/src/diagrams/treeView/parser.ts @@ -72,10 +72,10 @@ export function parseNodeContent(raw: string): ParsedNodeContent { remaining = remaining.replace(classMatch[0], '').trim(); } - // Extract icon(name) - const iconMatch = /icon\(([\w-]+)\)/.exec(remaining); + // Extract icon(name) or icon(none) or icon() to suppress + const iconMatch = /icon\(([\w-]*)\)/.exec(remaining); if (iconMatch) { - iconId = iconMatch[1]; + iconId = iconMatch[1] || 'none'; } // Detect directory: trailing / on the name diff --git a/packages/mermaid/src/diagrams/treeView/renderer.ts b/packages/mermaid/src/diagrams/treeView/renderer.ts index 1ca4af124b2..d895cf7ae11 100644 --- a/packages/mermaid/src/diagrams/treeView/renderer.ts +++ b/packages/mermaid/src/diagrams/treeView/renderer.ts @@ -23,7 +23,7 @@ interface RenderInfo { const injectIconDefs = (svg: D3SVGElement, root: Node, diagramId: string) => { const usedIcons = new Set(); const collect = (node: Node) => { - if (node.iconId) { + if (node.iconId && node.iconId !== 'none') { usedIcons.add(node.iconId); } node.children.forEach(collect); @@ -59,9 +59,10 @@ const positionLabel = ( cssClasses += ` ${node.cssClass}`; } - // Icon + // Icon — skip if iconId is 'none' or showIcons is disabled const iconOffset = ICON_SIZE + ICON_GAP; - if (node.iconId) { + const showIcon = config.showIcons !== false && node.iconId && node.iconId !== 'none'; + if (showIcon) { nodeGroup .append('use') .attr('href', `#tv-icon-${diagramId}-${node.iconId}`) @@ -80,12 +81,12 @@ const positionLabel = ( .attr('class', cssClasses); const { height: labelHeight, width: labelWidth } = label.node()!.getBBox(); const height = labelHeight + config.paddingY * 2; - const labelX = x + config.paddingX + (node.iconId ? iconOffset : 0); + const labelX = x + config.paddingX + (showIcon ? iconOffset : 0); label.attr('x', labelX); label.attr('y', y + height / 2); const labelRightEdge = labelX + labelWidth; - const width = labelWidth + config.paddingX * 2 + (node.iconId ? iconOffset : 0); + const width = labelWidth + config.paddingX * 2 + (showIcon ? iconOffset : 0); node.BBox = { x, y, width, height }; // Highlight background rect (sized later in drawTree) @@ -219,7 +220,9 @@ const draw: DrawDefinition = (text, id, _ver, diagObj) => { const svg = selectSvgElement(id); // Inject icon definitions (scoped to diagramId to avoid duplicates) - injectIconDefs(svg, root, id); + if (config.showIcons !== false) { + injectIconDefs(svg, root, id); + } const treeElem = svg.append('g'); treeElem.attr('class', 'tree-view'); diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index f27386aabbf..1a4b96ae82f 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -2352,6 +2352,10 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) type: number minimum: 0 default: 1 + showIcons: + description: Whether to show file/folder icons next to labels + type: boolean + default: true RadarDiagramConfig: title: Radar Diagram Config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40eacdcbea1..049ba1403f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,6 +302,9 @@ importers: '@iconify/types': specifier: ^2.0.0 version: 2.0.0 + '@mdi/js': + specifier: ^7.4.47 + version: 7.4.47 '@types/cytoscape': specifier: ^3.21.9 version: 3.21.9 @@ -2585,6 +2588,9 @@ packages: '@mdi/font@7.4.47': resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==} + '@mdi/js@7.4.47': + resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==} + '@microsoft/tsdoc-config@0.17.1': resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==} @@ -12483,6 +12489,8 @@ snapshots: '@mdi/font@7.4.47': {} + '@mdi/js@7.4.47': {} + '@microsoft/tsdoc-config@0.17.1': dependencies: '@microsoft/tsdoc': 0.15.1 From a4fcb887988806aa2bca740445cb14bd7c8ca0c0 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 26 Mar 2026 16:58:03 +1100 Subject: [PATCH 03/11] fix(treeView): correct icon provenance comment --- packages/mermaid/src/diagrams/treeView/icons.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/treeView/icons.ts b/packages/mermaid/src/diagrams/treeView/icons.ts index 1db4cb88edc..121e99fc66d 100644 --- a/packages/mermaid/src/diagrams/treeView/icons.ts +++ b/packages/mermaid/src/diagrams/treeView/icons.ts @@ -117,7 +117,7 @@ export function resolveIcon(label: string, nodeType: NodeType): string { /** * Inline SVG icon paths, keyed by icon ID. * Each value is an SVG path `d` attribute for a 16x16 viewBox. - * Paths are based on generic file-type iconography (not copied from any icon set). + * Paths were generated by LLM for this project. */ export const ICON_PATHS: Record = { // Folder: simple folder shape From b707bb320d109bf3674898fab038296795fc1254 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Fri, 27 Mar 2026 10:05:16 +1100 Subject: [PATCH 04/11] fix: replace LLM-generated icons with MDI paths, fix viewBox and lockfile - Swap all 30 icon SVG paths to genuine Material Design Icons (Apache 2.0) - Update viewBox from 16x16 to 24x24 to match MDI coordinate system - Remove stale @mdi/js reference from pnpm-lock.yaml --- .../mermaid/src/diagrams/treeView/icons.ts | 99 +++++++------------ .../mermaid/src/diagrams/treeView/renderer.ts | 2 +- pnpm-lock.yaml | 8 -- 3 files changed, 37 insertions(+), 72 deletions(-) diff --git a/packages/mermaid/src/diagrams/treeView/icons.ts b/packages/mermaid/src/diagrams/treeView/icons.ts index 121e99fc66d..309f54d8bb7 100644 --- a/packages/mermaid/src/diagrams/treeView/icons.ts +++ b/packages/mermaid/src/diagrams/treeView/icons.ts @@ -116,83 +116,56 @@ export function resolveIcon(label: string, nodeType: NodeType): string { /** * Inline SVG icon paths, keyed by icon ID. - * Each value is an SVG path `d` attribute for a 16x16 viewBox. - * Paths were generated by LLM for this project. + * Each value is an SVG path `d` attribute for a 24×24 viewBox. + * Icon paths from Material Design Icons by Pictogrammers (Apache 2.0). + * https://github.com/Templarian/MaterialDesign */ export const ICON_PATHS: Record = { - // Folder: simple folder shape - folder: 'M1 3h5l2-2h7v1h-6.5L6.5 4H1V3zm0 2h14v9H1V5zm1 1v7h12V6H2z', - // Generic file - file: 'M3 1h7l4 4v10H3V1zm1 1v12h9V5.5L9.5 2H4zm6 0v4h4', - // JavaScript + folder: + 'M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z', + file: 'M13,9V3.5L18.5,9M6,2C4.89,2 4,2.89 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6Z', javascript: - 'M0 2h16v12H0V2zm1 1v10h14V3H1zm4 3.5h1.5v4c0 .8-.4 1.2-1.2 1.2-.4 0-.7-.1-.9-.2l.2-1c.1.1.3.1.4.1.3 0 .4-.1.4-.5V6.5H5zm3 0h1.5v2.3c0 .2 0 .4.1.5.1.2.3.3.5.3.4 0 .6-.2.6-.8V6.5H12v2.7c0 1.2-.6 1.8-1.7 1.8-.5 0-.9-.1-1.2-.4l.2-1c.2.2.5.3.7.3.4 0 .6-.2.6-.7V6.5H8z', - // TypeScript + 'M3,3H21V21H3V3M7.73,18.04C8.13,18.89 8.92,19.59 10.27,19.59C11.77,19.59 12.8,18.79 12.8,17.04V11.26H11.1V17C11.1,17.86 10.75,18.08 10.2,18.08C9.62,18.08 9.38,17.68 9.11,17.21L7.73,18.04M13.71,17.86C14.21,18.84 15.22,19.59 16.8,19.59C18.4,19.59 19.6,18.76 19.6,17.23C19.6,15.82 18.79,15.19 17.35,14.57L16.93,14.39C16.2,14.08 15.89,13.87 15.89,13.37C15.89,12.96 16.2,12.64 16.7,12.64C17.18,12.64 17.5,12.85 17.79,13.37L19.1,12.5C18.55,11.54 17.77,11.17 16.7,11.17C15.19,11.17 14.22,12.13 14.22,13.4C14.22,14.78 15.03,15.43 16.25,15.95L16.67,16.13C17.45,16.47 17.91,16.68 17.91,17.26C17.91,17.74 17.46,18.09 16.76,18.09C15.93,18.09 15.45,17.66 15.09,17.06L13.71,17.86Z', typescript: - 'M0 2h16v12H0V2zm1 1v10h14V3H1zm4.5 2.5h5v1.2H8.8V11H7.2V6.7H5.5V5.5zm5.3.1c.7 0 1.3.2 1.7.5l-.5.9c-.3-.2-.7-.3-1.1-.3-.5 0-.7.2-.7.4 0 .3.3.4.9.6.9.3 1.4.7 1.4 1.5 0 .9-.7 1.5-1.8 1.5-.8 0-1.4-.2-1.9-.7l.6-.9c.4.3.8.5 1.3.5.5 0 .7-.2.7-.5 0-.3-.2-.4-.8-.6-1-.3-1.5-.7-1.5-1.5 0-.8.7-1.4 1.7-1.4z', - // Python + 'M3,3H21V21H3V3M13.71,17.86C14.21,18.84 15.22,19.59 16.8,19.59C18.4,19.59 19.6,18.76 19.6,17.23C19.6,15.82 18.79,15.19 17.35,14.57L16.93,14.39C16.2,14.08 15.89,13.87 15.89,13.37C15.89,12.96 16.2,12.64 16.7,12.64C17.18,12.64 17.5,12.85 17.79,13.37L19.1,12.5C18.55,11.54 17.77,11.17 16.7,11.17C15.19,11.17 14.22,12.13 14.22,13.4C14.22,14.78 15.03,15.43 16.25,15.95L16.67,16.13C17.45,16.47 17.91,16.68 17.91,17.26C17.91,17.74 17.46,18.09 16.76,18.09C15.93,18.09 15.45,17.66 15.09,17.06L13.71,17.86M13,11.25H8V12.75H9.5V20H11.25V12.75H13V11.25Z', python: - 'M8 1C5 1 5 2.5 5 2.5V4h3v1H4S2 4.5 2 8s1.5 3 1.5 3H5V9.5S5 8 6.5 8H10c1 0 1.5-.5 1.5-1.5V3S12 1 8 1zM6.5 2.5a.5.5 0 110 1 .5.5 0 010-1zM8 15c3 0 3-1.5 3-1.5V12H8v-1h4s2 .5 2-3-1.5-3-1.5-3H11v1.5s0 1.5-1.5 1.5H6c-1 0-1.5.5-1.5 1.5V13s-.5 2 3.5 2zm1.5-1.5a.5.5 0 110-1 .5.5 0 010 1z', - // React (JSX/TSX) + 'M19.14,7.5A2.86,2.86 0 0,1 22,10.36V14.14A2.86,2.86 0 0,1 19.14,17H12C12,17.39 12.32,17.96 12.71,17.96H17V19.64A2.86,2.86 0 0,1 14.14,22.5H9.86A2.86,2.86 0 0,1 7,19.64V15.89C7,14.31 8.28,13.04 9.86,13.04H15.11C16.69,13.04 17.96,11.76 17.96,10.18V7.5H19.14M14.86,19.29C14.46,19.29 14.14,19.59 14.14,20.18C14.14,20.77 14.46,20.89 14.86,20.89A0.71,0.71 0 0,0 15.57,20.18C15.57,19.59 15.25,19.29 14.86,19.29M4.86,17.5C3.28,17.5 2,16.22 2,14.64V10.86C2,9.28 3.28,8 4.86,8H12C12,7.61 11.68,7.04 11.29,7.04H7V5.36C7,3.78 8.28,2.5 9.86,2.5H14.14C15.72,2.5 17,3.78 17,5.36V9.11C17,10.69 15.72,11.96 14.14,11.96H8.89C7.31,11.96 6.04,13.24 6.04,14.82V17.5H4.86M9.14,5.71C9.54,5.71 9.86,5.41 9.86,4.82C9.86,4.23 9.54,4.11 9.14,4.11C8.75,4.11 8.43,4.23 8.43,4.82C8.43,5.41 8.75,5.71 9.14,5.71Z', react: - 'M8 9.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm0-7C5 2.5 2.5 4.5 2.5 5s2 3 5.5 3 5.5-2.5 5.5-3S11 2.5 8 2.5zm0 11c3 0 5.5-2 5.5-2.5s-2-3-5.5-3-5.5 2.5-5.5 3S5 13.5 8 13.5z', - // JSON - json: 'M4 2C3 2 2 3 2 4v3c0 1-1 1-1 1s1 0 1 1v3c0 1 1 2 2 2h1v-1H4c-.5 0-1-.5-1-1V9c0-1-.5-1.5-1-1.5.5 0 1-.5 1-1.5V3c0-.5.5-1 1-1h1V1H4zm8 0c1 0 2 1 2 2v3c0 1 1 1 1 1s-1 0-1 1v3c0 1-1 2-2 2h-1v-1h1c.5 0 1-.5 1-1V9c0-1 .5-1.5 1-1.5-.5 0-1-.5-1-1.5V3c0-.5-.5-1-1-1h-1V1h1z', - // Markdown + 'M12,10.11C13.03,10.11 13.87,10.95 13.87,12C13.87,13 13.03,13.85 12,13.85C10.97,13.85 10.13,13 10.13,12C10.13,10.95 10.97,10.11 12,10.11M7.37,20C8,20.38 9.38,19.8 10.97,18.3C10.45,17.71 9.94,17.07 9.46,16.4C8.64,16.32 7.83,16.2 7.06,16.04C6.55,18.18 6.74,19.65 7.37,20M8.08,14.26L7.79,13.75C7.68,14.04 7.57,14.33 7.5,14.61C7.77,14.67 8.07,14.72 8.38,14.77C8.28,14.6 8.18,14.43 8.08,14.26M14.62,13.5L15.43,12L14.62,10.5C14.32,9.97 14,9.5 13.71,9.03C13.17,9 12.6,9 12,9C11.4,9 10.83,9 10.29,9.03C10,9.5 9.68,9.97 9.38,10.5L8.57,12L9.38,13.5C9.68,14.03 10,14.5 10.29,14.97C10.83,15 11.4,15 12,15C12.6,15 13.17,15 13.71,14.97C14,14.5 14.32,14.03 14.62,13.5M12,6.78C11.81,7 11.61,7.23 11.41,7.5C11.61,7.5 11.8,7.5 12,7.5C12.2,7.5 12.39,7.5 12.59,7.5C12.39,7.23 12.19,7 12,6.78M12,17.22C12.19,17 12.39,16.77 12.59,16.5C12.39,16.5 12.2,16.5 12,16.5C11.8,16.5 11.61,16.5 11.41,16.5C11.61,16.77 11.81,17 12,17.22M16.62,4C16,3.62 14.62,4.2 13.03,5.7C13.55,6.29 14.06,6.93 14.54,7.6C15.36,7.68 16.17,7.8 16.94,7.96C17.45,5.82 17.26,4.35 16.62,4M15.92,9.74L16.21,10.25C16.32,9.96 16.43,9.67 16.5,9.39C16.23,9.33 15.93,9.28 15.62,9.23C15.72,9.4 15.82,9.57 15.92,9.74M17.37,2.69C18.84,3.53 19,5.74 18.38,8.32C20.92,9.07 22.75,10.31 22.75,12C22.75,13.69 20.92,14.93 18.38,15.68C19,18.26 18.84,20.47 17.37,21.31C15.91,22.15 13.92,21.19 12,19.36C10.08,21.19 8.09,22.15 6.62,21.31C5.16,20.47 5,18.26 5.62,15.68C3.08,14.93 1.25,13.69 1.25,12C1.25,10.31 3.08,9.07 5.62,8.32C5,5.74 5.16,3.53 6.62,2.69C8.09,1.85 10.08,2.81 12,4.64C13.92,2.81 15.91,1.85 17.37,2.69M17.08,12C17.42,12.75 17.72,13.5 17.97,14.26C20.07,13.63 21.25,12.73 21.25,12C21.25,11.27 20.07,10.37 17.97,9.74C17.72,10.5 17.42,11.25 17.08,12M6.92,12C6.58,11.25 6.28,10.5 6.03,9.74C3.93,10.37 2.75,11.27 2.75,12C2.75,12.73 3.93,13.63 6.03,14.26C6.28,13.5 6.58,12.75 6.92,12M15.92,14.26C15.82,14.43 15.72,14.6 15.62,14.77C15.93,14.72 16.23,14.67 16.5,14.61C16.43,14.33 16.32,14.04 16.21,13.75L15.92,14.26M13.03,18.3C14.62,19.8 16,20.38 16.62,20C17.26,19.65 17.45,18.18 16.94,16.04C16.17,16.2 15.36,16.32 14.54,16.4C14.06,17.07 13.55,17.71 13.03,18.3M8.08,9.74C8.18,9.57 8.28,9.4 8.38,9.23C8.07,9.28 7.77,9.33 7.5,9.39C7.57,9.67 7.68,9.96 7.79,10.25L8.08,9.74M10.97,5.7C9.38,4.2 8,3.62 7.37,4C6.74,4.35 6.55,5.82 7.06,7.96C7.83,7.8 8.64,7.68 9.46,7.6C9.94,6.93 10.45,6.29 10.97,5.7Z', + json: 'M5,3H7V5H5V10A2,2 0 0,1 3,12A2,2 0 0,1 5,14V19H7V21H5C3.93,20.73 3,20.1 3,19V15A2,2 0 0,0 1,13H0V11H1A2,2 0 0,0 3,9V5A2,2 0 0,1 5,3M19,3A2,2 0 0,1 21,5V9A2,2 0 0,0 23,11H24V13H23A2,2 0 0,0 21,15V19A2,2 0 0,1 19,21H17V19H19V14A2,2 0 0,1 21,12A2,2 0 0,1 19,10V5H17V3H19M12,15A1,1 0 0,1 13,16A1,1 0 0,1 12,17A1,1 0 0,1 11,16A1,1 0 0,1 12,15M8,15A1,1 0 0,1 9,16A1,1 0 0,1 8,17A1,1 0 0,1 7,16A1,1 0 0,1 8,15M16,15A1,1 0 0,1 17,16A1,1 0 0,1 16,17A1,1 0 0,1 15,16A1,1 0 0,1 16,15Z', markdown: - 'M2 3h12v10H2V3zm1 1v8h10V4H3zm1.5 6V6.5l2 2.5 2-2.5V10h1V6h-1l-2 2.5L4.5 6h-1v4h1zm7-2l-1.5 2h1V10h1V8h1L11.5 6z', - // HTML - html: 'M2 1l1 14 5 1 5-1 1-14H2zm3 5h6l-.2 2H7l.2 2h3.4l-.3 3-2.3.7-2.3-.7-.1-1.5h1.5l.1.8 .8.2.8-.2.1-1.3H5.5L5 6z', - // CSS - css: 'M1 1h14l-1.3 12.5L8 15l-5.7-1.5L1 1zm2.2 1.8l1 9.2L8 13.2l3.8-1.2 1-9.2H3.2zm1.3 2h7l-.2 1.5H7l.2 1.5h4l-.4 3.5L8 11.5l-2.8-.8-.2-1.4h1.5l.1.7 1.4.4 1.4-.4.2-1.5H5.2l-.4-3', - // Image - image: 'M2 3h12v10H2V3zm1 1v6l3-3 2 2 2-3 3 4V4H3zm3 1.5a1 1 0 100 2 1 1 0 000-2z', - // Config/gear + 'M20.56 18H3.44C2.65 18 2 17.37 2 16.59V7.41C2 6.63 2.65 6 3.44 6H20.56C21.35 6 22 6.63 22 7.41V16.59C22 17.37 21.35 18 20.56 18M6.81 15.19V11.53L8.73 13.88L10.65 11.53V15.19H12.58V8.81H10.65L8.73 11.16L6.81 8.81H4.89V15.19H6.81M19.69 12H17.77V8.81H15.85V12H13.92L16.81 15.28L19.69 12Z', + html: 'M12,17.56L16.07,16.43L16.62,10.33H9.38L9.2,8.3H16.8L17,6.31H7L7.56,12.32H14.45L14.22,14.9L12,15.5L9.78,14.9L9.64,13.24H7.64L7.93,16.43L12,17.56M4.07,3H19.93L18.5,19.2L12,21L5.5,19.2L4.07,3Z', + css: 'M5,3L4.35,6.34H17.94L17.5,8.5H3.92L3.26,11.83H16.85L16.09,15.64L10.61,17.45L5.86,15.64L6.19,14H2.85L2.06,18L9.91,21L18.96,18L20.16,11.97L20.4,10.76L21.94,3H5Z', + image: + 'M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z', config: - 'M7 1l-.5 2-.7.3-1.5-1.2-1.4 1.4 1.2 1.5-.3.7-2 .5v2l2 .5.3.7-1.2 1.5 1.4 1.4 1.5-1.2.7.3.5 2h2l.5-2 .7-.3 1.5 1.2 1.4-1.4-1.2-1.5.3-.7 2-.5V7l-2-.5-.3-.7 1.2-1.5-1.4-1.4-1.5 1.2-.7-.3L9 1H7zm1 5a2 2 0 110 4 2 2 0 010-4z', - // Terminal/shell - terminal: 'M2 3h12v10H2V3zm1 1v8h10V4H3zm1.5 1L7 7.5 4.5 10l1 1L9 7.5 5.5 4l-1 1zM9 10h3v1H9v-1z', - // Git - git: 'M15 7.5l-6.5-6.5a.7.7 0 00-1 0L6 2.5 7.5 4a1 1 0 011.2 1.2L10 6.5a1 1 0 11-.6.6L8 5.7V11a1 1 0 11-1 0V5.5a1 1 0 01-.5-1.3L5 2.6 1.5 6a.7.7 0 000 1L8 13.5a.7.7 0 001 0L15 7.5z', - // Docker + 'M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z', + terminal: + 'M20,19V7H4V19H20M20,3A2,2 0 0,1 22,5V19A2,2 0 0,1 20,21H4A2,2 0 0,1 2,19V5C2,3.89 2.9,3 4,3H20M13,17V15H18V17H13M9.58,13L5.57,9H8.4L11.7,12.3C12.09,12.69 12.09,13.33 11.7,13.72L8.42,17H5.59L9.58,13Z', + git: 'M2.6,10.59L8.38,4.8L10.07,6.5C9.83,7.35 10.22,8.28 11,8.73V14.27C10.4,14.61 10,15.26 10,16A2,2 0 0,0 12,18A2,2 0 0,0 14,16C14,15.26 13.6,14.61 13,14.27V9.41L15.07,11.5C15,11.65 15,11.82 15,12A2,2 0 0,0 17,14A2,2 0 0,0 19,12A2,2 0 0,0 17,10C16.82,10 16.65,10 16.5,10.07L13.93,7.5C14.19,6.57 13.71,5.55 12.78,5.16C12.35,5 11.9,4.96 11.5,5.07L9.8,3.38L10.59,2.6C11.37,1.81 12.63,1.81 13.41,2.6L21.4,10.59C22.19,11.37 22.19,12.63 21.4,13.41L13.41,21.4C12.63,22.19 11.37,22.19 10.59,21.4L2.6,13.41C1.81,12.63 1.81,11.37 2.6,10.59Z', docker: - 'M9 3H7v2h2V3zM6 3H4v2h2V3zM6 6H4v2h2V6zM9 6H7v2h2V6zm3-3h-2v2h2V3zm-3-3H7v2h2V0zm3 6h-2v2h2V6zm3 1c-.5-.3-1.5-.4-2 0-.2-.8-.7-1.5-1.5-1.8l-.3-.2-.2.3c-.3.5-.4 1.2-.3 1.7-1-.5-2-.4-2-.4H1c-.2 1 0 3 1 4.5S4.5 14 7 14c4 0 7-2 8.5-5.5 1 0 2-.1 2.5-1l.2-.3-.5-.2z', - // Database/SQL + 'M21.81 10.25C21.75 10.21 21.25 9.82 20.17 9.82C19.89 9.82 19.61 9.85 19.33 9.9C19.12 8.5 17.95 7.79 17.9 7.76L17.61 7.59L17.43 7.86C17.19 8.22 17 8.63 16.92 9.05C16.72 9.85 16.84 10.61 17.25 11.26C16.76 11.54 15.96 11.61 15.79 11.61H2.62C2.28 11.61 2 11.89 2 12.24C2 13.39 2.18 14.54 2.58 15.62C3.03 16.81 3.71 17.69 4.58 18.23C5.56 18.83 7.17 19.17 9 19.17C9.79 19.17 10.61 19.1 11.42 18.95C12.54 18.75 13.62 18.36 14.61 17.79C15.43 17.32 16.16 16.72 16.78 16C17.83 14.83 18.45 13.5 18.9 12.35H19.09C20.23 12.35 20.94 11.89 21.33 11.5C21.59 11.26 21.78 10.97 21.92 10.63L22 10.39L21.81 10.25M3.85 11.24H5.61C5.69 11.24 5.77 11.17 5.77 11.08V9.5C5.77 9.42 5.7 9.34 5.61 9.34H3.85C3.76 9.34 3.69 9.41 3.69 9.5V11.08C3.7 11.17 3.76 11.24 3.85 11.24M6.28 11.24H8.04C8.12 11.24 8.2 11.17 8.2 11.08V9.5C8.2 9.42 8.13 9.34 8.04 9.34H6.28C6.19 9.34 6.12 9.41 6.12 9.5V11.08C6.13 11.17 6.19 11.24 6.28 11.24M8.75 11.24H10.5C10.6 11.24 10.67 11.17 10.67 11.08V9.5C10.67 9.42 10.61 9.34 10.5 9.34H8.75C8.67 9.34 8.6 9.41 8.6 9.5V11.08C8.6 11.17 8.66 11.24 8.75 11.24M11.19 11.24H12.96C13.04 11.24 13.11 11.17 13.11 11.08V9.5C13.11 9.42 13.05 9.34 12.96 9.34H11.19C11.11 9.34 11.04 9.41 11.04 9.5V11.08C11.04 11.17 11.11 11.24 11.19 11.24M6.28 9H8.04C8.12 9 8.2 8.91 8.2 8.82V7.25C8.2 7.16 8.13 7.09 8.04 7.09H6.28C6.19 7.09 6.12 7.15 6.12 7.25V8.82C6.13 8.91 6.19 9 6.28 9M8.75 9H10.5C10.6 9 10.67 8.91 10.67 8.82V7.25C10.67 7.16 10.61 7.09 10.5 7.09H8.75C8.67 7.09 8.6 7.15 8.6 7.25V8.82C8.6 8.91 8.66 9 8.75 9M11.19 9H12.96C13.04 9 13.11 8.91 13.11 8.82V7.25C13.11 7.16 13.04 7.09 12.96 7.09H11.19C11.11 7.09 11.04 7.15 11.04 7.25V8.82C11.04 8.91 11.11 9 11.19 9M11.19 6.72H12.96C13.04 6.72 13.11 6.65 13.11 6.56V5C13.11 4.9 13.04 4.83 12.96 4.83H11.19C11.11 4.83 11.04 4.89 11.04 5V6.56C11.04 6.64 11.11 6.72 11.19 6.72M13.65 11.24H15.41C15.5 11.24 15.57 11.17 15.57 11.08V9.5C15.57 9.42 15.5 9.34 15.41 9.34H13.65C13.57 9.34 13.5 9.41 13.5 9.5V11.08C13.5 11.17 13.57 11.24 13.65 11.24', database: - 'M8 1C5 1 2 1.7 2 3v10c0 1.3 3 2 6 2s6-.7 6-2V3c0-1.3-3-2-6-2zm0 1c3 0 5 .7 5 1s-2 1-5 1S3 3.3 3 3s2-1 5-1zM3 5c1 .6 3 1 5 1s4-.4 5-1v2c0 .3-2 1-5 1s-5-.7-5-1V5zm0 4c1 .6 3 1 5 1s4-.4 5-1v2c0 .3-2 1-5 1s-5-.7-5-1V9zm0 4c1 .6 3 1 5 1s4-.4 5-1v0c0 .3-2 1-5 1s-5-.7-5-1v0z', - // Lock - lock: 'M5 7V5a3 3 0 016 0v2h1v7H4V7h1zm1 0h4V5a2 2 0 00-4 0v2zm2 2a1 1 0 00-.5 1.9V12h1v-1.1A1 1 0 008 9z', - // Env/key - env: 'M10 1a4 4 0 00-3.9 5L1 11v3h3v-2h2v-2h2l.6-.6A4 4 0 1010 1zm1 3a1 1 0 110-2 1 1 0 010 2z', - // License + 'M12,3C7.58,3 4,4.79 4,7C4,9.21 7.58,11 12,11C16.42,11 20,9.21 20,7C20,4.79 16.42,3 12,3M4,9V12C4,14.21 7.58,16 12,16C16.42,16 20,14.21 20,12V9C20,11.21 16.42,13 12,13C7.58,13 4,11.21 4,9M4,14V17C4,19.21 7.58,21 12,21C16.42,21 20,19.21 20,17V14C20,16.21 16.42,18 12,18C7.58,18 4,16.21 4,14Z', + lock: 'M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z', + env: 'M7 14C5.9 14 5 13.1 5 12S5.9 10 7 10 9 10.9 9 12 8.1 14 7 14M12.6 10C11.8 7.7 9.6 6 7 6C3.7 6 1 8.7 1 12S3.7 18 7 18C9.6 18 11.8 16.3 12.6 14H16V18H20V14H23V10H12.6Z', license: - 'M8 1a7 7 0 100 14A7 7 0 008 1zm0 1a6 6 0 110 12A6 6 0 018 2zm0 2a2 2 0 100 4 2 2 0 000-4zm-3 6c0-1 1.5-1.5 3-1.5s3 .5 3 1.5v1H5v-1z', - // Ruby - ruby: 'M2 8l4-6h4l4 6-6 6-6-6zm3-1h6l-3 5-3-5z', - // Rust - rust: 'M8 2a6 6 0 100 12A6 6 0 008 2zm0 1a5 5 0 110 10A5 5 0 018 3zm-1 2v2H5v2h2v2h2V9h2V7H9V5H7z', - // Go - go: 'M3 7c0-3 2-5 5-5s5 2 5 5v2c0 3-2 5-5 5s-5-2-5-5V7zm2 0v2c0 2 1.5 3 3 3s3-1 3-3V7c0-2-1.5-3-3-3S5 5 5 7z', - // Java - java: 'M6 2s-.5 2 1.5 3C9 6 9 7 9 7s1-1 0-2.5C8 3 6 2 6 2zm-1 6s-2 .5 0 1.5c2 1 5 1 7 0 0 0 .5-.5-1-.5s-3 .5-6-1zm1 2s-3 .5 0 1.5c2.5.8 6 .5 7 0 0 0 .5-.5-1.5-.5s-3 .5-5.5-1zm2-5c-2 0-4 1-4 2s2 2 4 2 4-1 4-2-2-2-4-2z', - // YAML - yaml: 'M2 3h12v10H2V3zm1 1v8h10V4H3zm2 1l1.5 2L5 9h1l1-1.5L8 9h1L7.5 7 9 5H8L7 6.5 6 5H5z', - // XML - xml: 'M5 5L2 8l3 3 1-1-2-2 2-2-1-1zm6 0l-1 1 2 2-2 2 1 1 3-3-3-3zM7 12l2-8h-1l-2 8h1z', - // Vue - vue: 'M1 2h4l3 5 3-5h4L8 14 1 2zm2 1l5 9 5-9h-2l-3 5-3-5H3z', - // Svelte + 'M9 10A3.04 3.04 0 0 1 12 7A3.04 3.04 0 0 1 15 10A3.04 3.04 0 0 1 12 13A3.04 3.04 0 0 1 9 10M12 19L16 20V16.92A7.54 7.54 0 0 1 12 18A7.54 7.54 0 0 1 8 16.92V20M12 4A5.78 5.78 0 0 0 7.76 5.74A5.78 5.78 0 0 0 6 10A5.78 5.78 0 0 0 7.76 14.23A5.78 5.78 0 0 0 12 16A5.78 5.78 0 0 0 16.24 14.23A5.78 5.78 0 0 0 18 10A5.78 5.78 0 0 0 16.24 5.74A5.78 5.78 0 0 0 12 4M20 10A8.04 8.04 0 0 1 19.43 12.8A7.84 7.84 0 0 1 18 15.28V23L12 21L6 23V15.28A7.9 7.9 0 0 1 4 10A7.68 7.68 0 0 1 6.33 4.36A7.73 7.73 0 0 1 12 2A7.73 7.73 0 0 1 17.67 4.36A7.68 7.68 0 0 1 20 10Z', + ruby: 'M18.8,2.07C21.32,2.5 22.04,4.23 22,6.04V6L20.86,20.93L6.08,21.94H6.09C4.86,21.89 2.13,21.77 2,17.95L3.37,15.45L6.14,21.91L8.5,14.29L8.45,14.3L8.47,14.28L16.18,16.74L14.19,8.96L21.54,8.5L15.75,3.76L18.8,2.06V2.07M2,17.91V17.93L2,17.91V17.91M6.28,6.23C9.24,3.28 13.07,1.54 14.54,3.03C16,4.5 14.46,8.12 11.5,11.06C8.5,14 4.73,15.84 3.26,14.36C1.79,12.87 3.3,9.17 6.27,6.23H6.28Z', + rust: 'M21.9 11.7L21 11.2V11L21.7 10.3C21.8 10.2 21.8 10 21.7 9.9L21.6 9.8L20.7 9.5C20.7 9.4 20.7 9.3 20.6 9.3L21.2 8.5C21.3 8.4 21.3 8.2 21.1 8.1C21.1 8.1 21 8.1 21 8L20 7.8C20 7.7 19.9 7.7 19.9 7.6L20.3 6.7V6.4C20.2 6.3 20.1 6.3 20 6.3H19C19 6.3 19 6.2 18.9 6.2L19.1 5.2C19.1 5 19 4.9 18.9 4.9H18.8L17.8 5.1C17.8 5 17.7 5 17.6 4.9V3.9C17.6 3.7 17.5 3.6 17.3 3.6H17.2L16.3 4H16.2L16 3C16 2.8 15.8 2.7 15.7 2.8H15.6L14.8 3.4C14.7 3.4 14.6 3.4 14.6 3.3L14.3 2.4C14.2 2.3 14.1 2.2 13.9 2.2C13.9 2.2 13.8 2.2 13.8 2.3L13 3H12.8L12.3 2.2C12.2 2 12 2 11.8 2L11.7 2.1L11.2 3H11L10.3 2.3C10.2 2.2 10 2.2 9.9 2.3L9.8 2.4L9.5 3.3C9.4 3.3 9.3 3.3 9.3 3.4L8.5 2.8C8.3 2.7 8.1 2.7 8 2.9V3L7.8 4C7.8 4 7.7 4 7.6 4.1L6.7 3.7C6.6 3.6 6.4 3.7 6.3 3.8V4.9C6.3 5 6.2 5 6.2 5.1L5.2 4.9C5 4.8 4.9 4.9 4.9 5.1V5.2L5.1 6.2C5 6.2 5 6.3 4.9 6.3H3.9C3.7 6.3 3.6 6.4 3.6 6.6V6.7L4 7.6V7.8L3 8C2.8 8 2.7 8.2 2.7 8.3V8.4L3.3 9.2C3.3 9.3 3.3 9.4 3.2 9.4L2.4 9.8C2.3 9.9 2.2 10 2.2 10.2C2.2 10.2 2.2 10.3 2.3 10.3L3 11V11.2L2.2 11.7C2 11.8 2 12 2 12.1L2.1 12.2L3 12.8V13L2.3 13.7C2.2 13.8 2.2 14 2.3 14.1L2.4 14.2L3.3 14.5C3.3 14.6 3.3 14.7 3.4 14.7L2.8 15.5C2.7 15.6 2.7 15.8 2.9 15.9C2.9 15.9 3 15.9 3 16L4 16.2C4 16.3 4.1 16.3 4.1 16.4L3.7 17.3C3.6 17.4 3.7 17.6 3.8 17.7H4.9C5 17.7 5 17.8 5.1 17.8L4.9 18.8C4.9 19 5 19.1 5.1 19.1H5.2L6.2 18.9C6.2 19 6.3 19 6.4 19.1V20.1C6.4 20.3 6.5 20.4 6.7 20.4H6.8L7.7 20H7.8L8 21C8 21.2 8.2 21.3 8.3 21.2H8.4L9.2 20.6C9.3 20.6 9.4 20.6 9.4 20.7L9.7 21.6C9.8 21.7 9.9 21.8 10.1 21.8C10.1 21.8 10.2 21.8 10.2 21.7L11 21H11.2L11.7 21.8C11.8 21.9 12 22 12.1 21.9L12.2 21.8L12.7 21H12.9L13.6 21.7C13.7 21.8 13.9 21.8 14 21.7L14.1 21.6L14.4 20.7C14.5 20.7 14.6 20.7 14.6 20.6L15.4 21.2C15.5 21.3 15.7 21.3 15.8 21.1C15.8 21.1 15.8 21 15.9 21L16.1 20C16.2 20 16.2 19.9 16.3 19.9L17.2 20.3C17.3 20.4 17.5 20.3 17.6 20.2V19.1L17.8 18.9L18.8 19.1C19 19.1 19.1 19 19.1 18.9V18.8L18.9 17.8L19.1 17.6H20.1C20.3 17.6 20.4 17.5 20.4 17.3V17.2L20 16.3C20 16.2 20.1 16.2 20.1 16.1L21.1 15.9C21.3 15.9 21.4 15.7 21.3 15.6V15.5L20.7 14.7L20.8 14.5L21.7 14.2C21.8 14.1 21.9 14 21.9 13.8C21.9 13.8 21.9 13.7 21.8 13.7L21 13V12.8L21.8 12.3C22 12.2 22 12 21.9 11.7C21.9 11.8 21.9 11.8 21.9 11.7M16.2 18.7C15.9 18.6 15.7 18.3 15.7 18C15.8 17.7 16.1 17.5 16.4 17.5C16.7 17.6 16.9 17.9 16.9 18.2C16.9 18.6 16.6 18.8 16.2 18.7M16 16.8C15.7 16.7 15.4 16.9 15.4 17.2L15 18.6C14.1 19 13.1 19.2 12 19.2C10.9 19.2 9.9 19 8.9 18.5L8.6 17.1C8.5 16.8 8.3 16.6 8 16.7L6.8 17C6.6 16.8 6.4 16.5 6.2 16.3H12.2C12.3 16.3 12.3 16.3 12.3 16.2V14.1C12.3 14 12.3 14 12.2 14H10.5V12.7H12.4C12.6 12.7 13.3 12.7 13.6 13.7C13.7 14 13.8 15 14 15.3C14.1 15.6 14.6 16.3 15.1 16.3H18.2C18 16.6 17.8 16.8 17.5 17.1L16 16.8M7.7 18.7C7.4 18.8 7.1 18.6 7 18.2C6.9 17.9 7.1 17.6 7.5 17.5S8.1 17.6 8.2 18C8.2 18.3 8 18.6 7.7 18.7M5.4 9.5C5.5 9.8 5.4 10.2 5.1 10.3C4.8 10.4 4.4 10.3 4.3 10C4.2 9.7 4.3 9.3 4.6 9.2C5 9.1 5.3 9.2 5.4 9.5M4.7 11.1L6 10.6C6.3 10.5 6.4 10.2 6.3 9.9L6 9.3H7V14H5C4.7 13 4.6 12.1 4.7 11.1M10.3 10.7V9.3H12.8C12.9 9.3 13.7 9.4 13.7 10C13.7 10.5 13.1 10.7 12.6 10.7H10.3M19.3 11.9V12.4H18.5C18.4 12.4 18.4 12.4 18.4 12.5V12.8C18.4 13.6 17.9 13.8 17.5 13.8C17.1 13.8 16.7 13.6 16.6 13.4C16.4 12.1 16 11.9 15.4 11.4C16.1 10.9 16.9 10.2 16.9 9.3C16.9 8.3 16.2 7.7 15.8 7.4C15.1 7 14.4 6.9 14.2 6.9H6.6C7.7 5.7 9.1 4.9 10.7 4.6L11.6 5.6C11.8 5.8 12.1 5.8 12.4 5.6L13.4 4.6C15.5 5 17.3 6.3 18.4 8.2L17.7 9.8C17.6 10.1 17.7 10.4 18 10.5L19.3 11.1V11.9M11.6 3.9C11.8 3.7 12.2 3.7 12.4 3.9C12.6 4.1 12.6 4.5 12.4 4.7C12.1 5 11.8 5 11.5 4.7C11.3 4.5 11.4 4.2 11.6 3.9M18.5 9.5C18.6 9.2 19 9.1 19.3 9.2C19.6 9.3 19.7 9.7 19.6 10C19.5 10.3 19.1 10.4 18.8 10.3C18.5 10.2 18.4 9.8 18.5 9.5Z', + go: 'M2.64,10.33L2.62,10.27L2.84,10L2.96,9.92H6.8L6.83,10L6.65,10.26L6.54,10.32L2.64,10.33M1.03,11.31L1,11.26L1.22,10.97L1.34,10.91H6.24L6.29,11L6.21,11.24L6.11,11.31H1.03M3.63,12.3L3.59,12.24L3.75,11.96L3.85,11.9H6L6.07,11.97L6.05,12.22L5.97,12.3H3.63M14.78,10.14L13,10.61C12.81,10.65 12.8,10.66 12.66,10.5C12.5,10.32 12.39,10.21 12.16,10.1C11.5,9.76 10.83,9.86 10.22,10.25C9.5,10.73 9.11,11.42 9.12,12.3C9.13,13.16 9.72,13.87 10.57,14C11.3,14.09 11.91,13.83 12.4,13.28L12.69,12.89H10.62C10.4,12.89 10.35,12.75 10.42,12.57L10.97,11.39C11,11.33 11.08,11.22 11.24,11.22H14.68C14.83,10.72 15.09,10.26 15.43,9.81C16.21,8.78 17.16,8.24 18.43,8C19.5,7.82 20.56,7.93 21.5,8.57C22.34,9.15 22.87,9.93 23,10.96C23.19,12.41 22.76,13.59 21.76,14.61C21.05,15.33 20.18,15.78 19.19,16L18.33,16.08C17.35,16.06 16.46,15.78 15.71,15.13C15.19,14.68 14.83,14.14 14.65,13.5C14.5,13.74 14.38,13.97 14.21,14.2C13.44,15.22 12.43,15.85 11.15,16C10.1,16.16 9.12,15.95 8.26,15.31C7.47,14.71 7,13.91 6.9,12.92C6.76,11.75 7.1,10.7 7.81,9.78C8.57,8.78 9.58,8.15 10.82,7.92C11.82,7.74 12.79,7.86 13.66,8.44C14.23,8.82 14.63,9.34 14.9,9.96C14.94,10.05 14.9,10.11 14.78,10.14M20.89,11.74L20.86,11.38C20.67,10.32 19.69,9.72 18.67,9.95C17.66,10.17 17,10.8 16.79,11.81C16.6,12.65 17,13.5 17.77,13.84C18.36,14.1 18.96,14.06 19.53,13.78C20.37,13.35 20.84,12.66 20.89,11.74Z', + java: 'M16.5,6.08C16.5,6.08 9.66,7.79 12.94,11.56C13.91,12.67 12.69,13.67 12.69,13.67C12.69,13.67 15.14,12.42 14,10.82C12.94,9.35 12.14,8.62 16.5,6.08M12.03,7.28C16.08,4.08 14,2 14,2C14.84,5.3 11.04,6.3 9.67,8.36C8.73,9.76 10.13,11.27 12,13C11.29,11.3 8.78,9.84 12.03,7.28M9.37,17.47C6.29,18.33 11.25,20.1 15.16,18.43C14.78,18.28 14.41,18.1 14.06,17.89C12.7,18.2 11.3,18.26 9.92,18.07C8.61,17.91 9.37,17.47 9.37,17.47M14.69,15.79C12.94,16.17 11.13,16.26 9.35,16.05C8.04,15.92 8.9,15.28 8.9,15.28C5.5,16.41 10.78,17.68 15.5,16.3C15.21,16.19 14.93,16 14.69,15.79M18.11,19.09C18.11,19.09 18.68,19.56 17.5,19.92C15.22,20.6 8.07,20.81 6.09,19.95C5.38,19.64 6.72,19.21 7.14,19.12C7.37,19.06 7.6,19.04 7.83,19.04C7.04,18.5 2.7,20.14 5.64,20.6C13.61,21.9 20.18,20 18.11,19.09M15.37,14.23C15.66,14.04 15.97,13.88 16.29,13.74C16.29,13.74 14.78,14 13.27,14.14C11.67,14.3 10.06,14.32 8.46,14.2C6.11,13.89 9.75,13 9.75,13C8.65,13 7.57,13.26 6.59,13.75C4.54,14.75 11.69,15.2 15.37,14.23M16.27,16.65C16.25,16.69 16.23,16.72 16.19,16.75C21.2,15.44 19.36,12.11 16.96,12.94C16.83,13 16.72,13.08 16.65,13.19C16.79,13.14 16.93,13.1 17.08,13.07C18.28,12.83 20,14.7 16.27,16.65M16.4,21.26C13.39,21.78 10.31,21.82 7.28,21.4C7.28,21.4 7.74,21.78 10.09,21.93C13.69,22.16 19.22,21.8 19.35,20.1C19.38,20.11 19.12,20.75 16.4,21.26Z', + yaml: 'M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6.12,15.5L9.86,19.24L11.28,17.83L8.95,15.5L11.28,13.17L9.86,11.76L6.12,15.5M17.28,15.5L13.54,11.76L12.12,13.17L14.45,15.5L12.12,17.83L13.54,19.24L17.28,15.5Z', + xml: 'M12.89,3L14.85,3.4L11.11,21L9.15,20.6L12.89,3M19.59,12L16,8.41V5.58L22.42,12L16,18.41V15.58L19.59,12M1.58,12L8,5.58V8.41L4.41,12L8,15.58V18.41L1.58,12Z', + vue: 'M2,3H5.5L12,15L18.5,3H22L12,21L2,3M6.5,3H9.5L12,7.58L14.5,3H17.5L12,13.08L6.5,3Z', svelte: 'M12 2C10 .5 7 1 6 3L4 6c-.7 1-.8 2.5-.3 3.5-1 1-1.2 3-.5 4C5 15.5 8 15 9 13l2-3c.7-1 .8-2.5.3-3.5 1-1 1.2-3 .5-4zm-4 9l-2 3c-.5.5-1.5.5-2 0-.5-.5-.5-1.5 0-2l2-3 2 2zm2-4L8 10l-2-2 2-3c.5-.5 1.5-.5 2 0 .5.5.5 1.5 0 2z', - // C# csharp: - 'M8 2a6 6 0 100 12A6 6 0 008 2zm-.5 3h1V6H10v1H8.5v2H10v1H8.5v1h-1v-1H6V9h1.5V7H6V6h1.5V5z', - // C++ - cpp: 'M8 2a6 6 0 100 12A6 6 0 008 2zm-1 3h1V6.5h1.5v1H8V9h1.5v1H8v1.5H7V10H5.5V9H7V7.5H5.5v-1H7V5z', - // C - c: 'M8 2a6 6 0 100 12A6 6 0 008 2zm1 3c1.5 0 2.5 1 3 2l-1.5.5C10.3 7 9.7 6.5 9 6.5 7.5 6.5 7 7.5 7 8s.5 1.5 2 1.5c.7 0 1.3-.5 1.5-1L12 9c-.5 1-1.5 2-3 2-2 0-3.5-1.5-3.5-3S7 5 9 5z', - // Kotlin - kotlin: 'M2 2h12L8 8l6 6H2V2zm2 2v8h6l-4-4 4-4H4z', + 'M11.5,15.97L11.91,18.41C11.65,18.55 11.23,18.68 10.67,18.8C10.1,18.93 9.43,19 8.66,19C6.45,18.96 4.79,18.3 3.68,17.04C2.56,15.77 2,14.16 2,12.21C2.05,9.9 2.72,8.13 4,6.89C5.32,5.64 6.96,5 8.94,5C9.69,5 10.34,5.07 10.88,5.19C11.42,5.31 11.82,5.44 12.08,5.59L11.5,8.08L10.44,7.74C10.04,7.64 9.58,7.59 9.05,7.59C7.89,7.58 6.93,7.95 6.18,8.69C5.42,9.42 5.03,10.54 5,12.03C5,13.39 5.37,14.45 6.08,15.23C6.79,16 7.79,16.4 9.07,16.41L10.4,16.29C10.83,16.21 11.19,16.1 11.5,15.97M13.89,19L14.5,15H13L13.34,13H14.84L15.16,11H13.66L14,9H15.5L16.11,5H18.11L17.5,9H18.5L19.11,5H21.11L20.5,9H22L21.66,11H20.16L19.84,13H21.34L21,15H19.5L18.89,19H16.89L17.5,15H16.5L15.89,19H13.89M16.84,13H17.84L18.16,11H17.16L16.84,13Z', + cpp: 'M10.5,15.97L10.91,18.41C10.65,18.55 10.23,18.68 9.67,18.8C9.1,18.93 8.43,19 7.66,19C5.45,18.96 3.79,18.3 2.68,17.04C1.56,15.77 1,14.16 1,12.21C1.05,9.9 1.72,8.13 3,6.89C4.32,5.64 5.96,5 7.94,5C8.69,5 9.34,5.07 9.88,5.19C10.42,5.31 10.82,5.44 11.08,5.59L10.5,8.08L9.44,7.74C9.04,7.64 8.58,7.59 8.05,7.59C6.89,7.58 5.93,7.95 5.18,8.69C4.42,9.42 4.03,10.54 4,12.03C4,13.39 4.37,14.45 5.08,15.23C5.79,16 6.79,16.4 8.07,16.41L9.4,16.29C9.83,16.21 10.19,16.1 10.5,15.97M11,11H13V9H15V11H17V13H15V15H13V13H11V11M18,11H20V9H22V11H24V13H22V15H20V13H18V11Z', + c: 'M15.45,15.97L15.87,18.41C15.61,18.55 15.19,18.68 14.63,18.8C14.06,18.93 13.39,19 12.62,19C10.41,18.96 8.75,18.3 7.64,17.04C6.5,15.77 5.96,14.16 5.96,12.21C6,9.9 6.68,8.13 8,6.89C9.28,5.64 10.92,5 12.9,5C13.65,5 14.3,5.07 14.84,5.19C15.38,5.31 15.78,5.44 16.04,5.59L15.44,8.08L14.4,7.74C14,7.64 13.53,7.59 13,7.59C11.85,7.58 10.89,7.95 10.14,8.69C9.38,9.42 9,10.54 8.96,12.03C8.97,13.39 9.33,14.45 10.04,15.23C10.75,16 11.74,16.4 13.03,16.41L14.36,16.29C14.79,16.21 15.15,16.1 15.45,15.97Z', + kotlin: 'M2 2H22L12 12L22 22H2Z', }; /** diff --git a/packages/mermaid/src/diagrams/treeView/renderer.ts b/packages/mermaid/src/diagrams/treeView/renderer.ts index d895cf7ae11..5c67cab050b 100644 --- a/packages/mermaid/src/diagrams/treeView/renderer.ts +++ b/packages/mermaid/src/diagrams/treeView/renderer.ts @@ -36,7 +36,7 @@ const injectIconDefs = (svg: D3SVGElement, root: Node, diagramId: defs .append('symbol') .attr('id', `tv-icon-${diagramId}-${iconId}`) - .attr('viewBox', '0 0 16 16') + .attr('viewBox', '0 0 24 24') .append('path') .attr('d', path); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 049ba1403f5..40eacdcbea1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -302,9 +302,6 @@ importers: '@iconify/types': specifier: ^2.0.0 version: 2.0.0 - '@mdi/js': - specifier: ^7.4.47 - version: 7.4.47 '@types/cytoscape': specifier: ^3.21.9 version: 3.21.9 @@ -2588,9 +2585,6 @@ packages: '@mdi/font@7.4.47': resolution: {integrity: sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==} - '@mdi/js@7.4.47': - resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==} - '@microsoft/tsdoc-config@0.17.1': resolution: {integrity: sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw==} @@ -12489,8 +12483,6 @@ snapshots: '@mdi/font@7.4.47': {} - '@mdi/js@7.4.47': {} - '@microsoft/tsdoc-config@0.17.1': dependencies: '@microsoft/tsdoc': 0.15.1 From dbfde57d959d0c7ac87e6fa5962b77234c5dc51f Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Fri, 27 Mar 2026 11:26:54 +1100 Subject: [PATCH 05/11] style: fix prettier formatting in CHANGELOG, treeView demo, and icon comment --- CHANGELOG.md | 2 +- demos/treeView.html | 220 +++++++++--------- .../mermaid/src/diagrams/treeView/icons.ts | 2 +- 3 files changed, 111 insertions(+), 113 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c68d3f76d18..63a51e673ea 120000 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -./packages/mermaid/CHANGELOG.md \ No newline at end of file +./packages/mermaid/CHANGELOG.md diff --git a/demos/treeView.html b/demos/treeView.html index 6f6e26252cf..054862c4e54 100644 --- a/demos/treeView.html +++ b/demos/treeView.html @@ -1,72 +1,71 @@ + + + + Mermaid TreeView Diagram Demo + + - + .demo .rendered .mermaid { + margin: 0; + } + + - -

TreeView Diagram Demo

-

- Each example shows the input syntax (left) alongside the - rendered output (right). -

+ +

TreeView Diagram Demo

+

+ Each example shows the input syntax (left) alongside the + rendered output (right). +

-

Basic TreeView (quoted labels)

-
-

-    
-
+    

Basic TreeView (quoted labels)

+
+

+      
+
 treeView-beta
 "docs"
     "build"
@@ -79,14 +78,14 @@ 

Basic TreeView (quoted labels)

"_templates" "div. Files"
+
-
-

Bare Labels with Icons

-
-

-    
-
+    

Bare Labels with Icons

+
+

+      
+
 treeView-beta
 my-project/
     src/
@@ -99,14 +98,14 @@ 

Bare Labels with Icons

package.json README.md
+
-
-

Annotations: :::class, icon(), ##

-
-

-    
-
+    

Annotations: :::class, icon(), ##

+
+

+      
+
 treeView-beta
 my-project/
     src/
@@ -119,14 +118,14 @@ 

Annotations: :::class, icon(), ##

Dockerfile package.json
+
-
-

Folder Names with Spaces

-
-

-    
-
+    

Folder Names with Spaces

+
+

+      
+
 treeView-beta
 My Project/
     Source Files/
@@ -138,14 +137,14 @@ 

Folder Names with Spaces

.gitignore README.md
+
-
-

Custom Config

-
-

-    
-
+    

Custom Config

+
+

+      
+
 ---
 config:
     treeView:
@@ -163,34 +162,33 @@ 

Custom Config

"src" "parser"
+
-
- - - \ No newline at end of file + import mermaid from './mermaid.esm.mjs'; + mermaid.initialize({ + logLevel: 1, + securityLevel: 'loose', + }); + + + diff --git a/packages/mermaid/src/diagrams/treeView/icons.ts b/packages/mermaid/src/diagrams/treeView/icons.ts index 309f54d8bb7..5b97a960ffa 100644 --- a/packages/mermaid/src/diagrams/treeView/icons.ts +++ b/packages/mermaid/src/diagrams/treeView/icons.ts @@ -117,7 +117,7 @@ export function resolveIcon(label: string, nodeType: NodeType): string { /** * Inline SVG icon paths, keyed by icon ID. * Each value is an SVG path `d` attribute for a 24×24 viewBox. - * Icon paths from Material Design Icons by Pictogrammers (Apache 2.0). + * Icon paths from Material Design Icons (Apache 2.0). * https://github.com/Templarian/MaterialDesign */ export const ICON_PATHS: Record = { From 2f384ae7dafb1f89e9e6fde2b37860bdc47e0190 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Fri, 27 Mar 2026 16:27:24 +1100 Subject: [PATCH 06/11] refactor(treeView): replace NODE_CONTENT with structured Langium terminals Replace monolithic NODE_CONTENT terminal with structured terminals (QUOTED_NAME, BARE_NAME, CLASS_ANNOTATION, ICON_ANNOTATION, DESC_ANNOTATION) so the grammar handles parsing instead of imperative regex in parser.ts. - Rewrite treeView.langium with 5 structured terminals - Update valueConverter.ts with converters for each terminal - Simplify parser.ts by removing parseNodeContent() entirely - Rewrite parser.spec.ts as integration tests - Update treeView.test.ts for new AST fields Addresses reviewer feedback from @lee-treehouse on PR #7527. --- .../src/diagrams/treeView/parser.spec.ts | 283 ++++++++++-------- .../mermaid/src/diagrams/treeView/parser.ts | 111 ++----- .../src/language/treeView/treeView.langium | 31 +- .../src/language/treeView/valueConverter.ts | 19 ++ packages/parser/tests/treeView.test.ts | 110 ++++++- 5 files changed, 310 insertions(+), 244 deletions(-) diff --git a/packages/mermaid/src/diagrams/treeView/parser.spec.ts b/packages/mermaid/src/diagrams/treeView/parser.spec.ts index 92a9ee602a5..7e487c4febb 100644 --- a/packages/mermaid/src/diagrams/treeView/parser.spec.ts +++ b/packages/mermaid/src/diagrams/treeView/parser.spec.ts @@ -1,210 +1,231 @@ -import { describe, expect, it } from 'vitest'; -import { parseNodeContent } from './parser.js'; +import { describe, expect, it, beforeEach } from 'vitest'; +import { parser } from './parser.js'; +import db from './db.js'; + +/** + * Integration tests for the treeView parser pipeline. + * + * Tests the full flow: Langium grammar → value converter → populate() → db. + * Previously these tested parseNodeContent() directly; now parsing is handled + * by the Langium grammar with structured terminals. + */ + +/** Helper: parse input and return the root's first child node from db */ +async function parseAndGetNode(line: string, index = 0) { + db.clear(); + await parser.parse(`treeView-beta\n${line}`); + const root = db.getRoot(); + return root.children[index]; +} + +describe('treeView parser integration', () => { + beforeEach(() => { + db.clear(); + }); -describe('parseNodeContent', () => { describe('bare labels', () => { - it('should parse a simple filename', () => { - const result = parseNodeContent('index.js'); - expect(result.name).toBe('index.js'); - expect(result.nodeType).toBe('file'); + it('should parse a simple filename', async () => { + const node = await parseAndGetNode('index.js'); + expect(node.name).toBe('index.js'); + expect(node.nodeType).toBe('file'); }); - it('should detect directory from trailing slash', () => { - const result = parseNodeContent('src/'); - expect(result.name).toBe('src'); - expect(result.nodeType).toBe('directory'); - expect(result.iconId).toBe('folder'); + it('should detect directory from trailing slash', async () => { + const node = await parseAndGetNode('src/'); + expect(node.name).toBe('src'); + expect(node.nodeType).toBe('directory'); + expect(node.iconId).toBe('folder'); }); - it('should parse dotfiles', () => { - const result = parseNodeContent('.gitignore'); - expect(result.name).toBe('.gitignore'); - expect(result.nodeType).toBe('file'); - expect(result.iconId).toBe('git'); + it('should parse dotfiles', async () => { + const node = await parseAndGetNode('.gitignore'); + expect(node.name).toBe('.gitignore'); + expect(node.nodeType).toBe('file'); + expect(node.iconId).toBe('git'); }); - it('should parse filenames with hyphens', () => { - const result = parseNodeContent('docker-compose.yml'); - expect(result.name).toBe('docker-compose.yml'); - expect(result.iconId).toBe('docker'); + it('should parse filenames with hyphens', async () => { + const node = await parseAndGetNode('docker-compose.yml'); + expect(node.name).toBe('docker-compose.yml'); + expect(node.iconId).toBe('docker'); }); }); describe('quoted labels', () => { - it('should strip double quotes', () => { - const result = parseNodeContent('"my file.js"'); - expect(result.name).toBe('my file.js'); - expect(result.nodeType).toBe('file'); + it('should strip double quotes', async () => { + const node = await parseAndGetNode('"my file.js"'); + expect(node.name).toBe('my file.js'); + expect(node.nodeType).toBe('file'); }); - it('should strip single quotes', () => { - const result = parseNodeContent("'my folder/'"); - expect(result.name).toBe('my folder'); - expect(result.nodeType).toBe('directory'); + it('should strip single quotes', async () => { + const node = await parseAndGetNode("'my folder/'"); + expect(node.name).toBe('my folder'); + expect(node.nodeType).toBe('directory'); }); - it('should handle empty quoted string', () => { - const result = parseNodeContent('""'); - expect(result.name).toBe(''); + it('should handle empty quoted string', async () => { + const node = await parseAndGetNode('""'); + expect(node.name).toBe(''); }); }); describe(':::class annotation', () => { - it('should extract class from bare label', () => { - const result = parseNodeContent('index.js :::highlight'); - expect(result.name).toBe('index.js'); - expect(result.cssClass).toBe('highlight'); + it('should extract class from bare label', async () => { + const node = await parseAndGetNode('index.js :::highlight'); + expect(node.name).toBe('index.js'); + expect(node.cssClass).toBe('highlight'); }); - it('should extract class from quoted label', () => { - const result = parseNodeContent('"my file.js" :::important'); - expect(result.name).toBe('my file.js'); - expect(result.cssClass).toBe('important'); + it('should extract class from quoted label', async () => { + const node = await parseAndGetNode('"my file.js" :::important'); + expect(node.name).toBe('my file.js'); + expect(node.cssClass).toBe('important'); }); - it('should handle class with hyphens', () => { - const result = parseNodeContent('file.ts :::my-class'); - expect(result.cssClass).toBe('my-class'); + it('should handle class with hyphens', async () => { + const node = await parseAndGetNode('file.ts :::my-class'); + expect(node.cssClass).toBe('my-class'); }); }); describe('icon() annotation', () => { - it('should extract icon override', () => { - const result = parseNodeContent('data.bin icon(database)'); - expect(result.name).toBe('data.bin'); - expect(result.iconId).toBe('database'); + it('should extract icon override', async () => { + const node = await parseAndGetNode('data.bin icon(database)'); + expect(node.name).toBe('data.bin'); + expect(node.iconId).toBe('database'); }); - it('should override auto-detected icon', () => { - const result = parseNodeContent('config.json icon(config)'); - expect(result.name).toBe('config.json'); - expect(result.iconId).toBe('config'); + it('should override auto-detected icon', async () => { + const node = await parseAndGetNode('config.json icon(config)'); + expect(node.name).toBe('config.json'); + expect(node.iconId).toBe('config'); }); }); describe('## description', () => { - it('should extract description from bare label', () => { - const result = parseNodeContent('index.js ## entry point'); - expect(result.name).toBe('index.js'); - expect(result.description).toBe('entry point'); + it('should extract description from bare label', async () => { + const node = await parseAndGetNode('index.js ## entry point'); + expect(node.name).toBe('index.js'); + expect(node.description).toBe('entry point'); }); - it('should extract description from quoted label', () => { - const result = parseNodeContent('"my file.js" ## the main file'); - expect(result.name).toBe('my file.js'); - expect(result.description).toBe('the main file'); + it('should extract description from quoted label', async () => { + const node = await parseAndGetNode('"my file.js" ## the main file'); + expect(node.name).toBe('my file.js'); + expect(node.description).toBe('the main file'); }); - it('should handle description with no text after ##', () => { - const result = parseNodeContent('file.txt ##'); - expect(result.name).toBe('file.txt'); - expect(result.description).toBeUndefined(); + it('should handle description with no text after ##', async () => { + const node = await parseAndGetNode('file.txt ##'); + expect(node.name).toBe('file.txt'); + expect(node.description).toBeUndefined(); }); }); describe('combined annotations', () => { - it('should handle all annotations together', () => { - const result = parseNodeContent('App.tsx :::highlight icon(react) ## main component'); - expect(result.name).toBe('App.tsx'); - expect(result.cssClass).toBe('highlight'); - expect(result.iconId).toBe('react'); - expect(result.description).toBe('main component'); + it('should handle all annotations together', async () => { + const node = await parseAndGetNode('App.tsx :::highlight icon(react) ## main component'); + expect(node.name).toBe('App.tsx'); + expect(node.cssClass).toBe('highlight'); + expect(node.iconId).toBe('react'); + expect(node.description).toBe('main component'); }); - it('should handle quoted label with all annotations', () => { - const result = parseNodeContent('"my app.tsx" :::highlight icon(react) ## main component'); - expect(result.name).toBe('my app.tsx'); - expect(result.cssClass).toBe('highlight'); - expect(result.iconId).toBe('react'); - expect(result.description).toBe('main component'); + it('should handle quoted label with all annotations', async () => { + const node = await parseAndGetNode('"my app.tsx" :::highlight icon(react) ## main component'); + expect(node.name).toBe('my app.tsx'); + expect(node.cssClass).toBe('highlight'); + expect(node.iconId).toBe('react'); + expect(node.description).toBe('main component'); }); - it('should handle directory with class', () => { - const result = parseNodeContent('src/ :::highlight'); - expect(result.name).toBe('src'); - expect(result.nodeType).toBe('directory'); - expect(result.cssClass).toBe('highlight'); - expect(result.iconId).toBe('folder'); + it('should handle directory with class', async () => { + const node = await parseAndGetNode('src/ :::highlight'); + expect(node.name).toBe('src'); + expect(node.nodeType).toBe('directory'); + expect(node.cssClass).toBe('highlight'); + expect(node.iconId).toBe('folder'); }); }); describe('bare labels with spaces', () => { - it('should handle folder names with spaces', () => { - const result = parseNodeContent('My Documents/'); - expect(result.name).toBe('My Documents'); - expect(result.nodeType).toBe('directory'); - expect(result.iconId).toBe('folder'); + it('should handle folder names with spaces', async () => { + const node = await parseAndGetNode('My Documents/'); + expect(node.name).toBe('My Documents'); + expect(node.nodeType).toBe('directory'); + expect(node.iconId).toBe('folder'); }); - it('should handle folder names with spaces and annotations', () => { - const result = parseNodeContent('My Documents/ :::highlight'); - expect(result.name).toBe('My Documents'); - expect(result.nodeType).toBe('directory'); - expect(result.cssClass).toBe('highlight'); + it('should handle folder names with spaces and annotations', async () => { + const node = await parseAndGetNode('My Documents/ :::highlight'); + expect(node.name).toBe('My Documents'); + expect(node.nodeType).toBe('directory'); + expect(node.cssClass).toBe('highlight'); }); - it('should handle file names with spaces', () => { - const result = parseNodeContent('my file.ts ## some description'); - expect(result.name).toBe('my file.ts'); - expect(result.nodeType).toBe('file'); - expect(result.iconId).toBe('typescript'); - expect(result.description).toBe('some description'); + it('should handle file names with spaces', async () => { + const node = await parseAndGetNode('my file.ts ## some description'); + expect(node.name).toBe('my file.ts'); + expect(node.nodeType).toBe('file'); + expect(node.iconId).toBe('typescript'); + expect(node.description).toBe('some description'); }); }); describe('icon auto-detection', () => { - it('should auto-detect TypeScript icon', () => { - const result = parseNodeContent('utils.ts'); - expect(result.iconId).toBe('typescript'); + it('should auto-detect TypeScript icon', async () => { + const node = await parseAndGetNode('utils.ts'); + expect(node.iconId).toBe('typescript'); }); - it('should auto-detect Python icon', () => { - const result = parseNodeContent('main.py'); - expect(result.iconId).toBe('python'); + it('should auto-detect Python icon', async () => { + const node = await parseAndGetNode('main.py'); + expect(node.iconId).toBe('python'); }); - it('should auto-detect Markdown icon', () => { - const result = parseNodeContent('README.md'); - expect(result.iconId).toBe('markdown'); + it('should auto-detect Markdown icon', async () => { + const node = await parseAndGetNode('README.md'); + expect(node.iconId).toBe('markdown'); }); - it('should fall back to generic file icon', () => { - const result = parseNodeContent('unknown.xyz'); - expect(result.iconId).toBe('file'); + it('should fall back to generic file icon', async () => { + const node = await parseAndGetNode('unknown.xyz'); + expect(node.iconId).toBe('file'); }); - it('should use folder icon for directories', () => { - const result = parseNodeContent('components/'); - expect(result.iconId).toBe('folder'); + it('should use folder icon for directories', async () => { + const node = await parseAndGetNode('components/'); + expect(node.iconId).toBe('folder'); }); }); describe('icon suppression', () => { - it('should set iconId to none for icon(none)', () => { - const result = parseNodeContent('index.js icon(none)'); - expect(result.name).toBe('index.js'); - expect(result.iconId).toBe('none'); + it('should set iconId to none for icon(none)', async () => { + const node = await parseAndGetNode('index.js icon(none)'); + expect(node.name).toBe('index.js'); + expect(node.iconId).toBe('none'); }); - it('should set iconId to none for empty icon()', () => { - const result = parseNodeContent('index.js icon()'); - expect(result.name).toBe('index.js'); - expect(result.iconId).toBe('none'); + it('should set iconId to none for empty icon()', async () => { + const node = await parseAndGetNode('index.js icon()'); + expect(node.name).toBe('index.js'); + expect(node.iconId).toBe('none'); }); - it('should suppress icon on a directory', () => { - const result = parseNodeContent('src/ icon(none)'); - expect(result.name).toBe('src'); - expect(result.nodeType).toBe('directory'); - expect(result.iconId).toBe('none'); + it('should suppress icon on a directory', async () => { + const node = await parseAndGetNode('src/ icon(none)'); + expect(node.name).toBe('src'); + expect(node.nodeType).toBe('directory'); + expect(node.iconId).toBe('none'); }); - it('should combine icon(none) with other annotations', () => { - const result = parseNodeContent('app.ts icon(none) :::highlight ## entry point'); - expect(result.name).toBe('app.ts'); - expect(result.iconId).toBe('none'); - expect(result.cssClass).toBe('highlight'); - expect(result.description).toBe('entry point'); + it('should combine icon(none) with other annotations', async () => { + const node = await parseAndGetNode('app.ts icon(none) :::highlight ## entry point'); + expect(node.name).toBe('app.ts'); + expect(node.iconId).toBe('none'); + expect(node.cssClass).toBe('highlight'); + expect(node.description).toBe('entry point'); }); }); }); diff --git a/packages/mermaid/src/diagrams/treeView/parser.ts b/packages/mermaid/src/diagrams/treeView/parser.ts index 8e36e20734e..ab734a00989 100644 --- a/packages/mermaid/src/diagrams/treeView/parser.ts +++ b/packages/mermaid/src/diagrams/treeView/parser.ts @@ -6,100 +6,37 @@ import db from './db.js'; import { resolveIcon } from './icons.js'; import type { NodeType } from './types.js'; -interface ParsedNodeContent { - name: string; - nodeType: NodeType; - cssClass?: string; - iconId?: string; - description?: string; -} +const populate = (ast: TreeView) => { + populateCommonDb(ast, db); + for (const node of ast.nodes) { + const level = node.indent ? parseInt(node.indent as unknown as string) : 0; -/** - * Parse a NODE_CONTENT string into its constituent parts. - * - * Supports: - * - Quoted labels: "my file" or 'my file' - * - Bare labels: index.js, src/, .gitignore - * - :::className annotation - * - icon(name) annotation - * - ## description (visible inline text) - * - * Order of annotations after the label does not matter. - */ -export function parseNodeContent(raw: string): ParsedNodeContent { - let remaining = raw.trim(); - let name: string; + // Name comes pre-cleaned from value converter (quotes stripped, etc.) + let name = node.name as unknown as string; - // Extract quoted label - if (remaining.startsWith('"') || remaining.startsWith("'")) { - const quote = remaining[0]; - const endIdx = remaining.indexOf(quote, 1); - if (endIdx !== -1) { - name = remaining.substring(1, endIdx); - remaining = remaining.substring(endIdx + 1).trim(); - } else { - // Unterminated quote — treat entire content as the name - name = remaining.substring(1); - remaining = ''; - } - } else { - // Bare label: extract up to first annotation marker - const markerMatch = /^(.*?)(?=\s+:::|(?:^|\s)icon\(|\s+##|$)/.exec(remaining); - if (markerMatch?.[1]) { - name = markerMatch[1].trim(); - remaining = remaining.substring(markerMatch[1].length).trim(); - } else { - name = remaining; - remaining = ''; + // Detect directory: trailing / on the name + const isDirectory = name.endsWith('/'); + if (isDirectory) { + name = name.slice(0, -1); } - } - - let cssClass: string | undefined; - let iconId: string | undefined; - let description: string | undefined; + const nodeType: NodeType = isDirectory ? 'directory' : 'file'; - // Extract ## description (must be extracted first since it runs to end-of-line) - const descMatch = /##\s*(.*)/.exec(remaining); - if (descMatch) { - description = descMatch[1].trim() || undefined; - remaining = remaining.substring(0, descMatch.index).trim(); - } - - // Extract :::className - const classMatch = /:::\s*([A-Z_a-z][\w-]*)/.exec(remaining); - if (classMatch) { - cssClass = classMatch[1]; - remaining = remaining.replace(classMatch[0], '').trim(); - } + // Read annotations directly from AST fields (cleaned by value converter) + const cssClass = (node.classAnnotation as unknown as string) || undefined; - // Extract icon(name) or icon(none) or icon() to suppress - const iconMatch = /icon\(([\w-]*)\)/.exec(remaining); - if (iconMatch) { - iconId = iconMatch[1] || 'none'; - } - - // Detect directory: trailing / on the name - const isDirectory = name.endsWith('/'); - if (isDirectory) { - name = name.slice(0, -1); - } - const nodeType: NodeType = isDirectory ? 'directory' : 'file'; - - // Resolve icon if not explicitly set - if (!iconId) { - iconId = resolveIcon(isDirectory ? name + '/' : name, nodeType); - } + // Icon: value converter extracts the name from icon(name) + // Empty string from icon() means suppress icon + const rawIcon = node.iconAnnotation as unknown as string | undefined; + let iconId: string | undefined; + if (rawIcon !== undefined) { + iconId = rawIcon || 'none'; // empty string → 'none' (suppress) + } else { + iconId = resolveIcon(isDirectory ? name + '/' : name, nodeType); + } - return { name, nodeType, cssClass, iconId, description }; -} + // Description comes pre-trimmed from value converter + const description = (node.descAnnotation as unknown as string) || undefined; -const populate = (ast: TreeView) => { - populateCommonDb(ast, db); - for (const node of ast.nodes) { - const level = node.indent ? parseInt(node.indent as unknown as string) : 0; - const { name, nodeType, cssClass, iconId, description } = parseNodeContent( - node.nodeContent as unknown as string - ); db.addNode(level, name, nodeType, cssClass, iconId, description); } }; diff --git a/packages/parser/src/language/treeView/treeView.langium b/packages/parser/src/language/treeView/treeView.langium index 6ba7b4ff937..505ab22d01d 100644 --- a/packages/parser/src/language/treeView/treeView.langium +++ b/packages/parser/src/language/treeView/treeView.langium @@ -2,8 +2,9 @@ * TreeView grammar for Langium * * Supports both quoted labels ("my file") and bare labels (index.js). - * NODE_CONTENT captures the entire rest-of-line content; detailed parsing - * of annotations (:::class, icon(), ## description) happens in parser.ts. + * Annotations (:::class, icon(), ## description) are parsed directly into + * AST fields by the grammar. Value conversion for stripping quotes, extracting + * class names, icon names, and description text happens in valueConverter.ts. * * The ML_COMMENT and NL hidden terminals handle whitespace, comments, and newlines * before the treeView keyword, allowing for empty lines and comments before the @@ -32,21 +33,31 @@ fragment TitleAndAccessibilities: ((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE))+ ; +// Annotation terminals with leading whitespace MUST be declared before INDENTATION +// so the lexer tries them first. When the lexer sees " :::highlight", the annotation +// terminal matches the full string (14 chars) vs INDENTATION matching just " " (1 char). +// Chevrotain uses declaration order for priority, so these must come first. +terminal CLASS_ANNOTATION: /[ \t]+:::[ \t]*[A-Za-z_][\w-]*/; +terminal ICON_ANNOTATION: /[ \t]+icon\([\w-]*\)/; +terminal DESC_ANNOTATION: /[ \t]+##[^\n\r]*/; + // This should be processed before whitespace is ignored terminal INDENTATION: /[ \t]{1,}/; // One or more spaces/tabs for indentation +// Name terminals +terminal QUOTED_NAME: /"[^"]*"|'[^']*'/; + hidden terminal WS: /[ \t]+/; // One or more spaces or tabs for hidden whitespace hidden terminal ML_COMMENT: /\%\%[^\n]*/; hidden terminal NL: /\r?\n/; -TreeNode: +// Bare name: starts with non-whitespace/non-quote, stops before annotation markers. +// Must be declared AFTER ML_COMMENT so that %% comment lines take priority. +terminal BARE_NAME: /(?!:::|icon\(|##)[^ \t\n\r"'](?:(?![ \t]+:::[ \t]*[A-Za-z_]|[ \t]+icon\(|[ \t]+##)[^\n\r])*/; + +TreeNode: // indent is converted to a length value by valueConverter indent=INDENTATION? - // Captures rest-of-line: name + optional :::class, icon(), ## description - // Detailed parsing of annotations happens in parser.ts - nodeContent=NODE_CONTENT + (name=QUOTED_NAME | name=BARE_NAME) + (classAnnotation=CLASS_ANNOTATION | iconAnnotation=ICON_ANNOTATION | descAnnotation=DESC_ANNOTATION)* ; - -// Matches from the first non-whitespace character to end of line. -// Must be declared AFTER ML_COMMENT so that %% comment lines take priority. -terminal NODE_CONTENT: /[^ \t\n\r][^\n\r]*/; diff --git a/packages/parser/src/language/treeView/valueConverter.ts b/packages/parser/src/language/treeView/valueConverter.ts index 9671bd6652b..fd182493a3d 100644 --- a/packages/parser/src/language/treeView/valueConverter.ts +++ b/packages/parser/src/language/treeView/valueConverter.ts @@ -10,6 +10,25 @@ export class TreeViewValueConverter extends AbstractMermaidValueConverter { if (rule.name === 'INDENTATION') { return input?.length || 0; } + if (rule.name === 'QUOTED_NAME') { + // Strip surrounding quotes: "name" or 'name' → name + return input.substring(1, input.length - 1); + } + if (rule.name === 'CLASS_ANNOTATION') { + // " :::className" → className (strip leading whitespace and :::) + const trimmed = input.trim(); + return trimmed.substring(3).trim(); + } + if (rule.name === 'ICON_ANNOTATION') { + // " icon(name)" → name, " icon()" → empty string + const trimmed = input.trim(); + return trimmed.substring(5, trimmed.length - 1); + } + if (rule.name === 'DESC_ANNOTATION') { + // " ## description text" → description text + const trimmed = input.trim(); + return trimmed.substring(2).trim(); + } return undefined; } } diff --git a/packages/parser/tests/treeView.test.ts b/packages/parser/tests/treeView.test.ts index 63e308ca2d2..7b1fe1ebc8f 100644 --- a/packages/parser/tests/treeView.test.ts +++ b/packages/parser/tests/treeView.test.ts @@ -25,7 +25,7 @@ describe('TreeView Parser', () => { expectNoErrorsOrAlternatives(result); expect(result.value.$type).toBe('TreeView'); expect(result.value.nodes).toHaveLength(1); - expect(result.value.nodes[0].nodeContent).toBe('"Root"'); + expect(result.value.nodes[0].name).toBe('Root'); expect(result.value.nodes[0].indent).toBe(undefined); }); @@ -34,7 +34,7 @@ describe('TreeView Parser', () => { expectNoErrorsOrAlternatives(result); expect(result.value.$type).toBe('TreeView'); expect(result.value.nodes).toHaveLength(1); - expect(result.value.nodes[0].nodeContent).toBe('"Multi Word Root"'); + expect(result.value.nodes[0].name).toBe('Multi Word Root'); expect(result.value.nodes[0].indent).toBe(undefined); }); @@ -44,16 +44,16 @@ describe('TreeView Parser', () => { expect(result.value.$type).toBe('TreeView'); expect(result.value.nodes).toHaveLength(4); - expect(result.value.nodes[0].nodeContent).toBe('"Root"'); + expect(result.value.nodes[0].name).toBe('Root'); expect(result.value.nodes[0].indent).toBe(undefined); - expect(result.value.nodes[1].nodeContent).toBe('"Child1"'); + expect(result.value.nodes[1].name).toBe('Child1'); expect(result.value.nodes[1].indent).toBe(4); - expect(result.value.nodes[2].nodeContent).toBe('"Child2"'); + expect(result.value.nodes[2].name).toBe('Child2'); expect(result.value.nodes[2].indent).toBe(4); - expect(result.value.nodes[3].nodeContent).toBe('"Child3"'); + expect(result.value.nodes[3].name).toBe('Child3'); expect(result.value.nodes[3].indent).toBe(8); }); @@ -61,15 +61,15 @@ describe('TreeView Parser', () => { const result = parse('treeView-beta\nindex.js'); expectNoErrorsOrAlternatives(result); expect(result.value.nodes).toHaveLength(1); - expect(result.value.nodes[0].nodeContent).toBe('index.js'); + expect(result.value.nodes[0].name).toBe('index.js'); }); it('should parse bare labels with directory trailing slash', () => { const result = parse('treeView-beta\nsrc/\n index.js'); expectNoErrorsOrAlternatives(result); expect(result.value.nodes).toHaveLength(2); - expect(result.value.nodes[0].nodeContent).toBe('src/'); - expect(result.value.nodes[1].nodeContent).toBe('index.js'); + expect(result.value.nodes[0].name).toBe('src/'); + expect(result.value.nodes[1].name).toBe('index.js'); expect(result.value.nodes[1].indent).toBe(4); }); @@ -77,9 +77,11 @@ describe('TreeView Parser', () => { const result = parse('treeView-beta\nindex.js :::highlight icon(javascript) ## entry point'); expectNoErrorsOrAlternatives(result); expect(result.value.nodes).toHaveLength(1); - expect(result.value.nodes[0].nodeContent).toBe( - 'index.js :::highlight icon(javascript) ## entry point' - ); + const node = result.value.nodes[0]; + expect(node.name).toBe('index.js'); + expect(node.classAnnotation).toBe('highlight'); + expect(node.iconAnnotation).toBe('javascript'); + expect(node.descAnnotation).toBe('entry point'); }); it('should parse quoted labels with annotations', () => { @@ -88,16 +90,92 @@ describe('TreeView Parser', () => { ); expectNoErrorsOrAlternatives(result); expect(result.value.nodes).toHaveLength(1); - expect(result.value.nodes[0].nodeContent).toBe( - '"my file.js" :::highlight icon(javascript) ## entry point' - ); + const node = result.value.nodes[0]; + expect(node.name).toBe('my file.js'); + expect(node.classAnnotation).toBe('highlight'); + expect(node.iconAnnotation).toBe('javascript'); + expect(node.descAnnotation).toBe('entry point'); }); it('should handle %% comments', () => { const result = parse('treeView-beta\n%% this is a comment\nindex.js'); expectNoErrorsOrAlternatives(result); expect(result.value.nodes).toHaveLength(1); - expect(result.value.nodes[0].nodeContent).toBe('index.js'); + expect(result.value.nodes[0].name).toBe('index.js'); + }); + }); + + describe('Structured terminals', () => { + it('should strip quotes from QUOTED_NAME', () => { + const result = parse('treeView-beta\n"my file.js"'); + expectNoErrorsOrAlternatives(result); + expect(result.value.nodes[0].name).toBe('my file.js'); + }); + + it('should strip single quotes from QUOTED_NAME', () => { + const result = parse("treeView-beta\n'my folder/'"); + expectNoErrorsOrAlternatives(result); + expect(result.value.nodes[0].name).toBe('my folder/'); + }); + + it('should extract class name from CLASS_ANNOTATION', () => { + const result = parse('treeView-beta\nindex.js :::highlight'); + expectNoErrorsOrAlternatives(result); + expect(result.value.nodes[0].classAnnotation).toBe('highlight'); + }); + + it('should handle class names with hyphens', () => { + const result = parse('treeView-beta\nfile.ts :::my-class'); + expectNoErrorsOrAlternatives(result); + expect(result.value.nodes[0].classAnnotation).toBe('my-class'); + }); + + it('should extract icon name from ICON_ANNOTATION', () => { + const result = parse('treeView-beta\ndata.bin icon(database)'); + expectNoErrorsOrAlternatives(result); + expect(result.value.nodes[0].iconAnnotation).toBe('database'); + }); + + it('should handle empty icon()', () => { + const result = parse('treeView-beta\nindex.js icon()'); + expectNoErrorsOrAlternatives(result); + expect(result.value.nodes[0].iconAnnotation).toBe(''); + }); + + it('should extract description from DESC_ANNOTATION', () => { + const result = parse('treeView-beta\nindex.js ## entry point'); + expectNoErrorsOrAlternatives(result); + expect(result.value.nodes[0].descAnnotation).toBe('entry point'); + }); + + it('should handle all annotations in any order', () => { + const result = parse('treeView-beta\napp.ts icon(none) :::highlight ## entry point'); + expectNoErrorsOrAlternatives(result); + const node = result.value.nodes[0]; + expect(node.name).toBe('app.ts'); + expect(node.iconAnnotation).toBe('none'); + expect(node.classAnnotation).toBe('highlight'); + expect(node.descAnnotation).toBe('entry point'); + }); + + it('should parse bare names with spaces before annotations', () => { + const result = parse('treeView-beta\nMy Documents/ :::highlight'); + expectNoErrorsOrAlternatives(result); + const node = result.value.nodes[0]; + expect(node.name).toBe('My Documents/'); + expect(node.classAnnotation).toBe('highlight'); + }); + + it('should parse dotfiles', () => { + const result = parse('treeView-beta\n.gitignore'); + expectNoErrorsOrAlternatives(result); + expect(result.value.nodes[0].name).toBe('.gitignore'); + }); + + it('should parse filenames with hyphens', () => { + const result = parse('treeView-beta\ndocker-compose.yml'); + expectNoErrorsOrAlternatives(result); + expect(result.value.nodes[0].name).toBe('docker-compose.yml'); }); }); From 51a829bbaea14b142e36cb4baf87561e2dc0ff74 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 2 Apr 2026 18:14:42 +1030 Subject: [PATCH 07/11] test: add unit tests for treeView icons, db, and valueConverter - icons.spec.ts: resolution priority, case sensitivity, edge cases, SVG path coverage - db.spec.ts: tree building, stack-based nesting, config, accessibility - treeViewValueConverter.test.ts: all structured terminal conversions Addresses missing coverage flagged by Codecov. --- .../mermaid/src/diagrams/treeView/db.spec.ts | 158 +++++++++++++ .../src/diagrams/treeView/icons.spec.ts | 218 ++++++++++++++++++ .../tests/treeViewValueConverter.test.ts | 98 ++++++++ 3 files changed, 474 insertions(+) create mode 100644 packages/mermaid/src/diagrams/treeView/db.spec.ts create mode 100644 packages/mermaid/src/diagrams/treeView/icons.spec.ts create mode 100644 packages/parser/tests/treeViewValueConverter.test.ts diff --git a/packages/mermaid/src/diagrams/treeView/db.spec.ts b/packages/mermaid/src/diagrams/treeView/db.spec.ts new file mode 100644 index 00000000000..a2aec702b9c --- /dev/null +++ b/packages/mermaid/src/diagrams/treeView/db.spec.ts @@ -0,0 +1,158 @@ +import { describe, expect, it, beforeEach } from 'vitest'; +import db from './db.js'; + +describe('treeView db', () => { + beforeEach(() => { + db.clear(); + }); + + describe('clear', () => { + it('should reset state to initial root', () => { + db.addNode(0, 'file.ts', 'file'); + db.clear(); + const root = db.getRoot(); + expect(root.children).toHaveLength(0); + expect(root.name).toBe('/'); + expect(root.level).toBe(-1); + }); + }); + + describe('getRoot', () => { + it('should return a root node at level -1', () => { + const root = db.getRoot(); + expect(root.level).toBe(-1); + expect(root.nodeType).toBe('directory'); + expect(root.children).toHaveLength(0); + }); + }); + + describe('getCount', () => { + it('should start at 1', () => { + expect(db.getCount()).toBe(1); + }); + + it('should increment with each added node', () => { + db.addNode(0, 'file1.ts', 'file'); + expect(db.getCount()).toBe(2); + db.addNode(0, 'file2.ts', 'file'); + expect(db.getCount()).toBe(3); + }); + }); + + describe('addNode', () => { + it('should add a child to root at level 0', () => { + db.addNode(0, 'index.ts', 'file'); + const root = db.getRoot(); + expect(root.children).toHaveLength(1); + expect(root.children[0].name).toBe('index.ts'); + expect(root.children[0].nodeType).toBe('file'); + expect(root.children[0].level).toBe(0); + }); + + it('should add nested children using increasing levels', () => { + db.addNode(0, 'src', 'directory'); + db.addNode(4, 'index.ts', 'file'); + const root = db.getRoot(); + expect(root.children).toHaveLength(1); + expect(root.children[0].name).toBe('src'); + expect(root.children[0].children).toHaveLength(1); + expect(root.children[0].children[0].name).toBe('index.ts'); + }); + + it('should pop stack when level decreases', () => { + db.addNode(0, 'src', 'directory'); + db.addNode(4, 'index.ts', 'file'); + db.addNode(0, 'README.md', 'file'); + const root = db.getRoot(); + expect(root.children).toHaveLength(2); + expect(root.children[0].name).toBe('src'); + expect(root.children[1].name).toBe('README.md'); + }); + + it('should add siblings at the same level', () => { + db.addNode(0, 'file1.ts', 'file'); + db.addNode(0, 'file2.ts', 'file'); + db.addNode(0, 'file3.ts', 'file'); + const root = db.getRoot(); + expect(root.children).toHaveLength(3); + }); + + it('should store cssClass', () => { + db.addNode(0, 'app.ts', 'file', 'highlight'); + const root = db.getRoot(); + expect(root.children[0].cssClass).toBe('highlight'); + }); + + it('should store iconId', () => { + db.addNode(0, 'data.bin', 'file', undefined, 'database'); + const root = db.getRoot(); + expect(root.children[0].iconId).toBe('database'); + }); + + it('should store description', () => { + db.addNode(0, 'index.ts', 'file', undefined, undefined, 'entry point'); + const root = db.getRoot(); + expect(root.children[0].description).toBe('entry point'); + }); + + it('should store all optional fields together', () => { + db.addNode(0, 'App.tsx', 'file', 'highlight', 'react', 'main component'); + const root = db.getRoot(); + const node = root.children[0]; + expect(node.name).toBe('App.tsx'); + expect(node.cssClass).toBe('highlight'); + expect(node.iconId).toBe('react'); + expect(node.description).toBe('main component'); + }); + + it('should build a deep tree', () => { + db.addNode(0, 'src', 'directory'); + db.addNode(4, 'components', 'directory'); + db.addNode(8, 'Button.tsx', 'file'); + db.addNode(8, 'Header.tsx', 'file'); + db.addNode(4, 'utils', 'directory'); + db.addNode(8, 'helpers.ts', 'file'); + const root = db.getRoot(); + expect(root.children[0].children[0].children).toHaveLength(2); + expect(root.children[0].children[1].name).toBe('utils'); + expect(root.children[0].children[1].children[0].name).toBe('helpers.ts'); + }); + + it('should assign unique incrementing ids', () => { + db.addNode(0, 'a', 'file'); + db.addNode(0, 'b', 'file'); + db.addNode(0, 'c', 'file'); + const root = db.getRoot(); + const ids = root.children.map((n) => n.id); + expect(ids).toEqual([1, 2, 3]); + }); + }); + + describe('getConfig', () => { + it('should return a config object with required fields', () => { + const config = db.getConfig(); + expect(config).toBeDefined(); + expect(typeof config.paddingX).toBe('number'); + expect(typeof config.paddingY).toBe('number'); + expect(typeof config.rowIndent).toBe('number'); + expect(typeof config.lineThickness).toBe('number'); + }); + }); + + describe('accessibility', () => { + it('should set and get accTitle', () => { + db.setAccTitle('My Tree'); + expect(db.getAccTitle()).toBe('My Tree'); + }); + + it('should set and get accDescription', () => { + db.setAccDescription('A file tree'); + expect(db.getAccDescription()).toBe('A file tree'); + }); + + it('should set and get diagram title', () => { + db.setDiagramTitle('File Structure'); + expect(db.getDiagramTitle()).toBe('File Structure'); + }); + }); +}); diff --git a/packages/mermaid/src/diagrams/treeView/icons.spec.ts b/packages/mermaid/src/diagrams/treeView/icons.spec.ts new file mode 100644 index 00000000000..5569a8d3d02 --- /dev/null +++ b/packages/mermaid/src/diagrams/treeView/icons.spec.ts @@ -0,0 +1,218 @@ +import { describe, expect, it } from 'vitest'; +import { resolveIcon, getIconPath, ICON_PATHS } from './icons.js'; + +describe('icons', () => { + describe('resolveIcon', () => { + // ------------------------------------------------------------------ + // Resolution priority (the interesting logic) + // ------------------------------------------------------------------ + describe('resolution priority', () => { + it('directory type always wins — even if the name matches a known file', () => { + // package.json would be "json" as a file, but directories must always be "folder" + expect(resolveIcon('package.json', 'directory')).toBe('folder'); + expect(resolveIcon('Dockerfile', 'directory')).toBe('folder'); + expect(resolveIcon('main.py', 'directory')).toBe('folder'); + }); + + it('exact filename beats extension match', () => { + // tsconfig.json: filename→typescript, extension→json → filename wins + expect(resolveIcon('tsconfig.json', 'file')).toBe('typescript'); + // .eslintrc.js: filename→config, extension→javascript → filename wins + expect(resolveIcon('.eslintrc.js', 'file')).toBe('config'); + // .prettierrc.json: filename→config, extension→json → filename wins + expect(resolveIcon('.prettierrc.json', 'file')).toBe('config'); + }); + + it('falls back to extension when filename is not in the known list', () => { + expect(resolveIcon('utils.ts', 'file')).toBe('typescript'); + expect(resolveIcon('App.tsx', 'file')).toBe('react'); + expect(resolveIcon('main.py', 'file')).toBe('python'); + }); + + it('falls back to "file" when nothing matches', () => { + expect(resolveIcon('data.xyz', 'file')).toBe('file'); + expect(resolveIcon('noext', 'file')).toBe('file'); + }); + }); + + // ------------------------------------------------------------------ + // Case sensitivity behaviour + // ------------------------------------------------------------------ + describe('case sensitivity', () => { + it('extension matching is case-insensitive', () => { + expect(resolveIcon('APP.TS', 'file')).toBe('typescript'); + expect(resolveIcon('styles.CSS', 'file')).toBe('css'); + expect(resolveIcon('Main.PY', 'file')).toBe('python'); + }); + + it('filename matching is case-sensitive', () => { + // "Dockerfile" is in the table, but "dockerfile" is not + expect(resolveIcon('Dockerfile', 'file')).toBe('docker'); + expect(resolveIcon('dockerfile', 'file')).toBe('file'); // falls through to no-extension fallback + // "Makefile" vs "makefile" + expect(resolveIcon('Makefile', 'file')).toBe('terminal'); + expect(resolveIcon('makefile', 'file')).toBe('file'); + }); + }); + + // ------------------------------------------------------------------ + // Filename lookups (spot-check representative samples, not every row) + // ------------------------------------------------------------------ + describe('known filenames', () => { + it.each([ + ['.gitignore', 'git'], + ['Dockerfile', 'docker'], + ['docker-compose.yml', 'docker'], + ['Makefile', 'terminal'], + ['LICENSE', 'license'], + ['.env', 'env'], + ['.env.local', 'env'], + ['tsconfig.json', 'typescript'], + ['package.json', 'json'], + ['package-lock.json', 'lock'], + ['yarn.lock', 'lock'], + ['pnpm-lock.yaml', 'lock'], + ] as const)('%s → %s', (filename, expectedIcon) => { + expect(resolveIcon(filename, 'file')).toBe(expectedIcon); + }); + }); + + // ------------------------------------------------------------------ + // Extension lookups (one representative per icon category) + // ------------------------------------------------------------------ + describe('extension mapping', () => { + it.each([ + ['app.js', 'javascript'], + ['App.tsx', 'react'], + ['utils.ts', 'typescript'], + ['main.py', 'python'], + ['app.rb', 'ruby'], + ['main.rs', 'rust'], + ['main.go', 'go'], + ['App.java', 'java'], + ['Prog.cs', 'csharp'], + ['main.cpp', 'cpp'], + ['main.c', 'c'], + ['data.json', 'json'], + ['conf.yaml', 'yaml'], + ['conf.toml', 'config'], + ['data.xml', 'xml'], + ['index.html', 'html'], + ['styles.css', 'css'], + ['notes.md', 'markdown'], + ['build.sh', 'terminal'], + ['logo.svg', 'image'], + ['query.sql', 'database'], + ['some.lock', 'lock'], + ['config.env', 'env'], + ['App.vue', 'vue'], + ['App.svelte', 'svelte'], + ] as const)('%s → %s', (filename, expectedIcon) => { + expect(resolveIcon(filename, 'file')).toBe(expectedIcon); + }); + }); + + // ------------------------------------------------------------------ + // Edge cases + // ------------------------------------------------------------------ + describe('edge cases', () => { + it('dotfiles without an extension match by exact filename', () => { + expect(resolveIcon('.gitignore', 'file')).toBe('git'); + expect(resolveIcon('.eslintrc', 'file')).toBe('config'); + expect(resolveIcon('.prettierrc', 'file')).toBe('config'); + }); + + it('dotfiles not in the known list fall back correctly', () => { + // .bashrc has no extension (dot is at index 0, so dotIdx=0 which fails the >0 check) + expect(resolveIcon('.bashrc', 'file')).toBe('file'); + }); + + it('files with multiple dots use the last extension', () => { + // "component.spec.ts" → last ext is .ts → typescript + expect(resolveIcon('component.spec.ts', 'file')).toBe('typescript'); + // "archive.tar.gz" → last ext is .gz → unknown → file + expect(resolveIcon('archive.tar.gz', 'file')).toBe('file'); + }); + + it('directory type overrides even for trailing-slash labels', () => { + expect(resolveIcon('src/', 'directory')).toBe('folder'); + }); + }); + }); + + describe('getIconPath', () => { + it('returns the SVG path for a known icon', () => { + expect(getIconPath('folder')).toBe(ICON_PATHS.folder); + expect(getIconPath('typescript')).toBe(ICON_PATHS.typescript); + }); + + it('falls back to the "file" SVG path for unknown icon IDs', () => { + expect(getIconPath('nonexistent')).toBe(ICON_PATHS.file); + expect(getIconPath('')).toBe(ICON_PATHS.file); + }); + + it('every icon ID that resolveIcon can return has a corresponding SVG path', () => { + // Collect all icon IDs that resolveIcon can produce + const resolvableIds = new Set(); + // From directories + resolvableIds.add('folder'); + // From fallback + resolvableIds.add('file'); + // Sample of extensions/filenames — all icon IDs that appear in the mappings + const testFiles = [ + 'app.js', + 'App.tsx', + 'utils.ts', + 'main.py', + 'app.rb', + 'main.rs', + 'main.go', + 'App.java', + 'Prog.cs', + 'main.cpp', + 'main.c', + 'data.json', + 'conf.yaml', + 'conf.toml', + 'data.xml', + 'index.html', + 'styles.css', + 'notes.md', + 'build.sh', + 'logo.svg', + 'query.sql', + 'some.lock', + 'config.env', + 'App.vue', + 'App.svelte', + '.gitignore', + 'Dockerfile', + 'LICENSE', + ]; + for (const f of testFiles) { + resolvableIds.add(resolveIcon(f, 'file')); + } + + for (const iconId of resolvableIds) { + const path = getIconPath(iconId); + expect(path, `icon "${iconId}" should have its own SVG path, not the fallback`).toBe( + ICON_PATHS[iconId] + ); + } + }); + }); + + describe('ICON_PATHS', () => { + it('includes at minimum the two mandatory icons (file and folder)', () => { + expect(ICON_PATHS.file).toBeDefined(); + expect(ICON_PATHS.folder).toBeDefined(); + }); + + it('every entry is a non-empty SVG path string', () => { + for (const [key, value] of Object.entries(ICON_PATHS)) { + expect(typeof value, `${key} should be a string`).toBe('string'); + expect(value.length, `${key} should have a non-empty path`).toBeGreaterThan(0); + } + }); + }); +}); diff --git a/packages/parser/tests/treeViewValueConverter.test.ts b/packages/parser/tests/treeViewValueConverter.test.ts new file mode 100644 index 00000000000..86bdf5ae5b8 --- /dev/null +++ b/packages/parser/tests/treeViewValueConverter.test.ts @@ -0,0 +1,98 @@ +import type { CstNode, GrammarAST } from 'langium'; +import { describe, expect, it } from 'vitest'; +import { TreeViewValueConverter } from '../src/language/treeView/valueConverter.js'; + +/** + * Subclass that exposes the protected runCustomConverter for testing. + */ +class TestableTreeViewValueConverter extends TreeViewValueConverter { + public runCustomConverter(rule: GrammarAST.AbstractRule, input: string, cstNode: CstNode) { + return super.runCustomConverter(rule, input, cstNode); + } +} + +function convert(converter: TestableTreeViewValueConverter, ruleName: string, input: string) { + const fakeRule = { name: ruleName } as unknown as GrammarAST.AbstractRule; + const fakeCstNode = {} as unknown as CstNode; + return converter.runCustomConverter(fakeRule, input, fakeCstNode); +} + +describe('TreeViewValueConverter', () => { + const converter = new TestableTreeViewValueConverter(); + + describe('INDENTATION', () => { + it('should return the length of whitespace', () => { + expect(convert(converter, 'INDENTATION', ' ')).toBe(4); + }); + + it('should return 0 for empty string', () => { + expect(convert(converter, 'INDENTATION', '')).toBe(0); + }); + + it('should return 8 for 8 spaces', () => { + expect(convert(converter, 'INDENTATION', ' ')).toBe(8); + }); + }); + + describe('QUOTED_NAME', () => { + it('should strip double quotes', () => { + expect(convert(converter, 'QUOTED_NAME', '"my file.js"')).toBe('my file.js'); + }); + + it('should strip single quotes', () => { + expect(convert(converter, 'QUOTED_NAME', "'my folder/'")).toBe('my folder/'); + }); + + it('should handle empty quoted string', () => { + expect(convert(converter, 'QUOTED_NAME', '""')).toBe(''); + }); + }); + + describe('CLASS_ANNOTATION', () => { + it('should extract class name', () => { + expect(convert(converter, 'CLASS_ANNOTATION', ' :::highlight')).toBe('highlight'); + }); + + it('should handle class with hyphens', () => { + expect(convert(converter, 'CLASS_ANNOTATION', ' :::my-class')).toBe('my-class'); + }); + + it('should trim surrounding whitespace', () => { + expect(convert(converter, 'CLASS_ANNOTATION', ' :::highlight ')).toBe('highlight'); + }); + }); + + describe('ICON_ANNOTATION', () => { + it('should extract icon name', () => { + expect(convert(converter, 'ICON_ANNOTATION', ' icon(database)')).toBe('database'); + }); + + it('should return empty string for icon()', () => { + expect(convert(converter, 'ICON_ANNOTATION', ' icon()')).toBe(''); + }); + + it('should handle icon(none)', () => { + expect(convert(converter, 'ICON_ANNOTATION', ' icon(none)')).toBe('none'); + }); + }); + + describe('DESC_ANNOTATION', () => { + it('should extract description text', () => { + expect(convert(converter, 'DESC_ANNOTATION', ' ## entry point')).toBe('entry point'); + }); + + it('should trim whitespace', () => { + expect(convert(converter, 'DESC_ANNOTATION', ' ## my description ')).toBe('my description'); + }); + + it('should handle ## with no trailing text', () => { + expect(convert(converter, 'DESC_ANNOTATION', ' ##')).toBe(''); + }); + }); + + describe('unknown rules', () => { + it('should return undefined for unrecognized rule names', () => { + expect(convert(converter, 'UNKNOWN_RULE', 'anything')).toBeUndefined(); + }); + }); +}); From a8c1f581fd8745fc7da8e4c900935194882e856c Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 2 Apr 2026 18:49:54 +1030 Subject: [PATCH 08/11] fix: use xlink:href for SVG icon references in treeView renderer Headless Chrome (used by Argos CI screenshots) does not resolve bare href on elements. Switch to xlink:href to match the convention used by other mermaid diagrams (e.g. sequence diagram actors). --- packages/mermaid/src/diagrams/treeView/renderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mermaid/src/diagrams/treeView/renderer.ts b/packages/mermaid/src/diagrams/treeView/renderer.ts index 5c67cab050b..e3709e6487d 100644 --- a/packages/mermaid/src/diagrams/treeView/renderer.ts +++ b/packages/mermaid/src/diagrams/treeView/renderer.ts @@ -65,7 +65,7 @@ const positionLabel = ( if (showIcon) { nodeGroup .append('use') - .attr('href', `#tv-icon-${diagramId}-${node.iconId}`) + .attr('xlink:href', `#tv-icon-${diagramId}-${node.iconId}`) .attr('x', x + config.paddingX) .attr('y', y + config.paddingY) .attr('width', ICON_SIZE) From ab41de3772e834d073a439f27f6478aba0895f5b Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Wed, 15 Apr 2026 09:49:59 +1000 Subject: [PATCH 09/11] fix: restore CHANGELOG.md symlink and add to .prettierignore The root CHANGELOG.md is a git symlink (mode 120000) to ./packages/mermaid/CHANGELOG.md. Commit dbfde57 inadvertently added a trailing newline to the symlink blob via prettier, which on Linux CI creates a broken symlink (the target path includes a literal newline). - Restore the symlink blob to the original 31 bytes (no trailing newline) - Add CHANGELOG.md to .prettierignore so prettier never touches it again --- .prettierignore | 2 ++ CHANGELOG.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index c700804260b..77c2aa9a84b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,6 @@ dist +# Symlink to packages/mermaid/CHANGELOG.md — prettier must not add a trailing newline +CHANGELOG.md cypress/platform/xss3.html .cache .pnpm-store diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a51e673ea..c68d3f76d18 120000 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1 @@ -./packages/mermaid/CHANGELOG.md +./packages/mermaid/CHANGELOG.md \ No newline at end of file From 31936858f230b1914125f01437ad6aea1ee1b742 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:56:19 +0000 Subject: [PATCH 10/11] [autofix.ci] apply automated fixes --- docs/syntax/treeView.md | 169 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 157 insertions(+), 12 deletions(-) diff --git a/docs/syntax/treeView.md b/docs/syntax/treeView.md index 45779743c4b..56dee85adf1 100644 --- a/docs/syntax/treeView.md +++ b/docs/syntax/treeView.md @@ -8,23 +8,140 @@ ## Introduction -A TreeView diagram is used to represent hierarchical data in the form of a directory-like structure. +A TreeView diagram is used to represent hierarchical data in the form of a directory-like structure, with file-type icons, connector lines, and optional annotations. ## Syntax -The structure of the tree depends only on indentation. +The structure of the tree depends only on indentation. Labels can be **bare** (unquoted) or **quoted** (for names containing spaces). +- Directories are indicated by a trailing `/` on the label — they get a folder icon and bold text. +- Files are auto-detected by extension and assigned a matching icon. +- Quoted labels (`"my file"`) support spaces in names. + +``` +treeView-beta + my-project/ + src/ + index.js + package.json + README.md +``` + +Quoted labels (backward compatible): + +``` +treeView-beta + "my project" + "folder with spaces" + "file.js" +``` + +## Annotations + +### Highlighting with :::class + +Annotate a node with `:::className` to apply a CSS class. A built-in `highlight` class is provided: + +```mermaid-example +treeView-beta + src/ + App.tsx :::highlight + index.js + package.json +``` + +```mermaid +treeView-beta + src/ + App.tsx :::highlight + index.js + package.json +``` + +### Inline descriptions with + +Add a visible description after `##` — rendered next to the label in italic: + +```mermaid-example +treeView-beta + src/ + index.js ## app entry point + config.ts ## runtime configuration + package.json ## project manifest +``` + +```mermaid +treeView-beta + src/ + index.js ## app entry point + config.ts ## runtime configuration + package.json ## project manifest ``` + +### Icon overrides with icon() + +Override the auto-detected icon with `icon(name)`: + +```mermaid-example +treeView-beta + data/ + model.bin icon(database) + weights.h5 icon(database) + src/ + index.js +``` + +```mermaid treeView-beta - "" - "" - "" - "" - "" + data/ + model.bin icon(database) + weights.h5 icon(database) + src/ + index.js +``` + +### Combined annotations + +Annotations can be combined in any order: + +```mermaid-example +treeView-beta + my-project/ + src/ + App.tsx :::highlight icon(react) ## main component + index.js ## entry point + .env ## environment variables + Dockerfile + package.json +``` + +```mermaid +treeView-beta + my-project/ + src/ + App.tsx :::highlight icon(react) ## main component + index.js ## entry point + .env ## environment variables + Dockerfile + package.json +``` + +## Comments + +Use `%%` for invisible comments (standard Mermaid convention): + +``` +treeView-beta + %% Generated files — do not edit + src/ + generated/ + index.js ``` ## Examples +Basic with quoted labels: + ```mermaid-example treeView-beta "packages" @@ -41,6 +158,8 @@ treeView-beta "parser" ``` +With custom config: + ```mermaid-example --- config: @@ -90,8 +209,34 @@ treeView-beta ### Theme Variables -| Property | Description | Default Value | -| ------------- | ---------------------- | ------------- | -| labelFontSize | Font size of the label | '16px' | -| labelColor | Color of the label | 'black' | -| lineColor | Color of the line | 'black' | +| Property | Description | Default Value | +| ---------------- | ---------------------------- | ------------- | +| labelFontSize | Font size of the label | '16px' | +| labelColor | Color of the label | 'black' | +| lineColor | Color of the line | 'black' | +| iconColor | Color of file-type icons | '#546e7a' | +| descriptionColor | Color of ## description text | '#999' | + +## Supported Icons + +Icons are auto-detected from file extensions and known filenames: + +| Extension / Filename | Icon | +| --------------------- | ---------- | +| `.js`, `.mjs`, `.cjs` | javascript | +| `.ts` | typescript | +| `.jsx`, `.tsx` | react | +| `.py` | python | +| `.json` | json | +| `.md`, `.mdx` | markdown | +| `.html`, `.htm` | html | +| `.css`, `.scss` | css | +| `.yaml`, `.yml` | yaml | +| `.sh`, `.bash` | terminal | +| `.sql`, `.db` | database | +| `.lock` | lock | +| `.gitignore` | git | +| `Dockerfile` | docker | +| `Makefile` | terminal | +| Directories (`/`) | folder | +| Unknown extension | file | From f599ef47881ac65f09f4e8bb7f2ff2fb1ead18ef Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Thu, 16 Apr 2026 08:27:42 +1000 Subject: [PATCH 11/11] address review: theme highlight colors, cssClass match, indent type, sanitizeText, docs - highlightBg/highlightStroke theme vars replace hardcoded highlight colors - cssClass uses split(/\s+/).includes() to avoid substring false positives - indent uses typeof check instead of parseInt - description text sanitized via sanitizeText(rawDesc, getConfig()) - docs: fix heading, descriptionColor default, add showIcons + highlight vars --- docs/syntax/treeView.md | 184 ++++++++++++++++-- .../mermaid/src/diagrams/treeView/parser.ts | 9 +- .../mermaid/src/diagrams/treeView/renderer.ts | 4 +- .../mermaid/src/diagrams/treeView/styles.ts | 19 +- .../mermaid/src/diagrams/treeView/types.ts | 2 + packages/mermaid/src/docs/syntax/treeView.md | 31 +-- 6 files changed, 206 insertions(+), 43 deletions(-) diff --git a/docs/syntax/treeView.md b/docs/syntax/treeView.md index 45779743c4b..5190d874bdd 100644 --- a/docs/syntax/treeView.md +++ b/docs/syntax/treeView.md @@ -8,23 +8,140 @@ ## Introduction -A TreeView diagram is used to represent hierarchical data in the form of a directory-like structure. +A TreeView diagram is used to represent hierarchical data in the form of a directory-like structure, with file-type icons, connector lines, and optional annotations. ## Syntax -The structure of the tree depends only on indentation. +The structure of the tree depends only on indentation. Labels can be **bare** (unquoted) or **quoted** (for names containing spaces). +- Directories are indicated by a trailing `/` on the label — they get a folder icon and bold text. +- Files are auto-detected by extension and assigned a matching icon. +- Quoted labels (`"my file"`) support spaces in names. + +``` +treeView-beta + my-project/ + src/ + index.js + package.json + README.md +``` + +Quoted labels (backward compatible): + +``` +treeView-beta + "my project" + "folder with spaces" + "file.js" +``` + +## Annotations + +### Highlighting with :::class + +Annotate a node with `:::className` to apply a CSS class. A built-in `highlight` class is provided: + +```mermaid-example +treeView-beta + src/ + App.tsx :::highlight + index.js + package.json +``` + +```mermaid +treeView-beta + src/ + App.tsx :::highlight + index.js + package.json +``` + +### Inline descriptions with `##` + +Add a visible description after `##` — rendered next to the label in italic: + +```mermaid-example +treeView-beta + src/ + index.js ## app entry point + config.ts ## runtime configuration + package.json ## project manifest +``` + +```mermaid +treeView-beta + src/ + index.js ## app entry point + config.ts ## runtime configuration + package.json ## project manifest ``` + +### Icon overrides with icon() + +Override the auto-detected icon with `icon(name)`: + +```mermaid-example +treeView-beta + data/ + model.bin icon(database) + weights.h5 icon(database) + src/ + index.js +``` + +```mermaid treeView-beta - "" - "" - "" - "" - "" + data/ + model.bin icon(database) + weights.h5 icon(database) + src/ + index.js +``` + +### Combined annotations + +Annotations can be combined in any order: + +```mermaid-example +treeView-beta + my-project/ + src/ + App.tsx :::highlight icon(react) ## main component + index.js ## entry point + .env ## environment variables + Dockerfile + package.json +``` + +```mermaid +treeView-beta + my-project/ + src/ + App.tsx :::highlight icon(react) ## main component + index.js ## entry point + .env ## environment variables + Dockerfile + package.json +``` + +## Comments + +Use `%%` for invisible comments (standard Mermaid convention): + +``` +treeView-beta + %% Generated files — do not edit + src/ + generated/ + index.js ``` ## Examples +Basic with quoted labels: + ```mermaid-example treeView-beta "packages" @@ -41,6 +158,8 @@ treeView-beta "parser" ``` +With custom config: + ```mermaid-example --- config: @@ -81,17 +200,46 @@ treeView-beta ## Config Variables -| Property | Description | Default Value | -| ------------- | ------------------------- | ------------- | -| rowIndent | Indentation for each row | 10 | -| paddingX | Horizontal padding of row | 5 | -| paddingY | Vertical padding of row | 5 | -| lineThickness | Thickness of the line | 1 | +| Property | Description | Default Value | +| ------------- | --------------------------------- | ------------- | +| rowIndent | Indentation for each row | 10 | +| paddingX | Horizontal padding of row | 5 | +| paddingY | Vertical padding of row | 5 | +| lineThickness | Thickness of the line | 1 | +| showIcons | Whether to show file/folder icons | true | ### Theme Variables -| Property | Description | Default Value | -| ------------- | ---------------------- | ------------- | -| labelFontSize | Font size of the label | '16px' | -| labelColor | Color of the label | 'black' | -| lineColor | Color of the line | 'black' | +| Property | Description | Default Value | +| ---------------- | ------------------------------ | -------------------- | +| labelFontSize | Font size of the label | '16px' | +| labelColor | Color of the label | 'black' | +| lineColor | Color of the line | 'black' | +| iconColor | Color of file-type icons | '#546e7a' | +| descriptionColor | Color of `##` description text | '#6a9955' | +| highlightBg | Highlight background fill | rgba(255,193,7,0.15) | +| highlightStroke | Highlight border stroke | #ffc107 | + +## Supported Icons + +Icons are auto-detected from file extensions and known filenames: + +| Extension / Filename | Icon | +| --------------------- | ---------- | +| `.js`, `.mjs`, `.cjs` | javascript | +| `.ts` | typescript | +| `.jsx`, `.tsx` | react | +| `.py` | python | +| `.json` | json | +| `.md`, `.mdx` | markdown | +| `.html`, `.htm` | html | +| `.css`, `.scss` | css | +| `.yaml`, `.yml` | yaml | +| `.sh`, `.bash` | terminal | +| `.sql`, `.db` | database | +| `.lock` | lock | +| `.gitignore` | git | +| `Dockerfile` | docker | +| `Makefile` | terminal | +| Directories (`/`) | folder | +| Unknown extension | file | diff --git a/packages/mermaid/src/diagrams/treeView/parser.ts b/packages/mermaid/src/diagrams/treeView/parser.ts index ab734a00989..0a0e6adce33 100644 --- a/packages/mermaid/src/diagrams/treeView/parser.ts +++ b/packages/mermaid/src/diagrams/treeView/parser.ts @@ -1,6 +1,8 @@ import { parse, type TreeView } from '@mermaid-js/parser'; +import { getConfig } from '../../config.js'; import type { ParserDefinition } from '../../diagram-api/types.js'; import { log } from '../../logger.js'; +import { sanitizeText } from '../common/common.js'; import { populateCommonDb } from '../common/populateCommonDb.js'; import db from './db.js'; import { resolveIcon } from './icons.js'; @@ -9,7 +11,7 @@ import type { NodeType } from './types.js'; const populate = (ast: TreeView) => { populateCommonDb(ast, db); for (const node of ast.nodes) { - const level = node.indent ? parseInt(node.indent as unknown as string) : 0; + const level = typeof node.indent === 'number' ? node.indent : 0; // Name comes pre-cleaned from value converter (quotes stripped, etc.) let name = node.name as unknown as string; @@ -34,8 +36,9 @@ const populate = (ast: TreeView) => { iconId = resolveIcon(isDirectory ? name + '/' : name, nodeType); } - // Description comes pre-trimmed from value converter - const description = (node.descAnnotation as unknown as string) || undefined; + // Description comes pre-trimmed from value converter; sanitize for defense in depth + const rawDesc = (node.descAnnotation as unknown as string) || undefined; + const description = rawDesc ? sanitizeText(rawDesc, getConfig()) : undefined; db.addNode(level, name, nodeType, cssClass, iconId, description); } diff --git a/packages/mermaid/src/diagrams/treeView/renderer.ts b/packages/mermaid/src/diagrams/treeView/renderer.ts index e3709e6487d..4870138d38d 100644 --- a/packages/mermaid/src/diagrams/treeView/renderer.ts +++ b/packages/mermaid/src/diagrams/treeView/renderer.ts @@ -90,7 +90,7 @@ const positionLabel = ( node.BBox = { x, y, width, height }; // Highlight background rect (sized later in drawTree) - if (node.cssClass?.includes('highlight')) { + if (node.cssClass?.split(/\s+/).includes('highlight')) { nodeGroup .insert('rect', ':first-child') .attr('x', x) @@ -196,7 +196,7 @@ const drawTree = ( // Phase 3: Size highlight background rects to full tree width for (const ri of renderInfos) { - if (ri.node.cssClass?.includes('highlight')) { + if (ri.node.cssClass?.split(/\s+/).includes('highlight')) { const rect = ri.nodeGroup.select('.treeView-highlight-bg'); if (!rect.empty()) { const rectWidth = totalWidth - ri.node.BBox!.x + 8; diff --git a/packages/mermaid/src/diagrams/treeView/styles.ts b/packages/mermaid/src/diagrams/treeView/styles.ts index 945eb17d958..9e7e6ab42b4 100644 --- a/packages/mermaid/src/diagrams/treeView/styles.ts +++ b/packages/mermaid/src/diagrams/treeView/styles.ts @@ -8,6 +8,8 @@ const defaultTreeViewDiagramStyles: Required = { lineColor: 'black', iconColor: '#546e7a', descriptionColor: '#6a9955', + highlightBg: 'rgba(255, 193, 7, 0.15)', + highlightStroke: '#ffc107', }; const styles: DiagramStylesProvider = ({ @@ -15,10 +17,15 @@ const styles: DiagramStylesProvider = ({ }: { treeView?: TreeViewDiagramStyles; }): string => { - const { labelFontSize, labelColor, lineColor, iconColor, descriptionColor } = cleanAndMerge( - defaultTreeViewDiagramStyles, - treeView - ); + const { + labelFontSize, + labelColor, + lineColor, + iconColor, + descriptionColor, + highlightBg, + highlightStroke, + } = cleanAndMerge(defaultTreeViewDiagramStyles, treeView); return ` .treeView-node-label { font-size: ${labelFontSize}; @@ -39,8 +46,8 @@ const styles: DiagramStylesProvider = ({ font-style: italic; } .treeView-highlight-bg { - fill: rgba(255, 193, 7, 0.15); - stroke: #ffc107; + fill: ${highlightBg}; + stroke: ${highlightStroke}; stroke-width: 1; } `; diff --git a/packages/mermaid/src/diagrams/treeView/types.ts b/packages/mermaid/src/diagrams/treeView/types.ts index b277af236e5..ea6aa0bb360 100644 --- a/packages/mermaid/src/diagrams/treeView/types.ts +++ b/packages/mermaid/src/diagrams/treeView/types.ts @@ -42,6 +42,8 @@ export interface TreeViewDiagramStyles { lineColor?: string; iconColor?: string; descriptionColor?: string; + highlightBg?: string; + highlightStroke?: string; } export type D3SVGElement = Selection; diff --git a/packages/mermaid/src/docs/syntax/treeView.md b/packages/mermaid/src/docs/syntax/treeView.md index f488bda0dc9..5ec55b1415f 100644 --- a/packages/mermaid/src/docs/syntax/treeView.md +++ b/packages/mermaid/src/docs/syntax/treeView.md @@ -44,7 +44,7 @@ treeView-beta package.json ``` -### Inline descriptions with +### Inline descriptions with `##` Add a visible description after `##` — rendered next to the label in italic: @@ -131,22 +131,25 @@ treeView-beta ## Config Variables -| Property | Description | Default Value | -| ------------- | ------------------------- | ------------- | -| rowIndent | Indentation for each row | 10 | -| paddingX | Horizontal padding of row | 5 | -| paddingY | Vertical padding of row | 5 | -| lineThickness | Thickness of the line | 1 | +| Property | Description | Default Value | +| ------------- | --------------------------------- | ------------- | +| rowIndent | Indentation for each row | 10 | +| paddingX | Horizontal padding of row | 5 | +| paddingY | Vertical padding of row | 5 | +| lineThickness | Thickness of the line | 1 | +| showIcons | Whether to show file/folder icons | true | ### Theme Variables -| Property | Description | Default Value | -| ---------------- | ---------------------------- | ------------- | -| labelFontSize | Font size of the label | '16px' | -| labelColor | Color of the label | 'black' | -| lineColor | Color of the line | 'black' | -| iconColor | Color of file-type icons | '#546e7a' | -| descriptionColor | Color of ## description text | '#999' | +| Property | Description | Default Value | +| ---------------- | ------------------------------ | -------------------- | +| labelFontSize | Font size of the label | '16px' | +| labelColor | Color of the label | 'black' | +| lineColor | Color of the line | 'black' | +| iconColor | Color of file-type icons | '#546e7a' | +| descriptionColor | Color of `##` description text | '#6a9955' | +| highlightBg | Highlight background fill | rgba(255,193,7,0.15) | +| highlightStroke | Highlight border stroke | #ffc107 | ## Supported Icons