diff --git a/.changeset/add-treeview-box-drawing-input.md b/.changeset/add-treeview-box-drawing-input.md
new file mode 100644
index 00000000000..42a8f6d7cbf
--- /dev/null
+++ b/.changeset/add-treeview-box-drawing-input.md
@@ -0,0 +1,7 @@
+---
+'mermaid': minor
+---
+
+feat: add box-drawing character input support for treeView diagrams
+
+Adds an alternative input syntax for treeView-beta diagrams using box-drawing characters (├──, └──, │). The parser auto-detects box-drawing format and converts it to the standard indent-based representation before parsing. Error messages remap line numbers back to the original input. Includes 42 unit tests and 4 AST equivalence integration tests.
diff --git a/demos/treeView.html b/demos/treeView.html
index 054862c4e54..256fc6966d0 100644
--- a/demos/treeView.html
+++ b/demos/treeView.html
@@ -140,6 +140,44 @@
diff --git a/docs/syntax/treeView.md b/docs/syntax/treeView.md
index 5190d874bdd..8248064727e 100644
--- a/docs/syntax/treeView.md
+++ b/docs/syntax/treeView.md
@@ -36,6 +36,82 @@ treeView-beta
"file.js"
```
+## Box-Drawing Input
+
+As an alternative to indentation, you can use box-drawing characters to define the tree structure. The parser auto-detects the format — no extra keyword or config is needed. This is how most file tree diagrams are drawn already, so you can turn those into Mermaid diagrams with very little effort.
+
+Both standard (`├──`, `└──`, `│`) and heavy (`┣━━`, `┗━━`, `┃`) Unicode variants are supported.
+
+```mermaid-example
+treeView-beta
+├── src/
+│ ├── index.ts
+│ └── utils.ts
+├── package.json
+└── README.md
+```
+
+```mermaid
+treeView-beta
+├── src/
+│ ├── index.ts
+│ └── utils.ts
+├── package.json
+└── README.md
+```
+
+All annotations work the same way — just append them after the label:
+
+```mermaid-example
+treeView-beta
+├── src/
+│ ├── App.tsx :::highlight icon(react) ## main component
+│ └── index.ts ## entry point
+├── .env ## environment variables
+├── Dockerfile
+└── package.json
+```
+
+```mermaid
+treeView-beta
+├── src/
+│ ├── App.tsx :::highlight icon(react) ## main component
+│ └── index.ts ## entry point
+├── .env ## environment variables
+├── Dockerfile
+└── package.json
+```
+
+Depth is inferred from the column position of the branch character, so deeper nesting works naturally:
+
+```mermaid-example
+treeView-beta
+├── packages/
+│ ├── mermaid/
+│ │ ├── src/
+│ │ │ ├── parser.ts
+│ │ │ └── renderer.ts
+│ │ └── package.json
+│ └── parser/
+│ └── src/
+└── README.md
+```
+
+```mermaid
+treeView-beta
+├── packages/
+│ ├── mermaid/
+│ │ ├── src/
+│ │ │ ├── parser.ts
+│ │ │ └── renderer.ts
+│ │ └── package.json
+│ └── parser/
+│ └── src/
+└── README.md
+```
+
+> **Note:** If a parse error occurs, line numbers in the error message refer to your original input. Tab characters are automatically expanded to spaces.
+
## Annotations
### Highlighting with :::class
diff --git a/packages/mermaid/src/diagrams/treeView/boxDrawingPreprocessor.spec.ts b/packages/mermaid/src/diagrams/treeView/boxDrawingPreprocessor.spec.ts
new file mode 100644
index 00000000000..dd3ac754048
--- /dev/null
+++ b/packages/mermaid/src/diagrams/treeView/boxDrawingPreprocessor.spec.ts
@@ -0,0 +1,514 @@
+import { describe, expect, it } from 'vitest';
+import {
+ isBoxDrawingFormat,
+ preprocessBoxDrawing,
+ remapErrorLines,
+} from './boxDrawingPreprocessor.js';
+
+describe('boxDrawingPreprocessor', () => {
+ describe('isBoxDrawingFormat', () => {
+ it('should detect standard box-drawing characters', () => {
+ expect(isBoxDrawingFormat(['├── file.txt', '└── other.txt'])).toBe(true);
+ });
+
+ it('should detect heavy box-drawing characters', () => {
+ expect(isBoxDrawingFormat(['┣━━ file.txt', '┗━━ other.txt'])).toBe(true);
+ });
+
+ it('should detect vertical continuation character', () => {
+ expect(isBoxDrawingFormat(['│ └── file.txt'])).toBe(true);
+ });
+
+ it('should return false for indent-only lines', () => {
+ expect(isBoxDrawingFormat([' file.txt', ' nested.txt'])).toBe(false);
+ });
+
+ it('should return false for empty lines', () => {
+ expect(isBoxDrawingFormat(['', ' '])).toBe(false);
+ });
+
+ it('should return false for plain text', () => {
+ expect(isBoxDrawingFormat(['root/', 'file.txt'])).toBe(false);
+ });
+ });
+
+ describe('preprocessBoxDrawing', () => {
+ describe('indent format passthrough', () => {
+ it('should return indent-based input unchanged', () => {
+ const input = 'treeView-beta\nroot/\n src/\n index.js';
+ const result = preprocessBoxDrawing(input);
+ expect(result.text).toBe(input);
+ expect(result.lineMap.size).toBe(0);
+ });
+
+ it('should return input without keyword unchanged', () => {
+ const input = 'not-a-keyword\nsome text';
+ const result = preprocessBoxDrawing(input);
+ expect(result.text).toBe(input);
+ expect(result.lineMap.size).toBe(0);
+ });
+ });
+
+ describe('standard tree output', () => {
+ it('should convert a simple tree', () => {
+ const input = [
+ 'treeView-beta',
+ 'root/',
+ '├── src/',
+ '│ └── index.js',
+ '└── README.md',
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[0]).toBe('treeView-beta');
+ expect(lines[1]).toBe('root/');
+ expect(lines[2]).toBe(' src/');
+ expect(lines[3]).toBe(' index.js');
+ expect(lines[4]).toBe(' README.md');
+ });
+
+ it('should handle a deeply nested tree', () => {
+ const input = [
+ 'treeView-beta',
+ 'root/',
+ '├── a/',
+ '│ ├── b/',
+ '│ │ ├── c/',
+ '│ │ │ └── deep.txt',
+ '│ │ └── d.txt',
+ '│ └── e.txt',
+ '└── f.txt',
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[0]).toBe('treeView-beta');
+ expect(lines[1]).toBe('root/');
+ expect(lines[2]).toBe(' a/');
+ expect(lines[3]).toBe(' b/');
+ expect(lines[4]).toBe(' c/');
+ expect(lines[5]).toBe(' deep.txt');
+ expect(lines[6]).toBe(' d.txt');
+ expect(lines[7]).toBe(' e.txt');
+ expect(lines[8]).toBe(' f.txt');
+ });
+
+ it('should handle a flat list (all at depth 1)', () => {
+ const input = [
+ 'treeView-beta',
+ 'root/',
+ '├── file1.txt',
+ '├── file2.txt',
+ '└── file3.txt',
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[1]).toBe('root/');
+ expect(lines[2]).toBe(' file1.txt');
+ expect(lines[3]).toBe(' file2.txt');
+ expect(lines[4]).toBe(' file3.txt');
+ });
+
+ it('should handle no root line (all box-drawing)', () => {
+ const input = ['treeView-beta', '├── file1.txt', '└── file2.txt'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[0]).toBe('treeView-beta');
+ expect(lines[1]).toBe(' file1.txt');
+ expect(lines[2]).toBe(' file2.txt');
+ });
+ });
+
+ describe('space continuation after └── (last child)', () => {
+ it('should handle space continuation correctly', () => {
+ const input = [
+ 'treeView-beta',
+ 'root/',
+ '├── a/',
+ '│ └── b.txt',
+ '└── c/',
+ ' └── d.txt',
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ // ' └── d.txt' → └ at col 4, segmentWidth=4, depth = 4/4 + 1 = 2
+ expect(lines[1]).toBe('root/');
+ expect(lines[2]).toBe(' a/');
+ expect(lines[3]).toBe(' b.txt');
+ expect(lines[4]).toBe(' c/');
+ expect(lines[5]).toBe(' d.txt');
+ });
+ });
+
+ describe('flexible prefix widths', () => {
+ it('should handle ├─── (extra dash)', () => {
+ const input = ['treeView-beta', 'root/', '├─── file.txt'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[2]).toBe(' file.txt');
+ });
+
+ it('should handle ├── with no trailing space', () => {
+ const input = ['treeView-beta', 'root/', '├──file.txt'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[2]).toBe(' file.txt');
+ });
+
+ it('should handle ├───── (many dashes)', () => {
+ const input = ['treeView-beta', 'root/', '├───── file.txt'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[2]).toBe(' file.txt');
+ });
+
+ it('should handle └─ (minimal dash)', () => {
+ const input = ['treeView-beta', 'root/', '└─ file.txt'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[2]).toBe(' file.txt');
+ });
+ });
+
+ describe('heavy Unicode variants', () => {
+ it('should handle heavy box-drawing characters', () => {
+ const input = [
+ 'treeView-beta',
+ 'root/',
+ '┣━━ src/',
+ '┃ ┗━━ index.js',
+ '┗━━ README.md',
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[0]).toBe('treeView-beta');
+ expect(lines[1]).toBe('root/');
+ expect(lines[2]).toBe(' src/');
+ expect(lines[3]).toBe(' index.js');
+ expect(lines[4]).toBe(' README.md');
+ });
+
+ it('should handle heavy variants with extra dashes', () => {
+ const input = ['treeView-beta', 'root/', '┣━━━ file.txt'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[2]).toBe(' file.txt');
+ });
+ });
+
+ describe('annotations', () => {
+ it('should preserve :::class annotation', () => {
+ const input = ['treeView-beta', '├── file.txt :::highlight'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[1]).toBe(' file.txt :::highlight');
+ });
+
+ it('should preserve icon() annotation', () => {
+ const input = ['treeView-beta', '├── data.bin icon(database)'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[1]).toBe(' data.bin icon(database)');
+ });
+
+ it('should preserve ## description annotation', () => {
+ const input = ['treeView-beta', '├── index.js ## entry point'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[1]).toBe(' index.js ## entry point');
+ });
+
+ it('should preserve all annotations combined', () => {
+ const input = [
+ 'treeView-beta',
+ '├── App.tsx :::highlight icon(react) ## main component',
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[1]).toBe(' App.tsx :::highlight icon(react) ## main component');
+ });
+ });
+
+ describe('comments and blank lines', () => {
+ it('should preserve %% comments', () => {
+ const input = [
+ 'treeView-beta',
+ '%% a comment',
+ '├── file.txt',
+ '%% another comment',
+ '└── other.txt',
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[1]).toBe('%% a comment');
+ expect(lines[2]).toBe(' file.txt');
+ expect(lines[3]).toBe('%% another comment');
+ expect(lines[4]).toBe(' other.txt');
+ });
+
+ it('should preserve blank lines', () => {
+ const input = ['treeView-beta', '', '├── file.txt', '', '└── other.txt'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[1]).toBe('');
+ expect(lines[2]).toBe(' file.txt');
+ expect(lines[3]).toBe('');
+ expect(lines[4]).toBe(' other.txt');
+ });
+ });
+
+ describe('metadata lines', () => {
+ it('should pass through title line', () => {
+ const input = ['treeView-beta', 'title My Tree', '├── file.txt', '└── other.txt'].join(
+ '\n'
+ );
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[1]).toBe('title My Tree');
+ expect(lines[2]).toBe(' file.txt');
+ });
+
+ it('should pass through accTitle line', () => {
+ const input = ['treeView-beta', 'accTitle: Accessible Title', '├── file.txt'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[1]).toBe('accTitle: Accessible Title');
+ expect(lines[2]).toBe(' file.txt');
+ });
+
+ it('should pass through accDescr line', () => {
+ const input = ['treeView-beta', 'accDescr: A description', '├── file.txt'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[1]).toBe('accDescr: A description');
+ expect(lines[2]).toBe(' file.txt');
+ });
+ });
+
+ describe('decoration-only lines', () => {
+ it('should skip lines with only │ characters', () => {
+ const input = ['treeView-beta', 'root/', '├── a.txt', '│', '└── b.txt'].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ // The │-only line should be skipped
+ expect(lines).toHaveLength(4);
+ expect(lines[0]).toBe('treeView-beta');
+ expect(lines[1]).toBe('root/');
+ expect(lines[2]).toBe(' a.txt');
+ expect(lines[3]).toBe(' b.txt');
+ });
+ });
+
+ describe('line mapping', () => {
+ it('should map output lines to original line numbers', () => {
+ const input = [
+ 'treeView-beta', // line 1
+ 'root/', // line 2
+ '├── src/', // line 3
+ '│ └── index.js', // line 4
+ '└── README.md', // line 5
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+
+ expect(result.lineMap.get(1)).toBe(1); // keyword
+ expect(result.lineMap.get(2)).toBe(2); // root/
+ expect(result.lineMap.get(3)).toBe(3); // src/
+ expect(result.lineMap.get(4)).toBe(4); // index.js
+ expect(result.lineMap.get(5)).toBe(5); // README.md
+ });
+
+ it('should handle skipped decoration lines in mapping', () => {
+ const input = [
+ 'treeView-beta', // line 1
+ '├── a.txt', // line 2
+ '│', // line 3 (decoration, skipped)
+ '└── b.txt', // line 4
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+
+ expect(result.lineMap.get(1)).toBe(1); // keyword
+ expect(result.lineMap.get(2)).toBe(2); // a.txt
+ expect(result.lineMap.get(3)).toBe(4); // b.txt (line 3 in output → line 4 in original)
+ });
+ });
+
+ describe('error cases', () => {
+ it('should throw on empty content after prefix', () => {
+ const input = ['treeView-beta', '├── '].join('\n');
+
+ expect(() => preprocessBoxDrawing(input)).toThrow(
+ 'Line 2: Empty node — expected a filename or directory name after the box-drawing prefix'
+ );
+ });
+
+ it('should normalize tabs to spaces', () => {
+ const input = ['treeView-beta', '├──\tfile.txt'].join('\n');
+ const { text } = preprocessBoxDrawing(input);
+ const lines = text.split('\n');
+ expect(lines[1]).toBe(' file.txt');
+ });
+
+ it('should handle tab-indented box-drawing lines', () => {
+ // Tab expands to 4 spaces, so `\t├──` puts the branch at column 4 → depth 2
+ const input = ['treeView-beta', '├── src/', '\t├── index.ts', '\t└── utils.ts'].join('\n');
+ const { text } = preprocessBoxDrawing(input);
+ const lines = text.split('\n');
+ expect(lines[1]).toBe(' src/');
+ // depth 2 = 8 spaces of indent
+ expect(lines[2]).toBe(' index.ts');
+ expect(lines[3]).toBe(' utils.ts');
+ });
+
+ it('should throw on indented line without box chars in box mode', () => {
+ const input = [
+ 'treeView-beta',
+ '├── src/',
+ ' index.js', // indent without box chars
+ '└── README.md',
+ ].join('\n');
+
+ expect(() => preprocessBoxDrawing(input)).toThrow(
+ 'Line 3: Unexpected indentation without box-drawing characters'
+ );
+ });
+
+ it('should include line number in empty-content error', () => {
+ const input = ['treeView-beta', 'root/', '├── src/', '│ └── '].join('\n');
+
+ expect(() => preprocessBoxDrawing(input)).toThrow('Line 4:');
+ });
+ });
+
+ describe('real-world tree command output', () => {
+ it('should handle typical Linux tree output', () => {
+ const input = [
+ 'treeView-beta',
+ 'my-project/',
+ '├── src/',
+ '│ ├── components/',
+ '│ │ └── MyComponent.js',
+ '│ ├── index.css',
+ '│ └── index.js',
+ '├── package.json',
+ '└── README.md',
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[0]).toBe('treeView-beta');
+ expect(lines[1]).toBe('my-project/');
+ expect(lines[2]).toBe(' src/');
+ expect(lines[3]).toBe(' components/');
+ expect(lines[4]).toBe(' MyComponent.js');
+ expect(lines[5]).toBe(' index.css');
+ expect(lines[6]).toBe(' index.js');
+ expect(lines[7]).toBe(' package.json');
+ expect(lines[8]).toBe(' README.md');
+ });
+
+ it('should handle tree output with annotations', () => {
+ const input = [
+ 'treeView-beta',
+ 'my-project/',
+ '├── src/ :::highlight',
+ '│ ├── App.tsx :::highlight icon(react) ## main component',
+ '│ └── index.ts ## entry point',
+ '├── package.json',
+ '└── README.md ## project docs',
+ ].join('\n');
+
+ const result = preprocessBoxDrawing(input);
+ const lines = result.text.split('\n');
+
+ expect(lines[2]).toBe(' src/ :::highlight');
+ expect(lines[3]).toBe(' App.tsx :::highlight icon(react) ## main component');
+ expect(lines[4]).toBe(' index.ts ## entry point');
+ expect(lines[5]).toBe(' package.json');
+ expect(lines[6]).toBe(' README.md ## project docs');
+ });
+ });
+ });
+
+ describe('remapErrorLines', () => {
+ it('should remap line numbers found in the map', () => {
+ const lineMap = new Map
([
+ [1, 1],
+ [2, 3],
+ [3, 5],
+ ]);
+
+ expect(remapErrorLines('Error at line 2: bad syntax', lineMap)).toBe(
+ 'Error at line 3: bad syntax'
+ );
+ });
+
+ it('should leave unmapped line numbers unchanged', () => {
+ const lineMap = new Map([[1, 1]]);
+
+ expect(remapErrorLines('Error at line 99: bad syntax', lineMap)).toBe(
+ 'Error at line 99: bad syntax'
+ );
+ });
+
+ it('should handle multiple line references', () => {
+ const lineMap = new Map([
+ [2, 4],
+ [5, 10],
+ ]);
+
+ expect(remapErrorLines('Errors at line 2 and line 5', lineMap)).toBe(
+ 'Errors at line 4 and line 10'
+ );
+ });
+
+ it('should be case-insensitive', () => {
+ const lineMap = new Map([[3, 7]]);
+
+ expect(remapErrorLines('Error at Line 3: bad', lineMap)).toBe('Error at line 7: bad');
+ });
+ });
+});
diff --git a/packages/mermaid/src/diagrams/treeView/boxDrawingPreprocessor.ts b/packages/mermaid/src/diagrams/treeView/boxDrawingPreprocessor.ts
new file mode 100644
index 00000000000..7b091baecdb
--- /dev/null
+++ b/packages/mermaid/src/diagrams/treeView/boxDrawingPreprocessor.ts
@@ -0,0 +1,209 @@
+/**
+ * Box-drawing pre-processor for treeView diagrams.
+ *
+ * Converts box-drawing character input (├──, └──, │) to indent-based input
+ * before Langium parsing. Supports both standard and heavy Unicode variants.
+ */
+
+// Character class regexes
+const ALL_BOX_CHARS = /[─━│┃└┗├┣]/;
+const BRANCH_CHAR = /[└┗├┣]/;
+const DASH_CHAR = /[─━]/;
+const DECORATION_ONLY = /^[\s│┃]+$/;
+const METADATA_LINE = /^\s*(title[\t ]|accTitle[\t ]*:|accDescr[\t ]*[:{])/;
+const COMMENT_LINE = /^\s*%%/;
+
+const INDENT_UNIT = ' '; // 4 spaces per depth level in output
+
+export interface PreprocessResult {
+ /** The (possibly transformed) input text */
+ text: string;
+ /** Maps output line numbers (1-based) → original line numbers (1-based). Empty if no transformation. */
+ lineMap: Map;
+}
+
+/**
+ * Detects whether any of the given lines contain box-drawing characters.
+ */
+export function isBoxDrawingFormat(lines: string[]): boolean {
+ return lines.some((line) => ALL_BOX_CHARS.test(line));
+}
+
+/**
+ * Infers the segment width (chars per depth level) by finding the first
+ * branch character (├/└/┣/┗) at a column position \> 0.
+ * Falls back to 4 if all branches are at column 0 or none exist.
+ */
+function inferSegmentWidth(contentLines: string[]): number {
+ for (const line of contentLines) {
+ const match = BRANCH_CHAR.exec(line);
+ if (match?.index && match.index > 0) {
+ return match.index;
+ }
+ }
+ return 4;
+}
+
+/**
+ * Remaps line numbers in an error message from output line numbers to original line numbers.
+ */
+export function remapErrorLines(message: string, lineMap: Map): string {
+ return message.replace(/\bline\s+(\d+)\b/gi, (match, lineStr: string) => {
+ const line = parseInt(lineStr, 10);
+ const original = lineMap.get(line);
+ return original ? `line ${original}` : match;
+ });
+}
+
+/**
+ * Pre-processes box-drawing formatted treeView input into indent-based format.
+ *
+ * If the input uses box-drawing characters (├── └── │ or heavy variants ┣━━ ┗━━ ┃),
+ * it is converted to indentation-based format that Langium can parse directly.
+ * If the input is already indent-based, it is returned unchanged.
+ *
+ * @returns The transformed text and a line mapping for error remapping.
+ */
+export function preprocessBoxDrawing(input: string): PreprocessResult {
+ const lines = input.split('\n');
+ const lineMap = new Map();
+
+ // Find keyword line
+ let keywordIdx = -1;
+ for (const [i, line] of lines.entries()) {
+ if (line.trim() === 'treeView-beta') {
+ keywordIdx = i;
+ break;
+ }
+ }
+
+ if (keywordIdx === -1) {
+ // No keyword found — return as-is (let Langium handle the error)
+ return { text: input, lineMap };
+ }
+
+ // Collect content line texts for format detection (skip blanks, comments, metadata, decoration)
+ const contentLineTexts: string[] = [];
+ for (let i = keywordIdx + 1; i < lines.length; i++) {
+ const line = lines[i];
+ const trimmed = line.trim();
+ if (trimmed === '' || COMMENT_LINE.test(line) || METADATA_LINE.test(line)) {
+ continue;
+ }
+ if (DECORATION_ONLY.test(line)) {
+ continue;
+ }
+ // Normalize tabs early so segment-width inference uses consistent column positions
+ contentLineTexts.push(line.replace(/\t/g, ' '));
+ }
+
+ // If no box-drawing characters found → return unchanged
+ if (!isBoxDrawingFormat(contentLineTexts)) {
+ return { text: input, lineMap };
+ }
+
+ // Infer segment width
+ const segmentWidth = inferSegmentWidth(contentLineTexts);
+
+ // Build output
+ const outputLines: string[] = [];
+ let outLineNo = 0;
+
+ // Pass through all lines up to and including keyword
+ for (let i = 0; i <= keywordIdx; i++) {
+ outputLines.push(lines[i]);
+ outLineNo++;
+ lineMap.set(outLineNo, i + 1);
+ }
+
+ // Process lines after keyword
+ for (let i = keywordIdx + 1; i < lines.length; i++) {
+ const line = lines[i];
+ const trimmed = line.trim();
+ const origLineNo = i + 1;
+
+ // Blank lines → pass through
+ if (trimmed === '') {
+ outputLines.push(line);
+ outLineNo++;
+ lineMap.set(outLineNo, origLineNo);
+ continue;
+ }
+
+ // Comments → pass through
+ if (COMMENT_LINE.test(line)) {
+ outputLines.push(line);
+ outLineNo++;
+ lineMap.set(outLineNo, origLineNo);
+ continue;
+ }
+
+ // Metadata (title, accTitle, accDescr) → pass through
+ if (METADATA_LINE.test(line)) {
+ outputLines.push(line);
+ outLineNo++;
+ lineMap.set(outLineNo, origLineNo);
+ continue;
+ }
+
+ // Decoration-only lines (│ + whitespace, no actual content) → skip
+ if (DECORATION_ONLY.test(line)) {
+ continue;
+ }
+
+ // Normalize tabs to spaces for consistent column-position math
+ const normalized = line.replace(/\t/g, ' ');
+
+ // Find branch character (├, └, ┣, ┗)
+ const branchMatch = BRANCH_CHAR.exec(normalized);
+
+ if (branchMatch?.index !== undefined) {
+ // Has branch char → compute depth from column position
+ const branchCol = branchMatch.index;
+ const depth = Math.round(branchCol / segmentWidth) + 1;
+
+ // Extract content: skip branch char, then dashes, then spaces
+ let pos = branchCol + 1;
+ while (pos < normalized.length && DASH_CHAR.test(normalized[pos])) {
+ pos++;
+ }
+ while (pos < normalized.length && normalized[pos] === ' ') {
+ pos++;
+ }
+ const content = normalized.slice(pos).trimEnd();
+
+ if (!content) {
+ throw new Error(
+ `Line ${origLineNo}: Empty node — expected a filename or directory name after the box-drawing prefix`
+ );
+ }
+
+ const indent = INDENT_UNIT.repeat(depth);
+ outputLines.push(indent + content);
+ outLineNo++;
+ lineMap.set(outLineNo, origLineNo);
+ } else if (/^[\s─━│┃└┗├┣]+$/.test(normalized)) {
+ // Entire line is box-drawing decoration and whitespace — skip
+ continue;
+ } else if (ALL_BOX_CHARS.test(normalized)) {
+ // Has box chars but no branch char — likely content containing a box char (e.g. "Section ─ A.txt")
+ // Treat as root-level item
+ outputLines.push(line);
+ outLineNo++;
+ lineMap.set(outLineNo, origLineNo);
+ } else if (/^\s+/.test(normalized)) {
+ // Leading whitespace without box chars in box mode → likely mixed format
+ throw new Error(
+ `Line ${origLineNo}: Unexpected indentation without box-drawing characters. ` +
+ `In box-drawing format, use ├── or └── prefixes for indented nodes.`
+ );
+ } else {
+ // No box chars, no leading whitespace → root-level item (depth 0)
+ outputLines.push(line);
+ outLineNo++;
+ lineMap.set(outLineNo, origLineNo);
+ }
+ }
+
+ return { text: outputLines.join('\n'), lineMap };
+}
diff --git a/packages/mermaid/src/diagrams/treeView/parser.spec.ts b/packages/mermaid/src/diagrams/treeView/parser.spec.ts
index 7e487c4febb..61e613d171a 100644
--- a/packages/mermaid/src/diagrams/treeView/parser.spec.ts
+++ b/packages/mermaid/src/diagrams/treeView/parser.spec.ts
@@ -1,6 +1,6 @@
-import { describe, expect, it, beforeEach } from 'vitest';
-import { parser } from './parser.js';
+import { beforeEach, describe, expect, it } from 'vitest';
import db from './db.js';
+import { parser } from './parser.js';
/**
* Integration tests for the treeView parser pipeline.
@@ -229,3 +229,150 @@ describe('treeView parser integration', () => {
});
});
});
+
+/**
+ * AST equivalence tests: box-drawing input should produce the same tree
+ * as equivalent indent-based input.
+ */
+
+interface NodeSnapshot {
+ name: string;
+ nodeType: string;
+ level: number;
+ cssClass?: string;
+ iconId?: string;
+ description?: string;
+ children: NodeSnapshot[];
+}
+
+function collectNodes(node: {
+ name: string;
+ nodeType: string;
+ level: number;
+ cssClass?: string;
+ iconId?: string;
+ description?: string;
+ children: (typeof node)[];
+}): NodeSnapshot {
+ return {
+ name: node.name,
+ nodeType: node.nodeType,
+ level: node.level,
+ ...(node.cssClass ? { cssClass: node.cssClass } : {}),
+ ...(node.iconId ? { iconId: node.iconId } : {}),
+ ...(node.description ? { description: node.description } : {}),
+ children: node.children.map(collectNodes),
+ };
+}
+
+describe('box-drawing ↔ indent equivalence', () => {
+ beforeEach(() => {
+ db.clear();
+ });
+
+ it('flat list', async () => {
+ const indent = `treeView-beta
+ src/
+ index.js
+ App.tsx
+ package.json`;
+
+ const boxDraw = `treeView-beta
+├── src/
+│ ├── index.js
+│ └── App.tsx
+└── package.json`;
+
+ db.clear();
+ await parser.parse(indent);
+ const indentTree = collectNodes(db.getRoot());
+
+ db.clear();
+ await parser.parse(boxDraw);
+ const boxTree = collectNodes(db.getRoot());
+
+ expect(boxTree).toEqual(indentTree);
+ });
+
+ it('nested directories', async () => {
+ const indent = `treeView-beta
+ src/
+ components/
+ Button.tsx
+ Modal.tsx
+ utils/
+ helpers.ts
+ README.md`;
+
+ const boxDraw = `treeView-beta
+├── src/
+│ ├── components/
+│ │ ├── Button.tsx
+│ │ └── Modal.tsx
+│ └── utils/
+│ └── helpers.ts
+└── README.md`;
+
+ db.clear();
+ await parser.parse(indent);
+ const indentTree = collectNodes(db.getRoot());
+
+ db.clear();
+ await parser.parse(boxDraw);
+ const boxTree = collectNodes(db.getRoot());
+
+ expect(boxTree).toEqual(indentTree);
+ });
+
+ it('with annotations', async () => {
+ const indent = `treeView-beta
+ src/
+ App.tsx :::highlight ## main component
+ index.js ## entry point
+ package.json`;
+
+ const boxDraw = `treeView-beta
+├── src/
+│ ├── App.tsx :::highlight ## main component
+│ └── index.js ## entry point
+└── package.json`;
+
+ db.clear();
+ await parser.parse(indent);
+ const indentTree = collectNodes(db.getRoot());
+
+ db.clear();
+ await parser.parse(boxDraw);
+ const boxTree = collectNodes(db.getRoot());
+
+ expect(boxTree).toEqual(indentTree);
+ });
+
+ it('deeply nested (4 levels)', async () => {
+ const indent = `treeView-beta
+ a/
+ b/
+ c/
+ d.txt
+ e.txt
+ f.txt`;
+
+ const boxDraw = `treeView-beta
+└── a/
+ ├── b/
+ │ ├── c/
+ │ │ └── d.txt
+ │ └── e.txt
+ └── f.txt`;
+
+ db.clear();
+ await parser.parse(indent);
+ const indentTree = collectNodes(db.getRoot());
+
+ db.clear();
+ await parser.parse(boxDraw);
+ const boxTree = collectNodes(db.getRoot());
+
+ expect(boxTree).toEqual(indentTree);
+ });
+});
diff --git a/packages/mermaid/src/diagrams/treeView/parser.ts b/packages/mermaid/src/diagrams/treeView/parser.ts
index 0a0e6adce33..6f7a6d2cf80 100644
--- a/packages/mermaid/src/diagrams/treeView/parser.ts
+++ b/packages/mermaid/src/diagrams/treeView/parser.ts
@@ -4,6 +4,7 @@ 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 { preprocessBoxDrawing, remapErrorLines } from './boxDrawingPreprocessor.js';
import db from './db.js';
import { resolveIcon } from './icons.js';
import type { NodeType } from './types.js';
@@ -46,8 +47,16 @@ const populate = (ast: TreeView) => {
export const parser: ParserDefinition = {
parse: async (input: string): Promise => {
- const ast = await parse('treeView', input);
- log.debug(ast);
- populate(ast);
+ const { text, lineMap } = preprocessBoxDrawing(input);
+ try {
+ const ast = await parse('treeView', text);
+ log.debug(ast);
+ populate(ast);
+ } catch (error) {
+ if (lineMap.size > 0 && error instanceof Error) {
+ error.message = remapErrorLines(error.message, lineMap);
+ }
+ throw error;
+ }
},
};
diff --git a/packages/mermaid/src/docs/syntax/treeView.md b/packages/mermaid/src/docs/syntax/treeView.md
index 5ec55b1415f..6218c6d16ff 100644
--- a/packages/mermaid/src/docs/syntax/treeView.md
+++ b/packages/mermaid/src/docs/syntax/treeView.md
@@ -30,6 +30,50 @@ treeView-beta
"file.js"
```
+## Box-Drawing Input
+
+As an alternative to indentation, you can use box-drawing characters to define the tree structure. The parser auto-detects the format — no extra keyword or config is needed. This is how most file tree diagrams are drawn already, so you can turn those into Mermaid diagrams with very little effort.
+
+Both standard (`├──`, `└──`, `│`) and heavy (`┣━━`, `┗━━`, `┃`) Unicode variants are supported.
+
+```mermaid-example
+treeView-beta
+├── src/
+│ ├── index.ts
+│ └── utils.ts
+├── package.json
+└── README.md
+```
+
+All annotations work the same way — just append them after the label:
+
+```mermaid-example
+treeView-beta
+├── src/
+│ ├── App.tsx :::highlight icon(react) ## main component
+│ └── index.ts ## entry point
+├── .env ## environment variables
+├── Dockerfile
+└── package.json
+```
+
+Depth is inferred from the column position of the branch character, so deeper nesting works naturally:
+
+```mermaid-example
+treeView-beta
+├── packages/
+│ ├── mermaid/
+│ │ ├── src/
+│ │ │ ├── parser.ts
+│ │ │ └── renderer.ts
+│ │ └── package.json
+│ └── parser/
+│ └── src/
+└── README.md
+```
+
+> **Note:** If a parse error occurs, line numbers in the error message refer to your original input. Tab characters are automatically expanded to spaces.
+
## Annotations
### Highlighting with :::class