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 @@

Folder Names with Spaces

+

Box-Drawing Input

+
+

+      
+
+treeView-beta
+├── src/
+│   ├── components/
+│   │   ├── Button.tsx
+│   │   └── Modal.tsx
+│   ├── utils/
+│   │   └── helpers.ts
+│   └── index.js
+├── docs/
+│   └── README.md
+├── package.json
+└── .gitignore
+        
+
+
+ +

Box-Drawing with Annotations

+
+

+      
+
+treeView-beta
+├── src/
+│   ├── App.tsx :::highlight ## main component
+│   ├── index.js ## entry point
+│   └── config.ts
+├── docs/
+│   └── guide.md ## user guide
+└── package.json
+        
+
+
+

Custom Config


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