mermaid-ast 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -131,6 +131,55 @@ detectDiagramType("classDiagram\n class Animal"); // "class"
131
131
  detectDiagramType("unknown diagram"); // null
132
132
  ```
133
133
 
134
+ ### Render Options (Pretty-Print)
135
+
136
+ All render functions accept an optional `RenderOptions` object to customize output formatting:
137
+
138
+ ```typescript
139
+ import { renderFlowchart, parseFlowchart } from "mermaid-ast";
140
+ import type { RenderOptions } from "mermaid-ast";
141
+
142
+ const ast = parseFlowchart(`flowchart LR
143
+ A[Start] --> B[Middle] --> C[End]
144
+ classDef highlight fill:#f9f
145
+ class A highlight`);
146
+
147
+ // Default output (4-space indent)
148
+ renderFlowchart(ast);
149
+
150
+ // Custom indent (2 spaces)
151
+ renderFlowchart(ast, { indent: 2 });
152
+
153
+ // Tab indent
154
+ renderFlowchart(ast, { indent: "tab" });
155
+
156
+ // Sort nodes alphabetically
157
+ renderFlowchart(ast, { sortNodes: true });
158
+
159
+ // Flowchart-specific: inline classes (A:::highlight instead of separate class statement)
160
+ renderFlowchart(ast, { inlineClasses: true });
161
+
162
+ // Flowchart-specific: chain links (A --> B --> C on one line)
163
+ renderFlowchart(ast, { compactLinks: true });
164
+
165
+ // Combine options
166
+ renderFlowchart(ast, {
167
+ indent: 2,
168
+ sortNodes: true,
169
+ inlineClasses: true,
170
+ compactLinks: true,
171
+ });
172
+ ```
173
+
174
+ #### Available Options
175
+
176
+ | Option | Type | Default | Description |
177
+ |--------|------|---------|-------------|
178
+ | `indent` | `number \| "tab"` | `4` | Number of spaces for indentation, or `"tab"` for tabs |
179
+ | `sortNodes` | `boolean` | `false` | Sort node/actor/class declarations alphabetically |
180
+ | `inlineClasses` | `boolean` | `false` | (Flowchart only) Use `A:::className` instead of separate `class` statements |
181
+ | `compactLinks` | `boolean` | `false` | (Flowchart only) Chain consecutive links: `A --> B --> C` |
182
+
134
183
  ## API Reference
135
184
 
136
185
  ### Core Functions
@@ -138,7 +187,7 @@ detectDiagramType("unknown diagram"); // null
138
187
  | Function | Description |
139
188
  |----------|-------------|
140
189
  | `parse(input: string): MermaidAST` | Parse any supported diagram |
141
- | `render(ast: MermaidAST): string` | Render any supported AST |
190
+ | `render(ast: MermaidAST, options?: RenderOptions): string` | Render any supported AST |
142
191
  | `detectDiagramType(input: string): DiagramType \| null` | Detect diagram type |
143
192
 
144
193
  ### Flowchart Functions
@@ -146,7 +195,7 @@ detectDiagramType("unknown diagram"); // null
146
195
  | Function | Description |
147
196
  |----------|-------------|
148
197
  | `parseFlowchart(input: string): FlowchartAST` | Parse flowchart diagram |
149
- | `renderFlowchart(ast: FlowchartAST): string` | Render flowchart AST |
198
+ | `renderFlowchart(ast: FlowchartAST, options?: RenderOptions): string` | Render flowchart AST |
150
199
  | `isFlowchartDiagram(input: string): boolean` | Check if input is flowchart |
151
200
 
152
201
  ### Sequence Diagram Functions
@@ -154,7 +203,7 @@ detectDiagramType("unknown diagram"); // null
154
203
  | Function | Description |
155
204
  |----------|-------------|
156
205
  | `parseSequence(input: string): SequenceAST` | Parse sequence diagram |
157
- | `renderSequence(ast: SequenceAST): string` | Render sequence AST |
206
+ | `renderSequence(ast: SequenceAST, options?: RenderOptions): string` | Render sequence AST |
158
207
  | `isSequenceDiagram(input: string): boolean` | Check if input is sequence |
159
208
 
160
209
  ### Class Diagram Functions
@@ -162,7 +211,7 @@ detectDiagramType("unknown diagram"); // null
162
211
  | Function | Description |
163
212
  |----------|-------------|
164
213
  | `parseClassDiagram(input: string): ClassDiagramAST` | Parse class diagram |
165
- | `renderClassDiagram(ast: ClassDiagramAST): string` | Render class diagram AST |
214
+ | `renderClassDiagram(ast: ClassDiagramAST, options?: RenderOptions): string` | Render class diagram AST |
166
215
  | `isClassDiagram(input: string): boolean` | Check if input is class diagram |
167
216
 
168
217
  ## Supported Flowchart Features
@@ -186,6 +235,25 @@ detectDiagramType("unknown diagram"); // null
186
235
  - **Notes**: `note left of`, `note right of`, `note over`
187
236
  - **Actor lifecycle**: `create`, `destroy`
188
237
 
238
+ ## Limitations
239
+
240
+ ### Comments Not Preserved
241
+
242
+ Mermaid supports `%%` line comments, but **comments are not preserved** during parsing. The JISON parsers discard comments during lexing, so they are not included in the AST and will not appear in rendered output.
243
+
244
+ ```mermaid
245
+ flowchart LR
246
+ %% This comment will be lost
247
+ A --> B
248
+ ```
249
+
250
+ After round-trip, the comment is gone:
251
+
252
+ ```mermaid
253
+ flowchart LR
254
+ A --> B
255
+ ```
256
+
189
257
  ## Supported Class Diagram Features
190
258
 
191
259
  - **Classes**: With labels, members (attributes and methods)
@@ -296,6 +364,8 @@ mermaid-ast/
296
364
 
297
365
  MIT
298
366
 
367
+ This project includes JISON parsers from [mermaid.js](https://github.com/mermaid-js/mermaid) (MIT License, Copyright (c) 2014 - 2022 Knut Sveidqvist). See [THIRD-PARTY-NOTICES.md](./THIRD-PARTY-NOTICES.md) for details.
368
+
299
369
  ---
300
370
 
301
371
  ## How This Library Was Built
@@ -4,8 +4,9 @@
4
4
  * Renders a Class Diagram AST back to Mermaid syntax.
5
5
  */
6
6
  import type { ClassDiagramAST } from "../types/class.js";
7
+ import type { RenderOptions } from "../types/render-options.js";
7
8
  /**
8
9
  * Render a ClassDiagramAST to Mermaid syntax
9
10
  */
10
- export declare function renderClassDiagram(ast: ClassDiagramAST): string;
11
+ export declare function renderClassDiagram(ast: ClassDiagramAST, options?: RenderOptions): string;
11
12
  //# sourceMappingURL=class-renderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"class-renderer.d.ts","sourceRoot":"","sources":["../../src/renderer/class-renderer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,eAAe,EAOhB,MAAM,mBAAmB,CAAC;AA2N3B;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,CAiE/D"}
1
+ {"version":3,"file":"class-renderer.d.ts","sourceRoot":"","sources":["../../src/renderer/class-renderer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,eAAe,EAOhB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AA4KhE;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAgExF"}
@@ -3,6 +3,8 @@
3
3
  *
4
4
  * Renders a Class Diagram AST back to Mermaid syntax.
5
5
  */
6
+ import { resolveOptions } from "../types/render-options.js";
7
+ import { indent, when, block, render } from "./doc.js";
6
8
  /**
7
9
  * Render relation type to symbol
8
10
  */
@@ -30,167 +32,128 @@ function renderLineType(lineType) {
30
32
  return lineType === "dotted" ? ".." : "--";
31
33
  }
32
34
  /**
33
- * Render a relationship
35
+ * Render a relationship to a string
34
36
  */
35
37
  function renderRelation(relation) {
36
38
  const { id1, id2, relation: rel, relationTitle1, relationTitle2, title } = relation;
37
- // Build the arrow
38
39
  const startType = renderRelationType(rel.type1, true);
39
40
  const endType = renderRelationType(rel.type2, false);
40
41
  const line = renderLineType(rel.lineType);
41
- let arrow = `${startType}${line}${endType}`;
42
- // Build the full statement
42
+ const arrow = `${startType}${line}${endType}`;
43
43
  let result = id1;
44
- // Add cardinality/title on id1 side
45
- if (relationTitle1) {
44
+ if (relationTitle1)
46
45
  result += ` "${relationTitle1}"`;
47
- }
48
46
  result += ` ${arrow}`;
49
- // Add cardinality/title on id2 side
50
- if (relationTitle2) {
47
+ if (relationTitle2)
51
48
  result += ` "${relationTitle2}"`;
52
- }
53
49
  result += ` ${id2}`;
54
- // Add label if present
55
- if (title) {
50
+ if (title)
56
51
  result += ` : ${title}`;
57
- }
58
52
  return result;
59
53
  }
60
54
  /**
61
- * Render a class member
55
+ * Render a class member to a string
62
56
  */
63
57
  function renderMember(member) {
64
58
  const visibility = member.visibility || "";
65
59
  return `${visibility}${member.text}`;
66
60
  }
67
61
  /**
68
- * Render a class definition
62
+ * Render a class definition to a Doc
69
63
  */
70
- function renderClass(cls, inNamespace = false, isReferencedInRelations = false) {
71
- const lines = [];
72
- const indent = inNamespace ? " " : " ";
73
- // Check if class needs a body
64
+ function renderClass(cls, isReferencedInRelations) {
74
65
  const hasBody = cls.members.length > 0;
75
66
  const hasLabel = cls.label && cls.label !== cls.id;
76
67
  if (hasBody) {
77
- // Class with body
78
- if (hasLabel) {
79
- lines.push(`${indent}class ${cls.id}["${cls.label}"] {`);
80
- }
81
- else {
82
- lines.push(`${indent}class ${cls.id} {`);
83
- }
84
- // Render members
85
- for (const member of cls.members) {
86
- lines.push(`${indent} ${renderMember(member)}`);
87
- }
88
- lines.push(`${indent}}`);
68
+ const header = hasLabel
69
+ ? `class ${cls.id}["${cls.label}"] {`
70
+ : `class ${cls.id} {`;
71
+ return block(header, cls.members.map(renderMember), "}");
89
72
  }
90
- else if (hasLabel) {
91
- // Class with label but no body
92
- lines.push(`${indent}class ${cls.id}["${cls.label}"]`);
73
+ if (hasLabel) {
74
+ return `class ${cls.id}["${cls.label}"]`;
93
75
  }
94
- else if (!isReferencedInRelations) {
95
- // Class with no body, no label, and not referenced in relations - must declare explicitly
96
- lines.push(`${indent}class ${cls.id}`);
76
+ // Class with no body, no label - only declare if not referenced in relations
77
+ if (!isReferencedInRelations) {
78
+ return `class ${cls.id}`;
97
79
  }
98
- // If no body and no label but referenced in relations, it's declared implicitly
99
- return lines;
80
+ // Implicitly declared via relations
81
+ return null;
100
82
  }
101
83
  /**
102
- * Render annotations for a class
84
+ * Render annotations for a class to a Doc
103
85
  */
104
86
  function renderAnnotations(cls) {
105
- const lines = [];
106
- for (const annotation of cls.annotations) {
107
- lines.push(` <<${annotation}>> ${cls.id}`);
108
- }
109
- return lines;
87
+ return cls.annotations.map((annotation) => `<<${annotation}>> ${cls.id}`);
110
88
  }
111
89
  /**
112
- * Render a namespace
90
+ * Render a namespace to a Doc
113
91
  */
114
- function renderNamespace(namespaceName, classIds, ast) {
115
- const lines = [];
116
- lines.push(` namespace ${namespaceName} {`);
117
- for (const classId of classIds) {
92
+ function renderNamespace(namespaceName, classIds, ast, classesInRelations) {
93
+ const classesDoc = classIds
94
+ .map((classId) => {
118
95
  const cls = ast.classes.get(classId);
119
- if (cls) {
120
- lines.push(...renderClass(cls, true));
121
- }
122
- }
123
- lines.push(` }`);
124
- return lines;
96
+ if (!cls)
97
+ return null;
98
+ return renderClass(cls, classesInRelations.has(classId));
99
+ })
100
+ .filter(Boolean);
101
+ return block(`namespace ${namespaceName} {`, classesDoc, "}");
125
102
  }
126
103
  /**
127
- * Render notes
104
+ * Render notes to a Doc
128
105
  */
129
106
  function renderNotes(notes) {
130
- const lines = [];
131
- for (const note of notes) {
132
- if (note.forClass) {
133
- lines.push(` note for ${note.forClass} "${note.text}"`);
134
- }
135
- else {
136
- lines.push(` note "${note.text}"`);
137
- }
138
- }
139
- return lines;
107
+ return notes.map((note) => note.forClass
108
+ ? `note for ${note.forClass} "${note.text}"`
109
+ : `note "${note.text}"`);
140
110
  }
141
111
  /**
142
- * Render class definitions (classDef)
112
+ * Render class definitions (classDef) to a Doc
143
113
  */
144
114
  function renderClassDefs(ast) {
145
- const lines = [];
146
- for (const [name, def] of ast.classDefs) {
115
+ const entries = [...ast.classDefs.entries()];
116
+ return entries.map(([name, def]) => {
147
117
  const styles = def.styles.join(",");
148
- lines.push(` classDef ${name} ${styles}`);
149
- }
150
- return lines;
118
+ return `classDef ${name} ${styles}`;
119
+ });
151
120
  }
152
121
  /**
153
- * Render CSS class assignments
122
+ * Render CSS class assignments to a Doc
154
123
  */
155
124
  function renderCssClasses(ast) {
156
- const lines = [];
125
+ const assignments = [];
157
126
  for (const [, cls] of ast.classes) {
158
127
  for (const cssClass of cls.cssClasses) {
159
- lines.push(` cssClass "${cls.id}" ${cssClass}`);
128
+ assignments.push(`cssClass "${cls.id}" ${cssClass}`);
160
129
  }
161
130
  }
162
- return lines;
131
+ return assignments;
163
132
  }
164
133
  /**
165
- * Render click handlers and links
134
+ * Render click handlers and links to a Doc
166
135
  */
167
136
  function renderClicks(ast) {
168
- const lines = [];
137
+ const clicks = [];
169
138
  for (const [, cls] of ast.classes) {
170
139
  if (cls.link) {
171
140
  const target = cls.linkTarget ? ` ${cls.linkTarget}` : "";
172
141
  const tooltip = cls.tooltip ? ` "${cls.tooltip}"` : "";
173
- lines.push(` link ${cls.id} "${cls.link}"${tooltip}${target}`);
142
+ clicks.push(`link ${cls.id} "${cls.link}"${tooltip}${target}`);
174
143
  }
175
144
  else if (cls.callback) {
176
145
  const args = cls.callbackArgs ? `("${cls.callbackArgs}")` : "";
177
146
  const tooltip = cls.tooltip ? ` "${cls.tooltip}"` : "";
178
- lines.push(` callback ${cls.id} "${cls.callback}"${args}${tooltip}`);
147
+ clicks.push(`callback ${cls.id} "${cls.callback}"${args}${tooltip}`);
179
148
  }
180
149
  }
181
- return lines;
150
+ return clicks;
182
151
  }
183
152
  /**
184
153
  * Render a ClassDiagramAST to Mermaid syntax
185
154
  */
186
- export function renderClassDiagram(ast) {
187
- const lines = [];
188
- // Header
189
- lines.push("classDiagram");
190
- // Direction if not default
191
- if (ast.direction && ast.direction !== "TB") {
192
- lines.push(` direction ${ast.direction}`);
193
- }
155
+ export function renderClassDiagram(ast, options) {
156
+ const opts = resolveOptions(options);
194
157
  // Track classes rendered in namespaces
195
158
  const classesInNamespaces = new Set();
196
159
  for (const [, ns] of ast.namespaces) {
@@ -204,35 +167,38 @@ export function renderClassDiagram(ast) {
204
167
  classesInRelations.add(relation.id1);
205
168
  classesInRelations.add(relation.id2);
206
169
  }
207
- // Render namespaces
208
- for (const [name, ns] of ast.namespaces) {
209
- lines.push(...renderNamespace(name, ns.classes, ast));
210
- }
211
- // Render annotations (before classes)
212
- for (const [, cls] of ast.classes) {
213
- if (cls.annotations.length > 0) {
214
- lines.push(...renderAnnotations(cls));
215
- }
216
- }
217
- // Render classes not in namespaces
218
- for (const [classId, cls] of ast.classes) {
219
- if (!classesInNamespaces.has(classId)) {
220
- const isReferencedInRelations = classesInRelations.has(classId);
221
- const classLines = renderClass(cls, false, isReferencedInRelations);
222
- lines.push(...classLines);
223
- }
224
- }
225
- // Render relations
226
- for (const relation of ast.relations) {
227
- lines.push(` ${renderRelation(relation)}`);
228
- }
229
- // Render notes
230
- lines.push(...renderNotes(ast.notes));
231
- // Render class definitions
232
- lines.push(...renderClassDefs(ast));
233
- // Render CSS class assignments
234
- lines.push(...renderCssClasses(ast));
235
- // Render click handlers
236
- lines.push(...renderClicks(ast));
237
- return lines.join("\n");
170
+ // Get classes (optionally sorted)
171
+ let classEntries = [...ast.classes.entries()];
172
+ if (opts.sortNodes) {
173
+ classEntries.sort((a, b) => a[0].localeCompare(b[0]));
174
+ }
175
+ // Build the document
176
+ const doc = [
177
+ "classDiagram",
178
+ indent([
179
+ // Direction
180
+ when(ast.direction && ast.direction !== "TB", `direction ${ast.direction}`),
181
+ // Namespaces
182
+ ...[...ast.namespaces.entries()].map(([name, ns]) => renderNamespace(name, ns.classes, ast, classesInRelations)),
183
+ // Annotations (before classes)
184
+ ...classEntries
185
+ .filter(([, cls]) => cls.annotations.length > 0)
186
+ .map(([, cls]) => renderAnnotations(cls)),
187
+ // Classes not in namespaces
188
+ ...classEntries
189
+ .filter(([classId]) => !classesInNamespaces.has(classId))
190
+ .map(([classId, cls]) => renderClass(cls, classesInRelations.has(classId))),
191
+ // Relations
192
+ ...ast.relations.map(renderRelation),
193
+ // Notes
194
+ renderNotes(ast.notes),
195
+ // Class definitions
196
+ renderClassDefs(ast),
197
+ // CSS class assignments
198
+ renderCssClasses(ast),
199
+ // Click handlers
200
+ renderClicks(ast),
201
+ ]),
202
+ ];
203
+ return render(doc, opts.indent);
238
204
  }