diff --git a/.build/jsonSchema.ts b/.build/jsonSchema.ts index c1ff5f481de..7715a96c434 100644 --- a/.build/jsonSchema.ts +++ b/.build/jsonSchema.ts @@ -27,6 +27,7 @@ const MERMAID_CONFIG_DIAGRAM_KEYS = [ 'sankey', 'block', 'packet', + 'treeView', 'architecture', 'radar', 'venn', diff --git a/.changeset/weak-tools-pay.md b/.changeset/weak-tools-pay.md new file mode 100644 index 00000000000..60776a1bb73 --- /dev/null +++ b/.changeset/weak-tools-pay.md @@ -0,0 +1,7 @@ +--- +'@mermaid-js/examples': minor +'mermaid': minor +'@mermaid-js/parser': minor +--- + +add new TreeView diagram diff --git a/cypress/integration/rendering/treeView.spec.ts b/cypress/integration/rendering/treeView.spec.ts new file mode 100644 index 00000000000..837f7d2521f --- /dev/null +++ b/cypress/integration/rendering/treeView.spec.ts @@ -0,0 +1,75 @@ +import { imgSnapshotTest } from '../../helpers/util'; + +describe('TreeView Diagram', () => { + it('should render a simple treeView diagram', () => { + imgSnapshotTest( + `treeView-beta + "file1.ts"` + ); + }); + + it('should render a complex treeView diagram', () => { + imgSnapshotTest( + `treeView-beta + "root" + "folder1" + "file1.js" + "file2.ts" + "folder2" + "file3.spec.ts" + "folder3" + "file4.ts" + "file5.ts" + "folder4" + "file6.ts" + "file7.ts"` + ); + }); + + it('should render a complex treeView diagram with multiple roots', () => { + imgSnapshotTest( + `treeView-beta + "folder1" + "file1.js" + "file2.ts" + "folder2" + "file3.spec.ts" + "folder3" + "file4.ts" + "file5.ts" + "folder4" + "file6.ts" + "file7.ts"` + ); + }); + + it('should render a treeView diagram with custom config', () => { + imgSnapshotTest( + ` +--- +config: + treeView: + rowIndent: 80 + lineThickness: 3 + themeVariables: + treeView: + labelFontSize: '20px' + labelColor: '#FF0000' + lineColor: '#00FF00' +--- +treeView-beta + "folder1" + "file1.js" + "file2.ts" + "folder2" + "file3.spec.ts" + "folder3" + "file4.ts" + "file5.ts" + "folder4" + "file6.ts" + "file7.ts" + ` + ); + }); +}); diff --git a/demos/index.html b/demos/index.html index c5a062cd94b..9a74804e579 100644 --- a/demos/index.html +++ b/demos/index.html @@ -1,105 +1,108 @@ - - - - - - Mermaid Quick Test Page - - - - - -

Mermaid quick test and demo pages

-

- Some of these pages have duplicates; some are slow to load because they have so many graphs. -

-

You can test custom code in the development page.

-

- If you'd like to clean up one of the pages, please feel free to - submit a pull request (PR). -

- - - - + + + + + + Mermaid Quick Test Page + + + + + +

Mermaid quick test and demo pages

+

+ Some of these pages have duplicates; some are slow to load because they have so many graphs. +

+

You can test custom code in the development page.

+

+ If you'd like to clean up one of the pages, please feel free to + submit a pull request (PR). +

+ + + + diff --git a/demos/treeView.html b/demos/treeView.html new file mode 100644 index 00000000000..174830fa102 --- /dev/null +++ b/demos/treeView.html @@ -0,0 +1,75 @@ + + + + + + Mermaid TreeView Diagram Demo + + + + +

TreeView Diagram Demo

+ +

Basic TreeView Example

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

TreeView Config Example

+
+    ---
+    config:
+        treeView:
+            rowIndent: 80
+            lineThickness: 3
+        themeVariables:
+            treeView:
+                labelFontSize: '20px'
+                labelColor: '#FF0000'
+                lineColor: '#00FF00'
+    ---
+    treeView-beta
+    "packages"
+        "mermaid"
+            "src"
+        "parser"
+    
+ + + + diff --git a/docs/config/setup/defaultConfig/variables/configKeys.md b/docs/config/setup/defaultConfig/variables/configKeys.md index 2e7258bb854..056ef6cbd2f 100644 --- a/docs/config/setup/defaultConfig/variables/configKeys.md +++ b/docs/config/setup/defaultConfig/variables/configKeys.md @@ -12,4 +12,4 @@ > `const` **configKeys**: `Set`<`string`> -Defined in: [packages/mermaid/src/defaultConfig.ts:298](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L298) +Defined in: [packages/mermaid/src/defaultConfig.ts:302](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L302) diff --git a/docs/config/setup/mermaid/interfaces/MermaidConfig.md b/docs/config/setup/mermaid/interfaces/MermaidConfig.md index 8d1fea23e59..dd84eaea0d0 100644 --- a/docs/config/setup/mermaid/interfaces/MermaidConfig.md +++ b/docs/config/setup/mermaid/interfaces/MermaidConfig.md @@ -105,7 +105,7 @@ You can set this attribute to base the seed on a static string. > `optional` **dompurifyConfig**: `Config` -Defined in: [packages/mermaid/src/config.type.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L222) +Defined in: [packages/mermaid/src/config.type.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L223) --- @@ -179,7 +179,7 @@ See > `optional` **fontSize**: `number` -Defined in: [packages/mermaid/src/config.type.ts:224](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L224) +Defined in: [packages/mermaid/src/config.type.ts:225](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L225) --- @@ -305,7 +305,7 @@ Defines which main look to use for the diagram. > `optional` **markdownAutoWrap**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:225](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L225) +Defined in: [packages/mermaid/src/config.type.ts:226](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L226) --- @@ -365,7 +365,7 @@ Defined in: [packages/mermaid/src/config.type.ts:208](https://github.com/mermaid > `optional` **radar**: `RadarDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:220](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L220) +Defined in: [packages/mermaid/src/config.type.ts:221](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L221) --- @@ -437,7 +437,7 @@ Defined in: [packages/mermaid/src/config.type.ts:205](https://github.com/mermaid > `optional` **suppressErrorRendering**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:231](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L231) +Defined in: [packages/mermaid/src/config.type.ts:232](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L232) Suppresses inserting 'Syntax error' diagram in the DOM. This is useful when you want to control how to handle syntax errors in your application. @@ -479,11 +479,19 @@ Defined in: [packages/mermaid/src/config.type.ts:203](https://github.com/mermaid --- +### treeView? + +> `optional` **treeView**: `TreeViewDiagramConfig` + +Defined in: [packages/mermaid/src/config.type.ts:220](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L220) + +--- + ### venn? > `optional` **venn**: `VennDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:221](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L221) +Defined in: [packages/mermaid/src/config.type.ts:222](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L222) --- @@ -491,7 +499,7 @@ Defined in: [packages/mermaid/src/config.type.ts:221](https://github.com/mermaid > `optional` **wrap**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:223](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L223) +Defined in: [packages/mermaid/src/config.type.ts:224](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L224) --- diff --git a/docs/syntax/treeView.md b/docs/syntax/treeView.md new file mode 100644 index 00000000000..4f4b1ee91cd --- /dev/null +++ b/docs/syntax/treeView.md @@ -0,0 +1,97 @@ +> **Warning** +> +> ## THIS IS AN AUTOGENERATED FILE. DO NOT EDIT. +> +> ## Please edit the corresponding file in [/packages/mermaid/src/docs/syntax/treeView.md](../../packages/mermaid/src/docs/syntax/treeView.md). + +# TreeView Diagram (v\+) + +## Introduction + +A TreeView diagram is used to represent hierarchical data in the form of a directory-like structure. + +## Syntax + +The structure of the tree depends only on indentation. + +``` +treeView-beta + "" + "" + "" + "" + "" +``` + +## Examples + +```mermaid-example +treeView-beta + "packages" + "mermaid" + "src" + "parser" +``` + +```mermaid +treeView-beta + "packages" + "mermaid" + "src" + "parser" +``` + +```mermaid-example +--- +config: + treeView: + rowIndent: 80 + lineThickness: 3 + themeVariables: + treeView: + labelFontSize: '20px' + labelColor: '#FF0000' + lineColor: '#00FF00' +--- +treeView-beta + "packages" + "mermaid" + "src" + "parser" +``` + +```mermaid +--- +config: + treeView: + rowIndent: 80 + lineThickness: 3 + themeVariables: + treeView: + labelFontSize: '20px' + labelColor: '#FF0000' + lineColor: '#00FF00' +--- +treeView-beta + "packages" + "mermaid" + "src" + "parser" +``` + +## 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 | + +### 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' | diff --git a/packages/examples/src/examples/tree-view.ts b/packages/examples/src/examples/tree-view.ts new file mode 100644 index 00000000000..009b24348ec --- /dev/null +++ b/packages/examples/src/examples/tree-view.ts @@ -0,0 +1,24 @@ +import type { DiagramMetadata } from '../types.js'; + +export default { + id: 'treeView', + name: 'TreeView', + description: 'Visualize hierarchical data as a tree structure', + examples: [ + { + title: 'Basic TreeView', + isDefault: true, + code: `treeView-beta + "docs" + "build" + "make.bat" + "Makefile" + "out" + "source" + "build" + "static" + "_templates" + "div. Files"`, + }, + ], +} satisfies DiagramMetadata; diff --git a/packages/examples/src/index.ts b/packages/examples/src/index.ts index 720d2d6da3b..09421075afb 100644 --- a/packages/examples/src/index.ts +++ b/packages/examples/src/index.ts @@ -23,6 +23,7 @@ import packetDiagram from './examples/packet.js'; import blockDiagram from './examples/block.js'; import treemapDiagram from './examples/treemap.js'; import vennDiagram from './examples/venn.js'; +import treeViewDiagram from './examples/tree-view.js'; export const diagramData: DiagramMetadata[] = [ flowChart, @@ -49,4 +50,5 @@ export const diagramData: DiagramMetadata[] = [ blockDiagram, treemapDiagram, vennDiagram, + treeViewDiagram, ]; diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index f2600160682..25759f9b80d 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -217,6 +217,7 @@ export interface MermaidConfig { sankey?: SankeyDiagramConfig; packet?: PacketDiagramConfig; block?: BlockDiagramConfig; + treeView?: TreeViewDiagramConfig; radar?: RadarDiagramConfig; venn?: VennDiagramConfig; dompurifyConfig?: DOMPurifyConfiguration; @@ -1613,6 +1614,30 @@ export interface PacketDiagramConfig extends BaseDiagramConfig { export interface BlockDiagramConfig extends BaseDiagramConfig { padding?: number; } +/** + * The object containing configurations specific for treeView diagrams. + * + * This interface was referenced by `MermaidConfig`'s JSON-Schema + * via the `definition` "TreeViewDiagramConfig". + */ +export interface TreeViewDiagramConfig extends BaseDiagramConfig { + /** + * Horizontal distance between rows differing by one level + */ + rowIndent?: number; + /** + * Horizontal padding of label + */ + paddingX?: number; + /** + * Vertical padding of label + */ + paddingY?: number; + /** + * Thickness of the line + */ + lineThickness?: number; +} /** * The object containing configurations specific for radar diagrams. * diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index 49731b311ab..659081921e3 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -261,6 +261,10 @@ const config: RequiredDeep = { packet: { ...defaultConfigJson.packet, }, + treeView: { + ...defaultConfigJson.treeView, + useWidth: undefined, + }, radar: { ...defaultConfigJson.radar, }, diff --git a/packages/mermaid/src/diagram-api/diagram-orchestration.ts b/packages/mermaid/src/diagram-api/diagram-orchestration.ts index e365a6227ce..88f71a07e9e 100644 --- a/packages/mermaid/src/diagram-api/diagram-orchestration.ts +++ b/packages/mermaid/src/diagram-api/diagram-orchestration.ts @@ -24,6 +24,7 @@ import sankey from '../diagrams/sankey/sankeyDetector.js'; import { packet } from '../diagrams/packet/detector.js'; import { radar } from '../diagrams/radar/detector.js'; import block from '../diagrams/block/blockDetector.js'; +import treeView from '../diagrams/treeView/detector.js'; import architecture from '../diagrams/architecture/architectureDetector.js'; import { ishikawa } from '../diagrams/ishikawa/ishikawaDetector.js'; import venn from '../diagrams/venn/vennDetector.js'; @@ -102,6 +103,7 @@ export const addDiagrams = () => { packet, xychart, block, + treeView, radar, ishikawa, treemap, diff --git a/packages/mermaid/src/diagrams/treeView/db.ts b/packages/mermaid/src/diagrams/treeView/db.ts new file mode 100644 index 00000000000..db18b948c5d --- /dev/null +++ b/packages/mermaid/src/diagrams/treeView/db.ts @@ -0,0 +1,79 @@ +import type { TreeViewDiagramConfig } from '../../config.type.js'; +import type { TreeViewDB, Node } from './types.js'; +import { getConfig as getCommonConfig } from '../../config.js'; +import DEFAULT_CONFIG from '../../defaultConfig.js'; +import { + clear as commonClear, + getAccDescription, + getAccTitle, + getDiagramTitle, + setAccDescription, + setAccTitle, + setDiagramTitle, +} from '../common/commonDb.js'; +import { cleanAndMerge } from '../../utils.js'; +import { ImperativeState } from '../../utils/imperativeState.js'; + +interface TreeViewState { + cnt: number; + stack: Node[]; +} + +const state = new ImperativeState(() => ({ + cnt: 1, + stack: [ + { + id: 0, + level: -1, + name: '/', + children: [], + }, + ], +})); + +const clear = () => { + state.reset(); + commonClear(); +}; + +const getRoot = () => { + return state.records.stack[0]; +}; + +const getCount = () => state.records.cnt; + +const defaultConfig: Required = DEFAULT_CONFIG.treeView; + +const getConfig = (): Required => { + return cleanAndMerge(defaultConfig, getCommonConfig().treeView); +}; + +const addNode = (level: number, name: string) => { + while (level <= state.records.stack[state.records.stack.length - 1].level) { + state.records.stack.pop(); + } + const node = { + id: state.records.cnt++, + level, + name, + children: [], + }; + state.records.stack[state.records.stack.length - 1].children.push(node); + state.records.stack.push(node); +}; + +const db: TreeViewDB = { + clear, + addNode, + getRoot, + getCount, + getConfig, + getAccTitle, + getAccDescription, + getDiagramTitle, + setAccDescription, + setAccTitle, + setDiagramTitle, +}; + +export default db; diff --git a/packages/mermaid/src/diagrams/treeView/detector.ts b/packages/mermaid/src/diagrams/treeView/detector.ts new file mode 100644 index 00000000000..ef4d84e44ed --- /dev/null +++ b/packages/mermaid/src/diagrams/treeView/detector.ts @@ -0,0 +1,21 @@ +import type { DiagramDetector, DiagramLoader } from '../../diagram-api/types.js'; +import type { ExternalDiagramDefinition } from '../../diagram-api/types.js'; + +const id = 'treeView'; + +const detector: DiagramDetector = (txt) => { + return /^\s*treeView-beta/.test(txt); +}; + +const loader: DiagramLoader = async () => { + const { diagram } = await import('./diagram.js'); + return { id, diagram }; +}; + +const plugin: ExternalDiagramDefinition = { + id, + detector, + loader, +}; + +export default plugin; diff --git a/packages/mermaid/src/diagrams/treeView/diagram.ts b/packages/mermaid/src/diagrams/treeView/diagram.ts new file mode 100644 index 00000000000..19d432d9684 --- /dev/null +++ b/packages/mermaid/src/diagrams/treeView/diagram.ts @@ -0,0 +1,12 @@ +import type { DiagramDefinition } from '../../diagram-api/types.js'; +import { parser } from './parser.js'; +import db from './db.js'; +import renderer from './renderer.js'; +import styles from './styles.js'; + +export const diagram: DiagramDefinition = { + db, + renderer, + parser, + styles, +}; diff --git a/packages/mermaid/src/diagrams/treeView/parser.ts b/packages/mermaid/src/diagrams/treeView/parser.ts new file mode 100644 index 00000000000..70c63c220b0 --- /dev/null +++ b/packages/mermaid/src/diagrams/treeView/parser.ts @@ -0,0 +1,18 @@ +import type { ParserDefinition } from '../../diagram-api/types.js'; +import { log } from '../../logger.js'; +import { populateCommonDb } from '../common/populateCommonDb.js'; +import db from './db.js'; +import { parse, type TreeView } from '@mermaid-js/parser'; + +const populate = (ast: TreeView) => { + populateCommonDb(ast, db); + ast.nodes.map((node) => db.addNode(node.indent ? parseInt(node.indent) : 0, node.name)); +}; + +export const parser: ParserDefinition = { + parse: async (input: string): Promise => { + const ast = await parse('treeView', input); + log.debug(ast); + populate(ast); + }, +}; diff --git a/packages/mermaid/src/diagrams/treeView/renderer.ts b/packages/mermaid/src/diagrams/treeView/renderer.ts new file mode 100644 index 00000000000..150f765b763 --- /dev/null +++ b/packages/mermaid/src/diagrams/treeView/renderer.ts @@ -0,0 +1,126 @@ +import type { DiagramRenderer, DrawDefinition } from '../../diagram-api/types.js'; +import { log } from '../../logger.js'; +import { selectSvgElement } from '../../rendering-util/selectSvgElement.js'; +import type { D3SVGElement, TreeViewDB } from './types.js'; +import { configureSvgSize } from '../../setupGraphViewbox.js'; +import type { TreeViewDiagramConfig } from '../../config.type.js'; +import type { Node } from './types.js'; + +const positionLabel = ( + x: number, + y: number, + node: Node, + domElem: D3SVGElement, + config: Required +) => { + const label = domElem + .append('text') + .text(node.name) + .attr('dominant-baseline', 'middle') + .attr('class', 'treeView-node-label'); + const { height: labelHeight, width: labelWidth } = label.node()!.getBBox(); + const height = labelHeight + config.paddingY * 2; + const width = labelWidth + config.paddingX * 2; + label.attr('x', x + config.paddingX); + label.attr('y', y + height / 2); + node.BBox = { + x, + y, + width, + height, + }; +}; + +const positionLine = ( + domElem: D3SVGElement, + x1: number, + y1: number, + x2: number, + y2: number, + lineThickness: number +) => { + return domElem + .append('line') + .attr('x1', x1) + .attr('y1', y1) + .attr('x2', x2) + .attr('y2', y2) + .attr('stroke-width', lineThickness) + .attr('class', 'treeView-node-line'); +}; + +const drawTree = ( + elem: D3SVGElement, + root: Node, + config: Required +) => { + let totalHeight = 0; + let totalWidth = 0; + const drawNode = ( + elem: D3SVGElement, + node: Node, + config: Required, + depth: number + ) => { + const indent = depth * (config.rowIndent + config.paddingX); + positionLabel(indent, totalHeight, node, elem, config); + const { height, width } = node.BBox!; + positionLine( + elem, + indent - config.rowIndent, + totalHeight + height / 2, + indent, + totalHeight + height / 2, + config.lineThickness + ); + + totalWidth = Math.max(totalWidth, indent + width); + totalHeight += height; + }; + + const processNode = (node: Node, depth = 0) => { + drawNode(elem, node, config, depth); + node.children.forEach((child) => { + processNode(child, depth + 1); + }); + const { x, y, height } = node.BBox!; + if (node.children.length) { + const { y: endY, height: endHeight } = node.children[node.children.length - 1].BBox!; + positionLine( + elem, + x + config.paddingX, + y + height, + x + config.paddingX, + endY + endHeight / 2 + config.lineThickness / 2, + config.lineThickness + ); + } + }; + + processNode(root); + return { totalHeight, totalWidth }; +}; + +const draw: DrawDefinition = (text, id, _ver, diagObj) => { + log.debug('Rendering treeView diagram\n' + text); + + const db = diagObj.db as TreeViewDB; + const root = db.getRoot(); + const config = db.getConfig(); + + const svg = selectSvgElement(id); + const treeElem = svg.append('g'); + treeElem.attr('class', 'tree-view'); + + const { totalHeight, totalWidth } = drawTree(treeElem, root, config); + /* -${config.lineThickness/2} is required for a line with x coordinate = 0 + as there is overflow to the left due to the line being centered */ + svg.attr('viewBox', `-${config.lineThickness / 2} 0 ${totalWidth} ${totalHeight}`); + configureSvgSize(svg, totalHeight, totalWidth, config.useMaxWidth); +}; + +const renderer: DiagramRenderer = { + draw, +}; + +export default renderer; diff --git a/packages/mermaid/src/diagrams/treeView/styles.ts b/packages/mermaid/src/diagrams/treeView/styles.ts new file mode 100644 index 00000000000..691e127acee --- /dev/null +++ b/packages/mermaid/src/diagrams/treeView/styles.ts @@ -0,0 +1,31 @@ +import type { DiagramStylesProvider } from '../../diagram-api/types.js'; +import { cleanAndMerge } from '../../utils.js'; +import type { TreeViewDiagramStyles } from './types.js'; + +const defaultTreeViewDiagramStyles: Required = { + labelFontSize: '16px', + labelColor: 'black', + lineColor: 'black', +}; + +const styles: DiagramStylesProvider = ({ + treeView, +}: { + treeView?: TreeViewDiagramStyles; +}): string => { + const { labelFontSize, labelColor, lineColor } = cleanAndMerge( + defaultTreeViewDiagramStyles, + treeView + ); + return ` + .treeView-node-label { + font-size: ${labelFontSize}; + fill: ${labelColor}; + } + .treeView-node-line { + stroke: ${lineColor}; + } + `; +}; + +export default styles; diff --git a/packages/mermaid/src/diagrams/treeView/types.ts b/packages/mermaid/src/diagrams/treeView/types.ts new file mode 100644 index 00000000000..43a7f4e6da3 --- /dev/null +++ b/packages/mermaid/src/diagrams/treeView/types.ts @@ -0,0 +1,32 @@ +import type { TreeViewDiagramConfig } from '../../config.type.js'; +import type { DiagramDBBase } from '../../diagram-api/types.js'; +import type { Selection } from 'd3-selection'; + +interface BBox { + x: number; + y: number; + width: number; + height: number; +} + +export interface Node { + id: number; + level: number; + name: string; + BBox?: BBox; + children: Node[]; +} + +export interface TreeViewDB extends DiagramDBBase { + addNode: (level: number, name: string) => void; + getRoot: () => Node; + getCount: () => number; +} + +export interface TreeViewDiagramStyles { + labelColor?: string; + labelFontSize?: string; + lineColor?: string; +} + +export type D3SVGElement = Selection; diff --git a/packages/mermaid/src/docs/.vitepress/config.ts b/packages/mermaid/src/docs/.vitepress/config.ts index 4bab6795b88..b74b326a105 100644 --- a/packages/mermaid/src/docs/.vitepress/config.ts +++ b/packages/mermaid/src/docs/.vitepress/config.ts @@ -191,6 +191,7 @@ function sidebarSyntax() { { text: 'Treemap 🔥', link: '/syntax/treemap' }, { text: 'Venn 🔥', link: '/syntax/venn' }, { text: 'Ishikawa 🔥', link: '/syntax/ishikawa' }, + { text: 'TreeView 🔥', link: '/syntax/treeView' }, { text: 'Other Examples', link: '/syntax/examples' }, ], }, diff --git a/packages/mermaid/src/docs/syntax/treeView.md b/packages/mermaid/src/docs/syntax/treeView.md new file mode 100644 index 00000000000..19cad4e9288 --- /dev/null +++ b/packages/mermaid/src/docs/syntax/treeView.md @@ -0,0 +1,64 @@ +# TreeView Diagram (v+) + +## Introduction + +A TreeView diagram is used to represent hierarchical data in the form of a directory-like structure. + +## Syntax + +The structure of the tree depends only on indentation. + +``` +treeView-beta + "" + "" + "" + "" + "" +``` + +## Examples + +```mermaid-example +treeView-beta + "packages" + "mermaid" + "src" + "parser" +``` + +```mermaid-example +--- +config: + treeView: + rowIndent: 80 + lineThickness: 3 + themeVariables: + treeView: + labelFontSize: '20px' + labelColor: '#FF0000' + lineColor: '#00FF00' +--- +treeView-beta + "packages" + "mermaid" + "src" + "parser" +``` + +## 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 | + +### 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' | diff --git a/packages/mermaid/src/mermaidAPI.spec.ts b/packages/mermaid/src/mermaidAPI.spec.ts index db7ef07254e..47ae1ad18de 100644 --- a/packages/mermaid/src/mermaidAPI.spec.ts +++ b/packages/mermaid/src/mermaidAPI.spec.ts @@ -763,6 +763,7 @@ graph TD;A--x|text including URL space|B;`) { textDiagramType: 'requirementDiagram', expectedType: 'requirement' }, { textDiagramType: 'sequenceDiagram', expectedType: 'sequence' }, { textDiagramType: 'stateDiagram-v2', expectedType: 'stateDiagram' }, + { textDiagramType: 'treeView-beta', expectedType: 'treeView' }, { textDiagramType: 'radar-beta', expectedType: 'radar' }, { textDiagramType: 'architecture-beta', expectedType: 'architecture' }, ]; @@ -776,7 +777,7 @@ graph TD;A--x|text including URL space|B;`) describe(`${testedDiagram.textDiagramType}`, () => { const diagramType = testedDiagram.textDiagramType; const content = testedDiagram.content || ''; - const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n ${content}`; + const diagramText = `${diagramType}\n accTitle: ${a11yTitle}\n accDescr: ${a11yDescr}\n${content}`; const expectedDiagramType = testedDiagram.expectedType; jsdomIt( diff --git a/packages/mermaid/src/rendering-util/multi-diagram-id-uniqueness.spec.ts b/packages/mermaid/src/rendering-util/multi-diagram-id-uniqueness.spec.ts index 026e223e56f..05e01a1e905 100644 --- a/packages/mermaid/src/rendering-util/multi-diagram-id-uniqueness.spec.ts +++ b/packages/mermaid/src/rendering-util/multi-diagram-id-uniqueness.spec.ts @@ -105,6 +105,12 @@ radar-beta "Section 2" "Leaf 2.1": 20`, + treeView: `treeView-beta +"packages" + "mermaid" + "src" + "parser"`, + ishikawa: `ishikawa-beta Root Cause Category A diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 7a0f07a500c..eb53c34b694 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -54,6 +54,7 @@ required: - sankey - packet - block + - treeView - look - venn properties: @@ -315,6 +316,8 @@ properties: $ref: '#/$defs/PacketDiagramConfig' block: $ref: '#/$defs/BlockDiagramConfig' + treeView: + $ref: '#/$defs/TreeViewDiagramConfig' radar: $ref: '#/$defs/RadarDiagramConfig' venn: @@ -2312,6 +2315,34 @@ $defs: # JSON Schema definition (maybe we should move these to a separate file) minimum: 0 default: 8 + TreeViewDiagramConfig: + title: TreeView Diagram Config + allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }] + description: The object containing configurations specific for treeView diagrams. + type: object + unevaluatedProperties: false + properties: + rowIndent: + description: Horizontal distance between rows differing by one level + type: number + minimum: 0 + default: 10 + paddingX: + description: Horizontal padding of label + type: number + minimum: 0 + default: 5 + paddingY: + description: Vertical padding of label + type: number + minimum: 0 + default: 5 + lineThickness: + description: Thickness of the line + type: number + minimum: 0 + default: 1 + RadarDiagramConfig: title: Radar Diagram Config allOf: [{ $ref: '#/$defs/BaseDiagramConfig' }] diff --git a/packages/mermaid/src/styles.spec.ts b/packages/mermaid/src/styles.spec.ts index fc38d7c4d55..5525c3e4e96 100644 --- a/packages/mermaid/src/styles.spec.ts +++ b/packages/mermaid/src/styles.spec.ts @@ -29,6 +29,7 @@ import timeline from './diagrams/timeline/styles.js'; import mindmap from './diagrams/mindmap/styles.js'; import packet from './diagrams/packet/styles.js'; import block from './diagrams/block/styles.js'; +import treeView from './diagrams/treeView/styles.js'; import radar from './diagrams/radar/styles.js'; import venn from './diagrams/venn/styles.js'; import themes from './themes/index.js'; @@ -101,6 +102,7 @@ describe('styles', () => { block, timeline, packet, + treeView, radar, venn, })) { diff --git a/packages/parser/langium-config.json b/packages/parser/langium-config.json index 85c722a02c0..391dc225400 100644 --- a/packages/parser/langium-config.json +++ b/packages/parser/langium-config.json @@ -16,6 +16,11 @@ "grammar": "src/language/pie/pie.langium", "fileExtensions": [".mmd", ".mermaid"] }, + { + "id": "treeView", + "grammar": "src/language/treeView/treeView.langium", + "fileExtensions": [".mmd", ".mermaid"] + }, { "id": "architecture", "grammar": "src/language/architecture/architecture.langium", diff --git a/packages/parser/src/language/index.ts b/packages/parser/src/language/index.ts index 5920a7010fa..b3a768b0b6a 100644 --- a/packages/parser/src/language/index.ts +++ b/packages/parser/src/language/index.ts @@ -13,6 +13,8 @@ export { Commit, Merge, Statement, + TreeView, + TreeNode, isInfo, isPacket, isPacketBlock, @@ -35,6 +37,7 @@ export { GitGraphGrammarGeneratedModule as GitGraphGeneratedModule, RadarGrammarGeneratedModule as RadarGeneratedModule, TreemapGrammarGeneratedModule as TreemapGeneratedModule, + TreeViewGrammarGeneratedModule as TreeViewGeneratedModule, } from './generated/module.js'; export * from './gitGraph/index.js'; @@ -42,6 +45,7 @@ export * from './common/index.js'; export * from './info/index.js'; export * from './packet/index.js'; export * from './pie/index.js'; +export * from './treeView/index.js'; export * from './architecture/index.js'; export * from './radar/index.js'; export * from './treemap/index.js'; diff --git a/packages/parser/src/language/treeView/index.ts b/packages/parser/src/language/treeView/index.ts new file mode 100644 index 00000000000..fd3c604b084 --- /dev/null +++ b/packages/parser/src/language/treeView/index.ts @@ -0,0 +1 @@ +export * from './module.js'; diff --git a/packages/parser/src/language/treeView/module.ts b/packages/parser/src/language/treeView/module.ts new file mode 100644 index 00000000000..13f7079b8ea --- /dev/null +++ b/packages/parser/src/language/treeView/module.ts @@ -0,0 +1,80 @@ +import type { + DefaultSharedCoreModuleContext, + LangiumCoreServices, + LangiumSharedCoreServices, + Module, + PartialLangiumCoreServices, +} from 'langium'; +import { + EmptyFileSystem, + createDefaultCoreModule, + createDefaultSharedCoreModule, + inject, +} from 'langium'; + +import { TreeViewValueConverter } from './valueConverter.js'; +import { + MermaidGeneratedSharedModule, + TreeViewGrammarGeneratedModule as TreeViewGeneratedModule, +} from '../generated/module.js'; +import { TreeViewTokenBuilder } from './tokenBuilder.js'; + +/** + * Declaration of `TreeView` services. + */ +interface TreeViewAddedServices { + parser: { + TokenBuilder: TreeViewTokenBuilder; + ValueConverter: TreeViewValueConverter; + }; +} + +/** + * Union of Langium default services and `TreeView` services. + */ +export type TreeViewServices = LangiumCoreServices & TreeViewAddedServices; + +/** + * Dependency injection module that overrides Langium default services and + * contributes the declared `TreeView` services. + */ +export const TreeViewModule: Module< + TreeViewServices, + PartialLangiumCoreServices & TreeViewAddedServices +> = { + parser: { + TokenBuilder: () => new TreeViewTokenBuilder(), + ValueConverter: () => new TreeViewValueConverter(), + }, +}; + +/** + * Create the full set of services required by Langium. + * + * First inject the shared services by merging two modules: + * - Langium default shared services + * - Services generated by langium-cli + * + * Then inject the language-specific services by merging three modules: + * - Langium default language-specific services + * - Services generated by langium-cli + * - Services specified in this file + * @param context - Optional module context with the LSP connection + * @returns An object wrapping the shared services and the language-specific services + */ +export function createTreeViewServices(context: DefaultSharedCoreModuleContext = EmptyFileSystem): { + shared: LangiumSharedCoreServices; + TreeView: TreeViewServices; +} { + const shared: LangiumSharedCoreServices = inject( + createDefaultSharedCoreModule(context), + MermaidGeneratedSharedModule + ); + const TreeView: TreeViewServices = inject( + createDefaultCoreModule({ shared }), + TreeViewGeneratedModule, + TreeViewModule + ); + shared.ServiceRegistry.register(TreeView); + return { shared, TreeView }; +} diff --git a/packages/parser/src/language/treeView/tokenBuilder.ts b/packages/parser/src/language/treeView/tokenBuilder.ts new file mode 100644 index 00000000000..5aacc5f903f --- /dev/null +++ b/packages/parser/src/language/treeView/tokenBuilder.ts @@ -0,0 +1,7 @@ +import { AbstractMermaidTokenBuilder } from '../common/index.js'; + +export class TreeViewTokenBuilder extends AbstractMermaidTokenBuilder { + public constructor() { + super(['treeView-beta']); + } +} diff --git a/packages/parser/src/language/treeView/treeView.langium b/packages/parser/src/language/treeView/treeView.langium new file mode 100644 index 00000000000..51443090fed --- /dev/null +++ b/packages/parser/src/language/treeView/treeView.langium @@ -0,0 +1,45 @@ +/** + * TreeView grammar for Langium + * Converted from treemap grammar + * + * The ML_COMMENT and NL hidden terminals handle whitespace, comments, and newlines + * before the treemap keyword, allowing for empty lines and comments before the + * treeView declaration. + */ +grammar TreeViewGrammar + +terminal ACC_DESCR: /[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/; +terminal ACC_TITLE: /[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/; +terminal TITLE: /[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/; + +interface TreeView { + nodes: TreeNode[] + title?: string + accTitle?: string + accDescr?: string +} + +entry TreeView returns TreeView: + "treeView-beta" + TitleAndAccessibilities? + nodes+=TreeNode* + ; + +fragment TitleAndAccessibilities: + ((accDescr=ACC_DESCR | accTitle=ACC_TITLE | title=TITLE))+ +; + +// This should be processed before whitespace is ignored +terminal INDENTATION: /[ \t]{1,}/; // One or more spaces/tabs for indentation + +hidden terminal WS: /[ \t]+/; // One or more spaces or tabs for hidden whitespace +hidden terminal ML_COMMENT: /\%\%[^\n]*/; +hidden terminal NL: /\r?\n/; + +TreeNode: + // actually contains the indent, but is converted to a length value by valueConverter + indent=INDENTATION? + name=STRING2 +; + +terminal STRING2: /"[^"]*"|'[^']*'/; diff --git a/packages/parser/src/language/treeView/valueConverter.ts b/packages/parser/src/language/treeView/valueConverter.ts new file mode 100644 index 00000000000..31fae101f6b --- /dev/null +++ b/packages/parser/src/language/treeView/valueConverter.ts @@ -0,0 +1,18 @@ +import type { CstNode, GrammarAST, ValueType } from 'langium'; +import { AbstractMermaidValueConverter } from '../common/index.js'; + +export class TreeViewValueConverter extends AbstractMermaidValueConverter { + protected runCustomConverter( + rule: GrammarAST.AbstractRule, + input: string, + _cstNode: CstNode + ): ValueType | undefined { + if (rule.name === 'INDENTATION') { + return input?.length || 0; + } else if (rule.name === 'STRING2') { + // Remove quotes + return input.substring(1, input.length - 1); + } + return undefined; + } +} diff --git a/packages/parser/src/parse.ts b/packages/parser/src/parse.ts index 27bf241ebd7..133a604fdef 100644 --- a/packages/parser/src/parse.ts +++ b/packages/parser/src/parse.ts @@ -1,8 +1,17 @@ import type { LangiumParser, ParseResult } from 'langium'; -import type { Info, Packet, Pie, Architecture, GitGraph, Radar, Treemap } from './index.js'; +import type { + Info, + Packet, + Pie, + Architecture, + GitGraph, + Radar, + Treemap, + TreeView, +} from './index.js'; -export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar; +export type DiagramAST = Info | Packet | Pie | Architecture | GitGraph | Radar | TreeView; const parsers: Record = {}; const initializers = { @@ -21,6 +30,11 @@ const initializers = { const parser = createPieServices().Pie.parser.LangiumParser; parsers.pie = parser; }, + treeView: async () => { + const { createTreeViewServices } = await import('./language/treeView/index.js'); + const parser = createTreeViewServices().TreeView.parser.LangiumParser; + parsers.treeView = parser; + }, architecture: async () => { const { createArchitectureServices } = await import('./language/architecture/index.js'); const parser = createArchitectureServices().Architecture.parser.LangiumParser; @@ -46,6 +60,7 @@ const initializers = { export async function parse(diagramType: 'info', text: string): Promise; export async function parse(diagramType: 'packet', text: string): Promise; export async function parse(diagramType: 'pie', text: string): Promise; +export async function parse(diagramType: 'treeView', text: string): Promise; export async function parse(diagramType: 'architecture', text: string): Promise; export async function parse(diagramType: 'gitGraph', text: string): Promise; export async function parse(diagramType: 'radar', text: string): Promise; diff --git a/packages/parser/tests/test-util.ts b/packages/parser/tests/test-util.ts index ee76271093e..d965814ed72 100644 --- a/packages/parser/tests/test-util.ts +++ b/packages/parser/tests/test-util.ts @@ -13,6 +13,8 @@ import type { PacketServices, GitGraph, GitGraphServices, + TreeView, + TreeViewServices, } from '../src/language/index.js'; import { createArchitectureServices, @@ -21,6 +23,7 @@ import { createRadarServices, createPacketServices, createGitGraphServices, + createTreeViewServices, } from '../src/language/index.js'; const consoleMock = vi.spyOn(console, 'log').mockImplementation(() => undefined); @@ -105,3 +108,14 @@ export function createGitGraphTestServices() { return { services: gitGraphServices, parse }; } export const gitGraphParse = createGitGraphTestServices().parse; + +const treeViewServices: TreeViewServices = createTreeViewServices().TreeView; +const treeViewParser: LangiumParser = treeViewServices.parser.LangiumParser; +export function createTreeViewTestServices() { + const parse = (input: string) => { + return treeViewParser.parse(input); + }; + + return { services: treeViewServices, parse }; +} +export const treeViewParse = createTreeViewTestServices().parse; diff --git a/packages/parser/tests/treeView.test.ts b/packages/parser/tests/treeView.test.ts new file mode 100644 index 00000000000..ce147e3f965 --- /dev/null +++ b/packages/parser/tests/treeView.test.ts @@ -0,0 +1,103 @@ +import { describe, expect, it } from 'vitest'; +import { expectNoErrorsOrAlternatives } from './test-util.js'; +import type { TreeView } from '../src/language/generated/ast.js'; +import type { LangiumParser } from 'langium'; +import { createTreeViewServices } from '../src/language/treeView/module.js'; + +describe('TreeView Parser', () => { + const services = createTreeViewServices().TreeView; + const parser: LangiumParser = services.parser.LangiumParser; + + const parse = (input: string) => { + return parser.parse(input); + }; + + describe('Basic Parsing', () => { + it('should parse empty treeView', () => { + const result = parse('treeView-beta'); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe('TreeView'); + expect(result.value.nodes).toHaveLength(0); + }); + + it('should parse a treeView with only a root node', () => { + const result = parse('treeView-beta\n"Root"'); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe('TreeView'); + expect(result.value.nodes).toHaveLength(1); + expect(result.value.nodes[0].name).toBe('Root'); + expect(result.value.nodes[0].indent).toBe(undefined); + }); + + it('should parse a treeView with multiple words within a node', () => { + const result = parse('treeView-beta\n"Multi Word Root"'); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe('TreeView'); + expect(result.value.nodes).toHaveLength(1); + expect(result.value.nodes[0].name).toBe('Multi Word Root'); + expect(result.value.nodes[0].indent).toBe(undefined); + }); + + it('should parse a treeView with child nodes', () => { + const result = parse(`treeView-beta\n"Root"\n "Child1"\n "Child2"\n "Child3"`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe('TreeView'); + expect(result.value.nodes).toHaveLength(4); + + expect(result.value.nodes[0].name).toBe('Root'); + expect(result.value.nodes[0].indent).toBe(undefined); + + expect(result.value.nodes[1].name).toBe('Child1'); + expect(result.value.nodes[1].indent).toBe(4); + + expect(result.value.nodes[2].name).toBe('Child2'); + expect(result.value.nodes[2].indent).toBe(4); + + expect(result.value.nodes[3].name).toBe('Child3'); + expect(result.value.nodes[3].indent).toBe(8); + }); + }); + + describe('Title and Accessibilities', () => { + it('should parse a treeView with title', () => { + const result = parse('treeView-beta\ntitle My TreeView Diagram\n"Root"\n "Child"'); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe('TreeView'); + expect(result.value.title).toBe('My TreeView Diagram'); + expect(result.value.nodes).toHaveLength(2); + }); + + it('should parse a treeView with accTitle', () => { + const result = parse('treeView-beta\naccTitle: Accessible Title\n"Root"\n "Child"'); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe('TreeView'); + expect(result.value.accTitle).toBe('Accessible Title'); + expect(result.value.nodes).toHaveLength(2); + }); + + it('should parse a treeView with accDescr', () => { + const result = parse( + 'treeView-beta\naccDescr: This is an accessible description\n"Root"\n "Child"' + ); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe('TreeView'); + expect(result.value.accDescr).toBe('This is an accessible description'); + expect(result.value.nodes).toHaveLength(2); + }); + + it('should parse a treeView with multiple accessibility attributes', () => { + const result = parse(`treeView-beta +title My TreeView Diagram +accTitle: Accessible Title +accDescr: This is an accessible description +"Root" + "Child"`); + expectNoErrorsOrAlternatives(result); + expect(result.value.$type).toBe('TreeView'); + expect(result.value.title).toBe('My TreeView Diagram'); + expect(result.value.accTitle).toBe('Accessible Title'); + expect(result.value.accDescr).toBe('This is an accessible description'); + expect(result.value.nodes).toHaveLength(2); + }); + }); +});